@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,47 @@
1
+ /** Spinner keyframes + the dark-mode view-transition circular reveal. */
2
+
3
+ @keyframes af-spin {
4
+ to {
5
+ transform: rotate(360deg);
6
+ }
7
+ }
8
+
9
+ @keyframes af-spin-reverse {
10
+ to {
11
+ transform: rotate(-360deg);
12
+ }
13
+ }
14
+
15
+ .af-spin {
16
+ animation: af-spin 1s linear infinite;
17
+ }
18
+
19
+ /* View Transitions for the animated dark toggle (see ActionDarkToggle). */
20
+ ::view-transition-old(root),
21
+ ::view-transition-new(root) {
22
+ animation: none;
23
+ mix-blend-mode: normal;
24
+ }
25
+
26
+ ::view-transition-old(root) {
27
+ z-index: 1;
28
+ }
29
+
30
+ ::view-transition-new(root) {
31
+ z-index: 9999;
32
+ }
33
+
34
+ html.dark::view-transition-old(root) {
35
+ z-index: 9999;
36
+ }
37
+
38
+ html.dark::view-transition-new(root) {
39
+ z-index: 1;
40
+ }
41
+
42
+ @media (prefers-reduced-motion: reduce) {
43
+ ::view-transition-old(root),
44
+ ::view-transition-new(root) {
45
+ animation: none !important;
46
+ }
47
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Root surface + a small set of CSS custom properties mirroring the token
3
+ * defaults. The overlay-engine override files reference these vars, so they
4
+ * recolor with light/dark automatically and work on plain `import` (without
5
+ * needing UnoCSS to process them).
6
+ */
7
+
8
+ :root {
9
+ --af-bg-base: #ffffff;
10
+ --af-bg-secondary: #f5f5f5;
11
+ --af-color-base: #262626; /* neutral-800 */
12
+ --af-color-muted: #525252; /* neutral-600 */
13
+ --af-border-base: rgba(136, 136, 136, 0.13); /* ~#8882 */
14
+ --af-border-mute: rgba(136, 136, 136, 0.07); /* ~#8881 */
15
+ --af-tooltip-bg: rgba(255, 255, 255, 0.75);
16
+ color-scheme: light;
17
+ }
18
+
19
+ html.dark {
20
+ --af-bg-base: #111111;
21
+ --af-bg-secondary: #1a1a1a;
22
+ --af-color-base: #e5e5e5; /* neutral-200 */
23
+ --af-color-muted: #a3a3a3; /* neutral-400 */
24
+ --af-tooltip-bg: rgba(17, 17, 17, 0.75);
25
+ color-scheme: dark;
26
+ }
27
+
28
+ html {
29
+ background-color: var(--af-bg-base);
30
+ color: var(--af-color-base);
31
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Recolor floating-vue tooltips/dropdowns to the design tokens. Driven by the
3
+ * `--af-*` custom properties from `base.css` so they follow light/dark.
4
+ */
5
+
6
+ .v-popper__popper .v-popper__inner {
7
+ background: var(--af-tooltip-bg);
8
+ color: var(--af-color-base);
9
+ border: 1px solid var(--af-border-base);
10
+ border-radius: 6px;
11
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
12
+ backdrop-filter: blur(8px);
13
+ padding: 4px 8px;
14
+ font-size: 12px;
15
+ }
16
+
17
+ .v-popper__popper .v-popper__arrow-outer {
18
+ border-color: var(--af-border-base);
19
+ }
20
+
21
+ .v-popper__popper .v-popper__arrow-inner {
22
+ border-color: var(--af-tooltip-bg);
23
+ visibility: visible;
24
+ }
25
+
26
+ .v-popper--theme-dropdown .v-popper__inner {
27
+ padding: 4px;
28
+ }
@@ -0,0 +1,7 @@
1
+ /* All design-system styles. Cherry-pick individual files instead if preferred. */
2
+ @import './base.css';
3
+ @import './scrollbar.css';
4
+ @import './animations.css';
5
+ @import './reka-ui.css';
6
+ @import './floating-vue.css';
7
+ @import './splitpanes.css';
@@ -0,0 +1,112 @@
1
+ /**
2
+ * reka-ui ships headless (unstyled) — most styling lives on the components'
3
+ * own token classes. This file carries only the shared niceties: open/close
4
+ * transitions for overlays, and locking background scroll behind modals.
5
+ */
6
+
7
+ [data-reka-popper-content-wrapper] {
8
+ z-index: 70;
9
+ }
10
+
11
+ /*
12
+ * Enter/exit animations. reka keeps the element mounted until a CSS *animation*
13
+ * finishes (its Presence watches `animation-name`), so these use `@keyframes`
14
+ * rather than transitions — otherwise the close (exit) frame never plays.
15
+ */
16
+
17
+ /* Backdrops + simple poppers (dropdowns): fade. */
18
+ [data-af-animate][data-state='open'] {
19
+ animation: af-fade-in 0.15s ease both;
20
+ }
21
+ [data-af-animate][data-state='closed'] {
22
+ animation: af-fade-out 0.15s ease both;
23
+ }
24
+
25
+ /* Modal content: fade + scale, keeping the translate(-50%, -50%) centering. */
26
+ [data-af-modal][data-state='open'] {
27
+ animation: af-modal-in 0.18s cubic-bezier(0.16, 1, 0.3, 1) both;
28
+ }
29
+ [data-af-modal][data-state='closed'] {
30
+ animation: af-modal-out 0.15s ease both;
31
+ }
32
+
33
+ /* Drawer content: slide from its edge; reverse on close. */
34
+ [data-af-drawer] {
35
+ animation-duration: 0.22s;
36
+ animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
37
+ animation-fill-mode: both;
38
+ }
39
+ [data-af-drawer][data-side='right'] {
40
+ animation-name: af-slide-right;
41
+ }
42
+ [data-af-drawer][data-side='left'] {
43
+ animation-name: af-slide-left;
44
+ }
45
+ [data-af-drawer][data-side='top'] {
46
+ animation-name: af-slide-top;
47
+ }
48
+ [data-af-drawer][data-side='bottom'] {
49
+ animation-name: af-slide-bottom;
50
+ }
51
+ [data-af-drawer][data-state='closed'] {
52
+ animation-direction: reverse;
53
+ }
54
+
55
+ @keyframes af-fade-in {
56
+ from {
57
+ opacity: 0;
58
+ }
59
+ }
60
+ @keyframes af-fade-out {
61
+ to {
62
+ opacity: 0;
63
+ }
64
+ }
65
+ @keyframes af-modal-in {
66
+ from {
67
+ opacity: 0;
68
+ transform: translate(-50%, -50%) scale(0.96);
69
+ }
70
+ to {
71
+ opacity: 1;
72
+ transform: translate(-50%, -50%) scale(1);
73
+ }
74
+ }
75
+ @keyframes af-modal-out {
76
+ from {
77
+ opacity: 1;
78
+ transform: translate(-50%, -50%) scale(1);
79
+ }
80
+ to {
81
+ opacity: 0;
82
+ transform: translate(-50%, -50%) scale(0.96);
83
+ }
84
+ }
85
+ @keyframes af-slide-right {
86
+ from {
87
+ transform: translateX(100%);
88
+ }
89
+ }
90
+ @keyframes af-slide-left {
91
+ from {
92
+ transform: translateX(-100%);
93
+ }
94
+ }
95
+ @keyframes af-slide-top {
96
+ from {
97
+ transform: translateY(-100%);
98
+ }
99
+ }
100
+ @keyframes af-slide-bottom {
101
+ from {
102
+ transform: translateY(100%);
103
+ }
104
+ }
105
+
106
+ @media (prefers-reduced-motion: reduce) {
107
+ [data-af-animate],
108
+ [data-af-modal],
109
+ [data-af-drawer] {
110
+ animation: none !important;
111
+ }
112
+ }
@@ -0,0 +1,24 @@
1
+ /** Thin, unobtrusive scrollbar that follows the theme. */
2
+
3
+ * {
4
+ scrollbar-width: thin;
5
+ scrollbar-color: rgba(136, 136, 136, 0.35) transparent;
6
+ }
7
+
8
+ *::-webkit-scrollbar {
9
+ width: 6px;
10
+ height: 6px;
11
+ }
12
+
13
+ *::-webkit-scrollbar-track {
14
+ background: transparent;
15
+ }
16
+
17
+ *::-webkit-scrollbar-thumb {
18
+ background: rgba(136, 136, 136, 0.3);
19
+ border-radius: 3px;
20
+ }
21
+
22
+ *::-webkit-scrollbar-thumb:hover {
23
+ background: rgba(136, 136, 136, 0.5);
24
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Splitpanes — base layout (so the `<SplitPane>` component works without
3
+ * importing the dependency's CSS) plus theming via the design tokens.
4
+ */
5
+
6
+ .splitpanes {
7
+ display: flex;
8
+ width: 100%;
9
+ height: 100%;
10
+ }
11
+ .splitpanes--vertical {
12
+ flex-direction: row;
13
+ }
14
+ .splitpanes--horizontal {
15
+ flex-direction: column;
16
+ }
17
+ .splitpanes--dragging * {
18
+ user-select: none;
19
+ }
20
+ .splitpanes__pane {
21
+ width: 100%;
22
+ height: 100%;
23
+ overflow: hidden;
24
+ }
25
+ .splitpanes--vertical > .splitpanes__pane {
26
+ transition: width 0.2s ease-out;
27
+ }
28
+ .splitpanes--horizontal > .splitpanes__pane {
29
+ transition: height 0.2s ease-out;
30
+ }
31
+ .splitpanes--dragging > .splitpanes__pane {
32
+ transition: none;
33
+ pointer-events: none;
34
+ }
35
+
36
+ /* Splitter — recolored to the design tokens. */
37
+ .splitpanes__splitter {
38
+ position: relative;
39
+ touch-action: none;
40
+ background-color: var(--af-border-base);
41
+ transition: background-color 0.2s;
42
+ }
43
+ .splitpanes__splitter:hover {
44
+ background-color: rgba(136, 136, 136, 0.4);
45
+ }
46
+ .splitpanes--vertical > .splitpanes__splitter {
47
+ min-width: 1px;
48
+ cursor: col-resize;
49
+ }
50
+ .splitpanes--horizontal > .splitpanes__splitter {
51
+ min-height: 1px;
52
+ cursor: row-resize;
53
+ }
54
+
55
+ /* Widen the invisible hit area so the 1px splitter is easy to grab. */
56
+ .splitpanes__splitter::before {
57
+ content: '';
58
+ position: absolute;
59
+ inset: -4px;
60
+ z-index: 1;
61
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Color ramps for the design system.
3
+ *
4
+ * Defaults are hand-tuned for AA contrast in both light and dark mode (the
5
+ * a11y contrast check guards this). A `generateColorRamp` helper produces a
6
+ * usable ramp from a single hex when a consumer overrides `primary` with a
7
+ * plain string rather than a full scale object — stepped in OKLCH via colorjs.io.
8
+ */
9
+ import Color from 'colorjs.io'
10
+
11
+ export type ColorRamp = Record<string | number, string>
12
+
13
+ /** Default antfu green — the brand color, used as `primary` unless overridden. */
14
+ export const primaryGreen: ColorRamp = {
15
+ 50: '#f4f9f1',
16
+ 100: '#e6f2e0',
17
+ 200: '#cde4c2',
18
+ 300: '#a8cf96',
19
+ 400: '#7eb267',
20
+ 500: '#5b9544',
21
+ 600: '#49833e',
22
+ 700: '#3b6832',
23
+ 800: '#31532b',
24
+ 900: '#2a4526',
25
+ 950: '#132611',
26
+ DEFAULT: '#49833e',
27
+ }
28
+
29
+ /**
30
+ * Semantic ramps sourced from `eslint/config-inspector` (the most complete
31
+ * reference set among the source projects). Added under semantic names so they
32
+ * never clobber the base preset's built-in palette (`amber`, `green`, `red`).
33
+ */
34
+ export const warning: ColorRamp = {
35
+ 50: '#fffaeb',
36
+ 100: '#fef0c7',
37
+ 200: '#fedf89',
38
+ 300: '#fec84b',
39
+ 400: '#fdb022',
40
+ 500: '#f79009',
41
+ 600: '#dc6803',
42
+ 700: '#b54708',
43
+ 800: '#93370d',
44
+ 900: '#7a2e0e',
45
+ 950: '#4e1d09',
46
+ DEFAULT: '#f79009',
47
+ }
48
+
49
+ export const success: ColorRamp = {
50
+ 50: '#ecfdf3',
51
+ 100: '#d1fadf',
52
+ 200: '#a6f4c5',
53
+ 300: '#6ce9a6',
54
+ 400: '#32d583',
55
+ 500: '#12b76a',
56
+ 600: '#039855',
57
+ 700: '#027a48',
58
+ 800: '#05603a',
59
+ 900: '#054f31',
60
+ 950: '#03281a',
61
+ DEFAULT: '#12b76a',
62
+ }
63
+
64
+ export const error: ColorRamp = {
65
+ 50: '#fff1f3',
66
+ 100: '#ffe4e8',
67
+ 200: '#fecdd6',
68
+ 300: '#fea3b4',
69
+ 400: '#fd6f8e',
70
+ 500: '#f63d68',
71
+ 600: '#e31b54',
72
+ 700: '#c01048',
73
+ 800: '#a11043',
74
+ 900: '#89123e',
75
+ 950: '#510322',
76
+ DEFAULT: '#f63d68',
77
+ }
78
+
79
+ const RAMP_STOPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const
80
+ /** Target OKLCH lightness (0–1) per stop. */
81
+ const RAMP_LIGHTNESS = [0.97, 0.94, 0.87, 0.78, 0.68, 0.58, 0.50, 0.42, 0.35, 0.28, 0.18] as const
82
+
83
+ /**
84
+ * Generate an 11-stop color ramp (`50`..`950` + `DEFAULT`) from a single color,
85
+ * preserving its OKLCH hue while stepping lightness across fixed stops (chroma is
86
+ * tapered toward the extremes so tints/shades stay clean). Stepped in OKLCH via
87
+ * colorjs.io for perceptually even results. Used when `primary` is a string.
88
+ *
89
+ * @param input - The base color as any CSS color string; becomes the ramp's `DEFAULT`.
90
+ * @returns A ramp keyed `50`–`950` plus `DEFAULT`.
91
+ *
92
+ * @example
93
+ * generateColorRamp('#0969da')
94
+ * // → { DEFAULT: '#0969da', 50: '#…', … , 950: '#…' }
95
+ */
96
+ export function generateColorRamp(input: string): ColorRamp {
97
+ const [, chroma, rawHue] = new Color(input).to('oklch').coords
98
+ const hue = Number.isFinite(rawHue) ? rawHue : 0
99
+ const ramp: ColorRamp = { DEFAULT: input }
100
+ RAMP_STOPS.forEach((stop, i) => {
101
+ const l = RAMP_LIGHTNESS[i]
102
+ // Taper chroma at the lightest/darkest stops so they don't look muddy.
103
+ const c = (chroma || 0) * (l > 0.9 || l < 0.25 ? 0.6 : 1)
104
+ ramp[stop] = new Color('oklch', [l, c, hue]).to('srgb').toGamut({ space: 'srgb' }).toString({ format: 'hex' })
105
+ })
106
+ return ramp
107
+ }
108
+
109
+ /**
110
+ * Normalize the `primary` option into a full color ramp: a string is expanded
111
+ * via {@link generateColorRamp}, a ramp object passes through, and `undefined`
112
+ * falls back to the default antfu green.
113
+ *
114
+ * @param primary - A hex string, a full ramp, or `undefined`.
115
+ * @returns The resolved {@link ColorRamp}.
116
+ *
117
+ * @example
118
+ * resolvePrimary() // → primaryGreen
119
+ * resolvePrimary('#0969da') // → generated ramp with DEFAULT '#0969da'
120
+ */
121
+ export function resolvePrimary(primary?: string | ColorRamp): ColorRamp {
122
+ if (!primary)
123
+ return primaryGreen
124
+ if (typeof primary === 'string')
125
+ return generateColorRamp(primary)
126
+ return primary
127
+ }
@@ -0,0 +1,99 @@
1
+ import type { Preset, UserShortcuts } from '@unocss/core'
2
+ import type { PresetAnthonyDesignOptions } from './options'
3
+ import { definePreset, mergeDeep } from '@unocss/core'
4
+ import { error, resolvePrimary, success, warning } from './colors'
5
+ import { DEFAULT_DARK_BG, DEFAULT_FONTS } from './options'
6
+ import { patternRules } from './patterns'
7
+ import { buildRules } from './rules'
8
+ import { severityShortcuts } from './severity'
9
+ import { buildShortcuts } from './shortcuts'
10
+
11
+ function assertOptions(options: PresetAnthonyDesignOptions): void {
12
+ const { primary, darkBackground } = options
13
+ if (primary != null && typeof primary !== 'string' && typeof primary !== 'object')
14
+ throw new TypeError(`[@antfu/design] \`primary\` must be a hex string or a color scale object, got ${typeof primary}.`)
15
+ if (darkBackground != null && typeof darkBackground !== 'string')
16
+ throw new TypeError(`[@antfu/design] \`darkBackground\` must be a CSS color string, got ${typeof darkBackground}.`)
17
+ }
18
+
19
+ /**
20
+ * `presetAnthonyDesign` — the **single** antfu design preset.
21
+ *
22
+ * One preset contributes the whole design layer: the theme scales (a `primary`
23
+ * ramp + `warning`/`success`/`error` + fonts), the semantic `*-base` shortcuts,
24
+ * the dynamic `badge-color-*` / `bg-glass` shortcuts, and the `color-scale-*`
25
+ * severity layer. It bundles **no** base preset, icons, web-fonts or reset — the
26
+ * consumer composes those themselves. The semantic layer is base-agnostic, so it
27
+ * resolves under Wind4, Wind3 or Mini.
28
+ *
29
+ * @param options - Theme + dark-surface options (see {@link PresetAnthonyDesignOptions}).
30
+ * @returns A single UnoCSS `Preset`.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { presetAnthonyDesign } from '@antfu/design/unocss'
35
+ * import { defineConfig, presetIcons, presetWebFonts, presetWind4 } from 'unocss'
36
+ *
37
+ * export default defineConfig({
38
+ * presets: [
39
+ * presetAnthonyDesign({ primary: '#49833E' }),
40
+ * presetWind4(), // a base preset is required — bring your own
41
+ * presetIcons(),
42
+ * presetWebFonts({ fonts: { sans: 'DM Sans', mono: 'DM Mono' } }),
43
+ * ],
44
+ * })
45
+ * ```
46
+ */
47
+ export const presetAnthonyDesign = definePreset((options: PresetAnthonyDesignOptions = {}): Preset => {
48
+ assertOptions(options)
49
+
50
+ const darkBackground = options.darkBackground ?? DEFAULT_DARK_BG
51
+ const primary = resolvePrimary(options.primary)
52
+ const fonts = { ...DEFAULT_FONTS, ...options.fonts }
53
+
54
+ const themeOverrides = mergeDeep(
55
+ {
56
+ colors: { primary, warning, success, error },
57
+ fontFamily: { sans: fonts.sans, mono: fonts.mono },
58
+ } as Record<string, any>,
59
+ (options.theme ?? {}) as Record<string, any>,
60
+ )
61
+
62
+ // Appended last so consumers can override the built-in layer precisely.
63
+ const extend = options.extendShortcuts == null
64
+ ? []
65
+ : Array.isArray(options.extendShortcuts)
66
+ ? options.extendShortcuts
67
+ : [options.extendShortcuts]
68
+
69
+ const shortcuts: UserShortcuts = [
70
+ ...buildShortcuts(darkBackground),
71
+ ...buildRules(darkBackground),
72
+ ...severityShortcuts,
73
+ ...extend,
74
+ ]
75
+
76
+ return {
77
+ name: '@antfu/design',
78
+ extendTheme: theme => mergeDeep(theme as any, themeOverrides as any),
79
+ shortcuts,
80
+ rules: patternRules,
81
+ }
82
+ })
83
+
84
+ export default presetAnthonyDesign
85
+
86
+ export {
87
+ type ColorRamp,
88
+ error as errorRamp,
89
+ generateColorRamp,
90
+ primaryGreen,
91
+ resolvePrimary,
92
+ success as successRamp,
93
+ warning as warningRamp,
94
+ } from './colors'
95
+
96
+ export type {
97
+ PresetAnthonyDesignOptions,
98
+ PresetAnthonyFonts,
99
+ } from './options'
@@ -0,0 +1,31 @@
1
+ import type { UserShortcuts } from '@unocss/core'
2
+ import type { ColorRamp } from './colors'
3
+
4
+ /** Default near-black used for dark surfaces. Overridable via `darkBackground`. */
5
+ export const DEFAULT_DARK_BG = '#111'
6
+
7
+ export const DEFAULT_FONTS = {
8
+ sans: 'DM Sans',
9
+ mono: 'DM Mono',
10
+ } as const
11
+
12
+ export interface PresetAnthonyFonts {
13
+ sans?: string
14
+ mono?: string
15
+ }
16
+
17
+ export interface PresetAnthonyDesignOptions {
18
+ /**
19
+ * Primary brand color — a single hex (a ramp is generated) or a full
20
+ * `{ 50..950, DEFAULT }` scale object. Defaults to the antfu green.
21
+ */
22
+ primary?: string | ColorRamp
23
+ /** Near-black for dark surfaces (`bg-base`, `bg-tooltip`, `bg-glass`, …). Default `#111`. */
24
+ darkBackground?: string
25
+ /** Font families. Defaults to DM Sans / DM Mono. The web fonts are the consumer's to load. */
26
+ fonts?: PresetAnthonyFonts
27
+ /** Extra theme fields, deep-merged into the generated theme. */
28
+ theme?: Record<string, any>
29
+ /** Extra shortcuts appended after the built-in layer (so they can override it). */
30
+ extendShortcuts?: UserShortcuts
31
+ }
@@ -0,0 +1,38 @@
1
+ import type { Rule } from '@unocss/core'
2
+
3
+ const DOT_COLOR = 'rgba(136, 136, 136, 0.25)'
4
+ const GRID_COLOR = 'rgba(136, 136, 136, 0.15)'
5
+
6
+ /**
7
+ * Background-pattern rules with a variable cell size in px:
8
+ *
9
+ * - `bg-dots` / `bg-dots-<n>` — a radial dot grid (default 16px).
10
+ * - `bg-grid` / `bg-grid-<n>` — crosshatch grid lines (default 16px).
11
+ *
12
+ * Real UnoCSS rules (they emit `background-image` + `background-size`), so the
13
+ * size is dynamic — `bg-dots-24`, `bg-grid-32`, … — rather than a fixed class.
14
+ * The dot/line color is a theme-neutral gray that reads in light and dark.
15
+ *
16
+ * @example
17
+ * // bg-dots → 16px dot grid
18
+ * // bg-dots-24 → 24px dot grid
19
+ * // bg-grid-32 → 32px grid lines
20
+ */
21
+ export const patternRules: Rule[] = [
22
+ [
23
+ /^bg-dots(?:-(\d+))?$/,
24
+ ([, size = '16']) => ({
25
+ 'background-image': `radial-gradient(${DOT_COLOR} 1px, transparent 1px)`,
26
+ 'background-size': `${size}px ${size}px`,
27
+ }),
28
+ { layer: 'default' },
29
+ ],
30
+ [
31
+ /^bg-grid(?:-(\d+))?$/,
32
+ ([, size = '16']) => ({
33
+ 'background-image': `linear-gradient(to right, ${GRID_COLOR} 1px, transparent 1px), linear-gradient(to bottom, ${GRID_COLOR} 1px, transparent 1px)`,
34
+ 'background-size': `${size}px ${size}px`,
35
+ }),
36
+ { layer: 'default' },
37
+ ],
38
+ ]
@@ -0,0 +1,26 @@
1
+ import type { DynamicShortcut, StaticShortcutMap } from '@unocss/core'
2
+
3
+ /**
4
+ * Dynamic, name-driven design shortcuts.
5
+ *
6
+ * In UnoCSS terms these expand to *other utility classes*, so they are dynamic
7
+ * shortcuts rather than raw-CSS rules — but they are the "dynamic rules" the
8
+ * design system exposes: `badge-color-<name>` and `bg-glass(:<n>)`.
9
+ */
10
+ export function buildRules(db: string): (StaticShortcutMap | DynamicShortcut)[] {
11
+ return [
12
+ // `badge-color-green`, `badge-color-blue`, … — a chip tinted by color name,
13
+ // dark-aware. The deterministic formula shared across the source projects.
14
+ [
15
+ /^badge-color-(\w+)$/,
16
+ ([, color]) => `bg-${color}-400/20 dark:bg-${color}-400/10 text-${color}-700 dark:text-${color}-300 border border-${color}-600/15 dark:border-${color}-300/15`,
17
+ { layer: 'shortcuts' },
18
+ ],
19
+ // `bg-glass` / `bg-glass:75` — translucent surface + backdrop blur.
20
+ [
21
+ /^bg-glass(?::(\d+))?$/,
22
+ ([, opacity = '50']) => `bg-white/${opacity} dark:bg-${db}/${opacity} backdrop-blur-7`,
23
+ { layer: 'shortcuts' },
24
+ ],
25
+ ]
26
+ }
@@ -0,0 +1,16 @@
1
+ import type { DynamicShortcut, StaticShortcutMap } from '@unocss/core'
2
+
3
+ /**
4
+ * Severity / freshness color scale, dark-aware (gray → lime → amber → orange →
5
+ * red). Unifies the duplicated severity/age/staleness ramps across the source
6
+ * projects (`color-scale-*`).
7
+ */
8
+ export const severityShortcuts: (StaticShortcutMap | DynamicShortcut)[] = [
9
+ {
10
+ 'color-scale-neutral': 'text-gray-700 dark:text-gray-300',
11
+ 'color-scale-low': 'text-lime-700 dark:text-lime-300 dark:saturate-75',
12
+ 'color-scale-medium': 'text-amber-700 dark:text-amber-300 dark:saturate-90',
13
+ 'color-scale-high': 'text-orange-700 dark:text-orange-300',
14
+ 'color-scale-critical': 'text-red-700 dark:text-red-300',
15
+ },
16
+ ]