Skip to content

Commit 3f5f1d2

Browse files
authored
feat(design): unify built-in plugins on a shared @internal/design system (#45)
1 parent 6bbe9d2 commit 3f5f1d2

60 files changed

Lines changed: 1071 additions & 1017 deletions

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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
3838
- Utility imports use the package-path form `devframe/utils/*`, never relative `../utils/*`.
3939
- Dependencies go through the pnpm catalogs in `pnpm-workspace.yaml` (`cli`, `inlined`, `testing`, `types`) — add to a catalog and reference as `catalog:<name>`, don't pin versions in `package.json`.
4040

41+
### Design system
42+
43+
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.
44+
45+
- **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.
46+
- **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.
47+
- **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.
48+
- **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.
49+
- **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.
50+
- **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.
51+
- **Plain `.ts`/vanilla views** must opt `.ts` into UnoCSS extraction (`content.pipeline.include`), since UnoCSS only scans framework files by default.
52+
4153
### Devframe design principles
4254

4355
These reinforce devframe's positioning as "the container for one devtool integration, portable to multiple viewers". When in doubt, err on the side of "devframe provides primitives, the hub provides UX".

alias.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ 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'),
4853
'@devframes/plugin-code-server/client': p('code-server/src/client/index.ts'),
4954
'@devframes/plugin-code-server/node': p('code-server/src/node/index.ts'),
5055
'@devframes/plugin-code-server/constants': p('code-server/src/constants.ts'),
@@ -70,11 +75,14 @@ export const alias = {
7075
'@devframes/plugin-inspect': p('inspect/src/index.ts'),
7176
}
7277

73-
// update tsconfig.base.json
78+
// update tsconfig.base.json — CSS aliases exist for Vite resolution only;
79+
// TypeScript resolves `*.css` side-effect imports through ambient shims.
7480
const raw = fs.readFileSync(join(root, 'tsconfig.base.json'), 'utf-8').trim()
7581
const tsconfig = JSON.parse(raw)
7682
tsconfig.compilerOptions.paths = Object.fromEntries(
77-
Object.entries(alias).map(([key, value]) => [key, [`./${relative(root, value)}`]]),
83+
Object.entries(alias)
84+
.filter(([key]) => !key.endsWith('.css'))
85+
.map(([key, value]) => [key, [`./${relative(root, value)}`]]),
7886
)
7987
const newRaw = JSON.stringify(tsconfig, null, 2)
8088
if (newRaw !== raw)

packages/design/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "@internal/design",
3+
"type": "module",
4+
"version": "0.5.4",
5+
"private": true,
6+
"description": "Internal, unpublished design system for devframe's built-in plugins — one UnoCSS preset, one token set, one shared component vocabulary, portable across frameworks. Consumed directly from source.",
7+
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
8+
"license": "MIT",
9+
"sideEffects": [
10+
"**/*.css"
11+
],
12+
"exports": {
13+
".": "./src/index.ts",
14+
"./preset": "./src/preset.ts",
15+
"./components": "./src/components.ts",
16+
"./tokens": "./src/tokens.ts",
17+
"./theme.css": "./src/theme.css",
18+
"./package.json": "./package.json"
19+
},
20+
"types": "./src/index.ts",
21+
"scripts": {
22+
"typecheck": "tsc --noEmit"
23+
},
24+
"peerDependencies": {
25+
"unocss": "^66.0.0"
26+
},
27+
"dependencies": {
28+
"@iconify-json/ph": "catalog:frontend"
29+
},
30+
"devDependencies": {
31+
"unocss": "catalog:frontend"
32+
}
33+
}

packages/design/src/components.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* Shared component recipes.
3+
*
4+
* These framework-neutral builders are the devframe "components": each returns
5+
* the canonical `df-*` class string for an element, so React (`className=`),
6+
* Svelte (`class=`) and vanilla DOM (`el.className =`) all describe the same
7+
* button, panel, tab or nav the same way and render identically.
8+
*
9+
* Because the class strings are assembled at runtime, the `df-*` vocabulary is
10+
* safelisted by the preset (see {@link DF_SAFELIST}) so UnoCSS always emits it
11+
* regardless of static extraction.
12+
*/
13+
14+
/** Join truthy class fragments into a single class string. */
15+
export function cx(...parts: Array<string | false | null | undefined>): string {
16+
return parts.filter(Boolean).join(' ')
17+
}
18+
19+
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link'
20+
export type ButtonSize = 'md' | 'sm' | 'lg' | 'icon' | 'icon-sm'
21+
22+
export interface ButtonProps {
23+
variant?: ButtonVariant
24+
size?: ButtonSize
25+
/** Extra classes appended after the recipe. */
26+
class?: string
27+
}
28+
29+
/** A button. `df-btn` + a variant, optionally a non-default size. */
30+
export function button({ variant = 'primary', size = 'md', class: extra }: ButtonProps = {}): string {
31+
return cx('df-btn', `df-btn-${variant}`, size !== 'md' && `df-btn-${size}`, extra)
32+
}
33+
34+
export type BadgeVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline'
35+
36+
export interface BadgeProps {
37+
variant?: BadgeVariant
38+
class?: string
39+
}
40+
41+
/** A solid/semantic badge (the variant class already includes the `df-badge` base). */
42+
export function badge({ variant = 'secondary', class: extra }: BadgeProps = {}): string {
43+
return cx(`df-badge-${variant}`, extra)
44+
}
45+
46+
/** A soft, palette-driven tag (`df-tag-blue`, `df-tag-amber`, …). */
47+
export function tag(color: string, extra?: string): string {
48+
return cx(`df-tag-${color}`, extra)
49+
}
50+
51+
/** The container for a set of segmented tabs. */
52+
export function tabsList(extra?: string): string {
53+
return cx('df-tabs-list', extra)
54+
}
55+
56+
/** A segmented tab (active state is driven by `data-state="active"` on the element). */
57+
export function tab(extra?: string): string {
58+
return cx('df-tab', extra)
59+
}
60+
61+
export interface NavTabProps {
62+
active?: boolean
63+
class?: string
64+
}
65+
66+
/** A closeable navigation tab (terminal sessions, open documents, …). */
67+
export function navTab({ active = false, class: extra }: NavTabProps = {}): string {
68+
return cx('df-navtab', active && 'df-navtab-active', extra)
69+
}
70+
71+
/** A top navigation bar. */
72+
export function nav(extra?: string): string {
73+
return cx('df-nav', extra)
74+
}
75+
76+
/** A secondary toolbar bar. */
77+
export function toolbar(extra?: string): string {
78+
return cx('df-toolbar', extra)
79+
}
80+
81+
/** A card surface. */
82+
export function card(extra?: string): string {
83+
return cx('df-card', extra)
84+
}
85+
86+
/** A flat panel surface. */
87+
export function panel(extra?: string): string {
88+
return cx('df-panel', extra)
89+
}
90+
91+
/** A text input / textarea. */
92+
export function input(extra?: string): string {
93+
return cx('df-input', extra)
94+
}
95+
96+
/** An inline link. */
97+
export function link(extra?: string): string {
98+
return cx('df-link', extra)
99+
}
100+
101+
export type DotState = 'running' | 'idle' | 'error'
102+
103+
/** A status dot for a lifecycle state. */
104+
export function dot(state: DotState, extra?: string): string {
105+
return cx('df-dot', `df-dot-${state}`, extra)
106+
}
107+
108+
/** An indeterminate spinner. */
109+
export function spinner(extra?: string): string {
110+
return cx('df-spinner', extra)
111+
}
112+
113+
/**
114+
* The full fixed `df-*` vocabulary. The preset safelists this so the runtime
115+
* builders above always have CSS to resolve to, even though their class strings
116+
* never appear literally in scanned source. A representative set of palette
117+
* tags is included for {@link tag}.
118+
*/
119+
export const DF_SAFELIST: string[] = [
120+
// buttons
121+
'df-btn',
122+
'df-btn-primary',
123+
'df-btn-secondary',
124+
'df-btn-outline',
125+
'df-btn-ghost',
126+
'df-btn-destructive',
127+
'df-btn-link',
128+
'df-btn-sm',
129+
'df-btn-lg',
130+
'df-btn-icon',
131+
'df-btn-icon-sm',
132+
// badges
133+
'df-badge',
134+
'df-badge-primary',
135+
'df-badge-secondary',
136+
'df-badge-success',
137+
'df-badge-warning',
138+
'df-badge-destructive',
139+
'df-badge-outline',
140+
// tabs + bars
141+
'df-tabs-list',
142+
'df-tab',
143+
'df-navtab',
144+
'df-navtab-active',
145+
'df-nav',
146+
'df-toolbar',
147+
// surfaces + controls
148+
'df-card',
149+
'df-panel',
150+
'df-input',
151+
'df-link',
152+
// status
153+
'df-dot',
154+
'df-dot-running',
155+
'df-dot-idle',
156+
'df-dot-error',
157+
'df-spinner',
158+
// common palette tags
159+
'df-tag-blue',
160+
'df-tag-amber',
161+
'df-tag-green',
162+
'df-tag-red',
163+
'df-tag-sky',
164+
'df-tag-violet',
165+
'df-tag-rose',
166+
]

packages/design/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export * from './components'
2+
export { presetDevframe, shortcuts } from './preset'
3+
export type { PresetDevframeOptions } from './preset'
4+
export { presetDevframe as default } from './preset'
5+
export {
6+
cssVar,
7+
PAIRED_TOKENS,
8+
radius,
9+
SOLO_TOKENS,
10+
TOKEN_PREFIX,
11+
tokenColors,
12+
} from './tokens'
13+
14+
export type { DesignToken, PairedToken, SoloToken } from './tokens'

0 commit comments

Comments
 (0)