Skip to content

Commit f07f367

Browse files
committed
feat(design): replace @internal/design with @antfu/design
Adopt @antfu/design as the one design system across every built-in plugin and example, so they look and feel like one product. Each app wires the @antfu/design UnoCSS preset directly (devframe's sage-green primary over Wind4, Phosphor icons, DM Sans/Mono, named z-layers). The Vue surface (inspect) imports @antfu/design components directly; the React, Svelte, Solid, Preact and vanilla surfaces render the same @antfu/design vocabulary through faithful, co-located framework ports. Remove the internal, source-only @internal/design package along with its df-* class vocabulary and --df-* token set, and update the plugin storybooks and the e2e selectors to match. Intentional, minor visual changes come with adopting the shared system.
1 parent 1d8b4c7 commit f07f367

97 files changed

Lines changed: 1984 additions & 1076 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,17 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
4040

4141
### Design system
4242

43-
All five built-in plugins — and every example under `examples/` — share one design system, `@internal/design`, so they look and feel like one product across frameworks (Git is React/Next, terminals is Svelte, code-server is vanilla DOM, inspect is Vue, a11y is Solid, the examples are Preact/Next/vanilla). It's a private, source-only package — never built or published; consumers import its TypeScript/CSS directly (resolved through `alias.ts` for bundlers and the package `exports` for config loaders), so editing it needs no rebuild.
43+
All five built-in plugins — and every example under `examples/` — share one design system, [`@antfu/design`](https://github.com/antfu/design), so they look and feel like one product across frameworks (Git is React/Next, terminals is Svelte, code-server is vanilla DOM, 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.
4444

4545
- **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).
46-
- **One preset to extend.** Each consumer's `uno.config.ts` is just `presets: [presetDevframe()]` (imported from `@internal/design/preset`). The preset bundles `presetWind4` + `presetIcons` (Phosphor) + the directive/variant-group transformers, the semantic token theme, and the shared `df-*` component shortcuts (which it safelists). Don't re-declare presets, palettes, or shortcuts per consumer.
47-
- **One token source.** Import `@internal/design/theme.css` once on the page (after the generated UnoCSS stylesheet so its base layer wins). Token *values* (the `--df-*` custom properties, light + dark via the `.dark` class) live only there — never hardcode a palette. The brand primary is devframe's sage green; flip the OS preference onto `<html class="dark">` from the SPA entry.
48-
- **Shared component vocabulary.** Build UI from the `df-*` classes (`df-btn`, `df-badge`, `df-tab`, `df-navtab`, `df-nav`, `df-nav-brand`, `df-toolbar`, `df-card`, `df-input`, `df-dot`, `df-tag-*`, …) and the semantic token utilities (`bg-primary`, `text-muted-foreground`, `bg-card`, `border-border`, …). Markup differs per framework; the classes resolve identically, which is what keeps the surfaces consistent.
49-
- **Component builders.** Prefer the framework-neutral recipes from `@internal/design/components` (`button`, `iconButton`, `badge`, `tab`, `tabsList`, `navTab`, `nav`, `navBrand`, `toolbar`, `card`, `panel`, `input`, `link`, `dot`, `spinner`, `tag`) over hand-written class strings — they return the canonical `df-*` classes and read the same in React (`className=`), Svelte/Vue (`class=`), vanilla DOM and Solid. Because these classes are assembled at runtime, the preset safelists the `df-*` vocabulary so UnoCSS always emits it; add new component classes to `DF_SAFELIST` when you extend the set.
50-
- **One nav, three buttons, one tab selector — strictly.** Every surface opens with the same top bar: `nav()` (a single row, one fixed height) led by `navBrand()` (a primary-tinted `i-ph-*` icon + the product name). Buttons come in exactly three forms and nothing else: a **text button** (`button({ variant, size })`), an **icon button** (`iconButton()` — bordered), and a **borderless icon button** (`iconButton({ variant: 'ghost' })`). Multi-view tools (inspect, git) switch views with the one shared segmented selector — `tabsList()` wrapping `tab()` buttons that carry `data-state="active"` and a leading `i-ph-*` icon per tab. Don't invent bespoke nav bars, button shapes, or tab styles.
51-
- **Icons** come from the shared Phosphor set (`i-ph-*`, duotone preferred) via `presetIcons` — use them everywhere instead of per-consumer icon libraries or bespoke SVG.
52-
- **A surface keeping its own component CSS** (inspect, a11y) re-bases its color/radius tokens onto the `--df-*` variables rather than hardcoding a palette, so it tracks the shared theme.
46+
- **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) + `presetWind4()` + `presetIcons()` (Phosphor) + `presetWebFonts()` (DM Sans / DM Mono) + `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. Keep the block identical across apps so the surfaces stay consistent.
47+
- **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 `<html>`, flipped from the OS preference in the SPA entry.
48+
- **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 code-server and 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.
49+
- **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). Don't invent bespoke nav bars, button shapes, or tab styles.
50+
- **Icons** come from the shared Phosphor set (`i-ph:*`, duotone preferred) via `presetIcons` — use them everywhere instead of per-consumer icon libraries or bespoke SVG.
51+
- **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.
5352
- **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.
53+
- **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](https://github.com/antfu/design/tree/main/storybook).
5454

5555
### Devframe design principles
5656

alias.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ export const alias = {
4545
'@devframes/hub': r('hub/src/index.ts'),
4646
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
4747
'@devframes/nuxt': r('nuxt/src/index.ts'),
48-
'@internal/design/preset': r('design/src/preset.ts'),
49-
'@internal/design/components': r('design/src/components.ts'),
50-
'@internal/design/tokens': r('design/src/tokens.ts'),
51-
'@internal/design/theme.css': r('design/src/theme.css'),
52-
'@internal/design': r('design/src/index.ts'),
5348
'@devframes/plugin-code-server/client': p('code-server/src/client/index.ts'),
5449
'@devframes/plugin-code-server/node': p('code-server/src/node/index.ts'),
5550
'@devframes/plugin-code-server/constants': p('code-server/src/constants.ts'),

examples/files-inspector/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"test": "vitest run"
1717
},
1818
"dependencies": {
19-
"@internal/design": "workspace:*",
19+
"@antfu/design": "catalog:frontend",
20+
"colorjs.io": "catalog:frontend",
2021
"devframe": "workspace:*",
2122
"preact": "catalog:frontend",
2223
"tinyglobby": "catalog:deps"

examples/files-inspector/src/client/app.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { DevframeScopedClientContext } from 'devframe/client'
2-
import { nav, navBrand, tab as tabClass, tabsList } from '@internal/design/components'
32
import { connectDevframe } from 'devframe/client'
43
import { useEffect, useState } from 'preact/hooks'
4+
import { nav, navBrand, tab as tabClass, tabsList } from './design'
55
import { About } from './routes/about'
66
import { Home } from './routes/home'
77

@@ -52,7 +52,7 @@ export function App() {
5252

5353
if (!ctx) {
5454
return (
55-
<div class="grid min-h-screen place-items-center bg-background text-muted-foreground font-sans text-sm">
55+
<div class="grid min-h-screen place-items-center bg-base color-muted font-sans text-sm">
5656
Connecting to devframe…
5757
</div>
5858
)
@@ -62,7 +62,7 @@ export function App() {
6262
const active = route === '/about' ? '/about' : '/'
6363

6464
return (
65-
<div class="flex flex-col min-h-screen bg-background text-foreground font-sans">
65+
<div class="flex flex-col min-h-screen bg-base color-base font-sans">
6666
<header class={nav()}>
6767
<span class={navBrand()}>
6868
<span class="i-ph-folder-duotone text-base color-active" />
@@ -88,7 +88,7 @@ export function App() {
8888

8989
<span class="flex-1" />
9090

91-
<small class="flex items-center gap-1.5 text-muted-foreground text-xs font-mono">
91+
<small class="flex items-center gap-1.5 color-muted text-xs font-mono">
9292
<span>base</span>
9393
<code class="color-base">{basePath}</code>
9494
<span class="op-mute">·</span>
@@ -97,7 +97,7 @@ export function App() {
9797
</small>
9898
</header>
9999

100-
<main class="scrollbar-slim min-h-0 flex-1 overflow-auto">
100+
<main class="min-h-0 flex-1 overflow-auto">
101101
{active === '/about'
102102
? <About ctx={ctx} basePath={basePath} />
103103
: <Home ctx={ctx} />}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// @unocss-include
2+
// Co-located devframe -> @antfu/design class helpers: framework-neutral builders
3+
// returning @antfu/design's semantic shortcut classes, so this surface looks
4+
// identical to the antfu Vue components. The `@unocss-include` marker makes
5+
// UnoCSS emit the runtime-assembled class chains below.
6+
// Tag palette kept literal for extraction: badge-color-blue badge-color-amber
7+
// badge-color-green badge-color-red badge-color-sky badge-color-violet
8+
// badge-color-rose badge-color-teal badge-color-orange badge-color-emerald
9+
10+
export function cx(...parts: Array<string | false | null | undefined>): string {
11+
return parts.filter(Boolean).join(' ')
12+
}
13+
14+
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link'
15+
export type ButtonSize = 'md' | 'sm' | 'lg'
16+
export interface ButtonProps { variant?: ButtonVariant, size?: ButtonSize, class?: string }
17+
18+
export function button({ variant = 'primary', size = 'md', class: extra }: ButtonProps = {}): string {
19+
const variantClass: Record<ButtonVariant, string> = {
20+
primary: 'btn-primary',
21+
secondary: 'btn-action',
22+
outline: 'btn-action',
23+
ghost: 'inline-flex items-center justify-center gap-1.5 rounded px2 py1 op75 hover:op100 hover:bg-active transition disabled:pointer-events-none disabled:op30!',
24+
destructive: 'btn-action text-error border-error/30!',
25+
link: 'inline-flex items-center gap-1.5 color-active hover:underline underline-offset-2',
26+
}
27+
const sizeClass = size === 'sm'
28+
? (variant === 'primary' ? 'text-sm px-2.5! py-1!' : 'text-sm')
29+
: size === 'lg' ? 'text-base px-4! py-2!' : ''
30+
return cx(variantClass[variant], sizeClass, extra)
31+
}
32+
33+
export type IconButtonVariant = 'outline' | 'ghost'
34+
export type IconButtonSize = 'md' | 'sm'
35+
export interface IconButtonProps { variant?: IconButtonVariant, size?: IconButtonSize, class?: string }
36+
37+
export function iconButton({ variant = 'outline', size = 'md', class: extra }: IconButtonProps = {}): string {
38+
const base = variant === 'ghost' ? 'btn-icon' : 'btn-icon-square'
39+
const sizeClass = size === 'sm' ? 'w-7! h-7! text-sm' : ''
40+
return cx(base, sizeClass, extra)
41+
}
42+
43+
export type BadgeVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline'
44+
export interface BadgeProps { variant?: BadgeVariant, class?: string }
45+
46+
export function badge({ variant = 'secondary', class: extra }: BadgeProps = {}): string {
47+
const variantClass: Record<BadgeVariant, string> = {
48+
primary: 'badge-active',
49+
secondary: 'badge-muted',
50+
success: 'badge badge-color-green',
51+
warning: 'badge badge-color-amber',
52+
destructive: 'badge badge-color-red',
53+
outline: 'badge border border-base',
54+
}
55+
return cx(variantClass[variant], extra)
56+
}
57+
58+
export function tag(color: string, extra?: string): string {
59+
return cx('badge', `badge-color-${color}`, extra)
60+
}
61+
62+
export function tabsList(extra?: string): string {
63+
return cx('inline-flex items-center gap-1 p-1 rounded-lg bg-secondary w-max', extra)
64+
}
65+
66+
export function tab(extra?: string): string {
67+
return cx(
68+
'px-3 py-1 rounded-md text-sm color-muted inline-flex gap-1.5 items-center whitespace-nowrap select-none cursor-pointer transition outline-none hover:color-base focus-visible:ring-2 focus-visible:ring-primary-500/40 data-[state=active]:bg-base data-[state=active]:color-base data-[state=active]:shadow-sm',
69+
extra,
70+
)
71+
}
72+
73+
export interface NavTabProps { active?: boolean, class?: string }
74+
export function navTab({ active = false, class: extra }: NavTabProps = {}): string {
75+
return cx(
76+
'relative inline-flex items-center gap-1.5 max-w-52 px-2 py-1 rounded-md border border-transparent text-sm op-fade select-none cursor-pointer transition hover:op100 hover:bg-active',
77+
active && 'op100! bg-active border-base! color-base',
78+
extra,
79+
)
80+
}
81+
82+
export function nav(extra?: string): string {
83+
return cx('flex items-center gap-2 shrink-0 h-10 px-3 border-b border-base bg-base z-nav', extra)
84+
}
85+
86+
export function navBrand(extra?: string): string {
87+
return cx('flex items-center gap-1.5 shrink-0 font-semibold text-sm select-none', extra)
88+
}
89+
90+
export function toolbar(extra?: string): string {
91+
return cx('flex items-center gap-2 shrink-0 h-8 px-2.5 border-b border-base bg-secondary text-sm', extra)
92+
}
93+
94+
export function card(extra?: string): string {
95+
return cx('flex flex-col rounded-xl border border-base bg-base shadow-sm', extra)
96+
}
97+
98+
export function panel(extra?: string): string {
99+
return cx('rounded-lg border border-base bg-base', extra)
100+
}
101+
102+
export function input(extra?: string): string {
103+
return cx('w-full min-w-0 rounded border border-base bg-base px-2.5 py-1 text-sm outline-none transition placeholder:color-faint focus-visible:border-active focus-visible:ring-2 focus-visible:ring-primary-500/40', extra)
104+
}
105+
106+
export function link(extra?: string): string {
107+
return cx('color-active hover:underline underline-offset-2', extra)
108+
}
109+
110+
export type DotState = 'running' | 'idle' | 'error'
111+
export function dot(state: DotState, extra?: string): string {
112+
const stateClass: Record<DotState, string> = {
113+
running: 'bg-success',
114+
idle: 'bg-neutral-400',
115+
error: 'bg-error',
116+
}
117+
return cx('inline-block size-1.5 rounded-full shrink-0', stateClass[state], extra)
118+
}
119+
120+
export function spinner(extra?: string): string {
121+
return cx('inline-block size-4 rounded-full border-2 border-current border-t-transparent animate-spin', extra)
122+
}

examples/files-inspector/src/client/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { render } from 'preact'
22
import { App } from './app'
33
import 'virtual:uno.css'
4-
import '@internal/design/theme.css'
4+
import '@antfu/design/styles.css'
55

66
// Shared design tokens flip on the `.dark` class; mirror the OS preference onto
77
// <html> (the built-in devframe plugins follow the same approach).

examples/files-inspector/src/client/routes/about.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@ export function About({ ctx, basePath }: { ctx: InspectorCtx, basePath: string }
2323
<h2 class="text-base font-semibold">About</h2>
2424
</div>
2525

26-
<p class="text-sm text-muted-foreground">
26+
<p class="text-sm color-muted">
2727
This page demonstrates that the SPA discovers its mount path at runtime —
2828
the same bundle works under any base path.
2929
</p>
3030

31-
<dl class="overflow-hidden rounded-md border border-border bg-card text-card-foreground">
31+
<dl class="overflow-hidden rounded-md border border-base bg-base color-base">
3232
{rows.map(({ label, value, icon }) => (
3333
<div
3434
key={label}
35-
class="flex items-center gap-3 border-b border-border px-3 py-2.5 last:border-b-0"
35+
class="flex items-center gap-3 border-b border-base px-3 py-2.5 last:border-b-0"
3636
>
37-
<span class={`${icon} shrink-0 text-muted-foreground`} />
38-
<dt class="w-40 shrink-0 text-sm text-muted-foreground">{label}</dt>
37+
<span class={`${icon} shrink-0 color-muted`} />
38+
<dt class="w-40 shrink-0 text-sm color-muted">{label}</dt>
3939
<dd class="m-0 min-w-0 flex-1 truncate font-mono text-sm">{value}</dd>
4040
</div>
4141
))}

examples/files-inspector/src/client/routes/home.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { InspectorCtx } from '../app'
2-
import { badge, button } from '@internal/design/components'
32
import { useEffect, useState } from 'preact/hooks'
3+
import { badge, button } from '../design'
44

55
export function Home({ ctx }: { ctx: InspectorCtx }) {
66
const [files, setFiles] = useState<string[]>([])
@@ -42,10 +42,10 @@ export function Home({ ctx }: { ctx: InspectorCtx }) {
4242
</button>
4343
</div>
4444

45-
<div class="overflow-hidden rounded-md border border-border bg-card text-card-foreground">
45+
<div class="overflow-hidden rounded-md border border-base bg-base color-base">
4646
{files.length === 0
4747
? (
48-
<p class="px-3 py-10 text-center text-sm text-muted-foreground">
48+
<p class="px-3 py-10 text-center text-sm color-muted">
4949
{loading ? 'Loading files…' : 'No files in the working directory.'}
5050
</p>
5151
)
@@ -54,9 +54,9 @@ export function Home({ ctx }: { ctx: InspectorCtx }) {
5454
{files.map(f => (
5555
<li
5656
key={f}
57-
class="flex items-center gap-2 border-b border-border px-3 py-1.5 text-sm transition-colors last:border-b-0 hover:bg-accent"
57+
class="flex items-center gap-2 border-b border-base px-3 py-1.5 text-sm transition-colors last:border-b-0 hover:bg-active"
5858
>
59-
<span class="i-ph-file-duotone shrink-0 text-muted-foreground" />
59+
<span class="i-ph-file-duotone shrink-0 color-muted" />
6060
<span class="truncate font-mono">{f}</span>
6161
</li>
6262
))}

0 commit comments

Comments
 (0)