Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions design/uno.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-<stop>` 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 /
* <alpha>)` at compile time — the `<alpha>` 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-<stop>, <hex>) 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, string>): 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
}
2 changes: 1 addition & 1 deletion examples/hub-hono-minimal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Open <http://localhost:5179> — 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 })`.

Expand Down
6 changes: 5 additions & 1 deletion examples/hub-hono-minimal/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
6 changes: 5 additions & 1 deletion examples/hub-next-minimal/src/client/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ async function loadHub(): Promise<HubInstance> {
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.
Expand Down
2 changes: 1 addition & 1 deletion examples/hub-nitro-minimal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Open <http://localhost:3000> - 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.
Expand Down
6 changes: 5 additions & 1 deletion examples/hub-nitro-minimal/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion examples/hub-rsbuild-minimal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<script type="module" src="/__devframes/embedded.js">`, so the floating dock mounts itself.
Expand Down
6 changes: 5 additions & 1 deletion examples/hub-rsbuild-minimal/rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ export default defineConfig({
hub ??= initHub({
base,
devframes: builtinDevframes,
ui: createUi(),
// Rebrand the reference UI to Rsbuild'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: '#ff5e00', productName: 'Devframes on Rsbuild' } }),
// 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
Expand Down
2 changes: 1 addition & 1 deletion examples/hub-vite-minimal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Open the printed URL - the host page carries the floating dock via one injected

[`vite.config.ts`](./vite.config.ts) is the entire host:

- `initHub({ devframes: [inspect, messages], ui: createUi() })` runs in Vite'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 Vite'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 Vite's own purple (`#646cff`) — a rebrand reaches every `primary`-based color in the dock, no CSS required.
- `server.middlewares.use(hub.nodeMiddleware)` mounts the whole `/__devframes/` namespace; the middleware self-filters by base and hands everything else back to Vite.
- The RPC WebSocket shares Vite's own dev server at `/__devframes/__ws` - zero extra ports.
- `transformIndexHtml` injects `<script type="module" src="/__devframes/embedded.js">` into the host page, so the floating dock mounts itself.
Expand Down
6 changes: 5 additions & 1 deletion examples/hub-vite-minimal/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ export default defineConfig({
const hub = initHub({
base,
devframes: builtinDevframes,
ui: createUi(),
// Rebrand the reference UI to Vite's own purple — 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: '#646cff', productName: 'Devframes on Vite' } }),
// 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
Expand Down
14 changes: 11 additions & 3 deletions packages/hub-ui/scripts/build-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { colors as c } from 'devframe/utils/colors'
import MagicString from 'magic-string'
import { glob } from 'tinyglobby'
import { createGenerator } from 'unocss'
import { namespaceShadowCssVars, shadowSurfaceSafelist } from '../../../design/uno.config'
import { namespaceShadowCssVars, rewireBakedPrimaryColors, shadowSurfaceSafelist } from '../../../design/uno.config'
import config from '../uno.config'

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

Expand Down
2 changes: 1 addition & 1 deletion packages/hub-ui/src/client/.generated/css.ts

Large diffs are not rendered by default.

16 changes: 13 additions & 3 deletions packages/hub-ui/src/client/primary-ramp.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
* Primary color as a single overridable variable: `--devframe-primary`.
*
* Appended AFTER the UnoCSS output (see `scripts/build-css.ts`) so this block
* wins over Wind4's own `:root, :host` primary declarations, and imported after
* wins over Wind3's own `:root, :host` primary declarations, and imported after
* `virtual:uno.css` in the Storybook preview for the same reason.
*
* Each Wind4 stop (`--colors-primary-*`) is derived from `--devframe-primary`
* via `color-mix`. When `--devframe-primary` is unset, the intermediate
* Each stop (`--colors-primary-*`) is derived from `--devframe-primary` via
* `color-mix`. When `--devframe-primary` is unset, the intermediate
* `--devframe-primary-<stop>` vars become guaranteed-invalid (their `color-mix`
* references an unset var with no fallback), so each `--colors-primary-<stop>`
* falls back to the exact devframe default hex - the default look is preserved,
Expand All @@ -15,6 +15,16 @@
* everything, and setting it on a group's chrome container retints just that
* group (the group-accent mechanism).
*
* The dock's shadow root is built on Wind3, which bakes `primary`-based
* utilities (`text-primary`, `bg-primary`, `btn-primary`, `ring-primary-500`,
* …) to literal `rgb()` triplets rather than referencing these variables —
* `scripts/build-css.ts` rewires those baked colors into CSS relative-color
* syntax reading `--colors-primary-<stop>` (see `rewireBakedPrimaryColors` in
* `design/uno.config.ts`) so this block actually retints them, not just the
* handful of rules (the glow gradient below) that reference the variables
* directly. The stops declared here (`DEFAULT`/`600`/`500`/`400`/`300`) must
* match `OVERRIDABLE_PRIMARY_STOPS` there.
*
* `:host` is the effective selector inside the dock's shadow root; `:root` is
* for the light-DOM Storybook preview (it matches nothing in the shadow root).
*
Expand Down
14 changes: 11 additions & 3 deletions packages/json-render-ui/scripts/build-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url'
import { colors as c } from 'devframe/utils/colors'
import { glob } from 'tinyglobby'
import { createGenerator } from 'unocss'
import { namespaceShadowCssVars, shadowSurfaceSafelist } from '../../../design/uno.config'
import { namespaceShadowCssVars, rewireBakedPrimaryColors, shadowSurfaceSafelist } from '../../../design/uno.config'
import config from '../uno.config'

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

Expand Down
2 changes: 1 addition & 1 deletion packages/json-render-ui/src/.generated/css.ts

Large diffs are not rendered by default.

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