diff --git a/.agents/03-stack-and-commands.md b/.agents/03-stack-and-commands.md index ecec5ba0..6b48e0f8 100644 --- a/.agents/03-stack-and-commands.md +++ b/.agents/03-stack-and-commands.md @@ -43,7 +43,7 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co ## Generated artifacts under `src/` -Ahead-of-time build artifacts that live under `src/` - the shadow-root stylesheets in `packages/hub-ui/src/client/.generated/` and `packages/json-render-ui/src/.generated/` - are **generated, not committed** (`.generated` is gitignored). Each owning package builds its own with `pnpm run build:css`; three things guarantee the file is on disk before anything imports it: the root `postinstall` runs `turbo run build:css`, the Turbo `typecheck` task depends on both `build:css` tasks, and each package's `build` script chains `build:css` first. A new generated-under-`src` artifact MUST follow the same shape - its own build script, declared `outputs` in `turbo.json`, and a `typecheck` dependency - and MUST NOT be checked in: a minified single-line blob conflicts on every concurrent edit. +Ahead-of-time build artifacts that live under `src/` - the shadow-root stylesheets in `packages/hub-ui/src/client/.generated/`, `packages/hub-ui-onboard/src/client/.generated/` and `packages/json-render-ui/src/.generated/` - are **generated, not committed** (`.generated` is gitignored). Each owning package builds its own with `pnpm run build:css`; three things guarantee the file is on disk before anything imports it: the root `postinstall` runs `turbo run build:css`, the Turbo `typecheck` task depends on every `build:css` task, and each package's `build` script chains `build:css` first. A new generated-under-`src` artifact MUST follow the same shape - its own build script, declared `outputs` in `turbo.json`, and a `typecheck` dependency - and MUST NOT be checked in: a minified single-line blob conflicts on every concurrent edit. ## `starter/` diff --git a/.agents/06-design-system.md b/.agents/06-design-system.md index 2870a002..2ddc30ab 100644 --- a/.agents/06-design-system.md +++ b/.agents/06-design-system.md @@ -12,9 +12,9 @@ Each consumer's `uno.config.ts` composes the same stack: `presetAnthonyDesign({ ## Wind4 by default, Wind3 for shadow roots -Ordinary surfaces (plugins served in iframes, examples in the page) use `presetWind4()`. A surface whose stylesheet is injected into a **shadow root** (`@devframes/hub-ui`'s dock custom element, `@devframes/json-render-ui`'s renderer module) MUST build on **`presetWind3()`** instead - via `createDesignConfig({ base: presetWind3() })`, or `presetWind3()` directly. Wind4 keeps `@antfu/design`'s theme in a document `:root {}` block and registers its `--un-*` custom properties with `@property { inherits: false }`, neither of which reaches a shadow tree - its `color-mix(var(--colors-*))` semantic utilities (`bg-base`, `color-base`, …) resolve to nothing inside a shadow root. Wind3 bakes the same shortcuts to concrete `rgb()` + `.dark` variants, self-contained in the shadow tree. +Ordinary surfaces (plugins served in iframes, examples in the page) use `presetWind4()`. A surface whose stylesheet is injected into a **shadow root** (`@devframes/hub-ui`'s dock custom element, `@devframes/json-render-ui`'s renderer module, `@devframes/hub-ui-onboard`'s floating button) MUST build on **`presetWind3()`** instead - via `createDesignConfig({ base: presetWind3() })`, or `presetWind3()` directly. Wind4 keeps `@antfu/design`'s theme in a document `:root {}` block and registers its `--un-*` custom properties with `@property { inherits: false }`, neither of which reaches a shadow tree - its `color-mix(var(--colors-*))` semantic utilities (`bg-base`, `color-base`, …) resolve to nothing inside a shadow root. Wind3 bakes the same shortcuts to concrete `rgb()` + `.dark` variants, self-contained in the shadow tree. -Two shadow-root gotchas the ahead-of-time CSS builder MUST compensate for (both handled in the shared `design/build-shadow-css.ts` pipeline, consumed by `packages/{hub-ui,json-render-ui}/scripts/build-css.ts`; the Vite `unocss/vite` path for standalone SPAs and Storybook is not affected): +Two shadow-root gotchas the ahead-of-time CSS builder MUST compensate for (both handled in the shared `design/build-shadow-css.ts` pipeline, consumed by `packages/{hub-ui,hub-ui-onboard,json-render-ui}/scripts/build-css.ts`; a surface that renders none of `@antfu/design`'s Vue components passes `scanDesignComponents: false` to keep its stylesheet small; the Vite `unocss/vite` path for standalone SPAs and Storybook is not affected): - **Plain-vs-variant shortcut drop.** When a semantic shortcut also appears **variant-prefixed** in the scanned sources (e.g. `@antfu/design`'s Tabs emits `data-[state=active]:bg-base`), a single-pass `generate(tokens)` drops the *plain* `.bg-base` / `.color-base` rule - so emit the surface tokens (`design/uno.config.ts`'s exported `shadowSurfaceSafelist`) in a **dedicated `generate()` pass** and append them. - **`--un-*` collision with a Wind4 host.** `@property` registrations are document-global, so a host page built on Wind4 registers `--un-bg-opacity` / `--un-border-opacity` / `--un-text-opacity` as `@property { syntax: '' }` for the whole document, including our shadow tree - which invalidates the *unitless* values Wind3 writes (`--un-border-opacity: 0.13`) and collapses the dependent `rgb(… / var(--un-*))` color (a visibly wrong border/background). Rename every `--un-` in the shadow stylesheet to a private prefix with `design/uno.config.ts`'s exported `namespaceShadowCssVars()` so it's immune to whatever the host registered. diff --git a/.agents/08-diagnostics.md b/.agents/08-diagnostics.md index 8dcbfb1d..5986e205 100644 --- a/.agents/08-diagnostics.md +++ b/.agents/08-diagnostics.md @@ -2,7 +2,7 @@ All node-side warnings and errors use structured diagnostics via [`nostics`](https://www.npmjs.com/package/nostics). Node-side code MUST NOT use raw `console.warn`, `console.error`, or `throw new Error` with ad-hoc messages - always define a coded diagnostic. Browser-only code is out of scope and keeps using `console.*` / `throw`. -Import `defineDiagnostics` (and `Diagnostic` for `instanceof` checks) from `devframe/utils/nostics`, never from `nostics` directly - it pre-wires devframe's ANSI console reporter, so a plugin's `diagnostics.ts` never builds its own reporter (`colors`, `ansiFormatter`) or depends on `nostics` itself. +Import `defineDiagnostics` (and `Diagnostic` for `instanceof` checks) from `devframe/utils/nostics`, never from `nostics` directly - it pre-wires devframe's ANSI console reporter, so a plugin's `diagnostics.ts` never builds its own reporter (`colors`, `ansiFormatter`) or depends on `nostics` itself. One exception: `@devframes/hub-ui-onboard` MUST stay free of `devframe` (a host ships it while devframe is not installed), so it imports `defineDiagnostics` and `createConsoleReporter` from `nostics` directly. ## Code ranges @@ -16,6 +16,7 @@ Prefix: **`DF`**. Codes are sequential 4-digit numbers (e.g. `DF0033`) - check t - `DF83xx` - messages - `DF84xx` - commands - `DF85xx` - built-in RPC commands +- `DF90xx` - `@devframes/hub-ui-onboard` (install, state file, hand-off) ## Adding a new error diff --git a/alias.ts b/alias.ts index 6940f8e2..b128d0ad 100644 --- a/alias.ts +++ b/alias.ts @@ -61,6 +61,7 @@ export const alias = { '@devframes/hub/types': r('hub/src/types/index.ts'), '@devframes/hub': r('hub/src/index.ts'), '@devframes/hub-ui': r('hub-ui/src/index.ts'), + '@devframes/hub-ui-onboard': r('hub-ui-onboard/src/index.ts'), '@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'), '@devframes/nuxt/single': r('nuxt/src/single.ts'), '@devframes/nuxt/hub/client': r('nuxt/src/hub-client.ts'), diff --git a/design/build-shadow-css.ts b/design/build-shadow-css.ts index c68f8608..686056ef 100644 --- a/design/build-shadow-css.ts +++ b/design/build-shadow-css.ts @@ -42,6 +42,12 @@ export interface BuildShadowCssOptions { * shadow trees on the same host page never collide. */ varPrefix: string + /** + * Also scan `@antfu/design`'s Vue components so the classes they use ship + * in the stylesheet. Default `true`; a surface that renders none of those + * components turns it off to keep the stylesheet small. + */ + scanDesignComponents?: boolean } export interface BuildShadowCssResult { @@ -63,7 +69,7 @@ export interface BuildShadowCssResult { * exempt from the `no-console` lint rule) prints its own summary line. */ export async function buildShadowCss(options: BuildShadowCssOptions): Promise { - const { srcDir, globs, config, primaryRampPath, userStylePath, varPrefix } = options + const { srcDir, globs, config, primaryRampPath, userStylePath, varPrefix, scanDesignComponents = true } = options const generatedCss = join(srcDir, '.generated/css.ts') const require = createRequire(import.meta.url) @@ -81,11 +87,13 @@ export async function buildShadowCss(options: BuildShadowCssOptions): Promiseembedded.js` URL, an Install action that runs the project's package manager, and an `onInstalled` hook that hands `base` to the real hub in the same process. See [Opt-in DevTools with Onboarding](/guide/hub-ui-onboard). + ## Renderer modules A dock type's renderer (e.g. [JSON-Render](/guide/json-render)) composes via `initHub({ renderers })`. Each registration `{ type, file, importName? }` (`file` = a prebuilt ES module exporting a `DockRenderer`) is served at `__renderers/.mjs` and published into the `devframe:dock-renderers` manifest; client runtimes import it lazily on first mount: diff --git a/docs/content/1.guide/23.hub-ui-onboard.md b/docs/content/1.guide/23.hub-ui-onboard.md new file mode 100644 index 00000000..f3fc9a9a --- /dev/null +++ b/docs/content/1.guide/23.hub-ui-onboard.md @@ -0,0 +1,187 @@ +--- +title: 'Opt-in DevTools with Onboarding' +navigation: + icon: i-lucide-download +description: '@devframes/hub-ui-onboard lets a host ship a 20 kB floating button instead of the hub, install the hub on demand, and hand the hub base to it without a restart.' +--- + +`@devframes/hub-ui-onboard` lets a host ship a 20 kB floating button instead of the hub, install the hub on demand, and hand the hub base to it in the same process. + +## Why + +A hub UI provider, its Vue runtime, and the devframes it mounts add tens of megabytes to a framework's install size. A host that wants DevTools as an opt-in can move those packages to optional peers and ship only this package: one browser file, a handful of routes, and three small runtime dependencies (a package-manager detector, a process runner, the diagnostics library). The user still discovers DevTools through the usual floating button; the first click installs them. + +## What the user sees + +The button sits at the bottom left, dimmed until hovered. It opens a panel with the product name and logo, one sentence, the exact command the install will run (for example `pnpm add -D @nuxt/devtools`), and three actions: + +- **Install** runs the command in the project. The panel shows progress, then either the real dock replaces the button in place, or the panel asks for a restart. +- **Hide for now** removes the button for the current browser tab. +- **Disable entirely** writes a state file so the host stops injecting the button on every later start. + +The panel follows the shared design tokens, the host's `primaryColor`, and the user's hub color scheme, so the swap to the real dock looks like one product. + +## Create the onboarding + +```ts +import { createOnboarding } from '@devframes/hub-ui-onboard' + +const onboarding = createOnboarding({ + packages: ['@devframes/hub', '@devframes/hub-ui'], + branding: { productName: 'My DevTools', logo: '/logo.svg', primaryColor: '#646cff' }, +}) +``` + +`createOnboarding()` returns four things: + +- `handler(request)`: a web-standard `Request => Response` handler for every path under `base` (default `/__devframes/`). +- `nodeMiddleware(req, res, next)`: the same handler as Connect middleware for Vite, Express, Fastify with `@fastify/middie`, or a plain `node:http` server. It calls `next()` for paths outside `base`. +- `scriptSrc`: `embedded.js`, the URL to inject as `