Skip to content
Closed
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
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
- Utility imports use the package-path form `devframe/utils/*`, never relative `../utils/*`.
- 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`.

### Design system

The built-in plugins share one design system, `@internal/design`, so they look and feel like one product across frameworks (the Git dashboard is React/Next, terminals is Svelte, code-server is vanilla DOM). It's an optional package plugins opt into — devframe core stays headless and ships no styling.

- **One preset to extend.** Each plugin's `uno.config.ts` is just `presets: [presetDevframe()]` (imported from `@internal/design/preset`). The preset bundles `presetWind4` + `presetIcons` + the directive/variant-group transformers, the semantic token theme, and the shared `df-*` component shortcuts. 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.
- **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. Non-UnoCSS consumers can link the prebuilt `@internal/design/style.css`.
- **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=`) and vanilla DOM. 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.
- **Plain `.ts`/vanilla views** must opt `.ts` into UnoCSS extraction (`content.pipeline.include`), since UnoCSS only scans framework files by default.

### Devframe design principles

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".
Expand Down
12 changes: 10 additions & 2 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ 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 All @@ -65,11 +70,14 @@ export const alias = {
'devframe': r('devframe/src'),
}

// update tsconfig.base.json
// update tsconfig.base.json — CSS aliases exist for Vite resolution only;
// TypeScript resolves `*.css` side-effect imports through ambient shims.
const raw = fs.readFileSync(join(root, 'tsconfig.base.json'), 'utf-8').trim()
const tsconfig = JSON.parse(raw)
tsconfig.compilerOptions.paths = Object.fromEntries(
Object.entries(alias).map(([key, value]) => [key, [`./${relative(root, value)}`]]),
Object.entries(alias)
.filter(([key]) => !key.endsWith('.css'))
.map(([key, value]) => [key, [`./${relative(root, value)}`]]),
)
const newRaw = JSON.stringify(tsconfig, null, 2)
if (newRaw !== raw)
Expand Down
43 changes: 43 additions & 0 deletions packages/design/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@internal/design",
"type": "module",
"version": "0.5.4",
"private": true,
"description": "Internal, unpublished design system for devframe's built-in plugins — one UnoCSS preset, one token set, one shared component vocabulary, portable across frameworks.",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
"sideEffects": [
"**/*.css"
],
"exports": {
".": "./dist/index.mjs",
"./preset": "./dist/preset.mjs",
"./components": "./dist/components.mjs",
"./tokens": "./dist/tokens.mjs",
"./theme.css": "./dist/theme.css",
"./style.css": "./dist/style.css",
"./package.json": "./package.json"
},
"types": "./dist/index.d.mts",
"files": [
"dist"
],
"scripts": {
"build": "tsdown && tsx scripts/build-css.ts",
"watch": "tsdown --watch",
"typecheck": "tsc --noEmit",
"prepack": "pnpm run build"
},
"peerDependencies": {
"unocss": "^66.0.0"
},
"dependencies": {
"@iconify-json/ph": "catalog:frontend"
},
"devDependencies": {
"@types/node": "catalog:types",
"tsdown": "catalog:build",
"tsx": "catalog:build",
"unocss": "catalog:frontend"
}
}
48 changes: 48 additions & 0 deletions packages/design/scripts/build-css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { createGenerator } from 'unocss'
import { presetDevframe, shortcuts } from '../src/preset'

/**
* Emit the design package's shipped CSS:
*
* - `dist/theme.css` — the `--df-*` token values + base layer, verbatim.
* - `dist/style.css` — a standalone stylesheet (UnoCSS preflights + every
* `df-*` component class + the tokens) for consumers that don't run UnoCSS
* themselves but still want the shared component vocabulary.
*/

const root = fileURLToPath(new URL('..', import.meta.url))
const distDir = resolve(root, 'dist')

async function main(): Promise<void> {
await mkdir(distDir, { recursive: true })

const theme = await readFile(resolve(root, 'src/theme.css'), 'utf8')
await writeFile(resolve(distDir, 'theme.css'), theme)

// Materialize every named `df-*` shortcut (and a few palette tags) so the
// standalone stylesheet carries the full shared component vocabulary.
const names = new Set<string>()
for (const entry of shortcuts) {
if (Array.isArray(entry))
continue
for (const key of Object.keys(entry))
names.add(key)
}
for (const color of ['blue', 'amber', 'green', 'red', 'sky', 'violet', 'rose'])
names.add(`df-tag-${color}`)

const generator = await createGenerator({ presets: [presetDevframe()] })
const { css } = await generator.generate(names, { preflights: true })

const banner = '/* @internal/design — prebuilt stylesheet. Generated by scripts/build-css.ts; do not edit. */\n'
await writeFile(resolve(distDir, 'style.css'), `${banner}${css}\n${theme}`)
}

main().catch((error) => {
console.error(error)
process.exit(1)
})
166 changes: 166 additions & 0 deletions packages/design/src/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* Shared component recipes.
*
* These framework-neutral builders are the devframe "components": each returns
* the canonical `df-*` class string for an element, so React (`className=`),
* Svelte (`class=`) and vanilla DOM (`el.className =`) all describe the same
* button, panel, tab or nav the same way and render identically.
*
* Because the class strings are assembled at runtime, the `df-*` vocabulary is
* safelisted by the preset (see {@link DF_SAFELIST}) so UnoCSS always emits it
* regardless of static extraction.
*/

/** Join truthy class fragments into a single class string. */
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' | 'icon' | 'icon-sm'

export interface ButtonProps {
variant?: ButtonVariant
size?: ButtonSize
/** Extra classes appended after the recipe. */
class?: string
}

/** A button. `df-btn` + a variant, optionally a non-default size. */
export function button({ variant = 'primary', size = 'md', class: extra }: ButtonProps = {}): string {
return cx('df-btn', `df-btn-${variant}`, size !== 'md' && `df-btn-${size}`, extra)
}

export type BadgeVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline'

export interface BadgeProps {
variant?: BadgeVariant
class?: string
}

/** A solid/semantic badge (the variant class already includes the `df-badge` base). */
export function badge({ variant = 'secondary', class: extra }: BadgeProps = {}): string {
return cx(`df-badge-${variant}`, extra)
}

/** A soft, palette-driven tag (`df-tag-blue`, `df-tag-amber`, …). */
export function tag(color: string, extra?: string): string {
return cx(`df-tag-${color}`, extra)
}

/** The container for a set of segmented tabs. */
export function tabsList(extra?: string): string {
return cx('df-tabs-list', extra)
}

/** A segmented tab (active state is driven by `data-state="active"` on the element). */
export function tab(extra?: string): string {
return cx('df-tab', extra)
}

export interface NavTabProps {
active?: boolean
class?: string
}

/** A closeable navigation tab (terminal sessions, open documents, …). */
export function navTab({ active = false, class: extra }: NavTabProps = {}): string {
return cx('df-navtab', active && 'df-navtab-active', extra)
}

/** A top navigation bar. */
export function nav(extra?: string): string {
return cx('df-nav', extra)
}

/** A secondary toolbar bar. */
export function toolbar(extra?: string): string {
return cx('df-toolbar', extra)
}

/** A card surface. */
export function card(extra?: string): string {
return cx('df-card', extra)
}

/** A flat panel surface. */
export function panel(extra?: string): string {
return cx('df-panel', extra)
}

/** A text input / textarea. */
export function input(extra?: string): string {
return cx('df-input', extra)
}

/** An inline link. */
export function link(extra?: string): string {
return cx('df-link', extra)
}

export type DotState = 'running' | 'idle' | 'error'

/** A status dot for a lifecycle state. */
export function dot(state: DotState, extra?: string): string {
return cx('df-dot', `df-dot-${state}`, extra)
}

/** An indeterminate spinner. */
export function spinner(extra?: string): string {
return cx('df-spinner', extra)
}

/**
* The full fixed `df-*` vocabulary. The preset safelists this so the runtime
* builders above always have CSS to resolve to, even though their class strings
* never appear literally in scanned source. A representative set of palette
* tags is included for {@link tag}.
*/
export const DF_SAFELIST: string[] = [
// buttons
'df-btn',
'df-btn-primary',
'df-btn-secondary',
'df-btn-outline',
'df-btn-ghost',
'df-btn-destructive',
'df-btn-link',
'df-btn-sm',
'df-btn-lg',
'df-btn-icon',
'df-btn-icon-sm',
// badges
'df-badge',
'df-badge-primary',
'df-badge-secondary',
'df-badge-success',
'df-badge-warning',
'df-badge-destructive',
'df-badge-outline',
// tabs + bars
'df-tabs-list',
'df-tab',
'df-navtab',
'df-navtab-active',
'df-nav',
'df-toolbar',
// surfaces + controls
'df-card',
'df-panel',
'df-input',
'df-link',
// status
'df-dot',
'df-dot-running',
'df-dot-idle',
'df-dot-error',
'df-spinner',
// common palette tags
'df-tag-blue',
'df-tag-amber',
'df-tag-green',
'df-tag-red',
'df-tag-sky',
'df-tag-violet',
'df-tag-rose',
]
14 changes: 14 additions & 0 deletions packages/design/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export * from './components'
export { presetDevframe, shortcuts } from './preset'
export type { PresetDevframeOptions } from './preset'
export { presetDevframe as default } from './preset'
export {
cssVar,
PAIRED_TOKENS,
radius,
SOLO_TOKENS,
TOKEN_PREFIX,
tokenColors,
} from './tokens'

export type { DesignToken, PairedToken, SoloToken } from './tokens'
Loading