@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,145 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface Column {
|
|
5
|
+
/** Property key on the row used for value lookup and sorting. */
|
|
6
|
+
key: string
|
|
7
|
+
/** Header label; falls back to `key`. */
|
|
8
|
+
label?: string
|
|
9
|
+
/** Cell + header text alignment. */
|
|
10
|
+
align?: 'left' | 'right' | 'center'
|
|
11
|
+
/** Allow clicking the header to sort by this column. */
|
|
12
|
+
sortable?: boolean
|
|
13
|
+
/** Fixed column width (any CSS length). */
|
|
14
|
+
width?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
columns: Column[]
|
|
20
|
+
rows: T[]
|
|
21
|
+
/** Stable key per row; defaults to the row index. */
|
|
22
|
+
rowKey?: (row: T, index: number) => string | number
|
|
23
|
+
/** Emit `sort` only and leave row ordering to the parent. */
|
|
24
|
+
manualSort?: boolean
|
|
25
|
+
/** Pin the header to the top while the body scrolls. */
|
|
26
|
+
stickyHeader?: boolean
|
|
27
|
+
}>(),
|
|
28
|
+
{ manualSort: false, stickyHeader: true },
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{
|
|
32
|
+
sort: [payload: { key: string, dir: 'asc' | 'desc' }]
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
defineSlots<{
|
|
36
|
+
cell?: (props: { row: T, column: Column, value: unknown }) => any
|
|
37
|
+
header?: (props: { column: Column }) => any
|
|
38
|
+
}>()
|
|
39
|
+
|
|
40
|
+
const sortBy = defineModel<string | undefined>('sortBy')
|
|
41
|
+
const sortDir = defineModel<'asc' | 'desc'>('sortDir', { default: 'asc' })
|
|
42
|
+
|
|
43
|
+
const alignClass = { left: 'text-left', right: 'text-right', center: 'text-center' }
|
|
44
|
+
|
|
45
|
+
function valueOf(row: T, key: string): unknown {
|
|
46
|
+
return (row as Record<string, unknown>)[key]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sortedRows = computed<T[]>(() => {
|
|
50
|
+
const key = sortBy.value
|
|
51
|
+
if (props.manualSort || !key)
|
|
52
|
+
return props.rows
|
|
53
|
+
const dir = sortDir.value === 'desc' ? -1 : 1
|
|
54
|
+
return [...props.rows].sort((a, b) => {
|
|
55
|
+
const av = valueOf(a, key)
|
|
56
|
+
const bv = valueOf(b, key)
|
|
57
|
+
if (av == null && bv == null)
|
|
58
|
+
return 0
|
|
59
|
+
if (av == null)
|
|
60
|
+
return -1 * dir
|
|
61
|
+
if (bv == null)
|
|
62
|
+
return 1 * dir
|
|
63
|
+
if (typeof av === 'number' && typeof bv === 'number')
|
|
64
|
+
return (av - bv) * dir
|
|
65
|
+
return String(av).localeCompare(String(bv)) * dir
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
function toggleSort(col: Column): void {
|
|
70
|
+
if (!col.sortable)
|
|
71
|
+
return
|
|
72
|
+
if (sortBy.value === col.key) {
|
|
73
|
+
sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
sortBy.value = col.key
|
|
77
|
+
sortDir.value = 'asc'
|
|
78
|
+
}
|
|
79
|
+
emit('sort', { key: col.key, dir: sortDir.value })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function rowKeyOf(row: T, index: number): string | number {
|
|
83
|
+
return props.rowKey ? props.rowKey(row, index) : index
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<table class="text-sm w-full border-collapse">
|
|
89
|
+
<thead :class="stickyHeader ? 'sticky top-0 z-nav bg-secondary' : ''">
|
|
90
|
+
<tr>
|
|
91
|
+
<th
|
|
92
|
+
v-for="col in columns"
|
|
93
|
+
:key="col.key"
|
|
94
|
+
scope="col"
|
|
95
|
+
class="color-muted font-medium px-3 py-2 border-b border-base"
|
|
96
|
+
:class="alignClass[col.align ?? 'left']"
|
|
97
|
+
:style="col.width ? { width: col.width } : undefined"
|
|
98
|
+
:aria-sort="sortBy === col.key ? (sortDir === 'asc' ? 'ascending' : 'descending') : undefined"
|
|
99
|
+
>
|
|
100
|
+
<slot name="header" :column="col">
|
|
101
|
+
<button
|
|
102
|
+
v-if="col.sortable"
|
|
103
|
+
type="button"
|
|
104
|
+
class="outline-none rounded inline-flex gap-1 max-w-full items-center hover:color-base focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
|
105
|
+
:class="[alignClass[col.align ?? 'left'], col.align === 'right' ? 'justify-end' : col.align === 'center' ? 'justify-center' : '']"
|
|
106
|
+
@click="toggleSort(col)"
|
|
107
|
+
>
|
|
108
|
+
<span class="truncate">{{ col.label ?? col.key }}</span>
|
|
109
|
+
<svg
|
|
110
|
+
viewBox="0 0 16 16"
|
|
111
|
+
width="12"
|
|
112
|
+
height="12"
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
class="shrink-0 transition"
|
|
115
|
+
:class="sortBy === col.key ? 'color-active' : 'op-mute'"
|
|
116
|
+
:style="sortBy === col.key && sortDir === 'desc' ? 'transform: rotate(180deg)' : undefined"
|
|
117
|
+
>
|
|
118
|
+
<path fill="currentColor" d="M8 4l3.5 5h-7z" />
|
|
119
|
+
</svg>
|
|
120
|
+
</button>
|
|
121
|
+
<span v-else class="max-w-full inline-block truncate">{{ col.label ?? col.key }}</span>
|
|
122
|
+
</slot>
|
|
123
|
+
</th>
|
|
124
|
+
</tr>
|
|
125
|
+
</thead>
|
|
126
|
+
<tbody>
|
|
127
|
+
<tr
|
|
128
|
+
v-for="(row, i) in sortedRows"
|
|
129
|
+
:key="rowKeyOf(row, i)"
|
|
130
|
+
class="border-b border-mute hover:bg-active"
|
|
131
|
+
>
|
|
132
|
+
<td
|
|
133
|
+
v-for="col in columns"
|
|
134
|
+
:key="col.key"
|
|
135
|
+
class="px-3 py-1.5"
|
|
136
|
+
:class="alignClass[col.align ?? 'left']"
|
|
137
|
+
>
|
|
138
|
+
<slot name="cell" :row="row" :column="col" :value="valueOf(row, col.key)">
|
|
139
|
+
{{ String(valueOf(row, col.key)) }}
|
|
140
|
+
</slot>
|
|
141
|
+
</td>
|
|
142
|
+
</tr>
|
|
143
|
+
</tbody>
|
|
144
|
+
</table>
|
|
145
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import LayoutExpandableList from './LayoutExpandableList.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/LayoutExpandableList',
|
|
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: { LayoutExpandableList },
|
|
17
|
+
setup() {
|
|
18
|
+
return { items: Array.from({ length: 12 }, (_, i) => `Item ${i + 1}`) }
|
|
19
|
+
},
|
|
20
|
+
template: `<div class="w-64">
|
|
21
|
+
<LayoutExpandableList :items="items" :max="4">
|
|
22
|
+
<template #default="{ item }">
|
|
23
|
+
<div class="text-sm py-1 border-b border-base">{{ item }}</div>
|
|
24
|
+
</template>
|
|
25
|
+
</LayoutExpandableList>
|
|
26
|
+
</div>`,
|
|
27
|
+
}),
|
|
28
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
items: T[]
|
|
7
|
+
/** How many to show before collapsing. */
|
|
8
|
+
max?: number
|
|
9
|
+
/** Reveal this many more per "Show more" click; omit to reveal all at once. */
|
|
10
|
+
step?: number
|
|
11
|
+
/** Show a control to reverse the list order. */
|
|
12
|
+
reversible?: boolean
|
|
13
|
+
}>(),
|
|
14
|
+
{ max: 5 },
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const slots = defineSlots<{
|
|
18
|
+
default?: (props: { item: T, index: number }) => any
|
|
19
|
+
title?: () => any
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const shown = ref(props.max)
|
|
23
|
+
const reversed = ref(false)
|
|
24
|
+
|
|
25
|
+
const ordered = computed(() => (reversed.value ? [...props.items].reverse() : props.items))
|
|
26
|
+
const visible = computed(() => ordered.value.slice(0, shown.value))
|
|
27
|
+
const hidden = computed(() => Math.max(0, props.items.length - shown.value))
|
|
28
|
+
const expanded = computed(() => shown.value >= props.items.length)
|
|
29
|
+
const hasHeader = computed(() => props.reversible || !!slots.title)
|
|
30
|
+
|
|
31
|
+
function more(): void {
|
|
32
|
+
shown.value = props.step ? Math.min(props.items.length, shown.value + props.step) : props.items.length
|
|
33
|
+
}
|
|
34
|
+
function showAll(): void {
|
|
35
|
+
shown.value = props.items.length
|
|
36
|
+
}
|
|
37
|
+
function collapse(): void {
|
|
38
|
+
shown.value = props.max
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div>
|
|
44
|
+
<div v-if="hasHeader" class="text-sm mb-1 flex items-center justify-between">
|
|
45
|
+
<slot name="title" />
|
|
46
|
+
<button
|
|
47
|
+
v-if="reversible"
|
|
48
|
+
type="button"
|
|
49
|
+
class="btn-action-sm"
|
|
50
|
+
:aria-pressed="reversed"
|
|
51
|
+
@click="reversed = !reversed"
|
|
52
|
+
>
|
|
53
|
+
<span aria-hidden="true">⇅</span>
|
|
54
|
+
Reverse
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="relative">
|
|
58
|
+
<template v-for="(item, i) in visible" :key="i">
|
|
59
|
+
<slot :item="item" :index="i" />
|
|
60
|
+
</template>
|
|
61
|
+
<div
|
|
62
|
+
v-if="!expanded"
|
|
63
|
+
class="h-8 pointer-events-none inset-x-0 bottom-0 absolute bg-gradient-more"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
<div v-if="hidden > 0 || expanded" class="text-sm mt-1 flex gap-3 items-center">
|
|
67
|
+
<button
|
|
68
|
+
v-if="hidden > 0"
|
|
69
|
+
type="button"
|
|
70
|
+
class="color-active outline-none rounded inline-flex gap-1 items-center hover:underline focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
|
71
|
+
@click="more()"
|
|
72
|
+
>
|
|
73
|
+
Show more
|
|
74
|
+
<span class="text-micro px-1.5 rounded-full bg-active inline-flex h-4 items-center tabular-nums">+{{ hidden }}</span>
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
v-if="step && hidden > step"
|
|
78
|
+
type="button"
|
|
79
|
+
class="outline-none rounded op-fade hover:op100 hover:underline focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
|
80
|
+
@click="showAll()"
|
|
81
|
+
>
|
|
82
|
+
Show all
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
v-if="expanded && items.length > max"
|
|
86
|
+
type="button"
|
|
87
|
+
class="outline-none rounded op-fade hover:op100 hover:underline focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
|
88
|
+
@click="collapse()"
|
|
89
|
+
>
|
|
90
|
+
Show less
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import LayoutPanelGrids from './LayoutPanelGrids.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Layout/LayoutPanelGrids',
|
|
6
|
+
component: LayoutPanelGrids,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
variant: { control: 'inline-radio', options: ['dots', 'grid'] },
|
|
10
|
+
},
|
|
11
|
+
args: { variant: 'dots', size: 16 },
|
|
12
|
+
} satisfies Meta<typeof LayoutPanelGrids>
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj<typeof meta>
|
|
16
|
+
|
|
17
|
+
export const Dots: Story = {
|
|
18
|
+
render: args => ({
|
|
19
|
+
components: { LayoutPanelGrids },
|
|
20
|
+
setup: () => ({ args }),
|
|
21
|
+
template: `<LayoutPanelGrids v-bind="args" class="rounded-lg border border-base">
|
|
22
|
+
<span class="text-sm color-muted">No data yet</span>
|
|
23
|
+
</LayoutPanelGrids>`,
|
|
24
|
+
}),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Grid: Story = { ...Dots, args: { variant: 'grid' } }
|
|
28
|
+
export const LargeCells: Story = { ...Dots, args: { variant: 'grid', size: 32 } }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
/** Background pattern style. */
|
|
7
|
+
variant?: 'dots' | 'grid'
|
|
8
|
+
/** Pattern cell size in px. */
|
|
9
|
+
size?: number
|
|
10
|
+
}>(),
|
|
11
|
+
{ variant: 'dots', size: 16 },
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// Use the static `bg-dots` / `bg-grid` classes (statically extractable by
|
|
15
|
+
// UnoCSS) for the pattern, and override the cell size via inline
|
|
16
|
+
// `background-size` — a runtime-built `bg-grid-${size}` class would not be
|
|
17
|
+
// generated, so the pattern wouldn't render.
|
|
18
|
+
const patternClass = computed(() => (props.variant === 'grid' ? 'bg-grid' : 'bg-dots'))
|
|
19
|
+
const sizeStyle = computed(() => ({ backgroundSize: `${props.size}px ${props.size}px` }))
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div class="flex min-h-40 items-center justify-center relative" :class="patternClass" :style="sizeStyle">
|
|
24
|
+
<slot />
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import LayoutSectionBlock from './LayoutSectionBlock.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Layout/LayoutSectionBlock',
|
|
6
|
+
component: LayoutSectionBlock,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { title: 'Section', icon: 'i-ph:folder' },
|
|
9
|
+
} satisfies Meta<typeof LayoutSectionBlock>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Open: Story = {
|
|
15
|
+
render: () => ({
|
|
16
|
+
components: { LayoutSectionBlock },
|
|
17
|
+
template: `<div class="w-80">
|
|
18
|
+
<LayoutSectionBlock title="Open by default" icon="i-ph:folder">
|
|
19
|
+
<p class="text-sm color-muted">Collapsible content.</p>
|
|
20
|
+
</LayoutSectionBlock>
|
|
21
|
+
</div>`,
|
|
22
|
+
}),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Collapsed: Story = {
|
|
26
|
+
render: () => ({
|
|
27
|
+
components: { LayoutSectionBlock },
|
|
28
|
+
template: `<div class="w-80 flex flex-col gap-2">
|
|
29
|
+
<LayoutSectionBlock title="Open by default" icon="i-ph:folder">
|
|
30
|
+
<p class="text-sm color-muted">Collapsible content.</p>
|
|
31
|
+
</LayoutSectionBlock>
|
|
32
|
+
<LayoutSectionBlock title="Collapsed" :open="false">
|
|
33
|
+
<p class="text-sm color-muted">Hidden until expanded.</p>
|
|
34
|
+
</LayoutSectionBlock>
|
|
35
|
+
</div>`,
|
|
36
|
+
}),
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { CollapsibleContent, CollapsibleRoot, CollapsibleTrigger } from 'reka-ui'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
title?: string
|
|
6
|
+
icon?: string
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const open = defineModel<boolean>('open', { default: true })
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<CollapsibleRoot v-model:open="open" class="border border-base rounded-lg overflow-hidden">
|
|
14
|
+
<CollapsibleTrigger
|
|
15
|
+
class="group text-sm font-medium px-3 py-2 outline-none bg-secondary flex gap-2 w-full transition items-center hover:bg-active focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
|
16
|
+
>
|
|
17
|
+
<svg
|
|
18
|
+
width="1em"
|
|
19
|
+
height="1em"
|
|
20
|
+
viewBox="0 0 24 24"
|
|
21
|
+
class="op-fade transition-transform group-data-[state=open]:rotate-90"
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
>
|
|
24
|
+
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="m9 6l6 6l-6 6" />
|
|
25
|
+
</svg>
|
|
26
|
+
<span v-if="icon" :class="icon" aria-hidden="true" />
|
|
27
|
+
<slot name="title">
|
|
28
|
+
{{ title }}
|
|
29
|
+
</slot>
|
|
30
|
+
</CollapsibleTrigger>
|
|
31
|
+
<CollapsibleContent>
|
|
32
|
+
<div class="p-3">
|
|
33
|
+
<slot />
|
|
34
|
+
</div>
|
|
35
|
+
</CollapsibleContent>
|
|
36
|
+
</CollapsibleRoot>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import LayoutSideNav from './LayoutSideNav.vue'
|
|
4
|
+
|
|
5
|
+
// `component`/`typeof` omitted: the `v-model` + header/footer slots don't fit
|
|
6
|
+
// Storybook's args typing for a render-based story. The render uses it directly.
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Layout/LayoutSideNav',
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
} satisfies Meta
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj
|
|
14
|
+
|
|
15
|
+
const items = [
|
|
16
|
+
{ value: 'modules', label: 'Modules', icon: 'i-catppuccin:folder' },
|
|
17
|
+
{ value: 'graph', label: 'Graph', icon: 'i-catppuccin:typescript' },
|
|
18
|
+
{ value: 'issues', label: 'Issues', icon: 'i-catppuccin:json', badge: 12 },
|
|
19
|
+
{ value: 'settings', label: 'Settings', icon: 'i-catppuccin:toml', disabled: true },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
render: () => ({
|
|
24
|
+
components: { LayoutSideNav },
|
|
25
|
+
setup() {
|
|
26
|
+
const active = ref('modules')
|
|
27
|
+
return { active, items }
|
|
28
|
+
},
|
|
29
|
+
template: `<div class="w-56 border border-base rounded-lg">
|
|
30
|
+
<LayoutSideNav v-model="active" :items="items" />
|
|
31
|
+
</div>`,
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
export interface NavItem {
|
|
3
|
+
value: string
|
|
4
|
+
label?: string
|
|
5
|
+
/** Leading icon class (e.g. `i-catppuccin:folder`). */
|
|
6
|
+
icon?: string
|
|
7
|
+
/** Render the row as a link instead of a button. */
|
|
8
|
+
href?: string
|
|
9
|
+
/** Optional trailing count/label chip. */
|
|
10
|
+
badge?: string | number
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
defineProps<{
|
|
15
|
+
items: NavItem[]
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
/** Currently active item `value`. */
|
|
19
|
+
const model = defineModel<string>()
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<nav class="p-2 flex flex-col gap-0.5">
|
|
24
|
+
<slot name="header" />
|
|
25
|
+
<component
|
|
26
|
+
:is="item.href ? 'a' : 'button'"
|
|
27
|
+
v-for="item in items"
|
|
28
|
+
:key="item.value"
|
|
29
|
+
:href="item.href"
|
|
30
|
+
:type="item.href ? undefined : 'button'"
|
|
31
|
+
:aria-current="item.value === model ? 'page' : undefined"
|
|
32
|
+
class="text-sm px-2 py-1.5 text-left outline-none rounded-md flex gap-2 w-full transition items-center focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
|
33
|
+
:class="[
|
|
34
|
+
item.value === model ? 'bg-active color-active font-medium' : 'color-muted hover:bg-active hover:color-base',
|
|
35
|
+
item.disabled ? 'op50 pointer-events-none' : '',
|
|
36
|
+
]"
|
|
37
|
+
@click="!item.href && (model = item.value)"
|
|
38
|
+
>
|
|
39
|
+
<span v-if="item.icon" :class="item.icon" aria-hidden="true" />
|
|
40
|
+
<span class="flex-1 truncate">{{ item.label ?? item.value }}</span>
|
|
41
|
+
<span
|
|
42
|
+
v-if="item.badge != null"
|
|
43
|
+
class="text-micro px-1.5 rounded-full bg-active"
|
|
44
|
+
>{{ item.badge }}</span>
|
|
45
|
+
</component>
|
|
46
|
+
<slot name="footer" />
|
|
47
|
+
</nav>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { Pane } from 'splitpanes'
|
|
3
|
+
import LayoutSplitPane from './LayoutSplitPane.vue'
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Layout/LayoutSplitPane',
|
|
7
|
+
component: LayoutSplitPane,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
} satisfies Meta<typeof LayoutSplitPane>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Horizontal: Story = {
|
|
15
|
+
render: () => ({
|
|
16
|
+
components: { LayoutSplitPane, Pane },
|
|
17
|
+
template: `<div class="border border-base rounded-lg h-64 w-full overflow-hidden">
|
|
18
|
+
<LayoutSplitPane>
|
|
19
|
+
<Pane :size="40">
|
|
20
|
+
<div class="text-sm color-muted h-full p-3">Left pane</div>
|
|
21
|
+
</Pane>
|
|
22
|
+
<Pane :size="60">
|
|
23
|
+
<div class="text-sm color-muted h-full p-3">Right pane</div>
|
|
24
|
+
</Pane>
|
|
25
|
+
</LayoutSplitPane>
|
|
26
|
+
</div>`,
|
|
27
|
+
}),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Vertical: Story = {
|
|
31
|
+
render: () => ({
|
|
32
|
+
components: { LayoutSplitPane, Pane },
|
|
33
|
+
template: `<div class="border border-base rounded-lg h-64 w-full overflow-hidden">
|
|
34
|
+
<LayoutSplitPane horizontal>
|
|
35
|
+
<Pane>
|
|
36
|
+
<div class="text-sm color-muted h-full p-3">Top pane</div>
|
|
37
|
+
</Pane>
|
|
38
|
+
<Pane>
|
|
39
|
+
<div class="text-sm color-muted h-full p-3">Bottom pane</div>
|
|
40
|
+
</Pane>
|
|
41
|
+
</LayoutSplitPane>
|
|
42
|
+
</div>`,
|
|
43
|
+
}),
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SplitpanesResizedPayload } from 'splitpanes'
|
|
3
|
+
import { useLocalStorage } from '@vueuse/core'
|
|
4
|
+
import { Splitpanes } from 'splitpanes'
|
|
5
|
+
import { ref } from 'vue'
|
|
6
|
+
// Base layout + theming ship together in `@antfu/design/styles/splitpanes.css`.
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(
|
|
9
|
+
defineProps<{
|
|
10
|
+
horizontal?: boolean
|
|
11
|
+
/** Persist pane sizes under this localStorage key (via VueUse). */
|
|
12
|
+
storageKey?: string
|
|
13
|
+
}>(),
|
|
14
|
+
{},
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const sizes = props.storageKey
|
|
18
|
+
? useLocalStorage<number[]>(props.storageKey, [])
|
|
19
|
+
: ref<number[]>([])
|
|
20
|
+
|
|
21
|
+
function onResized(payload: SplitpanesResizedPayload): void {
|
|
22
|
+
sizes.value = payload.panes.map(p => p.size)
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<Splitpanes :horizontal="horizontal" @resized="onResized">
|
|
28
|
+
<slot :sizes="sizes" />
|
|
29
|
+
</Splitpanes>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import LayoutTabs from './LayoutTabs.vue'
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Layout/LayoutTabs',
|
|
7
|
+
component: LayoutTabs,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
variant: { control: 'inline-radio', options: ['underline', 'segment'] },
|
|
11
|
+
},
|
|
12
|
+
args: { tabs: [] }, // required prop; each story supplies real tabs via render
|
|
13
|
+
} satisfies Meta<typeof LayoutTabs>
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
type Story = StoryObj<typeof meta>
|
|
17
|
+
|
|
18
|
+
export const Underline: Story = {
|
|
19
|
+
render: () => ({
|
|
20
|
+
components: { LayoutTabs },
|
|
21
|
+
setup() {
|
|
22
|
+
return {
|
|
23
|
+
tab: ref('overview'),
|
|
24
|
+
tabs: [
|
|
25
|
+
{ value: 'overview', label: 'Overview', count: 12 },
|
|
26
|
+
{ value: 'deps', label: 'Dependencies', count: 48 },
|
|
27
|
+
{ value: 'settings', label: 'Settings' },
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
template: `<LayoutTabs v-model="tab" :tabs="tabs" class="w-96" />`,
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Segment: Story = {
|
|
36
|
+
render: () => ({
|
|
37
|
+
components: { LayoutTabs },
|
|
38
|
+
setup() {
|
|
39
|
+
return { tab: ref('list'), tabs: [{ value: 'list', label: 'List' }, { value: 'grid', label: 'Grid' }] }
|
|
40
|
+
},
|
|
41
|
+
template: `<LayoutTabs v-model="tab" :tabs="tabs" variant="segment" />`,
|
|
42
|
+
}),
|
|
43
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { TabsIndicator, TabsList, TabsRoot, TabsTrigger } from 'reka-ui'
|
|
3
|
+
|
|
4
|
+
export interface TabItem {
|
|
5
|
+
value: string
|
|
6
|
+
label?: string
|
|
7
|
+
icon?: string
|
|
8
|
+
/** Optional count chip. */
|
|
9
|
+
count?: number
|
|
10
|
+
/** Render this trigger as a link (nav-style tabs). */
|
|
11
|
+
href?: string
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
withDefaults(
|
|
16
|
+
defineProps<{
|
|
17
|
+
tabs: TabItem[]
|
|
18
|
+
/** `underline` (default) or `segment` (pill switcher). */
|
|
19
|
+
variant?: 'underline' | 'segment'
|
|
20
|
+
}>(),
|
|
21
|
+
{ variant: 'underline' },
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const model = defineModel<string>()
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<TabsRoot v-model="model" :class="variant === 'segment' ? '' : 'border-b border-base'">
|
|
29
|
+
<TabsList
|
|
30
|
+
class="flex items-center relative"
|
|
31
|
+
:class="variant === 'segment' ? 'gap-1 p-1 rounded-lg bg-secondary w-max' : 'gap-1'"
|
|
32
|
+
>
|
|
33
|
+
<TabsTrigger
|
|
34
|
+
v-for="tab in tabs"
|
|
35
|
+
:key="tab.value"
|
|
36
|
+
:value="tab.value"
|
|
37
|
+
:as="tab.href ? 'a' : 'button'"
|
|
38
|
+
:href="tab.href"
|
|
39
|
+
:disabled="tab.disabled"
|
|
40
|
+
class="text-sm color-muted outline-none flex gap-1.5 transition items-center relative disabled:op50 focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
|
41
|
+
:class="variant === 'segment'
|
|
42
|
+
? 'px-3 py-1 rounded-md data-[state=active]:bg-base data-[state=active]:color-base data-[state=active]:shadow-sm'
|
|
43
|
+
: 'px-3 py-2 -mb-px border-b-2 border-transparent hover:color-base data-[state=active]:color-active data-[state=active]:border-primary-500 dark:data-[state=active]:border-primary-400'"
|
|
44
|
+
>
|
|
45
|
+
<span v-if="tab.icon" :class="tab.icon" aria-hidden="true" />
|
|
46
|
+
{{ tab.label ?? tab.value }}
|
|
47
|
+
<span
|
|
48
|
+
v-if="tab.count != null"
|
|
49
|
+
class="text-micro font-mono px-1.5 rounded-full bg-active inline-flex h-5 min-w-5 items-center justify-center tabular-nums"
|
|
50
|
+
>{{ tab.count }}</span>
|
|
51
|
+
</TabsTrigger>
|
|
52
|
+
<TabsIndicator v-if="variant === 'underline'" class="rounded-full bg-primary-500 h-0.5 w-[--reka-tabs-indicator-size] translate-x-[--reka-tabs-indicator-position] transition-all duration-200 bottom-0 left-0 absolute" />
|
|
53
|
+
</TabsList>
|
|
54
|
+
<slot />
|
|
55
|
+
</TabsRoot>
|
|
56
|
+
</template>
|