@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.
Files changed (111) hide show
  1. package/dist/index.js +1899 -493
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/navigation-menu/NavigationMenu.vue.d.ts +33 -0
  4. package/dist/types/components/navigation-menu/NavigationMenu.vue.d.ts.map +1 -0
  5. package/dist/types/components/navigation-menu/NavigationMenuContent.vue.d.ts +29 -0
  6. package/dist/types/components/navigation-menu/NavigationMenuContent.vue.d.ts.map +1 -0
  7. package/dist/types/components/navigation-menu/NavigationMenuItem.vue.d.ts +16 -0
  8. package/dist/types/components/navigation-menu/NavigationMenuItem.vue.d.ts.map +1 -0
  9. package/dist/types/components/navigation-menu/NavigationMenuLink.vue.d.ts +33 -0
  10. package/dist/types/components/navigation-menu/NavigationMenuLink.vue.d.ts.map +1 -0
  11. package/dist/types/components/navigation-menu/NavigationMenuTrigger.vue.d.ts +19 -0
  12. package/dist/types/components/navigation-menu/NavigationMenuTrigger.vue.d.ts.map +1 -0
  13. package/dist/types/components/navigation-menu/composables/useNavigationMenuKeyboard.d.ts +4 -0
  14. package/dist/types/components/navigation-menu/composables/useNavigationMenuKeyboard.d.ts.map +1 -0
  15. package/dist/types/components/navigation-menu/composables/useNavigationMenuLayout.d.ts +12 -0
  16. package/dist/types/components/navigation-menu/composables/useNavigationMenuLayout.d.ts.map +1 -0
  17. package/dist/types/components/navigation-menu/context.d.ts +10 -0
  18. package/dist/types/components/navigation-menu/context.d.ts.map +1 -0
  19. package/dist/types/components/navigation-menu/navigation-menu.variants.d.ts +112 -0
  20. package/dist/types/components/navigation-menu/navigation-menu.variants.d.ts.map +1 -0
  21. package/dist/types/components/navigation-menu/types.d.ts +17 -0
  22. package/dist/types/components/navigation-menu/types.d.ts.map +1 -0
  23. package/dist/types/components/rating/Rating.vue.d.ts +1 -0
  24. package/dist/types/components/rating/Rating.vue.d.ts.map +1 -1
  25. package/dist/types/components/rating/composables/useRatingScale.d.ts +8 -0
  26. package/dist/types/components/rating/composables/useRatingScale.d.ts.map +1 -0
  27. package/dist/types/components/stepper/Stepper.vue.d.ts +36 -0
  28. package/dist/types/components/stepper/Stepper.vue.d.ts.map +1 -0
  29. package/dist/types/components/stepper/StepperStep.vue.d.ts +33 -0
  30. package/dist/types/components/stepper/StepperStep.vue.d.ts.map +1 -0
  31. package/dist/types/components/stepper/composables/useStepper.d.ts +13 -0
  32. package/dist/types/components/stepper/composables/useStepper.d.ts.map +1 -0
  33. package/dist/types/components/stepper/stepper.variants.d.ts +58 -0
  34. package/dist/types/components/stepper/stepper.variants.d.ts.map +1 -0
  35. package/dist/types/components/stepper/types.d.ts +45 -0
  36. package/dist/types/components/stepper/types.d.ts.map +1 -0
  37. package/dist/types/components/timeline/Timeline.vue.d.ts +36 -0
  38. package/dist/types/components/timeline/Timeline.vue.d.ts.map +1 -0
  39. package/dist/types/components/timeline/timeline.variants.d.ts +51 -0
  40. package/dist/types/components/timeline/timeline.variants.d.ts.map +1 -0
  41. package/dist/types/components/timeline/types.d.ts +18 -0
  42. package/dist/types/components/timeline/types.d.ts.map +1 -0
  43. package/dist/types/components/toolbar/Toolbar.vue.d.ts +20 -0
  44. package/dist/types/components/toolbar/Toolbar.vue.d.ts.map +1 -0
  45. package/dist/types/components/toolbar/ToolbarButton.vue.d.ts +18 -0
  46. package/dist/types/components/toolbar/ToolbarButton.vue.d.ts.map +1 -0
  47. package/dist/types/components/toolbar/ToolbarControl.vue.d.ts +22 -0
  48. package/dist/types/components/toolbar/ToolbarControl.vue.d.ts.map +1 -0
  49. package/dist/types/components/toolbar/ToolbarLink.vue.d.ts +24 -0
  50. package/dist/types/components/toolbar/ToolbarLink.vue.d.ts.map +1 -0
  51. package/dist/types/components/toolbar/ToolbarSeparator.vue.d.ts +10 -0
  52. package/dist/types/components/toolbar/ToolbarSeparator.vue.d.ts.map +1 -0
  53. package/dist/types/components/toolbar/ToolbarToggleGroup.vue.d.ts +31 -0
  54. package/dist/types/components/toolbar/ToolbarToggleGroup.vue.d.ts.map +1 -0
  55. package/dist/types/components/toolbar/ToolbarToggleItem.vue.d.ts +21 -0
  56. package/dist/types/components/toolbar/ToolbarToggleItem.vue.d.ts.map +1 -0
  57. package/dist/types/components/toolbar/context.d.ts +13 -0
  58. package/dist/types/components/toolbar/context.d.ts.map +1 -0
  59. package/dist/types/components/toolbar/toolbar.variants.d.ts +64 -0
  60. package/dist/types/components/toolbar/toolbar.variants.d.ts.map +1 -0
  61. package/dist/types/components/toolbar/types.d.ts +28 -0
  62. package/dist/types/components/toolbar/types.d.ts.map +1 -0
  63. package/dist/types/index.d.ts +17 -0
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/dist/types/locale/en-US.d.ts.map +1 -1
  66. package/dist/types/locale/index.d.ts.map +1 -1
  67. package/dist/types/locale/types.d.ts +7 -0
  68. package/dist/types/locale/types.d.ts.map +1 -1
  69. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/components/navigation-menu/NavigationMenu.vue +84 -0
  72. package/src/components/navigation-menu/NavigationMenuContent.vue +25 -0
  73. package/src/components/navigation-menu/NavigationMenuItem.vue +13 -0
  74. package/src/components/navigation-menu/NavigationMenuLink.vue +82 -0
  75. package/src/components/navigation-menu/NavigationMenuTrigger.vue +44 -0
  76. package/src/components/navigation-menu/composables/useNavigationMenuKeyboard.ts +33 -0
  77. package/src/components/navigation-menu/composables/useNavigationMenuLayout.ts +72 -0
  78. package/src/components/navigation-menu/context.ts +19 -0
  79. package/src/components/navigation-menu/navigation-menu.variants.ts +82 -0
  80. package/src/components/navigation-menu/types.ts +17 -0
  81. package/src/components/rating/Rating.vue +22 -8
  82. package/src/components/rating/composables/useRatingScale.ts +24 -0
  83. package/src/components/sidebar/Sidebar.vue +1 -1
  84. package/src/components/stepper/Stepper.vue +95 -0
  85. package/src/components/stepper/StepperStep.vue +102 -0
  86. package/src/components/stepper/composables/useStepper.ts +112 -0
  87. package/src/components/stepper/stepper.variants.ts +39 -0
  88. package/src/components/stepper/types.ts +51 -0
  89. package/src/components/timeline/Timeline.vue +109 -0
  90. package/src/components/timeline/timeline.variants.ts +31 -0
  91. package/src/components/timeline/types.ts +20 -0
  92. package/src/components/toolbar/Toolbar.vue +45 -0
  93. package/src/components/toolbar/ToolbarButton.vue +26 -0
  94. package/src/components/toolbar/ToolbarControl.vue +46 -0
  95. package/src/components/toolbar/ToolbarLink.vue +40 -0
  96. package/src/components/toolbar/ToolbarSeparator.vue +20 -0
  97. package/src/components/toolbar/ToolbarToggleGroup.vue +42 -0
  98. package/src/components/toolbar/ToolbarToggleItem.vue +34 -0
  99. package/src/components/toolbar/context.ts +29 -0
  100. package/src/components/toolbar/toolbar.variants.ts +37 -0
  101. package/src/components/toolbar/types.ts +30 -0
  102. package/src/index.ts +45 -0
  103. package/src/locale/en-US.ts +7 -0
  104. package/src/locale/index.ts +1 -0
  105. package/src/locale/types.ts +7 -0
  106. package/src/locale/zh-CN.ts +7 -0
  107. package/src/styles/interaction.css +1 -0
  108. package/src/styles/motion.css +66 -0
  109. package/src/styles/stepper.css +41 -0
  110. package/src/styles/timeline.css +144 -0
  111. 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>