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 ✗
+
+
+ + + diff --git a/docs/design/theme-exploration.md b/docs/design/theme-exploration.md index 045087e..180b7df 100644 --- a/docs/design/theme-exploration.md +++ b/docs/design/theme-exploration.md @@ -79,6 +79,139 @@ 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. 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) + +```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..602c083 --- /dev/null +++ b/scripts/generate_palette_diagrams.py @@ -0,0 +1,261 @@ +#!/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 + 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 +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 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} + + + +""" + + +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("") + 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. A standalone HTML version for") + print("the dossier, with each token's computed contrast ratio and AA") + print("verdict, lives in [palettes.html](palettes.html) (same generator,") + print("`--html` flag).") + 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())