diff --git a/design/uno.config.ts b/design/uno.config.ts index c85684aa..617f876a 100644 --- a/design/uno.config.ts +++ b/design/uno.config.ts @@ -123,3 +123,57 @@ export const shadowSurfaceSafelist: string[] = [ 'color-active', 'border-base', ] + +/** + * The primary-ramp stops a shadow-root surface's `primary-ramp.css` exposes + * as overridable `--colors-primary-` custom properties (derived from + * `--devframe-primary`). Must match that file's declarations exactly. + */ +const OVERRIDABLE_PRIMARY_STOPS = ['DEFAULT', '600', '500', '400', '300'] as const + +function hexToRgbTriplet(hex: string): string | undefined { + const match = /^#([0-9a-f]{6})$/i.exec(hex) + if (!match) + return undefined + const int = Number.parseInt(match[1], 16) + return `${(int >> 16) & 255} ${(int >> 8) & 255} ${int & 255}` +} + +/** + * Rewire a Wind3-compiled shadow-root stylesheet's baked-in `primary` theme + * colors into CSS relative-color syntax reading the live `--colors-primary-*` + * variables `primary-ramp.css` derives from `--devframe-primary`. + * + * Wind3 (unlike Wind4) resolves each theme color to a literal `rgb(r g b / + * )` at compile time — the `` slot is already dynamic (a slash + * literal, or the utility's own `--un-*-opacity` variable), but the base `r g + * b` triplet is baked in, so every `primary`-based utility (`text-primary`, + * `bg-primary`, `btn-primary`, `ring-primary-500`, …) ignores + * `--devframe-primary` entirely — only hand-written rules that already + * reference `--colors-primary-*` directly (the dock's glow gradient, + * `primary-ramp.css` itself) retint. Swapping the baked triplet for `from + * var(--colors-primary-, ) r g b` keeps that exact alpha + * mechanism intact while sourcing the base color from the variable — a + * rebrand's `--devframe-primary` now reaches every baked utility too. + * + * Call once per generated pass, after `generator.generate(...)`, passing the + * resolved `generator.config.theme.colors.primary` ramp. + * + * @param css - The compiled Wind3 CSS (pre-`--un-*` namespacing). + * @param primaryRamp - The generator's resolved `theme.colors.primary` ramp. + */ +export function rewireBakedPrimaryColors(css: string, primaryRamp: Record): string { + let out = css + for (const stop of OVERRIDABLE_PRIMARY_STOPS) { + const hex = primaryRamp[stop] + const rgb = hex && hexToRgbTriplet(hex) + if (!rgb) + continue + const varName = stop === 'DEFAULT' ? '--colors-primary-DEFAULT' : `--colors-primary-${stop}` + out = out.replace( + new RegExp(String.raw`rgb\(${rgb}(?!\d)`, 'g'), + `rgb(from var(${varName}, ${hex}) r g b`, + ) + } + return out +} diff --git a/examples/hub-hono-minimal/README.md b/examples/hub-hono-minimal/README.md index 65dcaf16..438df3d4 100644 --- a/examples/hub-hono-minimal/README.md +++ b/examples/hub-hono-minimal/README.md @@ -11,7 +11,7 @@ Open — the host page carries the floating dock via one ## How it works -- [`src/app.ts`](./src/app.ts) — runtime-agnostic: `initHub({ devframes, ui: createUi() })` plus `app.all('/__devframes/*', c => hub.handler(c.req.raw))`. Everything — frame SPAs, `__connection.json`, `__index.json`, `embedded.js`, `__client-imports.js` — flows through that one route. The instance is memoized on `globalThis` so a dev-time reload reuses the live hub. It configures no WebSocket transport, so each entry below wires the socket its runtime's way; both end up serving `/__devframes/__ws` on the app's own origin, which is what the hub advertises either way. +- [`src/app.ts`](./src/app.ts) — runtime-agnostic: `initHub({ devframes, ui: createUi({ branding }) })` (rebranded to Hono's own orange, `#e36002`) plus `app.all('/__devframes/*', c => hub.handler(c.req.raw))`. Everything — frame SPAs, `__connection.json`, `__index.json`, `embedded.js`, `__client-imports.js` — flows through that one route. The instance is memoized on `globalThis` so a dev-time reload reuses the live hub. It configures no WebSocket transport, so each entry below wires the socket its runtime's way; both end up serving `/__devframes/__ws` on the app's own origin, which is what the hub advertises either way. - [`src/server.ts`](./src/server.ts) — Node: `@hono/node-server`'s `serve()` returns the `node:http` server, and `hub.attach(server)` routes its upgrade events to the shared RPC socket. - [`src/bun.ts`](./src/bun.ts) — Bun: upgrades arrive as fetch requests, so this entry binds Bun's own transport to the hub context with `createContextRpcServer` + `attachBunWsTransport` and answers the upgrade route inside `Bun.serve({ fetch, websocket })`. diff --git a/examples/hub-hono-minimal/src/app.ts b/examples/hub-hono-minimal/src/app.ts index 8bffe816..e2bc4ce6 100644 --- a/examples/hub-hono-minimal/src/app.ts +++ b/examples/hub-hono-minimal/src/app.ts @@ -38,7 +38,11 @@ export const hub: HubInstance = globalRef.__hubHonoMinimal ??= initHub({ createOgDevframe(), createAssetsDevframe({ watch: false }), ], - ui: createUi(), + // Rebrand the reference UI to Hono's own orange — one field, no CSS: + // `createUi`'s `branding` option publishes `branding.json`, which the dock + // fetches at boot and feeds into `--devframe-primary` (see + // `@devframes/hub-ui`'s `primary-ramp.css`). + ui: createUi({ branding: { primaryColor: '#e36002', productName: 'Devframes on Hono' } }), // Single-user localhost demo: reachable only on loopback, so it opts out // of the gate for a no-friction dev experience. A hub reachable beyond // localhost should gate (see docs/guide/security.md). diff --git a/examples/hub-next-minimal/src/client/hub.ts b/examples/hub-next-minimal/src/client/hub.ts index a9ab1dcb..aeb006b9 100644 --- a/examples/hub-next-minimal/src/client/hub.ts +++ b/examples/hub-next-minimal/src/client/hub.ts @@ -76,7 +76,11 @@ async function loadHub(): Promise { base: DEVFRAMES_HUB_BASE, ws: { sidecar: true }, devframes, - ui: (hubUi.createUi as typeof CreateUi)(), + // Rebrand the reference UI to Next.js/Vercel's monochrome black — one + // field, no CSS: `createUi`'s `branding` option publishes + // `branding.json`, which the dock fetches at boot and feeds into + // `--devframe-primary` (see `@devframes/hub-ui`'s `primary-ramp.css`). + ui: (hubUi.createUi as typeof CreateUi)({ branding: { primaryColor: '#000000', productName: 'Devframes on Next.js' } }), // Serve the reference json-render frontend as a prebuilt renderer module // — the one-liner that makes `'json-render'` docks render in the prebuilt // viewer. Swap it for any community implementation of the same contract. diff --git a/examples/hub-nitro-minimal/README.md b/examples/hub-nitro-minimal/README.md index 851f94ed..64584c96 100644 --- a/examples/hub-nitro-minimal/README.md +++ b/examples/hub-nitro-minimal/README.md @@ -10,7 +10,7 @@ Open - the host page carries the floating dock via one s ## How it works -- [`hub.ts`](./hub.ts) - `initHub({ devframes, ui: createUi(), key })`: mounts the Inspect and Messages plugins against one shared hub context, fills the hub's `ui` slot with `@devframes/hub-ui`'s prebuilt viewer + floating-dock bootstrap, and memoizes the instance across Nitro's dev-time module reloads. +- [`hub.ts`](./hub.ts) - `initHub({ devframes, ui: createUi({ branding }) })`: mounts the Inspect and Messages plugins against one shared hub context, fills the hub's `ui` slot with `@devframes/hub-ui`'s prebuilt viewer + floating-dock bootstrap (rebranded to Nitro's own pink/red, `#ff2056`), and memoizes the instance across Nitro's dev-time module reloads. - [`routes/__devframes/[...path].ts`](./routes/__devframes/%5B...path%5D.ts) (and its `index.ts` sibling for the namespace root) - the delegation: every request under `/__devframes/` becomes `hub.handler(event.req)`, web-standard Request in, Response out. Everything - frame SPAs, `__connection.json`, `__index.json`, `embedded.js`, `__client-imports.js` - flows through it. - [`nitro.config.ts`](./nitro.config.ts) - keeps the devframe packages external so their prebuilt client assets resolve from the packages themselves rather than Nitro's build output. - The RPC WebSocket runs on a side-car port - Nitro handlers hand over `Request`s, so `ws: { sidecar: true }` asks for one - advertised through `__connection.json`; the browser client discovers it automatically. diff --git a/examples/hub-nitro-minimal/hub.ts b/examples/hub-nitro-minimal/hub.ts index 3491a478..e98d1654 100644 --- a/examples/hub-nitro-minimal/hub.ts +++ b/examples/hub-nitro-minimal/hub.ts @@ -41,7 +41,11 @@ export const hub: HubInstance = globalRef.__hubNitroMinimal ??= initHub({ createOgDevframe(), createAssetsDevframe({ watch: false }), ], - ui: createUi(), + // Rebrand the reference UI to Nitro's own pink/red — one field, no CSS: + // `createUi`'s `branding` option publishes `branding.json`, which the dock + // fetches at boot and feeds into `--devframe-primary` (see + // `@devframes/hub-ui`'s `primary-ramp.css`). + ui: createUi({ branding: { primaryColor: '#ff2056', productName: 'Devframes on Nitro' } }), // Single-user localhost demo: reachable only on loopback, so it opts out // of the gate for a no-friction dev experience. A hub reachable beyond // localhost should gate (see docs/guide/security.md). diff --git a/examples/hub-rsbuild-minimal/README.md b/examples/hub-rsbuild-minimal/README.md index e79109cf..d9913fc9 100644 --- a/examples/hub-rsbuild-minimal/README.md +++ b/examples/hub-rsbuild-minimal/README.md @@ -12,7 +12,7 @@ Open the printed URL - the host page carries the floating dock via one injected [`rsbuild.config.ts`](./rsbuild.config.ts) is the entire host: -- `initHub({ devframes: [inspect, messages], ui: createUi() })` runs in Rsbuild's Node config process (never bundled into the browser), so `createUi()`'s prebuilt viewer/dock and the plugins' node code work unchanged. +- `initHub({ devframes: [inspect, messages], ui: createUi({ branding }) })` runs in Rsbuild's Node config process (never bundled into the browser), so `createUi()`'s prebuilt viewer/dock and the plugins' node code work unchanged. `branding.primaryColor` is Rsbuild's own orange (`#ff5e00`) — a rebrand reaches every `primary`-based color in the dock, no CSS required. - `dev.setupMiddlewares` unshifts `hub.nodeMiddleware`, which owns the whole `/__devframes/` namespace and hands everything else back to Rsbuild. - The RPC WebSocket runs on a side-car port (`ws: { sidecar: true }`, since Rsbuild's middleware stack never hands over upgrades), advertised through `__connection.json`; the browser client discovers it automatically. - `html.tags` injects `