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