@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
|
+
<script setup lang="ts">
|
|
2
|
+
import { vTooltip } from 'floating-vue'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
icon?: string
|
|
8
|
+
/** Tooltip text (floating-vue). Requires `@antfu/design/styles/floating-vue.css`. */
|
|
9
|
+
tooltip?: string
|
|
10
|
+
active?: boolean
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
/** Accessible label when the button has no visible text. */
|
|
13
|
+
label?: string
|
|
14
|
+
size?: 'sm' | 'md' | 'lg'
|
|
15
|
+
/** Compact, square (non-circular) icon button for dense toolbars. */
|
|
16
|
+
compact?: boolean
|
|
17
|
+
/** Class(es) applied when `active` — overrides the default `color-active bg-active` tint. */
|
|
18
|
+
activeClass?: string
|
|
19
|
+
}>(),
|
|
20
|
+
{ size: 'md' },
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const SIZE: Record<NonNullable<typeof props.size>, string> = {
|
|
24
|
+
sm: 'w-7! h-7! text-sm',
|
|
25
|
+
md: '',
|
|
26
|
+
lg: 'w-11! h-11! text-lg',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const baseClass = computed(() => (props.compact ? 'btn-icon-compact' : 'btn-icon'))
|
|
30
|
+
const sizeClass = computed(() => (props.compact ? '' : SIZE[props.size]))
|
|
31
|
+
const activeStateClass = computed(() => (props.active ? (props.activeClass || 'color-active bg-active op100') : ''))
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<button
|
|
36
|
+
v-tooltip="tooltip"
|
|
37
|
+
type="button"
|
|
38
|
+
:class="[baseClass, sizeClass, activeStateClass]"
|
|
39
|
+
:disabled="disabled"
|
|
40
|
+
:aria-label="label ?? tooltip"
|
|
41
|
+
:aria-pressed="active || undefined"
|
|
42
|
+
>
|
|
43
|
+
<span v-if="icon" :class="icon" aria-hidden="true" />
|
|
44
|
+
<slot />
|
|
45
|
+
<slot name="badge" />
|
|
46
|
+
</button>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayAvatar from './DisplayAvatar.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayAvatar',
|
|
6
|
+
component: DisplayAvatar,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { name: 'Anthony Fu', size: 32 },
|
|
9
|
+
} satisfies Meta<typeof DisplayAvatar>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const WithImage: Story = {
|
|
15
|
+
args: { src: 'https://github.com/antfu.png', name: 'Anthony Fu' },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const InitialsFallback: Story = {
|
|
19
|
+
args: { name: 'Anthony Fu' },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const Square: Story = {
|
|
23
|
+
args: { name: 'Vite Press', square: true, size: 48 },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const Gallery: Story = {
|
|
27
|
+
render: () => ({
|
|
28
|
+
components: { DisplayAvatar },
|
|
29
|
+
setup() {
|
|
30
|
+
return { names: ['Vue', 'React Native', 'svelte-kit', 'unocss', 'Anthony Fu', 'esbuild'] }
|
|
31
|
+
},
|
|
32
|
+
template: `<div class="flex flex-wrap gap-2 items-center">
|
|
33
|
+
<DisplayAvatar v-for="n in names" :key="n" :name="n" :size="40" />
|
|
34
|
+
</div>`,
|
|
35
|
+
}),
|
|
36
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useColorScheme } from '../../composables/colorScheme'
|
|
4
|
+
import { getHashColorFromString } from '../../utils/color'
|
|
5
|
+
import DisplaySafeImage from './DisplaySafeImage.vue'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
/** Image source. Falls back to initials when absent or failed. */
|
|
10
|
+
src?: string
|
|
11
|
+
/** Name driving the initials and the hash-tinted fallback color. */
|
|
12
|
+
name?: string
|
|
13
|
+
/** Size in pixels (width and height). */
|
|
14
|
+
size?: number
|
|
15
|
+
/** Use rounded-corner square instead of a circle. */
|
|
16
|
+
square?: boolean
|
|
17
|
+
/**
|
|
18
|
+
* The app's current color scheme — tunes the fallback hash color for
|
|
19
|
+
* contrast. Falls back to {@link provideColorScheme} context, then `'light'`.
|
|
20
|
+
*/
|
|
21
|
+
colorScheme?: 'light' | 'dark'
|
|
22
|
+
}>(),
|
|
23
|
+
{ size: 32 },
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const scheme = useColorScheme(() => props.colorScheme)
|
|
27
|
+
const dark = computed(() => scheme.value === 'dark')
|
|
28
|
+
|
|
29
|
+
const initials = computed(() =>
|
|
30
|
+
(props.name ?? '')
|
|
31
|
+
.split(/[\s/-]+/)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.slice(0, 2)
|
|
34
|
+
.map(part => part[0]!.toUpperCase())
|
|
35
|
+
.join(''),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const fallbackStyle = computed(() => ({
|
|
39
|
+
background: getHashColorFromString(props.name ?? '', 0.18, dark.value),
|
|
40
|
+
color: getHashColorFromString(props.name ?? '', 1, dark.value),
|
|
41
|
+
}))
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<span
|
|
46
|
+
class="text-xs font-medium inline-flex shrink-0 select-none items-center justify-center overflow-hidden"
|
|
47
|
+
:class="square ? 'rounded-md' : 'rounded-full'"
|
|
48
|
+
:style="{ width: `${size}px`, height: `${size}px` }"
|
|
49
|
+
>
|
|
50
|
+
<DisplaySafeImage :src="src" :alt="name">
|
|
51
|
+
<template #fallback>
|
|
52
|
+
<span class="inline-flex h-full w-full items-center justify-center" :style="fallbackStyle">
|
|
53
|
+
{{ initials }}
|
|
54
|
+
</span>
|
|
55
|
+
</template>
|
|
56
|
+
</DisplaySafeImage>
|
|
57
|
+
</span>
|
|
58
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayBadge from './DisplayBadge.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayBadge',
|
|
6
|
+
component: DisplayBadge,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
variant: { control: 'inline-radio', options: ['subtle', 'solid'] },
|
|
10
|
+
size: { control: 'inline-radio', options: ['sm', 'md'] },
|
|
11
|
+
},
|
|
12
|
+
args: { text: 'vite', variant: 'subtle', size: 'md' },
|
|
13
|
+
} satisfies Meta<typeof DisplayBadge>
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
type Story = StoryObj<typeof meta>
|
|
17
|
+
|
|
18
|
+
export const HashColored: Story = { args: { text: 'unocss' } }
|
|
19
|
+
export const Solid: Story = { args: { text: 'rolldown', variant: 'solid' } }
|
|
20
|
+
export const Palette: Story = { args: { text: 'esm', color: 'green' } }
|
|
21
|
+
export const Muted: Story = { args: { text: 'unknown', color: false } }
|
|
22
|
+
export const WithIcon: Story = { args: { text: 'stable', icon: 'i-ph:seal-check', color: 'green' } }
|
|
23
|
+
|
|
24
|
+
export const Gallery: Story = {
|
|
25
|
+
render: () => ({
|
|
26
|
+
components: { DisplayBadge },
|
|
27
|
+
template: `<div class="flex flex-wrap gap-2">
|
|
28
|
+
<DisplayBadge v-for="n in ['vue','react','svelte','vite','unocss','nuxt','rolldown','eslint']" :key="n" :text="n" />
|
|
29
|
+
</div>`,
|
|
30
|
+
}),
|
|
31
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useColorScheme } from '../../composables/colorScheme'
|
|
4
|
+
import { getHashColorFromString, getHsla } from '../../utils/color'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(
|
|
7
|
+
defineProps<{
|
|
8
|
+
/** Text content (also the hash seed when `color` is `true`). */
|
|
9
|
+
text?: string
|
|
10
|
+
/**
|
|
11
|
+
* - `true` (default): deterministic color hashed from `text`
|
|
12
|
+
* - `false`: neutral / muted
|
|
13
|
+
* - `number`: explicit hue (0–360)
|
|
14
|
+
* - `'#...'` / `'hsl(...)'` / `'rgb(...)'`: explicit CSS color
|
|
15
|
+
* - any other string: a palette name → `badge-color-<name>`
|
|
16
|
+
*/
|
|
17
|
+
color?: boolean | number | string
|
|
18
|
+
/** `subtle` = tinted background, `solid` = filled. */
|
|
19
|
+
variant?: 'subtle' | 'solid'
|
|
20
|
+
size?: 'sm' | 'md'
|
|
21
|
+
/** Leading icon class (e.g. `i-ph:seal-check`). */
|
|
22
|
+
icon?: string
|
|
23
|
+
/** Render as another element/component. */
|
|
24
|
+
as?: string
|
|
25
|
+
/**
|
|
26
|
+
* The app's current color scheme — tunes hash colors for contrast. Falls
|
|
27
|
+
* back to {@link provideColorScheme} context, then `'light'`.
|
|
28
|
+
*/
|
|
29
|
+
colorScheme?: 'light' | 'dark'
|
|
30
|
+
}>(),
|
|
31
|
+
{
|
|
32
|
+
color: true,
|
|
33
|
+
variant: 'subtle',
|
|
34
|
+
size: 'md',
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
function isCssColor(value: string): boolean {
|
|
39
|
+
return /^#|^hsl|^rgb|^var\(/.test(value)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const scheme = useColorScheme(() => props.colorScheme)
|
|
43
|
+
const dark = computed(() => scheme.value === 'dark')
|
|
44
|
+
|
|
45
|
+
const seedColor = computed<string | undefined>(() => {
|
|
46
|
+
const { color, text } = props
|
|
47
|
+
if (typeof color === 'number')
|
|
48
|
+
return getHsla(color, 1, dark.value)
|
|
49
|
+
if (typeof color === 'string' && isCssColor(color))
|
|
50
|
+
return color
|
|
51
|
+
if (color === true && text)
|
|
52
|
+
return getHashColorFromString(text, 1, dark.value)
|
|
53
|
+
return undefined
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const seedBg = computed<string | undefined>(() => {
|
|
57
|
+
const { color, text } = props
|
|
58
|
+
if (typeof color === 'number')
|
|
59
|
+
return getHsla(color, props.variant === 'solid' ? 1 : 0.12, dark.value)
|
|
60
|
+
if (typeof color === 'string' && isCssColor(color))
|
|
61
|
+
return color
|
|
62
|
+
if (color === true && text)
|
|
63
|
+
return getHashColorFromString(text, props.variant === 'solid' ? 1 : 0.12, dark.value)
|
|
64
|
+
return undefined
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const style = computed(() => {
|
|
68
|
+
if (!seedColor.value)
|
|
69
|
+
return undefined
|
|
70
|
+
return props.variant === 'solid'
|
|
71
|
+
? { color: '#fff', background: seedBg.value }
|
|
72
|
+
: { color: seedColor.value, background: seedBg.value }
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const paletteClass = computed(() =>
|
|
76
|
+
typeof props.color === 'string' && !isCssColor(props.color)
|
|
77
|
+
? `badge-color-${props.color}`
|
|
78
|
+
: props.color === false
|
|
79
|
+
? 'badge-muted'
|
|
80
|
+
: '',
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const sizeClass = computed(() =>
|
|
84
|
+
props.size === 'sm' ? 'text-micro px-1.5 py-0.25' : 'text-xs px-2 py-0.5',
|
|
85
|
+
)
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<component
|
|
90
|
+
:is="as || 'span'"
|
|
91
|
+
class="badge"
|
|
92
|
+
:class="[sizeClass, paletteClass]"
|
|
93
|
+
:style="style"
|
|
94
|
+
>
|
|
95
|
+
<span v-if="icon" :class="icon" aria-hidden="true" />
|
|
96
|
+
<slot>{{ text }}</slot>
|
|
97
|
+
</component>
|
|
98
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayBytes from './DisplayBytes.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayBytes',
|
|
6
|
+
component: DisplayBytes,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { bytes: 204800 },
|
|
9
|
+
} satisfies Meta<typeof DisplayBytes>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = { args: { bytes: 204800 } }
|
|
15
|
+
export const Colorized: Story = { args: { bytes: 2097152, colorize: true } }
|
|
16
|
+
export const WithTotal: Story = { args: { bytes: 524288, total: 1048576 } }
|
|
17
|
+
|
|
18
|
+
export const Scale: Story = {
|
|
19
|
+
render: () => ({
|
|
20
|
+
components: { DisplayBytes },
|
|
21
|
+
template: `<div class="flex flex-col gap-1">
|
|
22
|
+
<DisplayBytes :bytes="512" colorize />
|
|
23
|
+
<DisplayBytes :bytes="204800" colorize />
|
|
24
|
+
<DisplayBytes :bytes="2097152" colorize />
|
|
25
|
+
<DisplayBytes :bytes="524288" :total="1048576" />
|
|
26
|
+
</div>`,
|
|
27
|
+
}),
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { formatBytes, getBytesColor } from '../../utils/format'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
bytes: number
|
|
8
|
+
base?: 1024 | 1000
|
|
9
|
+
/** Maximum decimal places before trailing-zero trimming. */
|
|
10
|
+
digits?: number
|
|
11
|
+
colorize?: boolean
|
|
12
|
+
/** When given, also show `bytes / total` as a percent. */
|
|
13
|
+
total?: number
|
|
14
|
+
mono?: boolean
|
|
15
|
+
}>(),
|
|
16
|
+
{ mono: true, base: 1024 },
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const parts = computed(() => formatBytes(props.bytes, { base: props.base, digits: props.digits }))
|
|
20
|
+
const colorClass = computed(() => (props.colorize ? getBytesColor(props.bytes) : ''))
|
|
21
|
+
const percent = computed(() =>
|
|
22
|
+
props.total && props.total > 0 ? (props.bytes / props.total) * 100 : undefined,
|
|
23
|
+
)
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<span :class="[colorClass, { 'font-mono tabular-nums': mono }]">
|
|
28
|
+
{{ parts[0] }}<span class="text-xs ml-0.5 op-fade">{{ parts[1] }}</span><span v-if="percent != null" class="text-xs ml-1 op-mute">{{ percent.toFixed(0) }}%</span>
|
|
29
|
+
</span>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayDate from './DisplayDate.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayDate',
|
|
6
|
+
component: DisplayDate,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { date: Date.now() }, // required prop; stories supply their own via render
|
|
9
|
+
} satisfies Meta<typeof DisplayDate>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Recent: Story = {
|
|
15
|
+
args: { date: Date.now() - 1000 * 60 * 5 },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Old: Story = {
|
|
19
|
+
args: { date: Date.now() - 1000 * 60 * 60 * 24 * 400 },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const Colorized: Story = {
|
|
23
|
+
render: () => ({
|
|
24
|
+
components: { DisplayDate },
|
|
25
|
+
setup() {
|
|
26
|
+
const now = Date.now()
|
|
27
|
+
return {
|
|
28
|
+
recent: now - 1000 * 60 * 5,
|
|
29
|
+
old: now - 1000 * 60 * 60 * 24 * 400,
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
template: `<div class="flex flex-col gap-1">
|
|
33
|
+
<DisplayDate :date="recent" colorize />
|
|
34
|
+
<DisplayDate :date="old" colorize />
|
|
35
|
+
</div>`,
|
|
36
|
+
}),
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useNow } from '@vueuse/core'
|
|
3
|
+
import { vTooltip } from 'floating-vue'
|
|
4
|
+
import { computed } from 'vue'
|
|
5
|
+
import { formatDateTime, formatTimeAgo, getAgeColor } from '../../utils/format'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
date: number | string | Date
|
|
10
|
+
/** Tint by age (freshness scale). */
|
|
11
|
+
colorize?: boolean
|
|
12
|
+
/** Update the relative label over time. */
|
|
13
|
+
live?: boolean
|
|
14
|
+
}>(),
|
|
15
|
+
{},
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const now = useNow({ interval: 30_000 })
|
|
19
|
+
const time = computed(() => new Date(props.date).getTime())
|
|
20
|
+
const relative = computed(() => formatTimeAgo(time.value, props.live ? now.value.getTime() : Date.now()))
|
|
21
|
+
const exact = computed(() => formatDateTime(time.value))
|
|
22
|
+
const colorClass = computed(() => (props.colorize ? getAgeColor(Math.abs(now.value.getTime() - time.value)) : ''))
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<time v-tooltip="exact" :datetime="new Date(time).toISOString()" :class="colorClass">
|
|
27
|
+
<slot :relative="relative" :exact="exact">{{ relative }}</slot>
|
|
28
|
+
</time>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayDonut from './DisplayDonut.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayDonut',
|
|
6
|
+
component: DisplayDonut,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { value: 0.6, size: 48, thickness: 4 },
|
|
9
|
+
} satisfies Meta<typeof DisplayDonut>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = { args: { value: 0.6 } }
|
|
15
|
+
export const CustomColor: Story = { args: { value: 0.75, size: 64, color: '#3178c6' } }
|
|
16
|
+
|
|
17
|
+
export const Sizes: Story = {
|
|
18
|
+
render: () => ({
|
|
19
|
+
components: { DisplayDonut },
|
|
20
|
+
template: `<div class="flex items-center gap-4">
|
|
21
|
+
<DisplayDonut :value="0.25" />
|
|
22
|
+
<DisplayDonut :value="0.6" :size="48" />
|
|
23
|
+
<DisplayDonut :value="0.9" :size="64" :thickness="6" />
|
|
24
|
+
</div>`,
|
|
25
|
+
}),
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { clamp } from '@antfu/utils'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
/** Progress 0..1. */
|
|
8
|
+
value: number
|
|
9
|
+
size?: number
|
|
10
|
+
thickness?: number
|
|
11
|
+
/** Override the foreground color (defaults to the active token). */
|
|
12
|
+
color?: string
|
|
13
|
+
}>(),
|
|
14
|
+
{ size: 36, thickness: 4 },
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const radius = computed(() => (props.size - props.thickness) / 2)
|
|
18
|
+
const circumference = computed(() => 2 * Math.PI * radius.value)
|
|
19
|
+
const offset = computed(() => circumference.value * (1 - clamp(props.value, 0, 1)))
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<svg :width="size" :height="size" :viewBox="`0 0 ${size} ${size}`" class="-rotate-90">
|
|
24
|
+
<circle
|
|
25
|
+
:cx="size / 2"
|
|
26
|
+
:cy="size / 2"
|
|
27
|
+
:r="radius"
|
|
28
|
+
fill="none"
|
|
29
|
+
:stroke-width="thickness"
|
|
30
|
+
class="op-mute stroke-current"
|
|
31
|
+
/>
|
|
32
|
+
<circle
|
|
33
|
+
:cx="size / 2"
|
|
34
|
+
:cy="size / 2"
|
|
35
|
+
:r="radius"
|
|
36
|
+
fill="none"
|
|
37
|
+
:stroke-width="thickness"
|
|
38
|
+
:stroke-dasharray="circumference"
|
|
39
|
+
:stroke-dashoffset="offset"
|
|
40
|
+
stroke-linecap="round"
|
|
41
|
+
class="transition-all stroke-current"
|
|
42
|
+
:class="{ 'color-active': !color }"
|
|
43
|
+
:style="color ? { color } : undefined"
|
|
44
|
+
/>
|
|
45
|
+
</svg>
|
|
46
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayDuration from './DisplayDuration.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayDuration',
|
|
6
|
+
component: DisplayDuration,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { ms: 750 },
|
|
9
|
+
} satisfies Meta<typeof DisplayDuration>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = { args: { ms: 750 } }
|
|
15
|
+
export const Colorized: Story = { args: { ms: 3200, colorize: true } }
|
|
16
|
+
|
|
17
|
+
export const Scale: Story = {
|
|
18
|
+
render: () => ({
|
|
19
|
+
components: { DisplayDuration },
|
|
20
|
+
template: `<div class="flex flex-col gap-1">
|
|
21
|
+
<DisplayDuration :ms="12" colorize />
|
|
22
|
+
<DisplayDuration :ms="180" colorize />
|
|
23
|
+
<DisplayDuration :ms="750" colorize />
|
|
24
|
+
<DisplayDuration :ms="3200" colorize />
|
|
25
|
+
<DisplayDuration :ms="9000" colorize />
|
|
26
|
+
</div>`,
|
|
27
|
+
}),
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DurationUnit } from '../../utils/format'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { formatDuration, getDurationColor } from '../../utils/format'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(
|
|
7
|
+
defineProps<{
|
|
8
|
+
/** Duration value, interpreted in `unit`. */
|
|
9
|
+
ms: number
|
|
10
|
+
/** Input unit: `'ns'`, `'us'`, `'ms'` (default) or `'s'`. */
|
|
11
|
+
unit?: DurationUnit
|
|
12
|
+
/** Tint by severity threshold. */
|
|
13
|
+
colorize?: boolean
|
|
14
|
+
mono?: boolean
|
|
15
|
+
}>(),
|
|
16
|
+
{ mono: true, unit: 'ms' },
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const FACTOR_MS: Record<DurationUnit, number> = { ns: 1e-6, us: 1e-3, ms: 1, s: 1000 }
|
|
20
|
+
const parts = computed(() => formatDuration(props.ms, { unit: props.unit }))
|
|
21
|
+
const colorClass = computed(() => (props.colorize ? getDurationColor(props.ms * FACTOR_MS[props.unit]) : ''))
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<span :class="[colorClass, { 'font-mono tabular-nums': mono }]">
|
|
26
|
+
{{ parts[0] }}<span class="text-xs ml-0.5 op-fade">{{ parts[1] }}</span>
|
|
27
|
+
</span>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayFileIcon from './DisplayFileIcon.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayFileIcon',
|
|
6
|
+
component: DisplayFileIcon,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { path: 'a.vue' },
|
|
9
|
+
} satisfies Meta<typeof DisplayFileIcon>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = { args: { path: 'component.vue' } }
|
|
15
|
+
|
|
16
|
+
export const ByType: Story = {
|
|
17
|
+
render: () => ({
|
|
18
|
+
components: { DisplayFileIcon },
|
|
19
|
+
template: `<div class="text-xl flex items-center gap-2">
|
|
20
|
+
<DisplayFileIcon path="a.vue" />
|
|
21
|
+
<DisplayFileIcon path="b.ts" />
|
|
22
|
+
<DisplayFileIcon path="c.json" />
|
|
23
|
+
<DisplayFileIcon path="d.css" />
|
|
24
|
+
<DisplayFileIcon path="e.md" />
|
|
25
|
+
</div>`,
|
|
26
|
+
}),
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { FileIconRule } from '../../utils/icon'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { getFileType, getFolderIcon } from '../../utils/icon'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
/** File path or module id (or folder name when `directory`). */
|
|
8
|
+
path: string
|
|
9
|
+
/** Override the default (catppuccin) file rule list — e.g. `vscodeFileIconRules`. */
|
|
10
|
+
rules?: FileIconRule[]
|
|
11
|
+
/** Render a folder icon instead of a file icon. */
|
|
12
|
+
directory?: boolean
|
|
13
|
+
/** For directories: show the open-folder glyph. */
|
|
14
|
+
open?: boolean
|
|
15
|
+
/** Named-folder lookup for directories. */
|
|
16
|
+
folderRules?: Record<string, string>
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const type = computed(() => {
|
|
20
|
+
if (props.directory) {
|
|
21
|
+
const name = props.path.replace(/\/+$/, '').split('/').pop() || 'folder'
|
|
22
|
+
return { name, icon: getFolderIcon(name, props.open, props.folderRules) }
|
|
23
|
+
}
|
|
24
|
+
return getFileType(props.path, props.rules)
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<span :class="type.icon" :title="type.name" aria-hidden="true" />
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import DisplayFilePath from './DisplayFilePath.vue'
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Display/DisplayFilePath',
|
|
6
|
+
component: DisplayFilePath,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
args: { path: '/project/src/components/Badge.vue', root: '/project' },
|
|
9
|
+
} satisfies Meta<typeof DisplayFilePath>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof meta>
|
|
13
|
+
|
|
14
|
+
export const WithRoot: Story = {
|
|
15
|
+
args: { path: '/project/src/components/Badge.vue', root: '/project' },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Module: Story = {
|
|
19
|
+
args: { path: '/project/node_modules/.pnpm/vue@3.5.0/node_modules/vue/dist/vue.mjs' },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const Examples: Story = {
|
|
23
|
+
render: () => ({
|
|
24
|
+
components: { DisplayFilePath },
|
|
25
|
+
template: `<div class="flex flex-col gap-2 max-w-md">
|
|
26
|
+
<DisplayFilePath path="/project/src/components/Badge.vue" root="/project" />
|
|
27
|
+
<DisplayFilePath path="/project/node_modules/.pnpm/vue@3.5.0/node_modules/vue/dist/vue.mjs" />
|
|
28
|
+
</div>`,
|
|
29
|
+
}),
|
|
30
|
+
}
|