Unify team grid, limit avatars to brand colours, keep duty tags on one line

This commit is contained in:
tleininger committed 2026-10-01 16:04:12 +02:00
1 parent a5a68ed122
commit 602f258257
4 files changed
+72 -72

No files matched your search

+19 -4
View File
@@ -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\">");
+19 -11
View File
@@ -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>
+31 -54
View File
@@ -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
View File
@@ -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