@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,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { vTooltip } from 'floating-vue'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { getFileIcon } from '../../utils/icon'
|
|
5
|
+
import { parseReadablePath } from '../../utils/path'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
path: string
|
|
10
|
+
/** Project root, trimmed from the display path. */
|
|
11
|
+
root?: string
|
|
12
|
+
/** Show a leading file icon. */
|
|
13
|
+
icon?: boolean
|
|
14
|
+
/** Link target. */
|
|
15
|
+
href?: string
|
|
16
|
+
/** Trailing line number (rendered `:line`). */
|
|
17
|
+
line?: number
|
|
18
|
+
/** Trailing column number (rendered `:line:col`, needs `line`). */
|
|
19
|
+
column?: number
|
|
20
|
+
/** Dim the directory portion of the path. */
|
|
21
|
+
dim?: boolean
|
|
22
|
+
/** Style as interactive (cursor + hover) for `@click` usage even without `href`. */
|
|
23
|
+
clickable?: boolean
|
|
24
|
+
/** Prefix substituted for a `.pnpm` store chunk (passed to the path parser). */
|
|
25
|
+
pnpmCollapse?: string
|
|
26
|
+
}>(),
|
|
27
|
+
{ root: '', icon: true, dim: true },
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const parsed = computed(() => parseReadablePath(props.path, props.root, { pnpmCollapse: props.pnpmCollapse }))
|
|
31
|
+
|
|
32
|
+
/** Split the display path into directory / base / query(+hash) for distinct styling. */
|
|
33
|
+
const segments = computed(() => {
|
|
34
|
+
const full = parsed.value.path
|
|
35
|
+
const q = full.search(/[?#]/)
|
|
36
|
+
const clean = q === -1 ? full : full.slice(0, q)
|
|
37
|
+
const query = q === -1 ? '' : full.slice(q)
|
|
38
|
+
const idx = clean.lastIndexOf('/')
|
|
39
|
+
const dir = idx === -1 ? '' : clean.slice(0, idx + 1)
|
|
40
|
+
const base = idx === -1 ? clean : clean.slice(idx + 1)
|
|
41
|
+
return { dir, base, query }
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const interactive = computed(() => props.href != null || props.clickable)
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<component
|
|
49
|
+
:is="href ? 'a' : 'span'"
|
|
50
|
+
v-tooltip="path"
|
|
51
|
+
:href="href"
|
|
52
|
+
class="text-sm font-mono inline-flex gap-1 max-w-full min-w-0 items-center"
|
|
53
|
+
:class="{ 'hover:color-active transition': interactive, 'cursor-pointer': clickable && !href }"
|
|
54
|
+
>
|
|
55
|
+
<span v-if="icon" :class="getFileIcon(segments.base)" class="op-fade shrink-0" aria-hidden="true" />
|
|
56
|
+
<span class="truncate">
|
|
57
|
+
<span v-if="segments.dir" :class="dim ? 'op-mute' : 'color-base'">{{ segments.dir }}</span><span class="color-base">{{ segments.base }}</span><span v-if="segments.query" class="op-mute italic">{{ segments.query }}</span><span v-if="line != null" class="op-mute">:{{ line }}<template v-if="column != null">:{{ column }}</template></span>
|
|
58
|
+
</span>
|
|
59
|
+
<span v-if="parsed.moduleName" class="text-xs op-mute shrink-0">({{ parsed.moduleName }})</span>
|
|
60
|
+
</component>
|
|
61
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayKbd from './DisplayKbd.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayKbd',
|
|
6
|
+
component: DisplayKbd,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { keys: 'mod+k' },
|
|
9
|
+
} satisfies Meta<typeof DisplayKbd>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = { args: { keys: 'mod+k' } }
|
|
15
|
+
export const Chord: Story = { args: { keys: 'g g' } }
|
|
16
|
+
|
|
17
|
+
export const Bindings: Story = {
|
|
18
|
+
render: () => ({
|
|
19
|
+
components: { DisplayKbd },
|
|
20
|
+
template: `<div class="flex items-center gap-3">
|
|
21
|
+
<DisplayKbd keys="mod+k" />
|
|
22
|
+
<DisplayKbd keys="shift+?" />
|
|
23
|
+
<DisplayKbd keys="g g" />
|
|
24
|
+
</div>`,
|
|
25
|
+
}),
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { bindingDisplay } from '../../utils/keybinding'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
/** A chord/binding string, e.g. `mod+k` or `g g`, rendered as platform glyphs. */
|
|
7
|
+
keys?: string
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const tokens = computed(() => (props.keys ? bindingDisplay(props.keys) : []))
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<span class="inline-flex gap-0.5 items-center">
|
|
15
|
+
<template v-if="keys">
|
|
16
|
+
<kbd
|
|
17
|
+
v-for="(token, i) in tokens"
|
|
18
|
+
:key="i"
|
|
19
|
+
class="text-micro color-muted leading-none font-mono px-1 border border-base rounded bg-secondary inline-flex h-5 min-w-5 items-center justify-center"
|
|
20
|
+
>{{ token }}</kbd>
|
|
21
|
+
</template>
|
|
22
|
+
<kbd
|
|
23
|
+
v-else
|
|
24
|
+
class="text-micro color-muted leading-none font-mono px-1.5 border border-base rounded bg-secondary inline-flex h-5 min-w-5 items-center justify-center"
|
|
25
|
+
><slot /></kbd>
|
|
26
|
+
</span>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayKeyValue from './DisplayKeyValue.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayKeyValue',
|
|
6
|
+
component: DisplayKeyValue,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { label: 'Version', value: '1.4.0' },
|
|
9
|
+
} satisfies Meta<typeof DisplayKeyValue>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Inline: Story = { args: { label: 'Version', value: '1.4.0' } }
|
|
15
|
+
export const Stacked: Story = { args: { label: 'Total size', value: '204 kB', orientation: 'stacked' } }
|
|
16
|
+
|
|
17
|
+
export const DetailRows: Story = {
|
|
18
|
+
render: () => ({
|
|
19
|
+
components: { DisplayKeyValue },
|
|
20
|
+
template: `<div class="w-72 flex flex-col gap-1">
|
|
21
|
+
<DisplayKeyValue label="Name" value="@antfu/design" />
|
|
22
|
+
<DisplayKeyValue label="Version" value="1.4.0" />
|
|
23
|
+
<DisplayKeyValue label="License" value="MIT" :mono="false" />
|
|
24
|
+
<DisplayKeyValue label="Downloads" value="1,204,558" />
|
|
25
|
+
</div>`,
|
|
26
|
+
}),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const StatStrip: Story = {
|
|
30
|
+
render: () => ({
|
|
31
|
+
components: { DisplayKeyValue },
|
|
32
|
+
template: `<div class="flex gap-6">
|
|
33
|
+
<DisplayKeyValue label="Files" value="128" orientation="stacked" />
|
|
34
|
+
<DisplayKeyValue label="Size" value="204 kB" orientation="stacked" />
|
|
35
|
+
<DisplayKeyValue label="Modules" value="42" orientation="stacked" />
|
|
36
|
+
</div>`,
|
|
37
|
+
}),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const WithBadge: Story = {
|
|
41
|
+
render: () => ({
|
|
42
|
+
components: { DisplayKeyValue },
|
|
43
|
+
template: `<div class="w-72 flex flex-col gap-3">
|
|
44
|
+
<DisplayKeyValue label="Status" value="passing">
|
|
45
|
+
<template #badge>
|
|
46
|
+
<span class="badge-color-green">ok</span>
|
|
47
|
+
</template>
|
|
48
|
+
</DisplayKeyValue>
|
|
49
|
+
<DisplayKeyValue label="Coverage" value="98%" orientation="stacked">
|
|
50
|
+
<template #badge>
|
|
51
|
+
<span class="badge-color-blue">+2%</span>
|
|
52
|
+
</template>
|
|
53
|
+
</DisplayKeyValue>
|
|
54
|
+
</div>`,
|
|
55
|
+
}),
|
|
56
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
/** The label text (falls back slot: `#label`). */
|
|
5
|
+
label?: string
|
|
6
|
+
/** The value (falls back slot: `#default`). */
|
|
7
|
+
value?: string | number
|
|
8
|
+
/** Inline row or stacked stat. */
|
|
9
|
+
orientation?: 'inline' | 'stacked'
|
|
10
|
+
/** Render the value with `font-mono tabular-nums`. */
|
|
11
|
+
mono?: boolean
|
|
12
|
+
}>(),
|
|
13
|
+
{ orientation: 'inline', mono: true },
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
defineSlots<{
|
|
17
|
+
/** The value content; falls back to the `value` prop. */
|
|
18
|
+
default?: () => any
|
|
19
|
+
/** The label content; falls back to the `label` prop. */
|
|
20
|
+
label?: () => any
|
|
21
|
+
/** Trailing badge / adornment. */
|
|
22
|
+
badge?: () => any
|
|
23
|
+
}>()
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div
|
|
28
|
+
v-if="orientation === 'stacked'"
|
|
29
|
+
class="flex flex-col gap-0.5"
|
|
30
|
+
>
|
|
31
|
+
<span class="text-micro color-faint tracking-wide uppercase">
|
|
32
|
+
<slot name="label">{{ label }}</slot>
|
|
33
|
+
</span>
|
|
34
|
+
<span class="text-lg color-base flex gap-2 items-center" :class="{ 'font-mono tabular-nums': mono }">
|
|
35
|
+
<slot>{{ value }}</slot>
|
|
36
|
+
<slot name="badge" />
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div
|
|
40
|
+
v-else
|
|
41
|
+
class="flex gap-2 items-center justify-between"
|
|
42
|
+
>
|
|
43
|
+
<span class="text-sm color-muted">
|
|
44
|
+
<slot name="label">{{ label }}</slot>
|
|
45
|
+
</span>
|
|
46
|
+
<span class="text-sm color-base flex gap-2 items-center" :class="{ 'font-mono tabular-nums': mono }">
|
|
47
|
+
<slot>{{ value }}</slot>
|
|
48
|
+
<slot name="badge" />
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayLabel from './DisplayLabel.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayLabel',
|
|
6
|
+
component: DisplayLabel,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { text: 'bug', color: '#d73a4a' },
|
|
9
|
+
} satisfies Meta<typeof DisplayLabel>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Colored: Story = { args: { text: 'bug', color: '#d73a4a' } }
|
|
15
|
+
export const Muted: Story = { args: { text: 'wontfix' } }
|
|
16
|
+
|
|
17
|
+
export const GitHubLabels: Story = {
|
|
18
|
+
render: () => ({
|
|
19
|
+
components: { DisplayLabel },
|
|
20
|
+
template: `<div class="flex flex-wrap gap-2">
|
|
21
|
+
<DisplayLabel text="bug" color="#d73a4a" />
|
|
22
|
+
<DisplayLabel text="enhancement" color="#a2eeef" />
|
|
23
|
+
<DisplayLabel text="docs" color="#0075ca" />
|
|
24
|
+
<DisplayLabel text="wontfix" />
|
|
25
|
+
</div>`,
|
|
26
|
+
}),
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useColorScheme } from '../../composables/colorScheme'
|
|
4
|
+
import { labelStyle } from '../../utils/color'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
/** Display text. */
|
|
8
|
+
text?: string
|
|
9
|
+
/** Base hex color; a contrast-aware tinted chip is derived from it. */
|
|
10
|
+
color?: string
|
|
11
|
+
/** The app's current color scheme. Falls back to context, then `'light'`. */
|
|
12
|
+
colorScheme?: 'light' | 'dark'
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const scheme = useColorScheme(() => props.colorScheme)
|
|
16
|
+
|
|
17
|
+
const style = computed(() => {
|
|
18
|
+
if (!props.color)
|
|
19
|
+
return undefined
|
|
20
|
+
const s = labelStyle(props.color, scheme.value === 'dark')
|
|
21
|
+
return { color: s.color, background: s.background, borderColor: s.borderColor }
|
|
22
|
+
})
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<span
|
|
27
|
+
class="text-xs leading-none font-medium px-1.5 py-0.5 border rounded-full inline-flex gap-1 items-center"
|
|
28
|
+
:class="{ 'badge-muted border-transparent': !color }"
|
|
29
|
+
:style="style"
|
|
30
|
+
>
|
|
31
|
+
<slot>{{ text }}</slot>
|
|
32
|
+
</span>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayNumber from './DisplayNumber.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayNumber',
|
|
6
|
+
component: DisplayNumber,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { value: 1234567 },
|
|
9
|
+
} satisfies Meta<typeof DisplayNumber>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = { args: { value: 1234567 } }
|
|
15
|
+
export const WithSuffix: Story = { args: { value: 42, suffix: 'ms' } }
|
|
16
|
+
export const Percent: Story = { args: { value: 0.42, options: { style: 'percent' } } }
|
|
17
|
+
|
|
18
|
+
export const Formats: Story = {
|
|
19
|
+
render: () => ({
|
|
20
|
+
components: { DisplayNumber },
|
|
21
|
+
template: `<div class="flex flex-col gap-2">
|
|
22
|
+
<DisplayNumber :value="1234567" />
|
|
23
|
+
<DisplayNumber :value="42" suffix="ms" />
|
|
24
|
+
<DisplayNumber :value="0.42" :options="{ style: 'percent' }" />
|
|
25
|
+
</div>`,
|
|
26
|
+
}),
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { formatNumber } from '../../utils/format'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
value: number
|
|
8
|
+
prefix?: string
|
|
9
|
+
suffix?: string
|
|
10
|
+
/** Forwarded to `Intl.NumberFormat`. */
|
|
11
|
+
options?: Intl.NumberFormatOptions
|
|
12
|
+
mono?: boolean
|
|
13
|
+
}>(),
|
|
14
|
+
{ mono: true },
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const formatted = computed(() => formatNumber(props.value, props.options))
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<span :class="{ 'font-mono tabular-nums': mono }">
|
|
22
|
+
<span v-if="prefix" class="op-fade">{{ prefix }}</span>{{ formatted }}<span v-if="suffix" class="text-xs ml-0.5 op-fade">{{ suffix }}</span>
|
|
23
|
+
</span>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayNumberBadge from './DisplayNumberBadge.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayNumberBadge',
|
|
6
|
+
component: DisplayNumberBadge,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { value: 128, prefix: '×' },
|
|
9
|
+
} satisfies Meta<typeof DisplayNumberBadge>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = { args: { value: 128, prefix: '×' } }
|
|
15
|
+
export const Colored: Story = { args: { value: 42, color: 'green' } }
|
|
16
|
+
|
|
17
|
+
export const Examples: Story = {
|
|
18
|
+
render: () => ({
|
|
19
|
+
components: { DisplayNumberBadge },
|
|
20
|
+
template: `<div class="flex items-center gap-2">
|
|
21
|
+
<DisplayNumberBadge :value="128" prefix="×" />
|
|
22
|
+
<DisplayNumberBadge :value="42" color="green" />
|
|
23
|
+
<DisplayNumberBadge :value="0.42" :options="{ style: 'percent' }" color="blue" />
|
|
24
|
+
</div>`,
|
|
25
|
+
}),
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import DisplayBadge from './DisplayBadge.vue'
|
|
3
|
+
import DisplayNumber from './DisplayNumber.vue'
|
|
4
|
+
|
|
5
|
+
withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
value: number
|
|
8
|
+
prefix?: string
|
|
9
|
+
suffix?: string
|
|
10
|
+
options?: Intl.NumberFormatOptions
|
|
11
|
+
/** DisplayBadge color (palette name, hue, css color); defaults to muted. */
|
|
12
|
+
color?: boolean | number | string
|
|
13
|
+
}>(),
|
|
14
|
+
{ color: false },
|
|
15
|
+
)
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<DisplayBadge :color="color" size="sm">
|
|
20
|
+
<DisplayNumber :value="value" :prefix="prefix" :suffix="suffix" :options="options" />
|
|
21
|
+
</DisplayBadge>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayPackageName from './DisplayPackageName.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayPackageName',
|
|
6
|
+
component: DisplayPackageName,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { name: '@antfu/design' },
|
|
9
|
+
} satisfies Meta<typeof DisplayPackageName>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Scoped: Story = { args: { name: '@antfu/design' } }
|
|
15
|
+
export const Unscoped: Story = { args: { name: 'unocss' } }
|
|
16
|
+
|
|
17
|
+
export const Examples: Story = {
|
|
18
|
+
render: () => ({
|
|
19
|
+
components: { DisplayPackageName },
|
|
20
|
+
template: `<div class="flex gap-3">
|
|
21
|
+
<DisplayPackageName name="@antfu/design" />
|
|
22
|
+
<DisplayPackageName name="@vueuse/core" />
|
|
23
|
+
<DisplayPackageName name="unocss" />
|
|
24
|
+
</div>`,
|
|
25
|
+
}),
|
|
26
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useColorScheme } from '../../composables/colorScheme'
|
|
4
|
+
import { getHashColorFromString, getPluginColor, stripPluginPrefix } from '../../utils/color'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(
|
|
7
|
+
defineProps<{
|
|
8
|
+
name: string
|
|
9
|
+
/** The app's current color scheme. Falls back to context, then `'light'`. */
|
|
10
|
+
colorScheme?: 'light' | 'dark'
|
|
11
|
+
/** Strip plugin prefixes (`vite-plugin-`, `unplugin-`, `vite:`, …) from the displayed name. */
|
|
12
|
+
strip?: boolean
|
|
13
|
+
/** Color the scope by ecosystem brand hue (vs a plain string hash). */
|
|
14
|
+
brand?: boolean
|
|
15
|
+
/** Extra brand hues merged over the defaults (see `getPluginColor`). */
|
|
16
|
+
brandHues?: Record<string, number>
|
|
17
|
+
}>(),
|
|
18
|
+
{ brand: true },
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const scheme = useColorScheme(() => props.colorScheme)
|
|
22
|
+
const dark = computed(() => scheme.value === 'dark')
|
|
23
|
+
|
|
24
|
+
const display = computed(() => (props.strip ? stripPluginPrefix(props.name) : props.name))
|
|
25
|
+
|
|
26
|
+
const parts = computed(() => {
|
|
27
|
+
const n = display.value
|
|
28
|
+
if (n.startsWith('@')) {
|
|
29
|
+
const slash = n.indexOf('/')
|
|
30
|
+
if (slash !== -1)
|
|
31
|
+
return { scope: n.slice(0, slash + 1), rest: n.slice(slash + 1) }
|
|
32
|
+
}
|
|
33
|
+
return { scope: '', rest: n }
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const scopeColor = computed(() => {
|
|
37
|
+
if (!parts.value.scope)
|
|
38
|
+
return undefined
|
|
39
|
+
return props.brand
|
|
40
|
+
? getPluginColor(props.name, 1, dark.value, props.brandHues)
|
|
41
|
+
: getHashColorFromString(parts.value.scope, 1, dark.value)
|
|
42
|
+
})
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<span class="text-sm font-mono">
|
|
47
|
+
<span v-if="parts.scope" :style="{ color: scopeColor }">{{ parts.scope }}</span><span class="color-base">{{ parts.rest }}</span>
|
|
48
|
+
</span>
|
|
49
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayProgressBar from './DisplayProgressBar.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayProgressBar',
|
|
6
|
+
component: DisplayProgressBar,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { value: 0.6, height: 6, rounded: true, label: 'Progress' },
|
|
9
|
+
decorators: [() => ({ template: '<div class="w-80"><story /></div>' })],
|
|
10
|
+
} satisfies Meta<typeof DisplayProgressBar>
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof meta>
|
|
14
|
+
|
|
15
|
+
export const Determinate: Story = {
|
|
16
|
+
args: { value: 0.6 },
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Indeterminate: Story = {
|
|
20
|
+
args: { value: undefined },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const CustomColor: Story = {
|
|
24
|
+
args: { value: 0.75, color: 'hsl(153, 65%, 40%)' },
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Tall: Story = {
|
|
28
|
+
args: { value: 0.4, height: 12 },
|
|
29
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
/** Progress from 0 to 1. Omit for an indeterminate (animated) bar. */
|
|
7
|
+
value?: number
|
|
8
|
+
/** Track height in pixels. */
|
|
9
|
+
height?: number
|
|
10
|
+
/**
|
|
11
|
+
* Bar color. A CSS color (`#…`, `rgb(…)`, `hsl(…)`, `var(…)`) is applied as
|
|
12
|
+
* a background; any other string is treated as a utility/token class.
|
|
13
|
+
* Defaults to `bg-primary-500`.
|
|
14
|
+
*/
|
|
15
|
+
color?: string
|
|
16
|
+
/** Round the track and bar ends. */
|
|
17
|
+
rounded?: boolean
|
|
18
|
+
/** Accessible label, exposed as `aria-label`. */
|
|
19
|
+
label?: string
|
|
20
|
+
}>(),
|
|
21
|
+
{ height: 6, rounded: true },
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
function isCssColor(value: string): boolean {
|
|
25
|
+
return /^#|^hsl|^rgb|^var\(/.test(value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const indeterminate = computed(() => props.value == null)
|
|
29
|
+
|
|
30
|
+
const clamped = computed(() => Math.min(Math.max(props.value ?? 0, 0), 1))
|
|
31
|
+
|
|
32
|
+
const valueNow = computed(() => (indeterminate.value ? undefined : Math.round(clamped.value * 100)))
|
|
33
|
+
|
|
34
|
+
const colorClass = computed(() =>
|
|
35
|
+
props.color == null
|
|
36
|
+
? 'bg-primary-500'
|
|
37
|
+
: isCssColor(props.color)
|
|
38
|
+
? ''
|
|
39
|
+
: props.color,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const colorStyle = computed(() =>
|
|
43
|
+
props.color != null && isCssColor(props.color) ? { background: props.color } : undefined,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const barStyle = computed(() => ({
|
|
47
|
+
...colorStyle.value,
|
|
48
|
+
...(indeterminate.value ? {} : { width: `${clamped.value * 100}%` }),
|
|
49
|
+
}))
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div
|
|
54
|
+
class="bg-active w-full overflow-hidden"
|
|
55
|
+
:class="{ 'rounded-full': rounded }"
|
|
56
|
+
:style="{ height: `${height}px` }"
|
|
57
|
+
role="progressbar"
|
|
58
|
+
:aria-valuenow="valueNow"
|
|
59
|
+
:aria-valuemin="indeterminate ? undefined : 0"
|
|
60
|
+
:aria-valuemax="indeterminate ? undefined : 100"
|
|
61
|
+
:aria-label="label"
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
class="h-full transition-all"
|
|
65
|
+
:class="[colorClass, { 'rounded-full': rounded, 'af-progress-indeterminate w-2/5': indeterminate }]"
|
|
66
|
+
:style="barStyle"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<style scoped>
|
|
72
|
+
@keyframes af-progress-slide {
|
|
73
|
+
0% {
|
|
74
|
+
transform: translateX(-100%);
|
|
75
|
+
}
|
|
76
|
+
100% {
|
|
77
|
+
transform: translateX(350%);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.af-progress-indeterminate {
|
|
82
|
+
animation: af-progress-slide 1.2s ease-in-out infinite;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@media (prefers-reduced-motion: reduce) {
|
|
86
|
+
.af-progress-indeterminate {
|
|
87
|
+
animation: none;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import type { ProportionSegment } from './DisplayProportionBar.vue'
|
|
3
|
+
import DisplayProportionBar from './DisplayProportionBar.vue'
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Display/DisplayProportionBar',
|
|
7
|
+
component: DisplayProportionBar,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
args: { segments: [] }, // required prop; stories supply their own via render
|
|
10
|
+
} satisfies Meta<typeof DisplayProportionBar>
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof meta>
|
|
14
|
+
|
|
15
|
+
const segments: ProportionSegment[] = [
|
|
16
|
+
{ value: 60, label: 'JavaScript', color: '#f1e05a' },
|
|
17
|
+
{ value: 30, label: 'TypeScript', color: '#3178c6' },
|
|
18
|
+
{ value: 10, label: 'CSS', color: '#563d7c' },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
args: { segments, height: 12 },
|
|
23
|
+
decorators: [() => ({ template: '<div class="w-80"><story /></div>' })],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const HashColored: Story = {
|
|
27
|
+
render: () => ({
|
|
28
|
+
components: { DisplayProportionBar },
|
|
29
|
+
setup() {
|
|
30
|
+
return {
|
|
31
|
+
segments: [
|
|
32
|
+
{ value: 5, label: 'vue' },
|
|
33
|
+
{ value: 3, label: 'react' },
|
|
34
|
+
{ value: 2, label: 'svelte' },
|
|
35
|
+
] as ProportionSegment[],
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
template: `<div class="w-80"><DisplayProportionBar :segments="segments" :height="12" /></div>`,
|
|
39
|
+
}),
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useColorScheme } from '../../composables/colorScheme'
|
|
4
|
+
import { getHashColorFromString } from '../../utils/color'
|
|
5
|
+
|
|
6
|
+
export interface ProportionSegment {
|
|
7
|
+
value: number
|
|
8
|
+
color?: string
|
|
9
|
+
label?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(
|
|
13
|
+
defineProps<{
|
|
14
|
+
segments: ProportionSegment[]
|
|
15
|
+
height?: number
|
|
16
|
+
/** The app's current color scheme. Falls back to context, then `'light'`. */
|
|
17
|
+
colorScheme?: 'light' | 'dark'
|
|
18
|
+
}>(),
|
|
19
|
+
{ height: 8 },
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const scheme = useColorScheme(() => props.colorScheme)
|
|
23
|
+
const total = computed(() => props.segments.reduce((sum, s) => sum + s.value, 0) || 1)
|
|
24
|
+
|
|
25
|
+
function segColor(seg: ProportionSegment, i: number): string {
|
|
26
|
+
return seg.color ?? getHashColorFromString(seg.label ?? String(i), 1, scheme.value === 'dark')
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div
|
|
32
|
+
class="rounded-full bg-active flex w-full overflow-hidden"
|
|
33
|
+
:style="{ height: `${height}px` }"
|
|
34
|
+
>
|
|
35
|
+
<div
|
|
36
|
+
v-for="(seg, i) in segments"
|
|
37
|
+
:key="i"
|
|
38
|
+
class="h-full"
|
|
39
|
+
:style="{ width: `${(seg.value / total) * 100}%`, background: segColor(seg, i) }"
|
|
40
|
+
:title="seg.label"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|