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
20 changes: 11 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co

### Design system

All five built-in plugins 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). 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.

- **One preset to extend.** Each plugin'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 plugin.
- **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 in a plugin. 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-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`, `badge`, `tab`, `navTab`, `nav`, `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 (`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.
- **Icons** come from the shared Phosphor set (`i-ph-*`, duotone preferred) via `presetIcons` — use them across every plugin instead of per-plugin icon libraries or bespoke SVG.
- **A plugin 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.
- **Plain `.ts`/vanilla views** must opt `.ts` into UnoCSS extraction (`content.pipeline.include`), since UnoCSS only scans framework files by default.
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.

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

### Devframe design principles

Expand Down
2 changes: 2 additions & 0 deletions examples/files-inspector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"test": "vitest run"
},
"dependencies": {
"@internal/design": "workspace:*",
"devframe": "workspace:*",
"preact": "catalog:frontend",
"tinyglobby": "catalog:deps"
Expand All @@ -24,6 +25,7 @@
"@preact/preset-vite": "catalog:build",
"get-port-please": "catalog:deps",
"h3": "catalog:deps",
"unocss": "catalog:frontend",
"vite": "catalog:build",
"vitest": "catalog:testing",
"ws": "catalog:deps"
Expand Down
92 changes: 54 additions & 38 deletions examples/files-inspector/src/client/app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
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 { About } from './routes/about'
Expand All @@ -7,6 +8,11 @@ import { Home } from './routes/home'
const NAMESPACE = 'devframe-files-inspector'
export type InspectorCtx = DevframeScopedClientContext<typeof NAMESPACE>

const NAV_ITEMS = [
{ route: '/', label: 'Home', icon: 'i-ph-house-duotone' },
{ route: '/about', label: 'About', icon: 'i-ph-info-duotone' },
] as const

function getBasePath(): string {
return new URL(document.baseURI).pathname
}
Expand Down Expand Up @@ -44,48 +50,58 @@ export function App() {
setRoute(to)
}

if (!ctx)
return <p>Connecting to devframe…</p>
if (!ctx) {
return (
<div class="grid min-h-screen place-items-center bg-background text-muted-foreground font-sans text-sm">
Connecting to devframe…
</div>
)
}

// Any non-/about route resolves to Home, mirroring the route switch below.
const active = route === '/about' ? '/about' : '/'

return (
<main>
<header>
<h1>Files Inspector</h1>
<nav>
<a
href={basePath}
onClick={(e) => {
e.preventDefault()
navigate('/')
}}
>
Home
</a>
{' · '}
<a
href={`${basePath}about`}
onClick={(e) => {
e.preventDefault()
navigate('/about')
}}
>
About
</a>
<div class="flex flex-col min-h-screen bg-background text-foreground font-sans">
<header class={nav()}>
<span class={navBrand()}>
<span class="i-ph-folder-duotone text-base color-active" />
<span>Files Inspector</span>
</span>

<nav class={tabsList()} role="tablist" aria-label="Views">
{NAV_ITEMS.map(({ route: r, label, icon }) => (
<button
key={r}
type="button"
role="tab"
aria-selected={active === r}
data-state={active === r ? 'active' : 'inactive'}
class={tabClass()}
onClick={() => navigate(r)}
>
<span class={icon} />
{label}
</button>
))}
</nav>
<small>
base:
{' '}
<code>{basePath}</code>
{' | '}
backend:
{' '}
<code>{ctx.base.connectionMeta.backend}</code>

<span class="flex-1" />

<small class="flex items-center gap-1.5 text-muted-foreground text-xs font-mono">
<span>base</span>
<code class="color-base">{basePath}</code>
<span class="op-mute">·</span>
<span>backend</span>
<code class="color-base">{ctx.base.connectionMeta.backend}</code>
</small>
</header>
<hr />
{route === '/about'
? <About ctx={ctx} basePath={basePath} />
: <Home ctx={ctx} />}
</main>

<main class="scrollbar-slim min-h-0 flex-1 overflow-auto">
{active === '/about'
? <About ctx={ctx} basePath={basePath} />
: <Home ctx={ctx} />}
</main>
</div>
)
}
12 changes: 12 additions & 0 deletions examples/files-inspector/src/client/main.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { render } from 'preact'
import { App } from './app'
import 'virtual:uno.css'
import '@internal/design/theme.css'

// Shared design tokens flip on the `.dark` class; mirror the OS preference onto
// <html> (the built-in devframe plugins follow the same approach).
const mq = window.matchMedia('(prefers-color-scheme: dark)')
function applyScheme(d: boolean) {
document.documentElement.classList.toggle('dark', d)
document.documentElement.classList.toggle('light', !d)
}
applyScheme(mq.matches)
mq.addEventListener('change', e => applyScheme(e.matches))

const root = document.getElementById('app')
if (!root)
Expand Down
39 changes: 27 additions & 12 deletions examples/files-inspector/src/client/routes/about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,35 @@ export function About({ ctx, basePath }: { ctx: InspectorCtx, basePath: string }
})
}, [ctx])

const rows = [
{ label: 'Resolved base path', value: basePath, icon: 'i-ph-path-duotone' },
{ label: 'Server cwd', value: cwd || '…', icon: 'i-ph-folder-duotone' },
{ label: 'RPC backend', value: ctx.base.connectionMeta.backend, icon: 'i-ph-plugs-connected-duotone' },
]

return (
<section>
<h2>About</h2>
<p>
This page demonstrates that the SPA discovers its mount path at
runtime — the same bundle works under any base path.
<section class="mx-auto flex w-full max-w-3xl flex-col gap-4 p-4">
<div class="flex items-center gap-2">
<span class="i-ph-info-duotone text-lg color-active" />
<h2 class="text-base font-semibold">About</h2>
</div>

<p class="text-sm text-muted-foreground">
This page demonstrates that the SPA discovers its mount path at runtime —
the same bundle works under any base path.
</p>
<dl>
<dt>Resolved base path</dt>
<dd><code>{basePath}</code></dd>
<dt>Server cwd</dt>
<dd><code>{cwd || '…'}</code></dd>
<dt>RPC backend</dt>
<dd><code>{ctx.base.connectionMeta.backend}</code></dd>

<dl class="overflow-hidden rounded-md border border-border bg-card text-card-foreground">
{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"
>
<span class={`${icon} shrink-0 text-muted-foreground`} />
<dt class="w-40 shrink-0 text-sm text-muted-foreground">{label}</dt>
<dd class="m-0 min-w-0 flex-1 truncate font-mono text-sm">{value}</dd>
</div>
))}
</dl>
</section>
)
Expand Down
55 changes: 40 additions & 15 deletions examples/files-inspector/src/client/routes/home.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { InspectorCtx } from '../app'
import { badge, button } from '@internal/design/components'
import { useEffect, useState } from 'preact/hooks'

export function Home({ ctx }: { ctx: InspectorCtx }) {
Expand All @@ -22,22 +23,46 @@ export function Home({ ctx }: { ctx: InspectorCtx }) {
}, [])

return (
<section>
<h2>
Files
{' '}
<small>
(
<section class="mx-auto flex w-full max-w-3xl flex-col gap-4 p-4">
<div class="flex items-center gap-2">
<span class="i-ph-files-duotone text-lg color-active" />
<h2 class="text-base font-semibold">Files</h2>
<span class={badge({ variant: 'secondary', class: 'font-mono tabular-nums' })}>
{files.length}
)
</small>
</h2>
<button onClick={refresh} disabled={loading}>
{loading ? 'Loading…' : 'Refresh'}
</button>
<ul>
{files.map(f => <li key={f}>{f}</li>)}
</ul>
</span>
<span class="flex-1" />
<button
type="button"
class={button({ variant: 'outline', size: 'sm' })}
onClick={refresh}
disabled={loading}
>
<span class={loading ? 'i-ph-arrows-clockwise animate-spin' : 'i-ph-arrows-clockwise'} />
{loading ? 'Loading…' : 'Refresh'}
</button>
</div>

<div class="overflow-hidden rounded-md border border-border bg-card text-card-foreground">
{files.length === 0
? (
<p class="px-3 py-10 text-center text-sm text-muted-foreground">
{loading ? 'Loading files…' : 'No files in the working directory.'}
</p>
)
: (
<ul>
{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"
>
<span class="i-ph-file-duotone shrink-0 text-muted-foreground" />
<span class="truncate font-mono">{f}</span>
</li>
))}
</ul>
)}
</div>
</section>
)
}
3 changes: 3 additions & 0 deletions examples/files-inspector/src/client/shims.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Side-effect style imports used by the Preact SPA entry.
declare module '*.css' {}
declare module 'virtual:uno.css' {}
3 changes: 2 additions & 1 deletion examples/files-inspector/src/client/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { fileURLToPath } from 'node:url'
import preact from '@preact/preset-vite'
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
import { alias } from '../../../../alias'

export default defineConfig({
base: './',
root: fileURLToPath(new URL('.', import.meta.url)),
resolve: { alias },
plugins: [preact()],
plugins: [UnoCSS(), preact()],
build: {
outDir: fileURLToPath(new URL('../../dist/client', import.meta.url)),
emptyOutDir: true,
Expand Down
9 changes: 9 additions & 0 deletions examples/files-inspector/uno.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { presetDevframe } from '@internal/design/preset'
import { defineConfig } from 'unocss'

// This example's Preact SPA extends the shared devframe design system for its
// tokens, `df-*` vocabulary and Phosphor icons — matching the built-in plugins.
export default defineConfig({
presets: [presetDevframe()],
content: { pipeline: { include: [/\.(?:[cm]?[jt]sx?|html)($|\?)/] } },
})
3 changes: 3 additions & 0 deletions examples/next-runtime-snapshot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"test": "vitest run"
},
"dependencies": {
"@internal/design": "workspace:*",
"devframe": "workspace:*",
"next": "catalog:frontend",
"react": "catalog:frontend",
Expand All @@ -25,8 +26,10 @@
"devDependencies": {
"@types/react": "catalog:types",
"@types/react-dom": "catalog:types",
"@unocss/postcss": "catalog:frontend",
"get-port-please": "catalog:deps",
"h3": "catalog:deps",
"unocss": "catalog:frontend",
"vitest": "catalog:testing",
"ws": "catalog:deps"
}
Expand Down
Loading
Loading