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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Ahead-of-time build artifacts that live under `src/` - the shadow-root styleshee
## Conventions

- RPC functions must use `defineRpcFunction`; always namespace IDs `devframes:plugin:<slug>:<fn-name>` for built-in devframes (the literal `plugin:` token mirrors the `@devframes/plugin-<slug>` package name on the wire - it is npm namespacing, not a concept).
- **No magic event names — use the centralized event maps.** Every event, broadcast, shared-state key, and channel name lives in one of two source-of-truth maps: `DEVFRAME_EVENTS` (`packages/devframe/src/events.ts`, re-exported from `devframe/constants`) for the core runtime, and `HUB_EVENTS` (`packages/hub/src/events.ts`, re-exported from `@devframes/hub/constants`) for the hub. Reference `DEVFRAME_EVENTS.*` / `HUB_EVENTS.*` at call sites (`.events.emit`/`.on`, `rpc.broadcast({ method })`, `sharedState.get(key)`, `defineHubRpcFunction({ name })`, `rpc.call`) instead of re-typing a string literal. The two maps and the [`docs/content/8.references/3.events.md`](docs/content/8.references/3.events.md) Events Reference are kept in lockstep: adding, renaming, or removing a name means editing the map **and** that page in the same change — every name in the maps appears in the tables, and vice versa. The only literals left are unavoidable type-position keys (the `EventEmitter<…>` maps in `types/*` and the `DevframeRpcClientFunctions`/`DevframeRpcServerFunctions` augmentations), which mirror the maps; a package that deliberately avoids a hub dependency (e.g. `@devframes/plugin-terminals`, which models the hub bridge structurally) keeps a local literal rather than importing `HUB_EVENTS`.
- **No magic event names: use the centralized event maps.** Every event, broadcast, shared-state key, and channel name lives in one of two source-of-truth maps: `DEVFRAME_EVENTS` (`packages/devframe/src/events.ts`, re-exported from `devframe/constants`) for the core runtime, and `HUB_EVENTS` (`packages/hub/src/events.ts`, re-exported from `@devframes/hub/constants`) for the hub. Reference `DEVFRAME_EVENTS.*` / `HUB_EVENTS.*` at call sites (`.events.emit`/`.on`, `rpc.broadcast({ method })`, `sharedState.get(key)`, `defineHubRpcFunction({ name })`, `rpc.call`) instead of re-typing a string literal. The two maps and the [`docs/content/8.references/3.events.md`](docs/content/8.references/3.events.md) Events Reference are kept in lockstep: adding, renaming, or removing a name means editing the map **and** that page in the same change; every name in the maps appears in the tables, and vice versa. The only literals left are unavoidable type-position keys (the `EventEmitter<…>` maps in `types/*` and the `DevframeRpcClientFunctions`/`DevframeRpcServerFunctions` augmentations), which mirror the maps; a package that deliberately avoids a hub dependency (e.g. `@devframes/plugin-terminals`, which models the hub bridge structurally) keeps a local literal rather than importing `HUB_EVENTS`.
- **Stay validator-neutral.** `devframe` and every `@devframes/*` package must not introduce a preferred schema validator dependency - no `valibot`, `zod`, `arktype`, etc. in their runtime `dependencies`. `args`/`returns`/flag schemas are typed against [Standard Schema](https://standardschema.dev/) (`@standard-schema/spec`, types-only); first-party code that needs to author a schema uses the built-in zero-dep `devframe/utils/simple-schema` builder (deliberately minimal - not a general validator). JSON-schema conversion uses each schema's own Standard JSON Schema converter (`~standard.jsonSchema`, implemented by e.g. zod 4) when present and degrades to a permissive object otherwise - no converter library and no vendor dependency is required. Docs, by contrast, should point *users* at a real validator for their own integrations - recommend **valibot** (lightest) or **zod** (worth reusing if they already pull it via the JSON-render or MCP integrations).
- Shared state via `devframe/utils/shared-state`; keep values serializable.
- Utility imports use the package-path form `devframe/utils/*`, never relative `../utils/*`.
Expand Down
10 changes: 6 additions & 4 deletions bump.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { syncStarterVersion } from './scripts/sync-starter-version.ts'

export default defineConfig({
all: true,
// `starter/` pins real `devframe`/`@devframes/*` versions (it's a
// copy-paste-ready template, not a workspace member consuming
// `catalog:`/`workspace:*`), so `bumpp -r` can't reach it on its own -
// sync it here, before the version-bump commit is made.
/**
* `starter/` pins real `devframe`/`@devframes/*` versions (it's a
* copy-paste-ready template, not a workspace member consuming
* `catalog:`/`workspace:*`), so `bumpp -r` can't reach it on its own -
* sync it here, before the version-bump commit is made.
*/
execute: async (operation) => {
await syncStarterVersion(operation.state.newVersion)
await x('pnpm', ['install', '--frozen-lockfile=false'], { nodeOptions: { stdio: 'inherit', cwd: process.cwd() } })
Expand Down
28 changes: 15 additions & 13 deletions design/build-shadow-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface BuildShadowCssOptions {
userStylePath?: string | readonly string[]
/**
* Prefix Wind's `--un-*` custom properties are renamed to (see
* `namespaceShadowCssVars`) unique per shadow-root surface so two
* `namespaceShadowCssVars`), unique per shadow-root surface so two
* shadow trees on the same host page never collide.
*/
varPrefix: string
Expand All @@ -51,15 +51,17 @@ export interface BuildShadowCssResult {
css: string
}

// Compile a shadow-root surface's UnoCSS output ahead of time into a plain
// string module (`<srcDir>/.generated/css.ts`) that the surface adopts into
// its shadow root — fully styled inside any host page without a global
// stylesheet, and immune to the host page's own styles leaking in. Shared by
// `@devframes/hub-ui`'s dock and `@devframes/json-render-ui`'s renderer
// module: same pipeline, same two shadow-root gotchas (see the root
// AGENTS.md "Design system" section), different source globs. Writes the
// generated file itself; returns stats so each caller (a `scripts/` entry,
// exempt from the `no-console` lint rule) prints its own summary line.
/**
* Compile a shadow-root surface's UnoCSS output ahead of time into a plain
* string module (`<srcDir>/.generated/css.ts`) that the surface adopts into
* its shadow root, fully styled inside any host page without a global
* stylesheet, and immune to the host page's own styles leaking in. Shared by
* `@devframes/hub-ui`'s dock and `@devframes/json-render-ui`'s renderer
* module: same pipeline, same two shadow-root gotchas (see the root
* AGENTS.md "Design system" section), different source globs. Writes the
* generated file itself; returns stats so each caller (a `scripts/` entry,
* exempt from the `no-console` lint rule) prints its own summary line.
*/
export async function buildShadowCss(options: BuildShadowCssOptions): Promise<BuildShadowCssResult> {
const { srcDir, globs, config, primaryRampPath, userStylePath, varPrefix } = options
const generatedCss = join(srcDir, '.generated/css.ts')
Expand All @@ -75,7 +77,7 @@ export async function buildShadowCss(options: BuildShadowCssOptions): Promise<Bu
// Shadow-root surfaces reuse `@antfu/design`'s Vue components (buttons,
// badges, …) directly. UnoCSS ignores `node_modules` by default, so their
// semantic shortcut classes (`btn-primary`, `btn-action`, `badge-*`, …)
// would be absent from the shadow-root stylesheet scan the design
// would be absent from the shadow-root stylesheet, so scan the design
// package's component sources too so those classes ship in the injected
// CSS.
const designComponentsDir = join(require.resolve('@antfu/design/package.json'), '..', 'components')
Expand Down Expand Up @@ -109,12 +111,12 @@ export async function buildShadowCss(options: BuildShadowCssOptions): Promise<Bu
const unoResult = await generator.generate(tokens)
// Wind3 drops a *plain* semantic shortcut (`.bg-base` / `.color-base`) from
// the main pass when the same shortcut also appears variant-prefixed in the
// sources (e.g. `@antfu/design`'s Tabs emits `data-[state=active]:bg-base`)
// sources (e.g. `@antfu/design`'s Tabs emits `data-[state=active]:bg-base`),
// 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-*`
// generate-time, so 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).
Expand Down
30 changes: 19 additions & 11 deletions design/design.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ export function navBrand(extra?: string): string {
return cx('flex items-center gap-1.5 shrink-0 font-semibold text-sm select-none', extra)
}

// Mirrors devframe's `DevframeConnectionStatus` (kept local so this class-helper
// module stays free of package imports); the two share the same string members.
/**
* Mirrors devframe's `DevframeConnectionStatus` (kept local so this class-helper
* module stays free of package imports); the two share the same string members.
*/
export type ConnectionStatus = 'connecting' | 'connected' | 'unauthorized' | 'disconnected' | 'error'

export interface ConnectionIndicator {
Expand All @@ -109,9 +111,11 @@ const CONNECTION_TONE: Record<Exclude<ConnectionStatus, 'connected'>, { label: s
error: { label: 'error', dot: 'bg-error' },
}

// The shared top-nav connection indicator: a small status dot + label. Returns
// `null` when the client is `connected`, so every surface renders the indicator
// only while the connection is not live.
/**
* The shared top-nav connection indicator: a small status dot + label. Returns
* `null` when the client is `connected`, so every surface renders the indicator
* only while the connection is not live.
*/
export function connectionIndicator(status: ConnectionStatus, extra?: string): ConnectionIndicator | null {
if (status === 'connected')
return null
Expand Down Expand Up @@ -167,18 +171,22 @@ const CONNECTION_STATE: Record<Exclude<ConnectionStatus, 'connected'>, Connectio
},
}

// The shared full-panel connection state copy: shown whenever the client isn't
// `connected`, so a surface never sits on an infinite spinner without saying
// why. Returns `null` when connected. Pair with the `connection*` class builders
// below so every surface renders the identical centered glyph + title + body.
/**
* The shared full-panel connection state copy: shown whenever the client isn't
* `connected`, so a surface never sits on an infinite spinner without saying
* why. Returns `null` when connected. Pair with the `connection*` class builders
* below so every surface renders the identical centered glyph + title + body.
*/
export function connectionState(status: ConnectionStatus): ConnectionStateCopy | null {
if (status === 'connected')
return null
return CONNECTION_STATE[status]
}

// Centered fill for the full-panel state; each surface adds its own fill
// strategy (`h-full`, `h-svh w-full`, `absolute inset-0`, …) via `extra`.
/**
* Centered fill for the full-panel state; each surface adds its own fill
* strategy (`h-full`, `h-svh w-full`, `absolute inset-0`, …) via `extra`.
*/
export function connectionPanel(extra?: string): string {
return cx('flex flex-col items-center justify-center gap-4 bg-base p-8 text-center', extra)
}
Expand Down
4 changes: 2 additions & 2 deletions design/dock-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// `ph:git-branch-duotone`) to its live, sanitized SVG markup, fetched from the
// public `api.iconify.design` CDN. Unlike a UnoCSS `preset-icons` class, this
// needs no `@iconify-json/*` collection installed and no hand-maintained
// id -> class table any Iconify id just works, at the cost of a network
// id -> class table, since any Iconify id just works, at the cost of a network
// round-trip on first render. We reuse @antfu/design's own fetcher, cache and
// sanitizer (`utils/iconify.ts`) rather than reimplementing them; only the id
// parsing and light/dark selection below are devframe-specific, mirroring the
Expand All @@ -19,7 +19,7 @@ const ICONIFY_ID = /^(?:i-)?([\w-]+):([\w-]+)$/

/**
* Resolve a dock icon (a `collection:icon` string, or a `{ light, dark }`
* pair — the `light` variant is fetched) to its sanitized SVG markup.
* pair whose `light` variant is fetched) to its sanitized SVG markup.
*
* Returns `undefined` when the id doesn't parse or the fetch fails, so the
* caller can fall back to a text initial.
Expand Down
58 changes: 32 additions & 26 deletions design/uno.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,27 @@ export interface CreateDesignConfigOptions {
* json-render renderer module) pass `presetWind3()` instead: Wind4 registers
* its theme + `--un-*` custom properties via `@property { inherits: false }`
* and keeps them in a document `:root {}` block, neither of which reaches a
* shadow tree so its `color-mix(var(--colors-*))` utilities resolve to
* shadow tree, so its `color-mix(var(--colors-*))` utilities resolve to
* nothing there. Wind3 bakes the same `@antfu/design` semantic utilities to
* concrete `rgb()` + `.dark` variants, which are self-contained inside a
* shadow root.
*/
base?: Preset<any> | Preset<any>[]
}

// Shared devframe UnoCSS base. Every plugin and example composes `@antfu/design`
// the same way — its preset (tuned to devframe's sage green) over a Wind base,
// Phosphor icons, DM Sans/Mono web fonts, and the directive/variant-group
// transformers — so the surfaces look and feel like one product across
// frameworks. Each app extends this via `mergeConfigs([designConfig, { … }])`
// and contributes only its own extraction globs (and any safelist).
//
// The shared web fonts (`sans`/`mono`), the named `z-*` layers and the `h-nav`
// navbar height live here so every surface shares one font stack, one z-index
// scale and one fixed navbar height. The `@antfu/design` preset blocks plain
// `z-<number>`, so the layers are named on purpose.
/**
* Shared devframe UnoCSS base. Every plugin and example composes `@antfu/design`
* the same way: its preset (tuned to devframe's sage green) over a Wind base,
* Phosphor icons, DM Sans/Mono web fonts, and the directive/variant-group
* transformers, so the surfaces look and feel like one product across
* frameworks. Each app extends this via `mergeConfigs([designConfig, { … }])`
* and contributes only its own extraction globs (and any safelist).
*
* The shared web fonts (`sans`/`mono`), the named `z-*` layers and the `h-nav`
* navbar height live here so every surface shares one font stack, one z-index
* scale and one fixed navbar height. The `@antfu/design` preset blocks plain
* `z-<number>`, so the layers are named on purpose.
*/
export function createDesignConfig(options: CreateDesignConfigOptions = {}) {
const base = options.base ?? presetWind4()
return defineConfig({
Expand All @@ -45,19 +47,23 @@ export function createDesignConfig(options: CreateDesignConfigOptions = {}) {
presetIcons({ scale: 1.1 }),
],
transformers: [transformerDirectives(), transformerVariantGroup()],
// The shared class-helper builders (`design/design.ts`) assemble their class
// chains at runtime, so every app scans that one file (it carries
// `@unocss-include`) for extraction regardless of its own framework globs.
/**
* The shared class-helper builders (`design/design.ts`) assemble their class
* chains at runtime, so every app scans that one file (it carries
* `@unocss-include`) for extraction regardless of its own framework globs.
*/
content: {
filesystem: [fileURLToPath(new URL('./design.ts', import.meta.url))],
},
// Wind leaves bare `border`/`border-b` at currentColor; restore the subtle
// shared border color (matching `border-base`) for unqualified borders.
/**
* Wind leaves bare `border`/`border-b` at currentColor; restore the subtle
* shared border color (matching `border-base`) for unqualified borders.
*/
preflights: [{ getCSS: () => '*,::before,::after{border-color:#8882}' }],
shortcuts: {
// Fixed navbar height, shared by every surface's top nav.
/** Fixed navbar height, shared by every surface's top nav. */
'h-nav': 'h-10',
// Named z-index layers, shared across every surface.
/** Named z-index layers, shared across every surface. */
'z-nav': 'z-[30]',
'z-dropdown': 'z-[40]',
'z-tooltip': 'z-[45]',
Expand All @@ -70,7 +76,7 @@ export function createDesignConfig(options: CreateDesignConfigOptions = {}) {
})
}

// The default shared base (Wind4), consumed by every plugin and example.
/** The default shared base (Wind4), consumed by every plugin and example. */
export const designConfig = createDesignConfig()

/**
Expand All @@ -91,7 +97,7 @@ export const designConfig = createDesignConfig()
* declared, so a host page built with Wind4 registers `--un-bg-opacity` /
* `--un-border-opacity` / `--un-text-opacity` (et al.) as
* `@property { syntax: '<percentage>'; inherits: false }` for the whole
* document including inside our shadow tree. Our shadow CSS is Wind3, which
* document, including inside our shadow tree. Our shadow CSS is Wind3, which
* sets those same vars **unitless** (`--un-border-opacity: 0.13`), so the
* global `<percentage>` registration makes every such declaration invalid and
* the dependent `color-mix()` / `rgb(… / var(--un-*))` value collapses (a
Expand All @@ -100,9 +106,9 @@ export const designConfig = createDesignConfig()
* The shadow stylesheet sets and reads these vars entirely within itself, so
* renaming every `--un-` to a per-surface prefix (`--un-jr-`, `--un-hub-`)
* keeps it self-consistent while making it immune to whatever the host page
* registered the renamed names are distinct properties the host's
* registered, since the renamed names are distinct properties the host's
* `@property --un-*` rules never match. Apply only to shadow-injected CSS
* (`hub-ui` dock, `json-render-ui` renderer module) the Vite-served SPAs own
* (`hub-ui` dock, `json-render-ui` renderer module); the Vite-served SPAs own
* their whole document and need no rename.
*
* @param css - The compiled shadow-root stylesheet.
Expand Down Expand Up @@ -145,15 +151,15 @@ function hexToRgbTriplet(hex: string): string | undefined {
* 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
* <alpha>)` at compile time, and 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
* `--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
* mechanism intact while sourcing the base color from the variable, so a
* rebrand's `--devframe-primary` now reaches every baked utility too.
*
* Call once per generated pass, after `generator.generate(...)`, passing the
Expand Down
Loading
Loading