From 5aaaf575e7738a85fc7d7653904481e5d7c1d9e8 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Sat, 15 Aug 2026 05:10:13 +0000 Subject: [PATCH] chore: extract shared buildShadowCss pipeline into design/ Both @devframes/hub-ui and @devframes/json-render-ui compiled their shadow-root UnoCSS stylesheet through a hand-copied ~110-line pipeline that encodes the repo's subtlest build knowledge (the plain-vs-variant shortcut drop and the --un-* @property host collision). Extract the shared logic into design/build-shadow-css.ts's buildShadowCss() so the two shadow-root gotchas have one home instead of two drifting copies. Each package's scripts/build-css.ts is now a ~20-line caller that supplies its own srcDir/globs/uno-config/primary-ramp/var-prefix and prints its own summary line (moved out of the shared module so it stays exempt from the no-console lint rule, which only allows console.* inside a scripts/ directory). magic-string, tinyglobby and @unocss/reset move from hub-ui's and json-render-ui's package.json to the root's, since the code that imports them now lives in design/ (part of the root workspace per knip.jsonc), matching how lightningcss was already declared there. Generated CSS output is verified byte-identical to the pre-refactor pipeline for both packages. --- AGENTS.md | 2 +- design/build-shadow-css.ts | 151 +++++++++++++++++++ package.json | 3 + packages/hub-ui/package.json | 2 - packages/hub-ui/scripts/build-css.ts | 113 ++------------ packages/json-render-ui/package.json | 2 - packages/json-render-ui/scripts/build-css.ts | 107 ++----------- pnpm-lock.yaml | 32 ++-- 8 files changed, 193 insertions(+), 219 deletions(-) create mode 100644 design/build-shadow-css.ts diff --git a/AGENTS.md b/AGENTS.md index 06557b50..31e93cd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,7 +62,7 @@ All five built-in plugins - and every example under `examples/` - share one desi - **Respect the skills.** This design system is built to the `antfu` and `antfu-design` skills (UnoCSS-first, class-based semantic tokens, dual light/dark, anti-slop) - load and follow them when building or changing any UI here. The surfaces deliberately echo the upstream devtools they descend from; reference their UI/UX when in doubt: [`antfu/node-modules-inspector`](https://github.com/antfu/node-modules-inspector), [`antfu/vite-plugin-inspect`](https://github.com/antfu/vite-plugin-inspect), [`eslint/config-inspector`](https://github.com/eslint/config-inspector), and [`vitejs/devtools` → `packages/rolldown`](https://github.com/vitejs/devtools/tree/main/packages/rolldown). - **One preset, wired per app.** Each consumer's `uno.config.ts` composes the same stack: `presetAnthonyDesign({ primary })` (from `@antfu/design/unocss`, tuned to devframe's sage green) + a Wind base + `presetIcons()` (Phosphor) + `transformerDirectives()` + `transformerVariantGroup()`, plus the named `z-*` layers the nav/overlay surfaces reference (`z-nav`, `z-dropdown`, `z-tooltip`, `z-toast`, `z-modal-*`, `z-drawer-*`) - `presetAnthonyDesign` blocks plain `z-` so every layer is named. The shared `design/uno.config.ts` exposes this as `designConfig` (the default, on `presetWind4()`) and a `createDesignConfig({ base })` factory; keep the block identical across apps so the surfaces stay consistent. -- **Wind4 by default, Wind3 for web components.** 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 - pass it 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 - so 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 `packages/{hub-ui,json-render-ui}/scripts/build-css.ts`; the Vite `unocss/vite` path for standalone SPAs and Storybook is not affected): +- **Wind4 by default, Wind3 for web components.** 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 - pass it 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 - so 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): - **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. - **Tokens are semantic shortcuts.** Build UI from `@antfu/design`'s class vocabulary - surfaces `bg-base` / `bg-secondary` / `bg-active`, text `color-base` / `color-muted` / `color-faint` / `color-active`, `border-base`, `op-fade` / `op-mute` - never a hardcoded palette. Import `@antfu/design/styles.css` (or cherry-pick `@antfu/design/styles/base.css` + `scrollbar.css`) once per page; dark mode is the `.dark` class on ``, flipped from the OS preference in the SPA entry. diff --git a/design/build-shadow-css.ts b/design/build-shadow-css.ts new file mode 100644 index 00000000..a102dbd2 --- /dev/null +++ b/design/build-shadow-css.ts @@ -0,0 +1,151 @@ +import type { UserConfig } from 'unocss' +import { Buffer } from 'node:buffer' +import fs from 'node:fs/promises' +import { createRequire } from 'node:module' +import { join } from 'node:path' +import { transform } from 'lightningcss' +import MagicString from 'magic-string' +import { glob } from 'tinyglobby' +import { createGenerator } from 'unocss' +import { namespaceShadowCssVars, rewireBakedPrimaryColors, shadowSurfaceSafelist } from './uno.config' + +// Story-only utility classes must not leak into a shipped shadow-root +// stylesheet. +const IGNORE = ['**/*.stories.*', '**/__tests__/**'] + +export interface BuildShadowCssOptions { + /** + * Absolute path of the package's UnoCSS-scanned source directory. The + * compiled stylesheet is written to `/.generated/css.ts`. + */ + srcDir: string + /** Glob patterns (relative to `srcDir`) UnoCSS extracts classes from. */ + globs: string[] + /** The package's own `uno.config` default export. */ + config: UserConfig + /** + * Absolute path to the primary-ramp override stylesheet, appended AFTER + * the UnoCSS output so its `:host`/`:root, :host` block wins over Wind's + * own primary declarations (see each package's `primary-ramp.css`). + */ + primaryRampPath: string + /** + * Absolute path to a hand-authored stylesheet run through the generator's + * configured transformers (directives, variant groups) and merged in + * right after the CSS reset. Omit for a package with no hand-written + * styles. + */ + userStylePath?: string + /** + * Prefix Wind's `--un-*` custom properties are renamed to (see + * `namespaceShadowCssVars`) — unique per shadow-root surface so two + * shadow trees on the same host page never collide. + */ + varPrefix: string +} + +export interface BuildShadowCssResult { + /** Number of source files scanned for class extraction. */ + sourceCount: number + /** The compiled, minified shadow-root stylesheet. */ + css: string +} + +// Compile a shadow-root surface's UnoCSS output ahead of time into a plain +// string module (`/.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 { + const { srcDir, globs, config, primaryRampPath, userStylePath, varPrefix } = options + const generatedCss = join(srcDir, '.generated/css.ts') + + const require = createRequire(import.meta.url) + const reset = await fs.readFile(require.resolve('@unocss/reset/tailwind.css'), 'utf-8') + const files = await glob(globs, { + cwd: srcDir, + absolute: true, + ignore: IGNORE, + }) + + // 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 + // package's component sources too so those classes ship in the injected + // CSS. + const designComponentsDir = join(require.resolve('@antfu/design/package.json'), '..', 'components') + const designFiles = await glob('**/*.vue', { + cwd: designComponentsDir, + absolute: true, + ignore: IGNORE, + }) + + const generator = await createGenerator(config) + + const tokens = new Set() + for (const file of [...files, ...designFiles]) { + const content = await fs.readFile(file, 'utf-8') + await generator.applyExtractors(content, file, tokens) + } + + // The hand-written stylesheet (if any) may use `--at-apply` — run it + // through the configured transformers (directives, variant groups) before + // merging. + const userStyle = userStylePath + ? new MagicString(await fs.readFile(userStylePath, 'utf-8').catch(() => '')) + : undefined + if (userStyle) { + for (const transformer of generator.config.transformers ?? []) { + await transformer.transform(userStyle, userStylePath!, { uno: generator } as any) + } + } + + const primaryRamp = await fs.readFile(primaryRampPath, 'utf-8') + 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`) — + // 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> }).colors?.primary ?? {} + const unoCss = rewireBakedPrimaryColors(unoResult.css, primaryTheme) + const surfacesCss = rewireBakedPrimaryColors(surfaces.css, primaryTheme) + // Namespace Wind's `--un-*` vars so this shadow-root stylesheet is immune + // to a host page's Wind4 `@property` registrations (see + // `namespaceShadowCssVars`). + let css = [ + reset, + userStyle?.toString(), + unoCss, + surfacesCss, + primaryRamp, + ].filter((part): part is string => part !== undefined).join('\n') + + css = namespaceShadowCssVars(css, varPrefix) + css = transform({ + filename: 'hub-ui.css', + code: Buffer.from(css), + minify: true, + }).code.toString() + + await fs.mkdir(join(srcDir, '.generated'), { recursive: true }) + await fs.writeFile(generatedCss, [ + `/* eslint-disable eslint-comments/no-unlimited-disable */`, + `/* eslint-disable */`, + `export default ${JSON.stringify(String(css))}`, + '', + ].join('\n')) + + return { sourceCount: files.length, css } +} diff --git a/package.json b/package.json index 397f2fba..408369be 100644 --- a/package.json +++ b/package.json @@ -45,17 +45,20 @@ "@types/node": "catalog:types", "@types/prompts": "catalog:types", "@types/ws": "catalog:types", + "@unocss/reset": "catalog:frontend", "bumpp": "catalog:tooling", "crossws": "catalog:deps", "eslint": "catalog:tooling", "h3": "catalog:deps", "knip": "catalog:tooling", "lightningcss": "catalog:build", + "magic-string": "catalog:build", "nano-staged": "catalog:tooling", "pathe": "catalog:deps", "prompts": "catalog:tooling", "simple-git-hooks": "catalog:tooling", "skills-npm": "catalog:tooling", + "tinyglobby": "catalog:deps", "tsnapi": "catalog:testing", "tsx": "catalog:build", "turbo": "catalog:build", diff --git a/packages/hub-ui/package.json b/packages/hub-ui/package.json index 0124839f..cee5955b 100644 --- a/packages/hub-ui/package.json +++ b/packages/hub-ui/package.json @@ -61,9 +61,7 @@ "dompurify": "catalog:frontend", "fuse.js": "catalog:frontend", "iframe-pane": "catalog:frontend", - "magic-string": "catalog:build", "storybook": "catalog:storybook", - "tinyglobby": "catalog:deps", "tsdown": "catalog:build", "tsx": "catalog:build", "unocss": "catalog:frontend", diff --git a/packages/hub-ui/scripts/build-css.ts b/packages/hub-ui/scripts/build-css.ts index 65b00906..a26d3f1d 100644 --- a/packages/hub-ui/scripts/build-css.ts +++ b/packages/hub-ui/scripts/build-css.ts @@ -1,110 +1,23 @@ -import { Buffer } from 'node:buffer' -import fs from 'node:fs/promises' -import { createRequire } from 'node:module' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { colors as c } from 'devframe/utils/colors' -import { transform } from 'lightningcss' -import MagicString from 'magic-string' -import { glob } from 'tinyglobby' -import { createGenerator } from 'unocss' -import { namespaceShadowCssVars, rewireBakedPrimaryColors, shadowSurfaceSafelist } from '../../../design/uno.config' +import { buildShadowCss } from '../../../design/build-shadow-css' import config from '../uno.config' -// Compile the components' UnoCSS output ahead of time into a plain string +// Compiles the components' UnoCSS output ahead of time into a plain string // module (`src/client/.generated/css.ts`) that `defineCustomElement` adopts // into each shadow root — the dock stays fully styled inside any host page // without a global stylesheet, and the host page's own styles can't leak in. +// See `design/build-shadow-css.ts` for the shared pipeline (mirrored by +// `@devframes/json-render-ui`'s `scripts/build-css.ts`). const SRC_DIR = fileURLToPath(new URL('../src/client', import.meta.url)) -const GLOBS = ['components/**/*.{ts,vue}', 'state/**/*.ts', 'embedded/**/*.ts', 'standalone/**/*.{ts,html}'] -// Story-only utility classes must not leak into the shipped stylesheet. -const IGNORE = ['**/*.stories.*', '**/__tests__/**'] -const USER_STYLE = join(SRC_DIR, 'style.css') -// The single-overridable-variable primary ramp. Appended AFTER the UnoCSS -// output so its `:host` block wins over Wind4's own `:root, :host` primary -// declarations (kept in its own file so the Storybook preview can import the -// exact same override after `virtual:uno.css`). See the file's own comment. -const PRIMARY_RAMP = join(SRC_DIR, 'primary-ramp.css') -const GENERATED_CSS = join(SRC_DIR, '.generated/css.ts') -export async function buildCSS(): Promise { - const require = createRequire(import.meta.url) - const reset = await fs.readFile(require.resolve('@unocss/reset/tailwind.css'), 'utf-8') - const files = await glob(GLOBS, { - cwd: SRC_DIR, - absolute: true, - ignore: IGNORE, - }) - - // The dock reuses `@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 package's - // component sources too so those classes ship in the injected CSS. - const designComponentsDir = join(require.resolve('@antfu/design/package.json'), '..', 'components') - const designFiles = await glob('**/*.vue', { - cwd: designComponentsDir, - absolute: true, - ignore: IGNORE, - }) - - const generator = await createGenerator(config) - - const tokens = new Set() - for (const file of [...files, ...designFiles]) { - const content = await fs.readFile(file, 'utf-8') - await generator.applyExtractors(content, file, tokens) - } - - // The hand-written stylesheet may use `--at-apply` — run it through the - // configured transformers (directives, variant groups) before merging. - const userStyle = new MagicString(await fs.readFile(USER_STYLE, 'utf-8').catch(() => '')) - for (const transformer of generator.config.transformers ?? []) { - await transformer.transform(userStyle, USER_STYLE, { uno: generator } as any) - } - - const primaryRamp = await fs.readFile(PRIMARY_RAMP, 'utf-8') - 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`) — - // 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> }).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`). - let css = [ - reset, - userStyle.toString(), - unoCss, - surfacesCss, - primaryRamp, - ].join('\n') - - css = namespaceShadowCssVars(css, '--un-hub-') - css = transform({ - filename: 'hub-ui.css', - code: Buffer.from(css), - minify: true, - }).code.toString() - - await fs.mkdir(join(SRC_DIR, '.generated'), { recursive: true }) - await fs.writeFile(GENERATED_CSS, [ - `/* eslint-disable eslint-comments/no-unlimited-disable */`, - `/* eslint-disable */`, - `export default ${JSON.stringify(String(css))}`, - '', - ].join('\n')) - console.log(`${c.green('✓')} CSS built (${files.length} sources, ${(css.length / 1024).toFixed(1)} kB)`) -} - -await buildCSS() +const { sourceCount, css } = await buildShadowCss({ + srcDir: SRC_DIR, + globs: ['components/**/*.{ts,vue}', 'state/**/*.ts', 'embedded/**/*.ts', 'standalone/**/*.{ts,html}'], + config, + primaryRampPath: join(SRC_DIR, 'primary-ramp.css'), + userStylePath: join(SRC_DIR, 'style.css'), + varPrefix: '--un-hub-', +}) +console.log(`${c.green('✓')} CSS built (${sourceCount} sources, ${(css.length / 1024).toFixed(1)} kB)`) diff --git a/packages/json-render-ui/package.json b/packages/json-render-ui/package.json index 7944a5e0..e3ace65c 100644 --- a/packages/json-render-ui/package.json +++ b/packages/json-render-ui/package.json @@ -61,11 +61,9 @@ "@storybook/addon-docs": "catalog:storybook", "@storybook/vue3-vite": "catalog:storybook", "@unocss/preset-icons": "catalog:frontend", - "@unocss/reset": "catalog:frontend", "@vitejs/plugin-vue": "catalog:build", "devframe": "workspace:*", "storybook": "catalog:storybook", - "tinyglobby": "catalog:deps", "tsdown": "catalog:build", "tsx": "catalog:build", "unocss": "catalog:frontend", diff --git a/packages/json-render-ui/scripts/build-css.ts b/packages/json-render-ui/scripts/build-css.ts index fe92dd45..fc7164e6 100644 --- a/packages/json-render-ui/scripts/build-css.ts +++ b/packages/json-render-ui/scripts/build-css.ts @@ -1,102 +1,23 @@ -import { Buffer } from 'node:buffer' -import fs from 'node:fs/promises' -import { createRequire } from 'node:module' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { colors as c } from 'devframe/utils/colors' -import { transform } from 'lightningcss' -import { glob } from 'tinyglobby' -import { createGenerator } from 'unocss' -import { namespaceShadowCssVars, rewireBakedPrimaryColors, shadowSurfaceSafelist } from '../../../design/uno.config' +import { buildShadowCss } from '../../../design/build-shadow-css' import config from '../uno.config' -// Compile the renderer's UnoCSS output ahead of time into a plain string +// Compiles the renderer's UnoCSS output ahead of time into a plain string // module (`src/.generated/css.ts`) that the prebuilt renderer module adopts // into the shadow root it attaches inside its mount container — the dock view // stays fully styled inside any host page (light DOM or a viewer's shadow // root) without a global stylesheet, and without leaking the reset into the -// host page. Mirrors `@devframes/hub-ui`'s `scripts/build-css.ts`. -const PKG_DIR = fileURLToPath(new URL('..', import.meta.url)) -const SRC_DIR = join(PKG_DIR, 'src') -const GLOBS = ['components/**/*.ts', 'renderer.ts', 'dock-renderer.ts', 'renderer-module/**/*.ts'] -// Story-only utility classes must not leak into the shipped stylesheet. -const IGNORE = ['**/*.stories.*', '**/__tests__/**'] -// The single-overridable-variable primary ramp, appended AFTER the UnoCSS -// output so its `:root, :host` block wins over Wind4's own primary -// declarations — a viewer's `--devframe-primary` branding inherits across the -// shadow boundary and retints the rendered views. See the file's own comment. -const PRIMARY_RAMP = join(SRC_DIR, 'renderer-module/primary-ramp.css') -const GENERATED_CSS = join(SRC_DIR, '.generated/css.ts') - -export async function buildCSS(): Promise { - const require = createRequire(import.meta.url) - const reset = await fs.readFile(require.resolve('@unocss/reset/tailwind.css'), 'utf-8') - const files = await glob(GLOBS, { - cwd: SRC_DIR, - absolute: true, - ignore: IGNORE, - }) - - // The catalog components reuse `@antfu/design`'s Vue components (buttons, - // badges, form controls, …) directly. UnoCSS ignores `node_modules` by - // default, so their semantic shortcut classes would be absent from the - // shadow-root stylesheet — scan the design package's component sources too. - const designComponentsDir = join(require.resolve('@antfu/design/package.json'), '..', 'components') - const designFiles = await glob('**/*.vue', { - cwd: designComponentsDir, - absolute: true, - ignore: IGNORE, - }) - - const generator = await createGenerator(config) - - const tokens = new Set() - for (const file of [...files, ...designFiles]) { - const content = await fs.readFile(file, 'utf-8') - await generator.applyExtractors(content, file, tokens) - } - - const primaryRamp = await fs.readFile(PRIMARY_RAMP, 'utf-8') - 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`) — - // 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> }).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`). - let css = [ - reset, - unoCss, - surfacesCss, - primaryRamp, - ].join('\n') - - css = namespaceShadowCssVars(css, '--un-jr-') - css = transform({ - filename: 'hub-ui.css', - code: Buffer.from(css), - minify: true, - }).code.toString() - - await fs.mkdir(join(SRC_DIR, '.generated'), { recursive: true }) - await fs.writeFile(GENERATED_CSS, [ - `/* eslint-disable eslint-comments/no-unlimited-disable */`, - `/* eslint-disable */`, - `export default ${JSON.stringify(String(css))}`, - '', - ].join('\n')) - console.log(`${c.green('✓')} CSS built (${files.length} sources, ${(css.length / 1024).toFixed(1)} kB)`) -} - -await buildCSS() +// host page. See `design/build-shadow-css.ts` for the shared pipeline +// (mirrored by `@devframes/hub-ui`'s `scripts/build-css.ts`). +const SRC_DIR = join(fileURLToPath(new URL('..', import.meta.url)), 'src') + +const { sourceCount, css } = await buildShadowCss({ + srcDir: SRC_DIR, + globs: ['components/**/*.ts', 'renderer.ts', 'dock-renderer.ts', 'renderer-module/**/*.ts'], + config, + primaryRampPath: join(SRC_DIR, 'renderer-module/primary-ramp.css'), + varPrefix: '--un-jr-', +}) +console.log(`${c.green('✓')} CSS built (${sourceCount} sources, ${(css.length / 1024).toFixed(1)} kB)`) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0786de3..7649267a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -406,6 +406,9 @@ importers: '@types/ws': specifier: catalog:types version: 8.18.1 + '@unocss/reset': + specifier: catalog:frontend + version: 66.7.5 bumpp: specifier: catalog:tooling version: 12.2.0 @@ -424,6 +427,9 @@ importers: lightningcss: specifier: catalog:build version: 1.33.0 + magic-string: + specifier: catalog:build + version: 1.1.1 nano-staged: specifier: catalog:tooling version: 1.0.2 @@ -439,6 +445,9 @@ importers: skills-npm: specifier: catalog:tooling version: 1.2.0 + tinyglobby: + specifier: catalog:deps + version: 0.2.17 tsnapi: specifier: catalog:testing version: 1.2.0(vitest@4.1.10(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))) @@ -1302,15 +1311,9 @@ importers: iframe-pane: specifier: catalog:frontend version: 1.1.0 - magic-string: - specifier: catalog:build - version: 1.1.1 storybook: specifier: catalog:storybook version: 10.5.7(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - tinyglobby: - specifier: catalog:deps - version: 0.2.17 tsdown: specifier: catalog:build version: 0.22.14(@volar/typescript@2.4.28(typescript@5.9.3))(oxc-resolver@11.24.2)(tsx@4.23.12)(typescript@5.9.3) @@ -1379,9 +1382,6 @@ importers: '@unocss/preset-icons': specifier: catalog:frontend version: 66.7.5 - '@unocss/reset': - specifier: catalog:frontend - version: 66.7.5 '@vitejs/plugin-vue': specifier: catalog:build version: 6.0.8(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) @@ -1391,9 +1391,6 @@ importers: storybook: specifier: catalog:storybook version: 10.5.7(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - tinyglobby: - specifier: catalog:deps - version: 0.2.17 tsdown: specifier: catalog:build version: 0.22.14(@volar/typescript@2.4.28(typescript@6.0.3))(oxc-resolver@11.24.2)(tsx@4.23.12)(typescript@6.0.3) @@ -8091,9 +8088,6 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magic-string@1.0.0: - resolution: {integrity: sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==} - magic-string@1.1.1: resolution: {integrity: sha512-qFemKPzc3ttrYVaMmnSkGtGc5nE6Ncl4bj7c9IE6C9OUIRXjf6PzJ+UZ1xhVIjYc7dolHq3qKzpAJPZUbvNj+A==} @@ -13441,7 +13435,7 @@ snapshots: '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.8(@typescript-eslint/types@8.67.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))': dependencies: deepmerge: 4.3.1 - magic-string: 1.0.0 + magic-string: 1.1.1 obug: 2.1.4 svelte: 5.56.8(@typescript-eslint/types@8.67.0) vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) @@ -16543,10 +16537,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@1.0.0: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@1.1.1: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -18231,7 +18221,7 @@ snapshots: rolldown-string@0.3.1(rolldown@1.2.3): dependencies: - magic-string: 1.0.0 + magic-string: 1.1.1 optionalDependencies: rolldown: 1.2.3