@antfu/design 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/a11y/cli.ts +73 -0
- package/a11y/index.ts +13 -0
- package/a11y/scan.ts +127 -0
- package/components/Action/ActionButton.stories.ts +56 -0
- package/components/Action/ActionButton.vue +57 -0
- package/components/Action/ActionDarkToggle.stories.ts +31 -0
- package/components/Action/ActionDarkToggle.vue +87 -0
- package/components/Action/ActionIconButton.stories.ts +47 -0
- package/components/Action/ActionIconButton.vue +47 -0
- package/components/Display/DisplayAvatar.stories.ts +36 -0
- package/components/Display/DisplayAvatar.vue +58 -0
- package/components/Display/DisplayBadge.stories.ts +31 -0
- package/components/Display/DisplayBadge.vue +98 -0
- package/components/Display/DisplayBytes.stories.ts +28 -0
- package/components/Display/DisplayBytes.vue +30 -0
- package/components/Display/DisplayDate.stories.ts +37 -0
- package/components/Display/DisplayDate.vue +29 -0
- package/components/Display/DisplayDonut.stories.ts +26 -0
- package/components/Display/DisplayDonut.vue +46 -0
- package/components/Display/DisplayDuration.stories.ts +28 -0
- package/components/Display/DisplayDuration.vue +28 -0
- package/components/Display/DisplayFileIcon.stories.ts +27 -0
- package/components/Display/DisplayFileIcon.vue +30 -0
- package/components/Display/DisplayFilePath.stories.ts +30 -0
- package/components/Display/DisplayFilePath.vue +61 -0
- package/components/Display/DisplayKbd.stories.ts +26 -0
- package/components/Display/DisplayKbd.vue +27 -0
- package/components/Display/DisplayKeyValue.stories.ts +56 -0
- package/components/Display/DisplayKeyValue.vue +51 -0
- package/components/Display/DisplayLabel.stories.ts +27 -0
- package/components/Display/DisplayLabel.vue +33 -0
- package/components/Display/DisplayNumber.stories.ts +27 -0
- package/components/Display/DisplayNumber.vue +24 -0
- package/components/Display/DisplayNumberBadge.stories.ts +26 -0
- package/components/Display/DisplayNumberBadge.vue +22 -0
- package/components/Display/DisplayPackageName.stories.ts +26 -0
- package/components/Display/DisplayPackageName.vue +49 -0
- package/components/Display/DisplayProgressBar.stories.ts +29 -0
- package/components/Display/DisplayProgressBar.vue +90 -0
- package/components/Display/DisplayProportionBar.stories.ts +40 -0
- package/components/Display/DisplayProportionBar.vue +43 -0
- package/components/Display/DisplaySafeImage.stories.ts +43 -0
- package/components/Display/DisplaySafeImage.vue +30 -0
- package/components/Display/DisplayStatusPill.stories.ts +34 -0
- package/components/Display/DisplayStatusPill.vue +42 -0
- package/components/Display/DisplayTree.stories.ts +76 -0
- package/components/Display/DisplayTree.vue +102 -0
- package/components/Display/DisplayVersion.stories.ts +25 -0
- package/components/Display/DisplayVersion.vue +21 -0
- package/components/Feedback/FeedbackEmptyState.stories.ts +38 -0
- package/components/Feedback/FeedbackEmptyState.vue +21 -0
- package/components/Feedback/FeedbackLoading.stories.ts +23 -0
- package/components/Feedback/FeedbackLoading.vue +21 -0
- package/components/Feedback/FeedbackSpinner.stories.ts +25 -0
- package/components/Feedback/FeedbackSpinner.vue +22 -0
- package/components/Feedback/FeedbackTip.stories.ts +34 -0
- package/components/Feedback/FeedbackTip.vue +29 -0
- package/components/Feedback/FeedbackToasts.stories.ts +40 -0
- package/components/Feedback/FeedbackToasts.vue +105 -0
- package/components/Form/FormCheckbox.stories.ts +36 -0
- package/components/Form/FormCheckbox.vue +30 -0
- package/components/Form/FormCombobox.stories.ts +35 -0
- package/components/Form/FormCombobox.vue +83 -0
- package/components/Form/FormField.stories.ts +56 -0
- package/components/Form/FormField.vue +36 -0
- package/components/Form/FormNumberInput.stories.ts +47 -0
- package/components/Form/FormNumberInput.vue +85 -0
- package/components/Form/FormRadioGroup.stories.ts +47 -0
- package/components/Form/FormRadioGroup.vue +43 -0
- package/components/Form/FormSearchField.stories.ts +22 -0
- package/components/Form/FormSearchField.vue +32 -0
- package/components/Form/FormSelect.stories.ts +47 -0
- package/components/Form/FormSelect.vue +56 -0
- package/components/Form/FormSwitch.stories.ts +36 -0
- package/components/Form/FormSwitch.vue +26 -0
- package/components/Form/FormTextInput.stories.ts +39 -0
- package/components/Form/FormTextInput.vue +51 -0
- package/components/Form/FormTextarea.stories.ts +47 -0
- package/components/Form/FormTextarea.vue +32 -0
- package/components/Layout/LayoutBreadcrumb.stories.ts +54 -0
- package/components/Layout/LayoutBreadcrumb.vue +54 -0
- package/components/Layout/LayoutCard.stories.ts +31 -0
- package/components/Layout/LayoutCard.vue +21 -0
- package/components/Layout/LayoutDataTable.stories.ts +77 -0
- package/components/Layout/LayoutDataTable.vue +145 -0
- package/components/Layout/LayoutExpandableList.stories.ts +28 -0
- package/components/Layout/LayoutExpandableList.vue +94 -0
- package/components/Layout/LayoutPanelGrids.stories.ts +28 -0
- package/components/Layout/LayoutPanelGrids.vue +26 -0
- package/components/Layout/LayoutSectionBlock.stories.ts +37 -0
- package/components/Layout/LayoutSectionBlock.vue +37 -0
- package/components/Layout/LayoutSideNav.stories.ts +33 -0
- package/components/Layout/LayoutSideNav.vue +48 -0
- package/components/Layout/LayoutSplitPane.stories.ts +44 -0
- package/components/Layout/LayoutSplitPane.vue +30 -0
- package/components/Layout/LayoutTabs.stories.ts +43 -0
- package/components/Layout/LayoutTabs.vue +56 -0
- package/components/Layout/LayoutToolbar.stories.ts +60 -0
- package/components/Layout/LayoutToolbar.vue +28 -0
- package/components/Layout/LayoutVirtualList.stories.ts +30 -0
- package/components/Layout/LayoutVirtualList.vue +82 -0
- package/components/Overlay/OverlayDrawer.stories.ts +47 -0
- package/components/Overlay/OverlayDrawer.vue +58 -0
- package/components/Overlay/OverlayDropdown.stories.ts +25 -0
- package/components/Overlay/OverlayDropdown.vue +30 -0
- package/components/Overlay/OverlayDropdownItem.stories.ts +26 -0
- package/components/Overlay/OverlayDropdownItem.vue +31 -0
- package/components/Overlay/OverlayDropdownLabel.vue +9 -0
- package/components/Overlay/OverlayDropdownSeparator.vue +7 -0
- package/components/Overlay/OverlayModal.stories.ts +33 -0
- package/components/Overlay/OverlayModal.vue +48 -0
- package/components/Overlay/OverlayTooltip.stories.ts +33 -0
- package/components/Overlay/OverlayTooltip.vue +38 -0
- package/composables/colorScheme.ts +58 -0
- package/composables/toast.ts +81 -0
- package/package.json +99 -0
- package/skills/antfu-design/SKILL.md +65 -0
- package/skills/antfu-design/references/advanced-patterns.md +39 -0
- package/skills/antfu-design/references/best-practices.md +54 -0
- package/skills/antfu-design/references/core-components.md +72 -0
- package/skills/antfu-design/references/core-setup.md +56 -0
- package/skills/antfu-design/references/core-tokens.md +100 -0
- package/skills/antfu-design/references/features-data-presentation.md +27 -0
- package/splitpanes.d.ts +70 -0
- package/styles/animations.css +47 -0
- package/styles/base.css +31 -0
- package/styles/floating-vue.css +28 -0
- package/styles/index.css +7 -0
- package/styles/reka-ui.css +112 -0
- package/styles/scrollbar.css +24 -0
- package/styles/splitpanes.css +61 -0
- package/unocss/colors.ts +127 -0
- package/unocss/index.ts +99 -0
- package/unocss/options.ts +31 -0
- package/unocss/patterns.ts +38 -0
- package/unocss/rules.ts +26 -0
- package/unocss/severity.ts +16 -0
- package/unocss/shortcuts.ts +68 -0
- package/utils/color.ts +328 -0
- package/utils/contrast.ts +118 -0
- package/utils/format.ts +389 -0
- package/utils/icon.ts +200 -0
- package/utils/index.ts +13 -0
- package/utils/keybinding.ts +199 -0
- package/utils/misc.ts +141 -0
- package/utils/path.ts +243 -0
- package/utils/semver.ts +147 -0
- package/utils/tree.ts +89 -0
|
@@ -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,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
|
+
}
|