@antfu/design 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/a11y/cli.ts +73 -0
  4. package/a11y/index.ts +13 -0
  5. package/a11y/scan.ts +127 -0
  6. package/components/Action/ActionButton.stories.ts +56 -0
  7. package/components/Action/ActionButton.vue +57 -0
  8. package/components/Action/ActionDarkToggle.stories.ts +31 -0
  9. package/components/Action/ActionDarkToggle.vue +87 -0
  10. package/components/Action/ActionIconButton.stories.ts +47 -0
  11. package/components/Action/ActionIconButton.vue +47 -0
  12. package/components/Display/DisplayAvatar.stories.ts +36 -0
  13. package/components/Display/DisplayAvatar.vue +58 -0
  14. package/components/Display/DisplayBadge.stories.ts +31 -0
  15. package/components/Display/DisplayBadge.vue +98 -0
  16. package/components/Display/DisplayBytes.stories.ts +28 -0
  17. package/components/Display/DisplayBytes.vue +30 -0
  18. package/components/Display/DisplayDate.stories.ts +37 -0
  19. package/components/Display/DisplayDate.vue +29 -0
  20. package/components/Display/DisplayDonut.stories.ts +26 -0
  21. package/components/Display/DisplayDonut.vue +46 -0
  22. package/components/Display/DisplayDuration.stories.ts +28 -0
  23. package/components/Display/DisplayDuration.vue +28 -0
  24. package/components/Display/DisplayFileIcon.stories.ts +27 -0
  25. package/components/Display/DisplayFileIcon.vue +30 -0
  26. package/components/Display/DisplayFilePath.stories.ts +30 -0
  27. package/components/Display/DisplayFilePath.vue +61 -0
  28. package/components/Display/DisplayKbd.stories.ts +26 -0
  29. package/components/Display/DisplayKbd.vue +27 -0
  30. package/components/Display/DisplayKeyValue.stories.ts +56 -0
  31. package/components/Display/DisplayKeyValue.vue +51 -0
  32. package/components/Display/DisplayLabel.stories.ts +27 -0
  33. package/components/Display/DisplayLabel.vue +33 -0
  34. package/components/Display/DisplayNumber.stories.ts +27 -0
  35. package/components/Display/DisplayNumber.vue +24 -0
  36. package/components/Display/DisplayNumberBadge.stories.ts +26 -0
  37. package/components/Display/DisplayNumberBadge.vue +22 -0
  38. package/components/Display/DisplayPackageName.stories.ts +26 -0
  39. package/components/Display/DisplayPackageName.vue +49 -0
  40. package/components/Display/DisplayProgressBar.stories.ts +29 -0
  41. package/components/Display/DisplayProgressBar.vue +90 -0
  42. package/components/Display/DisplayProportionBar.stories.ts +40 -0
  43. package/components/Display/DisplayProportionBar.vue +43 -0
  44. package/components/Display/DisplaySafeImage.stories.ts +43 -0
  45. package/components/Display/DisplaySafeImage.vue +30 -0
  46. package/components/Display/DisplayStatusPill.stories.ts +34 -0
  47. package/components/Display/DisplayStatusPill.vue +42 -0
  48. package/components/Display/DisplayTree.stories.ts +76 -0
  49. package/components/Display/DisplayTree.vue +102 -0
  50. package/components/Display/DisplayVersion.stories.ts +25 -0
  51. package/components/Display/DisplayVersion.vue +21 -0
  52. package/components/Feedback/FeedbackEmptyState.stories.ts +38 -0
  53. package/components/Feedback/FeedbackEmptyState.vue +21 -0
  54. package/components/Feedback/FeedbackLoading.stories.ts +23 -0
  55. package/components/Feedback/FeedbackLoading.vue +21 -0
  56. package/components/Feedback/FeedbackSpinner.stories.ts +25 -0
  57. package/components/Feedback/FeedbackSpinner.vue +22 -0
  58. package/components/Feedback/FeedbackTip.stories.ts +34 -0
  59. package/components/Feedback/FeedbackTip.vue +29 -0
  60. package/components/Feedback/FeedbackToasts.stories.ts +40 -0
  61. package/components/Feedback/FeedbackToasts.vue +105 -0
  62. package/components/Form/FormCheckbox.stories.ts +36 -0
  63. package/components/Form/FormCheckbox.vue +30 -0
  64. package/components/Form/FormCombobox.stories.ts +35 -0
  65. package/components/Form/FormCombobox.vue +83 -0
  66. package/components/Form/FormField.stories.ts +56 -0
  67. package/components/Form/FormField.vue +36 -0
  68. package/components/Form/FormNumberInput.stories.ts +47 -0
  69. package/components/Form/FormNumberInput.vue +85 -0
  70. package/components/Form/FormRadioGroup.stories.ts +47 -0
  71. package/components/Form/FormRadioGroup.vue +43 -0
  72. package/components/Form/FormSearchField.stories.ts +22 -0
  73. package/components/Form/FormSearchField.vue +32 -0
  74. package/components/Form/FormSelect.stories.ts +47 -0
  75. package/components/Form/FormSelect.vue +56 -0
  76. package/components/Form/FormSwitch.stories.ts +36 -0
  77. package/components/Form/FormSwitch.vue +26 -0
  78. package/components/Form/FormTextInput.stories.ts +39 -0
  79. package/components/Form/FormTextInput.vue +51 -0
  80. package/components/Form/FormTextarea.stories.ts +47 -0
  81. package/components/Form/FormTextarea.vue +32 -0
  82. package/components/Layout/LayoutBreadcrumb.stories.ts +54 -0
  83. package/components/Layout/LayoutBreadcrumb.vue +54 -0
  84. package/components/Layout/LayoutCard.stories.ts +31 -0
  85. package/components/Layout/LayoutCard.vue +21 -0
  86. package/components/Layout/LayoutDataTable.stories.ts +77 -0
  87. package/components/Layout/LayoutDataTable.vue +145 -0
  88. package/components/Layout/LayoutExpandableList.stories.ts +28 -0
  89. package/components/Layout/LayoutExpandableList.vue +94 -0
  90. package/components/Layout/LayoutPanelGrids.stories.ts +28 -0
  91. package/components/Layout/LayoutPanelGrids.vue +26 -0
  92. package/components/Layout/LayoutSectionBlock.stories.ts +37 -0
  93. package/components/Layout/LayoutSectionBlock.vue +37 -0
  94. package/components/Layout/LayoutSideNav.stories.ts +33 -0
  95. package/components/Layout/LayoutSideNav.vue +48 -0
  96. package/components/Layout/LayoutSplitPane.stories.ts +44 -0
  97. package/components/Layout/LayoutSplitPane.vue +30 -0
  98. package/components/Layout/LayoutTabs.stories.ts +43 -0
  99. package/components/Layout/LayoutTabs.vue +56 -0
  100. package/components/Layout/LayoutToolbar.stories.ts +60 -0
  101. package/components/Layout/LayoutToolbar.vue +28 -0
  102. package/components/Layout/LayoutVirtualList.stories.ts +30 -0
  103. package/components/Layout/LayoutVirtualList.vue +82 -0
  104. package/components/Overlay/OverlayDrawer.stories.ts +47 -0
  105. package/components/Overlay/OverlayDrawer.vue +58 -0
  106. package/components/Overlay/OverlayDropdown.stories.ts +25 -0
  107. package/components/Overlay/OverlayDropdown.vue +30 -0
  108. package/components/Overlay/OverlayDropdownItem.stories.ts +26 -0
  109. package/components/Overlay/OverlayDropdownItem.vue +31 -0
  110. package/components/Overlay/OverlayDropdownLabel.vue +9 -0
  111. package/components/Overlay/OverlayDropdownSeparator.vue +7 -0
  112. package/components/Overlay/OverlayModal.stories.ts +33 -0
  113. package/components/Overlay/OverlayModal.vue +48 -0
  114. package/components/Overlay/OverlayTooltip.stories.ts +33 -0
  115. package/components/Overlay/OverlayTooltip.vue +38 -0
  116. package/composables/colorScheme.ts +58 -0
  117. package/composables/toast.ts +81 -0
  118. package/package.json +99 -0
  119. package/skills/antfu-design/SKILL.md +65 -0
  120. package/skills/antfu-design/references/advanced-patterns.md +39 -0
  121. package/skills/antfu-design/references/best-practices.md +54 -0
  122. package/skills/antfu-design/references/core-components.md +72 -0
  123. package/skills/antfu-design/references/core-setup.md +56 -0
  124. package/skills/antfu-design/references/core-tokens.md +100 -0
  125. package/skills/antfu-design/references/features-data-presentation.md +27 -0
  126. package/splitpanes.d.ts +70 -0
  127. package/styles/animations.css +47 -0
  128. package/styles/base.css +31 -0
  129. package/styles/floating-vue.css +28 -0
  130. package/styles/index.css +7 -0
  131. package/styles/reka-ui.css +112 -0
  132. package/styles/scrollbar.css +24 -0
  133. package/styles/splitpanes.css +61 -0
  134. package/unocss/colors.ts +127 -0
  135. package/unocss/index.ts +99 -0
  136. package/unocss/options.ts +31 -0
  137. package/unocss/patterns.ts +38 -0
  138. package/unocss/rules.ts +26 -0
  139. package/unocss/severity.ts +16 -0
  140. package/unocss/shortcuts.ts +68 -0
  141. package/utils/color.ts +328 -0
  142. package/utils/contrast.ts +118 -0
  143. package/utils/format.ts +389 -0
  144. package/utils/icon.ts +200 -0
  145. package/utils/index.ts +13 -0
  146. package/utils/keybinding.ts +199 -0
  147. package/utils/misc.ts +141 -0
  148. package/utils/path.ts +243 -0
  149. package/utils/semver.ts +147 -0
  150. package/utils/tree.ts +89 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A controlled toast queue for {@link FeedbackToasts}.
3
+ *
4
+ * The package stays stateless — `FeedbackToasts` is presentational and the *app*
5
+ * owns the list. This composable is the ergonomic owner: create it once and share
6
+ * the returned `toasts` ref with the component, then `add`/`dismiss` from anywhere.
7
+ * Share a single instance app-wide via `provide`/`inject` or a module-level export.
8
+ *
9
+ * @example
10
+ * const { toasts, add, dismiss } = useToast()
11
+ * add('Saved', { type: 'success', duration: 3000 })
12
+ * // <FeedbackToasts :items="toasts" @dismiss="dismiss" />
13
+ */
14
+ import type { Ref } from 'vue'
15
+ import type { ToastItem, ToastType } from '../components/Feedback/FeedbackToasts.vue'
16
+ import { ref } from 'vue'
17
+
18
+ /** Options accepted when pushing a toast (everything but `id`/`message`). */
19
+ export type ToastInput = Omit<ToastItem, 'id' | 'message'> & { id?: ToastItem['id'] }
20
+
21
+ export interface ToastQueue {
22
+ /** The reactive toast list — pass to `<FeedbackToasts :items>`. */
23
+ toasts: Ref<ToastItem[]>
24
+ /** Push a toast; returns its id. Auto-dismisses after `duration` ms when set. */
25
+ add: (message: string, options?: ToastInput) => ToastItem['id']
26
+ /** Patch an existing toast (e.g. update `progress`). */
27
+ update: (id: ToastItem['id'], patch: Partial<ToastItem>) => void
28
+ /** Remove a toast by id. */
29
+ dismiss: (id: ToastItem['id']) => void
30
+ /** Remove all toasts. */
31
+ clear: () => void
32
+ /** Shorthands that preset `type`. */
33
+ success: (message: string, options?: ToastInput) => ToastItem['id']
34
+ error: (message: string, options?: ToastInput) => ToastItem['id']
35
+ info: (message: string, options?: ToastInput) => ToastItem['id']
36
+ warning: (message: string, options?: ToastInput) => ToastItem['id']
37
+ }
38
+
39
+ let counter = 0
40
+
41
+ /**
42
+ * Create a toast queue. Stateless package, caller owns the instance.
43
+ *
44
+ * @returns A {@link ToastQueue}.
45
+ */
46
+ export function useToast(): ToastQueue {
47
+ const toasts = ref<ToastItem[]>([])
48
+
49
+ function dismiss(id: ToastItem['id']): void {
50
+ toasts.value = toasts.value.filter(t => t.id !== id)
51
+ }
52
+
53
+ function update(id: ToastItem['id'], patch: Partial<ToastItem>): void {
54
+ const item = toasts.value.find(t => t.id === id)
55
+ if (item)
56
+ Object.assign(item, patch)
57
+ }
58
+
59
+ function add(message: string, options: ToastInput = {}): ToastItem['id'] {
60
+ const { id = `t${++counter}`, duration, ...rest } = options
61
+ toasts.value.push({ id, message, ...rest })
62
+ if (duration != null && duration > 0)
63
+ setTimeout(dismiss, duration, id)
64
+ return id
65
+ }
66
+
67
+ const withType = (type: ToastType) => (message: string, options: ToastInput = {}) =>
68
+ add(message, { type, ...options })
69
+
70
+ return {
71
+ toasts,
72
+ add,
73
+ update,
74
+ dismiss,
75
+ clear: () => { toasts.value = [] },
76
+ success: withType('success'),
77
+ error: withType('error'),
78
+ info: withType('info'),
79
+ warning: withType('warning'),
80
+ }
81
+ }
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@antfu/design",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "A customizable, composable design system for devtools-style Vue apps — a UnoCSS preset, Vue primitives, a design skill, and an a11y contrast check",
6
+ "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
+ "license": "MIT",
8
+ "funding": "https://github.com/sponsors/antfu",
9
+ "homepage": "https://github.com/antfu/design#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/antfu/design.git",
13
+ "directory": "packages/design"
14
+ },
15
+ "bugs": "https://github.com/antfu/design/issues",
16
+ "keywords": [
17
+ "unocss",
18
+ "unocss-preset",
19
+ "vue",
20
+ "design-system",
21
+ "components",
22
+ "devtools",
23
+ "antfu"
24
+ ],
25
+ "sideEffects": false,
26
+ "exports": {
27
+ "./unocss": "./unocss/index.ts",
28
+ "./utils": "./utils/index.ts",
29
+ "./composables/*": "./composables/*",
30
+ "./a11y": "./a11y/index.ts",
31
+ "./styles.css": "./styles/index.css",
32
+ "./styles/*": "./styles/*",
33
+ "./components/*": "./components/*",
34
+ "./splitpanes.d.ts": "./splitpanes.d.ts",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "a11y",
39
+ "components",
40
+ "composables",
41
+ "skills",
42
+ "splitpanes.d.ts",
43
+ "styles",
44
+ "unocss",
45
+ "utils"
46
+ ],
47
+ "peerDependencies": {
48
+ "@axe-core/playwright": "^4.0.0",
49
+ "playwright": "^1.0.0",
50
+ "unocss": ">=66.0.0",
51
+ "vue": "^3.5.0"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "@axe-core/playwright": {
55
+ "optional": true
56
+ },
57
+ "playwright": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "dependencies": {
62
+ "@antfu/utils": "^9.3.0",
63
+ "@iconify-json/catppuccin": "^1.2.17",
64
+ "@tanstack/vue-virtual": "^3.13.29",
65
+ "@unocss/core": "^66.7.2",
66
+ "@vueuse/core": "^14.3.0",
67
+ "colorjs.io": "^0.6.1",
68
+ "floating-vue": "^5.2.2",
69
+ "reka-ui": "^2.10.0",
70
+ "splitpanes": "^4.1.2"
71
+ },
72
+ "devDependencies": {
73
+ "@arethetypeswrong/cli": "^0.18.2",
74
+ "@axe-core/playwright": "^4.12.1",
75
+ "@storybook/vue3-vite": "^10.4.6",
76
+ "@unocss/preset-icons": "^66.7.2",
77
+ "@unocss/preset-mini": "^66.7.2",
78
+ "@unocss/preset-web-fonts": "^66.7.2",
79
+ "@unocss/preset-wind3": "^66.7.2",
80
+ "@unocss/preset-wind4": "^66.7.2",
81
+ "@vitejs/plugin-vue": "^6.0.7",
82
+ "@vue/test-utils": "^2.4.11",
83
+ "happy-dom": "^20.10.6",
84
+ "playwright": "^1.61.1",
85
+ "tsdown": "^0.22.0",
86
+ "tsdown-stale-guard": "^0.1.2",
87
+ "tsnapi": "^0.3.3",
88
+ "unocss": "^66.7.2",
89
+ "unplugin-vue": "^7.2.0",
90
+ "vue": "^3.5.38",
91
+ "vue-tsc": "^3.3.5"
92
+ },
93
+ "scripts": {
94
+ "docs:gen": "tsx scripts/gen-tokens.ts",
95
+ "lint": "eslint",
96
+ "typecheck": "vue-tsc --noEmit",
97
+ "test": "vitest run"
98
+ }
99
+ }
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: antfu-design
3
+ description: Use when building or restyling devtools-style Vue 3 UIs with @antfu/design — wiring the composable UnoCSS preset, using the semantic token vocabulary (bg-base, color-base, badge-color-*, color-scale-*), choosing prefixed primitives (DisplayBadge, ActionButton, DisplayFilePath, OverlayModal, …), and keeping light/dark contrast and the "anti-slop" rules. Reach for it whenever generating or reviewing UI in a project that depends on @antfu/design.
4
+ metadata:
5
+ author: antfu
6
+ version: 2026.06.26
7
+ ---
8
+
9
+ # Doing good design with `@antfu/design`
10
+
11
+ `@antfu/design` is a shared design layer for devtools-style Vue apps: a
12
+ **composable UnoCSS preset** (`presetAnthonyDesign`), a set of **token-driven Vue
13
+ primitives**, and a **color-contrast a11y check**. This skill is how to use it
14
+ *well* — the vocabulary, the defaults, and the taste.
15
+
16
+ ## The three rules that matter most
17
+
18
+ 1. **Own the tokens, not the colors.** Never hard-code a hex or a raw Tailwind
19
+ color in app UI. Reach for the semantic layer first: `bg-base`, `color-base`,
20
+ `color-muted`, `border-base`, `op-fade`, `btn-action`, `badge`. They are
21
+ defined once (in the preset) and adapt to light/dark automatically. See
22
+ [references/core-tokens.md](references/core-tokens.md).
23
+
24
+ 2. **Light/dark parity is not optional.** Every surface and text token has a dark
25
+ variant baked in. If you write a one-off color, you have just created a
26
+ dark-mode bug. The standing contrast scan (`@antfu/design/a11y`) will catch the
27
+ worst of it — don't rely on it as a substitute for using the tokens.
28
+
29
+ 3. **No slop.** Technical values are `font-mono tabular-nums`. No em-dash–laden
30
+ prose in UI copy (the **dash ban**). Prefer one obvious affordance over three
31
+ competing ones. See [references/best-practices.md](references/best-practices.md).
32
+
33
+ ## When to reach for what
34
+
35
+ - Showing a count, size, duration, or date → a **display component**
36
+ (`DisplayNumber`, `DisplayBytes`, `DisplayDuration`, `DisplayDate`), not raw text.
37
+ - A status, type, or tag → `DisplayBadge` (hash- or palette-colored) or `DisplayLabel`.
38
+ - A file/module path → `DisplayFilePath` (truncates, dims directories, links).
39
+ - An overlay → `OverlayModal` / `OverlayDrawer` (reka-ui), a popover → `OverlayTooltip` / `OverlayDropdown`.
40
+ - A severity (fresh→stale, fast→slow, small→large) → the `color-scale-*` ramp via
41
+ the display components' `colorize` prop, never ad-hoc red/green.
42
+
43
+ Components are categorized and prefixed by category (`Display*`, `Form*`,
44
+ `Overlay*`, `Layout*`, `Action*`, `Feedback*`). Dark mode is the app's to own —
45
+ the package ships no `isDark`/`toggleDark`; components that vary by scheme take a
46
+ `colorScheme: 'light' | 'dark'` prop, and `ActionDarkToggle` is controlled.
47
+
48
+ Full catalog: [references/core-components.md](references/core-components.md).
49
+
50
+ ## Setup
51
+
52
+ It's a **single preset, not self-contained** — it bundles no base preset,
53
+ icons, fonts, or reset. You add those. See
54
+ [references/core-setup.md](references/core-setup.md) for wiring the preset,
55
+ importing styles, and pointing UnoCSS at the package so the components' classes
56
+ get generated.
57
+
58
+ ## References
59
+
60
+ - [core-setup.md](references/core-setup.md) — install + wire the preset, import styles.
61
+ - [core-tokens.md](references/core-tokens.md) — the canonical token table (generated from the preset).
62
+ - [core-components.md](references/core-components.md) — the component catalog with import paths.
63
+ - [best-practices.md](references/best-practices.md) — class-over-attributify, parity, mono values, the dash ban, the "three dials".
64
+ - [features-data-presentation.md](references/features-data-presentation.md) — presenting numbers, sizes, durations, paths.
65
+ - [advanced-patterns.md](references/advanced-patterns.md) — composition patterns and a redesign protocol.
@@ -0,0 +1,39 @@
1
+ # Advanced patterns
2
+
3
+ ## Composition over configuration
4
+
5
+ The primitives are deliberately thin so you compose them into app-local
6
+ patterns rather than reaching for a mega-component:
7
+
8
+ - **Stat row** — a label + a mono value + an optional `DisplayBadge`:
9
+ `color-muted` label, `DisplayNumber`/`DisplayBytes` value.
10
+ - **Toolbar** — a glass sticky bar (`bg-glass z-nav`) with a `FormSearchField` and
11
+ `ActionIconButton`s.
12
+ - **Panel** — `LayoutCard` + `LayoutSectionBlock`s; `FeedbackEmptyState` when there's nothing.
13
+ - **Detail list** — `LayoutVirtualList` of rows, each a `DisplayFilePath`/`DisplayPackageName` +
14
+ display badges, colorized by one severity dimension.
15
+
16
+ When such a composition recurs across screens, lift it into a local component —
17
+ not into the design system (the system stays primitive-focused).
18
+
19
+ ## Overriding a primitive
20
+
21
+ Because components are thin and token-driven, overriding is usually a prop or a
22
+ token, not a fork. When you genuinely need to fork one, copy the single readable
23
+ file from `@antfu/design/components/<Name>.vue` — that subpath exists for exactly
24
+ this. Keep using the same tokens so it stays on-theme.
25
+
26
+ ## Redesign protocol
27
+
28
+ When restyling an existing screen onto `@antfu/design`:
29
+
30
+ 1. **Map colors to tokens first.** Replace every raw color with a semantic token
31
+ (`bg-base`, `color-muted`, `border-base`, `color-scale-*`). Resist new colors.
32
+ 2. **Swap raw elements for primitives** one family at a time (badges, then
33
+ buttons, then inputs…), checking the visual diff after each.
34
+ 3. **Run the contrast scan** (`@antfu/design/a11y`, or `tsx …/a11y/cli.ts <url>`) in light *and* dark.
35
+ 4. **Tune the three dials** (density, hierarchy, affordance) — see best-practices.
36
+
37
+ The goal of a migration is near-zero visual diff: you are unifying what exists,
38
+ not redesigning it. Per-project theme config (primary color, fonts, base) is
39
+ expected, not a regression.
@@ -0,0 +1,54 @@
1
+ # Best practices
2
+
3
+ Preserved from the original antfu design philosophy, reframed around the package.
4
+
5
+ ## Class over attributify
6
+
7
+ Write utilities as **classes**, not attributes. Classes are greppable, sort
8
+ cleanly (the UnoCSS ESLint plugin orders them), and read consistently.
9
+
10
+ ```vue
11
+ <!-- good -->
12
+ <div class="flex items-center gap-2 color-muted">
13
+ <!-- avoid -->
14
+ <div flex items-center gap-2 color-muted>
15
+ ```
16
+
17
+ ## Light/dark parity
18
+
19
+ Never write a one-off color. Every surface/text/border has a token with a dark
20
+ variant baked in (`bg-base`, `color-base`, `border-base`, …). A raw `bg-white`
21
+ or `text-gray-800` is a dark-mode bug waiting to happen. When you truly need a
22
+ custom color, define it through the theme (a `primary`/`warning`/… ramp) so both
23
+ modes are covered.
24
+
25
+ ## Mono + tabular for technical values
26
+
27
+ Numbers, sizes, durations, versions, hashes, paths → `font-mono tabular-nums`
28
+ so columns align and digits don't jitter. Prefer the display components
29
+ (`DisplayNumber`, `DisplayBytes`, `DisplayDuration`, `DisplayVersion`) which already do
30
+ this.
31
+
32
+ ## Anti-slop: the dash ban
33
+
34
+ No em-dash–driven prose in UI copy or generated text. Keep microcopy short and
35
+ declarative. Don't pad labels ("Click here to view the…") — name the thing.
36
+
37
+ ## The three dials
38
+
39
+ When reviewing a screen, tune in this order:
40
+
41
+ 1. **Density** — how much breathing room (`gap`, `p`, line-height). Devtools UIs
42
+ skew dense; be deliberate, not cramped.
43
+ 2. **Hierarchy** — `color-base` vs `color-muted` vs `color-faint`, weight, size.
44
+ Most text is muted; reserve `color-base` for what matters and `color-active`
45
+ for the one accent.
46
+ 3. **Affordance** — one obvious action per context. Prefer a single
47
+ `btn-primary` over three competing buttons. Severity via `color-scale-*`, not
48
+ ad-hoc red/green.
49
+
50
+ ## Severity, not vibes
51
+
52
+ Fresh→stale, fast→slow, small→large all map to the same five-stop ramp
53
+ (`color-scale-neutral|low|medium|high|critical`). Use the `colorize` prop on the
54
+ display components instead of hand-picking colors per case.
@@ -0,0 +1,72 @@
1
+ # Component catalog
2
+
3
+ All from `@antfu/design`, explicit imports, token-driven, dark-aware. Components
4
+ are grouped into categories and **prefixed with their category** (e.g.
5
+ `DisplayBadge`, `FormTextInput`, `OverlayModal`) — the import path mirrors this
6
+ (`@antfu/design/components/Display/DisplayBadge.vue`). Overlay behavior comes
7
+ from reka-ui; tooltips/poppers from floating-vue; resizable panes from
8
+ splitpanes — all themed through the shipped CSS overrides.
9
+
10
+ > **Dark mode is the app's to own.** The package does not ship `isDark`/`toggleDark`.
11
+ > Components that vary by scheme (e.g. hash-colored `DisplayBadge`, `DisplayLabel`,
12
+ > `DisplayPackageName`, `DisplayProportionBar`) take a `colorScheme: 'light' | 'dark'`
13
+ > prop. `ActionDarkToggle` is **controlled**: `colorScheme` prop + `update:colorScheme`.
14
+
15
+ ## Action
16
+
17
+ | Component | Use it for |
18
+ |---|---|
19
+ | `ActionButton` | actions. `variant` action/primary/text, polymorphic `href`/`as`, `icon`, `loading`. |
20
+ | `ActionIconButton` | a round icon-only button with a `tooltip`, `active` state, `#badge`. |
21
+ | `ActionDarkToggle` | controlled dark toggle (`v-model:colorScheme`) with a view-transition reveal. |
22
+
23
+ ## Display
24
+
25
+ | Component | Use it for |
26
+ |---|---|
27
+ | `DisplayBadge` | a status/type/tag chip — hash-colored from text, a palette name (`color="green"`), a hue, or muted. `colorScheme`, `variant`, `size`, `icon`. |
28
+ | `DisplayNumber` / `DisplayNumberBadge` | formatted numbers (`Intl`), mono + tabular, `prefix`/`suffix`. |
29
+ | `DisplayDuration` | ms → human, `colorize` by severity. |
30
+ | `DisplayBytes` | humanized size, `colorize`, percent of `total`. |
31
+ | `DisplayDate` | relative time + exact-date tooltip, `colorize` by age, `live`. |
32
+ | `DisplayVersion` | `vX.Y.Z` prefix logic. |
33
+ | `DisplayPackageName` | scope colored by hash (`colorScheme`). |
34
+ | `DisplayFilePath` | truncates, dims directories, decodes `.pnpm`, icon, link. |
35
+ | `DisplayFileIcon` | ext → icon via a configurable rule list. |
36
+ | `DisplayLabel` | hex → contrast-aware tinted chip (`colorScheme`). |
37
+ | `DisplayStatusPill` | severity dot + label, `pulse`. |
38
+ | `DisplayDonut` / `DisplayProportionBar` | progress ring / stacked proportion bar. |
39
+ | `DisplayKbd` | renders `mod+k` as platform glyphs. |
40
+
41
+ ## Form
42
+
43
+ `FormTextInput`, `FormSearchField` (icon + `DisplayKbd` hint + clear),
44
+ `FormCheckbox`, `FormSwitch`, `FormRadioGroup`, `FormSelect`.
45
+
46
+ ## Overlay
47
+
48
+ `OverlayModal`, `OverlayDrawer`, `OverlayDropdown` (+ `OverlayDropdownItem`),
49
+ `OverlayTooltip`.
50
+
51
+ ## Layout
52
+
53
+ `LayoutCard`, `LayoutSectionBlock` (collapsible), `LayoutSplitPane` (+ `Pane`),
54
+ `LayoutTabs` (underline/segment, `count` chips), `LayoutVirtualList`
55
+ (`@tanstack/vue-virtual`), `LayoutExpandableList` ("show N more").
56
+
57
+ ## Feedback
58
+
59
+ `FeedbackSpinner`, `FeedbackLoading`, `FeedbackEmptyState`, `FeedbackTip`
60
+ (info/success/warning/error), `FeedbackToastProvider` (+ `useNotification()`).
61
+
62
+ ## Composables & utils
63
+
64
+ - `@antfu/design/composables` — `useInputFocus`, `provideNotification` / `useNotification`.
65
+ (Dark mode, clipboard and persisted state are **not** wrapped — use the app's own
66
+ state and VueUse `useClipboard` / `useLocalStorage` directly.)
67
+ - `@antfu/design/utils` — `getHashColorFromString`, `getPluginColor`, `labelStyle`
68
+ (all take an explicit dark flag), `formatBytes`/`formatDuration`/`formatTimeAgo`
69
+ (+ severity colors), `parseReadablePath`, `parseSemverRange`, `toTree`,
70
+ `parseChord`/`bindingDisplay`, WCAG `contrastRatio`.
71
+
72
+ > Out this iteration: `CodeBlock` / `DiffView` and the command palette (deferred).
@@ -0,0 +1,56 @@
1
+ # Setup
2
+
3
+ Install the package and a UnoCSS base. It's a **single preset, not
4
+ self-contained** — it bundles no base preset, icons, fonts, or reset.
5
+
6
+ ```bash
7
+ pnpm add @antfu/design unocss
8
+ ```
9
+
10
+ ## Wiring the preset
11
+
12
+ ```ts
13
+ // uno.config.ts
14
+ import { presetAnthonyDesign } from '@antfu/design/unocss'
15
+ import { defineConfig, presetIcons, presetWebFonts, presetWind4 } from 'unocss'
16
+
17
+ export default defineConfig({
18
+ presets: [
19
+ presetAnthonyDesign({
20
+ primary: '#49833E', // string | full color-scale object (default antfu green)
21
+ darkBackground: '#111', // near-black for dark surfaces
22
+ }),
23
+ presetWind4(), // a base preset is REQUIRED — shortcuts expand into its utilities
24
+ presetIcons(),
25
+ presetWebFonts({ fonts: { sans: 'DM Sans', mono: 'DM Mono' } }),
26
+ ],
27
+ // Generate the components' classes by scanning the installed package:
28
+ content: { pipeline: { include: [/@antfu\/design/] } },
29
+ })
30
+ ```
31
+
32
+ > The base preset is **yours** to choose (`presetWind4`, `presetWind3` or
33
+ > `presetMini`). Without one, the semantic shortcuts have nothing to expand into.
34
+ > The design layer is **one preset** — there are no sub-presets to compose.
35
+
36
+ ## Styles
37
+
38
+ ```ts
39
+ import '@antfu/design/styles.css' // everything
40
+ // …or cherry-pick:
41
+ import '@antfu/design/styles/floating-vue.css'
42
+ ```
43
+
44
+ Add a reset yourself (`@unocss/reset`) — the design system does not bundle one.
45
+
46
+ ## Components
47
+
48
+ Imported by **full path** (no barrel), categorized and category-prefixed:
49
+
50
+ ```ts
51
+ import ActionButton from '@antfu/design/components/Action/ActionButton.vue'
52
+ import DisplayBadge from '@antfu/design/components/Display/DisplayBadge.vue'
53
+ import OverlayModal from '@antfu/design/components/Overlay/OverlayModal.vue'
54
+ ```
55
+
56
+ The package ships raw `.ts` / `.vue` — your build compiles it. No auto-import resolver.
@@ -0,0 +1,100 @@
1
+ # Token reference
2
+
3
+ This is the **canonical token vocabulary** — the contract between the preset,
4
+ the components, and your app code. Use these names verbatim; do not hand-roll
5
+ equivalents.
6
+
7
+ The table below is **generated from the preset** (`pnpm docs:gen`), so it can
8
+ never drift from what the shortcuts actually resolve to.
9
+
10
+ <!-- TOKENS:START -->
11
+ ### Semantic & composite shortcuts
12
+
13
+ | Token | Expands to |
14
+ |---|---|
15
+ | `color-base` | `color-neutral-800 dark:color-neutral-200` |
16
+ | `color-muted` | `color-neutral-600 dark:color-neutral-400` |
17
+ | `color-faint` | `color-neutral-500 dark:color-neutral-500` |
18
+ | `color-active` | `color-primary-600 dark:color-primary-300` |
19
+ | `bg-base` | `bg-white dark:bg-#111` |
20
+ | `bg-secondary` | `bg-#f5f5f5 dark:bg-#1a1a1a` |
21
+ | `bg-active` | `bg-#8881` |
22
+ | `bg-hover` | `bg-primary/5` |
23
+ | `bg-code` | `bg-gray-500/5` |
24
+ | `bg-tooltip` | `bg-white/75 dark:bg-#111/75 backdrop-blur-8` |
25
+ | `bg-gradient-more` | `bg-gradient-to-t from-white via-white/80 to-white/0 dark:from-#111 dark:via-#111/80 dark:to-#111/0` |
26
+ | `border-base` | `border-#8882` |
27
+ | `border-mute` | `border-#8881` |
28
+ | `border-active` | `border-primary-600/25 dark:border-primary-400/25` |
29
+ | `ring-base` | `ring-#8882` |
30
+ | `op-fade` | `op65 dark:op55` |
31
+ | `op-mute` | `op30 dark:op25` |
32
+ | `btn-action` | `border border-base rounded flex gap-2 items-center px2 py1 op75 hover:op100 hover:bg-active transition disabled:pointer-events-none disabled:op30! outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40` |
33
+ | `btn-action-sm` | `btn-action text-sm` |
34
+ | `btn-action-active` | `color-active border-active! bg-active op100!` |
35
+ | `btn-icon` | `w-9 h-9 rounded-full op-fade hover:op100 hover:bg-active transition flex items-center justify-center disabled:pointer-events-none disabled:op30 outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40` |
36
+ | `btn-icon-compact` | `w-6 h-6 rounded op-fade hover:op100 hover:bg-active transition flex items-center justify-center disabled:pointer-events-none disabled:op30 outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40` |
37
+ | `btn-primary` | `px3 py1.5 rounded flex gap-2 items-center bg-primary-500 hover:bg-primary-600 text-white transition disabled:op50 disabled:pointer-events-none outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40` |
38
+ | `badge` | `inline-flex items-center gap-1 px-1.5 py-0.5 rounded-md text-xs font-medium leading-none` |
39
+ | `badge-active` | `badge bg-active color-active` |
40
+ | `badge-muted` | `badge bg-#8881 color-muted` |
41
+
42
+ ### Severity scale
43
+
44
+ | Token | Expands to |
45
+ |---|---|
46
+ | `color-scale-neutral` | `text-gray-700 dark:text-gray-300` |
47
+ | `color-scale-low` | `text-lime-700 dark:text-lime-300 dark:saturate-75` |
48
+ | `color-scale-medium` | `text-amber-700 dark:text-amber-300 dark:saturate-90` |
49
+ | `color-scale-high` | `text-orange-700 dark:text-orange-300` |
50
+ | `color-scale-critical` | `text-red-700 dark:text-red-300` |
51
+
52
+ ### Type sizes
53
+
54
+ | Token | Expands to |
55
+ |---|---|
56
+ | `text-micro` | `text-[10px] leading-[1.4]` |
57
+ | `text-mini` | `text-[11px] leading-[1.45]` |
58
+ | `text-compact` | `text-[12px] leading-[1.5]` |
59
+
60
+ ### Named z-index layers
61
+
62
+ | Token | Expands to |
63
+ |---|---|
64
+ | `z-nav` | `z-[30]` |
65
+ | `z-dropdown` | `z-[40]` |
66
+ | `z-tooltip` | `z-[45]` |
67
+ | `z-toast` | `z-[50]` |
68
+ | `z-modal-backdrop` | `z-[60]` |
69
+ | `z-modal-content` | `z-[70]` |
70
+ | `z-drawer-backdrop` | `z-[80]` |
71
+ | `z-drawer-content` | `z-[90]` |
72
+
73
+ ### Dynamic
74
+
75
+ | Token | Expands to |
76
+ |---|---|
77
+ | `badge-color-<name>` | a chip tinted by any palette color name (dark-aware) |
78
+ | `bg-glass` / `bg-glass:<n>` | translucent surface + `backdrop-blur` |
79
+ | `bg-dots` / `bg-dots-<n>` | radial dot-grid background, variable cell size in px (default 16) |
80
+ | `bg-grid` / `bg-grid-<n>` | crosshatch grid-lines background, variable cell size in px (default 16) |
81
+ <!-- TOKENS:END -->
82
+
83
+ ## How to read it
84
+
85
+ - **Semantic shortcuts** (`bg-base`, `color-muted`, `border-base`, `op-fade`, …)
86
+ are the everyday vocabulary. They expand to the listed utilities and carry a
87
+ dark variant.
88
+ - **Composite shortcuts** (`btn-action`, `btn-primary`, `badge`) expand to a full
89
+ recipe — use them as-is.
90
+ - **Dynamic** `badge-color-<name>` tints a chip by any palette color name; `bg-glass`
91
+ / `bg-glass:<n>` makes a translucent blurred surface.
92
+ - **Severity** `color-scale-{neutral,low,medium,high,critical}` is the one ramp
93
+ for fresh→stale / fast→slow / small→large. Prefer the `colorize` prop on display
94
+ components over using these directly.
95
+ - **Named z-index layers** (`z-nav` < `z-dropdown` < `z-tooltip` < `z-toast` <
96
+ `z-modal-backdrop` < `z-modal-content` < `z-drawer-backdrop` < `z-drawer-content`)
97
+ — never raw `z-<n>`.
98
+ - **Theme**: `font-sans` = DM Sans, `font-mono` = DM Mono; extra sizes
99
+ `text-micro` / `text-mini` / `text-compact`; color ramps `primary` (default
100
+ antfu green), `warning`, `success`, `error`.
@@ -0,0 +1,27 @@
1
+ # Presenting data
2
+
3
+ Devtools UIs are mostly *data*. Reach for a display component before raw text —
4
+ they encode the formatting + severity decisions consistently.
5
+
6
+ | You have… | Use | Notes |
7
+ |---|---|---|
8
+ | a count / quantity | `DisplayNumber` / `DisplayNumberBadge` | `Intl` formatting, mono + tabular, prefix/suffix |
9
+ | a byte size | `DisplayBytes` | `base` 1024/1000, `colorize`, percent of `total` |
10
+ | an elapsed time | `DisplayDuration` | ms → human, `colorize` (fast=neutral … slow=critical) |
11
+ | a timestamp | `DisplayDate` | relative + exact tooltip, `colorize` by age, `live` |
12
+ | a module / file path | `DisplayFilePath` | truncates, dims directories, decodes `.pnpm` → `~`, icon |
13
+ | a package name | `DisplayPackageName` | scope colored by hash |
14
+ | a version | `DisplayVersion` | `vX.Y.Z` prefix logic |
15
+ | a proportion | `DisplayProportionBar` / `DisplayDonut` | stacked % bar / progress ring |
16
+ | a status / health | `DisplayStatusPill` | severity dot + label, `pulse` |
17
+ | a long list | `LayoutVirtualList` / `LayoutExpandableList` | virtualize, or "show N more" with a fade |
18
+
19
+ ## Rules of thumb
20
+
21
+ - **Colorize by meaning, once.** Turn on `colorize` for the one dimension that
22
+ carries signal (e.g. staleness in a list of dates); don't rainbow everything.
23
+ - **Right-align numeric columns** and keep them `tabular-nums` so they scan.
24
+ - **Truncate paths, never wrap them.** `DisplayFilePath` keeps the basename legible and
25
+ exposes the full path on hover.
26
+ - **Hash colors for identity, palette colors for category.** A package name gets
27
+ a stable hash color; a known type (esm/cjs) gets a fixed `badge-color-*`.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Opt-in fallback types for `splitpanes` — for consumers whose installed
3
+ * `splitpanes` predates bundled TypeScript types (v4.1.2+ ships its own, so you
4
+ * usually don't need this).
5
+ *
6
+ * It is deliberately NOT part of this package's own `tsconfig` (which would
7
+ * duplicate-declare against the real types). Consumers on an older splitpanes
8
+ * can opt in by adding to a `.d.ts` in their project:
9
+ *
10
+ * ```ts
11
+ * /// <reference types="@antfu/design/splitpanes.d.ts" />
12
+ * ```
13
+ */
14
+ declare module 'splitpanes' {
15
+ import type { DefineComponent } from 'vue'
16
+
17
+ export interface PaneData {
18
+ min: number
19
+ max: number
20
+ size: number
21
+ }
22
+
23
+ export interface Pane {
24
+ id: number
25
+ el: HTMLElement | null
26
+ min: number
27
+ max: number
28
+ givenSize: number | null
29
+ size: number
30
+ index: number
31
+ }
32
+
33
+ export interface SplitpanesReadyPayload {
34
+ panes: PaneData[]
35
+ }
36
+
37
+ export interface SplitpanesResizePayload {
38
+ event: MouseEvent | TouchEvent
39
+ index: number
40
+ prevPane: Pane | undefined
41
+ nextPane: Pane | undefined
42
+ panes: PaneData[]
43
+ }
44
+
45
+ /** `resized` fires on splitter drag-end AND after pane add/remove (latter omit event/index). */
46
+ export interface SplitpanesResizedPayload {
47
+ event?: MouseEvent | TouchEvent
48
+ index?: number
49
+ prevPane?: Pane
50
+ nextPane?: Pane
51
+ panes: PaneData[]
52
+ }
53
+
54
+ export interface SplitpanesProps {
55
+ horizontal?: boolean
56
+ pushOtherPanes?: boolean
57
+ maximizePanes?: boolean
58
+ rtl?: boolean
59
+ firstSplitter?: boolean
60
+ }
61
+
62
+ export interface PaneProps {
63
+ size?: number | string
64
+ minSize?: number | string
65
+ maxSize?: number | string
66
+ }
67
+
68
+ export const Splitpanes: DefineComponent<SplitpanesProps>
69
+ export const Pane: DefineComponent<PaneProps>
70
+ }