Skip to content

Restyle four UI surfaces onto one visual system - #67

Merged
TsekaLuk merged 4 commits into
mainfrom
feat/ui-visual-system
Aug 21, 2026
Merged

Restyle four UI surfaces onto one visual system#67
TsekaLuk merged 4 commits into
mainfrom
feat/ui-visual-system

Conversation

@TsekaLuk

@TsekaLuk TsekaLuk commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Why

Four surfaces had each drifted into their own dialect: four competing row containers in Settings alone, two nav rails sharing no tokens, two different loading skeletons for the same card grid, and radii, type steps and muted fills invented file by file. This lands one contract — spacing rhythm, radii, type scale, button hierarchy — and moves Settings, the release-notes dialog, project home and the project directory onto it.

Layout and visuals only. No data flow, hook, state shape or handler changed.

Two bugs found by rendering, not by reading

  • The project-directory header was broken at ≥1100px. order-last w-full max-w-xl does not wrap: per Flexbox §9.3 the flex base size is clamped by max-width, so it fitted on line one and the description rendered beside the title — running under the brandmark and squeezing the primary action between them. Fixed by moving the measure cap to an inner span so basis-full can actually break the line. Verified by measuring element positions at 1440px and 1024px.
  • A duplicate nested landmark. Adding a section header to Updates wrapped a region named "Updates" inside one named "Updates and support" — landmark noise for screen readers, and an ambiguous getByRole('region', {name:'Updates'}). The wrapper is a plain div now; its two children were already the landmarks.

A first-run picker was built and then deleted

An "areas of work" onboarding modal was built in this branch and removed again in c46416c. Writing down the trade killed it: the six areas it asks about are the six composer presets, which already sit as an always-visible chip row above the home composer. The only honest use for a stored selection is reordering those six chips — and the modal blocks first launch to collect it, before the user has the context to answer.

The pattern works where it was copied from because those areas route to genuinely different agent workflows. Ours are six prompt prefixes on one composer. The layout transfers; the interaction does not. The full implementation, tests included, is recoverable from 10e55aa.

Security

js/xss-through-dom on the attachment thumbnail is fixed rather than dismissed (e3c265d). The alert is pre-existing — the same one is open against main, and the security-relevant code was byte-identical; CodeQL attributed it here because a neighbouring className changed. startsWith("image/") also matches image/svg+xml, and an SVG behind an object URL is a document that can carry external references. Not a live hole (<img> does not run scripts in SVG), but the preview never needed anything wider than raster, so it now takes an allowlist. This clears the standing alert on main too.

Verification

pnpm lint · tsc --noEmit -p tsconfig.app.json · 463 vitest suites · 136 Playwright visual tests · pnpm build — all pass. Behaviour parity with main was checked mechanically, not by eye: declarations, hook-call histogram, every onClick/onChange/onSelect/onSubmit/onKeyDown, and the values of all 21 aria-labels. Visual baselines regenerated only for widths that intentionally changed.

Deliberately left alone

ModelSlot / ProviderForm / ProviderDirectory still carry pre-existing contract violations and need their own pass. Error states have no shared pattern across surfaces. The amber warning literals have no achromatic token to move to.

🤖 Generated with Claude Code

Settings, the release-notes dialog, project home, the project directory and a
new first-run picker were each drifting: four competing row containers, two nav
rails sharing no tokens, two loading skeletons for the same grid, and radii,
type steps and muted fills invented per file. This lands one contract — spacing
rhythm, radii, type scale, button hierarchy — and moves all five onto it.

Layout and visuals only. No data flow, hook, state shape or handler changed;
parity was checked mechanically against HEAD rather than by eye (declarations,
hook-call histogram, every onClick/onChange/onSelect/onSubmit/onKeyDown, and the
values — not just the count — of all 21 aria-labels).

Two bugs surfaced by rendering rather than reading:

- The project-directory header was broken at ≥1100px. `order-last w-full
  max-w-xl` does not wrap: per Flexbox §9.3 the flex base size is clamped by
  max-width, so it fitted on line one and the description rendered beside the
  title, under the brandmark and squeezing the primary action. The measure cap
  moved to an inner span so `basis-full` can break the line.
- Adding a section header to Updates wrapped a region named "Updates" in one
  named "Updates and support" — duplicate nested landmarks, and an ambiguous
  `getByRole('region', {name:'Updates'})`. The wrapper is a plain div now; its
  two children were already the landmarks.

The onboarding picker's tile descriptions reused the home composer preset ids,
which are prompt *prefixes* — they rendered as half-sentences trailing off
mid-clause ("Design a responsive web experience for"). They have their own ids
now. Its dialog copy also claimed Cutout uses the selection "to decide what to
show you first", which is not true of anything yet; softened while the id is
still new.

The picker ships unreferenced by design: `initializeWorkAreasOnboarding` returns
shouldOpen:false unconditionally and nothing imports the dialog. When it opens,
and what the stored areas affect, are still product decisions.

43 new message ids translated into all four shipped locales. Visual baselines
regenerated for the widths that intentionally changed.

Known gaps left alone deliberately: ModelSlot/ProviderForm/ProviderDirectory
still carry pre-existing contract violations and need their own pass; error
states have no shared pattern; the amber warning literals have no achromatic
token to move to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/components/home/ProjectHome.tsx Dismissed
TsekaLuk and others added 2 commits August 21, 2026 11:29
`startsWith("image/")` also matches `image/svg+xml`, so an attached SVG got an
object URL and was rendered through `<img>`. Browsers do not run scripts in SVG
loaded that way, so this was not a live hole — but an SVG behind a blob URL is a
document that can carry its own external references, and the preview never
needed anything wider than raster. An allowlist costs nothing.

This is the `js/xss-through-dom` alert CodeQL raised on the PR. The alert is
pre-existing — the same one is open against main, and the security-relevant code
here is byte-identical to main; CodeQL only attributed it to this branch because
a neighbouring className changed. Fixing it rather than dismissing it also
clears the standing alert on main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It was built from a competitor screenshot, not from a question we needed
answered, and the code makes that plain.

The six areas it asks about are the six composer presets, which already sit as
an always-visible chip row above the home composer (ProjectHome.tsx:378 defines
them, :470 renders them in one `overflow-x-auto` line). Clicking one prefills a
prompt prefix. So the only honest thing a stored selection could do is reorder
those six chips — and the modal blocks first launch to collect it, before the
user has the context to answer.

That trade does not survive being written down: friction at the worst possible
moment, persistent state, 13 message ids across five locales, and a surface to
maintain, in exchange for saving one scan of a six-item row that is already
fully visible.

The pattern works where it was copied from because those areas route to
genuinely different agent workflows. Ours are six prompt prefixes on one
composer. The layout transfers; the interaction does not.

Its two available fates were both worse than deletion: stay unreferenced as
dead code that reads as live, or get wired up and ship the friction.

The full implementation, tests and fixtures included, is at 10e55aa:

  git checkout 10e55aa -- src/components/onboarding src/hooks/queries/work-areas.ts \
    src/services/work-areas-prefs.local.ts tests/visual/areas-of-work.spec.ts \
    tests/visual/fixtures/areas-of-work.*

If onboarding is wanted, it should start from what we need to learn and what
changes once we know it — not from a screenshot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@TsekaLuk TsekaLuk changed the title Restyle five UI surfaces onto one visual system Restyle four UI surfaces onto one visual system Aug 21, 2026
The provider row rendered " 个模型" with no number. All four translations of
`settings.catalog_model_count` wrote `{0}` while the source message names the
placeholder `{catalogSize}`, so the value never bound and the count rendered
empty.

lingui names a placeholder after the expression behind it: a bare identifier
becomes `{catalogSize}`, a complex expression degrades to positional `{0}`,
`{1}`. `settings.provider_option_with_count` interpolates
`providerCatalogModels(provider).length`, so it really is `{0}`/`{1}` and
rendered fine — which is why the model picker showed "MOX (20 models)" while
the row beside it showed nothing. The translations were written to the wrong
convention for this one id.

Nothing in the pipeline could see it: the catalog compiles, `i18n:extract`
reports zero missing entries, and no unit or visual test asserts the rendered
count. It shipped in v0.1.25 and was found by a user screenshot. So this also
adds `src/locales/placeholder-parity.test.ts`, which compares placeholder names
between each locale and the source catalog. Verified by reintroducing the bug:
the test fails with the exact id and both names, and passes once restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@TsekaLuk
TsekaLuk merged commit cb80044 into main Aug 21, 2026
14 checks passed
@TsekaLuk
TsekaLuk deleted the feat/ui-visual-system branch August 21, 2026 09:00
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.

2 participants