Skip to content

Commit 564d777

Browse files
committed
fix(hub-ui,json-render-ui): rewire baked Wind3 colors so branding's primaryColor actually retints the UI
Both shadow-root packages compile their utility CSS with Wind3, which bakes theme colors to literal rgb() triplets at build time instead of referencing CSS variables. --devframe-primary (branding's primaryColor) was only ever consumed by the unused --colors-primary-* variables in primary-ramp.css, so overriding it never reached any actual primary/bg-primary/btn-primary utility - the dock, its hover states, and the settings panels stayed the default sage green regardless of branding. Post-process the compiled Wind3 CSS to rewrite each baked primary-ramp triplet into CSS relative-color syntax (rgb(from var(--colors-primary-<stop>, <hex>) r g b / <alpha>)), preserving Wind3's existing opacity mechanics while sourcing the base color from the live variable. Verified end-to-end in Chromium that overriding --devframe-primary now retints text-primary, bg-primary, and btn-primary (including its hover/focus-ring states).
1 parent b9f5c2f commit 564d777

7 files changed

Lines changed: 101 additions & 13 deletions

File tree

design/uno.config.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,57 @@ export const shadowSurfaceSafelist: string[] = [
123123
'color-active',
124124
'border-base',
125125
]
126+
127+
/**
128+
* The primary-ramp stops a shadow-root surface's `primary-ramp.css` exposes
129+
* as overridable `--colors-primary-<stop>` custom properties (derived from
130+
* `--devframe-primary`). Must match that file's declarations exactly.
131+
*/
132+
const OVERRIDABLE_PRIMARY_STOPS = ['DEFAULT', '600', '500', '400', '300'] as const
133+
134+
function hexToRgbTriplet(hex: string): string | undefined {
135+
const match = /^#([0-9a-f]{6})$/i.exec(hex)
136+
if (!match)
137+
return undefined
138+
const int = Number.parseInt(match[1], 16)
139+
return `${(int >> 16) & 255} ${(int >> 8) & 255} ${int & 255}`
140+
}
141+
142+
/**
143+
* Rewire a Wind3-compiled shadow-root stylesheet's baked-in `primary` theme
144+
* colors into CSS relative-color syntax reading the live `--colors-primary-*`
145+
* variables `primary-ramp.css` derives from `--devframe-primary`.
146+
*
147+
* Wind3 (unlike Wind4) resolves each theme color to a literal `rgb(r g b /
148+
* <alpha>)` at compile time — the `<alpha>` slot is already dynamic (a slash
149+
* literal, or the utility's own `--un-*-opacity` variable), but the base `r g
150+
* b` triplet is baked in, so every `primary`-based utility (`text-primary`,
151+
* `bg-primary`, `btn-primary`, `ring-primary-500`, …) ignores
152+
* `--devframe-primary` entirely — only hand-written rules that already
153+
* reference `--colors-primary-*` directly (the dock's glow gradient,
154+
* `primary-ramp.css` itself) retint. Swapping the baked triplet for `from
155+
* var(--colors-primary-<stop>, <hex>) r g b` keeps that exact alpha
156+
* mechanism intact while sourcing the base color from the variable — a
157+
* rebrand's `--devframe-primary` now reaches every baked utility too.
158+
*
159+
* Call once per generated pass, after `generator.generate(...)`, passing the
160+
* resolved `generator.config.theme.colors.primary` ramp.
161+
*
162+
* @param css - The compiled Wind3 CSS (pre-`--un-*` namespacing).
163+
* @param primaryRamp - The generator's resolved `theme.colors.primary` ramp.
164+
*/
165+
export function rewireBakedPrimaryColors(css: string, primaryRamp: Record<string, string>): string {
166+
let out = css
167+
for (const stop of OVERRIDABLE_PRIMARY_STOPS) {
168+
const hex = primaryRamp[stop]
169+
const rgb = hex && hexToRgbTriplet(hex)
170+
if (!rgb)
171+
continue
172+
const varName = stop === 'DEFAULT' ? '--colors-primary-DEFAULT' : `--colors-primary-${stop}`
173+
out = out.replace(
174+
new RegExp(String.raw`rgb\(${rgb}(?!\d)`, 'g'),
175+
`rgb(from var(${varName}, ${hex}) r g b`,
176+
)
177+
}
178+
return out
179+
}

packages/hub-ui/scripts/build-css.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { colors as c } from 'devframe/utils/colors'
66
import MagicString from 'magic-string'
77
import { glob } from 'tinyglobby'
88
import { createGenerator } from 'unocss'
9-
import { namespaceShadowCssVars, shadowSurfaceSafelist } from '../../../design/uno.config'
9+
import { namespaceShadowCssVars, rewireBakedPrimaryColors, shadowSurfaceSafelist } from '../../../design/uno.config'
1010
import config from '../uno.config'
1111

1212
// Compile the components' UnoCSS output ahead of time into a plain string
@@ -69,14 +69,22 @@ export async function buildCSS(): Promise<void> {
6969
// a shortcut+variant interaction. Generate the shadow-surface tokens in a
7070
// dedicated pass so their plain (and `.dark`) rules are always present.
7171
const surfaces = await generator.generate(shadowSurfaceSafelist.join(' '))
72+
// Wind3 bakes the `primary` theme color to literal `rgb()` triplets at
73+
// generate-time — rewire them to read the live `--colors-primary-*`
74+
// variables `primary-ramp.css` derives from `--devframe-primary`, so a
75+
// rebrand actually retints `text-primary`/`bg-primary`/`btn-primary`/…
76+
// (see `rewireBakedPrimaryColors`'s own comment).
77+
const primaryTheme = (generator.config.theme as { colors?: Record<string, Record<string, string>> }).colors?.primary ?? {}
78+
const unoCss = rewireBakedPrimaryColors(unoResult.css, primaryTheme)
79+
const surfacesCss = rewireBakedPrimaryColors(surfaces.css, primaryTheme)
7280
// Namespace Wind's `--un-*` vars (→ `--un-hub-*`) so this shadow-root
7381
// stylesheet is immune to a host page's Wind4 `@property` registrations
7482
// (see `namespaceShadowCssVars`).
7583
const css = namespaceShadowCssVars([
7684
reset,
7785
userStyle.toString(),
78-
unoResult.css,
79-
surfaces.css,
86+
unoCss,
87+
surfacesCss,
8088
primaryRamp,
8189
].join('\n'), '--un-hub-')
8290

packages/hub-ui/src/client/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/hub-ui/src/client/primary-ramp.css

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
* Primary color as a single overridable variable: `--devframe-primary`.
33
*
44
* Appended AFTER the UnoCSS output (see `scripts/build-css.ts`) so this block
5-
* wins over Wind4's own `:root, :host` primary declarations, and imported after
5+
* wins over Wind3's own `:root, :host` primary declarations, and imported after
66
* `virtual:uno.css` in the Storybook preview for the same reason.
77
*
8-
* Each Wind4 stop (`--colors-primary-*`) is derived from `--devframe-primary`
9-
* via `color-mix`. When `--devframe-primary` is unset, the intermediate
8+
* Each stop (`--colors-primary-*`) is derived from `--devframe-primary` via
9+
* `color-mix`. When `--devframe-primary` is unset, the intermediate
1010
* `--devframe-primary-<stop>` vars become guaranteed-invalid (their `color-mix`
1111
* references an unset var with no fallback), so each `--colors-primary-<stop>`
1212
* falls back to the exact devframe default hex - the default look is preserved,
@@ -15,6 +15,16 @@
1515
* everything, and setting it on a group's chrome container retints just that
1616
* group (the group-accent mechanism).
1717
*
18+
* The dock's shadow root is built on Wind3, which bakes `primary`-based
19+
* utilities (`text-primary`, `bg-primary`, `btn-primary`, `ring-primary-500`,
20+
* …) to literal `rgb()` triplets rather than referencing these variables —
21+
* `scripts/build-css.ts` rewires those baked colors into CSS relative-color
22+
* syntax reading `--colors-primary-<stop>` (see `rewireBakedPrimaryColors` in
23+
* `design/uno.config.ts`) so this block actually retints them, not just the
24+
* handful of rules (the glow gradient below) that reference the variables
25+
* directly. The stops declared here (`DEFAULT`/`600`/`500`/`400`/`300`) must
26+
* match `OVERRIDABLE_PRIMARY_STOPS` there.
27+
*
1828
* `:host` is the effective selector inside the dock's shadow root; `:root` is
1929
* for the light-DOM Storybook preview (it matches nothing in the shadow root).
2030
*

packages/json-render-ui/scripts/build-css.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url'
55
import { colors as c } from 'devframe/utils/colors'
66
import { glob } from 'tinyglobby'
77
import { createGenerator } from 'unocss'
8-
import { namespaceShadowCssVars, shadowSurfaceSafelist } from '../../../design/uno.config'
8+
import { namespaceShadowCssVars, rewireBakedPrimaryColors, shadowSurfaceSafelist } from '../../../design/uno.config'
99
import config from '../uno.config'
1010

1111
// Compile the renderer's UnoCSS output ahead of time into a plain string
@@ -62,13 +62,21 @@ export async function buildCSS(): Promise<void> {
6262
// a shortcut+variant interaction. Generate the shadow-surface tokens in a
6363
// dedicated pass so their plain (and `.dark`) rules are always present.
6464
const surfaces = await generator.generate(shadowSurfaceSafelist.join(' '))
65+
// Wind3 bakes the `primary` theme color to literal `rgb()` triplets at
66+
// generate-time — rewire them to read the live `--colors-primary-*`
67+
// variables `primary-ramp.css` derives from `--devframe-primary`, so a
68+
// rebranded hub actually retints the rendered views (see
69+
// `rewireBakedPrimaryColors`'s own comment).
70+
const primaryTheme = (generator.config.theme as { colors?: Record<string, Record<string, string>> }).colors?.primary ?? {}
71+
const unoCss = rewireBakedPrimaryColors(unoResult.css, primaryTheme)
72+
const surfacesCss = rewireBakedPrimaryColors(surfaces.css, primaryTheme)
6573
// Namespace Wind's `--un-*` vars (→ `--un-jr-*`) so this shadow-root
6674
// stylesheet is immune to a host page's Wind4 `@property` registrations
6775
// (see `namespaceShadowCssVars`).
6876
const css = namespaceShadowCssVars([
6977
reset,
70-
unoResult.css,
71-
surfaces.css,
78+
unoCss,
79+
surfacesCss,
7280
primaryRamp,
7381
].join('\n'), '--un-jr-')
7482

packages/json-render-ui/src/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/json-render-ui/src/renderer-module/primary-ramp.css

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@
22
* Primary color as a single overridable variable: `--devframe-primary`.
33
*
44
* Mirrors `@devframes/hub-ui`'s ramp (see its `src/client/primary-ramp.css`
5-
* for the full derivation notes): each Wind4 stop (`--colors-primary-*`) is
5+
* for the full derivation notes): each stop (`--colors-primary-*`) is
66
* derived from `--devframe-primary` via `color-mix`, falling back to the
77
* devframe default sage green when it is unset.
88
*
99
* Appended AFTER the UnoCSS output (see `scripts/build-css.ts`) so this block
10-
* wins over Wind4's own `:root, :host` primary declarations. `:host` is the
10+
* wins over Wind3's own `:root, :host` primary declarations. `:host` is the
1111
* effective selector inside the renderer module's shadow root — its host is
1212
* the viewer-owned mount container, and `--devframe-primary` (set by a
1313
* viewer's branding on any ancestor) inherits across the shadow boundary onto
1414
* it, so a rebranded hub retints the rendered views too.
15+
*
16+
* The renderer module's shadow root is built on Wind3, which bakes
17+
* `primary`-based utilities to literal `rgb()` triplets rather than
18+
* referencing these variables — `scripts/build-css.ts` rewires those baked
19+
* colors into CSS relative-color syntax reading `--colors-primary-<stop>`
20+
* (see `rewireBakedPrimaryColors` in `design/uno.config.ts`) so this block
21+
* actually retints them. The stops declared here (`DEFAULT`/`600`/`500`/
22+
* `400`/`300`) must match `OVERRIDABLE_PRIMARY_STOPS` there.
1523
*/
1624
:root,
1725
:host {

0 commit comments

Comments
 (0)