Render board members as person cards via ::: team block #56
No files matched your search
@@ -28,13 +28,28 @@ public class TeamMemberTests
|
|||||||
// Pinned values: the colour must not change between restarts or releases, so a
|
// Pinned values: the colour must not change between restarts or releases, so a
|
||||||
// change to the hash shows up here instead of recolouring the board.
|
// change to the hash shows up here instead of recolouring the board.
|
||||||
TeamMember.ColorOf("Maik Palm").ShouldBe(TeamMember.ColorOf(" Maik Palm "));
|
TeamMember.ColorOf("Maik Palm").ShouldBe(TeamMember.ColorOf(" Maik Palm "));
|
||||||
TeamMember.ColorOf("Maik Palm").ShouldBe(4);
|
TeamMember.ColorOf("Maik Palm").ShouldBe(2);
|
||||||
TeamMember.ColorOf("Gentiana Erol").ShouldBe(5);
|
TeamMember.ColorOf("Gentiana Erol").ShouldBe(2);
|
||||||
|
TeamMember.ColorOf("Raul Chira").ShouldBe(1);
|
||||||
|
|
||||||
new[] { "Maik Palm", "Gentiana Erol", "Raul Chira", "Marion Leininger", "Isabell Vesper", "" }
|
new[] { "Maik Palm", "Gentiana Erol", "Raul Chira", "Marion Leininger", "Isabell Vesper", "" }
|
||||||
.ShouldAllBe(name => TeamMember.ColorOf(name) >= 1 && TeamMember.ColorOf(name) <= TeamMember.ColorCount);
|
.ShouldAllBe(name => TeamMember.ColorOf(name) >= 1 && TeamMember.ColorOf(name) <= TeamMember.ColorCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Board_uses_both_colours_about_evenly()
|
||||||
|
{
|
||||||
|
// Two colours only make sense if both show up; the real board (ten names)
|
||||||
|
// must not land mostly on one.
|
||||||
|
string[] board =
|
||||||
|
[
|
||||||
|
"Maik Palm", "Gentiana Erol", "Raul Chira", "Marion Leininger", "Jenny Reger-Stilgenbauer",
|
||||||
|
"Mariella Ianello", "Isabell Vesper", "Susanne Naumburg", "Rebecca Goldmann", "Michael Weber",
|
||||||
|
];
|
||||||
|
|
||||||
|
board.Count(name => TeamMember.ColorOf(name) == 1).ShouldBeInRange(4, 6);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Item_with_role_duties_and_note_is_read_in_full()
|
public void Item_with_role_duties_and_note_is_read_in_full()
|
||||||
{
|
{
|
||||||
@@ -101,7 +116,7 @@ public class TeamMemberTests
|
|||||||
|
|
||||||
html.ShouldContain("<div class=\"team\">");
|
html.ShouldContain("<div class=\"team\">");
|
||||||
html.ShouldContain($"<li class=\"{TeamMember.CardClass}\">");
|
html.ShouldContain($"<li class=\"{TeamMember.CardClass}\">");
|
||||||
html.ShouldContain("<span class=\"team-avatar team-avatar-4\" aria-hidden=\"true\">MP</span>");
|
html.ShouldContain("<span class=\"team-avatar team-avatar-2\" aria-hidden=\"true\">MP</span>");
|
||||||
html.ShouldContain("<strong class=\"team-name\">Maik Palm</strong>");
|
html.ShouldContain("<strong class=\"team-name\">Maik Palm</strong>");
|
||||||
html.ShouldContain("<span class=\"team-role\">Vorsitzender</span>");
|
html.ShouldContain("<span class=\"team-role\">Vorsitzender</span>");
|
||||||
html.ShouldContain("<span class=\"team-duty\">Schulkonferenz</span>");
|
html.ShouldContain("<span class=\"team-duty\">Schulkonferenz</span>");
|
||||||
@@ -138,7 +153,7 @@ public class TeamMemberTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Mistyped_modifier_keeps_the_grid()
|
public void Mistyped_modifier_keeps_the_grid()
|
||||||
{
|
{
|
||||||
// A typo in the modifier costs the larger size, not the cards.
|
// A typo in the modifier costs the highlight, not the cards.
|
||||||
var html = Markdown.ToHtml("::: team grss\n- **Maik Palm**\n:::");
|
var html = Markdown.ToHtml("::: team grss\n- **Maik Palm**\n:::");
|
||||||
|
|
||||||
html.ShouldContain("<div class=\"team\">");
|
html.ShouldContain("<div class=\"team\">");
|
||||||
|
|||||||
@@ -49,13 +49,14 @@ public sealed record TeamMember(string Name, string? Role, IReadOnlyList<string>
|
|||||||
public const string ContainerName = "team";
|
public const string ContainerName = "team";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The modifier for larger cards, <c>::: team gross</c>; also the class it adds
|
/// The modifier for the chair, <c>::: team gross</c>; also the class it adds to
|
||||||
/// to the block.
|
/// the block. The cards keep the shared raster; only the role chip is
|
||||||
|
/// highlighted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// A word after the block name rather than a block of its own
|
/// A word after the block name rather than a block of its own
|
||||||
/// (<c>::: team-gross</c>): the block stays <c>team</c>, so a typo in the
|
/// (<c>::: team-gross</c>): the block stays <c>team</c>, so a typo in the
|
||||||
/// modifier only costs the larger size, never the whole grid. <c>groß</c> is
|
/// modifier only costs the highlight, never the whole grid. <c>groß</c> is
|
||||||
/// accepted too; <c>gross</c> is the documented form because it types the same
|
/// accepted too; <c>gross</c> is the documented form because it types the same
|
||||||
/// on every keyboard.
|
/// on every keyboard.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
@@ -73,9 +74,14 @@ public sealed record TeamMember(string Name, string? Role, IReadOnlyList<string>
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How many avatar colours there are; <c>app.css</c> defines
|
/// How many avatar colours there are; <c>app.css</c> defines
|
||||||
/// <c>--color-avatar-1</c> to <c>--color-avatar-5</c> to match.
|
/// <c>--color-avatar-1</c> (brand blue) and <c>--color-avatar-2</c> (brand
|
||||||
|
/// green) to match.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int ColorCount = 5;
|
/// <remarks>
|
||||||
|
/// Only the two brand colours: more hues made the board look busy and pulled it
|
||||||
|
/// away from the IGMH palette.
|
||||||
|
/// </remarks>
|
||||||
|
public const int ColorCount = 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// FNV-1a offset basis and prime (32 bit), see <see cref="Fnv"/>.
|
/// FNV-1a offset basis and prime (32 bit), see <see cref="Fnv"/>.
|
||||||
@@ -135,14 +141,16 @@ public sealed record TeamMember(string Name, string? Role, IReadOnlyList<string>
|
|||||||
/// is a fixed hash (FNV-1a) and not <see cref="string.GetHashCode()"/>, which
|
/// is a fixed hash (FNV-1a) and not <see cref="string.GetHashCode()"/>, which
|
||||||
/// .NET randomises per process. Surrounding whitespace does not count.
|
/// .NET randomises per process. Surrounding whitespace does not count.
|
||||||
/// <para>
|
/// <para>
|
||||||
/// FNV-1a alone spreads short names badly over its low bits, which the
|
/// FNV-1a alone spreads short names badly, so the MurmurHash3 finaliser
|
||||||
/// remainder by <see cref="ColorCount"/> reads: four of the six "Beisitz"
|
/// (<see cref="Mix"/>) stirs all bits first. The colour is then taken from
|
||||||
/// members shared one colour. The MurmurHash3 finaliser (<see cref="Mix"/>)
|
/// the <b>high</b> bits (multiply by <see cref="ColorCount"/>, keep the top
|
||||||
/// stirs all bits into the low ones first.
|
/// 32 bits) rather than a remainder, which would read only the low bits:
|
||||||
|
/// with two colours that put eight of the ten board members on the same
|
||||||
|
/// one.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static int ColorOf(string name) =>
|
public static int ColorOf(string name) =>
|
||||||
(int)(Mix(Fnv(name.Trim())) % ColorCount) + 1;
|
(int)(((ulong)Mix(Fnv(name.Trim())) * ColorCount) >> 32) + 1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads a person from the first paragraph of a list item.
|
/// Reads a person from the first paragraph of a list item.
|
||||||
@@ -207,7 +215,7 @@ public sealed record TeamMember(string Name, string? Role, IReadOnlyList<string>
|
|||||||
string.Equals(container.Info, ContainerName, StringComparison.OrdinalIgnoreCase);
|
string.Equals(container.Info, ContainerName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a team block asks for larger cards (<c>::: team gross</c>).
|
/// Whether a team block asks for the chair highlight (<c>::: team gross</c>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="container">The team block.</param>
|
/// <param name="container">The team block.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
|
|||||||
@@ -74,20 +74,16 @@
|
|||||||
--color-button-light-hover: #eaf3f9;
|
--color-button-light-hover: #eaf3f9;
|
||||||
--color-on-button-light: #1f5477;
|
--color-on-button-light: #1f5477;
|
||||||
|
|
||||||
/* Initials avatars on the person cards (::: team, see TeamMember.cs). Five
|
/* Initials avatars on the person cards (::: team, see TeamMember.cs): only the
|
||||||
soft tints, each with a deep text shade of the same hue, so a board of ten
|
two brand colours, blue and green tint, so the board stays in the IGMH palette.
|
||||||
people looks lively but calm. The colour is picked from the name, so it means
|
The colour is picked from the name, so it means nothing -- it only helps tell
|
||||||
nothing -- it only helps tell the cards apart. Every pair >= 5.3:1. */
|
the cards apart. The tints follow their tokens into dark mode; the green text
|
||||||
--color-avatar-1: #dbeaf5; /* blue */
|
needs its own shade, because --color-accent-dark is too light for small text
|
||||||
--color-on-avatar-1: #1f5477;
|
on the tint (3.6:1). Blue 7.2:1, green 5.9:1. */
|
||||||
--color-avatar-2: #dcefd8; /* green */
|
--color-avatar-1: var(--color-brand-tint);
|
||||||
|
--color-on-avatar-1: var(--color-brand-dark);
|
||||||
|
--color-avatar-2: var(--color-accent-tint);
|
||||||
--color-on-avatar-2: #2f6b33;
|
--color-on-avatar-2: #2f6b33;
|
||||||
--color-avatar-3: #fbeec5; /* warm yellow */
|
|
||||||
--color-on-avatar-3: #7a5a00;
|
|
||||||
--color-avatar-4: #f8dfe0; /* rose */
|
|
||||||
--color-on-avatar-4: #8e2f3a;
|
|
||||||
--color-avatar-5: #e7e0f3; /* violet */
|
|
||||||
--color-on-avatar-5: #553a8a;
|
|
||||||
|
|
||||||
/* Keyboard focus ring (:focus-visible). Its own token so it can stay strong on
|
/* Keyboard focus ring (:focus-visible). Its own token so it can stay strong on
|
||||||
the light page (dark blue, 8:1 on white) and flip to a light blue on the dark
|
the light page (dark blue, 8:1 on white) and flip to a light blue on the dark
|
||||||
@@ -196,18 +192,9 @@
|
|||||||
--color-button-light-hover: #d3dee6;
|
--color-button-light-hover: #d3dee6;
|
||||||
--color-on-button-light: #1a3d57; /* 9:1 on the eased white */
|
--color-on-button-light: #1a3d57; /* 9:1 on the eased white */
|
||||||
|
|
||||||
/* Avatars: the tints turn into deep shades, the text into light ones; each
|
/* Avatar green text: the brightened accent, 6.7:1 on the dark green tint
|
||||||
pair >= 7:1, so the circles do not glare on the dark cards. */
|
(the blue pair follows its tokens, 8.6:1). */
|
||||||
--color-avatar-1: #1f3a4f;
|
--color-on-avatar-2: var(--color-accent);
|
||||||
--color-on-avatar-1: #a3d5f4;
|
|
||||||
--color-avatar-2: #24402a;
|
|
||||||
--color-on-avatar-2: #a6dc9e;
|
|
||||||
--color-avatar-3: #4a3d14;
|
|
||||||
--color-on-avatar-3: #f3d98a;
|
|
||||||
--color-avatar-4: #4d2a2f;
|
|
||||||
--color-on-avatar-4: #f4b8bf;
|
|
||||||
--color-avatar-5: #3a3150;
|
|
||||||
--color-on-avatar-5: #cdbdf0;
|
|
||||||
|
|
||||||
--color-focus: #a3d5f4; /* light blue: a dark ring would vanish here */
|
--color-focus: #a3d5f4; /* light blue: a dark ring would vanish here */
|
||||||
|
|
||||||
@@ -882,12 +869,16 @@ h1:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ::: team -- one person card per list item (Markdown.cs/TeamMember.cs build the
|
/* ::: team -- one person card per list item (Markdown.cs/TeamMember.cs build the
|
||||||
avatar, name, role chip, duty tags and note). A grid like the kacheln, a little
|
avatar, name, role chip, duty tags and note). Each "## Vorsitz" etc. above stays
|
||||||
wider so a long double name still fits its line. Each "## Vorsitz" etc. above
|
a normal heading; every group is a block of its own. One raster for all of them,
|
||||||
stays a normal heading; every group is a block of its own. */
|
so the cards line up across groups: a group of one person gets the same card
|
||||||
|
width as a group of six, not a stretched one. 17rem fits a long double name.
|
||||||
|
align-items: start lets every card be as tall as its content -- a card without
|
||||||
|
duties is not stretched to its neighbour's height. */
|
||||||
.markdown-body .team > ul {
|
.markdown-body .team > ul {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
|
||||||
|
align-items: start;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
max-width: none;
|
max-width: none;
|
||||||
margin: 0 0 var(--space-4);
|
margin: 0 0 var(--space-4);
|
||||||
@@ -900,8 +891,7 @@ h1:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* The card: avatar in a column of its own, everything else stacked beside it.
|
/* The card: avatar in a column of its own, everything else stacked beside it.
|
||||||
align-content keeps a short card's text at the top when a long neighbour in the
|
Hover only deepens the shadow -- nothing moves. */
|
||||||
same row stretches it. Hover only deepens the shadow -- nothing moves. */
|
|
||||||
.markdown-body .team-card {
|
.markdown-body .team-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
@@ -949,9 +939,6 @@ h1:focus {
|
|||||||
|
|
||||||
.markdown-body .team-avatar-1 { background: var(--color-avatar-1); color: var(--color-on-avatar-1); }
|
.markdown-body .team-avatar-1 { background: var(--color-avatar-1); color: var(--color-on-avatar-1); }
|
||||||
.markdown-body .team-avatar-2 { background: var(--color-avatar-2); color: var(--color-on-avatar-2); }
|
.markdown-body .team-avatar-2 { background: var(--color-avatar-2); color: var(--color-on-avatar-2); }
|
||||||
.markdown-body .team-avatar-3 { background: var(--color-avatar-3); color: var(--color-on-avatar-3); }
|
|
||||||
.markdown-body .team-avatar-4 { background: var(--color-avatar-4); color: var(--color-on-avatar-4); }
|
|
||||||
.markdown-body .team-avatar-5 { background: var(--color-avatar-5); color: var(--color-on-avatar-5); }
|
|
||||||
|
|
||||||
.markdown-body .team-name {
|
.markdown-body .team-name {
|
||||||
color: var(--color-heading);
|
color: var(--color-heading);
|
||||||
@@ -973,7 +960,9 @@ h1:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Duties as small outlined tags: quieter than the role chip, because the role
|
/* Duties as small outlined tags: quieter than the role chip, because the role
|
||||||
says who someone is and the duties only what they look after. */
|
says who someone is and the duties only what they look after. A tag never breaks
|
||||||
|
inside ("Stellvertretende Schulkonferenz" stays one piece); the row wraps between
|
||||||
|
tags instead. */
|
||||||
.markdown-body .team-duties {
|
.markdown-body .team-duties {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -982,6 +971,7 @@ h1:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .team-duty {
|
.markdown-body .team-duty {
|
||||||
|
white-space: nowrap;
|
||||||
padding: 0.05rem var(--space-2);
|
padding: 0.05rem var(--space-2);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
@@ -995,25 +985,12 @@ h1:focus {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ::: team gross -- the chair: wider cards, a bigger avatar and name, and a green
|
/* ::: team gross -- the chair. Same raster and card as every other group, so the
|
||||||
edge so the first group reads as the lead of the page. */
|
page keeps one rhythm; only the role chip stands out, filled in the button blue
|
||||||
.markdown-body .team.gross > ul {
|
instead of the pale tint (white on it 5.4:1, in dark mode too). */
|
||||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
.markdown-body .team.gross .team-role {
|
||||||
}
|
background: var(--color-button);
|
||||||
|
color: var(--color-on-button);
|
||||||
.markdown-body .team.gross .team-card {
|
|
||||||
padding: var(--space-4);
|
|
||||||
border-top: 4px solid var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body .team.gross .team-avatar {
|
|
||||||
width: 4rem;
|
|
||||||
height: 4rem;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body .team.gross .team-name {
|
|
||||||
font-size: 1.35rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tables (pipe tables): not in the content yet, styled ahead of time. As a block
|
/* Tables (pipe tables): not in the content yet, styled ahead of time. As a block
|
||||||
|
|||||||
+3
-3
@@ -341,9 +341,9 @@ Listenpunkt ist eine Person:
|
|||||||
Folgezeilen mit zwei Leerzeichen einrücken (wie oben).
|
Folgezeilen mit zwei Leerzeichen einrücken (wie oben).
|
||||||
- Initialen und Farbe des Kreises entstehen **automatisch aus dem Namen**; es gibt
|
- Initialen und Farbe des Kreises entstehen **automatisch aus dem Namen**; es gibt
|
||||||
bewusst keine Fotos (siehe `docs/recht.md`).
|
bewusst keine Fotos (siehe `docs/recht.md`).
|
||||||
- `::: team gross` macht die Karten größer und hebt sie hervor, gedacht für den
|
- `::: team gross` ist für den Vorsitz gedacht: Die Karten bleiben gleich groß,
|
||||||
Vorsitz. Ein vertipptes Wort nach `team` schadet nicht: Die Karten erscheinen
|
nur das Rollen-Etikett wird kräftig blau hervorgehoben. Ein vertipptes Wort
|
||||||
dann in normaler Größe.
|
nach `team` schadet nicht: Die Etiketten erscheinen dann einfach normal.
|
||||||
- Die Überschriften (`## Vorsitz`, `## Beisitz` …) stehen **außerhalb** der
|
- Die Überschriften (`## Vorsitz`, `## Beisitz` …) stehen **außerhalb** der
|
||||||
Blöcke; jede Gruppe ist ein eigener `::: team`-Block.
|
Blöcke; jede Gruppe ist ein eigener `::: team`-Block.
|
||||||
- Beginnt ein Listenpunkt nicht mit einem fetten Namen, wird er eine schlichte
|
- Beginnt ein Listenpunkt nicht mit einem fetten Namen, wird er eine schlichte
|
||||||
|
|||||||
Reference in new issue
Block a user