Restyle four UI surfaces onto one visual system - #67
Merged
Conversation
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>
`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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
order-last w-full max-w-xldoes not wrap: per Flexbox §9.3 the flex base size is clamped bymax-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 innerspansobasis-fullcan actually break the line. Verified by measuring element positions at 1440px and 1024px.getByRole('region', {name:'Updates'}). The wrapper is a plaindivnow; 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-domon the attachment thumbnail is fixed rather than dismissed (e3c265d). The alert is pre-existing — the same one is open againstmain, and the security-relevant code was byte-identical; CodeQL attributed it here because a neighbouring className changed.startsWith("image/")also matchesimage/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 onmaintoo.Verification
pnpm lint·tsc --noEmit -p tsconfig.app.json· 463 vitest suites · 136 Playwright visual tests ·pnpm build— all pass. Behaviour parity withmainwas checked mechanically, not by eye: declarations, hook-call histogram, everyonClick/onChange/onSelect/onSubmit/onKeyDown, and the values of all 21aria-labels. Visual baselines regenerated only for widths that intentionally changed.Deliberately left alone
ModelSlot/ProviderForm/ProviderDirectorystill 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