@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,60 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+ import ActionIconButton from '../Action/ActionIconButton.vue'
4
+ import FormSearchField from '../Form/FormSearchField.vue'
5
+ import LayoutToolbar from './LayoutToolbar.vue'
6
+
7
+ const meta = {
8
+ title: 'Layout/LayoutToolbar',
9
+ component: LayoutToolbar,
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ sticky: { control: 'boolean' },
13
+ glass: { control: 'boolean' },
14
+ },
15
+ args: { sticky: true, glass: true },
16
+ } satisfies Meta<typeof LayoutToolbar>
17
+
18
+ export default meta
19
+ type Story = StoryObj<typeof meta>
20
+
21
+ export const Default: Story = {
22
+ render: () => ({
23
+ components: { LayoutToolbar, ActionIconButton, FormSearchField },
24
+ setup() {
25
+ return { search: ref('') }
26
+ },
27
+ template: `<div class="border border-base rounded-lg w-full overflow-hidden">
28
+ <LayoutToolbar>
29
+ <template #start>
30
+ <span class="i-catppuccin:folder-app text-lg" aria-hidden="true" />
31
+ <span class="text-sm color-base font-medium">Project</span>
32
+ </template>
33
+ <template #search>
34
+ <FormSearchField v-model="search" placeholder="Search files…" />
35
+ </template>
36
+ <template #end>
37
+ <ActionIconButton icon="i-catppuccin:folder-config" tooltip="Settings" />
38
+ <ActionIconButton icon="i-catppuccin:git" tooltip="Repository" />
39
+ </template>
40
+ </LayoutToolbar>
41
+ <div class="text-sm color-muted p-4">Page content…</div>
42
+ </div>`,
43
+ }),
44
+ }
45
+
46
+ export const Opaque: Story = {
47
+ render: () => ({
48
+ components: { LayoutToolbar, ActionIconButton },
49
+ template: `<div class="border border-base rounded-lg w-full overflow-hidden">
50
+ <LayoutToolbar :glass="false" :sticky="false">
51
+ <template #start>
52
+ <span class="text-sm color-base font-medium">Dashboard</span>
53
+ </template>
54
+ <template #end>
55
+ <ActionIconButton icon="i-catppuccin:folder-config" tooltip="Settings" />
56
+ </template>
57
+ </LayoutToolbar>
58
+ </div>`,
59
+ }),
60
+ }
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ withDefaults(
3
+ defineProps<{
4
+ /** Stick to the top of the scroll container with `z-nav`. */
5
+ sticky?: boolean
6
+ /** Translucent `bg-glass` surface; set `false` for an opaque `bg-base`. */
7
+ glass?: boolean
8
+ }>(),
9
+ { sticky: true, glass: true },
10
+ )
11
+ </script>
12
+
13
+ <template>
14
+ <div
15
+ class="px-3 py-2 border-b border-base flex gap-3 items-center"
16
+ :class="[sticky ? 'sticky top-0 z-nav' : '', glass ? 'bg-glass' : 'bg-base']"
17
+ >
18
+ <slot name="start">
19
+ <slot />
20
+ </slot>
21
+ <div v-if="$slots.search" class="flex-1 min-w-0">
22
+ <slot name="search" />
23
+ </div>
24
+ <div v-if="$slots.end" class="flex gap-1 items-center">
25
+ <slot name="end" />
26
+ </div>
27
+ </div>
28
+ </template>
@@ -0,0 +1,30 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import LayoutVirtualList from './LayoutVirtualList.vue'
3
+
4
+ // `component` is omitted: this is a generic SFC, which doesn't fit Storybook's
5
+ // `Meta<typeof Component>` typing. The render below uses it directly.
6
+ const meta = {
7
+ title: 'Layout/LayoutVirtualList',
8
+ tags: ['autodocs'],
9
+ } satisfies Meta
10
+
11
+ export default meta
12
+ type Story = StoryObj
13
+
14
+ export const Default: Story = {
15
+ render: () => ({
16
+ components: { LayoutVirtualList },
17
+ setup() {
18
+ return { items: Array.from({ length: 10000 }, (_, i) => `Row ${i + 1}`) }
19
+ },
20
+ template: `<div class="border border-base rounded-lg h-72 w-72">
21
+ <LayoutVirtualList :items="items" class="h-full" :estimate-size="32">
22
+ <template #default="{ item, index }">
23
+ <div class="text-sm font-mono px-3 py-1.5 border-b border-base flex h-8 items-center" :class="{ 'bg-secondary': index % 2 }">
24
+ {{ item }}
25
+ </div>
26
+ </template>
27
+ </LayoutVirtualList>
28
+ </div>`,
29
+ }),
30
+ }
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts" generic="T">
2
+ import { useVirtualizer, useWindowVirtualizer } from '@tanstack/vue-virtual'
3
+ import { computed, onMounted, ref } from 'vue'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ items: T[]
8
+ /** Estimated item extent in px (the fixed size unless `dynamic`). */
9
+ estimateSize?: number
10
+ overscan?: number
11
+ horizontal?: boolean
12
+ /** Measure each rendered row for variable heights (vs a fixed estimate). */
13
+ dynamic?: boolean
14
+ /** Virtualize against window scroll instead of an inner scroll box (set once). */
15
+ windowScroll?: boolean
16
+ }>(),
17
+ { estimateSize: 36, overscan: 8 },
18
+ )
19
+
20
+ const parentRef = ref<HTMLElement | null>(null)
21
+ const scrollMargin = ref(0)
22
+ onMounted(() => {
23
+ if (props.windowScroll && parentRef.value)
24
+ scrollMargin.value = parentRef.value.offsetTop
25
+ })
26
+
27
+ const common = computed(() => ({
28
+ count: props.items.length,
29
+ estimateSize: () => props.estimateSize,
30
+ overscan: props.overscan,
31
+ horizontal: props.horizontal,
32
+ }))
33
+
34
+ const virtualizer = props.windowScroll
35
+ ? useWindowVirtualizer(computed(() => ({ ...common.value, scrollMargin: scrollMargin.value })))
36
+ : useVirtualizer(computed(() => ({ ...common.value, getScrollElement: () => parentRef.value })))
37
+
38
+ const rows = computed(() => virtualizer.value.getVirtualItems())
39
+ const totalSize = computed(() => virtualizer.value.getTotalSize())
40
+
41
+ function offsetOf(start: number): number {
42
+ return start - (props.windowScroll ? scrollMargin.value : 0)
43
+ }
44
+
45
+ function measure(el: Element | null): void {
46
+ if (props.dynamic && el instanceof Element)
47
+ virtualizer.value.measureElement(el)
48
+ }
49
+
50
+ defineExpose({
51
+ /** Scroll a row into view by index. */
52
+ scrollToIndex: (index: number, options?: { align?: 'start' | 'center' | 'end' | 'auto' }) =>
53
+ virtualizer.value.scrollToIndex(index, options),
54
+ /** Scroll to a pixel offset. */
55
+ scrollToOffset: (offset: number, options?: { align?: 'start' | 'center' | 'end' | 'auto' }) =>
56
+ virtualizer.value.scrollToOffset(offset, options),
57
+ /** The underlying TanStack virtualizer. */
58
+ virtualizer,
59
+ })
60
+ </script>
61
+
62
+ <template>
63
+ <div ref="parentRef" :class="windowScroll ? '' : 'overflow-auto'">
64
+ <div :style="{ position: 'relative', [horizontal ? 'width' : 'height']: `${totalSize}px` }">
65
+ <div
66
+ v-for="row in rows"
67
+ :key="row.index"
68
+ :ref="dynamic ? (el) => measure(el as Element | null) : undefined"
69
+ :style="{
70
+ position: 'absolute',
71
+ top: 0,
72
+ left: 0,
73
+ [horizontal ? 'height' : 'width']: '100%',
74
+ transform: horizontal ? `translateX(${offsetOf(row.start)}px)` : `translateY(${offsetOf(row.start)}px)`,
75
+ }"
76
+ :data-index="row.index"
77
+ >
78
+ <slot :item="items[row.index]" :index="row.index" />
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </template>
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+ import ActionButton from '../Action/ActionButton.vue'
4
+ import OverlayDrawer from './OverlayDrawer.vue'
5
+
6
+ const meta = {
7
+ title: 'Overlay/OverlayDrawer',
8
+ component: OverlayDrawer,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ side: { control: 'inline-radio', options: ['left', 'right', 'top', 'bottom'] },
12
+ },
13
+ args: { title: 'Filters', side: 'right' },
14
+ } satisfies Meta<typeof OverlayDrawer>
15
+
16
+ export default meta
17
+ type Story = StoryObj<typeof meta>
18
+
19
+ export const Default: Story = {
20
+ render: () => ({
21
+ components: { OverlayDrawer, ActionButton },
22
+ setup() {
23
+ return { open: ref(false) }
24
+ },
25
+ template: `<div>
26
+ <ActionButton @click="open = true">Open drawer</ActionButton>
27
+ <OverlayDrawer v-model:open="open" title="Filters">
28
+ <p class="text-sm color-muted">Drawer content slides from the edge.</p>
29
+ </OverlayDrawer>
30
+ </div>`,
31
+ }),
32
+ }
33
+
34
+ export const LeftSide: Story = {
35
+ render: () => ({
36
+ components: { OverlayDrawer, ActionButton },
37
+ setup() {
38
+ return { open: ref(false) }
39
+ },
40
+ template: `<div>
41
+ <ActionButton @click="open = true">Open left drawer</ActionButton>
42
+ <OverlayDrawer v-model:open="open" title="Navigation" side="left">
43
+ <p class="text-sm color-muted">Slides in from the left.</p>
44
+ </OverlayDrawer>
45
+ </div>`,
46
+ }),
47
+ }
@@ -0,0 +1,58 @@
1
+ <script setup lang="ts">
2
+ import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from 'reka-ui'
3
+
4
+ withDefaults(
5
+ defineProps<{
6
+ title?: string
7
+ side?: 'left' | 'right' | 'top' | 'bottom'
8
+ }>(),
9
+ { side: 'right' },
10
+ )
11
+
12
+ const open = defineModel<boolean>('open')
13
+
14
+ const SIDE_CLASS = {
15
+ right: 'right-0 top-0 h-full w-80 max-w-[90vw] border-l',
16
+ left: 'left-0 top-0 h-full w-80 max-w-[90vw] border-r',
17
+ top: 'top-0 inset-x-0 h-1/3 border-b',
18
+ bottom: 'bottom-0 inset-x-0 h-1/3 border-t',
19
+ } as const
20
+ </script>
21
+
22
+ <template>
23
+ <DialogRoot v-model:open="open">
24
+ <DialogTrigger v-if="$slots.trigger" as-child>
25
+ <slot name="trigger" />
26
+ </DialogTrigger>
27
+ <DialogPortal>
28
+ <DialogOverlay class="bg-black/40 inset-0 fixed z-drawer-backdrop backdrop-blur-sm" data-af-animate />
29
+ <DialogContent
30
+ class="outline-none border-base bg-base flex flex-col shadow-2xl fixed z-drawer-content"
31
+ :class="SIDE_CLASS[side]"
32
+ data-af-drawer
33
+ :data-side="side"
34
+ >
35
+ <header class="px-4 py-3 border-b border-base flex shrink-0 gap-4 items-center justify-between">
36
+ <DialogTitle v-if="title" class="color-base font-medium">
37
+ {{ title }}
38
+ </DialogTitle>
39
+ <DialogDescription v-if="$slots.description" class="sr-only">
40
+ <slot name="description" />
41
+ </DialogDescription>
42
+ <slot name="header" />
43
+ <DialogClose class="btn-icon shrink-0 h-7 w-7" aria-label="Close">
44
+ <svg width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true">
45
+ <path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M6 6l12 12M18 6L6 18" />
46
+ </svg>
47
+ </DialogClose>
48
+ </header>
49
+ <div class="p-4 flex-1 overflow-auto">
50
+ <slot />
51
+ </div>
52
+ <footer v-if="$slots.footer" class="px-4 py-3 border-t border-base flex shrink-0 gap-2 items-center justify-end">
53
+ <slot name="footer" />
54
+ </footer>
55
+ </DialogContent>
56
+ </DialogPortal>
57
+ </DialogRoot>
58
+ </template>
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import ActionButton from '../Action/ActionButton.vue'
3
+ import OverlayDropdown from './OverlayDropdown.vue'
4
+ import OverlayDropdownItem from './OverlayDropdownItem.vue'
5
+
6
+ const meta = {
7
+ title: 'Overlay/OverlayDropdown',
8
+ component: OverlayDropdown,
9
+ tags: ['autodocs'],
10
+ } satisfies Meta<typeof OverlayDropdown>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ export const Default: Story = {
16
+ render: () => ({
17
+ components: { OverlayDropdown, OverlayDropdownItem, ActionButton },
18
+ template: `<OverlayDropdown>
19
+ <template #trigger><ActionButton icon="i-ph:gear">Menu</ActionButton></template>
20
+ <OverlayDropdownItem icon="i-ph:folder">Open</OverlayDropdownItem>
21
+ <OverlayDropdownItem icon="i-ph:pencil-simple">Rename</OverlayDropdownItem>
22
+ <OverlayDropdownItem icon="i-ph:trash">Delete</OverlayDropdownItem>
23
+ </OverlayDropdown>`,
24
+ }),
25
+ }
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { DropdownMenuContent, DropdownMenuPortal, DropdownMenuRoot, DropdownMenuTrigger } from 'reka-ui'
3
+
4
+ withDefaults(
5
+ defineProps<{
6
+ placement?: 'top' | 'right' | 'bottom' | 'left'
7
+ align?: 'start' | 'center' | 'end'
8
+ }>(),
9
+ { placement: 'bottom', align: 'start' },
10
+ )
11
+ </script>
12
+
13
+ <template>
14
+ <DropdownMenuRoot>
15
+ <DropdownMenuTrigger as-child>
16
+ <slot name="trigger" />
17
+ </DropdownMenuTrigger>
18
+ <DropdownMenuPortal>
19
+ <DropdownMenuContent
20
+ :side="placement"
21
+ :align="align"
22
+ :side-offset="6"
23
+ class="p-1 outline-none border border-base rounded-lg bg-base min-w-40 shadow-lg z-dropdown"
24
+ data-af-animate
25
+ >
26
+ <slot />
27
+ </DropdownMenuContent>
28
+ </DropdownMenuPortal>
29
+ </DropdownMenuRoot>
30
+ </template>
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import ActionButton from '../Action/ActionButton.vue'
3
+ import OverlayDropdown from './OverlayDropdown.vue'
4
+ import OverlayDropdownItem from './OverlayDropdownItem.vue'
5
+
6
+ const meta = {
7
+ title: 'Overlay/OverlayDropdownItem',
8
+ component: OverlayDropdownItem,
9
+ tags: ['autodocs'],
10
+ } satisfies Meta<typeof OverlayDropdownItem>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ // DropdownItem only renders meaningfully inside a Dropdown menu.
16
+ export const InDropdown: Story = {
17
+ render: () => ({
18
+ components: { OverlayDropdown, OverlayDropdownItem, ActionButton },
19
+ template: `<OverlayDropdown>
20
+ <template #trigger><ActionButton icon="i-ph:gear">Actions</ActionButton></template>
21
+ <OverlayDropdownItem icon="i-ph:folder">Open</OverlayDropdownItem>
22
+ <OverlayDropdownItem icon="i-ph:pencil-simple">Rename</OverlayDropdownItem>
23
+ <OverlayDropdownItem icon="i-ph:trash" disabled>Delete (disabled)</OverlayDropdownItem>
24
+ </OverlayDropdown>`,
25
+ }),
26
+ }
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ import { DropdownMenuItem } from 'reka-ui'
3
+ import DisplayKbd from '../Display/DisplayKbd.vue'
4
+
5
+ withDefaults(
6
+ defineProps<{
7
+ icon?: string
8
+ disabled?: boolean
9
+ /** `danger` tints the item red for destructive actions. */
10
+ variant?: 'default' | 'danger'
11
+ /** Keyboard-shortcut hint shown trailing (a chord string, e.g. `mod+c`). */
12
+ shortcut?: string
13
+ }>(),
14
+ { variant: 'default' },
15
+ )
16
+
17
+ const emit = defineEmits<{ select: [] }>()
18
+ </script>
19
+
20
+ <template>
21
+ <DropdownMenuItem
22
+ :disabled="disabled"
23
+ class="text-sm px-2 py-1.5 outline-none rounded-md flex gap-2 cursor-pointer select-none transition items-center data-[highlighted]:bg-active data-[disabled]:op50 data-[disabled]:pointer-events-none"
24
+ :class="variant === 'danger' ? 'text-red-600 dark:text-red-400 data-[highlighted]:bg-red-500/10' : 'color-base'"
25
+ @select="emit('select')"
26
+ >
27
+ <span v-if="icon" :class="icon" class="op-fade" aria-hidden="true" />
28
+ <span class="flex-1"><slot /></span>
29
+ <DisplayKbd v-if="shortcut" :keys="shortcut" class="op-fade" />
30
+ </DropdownMenuItem>
31
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ import { DropdownMenuLabel } from 'reka-ui'
3
+ </script>
4
+
5
+ <template>
6
+ <DropdownMenuLabel class="text-micro color-faint tracking-wide font-medium px-2 py-1 select-none uppercase">
7
+ <slot />
8
+ </DropdownMenuLabel>
9
+ </template>
@@ -0,0 +1,7 @@
1
+ <script setup lang="ts">
2
+ import { DropdownMenuSeparator } from 'reka-ui'
3
+ </script>
4
+
5
+ <template>
6
+ <DropdownMenuSeparator class="mx--1 my-1 border-t border-base" />
7
+ </template>
@@ -0,0 +1,33 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+ import ActionButton from '../Action/ActionButton.vue'
4
+ import OverlayModal from './OverlayModal.vue'
5
+
6
+ const meta = {
7
+ title: 'Overlay/OverlayModal',
8
+ component: OverlayModal,
9
+ tags: ['autodocs'],
10
+ args: { title: 'Confirm action', description: 'This cannot be undone.' },
11
+ } satisfies Meta<typeof OverlayModal>
12
+
13
+ export default meta
14
+ type Story = StoryObj<typeof meta>
15
+
16
+ export const Default: Story = {
17
+ render: () => ({
18
+ components: { OverlayModal, ActionButton },
19
+ setup() {
20
+ return { open: ref(false) }
21
+ },
22
+ template: `<div>
23
+ <ActionButton @click="open = true">Open modal</ActionButton>
24
+ <OverlayModal v-model:open="open" title="Confirm action" description="This cannot be undone.">
25
+ <p class="text-sm color-muted">Modal body content goes here.</p>
26
+ <template #footer>
27
+ <ActionButton variant="text" @click="open = false">Cancel</ActionButton>
28
+ <ActionButton variant="primary" @click="open = false">Confirm</ActionButton>
29
+ </template>
30
+ </OverlayModal>
31
+ </div>`,
32
+ }),
33
+ }
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from 'reka-ui'
3
+
4
+ defineProps<{
5
+ title?: string
6
+ description?: string
7
+ }>()
8
+
9
+ const open = defineModel<boolean>('open')
10
+ </script>
11
+
12
+ <template>
13
+ <DialogRoot v-model:open="open">
14
+ <DialogTrigger v-if="$slots.trigger" as-child>
15
+ <slot name="trigger" />
16
+ </DialogTrigger>
17
+ <DialogPortal>
18
+ <DialogOverlay class="bg-black/40 inset-0 fixed z-modal-backdrop backdrop-blur-sm" data-af-animate />
19
+ <DialogContent
20
+ class="outline-none border border-base rounded-xl bg-base max-w-lg w-[90vw] shadow-2xl left-1/2 top-1/2 fixed z-modal-content -translate-x-1/2 -translate-y-1/2"
21
+ data-af-modal
22
+ >
23
+ <header v-if="title || description || $slots.header" class="px-4 py-3 border-b border-base flex gap-4 items-start justify-between">
24
+ <div class="min-w-0">
25
+ <DialogTitle v-if="title" class="color-base font-medium">
26
+ {{ title }}
27
+ </DialogTitle>
28
+ <DialogDescription v-if="description" class="text-sm op-fade">
29
+ {{ description }}
30
+ </DialogDescription>
31
+ <slot name="header" />
32
+ </div>
33
+ <DialogClose class="btn-icon shrink-0 h-7 w-7" aria-label="Close">
34
+ <svg width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true">
35
+ <path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M6 6l12 12M18 6L6 18" />
36
+ </svg>
37
+ </DialogClose>
38
+ </header>
39
+ <div class="p-4">
40
+ <slot />
41
+ </div>
42
+ <footer v-if="$slots.footer" class="px-4 py-3 border-t border-base flex gap-2 justify-end">
43
+ <slot name="footer" />
44
+ </footer>
45
+ </DialogContent>
46
+ </DialogPortal>
47
+ </DialogRoot>
48
+ </template>
@@ -0,0 +1,33 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import ActionButton from '../Action/ActionButton.vue'
3
+ import OverlayTooltip from './OverlayTooltip.vue'
4
+
5
+ const meta = {
6
+ title: 'Overlay/OverlayTooltip',
7
+ component: OverlayTooltip,
8
+ tags: ['autodocs'],
9
+ args: { content: 'Tooltip content' },
10
+ } satisfies Meta<typeof OverlayTooltip>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ export const Default: Story = {
16
+ render: () => ({
17
+ components: { OverlayTooltip, ActionButton },
18
+ template: `<OverlayTooltip content="Tooltip content"><ActionButton>Hover me</ActionButton></OverlayTooltip>`,
19
+ }),
20
+ }
21
+
22
+ export const RichContent: Story = {
23
+ render: () => ({
24
+ components: { OverlayTooltip, ActionButton },
25
+ template: `<OverlayTooltip placement="bottom">
26
+ <ActionButton>Hover for rich content</ActionButton>
27
+ <template #content>
28
+ <div class="font-medium">Title</div>
29
+ <div class="text-xs op75">A richer popper body.</div>
30
+ </template>
31
+ </OverlayTooltip>`,
32
+ }),
33
+ }
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import { Tooltip as VTooltip } from 'floating-vue'
3
+
4
+ withDefaults(
5
+ defineProps<{
6
+ /** Tooltip text (use the `content` slot for rich content). */
7
+ content?: string
8
+ placement?: string
9
+ distance?: number
10
+ /** Show/hide delay in ms, or `{ show, hide }`. */
11
+ delay?: number | { show?: number, hide?: number }
12
+ /** What triggers the tooltip, e.g. `['hover', 'focus']` or `['click']`. */
13
+ triggers?: string[]
14
+ /** Programmatic open state (bypasses triggers when set). */
15
+ shown?: boolean
16
+ disabled?: boolean
17
+ }>(),
18
+ { placement: 'top', distance: 6 },
19
+ )
20
+ </script>
21
+
22
+ <template>
23
+ <VTooltip
24
+ :placement="placement as any"
25
+ :distance="distance"
26
+ :delay="delay as any"
27
+ :triggers="triggers as any"
28
+ :shown="shown"
29
+ :disabled="disabled"
30
+ >
31
+ <slot />
32
+ <template #popper>
33
+ <slot name="content">
34
+ {{ content }}
35
+ </slot>
36
+ </template>
37
+ </VTooltip>
38
+ </template>
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Opt-in color-scheme context.
3
+ *
4
+ * The package is stateless: most components flip light/dark automatically from
5
+ * the `html.dark` class + `--af-*` tokens, and the handful that compute colors in
6
+ * JS (badges/labels/proportion bars) take a `colorScheme` prop. Threading that
7
+ * prop everywhere is tedious, so this provides an *opt-in* context: call
8
+ * {@link provideColorScheme} once near the app root (feeding it your own
9
+ * app-owned dark ref), and scheme-aware components fall back to it when their
10
+ * prop is omitted. The package itself owns no state — the context only *reads* a
11
+ * ref you pass in.
12
+ */
13
+ import type { ComputedRef, InjectionKey, MaybeRefOrGetter, Ref } from 'vue'
14
+ import { computed, inject, provide, toRef } from 'vue'
15
+
16
+ export type ColorScheme = 'light' | 'dark'
17
+
18
+ /** Injection key for the active {@link ColorScheme}. */
19
+ export const colorSchemeKey: InjectionKey<Ref<ColorScheme>> = Symbol('antfu-design-color-scheme')
20
+
21
+ /**
22
+ * Provide the active color scheme to descendant components (call in `setup`).
23
+ *
24
+ * Accepts a ref, a getter, or a plain value — pass a getter/ref bound to your own
25
+ * dark-mode state so the context stays reactive. The package stays stateless: it
26
+ * only reads the value you provide.
27
+ *
28
+ * @param scheme - The current scheme as a ref, getter, or value.
29
+ * @returns The resolved {@link ColorScheme} ref that was provided.
30
+ *
31
+ * @example
32
+ * const isDark = useDark()
33
+ * provideColorScheme(() => isDark.value ? 'dark' : 'light')
34
+ */
35
+ export function provideColorScheme(scheme: MaybeRefOrGetter<ColorScheme>): Ref<ColorScheme> {
36
+ const ref = toRef(scheme) as Ref<ColorScheme>
37
+ provide(colorSchemeKey, ref)
38
+ return ref
39
+ }
40
+
41
+ /**
42
+ * Resolve the effective color scheme: explicit prop → provided context → `'light'`.
43
+ *
44
+ * Components pass a getter for their own `colorScheme` prop so an explicit prop
45
+ * always wins; otherwise the value from {@link provideColorScheme} is used, and
46
+ * finally `'light'` as the default.
47
+ *
48
+ * @param prop - Getter for the component's own `colorScheme` prop (optional).
49
+ * @returns A computed {@link ColorScheme}.
50
+ *
51
+ * @example
52
+ * const scheme = useColorScheme(() => props.colorScheme)
53
+ * const dark = computed(() => scheme.value === 'dark')
54
+ */
55
+ export function useColorScheme(prop?: () => ColorScheme | undefined): ComputedRef<ColorScheme> {
56
+ const injected = inject(colorSchemeKey, undefined)
57
+ return computed<ColorScheme>(() => prop?.() ?? injected?.value ?? 'light')
58
+ }