From dfe8a0c9d55e8b54d95771ba4b582a3fa2053398 Mon Sep 17 00:00:00 2001 From: Eric Bouchut Date: Wed, 15 Jul 2026 21:49:55 +0200 Subject: [PATCH 1/2] docs(design): Add visual palette swatches to the theme study A new Color palettes section in theme-exploration.md shows every design token of the four palettes (Catppuccin Latte and Mocha, Soft Paper light and dark) as a colored swatch grid rendered natively by GitHub from Mermaid flowcharts, so no image file is committed. The diagrams are generated from the theme stylesheets (the source of truth) by the new scripts/generate_palette_diagrams.py, which picks each swatch label's color by WCAG relative luminance so the token name stays readable on its own background. Mermaid's block-beta grid would be the natural shape but it ignores the label color, leaving dark swatches unreadable, so the script emits flowcharts whose disconnected rows of invisible links stack into a grid; verified with mermaid-cli. Regenerate after any token change: python3 scripts/generate_palette_diagrams.py --- docs/design/theme-exploration.md | 130 +++++++++++++++++++++++++++ scripts/generate_palette_diagrams.py | 116 ++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 scripts/generate_palette_diagrams.py diff --git a/docs/design/theme-exploration.md b/docs/design/theme-exploration.md index 045087e..570bc74 100644 --- a/docs/design/theme-exploration.md +++ b/docs/design/theme-exploration.md @@ -79,6 +79,136 @@ semantic colors are darkened to pass 4.5:1. Character: calm, warm, reflective; reads like paper. Lower-key than Catppuccin, closer to a reading environment than an interactive app. +## Color palettes + + + +Every swatch below comes straight from the theme stylesheets; +GitHub renders these Mermaid blocks with the real colors, so no +image is committed. Values are the shipped (contrast-adjusted) +tokens, not the upstream palettes. + +### Catppuccin Latte (light) + +```mermaid +flowchart LR + t0["bg
#eff1f5"] ~~~ t1["surface
#e6e9ef"] ~~~ t2["surface-deep
#dce0e8"] ~~~ t3["border
#ccd0da"] ~~~ t4["text
#4c4f69"] + t5["text-muted
#5c5f77"] ~~~ t6["primary
#8839ef"] ~~~ t7["on-primary
#ffffff"] ~~~ t8["link
#1a5cd7"] ~~~ t9["success
#2f7a1f"] + t10["warning
#8f5b08"] ~~~ t11["error
#d20f39"] ~~~ t12["focus
#1e66f5"] ~~~ t13["primary-soft
#eadcfd"] ~~~ t14["code-bg
#1e1e2e"] + t15["code-text
#cdd6f4"] ~~~ t16["code-keyword
#cba6f7"] ~~~ t17["code-string
#a6e3a1"] ~~~ t18["code-function
#89b4fa"] ~~~ t19["code-comment
#9399b2"] + style t0 fill:#eff1f5,stroke:#7f7f7f,color:#11111b + style t1 fill:#e6e9ef,stroke:#7f7f7f,color:#11111b + style t2 fill:#dce0e8,stroke:#7f7f7f,color:#11111b + style t3 fill:#ccd0da,stroke:#7f7f7f,color:#11111b + style t4 fill:#4c4f69,stroke:#7f7f7f,color:#f8f8f8 + style t5 fill:#5c5f77,stroke:#7f7f7f,color:#f8f8f8 + style t6 fill:#8839ef,stroke:#7f7f7f,color:#f8f8f8 + style t7 fill:#ffffff,stroke:#7f7f7f,color:#11111b + style t8 fill:#1a5cd7,stroke:#7f7f7f,color:#f8f8f8 + style t9 fill:#2f7a1f,stroke:#7f7f7f,color:#f8f8f8 + style t10 fill:#8f5b08,stroke:#7f7f7f,color:#f8f8f8 + style t11 fill:#d20f39,stroke:#7f7f7f,color:#f8f8f8 + style t12 fill:#1e66f5,stroke:#7f7f7f,color:#f8f8f8 + style t13 fill:#eadcfd,stroke:#7f7f7f,color:#11111b + style t14 fill:#1e1e2e,stroke:#7f7f7f,color:#f8f8f8 + style t15 fill:#cdd6f4,stroke:#7f7f7f,color:#11111b + style t16 fill:#cba6f7,stroke:#7f7f7f,color:#11111b + style t17 fill:#a6e3a1,stroke:#7f7f7f,color:#11111b + style t18 fill:#89b4fa,stroke:#7f7f7f,color:#11111b + style t19 fill:#9399b2,stroke:#7f7f7f,color:#f8f8f8 +``` + +### Catppuccin Mocha (dark) + +```mermaid +flowchart LR + t0["bg
#1e1e2e"] ~~~ t1["surface
#181825"] ~~~ t2["surface-deep
#11111b"] ~~~ t3["border
#313244"] ~~~ t4["text
#cdd6f4"] + t5["text-muted
#bac2de"] ~~~ t6["primary
#cba6f7"] ~~~ t7["on-primary
#11111b"] ~~~ t8["link
#89b4fa"] ~~~ t9["success
#a6e3a1"] + t10["warning
#f9e2af"] ~~~ t11["error
#f38ba8"] ~~~ t12["focus
#89b4fa"] ~~~ t13["primary-soft
#2b2440"] ~~~ t14["code-bg
#11111b"] + t15["code-text
#cdd6f4"] ~~~ t16["code-keyword
#cba6f7"] ~~~ t17["code-string
#a6e3a1"] ~~~ t18["code-function
#89b4fa"] ~~~ t19["code-comment
#9399b2"] + style t0 fill:#1e1e2e,stroke:#7f7f7f,color:#f8f8f8 + style t1 fill:#181825,stroke:#7f7f7f,color:#f8f8f8 + style t2 fill:#11111b,stroke:#7f7f7f,color:#f8f8f8 + style t3 fill:#313244,stroke:#7f7f7f,color:#f8f8f8 + style t4 fill:#cdd6f4,stroke:#7f7f7f,color:#11111b + style t5 fill:#bac2de,stroke:#7f7f7f,color:#11111b + style t6 fill:#cba6f7,stroke:#7f7f7f,color:#11111b + style t7 fill:#11111b,stroke:#7f7f7f,color:#f8f8f8 + style t8 fill:#89b4fa,stroke:#7f7f7f,color:#11111b + style t9 fill:#a6e3a1,stroke:#7f7f7f,color:#11111b + style t10 fill:#f9e2af,stroke:#7f7f7f,color:#11111b + style t11 fill:#f38ba8,stroke:#7f7f7f,color:#11111b + style t12 fill:#89b4fa,stroke:#7f7f7f,color:#11111b + style t13 fill:#2b2440,stroke:#7f7f7f,color:#f8f8f8 + style t14 fill:#11111b,stroke:#7f7f7f,color:#f8f8f8 + style t15 fill:#cdd6f4,stroke:#7f7f7f,color:#11111b + style t16 fill:#cba6f7,stroke:#7f7f7f,color:#11111b + style t17 fill:#a6e3a1,stroke:#7f7f7f,color:#11111b + style t18 fill:#89b4fa,stroke:#7f7f7f,color:#11111b + style t19 fill:#9399b2,stroke:#7f7f7f,color:#f8f8f8 +``` + +### Soft Paper light + +```mermaid +flowchart LR + t0["bg
#eee6dd"] ~~~ t1["surface
#e6dbd1"] ~~~ t2["surface-deep
#ddd0c6"] ~~~ t3["border
#dcd3cb"] ~~~ t4["text
#575279"] + t5["text-muted
#525252"] ~~~ t6["primary
#286983"] ~~~ t7["on-primary
#ffffff"] ~~~ t8["link
#286983"] ~~~ t9["success
#2f6a4a"] + t10["warning
#7d570c"] ~~~ t11["error
#94425a"] ~~~ t12["focus
#286983"] ~~~ t13["primary-soft
#dfe9ec"] ~~~ t14["code-bg
#303446"] + t15["code-text
#c6ceef"] ~~~ t16["code-keyword
#bb93d6"] ~~~ t17["code-string
#67c48f"] ~~~ t18["code-function
#8caaee"] ~~~ t19["code-comment
#838ba7"] + style t0 fill:#eee6dd,stroke:#7f7f7f,color:#11111b + style t1 fill:#e6dbd1,stroke:#7f7f7f,color:#11111b + style t2 fill:#ddd0c6,stroke:#7f7f7f,color:#11111b + style t3 fill:#dcd3cb,stroke:#7f7f7f,color:#11111b + style t4 fill:#575279,stroke:#7f7f7f,color:#f8f8f8 + style t5 fill:#525252,stroke:#7f7f7f,color:#f8f8f8 + style t6 fill:#286983,stroke:#7f7f7f,color:#f8f8f8 + style t7 fill:#ffffff,stroke:#7f7f7f,color:#11111b + style t8 fill:#286983,stroke:#7f7f7f,color:#f8f8f8 + style t9 fill:#2f6a4a,stroke:#7f7f7f,color:#f8f8f8 + style t10 fill:#7d570c,stroke:#7f7f7f,color:#f8f8f8 + style t11 fill:#94425a,stroke:#7f7f7f,color:#f8f8f8 + style t12 fill:#286983,stroke:#7f7f7f,color:#f8f8f8 + style t13 fill:#dfe9ec,stroke:#7f7f7f,color:#11111b + style t14 fill:#303446,stroke:#7f7f7f,color:#f8f8f8 + style t15 fill:#c6ceef,stroke:#7f7f7f,color:#11111b + style t16 fill:#bb93d6,stroke:#7f7f7f,color:#11111b + style t17 fill:#67c48f,stroke:#7f7f7f,color:#11111b + style t18 fill:#8caaee,stroke:#7f7f7f,color:#11111b + style t19 fill:#838ba7,stroke:#7f7f7f,color:#f8f8f8 +``` + +### Soft Paper dark + +```mermaid +flowchart LR + t0["bg
#303446"] ~~~ t1["surface
#292c3c"] ~~~ t2["surface-deep
#232634"] ~~~ t3["border
#414459"] ~~~ t4["text
#c6ceef"] + t5["text-muted
#b5bddc"] ~~~ t6["primary
#8caaee"] ~~~ t7["on-primary
#232634"] ~~~ t8["link
#8caaee"] ~~~ t9["success
#67c48f"] + t10["warning
#c9be3e"] ~~~ t11["error
#e78284"] ~~~ t12["focus
#8caaee"] ~~~ t13["primary-soft
#3b415c"] ~~~ t14["code-bg
#232634"] + t15["code-text
#c6ceef"] ~~~ t16["code-keyword
#bb93d6"] ~~~ t17["code-string
#67c48f"] ~~~ t18["code-function
#8caaee"] ~~~ t19["code-comment
#838ba7"] + style t0 fill:#303446,stroke:#7f7f7f,color:#f8f8f8 + style t1 fill:#292c3c,stroke:#7f7f7f,color:#f8f8f8 + style t2 fill:#232634,stroke:#7f7f7f,color:#f8f8f8 + style t3 fill:#414459,stroke:#7f7f7f,color:#f8f8f8 + style t4 fill:#c6ceef,stroke:#7f7f7f,color:#11111b + style t5 fill:#b5bddc,stroke:#7f7f7f,color:#11111b + style t6 fill:#8caaee,stroke:#7f7f7f,color:#11111b + style t7 fill:#232634,stroke:#7f7f7f,color:#f8f8f8 + style t8 fill:#8caaee,stroke:#7f7f7f,color:#11111b + style t9 fill:#67c48f,stroke:#7f7f7f,color:#11111b + style t10 fill:#c9be3e,stroke:#7f7f7f,color:#11111b + style t11 fill:#e78284,stroke:#7f7f7f,color:#f8f8f8 + style t12 fill:#8caaee,stroke:#7f7f7f,color:#11111b + style t13 fill:#3b415c,stroke:#7f7f7f,color:#f8f8f8 + style t14 fill:#232634,stroke:#7f7f7f,color:#f8f8f8 + style t15 fill:#c6ceef,stroke:#7f7f7f,color:#11111b + style t16 fill:#bb93d6,stroke:#7f7f7f,color:#11111b + style t17 fill:#67c48f,stroke:#7f7f7f,color:#11111b + style t18 fill:#8caaee,stroke:#7f7f7f,color:#11111b + style t19 fill:#838ba7,stroke:#7f7f7f,color:#f8f8f8 +``` + ## Structural tokens (theme-independent) Defined once in `base.css`, identical for both themes: diff --git a/scripts/generate_palette_diagrams.py b/scripts/generate_palette_diagrams.py new file mode 100644 index 0000000..f260303 --- /dev/null +++ b/scripts/generate_palette_diagrams.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Generate the "Color palettes" Mermaid diagrams from the theme CSS. + +Reads the design tokens straight from the two theme stylesheets (the source +of truth) and prints a Markdown section containing one Mermaid `block-beta` +swatch grid per theme and mode. GitHub renders Mermaid natively, so the +palette is visible in the browser without committing any image. + +Each swatch is filled with the token's color; the label color (dark or +light) is chosen by WCAG relative luminance so the token name stays +readable on its own swatch. + +Usage: python3 scripts/generate_palette_diagrams.py +Paste the output over the "Color palettes" section of +docs/design/theme-exploration.md whenever a token changes. +""" + +import re +import sys + +THEMES = [ + ("Catppuccin", "src/main/resources/static/css/theme-catppuccin.css", + "Latte (light)", "Mocha (dark)"), + ("Soft Paper", "src/main/resources/static/css/theme-soft-paper.css", + "light", "dark"), +] + +COLUMNS = 5 + +TOKEN_RE = re.compile(r"^\s*(--[a-z-]+)\s*:\s*(#[0-9a-fA-F]{3,6})\s*;") + + +def parse_blocks(path: str) -> tuple[dict[str, str], dict[str, str]]: + """Return (light, dark) ordered token->hex maps from a theme stylesheet. + + The light palette is the first `:root` block; the dark palette is the + `:root` inside `@media (prefers-color-scheme: dark)`. + """ + light: dict[str, str] = {} + dark: dict[str, str] = {} + in_dark = False + for line in open(path): + if "prefers-color-scheme: dark" in line: + in_dark = True + match = TOKEN_RE.match(line) + if match: + (dark if in_dark else light)[match.group(1)] = match.group(2).lower() + return light, dark + + +def luminance(hex_color: str) -> float: + """WCAG relative luminance of a #rrggbb color.""" + hex_color = hex_color.lstrip("#") + if len(hex_color) == 3: + hex_color = "".join(c * 2 for c in hex_color) + channels = [] + for i in (0, 2, 4): + c = int(hex_color[i:i + 2], 16) / 255 + channels.append(c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4) + r, g, b = channels + return 0.2126 * r + 0.7152 * g + 0.0722 * b + + +def label_color(background: str) -> str: + """Dark label on light swatches, light label on dark ones.""" + return "#11111b" if luminance(background) > 0.35 else "#f8f8f8" + + +def diagram(tokens: dict[str, str]) -> str: + """A flowchart whose disconnected rows stack vertically: each row is a + chain of COLUMNS nodes joined by invisible links (~~~). block-beta would + be the natural fit but it ignores the label color, which makes dark + swatches unreadable; flowchart honors it.""" + lines = ["```mermaid", "flowchart LR"] + ids = [] + row: list[str] = [] + for index, (token, value) in enumerate(tokens.items()): + short = token.removeprefix("--color-").removeprefix("--") + block_id = f"t{index}" + ids.append((block_id, value)) + row.append(f'{block_id}["{short}
{value}"]') + if len(row) == COLUMNS: + lines.append(" " + " ~~~ ".join(row)) + row = [] + if row: + lines.append(" " + " ~~~ ".join(row)) + for block_id, value in ids: + lines.append( + f" style {block_id} fill:{value},stroke:#7f7f7f," + f"color:{label_color(value)}") + lines.append("```") + return "\n".join(lines) + + +def main() -> int: + print("## Color palettes") + print() + print("") + print() + print("Every swatch below comes straight from the theme stylesheets;") + print("GitHub renders these Mermaid blocks with the real colors, so no") + print("image is committed. Values are the shipped (contrast-adjusted)") + print("tokens, not the upstream palettes.") + for name, path, light_label, dark_label in THEMES: + light, dark = parse_blocks(path) + for label, tokens in ((light_label, light), (dark_label, dark)): + print() + print(f"### {name} {label}") + print() + print(diagram(tokens)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From ba42a4d0f1dd8e041280734deefc757dac2fd6c4 Mon Sep 17 00:00:00 2001 From: Eric Bouchut Date: Wed, 15 Jul 2026 22:12:19 +0200 Subject: [PATCH 2/2] docs(design): Add the standalone HTML palette page for the dossier docs/design/palettes.html shows the four palettes as side by side cards, each rendered with its own background, text, and border colors, and every token with its computed WCAG contrast ratio against its reference background (text tokens against bg, on-primary against primary, code tokens against code-bg, focus against bg at the 3:1 UI threshold) plus the AA verdict. Surface tokens are marked as decor. Self contained, French, print friendly: made to be attached to the dossier as is. The page is emitted by the same generator as the Mermaid section (scripts/generate_palette_diagrams.py, new --html flag), so both views regenerate from the theme stylesheets and cannot drift. The computed ratios were checked against the ones documented in the stylesheet comments (exact match). The Color palettes section of the theme study now links to the page. Side finding surfaced by the page: Soft Paper's code-comment token fails AA against code-bg in both modes (3.65:1 and 4.45:1); harmless today (code tokens only paint the decorative aria-hidden hero card) but to fix before lesson code blocks ship with that theme. --- docs/design/palettes.html | 158 +++++++++++++++++++++++++++ docs/design/theme-exploration.md | 5 +- scripts/generate_palette_diagrams.py | 153 +++++++++++++++++++++++++- 3 files changed, 311 insertions(+), 5 deletions(-) create mode 100644 docs/design/palettes.html diff --git a/docs/design/palettes.html b/docs/design/palettes.html new file mode 100644 index 0000000..f4bb124 --- /dev/null +++ b/docs/design/palettes.html @@ -0,0 +1,158 @@ + + + + + +learn-dev : palettes de couleurs + + + +

learn-dev : palettes de couleurs

+

+ Annexe du dossier projet. Chaque carte ci-dessous est rendue avec les + couleurs de sa propre palette (fond, texte et bordure réels) ; chaque + jeton indique son ratio de contraste WCAG calculé contre son fond de + référence, avec le verdict AA (texte : 4,5:1 ; composant d'interface : + 3:1). « décor » signale les jetons de surface sans exigence de contraste + propre. Les valeurs proviennent directement des feuilles de style du + thème ; ce fichier est généré par + scripts/generate_palette_diagrams.py --html. +

+

Catppuccin

+
+
+

Catppuccin Latte (light)

+ + + + + + + + + + + + + + + + + + + + + +
bg#eff1f5décor
surface#e6e9efdécor
surface-deep#dce0e8décor
border#ccd0dadécor
text#4c4f697.06:1 sur bgAA ✓
text-muted#5c5f775.53:1 sur bgAA ✓
primary#8839ef4.79:1 sur bgAA ✓
on-primary#ffffff5.41:1 sur primaryAA ✓
link#1a5cd75.22:1 sur bgAA ✓
success#2f7a1f4.73:1 sur bgAA ✓
warning#8f5b085.06:1 sur bgAA ✓
error#d20f394.80:1 sur bgAA ✓
focus#1e66f54.34:1 sur bgUI 3:1 ✓
primary-soft#eadcfddécor
code-bg#1e1e2edécor
code-text#cdd6f411.34:1 sur code-bgAA ✓
code-keyword#cba6f78.07:1 sur code-bgAA ✓
code-string#a6e3a111.03:1 sur code-bgAA ✓
code-function#89b4fa7.79:1 sur code-bgAA ✓
code-comment#9399b25.81:1 sur code-bgAA ✓
+
+
+

Catppuccin Mocha (dark)

+ + + + + + + + + + + + + + + + + + + + + +
bg#1e1e2edécor
surface#181825décor
surface-deep#11111bdécor
border#313244décor
text#cdd6f411.34:1 sur bgAA ✓
text-muted#bac2de9.26:1 sur bgAA ✓
primary#cba6f78.07:1 sur bgAA ✓
on-primary#11111b9.23:1 sur primaryAA ✓
link#89b4fa7.79:1 sur bgAA ✓
success#a6e3a111.03:1 sur bgAA ✓
warning#f9e2af12.91:1 sur bgAA ✓
error#f38ba87.08:1 sur bgAA ✓
focus#89b4fa7.79:1 sur bgUI 3:1 ✓
primary-soft#2b2440décor
code-bg#11111bdécor
code-text#cdd6f412.97:1 sur code-bgAA ✓
code-keyword#cba6f79.23:1 sur code-bgAA ✓
code-string#a6e3a112.61:1 sur code-bgAA ✓
code-function#89b4fa8.91:1 sur code-bgAA ✓
code-comment#9399b26.64:1 sur code-bgAA ✓
+
+
+

Soft Paper

+
+
+

Soft Paper light

+ + + + + + + + + + + + + + + + + + + + + +
bg#eee6dddécor
surface#e6dbd1décor
surface-deep#ddd0c6décor
border#dcd3cbdécor
text#5752795.89:1 sur bgAA ✓
text-muted#5252526.32:1 sur bgAA ✓
primary#2869834.94:1 sur bgAA ✓
on-primary#ffffff6.11:1 sur primaryAA ✓
link#2869834.94:1 sur bgAA ✓
success#2f6a4a5.18:1 sur bgAA ✓
warning#7d570c5.25:1 sur bgAA ✓
error#94425a5.34:1 sur bgAA ✓
focus#2869834.94:1 sur bgUI 3:1 ✓
primary-soft#dfe9ecdécor
code-bg#303446décor
code-text#c6ceef7.90:1 sur code-bgAA ✓
code-keyword#bb93d64.84:1 sur code-bgAA ✓
code-string#67c48f5.79:1 sur code-bgAA ✓
code-function#8caaee5.34:1 sur code-bgAA ✓
code-comment#838ba73.65:1 sur code-bgAA ✗
+
+
+

Soft Paper dark

+ + + + + + + + + + + + + + + + + + + + + +
bg#303446décor
surface#292c3cdécor
surface-deep#232634décor
border#414459décor
text#c6ceef7.90:1 sur bgAA ✓
text-muted#b5bddc6.61:1 sur bgAA ✓
primary#8caaee5.34:1 sur bgAA ✓
on-primary#2326346.51:1 sur primaryAA ✓
link#8caaee5.34:1 sur bgAA ✓
success#67c48f5.79:1 sur bgAA ✓
warning#c9be3e6.40:1 sur bgAA ✓
error#e782844.65:1 sur bgAA ✓
focus#8caaee5.34:1 sur bgUI 3:1 ✓
primary-soft#3b415cdécor
code-bg#232634décor
code-text#c6ceef9.64:1 sur code-bgAA ✓
code-keyword#bb93d65.91:1 sur code-bgAA ✓
code-string#67c48f7.06:1 sur code-bgAA ✓
code-function#8caaee6.51:1 sur code-bgAA ✓
code-comment#838ba74.45:1 sur code-bgAA ✗
+
+
+
+ Source : src/main/resources/static/css/theme-*.css · + Étude complète : theme-exploration.md · + Méthode des ratios : formule de luminance relative WCAG. +
+ + diff --git a/docs/design/theme-exploration.md b/docs/design/theme-exploration.md index 570bc74..180b7df 100644 --- a/docs/design/theme-exploration.md +++ b/docs/design/theme-exploration.md @@ -87,7 +87,10 @@ Catppuccin, closer to a reading environment than an interactive app. Every swatch below comes straight from the theme stylesheets; GitHub renders these Mermaid blocks with the real colors, so no image is committed. Values are the shipped (contrast-adjusted) -tokens, not the upstream palettes. +tokens, not the upstream palettes. A standalone HTML version for +the dossier, with each token's computed contrast ratio and AA +verdict, lives in [palettes.html](palettes.html) (same generator, +`--html` flag). ### Catppuccin Latte (light) diff --git a/scripts/generate_palette_diagrams.py b/scripts/generate_palette_diagrams.py index f260303..602c083 100644 --- a/scripts/generate_palette_diagrams.py +++ b/scripts/generate_palette_diagrams.py @@ -10,9 +10,16 @@ light) is chosen by WCAG relative luminance so the token name stays readable on its own swatch. -Usage: python3 scripts/generate_palette_diagrams.py -Paste the output over the "Color palettes" section of -docs/design/theme-exploration.md whenever a token changes. +Usage: + python3 scripts/generate_palette_diagrams.py + Print the Markdown section; paste it over the "Color palettes" + section of docs/design/theme-exploration.md. + python3 scripts/generate_palette_diagrams.py --html + Write docs/design/palettes.html, the standalone French palette page + for the dossier: same data, plus the contrast ratio of every token + against its reference background and the AA verdict. + +Run both after any token change. """ import re @@ -92,7 +99,142 @@ def diagram(tokens: dict[str, str]) -> str: return "\n".join(lines) +def contrast(color_a: str, color_b: str) -> float: + """WCAG contrast ratio between two #rrggbb colors.""" + lum_a, lum_b = luminance(color_a), luminance(color_b) + lighter, darker = max(lum_a, lum_b), min(lum_a, lum_b) + return (lighter + 0.05) / (darker + 0.05) + + +# How each token is judged: (reference token or None, threshold or None). +# None reference means decorative/surface: no contrast requirement of its own. +JUDGMENTS = { + "--color-text": ("--color-bg", 4.5), + "--color-text-muted": ("--color-bg", 4.5), + "--color-primary": ("--color-bg", 4.5), + "--color-on-primary": ("--color-primary", 4.5), + "--color-link": ("--color-bg", 4.5), + "--color-success": ("--color-bg", 4.5), + "--color-warning": ("--color-bg", 4.5), + "--color-error": ("--color-bg", 4.5), + "--color-focus": ("--color-bg", 3.0), # UI component (RGAA/WCAG 1.4.11) + "--code-text": ("--code-bg", 4.5), + "--code-keyword": ("--code-bg", 4.5), + "--code-string": ("--code-bg", 4.5), + "--code-function": ("--code-bg", 4.5), + "--code-comment": ("--code-bg", 4.5), +} + + +def html_rows(tokens: dict[str, str]) -> str: + rows = [] + for token, value in tokens.items(): + short = token.removeprefix("--color-").removeprefix("--") + reference, threshold = JUDGMENTS.get(token, (None, None)) + if reference: + ratio = contrast(value, tokens[reference]) + verdict = "pass" if ratio >= threshold else "fail" + badge = (f'{ratio:.2f}:1 sur ' + f'{reference.removeprefix("--color-").removeprefix("--")}' + f'' + f'{"AA" if threshold == 4.5 else "UI 3:1"}' + f'{" ✓" if verdict == "pass" else " ✗"}') + else: + badge = 'décor' + rows.append( + f' ' + f'{short}' + f'{value}{badge}') + return "\n".join(rows) + + +HTML_TEMPLATE = """ + + + + +learn-dev : palettes de couleurs + + + +

learn-dev : palettes de couleurs

+

+ Annexe du dossier projet. Chaque carte ci-dessous est rendue avec les + couleurs de sa propre palette (fond, texte et bordure réels) ; chaque + jeton indique son ratio de contraste WCAG calculé contre son fond de + référence, avec le verdict AA (texte : 4,5:1 ; composant d'interface : + 3:1). « décor » signale les jetons de surface sans exigence de contraste + propre. Les valeurs proviennent directement des feuilles de style du + thème ; ce fichier est généré par + scripts/generate_palette_diagrams.py --html. +

+{sections} +
+ Source : src/main/resources/static/css/theme-*.css · + Étude complète : theme-exploration.md · + Méthode des ratios : formule de luminance relative WCAG. +
+ + +""" + + +def html_section(name: str, modes: list[tuple[str, dict[str, str]]]) -> str: + cards = [] + for label, tokens in modes: + bg = tokens["--color-bg"] + text = tokens["--color-text"] + border = tokens["--color-border"] + cards.append(f"""
+

{name} {label}

+ +{html_rows(tokens)} +
+
""") + return (f"

{name}

\n" + f'
\n' + "\n".join(cards) + "\n
") + + +def write_html(path: str) -> None: + sections = [] + for name, css_path, light_label, dark_label in THEMES: + light, dark = parse_blocks(css_path) + sections.append(html_section( + name, [(light_label, light), (dark_label, dark)])) + with open(path, "w") as handle: + handle.write(HTML_TEMPLATE.format(sections="\n".join(sections))) + print(f"Wrote {path}") + + def main() -> int: + if "--html" in sys.argv: + write_html("docs/design/palettes.html") + return 0 print("## Color palettes") print() print("