refactor(ui): success tone + Heading props, and migrate the colour/heading sites - #2639
refactor(ui): success tone + Heading props, and migrate the colour/heading sites#2639camielvs wants to merge 3 commits into
Conversation
Two gaps that blocked migrating call sites onto the primitives: - No tone mapped to green, so success text had to use raw palette classes. `--success` and `--color-success` already exist in both palettes, so the variant is all that was missing. - `Heading` accepted only `children` and `level` and dropped everything else, while its sibling `Paragraph` forwards rest props to `Text`. Headings that needed a tone, size, weight or className could not use it at all. `Heading` now forwards rest props, with the level-derived size/weight kept as defaults a caller can override. `as` is omitted from the accepted props and `role`/`aria-level` are applied after the spread, so neither the element nor the accessibility attributes can be overridden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
19 sites across 13 files: gray-* → subdued, red-* → critical, green-* → success. 7 were raw <p>/<h4> and become Paragraph/Heading; 12 already used Text and only needed the className swapped for a tone. This also fixes ImportComponent.tsx, where a raw palette colour was paired with dark:text-muted-foreground — the light and dark variants disagreed, and subdued is what the dark half already asked for. Size and weight are preserved per site. The two ImportPipeline headings pass size="md" to keep the base font size a raw <h4> inherited, and keep font-medium via className since there is no matching weight variant. Out of scope, left alone: container border/background palette classes (tone only covers text), lucide icon colours, and Label, which takes no tone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
13 sites across 9 files: 10 `Text as="h*"` and 3 raw <h4>. Every site keeps
its exact size, weight, tone and className, so the only rendering change is
the role="heading" and aria-level Heading adds.
Two of them — PressedKeysList and StatComponents — were already passing
role="heading" and aria-level={3} by hand, i.e. reimplementing Heading at the
call site. SearchFilter's bare <h4> passes size="md" to keep the base font
size it inherited as raw markup.
Headings in test files stay raw markup: they are fixtures, not product UI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🎩 PreviewA preview build has been created at: |
morgan-wowk
left a comment
There was a problem hiding this comment.
🤖 Agent review. The tone/Heading migration is mostly sound — no heading-level or a11y regressions (role/aria-level applied after the {...rest} spread, as omitted). Two visual items to confirm (inline).
| <InlineStack gap="2"> | ||
| <Icon name="OctagonAlert" size="lg" className="text-destructive" /> | ||
| <Text tone="critical" as="h2" size="lg"> | ||
| <Heading level={2} tone="critical" size="lg"> |
There was a problem hiding this comment.
🤖 This silently changes weight 400→600: the old <Text ... size="lg"> had no weight (default regular, dead class → inherited ~400), but <Heading level={2}> defaults level<3 to semibold (600). That contradicts the PR's stated invariant ('the only rendering change is role/aria-level'). Likely a benign bolding, but pass weight="regular" to truly preserve, or confirm it's intended.
| {successMessage && ( | ||
| <div className="border border-green-200 bg-green-50 p-3 rounded-md mt-4 dark:border-green-500/30 dark:bg-green-500/10"> | ||
| <h4 className="text-green-600 font-medium mb-1 dark:text-green-300"> | ||
| <Heading |
There was a problem hiding this comment.
🤖 Possible contrast regression: the success heading/message move from text-green-600 / dark:text-green-300 to tone="success" (text-success = oklch(0.7 0.18 140)) while the container stays bg-green-50. text-success at L=0.7 is lighter than green-600, so light-mode contrast on the pale-green box drops and may fall below WCAG AA. Also --success is identical in light and dark, so the previously-distinct dark-mode green (green-300) is gone. Worth a light+dark AA check on this success box (same applies to the <Paragraph tone="success"> just below).
Resolves B18 and B19b of #2626. Three commits: the primitive changes both items needed, then one migration commit each.
Why the primitive had to change first
Both entries asked for call-site migrations that were impossible as the primitives stood:
tonemapped to green, so success text had to use raw palette classessuccess: "text-success".--successand--color-successalready existed in both palettes — only the variant was missingHeadingaccepted onlychildrenandleveland dropped every other prop, while its siblingParagraphforwards rest props toText. Any heading needing a tone, size, weight or className could not use itHeadingnow forwards rest props, with the level-derived size/weight as overridable defaultsHeading's widening is deliberately conservative:asis omitted from the accepted props so the element can't be swapped, androle="heading"/aria-levelare applied after the spread so a caller can't clobber the accessibility attributes.B18 — 19 colour sites across 13 files
gray-*→subdued,red-*→critical,green-*→success. 7 were raw<p>/<h4>and becameParagraph/Heading; 12 already usedTextand only neededclassNameswapped for atone.The entry claimed 20 sites; 19 is what is actually there. #2631 landed in the meantime and covered a different set — I re-derived the list rather than trusting the count.
This also fixes a real inconsistency at ImportComponent.tsx:228, which read
text-sm text-gray-600 dark:text-muted-foreground— the dark variant was already asking forsubduedwhile the light half stayed raw palette.Deliberately out of scope, since
toneonly covers text: containerborder-*/bg-*palette classes (e.g. the green/red callout boxes inImportPipeline), lucide icon colours, andLabel, which takes notone.B19b — 13 heading sites across 9 files
10
Text as="h*"plus 3 raw<h4>. Every site keeps its exactsize,weight,toneandclassName, so the only rendering change is therole="heading"andaria-levelthatHeadingadds — which is the point of the migration.Two sites were already hand-rolling exactly that: PressedKeysList.tsx and StatComponents.tsx both passed
role="heading"andaria-level={3}toTextthemselves. They now say<Heading level={3}>.The entry counted "10 raw
<h1>–<h6>in 5 files"; 5 of those are in.test.tsxfixtures and stay raw markup — they are asserting rendering, not product UI. The 5 real ones are the 2 inImportPipeline(migrated in the B18 commit, since colour was the driver) and 3 here.Verified by compiling the theme, not by eye
text-successis a real utility only because--color-successis registered inglobal.css's@themeblock — in a stock Tailwind build it generates nothing. Rather than assume, I compiled the actual entry (src/styles/global.css) through Tailwind 4.3.3'scompile()API and asked which candidates emit a rule:Three dead variant values, pre-existing — found, not fixed
textVariantsemits three class names Tailwind never generates:size="md"text-mdtext-baseTextdefault — everyTextwithout an explicit sizeweight="regular"font-regularfont-normalTextdefault — everyTexttone="inverted"text-inverted--invertedtoken exists at allThe two defaults are why the codebase looks fine: emitting a dead class leaves the browser default in place, which is what
text-base/font-normalwould have given anyway. So this is latent, not a visible bug — but it does meansizeandweightsilently have no "reset to default" value, andtone="inverted"would render nothing if anyone used it.It is also load-bearing in this PR in a benign way:
size="md"is how a migrated raw element keeps the base font size it inherited, which is whySearchFilter's<h4>and the twoImportPipelineheadings pass it. Fixing the variants would change rendering at all 22size="md"sites plus every defaultedText, so it belongs in its own PR with its own visual diff. Worth filing separately.Validation
typecheck·lint·knip·prettierall clean — 191 files / 1,966 tests passing. No raw<h1>–<h6>and noText as="h*"remain outside test fixtures andtypography.tsxitself.Once merged, the B18 and B19b entries in #2626's Part B can be struck. B19a (the 27
flex flex-col→BlockStacksites) is untouched and still open — it needs a separate per-area ruling onw-full/items-start.