@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,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import FormSelect from './FormSelect.vue'
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Form/FormSelect',
|
|
7
|
+
component: FormSelect,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
args: { options: [] }, // required prop; each story supplies real options via render
|
|
10
|
+
} satisfies Meta<typeof FormSelect>
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof meta>
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: () => ({
|
|
17
|
+
components: { FormSelect },
|
|
18
|
+
setup() {
|
|
19
|
+
return {
|
|
20
|
+
value: ref('green'),
|
|
21
|
+
options: [
|
|
22
|
+
{ value: 'green', label: 'antfu green' },
|
|
23
|
+
{ value: 'blue', label: 'GitHub blue' },
|
|
24
|
+
{ value: 'purple', label: 'Vite purple' },
|
|
25
|
+
],
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
template: `<FormSelect v-model="value" :options="options" placeholder="Pick a theme" />`,
|
|
29
|
+
}),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Empty: Story = {
|
|
33
|
+
render: () => ({
|
|
34
|
+
components: { FormSelect },
|
|
35
|
+
setup() {
|
|
36
|
+
return {
|
|
37
|
+
value: ref<string>(),
|
|
38
|
+
options: [
|
|
39
|
+
{ value: 'a', label: 'Option A' },
|
|
40
|
+
{ value: 'b', label: 'Option B' },
|
|
41
|
+
{ value: 'c', label: 'Disabled', disabled: true },
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
template: `<FormSelect v-model="value" :options="options" placeholder="Select…" />`,
|
|
46
|
+
}),
|
|
47
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { SelectContent, SelectIcon, SelectItem, SelectItemIndicator, SelectItemText, SelectPortal, SelectRoot, SelectTrigger, SelectValue, SelectViewport } from 'reka-ui'
|
|
3
|
+
|
|
4
|
+
export interface SelectOption {
|
|
5
|
+
value: string
|
|
6
|
+
label?: string
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
defineProps<{
|
|
11
|
+
options: SelectOption[]
|
|
12
|
+
placeholder?: string
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const model = defineModel<string>()
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<SelectRoot v-model="model" :disabled="disabled">
|
|
21
|
+
<SelectTrigger
|
|
22
|
+
class="text-sm px-2.5 outline-none border border-base rounded bg-base inline-flex gap-2 h-9 min-w-40 transition items-center justify-between data-[disabled]:op50 data-[disabled]:pointer-events-none focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
|
23
|
+
>
|
|
24
|
+
<SelectValue :placeholder="placeholder ?? 'Select…'" />
|
|
25
|
+
<SelectIcon class="op-fade">
|
|
26
|
+
<svg width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true">
|
|
27
|
+
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="m6 9l6 6l6-6" />
|
|
28
|
+
</svg>
|
|
29
|
+
</SelectIcon>
|
|
30
|
+
</SelectTrigger>
|
|
31
|
+
<SelectPortal>
|
|
32
|
+
<SelectContent
|
|
33
|
+
position="popper"
|
|
34
|
+
:side-offset="6"
|
|
35
|
+
class="border border-base rounded-lg bg-base min-w-[--reka-select-trigger-width] shadow-lg z-dropdown overflow-hidden"
|
|
36
|
+
>
|
|
37
|
+
<SelectViewport class="p-1">
|
|
38
|
+
<SelectItem
|
|
39
|
+
v-for="opt in options"
|
|
40
|
+
:key="opt.value"
|
|
41
|
+
:value="opt.value"
|
|
42
|
+
:disabled="opt.disabled"
|
|
43
|
+
class="text-sm color-base py-1.5 pl-7 pr-2 outline-none rounded-md flex gap-2 cursor-pointer select-none transition items-center relative data-[highlighted]:bg-active data-[disabled]:op50 data-[disabled]:pointer-events-none"
|
|
44
|
+
>
|
|
45
|
+
<SelectItemIndicator class="color-active inline-flex items-center left-1.5 absolute">
|
|
46
|
+
<svg width="0.85em" height="0.85em" viewBox="0 0 24 24" aria-hidden="true">
|
|
47
|
+
<path fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="M20 6L9 17l-5-5" />
|
|
48
|
+
</svg>
|
|
49
|
+
</SelectItemIndicator>
|
|
50
|
+
<SelectItemText>{{ opt.label ?? opt.value }}</SelectItemText>
|
|
51
|
+
</SelectItem>
|
|
52
|
+
</SelectViewport>
|
|
53
|
+
</SelectContent>
|
|
54
|
+
</SelectPortal>
|
|
55
|
+
</SelectRoot>
|
|
56
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import FormSwitch from './FormSwitch.vue'
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Form/FormSwitch',
|
|
7
|
+
component: FormSwitch,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
} satisfies Meta<typeof FormSwitch>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => ({
|
|
16
|
+
components: { FormSwitch },
|
|
17
|
+
setup() {
|
|
18
|
+
return { on: ref(true) }
|
|
19
|
+
},
|
|
20
|
+
template: `<FormSwitch v-model="on" label="Dark surfaces" />`,
|
|
21
|
+
}),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const States: Story = {
|
|
25
|
+
render: () => ({
|
|
26
|
+
components: { FormSwitch },
|
|
27
|
+
setup() {
|
|
28
|
+
return { a: ref(true), b: ref(false), c: ref(false) }
|
|
29
|
+
},
|
|
30
|
+
template: `<div class="flex flex-col gap-3">
|
|
31
|
+
<FormSwitch v-model="a" label="On" />
|
|
32
|
+
<FormSwitch v-model="b" label="Off" />
|
|
33
|
+
<FormSwitch v-model="c" label="Disabled" disabled />
|
|
34
|
+
</div>`,
|
|
35
|
+
}),
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { SwitchRoot, SwitchThumb } from 'reka-ui'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
label?: string
|
|
6
|
+
disabled?: boolean
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const model = defineModel<boolean>({ default: false })
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<label
|
|
14
|
+
class="text-sm inline-flex gap-2 cursor-pointer select-none items-center"
|
|
15
|
+
:class="{ 'op50 pointer-events-none': disabled }"
|
|
16
|
+
>
|
|
17
|
+
<SwitchRoot
|
|
18
|
+
v-model="model"
|
|
19
|
+
:disabled="disabled"
|
|
20
|
+
class="outline-none rounded-full bg-active h-5 w-9 transition relative data-[state=checked]:bg-primary-500 focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
|
21
|
+
>
|
|
22
|
+
<SwitchThumb class="rounded-full bg-white h-4 w-4 block shadow translate-x-0.5 transition-transform data-[state=checked]:translate-x-4" />
|
|
23
|
+
</SwitchRoot>
|
|
24
|
+
<span v-if="label || $slots.default"><slot>{{ label }}</slot></span>
|
|
25
|
+
</label>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import FormTextInput from './FormTextInput.vue'
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Form/FormTextInput',
|
|
7
|
+
component: FormTextInput,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
size: { control: 'inline-radio', options: ['sm', 'md'] },
|
|
11
|
+
},
|
|
12
|
+
} satisfies Meta<typeof FormTextInput>
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj<typeof meta>
|
|
16
|
+
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
render: () => ({
|
|
19
|
+
components: { FormTextInput },
|
|
20
|
+
setup() {
|
|
21
|
+
return { text: ref('') }
|
|
22
|
+
},
|
|
23
|
+
template: `<div class="w-72"><FormTextInput v-model="text" placeholder="Your name" icon="i-ph:folder" clearable /></div>`,
|
|
24
|
+
}),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const States: Story = {
|
|
28
|
+
render: () => ({
|
|
29
|
+
components: { FormTextInput },
|
|
30
|
+
setup() {
|
|
31
|
+
return { a: ref('Hello'), b: ref(''), c: ref('Locked') }
|
|
32
|
+
},
|
|
33
|
+
template: `<div class="flex flex-col gap-3 w-72">
|
|
34
|
+
<FormTextInput v-model="a" placeholder="Default" clearable />
|
|
35
|
+
<FormTextInput v-model="b" placeholder="Small" size="sm" />
|
|
36
|
+
<FormTextInput v-model="c" placeholder="Disabled" disabled />
|
|
37
|
+
</div>`,
|
|
38
|
+
}),
|
|
39
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
icon?: string
|
|
5
|
+
clearable?: boolean
|
|
6
|
+
placeholder?: string
|
|
7
|
+
type?: string
|
|
8
|
+
size?: 'sm' | 'md'
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
/** Render the invalid (red) state. */
|
|
11
|
+
invalid?: boolean
|
|
12
|
+
}>(),
|
|
13
|
+
{ type: 'text', size: 'md' },
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const model = defineModel<string>({ default: '' })
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<label
|
|
21
|
+
class="px-2 border rounded bg-base inline-flex gap-2 transition items-center focus-within:ring-2"
|
|
22
|
+
:class="[
|
|
23
|
+
size === 'sm' ? 'h-7 text-sm' : 'h-9',
|
|
24
|
+
invalid ? 'border-red-500/60 focus-within:ring-red-500/40' : 'border-base focus-within:ring-primary-500/40',
|
|
25
|
+
{ 'op50 pointer-events-none': disabled },
|
|
26
|
+
]"
|
|
27
|
+
>
|
|
28
|
+
<span v-if="icon" :class="icon" class="op-fade shrink-0" aria-hidden="true" />
|
|
29
|
+
<slot name="prefix" />
|
|
30
|
+
<input
|
|
31
|
+
v-model="model"
|
|
32
|
+
:type="type"
|
|
33
|
+
:placeholder="placeholder"
|
|
34
|
+
:disabled="disabled"
|
|
35
|
+
:aria-invalid="invalid || undefined"
|
|
36
|
+
class="color-base outline-none bg-transparent flex-1 min-w-0 placeholder:op-mute"
|
|
37
|
+
>
|
|
38
|
+
<button
|
|
39
|
+
v-if="clearable && model"
|
|
40
|
+
type="button"
|
|
41
|
+
class="op-fade shrink-0 transition hover:op100"
|
|
42
|
+
aria-label="Clear"
|
|
43
|
+
@click="model = ''"
|
|
44
|
+
>
|
|
45
|
+
<svg width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true">
|
|
46
|
+
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M6 6l12 12M18 6L6 18" />
|
|
47
|
+
</svg>
|
|
48
|
+
</button>
|
|
49
|
+
<slot name="suffix" />
|
|
50
|
+
</label>
|
|
51
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import FormTextarea from './FormTextarea.vue'
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Form/FormTextarea',
|
|
7
|
+
component: FormTextarea,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
args: { placeholder: 'Write something…', rows: 4, resize: true },
|
|
10
|
+
} satisfies Meta<typeof FormTextarea>
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof meta>
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: args => ({
|
|
17
|
+
components: { FormTextarea },
|
|
18
|
+
setup() {
|
|
19
|
+
return { args, text: ref('') }
|
|
20
|
+
},
|
|
21
|
+
template: `<div class="w-72"><FormTextarea v-bind="args" v-model="text" /></div>`,
|
|
22
|
+
}),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Invalid: Story = {
|
|
26
|
+
render: args => ({
|
|
27
|
+
components: { FormTextarea },
|
|
28
|
+
setup() {
|
|
29
|
+
return { args, text: ref('Too short') }
|
|
30
|
+
},
|
|
31
|
+
template: `<div class="w-72"><FormTextarea v-bind="args" v-model="text" invalid /></div>`,
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const States: Story = {
|
|
36
|
+
render: () => ({
|
|
37
|
+
components: { FormTextarea },
|
|
38
|
+
setup() {
|
|
39
|
+
return { a: ref('Resizable by default'), b: ref('No resize handle'), c: ref('Disabled') }
|
|
40
|
+
},
|
|
41
|
+
template: `<div class="flex flex-col gap-3 w-72">
|
|
42
|
+
<FormTextarea v-model="a" placeholder="Default" />
|
|
43
|
+
<FormTextarea v-model="b" placeholder="Fixed" :resize="false" />
|
|
44
|
+
<FormTextarea v-model="c" placeholder="Disabled" disabled />
|
|
45
|
+
</div>`,
|
|
46
|
+
}),
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
placeholder?: string
|
|
5
|
+
/** Visible rows. */
|
|
6
|
+
rows?: number
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
/** Render the invalid (red) state. */
|
|
9
|
+
invalid?: boolean
|
|
10
|
+
/** Allow the user to resize the textarea. */
|
|
11
|
+
resize?: boolean
|
|
12
|
+
}>(),
|
|
13
|
+
{ rows: 4, resize: true },
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const model = defineModel<string>({ default: '' })
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<textarea
|
|
21
|
+
v-model="model"
|
|
22
|
+
:rows="rows"
|
|
23
|
+
:placeholder="placeholder"
|
|
24
|
+
:disabled="disabled"
|
|
25
|
+
:aria-invalid="invalid || undefined"
|
|
26
|
+
class="color-base px-2 py-1.5 outline-none border rounded bg-base w-full transition placeholder:op-mute focus:ring-2"
|
|
27
|
+
:class="[
|
|
28
|
+
invalid ? 'border-red-500/60 focus:ring-red-500/40' : 'border-base focus:ring-primary-500/40',
|
|
29
|
+
{ 'resize-none': !resize, 'op50 pointer-events-none': disabled },
|
|
30
|
+
]"
|
|
31
|
+
/>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import LayoutBreadcrumb from './LayoutBreadcrumb.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Layout/LayoutBreadcrumb',
|
|
6
|
+
component: LayoutBreadcrumb,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: {
|
|
9
|
+
items: [
|
|
10
|
+
{ label: 'Home', href: '#' },
|
|
11
|
+
{ label: 'Projects', href: '#' },
|
|
12
|
+
{ label: 'design', href: '#' },
|
|
13
|
+
{ label: 'LayoutBreadcrumb.vue' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
} satisfies Meta<typeof LayoutBreadcrumb>
|
|
17
|
+
|
|
18
|
+
export default meta
|
|
19
|
+
type Story = StoryObj<typeof meta>
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {}
|
|
22
|
+
|
|
23
|
+
export const ChevronSeparator: Story = {
|
|
24
|
+
render: args => ({
|
|
25
|
+
components: { LayoutBreadcrumb },
|
|
26
|
+
setup() {
|
|
27
|
+
return { args }
|
|
28
|
+
},
|
|
29
|
+
template: `<LayoutBreadcrumb v-bind="args">
|
|
30
|
+
<template #separator>
|
|
31
|
+
<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
|
|
32
|
+
<path fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M6 3l5 5-5 5" />
|
|
33
|
+
</svg>
|
|
34
|
+
</template>
|
|
35
|
+
</LayoutBreadcrumb>`,
|
|
36
|
+
}),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const Truncated: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
items: [
|
|
42
|
+
{ label: 'Home', href: '#' },
|
|
43
|
+
{ label: 'A very long intermediate section name that should truncate', href: '#' },
|
|
44
|
+
{ label: 'Another long current page title that is also quite verbose' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
render: args => ({
|
|
48
|
+
components: { LayoutBreadcrumb },
|
|
49
|
+
setup() {
|
|
50
|
+
return { args }
|
|
51
|
+
},
|
|
52
|
+
template: `<div class="w-72"><LayoutBreadcrumb v-bind="args" /></div>`,
|
|
53
|
+
}),
|
|
54
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
export interface Crumb {
|
|
3
|
+
label: string
|
|
4
|
+
/** Link target; the last crumb is rendered as plain text regardless. */
|
|
5
|
+
href?: string
|
|
6
|
+
/** Optional leading icon class. */
|
|
7
|
+
icon?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
withDefaults(
|
|
11
|
+
defineProps<{
|
|
12
|
+
items: Crumb[]
|
|
13
|
+
/** Text rendered between crumbs; overridden by the `#separator` slot. */
|
|
14
|
+
separator?: string
|
|
15
|
+
}>(),
|
|
16
|
+
{ separator: '/' },
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
defineSlots<{
|
|
20
|
+
separator?: () => any
|
|
21
|
+
}>()
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<nav aria-label="Breadcrumb">
|
|
26
|
+
<ol class="text-sm flex flex-wrap min-w-0 items-center">
|
|
27
|
+
<li
|
|
28
|
+
v-for="(item, i) in items"
|
|
29
|
+
:key="i"
|
|
30
|
+
class="flex min-w-0 items-center"
|
|
31
|
+
>
|
|
32
|
+
<span v-if="i > 0" class="mx-1 op-mute shrink-0" aria-hidden="true">
|
|
33
|
+
<slot name="separator">{{ separator }}</slot>
|
|
34
|
+
</span>
|
|
35
|
+
<a
|
|
36
|
+
v-if="item.href && i < items.length - 1"
|
|
37
|
+
:href="item.href"
|
|
38
|
+
class="color-muted outline-none rounded inline-flex gap-1 min-w-0 transition items-center hover:color-active focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
|
39
|
+
>
|
|
40
|
+
<span v-if="item.icon" :class="item.icon" class="shrink-0" aria-hidden="true" />
|
|
41
|
+
<span class="truncate">{{ item.label }}</span>
|
|
42
|
+
</a>
|
|
43
|
+
<span
|
|
44
|
+
v-else
|
|
45
|
+
class="color-base inline-flex gap-1 min-w-0 items-center"
|
|
46
|
+
:aria-current="i === items.length - 1 ? 'page' : undefined"
|
|
47
|
+
>
|
|
48
|
+
<span v-if="item.icon" :class="item.icon" class="shrink-0" aria-hidden="true" />
|
|
49
|
+
<span class="truncate">{{ item.label }}</span>
|
|
50
|
+
</span>
|
|
51
|
+
</li>
|
|
52
|
+
</ol>
|
|
53
|
+
</nav>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import LayoutCard from './LayoutCard.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Layout/LayoutCard',
|
|
6
|
+
component: LayoutCard,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
} satisfies Meta<typeof LayoutCard>
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
type Story = StoryObj<typeof meta>
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
render: () => ({
|
|
15
|
+
components: { LayoutCard },
|
|
16
|
+
template: `<LayoutCard class="w-64">
|
|
17
|
+
<div class="font-medium">Card title</div>
|
|
18
|
+
<p class="text-sm color-muted mt-1">A bordered, token-driven surface.</p>
|
|
19
|
+
</LayoutCard>`,
|
|
20
|
+
}),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Elevated: Story = {
|
|
24
|
+
render: () => ({
|
|
25
|
+
components: { LayoutCard },
|
|
26
|
+
template: `<LayoutCard class="w-64" elevated>
|
|
27
|
+
<div class="font-medium">Elevated card</div>
|
|
28
|
+
<p class="text-sm color-muted mt-1">With a subtle drop shadow.</p>
|
|
29
|
+
</LayoutCard>`,
|
|
30
|
+
}),
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
as?: string
|
|
5
|
+
padding?: boolean
|
|
6
|
+
/** Subtle drop shadow. */
|
|
7
|
+
elevated?: boolean
|
|
8
|
+
}>(),
|
|
9
|
+
{ padding: true },
|
|
10
|
+
)
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<component
|
|
15
|
+
:is="as || 'div'"
|
|
16
|
+
class="border border-base rounded-xl bg-base"
|
|
17
|
+
:class="[{ 'p-4': padding }, { 'shadow-sm': elevated }]"
|
|
18
|
+
>
|
|
19
|
+
<slot />
|
|
20
|
+
</component>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import LayoutDataTable from './LayoutDataTable.vue'
|
|
4
|
+
|
|
5
|
+
// `component` is omitted: this is a generic SFC, which doesn't fit Storybook's
|
|
6
|
+
// `Meta<typeof Component>` typing. The render below uses it directly.
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Layout/LayoutDataTable',
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
} satisfies Meta
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj
|
|
14
|
+
|
|
15
|
+
const columns = [
|
|
16
|
+
{ key: 'name', label: 'Name', sortable: true },
|
|
17
|
+
{ key: 'type', label: 'Type' },
|
|
18
|
+
{ key: 'size', label: 'Size', align: 'right' as const, sortable: true, width: '6rem' },
|
|
19
|
+
{ key: 'time', label: 'Modified', align: 'right' as const, sortable: true, width: '8rem' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const rows = [
|
|
23
|
+
{ name: 'index.ts', type: 'file', size: 1240, time: '2026-06-20' },
|
|
24
|
+
{ name: 'components', type: 'dir', size: 8, time: '2026-06-25' },
|
|
25
|
+
{ name: 'README.md', type: 'file', size: 3072, time: '2026-06-18' },
|
|
26
|
+
{ name: 'package.json', type: 'file', size: 856, time: '2026-06-26' },
|
|
27
|
+
{ name: 'utils', type: 'dir', size: 12, time: '2026-06-22' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export const Default: Story = {
|
|
31
|
+
render: () => ({
|
|
32
|
+
components: { LayoutDataTable },
|
|
33
|
+
setup() {
|
|
34
|
+
return { columns, rows, sortBy: ref<string | undefined>('name') }
|
|
35
|
+
},
|
|
36
|
+
template: `<div class="border border-base rounded-lg max-w-2xl overflow-hidden">
|
|
37
|
+
<LayoutDataTable v-model:sortBy="sortBy" :columns="columns" :rows="rows" />
|
|
38
|
+
</div>`,
|
|
39
|
+
}),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const CustomCells: Story = {
|
|
43
|
+
render: () => ({
|
|
44
|
+
components: { LayoutDataTable },
|
|
45
|
+
setup() {
|
|
46
|
+
return { columns, rows }
|
|
47
|
+
},
|
|
48
|
+
template: `<div class="border border-base rounded-lg max-w-2xl overflow-hidden">
|
|
49
|
+
<LayoutDataTable :columns="columns" :rows="rows" :sticky-header="false">
|
|
50
|
+
<template #cell="{ column, value }">
|
|
51
|
+
<span v-if="column.key === 'name'" class="font-mono">{{ value }}</span>
|
|
52
|
+
<span v-else-if="column.key === 'type'" class="badge-muted">{{ value }}</span>
|
|
53
|
+
<span v-else-if="column.key === 'size'" class="tabular-nums">{{ value }} B</span>
|
|
54
|
+
<span v-else class="color-muted tabular-nums">{{ value }}</span>
|
|
55
|
+
</template>
|
|
56
|
+
</LayoutDataTable>
|
|
57
|
+
</div>`,
|
|
58
|
+
}),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const StickyHeader: Story = {
|
|
62
|
+
render: () => ({
|
|
63
|
+
components: { LayoutDataTable },
|
|
64
|
+
setup() {
|
|
65
|
+
const many = Array.from({ length: 30 }, (_, i) => ({
|
|
66
|
+
name: `file-${i + 1}.ts`,
|
|
67
|
+
type: i % 3 === 0 ? 'dir' : 'file',
|
|
68
|
+
size: (i + 1) * 128,
|
|
69
|
+
time: `2026-06-${String((i % 28) + 1).padStart(2, '0')}`,
|
|
70
|
+
}))
|
|
71
|
+
return { columns, rows: many, sortBy: ref<string | undefined>('size'), sortDir: ref<'asc' | 'desc'>('desc') }
|
|
72
|
+
},
|
|
73
|
+
template: `<div class="border border-base rounded-lg max-w-2xl h-64 overflow-auto">
|
|
74
|
+
<LayoutDataTable v-model:sortBy="sortBy" v-model:sortDir="sortDir" :columns="columns" :rows="rows" />
|
|
75
|
+
</div>`,
|
|
76
|
+
}),
|
|
77
|
+
}
|