Skip to content

Visual modernization: Inter, true-neutral ramp, retuned type scale, real design system page - #815

Merged
rob-gordon merged 2 commits into
devfrom
robgordon/visual-modernization
Aug 17, 2026
Merged

Visual modernization: Inter, true-neutral ramp, retuned type scale, real design system page#815
rob-gordon merged 2 commits into
devfrom
robgordon/visual-modernization

Conversation

@rob-gordon

Copy link
Copy Markdown
Member

A "fresh coat of paint" pass. Two commits, deliberately separated — the first is a pure addition + bug fixes and is safe to ship on its own.

⚠️ Merge timing is a judgement call, not a blocker. The pricing revert (#813 / v1.68.0) shipped the same day this was built. Landing a repaint in the same measurement window means neither result is cleanly attributable, and per the subscription-cliff decomposition a repaint plausibly only moves home→pricing (÷1.5), not the pricing→purchase ÷3. Suggest holding until the pricing read is in.

Why it looked dated — measured, not vibes

Every cause was a token, not markup. Settings.tsx is already clean Tailwind; it looked old because of what it was pointing at.

  1. The neutral ramp was warm/mauve. Every stop was red-dominant (#fcf8fb, #f3eef0, #362e2e) — which is why bg-neutral-100 buttons on /s read visibly pink. 571 usages.
  2. The type scale ran a full step large with 1.16 body leading (base was 19px/22px).
  3. IBM Plex Sans carried the whole interface, with font-bold 72 times.
  4. No radius or shadow disciplineborderRadius/boxShadow weren't in the config at all, and rounded-xs (not a Tailwind 3 class) was silently dead.
  5. Label was 2.3:1 contrast — failed WCAG AA, visibly washed out on /o.

What changed

Type. Inter for the interface, self-hosted and subset to latin + latin-ext with the opsz axis pinned to 14 — 874KB TTF → 101KB woff2 keeping the full 100–900 weight range. IBM Plex Sans stays as the display face for the wordmark and page titles. Scale retuned to 12/14/16/18/20/24/30/36/48 with body leading 1.16 → 1.5.

Colour. True-neutral ramp; background #FAFAF7#FAFAFA; foreground unified at #18181B across Tailwind and slang (whose hand-typed *Hsl siblings were drifting). neutral-500 on white goes 3.9:1 → 4.83:1, so Label now passes AA. blue/purple/green/orange deliberately untouched — purple is the pricing identity and blue reaches the frozen pricing surface for no visible gain.

Design system. /d rebuilt from 81 lines into a real inventory — foundations with live contrast readouts, every button variant × size × state, forms, overlays, and the recurring page patterns that aren't components yet. Everything is imported from where it lives, so it can't drift. Tokens live in src/lib/designTokens.js, which tailwind.config.js requires and the page renders. Gated on REACT_APP_VERCEL_ENV !== "production".

Two traps worth knowing about

slang.css was deciding the UI font, not tailwind.config.js. It sets html { font-family } after Tailwind's preflight at equal specificity. Editing the Tailwind config alone would have changed nothing. Overridden via :root, which beats slang's :where(:root).

TextResizer was inheriting the page font to measure node widths. toTheme.ts emits node styles under :childless, but findFontData matches selectorText === "node" exactly — so fontData is {} for every chart. Changing the typeface would have silently resized nodes on any chart whose CSS reads data(shapeWidth). The visual suite could never have caught this: it renders /f, which mounts <Graph> without GraphWrapper, so the resizer isn't even on the page. Font is now pinned in TextResizer.module.css — a no-op today, load-bearing forever after. Please don't remove it.

/pricing is repaired, not restyled

Six sites used arbitrary values that don't move with a scale change. Pricing2.tsx:398 would have gone from 18/19/23 to 18/16/18 — an inverted responsive ramp. Each was re-pointed at on-scale sizes reproducing the shipped rendering.

Two were settled by measuring in the browser rather than arguing:

  • The / optical nudge: the old 50/33px pair at -4px sat 2px below the price's optical centre, so 48/24px needs -7px for the same 2px.
  • sm:min-h-[210px] kept unchanged. Card content measures 183px now vs ~192px before — the floor was already active, dead space grew ~9px, and deleting it would have made the cards visibly shorter. Now carries a comment explaining it's a deliberate visual floor, not a grid equaliser.

Also fixed

Five latent class-name bugs, all verified dead in the current build — including Dialog.tsx's missing space, which was gluing ff-dialog onto the next class and therefore breaking Sandbox.tsx's lookup, so the sandbox upgrade prompt fired over open dialogs instead of waiting. Plus four text controls sitting under the 16px iOS auto-zoom threshold (two pushed there by this change, two already broken).

Verification

Check Result
pnpm -F app check / pnpm -F api check pass
pnpm -F app test 25 suites, 564 passed
pnpm -F app e2e 18 passed, 16 skipped by design, 0 failed
pnpm -F app visual 10/10 goldens pass UNCHANGED (run twice)
production build font asset + preload + inlined @font-face present; /d absent from main bundle

Manual pass in light and dark across /, /d, /s, /o, /l, /pricing. Glyph coverage measured per locale: 0 missing for fr/de/es/pt-br; ko/hi/zh fall through to system faces exactly as they did with Plex.

  • Docs updated — n/a in-repo; see note below
  • QA on mobile — text controls audited to ≥16px
  • Dark Mode — every surface checked; Pricing2.tsx:386 gained a border after its shadow-md-only separation stopped reading
  • GA Instrumentation — n/a, no tracking changes
  • Translations — no copy changed, so no catalog regeneration

Two follow-ups, neither in this diff: CLAUDE.md is stale — it says 13 templates (actually 10) and 23 suites/322 tests (actually 25/564). And the two commits are separated on purpose, so a squash-merge would collapse that.

🤖 Generated with Claude Code

rob-gordon and others added 2 commits August 17, 2026 15:47
Groundwork for the visual modernization pass. Pure addition and repair —
nothing here changes how the app looks yet.

Design system (/d):
- Rebuild as a real inventory: foundations (ramps with contrast readouts,
  type scale, radius, shadow, focus), typography, every button variant,
  forms, feedback/overlays, layout primitives, and the recurring page
  patterns that are not components yet. Every element is imported from
  where it lives so the page cannot drift from reality.
- Self-contained light/dark toggle that restores on unmount.
- Gate the route behind REACT_APP_VERCEL_ENV !== "production" (the house
  idiom, per PosthogWrapper) plus a noindex meta.

New src/lib/designTokens.js: single source of truth that tailwind.config.js
requires and /d imports, so the swatches cannot lie.

Pin the TextResizer font. This hidden element measures node label text for
getSize(), whose output lands in every node's data. findFontData() only
reads a rule whose selector is exactly `node`, but toTheme() emits node
styles under `:childless` — so fontData is {} for every chart and the
element was inheriting the UI font. Without this pin, changing the app
typeface would silently resize nodes on any chart whose CSS reads
data(shapeWidth). No-op today; load-bearing next commit.

Bug fixes, all verified dead in the current build:
- Dialog.tsx: a missing space glued `ff-dialog` onto the next class,
  producing `ff-dialogdata-[state=open]:...`. Cost overflowV dialogs their
  open animation AND broke Sandbox.tsx's `.ff-dialog` lookup, so the
  sandbox upgrade prompt fired over open dialogs instead of waiting.
- Select.tsx: `focus:show-md` (typo for shadow-md), `rounded-xs` (not a
  Tailwind 3 class), and a duplicated `flex`.
- LogIn.tsx: `text-md`, which has no key in the scale.
- ImportDataDialog.tsx: `italics` typo.
- Typography.tsx: `text-${size}` cannot be extracted by Tailwind's scanner;
  it only worked because every value happened to appear as a literal
  elsewhere. Replaced with a static map before the page pass can remove the
  last literal and turn a latent bug into a live one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The app read as dated for four measurable reasons, all of them tokens
rather than markup:

- The `neutral` ramp was warm/mauve, not neutral. Every stop was
  red-dominant (#fcf8fb, #f3eef0, #362e2e), which is why neutral surfaces
  looked visibly pink. 571 usages, all fixed by one config change.
- The type scale ran a full step large with 1.16 body leading
  (base was 19px/22px), making everything chunky and hard to read.
- IBM Plex Sans carried the whole interface, with font-bold 72 times.
- No radius or shadow discipline; borderRadius and boxShadow were not in
  the config at all.

Tokens now live in src/lib/designTokens.js, which tailwind.config.js
requires and /d renders, so the design system page cannot drift.

Type:
- Inter for the interface, self-hosted and subset to latin + latin-ext
  with the opsz axis pinned to 14 — 874KB TTF down to a 101KB woff2 that
  keeps the full 100-900 weight range. Preloaded, declared in index.html.
  Verified: full glyph coverage for fr/de/es/pt-br; ko/hi/zh fall through
  to system faces exactly as they did with Plex.
- IBM Plex Sans stays on the Google CDN as the display face (wordmark,
  page titles), because the same family name is a legacy chart font and
  customCssOnly charts depend on document-level availability.
- slang.css sets `html { font-family }` after Tailwind's preflight at
  equal specificity, so it — not tailwind.config.js — was deciding the UI
  font. Overridden via :root, which beats slang's :where(:root).
- Scale retuned to 12/14/16/18/20/24/30/36/48 with real body leading
  (1.16 -> 1.5) and optical tracking at display sizes.

Colour: neutral ramp swapped for a true neutral; background #FAFAF7 ->
#FAFAFA; foreground unified at #18181B across Tailwind and slang, whose
hand-typed *Hsl siblings were updated with it. neutral-500 on white goes
3.9:1 -> 4.83:1, so Label (previously 2.3:1) now passes WCAG AA.
blue/purple/green/orange deliberately untouched — purple is the pricing
identity and blue reaches the frozen pricing surface for no visible gain.

Focus ring unified on blue-600; it previously shipped as three different
values in three spellings.

/pricing is repaired, not restyled. Six sites used arbitrary values that
do not move with a scale change — Pricing2.tsx:398 would have inverted
from 18/19/23 to 18/16/18. Each was re-pointed at on-scale sizes that
reproduce the shipped rendering. The "/" optical nudge was re-measured in
the browser (the old 50/33px pair at -4px sat 2px below the price's
optical centre; 48/24px needs -7px for the same 2px). min-h-[210px] was
measured and kept: content is 183px, so the floor stays active and the
card outline is unchanged.

Also fixed four text controls that sat under the 16px iOS auto-zoom
threshold (two pushed there by this change, two already broken).

Verified: 25 suites / 564 unit tests, 18 e2e, and all 10 template visual
goldens pass UNCHANGED — chart rendering is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flowchart-fun Ready Ready Preview Aug 17, 2026 8:15pm

Request Review

@rob-gordon
rob-gordon added this pull request to the merge queue Aug 17, 2026
Merged via the queue into dev with commit f7cd08a Aug 17, 2026
5 checks passed
@rob-gordon
rob-gordon deleted the robgordon/visual-modernization branch August 17, 2026 20:20
@rob-gordon rob-gordon mentioned this pull request Aug 17, 2026
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