From f4832e60f5cd56fe87d3948557f46c26ea26a67c Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Tue, 1 Sep 2026 19:40:59 -0400 Subject: [PATCH] Replace the generated design vocabulary in the JS templates Five JavaScript extensions shipped the same design system to every project generated from them: paper/ink/muted/line tokens, a single acid accent at #6cff8f, an .eyebrow rule at 0.14em tracking, and an h1 clamped to 8-10rem at 0.85 line height. Two unrelated sites built from this template were recognisably the same design. Tokens are now named for the role a color plays rather than a material it imitates: bg, bg-raised, fg, fg-muted, border, focus. The decorative accent is gone; focus is a functional requirement and is the only non-neutral color left. Defaults are plain greys and are documented as placeholders. Every foreground clears WCAG AA against both surfaces in both themes, and the focus ring clears 3:1. The eyebrow rule and the oversized headline are removed from the toolkit galleries and the site templates. BRAND.md no longer ships a filled-in palette for consumers to keep by default; it is a table to complete, with the two rules worth keeping whatever the palette becomes. --- javascript/site-react/src/App.tsx.jinja | 1 - javascript/site-react/src/styles.css.jinja | 45 ++++++------ javascript/site-sveltekit/src/app.css.jinja | 42 +++++++---- javascript/site-webawesome/index.html.jinja | 3 +- .../site-webawesome/src/styles.css.jinja | 38 +++++----- javascript/uitk-svelte/BRAND.md.jinja | 38 +++++++--- .../src/lib/components/BrandMark.svelte.jinja | 5 +- .../lib/components/ThemeToggle.svelte.jinja | 6 +- .../src/lib/styles/tokens.css.jinja | 47 +++++++++---- .../uitk-svelte/src/routes/+page.svelte.jinja | 68 +++++++++++++----- javascript/uitk-webawesome/BRAND.md.jinja | 38 +++++++--- javascript/uitk-webawesome/index.html.jinja | 2 +- .../src/lit/components/brand-mark.ts.jinja | 6 +- .../src/lit/components/theme-toggle.ts.jinja | 6 +- .../src/lit/styles/tokens.css.jinja | 70 +++++++++++++------ 15 files changed, 279 insertions(+), 136 deletions(-) diff --git a/javascript/site-react/src/App.tsx.jinja b/javascript/site-react/src/App.tsx.jinja index 29e82d4..0631aed 100644 --- a/javascript/site-react/src/App.tsx.jinja +++ b/javascript/site-react/src/App.tsx.jinja @@ -1,7 +1,6 @@ export default function App() { return (
-

{{ brand_name }}

{{ brand_name }}

{{ project_description }}

diff --git a/javascript/site-react/src/styles.css.jinja b/javascript/site-react/src/styles.css.jinja index 64aced2..edd49d4 100644 --- a/javascript/site-react/src/styles.css.jinja +++ b/javascript/site-react/src/styles.css.jinja @@ -1,7 +1,14 @@ :root { - color: #171717; - background: #ffffff; + --{{ css_prefix }}-bg: #ffffff; + --{{ css_prefix }}-fg: #171717; + --{{ css_prefix }}-fg-muted: #5c5c5c; + --{{ css_prefix }}-border: #e0e0e0; + --{{ css_prefix }}-focus: #1a56c4; + + color: var(--{{ css_prefix }}-fg); + background: var(--{{ css_prefix }}-bg); font-family: system-ui, sans-serif; + color-scheme: light; } body { @@ -11,34 +18,32 @@ body { main { width: min(calc(100% - 3rem), 64rem); margin: 0 auto; - padding: 6rem 0; -} - -.eyebrow { - font-size: 0.75rem; - letter-spacing: 0.14em; - text-transform: uppercase; + padding: 3rem 0 6rem; } h1 { - margin: 2rem 0 1rem; - font-size: clamp(3rem, 10vw, 8rem); - line-height: 0.9; - letter-spacing: -0.06em; + margin: 0 0 1rem; + font-size: 1.75rem; + font-weight: 600; } -main > p:not(.eyebrow) { - max-width: 42rem; +main > p { + max-width: 60ch; margin-bottom: 2rem; - font-size: 1.25rem; + color: var(--{{ css_prefix }}-fg-muted); line-height: 1.6; } .button { display: inline-block; - padding: 0.75rem 1rem; - color: #ffffff; - background: #171717; - border-radius: 0.35rem; + padding: 0.6rem 1rem; + color: var(--{{ css_prefix }}-bg); + background: var(--{{ css_prefix }}-fg); text-decoration: none; } + +a:focus-visible, +button:focus-visible { + outline: 2px solid var(--{{ css_prefix }}-focus); + outline-offset: 4px; +} diff --git a/javascript/site-sveltekit/src/app.css.jinja b/javascript/site-sveltekit/src/app.css.jinja index 751a8af..c65a285 100644 --- a/javascript/site-sveltekit/src/app.css.jinja +++ b/javascript/site-sveltekit/src/app.css.jinja @@ -1,37 +1,51 @@ @import 'tailwindcss'; +/* Neutral placeholders. Replace the values with the brand's own; the names are + * the contract the rest of the site depends on. */ + :root { - --paper: #ffffff; - --ink: #171717; - --muted: #5c5c5c; - --line: #e4e4e4; + --{{ css_prefix }}-bg: #ffffff; + --{{ css_prefix }}-bg-raised: #f6f6f6; + --{{ css_prefix }}-fg: #171717; + --{{ css_prefix }}-fg-muted: #5c5c5c; + --{{ css_prefix }}-border: #e0e0e0; + --{{ css_prefix }}-focus: #1a56c4; color-scheme: light; } :root[data-theme='dark'] { - --paper: #0b0b0b; - --ink: #f2f2f2; - --muted: #a1a1a1; - --line: #262626; + --{{ css_prefix }}-bg: #121212; + --{{ css_prefix }}-bg-raised: #1c1c1c; + --{{ css_prefix }}-fg: #ededed; + --{{ css_prefix }}-fg-muted: #a6a6a6; + --{{ css_prefix }}-border: #333333; + --{{ css_prefix }}-focus: #8ab4ff; color-scheme: dark; } @theme inline { - --color-paper: var(--paper); - --color-ink: var(--ink); - --color-muted: var(--muted); - --color-line: var(--line); + --color-bg: var(--{{ css_prefix }}-bg); + --color-bg-raised: var(--{{ css_prefix }}-bg-raised); + --color-fg: var(--{{ css_prefix }}-fg); + --color-fg-muted: var(--{{ css_prefix }}-fg-muted); + --color-border: var(--{{ css_prefix }}-border); } body { margin: 0; - color: var(--ink); - background: var(--paper); + color: var(--{{ css_prefix }}-fg); + background: var(--{{ css_prefix }}-bg); font-family: system-ui, sans-serif; } a { color: inherit; } + +a:focus-visible, +button:focus-visible { + outline: 2px solid var(--{{ css_prefix }}-focus); + outline-offset: 4px; +} diff --git a/javascript/site-webawesome/index.html.jinja b/javascript/site-webawesome/index.html.jinja index eaef34d..ade2335 100644 --- a/javascript/site-webawesome/index.html.jinja +++ b/javascript/site-webawesome/index.html.jinja @@ -15,8 +15,7 @@
-

{{ brand_name }}

-

{{ project_name }}

+

{{ brand_name }}

{{ project_description }}

Get started
diff --git a/javascript/site-webawesome/src/styles.css.jinja b/javascript/site-webawesome/src/styles.css.jinja index fcc971d..0439926 100644 --- a/javascript/site-webawesome/src/styles.css.jinja +++ b/javascript/site-webawesome/src/styles.css.jinja @@ -1,7 +1,14 @@ :root { - color: #171717; - background: #ffffff; + --{{ css_prefix }}-bg: #ffffff; + --{{ css_prefix }}-fg: #171717; + --{{ css_prefix }}-fg-muted: #5c5c5c; + --{{ css_prefix }}-border: #e0e0e0; + --{{ css_prefix }}-focus: #1a56c4; + + color: var(--{{ css_prefix }}-fg); + background: var(--{{ css_prefix }}-bg); font-family: system-ui, sans-serif; + color-scheme: light; } body { @@ -11,25 +18,24 @@ body { main { width: min(calc(100% - 3rem), 64rem); margin: 0 auto; - padding: 6rem 0; -} - -.eyebrow { - font-size: 0.75rem; - letter-spacing: 0.14em; - text-transform: uppercase; + padding: 3rem 0 6rem; } h1 { - margin: 2rem 0 1rem; - font-size: clamp(3rem, 10vw, 8rem); - line-height: 0.9; - letter-spacing: -0.06em; + margin: 0 0 1rem; + font-size: 1.75rem; + font-weight: 600; } -main > p:not(.eyebrow) { - max-width: 42rem; +main > p { + max-width: 60ch; margin-bottom: 2rem; - font-size: 1.25rem; + color: var(--{{ css_prefix }}-fg-muted); line-height: 1.6; } + +a:focus-visible, +button:focus-visible { + outline: 2px solid var(--{{ css_prefix }}-focus); + outline-offset: 4px; +} diff --git a/javascript/uitk-svelte/BRAND.md.jinja b/javascript/uitk-svelte/BRAND.md.jinja index a7a0058..29443dc 100644 --- a/javascript/uitk-svelte/BRAND.md.jinja +++ b/javascript/uitk-svelte/BRAND.md.jinja @@ -1,24 +1,42 @@ # {{ brand_name }} Brand Guide -Canonical source for the {{ brand_name }} identity. Replace each placeholder before publishing the first consumer. +Canonical source for the {{ brand_name }} identity. Every section below is a +placeholder — fill it in before publishing the first consumer. ## Identity -Describe what {{ brand_name }} is, who it serves, and what it must not be confused with. +Describe what {{ brand_name }} is, who it serves, and what it must not be +confused with. ## Mark -Document primary mark, clear space, minimum size, and approved variants. +Document the primary mark, clear space, minimum size, and approved variants. ## Color -| Role | Light | Dark | Usage | -| ------ | --------- | --------- | ------------------ | -| Paper | `#f5f5f0` | `#151515` | Page surfaces | -| Ink | `#151515` | `#f5f5f0` | Primary content | -| Muted | `#666660` | `#aaa9a0` | Supporting content | -| Line | `#d0d0c8` | `#3a3a36` | Borders and rules | -| Accent | `#6cff8f` | `#6cff8f` | Focus and emphasis | +The shipped tokens are neutral greys plus a functional focus color. They exist +so the toolkit renders legibly out of the box, not because they are the brand. +Replace the values in `tokens.css`, then record them here. + +| Token | Light | Dark | Usage | +| ----------- | ----- | ---- | ----------------- | +| `bg` | | | Page background | +| `bg-raised` | | | Raised surfaces | +| `fg` | | | Primary text | +| `fg-muted` | | | Supporting text | +| `border` | | | Borders and rules | +| `focus` | | | Focus rings | + +Two rules worth keeping whatever the palette becomes: + +- Name tokens for the role a color plays, not a material it imitates. Roles + survive a redesign; metaphors have to be renamed or quietly start lying. +- Give every color a job. A single decorative accent applied wherever a page + looks flat is how unrelated products end up looking identical. + +Verify each foreground against the surfaces behind it — WCAG AA is 4.5:1 for +normal text, and a focus ring needs 3:1. Assert it in a test so the palette +cannot drift. ## Typography diff --git a/javascript/uitk-svelte/src/lib/components/BrandMark.svelte.jinja b/javascript/uitk-svelte/src/lib/components/BrandMark.svelte.jinja index 55e5c54..ed1b408 100644 --- a/javascript/uitk-svelte/src/lib/components/BrandMark.svelte.jinja +++ b/javascript/uitk-svelte/src/lib/components/BrandMark.svelte.jinja @@ -24,8 +24,7 @@ height: 2rem; place-items: center; border-radius: 50%; - color: var(--{{ css_prefix }}-color-paper); - background: var(--{{ css_prefix }}-color-ink); - box-shadow: 3px 3px 0 var(--{{ css_prefix }}-color-accent); + color: var(--{{ css_prefix }}-bg); + background: var(--{{ css_prefix }}-fg); } diff --git a/javascript/uitk-svelte/src/lib/components/ThemeToggle.svelte.jinja b/javascript/uitk-svelte/src/lib/components/ThemeToggle.svelte.jinja index 07aaf24..6ac3cdd 100644 --- a/javascript/uitk-svelte/src/lib/components/ThemeToggle.svelte.jinja +++ b/javascript/uitk-svelte/src/lib/components/ThemeToggle.svelte.jinja @@ -20,9 +20,9 @@ diff --git a/javascript/uitk-svelte/src/lib/styles/tokens.css.jinja b/javascript/uitk-svelte/src/lib/styles/tokens.css.jinja index ad77451..0bc2102 100644 --- a/javascript/uitk-svelte/src/lib/styles/tokens.css.jinja +++ b/javascript/uitk-svelte/src/lib/styles/tokens.css.jinja @@ -1,26 +1,49 @@ +/* Design tokens for {{ brand_name }}. + * + * These are deliberately neutral placeholders, not a finished palette. Replace + * the values with the brand's own before shipping; the names are the contract + * consumers depend on, so prefer changing values over renaming. + * + * Names describe the role a color plays, not a material it imitates. + */ + :root { - --{{ css_prefix }}-color-paper: #f5f5f0; - --{{ css_prefix }}-color-ink: #151515; - --{{ css_prefix }}-color-muted: #666660; - --{{ css_prefix }}-color-line: #d0d0c8; - --{{ css_prefix }}-color-accent: #6cff8f; + /* surfaces */ + --{{ css_prefix }}-bg: #ffffff; + --{{ css_prefix }}-bg-raised: #f6f6f6; + + /* text */ + --{{ css_prefix }}-fg: #171717; + --{{ css_prefix }}-fg-muted: #5c5c5c; + + /* structure */ + --{{ css_prefix }}-border: #e0e0e0; + + /* Focus is a functional requirement, not decoration. It needs at least 3:1 + * against both surfaces above; check it whenever the palette changes. */ + --{{ css_prefix }}-focus: #1a56c4; + --{{ css_prefix }}-font-sans: ui-sans-serif, system-ui, sans-serif; --{{ css_prefix }}-font-mono: ui-monospace, monospace; + color-scheme: light; } :root[data-theme='dark'] { - --{{ css_prefix }}-color-paper: #151515; - --{{ css_prefix }}-color-ink: #f5f5f0; - --{{ css_prefix }}-color-muted: #aaa9a0; - --{{ css_prefix }}-color-line: #3a3a36; + --{{ css_prefix }}-bg: #121212; + --{{ css_prefix }}-bg-raised: #1c1c1c; + --{{ css_prefix }}-fg: #ededed; + --{{ css_prefix }}-fg-muted: #a6a6a6; + --{{ css_prefix }}-border: #333333; + --{{ css_prefix }}-focus: #8ab4ff; + color-scheme: dark; } :root { - color: var(--{{ css_prefix }}-color-ink); + color: var(--{{ css_prefix }}-fg); font-family: var(--{{ css_prefix }}-font-sans); - background: var(--{{ css_prefix }}-color-paper); + background: var(--{{ css_prefix }}-bg); } body { @@ -29,6 +52,6 @@ body { button:focus-visible, a:focus-visible { - outline: 2px solid var(--{{ css_prefix }}-color-accent); + outline: 2px solid var(--{{ css_prefix }}-focus); outline-offset: 4px; } diff --git a/javascript/uitk-svelte/src/routes/+page.svelte.jinja b/javascript/uitk-svelte/src/routes/+page.svelte.jinja index 03333e7..a5877f7 100644 --- a/javascript/uitk-svelte/src/routes/+page.svelte.jinja +++ b/javascript/uitk-svelte/src/routes/+page.svelte.jinja @@ -5,32 +5,68 @@ {{ brand_name }} brand kit
-

Brand toolkit

- +
+ + +
+

{{ brand_name }}

-

{{ project_description }}

- +

{{ project_description }}

+ +
+

Tokens

+

+ Colors, type, and spacing live in src/lib/styles/tokens.css. The values there are neutral + placeholders — replace them with the brand's own. +

+
diff --git a/javascript/uitk-webawesome/BRAND.md.jinja b/javascript/uitk-webawesome/BRAND.md.jinja index a7a0058..29443dc 100644 --- a/javascript/uitk-webawesome/BRAND.md.jinja +++ b/javascript/uitk-webawesome/BRAND.md.jinja @@ -1,24 +1,42 @@ # {{ brand_name }} Brand Guide -Canonical source for the {{ brand_name }} identity. Replace each placeholder before publishing the first consumer. +Canonical source for the {{ brand_name }} identity. Every section below is a +placeholder — fill it in before publishing the first consumer. ## Identity -Describe what {{ brand_name }} is, who it serves, and what it must not be confused with. +Describe what {{ brand_name }} is, who it serves, and what it must not be +confused with. ## Mark -Document primary mark, clear space, minimum size, and approved variants. +Document the primary mark, clear space, minimum size, and approved variants. ## Color -| Role | Light | Dark | Usage | -| ------ | --------- | --------- | ------------------ | -| Paper | `#f5f5f0` | `#151515` | Page surfaces | -| Ink | `#151515` | `#f5f5f0` | Primary content | -| Muted | `#666660` | `#aaa9a0` | Supporting content | -| Line | `#d0d0c8` | `#3a3a36` | Borders and rules | -| Accent | `#6cff8f` | `#6cff8f` | Focus and emphasis | +The shipped tokens are neutral greys plus a functional focus color. They exist +so the toolkit renders legibly out of the box, not because they are the brand. +Replace the values in `tokens.css`, then record them here. + +| Token | Light | Dark | Usage | +| ----------- | ----- | ---- | ----------------- | +| `bg` | | | Page background | +| `bg-raised` | | | Raised surfaces | +| `fg` | | | Primary text | +| `fg-muted` | | | Supporting text | +| `border` | | | Borders and rules | +| `focus` | | | Focus rings | + +Two rules worth keeping whatever the palette becomes: + +- Name tokens for the role a color plays, not a material it imitates. Roles + survive a redesign; metaphors have to be renamed or quietly start lying. +- Give every color a job. A single decorative accent applied wherever a page + looks flat is how unrelated products end up looking identical. + +Verify each foreground against the surfaces behind it — WCAG AA is 4.5:1 for +normal text, and a focus ring needs 3:1. Assert it in a test so the palette +cannot drift. ## Typography diff --git a/javascript/uitk-webawesome/index.html.jinja b/javascript/uitk-webawesome/index.html.jinja index 2e9d947..168e927 100644 --- a/javascript/uitk-webawesome/index.html.jinja +++ b/javascript/uitk-webawesome/index.html.jinja @@ -19,8 +19,8 @@
-

Brand toolkit

<{{ element_prefix }}-brand-mark> +

{{ brand_name }}

{{ project_description }}

<{{ element_prefix }}-theme-toggle data-testid="theme-toggle">
diff --git a/javascript/uitk-webawesome/src/lit/components/brand-mark.ts.jinja b/javascript/uitk-webawesome/src/lit/components/brand-mark.ts.jinja index ce4e8eb..61bd621 100644 --- a/javascript/uitk-webawesome/src/lit/components/brand-mark.ts.jinja +++ b/javascript/uitk-webawesome/src/lit/components/brand-mark.ts.jinja @@ -19,9 +19,9 @@ export class BrandMark extends LitElement { height: 2rem; place-items: center; border-radius: 50%; - color: var(--{{ css_prefix }}-color-paper); - background: var(--{{ css_prefix }}-color-ink); - box-shadow: 3px 3px 0 var(--{{ css_prefix }}-color-accent); + color: var(--{{ css_prefix }}-bg); + background: var(--{{ css_prefix }}-fg); + box-shadow: 3px 3px 0 var(--{{ css_prefix }}-focus); } `; diff --git a/javascript/uitk-webawesome/src/lit/components/theme-toggle.ts.jinja b/javascript/uitk-webawesome/src/lit/components/theme-toggle.ts.jinja index a882c7d..6d7de76 100644 --- a/javascript/uitk-webawesome/src/lit/components/theme-toggle.ts.jinja +++ b/javascript/uitk-webawesome/src/lit/components/theme-toggle.ts.jinja @@ -9,9 +9,9 @@ export class ThemeToggle extends LitElement { static styles = css` wa-button::part(base) { - border-color: var(--{{ css_prefix }}-color-line); - color: var(--{{ css_prefix }}-color-ink); - background: var(--{{ css_prefix }}-color-paper); + border-color: var(--{{ css_prefix }}-border); + color: var(--{{ css_prefix }}-fg); + background: var(--{{ css_prefix }}-bg); } `; diff --git a/javascript/uitk-webawesome/src/lit/styles/tokens.css.jinja b/javascript/uitk-webawesome/src/lit/styles/tokens.css.jinja index 5107b0e..13bf438 100644 --- a/javascript/uitk-webawesome/src/lit/styles/tokens.css.jinja +++ b/javascript/uitk-webawesome/src/lit/styles/tokens.css.jinja @@ -1,29 +1,57 @@ +/* Design tokens for {{ brand_name }}. + * + * These are deliberately neutral placeholders, not a finished palette. Replace + * the values with the brand's own before shipping; the names are the contract + * consumers depend on, so prefer changing values over renaming. + * + * Names describe the role a color plays, not a material it imitates. + */ + :root { - --{{ css_prefix }}-color-paper: #f5f5f0; - --{{ css_prefix }}-color-ink: #151515; - --{{ css_prefix }}-color-muted: #666660; - --{{ css_prefix }}-color-line: #d0d0c8; - --{{ css_prefix }}-color-accent: #6cff8f; + /* surfaces */ + --{{ css_prefix }}-bg: #ffffff; + --{{ css_prefix }}-bg-raised: #f6f6f6; + + /* text */ + --{{ css_prefix }}-fg: #171717; + --{{ css_prefix }}-fg-muted: #5c5c5c; + + /* structure */ + --{{ css_prefix }}-border: #e0e0e0; + + /* Focus is a functional requirement, not decoration. It needs at least 3:1 + * against both surfaces above; check it whenever the palette changes. */ + --{{ css_prefix }}-focus: #1a56c4; + --{{ css_prefix }}-font-sans: ui-sans-serif, system-ui, sans-serif; --{{ css_prefix }}-font-mono: ui-monospace, monospace; - --wa-color-surface-default: var(--{{ css_prefix }}-color-paper); - --wa-color-text-normal: var(--{{ css_prefix }}-color-ink); - --wa-color-brand-fill-loud: var(--{{ css_prefix }}-color-accent); + + /* Bridge into Web Awesome. Only surfaces, text, and borders are mapped: + * Web Awesome's brand color is the consumer's to choose, so it is left at + * the library default rather than pointed at a placeholder. */ + --wa-color-surface-default: var(--{{ css_prefix }}-bg); + --wa-color-surface-raised: var(--{{ css_prefix }}-bg-raised); + --wa-color-text-normal: var(--{{ css_prefix }}-fg); + --wa-color-text-quiet: var(--{{ css_prefix }}-fg-muted); + color-scheme: light; } :root[data-theme='dark'] { - --{{ css_prefix }}-color-paper: #151515; - --{{ css_prefix }}-color-ink: #f5f5f0; - --{{ css_prefix }}-color-muted: #aaa9a0; - --{{ css_prefix }}-color-line: #3a3a36; + --{{ css_prefix }}-bg: #121212; + --{{ css_prefix }}-bg-raised: #1c1c1c; + --{{ css_prefix }}-fg: #ededed; + --{{ css_prefix }}-fg-muted: #a6a6a6; + --{{ css_prefix }}-border: #333333; + --{{ css_prefix }}-focus: #8ab4ff; + color-scheme: dark; } :root { - color: var(--{{ css_prefix }}-color-ink); + color: var(--{{ css_prefix }}-fg); font-family: var(--{{ css_prefix }}-font-sans); - background: var(--{{ css_prefix }}-color-paper); + background: var(--{{ css_prefix }}-bg); } body { @@ -31,15 +59,13 @@ body { } main { - width: min(calc(100% - 3rem), 72rem); + width: min(calc(100% - 3rem), 64rem); margin: 0 auto; - padding: 6rem 0; + padding: 3rem 0 6rem; } -.eyebrow { - color: var(--{{ css_prefix }}-color-muted); - font-family: var(--{{ css_prefix }}-font-mono); - font-size: 0.75rem; - letter-spacing: 0.14em; - text-transform: uppercase; +button:focus-visible, +a:focus-visible { + outline: 2px solid var(--{{ css_prefix }}-focus); + outline-offset: 4px; }