@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,68 @@
|
|
|
1
|
+
import type { DynamicShortcut, StaticShortcutMap } from '@unocss/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the semantic shortcut map. Written **base-agnostic** (uses only
|
|
5
|
+
* `/opacity` modifiers + hex-with-alpha, never colon-opacity) so it resolves
|
|
6
|
+
* identically under Wind4, Wind3 and Mini — the `shared-shortcuts.ts` from
|
|
7
|
+
* `@vitejs/devtools-ui` is the proven template this generalizes.
|
|
8
|
+
*
|
|
9
|
+
* `db` is the configurable near-black for dark surfaces.
|
|
10
|
+
*/
|
|
11
|
+
export function buildShortcuts(db: string): (StaticShortcutMap | DynamicShortcut)[] {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
// ── Text ──────────────────────────────────────────────────────────
|
|
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
|
+
|
|
20
|
+
// ── Surfaces ──────────────────────────────────────────────────────
|
|
21
|
+
'bg-base': `bg-white dark:bg-${db}`,
|
|
22
|
+
'bg-secondary': 'bg-#f5f5f5 dark:bg-#1a1a1a',
|
|
23
|
+
'bg-active': 'bg-#8881',
|
|
24
|
+
'bg-hover': 'bg-primary/5',
|
|
25
|
+
'bg-code': 'bg-gray-500/5',
|
|
26
|
+
'bg-tooltip': `bg-white/75 dark:bg-${db}/75 backdrop-blur-8`,
|
|
27
|
+
'bg-gradient-more': `bg-gradient-to-t from-white via-white/80 to-white/0 dark:from-${db} dark:via-${db}/80 dark:to-${db}/0`,
|
|
28
|
+
|
|
29
|
+
// ── Borders / rings ───────────────────────────────────────────────
|
|
30
|
+
'border-base': 'border-#8882',
|
|
31
|
+
'border-mute': 'border-#8881',
|
|
32
|
+
'border-active': 'border-primary-600/25 dark:border-primary-400/25',
|
|
33
|
+
'ring-base': 'ring-#8882',
|
|
34
|
+
|
|
35
|
+
// ── Opacity ───────────────────────────────────────────────────────
|
|
36
|
+
'op-fade': 'op65 dark:op55',
|
|
37
|
+
'op-mute': 'op30 dark:op25',
|
|
38
|
+
|
|
39
|
+
// ── Buttons ───────────────────────────────────────────────────────
|
|
40
|
+
'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',
|
|
41
|
+
'btn-action-sm': 'btn-action text-sm',
|
|
42
|
+
'btn-action-active': 'color-active border-active! bg-active op100!',
|
|
43
|
+
'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',
|
|
44
|
+
'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',
|
|
45
|
+
'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',
|
|
46
|
+
|
|
47
|
+
// ── Badges ────────────────────────────────────────────────────────
|
|
48
|
+
'badge': 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded-md text-xs font-medium leading-none',
|
|
49
|
+
'badge-active': 'badge bg-active color-active',
|
|
50
|
+
'badge-muted': 'badge bg-#8881 color-muted',
|
|
51
|
+
|
|
52
|
+
// ── Type sizes (base-agnostic) ────────────────────────────────────
|
|
53
|
+
'text-micro': 'text-[10px] leading-[1.4]',
|
|
54
|
+
'text-mini': 'text-[11px] leading-[1.45]',
|
|
55
|
+
'text-compact': 'text-[12px] leading-[1.5]',
|
|
56
|
+
|
|
57
|
+
// ── Named z-index layers (ascending) ──────────────────────────────
|
|
58
|
+
'z-nav': 'z-[30]',
|
|
59
|
+
'z-dropdown': 'z-[40]',
|
|
60
|
+
'z-tooltip': 'z-[45]',
|
|
61
|
+
'z-toast': 'z-[50]',
|
|
62
|
+
'z-modal-backdrop': 'z-[60]',
|
|
63
|
+
'z-modal-content': 'z-[70]',
|
|
64
|
+
'z-drawer-backdrop': 'z-[80]',
|
|
65
|
+
'z-drawer-content': 'z-[90]',
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
}
|
package/utils/color.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic string → HSL color utilities.
|
|
3
|
+
*
|
|
4
|
+
* Hue-based so the same string always maps to the same hue, with saturation /
|
|
5
|
+
* lightness chosen to "adapt better contrast in light/dark mode" (the note from
|
|
6
|
+
* config-inspector's color composable). Pure and framework-agnostic — the
|
|
7
|
+
* dark-mode flag is an explicit argument so these stay unit-testable without a
|
|
8
|
+
* DOM. Vue callers pass the scheme flag (e.g. `colorScheme === 'dark'`).
|
|
9
|
+
*
|
|
10
|
+
* Color-space manipulation (the OKLCH `labelStyle`) is delegated to colorjs.io.
|
|
11
|
+
*/
|
|
12
|
+
import Color from 'colorjs.io'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build an `hsla()` string for a given hue, dark-aware.
|
|
16
|
+
*
|
|
17
|
+
* Saturation and lightness are chosen per scheme (65%/40% in light mode, 50%/60%
|
|
18
|
+
* in dark) so the resulting color keeps reasonable contrast against the page.
|
|
19
|
+
*
|
|
20
|
+
* @param hue - The hue in degrees (0–360).
|
|
21
|
+
* @param opacity - The alpha channel, a number or CSS string. Defaults to `1`.
|
|
22
|
+
* @param dark - Whether to use the dark-mode saturation/lightness. Defaults to `false`.
|
|
23
|
+
* @returns An `hsla(...)` CSS color string.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* getHsla(180) // → 'hsla(180, 65%, 40%, 1)'
|
|
27
|
+
* getHsla(180, 0.5) // → 'hsla(180, 65%, 40%, 0.5)'
|
|
28
|
+
* getHsla(180, 1, true) // → 'hsla(180, 50%, 60%, 1)'
|
|
29
|
+
*/
|
|
30
|
+
export function getHsla(
|
|
31
|
+
hue: number,
|
|
32
|
+
opacity: number | string = 1,
|
|
33
|
+
dark = false,
|
|
34
|
+
): string {
|
|
35
|
+
const saturation = dark ? 50 : 65
|
|
36
|
+
const lightness = dark ? 60 : 40
|
|
37
|
+
return `hsla(${hue}, ${saturation}%, ${lightness}%, ${opacity})`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Map an arbitrary string to a stable hue and return an `hsla()` color.
|
|
42
|
+
*
|
|
43
|
+
* Uses a deterministic string hash folded into the 0–360 hue range, so the same
|
|
44
|
+
* input always yields the same color across runs and machines.
|
|
45
|
+
*
|
|
46
|
+
* @param name - The string to derive a hue from.
|
|
47
|
+
* @param opacity - The alpha channel, a number or CSS string. Defaults to `1`.
|
|
48
|
+
* @param dark - Whether to use the dark-mode saturation/lightness. Defaults to `false`.
|
|
49
|
+
* @returns A deterministic `hsla(...)` CSS color string for the input.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* getHashColorFromString('Vite') === getHashColorFromString('Vite') // → true (deterministic)
|
|
53
|
+
* getHashColorFromString('Vite') !== getHashColorFromString('DevTools') // → true
|
|
54
|
+
*/
|
|
55
|
+
export function getHashColorFromString(
|
|
56
|
+
name: string,
|
|
57
|
+
opacity: number | string = 1,
|
|
58
|
+
dark = false,
|
|
59
|
+
): string {
|
|
60
|
+
let hash = 0
|
|
61
|
+
for (let i = 0; i < name.length; i++)
|
|
62
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash)
|
|
63
|
+
const h = ((hash % 360) + 360) % 360
|
|
64
|
+
return getHsla(h, opacity, dark)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Curated brand hues (in degrees) for well-known ecosystems, so common
|
|
69
|
+
* package/plugin names get an on-brand color instead of an arbitrary hash.
|
|
70
|
+
* Overridable by callers via {@link getPluginColor}'s `map` argument.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* defaultBrandHues.vue // → 153
|
|
74
|
+
* defaultBrandHues.react // → 193
|
|
75
|
+
*/
|
|
76
|
+
export const defaultBrandHues: Record<string, number> = {
|
|
77
|
+
vue: 153,
|
|
78
|
+
nuxt: 153,
|
|
79
|
+
vite: 265,
|
|
80
|
+
react: 193,
|
|
81
|
+
preact: 280,
|
|
82
|
+
solid: 217,
|
|
83
|
+
svelte: 15,
|
|
84
|
+
angular: 348,
|
|
85
|
+
qwik: 265,
|
|
86
|
+
lit: 210,
|
|
87
|
+
astro: 270,
|
|
88
|
+
remix: 220,
|
|
89
|
+
next: 220,
|
|
90
|
+
node: 120,
|
|
91
|
+
deno: 160,
|
|
92
|
+
bun: 45,
|
|
93
|
+
npm: 0,
|
|
94
|
+
pnpm: 35,
|
|
95
|
+
yarn: 200,
|
|
96
|
+
ts: 211,
|
|
97
|
+
typescript: 211,
|
|
98
|
+
js: 47,
|
|
99
|
+
javascript: 47,
|
|
100
|
+
unocss: 190,
|
|
101
|
+
tailwind: 198,
|
|
102
|
+
rollup: 12,
|
|
103
|
+
rolldown: 25,
|
|
104
|
+
webpack: 200,
|
|
105
|
+
esbuild: 49,
|
|
106
|
+
vitest: 120,
|
|
107
|
+
jest: 340,
|
|
108
|
+
eslint: 265,
|
|
109
|
+
prettier: 330,
|
|
110
|
+
electron: 200,
|
|
111
|
+
tauri: 200,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface LabelStyle {
|
|
115
|
+
color: string
|
|
116
|
+
background: string
|
|
117
|
+
borderColor: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const labelCache = new Map<string, LabelStyle>()
|
|
121
|
+
|
|
122
|
+
function oklchToHex(l: number, c: number, h: number): string {
|
|
123
|
+
return new Color('oklch', [l, c, h]).to('srgb').toGamut({ space: 'srgb' }).toString({ format: 'hex', collapse: false })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Contrast-aware foreground / background / border for a label from a base color,
|
|
128
|
+
* dark-aware. Uses OKLCH (via colorjs.io) for perceptually even results — the
|
|
129
|
+
* chroma is clamped so arbitrary inputs never look garish — and caches by
|
|
130
|
+
* `color|scheme`. (The OKLCH lightness/chroma stops match ghfs's `labelStyle`.)
|
|
131
|
+
*
|
|
132
|
+
* @param input - The base color as any CSS color string.
|
|
133
|
+
* @param dark - Whether to produce the dark-mode variant. Defaults to `false`.
|
|
134
|
+
* @returns A {@link LabelStyle} with hex `color`, `background` and `borderColor`.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* labelStyle('#d73a4a') // → { color: '#…', background: '#…', borderColor: '#…' } (light)
|
|
138
|
+
* labelStyle('#d73a4a', true) // → the dark-mode variant
|
|
139
|
+
*/
|
|
140
|
+
export function labelStyle(input: string, dark = false): LabelStyle {
|
|
141
|
+
const key = `${input}|${dark ? 'd' : 'l'}`
|
|
142
|
+
const cached = labelCache.get(key)
|
|
143
|
+
if (cached)
|
|
144
|
+
return cached
|
|
145
|
+
|
|
146
|
+
const [, rawChroma, rawHue] = new Color(input).to('oklch').coords
|
|
147
|
+
const h = rawHue == null || !Number.isFinite(rawHue) ? 0 : rawHue
|
|
148
|
+
const c = Math.min(rawChroma == null || !Number.isFinite(rawChroma) ? 0 : rawChroma, 0.18)
|
|
149
|
+
|
|
150
|
+
const style: LabelStyle = dark
|
|
151
|
+
? {
|
|
152
|
+
color: oklchToHex(0.80, c, h),
|
|
153
|
+
background: oklchToHex(0.22, c * 0.5, h),
|
|
154
|
+
borderColor: oklchToHex(0.38, c * 0.7, h),
|
|
155
|
+
}
|
|
156
|
+
: {
|
|
157
|
+
color: oklchToHex(0.42, c, h),
|
|
158
|
+
background: oklchToHex(0.94, c * 0.35, h),
|
|
159
|
+
borderColor: oklchToHex(0.82, c * 0.5, h),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
labelCache.set(key, style)
|
|
163
|
+
return style
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The plugin prefixes {@link stripPluginPrefix} strips by default. Includes the
|
|
168
|
+
* common build-tool plugin conventions plus the `vite:`/`rollup:`-style internal
|
|
169
|
+
* namespace prefixes used by core plugins.
|
|
170
|
+
*/
|
|
171
|
+
export const defaultPluginPrefixes: readonly string[] = [
|
|
172
|
+
'vite-plugin-',
|
|
173
|
+
'rollup-plugin-',
|
|
174
|
+
'webpack-plugin-',
|
|
175
|
+
'unplugin-',
|
|
176
|
+
'nuxt-',
|
|
177
|
+
'eslint-plugin-',
|
|
178
|
+
'postcss-',
|
|
179
|
+
'vite:',
|
|
180
|
+
'rollup:',
|
|
181
|
+
'rolldown:',
|
|
182
|
+
'webpack:',
|
|
183
|
+
'__',
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
function buildPrefixRe(prefixes: readonly string[]): RegExp {
|
|
187
|
+
const alt = prefixes.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
|
|
188
|
+
return new RegExp(`^(?:@[^/]+\\/)?(?:${alt})`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const DEFAULT_PREFIX_RE = buildPrefixRe(defaultPluginPrefixes)
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Strip common tool prefixes/scopes to get the meaningful part of a name.
|
|
195
|
+
*
|
|
196
|
+
* Removes a recognised plugin prefix (`vite-plugin-`, `rollup-plugin-`,
|
|
197
|
+
* `unplugin-`, `nuxt-`, `eslint-plugin-`, `postcss-`, the `vite:`/`rollup:`
|
|
198
|
+
* internal namespaces, …, optionally scoped) and any remaining leading
|
|
199
|
+
* `@scope/`. Pass a custom prefix list or `RegExp` to override the defaults.
|
|
200
|
+
*
|
|
201
|
+
* @param name - The package or plugin name to strip.
|
|
202
|
+
* @param prefixes - Custom prefix list or anchored `RegExp`. Defaults to {@link defaultPluginPrefixes}.
|
|
203
|
+
* @returns The meaningful suffix of the name.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* stripPluginPrefix('vite-plugin-inspect') // → 'inspect'
|
|
207
|
+
* stripPluginPrefix('@antfu/eslint-config') // → 'eslint-config'
|
|
208
|
+
* stripPluginPrefix('vite:import-analysis') // → 'import-analysis'
|
|
209
|
+
*/
|
|
210
|
+
export function stripPluginPrefix(name: string, prefixes?: readonly string[] | RegExp): string {
|
|
211
|
+
const re = prefixes == null
|
|
212
|
+
? DEFAULT_PREFIX_RE
|
|
213
|
+
: prefixes instanceof RegExp ? prefixes : buildPrefixRe(prefixes)
|
|
214
|
+
return name.replace(re, '').replace(/^@[^/]+\//, '')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Color a package/plugin name: brand hue when recognised, deterministic hash
|
|
219
|
+
* otherwise.
|
|
220
|
+
*
|
|
221
|
+
* The name is stripped of plugin prefixes and lower-cased, then matched against
|
|
222
|
+
* `map` (exact, or as a `key-` / `key.` prefix). On a hit the brand hue is used;
|
|
223
|
+
* otherwise it falls back to {@link getHashColorFromString} on the original name.
|
|
224
|
+
*
|
|
225
|
+
* @param name - The package or plugin name to color.
|
|
226
|
+
* @param opacity - The alpha channel, a number or CSS string. Defaults to `1`.
|
|
227
|
+
* @param dark - Whether to use the dark-mode variant. Defaults to `false`.
|
|
228
|
+
* @param map - Extra brand hues, **merged over** {@link defaultBrandHues} (pass only your additions/overrides).
|
|
229
|
+
* @returns An `hsla(...)` CSS color string.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* getPluginColor('vue') // → 'hsla(153, 65%, 40%, 1)'
|
|
233
|
+
* getPluginColor('react') // → 'hsla(193, 65%, 40%, 1)'
|
|
234
|
+
* getPluginColor('vite-plugin-vue') // → 'hsla(153, 65%, 40%, 1)' (matched by suffix)
|
|
235
|
+
* getPluginColor('acme', 1, false, { acme: 300 }) // → custom hue merged over the defaults
|
|
236
|
+
*/
|
|
237
|
+
export function getPluginColor(
|
|
238
|
+
name: string,
|
|
239
|
+
opacity: number | string = 1,
|
|
240
|
+
dark = false,
|
|
241
|
+
map: Record<string, number> = {},
|
|
242
|
+
): string {
|
|
243
|
+
const hues = map === defaultBrandHues ? defaultBrandHues : { ...defaultBrandHues, ...map }
|
|
244
|
+
const bare = stripPluginPrefix(name).toLowerCase()
|
|
245
|
+
const key = Object.keys(hues).find(k => bare === k || bare.startsWith(`${k}-`) || bare.startsWith(`${k}.`))
|
|
246
|
+
if (key != null)
|
|
247
|
+
return getHsla(hues[key], opacity, dark)
|
|
248
|
+
return getHashColorFromString(name, opacity, dark)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Color math (colorjs.io) ───────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Convert any CSS color to a `#rrggbb[aa]` hex string (gamut-mapped to sRGB).
|
|
255
|
+
*
|
|
256
|
+
* @param input - Any CSS color string (hex, `rgb()`, `hsl()`, named, `oklch()`, …).
|
|
257
|
+
* @returns A sRGB hex string.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* toHex('hsl(0, 100%, 50%)') // → '#ff0000'
|
|
261
|
+
*/
|
|
262
|
+
export function toHex(input: string): string {
|
|
263
|
+
return new Color(input).to('srgb').toGamut({ space: 'srgb' }).toString({ format: 'hex', collapse: false })
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Lighten a color by an OKLCH lightness delta (perceptually even).
|
|
268
|
+
*
|
|
269
|
+
* @param input - The base color as any CSS color string.
|
|
270
|
+
* @param amount - Lightness delta in the 0–1 OKLCH range. Defaults to `0.1`.
|
|
271
|
+
* @returns The lightened color as a sRGB hex string.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* lighten('#336699', 0.1) // → a lighter blue
|
|
275
|
+
*/
|
|
276
|
+
export function lighten(input: string, amount = 0.1): string {
|
|
277
|
+
const c = new Color(input).to('oklch')
|
|
278
|
+
c.l = Math.max(0, Math.min(1, (c.l ?? 0) + amount))
|
|
279
|
+
return c.to('srgb').toGamut({ space: 'srgb' }).toString({ format: 'hex', collapse: false })
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Darken a color by an OKLCH lightness delta (perceptually even).
|
|
284
|
+
*
|
|
285
|
+
* @param input - The base color as any CSS color string.
|
|
286
|
+
* @param amount - Lightness delta in the 0–1 OKLCH range. Defaults to `0.1`.
|
|
287
|
+
* @returns The darkened color as a sRGB hex string.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* darken('#336699', 0.1) // → a darker blue
|
|
291
|
+
*/
|
|
292
|
+
export function darken(input: string, amount = 0.1): string {
|
|
293
|
+
return lighten(input, -amount)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Mix two colors in OKLCH space.
|
|
298
|
+
*
|
|
299
|
+
* @param a - The first color as any CSS color string.
|
|
300
|
+
* @param b - The second color as any CSS color string.
|
|
301
|
+
* @param weight - Weight of `b` in the mix, 0–1. Defaults to `0.5`.
|
|
302
|
+
* @returns The mixed color as a sRGB hex string.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* mix('#ff0000', '#0000ff') // → a purple midpoint
|
|
306
|
+
*/
|
|
307
|
+
export function mix(a: string, b: string, weight = 0.5): string {
|
|
308
|
+
return (Color.mix(new Color(a), new Color(b), weight, { space: 'oklch' }) as Color)
|
|
309
|
+
.to('srgb')
|
|
310
|
+
.toGamut({ space: 'srgb' })
|
|
311
|
+
.toString({ format: 'hex', collapse: false })
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Set a color's alpha channel, returning an `rgb(...)`/`rgba(...)` string.
|
|
316
|
+
*
|
|
317
|
+
* @param input - The base color as any CSS color string.
|
|
318
|
+
* @param alpha - The alpha channel, 0–1.
|
|
319
|
+
* @returns The color with the given alpha, as a CSS string.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* withAlpha('#336699', 0.5) // → 'rgb(51 102 153 / 0.5)'
|
|
323
|
+
*/
|
|
324
|
+
export function withAlpha(input: string, alpha: number): string {
|
|
325
|
+
const c = new Color(input).to('srgb')
|
|
326
|
+
c.alpha = alpha
|
|
327
|
+
return c.toString()
|
|
328
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WCAG contrast helpers — relative luminance and contrast ratio — built on
|
|
3
|
+
* [colorjs.io](https://colorjs.io) rather than hand-rolled color math, so token
|
|
4
|
+
* pairs can be asserted in unit tests (e.g. `color-base` on `bg-base` meets AA
|
|
5
|
+
* in both themes). Complements the runnable axe-core scan in `../a11y`.
|
|
6
|
+
*/
|
|
7
|
+
import Color from 'colorjs.io'
|
|
8
|
+
|
|
9
|
+
export interface RGB {
|
|
10
|
+
r: number
|
|
11
|
+
g: number
|
|
12
|
+
b: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toColor(input: string | RGB): Color {
|
|
16
|
+
return typeof input === 'string'
|
|
17
|
+
? new Color(input)
|
|
18
|
+
: new Color('srgb', [input.r / 255, input.g / 255, input.b / 255])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse any CSS color into `{ r, g, b }` (0–255), via colorjs.io. An already
|
|
23
|
+
* parsed {@link RGB} is returned unchanged.
|
|
24
|
+
*
|
|
25
|
+
* @param input - A CSS color string (hex, `rgb()`, `hsl()`, named, …) or an {@link RGB}.
|
|
26
|
+
* @returns The color as `{ r, g, b }` with 0–255 channels.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* parseColor('#fff') // → { r: 255, g: 255, b: 255 }
|
|
30
|
+
* parseColor('#000000') // → { r: 0, g: 0, b: 0 }
|
|
31
|
+
* parseColor('white') // → { r: 255, g: 255, b: 255 }
|
|
32
|
+
*/
|
|
33
|
+
export function parseColor(input: string | RGB): RGB {
|
|
34
|
+
if (typeof input !== 'string')
|
|
35
|
+
return input
|
|
36
|
+
const [r, g, b] = new Color(input).to('srgb').coords
|
|
37
|
+
return { r: Math.round((r ?? 0) * 255), g: Math.round((g ?? 0) * 255), b: Math.round((b ?? 0) * 255) }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* WCAG relative luminance of a color (0 = black, 1 = white).
|
|
42
|
+
*
|
|
43
|
+
* @param color - The color as a CSS string or {@link RGB} object.
|
|
44
|
+
* @returns The relative luminance in the range 0–1.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* relativeLuminance('#000') // → 0
|
|
48
|
+
* relativeLuminance('#fff') // → 1
|
|
49
|
+
*/
|
|
50
|
+
export function relativeLuminance(color: string | RGB): number {
|
|
51
|
+
return toColor(color).luminance
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* WCAG 2.1 contrast ratio between two colors (1 → 21). Argument order does not
|
|
56
|
+
* matter.
|
|
57
|
+
*
|
|
58
|
+
* @param a - The first color, as a CSS string or {@link RGB} object.
|
|
59
|
+
* @param b - The second color, as a CSS string or {@link RGB} object.
|
|
60
|
+
* @returns The contrast ratio, from `1` (identical) up to `21` (black on white).
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* Math.round(contrastRatio('#000', '#fff')) // → 21
|
|
64
|
+
*/
|
|
65
|
+
export function contrastRatio(a: string | RGB, b: string | RGB): number {
|
|
66
|
+
return toColor(a).contrast(toColor(b), 'WCAG21')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type ContrastLevel = 'AA' | 'AAA'
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether a ratio satisfies a WCAG level for normal or large text.
|
|
73
|
+
*
|
|
74
|
+
* Thresholds are 4.5 (AA) / 7 (AAA) for normal text and 3 (AA) / 4.5 (AAA) for
|
|
75
|
+
* large text.
|
|
76
|
+
*
|
|
77
|
+
* @param ratio - The contrast ratio to test (see {@link contrastRatio}).
|
|
78
|
+
* @param level - The WCAG conformance level, `'AA'` or `'AAA'`. Defaults to `'AA'`.
|
|
79
|
+
* @param large - Whether to use the relaxed large-text thresholds. Defaults to `false`.
|
|
80
|
+
* @returns `true` if the ratio meets or exceeds the threshold.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* meetsContrast(4.5) // → true
|
|
84
|
+
* meetsContrast(4.49) // → false
|
|
85
|
+
*/
|
|
86
|
+
export function meetsContrast(ratio: number, level: ContrastLevel = 'AA', large = false): boolean {
|
|
87
|
+
const thresholds = { AA: large ? 3 : 4.5, AAA: large ? 4.5 : 7 }
|
|
88
|
+
return ratio >= thresholds[level]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ContrastResult {
|
|
92
|
+
ratio: number
|
|
93
|
+
AA: boolean
|
|
94
|
+
AALarge: boolean
|
|
95
|
+
AAA: boolean
|
|
96
|
+
AAALarge: boolean
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Full contrast report between a foreground and background color.
|
|
101
|
+
*
|
|
102
|
+
* @param foreground - The foreground color, as a CSS string or {@link RGB} object.
|
|
103
|
+
* @param background - The background color, as a CSS string or {@link RGB} object.
|
|
104
|
+
* @returns A {@link ContrastResult} with the rounded `ratio` and per-level booleans.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* checkContrast('#525252', '#fff').AA // → true
|
|
108
|
+
*/
|
|
109
|
+
export function checkContrast(foreground: string | RGB, background: string | RGB): ContrastResult {
|
|
110
|
+
const ratio = Math.round(contrastRatio(foreground, background) * 100) / 100
|
|
111
|
+
return {
|
|
112
|
+
ratio,
|
|
113
|
+
AA: meetsContrast(ratio, 'AA'),
|
|
114
|
+
AALarge: meetsContrast(ratio, 'AA', true),
|
|
115
|
+
AAA: meetsContrast(ratio, 'AAA'),
|
|
116
|
+
AAALarge: meetsContrast(ratio, 'AAA', true),
|
|
117
|
+
}
|
|
118
|
+
}
|