diff --git a/AGENTS.md b/AGENTS.md index 787c778..c1debae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 `` 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-` 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 ``, 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 diff --git a/alias.ts b/alias.ts index 05744d1..369996e 100644 --- a/alias.ts +++ b/alias.ts @@ -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'), diff --git a/examples/files-inspector/package.json b/examples/files-inspector/package.json index 72a403b..eb59274 100644 --- a/examples/files-inspector/package.json +++ b/examples/files-inspector/package.json @@ -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" diff --git a/examples/files-inspector/src/client/app.tsx b/examples/files-inspector/src/client/app.tsx index 3d1f21f..a48a9e2 100644 --- a/examples/files-inspector/src/client/app.tsx +++ b/examples/files-inspector/src/client/app.tsx @@ -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' @@ -52,7 +52,7 @@ export function App() { if (!ctx) { return ( -
+
Connecting to devframe…
) @@ -62,7 +62,7 @@ export function App() { const active = route === '/about' ? '/about' : '/' return ( -
+
@@ -88,7 +88,7 @@ export function App() { - + base {basePath} · @@ -97,7 +97,7 @@ export function App() {
-
+
{active === '/about' ? : } diff --git a/examples/files-inspector/src/client/design.ts b/examples/files-inspector/src/client/design.ts new file mode 100644 index 0000000..9bedf72 --- /dev/null +++ b/examples/files-inspector/src/client/design.ts @@ -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 { + 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 = { + 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 = { + 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 = { + 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) +} diff --git a/examples/files-inspector/src/client/main.tsx b/examples/files-inspector/src/client/main.tsx index 332ca04..3d089eb 100644 --- a/examples/files-inspector/src/client/main.tsx +++ b/examples/files-inspector/src/client/main.tsx @@ -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 // (the built-in devframe plugins follow the same approach). diff --git a/examples/files-inspector/src/client/routes/about.tsx b/examples/files-inspector/src/client/routes/about.tsx index 831f895..7f3fafb 100644 --- a/examples/files-inspector/src/client/routes/about.tsx +++ b/examples/files-inspector/src/client/routes/about.tsx @@ -23,19 +23,19 @@ export function About({ ctx, basePath }: { ctx: InspectorCtx, basePath: string }

About

-

+

This page demonstrates that the SPA discovers its mount path at runtime — the same bundle works under any base path.

-
+
{rows.map(({ label, value, icon }) => (
- -
{label}
+ +
{label}
{value}
))} diff --git a/examples/files-inspector/src/client/routes/home.tsx b/examples/files-inspector/src/client/routes/home.tsx index 670d76f..babbe91 100644 --- a/examples/files-inspector/src/client/routes/home.tsx +++ b/examples/files-inspector/src/client/routes/home.tsx @@ -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([]) @@ -42,10 +42,10 @@ export function Home({ ctx }: { ctx: InspectorCtx }) {
-
+
{files.length === 0 ? ( -

+

{loading ? 'Loading files…' : 'No files in the working directory.'}

) @@ -54,9 +54,9 @@ export function Home({ ctx }: { ctx: InspectorCtx }) { {files.map(f => (
  • - + {f}
  • ))} diff --git a/examples/files-inspector/uno.config.ts b/examples/files-inspector/uno.config.ts index a838e1c..f6e6ff9 100644 --- a/examples/files-inspector/uno.config.ts +++ b/examples/files-inspector/uno.config.ts @@ -1,9 +1,36 @@ -import { presetDevframe } from '@internal/design/preset' -import { defineConfig } from 'unocss' +import { presetAnthonyDesign } from '@antfu/design/unocss' +import { + defineConfig, + presetIcons, + presetWebFonts, + presetWind4, + transformerDirectives, + transformerVariantGroup, +} 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. +// This example's Preact SPA uses `@antfu/design` directly for its semantic +// tokens, class vocabulary and Phosphor icons — matching the built-in plugins. +// The named `z-*` layers are the app's to own (the preset blocks plain `z-`). export default defineConfig({ - presets: [presetDevframe()], + presets: [ + presetAnthonyDesign({ primary: '#3a6a45' }), + presetWind4(), + presetIcons({ scale: 1.1 }), + presetWebFonts({ provider: 'none', fonts: { sans: 'DM Sans', mono: 'DM Mono' } }), + ], + transformers: [transformerDirectives(), transformerVariantGroup()], + // Wind4 leaves bare `border`/`border-b` at currentColor; restore the subtle + // shared border color (matching `border-base`) for unqualified borders. + preflights: [{ getCSS: () => '*,::before,::after{border-color:#8882}' }], + shortcuts: { + 'z-nav': 'z-[30]', + 'z-dropdown': 'z-[40]', + 'z-tooltip': 'z-[45]', + 'z-toast': 'z-[50]', + 'z-modal-backdrop': 'z-[60]', + 'z-modal-content': 'z-[70]', + 'z-drawer-backdrop': 'z-[80]', + 'z-drawer-content': 'z-[90]', + }, content: { pipeline: { include: [/\.(?:[cm]?[jt]sx?|html)($|\?)/] } }, }) diff --git a/examples/minimal-next-devframe-hub/package.json b/examples/minimal-next-devframe-hub/package.json index 56ef7a0..c5ffef4 100644 --- a/examples/minimal-next-devframe-hub/package.json +++ b/examples/minimal-next-devframe-hub/package.json @@ -11,13 +11,14 @@ "test": "vitest run --config vitest.config.ts" }, "dependencies": { + "@antfu/design": "catalog:frontend", "@devframes/hub": "workspace:*", "@devframes/plugin-a11y": "workspace:*", "@devframes/plugin-code-server": "workspace:*", "@devframes/plugin-git": "workspace:*", "@devframes/plugin-inspect": "workspace:*", "@devframes/plugin-terminals": "workspace:*", - "@internal/design": "workspace:*", + "colorjs.io": "catalog:frontend", "devframe": "workspace:*", "next": "catalog:frontend", "react": "catalog:frontend", diff --git a/examples/minimal-next-devframe-hub/src/client/app/globals.css b/examples/minimal-next-devframe-hub/src/client/app/globals.css index 4c9ee56..b8107ab 100644 --- a/examples/minimal-next-devframe-hub/src/client/app/globals.css +++ b/examples/minimal-next-devframe-hub/src/client/app/globals.css @@ -1,7 +1,7 @@ -/* The hub UI's design tokens, base layer, and component vocabulary come from the - shared devframe design system. `@unocss` is replaced by UnoCSS (via - `@unocss/postcss`, see ../postcss.config.mjs) with the generated preflights + - utilities; `@internal/design/theme.css` (imported after this file in - layout.tsx) supplies the `--df-*` token values + base element styling. */ +/* The hub UI's design tokens, base layer, and semantic class vocabulary come + from @antfu/design. `@unocss` is replaced by UnoCSS (via `@unocss/postcss`, + see ../postcss.config.mjs) with the generated preflights + utilities; + `@antfu/design/styles.css` (imported after this file in layout.tsx) supplies + the token values + base element styling. */ @unocss; diff --git a/examples/minimal-next-devframe-hub/src/client/app/layout.tsx b/examples/minimal-next-devframe-hub/src/client/app/layout.tsx index 8146901..9ba841f 100644 --- a/examples/minimal-next-devframe-hub/src/client/app/layout.tsx +++ b/examples/minimal-next-devframe-hub/src/client/app/layout.tsx @@ -1,14 +1,14 @@ import type { Metadata } from 'next' import type { ReactNode } from 'react' import './globals.css' -import '@internal/design/theme.css' +import '@antfu/design/styles.css' export const metadata: Metadata = { title: 'Minimal Next Devframe Hub', description: 'A Next.js host for the @devframes/hub protocol.', } -// Follow the OS theme before paint (@internal/design dark: is class-based). +// Follow the OS theme before paint (@antfu/design dark: is class-based). const themeScript = `(function(){try{if(window.matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.classList.add('dark')}catch(e){}})();` export default function RootLayout({ children }: { children: ReactNode }) { diff --git a/examples/minimal-next-devframe-hub/src/client/app/page.tsx b/examples/minimal-next-devframe-hub/src/client/app/page.tsx index d04ab77..54b8394 100644 --- a/examples/minimal-next-devframe-hub/src/client/app/page.tsx +++ b/examples/minimal-next-devframe-hub/src/client/app/page.tsx @@ -31,7 +31,7 @@ function DockIcon({ entry }: { entry: DevframeDockEntry }) { if (cls) return const initial = (entry.title?.[0] ?? '?').toUpperCase() - return {initial} + return {initial} } export default function Page() { @@ -142,24 +142,24 @@ export default function Page() { } } - const statusDot = status.kind === 'ready' ? 'df-dot-running' : status.kind === 'error' ? 'df-dot-error' : 'df-dot-idle' - const titleClass = 'mb2 text-[0.68rem] uppercase tracking-wider text-muted-foreground' - const rowClass = 'df-panel px2.5 py1.5 text-xs font-mono' + const statusDot = status.kind === 'ready' ? 'bg-success' : status.kind === 'error' ? 'bg-error' : 'bg-neutral-400' + const titleClass = 'mb2 text-[0.68rem] uppercase tracking-wider color-muted' + const rowClass = 'rounded-lg border border-base bg-base px2.5 py1.5 text-xs font-mono' return (

    Minimal Next Devframe Hub

    - + {status.text}

    -

    a vite-devtools-style hub on Next.js you can copy

    +

    a vite-devtools-style hub on Next.js you can copy

    -
    -
    +
    -

    Commands

    -
    • Waiting for snapshot…
    +

    Commands

    +
    • Waiting for snapshot…
    - +
    -

    Messages

    -
    • No messages yet.
    +

    Messages

    +
    • No messages yet.
    -

    Terminals

    -
    • No terminal sessions.
    +

    Terminals

    +
    • No terminal sessions.
    diff --git a/examples/minimal-vite-devframe-hub/package.json b/examples/minimal-vite-devframe-hub/package.json index 50098b1..649c0a4 100644 --- a/examples/minimal-vite-devframe-hub/package.json +++ b/examples/minimal-vite-devframe-hub/package.json @@ -10,13 +10,14 @@ "build": "vite build" }, "dependencies": { + "@antfu/design": "catalog:frontend", "@devframes/hub": "workspace:*", "@devframes/plugin-a11y": "workspace:*", "@devframes/plugin-code-server": "workspace:*", "@devframes/plugin-git": "workspace:*", "@devframes/plugin-inspect": "workspace:*", "@devframes/plugin-terminals": "workspace:*", - "@internal/design": "workspace:*", + "colorjs.io": "catalog:frontend", "devframe": "workspace:*" }, "devDependencies": { diff --git a/examples/minimal-vite-devframe-hub/src/client/main.ts b/examples/minimal-vite-devframe-hub/src/client/main.ts index a5cce68..2af62eb 100644 --- a/examples/minimal-vite-devframe-hub/src/client/main.ts +++ b/examples/minimal-vite-devframe-hub/src/client/main.ts @@ -7,7 +7,7 @@ import type { import { connectDevframe } from '@devframes/hub/client' import { iconClass } from './icons' import 'virtual:uno.css' -import '@internal/design/theme.css' +import '@antfu/design/styles.css' const HUB_BASE = '/__hub/' @@ -22,13 +22,13 @@ const iframeEl = document.querySelector('#dock-iframe')! let selectedDockId: string | null = null function setStatus(text: string, kind?: 'ready' | 'error') { - const dot = kind === 'ready' ? 'df-dot-running' : kind === 'error' ? 'df-dot-error' : 'df-dot-idle' - connEl.innerHTML = `${text}` + const dot = kind === 'ready' ? 'bg-success' : kind === 'error' ? 'bg-error' : 'bg-neutral-400' + connEl.innerHTML = `${text}` } function renderList(host: HTMLElement, items: readonly T[], render: (item: T) => string) { if (!items.length) { - host.innerHTML = '
  • empty
  • ' + host.innerHTML = '
  • empty
  • ' return } host.innerHTML = items.map(render).join('') @@ -40,7 +40,7 @@ function dockIcon(entry: DevframeDockEntry): string { if (cls) return `` const initial = (entry.title?.[0] ?? '?').toUpperCase() - return `${initial}` + return `${initial}` } function isIframeDock(d: DevframeDockEntry): d is DevframeDockEntry & { type: 'iframe', url: string } { @@ -74,7 +74,7 @@ async function main() { } renderList(docksEl, iframeDocks, d => - `
  • `) + `
  • `) const selected = iframeDocks.find(d => d.id === selectedDockId) if (selected && iframeEl.getAttribute('src') !== selected.url) @@ -101,7 +101,7 @@ async function main() { { initialValue: [] }, ) const renderCommands = () => renderList(commandsEl, commands.value() ?? [], c => - `
  • ${c.title} ${c.id}
  • `) + `
  • ${c.title} ${c.id}
  • `) commands.on('updated', renderCommands) renderCommands() @@ -113,7 +113,7 @@ async function main() { 'minimal-vite-devframe-hub:messages:list' as any, ) as DevframeMessageEntry[] renderList(messagesEl, entries, m => - `
  • [${m.level}] ${m.message}
  • `) + `
  • [${m.level}] ${m.message}
  • `) } await refreshMessages() @@ -123,7 +123,7 @@ async function main() { 'minimal-vite-devframe-hub:terminals:list' as any, ) as Pick[] renderList(terminalsEl, sessions, t => - `
  • ${t.title} ${t.id} · ${t.status}
  • `) + `
  • ${t.title} ${t.id} · ${t.status}
  • `) } await refreshTerminals() diff --git a/examples/minimal-vite-devframe-hub/uno.config.ts b/examples/minimal-vite-devframe-hub/uno.config.ts index d932d88..120a880 100644 --- a/examples/minimal-vite-devframe-hub/uno.config.ts +++ b/examples/minimal-vite-devframe-hub/uno.config.ts @@ -1,10 +1,39 @@ -import { presetDevframe } from '@internal/design/preset' -import { defineConfig } from 'unocss' +import { presetAnthonyDesign } from '@antfu/design/unocss' +import { + defineConfig, + presetIcons, + presetWebFonts, + presetWind4, + transformerDirectives, + transformerVariantGroup, +} from 'unocss' -// The hub UI extends the shared devframe design system — one preset carries the -// semantic token theme, the `df-*` component vocabulary, Phosphor icons, and the -// directive/variant-group transformers. Pair with `@internal/design/theme.css` -// (imported in `src/client/main.ts`). +// The hub UI uses `@antfu/design` directly — its preset (tuned to devframe's +// sage green) over a Wind4 base, Phosphor icons, DM Sans/Mono and the +// directive/variant-group transformers. Pair with `@antfu/design/styles.css` +// (imported in `src/client/main.ts`). The named `z-*` layers are the app's to +// own (the preset blocks plain `z-`). `.ts` is opted into extraction since +// the hub authors its class strings in vanilla `src/client/main.ts`. export default defineConfig({ - presets: [presetDevframe()], + presets: [ + presetAnthonyDesign({ primary: '#3a6a45' }), + presetWind4(), + presetIcons({ scale: 1.1 }), + presetWebFonts({ provider: 'none', fonts: { sans: 'DM Sans', mono: 'DM Mono' } }), + ], + transformers: [transformerDirectives(), transformerVariantGroup()], + // Wind4 leaves bare `border`/`border-b` at currentColor; restore the subtle + // shared border color (matching `border-base`) for unqualified borders. + preflights: [{ getCSS: () => '*,::before,::after{border-color:#8882}' }], + shortcuts: { + 'z-nav': 'z-[30]', + 'z-dropdown': 'z-[40]', + 'z-tooltip': 'z-[45]', + 'z-toast': 'z-[50]', + 'z-modal-backdrop': 'z-[60]', + 'z-modal-content': 'z-[70]', + 'z-drawer-backdrop': 'z-[80]', + 'z-drawer-content': 'z-[90]', + }, + content: { pipeline: { include: [/\.(?:[cm]?[jt]sx?|html)($|\?)/] } }, }) diff --git a/examples/next-runtime-snapshot/package.json b/examples/next-runtime-snapshot/package.json index 9537a47..4a7c664 100644 --- a/examples/next-runtime-snapshot/package.json +++ b/examples/next-runtime-snapshot/package.json @@ -17,7 +17,8 @@ "test": "vitest run" }, "dependencies": { - "@internal/design": "workspace:*", + "@antfu/design": "catalog:frontend", + "colorjs.io": "catalog:frontend", "devframe": "workspace:*", "next": "catalog:frontend", "react": "catalog:frontend", diff --git a/examples/next-runtime-snapshot/src/client/app/components/snapshot-env.tsx b/examples/next-runtime-snapshot/src/client/app/components/snapshot-env.tsx index c9fccb5..c9d2aa1 100644 --- a/examples/next-runtime-snapshot/src/client/app/components/snapshot-env.tsx +++ b/examples/next-runtime-snapshot/src/client/app/components/snapshot-env.tsx @@ -1,8 +1,8 @@ 'use client' import type { EnvSnapshot } from '../../../devframe' -import { card, input as inputClass } from '@internal/design/components' import { useCallback, useEffect, useState } from 'react' +import { card, input as inputClass } from '../design' import { useRpc } from './connect' export function SnapshotEnv() { @@ -36,7 +36,7 @@ export function SnapshotEnv() { Environment {snap && ( - + {snap.entries.length} {' / '} {snap.total} @@ -51,21 +51,21 @@ export function SnapshotEnv() { placeholder="Regex filter (case-insensitive) — e.g. NODE | PATH | HOME" aria-label="Environment variable filter (case-insensitive regex)" /> - {snap === null &&

    Loading…

    } + {snap === null &&

    Loading…

    } {snap && snap.entries.length === 0 && ( -

    +

    {loading ? 'Searching…' : 'No environment variables match this pattern.'}

    )} {snap && snap.entries.length > 0 && ( -
    +
    {snap.entries.map(entry => (
    - {entry.key} - {entry.value} + {entry.key} + {entry.value}
    ))}
    diff --git a/examples/next-runtime-snapshot/src/client/app/components/snapshot-memory.tsx b/examples/next-runtime-snapshot/src/client/app/components/snapshot-memory.tsx index e45b714..6b7ab9e 100644 --- a/examples/next-runtime-snapshot/src/client/app/components/snapshot-memory.tsx +++ b/examples/next-runtime-snapshot/src/client/app/components/snapshot-memory.tsx @@ -1,8 +1,8 @@ 'use client' import type { MemorySnapshot } from '../../../devframe' -import { button, card } from '@internal/design/components' import { useCallback, useEffect, useState } from 'react' +import { button, card } from '../design' import { useRpc } from './connect' function fmtBytes(bytes: number): string { @@ -63,21 +63,21 @@ export function SnapshotMemory() { {snap ? (
    - uptime + uptime {fmtUptime(snap.uptimeSeconds)} - rss + rss {fmtBytes(snap.memory.rss)} - heap used + heap used {fmtBytes(snap.memory.heapUsed)} - heap total + heap total {fmtBytes(snap.memory.heapTotal)} - external + external {fmtBytes(snap.memory.external)} - array buffers + array buffers {fmtBytes(snap.memory.arrayBuffers)}
    ) - :

    Loading…

    } + :

    Loading…

    } ) } diff --git a/examples/next-runtime-snapshot/src/client/app/components/snapshot-system.tsx b/examples/next-runtime-snapshot/src/client/app/components/snapshot-system.tsx index 12b1c63..26d3a34 100644 --- a/examples/next-runtime-snapshot/src/client/app/components/snapshot-system.tsx +++ b/examples/next-runtime-snapshot/src/client/app/components/snapshot-system.tsx @@ -1,8 +1,8 @@ 'use client' import type { SystemInfo } from '../../../devframe' -import { card } from '@internal/design/components' import { useEffect, useState } from 'react' +import { card } from '../design' import { useRpc } from './connect' function formatStartedAt(epoch: number): string { @@ -35,19 +35,19 @@ export function SnapshotSystem() { {info ? (
    - node + node {info.node} - platform + platform {`${info.platform} (${info.arch})`} - pid + pid {info.pid} - cwd + cwd {info.cwd} - started + started {formatStartedAt(info.startedAt)}
    ) - :

    Loading…

    } + :

    Loading…

    } ) } diff --git a/examples/next-runtime-snapshot/src/client/app/design.ts b/examples/next-runtime-snapshot/src/client/app/design.ts new file mode 100644 index 0000000..9bedf72 --- /dev/null +++ b/examples/next-runtime-snapshot/src/client/app/design.ts @@ -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 { + 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 = { + 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 = { + 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 = { + 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) +} diff --git a/examples/next-runtime-snapshot/src/client/app/globals.css b/examples/next-runtime-snapshot/src/client/app/globals.css index d395a79..df15fe0 100644 --- a/examples/next-runtime-snapshot/src/client/app/globals.css +++ b/examples/next-runtime-snapshot/src/client/app/globals.css @@ -1,6 +1,6 @@ -/* Design tokens, the base layer, and the `df-*` component vocabulary all come - from the shared devframe design system. `@unocss` is replaced by UnoCSS with - the generated preflights + utilities; `@internal/design/theme.css` (imported - after this file in layout.tsx) supplies the `--df-*` token values + base. */ +/* Design tokens, the base layer, and the semantic class vocabulary all come + from @antfu/design. `@unocss` is replaced by UnoCSS with the generated + preflights + utilities; `@antfu/design/styles.css` (imported after this file + in layout.tsx) supplies the token values + base element styling. */ @unocss; diff --git a/examples/next-runtime-snapshot/src/client/app/layout.tsx b/examples/next-runtime-snapshot/src/client/app/layout.tsx index 12324cb..d220b3a 100644 --- a/examples/next-runtime-snapshot/src/client/app/layout.tsx +++ b/examples/next-runtime-snapshot/src/client/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from 'next' import type { ReactNode } from 'react' import './globals.css' -import '@internal/design/theme.css' +import '@antfu/design/styles.css' export const metadata: Metadata = { title: 'Next Runtime Snapshot', diff --git a/examples/next-runtime-snapshot/src/client/app/page.tsx b/examples/next-runtime-snapshot/src/client/app/page.tsx index 382065b..db45197 100644 --- a/examples/next-runtime-snapshot/src/client/app/page.tsx +++ b/examples/next-runtime-snapshot/src/client/app/page.tsx @@ -1,24 +1,24 @@ 'use client' -import { nav, navBrand } from '@internal/design/components' import { RpcProvider, useRpc } from './components/connect' import { SnapshotEnv } from './components/snapshot-env' import { SnapshotMemory } from './components/snapshot-memory' import { SnapshotSystem } from './components/snapshot-system' +import { nav, navBrand } from './design' function StatusBar() { const { ctx, error } = useRpc() const dot = error - ? 'df-dot df-dot-error' + ? 'inline-block size-1.5 rounded-full shrink-0 bg-error' : ctx - ? 'df-dot df-dot-running' - : 'df-dot df-dot-idle' + ? 'inline-block size-1.5 rounded-full shrink-0 bg-success' + : 'inline-block size-1.5 rounded-full shrink-0 bg-neutral-400' return ( - + {error ? ( - + connection failed — {' '} {error} @@ -29,7 +29,7 @@ function StatusBar() { <> backend: {' '} - {ctx.base.connectionMeta.backend} + {ctx.base.connectionMeta.backend} ) : 'connecting…'} @@ -40,7 +40,7 @@ function StatusBar() { export default function Page() { return ( -
    +
    @@ -51,7 +51,7 @@ export default function Page() {
    -

    +

    devframe + Next.js App Router · live RPC into the host Node process

    diff --git a/examples/next-runtime-snapshot/uno.config.ts b/examples/next-runtime-snapshot/uno.config.ts index 775ee9d..2499c33 100644 --- a/examples/next-runtime-snapshot/uno.config.ts +++ b/examples/next-runtime-snapshot/uno.config.ts @@ -1,15 +1,42 @@ import { fileURLToPath } from 'node:url' -import { presetDevframe } from '@internal/design/preset' -import { defineConfig } from 'unocss' +import { presetAnthonyDesign } from '@antfu/design/unocss' +import { + defineConfig, + presetIcons, + presetWebFonts, + presetWind4, + transformerDirectives, + transformerVariantGroup, +} from 'unocss' -// This example extends the shared devframe design system. `@unocss/postcss` -// (see src/client/postcss.config.mjs) loads this config; the absolute glob keeps -// class extraction working regardless of the directory Next builds from. +// `@unocss/postcss` (see src/client/postcss.config.mjs) loads this config; the +// absolute glob keeps class extraction working regardless of the directory Next +// builds from. The co-located `app/design.ts` (carrying `@unocss-include`) is +// covered by the same glob. const client = fileURLToPath(new URL('./src/client', import.meta.url)) export default defineConfig({ + presets: [ + presetAnthonyDesign({ primary: '#3a6a45' }), + presetWind4(), + presetIcons({ scale: 1.1 }), + presetWebFonts({ provider: 'none', fonts: { sans: 'DM Sans', mono: 'DM Mono' } }), + ], + transformers: [transformerDirectives(), transformerVariantGroup()], + // Wind4 leaves bare `border`/`border-b` at currentColor; restore the subtle + // shared border color (matching `border-base`) for unqualified borders. + preflights: [{ getCSS: () => '*,::before,::after{border-color:#8882}' }], + shortcuts: { + 'z-nav': 'z-[30]', + 'z-dropdown': 'z-[40]', + 'z-tooltip': 'z-[45]', + 'z-toast': 'z-[50]', + 'z-modal-backdrop': 'z-[60]', + 'z-modal-content': 'z-[70]', + 'z-drawer-backdrop': 'z-[80]', + 'z-drawer-content': 'z-[90]', + }, content: { filesystem: [`${client}/app/**/*.{ts,tsx}`], }, - presets: [presetDevframe()], }) diff --git a/examples/streaming-chat/package.json b/examples/streaming-chat/package.json index d742791..60c5154 100644 --- a/examples/streaming-chat/package.json +++ b/examples/streaming-chat/package.json @@ -16,7 +16,8 @@ "test": "vitest run" }, "dependencies": { - "@internal/design": "workspace:*", + "@antfu/design": "catalog:frontend", + "colorjs.io": "catalog:frontend", "devframe": "workspace:*", "preact": "catalog:frontend" }, diff --git a/examples/streaming-chat/src/client/app.tsx b/examples/streaming-chat/src/client/app.tsx index 7c5354f..142b51c 100644 --- a/examples/streaming-chat/src/client/app.tsx +++ b/examples/streaming-chat/src/client/app.tsx @@ -1,10 +1,10 @@ import type { DevframeScopedClientContext } from 'devframe/client' import type { StreamReader } from 'devframe/utils/streaming-channel' import type { ChatHistory, ChatMessage } from '../types' -import { button, cx, input, nav, navBrand, spinner } from '@internal/design/components' import { connectDevframe } from 'devframe/client' import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' import { CHANNEL, HISTORY, NAMESPACE } from '../constants' +import { button, cx, input, nav, navBrand, spinner } from './design' type ChatCtx = DevframeScopedClientContext @@ -167,8 +167,8 @@ export function App() { if (!ctx) { return ( -
    -

    +

    +

    Connecting to devframe…

    @@ -177,13 +177,13 @@ export function App() { } return ( -
    +
    Streaming Chat -