@antfu/design 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/a11y/cli.ts +73 -0
  4. package/a11y/index.ts +13 -0
  5. package/a11y/scan.ts +127 -0
  6. package/components/Action/ActionButton.stories.ts +56 -0
  7. package/components/Action/ActionButton.vue +57 -0
  8. package/components/Action/ActionDarkToggle.stories.ts +31 -0
  9. package/components/Action/ActionDarkToggle.vue +87 -0
  10. package/components/Action/ActionIconButton.stories.ts +47 -0
  11. package/components/Action/ActionIconButton.vue +47 -0
  12. package/components/Display/DisplayAvatar.stories.ts +36 -0
  13. package/components/Display/DisplayAvatar.vue +58 -0
  14. package/components/Display/DisplayBadge.stories.ts +31 -0
  15. package/components/Display/DisplayBadge.vue +98 -0
  16. package/components/Display/DisplayBytes.stories.ts +28 -0
  17. package/components/Display/DisplayBytes.vue +30 -0
  18. package/components/Display/DisplayDate.stories.ts +37 -0
  19. package/components/Display/DisplayDate.vue +29 -0
  20. package/components/Display/DisplayDonut.stories.ts +26 -0
  21. package/components/Display/DisplayDonut.vue +46 -0
  22. package/components/Display/DisplayDuration.stories.ts +28 -0
  23. package/components/Display/DisplayDuration.vue +28 -0
  24. package/components/Display/DisplayFileIcon.stories.ts +27 -0
  25. package/components/Display/DisplayFileIcon.vue +30 -0
  26. package/components/Display/DisplayFilePath.stories.ts +30 -0
  27. package/components/Display/DisplayFilePath.vue +61 -0
  28. package/components/Display/DisplayKbd.stories.ts +26 -0
  29. package/components/Display/DisplayKbd.vue +27 -0
  30. package/components/Display/DisplayKeyValue.stories.ts +56 -0
  31. package/components/Display/DisplayKeyValue.vue +51 -0
  32. package/components/Display/DisplayLabel.stories.ts +27 -0
  33. package/components/Display/DisplayLabel.vue +33 -0
  34. package/components/Display/DisplayNumber.stories.ts +27 -0
  35. package/components/Display/DisplayNumber.vue +24 -0
  36. package/components/Display/DisplayNumberBadge.stories.ts +26 -0
  37. package/components/Display/DisplayNumberBadge.vue +22 -0
  38. package/components/Display/DisplayPackageName.stories.ts +26 -0
  39. package/components/Display/DisplayPackageName.vue +49 -0
  40. package/components/Display/DisplayProgressBar.stories.ts +29 -0
  41. package/components/Display/DisplayProgressBar.vue +90 -0
  42. package/components/Display/DisplayProportionBar.stories.ts +40 -0
  43. package/components/Display/DisplayProportionBar.vue +43 -0
  44. package/components/Display/DisplaySafeImage.stories.ts +43 -0
  45. package/components/Display/DisplaySafeImage.vue +30 -0
  46. package/components/Display/DisplayStatusPill.stories.ts +34 -0
  47. package/components/Display/DisplayStatusPill.vue +42 -0
  48. package/components/Display/DisplayTree.stories.ts +76 -0
  49. package/components/Display/DisplayTree.vue +102 -0
  50. package/components/Display/DisplayVersion.stories.ts +25 -0
  51. package/components/Display/DisplayVersion.vue +21 -0
  52. package/components/Feedback/FeedbackEmptyState.stories.ts +38 -0
  53. package/components/Feedback/FeedbackEmptyState.vue +21 -0
  54. package/components/Feedback/FeedbackLoading.stories.ts +23 -0
  55. package/components/Feedback/FeedbackLoading.vue +21 -0
  56. package/components/Feedback/FeedbackSpinner.stories.ts +25 -0
  57. package/components/Feedback/FeedbackSpinner.vue +22 -0
  58. package/components/Feedback/FeedbackTip.stories.ts +34 -0
  59. package/components/Feedback/FeedbackTip.vue +29 -0
  60. package/components/Feedback/FeedbackToasts.stories.ts +40 -0
  61. package/components/Feedback/FeedbackToasts.vue +105 -0
  62. package/components/Form/FormCheckbox.stories.ts +36 -0
  63. package/components/Form/FormCheckbox.vue +30 -0
  64. package/components/Form/FormCombobox.stories.ts +35 -0
  65. package/components/Form/FormCombobox.vue +83 -0
  66. package/components/Form/FormField.stories.ts +56 -0
  67. package/components/Form/FormField.vue +36 -0
  68. package/components/Form/FormNumberInput.stories.ts +47 -0
  69. package/components/Form/FormNumberInput.vue +85 -0
  70. package/components/Form/FormRadioGroup.stories.ts +47 -0
  71. package/components/Form/FormRadioGroup.vue +43 -0
  72. package/components/Form/FormSearchField.stories.ts +22 -0
  73. package/components/Form/FormSearchField.vue +32 -0
  74. package/components/Form/FormSelect.stories.ts +47 -0
  75. package/components/Form/FormSelect.vue +56 -0
  76. package/components/Form/FormSwitch.stories.ts +36 -0
  77. package/components/Form/FormSwitch.vue +26 -0
  78. package/components/Form/FormTextInput.stories.ts +39 -0
  79. package/components/Form/FormTextInput.vue +51 -0
  80. package/components/Form/FormTextarea.stories.ts +47 -0
  81. package/components/Form/FormTextarea.vue +32 -0
  82. package/components/Layout/LayoutBreadcrumb.stories.ts +54 -0
  83. package/components/Layout/LayoutBreadcrumb.vue +54 -0
  84. package/components/Layout/LayoutCard.stories.ts +31 -0
  85. package/components/Layout/LayoutCard.vue +21 -0
  86. package/components/Layout/LayoutDataTable.stories.ts +77 -0
  87. package/components/Layout/LayoutDataTable.vue +145 -0
  88. package/components/Layout/LayoutExpandableList.stories.ts +28 -0
  89. package/components/Layout/LayoutExpandableList.vue +94 -0
  90. package/components/Layout/LayoutPanelGrids.stories.ts +28 -0
  91. package/components/Layout/LayoutPanelGrids.vue +26 -0
  92. package/components/Layout/LayoutSectionBlock.stories.ts +37 -0
  93. package/components/Layout/LayoutSectionBlock.vue +37 -0
  94. package/components/Layout/LayoutSideNav.stories.ts +33 -0
  95. package/components/Layout/LayoutSideNav.vue +48 -0
  96. package/components/Layout/LayoutSplitPane.stories.ts +44 -0
  97. package/components/Layout/LayoutSplitPane.vue +30 -0
  98. package/components/Layout/LayoutTabs.stories.ts +43 -0
  99. package/components/Layout/LayoutTabs.vue +56 -0
  100. package/components/Layout/LayoutToolbar.stories.ts +60 -0
  101. package/components/Layout/LayoutToolbar.vue +28 -0
  102. package/components/Layout/LayoutVirtualList.stories.ts +30 -0
  103. package/components/Layout/LayoutVirtualList.vue +82 -0
  104. package/components/Overlay/OverlayDrawer.stories.ts +47 -0
  105. package/components/Overlay/OverlayDrawer.vue +58 -0
  106. package/components/Overlay/OverlayDropdown.stories.ts +25 -0
  107. package/components/Overlay/OverlayDropdown.vue +30 -0
  108. package/components/Overlay/OverlayDropdownItem.stories.ts +26 -0
  109. package/components/Overlay/OverlayDropdownItem.vue +31 -0
  110. package/components/Overlay/OverlayDropdownLabel.vue +9 -0
  111. package/components/Overlay/OverlayDropdownSeparator.vue +7 -0
  112. package/components/Overlay/OverlayModal.stories.ts +33 -0
  113. package/components/Overlay/OverlayModal.vue +48 -0
  114. package/components/Overlay/OverlayTooltip.stories.ts +33 -0
  115. package/components/Overlay/OverlayTooltip.vue +38 -0
  116. package/composables/colorScheme.ts +58 -0
  117. package/composables/toast.ts +81 -0
  118. package/package.json +99 -0
  119. package/skills/antfu-design/SKILL.md +65 -0
  120. package/skills/antfu-design/references/advanced-patterns.md +39 -0
  121. package/skills/antfu-design/references/best-practices.md +54 -0
  122. package/skills/antfu-design/references/core-components.md +72 -0
  123. package/skills/antfu-design/references/core-setup.md +56 -0
  124. package/skills/antfu-design/references/core-tokens.md +100 -0
  125. package/skills/antfu-design/references/features-data-presentation.md +27 -0
  126. package/splitpanes.d.ts +70 -0
  127. package/styles/animations.css +47 -0
  128. package/styles/base.css +31 -0
  129. package/styles/floating-vue.css +28 -0
  130. package/styles/index.css +7 -0
  131. package/styles/reka-ui.css +112 -0
  132. package/styles/scrollbar.css +24 -0
  133. package/styles/splitpanes.css +61 -0
  134. package/unocss/colors.ts +127 -0
  135. package/unocss/index.ts +99 -0
  136. package/unocss/options.ts +31 -0
  137. package/unocss/patterns.ts +38 -0
  138. package/unocss/rules.ts +26 -0
  139. package/unocss/severity.ts +16 -0
  140. package/unocss/shortcuts.ts +68 -0
  141. package/utils/color.ts +328 -0
  142. package/utils/contrast.ts +118 -0
  143. package/utils/format.ts +389 -0
  144. package/utils/icon.ts +200 -0
  145. package/utils/index.ts +13 -0
  146. package/utils/keybinding.ts +199 -0
  147. package/utils/misc.ts +141 -0
  148. package/utils/path.ts +243 -0
  149. package/utils/semver.ts +147 -0
  150. package/utils/tree.ts +89 -0
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ import { vTooltip } from 'floating-vue'
3
+ import { computed } from 'vue'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ icon?: string
8
+ /** Tooltip text (floating-vue). Requires `@antfu/design/styles/floating-vue.css`. */
9
+ tooltip?: string
10
+ active?: boolean
11
+ disabled?: boolean
12
+ /** Accessible label when the button has no visible text. */
13
+ label?: string
14
+ size?: 'sm' | 'md' | 'lg'
15
+ /** Compact, square (non-circular) icon button for dense toolbars. */
16
+ compact?: boolean
17
+ /** Class(es) applied when `active` — overrides the default `color-active bg-active` tint. */
18
+ activeClass?: string
19
+ }>(),
20
+ { size: 'md' },
21
+ )
22
+
23
+ const SIZE: Record<NonNullable<typeof props.size>, string> = {
24
+ sm: 'w-7! h-7! text-sm',
25
+ md: '',
26
+ lg: 'w-11! h-11! text-lg',
27
+ }
28
+
29
+ const baseClass = computed(() => (props.compact ? 'btn-icon-compact' : 'btn-icon'))
30
+ const sizeClass = computed(() => (props.compact ? '' : SIZE[props.size]))
31
+ const activeStateClass = computed(() => (props.active ? (props.activeClass || 'color-active bg-active op100') : ''))
32
+ </script>
33
+
34
+ <template>
35
+ <button
36
+ v-tooltip="tooltip"
37
+ type="button"
38
+ :class="[baseClass, sizeClass, activeStateClass]"
39
+ :disabled="disabled"
40
+ :aria-label="label ?? tooltip"
41
+ :aria-pressed="active || undefined"
42
+ >
43
+ <span v-if="icon" :class="icon" aria-hidden="true" />
44
+ <slot />
45
+ <slot name="badge" />
46
+ </button>
47
+ </template>
@@ -0,0 +1,36 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayAvatar from './DisplayAvatar.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayAvatar',
6
+ component: DisplayAvatar,
7
+ tags: ['autodocs'],
8
+ args: { name: 'Anthony Fu', size: 32 },
9
+ } satisfies Meta<typeof DisplayAvatar>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const WithImage: Story = {
15
+ args: { src: 'https://github.com/antfu.png', name: 'Anthony Fu' },
16
+ }
17
+
18
+ export const InitialsFallback: Story = {
19
+ args: { name: 'Anthony Fu' },
20
+ }
21
+
22
+ export const Square: Story = {
23
+ args: { name: 'Vite Press', square: true, size: 48 },
24
+ }
25
+
26
+ export const Gallery: Story = {
27
+ render: () => ({
28
+ components: { DisplayAvatar },
29
+ setup() {
30
+ return { names: ['Vue', 'React Native', 'svelte-kit', 'unocss', 'Anthony Fu', 'esbuild'] }
31
+ },
32
+ template: `<div class="flex flex-wrap gap-2 items-center">
33
+ <DisplayAvatar v-for="n in names" :key="n" :name="n" :size="40" />
34
+ </div>`,
35
+ }),
36
+ }
@@ -0,0 +1,58 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useColorScheme } from '../../composables/colorScheme'
4
+ import { getHashColorFromString } from '../../utils/color'
5
+ import DisplaySafeImage from './DisplaySafeImage.vue'
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ /** Image source. Falls back to initials when absent or failed. */
10
+ src?: string
11
+ /** Name driving the initials and the hash-tinted fallback color. */
12
+ name?: string
13
+ /** Size in pixels (width and height). */
14
+ size?: number
15
+ /** Use rounded-corner square instead of a circle. */
16
+ square?: boolean
17
+ /**
18
+ * The app's current color scheme — tunes the fallback hash color for
19
+ * contrast. Falls back to {@link provideColorScheme} context, then `'light'`.
20
+ */
21
+ colorScheme?: 'light' | 'dark'
22
+ }>(),
23
+ { size: 32 },
24
+ )
25
+
26
+ const scheme = useColorScheme(() => props.colorScheme)
27
+ const dark = computed(() => scheme.value === 'dark')
28
+
29
+ const initials = computed(() =>
30
+ (props.name ?? '')
31
+ .split(/[\s/-]+/)
32
+ .filter(Boolean)
33
+ .slice(0, 2)
34
+ .map(part => part[0]!.toUpperCase())
35
+ .join(''),
36
+ )
37
+
38
+ const fallbackStyle = computed(() => ({
39
+ background: getHashColorFromString(props.name ?? '', 0.18, dark.value),
40
+ color: getHashColorFromString(props.name ?? '', 1, dark.value),
41
+ }))
42
+ </script>
43
+
44
+ <template>
45
+ <span
46
+ class="text-xs font-medium inline-flex shrink-0 select-none items-center justify-center overflow-hidden"
47
+ :class="square ? 'rounded-md' : 'rounded-full'"
48
+ :style="{ width: `${size}px`, height: `${size}px` }"
49
+ >
50
+ <DisplaySafeImage :src="src" :alt="name">
51
+ <template #fallback>
52
+ <span class="inline-flex h-full w-full items-center justify-center" :style="fallbackStyle">
53
+ {{ initials }}
54
+ </span>
55
+ </template>
56
+ </DisplaySafeImage>
57
+ </span>
58
+ </template>
@@ -0,0 +1,31 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayBadge from './DisplayBadge.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayBadge',
6
+ component: DisplayBadge,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ variant: { control: 'inline-radio', options: ['subtle', 'solid'] },
10
+ size: { control: 'inline-radio', options: ['sm', 'md'] },
11
+ },
12
+ args: { text: 'vite', variant: 'subtle', size: 'md' },
13
+ } satisfies Meta<typeof DisplayBadge>
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof meta>
17
+
18
+ export const HashColored: Story = { args: { text: 'unocss' } }
19
+ export const Solid: Story = { args: { text: 'rolldown', variant: 'solid' } }
20
+ export const Palette: Story = { args: { text: 'esm', color: 'green' } }
21
+ export const Muted: Story = { args: { text: 'unknown', color: false } }
22
+ export const WithIcon: Story = { args: { text: 'stable', icon: 'i-ph:seal-check', color: 'green' } }
23
+
24
+ export const Gallery: Story = {
25
+ render: () => ({
26
+ components: { DisplayBadge },
27
+ template: `<div class="flex flex-wrap gap-2">
28
+ <DisplayBadge v-for="n in ['vue','react','svelte','vite','unocss','nuxt','rolldown','eslint']" :key="n" :text="n" />
29
+ </div>`,
30
+ }),
31
+ }
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useColorScheme } from '../../composables/colorScheme'
4
+ import { getHashColorFromString, getHsla } from '../../utils/color'
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ /** Text content (also the hash seed when `color` is `true`). */
9
+ text?: string
10
+ /**
11
+ * - `true` (default): deterministic color hashed from `text`
12
+ * - `false`: neutral / muted
13
+ * - `number`: explicit hue (0–360)
14
+ * - `'#...'` / `'hsl(...)'` / `'rgb(...)'`: explicit CSS color
15
+ * - any other string: a palette name → `badge-color-<name>`
16
+ */
17
+ color?: boolean | number | string
18
+ /** `subtle` = tinted background, `solid` = filled. */
19
+ variant?: 'subtle' | 'solid'
20
+ size?: 'sm' | 'md'
21
+ /** Leading icon class (e.g. `i-ph:seal-check`). */
22
+ icon?: string
23
+ /** Render as another element/component. */
24
+ as?: string
25
+ /**
26
+ * The app's current color scheme — tunes hash colors for contrast. Falls
27
+ * back to {@link provideColorScheme} context, then `'light'`.
28
+ */
29
+ colorScheme?: 'light' | 'dark'
30
+ }>(),
31
+ {
32
+ color: true,
33
+ variant: 'subtle',
34
+ size: 'md',
35
+ },
36
+ )
37
+
38
+ function isCssColor(value: string): boolean {
39
+ return /^#|^hsl|^rgb|^var\(/.test(value)
40
+ }
41
+
42
+ const scheme = useColorScheme(() => props.colorScheme)
43
+ const dark = computed(() => scheme.value === 'dark')
44
+
45
+ const seedColor = computed<string | undefined>(() => {
46
+ const { color, text } = props
47
+ if (typeof color === 'number')
48
+ return getHsla(color, 1, dark.value)
49
+ if (typeof color === 'string' && isCssColor(color))
50
+ return color
51
+ if (color === true && text)
52
+ return getHashColorFromString(text, 1, dark.value)
53
+ return undefined
54
+ })
55
+
56
+ const seedBg = computed<string | undefined>(() => {
57
+ const { color, text } = props
58
+ if (typeof color === 'number')
59
+ return getHsla(color, props.variant === 'solid' ? 1 : 0.12, dark.value)
60
+ if (typeof color === 'string' && isCssColor(color))
61
+ return color
62
+ if (color === true && text)
63
+ return getHashColorFromString(text, props.variant === 'solid' ? 1 : 0.12, dark.value)
64
+ return undefined
65
+ })
66
+
67
+ const style = computed(() => {
68
+ if (!seedColor.value)
69
+ return undefined
70
+ return props.variant === 'solid'
71
+ ? { color: '#fff', background: seedBg.value }
72
+ : { color: seedColor.value, background: seedBg.value }
73
+ })
74
+
75
+ const paletteClass = computed(() =>
76
+ typeof props.color === 'string' && !isCssColor(props.color)
77
+ ? `badge-color-${props.color}`
78
+ : props.color === false
79
+ ? 'badge-muted'
80
+ : '',
81
+ )
82
+
83
+ const sizeClass = computed(() =>
84
+ props.size === 'sm' ? 'text-micro px-1.5 py-0.25' : 'text-xs px-2 py-0.5',
85
+ )
86
+ </script>
87
+
88
+ <template>
89
+ <component
90
+ :is="as || 'span'"
91
+ class="badge"
92
+ :class="[sizeClass, paletteClass]"
93
+ :style="style"
94
+ >
95
+ <span v-if="icon" :class="icon" aria-hidden="true" />
96
+ <slot>{{ text }}</slot>
97
+ </component>
98
+ </template>
@@ -0,0 +1,28 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayBytes from './DisplayBytes.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayBytes',
6
+ component: DisplayBytes,
7
+ tags: ['autodocs'],
8
+ args: { bytes: 204800 },
9
+ } satisfies Meta<typeof DisplayBytes>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Default: Story = { args: { bytes: 204800 } }
15
+ export const Colorized: Story = { args: { bytes: 2097152, colorize: true } }
16
+ export const WithTotal: Story = { args: { bytes: 524288, total: 1048576 } }
17
+
18
+ export const Scale: Story = {
19
+ render: () => ({
20
+ components: { DisplayBytes },
21
+ template: `<div class="flex flex-col gap-1">
22
+ <DisplayBytes :bytes="512" colorize />
23
+ <DisplayBytes :bytes="204800" colorize />
24
+ <DisplayBytes :bytes="2097152" colorize />
25
+ <DisplayBytes :bytes="524288" :total="1048576" />
26
+ </div>`,
27
+ }),
28
+ }
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { formatBytes, getBytesColor } from '../../utils/format'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ bytes: number
8
+ base?: 1024 | 1000
9
+ /** Maximum decimal places before trailing-zero trimming. */
10
+ digits?: number
11
+ colorize?: boolean
12
+ /** When given, also show `bytes / total` as a percent. */
13
+ total?: number
14
+ mono?: boolean
15
+ }>(),
16
+ { mono: true, base: 1024 },
17
+ )
18
+
19
+ const parts = computed(() => formatBytes(props.bytes, { base: props.base, digits: props.digits }))
20
+ const colorClass = computed(() => (props.colorize ? getBytesColor(props.bytes) : ''))
21
+ const percent = computed(() =>
22
+ props.total && props.total > 0 ? (props.bytes / props.total) * 100 : undefined,
23
+ )
24
+ </script>
25
+
26
+ <template>
27
+ <span :class="[colorClass, { 'font-mono tabular-nums': mono }]">
28
+ {{ parts[0] }}<span class="text-xs ml-0.5 op-fade">{{ parts[1] }}</span><span v-if="percent != null" class="text-xs ml-1 op-mute">{{ percent.toFixed(0) }}%</span>
29
+ </span>
30
+ </template>
@@ -0,0 +1,37 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayDate from './DisplayDate.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayDate',
6
+ component: DisplayDate,
7
+ tags: ['autodocs'],
8
+ args: { date: Date.now() }, // required prop; stories supply their own via render
9
+ } satisfies Meta<typeof DisplayDate>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Recent: Story = {
15
+ args: { date: Date.now() - 1000 * 60 * 5 },
16
+ }
17
+
18
+ export const Old: Story = {
19
+ args: { date: Date.now() - 1000 * 60 * 60 * 24 * 400 },
20
+ }
21
+
22
+ export const Colorized: Story = {
23
+ render: () => ({
24
+ components: { DisplayDate },
25
+ setup() {
26
+ const now = Date.now()
27
+ return {
28
+ recent: now - 1000 * 60 * 5,
29
+ old: now - 1000 * 60 * 60 * 24 * 400,
30
+ }
31
+ },
32
+ template: `<div class="flex flex-col gap-1">
33
+ <DisplayDate :date="recent" colorize />
34
+ <DisplayDate :date="old" colorize />
35
+ </div>`,
36
+ }),
37
+ }
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ import { useNow } from '@vueuse/core'
3
+ import { vTooltip } from 'floating-vue'
4
+ import { computed } from 'vue'
5
+ import { formatDateTime, formatTimeAgo, getAgeColor } from '../../utils/format'
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ date: number | string | Date
10
+ /** Tint by age (freshness scale). */
11
+ colorize?: boolean
12
+ /** Update the relative label over time. */
13
+ live?: boolean
14
+ }>(),
15
+ {},
16
+ )
17
+
18
+ const now = useNow({ interval: 30_000 })
19
+ const time = computed(() => new Date(props.date).getTime())
20
+ const relative = computed(() => formatTimeAgo(time.value, props.live ? now.value.getTime() : Date.now()))
21
+ const exact = computed(() => formatDateTime(time.value))
22
+ const colorClass = computed(() => (props.colorize ? getAgeColor(Math.abs(now.value.getTime() - time.value)) : ''))
23
+ </script>
24
+
25
+ <template>
26
+ <time v-tooltip="exact" :datetime="new Date(time).toISOString()" :class="colorClass">
27
+ <slot :relative="relative" :exact="exact">{{ relative }}</slot>
28
+ </time>
29
+ </template>
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayDonut from './DisplayDonut.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayDonut',
6
+ component: DisplayDonut,
7
+ tags: ['autodocs'],
8
+ args: { value: 0.6, size: 48, thickness: 4 },
9
+ } satisfies Meta<typeof DisplayDonut>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Default: Story = { args: { value: 0.6 } }
15
+ export const CustomColor: Story = { args: { value: 0.75, size: 64, color: '#3178c6' } }
16
+
17
+ export const Sizes: Story = {
18
+ render: () => ({
19
+ components: { DisplayDonut },
20
+ template: `<div class="flex items-center gap-4">
21
+ <DisplayDonut :value="0.25" />
22
+ <DisplayDonut :value="0.6" :size="48" />
23
+ <DisplayDonut :value="0.9" :size="64" :thickness="6" />
24
+ </div>`,
25
+ }),
26
+ }
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ import { clamp } from '@antfu/utils'
3
+ import { computed } from 'vue'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ /** Progress 0..1. */
8
+ value: number
9
+ size?: number
10
+ thickness?: number
11
+ /** Override the foreground color (defaults to the active token). */
12
+ color?: string
13
+ }>(),
14
+ { size: 36, thickness: 4 },
15
+ )
16
+
17
+ const radius = computed(() => (props.size - props.thickness) / 2)
18
+ const circumference = computed(() => 2 * Math.PI * radius.value)
19
+ const offset = computed(() => circumference.value * (1 - clamp(props.value, 0, 1)))
20
+ </script>
21
+
22
+ <template>
23
+ <svg :width="size" :height="size" :viewBox="`0 0 ${size} ${size}`" class="-rotate-90">
24
+ <circle
25
+ :cx="size / 2"
26
+ :cy="size / 2"
27
+ :r="radius"
28
+ fill="none"
29
+ :stroke-width="thickness"
30
+ class="op-mute stroke-current"
31
+ />
32
+ <circle
33
+ :cx="size / 2"
34
+ :cy="size / 2"
35
+ :r="radius"
36
+ fill="none"
37
+ :stroke-width="thickness"
38
+ :stroke-dasharray="circumference"
39
+ :stroke-dashoffset="offset"
40
+ stroke-linecap="round"
41
+ class="transition-all stroke-current"
42
+ :class="{ 'color-active': !color }"
43
+ :style="color ? { color } : undefined"
44
+ />
45
+ </svg>
46
+ </template>
@@ -0,0 +1,28 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayDuration from './DisplayDuration.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayDuration',
6
+ component: DisplayDuration,
7
+ tags: ['autodocs'],
8
+ args: { ms: 750 },
9
+ } satisfies Meta<typeof DisplayDuration>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Default: Story = { args: { ms: 750 } }
15
+ export const Colorized: Story = { args: { ms: 3200, colorize: true } }
16
+
17
+ export const Scale: Story = {
18
+ render: () => ({
19
+ components: { DisplayDuration },
20
+ template: `<div class="flex flex-col gap-1">
21
+ <DisplayDuration :ms="12" colorize />
22
+ <DisplayDuration :ms="180" colorize />
23
+ <DisplayDuration :ms="750" colorize />
24
+ <DisplayDuration :ms="3200" colorize />
25
+ <DisplayDuration :ms="9000" colorize />
26
+ </div>`,
27
+ }),
28
+ }
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import type { DurationUnit } from '../../utils/format'
3
+ import { computed } from 'vue'
4
+ import { formatDuration, getDurationColor } from '../../utils/format'
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ /** Duration value, interpreted in `unit`. */
9
+ ms: number
10
+ /** Input unit: `'ns'`, `'us'`, `'ms'` (default) or `'s'`. */
11
+ unit?: DurationUnit
12
+ /** Tint by severity threshold. */
13
+ colorize?: boolean
14
+ mono?: boolean
15
+ }>(),
16
+ { mono: true, unit: 'ms' },
17
+ )
18
+
19
+ const FACTOR_MS: Record<DurationUnit, number> = { ns: 1e-6, us: 1e-3, ms: 1, s: 1000 }
20
+ const parts = computed(() => formatDuration(props.ms, { unit: props.unit }))
21
+ const colorClass = computed(() => (props.colorize ? getDurationColor(props.ms * FACTOR_MS[props.unit]) : ''))
22
+ </script>
23
+
24
+ <template>
25
+ <span :class="[colorClass, { 'font-mono tabular-nums': mono }]">
26
+ {{ parts[0] }}<span class="text-xs ml-0.5 op-fade">{{ parts[1] }}</span>
27
+ </span>
28
+ </template>
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayFileIcon from './DisplayFileIcon.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayFileIcon',
6
+ component: DisplayFileIcon,
7
+ tags: ['autodocs'],
8
+ args: { path: 'a.vue' },
9
+ } satisfies Meta<typeof DisplayFileIcon>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Default: Story = { args: { path: 'component.vue' } }
15
+
16
+ export const ByType: Story = {
17
+ render: () => ({
18
+ components: { DisplayFileIcon },
19
+ template: `<div class="text-xl flex items-center gap-2">
20
+ <DisplayFileIcon path="a.vue" />
21
+ <DisplayFileIcon path="b.ts" />
22
+ <DisplayFileIcon path="c.json" />
23
+ <DisplayFileIcon path="d.css" />
24
+ <DisplayFileIcon path="e.md" />
25
+ </div>`,
26
+ }),
27
+ }
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import type { FileIconRule } from '../../utils/icon'
3
+ import { computed } from 'vue'
4
+ import { getFileType, getFolderIcon } from '../../utils/icon'
5
+
6
+ const props = defineProps<{
7
+ /** File path or module id (or folder name when `directory`). */
8
+ path: string
9
+ /** Override the default (catppuccin) file rule list — e.g. `vscodeFileIconRules`. */
10
+ rules?: FileIconRule[]
11
+ /** Render a folder icon instead of a file icon. */
12
+ directory?: boolean
13
+ /** For directories: show the open-folder glyph. */
14
+ open?: boolean
15
+ /** Named-folder lookup for directories. */
16
+ folderRules?: Record<string, string>
17
+ }>()
18
+
19
+ const type = computed(() => {
20
+ if (props.directory) {
21
+ const name = props.path.replace(/\/+$/, '').split('/').pop() || 'folder'
22
+ return { name, icon: getFolderIcon(name, props.open, props.folderRules) }
23
+ }
24
+ return getFileType(props.path, props.rules)
25
+ })
26
+ </script>
27
+
28
+ <template>
29
+ <span :class="type.icon" :title="type.name" aria-hidden="true" />
30
+ </template>
@@ -0,0 +1,30 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayFilePath from './DisplayFilePath.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayFilePath',
6
+ component: DisplayFilePath,
7
+ tags: ['autodocs'],
8
+ args: { path: '/project/src/components/Badge.vue', root: '/project' },
9
+ } satisfies Meta<typeof DisplayFilePath>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const WithRoot: Story = {
15
+ args: { path: '/project/src/components/Badge.vue', root: '/project' },
16
+ }
17
+
18
+ export const Module: Story = {
19
+ args: { path: '/project/node_modules/.pnpm/vue@3.5.0/node_modules/vue/dist/vue.mjs' },
20
+ }
21
+
22
+ export const Examples: Story = {
23
+ render: () => ({
24
+ components: { DisplayFilePath },
25
+ template: `<div class="flex flex-col gap-2 max-w-md">
26
+ <DisplayFilePath path="/project/src/components/Badge.vue" root="/project" />
27
+ <DisplayFilePath path="/project/node_modules/.pnpm/vue@3.5.0/node_modules/vue/dist/vue.mjs" />
28
+ </div>`,
29
+ }),
30
+ }