Skip to content

Latest commit

 

History

History
35 lines (22 loc) · 7.86 KB

06-design-system.md

File metadata and controls

35 lines (22 loc) · 7.86 KB

06 - Design system

All five built-in plugins - and every example under examples/ - share one design system, @antfu/design, so they look and feel like one product across frameworks (git is React/Next, terminals is Svelte, code-server is Vue, inspect is Vue, a11y is Solid, the examples are Preact/Next/vanilla). It's a dev dependency consumed at build time: its UnoCSS preset and shipped styles drive every surface, and its Vue components are the canonical reference every framework matches. There is no shared internal design package - each app wires the preset itself and owns its own component ports.

Skills and references

Building or changing any UI here MUST follow the antfu and antfu-design skills (UnoCSS-first, class-based semantic tokens, dual light/dark, anti-slop) - load them first. The surfaces deliberately echo the upstream devtools they descend from; reference their UI/UX when in doubt: antfu/node-modules-inspector, antfu/vite-plugin-inspect, eslint/config-inspector, and vitejs/devtools → 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-<number> 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 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, @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,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: '<percentage>' } 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, components, layout

  • Tokens are semantic shortcuts. UI MUST build 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 <html>, flipped from the OS preference in the SPA entry.
  • Vue uses the components directly; other frameworks port them. The Vue surface (inspect) imports components straight from @antfu/design/components/* (ActionButton, ActionIconButton, DisplayBadge, LayoutTabs, LayoutToolbar, LayoutCard, …). Every non-Vue surface ports the components it needs into its own framework - React in git and the Next examples, Svelte in terminals, Solid in a11y, Preact in the Preact examples, vanilla DOM helpers in the Vite hub - mirroring the upstream component's markup, classes and behavior so it renders identically. Port on demand: recreate only what a surface uses, and keep each port faithful to its @antfu/design source.
  • One nav, three buttons, one tab selector - strictly. Every surface opens with the same top bar - a LayoutToolbar-style row led by a brand block (a primary-tinted i-ph:* icon + the product name). Buttons come in exactly three forms: a text button (ActionButton → btn-action / btn-primary), a bordered icon button (ActionIconButton → btn-icon-square), and a borderless icon button (round btn-icon). Multi-view tools (inspect, git) switch views with the one shared segmented selector (LayoutTabs variant="segment": a bg-secondary track with data-[state=active]:bg-base triggers). Bespoke nav bars, button shapes, or tab styles MUST NOT be invented.
  • Icons come from the shared Phosphor set (i-ph:*, duotone preferred) via presetIcons - everywhere, instead of per-consumer icon libraries or bespoke SVG.
  • A surface keeping its own component CSS (inspect, a11y) sources every color from @antfu/design's semantic shortcuts via --at-apply (expanded by transformerDirectives) rather than hardcoding a palette, so it tracks the shared theme and the .dark class.
  • Plain .ts/vanilla views MUST opt .ts into UnoCSS extraction (content.pipeline.include for Vite, or content.filesystem globs for the @unocss/postcss setup Next uses), since UnoCSS only scans framework files by default.
  • Before making new components or inline DOM elements, always check whether an @antfu/design component can be reused.
  • hub-ui text goes through t(). Every user-facing string in packages/hub-ui/src/client (templates, tooltips, aria-labels, command titles, confirm copy) MUST be a key in client/i18n/locales/en.json rendered with t() / tAround() from client/i18n, never a literal. A new key lands in every locale file in the same change; the i18n test enforces the lockstep. Brand names and DF diagnostics stay untranslated.

Storybook

Each plugin's storybook follows one setup - co-located *.stories.*, a viteFinal that adds the framework plugin + unocss/vite (pointed at the plugin's uno.config), @antfu/design/styles.css, a theme toggle on the .dark class, and a bg-base color-base decorator. The Vue surface (inspect, @storybook/vue3-vite) showcases the @antfu/design components in real use - the visual reference the React/Svelte/Solid/vanilla ports match, mirroring @antfu/design's own storybook.