Skip to content

Commit 480e5d1

Browse files
committed
fix(gallery): define --destructive, and correct the @theme inline rule
Delta-review fixes. --destructive and --destructive-foreground were mapped in public/input.css and used at 20 call sites for validation errors, but nothing ever defined them. An undefined var() is invalid at computed-value time, so every error message in the auth, forms, file-storage, rate-limit and server-action demos painted in the ordinary foreground colour, and the destructive button variant had no fill. Measured in the browser before and after: text-destructive was byte-identical to body text, and now resolves to the red. Pre-existing, but it is the same dead-token class the previous commit audited, in the same block, and it undercut this PR's claim that only token VALUES moved. --color-success went the other way and is removed. Nothing in the app uses success in any form, so defining it would have added exactly the kind of dead token the last round removed three of. The styling reference's new rule was wrong and is rewritten against a measurement rather than an assumption. A component's static styles IS inside the scanned source, so a raw var(--color-x) written there forces emission even under @theme inline; and the emission rule is about HOW a token is referenced, not about which block declares it. Both forms drop a token nothing references. The cell that actually bites is inline plus utility-only usage. The gallery's plain @theme is therefore a fine choice but not, as previously written, a load-bearing one, and the shadow demo was never at risk. Also corrects two comments this PR had made stale: the toggle's .dark mirror runs only on click while the bootstrap writes data-theme alone, so the class is absent on every page load and is not the kit-compatibility path the comment claimed; and --primary-tint now derives from --ring, not --primary.
1 parent 73f22c9 commit 480e5d1

4 files changed

Lines changed: 30 additions & 12 deletions

File tree

.agents/skills/webjs/references/styling.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,18 @@ The default stack is a static compiled Tailwind stylesheet (`css:build` compiles
109109

110110
**Two halves.** (1) `public/input.css` MAPS token names into Tailwind with `@theme inline` (`--color-background: var(--background)`), so `bg-background` resolves to `var(--background)`. That is infrastructure; leave it. (2) The root layout (`app/layout.ts`) DEFINES the values as plain CSS custom properties in a `<style>` block. That is your palette; make it your own. A freshly cleared app (after `npm run gallery:clear`) ships only the OS system-colour base (`Canvas` / `CanvasText`) with NO tokens, so building this palette is your first styling step.
111111

112-
**A token that raw CSS reads must come from a plain `@theme`, not `@theme inline`.** The two are not interchangeable and the difference is silent. A plain `@theme { --color-ring: var(--ring) }` emits `--color-ring` as a real custom property on `:root`, so anything can inherit it; `@theme inline` emits no custom property at all and instead substitutes the value directly into each compiled utility. Utilities work either way, which is why the mistake hides. It bites where Tailwind cannot see the CSS: a shadow-DOM component's `static styles` is outside the scanned source, so a `var(--color-ring)` written there resolves against whatever the shadow tree inherits, and under `inline` that is nothing, leaving the rule at its initial value (a border or outline silently falls back to `currentColor`). So if any shadow component reads a token by name, map that token with a plain `@theme`. A raw `var(--color-x)` written in the SAME stylesheet forces emission even under `inline`, which is why the `@webjsdev/ui` kit theme works despite using `inline`.
112+
**`@theme` and `@theme inline` differ in whether the token reaches `:root`, and the difference is silent.** Measured on `tailwindcss@4.3.0`, a token mapped in a theme block is emitted as a real `:root` custom property when:
113+
114+
| block | token used only through a utility (`border-border`) | token written as a raw `var(--color-x)` in any SCANNED file | token unused |
115+
|---|---|---|---|
116+
| `@theme` | emitted | emitted | not emitted |
117+
| `@theme inline` | NOT emitted (the value is substituted into the utility) | emitted | not emitted |
118+
119+
The one cell that bites is `inline` plus utility-only usage. Nothing on the page can then inherit `--color-x`, so a raw `var(--color-x)` written somewhere Tailwind never scanned resolves to nothing and the declaration falls back to its initial value (a border or outline silently becomes `currentColor`).
120+
121+
"Scanned" is wider than it looks, and this is the part worth knowing: Tailwind scans source files as raw text, so a `var(--color-ring)` inside a component's `static styles` template DOES count and forces emission, exactly like one in the stylesheet. That is why the `@webjsdev/ui` kit theme works despite using `inline`. So the rule is not "shadow components need a plain `@theme`". It is: **if a token is only ever used through utilities, and something outside the scanned source needs to inherit it, map that token with a plain `@theme`.** Anything under a configured `@source` is scanned and needs no special handling.
122+
123+
Whichever form you use, a token nothing references is dropped in both, so an unused mapping is dead configuration rather than a safety net.
113124

114125
**Light and dark, defined once (DRY).** Write each colour token ONE time with the native CSS `light-dark(LIGHT, DARK)` function and let `color-scheme` pick the side. The default `color-scheme: light dark` follows the OS; a `[data-theme]` attribute forces one. No duplicated light/dark blocks:
115126

gallery/app/layout.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ export default function RootLayout({ children }: { children: unknown }) {
109109
--border-strong: light-dark(oklch(0.78 0.014 70 / 0.95), oklch(0.36 0.02 60 / 0.95));
110110
--input: light-dark(oklch(0.88 0.012 70 / 0.9), oklch(0.24 0.015 60 / 0.9));
111111
--ring: light-dark(oklch(0.63 0.17 50), oklch(0.78 0.18 58));
112+
/* public/input.css maps --color-destructive, and 20 call sites across
113+
the demos use text-destructive / bg-destructive for validation
114+
errors, but nothing ever DEFINED it. An undefined var() left every
115+
error message painting in the ordinary foreground colour and the
116+
destructive button variant with no fill at all. Values follow the
117+
@webjsdev/ui registry theme, warmed a few degrees of hue to sit in
118+
this palette rather than beside it. */
119+
--destructive: light-dark(oklch(0.58 0.22 27), oklch(0.70 0.19 22));
120+
--destructive-foreground: light-dark(oklch(0.99 0.005 60), oklch(0.14 0.015 60));
112121
113122
--primary-tint: color-mix(in oklch, var(--ring) 22%, transparent);
114123
--accent-tint: color-mix(in oklch, var(--ring) 14%, transparent);

gallery/components/theme-toggle.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@ export class ThemeToggle extends WebComponent {
3535
// everything this app actually reads: the palette blocks in app/layout.ts
3636
// and the @custom-variant dark in public/input.css, which is keyed off
3737
// [data-theme] so the logo swap still works with JavaScript off. The .dark
38-
// class is inert HERE (this app imports only tailwindcss, never the kit
39-
// theme CSS that defines the &:is(.dark *) form), and is kept because it is
40-
// the signal @webjsdev/ui components read, so a demo that adds one works
41-
// without having to remember this line.
38+
// class is inert HERE, since this app imports only tailwindcss and never
39+
// the kit theme CSS that defines the &:is(.dark *) form. Kept only so the
40+
// two signals do not visibly disagree while the reader is clicking. It is
41+
// NOT a working kit-compatibility path: this runs only on a click, the
42+
// bootstrap in app/layout.ts writes data-theme alone, so on every page load
43+
// the class is absent whatever the stored theme is. A demo that adds a kit
44+
// component needs the class written at bootstrap too.
4245
const osLight = window.matchMedia('(prefers-color-scheme: light)').matches;
4346
const dark = next === 'dark' || (next === 'system' && !osLight);
4447
document.documentElement.classList.toggle('dark', dark);

gallery/public/input.css

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,10 @@
5757
--color-input: var(--input);
5858
--color-ring: var(--ring);
5959

60-
/* A translucent brand tint derived from --primary (the same pattern the
61-
scaffold's --primary-tint uses), for focus rings and the logo glow. */
60+
/* A translucent brand tint for focus rings, derived from --ring in
61+
app/layout.ts so the ring and its halo are the same hue. */
6262
--color-primary-tint: var(--primary-tint);
6363

64-
/* One extra semantic shadcn does not ship: success (a couple of status
65-
affordances use it). Added the canonical @theme way, not a parallel
66-
vocabulary. */
67-
--color-success: var(--success);
68-
6964
--font-display: var(--font-display);
7065
--font-sans: var(--font-sans);
7166
--font-serif: var(--font-serif);

0 commit comments

Comments
 (0)