Design/home hero #54

Merged
Tom merged 3 commits from design/home-hero into main 2026-10-01 14:45:48 +02:00
4 changed files with 41 additions and 44 deletions
Showing only changes of commit 6e5a30607e - Show all commits

No files matched your search

+10 -8
View File
@@ -104,11 +104,17 @@ public class MarkdownTests
} }
[Fact] [Fact]
public void First_sentence_longer_than_the_limit_is_cut_between_words() public void First_sentence_drops_a_colon_that_leads_into_a_list() =>
{ Markdown.FirstSentence("Formulare zum Herunterladen:\n\n- Antrag")
var sentence = Markdown.FirstSentence("Eins zwei drei vier fünf.", maxLength: 12); .ShouldBe("Formulare zum Herunterladen");
sentence.ShouldBe("Eins zwei…"); [Fact]
public void First_sentence_is_never_cut_short()
{
// Long teasers are clamped by the tile's CSS, not by a character count.
var sentence = string.Join(' ', Enumerable.Repeat("Wort", 60)) + ".";
Markdown.FirstSentence(sentence).ShouldBe(sentence);
} }
[Theory] [Theory]
@@ -119,8 +125,4 @@ public class MarkdownTests
[InlineData("::: hinweis\nText im Baustein.\n:::")] [InlineData("::: hinweis\nText im Baustein.\n:::")]
public void First_sentence_is_empty_without_a_top_level_paragraph(string? markdown) => public void First_sentence_is_empty_without_a_top_level_paragraph(string? markdown) =>
Markdown.FirstSentence(markdown).ShouldBe(""); Markdown.FirstSentence(markdown).ShouldBe("");
[Fact]
public void First_sentence_rejects_a_limit_below_one() =>
Should.Throw<ArgumentOutOfRangeException>(() => Markdown.FirstSentence("Text.", maxLength: 0));
} }
+17 -2
View File
@@ -38,8 +38,10 @@
/* On the gradient everything is white: the global heading colour is a blue that /* On the gradient everything is white: the global heading colour is a blue that
would vanish here. */ would vanish here. */
/* balance evens out the lines, so a short last word ("IGMH") never wraps alone. */
.hero h1 { .hero h1 {
color: var(--color-on-header); color: var(--color-on-header);
text-wrap: balance;
} }
/* The intro is editor Markdown, rendered without scope attributes, so it is reached /* The intro is editor Markdown, rendered without scope attributes, so it is reached
@@ -289,14 +291,23 @@
align-self: start; align-self: start;
} }
/* The teaser is a whole sentence; the tile cuts it visually after three lines,
so the tiles of a row keep a similar height whatever the editor wrote. The
-webkit- box is the form every current browser supports for line-clamp. */
.tile-text { .tile-text {
display: block; display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
} }
} }
/* --- Embeds ------------------------------------------------------------------- /* --- Embeds -------------------------------------------------------------------
The blocks the home page opts into (posts, events) sit side by side from desktop The blocks the home page opts into (posts, events) sit side by side from desktop
width, so the start page shows both without a long scroll. One column below. */ width, so the start page shows both without a long scroll. One column below.
In the grid the blocks' own top margin no longer collapses with the margin of
the tiles above, so it is dropped there; otherwise the gap would be doubled. */
@media (min-width: 64rem) { @media (min-width: 64rem) {
.home-embeds { .home-embeds {
display: grid; display: grid;
@@ -304,4 +315,8 @@
gap: var(--space-5); gap: var(--space-5);
align-items: start; align-items: start;
} }
.home-embeds ::deep .embed {
margin-top: 0;
}
} }
+12 -33
View File
@@ -84,18 +84,12 @@ public static partial class Markdown
/// short description on a home page tile. /// short description on a home page tile.
/// </summary> /// </summary>
/// <param name="markdown">The editor's Markdown; may be <see langword="null"/>.</param> /// <param name="markdown">The editor's Markdown; may be <see langword="null"/>.</param>
/// <param name="maxLength">
/// The longest sentence kept as is; a longer one is cut at the last word
/// boundary before the limit and ends in <c>…</c>.
/// </param>
/// <returns> /// <returns>
/// The first sentence of the first top-level paragraph, with all Markdown /// The first sentence of the first top-level paragraph, with all Markdown
/// removed and whitespace collapsed, or an empty string when there is no such /// removed, whitespace collapsed and a trailing colon dropped, or an empty
/// paragraph (empty body, or only headings, lists and blocks). /// string when there is no such paragraph (empty body, or only headings, lists
/// and blocks).
/// </returns> /// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="maxLength"/> is zero or negative.
/// </exception>
/// <remarks> /// <remarks>
/// Only top-level paragraphs count: a heading repeats the title, and the text in /// Only top-level paragraphs count: a heading repeats the title, and the text in
/// a list or a <c>:::</c> block is rarely a sentence that describes the page. /// a list or a <c>:::</c> block is rarely a sentence that describes the page.
@@ -106,6 +100,12 @@ public static partial class Markdown
/// longer abbreviation such as <c>bzw.</c> still does; that costs the rest /// longer abbreviation such as <c>bzw.</c> still does; that costs the rest
/// of a teaser, never the page. /// of a teaser, never the page.
/// </para> /// </para>
/// <para>
/// A paragraph that leads into a list often ends in <c>:</c> without a
/// period; as a teaser the colon would point at a list that is not there.
/// The sentence is returned whole: a long one is cut by the tile's CSS,
/// which knows the space it has, not by a character count.
/// </para>
/// </remarks> /// </remarks>
/// <example> /// <example>
/// <code> /// <code>
@@ -113,13 +113,11 @@ public static partial class Markdown
/// // "Wir sind sieben Eltern." /// // "Wir sind sieben Eltern."
/// </code> /// </code>
/// </example> /// </example>
public static string FirstSentence(string? markdown, int maxLength = 140) => public static string FirstSentence(string? markdown) =>
maxLength <= 0 string.IsNullOrWhiteSpace(markdown)
? throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, "Must be positive.")
: string.IsNullOrWhiteSpace(markdown)
? "" ? ""
: Parse(markdown).OfType<ParagraphBlock>().FirstOrDefault()?.Inline is { } inline : Parse(markdown).OfType<ParagraphBlock>().FirstOrDefault()?.Inline is { } inline
? Shorten(UpToSentenceEnd(PlainText(inline)), maxLength) ? UpToSentenceEnd(PlainText(inline)).TrimEnd(':', ' ')
: ""; : "";
/// <summary> /// <summary>
@@ -194,25 +192,6 @@ public static partial class Markdown
private static string UpToSentenceEnd(string text) => private static string UpToSentenceEnd(string text) =>
SentenceEnd().Match(text) is { Success: true } end ? text[..(end.Index + 1)] : text; SentenceEnd().Match(text) is { Success: true } end ? text[..(end.Index + 1)] : text;
/// <summary>
/// Shortens a text to at most <paramref name="maxLength"/> characters plus an
/// ellipsis, cutting between words.
/// </summary>
/// <param name="text">Plain text.</param>
/// <param name="maxLength">The longest text kept as is.</param>
/// <returns>The text, shortened if needed.</returns>
/// <remarks>
/// The search for a space starts at <paramref name="maxLength"/> itself, so a
/// word that ends exactly at the limit is kept. A single word longer than the
/// limit is cut hard.
/// </remarks>
private static string Shorten(string text, int maxLength) =>
text.Length <= maxLength
? text
: text.LastIndexOf(' ', maxLength) is > 0 and var space
? $"{text[..space].TrimEnd(',', ';', ':', '-', '–')}…"
: $"{text[..maxLength]}…";
/// <summary> /// <summary>
/// Matches the end mark of a sentence: <c>.</c>, <c>!</c> or <c>?</c> before a /// Matches the end mark of a sentence: <c>.</c>, <c>!</c> or <c>?</c> before a
/// space or the end, unless it follows a lone letter (<c>z.</c>) or a number /// space or the end, unless it follows a lone letter (<c>z.</c>) or a number
+2 -1
View File
@@ -131,7 +131,8 @@ Darunter erscheinen automatisch:
keinen, entfällt die Karte. keinen, entfällt die Karte.
- **Kacheln** — eine pro öffentlicher Seite mit `location = header` (außer der - **Kacheln** — eine pro öffentlicher Seite mit `location = header` (außer der
Startseite), in Menü-Reihenfolge. Der Kacheltext ist der **erste Satz** des Startseite), in Menü-Reihenfolge. Der Kacheltext ist der **erste Satz** des
`body` (ohne Überschrift, ggf. gekürzt); beginnt der Text mit einer Liste oder `body` (ohne Überschrift, ein Doppelpunkt am Ende fällt weg; ein langer Satz
wird nach drei Zeilen mit „…“ abgeschnitten); beginnt der Text mit einer Liste oder
einem Baustein, zeigt die Kachel nur den Titel. Auf dem Handy zeigen die einem Baustein, zeigt die Kachel nur den Titel. Auf dem Handy zeigen die
Kacheln immer nur Symbol und Titel (zwei Spalten), ab Tablet-Breite auch den Kacheln immer nur Symbol und Titel (zwei Spalten), ab Tablet-Breite auch den
Text (drei Spalten). Das Symbol hängt am Slug und ist Text (drei Spalten). Das Symbol hängt am Slug und ist