diff --git a/README.md b/README.md index 02ada78..12106dd 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Quick start · Packages · Layout · + Theming · Documentation · Contributing

@@ -62,6 +63,43 @@ and naturally flowing content in HTML. ![The same MarkupString figure rendered as ANSI terminal art and an HTML image](docs/assets/showcase-image-drawing.png) +## Theme a whole layout + +A theme changes semantic parts together: borders, titles, labels, bullets, guides, gauges, table +headings, and striped rows. Presets can change the drawing characters too; generated palettes build +accessible colour roles from a single seed. + +```csharp +var registry = MarkupRegistry.Empty.WithAnsi().WithHtml(); + +var sheet = new Stack( +[ + new Fields( + [ + new Field(MarkupText.Plain("Name"), MarkupText.Plain("Lyra Vale")), + new Field(MarkupText.Plain("Role"), MarkupText.Plain("Wayfinder")), + ]), + new Gauge(7, 10) { Label = MarkupText.Plain("Trail") }, +]).Bordered(MarkupText.Plain("WAYFINDER'S JOURNAL")); + +var fantasy = sheet.Themed(ThemePalette.Preset("fantasy")!.ToLayoutTheme()); +var housePalette = ThemePalette.Generate( + new RgbColor(34, 211, 238), + ThemeHarmony.Triadic, + contrast: 0.35); +var house = sheet.Themed(housePalette.ToLayoutTheme()); + +var value = BlockLayout.Build(house, width: 46); +var ansi = value.Render(MarkupFormat.Ansi, registry); +var html = value.Render(MarkupFormat.Html, registry); +``` + +![The same MarkupString layout rendered with a fantasy preset and a generated triadic palette](docs/assets/showcase-theming.png) + +Use `.Themed(...)` to override the surrounding theme, or `.ThemedUnder(...)` to provide defaults +that a reader's theme can replace. See [themes and palettes](docs/layout.md#themes-and-palettes) for +the built-in presets, light and dark modes, custom glyphs, base16 schemes, JSON, and contrast checks. + ## Why MarkupString? - **One source of truth** — keep content and semantics together, then choose the output format at diff --git a/docs/assets/showcase-box-drawing.png b/docs/assets/showcase-box-drawing.png index 7164b7d..f9afaca 100644 Binary files a/docs/assets/showcase-box-drawing.png and b/docs/assets/showcase-box-drawing.png differ diff --git a/docs/assets/showcase-image-drawing.png b/docs/assets/showcase-image-drawing.png index 6623725..a91a29a 100644 Binary files a/docs/assets/showcase-image-drawing.png and b/docs/assets/showcase-image-drawing.png differ diff --git a/docs/assets/showcase-theming.png b/docs/assets/showcase-theming.png new file mode 100644 index 0000000..fd56dc8 Binary files /dev/null and b/docs/assets/showcase-theming.png differ diff --git a/docs/showcase/Program.cs b/docs/showcase/Program.cs index efa228b..f977ce7 100644 --- a/docs/showcase/Program.cs +++ b/docs/showcase/Program.cs @@ -74,6 +74,31 @@ figureValue, registry); +var character = new Stack( +[ + new Fields( + [ + new Field(P("Name"), P("Lyra Vale")), + new Field(P("Role"), P("Wayfinder")), + new Field(P("Region"), P("Glasswood")), + new Field(P("Status"), P("Ready")), + ]) { Columns = 2, Gap = 3, Leader = P("·") }, + new Rule(P("CURRENT QUEST")), + new Gauge(7, 10) { Label = P("Trail"), Show = GaugeShow.Percent }, + new Bullets([P("Map the moonwell"), P("Return before dawn")]), +]).Bordered(P("WAYFINDER'S JOURNAL")); + +var fantasy = BlockLayout.Build(character.Themed(ThemePalette.Preset("fantasy")!.ToLayoutTheme()), 46); +var generatedPalette = ThemePalette.Generate(new RgbColor(34, 211, 238), ThemeHarmony.Triadic, contrast: 0.35); +var generated = BlockLayout.Build(character.Themed(generatedPalette.ToLayoutTheme()), 46); +WriteThemePage( + Path.Combine(output, "theming.html"), + "One layout, a whole new world", + "Presets and generated palettes recolour every semantic part — and can change the shapes too.", + fantasy, + generated, + registry); + static MarkupText P(string text) => MarkupText.Plain(text); static void WritePage(string path, string title, string subtitle, MarkupText value, MarkupRegistry registry) @@ -81,7 +106,45 @@ static void WritePage(string path, string title, string subtitle, MarkupText val var ansi = value.Render(MarkupFormat.Ansi, registry); var ansiForBrowser = AnsiEscapeParser.Parse(ansi).Render(MarkupFormat.Html, registry); var html = value.Render(MarkupFormat.Html, registry); - var escapedAnsi = WebUtility.HtmlEncode(ansi.Replace("\u001b", "\\e", StringComparison.Ordinal)); + WriteComparisonPage( + path, title, subtitle, "MarkupText → renderer → native output", + "ANSI terminal", "SGR + cells", "terminal", ansiForBrowser, + "HTML browser", "semantic elements", "browser", html); +} + +static void WriteThemePage( + string path, + string title, + string subtitle, + MarkupText preset, + MarkupText generated, + MarkupRegistry registry) +{ + var presetHtml = AnsiEscapeParser.Parse(preset.Render(MarkupFormat.Ansi, registry)).Render(MarkupFormat.Html, registry); + var generatedHtml = AnsiEscapeParser.Parse(generated.Render(MarkupFormat.Ansi, registry)).Render(MarkupFormat.Html, registry); + WriteComparisonPage( + path, title, subtitle, "layout → palette → themed output", + "Fantasy preset", "colours + glyphs", "terminal", presetHtml, + "Generated palette", "triadic harmony", "terminal", generatedHtml); +} + +static void WriteComparisonPage( + string path, + string title, + string subtitle, + string flowHtml, + string leftLabel, + string leftNative, + string leftClass, + string leftContent, + string rightLabel, + string rightNative, + string rightClass, + string rightContent) +{ + static string PanelBody(string cssClass, string content) => cssClass == "terminal" + ? "
" + content + "
" + : "
" + content + "
"; File.WriteAllText(path, $$""" @@ -117,26 +180,24 @@ static void WritePage(string path, string title, string subtitle, MarkupText val .browser .ms-figure-image { max-width: 220px; margin: 0 20px 14px 0; filter: drop-shadow(0 10px 20px rgba(0, 0, 0, .35)); } .browser .ms-figure { font-family: "DejaVu Sans", sans-serif; } .browser .ms-text { line-height: 1.65; } -.source { display: none; }

{{WebUtility.HtmlEncode(title)}}

{{WebUtility.HtmlEncode(subtitle)}}

-
MarkupText → renderer → native output
+
{{flowHtml}}
-
ANSI terminalSGR + cells
-
{{ansiForBrowser}}
+
{{WebUtility.HtmlEncode(leftLabel)}}{{WebUtility.HtmlEncode(leftNative)}}
+ {{PanelBody(leftClass, leftContent)}}
-
HTML browsersemantic elements
-
{{html}}
+
{{WebUtility.HtmlEncode(rightLabel)}}{{WebUtility.HtmlEncode(rightNative)}}
+ {{PanelBody(rightClass, rightContent)}}
-
{{escapedAnsi}}
diff --git a/docs/showcase/README.md b/docs/showcase/README.md index fd5f748..a4bbffd 100644 --- a/docs/showcase/README.md +++ b/docs/showcase/README.md @@ -1,6 +1,6 @@ # README showcase captures -The two README screenshots are generated from MarkupString values rather than recreated by hand. +The README screenshots are generated from MarkupString values rather than recreated by hand. The checked-in captures use this supported Linux x64 environment: - the .NET SDK selected by the repository's `global.json`; @@ -23,7 +23,9 @@ present. After restoring the repository once, run from its root: docs/showcase/capture.sh ``` -Each page places the same value's ANSI rendering beside its semantic HTML rendering. The ANSI side -is parsed back only to make its terminal styling visible in the browser capture; its text and SGR -styling come from `Render(MarkupFormat.Ansi)`. The capture script applies rounded alpha masks and -transparent padding so the dark cards sit cleanly on both GitHub README themes. +The format pages place the same value's ANSI rendering beside its semantic HTML rendering. The +theme page places the same layout under a preset and a generated palette. ANSI is parsed back only +to make its terminal styling visible in the browser capture; its text and SGR styling come from +`Render(MarkupFormat.Ansi)`. The capture script applies rounded alpha masks and transparent padding +so the dark cards sit cleanly on both GitHub README themes, and omits volatile PNG timestamps so a +repeat capture is byte-for-byte identical. diff --git a/docs/showcase/capture.sh b/docs/showcase/capture.sh index 77f645e..927d383 100755 --- a/docs/showcase/capture.sh +++ b/docs/showcase/capture.sh @@ -58,8 +58,10 @@ capture() { "$raw" magick composite -compose CopyOpacity "$mask" "$raw" "$clipped" - magick "$clipped" -bordercolor none -border 18 "$asset_dir/$asset" + magick "$clipped" -bordercolor none -border 18 \ + -define png:exclude-chunk=date,time "$asset_dir/$asset" } capture box-drawing.html showcase-box-drawing.png capture image-drawing.html showcase-image-drawing.png +capture theming.html showcase-theming.png