Skip to content

refactor(ui): success tone + Heading props, and migrate the colour/heading sites - #2639

Open
camielvs wants to merge 3 commits into
masterfrom
feat/typography-success-tone-and-heading-props
Open

refactor(ui): success tone + Heading props, and migrate the colour/heading sites#2639
camielvs wants to merge 3 commits into
masterfrom
feat/typography-success-tone-and-heading-props

Conversation

@camielvs

@camielvs camielvs commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Resolves B18 and B19b of #2626. Three commits: the primitive changes both items needed, then one migration commit each.

⚠️ Needs a visual diff — this changes rendered colour and adds heading semantics. Every site was migrated to preserve its exact size, weight and spacing, but that claim is worth checking against the running UI.

  • Visually diffed the rendered output — no layout/spacing/colour regression

Why the primitive had to change first

Both entries asked for call-site migrations that were impossible as the primitives stood:

Blocker Fix
No tone mapped to green, so success text had to use raw palette classes Added success: "text-success". --success and --color-success already existed in both palettes — only the variant was missing
Heading accepted only children and level and dropped every other prop, while its sibling Paragraph forwards rest props to Text. Any heading needing a tone, size, weight or className could not use it Heading now forwards rest props, with the level-derived size/weight as overridable defaults

Heading's widening is deliberately conservative: as is omitted from the accepted props so the element can't be swapped, and role="heading" / aria-level are 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 became Paragraph/Heading; 12 already used Text and only needed className swapped for a tone.

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 for subdued while the light half stayed raw palette.

Deliberately out of scope, since tone only covers text: container border-*/bg-* palette classes (e.g. the green/red callout boxes in ImportPipeline), lucide icon colours, and Label, which takes no tone.

B19b — 13 heading sites across 9 files

10 Text as="h*" plus 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 that Heading adds — which is the point of the migration.

Two sites were already hand-rolling exactly that: PressedKeysList.tsx and StatComponents.tsx both passed role="heading" and aria-level={3} to Text themselves. They now say <Heading level={3}>.

The entry counted "10 raw <h1><h6> in 5 files"; 5 of those are in .test.tsx fixtures and stay raw markup — they are asserting rendering, not product UI. The 5 real ones are the 2 in ImportPipeline (migrated in the B18 commit, since colour was the driver) and 3 here.

Verified by compiling the theme, not by eye

text-success is a real utility only because --color-success is registered in global.css's @theme block — 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's compile() API and asked which candidates emit a rule:

text-success           GENERATED     ← the new tone renders
text-warning           GENERATED
text-destructive       GENERATED
text-muted-foreground  GENERATED
text-md                -- dead --
font-regular           -- dead --
text-inverted          -- dead --

Three dead variant values, pre-existing — found, not fixed

textVariants emits three class names Tailwind never generates:

Variant value Emits Should be Used
size="md" text-md text-base the Text default — every Text without an explicit size
weight="regular" font-regular font-normal the Text default — every Text
tone="inverted" text-inverted no --inverted token exists at all 0 sites

The two defaults are why the codebase looks fine: emitting a dead class leaves the browser default in place, which is what text-base / font-normal would have given anyway. So this is latent, not a visible bug — but it does mean size and weight silently have no "reset to default" value, and tone="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 why SearchFilter's <h4> and the two ImportPipeline headings pass it. Fixing the variants would change rendering at all 22 size="md" sites plus every defaulted Text, so it belongs in its own PR with its own visual diff. Worth filing separately.

Validation

typecheck · lint · knip · prettier all clean — 191 files / 1,966 tests passing. No raw <h1><h6> and no Text as="h*" remain outside test fixtures and typography.tsx itself.

Once merged, the B18 and B19b entries in #2626's Part B can be struck. B19a (the 27 flex flex-colBlockStack sites) is untouched and still open — it needs a separate per-area ruling on w-full/items-start.

camielvs and others added 3 commits August 14, 2026 16:53
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>
@camielvs
camielvs requested a review from a team as a code owner August 15, 2026 00:06
@github-actions

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: feat/typography-success-tone-and-heading-props/bd6e293

@morgan-wowk morgan-wowk left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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).

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