@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.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/a11y/cli.ts +73 -0
- package/a11y/index.ts +13 -0
- package/a11y/scan.ts +127 -0
- package/components/Action/ActionButton.stories.ts +56 -0
- package/components/Action/ActionButton.vue +57 -0
- package/components/Action/ActionDarkToggle.stories.ts +31 -0
- package/components/Action/ActionDarkToggle.vue +87 -0
- package/components/Action/ActionIconButton.stories.ts +47 -0
- package/components/Action/ActionIconButton.vue +47 -0
- package/components/Display/DisplayAvatar.stories.ts +36 -0
- package/components/Display/DisplayAvatar.vue +58 -0
- package/components/Display/DisplayBadge.stories.ts +31 -0
- package/components/Display/DisplayBadge.vue +98 -0
- package/components/Display/DisplayBytes.stories.ts +28 -0
- package/components/Display/DisplayBytes.vue +30 -0
- package/components/Display/DisplayDate.stories.ts +37 -0
- package/components/Display/DisplayDate.vue +29 -0
- package/components/Display/DisplayDonut.stories.ts +26 -0
- package/components/Display/DisplayDonut.vue +46 -0
- package/components/Display/DisplayDuration.stories.ts +28 -0
- package/components/Display/DisplayDuration.vue +28 -0
- package/components/Display/DisplayFileIcon.stories.ts +27 -0
- package/components/Display/DisplayFileIcon.vue +30 -0
- package/components/Display/DisplayFilePath.stories.ts +30 -0
- package/components/Display/DisplayFilePath.vue +61 -0
- package/components/Display/DisplayKbd.stories.ts +26 -0
- package/components/Display/DisplayKbd.vue +27 -0
- package/components/Display/DisplayKeyValue.stories.ts +56 -0
- package/components/Display/DisplayKeyValue.vue +51 -0
- package/components/Display/DisplayLabel.stories.ts +27 -0
- package/components/Display/DisplayLabel.vue +33 -0
- package/components/Display/DisplayNumber.stories.ts +27 -0
- package/components/Display/DisplayNumber.vue +24 -0
- package/components/Display/DisplayNumberBadge.stories.ts +26 -0
- package/components/Display/DisplayNumberBadge.vue +22 -0
- package/components/Display/DisplayPackageName.stories.ts +26 -0
- package/components/Display/DisplayPackageName.vue +49 -0
- package/components/Display/DisplayProgressBar.stories.ts +29 -0
- package/components/Display/DisplayProgressBar.vue +90 -0
- package/components/Display/DisplayProportionBar.stories.ts +40 -0
- package/components/Display/DisplayProportionBar.vue +43 -0
- package/components/Display/DisplaySafeImage.stories.ts +43 -0
- package/components/Display/DisplaySafeImage.vue +30 -0
- package/components/Display/DisplayStatusPill.stories.ts +34 -0
- package/components/Display/DisplayStatusPill.vue +42 -0
- package/components/Display/DisplayTree.stories.ts +76 -0
- package/components/Display/DisplayTree.vue +102 -0
- package/components/Display/DisplayVersion.stories.ts +25 -0
- package/components/Display/DisplayVersion.vue +21 -0
- package/components/Feedback/FeedbackEmptyState.stories.ts +38 -0
- package/components/Feedback/FeedbackEmptyState.vue +21 -0
- package/components/Feedback/FeedbackLoading.stories.ts +23 -0
- package/components/Feedback/FeedbackLoading.vue +21 -0
- package/components/Feedback/FeedbackSpinner.stories.ts +25 -0
- package/components/Feedback/FeedbackSpinner.vue +22 -0
- package/components/Feedback/FeedbackTip.stories.ts +34 -0
- package/components/Feedback/FeedbackTip.vue +29 -0
- package/components/Feedback/FeedbackToasts.stories.ts +40 -0
- package/components/Feedback/FeedbackToasts.vue +105 -0
- package/components/Form/FormCheckbox.stories.ts +36 -0
- package/components/Form/FormCheckbox.vue +30 -0
- package/components/Form/FormCombobox.stories.ts +35 -0
- package/components/Form/FormCombobox.vue +83 -0
- package/components/Form/FormField.stories.ts +56 -0
- package/components/Form/FormField.vue +36 -0
- package/components/Form/FormNumberInput.stories.ts +47 -0
- package/components/Form/FormNumberInput.vue +85 -0
- package/components/Form/FormRadioGroup.stories.ts +47 -0
- package/components/Form/FormRadioGroup.vue +43 -0
- package/components/Form/FormSearchField.stories.ts +22 -0
- package/components/Form/FormSearchField.vue +32 -0
- package/components/Form/FormSelect.stories.ts +47 -0
- package/components/Form/FormSelect.vue +56 -0
- package/components/Form/FormSwitch.stories.ts +36 -0
- package/components/Form/FormSwitch.vue +26 -0
- package/components/Form/FormTextInput.stories.ts +39 -0
- package/components/Form/FormTextInput.vue +51 -0
- package/components/Form/FormTextarea.stories.ts +47 -0
- package/components/Form/FormTextarea.vue +32 -0
- package/components/Layout/LayoutBreadcrumb.stories.ts +54 -0
- package/components/Layout/LayoutBreadcrumb.vue +54 -0
- package/components/Layout/LayoutCard.stories.ts +31 -0
- package/components/Layout/LayoutCard.vue +21 -0
- package/components/Layout/LayoutDataTable.stories.ts +77 -0
- package/components/Layout/LayoutDataTable.vue +145 -0
- package/components/Layout/LayoutExpandableList.stories.ts +28 -0
- package/components/Layout/LayoutExpandableList.vue +94 -0
- package/components/Layout/LayoutPanelGrids.stories.ts +28 -0
- package/components/Layout/LayoutPanelGrids.vue +26 -0
- package/components/Layout/LayoutSectionBlock.stories.ts +37 -0
- package/components/Layout/LayoutSectionBlock.vue +37 -0
- package/components/Layout/LayoutSideNav.stories.ts +33 -0
- package/components/Layout/LayoutSideNav.vue +48 -0
- package/components/Layout/LayoutSplitPane.stories.ts +44 -0
- package/components/Layout/LayoutSplitPane.vue +30 -0
- package/components/Layout/LayoutTabs.stories.ts +43 -0
- package/components/Layout/LayoutTabs.vue +56 -0
- package/components/Layout/LayoutToolbar.stories.ts +60 -0
- package/components/Layout/LayoutToolbar.vue +28 -0
- package/components/Layout/LayoutVirtualList.stories.ts +30 -0
- package/components/Layout/LayoutVirtualList.vue +82 -0
- package/components/Overlay/OverlayDrawer.stories.ts +47 -0
- package/components/Overlay/OverlayDrawer.vue +58 -0
- package/components/Overlay/OverlayDropdown.stories.ts +25 -0
- package/components/Overlay/OverlayDropdown.vue +30 -0
- package/components/Overlay/OverlayDropdownItem.stories.ts +26 -0
- package/components/Overlay/OverlayDropdownItem.vue +31 -0
- package/components/Overlay/OverlayDropdownLabel.vue +9 -0
- package/components/Overlay/OverlayDropdownSeparator.vue +7 -0
- package/components/Overlay/OverlayModal.stories.ts +33 -0
- package/components/Overlay/OverlayModal.vue +48 -0
- package/components/Overlay/OverlayTooltip.stories.ts +33 -0
- package/components/Overlay/OverlayTooltip.vue +38 -0
- package/composables/colorScheme.ts +58 -0
- package/composables/toast.ts +81 -0
- package/package.json +99 -0
- package/skills/antfu-design/SKILL.md +65 -0
- package/skills/antfu-design/references/advanced-patterns.md +39 -0
- package/skills/antfu-design/references/best-practices.md +54 -0
- package/skills/antfu-design/references/core-components.md +72 -0
- package/skills/antfu-design/references/core-setup.md +56 -0
- package/skills/antfu-design/references/core-tokens.md +100 -0
- package/skills/antfu-design/references/features-data-presentation.md +27 -0
- package/splitpanes.d.ts +70 -0
- package/styles/animations.css +47 -0
- package/styles/base.css +31 -0
- package/styles/floating-vue.css +28 -0
- package/styles/index.css +7 -0
- package/styles/reka-ui.css +112 -0
- package/styles/scrollbar.css +24 -0
- package/styles/splitpanes.css +61 -0
- package/unocss/colors.ts +127 -0
- package/unocss/index.ts +99 -0
- package/unocss/options.ts +31 -0
- package/unocss/patterns.ts +38 -0
- package/unocss/rules.ts +26 -0
- package/unocss/severity.ts +16 -0
- package/unocss/shortcuts.ts +68 -0
- package/utils/color.ts +328 -0
- package/utils/contrast.ts +118 -0
- package/utils/format.ts +389 -0
- package/utils/icon.ts +200 -0
- package/utils/index.ts +13 -0
- package/utils/keybinding.ts +199 -0
- package/utils/misc.ts +141 -0
- package/utils/path.ts +243 -0
- package/utils/semver.ts +147 -0
- 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
|
+
}
|
package/styles/base.css
ADDED
|
@@ -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
|
+
}
|
package/styles/index.css
ADDED
|
@@ -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
|
+
}
|
package/unocss/colors.ts
ADDED
|
@@ -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
|
+
}
|
package/unocss/index.ts
ADDED
|
@@ -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
|
+
]
|
package/unocss/rules.ts
ADDED
|
@@ -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
|
+
]
|