@hina-ui/vue 1.7.2 → 1.7.3
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/dist/index.js +1899 -493
- package/dist/index.js.map +1 -1
- package/dist/types/components/navigation-menu/NavigationMenu.vue.d.ts +33 -0
- package/dist/types/components/navigation-menu/NavigationMenu.vue.d.ts.map +1 -0
- package/dist/types/components/navigation-menu/NavigationMenuContent.vue.d.ts +29 -0
- package/dist/types/components/navigation-menu/NavigationMenuContent.vue.d.ts.map +1 -0
- package/dist/types/components/navigation-menu/NavigationMenuItem.vue.d.ts +16 -0
- package/dist/types/components/navigation-menu/NavigationMenuItem.vue.d.ts.map +1 -0
- package/dist/types/components/navigation-menu/NavigationMenuLink.vue.d.ts +33 -0
- package/dist/types/components/navigation-menu/NavigationMenuLink.vue.d.ts.map +1 -0
- package/dist/types/components/navigation-menu/NavigationMenuTrigger.vue.d.ts +19 -0
- package/dist/types/components/navigation-menu/NavigationMenuTrigger.vue.d.ts.map +1 -0
- package/dist/types/components/navigation-menu/composables/useNavigationMenuKeyboard.d.ts +4 -0
- package/dist/types/components/navigation-menu/composables/useNavigationMenuKeyboard.d.ts.map +1 -0
- package/dist/types/components/navigation-menu/composables/useNavigationMenuLayout.d.ts +12 -0
- package/dist/types/components/navigation-menu/composables/useNavigationMenuLayout.d.ts.map +1 -0
- package/dist/types/components/navigation-menu/context.d.ts +10 -0
- package/dist/types/components/navigation-menu/context.d.ts.map +1 -0
- package/dist/types/components/navigation-menu/navigation-menu.variants.d.ts +112 -0
- package/dist/types/components/navigation-menu/navigation-menu.variants.d.ts.map +1 -0
- package/dist/types/components/navigation-menu/types.d.ts +17 -0
- package/dist/types/components/navigation-menu/types.d.ts.map +1 -0
- package/dist/types/components/rating/Rating.vue.d.ts +1 -0
- package/dist/types/components/rating/Rating.vue.d.ts.map +1 -1
- package/dist/types/components/rating/composables/useRatingScale.d.ts +8 -0
- package/dist/types/components/rating/composables/useRatingScale.d.ts.map +1 -0
- package/dist/types/components/stepper/Stepper.vue.d.ts +36 -0
- package/dist/types/components/stepper/Stepper.vue.d.ts.map +1 -0
- package/dist/types/components/stepper/StepperStep.vue.d.ts +33 -0
- package/dist/types/components/stepper/StepperStep.vue.d.ts.map +1 -0
- package/dist/types/components/stepper/composables/useStepper.d.ts +13 -0
- package/dist/types/components/stepper/composables/useStepper.d.ts.map +1 -0
- package/dist/types/components/stepper/stepper.variants.d.ts +58 -0
- package/dist/types/components/stepper/stepper.variants.d.ts.map +1 -0
- package/dist/types/components/stepper/types.d.ts +45 -0
- package/dist/types/components/stepper/types.d.ts.map +1 -0
- package/dist/types/components/timeline/Timeline.vue.d.ts +36 -0
- package/dist/types/components/timeline/Timeline.vue.d.ts.map +1 -0
- package/dist/types/components/timeline/timeline.variants.d.ts +51 -0
- package/dist/types/components/timeline/timeline.variants.d.ts.map +1 -0
- package/dist/types/components/timeline/types.d.ts +18 -0
- package/dist/types/components/timeline/types.d.ts.map +1 -0
- package/dist/types/components/toolbar/Toolbar.vue.d.ts +20 -0
- package/dist/types/components/toolbar/Toolbar.vue.d.ts.map +1 -0
- package/dist/types/components/toolbar/ToolbarButton.vue.d.ts +18 -0
- package/dist/types/components/toolbar/ToolbarButton.vue.d.ts.map +1 -0
- package/dist/types/components/toolbar/ToolbarControl.vue.d.ts +22 -0
- package/dist/types/components/toolbar/ToolbarControl.vue.d.ts.map +1 -0
- package/dist/types/components/toolbar/ToolbarLink.vue.d.ts +24 -0
- package/dist/types/components/toolbar/ToolbarLink.vue.d.ts.map +1 -0
- package/dist/types/components/toolbar/ToolbarSeparator.vue.d.ts +10 -0
- package/dist/types/components/toolbar/ToolbarSeparator.vue.d.ts.map +1 -0
- package/dist/types/components/toolbar/ToolbarToggleGroup.vue.d.ts +31 -0
- package/dist/types/components/toolbar/ToolbarToggleGroup.vue.d.ts.map +1 -0
- package/dist/types/components/toolbar/ToolbarToggleItem.vue.d.ts +21 -0
- package/dist/types/components/toolbar/ToolbarToggleItem.vue.d.ts.map +1 -0
- package/dist/types/components/toolbar/context.d.ts +13 -0
- package/dist/types/components/toolbar/context.d.ts.map +1 -0
- package/dist/types/components/toolbar/toolbar.variants.d.ts +64 -0
- package/dist/types/components/toolbar/toolbar.variants.d.ts.map +1 -0
- package/dist/types/components/toolbar/types.d.ts +28 -0
- package/dist/types/components/toolbar/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/locale/en-US.d.ts.map +1 -1
- package/dist/types/locale/index.d.ts.map +1 -1
- package/dist/types/locale/types.d.ts +7 -0
- package/dist/types/locale/types.d.ts.map +1 -1
- package/dist/types/locale/zh-CN.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/navigation-menu/NavigationMenu.vue +84 -0
- package/src/components/navigation-menu/NavigationMenuContent.vue +25 -0
- package/src/components/navigation-menu/NavigationMenuItem.vue +13 -0
- package/src/components/navigation-menu/NavigationMenuLink.vue +82 -0
- package/src/components/navigation-menu/NavigationMenuTrigger.vue +44 -0
- package/src/components/navigation-menu/composables/useNavigationMenuKeyboard.ts +33 -0
- package/src/components/navigation-menu/composables/useNavigationMenuLayout.ts +72 -0
- package/src/components/navigation-menu/context.ts +19 -0
- package/src/components/navigation-menu/navigation-menu.variants.ts +82 -0
- package/src/components/navigation-menu/types.ts +17 -0
- package/src/components/rating/Rating.vue +22 -8
- package/src/components/rating/composables/useRatingScale.ts +24 -0
- package/src/components/sidebar/Sidebar.vue +1 -1
- package/src/components/stepper/Stepper.vue +95 -0
- package/src/components/stepper/StepperStep.vue +102 -0
- package/src/components/stepper/composables/useStepper.ts +112 -0
- package/src/components/stepper/stepper.variants.ts +39 -0
- package/src/components/stepper/types.ts +51 -0
- package/src/components/timeline/Timeline.vue +109 -0
- package/src/components/timeline/timeline.variants.ts +31 -0
- package/src/components/timeline/types.ts +20 -0
- package/src/components/toolbar/Toolbar.vue +45 -0
- package/src/components/toolbar/ToolbarButton.vue +26 -0
- package/src/components/toolbar/ToolbarControl.vue +46 -0
- package/src/components/toolbar/ToolbarLink.vue +40 -0
- package/src/components/toolbar/ToolbarSeparator.vue +20 -0
- package/src/components/toolbar/ToolbarToggleGroup.vue +42 -0
- package/src/components/toolbar/ToolbarToggleItem.vue +34 -0
- package/src/components/toolbar/context.ts +29 -0
- package/src/components/toolbar/toolbar.variants.ts +37 -0
- package/src/components/toolbar/types.ts +30 -0
- package/src/index.ts +45 -0
- package/src/locale/en-US.ts +7 -0
- package/src/locale/index.ts +1 -0
- package/src/locale/types.ts +7 -0
- package/src/locale/zh-CN.ts +7 -0
- package/src/styles/interaction.css +1 -0
- package/src/styles/motion.css +66 -0
- package/src/styles/stepper.css +41 -0
- package/src/styles/timeline.css +144 -0
- package/src/styles/tokens.css +2 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends StepperItem = StepperItem">
|
|
2
|
+
import { StepperRoot } from 'reka-ui'
|
|
3
|
+
import { useId } from 'vue'
|
|
4
|
+
import { cn } from '../../lib/cn'
|
|
5
|
+
import { useDirection } from '../../lib/useDirection'
|
|
6
|
+
import { useUiLocale } from '../../locale'
|
|
7
|
+
import StepperStep from './StepperStep.vue'
|
|
8
|
+
import { stepper } from './stepper.variants'
|
|
9
|
+
import { useStepper } from './composables/useStepper'
|
|
10
|
+
import type { StepperItem, StepperNavigation, StepperProps, StepperSlotProps } from './types'
|
|
11
|
+
|
|
12
|
+
defineOptions({ name: 'HnStepper' })
|
|
13
|
+
const props = withDefaults(defineProps<StepperProps<T>>(), {
|
|
14
|
+
defaultValue: 1,
|
|
15
|
+
orientation: 'horizontal',
|
|
16
|
+
linear: true,
|
|
17
|
+
})
|
|
18
|
+
const emit = defineEmits<{ 'update:modelValue': [step: number]; error: [error: unknown] }>()
|
|
19
|
+
const slots = defineSlots<{
|
|
20
|
+
default?(props: StepperNavigation<T>): unknown
|
|
21
|
+
indicator?(props: StepperSlotProps<T>): unknown
|
|
22
|
+
title?(props: StepperSlotProps<T>): unknown
|
|
23
|
+
description?(props: StepperSlotProps<T>): unknown
|
|
24
|
+
}>()
|
|
25
|
+
const t = useUiLocale()
|
|
26
|
+
const id = useId()
|
|
27
|
+
const { root, direction, rootDirection } = useDirection(() => props.dir)
|
|
28
|
+
const { step, pending, entries, navigation, next, prev, goTo, canNext, canPrev } = useStepper(
|
|
29
|
+
props,
|
|
30
|
+
value => emit('update:modelValue', value),
|
|
31
|
+
error => emit('error', error),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
defineExpose({ step, pending, next, prev, goTo, canNext, canPrev })
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<div
|
|
39
|
+
ref="root"
|
|
40
|
+
data-hn-stepper
|
|
41
|
+
:dir="rootDirection"
|
|
42
|
+
:class="cn(stepper({ size: props.size }), props.class)"
|
|
43
|
+
:aria-busy="pending || undefined"
|
|
44
|
+
>
|
|
45
|
+
<StepperRoot
|
|
46
|
+
:model-value="step"
|
|
47
|
+
:orientation="props.orientation"
|
|
48
|
+
:dir="direction"
|
|
49
|
+
:linear="props.linear"
|
|
50
|
+
:aria-label="props.label ?? t.stepper.label"
|
|
51
|
+
class="min-w-0 [&>[role=status]]:hidden"
|
|
52
|
+
@update:model-value="value => value !== undefined && goTo(value)"
|
|
53
|
+
>
|
|
54
|
+
<ol
|
|
55
|
+
role="list"
|
|
56
|
+
class="hn-stepper-list m-0 grid min-w-0 list-none p-0"
|
|
57
|
+
:data-orientation="props.orientation"
|
|
58
|
+
>
|
|
59
|
+
<StepperStep
|
|
60
|
+
v-for="entry in entries"
|
|
61
|
+
:id="`${id}-${entry.step}`"
|
|
62
|
+
:key="entry.step"
|
|
63
|
+
:entry="entry"
|
|
64
|
+
:orientation="props.orientation"
|
|
65
|
+
:pending="pending"
|
|
66
|
+
:last="entry.step === entries.length"
|
|
67
|
+
:panel-id="slots.default ? `${id}-panel` : undefined"
|
|
68
|
+
@select="goTo"
|
|
69
|
+
>
|
|
70
|
+
<template v-if="slots.indicator" #indicator="scope">
|
|
71
|
+
<slot name="indicator" v-bind="scope" />
|
|
72
|
+
</template>
|
|
73
|
+
<template v-if="slots.title" #title="scope">
|
|
74
|
+
<slot name="title" v-bind="scope" />
|
|
75
|
+
</template>
|
|
76
|
+
<template v-if="slots.description" #description="scope">
|
|
77
|
+
<slot name="description" v-bind="scope" />
|
|
78
|
+
</template>
|
|
79
|
+
</StepperStep>
|
|
80
|
+
</ol>
|
|
81
|
+
</StepperRoot>
|
|
82
|
+
<div
|
|
83
|
+
v-if="slots.default"
|
|
84
|
+
:id="`${id}-panel`"
|
|
85
|
+
:role="step ? 'region' : undefined"
|
|
86
|
+
:aria-labelledby="step ? `${id}-${step}-title` : undefined"
|
|
87
|
+
class="min-w-0"
|
|
88
|
+
>
|
|
89
|
+
<slot v-bind="navigation" />
|
|
90
|
+
</div>
|
|
91
|
+
<span v-if="entries.length" class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
|
92
|
+
{{ t.stepper.progress(step, entries.length) }}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends StepperItem = StepperItem">
|
|
2
|
+
import { CircleAlert, Check } from '@lucide/vue'
|
|
3
|
+
import {
|
|
4
|
+
StepperItem as RekaStepperItem,
|
|
5
|
+
StepperTrigger,
|
|
6
|
+
StepperTitle,
|
|
7
|
+
StepperDescription,
|
|
8
|
+
StepperIndicator,
|
|
9
|
+
StepperSeparator,
|
|
10
|
+
} from 'reka-ui'
|
|
11
|
+
import { useUiLocale } from '../../locale'
|
|
12
|
+
import Spinner from '../spinner/Spinner.vue'
|
|
13
|
+
import { stepperIndicator, stepperTrigger } from './stepper.variants'
|
|
14
|
+
import type { StepperItem, StepperSlotProps, StepperOrientation } from './types'
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
entry: StepperSlotProps<T>
|
|
18
|
+
id: string
|
|
19
|
+
panelId?: string
|
|
20
|
+
pending: boolean
|
|
21
|
+
last: boolean
|
|
22
|
+
orientation: StepperOrientation
|
|
23
|
+
}>()
|
|
24
|
+
const emit = defineEmits<{ select: [step: number] }>()
|
|
25
|
+
const slots = defineSlots<{
|
|
26
|
+
indicator?(props: StepperSlotProps<T>): unknown
|
|
27
|
+
title?(props: StepperSlotProps<T>): unknown
|
|
28
|
+
description?(props: StepperSlotProps<T>): unknown
|
|
29
|
+
}>()
|
|
30
|
+
const t = useUiLocale()
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<RekaStepperItem
|
|
35
|
+
as="li"
|
|
36
|
+
:step="entry.step"
|
|
37
|
+
:disabled="entry.disabled"
|
|
38
|
+
:completed="entry.state === 'completed'"
|
|
39
|
+
:aria-current="undefined"
|
|
40
|
+
:data-hn-state="entry.state"
|
|
41
|
+
class="hn-stepper-item min-w-0"
|
|
42
|
+
>
|
|
43
|
+
<StepperTrigger
|
|
44
|
+
:id="`${id}-trigger`"
|
|
45
|
+
:aria-labelledby="`${id}-title`"
|
|
46
|
+
:aria-describedby="
|
|
47
|
+
[
|
|
48
|
+
entry.item.description || slots.description ? `${id}-description` : undefined,
|
|
49
|
+
entry.state === 'error' || entry.state === 'completed' ? `${id}-status` : undefined,
|
|
50
|
+
]
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.join(' ') || undefined
|
|
53
|
+
"
|
|
54
|
+
:aria-controls="panelId"
|
|
55
|
+
:aria-current="entry.active ? 'step' : undefined"
|
|
56
|
+
:aria-disabled="props.pending || undefined"
|
|
57
|
+
:data-disabled="entry.disabled || props.pending ? '' : undefined"
|
|
58
|
+
:class="stepperTrigger({ orientation: props.orientation })"
|
|
59
|
+
@mousedown.capture.stop
|
|
60
|
+
@click="emit('select', entry.step)"
|
|
61
|
+
>
|
|
62
|
+
<StepperIndicator :class="stepperIndicator({ state: entry.state })" aria-hidden="true">
|
|
63
|
+
<slot name="indicator" v-bind="entry">
|
|
64
|
+
<Spinner v-if="props.pending && entry.active" size="sm" />
|
|
65
|
+
<CircleAlert v-else-if="entry.state === 'error'" />
|
|
66
|
+
<Check v-else-if="entry.state === 'completed'" />
|
|
67
|
+
<template v-else>{{ entry.step }}</template>
|
|
68
|
+
</slot>
|
|
69
|
+
</StepperIndicator>
|
|
70
|
+
<span class="flex min-w-0 flex-1 flex-col justify-center gap-1 self-stretch wrap-anywhere">
|
|
71
|
+
<StepperTitle
|
|
72
|
+
as="span"
|
|
73
|
+
:id="`${id}-title`"
|
|
74
|
+
class="font-medium"
|
|
75
|
+
:class="entry.state === 'error' ? 'text-danger-text' : undefined"
|
|
76
|
+
>
|
|
77
|
+
<slot name="title" v-bind="entry">{{ entry.item.title }}</slot>
|
|
78
|
+
</StepperTitle>
|
|
79
|
+
<StepperDescription
|
|
80
|
+
v-if="entry.item.description || slots.description"
|
|
81
|
+
as="span"
|
|
82
|
+
:id="`${id}-description`"
|
|
83
|
+
class="text-muted text-xs"
|
|
84
|
+
>
|
|
85
|
+
<slot name="description" v-bind="entry">{{ entry.item.description }}</slot>
|
|
86
|
+
</StepperDescription>
|
|
87
|
+
<span
|
|
88
|
+
v-if="entry.state === 'error' || entry.state === 'completed'"
|
|
89
|
+
:id="`${id}-status`"
|
|
90
|
+
class="sr-only"
|
|
91
|
+
>
|
|
92
|
+
{{ entry.state === 'error' ? t.stepper.error : t.stepper.completed }}
|
|
93
|
+
</span>
|
|
94
|
+
</span>
|
|
95
|
+
</StepperTrigger>
|
|
96
|
+
<StepperSeparator
|
|
97
|
+
v-if="!props.last"
|
|
98
|
+
class="hn-stepper-separator hn-transition-base"
|
|
99
|
+
:class="entry.state === 'completed' ? 'bg-accent' : 'bg-line'"
|
|
100
|
+
/>
|
|
101
|
+
</RekaStepperItem>
|
|
102
|
+
</template>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useVModel } from '@vueuse/core'
|
|
2
|
+
import { computed, onScopeDispose, ref, watch } from 'vue'
|
|
3
|
+
import type { StepperItem, StepperNavigation, StepperProps, StepperSlotProps } from '../types'
|
|
4
|
+
|
|
5
|
+
export function useStepper<T extends StepperItem>(
|
|
6
|
+
props: StepperProps<T>,
|
|
7
|
+
update: (step: number) => void,
|
|
8
|
+
onError: (error: unknown) => void,
|
|
9
|
+
) {
|
|
10
|
+
const model = useVModel(props, 'modelValue', (_, value) => update(value!), {
|
|
11
|
+
defaultValue: props.defaultValue ?? 1,
|
|
12
|
+
passive: (props.modelValue === undefined) as false,
|
|
13
|
+
})
|
|
14
|
+
const pending = ref(false)
|
|
15
|
+
const total = computed(() => props.items.length)
|
|
16
|
+
const step = computed(() =>
|
|
17
|
+
total.value
|
|
18
|
+
? Math.min(
|
|
19
|
+
total.value,
|
|
20
|
+
Math.max(1, Math.floor(Number.isFinite(model.value) ? model.value! : 1)),
|
|
21
|
+
)
|
|
22
|
+
: 0,
|
|
23
|
+
)
|
|
24
|
+
let version = 0
|
|
25
|
+
|
|
26
|
+
function invalidate() {
|
|
27
|
+
version++
|
|
28
|
+
pending.value = false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
watch(
|
|
32
|
+
[
|
|
33
|
+
model,
|
|
34
|
+
() => JSON.stringify(props.items.map(item => [item.title, !!item.disabled])),
|
|
35
|
+
() => props.disabled,
|
|
36
|
+
() => props.linear,
|
|
37
|
+
() => props.beforeChange,
|
|
38
|
+
],
|
|
39
|
+
invalidate,
|
|
40
|
+
{ flush: 'sync' },
|
|
41
|
+
)
|
|
42
|
+
onScopeDispose(invalidate)
|
|
43
|
+
|
|
44
|
+
function available(target: number) {
|
|
45
|
+
return (
|
|
46
|
+
Number.isInteger(target) &&
|
|
47
|
+
target >= 1 &&
|
|
48
|
+
target <= total.value &&
|
|
49
|
+
!props.disabled &&
|
|
50
|
+
!props.items[target - 1]?.disabled &&
|
|
51
|
+
(props.linear === false || target <= step.value + 1)
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const canNext = computed(() => !pending.value && step.value > 0 && available(step.value + 1))
|
|
56
|
+
const canPrev = computed(() => !pending.value && available(step.value - 1))
|
|
57
|
+
|
|
58
|
+
async function goTo(target: number) {
|
|
59
|
+
if (pending.value || !available(target) || target === step.value) return false
|
|
60
|
+
const ticket = ++version
|
|
61
|
+
const from = step.value
|
|
62
|
+
try {
|
|
63
|
+
if (props.beforeChange) {
|
|
64
|
+
pending.value = true
|
|
65
|
+
const allowed = await props.beforeChange(target, from)
|
|
66
|
+
if (allowed === false) return false
|
|
67
|
+
}
|
|
68
|
+
if (ticket !== version || from !== step.value || !available(target)) return false
|
|
69
|
+
pending.value = false
|
|
70
|
+
model.value = target
|
|
71
|
+
return true
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (ticket === version) onError(error)
|
|
74
|
+
return false
|
|
75
|
+
} finally {
|
|
76
|
+
if (ticket === version) pending.value = false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const next = () => goTo(step.value + 1)
|
|
81
|
+
const prev = () => goTo(step.value - 1)
|
|
82
|
+
const entries = computed<StepperSlotProps<T>[]>(() =>
|
|
83
|
+
props.items.map((item, index) => ({
|
|
84
|
+
item,
|
|
85
|
+
index,
|
|
86
|
+
step: index + 1,
|
|
87
|
+
active: step.value === index + 1,
|
|
88
|
+
pending: pending.value && step.value === index + 1,
|
|
89
|
+
disabled: !available(index + 1),
|
|
90
|
+
state: item.error
|
|
91
|
+
? 'error'
|
|
92
|
+
: item.completed || index + 1 < step.value
|
|
93
|
+
? 'completed'
|
|
94
|
+
: index + 1 === step.value
|
|
95
|
+
? 'active'
|
|
96
|
+
: 'inactive',
|
|
97
|
+
})),
|
|
98
|
+
)
|
|
99
|
+
const navigation = computed<StepperNavigation<T>>(() => ({
|
|
100
|
+
step: step.value,
|
|
101
|
+
item: props.items[step.value - 1],
|
|
102
|
+
total: total.value,
|
|
103
|
+
pending: pending.value,
|
|
104
|
+
canNext: canNext.value,
|
|
105
|
+
canPrev: canPrev.value,
|
|
106
|
+
next,
|
|
107
|
+
prev,
|
|
108
|
+
goTo,
|
|
109
|
+
}))
|
|
110
|
+
|
|
111
|
+
return { step, pending, entries, navigation, next, prev, goTo, canNext, canPrev }
|
|
112
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { tv } from '../../lib/tv'
|
|
2
|
+
|
|
3
|
+
export const stepper = tv({
|
|
4
|
+
base: 'flex w-full min-w-0 flex-col gap-(--hn-stack-gap)',
|
|
5
|
+
variants: {
|
|
6
|
+
size: {
|
|
7
|
+
sm: 'text-sm [--hn-stepper-indicator:var(--hn-control-h-sm)]',
|
|
8
|
+
md: 'text-sm [--hn-stepper-indicator:var(--hn-control-h-md)]',
|
|
9
|
+
lg: 'text-base [--hn-stepper-indicator:var(--hn-control-h-lg)]',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: { size: 'md' },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const stepperTrigger = tv({
|
|
16
|
+
base: [
|
|
17
|
+
'hn-focus-ring cursor-pointer text-fg relative flex min-w-0 gap-(--hn-inline-gap) rounded-md p-1',
|
|
18
|
+
'disabled:cursor-not-allowed disabled:opacity-50 data-disabled:cursor-not-allowed',
|
|
19
|
+
],
|
|
20
|
+
variants: {
|
|
21
|
+
orientation: {
|
|
22
|
+
horizontal: 'w-full flex-col items-center text-center',
|
|
23
|
+
vertical: 'items-start text-start',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const stepperIndicator = tv({
|
|
29
|
+
base: 'hn-transition-base flex size-(--hn-stepper-indicator) shrink-0 items-center justify-center rounded-full border text-sm font-medium tabular-nums [&_svg]:size-[1em]',
|
|
30
|
+
variants: {
|
|
31
|
+
state: {
|
|
32
|
+
inactive: 'border-line text-muted bg-surface',
|
|
33
|
+
active: 'border-accent bg-accent text-accent-on',
|
|
34
|
+
completed: 'border-transparent bg-accent-soft text-accent-text',
|
|
35
|
+
error: 'border-danger bg-danger-soft text-danger-text',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: { state: 'inactive' },
|
|
39
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type StepperOrientation = 'horizontal' | 'vertical'
|
|
2
|
+
export type StepperSize = 'sm' | 'md' | 'lg'
|
|
3
|
+
export type StepperState = 'inactive' | 'active' | 'completed' | 'error'
|
|
4
|
+
export type StepperBeforeChange = (
|
|
5
|
+
step: number,
|
|
6
|
+
previousStep: number,
|
|
7
|
+
) => boolean | void | Promise<boolean | void>
|
|
8
|
+
|
|
9
|
+
export interface StepperItem {
|
|
10
|
+
title: string
|
|
11
|
+
description?: string
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
completed?: boolean
|
|
14
|
+
error?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StepperProps<T extends StepperItem = StepperItem> {
|
|
18
|
+
items: T[]
|
|
19
|
+
modelValue?: number
|
|
20
|
+
defaultValue?: number
|
|
21
|
+
orientation?: StepperOrientation
|
|
22
|
+
size?: StepperSize
|
|
23
|
+
linear?: boolean
|
|
24
|
+
disabled?: boolean
|
|
25
|
+
beforeChange?: StepperBeforeChange
|
|
26
|
+
label?: string
|
|
27
|
+
dir?: 'ltr' | 'rtl'
|
|
28
|
+
class?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface StepperSlotProps<T extends StepperItem = StepperItem> {
|
|
32
|
+
item: T
|
|
33
|
+
index: number
|
|
34
|
+
step: number
|
|
35
|
+
state: StepperState
|
|
36
|
+
active: boolean
|
|
37
|
+
pending: boolean
|
|
38
|
+
disabled: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StepperNavigation<T extends StepperItem = StepperItem> {
|
|
42
|
+
step: number
|
|
43
|
+
item: T | undefined
|
|
44
|
+
total: number
|
|
45
|
+
pending: boolean
|
|
46
|
+
canNext: boolean
|
|
47
|
+
canPrev: boolean
|
|
48
|
+
next: () => Promise<boolean>
|
|
49
|
+
prev: () => Promise<boolean>
|
|
50
|
+
goTo: (step: number) => Promise<boolean>
|
|
51
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends TimelineItem = TimelineItem">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { cn } from '../../lib/cn'
|
|
4
|
+
import { timeline, timelineMarker } from './timeline.variants'
|
|
5
|
+
import type {
|
|
6
|
+
TimelineAlign,
|
|
7
|
+
TimelineItem,
|
|
8
|
+
TimelineOrientation,
|
|
9
|
+
TimelineSize,
|
|
10
|
+
TimelineSlotProps,
|
|
11
|
+
TimelineTone,
|
|
12
|
+
} from './types'
|
|
13
|
+
|
|
14
|
+
defineOptions({ name: 'HnTimeline' })
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(
|
|
17
|
+
defineProps<{
|
|
18
|
+
items: T[]
|
|
19
|
+
orientation?: TimelineOrientation
|
|
20
|
+
align?: TimelineAlign
|
|
21
|
+
size?: TimelineSize
|
|
22
|
+
tone?: TimelineTone
|
|
23
|
+
timePosition?: 'content' | 'opposite'
|
|
24
|
+
reverse?: boolean
|
|
25
|
+
class?: string
|
|
26
|
+
}>(),
|
|
27
|
+
{ orientation: 'vertical', align: 'start', timePosition: 'content', reverse: false },
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const slots = defineSlots<{
|
|
31
|
+
marker?(props: TimelineSlotProps<T>): unknown
|
|
32
|
+
title?(props: TimelineSlotProps<T>): unknown
|
|
33
|
+
description?(props: TimelineSlotProps<T>): unknown
|
|
34
|
+
time?(props: TimelineSlotProps<T>): unknown
|
|
35
|
+
content?(props: TimelineSlotProps<T>): unknown
|
|
36
|
+
opposite?(props: TimelineSlotProps<T>): unknown
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
const entries = computed(() => (props.reverse ? [...props.items].reverse() : props.items))
|
|
40
|
+
function hasOpposite() {
|
|
41
|
+
return (
|
|
42
|
+
!!slots.opposite ||
|
|
43
|
+
(props.timePosition === 'opposite' && (!!slots.time || props.items.some(item => item.time)))
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<ol
|
|
50
|
+
data-hn-timeline
|
|
51
|
+
role="list"
|
|
52
|
+
:data-orientation="props.orientation"
|
|
53
|
+
:data-align="props.align"
|
|
54
|
+
:data-opposite="hasOpposite() || undefined"
|
|
55
|
+
:class="cn(timeline({ size: props.size }), props.class)"
|
|
56
|
+
>
|
|
57
|
+
<li
|
|
58
|
+
v-for="(item, index) in entries"
|
|
59
|
+
:key="item.id ?? index"
|
|
60
|
+
class="hn-timeline-item min-w-0"
|
|
61
|
+
:data-side="
|
|
62
|
+
props.align === 'end' || (props.align === 'alternate' && index % 2 === 1) ? 'end' : 'start'
|
|
63
|
+
"
|
|
64
|
+
>
|
|
65
|
+
<div v-if="hasOpposite()" class="hn-timeline-opposite text-muted min-w-0 wrap-anywhere">
|
|
66
|
+
<slot name="opposite" :item="item" :index="index">
|
|
67
|
+
<slot v-if="props.timePosition === 'opposite'" name="time" :item="item" :index="index">
|
|
68
|
+
<component
|
|
69
|
+
:is="item.dateTime ? 'time' : 'span'"
|
|
70
|
+
v-if="item.time"
|
|
71
|
+
:datetime="item.dateTime"
|
|
72
|
+
class="text-xs tabular-nums"
|
|
73
|
+
>
|
|
74
|
+
{{ item.time }}
|
|
75
|
+
</component>
|
|
76
|
+
</slot>
|
|
77
|
+
</slot>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="hn-timeline-separator" aria-hidden="true">
|
|
80
|
+
<span :class="timelineMarker({ tone: item.tone ?? props.tone })">
|
|
81
|
+
<slot name="marker" :item="item" :index="index">
|
|
82
|
+
<span class="bg-surface size-2.5 rounded-full border-2 border-current" />
|
|
83
|
+
</slot>
|
|
84
|
+
</span>
|
|
85
|
+
<span v-if="index < entries.length - 1" class="hn-timeline-connector bg-line" />
|
|
86
|
+
</div>
|
|
87
|
+
<div class="hn-timeline-content min-w-0 wrap-anywhere">
|
|
88
|
+
<slot name="content" :item="item" :index="index">
|
|
89
|
+
<div
|
|
90
|
+
v-if="props.timePosition === 'content' && (item.time || slots.time)"
|
|
91
|
+
class="text-muted mb-1 text-xs tabular-nums"
|
|
92
|
+
>
|
|
93
|
+
<slot name="time" :item="item" :index="index">
|
|
94
|
+
<component :is="item.dateTime ? 'time' : 'span'" :datetime="item.dateTime">
|
|
95
|
+
{{ item.time }}
|
|
96
|
+
</component>
|
|
97
|
+
</slot>
|
|
98
|
+
</div>
|
|
99
|
+
<div v-if="item.title || slots.title" class="font-medium">
|
|
100
|
+
<slot name="title" :item="item" :index="index">{{ item.title }}</slot>
|
|
101
|
+
</div>
|
|
102
|
+
<div v-if="item.description || slots.description" class="text-muted mt-1">
|
|
103
|
+
<slot name="description" :item="item" :index="index">{{ item.description }}</slot>
|
|
104
|
+
</div>
|
|
105
|
+
</slot>
|
|
106
|
+
</div>
|
|
107
|
+
</li>
|
|
108
|
+
</ol>
|
|
109
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { tv, type VariantProps } from '../../lib/tv'
|
|
2
|
+
|
|
3
|
+
export const timeline = tv({
|
|
4
|
+
base: 'hn-timeline text-fg m-0 grid w-full min-w-0 list-none p-0',
|
|
5
|
+
variants: {
|
|
6
|
+
size: {
|
|
7
|
+
sm: 'text-xs [--hn-timeline-gap:var(--hn-stack-gap)]',
|
|
8
|
+
md: 'text-sm [--hn-timeline-gap:calc(var(--hn-stack-gap)*1.5)]',
|
|
9
|
+
lg: 'text-base [--hn-timeline-gap:calc(var(--hn-stack-gap)*2)]',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
defaultVariants: { size: 'md' },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const timelineMarker = tv({
|
|
16
|
+
base: 'hn-timeline-marker flex shrink-0 items-center justify-center [&_svg]:size-[1em]',
|
|
17
|
+
variants: {
|
|
18
|
+
tone: {
|
|
19
|
+
accent: 'text-accent-text',
|
|
20
|
+
neutral: 'text-muted',
|
|
21
|
+
success: 'text-success-text',
|
|
22
|
+
warning: 'text-warning-text',
|
|
23
|
+
danger: 'text-danger-text',
|
|
24
|
+
info: 'text-info-text',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: { tone: 'accent' },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export type TimelineVariants = VariantProps<typeof timeline>
|
|
31
|
+
export type TimelineMarkerVariants = VariantProps<typeof timelineMarker>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TimelineVariants, TimelineMarkerVariants } from './timeline.variants'
|
|
2
|
+
|
|
3
|
+
export type TimelineTone = NonNullable<TimelineMarkerVariants['tone']>
|
|
4
|
+
export type TimelineSize = NonNullable<TimelineVariants['size']>
|
|
5
|
+
export type TimelineOrientation = 'vertical' | 'horizontal'
|
|
6
|
+
export type TimelineAlign = 'start' | 'end' | 'alternate'
|
|
7
|
+
|
|
8
|
+
export interface TimelineItem {
|
|
9
|
+
id?: string | number
|
|
10
|
+
title?: string
|
|
11
|
+
description?: string
|
|
12
|
+
time?: string
|
|
13
|
+
dateTime?: string
|
|
14
|
+
tone?: TimelineTone
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TimelineSlotProps<T extends TimelineItem = TimelineItem> {
|
|
18
|
+
item: T
|
|
19
|
+
index: number
|
|
20
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { toRef } from 'vue'
|
|
3
|
+
import { ToolbarRoot } from 'reka-ui'
|
|
4
|
+
import { cn } from '../../lib/cn'
|
|
5
|
+
import { useAccessibleName } from '../../lib/a11y'
|
|
6
|
+
import { useDirection } from '../../lib/useDirection'
|
|
7
|
+
import { provideToolbar } from './context'
|
|
8
|
+
import { toolbar } from './toolbar.variants'
|
|
9
|
+
import type { ToolbarProps } from './types'
|
|
10
|
+
|
|
11
|
+
defineOptions({ name: 'HnToolbar' })
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<ToolbarProps>(), {
|
|
14
|
+
orientation: 'horizontal',
|
|
15
|
+
loop: true,
|
|
16
|
+
disabled: false,
|
|
17
|
+
size: 'md',
|
|
18
|
+
variant: 'primary',
|
|
19
|
+
})
|
|
20
|
+
const { root, direction, rootDirection } = useDirection(() => props.dir)
|
|
21
|
+
|
|
22
|
+
provideToolbar({
|
|
23
|
+
orientation: toRef(props, 'orientation'),
|
|
24
|
+
size: toRef(props, 'size'),
|
|
25
|
+
disabled: toRef(props, 'disabled'),
|
|
26
|
+
})
|
|
27
|
+
useAccessibleName('Toolbar', () => !props.label)
|
|
28
|
+
defineSlots<{ default(): unknown }>()
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<ToolbarRoot
|
|
33
|
+
as-child
|
|
34
|
+
:orientation="props.orientation"
|
|
35
|
+
:dir="direction"
|
|
36
|
+
:loop="props.loop"
|
|
37
|
+
:aria-label="props.label"
|
|
38
|
+
:aria-disabled="props.disabled || undefined"
|
|
39
|
+
:class="cn(toolbar({ orientation: props.orientation, variant: props.variant }), props.class)"
|
|
40
|
+
>
|
|
41
|
+
<div ref="root" :dir="rootDirection" data-hn-toolbar>
|
|
42
|
+
<slot />
|
|
43
|
+
</div>
|
|
44
|
+
</ToolbarRoot>
|
|
45
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { ToolbarButton as RekaToolbarButton } from 'reka-ui'
|
|
4
|
+
import ToolbarControl from './ToolbarControl.vue'
|
|
5
|
+
import { useToolbar } from './context'
|
|
6
|
+
import type { ToolbarControlProps } from './types'
|
|
7
|
+
|
|
8
|
+
defineOptions({ name: 'HnToolbarButton', inheritAttrs: false })
|
|
9
|
+
const props = withDefaults(defineProps<ToolbarControlProps>(), { tooltip: true, ripple: true })
|
|
10
|
+
const toolbar = useToolbar()
|
|
11
|
+
const disabled = computed(() => toolbar.disabled.value || props.disabled || props.loading)
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<RekaToolbarButton as-child :as="props.as" :disabled="disabled">
|
|
16
|
+
<ToolbarControl
|
|
17
|
+
v-bind="{ ...$attrs, ...props }"
|
|
18
|
+
:disabled="disabled"
|
|
19
|
+
:size="props.size ?? toolbar.size.value"
|
|
20
|
+
>
|
|
21
|
+
<template v-for="(_, name) in $slots" #[name]>
|
|
22
|
+
<slot :name="name" />
|
|
23
|
+
</template>
|
|
24
|
+
</ToolbarControl>
|
|
25
|
+
</RekaToolbarButton>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { injectTooltipProviderContext } from 'reka-ui'
|
|
3
|
+
import { Passthrough } from '../../lib/passthrough'
|
|
4
|
+
import Button from '../button/Button.vue'
|
|
5
|
+
import Tooltip from '../tooltip/Tooltip.vue'
|
|
6
|
+
import type { ToolbarControlProps } from './types'
|
|
7
|
+
|
|
8
|
+
defineOptions({ inheritAttrs: false })
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<ToolbarControlProps>(), {
|
|
11
|
+
as: 'button',
|
|
12
|
+
tooltip: true,
|
|
13
|
+
side: 'top',
|
|
14
|
+
variant: 'ghost',
|
|
15
|
+
tone: 'neutral',
|
|
16
|
+
ripple: true,
|
|
17
|
+
})
|
|
18
|
+
const Wrapper = injectTooltipProviderContext(null) ? Tooltip : Passthrough
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<Wrapper
|
|
23
|
+
:disabled="!props.tooltip || !props.label"
|
|
24
|
+
:content="props.label ?? ''"
|
|
25
|
+
:side="props.side"
|
|
26
|
+
>
|
|
27
|
+
<Button
|
|
28
|
+
v-bind="$attrs"
|
|
29
|
+
:as="props.as"
|
|
30
|
+
:as-child="props.asChild"
|
|
31
|
+
:aria-label="props.label"
|
|
32
|
+
:icon-only="!!props.label"
|
|
33
|
+
:size="props.size"
|
|
34
|
+
:variant="props.variant"
|
|
35
|
+
:tone="props.tone"
|
|
36
|
+
:disabled="props.disabled"
|
|
37
|
+
:loading="props.loading"
|
|
38
|
+
:ripple="props.ripple"
|
|
39
|
+
:class="props.class"
|
|
40
|
+
>
|
|
41
|
+
<template v-for="(_, name) in $slots" #[name]>
|
|
42
|
+
<slot :name="name" />
|
|
43
|
+
</template>
|
|
44
|
+
</Button>
|
|
45
|
+
</Wrapper>
|
|
46
|
+
</template>
|