|
| 1 | +import type { Preset } from 'unocss' |
| 2 | +import { |
| 3 | + definePreset, |
| 4 | + presetIcons, |
| 5 | + presetWind4, |
| 6 | + transformerDirectives, |
| 7 | + transformerVariantGroup, |
| 8 | +} from 'unocss' |
| 9 | +import { radius, tokenColors } from './tokens' |
| 10 | + |
| 11 | +export interface PresetDevframeOptions { |
| 12 | + /** |
| 13 | + * Options forwarded to `presetIcons`. The default icon scale matches the |
| 14 | + * inline-with-text sizing used across the built-in plugins. |
| 15 | + */ |
| 16 | + icons?: Parameters<typeof presetIcons>[0] |
| 17 | +} |
| 18 | + |
| 19 | +/** |
| 20 | + * The shared `df-*` component vocabulary. These shortcuts are the cross-frame |
| 21 | + * "components": markup differs per framework (React, Svelte, vanilla DOM) but a |
| 22 | + * `df-btn df-btn-primary` button — or a `df-badge`, `df-tab`, `df-card`, … — |
| 23 | + * resolves to the same CSS everywhere, so they look and feel identical. The |
| 24 | + * definitions mirror the flagship shadcn/ui primitives so a hand-written |
| 25 | + * `df-btn` and a generated `<Button>` are visually interchangeable. |
| 26 | + */ |
| 27 | +export const shortcuts = [ |
| 28 | + { |
| 29 | + // Foundations — friendly aliases onto the semantic token palette. |
| 30 | + 'bg-base': 'bg-background', |
| 31 | + 'color-base': 'text-foreground', |
| 32 | + 'border-base': 'border-border', |
| 33 | + 'bg-active': 'bg-accent', |
| 34 | + 'color-active': 'text-primary', |
| 35 | + 'border-active': 'border-primary/40', |
| 36 | + 'op-fade': 'op65 dark:op75', |
| 37 | + 'op-mute': 'op40 dark:op45', |
| 38 | + |
| 39 | + // Named depth layers so chrome stacks predictably. |
| 40 | + 'z-toolbar': 'z-20', |
| 41 | + 'z-nav': 'z-30', |
| 42 | + |
| 43 | + // Buttons. |
| 44 | + 'df-btn': 'inline-flex items-center justify-center gap-2 shrink-0 whitespace-nowrap select-none cursor-pointer rounded-md text-sm font-medium h-9 px-4 py-2 outline-none transition-colors [&_svg]:pointer-events-none [&_svg]:shrink-0 disabled:(pointer-events-none op50) focus-visible:(ring-[3px] ring-ring/50)', |
| 45 | + 'df-btn-primary': 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', |
| 46 | + 'df-btn-secondary': 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', |
| 47 | + 'df-btn-outline': 'border border-border bg-background shadow-xs hover:(bg-accent text-accent-foreground)', |
| 48 | + 'df-btn-ghost': 'hover:(bg-accent text-accent-foreground)', |
| 49 | + 'df-btn-destructive': 'bg-destructive text-white shadow-xs hover:bg-destructive/90', |
| 50 | + 'df-btn-link': 'text-primary underline-offset-4 hover:underline', |
| 51 | + // Size modifiers override the base size with `!` so they win regardless of |
| 52 | + // stylesheet order — no `tailwind-merge` needed in Svelte/vanilla markup. |
| 53 | + 'df-btn-sm': 'h-8! gap-1.5! px-3!', |
| 54 | + 'df-btn-lg': 'h-10! px-6!', |
| 55 | + 'df-btn-icon': 'w-9! px-0!', |
| 56 | + 'df-btn-icon-sm': 'h-7! w-7! px-0!', |
| 57 | + |
| 58 | + // Badges (solid / semantic) and soft tags share a silhouette. |
| 59 | + 'df-badge': 'inline-flex items-center justify-center gap-1 w-fit shrink-0 whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium', |
| 60 | + 'df-badge-primary': 'df-badge border-transparent bg-primary text-primary-foreground', |
| 61 | + 'df-badge-secondary': 'df-badge border-transparent bg-secondary text-secondary-foreground', |
| 62 | + 'df-badge-success': 'df-badge border-success/20 bg-success/15 text-success', |
| 63 | + 'df-badge-warning': 'df-badge border-warning/20 bg-warning/15 text-warning', |
| 64 | + 'df-badge-destructive': 'df-badge border-transparent bg-destructive text-white', |
| 65 | + 'df-badge-outline': 'df-badge text-foreground', |
| 66 | + |
| 67 | + // Segmented tabs (mirror the shadcn segmented control). |
| 68 | + 'df-tabs-list': 'inline-flex items-center justify-center w-fit h-9 p-[3px] rounded-lg bg-muted text-muted-foreground', |
| 69 | + 'df-tab': 'inline-flex flex-1 items-center justify-center gap-1.5 h-[calc(100%-1px)] px-2 py-1 rounded-md border border-transparent text-sm font-medium whitespace-nowrap select-none cursor-pointer outline-none transition-colors disabled:(pointer-events-none op50) focus-visible:(ring-[3px] ring-ring/50) data-[state=active]:(bg-background text-foreground shadow-sm)', |
| 70 | + |
| 71 | + // Closeable navigation tabs (e.g. terminal sessions, open documents). |
| 72 | + 'df-navtab': '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-colors hover:(op100 bg-accent)', |
| 73 | + 'df-navtab-active': 'op100! bg-accent border-base! color-base', |
| 74 | + |
| 75 | + // Surfaces. |
| 76 | + 'df-card': 'flex flex-col rounded-xl border border-border bg-card text-card-foreground shadow-sm', |
| 77 | + 'df-panel': 'rounded-lg border border-border bg-card text-card-foreground', |
| 78 | + |
| 79 | + // Form controls. |
| 80 | + 'df-input': 'flex w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none transition-colors placeholder:text-muted-foreground disabled:(cursor-not-allowed op50) focus-visible:(border-ring ring-[3px] ring-ring/50)', |
| 81 | + |
| 82 | + // Links. |
| 83 | + 'df-link': 'text-primary underline-offset-4 hover:underline', |
| 84 | + |
| 85 | + // Status dots — real lifecycle states only. |
| 86 | + 'df-dot': 'inline-block h-1.5 w-1.5 rounded-full shrink-0', |
| 87 | + 'df-dot-running': 'bg-success', |
| 88 | + 'df-dot-idle': 'bg-muted-foreground', |
| 89 | + 'df-dot-error': 'bg-destructive', |
| 90 | + |
| 91 | + // Indeterminate spinner. |
| 92 | + 'df-spinner': 'inline-block size-4 rounded-full border-2 border-current border-t-transparent animate-spin', |
| 93 | + }, |
| 94 | + |
| 95 | + // Soft, palette-driven tags: `df-tag-blue`, `df-tag-amber`, … for ad-hoc |
| 96 | + // categorical labels that fall outside the semantic palette. |
| 97 | + [ |
| 98 | + /^df-tag-(\w+)$/, |
| 99 | + ([, color]: string[]) => |
| 100 | + `inline-flex items-center gap-1 w-fit whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium border-${color}-500/20 bg-${color}-400/15 text-${color}-700 dark:text-${color}-300`, |
| 101 | + ] as [RegExp, (match: string[]) => string], |
| 102 | +] |
| 103 | + |
| 104 | +/** |
| 105 | + * The single UnoCSS preset every devframe plugin extends. It bundles |
| 106 | + * `presetWind4` (Tailwind-compatible utilities + class-based `dark:`), |
| 107 | + * `presetIcons` (Phosphor via `@iconify-json/ph`), the directive + variant-group |
| 108 | + * transformers, the semantic token theme, and the shared `df-*` shortcuts. |
| 109 | + * |
| 110 | + * A plugin's entire `uno.config.ts` becomes: |
| 111 | + * |
| 112 | + * ```ts |
| 113 | + * import { presetDevframe } from '@internal/design/preset' |
| 114 | + * import { defineConfig } from 'unocss' |
| 115 | + * |
| 116 | + * export default defineConfig({ presets: [presetDevframe()] }) |
| 117 | + * ``` |
| 118 | + * |
| 119 | + * Pair it with `import '@internal/design/theme.css'` once on the page to define |
| 120 | + * the `--df-*` token values and base element styling. |
| 121 | + */ |
| 122 | +export function presetDevframe(options: PresetDevframeOptions = {}): Preset { |
| 123 | + return definePreset({ |
| 124 | + name: '@internal/design/preset', |
| 125 | + theme: { |
| 126 | + colors: tokenColors, |
| 127 | + radius, |
| 128 | + }, |
| 129 | + shortcuts, |
| 130 | + presets: [ |
| 131 | + presetWind4(), |
| 132 | + presetIcons({ scale: 1.1, ...options.icons }), |
| 133 | + ], |
| 134 | + transformers: [ |
| 135 | + transformerDirectives(), |
| 136 | + transformerVariantGroup(), |
| 137 | + ], |
| 138 | + }) |
| 139 | +} |
| 140 | + |
| 141 | +export default presetDevframe |
0 commit comments