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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co

### Design system

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.
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.

- **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 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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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).

### Devframe design principles

Expand Down
5 changes: 0 additions & 5 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,6 @@ export const alias = {
'@devframes/hub': r('hub/src/index.ts'),
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
'@devframes/nuxt': r('nuxt/src/index.ts'),
'@internal/design/preset': r('design/src/preset.ts'),
'@internal/design/components': r('design/src/components.ts'),
'@internal/design/tokens': r('design/src/tokens.ts'),
'@internal/design/theme.css': r('design/src/theme.css'),
'@internal/design': r('design/src/index.ts'),
'@devframes/plugin-code-server/client': p('code-server/src/client/index.ts'),
'@devframes/plugin-code-server/node': p('code-server/src/node/index.ts'),
'@devframes/plugin-code-server/constants': p('code-server/src/constants.ts'),
Expand Down
3 changes: 2 additions & 1 deletion examples/files-inspector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"test": "vitest run"
},
"dependencies": {
"@internal/design": "workspace:*",
"@antfu/design": "catalog:frontend",
"colorjs.io": "catalog:frontend",
"devframe": "workspace:*",
"preact": "catalog:frontend",
"tinyglobby": "catalog:deps"
Expand Down
10 changes: 5 additions & 5 deletions examples/files-inspector/src/client/app.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { DevframeScopedClientContext } from 'devframe/client'
import { nav, navBrand, tab as tabClass, tabsList } from '@internal/design/components'
import { connectDevframe } from 'devframe/client'
import { useEffect, useState } from 'preact/hooks'
import { nav, navBrand, tab as tabClass, tabsList } from './design'
import { About } from './routes/about'
import { Home } from './routes/home'

Expand Down Expand Up @@ -52,7 +52,7 @@ export function App() {

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

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

<span class="flex-1" />

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

<main class="scrollbar-slim min-h-0 flex-1 overflow-auto">
<main class="min-h-0 flex-1 overflow-auto">
{active === '/about'
? <About ctx={ctx} basePath={basePath} />
: <Home ctx={ctx} />}
Expand Down
122 changes: 122 additions & 0 deletions examples/files-inspector/src/client/design.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// @unocss-include
// Co-located devframe -> @antfu/design class helpers: framework-neutral builders
// returning @antfu/design's semantic shortcut classes, so this surface looks
// identical to the antfu Vue components. The `@unocss-include` marker makes
// UnoCSS emit the runtime-assembled class chains below.
// Tag palette kept literal for extraction: badge-color-blue badge-color-amber
// badge-color-green badge-color-red badge-color-sky badge-color-violet
// badge-color-rose badge-color-teal badge-color-orange badge-color-emerald

export function cx(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(' ')
}

export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link'
export type ButtonSize = 'md' | 'sm' | 'lg'
export interface ButtonProps { variant?: ButtonVariant, size?: ButtonSize, class?: string }

export function button({ variant = 'primary', size = 'md', class: extra }: ButtonProps = {}): string {
const variantClass: Record<ButtonVariant, string> = {
primary: 'btn-primary',
secondary: 'btn-action',
outline: 'btn-action',
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!',
destructive: 'btn-action text-error border-error/30!',
link: 'inline-flex items-center gap-1.5 color-active hover:underline underline-offset-2',
}
const sizeClass = size === 'sm'
? (variant === 'primary' ? 'text-sm px-2.5! py-1!' : 'text-sm')
: size === 'lg' ? 'text-base px-4! py-2!' : ''
return cx(variantClass[variant], sizeClass, extra)
}

export type IconButtonVariant = 'outline' | 'ghost'
export type IconButtonSize = 'md' | 'sm'
export interface IconButtonProps { variant?: IconButtonVariant, size?: IconButtonSize, class?: string }

export function iconButton({ variant = 'outline', size = 'md', class: extra }: IconButtonProps = {}): string {
const base = variant === 'ghost' ? 'btn-icon' : 'btn-icon-square'
const sizeClass = size === 'sm' ? 'w-7! h-7! text-sm' : ''
return cx(base, sizeClass, extra)
}

export type BadgeVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline'
export interface BadgeProps { variant?: BadgeVariant, class?: string }

export function badge({ variant = 'secondary', class: extra }: BadgeProps = {}): string {
const variantClass: Record<BadgeVariant, string> = {
primary: 'badge-active',
secondary: 'badge-muted',
success: 'badge badge-color-green',
warning: 'badge badge-color-amber',
destructive: 'badge badge-color-red',
outline: 'badge border border-base',
}
return cx(variantClass[variant], extra)
}

export function tag(color: string, extra?: string): string {
return cx('badge', `badge-color-${color}`, extra)
}

export function tabsList(extra?: string): string {
return cx('inline-flex items-center gap-1 p-1 rounded-lg bg-secondary w-max', extra)
}

export function tab(extra?: string): string {
return cx(
'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',
extra,
)
}

export interface NavTabProps { active?: boolean, class?: string }
export function navTab({ active = false, class: extra }: NavTabProps = {}): string {
return cx(
'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',
active && 'op100! bg-active border-base! color-base',
extra,
)
}

export function nav(extra?: string): string {
return cx('flex items-center gap-2 shrink-0 h-10 px-3 border-b border-base bg-base z-nav', extra)
}

export function navBrand(extra?: string): string {
return cx('flex items-center gap-1.5 shrink-0 font-semibold text-sm select-none', extra)
}

export function toolbar(extra?: string): string {
return cx('flex items-center gap-2 shrink-0 h-8 px-2.5 border-b border-base bg-secondary text-sm', extra)
}

export function card(extra?: string): string {
return cx('flex flex-col rounded-xl border border-base bg-base shadow-sm', extra)
}

export function panel(extra?: string): string {
return cx('rounded-lg border border-base bg-base', extra)
}

export function input(extra?: string): string {
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)
}

export function link(extra?: string): string {
return cx('color-active hover:underline underline-offset-2', extra)
}

export type DotState = 'running' | 'idle' | 'error'
export function dot(state: DotState, extra?: string): string {
const stateClass: Record<DotState, string> = {
running: 'bg-success',
idle: 'bg-neutral-400',
error: 'bg-error',
}
return cx('inline-block size-1.5 rounded-full shrink-0', stateClass[state], extra)
}

export function spinner(extra?: string): string {
return cx('inline-block size-4 rounded-full border-2 border-current border-t-transparent animate-spin', extra)
}
2 changes: 1 addition & 1 deletion examples/files-inspector/src/client/main.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { render } from 'preact'
import { App } from './app'
import 'virtual:uno.css'
import '@internal/design/theme.css'
import '@antfu/design/styles.css'

// Shared design tokens flip on the `.dark` class; mirror the OS preference onto
// <html> (the built-in devframe plugins follow the same approach).
Expand Down
10 changes: 5 additions & 5 deletions examples/files-inspector/src/client/routes/about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ export function About({ ctx, basePath }: { ctx: InspectorCtx, basePath: string }
<h2 class="text-base font-semibold">About</h2>
</div>

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

<dl class="overflow-hidden rounded-md border border-border bg-card text-card-foreground">
<dl class="overflow-hidden rounded-md border border-base bg-base color-base">
{rows.map(({ label, value, icon }) => (
<div
key={label}
class="flex items-center gap-3 border-b border-border px-3 py-2.5 last:border-b-0"
class="flex items-center gap-3 border-b border-base px-3 py-2.5 last:border-b-0"
>
<span class={`${icon} shrink-0 text-muted-foreground`} />
<dt class="w-40 shrink-0 text-sm text-muted-foreground">{label}</dt>
<span class={`${icon} shrink-0 color-muted`} />
<dt class="w-40 shrink-0 text-sm color-muted">{label}</dt>
<dd class="m-0 min-w-0 flex-1 truncate font-mono text-sm">{value}</dd>
</div>
))}
Expand Down
10 changes: 5 additions & 5 deletions examples/files-inspector/src/client/routes/home.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { InspectorCtx } from '../app'
import { badge, button } from '@internal/design/components'
import { useEffect, useState } from 'preact/hooks'
import { badge, button } from '../design'

export function Home({ ctx }: { ctx: InspectorCtx }) {
const [files, setFiles] = useState<string[]>([])
Expand Down Expand Up @@ -42,10 +42,10 @@ export function Home({ ctx }: { ctx: InspectorCtx }) {
</button>
</div>

<div class="overflow-hidden rounded-md border border-border bg-card text-card-foreground">
<div class="overflow-hidden rounded-md border border-base bg-base color-base">
{files.length === 0
? (
<p class="px-3 py-10 text-center text-sm text-muted-foreground">
<p class="px-3 py-10 text-center text-sm color-muted">
{loading ? 'Loading files…' : 'No files in the working directory.'}
</p>
)
Expand All @@ -54,9 +54,9 @@ export function Home({ ctx }: { ctx: InspectorCtx }) {
{files.map(f => (
<li
key={f}
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"
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"
>
<span class="i-ph-file-duotone shrink-0 text-muted-foreground" />
<span class="i-ph-file-duotone shrink-0 color-muted" />
<span class="truncate font-mono">{f}</span>
</li>
))}
Expand Down
Loading
Loading