@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,43 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplaySafeImage from './DisplaySafeImage.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplaySafeImage',
6
+ component: DisplaySafeImage,
7
+ tags: ['autodocs'],
8
+ args: { alt: 'avatar' },
9
+ decorators: [() => ({ template: '<div class="w-16 h-16 border border-base rounded overflow-hidden"><story /></div>' })],
10
+ } satisfies Meta<typeof DisplaySafeImage>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ export const Loaded: Story = {
16
+ args: { src: 'https://github.com/antfu.png' },
17
+ }
18
+
19
+ export const BrokenSrc: Story = {
20
+ render: () => ({
21
+ components: { DisplaySafeImage },
22
+ template: `<DisplaySafeImage src="https://example.com/does-not-exist.png" alt="broken">
23
+ <template #fallback>
24
+ <div class="w-full h-full bg-secondary color-muted text-xs inline-flex items-center justify-center">
25
+ fallback
26
+ </div>
27
+ </template>
28
+ </DisplaySafeImage>`,
29
+ }),
30
+ }
31
+
32
+ export const NoSrc: Story = {
33
+ render: () => ({
34
+ components: { DisplaySafeImage },
35
+ template: `<DisplaySafeImage>
36
+ <template #fallback>
37
+ <div class="w-full h-full bg-secondary color-muted text-xs inline-flex items-center justify-center">
38
+ no src
39
+ </div>
40
+ </template>
41
+ </DisplaySafeImage>`,
42
+ }),
43
+ }
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ /** Image source. When absent or failed, the `#fallback` slot is shown instead. */
7
+ src?: string
8
+ /** Alternative text, forwarded to the `<img>`. */
9
+ alt?: string
10
+ }>(),
11
+ {},
12
+ )
13
+
14
+ const errored = ref(false)
15
+
16
+ watch(() => props.src, () => {
17
+ errored.value = false
18
+ })
19
+ </script>
20
+
21
+ <template>
22
+ <img
23
+ v-if="src && !errored"
24
+ :src="src"
25
+ :alt="alt"
26
+ class="h-full w-full block object-cover"
27
+ @error="errored = true"
28
+ >
29
+ <slot v-else name="fallback" />
30
+ </template>
@@ -0,0 +1,34 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayStatusPill from './DisplayStatusPill.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayStatusPill',
6
+ component: DisplayStatusPill,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ status: {
10
+ control: 'select',
11
+ options: ['neutral', 'low', 'medium', 'high', 'critical', 'success', 'error', 'warning', 'info'],
12
+ },
13
+ },
14
+ args: { status: 'success', label: 'Online' },
15
+ } satisfies Meta<typeof DisplayStatusPill>
16
+
17
+ export default meta
18
+ type Story = StoryObj<typeof meta>
19
+
20
+ export const Success: Story = { args: { status: 'success', label: 'Online' } }
21
+ export const Pulsing: Story = { args: { status: 'high', label: 'Degraded', pulse: true } }
22
+
23
+ export const Statuses: Story = {
24
+ render: () => ({
25
+ components: { DisplayStatusPill },
26
+ template: `<div class="flex flex-wrap gap-4">
27
+ <DisplayStatusPill status="success" label="Online" />
28
+ <DisplayStatusPill status="high" label="Degraded" pulse />
29
+ <DisplayStatusPill status="critical" label="Down" />
30
+ <DisplayStatusPill status="info" label="Info" />
31
+ <DisplayStatusPill status="neutral" label="Idle" />
32
+ </div>`,
33
+ }),
34
+ }
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ status?: 'neutral' | 'low' | 'medium' | 'high' | 'critical' | 'success' | 'error' | 'warning' | 'info'
7
+ label?: string
8
+ /** Override the dot color with any CSS color (wins over `status`). */
9
+ color?: string
10
+ /** Animate the dot (e.g. live/running). */
11
+ pulse?: boolean
12
+ }>(),
13
+ { status: 'neutral' },
14
+ )
15
+
16
+ // Severity statuses follow the themeable `color-scale-*` tokens (dark-aware) via
17
+ // `bg-current`; semantic statuses use fixed palette colors.
18
+ const DOT: Record<NonNullable<typeof props.status>, string> = {
19
+ neutral: 'color-scale-neutral bg-current',
20
+ low: 'color-scale-low bg-current',
21
+ medium: 'color-scale-medium bg-current',
22
+ high: 'color-scale-high bg-current',
23
+ critical: 'color-scale-critical bg-current',
24
+ success: 'text-green-500 bg-current',
25
+ error: 'text-red-500 bg-current',
26
+ warning: 'text-amber-500 bg-current',
27
+ info: 'text-blue-500 bg-current',
28
+ }
29
+
30
+ const dotClass = computed(() => (props.color ? '' : DOT[props.status]))
31
+ const dotStyle = computed(() => (props.color ? { background: props.color } : undefined))
32
+ </script>
33
+
34
+ <template>
35
+ <span class="text-sm inline-flex gap-1.5 items-center">
36
+ <span class="flex h-2 w-2 relative">
37
+ <span v-if="pulse" class="rounded-full opacity-75 inline-flex h-full w-full absolute animate-ping" :class="dotClass" :style="dotStyle" />
38
+ <span class="rounded-full inline-flex h-2 w-2 relative" :class="dotClass" :style="dotStyle" />
39
+ </span>
40
+ <slot>{{ label }}</slot>
41
+ </span>
42
+ </template>
@@ -0,0 +1,76 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayTree from './DisplayTree.vue'
3
+
4
+ // `component` is omitted: this is a generic SFC, which doesn't fit Storybook's
5
+ // `Meta<typeof Component>` typing. The renders below use it directly.
6
+ const meta = {
7
+ title: 'Display/DisplayTree',
8
+ tags: ['autodocs'],
9
+ } satisfies Meta
10
+
11
+ export default meta
12
+ type Story = StoryObj
13
+
14
+ const files = [
15
+ 'src/main.ts',
16
+ 'src/App.vue',
17
+ 'src/components/Button.vue',
18
+ 'src/components/Input.vue',
19
+ 'src/utils/format.ts',
20
+ 'src/utils/tree.ts',
21
+ 'package.json',
22
+ 'README.md',
23
+ ]
24
+
25
+ export const Default: Story = {
26
+ render: () => ({
27
+ components: { DisplayTree },
28
+ setup() {
29
+ return { items: files, getPath: (f: string) => f }
30
+ },
31
+ template: `<div class="w-72">
32
+ <DisplayTree :items="items" :get-path="getPath" />
33
+ </div>`,
34
+ }),
35
+ }
36
+
37
+ export const WithFileIcons: Story = {
38
+ render: () => ({
39
+ components: { DisplayTree },
40
+ setup() {
41
+ return { items: files, getPath: (f: string) => f }
42
+ },
43
+ template: `<div class="w-72">
44
+ <DisplayTree :items="items" :get-path="getPath" file-icons />
45
+ </div>`,
46
+ }),
47
+ }
48
+
49
+ export const Collapsed: Story = {
50
+ render: () => ({
51
+ components: { DisplayTree },
52
+ setup() {
53
+ return { items: files, getPath: (f: string) => f }
54
+ },
55
+ template: `<div class="w-72">
56
+ <DisplayTree :items="items" :get-path="getPath" :default-expanded="false" file-icons />
57
+ </div>`,
58
+ }),
59
+ }
60
+
61
+ export const CustomLeaf: Story = {
62
+ render: () => ({
63
+ components: { DisplayTree },
64
+ setup() {
65
+ return { items: files, getPath: (f: string) => f }
66
+ },
67
+ template: `<div class="w-72">
68
+ <DisplayTree :items="items" :get-path="getPath" file-icons>
69
+ <template #leaf="{ node }">
70
+ <span class="color-base">{{ node.name }}</span>
71
+ <span class="text-micro color-faint">leaf</span>
72
+ </template>
73
+ </DisplayTree>
74
+ </div>`,
75
+ }),
76
+ }
@@ -0,0 +1,102 @@
1
+ <script setup lang="ts" generic="T">
2
+ import type { TreeNode } from '../../utils/tree'
3
+ import { computed, ref } from 'vue'
4
+ import { getFileIcon, getFolderIcon } from '../../utils/icon'
5
+ import { toTree } from '../../utils/tree'
6
+
7
+ defineOptions({ name: 'DisplayTree' })
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ /** Flat list of items to nest into a tree (top-level usage). */
12
+ items?: T[]
13
+ /** Maps an item to its `separator`-delimited path. */
14
+ getPath?: (item: T) => string
15
+ /** Pre-built nodes (used internally for recursion; pass `items`+`getPath` instead). */
16
+ nodes?: TreeNode<T>[]
17
+ /** Segment separator passed to `toTree`. Defaults to `/`. */
18
+ separator?: string
19
+ /** Collapse single-child chains into one node. */
20
+ flatten?: boolean
21
+ /** Whether branches start expanded. */
22
+ defaultExpanded?: boolean
23
+ /** Show a `getFileIcon` glyph on leaf nodes. */
24
+ fileIcons?: boolean
25
+ }>(),
26
+ { flatten: true, defaultExpanded: true, fileIcons: false },
27
+ )
28
+
29
+ defineSlots<{
30
+ /** Fully customize any node's label. */
31
+ default?: (props: { node: TreeNode<T> }) => any
32
+ /** Customize a leaf node's label (falls back to `default`, then `node.name`). */
33
+ leaf?: (props: { node: TreeNode<T> }) => any
34
+ }>()
35
+
36
+ const tree = computed<TreeNode<T>[]>(() =>
37
+ props.nodes
38
+ ?? (props.items && props.getPath
39
+ ? toTree(props.items, props.getPath, { separator: props.separator, flatten: props.flatten })
40
+ : []),
41
+ )
42
+
43
+ // Per-node expansion state, defaulting to `defaultExpanded`.
44
+ const openState = ref<Record<string, boolean>>({})
45
+ function isOpen(path: string): boolean {
46
+ return openState.value[path] ?? props.defaultExpanded
47
+ }
48
+ function toggle(path: string): void {
49
+ openState.value[path] = !isOpen(path)
50
+ }
51
+ </script>
52
+
53
+ <template>
54
+ <ul class="text-sm">
55
+ <li v-for="node in tree" :key="node.path">
56
+ <button
57
+ v-if="node.children.length"
58
+ type="button"
59
+ class="px-1 py-0.5 text-left outline-none rounded flex gap-1 w-full transition items-center hover:bg-active focus-visible:ring-2 focus-visible:ring-primary-500/40"
60
+ :aria-expanded="isOpen(node.path)"
61
+ @click="toggle(node.path)"
62
+ >
63
+ <svg
64
+ width="1em" height="1em" viewBox="0 0 24 24"
65
+ class="op-fade shrink-0 transition-transform"
66
+ :class="{ 'rotate-90': isOpen(node.path) }"
67
+ aria-hidden="true"
68
+ >
69
+ <path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="m9 6l6 6l-6 6" />
70
+ </svg>
71
+ <span v-if="fileIcons" :class="getFolderIcon(node.name.split('/').pop(), isOpen(node.path))" class="op-fade shrink-0" aria-hidden="true" />
72
+ <span class="truncate"><slot :node="node">{{ node.name }}</slot></span>
73
+ </button>
74
+
75
+ <div v-else class="px-1 py-0.5 pl-5 rounded flex gap-1 items-center hover:bg-active">
76
+ <span v-if="fileIcons" :class="getFileIcon(node.path)" class="op-fade shrink-0" aria-hidden="true" />
77
+ <span class="truncate">
78
+ <slot name="leaf" :node="node">
79
+ <slot :node="node">{{ node.name }}</slot>
80
+ </slot>
81
+ </span>
82
+ </div>
83
+
84
+ <DisplayTree
85
+ v-if="node.children.length && isOpen(node.path)"
86
+ :nodes="node.children"
87
+ :separator="separator"
88
+ :flatten="flatten"
89
+ :default-expanded="defaultExpanded"
90
+ :file-icons="fileIcons"
91
+ class="ml-1.5 pl-4 border-l border-base"
92
+ >
93
+ <template v-if="$slots.default" #default="{ node: n }">
94
+ <slot :node="n" />
95
+ </template>
96
+ <template v-if="$slots.leaf" #leaf="{ node: n }">
97
+ <slot name="leaf" :node="n" />
98
+ </template>
99
+ </DisplayTree>
100
+ </li>
101
+ </ul>
102
+ </template>
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DisplayVersion from './DisplayVersion.vue'
3
+
4
+ const meta = {
5
+ title: 'Display/DisplayVersion',
6
+ component: DisplayVersion,
7
+ tags: ['autodocs'],
8
+ args: { version: '1.2.3' },
9
+ } satisfies Meta<typeof DisplayVersion>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const WithPrefix: Story = { args: { version: '1.2.3' } }
15
+ export const NoPrefix: Story = { args: { version: 'v0.1.0', prefix: false } }
16
+
17
+ export const Examples: Story = {
18
+ render: () => ({
19
+ components: { DisplayVersion },
20
+ template: `<div class="flex gap-3">
21
+ <DisplayVersion version="1.2.3" />
22
+ <DisplayVersion version="v0.1.0" :prefix="false" />
23
+ </div>`,
24
+ }),
25
+ }
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ version: string
7
+ /** Render with a leading `v`. */
8
+ prefix?: boolean
9
+ }>(),
10
+ { prefix: true },
11
+ )
12
+
13
+ const display = computed(() => {
14
+ const v = props.version.replace(/^v/i, '')
15
+ return props.prefix ? `v${v}` : v
16
+ })
17
+ </script>
18
+
19
+ <template>
20
+ <span class="font-mono tabular-nums">{{ display }}</span>
21
+ </template>
@@ -0,0 +1,38 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import ActionButton from '../Action/ActionButton.vue'
3
+ import FeedbackEmptyState from './FeedbackEmptyState.vue'
4
+
5
+ const meta = {
6
+ title: 'Feedback/FeedbackEmptyState',
7
+ component: FeedbackEmptyState,
8
+ tags: ['autodocs'],
9
+ args: { title: 'No results', icon: 'i-ph:folder' },
10
+ } satisfies Meta<typeof FeedbackEmptyState>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ export const Default: Story = {
16
+ render: () => ({
17
+ components: { FeedbackEmptyState },
18
+ template: `<div class="border border-base rounded-lg w-96">
19
+ <FeedbackEmptyState title="No results" icon="i-ph:folder">
20
+ <template #hint>Try a different search term.</template>
21
+ </FeedbackEmptyState>
22
+ </div>`,
23
+ }),
24
+ }
25
+
26
+ export const WithActions: Story = {
27
+ render: () => ({
28
+ components: { FeedbackEmptyState, ActionButton },
29
+ template: `<div class="border border-base rounded-lg w-96">
30
+ <FeedbackEmptyState title="No projects yet" icon="i-ph:folder">
31
+ <template #hint>Create your first project to get started.</template>
32
+ <template #actions>
33
+ <ActionButton variant="primary" icon="i-ph:folder">New project</ActionButton>
34
+ </template>
35
+ </FeedbackEmptyState>
36
+ </div>`,
37
+ }),
38
+ }
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ title?: string
4
+ icon?: string
5
+ }>()
6
+ </script>
7
+
8
+ <template>
9
+ <div class="px-6 py-10 text-center op-fade flex flex-col gap-2 items-center justify-center">
10
+ <span v-if="icon" :class="icon" class="text-3xl op-mute" aria-hidden="true" />
11
+ <div v-if="title || $slots.default" class="text-sm color-base font-medium">
12
+ <slot>{{ title }}</slot>
13
+ </div>
14
+ <div v-if="$slots.hint" class="text-xs op-mute max-w-prose">
15
+ <slot name="hint" />
16
+ </div>
17
+ <div v-if="$slots.actions" class="mt-2 flex gap-2 items-center">
18
+ <slot name="actions" />
19
+ </div>
20
+ </div>
21
+ </template>
@@ -0,0 +1,23 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import FeedbackLoading from './FeedbackLoading.vue'
3
+
4
+ const meta = {
5
+ title: 'Feedback/FeedbackLoading',
6
+ component: FeedbackLoading,
7
+ tags: ['autodocs'],
8
+ args: { text: 'Loading data…' },
9
+ } satisfies Meta<typeof FeedbackLoading>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Default: Story = { args: { text: 'Loading data…' } }
15
+
16
+ export const Panel: Story = {
17
+ render: () => ({
18
+ components: { FeedbackLoading },
19
+ template: `<div class="border border-base rounded-lg h-40 w-72">
20
+ <FeedbackLoading text="Loading…" panel />
21
+ </div>`,
22
+ }),
23
+ }
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import FeedbackSpinner from './FeedbackSpinner.vue'
3
+
4
+ defineProps<{
5
+ text?: string
6
+ /** Fill and center within the parent. */
7
+ panel?: boolean
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <div
13
+ class="op-fade flex gap-2 items-center justify-center"
14
+ :class="panel ? 'h-full w-full py-12' : 'py-4'"
15
+ >
16
+ <FeedbackSpinner />
17
+ <span v-if="text || $slots.default" class="text-sm">
18
+ <slot>{{ text }}</slot>
19
+ </span>
20
+ </div>
21
+ </template>
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import FeedbackSpinner from './FeedbackSpinner.vue'
3
+
4
+ const meta = {
5
+ title: 'Feedback/FeedbackSpinner',
6
+ component: FeedbackSpinner,
7
+ tags: ['autodocs'],
8
+ args: { size: '1.5em' },
9
+ } satisfies Meta<typeof FeedbackSpinner>
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Default: Story = { args: { size: '1.5em' } }
15
+
16
+ export const Sizes: Story = {
17
+ render: () => ({
18
+ components: { FeedbackSpinner },
19
+ template: `<div class="flex items-center gap-6">
20
+ <FeedbackSpinner />
21
+ <FeedbackSpinner size="24" />
22
+ <FeedbackSpinner size="2em" />
23
+ </div>`,
24
+ }),
25
+ }
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ /** Diameter; a number is treated as px. */
7
+ size?: string | number
8
+ }>(),
9
+ { size: '1em' },
10
+ )
11
+
12
+ const dim = computed(() => (typeof props.size === 'number' ? `${props.size}px` : props.size))
13
+ </script>
14
+
15
+ <template>
16
+ <span
17
+ class="border-2 border-current border-t-transparent rounded-full inline-block animate-spin"
18
+ :style="{ width: dim, height: dim }"
19
+ role="status"
20
+ aria-label="Loading"
21
+ />
22
+ </template>
@@ -0,0 +1,34 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import FeedbackTip from './FeedbackTip.vue'
3
+
4
+ const meta = {
5
+ title: 'Feedback/FeedbackTip',
6
+ component: FeedbackTip,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ type: { control: 'inline-radio', options: ['info', 'success', 'warning', 'error'] },
10
+ },
11
+ args: { type: 'info' },
12
+ } satisfies Meta<typeof FeedbackTip>
13
+
14
+ export default meta
15
+ type Story = StoryObj<typeof meta>
16
+
17
+ export const Info: Story = {
18
+ render: () => ({
19
+ components: { FeedbackTip },
20
+ template: `<FeedbackTip type="info" class="w-96">An informational note.</FeedbackTip>`,
21
+ }),
22
+ }
23
+
24
+ export const Types: Story = {
25
+ render: () => ({
26
+ components: { FeedbackTip },
27
+ template: `<div class="flex flex-col gap-3 w-96">
28
+ <FeedbackTip type="info" icon="i-ph:folder">An informational note.</FeedbackTip>
29
+ <FeedbackTip type="success">Saved successfully.</FeedbackTip>
30
+ <FeedbackTip type="warning">Careful with this setting.</FeedbackTip>
31
+ <FeedbackTip type="error">Something went wrong.</FeedbackTip>
32
+ </div>`,
33
+ }),
34
+ }
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ type?: 'info' | 'success' | 'warning' | 'error'
7
+ icon?: string
8
+ }>(),
9
+ { type: 'info' },
10
+ )
11
+
12
+ const COLORS = {
13
+ info: 'bg-blue-400/10 border-blue-500/20 text-blue-700 dark:text-blue-300',
14
+ success: 'bg-green-400/10 border-green-500/20 text-green-700 dark:text-green-300',
15
+ warning: 'bg-amber-400/10 border-amber-500/20 text-amber-700 dark:text-amber-300',
16
+ error: 'bg-red-400/10 border-red-500/20 text-red-700 dark:text-red-300',
17
+ } as const
18
+
19
+ const cls = computed(() => COLORS[props.type])
20
+ </script>
21
+
22
+ <template>
23
+ <div class="text-sm p-3 border rounded-lg flex gap-2" :class="cls" role="note">
24
+ <span v-if="icon" :class="icon" class="mt-0.5 shrink-0" aria-hidden="true" />
25
+ <div class="min-w-0">
26
+ <slot />
27
+ </div>
28
+ </div>
29
+ </template>
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import type { ToastItem } from './FeedbackToasts.vue'
3
+ import { ref } from 'vue'
4
+ import ActionButton from '../Action/ActionButton.vue'
5
+ import FeedbackToasts from './FeedbackToasts.vue'
6
+
7
+ const meta = {
8
+ title: 'Feedback/FeedbackToasts',
9
+ component: FeedbackToasts,
10
+ tags: ['autodocs'],
11
+ args: { items: [] }, // required prop; the story drives a live list via render
12
+ } satisfies Meta<typeof FeedbackToasts>
13
+
14
+ export default meta
15
+ type Story = StoryObj<typeof meta>
16
+
17
+ // Toasts are controlled: the app owns the list (here a local ref) and removes
18
+ // items on `dismiss` — the package holds no queue state.
19
+ export const Default: Story = {
20
+ render: () => ({
21
+ components: { ActionButton, FeedbackToasts },
22
+ setup() {
23
+ const items = ref<ToastItem[]>([])
24
+ let counter = 0
25
+ function dismiss(id: string | number): void {
26
+ items.value = items.value.filter(i => i.id !== id)
27
+ }
28
+ function show(): void {
29
+ const id = ++counter
30
+ items.value.push({ id, title: 'Saved', message: 'Your changes were saved.', type: 'success', icon: 'i-ph:folder' })
31
+ setTimeout(dismiss, 4000, id)
32
+ }
33
+ return { items, show, dismiss }
34
+ },
35
+ template: `<div>
36
+ <ActionButton @click="show">Show toast</ActionButton>
37
+ <FeedbackToasts :items="items" position="bottom-right" @dismiss="dismiss" />
38
+ </div>`,
39
+ }),
40
+ }