@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
package/utils/format.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// @unocss-include — this file embeds `color-scale-*` utility classes that a
|
|
2
|
+
// consumer's UnoCSS must generate; the marker forces full extraction.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure formatting helpers shared by the display components, plus the
|
|
6
|
+
* severity/age → `color-scale-*` mappings. Re-implemented once here instead of
|
|
7
|
+
* being duplicated in every badge across the source projects.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The severity color-scale class names (see `presetAnthonySeverity`).
|
|
12
|
+
*
|
|
13
|
+
* Maps each severity level to its `color-scale-*` CSS class, ordered from least
|
|
14
|
+
* to most severe.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* colorScale.neutral // → 'color-scale-neutral'
|
|
18
|
+
* colorScale.critical // → 'color-scale-critical'
|
|
19
|
+
*/
|
|
20
|
+
export const colorScale = {
|
|
21
|
+
neutral: 'color-scale-neutral',
|
|
22
|
+
low: 'color-scale-low',
|
|
23
|
+
medium: 'color-scale-medium',
|
|
24
|
+
high: 'color-scale-high',
|
|
25
|
+
critical: 'color-scale-critical',
|
|
26
|
+
} as const
|
|
27
|
+
|
|
28
|
+
export type ColorScaleClass = (typeof colorScale)[keyof typeof colorScale]
|
|
29
|
+
export type SeverityScale = readonly (readonly [max: number, cls: ColorScaleClass])[]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Map a value through an ascending threshold scale to a color-scale class.
|
|
33
|
+
*
|
|
34
|
+
* Returns the class of the first `[max, cls]` entry whose `max` the value does
|
|
35
|
+
* not exceed; if the value is above every threshold, the last entry's class is
|
|
36
|
+
* returned.
|
|
37
|
+
*
|
|
38
|
+
* @param value - The numeric value to classify.
|
|
39
|
+
* @param scale - Ascending list of `[max, colorScaleClass]` thresholds.
|
|
40
|
+
* @returns The matching {@link ColorScaleClass}.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* mapSeverity(999, [[10, 'color-scale-low']]) // → 'color-scale-low' (above all thresholds)
|
|
44
|
+
*/
|
|
45
|
+
export function mapSeverity(value: number, scale: SeverityScale): ColorScaleClass {
|
|
46
|
+
for (const [max, cls] of scale) {
|
|
47
|
+
if (value <= max)
|
|
48
|
+
return cls
|
|
49
|
+
}
|
|
50
|
+
return scale[scale.length - 1][1]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Locale ──────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The default BCP-47 locale used by the `Intl`-backed formatters
|
|
57
|
+
* (`formatNumber`, `formatPercent`, `formatDateTime`) when no `locale` argument
|
|
58
|
+
* is passed. Defaults to `'en-US'`; set it once at app startup for i18n.
|
|
59
|
+
*/
|
|
60
|
+
let defaultLocale = 'en-US'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Set the process-wide default locale for the `Intl`-backed formatters.
|
|
64
|
+
*
|
|
65
|
+
* @param locale - A BCP-47 locale tag, e.g. `'de-DE'` or `'ja-JP'`.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* setDefaultLocale('de-DE')
|
|
69
|
+
* formatNumber(1234567) // → '1.234.567'
|
|
70
|
+
*/
|
|
71
|
+
export function setDefaultLocale(locale: string): void {
|
|
72
|
+
defaultLocale = locale
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the current default locale used by the `Intl`-backed formatters.
|
|
77
|
+
*
|
|
78
|
+
* @returns The current default BCP-47 locale tag.
|
|
79
|
+
*/
|
|
80
|
+
export function getDefaultLocale(): string {
|
|
81
|
+
return defaultLocale
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Numbers ───────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Format a number with locale-aware grouping via `Intl.NumberFormat`.
|
|
88
|
+
*
|
|
89
|
+
* @param value - The number to format.
|
|
90
|
+
* @param options - Optional `Intl.NumberFormatOptions` to customize formatting.
|
|
91
|
+
* @param locale - BCP-47 locale tag. Defaults to {@link getDefaultLocale}.
|
|
92
|
+
* @returns The formatted, grouped number string.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* formatNumber(1234567) // → '1,234,567'
|
|
96
|
+
* formatNumber(1234567, undefined, 'de-DE') // → '1.234.567'
|
|
97
|
+
*/
|
|
98
|
+
export function formatNumber(value: number, options?: Intl.NumberFormatOptions, locale?: string): string {
|
|
99
|
+
return new Intl.NumberFormat(locale ?? defaultLocale, options).format(value)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Format a 0–1 fraction as a locale-aware percentage.
|
|
104
|
+
*
|
|
105
|
+
* @param value - The fraction to format, where `1` represents 100%.
|
|
106
|
+
* @param digits - Maximum number of fraction digits. Defaults to `1`.
|
|
107
|
+
* @param locale - BCP-47 locale tag. Defaults to {@link getDefaultLocale}.
|
|
108
|
+
* @returns The formatted percentage string.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* formatPercent(0.1234) // → '12.3%'
|
|
112
|
+
* formatPercent(0.5, 0) // → '50%'
|
|
113
|
+
*/
|
|
114
|
+
export function formatPercent(value: number, digits = 1, locale?: string): string {
|
|
115
|
+
return new Intl.NumberFormat(locale ?? defaultLocale, {
|
|
116
|
+
style: 'percent',
|
|
117
|
+
minimumFractionDigits: 0,
|
|
118
|
+
maximumFractionDigits: digits,
|
|
119
|
+
}).format(value)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Bytes ─────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
const BYTE_UNITS_1024 = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] as const
|
|
125
|
+
const BYTE_UNITS_1000 = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'] as const
|
|
126
|
+
|
|
127
|
+
export interface FormatBytesOptions {
|
|
128
|
+
/** `1024` (binary, default) or `1000` (decimal). The source projects disagree, so it is an option. */
|
|
129
|
+
base?: 1024 | 1000
|
|
130
|
+
digits?: number
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Humanize a byte count → `[value, unit]`.
|
|
135
|
+
*
|
|
136
|
+
* Picks the largest unit whose value is ≥ 1, formats to `digits` decimals with
|
|
137
|
+
* trailing zeros trimmed, and returns the value and unit separately so callers
|
|
138
|
+
* can style them independently. Non-positive or falsy inputs yield `['0', 'B']`.
|
|
139
|
+
*
|
|
140
|
+
* @param bytes - The byte count to humanize.
|
|
141
|
+
* @param options - Formatting options.
|
|
142
|
+
* @param options.base - `1024` (binary, default) or `1000` (decimal).
|
|
143
|
+
* @param options.digits - Maximum decimal places before trimming. Defaults to `2`.
|
|
144
|
+
* @returns A `[value, unit]` tuple, e.g. `['1.5', 'KB']`.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* formatBytes(0) // → ['0', 'B']
|
|
148
|
+
* formatBytes(512) // → ['512', 'B']
|
|
149
|
+
* formatBytes(1024) // → ['1', 'KB']
|
|
150
|
+
* formatBytes(1536) // → ['1.5', 'KB']
|
|
151
|
+
* formatBytes(1_000_000, { base: 1000 }) // → ['1', 'MB']
|
|
152
|
+
*/
|
|
153
|
+
export function formatBytes(bytes: number, options: FormatBytesOptions = {}): [string, string] {
|
|
154
|
+
const { base = 1024, digits = 2 } = options
|
|
155
|
+
if (!bytes || bytes < 0)
|
|
156
|
+
return ['0', 'B']
|
|
157
|
+
const units = base === 1000 ? BYTE_UNITS_1000 : BYTE_UNITS_1024
|
|
158
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(base)), units.length - 1)
|
|
159
|
+
if (i === 0)
|
|
160
|
+
return [String(bytes), 'B']
|
|
161
|
+
const value = (bytes / base ** i).toFixed(digits).replace(/\.?0+$/, '')
|
|
162
|
+
return [value, units[i]]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Byte length of a string's UTF-8 encoding.
|
|
167
|
+
*
|
|
168
|
+
* Counts encoded bytes rather than code units, so multi-byte characters are
|
|
169
|
+
* measured accurately.
|
|
170
|
+
*
|
|
171
|
+
* @param str - The string to measure.
|
|
172
|
+
* @returns The number of UTF-8 bytes.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* getContentByteSize('hello') // → 5
|
|
176
|
+
* getContentByteSize('héllo') // → 6 (é encodes to 2 bytes)
|
|
177
|
+
*/
|
|
178
|
+
export function getContentByteSize(str: string): number {
|
|
179
|
+
return new TextEncoder().encode(str).length
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const KB = 1024
|
|
183
|
+
const MB = 1024 * KB
|
|
184
|
+
|
|
185
|
+
/** Default severity thresholds for byte sizes. Pass a custom one to {@link getBytesColor}. */
|
|
186
|
+
export const BYTES_SCALE: SeverityScale = [
|
|
187
|
+
[80 * KB, colorScale.neutral],
|
|
188
|
+
[500 * KB, colorScale.low],
|
|
189
|
+
[MB, colorScale.medium],
|
|
190
|
+
[10 * MB, colorScale.high],
|
|
191
|
+
[Number.POSITIVE_INFINITY, colorScale.critical],
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Color a byte size on a severity scale (small = neutral, large = critical).
|
|
196
|
+
*
|
|
197
|
+
* Default thresholds: ≤80 KB neutral, ≤500 KB low, ≤1 MB medium, ≤10 MB high, else critical.
|
|
198
|
+
*
|
|
199
|
+
* @param bytes - The byte size to classify.
|
|
200
|
+
* @param scale - Ascending threshold scale. Defaults to {@link BYTES_SCALE}.
|
|
201
|
+
* @returns The matching {@link ColorScaleClass}.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* getBytesColor(10 * 1024) // → 'color-scale-neutral'
|
|
205
|
+
* getBytesColor(50 * 1024 * 1024) // → 'color-scale-critical'
|
|
206
|
+
*/
|
|
207
|
+
export function getBytesColor(bytes: number, scale: SeverityScale = BYTES_SCALE): ColorScaleClass {
|
|
208
|
+
return mapSeverity(bytes, scale)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Durations ───────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/** Input time unit accepted by {@link formatDuration}. */
|
|
214
|
+
export type DurationUnit = 'ns' | 'us' | 'ms' | 's'
|
|
215
|
+
|
|
216
|
+
const DURATION_FACTOR_MS: Record<DurationUnit, number> = { ns: 1e-6, us: 1e-3, ms: 1, s: 1000 }
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Humanize a duration → `[value, unit]`, accepting nanosecond/microsecond input.
|
|
220
|
+
*
|
|
221
|
+
* Input is interpreted in `options.unit` (default `'ms'`) and scaled to
|
|
222
|
+
* milliseconds, then formatted: sub-millisecond values from a finer input unit
|
|
223
|
+
* render as `ns`/`µs`, otherwise the ladder runs `ms` → `s` → `min` → `h` → `d`.
|
|
224
|
+
* For backwards compatibility a sub-1 **ms** input still renders `['<1', 'ms']`,
|
|
225
|
+
* and `null`/`undefined` renders `['', '-']`.
|
|
226
|
+
*
|
|
227
|
+
* @param value - The duration in `options.unit`, or `null`/`undefined` for no value.
|
|
228
|
+
* @param options - Formatting options.
|
|
229
|
+
* @param options.unit - Input unit: `'ns'`, `'us'`, `'ms'` (default) or `'s'`.
|
|
230
|
+
* @returns A `[value, unit]` tuple, e.g. `['1.5', 's']`.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* formatDuration(null) // → ['', '-']
|
|
234
|
+
* formatDuration(0.5) // → ['<1', 'ms']
|
|
235
|
+
* formatDuration(250) // → ['250', 'ms']
|
|
236
|
+
* formatDuration(1500) // → ['1.5', 's']
|
|
237
|
+
* formatDuration(1500, { unit: 'ns' }) // → ['1.5', 'µs']
|
|
238
|
+
* formatDuration(820, { unit: 'us' }) // → ['820', 'µs']
|
|
239
|
+
*/
|
|
240
|
+
export function formatDuration(
|
|
241
|
+
value: number | null | undefined,
|
|
242
|
+
options: { unit?: DurationUnit } = {},
|
|
243
|
+
): [string, string] {
|
|
244
|
+
if (value == null)
|
|
245
|
+
return ['', '-']
|
|
246
|
+
const unit = options.unit ?? 'ms'
|
|
247
|
+
const ms = value * DURATION_FACTOR_MS[unit]
|
|
248
|
+
if (ms < 1) {
|
|
249
|
+
if (unit === 'ms')
|
|
250
|
+
return ['<1', 'ms']
|
|
251
|
+
const us = ms * 1000
|
|
252
|
+
if (us < 1)
|
|
253
|
+
return [Math.round(us * 1000).toString(), 'ns']
|
|
254
|
+
return [(us < 10 ? us.toFixed(1) : us.toFixed(0)).replace(/\.0$/, ''), 'µs']
|
|
255
|
+
}
|
|
256
|
+
if (ms < 1000)
|
|
257
|
+
return [ms.toFixed(0), 'ms']
|
|
258
|
+
if (ms < 60_000)
|
|
259
|
+
return [(ms / 1000).toFixed(1), 's']
|
|
260
|
+
if (ms < 3_600_000)
|
|
261
|
+
return [(ms / 60_000).toFixed(1), 'min']
|
|
262
|
+
if (ms < 86_400_000)
|
|
263
|
+
return [(ms / 3_600_000).toFixed(1), 'h']
|
|
264
|
+
return [(ms / 86_400_000).toFixed(1), 'd']
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Default severity thresholds (in ms) for durations. Pass a custom one to {@link getDurationColor}. */
|
|
268
|
+
export const DURATION_SCALE: SeverityScale = [
|
|
269
|
+
[50, colorScale.neutral],
|
|
270
|
+
[200, colorScale.low],
|
|
271
|
+
[1000, colorScale.medium],
|
|
272
|
+
[5000, colorScale.high],
|
|
273
|
+
[Number.POSITIVE_INFINITY, colorScale.critical],
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Color a duration on a severity scale (fast = neutral, slow = critical).
|
|
278
|
+
*
|
|
279
|
+
* Default thresholds: ≤50 ms neutral, ≤200 ms low, ≤1000 ms medium, ≤5000 ms high, else critical.
|
|
280
|
+
*
|
|
281
|
+
* @param ms - The duration in milliseconds to classify.
|
|
282
|
+
* @param scale - Ascending threshold scale (in ms). Defaults to {@link DURATION_SCALE}.
|
|
283
|
+
* @returns The matching {@link ColorScaleClass}.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* getDurationColor(30) // → 'color-scale-neutral'
|
|
287
|
+
* getDurationColor(500) // → 'color-scale-medium'
|
|
288
|
+
* getDurationColor(10_000) // → 'color-scale-critical'
|
|
289
|
+
*/
|
|
290
|
+
export function getDurationColor(ms: number, scale: SeverityScale = DURATION_SCALE): ColorScaleClass {
|
|
291
|
+
return mapSeverity(ms, scale)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Relative time ─────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
const SECOND = 1000
|
|
297
|
+
const MINUTE = 60 * SECOND
|
|
298
|
+
const HOUR = 60 * MINUTE
|
|
299
|
+
const DAY = 24 * HOUR
|
|
300
|
+
const MONTH = 30 * DAY
|
|
301
|
+
const YEAR = 365 * DAY
|
|
302
|
+
|
|
303
|
+
const TIME_UNITS: readonly (readonly [limit: number, divisor: number, unit: string])[] = [
|
|
304
|
+
[MINUTE, SECOND, 's'],
|
|
305
|
+
[HOUR, MINUTE, 'min'],
|
|
306
|
+
[DAY, HOUR, 'h'],
|
|
307
|
+
[MONTH, DAY, 'd'],
|
|
308
|
+
[YEAR, MONTH, 'mo'],
|
|
309
|
+
[Number.POSITIVE_INFINITY, YEAR, 'y'],
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* No-dependency relative time: `"3 d ago"`, `"in 2 h"`, `"just now"`.
|
|
314
|
+
*
|
|
315
|
+
* Computes the signed delta between `input` and `now`, picks the largest unit
|
|
316
|
+
* (`s`/`min`/`h`/`d`/`mo`/`y`) and renders past times as `"N unit ago"` and
|
|
317
|
+
* future times as `"in N unit"`. Deltas under a second are `"just now"`.
|
|
318
|
+
*
|
|
319
|
+
* @param input - The timestamp to describe, as a `Date` or epoch milliseconds.
|
|
320
|
+
* @param now - Reference time in epoch milliseconds. Defaults to `Date.now()`.
|
|
321
|
+
* @returns A human-readable relative time string.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* const now = 1_000_000_000_000
|
|
325
|
+
* formatTimeAgo(now, now) // → 'just now'
|
|
326
|
+
* formatTimeAgo(now - 3 * 86_400_000, now) // → '3 d ago'
|
|
327
|
+
* formatTimeAgo(now + 2 * 3_600_000, now) // → 'in 2 h'
|
|
328
|
+
*/
|
|
329
|
+
export function formatTimeAgo(input: Date | number, now: number = Date.now()): string {
|
|
330
|
+
const time = input instanceof Date ? input.getTime() : input
|
|
331
|
+
const delta = now - time
|
|
332
|
+
const abs = Math.abs(delta)
|
|
333
|
+
if (abs < SECOND)
|
|
334
|
+
return 'just now'
|
|
335
|
+
for (const [limit, divisor, unit] of TIME_UNITS) {
|
|
336
|
+
if (abs < limit) {
|
|
337
|
+
const value = Math.round(abs / divisor)
|
|
338
|
+
return delta >= 0 ? `${value} ${unit} ago` : `in ${value} ${unit}`
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return ''
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Default severity thresholds (in ms of age) for freshness. Pass a custom one to {@link getAgeColor}. */
|
|
345
|
+
export const AGE_SCALE: SeverityScale = [
|
|
346
|
+
[180 * DAY, colorScale.neutral],
|
|
347
|
+
[365 * DAY, colorScale.low],
|
|
348
|
+
[3 * YEAR, colorScale.medium],
|
|
349
|
+
[5 * YEAR, colorScale.high],
|
|
350
|
+
[Number.POSITIVE_INFINITY, colorScale.critical],
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Color an age (ms since something) on the freshness scale.
|
|
355
|
+
*
|
|
356
|
+
* Default thresholds: ≤180 d neutral, ≤365 d low, ≤3 y medium, ≤5 y high, else critical.
|
|
357
|
+
*
|
|
358
|
+
* @param ageMs - The age in milliseconds to classify.
|
|
359
|
+
* @param scale - Ascending threshold scale (in ms). Defaults to {@link AGE_SCALE}.
|
|
360
|
+
* @returns The matching {@link ColorScaleClass}.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* getAgeColor(10 * 86_400_000) // → 'color-scale-neutral' (10 days old)
|
|
364
|
+
*/
|
|
365
|
+
export function getAgeColor(ageMs: number, scale: SeverityScale = AGE_SCALE): ColorScaleClass {
|
|
366
|
+
return mapSeverity(ageMs, scale)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Format a date/time with a locale-aware `Intl.DateTimeFormat`.
|
|
371
|
+
*
|
|
372
|
+
* Defaults to a medium date and short time when no options are given. Exact
|
|
373
|
+
* output depends on the host locale data and time zone.
|
|
374
|
+
*
|
|
375
|
+
* @param input - The date to format, as a `Date` or epoch milliseconds.
|
|
376
|
+
* @param options - Optional `Intl.DateTimeFormatOptions`. Defaults to `{ dateStyle: 'medium', timeStyle: 'short' }`.
|
|
377
|
+
* @param locale - BCP-47 locale tag. Defaults to {@link getDefaultLocale}.
|
|
378
|
+
* @returns The formatted date/time string.
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* formatDateTime(new Date('2026-06-26T15:30:00Z'))
|
|
382
|
+
* // → 'Jun 26, 2026, 3:30 PM' (UTC; varies by time zone)
|
|
383
|
+
*/
|
|
384
|
+
export function formatDateTime(input: Date | number, options?: Intl.DateTimeFormatOptions, locale?: string): string {
|
|
385
|
+
return new Intl.DateTimeFormat(locale ?? defaultLocale, options ?? {
|
|
386
|
+
dateStyle: 'medium',
|
|
387
|
+
timeStyle: 'short',
|
|
388
|
+
}).format(input)
|
|
389
|
+
}
|
package/utils/icon.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// @unocss-include — this file embeds `i-catppuccin:*` icon classes that a
|
|
2
|
+
// consumer's UnoCSS must generate; the marker forces full extraction.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Map a file path / module id to an icon class. The rule list is configurable
|
|
6
|
+
* — the default targets `@iconify-json/catppuccin`, but a caller can pass a
|
|
7
|
+
* vscode-icons / octicon map instead. Backs the `FileIcon` component.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface FileIconRule {
|
|
11
|
+
match: RegExp
|
|
12
|
+
name: string
|
|
13
|
+
/** UnoCSS icon class, e.g. `i-catppuccin:typescript`. */
|
|
14
|
+
icon: string
|
|
15
|
+
description?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default file-icon rule list targeting `@iconify-json/catppuccin`.
|
|
20
|
+
*
|
|
21
|
+
* Each rule pairs a filename `RegExp` with a `name` and UnoCSS `icon` class; the
|
|
22
|
+
* list is ordered so the first matching rule wins (e.g. `.d.ts` before `.ts`).
|
|
23
|
+
* Pass a custom list to {@link getFileType}/{@link getFileIcon} to use a
|
|
24
|
+
* different icon set.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* defaultFileIconRules[0] // → { match: /\.vue$/, name: 'vue', icon: 'i-catppuccin:vue' }
|
|
28
|
+
*/
|
|
29
|
+
export const defaultFileIconRules: FileIconRule[] = [
|
|
30
|
+
{ match: /\.vue$/, name: 'vue', icon: 'i-catppuccin:vue' },
|
|
31
|
+
{ match: /\.tsx$/, name: 'tsx', icon: 'i-catppuccin:typescript-react' },
|
|
32
|
+
{ match: /\.jsx$/, name: 'jsx', icon: 'i-catppuccin:javascript-react' },
|
|
33
|
+
{ match: /\.d\.[cm]?ts$/, name: 'dts', icon: 'i-catppuccin:typescript-def' },
|
|
34
|
+
{ match: /\.[cm]?ts$/, name: 'ts', icon: 'i-catppuccin:typescript' },
|
|
35
|
+
{ match: /\.[cm]?js$/, name: 'js', icon: 'i-catppuccin:javascript' },
|
|
36
|
+
{ match: /\.json5?$/, name: 'json', icon: 'i-catppuccin:json' },
|
|
37
|
+
{ match: /\.ya?ml$/, name: 'yaml', icon: 'i-catppuccin:yaml' },
|
|
38
|
+
{ match: /\.toml$/, name: 'toml', icon: 'i-catppuccin:toml' },
|
|
39
|
+
{ match: /\.md$/, name: 'markdown', icon: 'i-catppuccin:markdown' },
|
|
40
|
+
{ match: /\.html?$/, name: 'html', icon: 'i-catppuccin:html' },
|
|
41
|
+
{ match: /\.(?:css|postcss)$/, name: 'css', icon: 'i-catppuccin:css' },
|
|
42
|
+
{ match: /\.s[ac]ss$/, name: 'sass', icon: 'i-catppuccin:sass' },
|
|
43
|
+
{ match: /\.svg$/, name: 'svg', icon: 'i-catppuccin:svg' },
|
|
44
|
+
{ match: /\.(?:png|jpe?g|gif|webp|avif|ico)$/, name: 'image', icon: 'i-catppuccin:image' },
|
|
45
|
+
{ match: /\.(?:woff2?|ttf|otf|eot)$/, name: 'font', icon: 'i-catppuccin:font' },
|
|
46
|
+
{ match: /\.wasm$/, name: 'wasm', icon: 'i-catppuccin:webassembly' },
|
|
47
|
+
{ match: /package\.json$/, name: 'npm', icon: 'i-catppuccin:npm' },
|
|
48
|
+
{ match: /\.(?:test|spec)\.[cm]?[jt]sx?$/, name: 'test', icon: 'i-catppuccin:typescript-test' },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
const FALLBACK_ICON = 'i-catppuccin:file'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Strip a trailing query and/or hash from a module id.
|
|
55
|
+
*
|
|
56
|
+
* Removes everything from the first `?` or `#` onward, so build-tool suffixes
|
|
57
|
+
* like `?vue&type=script` or `?v=123` don't defeat extension matching.
|
|
58
|
+
*
|
|
59
|
+
* @param id - The module id to clean.
|
|
60
|
+
* @returns The id with any query/hash removed.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* stripModuleQuery('/src/App.vue?vue&type=script') // → '/src/App.vue'
|
|
64
|
+
*/
|
|
65
|
+
export function stripModuleQuery(id: string): string {
|
|
66
|
+
return id.replace(/[?#].*$/, '')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve `{ name, icon }` for a path. Returns the fallback when nothing matches.
|
|
71
|
+
*
|
|
72
|
+
* Strips any query/hash, then returns the first matching rule's metadata; if no
|
|
73
|
+
* rule matches, returns the generic file fallback (`i-catppuccin:file`).
|
|
74
|
+
*
|
|
75
|
+
* @param path - The file path or module id to classify.
|
|
76
|
+
* @param rules - Rule list to match against. Defaults to {@link defaultFileIconRules}.
|
|
77
|
+
* @returns The matched `{ name, icon, description? }`, or the file fallback.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* getFileType('/src/App.vue') // → { name: 'vue', icon: 'i-catppuccin:vue' }
|
|
81
|
+
* getFileType('/src/file.unknown') // → { name: 'file', icon: 'i-catppuccin:file' }
|
|
82
|
+
*/
|
|
83
|
+
export function getFileType(
|
|
84
|
+
path: string,
|
|
85
|
+
rules: FileIconRule[] = defaultFileIconRules,
|
|
86
|
+
): { name: string, icon: string, description?: string } {
|
|
87
|
+
const clean = stripModuleQuery(path)
|
|
88
|
+
for (const rule of rules) {
|
|
89
|
+
if (rule.match.test(clean))
|
|
90
|
+
return { name: rule.name, icon: rule.icon, description: rule.description }
|
|
91
|
+
}
|
|
92
|
+
return { name: 'file', icon: FALLBACK_ICON }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Convenience: just the icon class for a path.
|
|
97
|
+
*
|
|
98
|
+
* Thin wrapper over {@link getFileType} that returns only the `icon` field.
|
|
99
|
+
*
|
|
100
|
+
* @param path - The file path or module id to classify.
|
|
101
|
+
* @param rules - Rule list to match against. Defaults to {@link defaultFileIconRules}.
|
|
102
|
+
* @returns The UnoCSS icon class, or the fallback `i-catppuccin:file`.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* getFileIcon('/src/main.ts') // → 'i-catppuccin:typescript'
|
|
106
|
+
*/
|
|
107
|
+
export function getFileIcon(path: string, rules?: FileIconRule[]): string {
|
|
108
|
+
return getFileType(path, rules).icon
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Folders ──────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
const FOLDER_ICON = 'i-catppuccin:folder'
|
|
114
|
+
const FOLDER_OPEN_ICON = 'i-catppuccin:folder-open'
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Named folder icons (catppuccin), keyed by lower-cased folder basename. Used by
|
|
118
|
+
* {@link getFolderIcon} so well-known directories get a recognisable glyph.
|
|
119
|
+
*/
|
|
120
|
+
export const defaultFolderIconRules: Record<string, string> = {
|
|
121
|
+
src: 'i-catppuccin:folder-src',
|
|
122
|
+
dist: 'i-catppuccin:folder-dist',
|
|
123
|
+
node_modules: 'i-catppuccin:folder-node',
|
|
124
|
+
test: 'i-catppuccin:folder-test',
|
|
125
|
+
tests: 'i-catppuccin:folder-test',
|
|
126
|
+
public: 'i-catppuccin:folder-public',
|
|
127
|
+
components: 'i-catppuccin:folder-components',
|
|
128
|
+
utils: 'i-catppuccin:folder-utils',
|
|
129
|
+
config: 'i-catppuccin:folder-config',
|
|
130
|
+
assets: 'i-catppuccin:folder-images',
|
|
131
|
+
scripts: 'i-catppuccin:folder-scripts',
|
|
132
|
+
styles: 'i-catppuccin:folder-css',
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Icon class for a directory, optionally open and optionally name-aware.
|
|
137
|
+
*
|
|
138
|
+
* Well-known folder names (`src`, `dist`, `node_modules`, …) map to a recognisable
|
|
139
|
+
* glyph; everything else uses the generic folder icon. When `open` is `true`, the
|
|
140
|
+
* generic open-folder glyph is returned.
|
|
141
|
+
*
|
|
142
|
+
* @param name - The folder basename (case-insensitive). Optional.
|
|
143
|
+
* @param open - Whether the folder is expanded. Defaults to `false`.
|
|
144
|
+
* @param rules - Named-folder lookup. Defaults to {@link defaultFolderIconRules}.
|
|
145
|
+
* @returns A UnoCSS icon class.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* getFolderIcon('src') // → 'i-catppuccin:folder-src'
|
|
149
|
+
* getFolderIcon('anything', true) // → 'i-catppuccin:folder-open'
|
|
150
|
+
*/
|
|
151
|
+
export function getFolderIcon(
|
|
152
|
+
name?: string,
|
|
153
|
+
open = false,
|
|
154
|
+
rules: Record<string, string> = defaultFolderIconRules,
|
|
155
|
+
): string {
|
|
156
|
+
if (open)
|
|
157
|
+
return FOLDER_OPEN_ICON
|
|
158
|
+
const named = name ? rules[name.toLowerCase()] : undefined
|
|
159
|
+
return named ?? FOLDER_ICON
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Alternate icon presets ─────────────────────────────────────────────────
|
|
163
|
+
// Opt-in rule lists for other icon collections. The consumer must install the
|
|
164
|
+
// matching `@iconify-json/*` collection and pass the preset to `getFileType` /
|
|
165
|
+
// the `FileIcon` component's `rules` prop.
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* File-icon rules targeting `@iconify-json/vscode-icons` (the Seti-style set).
|
|
169
|
+
* Pass to {@link getFileType}/{@link getFileIcon} or the `FileIcon` `rules` prop.
|
|
170
|
+
*/
|
|
171
|
+
export const vscodeFileIconRules: FileIconRule[] = [
|
|
172
|
+
{ match: /\.vue$/, name: 'vue', icon: 'i-vscode-icons:file-type-vue' },
|
|
173
|
+
{ match: /\.tsx$/, name: 'tsx', icon: 'i-vscode-icons:file-type-reactts' },
|
|
174
|
+
{ match: /\.jsx$/, name: 'jsx', icon: 'i-vscode-icons:file-type-reactjs' },
|
|
175
|
+
{ match: /\.d\.[cm]?ts$/, name: 'dts', icon: 'i-vscode-icons:file-type-typescriptdef' },
|
|
176
|
+
{ match: /\.[cm]?ts$/, name: 'ts', icon: 'i-vscode-icons:file-type-typescript' },
|
|
177
|
+
{ match: /\.[cm]?js$/, name: 'js', icon: 'i-vscode-icons:file-type-js' },
|
|
178
|
+
{ match: /\.json5?$/, name: 'json', icon: 'i-vscode-icons:file-type-json' },
|
|
179
|
+
{ match: /\.ya?ml$/, name: 'yaml', icon: 'i-vscode-icons:file-type-yaml' },
|
|
180
|
+
{ match: /\.toml$/, name: 'toml', icon: 'i-vscode-icons:file-type-toml' },
|
|
181
|
+
{ match: /\.md$/, name: 'markdown', icon: 'i-vscode-icons:file-type-markdown' },
|
|
182
|
+
{ match: /\.html?$/, name: 'html', icon: 'i-vscode-icons:file-type-html' },
|
|
183
|
+
{ match: /\.(?:css|postcss)$/, name: 'css', icon: 'i-vscode-icons:file-type-css' },
|
|
184
|
+
{ match: /\.s[ac]ss$/, name: 'sass', icon: 'i-vscode-icons:file-type-sass' },
|
|
185
|
+
{ match: /\.svg$/, name: 'svg', icon: 'i-vscode-icons:file-type-svg' },
|
|
186
|
+
{ match: /\.(?:png|jpe?g|gif|webp|avif|ico)$/, name: 'image', icon: 'i-vscode-icons:file-type-image' },
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Minimal monochrome file-icon rules targeting `@iconify-json/octicon` — handy
|
|
191
|
+
* for terminals/GitHub-style UIs where a single-color glyph reads better.
|
|
192
|
+
*/
|
|
193
|
+
export const octiconFileIconRules: FileIconRule[] = [
|
|
194
|
+
{ match: /\.json5?$/, name: 'json', icon: 'i-octicon:file-code-16' },
|
|
195
|
+
{ match: /\.md$/, name: 'markdown', icon: 'i-octicon:markdown-16' },
|
|
196
|
+
{ match: /\.(?:png|jpe?g|gif|webp|avif|svg|ico)$/, name: 'image', icon: 'i-octicon:image-16' },
|
|
197
|
+
{ match: /\.(?:zip|tar|gz|tgz)$/, name: 'archive', icon: 'i-octicon:file-zip-16' },
|
|
198
|
+
{ match: /\.[cm]?[jt]sx?$/, name: 'code', icon: 'i-octicon:file-code-16' },
|
|
199
|
+
{ match: /./, name: 'file', icon: 'i-octicon:file-16' },
|
|
200
|
+
]
|
package/utils/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './color'
|
|
2
|
+
|
|
3
|
+
export * from './contrast'
|
|
4
|
+
export * from './format'
|
|
5
|
+
export * from './icon'
|
|
6
|
+
export * from './keybinding'
|
|
7
|
+
export * from './misc'
|
|
8
|
+
export * from './path'
|
|
9
|
+
export * from './semver'
|
|
10
|
+
export * from './tree'
|
|
11
|
+
// Generic helpers come from @antfu/utils (re-exported so `@antfu/design/utils`
|
|
12
|
+
// stays a one-stop import), so we don't reimplement or ship our own.
|
|
13
|
+
export { type Arrayable, clamp, toArray } from '@antfu/utils'
|