Skip to content

Generate the site's design tokens too, and fix the grey it exposed - #285

Merged
fstubner merged 1 commit into
mainfrom
fix/site-token-generation
Aug 29, 2026
Merged

fstubner merged 1 commit into
mainfrom
fix/site-token-generation

Conversation

@fstubner

Copy link
Copy Markdown
Owner

site/design-tokens.json was hand-written — the exact defect #283 argued against, on the other surface.

The stated reason it was hand-written did not survive checking

The comment in site/src/styles/tokens.css said its values were measured from a served build rather than copied from the CSS, because several are reached through --sl-color-text and other indirections and "the declared value and the painted one are not always the same."

That concern was real but does not apply to the keys the gate reads. Generating the file reproduced eight of its ten hand-measured values byte for byte — the entire dark theme, and the accent that resolves through var(--netscli-accent). Only the two I deliberately changed differ.

What changed, and why

before after
light text-muted #667387 #647084 4.44:1 on the card surface → 4.62:1
light surface-base #fbfbfb (bg) #f4f6f8 (gray-6) pin the gate to the tightest surface

--sl-color-gray-3 is the docs footer, the built-with row and the mobile section labels — much of it at 12px. It had already been raised once (#718096 → #667387) to clear 4.5:1 against the page background. Nothing checked it against --sl-color-gray-6, the slightly darker card surface, where it sat at 4.44:1. Mapping surface-base to the flattering surface is precisely what let that hide, which is why it now maps to the tight one, matching the rule #283 set for the app.

One script, two surfaces

Rather than duplicate the contrast maths, scripts/design-tokens.mjs takes a target table. Two parser changes were needed for the site, both load-bearing:

  • Selector-list matching. Starlight's tokens live on html[data-theme='dark'], html[data-theme='dark'] ::backdrop {. The old anchored ^selector\s*\{ match misses that entirely and would have reported "no rule" rather than reading it.
  • One level of var() resolution, for --sl-color-accent: var(--netscli-accent).

The check now runs in both workflows

ci.yml gates it on code, which a site-only PR sets to false — so without adding it to site.yml, the site's own contrast could regress with nothing looking at it. It takes milliseconds; running it twice means neither workflow can be the one that forgot.

Verification

  • 124 contrast pairs ≥ 4.5:1 across both surfaces (84 app + 40 site).
  • Sabotage-checked. Reverting gray-3 to its old #718096 fails with all four light surfaces named and reproduces the historical 3.88:1 figure; hand-editing either JSON is caught as drift; regressing an app token still fails. All restored, green.
    • Worth noting: my first sabotage attempt passed when it shouldn't have — String.replace had hit the hex inside the comment I'd just written, and comments are stripped before parsing. The test was wrong, not the guard, but it is the reason to re-target and re-run rather than accept a convenient result.
  • Site gate green on this branch: build ok, astro check 0 errors/warnings/hints, dead-CSS budget 24/24, changelog dates 8/8, file-size guard clean, and axe: 0 violations across 14 pages in both themes.

site/design-tokens.json was hand-written -- the exact defect #283 argued
against, on the other surface. The reasoning at the time was that values reached
through --sl-color-text and other indirections are not always the declared one,
so they were measured from a served build instead. That concern was real but did
not apply here: generating the file reproduced eight of its ten hand-measured
values byte for byte, including the whole dark theme and the accent that
resolves through `var(--netscli-accent)`.

The two that changed are deliberate:

- light text-muted #667387 -> #647084. --sl-color-gray-3 is the docs footer,
  built-with row and mobile section labels, much of it at 12px. It had been
  raised once already, to clear 4.5:1 against the page background, but nothing
  checked it against --sl-color-gray-6, the card surface, where it sat at
  4.44:1. It now clears every light surface.
- light surface-base now maps to gray-6 rather than bg, matching the app's
  rule of pinning the gate to the tightest surface rather than the flattering
  one. Mapping bg is what let 4.44:1 go unnoticed.

One script now covers both surfaces rather than duplicating the contrast maths.
Two parser changes were needed for the site: selector-list matching, because
Starlight's tokens live on `html[data-theme='dark'], html[data-theme='dark']
::backdrop {` which an anchored match misses entirely; and one level of var()
resolution, for `--sl-color-accent: var(--netscli-accent)`.

The check now runs in site.yml as well as ci.yml. ci.yml gates it on `code`,
which a site-only pull request sets to false, so the site's own contrast could
otherwise regress with nothing looking at it. It takes milliseconds, so neither
workflow can be the one that forgot.
@github-actions

Copy link
Copy Markdown
Contributor

Site preview: https://pr-285.netscli-site-preview.pages.dev

Built from d8a69c5 with NETSCLI_PREVIEW=1 — noindex, and analytics disabled so it does not report into netscli.com's numbers.

Production is unaffected: netscli.com is served from GitHub Pages via pages.yml, which is manual-only.

@fstubner
fstubner merged commit 6f3b9ea into main Aug 29, 2026
16 checks passed
@fstubner
fstubner deleted the fix/site-token-generation branch August 29, 2026 03:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant