@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,61 @@
1
+ <script setup lang="ts">
2
+ import { vTooltip } from 'floating-vue'
3
+ import { computed } from 'vue'
4
+ import { getFileIcon } from '../../utils/icon'
5
+ import { parseReadablePath } from '../../utils/path'
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ path: string
10
+ /** Project root, trimmed from the display path. */
11
+ root?: string
12
+ /** Show a leading file icon. */
13
+ icon?: boolean
14
+ /** Link target. */
15
+ href?: string
16
+ /** Trailing line number (rendered `:line`). */
17
+ line?: number
18
+ /** Trailing column number (rendered `:line:col`, needs `line`). */
19
+ column?: number
20
+ /** Dim the directory portion of the path. */
21
+ dim?: boolean
22
+ /** Style as interactive (cursor + hover) for `@click` usage even without `href`. */
23
+ clickable?: boolean
24
+ /** Prefix substituted for a `.pnpm` store chunk (passed to the path parser). */
25
+ pnpmCollapse?: string
26
+ }>(),
27
+ { root: '', icon: true, dim: true },
28
+ )
29
+
30
+ const parsed = computed(() => parseReadablePath(props.path, props.root, { pnpmCollapse: props.pnpmCollapse }))
31
+
32
+ /** Split the display path into directory / base / query(+hash) for distinct styling. */
33
+ const segments = computed(() => {
34
+ const full = parsed.value.path
35
+ const q = full.search(/[?#]/)
36
+ const clean = q === -1 ? full : full.slice(0, q)
37
+ const query = q === -1 ? '' : full.slice(q)
38
+ const idx = clean.lastIndexOf('/')
39
+ const dir = idx === -1 ? '' : clean.slice(0, idx + 1)
40
+ const base = idx === -1 ? clean : clean.slice(idx + 1)
41
+ return { dir, base, query }
42
+ })
43
+
44
+ const interactive = computed(() => props.href != null || props.clickable)
45
+ </script>
46
+
47
+ <template>
48
+ <component
49
+ :is="href ? 'a' : 'span'"
50
+ v-tooltip="path"
51
+ :href="href"
52
+ class="text-sm font-mono inline-flex gap-1 max-w-full min-w-0 items-center"
53
+ :class="{ 'hover:color-active transition': interactive, 'cursor-pointer': clickable && !href }"
54
+ >
55
+ <span v-if="icon" :class="getFileIcon(segments.base)" class="op-fade shrink-0" aria-hidden="true" />
56
+ <span class="truncate">
57
+ <span v-if="segments.dir" :class="dim ? 'op-mute' : 'color-base'">{{ segments.dir }}</span><span class="color-base">{{ segments.base }}</span><span v-if="segments.query" class="op-mute italic">{{ segments.query }}</span><span v-if="line != null" class="op-mute">:{{ line }}<template v-if="column != null">:{{ column }}</template></span>
58
+ </span>
59
+ <span v-if="parsed.moduleName" class="text-xs op-mute shrink-0">({{ parsed.moduleName }})</span>
60
+ </component>
61
+ </template>
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayKbd from './DisplayKbd.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayKbd',
6
+ component: DisplayKbd,
7
+ tags: ['autodocs'],
8
+ args: { keys: 'mod+k' },
9
+ } satisfies Meta<typeof DisplayKbd>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Default: Story = { args: { keys: 'mod+k' } }
15
+ export const Chord: Story = { args: { keys: 'g g' } }
16
+
17
+ export const Bindings: Story = {
18
+ render: () => ({
19
+ components: { DisplayKbd },
20
+ template: `<div class="flex items-center gap-3">
21
+ <DisplayKbd keys="mod+k" />
22
+ <DisplayKbd keys="shift+?" />
23
+ <DisplayKbd keys="g g" />
24
+ </div>`,
25
+ }),
26
+ }
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { bindingDisplay } from '../../utils/keybinding'
4
+
5
+ const props = defineProps<{
6
+ /** A chord/binding string, e.g. `mod+k` or `g g`, rendered as platform glyphs. */
7
+ keys?: string
8
+ }>()
9
+
10
+ const tokens = computed(() => (props.keys ? bindingDisplay(props.keys) : []))
11
+ </script>
12
+
13
+ <template>
14
+ <span class="inline-flex gap-0.5 items-center">
15
+ <template v-if="keys">
16
+ <kbd
17
+ v-for="(token, i) in tokens"
18
+ :key="i"
19
+ class="text-micro color-muted leading-none font-mono px-1 border border-base rounded bg-secondary inline-flex h-5 min-w-5 items-center justify-center"
20
+ >{{ token }}</kbd>
21
+ </template>
22
+ <kbd
23
+ v-else
24
+ class="text-micro color-muted leading-none font-mono px-1.5 border border-base rounded bg-secondary inline-flex h-5 min-w-5 items-center justify-center"
25
+ ><slot /></kbd>
26
+ </span>
27
+ </template>
@@ -0,0 +1,56 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayKeyValue from './DisplayKeyValue.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayKeyValue',
6
+ component: DisplayKeyValue,
7
+ tags: ['autodocs'],
8
+ args: { label: 'Version', value: '1.4.0' },
9
+ } satisfies Meta<typeof DisplayKeyValue>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Inline: Story = { args: { label: 'Version', value: '1.4.0' } }
15
+ export const Stacked: Story = { args: { label: 'Total size', value: '204 kB', orientation: 'stacked' } }
16
+
17
+ export const DetailRows: Story = {
18
+ render: () => ({
19
+ components: { DisplayKeyValue },
20
+ template: `<div class="w-72 flex flex-col gap-1">
21
+ <DisplayKeyValue label="Name" value="@antfu/design" />
22
+ <DisplayKeyValue label="Version" value="1.4.0" />
23
+ <DisplayKeyValue label="License" value="MIT" :mono="false" />
24
+ <DisplayKeyValue label="Downloads" value="1,204,558" />
25
+ </div>`,
26
+ }),
27
+ }
28
+
29
+ export const StatStrip: Story = {
30
+ render: () => ({
31
+ components: { DisplayKeyValue },
32
+ template: `<div class="flex gap-6">
33
+ <DisplayKeyValue label="Files" value="128" orientation="stacked" />
34
+ <DisplayKeyValue label="Size" value="204 kB" orientation="stacked" />
35
+ <DisplayKeyValue label="Modules" value="42" orientation="stacked" />
36
+ </div>`,
37
+ }),
38
+ }
39
+
40
+ export const WithBadge: Story = {
41
+ render: () => ({
42
+ components: { DisplayKeyValue },
43
+ template: `<div class="w-72 flex flex-col gap-3">
44
+ <DisplayKeyValue label="Status" value="passing">
45
+ <template #badge>
46
+ <span class="badge-color-green">ok</span>
47
+ </template>
48
+ </DisplayKeyValue>
49
+ <DisplayKeyValue label="Coverage" value="98%" orientation="stacked">
50
+ <template #badge>
51
+ <span class="badge-color-blue">+2%</span>
52
+ </template>
53
+ </DisplayKeyValue>
54
+ </div>`,
55
+ }),
56
+ }
@@ -0,0 +1,51 @@
1
+ <script setup lang="ts">
2
+ withDefaults(
3
+ defineProps<{
4
+ /** The label text (falls back slot: `#label`). */
5
+ label?: string
6
+ /** The value (falls back slot: `#default`). */
7
+ value?: string | number
8
+ /** Inline row or stacked stat. */
9
+ orientation?: 'inline' | 'stacked'
10
+ /** Render the value with `font-mono tabular-nums`. */
11
+ mono?: boolean
12
+ }>(),
13
+ { orientation: 'inline', mono: true },
14
+ )
15
+
16
+ defineSlots<{
17
+ /** The value content; falls back to the `value` prop. */
18
+ default?: () => any
19
+ /** The label content; falls back to the `label` prop. */
20
+ label?: () => any
21
+ /** Trailing badge / adornment. */
22
+ badge?: () => any
23
+ }>()
24
+ </script>
25
+
26
+ <template>
27
+ <div
28
+ v-if="orientation === 'stacked'"
29
+ class="flex flex-col gap-0.5"
30
+ >
31
+ <span class="text-micro color-faint tracking-wide uppercase">
32
+ <slot name="label">{{ label }}</slot>
33
+ </span>
34
+ <span class="text-lg color-base flex gap-2 items-center" :class="{ 'font-mono tabular-nums': mono }">
35
+ <slot>{{ value }}</slot>
36
+ <slot name="badge" />
37
+ </span>
38
+ </div>
39
+ <div
40
+ v-else
41
+ class="flex gap-2 items-center justify-between"
42
+ >
43
+ <span class="text-sm color-muted">
44
+ <slot name="label">{{ label }}</slot>
45
+ </span>
46
+ <span class="text-sm color-base flex gap-2 items-center" :class="{ 'font-mono tabular-nums': mono }">
47
+ <slot>{{ value }}</slot>
48
+ <slot name="badge" />
49
+ </span>
50
+ </div>
51
+ </template>
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayLabel from './DisplayLabel.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayLabel',
6
+ component: DisplayLabel,
7
+ tags: ['autodocs'],
8
+ args: { text: 'bug', color: '#d73a4a' },
9
+ } satisfies Meta<typeof DisplayLabel>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Colored: Story = { args: { text: 'bug', color: '#d73a4a' } }
15
+ export const Muted: Story = { args: { text: 'wontfix' } }
16
+
17
+ export const GitHubLabels: Story = {
18
+ render: () => ({
19
+ components: { DisplayLabel },
20
+ template: `<div class="flex flex-wrap gap-2">
21
+ <DisplayLabel text="bug" color="#d73a4a" />
22
+ <DisplayLabel text="enhancement" color="#a2eeef" />
23
+ <DisplayLabel text="docs" color="#0075ca" />
24
+ <DisplayLabel text="wontfix" />
25
+ </div>`,
26
+ }),
27
+ }
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useColorScheme } from '../../composables/colorScheme'
4
+ import { labelStyle } from '../../utils/color'
5
+
6
+ const props = defineProps<{
7
+ /** Display text. */
8
+ text?: string
9
+ /** Base hex color; a contrast-aware tinted chip is derived from it. */
10
+ color?: string
11
+ /** The app's current color scheme. Falls back to context, then `'light'`. */
12
+ colorScheme?: 'light' | 'dark'
13
+ }>()
14
+
15
+ const scheme = useColorScheme(() => props.colorScheme)
16
+
17
+ const style = computed(() => {
18
+ if (!props.color)
19
+ return undefined
20
+ const s = labelStyle(props.color, scheme.value === 'dark')
21
+ return { color: s.color, background: s.background, borderColor: s.borderColor }
22
+ })
23
+ </script>
24
+
25
+ <template>
26
+ <span
27
+ class="text-xs leading-none font-medium px-1.5 py-0.5 border rounded-full inline-flex gap-1 items-center"
28
+ :class="{ 'badge-muted border-transparent': !color }"
29
+ :style="style"
30
+ >
31
+ <slot>{{ text }}</slot>
32
+ </span>
33
+ </template>
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayNumber from './DisplayNumber.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayNumber',
6
+ component: DisplayNumber,
7
+ tags: ['autodocs'],
8
+ args: { value: 1234567 },
9
+ } satisfies Meta<typeof DisplayNumber>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Default: Story = { args: { value: 1234567 } }
15
+ export const WithSuffix: Story = { args: { value: 42, suffix: 'ms' } }
16
+ export const Percent: Story = { args: { value: 0.42, options: { style: 'percent' } } }
17
+
18
+ export const Formats: Story = {
19
+ render: () => ({
20
+ components: { DisplayNumber },
21
+ template: `<div class="flex flex-col gap-2">
22
+ <DisplayNumber :value="1234567" />
23
+ <DisplayNumber :value="42" suffix="ms" />
24
+ <DisplayNumber :value="0.42" :options="{ style: 'percent' }" />
25
+ </div>`,
26
+ }),
27
+ }
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { formatNumber } from '../../utils/format'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ value: number
8
+ prefix?: string
9
+ suffix?: string
10
+ /** Forwarded to `Intl.NumberFormat`. */
11
+ options?: Intl.NumberFormatOptions
12
+ mono?: boolean
13
+ }>(),
14
+ { mono: true },
15
+ )
16
+
17
+ const formatted = computed(() => formatNumber(props.value, props.options))
18
+ </script>
19
+
20
+ <template>
21
+ <span :class="{ 'font-mono tabular-nums': mono }">
22
+ <span v-if="prefix" class="op-fade">{{ prefix }}</span>{{ formatted }}<span v-if="suffix" class="text-xs ml-0.5 op-fade">{{ suffix }}</span>
23
+ </span>
24
+ </template>
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayNumberBadge from './DisplayNumberBadge.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayNumberBadge',
6
+ component: DisplayNumberBadge,
7
+ tags: ['autodocs'],
8
+ args: { value: 128, prefix: '×' },
9
+ } satisfies Meta<typeof DisplayNumberBadge>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Default: Story = { args: { value: 128, prefix: '×' } }
15
+ export const Colored: Story = { args: { value: 42, color: 'green' } }
16
+
17
+ export const Examples: Story = {
18
+ render: () => ({
19
+ components: { DisplayNumberBadge },
20
+ template: `<div class="flex items-center gap-2">
21
+ <DisplayNumberBadge :value="128" prefix="×" />
22
+ <DisplayNumberBadge :value="42" color="green" />
23
+ <DisplayNumberBadge :value="0.42" :options="{ style: 'percent' }" color="blue" />
24
+ </div>`,
25
+ }),
26
+ }
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import DisplayBadge from './DisplayBadge.vue'
3
+ import DisplayNumber from './DisplayNumber.vue'
4
+
5
+ withDefaults(
6
+ defineProps<{
7
+ value: number
8
+ prefix?: string
9
+ suffix?: string
10
+ options?: Intl.NumberFormatOptions
11
+ /** DisplayBadge color (palette name, hue, css color); defaults to muted. */
12
+ color?: boolean | number | string
13
+ }>(),
14
+ { color: false },
15
+ )
16
+ </script>
17
+
18
+ <template>
19
+ <DisplayBadge :color="color" size="sm">
20
+ <DisplayNumber :value="value" :prefix="prefix" :suffix="suffix" :options="options" />
21
+ </DisplayBadge>
22
+ </template>
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayPackageName from './DisplayPackageName.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayPackageName',
6
+ component: DisplayPackageName,
7
+ tags: ['autodocs'],
8
+ args: { name: '@antfu/design' },
9
+ } satisfies Meta<typeof DisplayPackageName>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Scoped: Story = { args: { name: '@antfu/design' } }
15
+ export const Unscoped: Story = { args: { name: 'unocss' } }
16
+
17
+ export const Examples: Story = {
18
+ render: () => ({
19
+ components: { DisplayPackageName },
20
+ template: `<div class="flex gap-3">
21
+ <DisplayPackageName name="@antfu/design" />
22
+ <DisplayPackageName name="@vueuse/core" />
23
+ <DisplayPackageName name="unocss" />
24
+ </div>`,
25
+ }),
26
+ }
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useColorScheme } from '../../composables/colorScheme'
4
+ import { getHashColorFromString, getPluginColor, stripPluginPrefix } from '../../utils/color'
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ name: string
9
+ /** The app's current color scheme. Falls back to context, then `'light'`. */
10
+ colorScheme?: 'light' | 'dark'
11
+ /** Strip plugin prefixes (`vite-plugin-`, `unplugin-`, `vite:`, …) from the displayed name. */
12
+ strip?: boolean
13
+ /** Color the scope by ecosystem brand hue (vs a plain string hash). */
14
+ brand?: boolean
15
+ /** Extra brand hues merged over the defaults (see `getPluginColor`). */
16
+ brandHues?: Record<string, number>
17
+ }>(),
18
+ { brand: true },
19
+ )
20
+
21
+ const scheme = useColorScheme(() => props.colorScheme)
22
+ const dark = computed(() => scheme.value === 'dark')
23
+
24
+ const display = computed(() => (props.strip ? stripPluginPrefix(props.name) : props.name))
25
+
26
+ const parts = computed(() => {
27
+ const n = display.value
28
+ if (n.startsWith('@')) {
29
+ const slash = n.indexOf('/')
30
+ if (slash !== -1)
31
+ return { scope: n.slice(0, slash + 1), rest: n.slice(slash + 1) }
32
+ }
33
+ return { scope: '', rest: n }
34
+ })
35
+
36
+ const scopeColor = computed(() => {
37
+ if (!parts.value.scope)
38
+ return undefined
39
+ return props.brand
40
+ ? getPluginColor(props.name, 1, dark.value, props.brandHues)
41
+ : getHashColorFromString(parts.value.scope, 1, dark.value)
42
+ })
43
+ </script>
44
+
45
+ <template>
46
+ <span class="text-sm font-mono">
47
+ <span v-if="parts.scope" :style="{ color: scopeColor }">{{ parts.scope }}</span><span class="color-base">{{ parts.rest }}</span>
48
+ </span>
49
+ </template>
@@ -0,0 +1,29 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayProgressBar from './DisplayProgressBar.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayProgressBar',
6
+ component: DisplayProgressBar,
7
+ tags: ['autodocs'],
8
+ args: { value: 0.6, height: 6, rounded: true, label: 'Progress' },
9
+ decorators: [() => ({ template: '<div class="w-80"><story /></div>' })],
10
+ } satisfies Meta<typeof DisplayProgressBar>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ export const Determinate: Story = {
16
+ args: { value: 0.6 },
17
+ }
18
+
19
+ export const Indeterminate: Story = {
20
+ args: { value: undefined },
21
+ }
22
+
23
+ export const CustomColor: Story = {
24
+ args: { value: 0.75, color: 'hsl(153, 65%, 40%)' },
25
+ }
26
+
27
+ export const Tall: Story = {
28
+ args: { value: 0.4, height: 12 },
29
+ }
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ /** Progress from 0 to 1. Omit for an indeterminate (animated) bar. */
7
+ value?: number
8
+ /** Track height in pixels. */
9
+ height?: number
10
+ /**
11
+ * Bar color. A CSS color (`#…`, `rgb(…)`, `hsl(…)`, `var(…)`) is applied as
12
+ * a background; any other string is treated as a utility/token class.
13
+ * Defaults to `bg-primary-500`.
14
+ */
15
+ color?: string
16
+ /** Round the track and bar ends. */
17
+ rounded?: boolean
18
+ /** Accessible label, exposed as `aria-label`. */
19
+ label?: string
20
+ }>(),
21
+ { height: 6, rounded: true },
22
+ )
23
+
24
+ function isCssColor(value: string): boolean {
25
+ return /^#|^hsl|^rgb|^var\(/.test(value)
26
+ }
27
+
28
+ const indeterminate = computed(() => props.value == null)
29
+
30
+ const clamped = computed(() => Math.min(Math.max(props.value ?? 0, 0), 1))
31
+
32
+ const valueNow = computed(() => (indeterminate.value ? undefined : Math.round(clamped.value * 100)))
33
+
34
+ const colorClass = computed(() =>
35
+ props.color == null
36
+ ? 'bg-primary-500'
37
+ : isCssColor(props.color)
38
+ ? ''
39
+ : props.color,
40
+ )
41
+
42
+ const colorStyle = computed(() =>
43
+ props.color != null && isCssColor(props.color) ? { background: props.color } : undefined,
44
+ )
45
+
46
+ const barStyle = computed(() => ({
47
+ ...colorStyle.value,
48
+ ...(indeterminate.value ? {} : { width: `${clamped.value * 100}%` }),
49
+ }))
50
+ </script>
51
+
52
+ <template>
53
+ <div
54
+ class="bg-active w-full overflow-hidden"
55
+ :class="{ 'rounded-full': rounded }"
56
+ :style="{ height: `${height}px` }"
57
+ role="progressbar"
58
+ :aria-valuenow="valueNow"
59
+ :aria-valuemin="indeterminate ? undefined : 0"
60
+ :aria-valuemax="indeterminate ? undefined : 100"
61
+ :aria-label="label"
62
+ >
63
+ <div
64
+ class="h-full transition-all"
65
+ :class="[colorClass, { 'rounded-full': rounded, 'af-progress-indeterminate w-2/5': indeterminate }]"
66
+ :style="barStyle"
67
+ />
68
+ </div>
69
+ </template>
70
+
71
+ <style scoped>
72
+ @keyframes af-progress-slide {
73
+ 0% {
74
+ transform: translateX(-100%);
75
+ }
76
+ 100% {
77
+ transform: translateX(350%);
78
+ }
79
+ }
80
+
81
+ .af-progress-indeterminate {
82
+ animation: af-progress-slide 1.2s ease-in-out infinite;
83
+ }
84
+
85
+ @media (prefers-reduced-motion: reduce) {
86
+ .af-progress-indeterminate {
87
+ animation: none;
88
+ }
89
+ }
90
+ </style>
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import type { ProportionSegment } from './DisplayProportionBar.vue'
3
+ import DisplayProportionBar from './DisplayProportionBar.vue'
4
+
5
+ const meta = {
6
+ title: 'Display/DisplayProportionBar',
7
+ component: DisplayProportionBar,
8
+ tags: ['autodocs'],
9
+ args: { segments: [] }, // required prop; stories supply their own via render
10
+ } satisfies Meta<typeof DisplayProportionBar>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ const segments: ProportionSegment[] = [
16
+ { value: 60, label: 'JavaScript', color: '#f1e05a' },
17
+ { value: 30, label: 'TypeScript', color: '#3178c6' },
18
+ { value: 10, label: 'CSS', color: '#563d7c' },
19
+ ]
20
+
21
+ export const Default: Story = {
22
+ args: { segments, height: 12 },
23
+ decorators: [() => ({ template: '<div class="w-80"><story /></div>' })],
24
+ }
25
+
26
+ export const HashColored: Story = {
27
+ render: () => ({
28
+ components: { DisplayProportionBar },
29
+ setup() {
30
+ return {
31
+ segments: [
32
+ { value: 5, label: 'vue' },
33
+ { value: 3, label: 'react' },
34
+ { value: 2, label: 'svelte' },
35
+ ] as ProportionSegment[],
36
+ }
37
+ },
38
+ template: `<div class="w-80"><DisplayProportionBar :segments="segments" :height="12" /></div>`,
39
+ }),
40
+ }
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useColorScheme } from '../../composables/colorScheme'
4
+ import { getHashColorFromString } from '../../utils/color'
5
+
6
+ export interface ProportionSegment {
7
+ value: number
8
+ color?: string
9
+ label?: string
10
+ }
11
+
12
+ const props = withDefaults(
13
+ defineProps<{
14
+ segments: ProportionSegment[]
15
+ height?: number
16
+ /** The app's current color scheme. Falls back to context, then `'light'`. */
17
+ colorScheme?: 'light' | 'dark'
18
+ }>(),
19
+ { height: 8 },
20
+ )
21
+
22
+ const scheme = useColorScheme(() => props.colorScheme)
23
+ const total = computed(() => props.segments.reduce((sum, s) => sum + s.value, 0) || 1)
24
+
25
+ function segColor(seg: ProportionSegment, i: number): string {
26
+ return seg.color ?? getHashColorFromString(seg.label ?? String(i), 1, scheme.value === 'dark')
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <div
32
+ class="rounded-full bg-active flex w-full overflow-hidden"
33
+ :style="{ height: `${height}px` }"
34
+ >
35
+ <div
36
+ v-for="(seg, i) in segments"
37
+ :key="i"
38
+ class="h-full"
39
+ :style="{ width: `${(seg.value / total) * 100}%`, background: segColor(seg, i) }"
40
+ :title="seg.label"
41
+ />
42
+ </div>
43
+ </template>