@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,84 @@
1
+ <script setup lang="ts">
2
+ import { computed, toRef } from 'vue'
3
+ import { NavigationMenuRoot, NavigationMenuList, NavigationMenuViewport } from 'reka-ui'
4
+ import { cn } from '../../lib/cn'
5
+ import { useAccessibleName } from '../../lib/a11y'
6
+ import { useDirection } from '../../lib/useDirection'
7
+ import { provideNavigationMenu } from './context'
8
+ import { useNavigationMenuKeyboard } from './composables/useNavigationMenuKeyboard'
9
+ import { useNavigationMenuLayout } from './composables/useNavigationMenuLayout'
10
+ import {
11
+ navigationMenu,
12
+ navigationMenuList,
13
+ navigationMenuViewport,
14
+ } from './navigation-menu.variants'
15
+ import type { NavigationMenuProps } from './types'
16
+
17
+ defineOptions({ name: 'HnNavigationMenu' })
18
+ const props = withDefaults(defineProps<NavigationMenuProps>(), {
19
+ orientation: 'horizontal',
20
+ size: 'md',
21
+ trigger: 'hover',
22
+ delayDuration: 200,
23
+ skipDelayDuration: 300,
24
+ align: 'center',
25
+ unmountOnHide: true,
26
+ })
27
+ const model = defineModel<string>({ default: '' })
28
+ const { root, direction, rootDirection } = useDirection(() => props.dir)
29
+ const onKeydown = useNavigationMenuKeyboard(
30
+ root,
31
+ () => props.orientation,
32
+ () => direction.value,
33
+ )
34
+ const { side, placement, style } = useNavigationMenuLayout(
35
+ root,
36
+ () => props.orientation,
37
+ () => direction.value,
38
+ )
39
+ const align = computed(() =>
40
+ props.orientation === 'horizontal' && direction.value === 'rtl' && props.align !== 'center'
41
+ ? props.align === 'start'
42
+ ? 'end'
43
+ : 'start'
44
+ : props.align,
45
+ )
46
+ provideNavigationMenu({ size: toRef(props, 'size'), orientation: toRef(props, 'orientation') })
47
+ useAccessibleName('NavigationMenu', () => !props.label)
48
+ defineSlots<{ default(props: { value: string }): unknown }>()
49
+ </script>
50
+
51
+ <template>
52
+ <NavigationMenuRoot
53
+ :key="props.orientation"
54
+ v-model="model"
55
+ as-child
56
+ :orientation="props.orientation"
57
+ :dir="direction"
58
+ :delay-duration="props.delayDuration"
59
+ :skip-delay-duration="props.skipDelayDuration"
60
+ :disable-hover-trigger="props.trigger === 'click'"
61
+ :disable-pointer-leave-close="props.trigger === 'click'"
62
+ :unmount-on-hide="props.unmountOnHide"
63
+ :aria-label="props.label"
64
+ :class="cn(navigationMenu(), props.class)"
65
+ @keydown.capture="onKeydown"
66
+ >
67
+ <nav ref="root" :dir="rootDirection" :style="style" data-hn-navigation-menu>
68
+ <NavigationMenuList
69
+ data-hn-navigation-list
70
+ :class="cn(navigationMenuList({ orientation: props.orientation }), props.listClass)"
71
+ >
72
+ <slot :value="model" />
73
+ </NavigationMenuList>
74
+ <NavigationMenuViewport
75
+ data-hn-navigation-viewport
76
+ :align="align"
77
+ :data-side="placement"
78
+ :class="
79
+ cn(navigationMenuViewport({ orientation: props.orientation, side }), props.viewportClass)
80
+ "
81
+ />
82
+ </nav>
83
+ </NavigationMenuRoot>
84
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ NavigationMenuContent as RekaNavigationMenuContent,
4
+ useForwardPropsEmits,
5
+ type NavigationMenuContentEmits,
6
+ } from 'reka-ui'
7
+ import { cn } from '../../lib/cn'
8
+ import { navigationMenuContent } from './navigation-menu.variants'
9
+
10
+ defineOptions({ name: 'HnNavigationMenuContent', inheritAttrs: false })
11
+ const props = withDefaults(defineProps<{ padded?: boolean; class?: string }>(), { padded: true })
12
+ const emit = defineEmits<NavigationMenuContentEmits>()
13
+ const events = useForwardPropsEmits({}, emit)
14
+ defineSlots<{ default(): unknown }>()
15
+ </script>
16
+
17
+ <template>
18
+ <RekaNavigationMenuContent
19
+ v-bind="{ ...$attrs, ...events }"
20
+ data-hn-navigation-content
21
+ :class="cn(navigationMenuContent({ padded: props.padded }), props.class)"
22
+ >
23
+ <slot />
24
+ </RekaNavigationMenuContent>
25
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ import { NavigationMenuItem as RekaNavigationMenuItem } from 'reka-ui'
3
+
4
+ defineOptions({ name: 'HnNavigationMenuItem' })
5
+ const props = defineProps<{ value?: string }>()
6
+ defineSlots<{ default(): unknown }>()
7
+ </script>
8
+
9
+ <template>
10
+ <RekaNavigationMenuItem :value="props.value">
11
+ <slot />
12
+ </RekaNavigationMenuItem>
13
+ </template>
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ NavigationMenuLink as RekaNavigationMenuLink,
4
+ type NavigationMenuLinkEmits,
5
+ type PrimitiveProps,
6
+ } from 'reka-ui'
7
+ import { cn } from '../../lib/cn'
8
+ import { useNavigationMenu } from './context'
9
+ import {
10
+ navigationMenuControl,
11
+ navigationMenuLink,
12
+ navigationMenuIcon,
13
+ } from './navigation-menu.variants'
14
+
15
+ defineOptions({ name: 'HnNavigationMenuLink', inheritAttrs: false })
16
+ const props = withDefaults(
17
+ defineProps<
18
+ PrimitiveProps & {
19
+ active?: boolean
20
+ disabled?: boolean
21
+ description?: string
22
+ class?: string
23
+ }
24
+ >(),
25
+ { as: 'a' },
26
+ )
27
+ const emit = defineEmits<NavigationMenuLinkEmits>()
28
+ const menu = useNavigationMenu()
29
+ defineSlots<{
30
+ default(): unknown
31
+ icon?(): unknown
32
+ trailing?(): unknown
33
+ description?(): unknown
34
+ }>()
35
+
36
+ function guard(event: MouseEvent) {
37
+ if (!props.disabled) return
38
+ event.preventDefault()
39
+ event.stopImmediatePropagation()
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <RekaNavigationMenuLink
45
+ v-bind="$attrs"
46
+ :as="props.as"
47
+ :as-child="props.asChild"
48
+ :active="props.active"
49
+ :aria-disabled="props.disabled || undefined"
50
+ :data-disabled="props.disabled ? '' : undefined"
51
+ :tabindex="props.disabled ? -1 : undefined"
52
+ data-hn-navigation-control
53
+ :class="
54
+ cn(
55
+ navigationMenuControl({ size: menu.size.value, orientation: menu.orientation.value }),
56
+ navigationMenuLink(),
57
+ props.class,
58
+ )
59
+ "
60
+ @click.capture="guard"
61
+ @select="emit('select', $event)"
62
+ >
63
+ <slot v-if="props.asChild" />
64
+ <template v-else>
65
+ <span v-if="$slots.icon" :class="navigationMenuIcon()" aria-hidden="true">
66
+ <slot name="icon" />
67
+ </span>
68
+ <span class="flex min-w-0 flex-1 flex-col gap-1">
69
+ <span><slot /></span>
70
+ <span
71
+ v-if="props.description || $slots.description"
72
+ class="text-muted text-xs leading-relaxed font-normal"
73
+ >
74
+ <slot name="description">{{ props.description }}</slot>
75
+ </span>
76
+ </span>
77
+ <span v-if="$slots.trailing" :class="navigationMenuIcon()">
78
+ <slot name="trailing" />
79
+ </span>
80
+ </template>
81
+ </RekaNavigationMenuLink>
82
+ </template>
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ import { NavigationMenuTrigger as RekaNavigationMenuTrigger } from 'reka-ui'
3
+ import { cn } from '../../lib/cn'
4
+ import DisclosureIcon from '../disclosure-icon/DisclosureIcon.vue'
5
+ import { useNavigationMenu } from './context'
6
+ import {
7
+ navigationMenuControl,
8
+ navigationMenuTrigger,
9
+ navigationMenuIcon,
10
+ navigationMenuDisclosure,
11
+ } from './navigation-menu.variants'
12
+
13
+ defineOptions({ name: 'HnNavigationMenuTrigger' })
14
+ const props = defineProps<{ disabled?: boolean; class?: string }>()
15
+ const menu = useNavigationMenu()
16
+ defineSlots<{ default(): unknown; icon?(): unknown; trailing?(): unknown }>()
17
+ </script>
18
+
19
+ <template>
20
+ <RekaNavigationMenuTrigger
21
+ type="button"
22
+ data-hn-navigation-control
23
+ :disabled="props.disabled"
24
+ :class="
25
+ cn(
26
+ navigationMenuControl({ size: menu.size.value, orientation: menu.orientation.value }),
27
+ navigationMenuTrigger(),
28
+ props.class,
29
+ )
30
+ "
31
+ >
32
+ <span v-if="$slots.icon" :class="navigationMenuIcon()" aria-hidden="true">
33
+ <slot name="icon" />
34
+ </span>
35
+ <span class="min-w-0 flex-1"><slot /></span>
36
+ <span :class="navigationMenuIcon()">
37
+ <slot name="trailing">
38
+ <span :class="navigationMenuDisclosure({ orientation: menu.orientation.value })">
39
+ <DisclosureIcon :direction="menu.orientation.value === 'vertical' ? 'end' : 'down'" />
40
+ </span>
41
+ </slot>
42
+ </span>
43
+ </RekaNavigationMenuTrigger>
44
+ </template>
@@ -0,0 +1,33 @@
1
+ import type { Ref } from 'vue'
2
+ import type { NavigationMenuOrientation } from '../types'
3
+
4
+ export function useNavigationMenuKeyboard(
5
+ root: Ref<HTMLElement | undefined>,
6
+ orientation: () => NavigationMenuOrientation,
7
+ direction: () => 'ltr' | 'rtl',
8
+ ) {
9
+ return (event: KeyboardEvent) => {
10
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return
11
+ const items = Array.from(
12
+ root.value?.querySelectorAll<HTMLElement>(
13
+ '[data-hn-navigation-list] > [data-menu-item] > [data-hn-navigation-control]',
14
+ ) ?? [],
15
+ ).filter(item => !item.hasAttribute('data-disabled'))
16
+ const index = items.indexOf(event.target as HTMLElement)
17
+ if (index < 0) return
18
+
19
+ const horizontal = orientation() === 'horizontal'
20
+ const next = horizontal ? (direction() === 'rtl' ? 'ArrowLeft' : 'ArrowRight') : 'ArrowDown'
21
+ const previous = horizontal ? (direction() === 'rtl' ? 'ArrowRight' : 'ArrowLeft') : 'ArrowUp'
22
+ let target: number
23
+ if (event.key === next) target = Math.min(index + 1, items.length - 1)
24
+ else if (event.key === previous) target = Math.max(index - 1, 0)
25
+ else if (event.key === 'Home') target = 0
26
+ else if (event.key === 'End') target = items.length - 1
27
+ else return
28
+
29
+ event.preventDefault()
30
+ event.stopPropagation()
31
+ items[target]?.focus()
32
+ }
33
+ }
@@ -0,0 +1,72 @@
1
+ import { computed, shallowRef, watch, type Ref } from 'vue'
2
+ import { useElementBounding, useResizeObserver, useWindowSize } from '@vueuse/core'
3
+ import type { NavigationMenuOrientation } from '../types'
4
+
5
+ export function useNavigationMenuLayout(
6
+ root: Ref<HTMLElement | undefined>,
7
+ orientation: () => NavigationMenuOrientation,
8
+ direction: () => 'ltr' | 'rtl',
9
+ ) {
10
+ const { left, right, update } = useElementBounding(root)
11
+ const { width } = useWindowSize()
12
+ const ancestors = shallowRef<HTMLElement[]>([])
13
+ const bounds = shallowRef({ start: 16, end: width.value - 16 })
14
+
15
+ function measure() {
16
+ let start = 16
17
+ let end = width.value - 16
18
+ for (const element of ancestors.value) {
19
+ const overflow = element.ownerDocument.defaultView?.getComputedStyle(element).overflowX
20
+ if (overflow === 'visible') continue
21
+ const rect = element.getBoundingClientRect()
22
+ start = Math.max(start, rect.left + element.clientLeft + 8)
23
+ end = Math.min(end, rect.left + element.clientLeft + element.clientWidth - 8)
24
+ }
25
+ bounds.value = { start, end }
26
+ }
27
+
28
+ watch(
29
+ root,
30
+ element => {
31
+ const parents: HTMLElement[] = []
32
+ for (let parent = element?.parentElement; parent; parent = parent.parentElement)
33
+ parents.push(parent)
34
+ ancestors.value = parents
35
+ measure()
36
+ },
37
+ { flush: 'post' },
38
+ )
39
+ watch([left, right, width], measure, { flush: 'post' })
40
+ useResizeObserver(ancestors, () => {
41
+ update()
42
+ measure()
43
+ })
44
+
45
+ const available = computed(() => {
46
+ const before = Math.max(0, left.value - bounds.value.start - 10)
47
+ const after = Math.max(0, bounds.value.end - right.value - 10)
48
+ return direction() === 'rtl' ? { start: after, end: before } : { start: before, end: after }
49
+ })
50
+ const side = computed(() =>
51
+ orientation() === 'vertical' &&
52
+ available.value.end < 320 &&
53
+ available.value.start > available.value.end
54
+ ? 'start'
55
+ : 'end',
56
+ )
57
+ const placement = computed(() => {
58
+ if (orientation() === 'horizontal') return 'bottom'
59
+ if (side.value === 'end') return direction() === 'rtl' ? 'left' : 'right'
60
+ return direction() === 'rtl' ? 'right' : 'left'
61
+ })
62
+ const style = computed(() =>
63
+ root.value
64
+ ? {
65
+ '--hn-navigation-max-width': `${orientation() === 'vertical' ? available.value[side.value] : Math.max(0, bounds.value.end - bounds.value.start - 2)}px`,
66
+ '--hn-navigation-min-x': `${bounds.value.start - left.value}px`,
67
+ '--hn-navigation-bound-end': `${bounds.value.end - left.value}px`,
68
+ }
69
+ : undefined,
70
+ )
71
+ return { side, placement, style }
72
+ }
@@ -0,0 +1,19 @@
1
+ import { inject, provide, type InjectionKey, type Ref } from 'vue'
2
+ import type { NavigationMenuOrientation, NavigationMenuSize } from './types'
3
+
4
+ interface NavigationMenuContext {
5
+ size: Readonly<Ref<NavigationMenuSize>>
6
+ orientation: Readonly<Ref<NavigationMenuOrientation>>
7
+ }
8
+
9
+ const key: InjectionKey<NavigationMenuContext> = Symbol('hn-navigation-menu')
10
+
11
+ export function provideNavigationMenu(context: NavigationMenuContext) {
12
+ provide(key, context)
13
+ }
14
+
15
+ export function useNavigationMenu() {
16
+ const context = inject(key)
17
+ if (!context) throw new Error('NavigationMenu controls must be inside NavigationMenu.')
18
+ return context
19
+ }
@@ -0,0 +1,82 @@
1
+ import { tv } from '../../lib/tv'
2
+
3
+ export const navigationMenu = tv({
4
+ base: 'relative w-fit max-w-full',
5
+ })
6
+
7
+ export const navigationMenuList = tv({
8
+ base: 'm-0 flex list-none gap-1 p-0',
9
+ variants: {
10
+ orientation: {
11
+ horizontal: 'flex-wrap items-center',
12
+ vertical: 'flex-col items-stretch',
13
+ },
14
+ },
15
+ })
16
+
17
+ export const navigationMenuControl = tv({
18
+ base: [
19
+ 'hn-interactive hn-state-layer hn-press-none relative isolate flex cursor-pointer items-center',
20
+ 'text-fg gap-(--hn-control-gap) rounded-md text-start font-medium no-underline outline-none select-none [&_svg]:shrink-0',
21
+ 'data-[state=open]:bg-inset data-active:text-accent-text data-active:bg-accent-soft',
22
+ 'data-disabled:pointer-events-none data-disabled:opacity-50',
23
+ ],
24
+ variants: {
25
+ size: {
26
+ sm: 'min-h-(--hn-control-h-sm) px-(--hn-control-px-sm) text-xs [&_svg]:size-3.5',
27
+ md: 'min-h-(--hn-control-h-md) px-(--hn-control-px-md) text-sm [&_svg]:size-4',
28
+ lg: 'min-h-(--hn-control-h-lg) px-(--hn-control-px-lg) text-base [&_svg]:size-[1.125rem]',
29
+ },
30
+ orientation: {
31
+ horizontal: '',
32
+ vertical: 'w-full',
33
+ },
34
+ },
35
+ defaultVariants: { size: 'md' },
36
+ })
37
+
38
+ export const navigationMenuTrigger = tv({
39
+ base: 'group/hn-disclosure',
40
+ })
41
+
42
+ export const navigationMenuIcon = tv({
43
+ base: 'inline-flex shrink-0 items-center justify-center gap-1',
44
+ })
45
+
46
+ export const navigationMenuDisclosure = tv({
47
+ base: 'inline-flex shrink-0 text-muted',
48
+ variants: {
49
+ orientation: { horizontal: '', vertical: 'rtl:-scale-x-100' },
50
+ },
51
+ })
52
+
53
+ export const navigationMenuLink = tv({
54
+ base: 'py-2',
55
+ })
56
+
57
+ export const navigationMenuContent = tv({
58
+ base: 'hn-anim-navigation-content absolute start-0 top-0 w-80 max-w-[min(calc(100vw-2rem),var(--hn-navigation-max-width,100vw))] outline-none rtl:start-auto rtl:end-0',
59
+ variants: {
60
+ padded: { true: 'p-2', false: '' },
61
+ },
62
+ defaultVariants: { padded: true },
63
+ })
64
+
65
+ export const navigationMenuViewport = tv({
66
+ base: [
67
+ 'hn-navigation-viewport bg-surface border-line absolute z-(--hn-z-overlay) overflow-hidden rounded-lg border shadow-md',
68
+ 'h-(--reka-navigation-menu-viewport-height) w-(--reka-navigation-menu-viewport-width)',
69
+ ],
70
+ variants: {
71
+ orientation: {
72
+ horizontal:
73
+ 'top-full mt-2 [left:clamp(var(--hn-navigation-min-x,0px),var(--reka-navigation-menu-viewport-left),calc(var(--hn-navigation-bound-end,100vw)-var(--reka-navigation-menu-viewport-width)-2px))]',
74
+ vertical: '[top:var(--reka-navigation-menu-viewport-top)]',
75
+ },
76
+ side: { start: '', end: '' },
77
+ },
78
+ compoundVariants: [
79
+ { orientation: 'vertical', side: 'end', class: 'start-full ms-2' },
80
+ { orientation: 'vertical', side: 'start', class: 'end-full me-2' },
81
+ ],
82
+ })
@@ -0,0 +1,17 @@
1
+ export type NavigationMenuSize = 'sm' | 'md' | 'lg'
2
+ export type NavigationMenuOrientation = 'horizontal' | 'vertical'
3
+
4
+ export interface NavigationMenuProps {
5
+ label?: string
6
+ orientation?: NavigationMenuOrientation
7
+ dir?: 'ltr' | 'rtl'
8
+ size?: NavigationMenuSize
9
+ trigger?: 'hover' | 'click'
10
+ delayDuration?: number
11
+ skipDelayDuration?: number
12
+ align?: 'start' | 'center' | 'end'
13
+ unmountOnHide?: boolean
14
+ class?: string
15
+ listClass?: string
16
+ viewportClass?: string
17
+ }
@@ -13,12 +13,14 @@
13
13
  type RatingVariants,
14
14
  } from './rating.variants'
15
15
  import { starFill } from './utils/fill'
16
+ import { useRatingScale } from './composables/useRatingScale'
16
17
 
17
18
  defineOptions({ name: 'HnRating', inheritAttrs: false })
18
19
 
19
20
  const props = withDefaults(
20
21
  defineProps<{
21
22
  max?: number
23
+ stars?: number
22
24
  step?: 1 | 0.5
23
25
  clearable?: boolean
24
26
  readonly?: boolean
@@ -31,6 +33,11 @@
31
33
  )
32
34
 
33
35
  const model = defineModel<number>({ default: 0 })
36
+ const { count, scaled, value, toScore } = useRatingScale(
37
+ model,
38
+ () => props.max,
39
+ () => props.stars,
40
+ )
34
41
 
35
42
  const t = useUiLocale()
36
43
 
@@ -46,12 +53,12 @@
46
53
  data-hn-rating
47
54
  data-readonly
48
55
  role="img"
49
- :aria-label="t.rating.label(model, props.max)"
56
+ :aria-label="scaled ? t.rating.score(model, props.max) : t.rating.label(model, props.max)"
50
57
  :class="cn(ratingRoot({ size: props.size }), props.class)"
51
58
  >
52
- <span v-for="index in props.max" :key="index" :class="ratingItem()">
59
+ <span v-for="index in count" :key="index" :class="ratingItem()">
53
60
  <Star :class="ratingStar()" />
54
- <span :class="ratingFill()" :style="{ width: starFill(model, index) }">
61
+ <span :class="ratingFill()" :style="{ width: starFill(value, index) }">
55
62
  <Star :class="ratingStar({ active: true })" />
56
63
  </span>
57
64
  </span>
@@ -66,15 +73,14 @@
66
73
  :aria-labelledby="labelledBy"
67
74
  :aria-describedby="describedBy"
68
75
  :aria-invalid="invalid || undefined"
69
- :model-value="model"
70
- :length="props.max"
76
+ v-model="value"
77
+ :length="count"
71
78
  :step="props.step"
72
79
  :clearable="props.clearable"
73
80
  hoverable
74
81
  :disabled="disabled"
75
- :name="props.name"
82
+ :name="scaled ? undefined : props.name"
76
83
  :class="cn(ratingRoot({ size: props.size }), props.class)"
77
- @update:model-value="model = $event"
78
84
  >
79
85
  <RatingItem
80
86
  v-for="item in items"
@@ -87,11 +93,19 @@
87
93
  v-for="step in steps"
88
94
  :key="step"
89
95
  :step="step"
90
- :aria-label="t.rating.star(step)"
96
+ :aria-label="scaled ? t.rating.score(toScore(step), props.max) : t.rating.star(step)"
91
97
  :class="ratingStep()"
92
98
  >
93
99
  <Star :class="ratingStar()" />
94
100
  </RatingItemIndicator>
95
101
  </RatingItem>
102
+ <input
103
+ v-if="scaled && props.name"
104
+ type="hidden"
105
+ :name="props.name"
106
+ :value="model"
107
+ :disabled="disabled"
108
+ :form="$attrs.form as string | undefined"
109
+ />
96
110
  </RatingRoot>
97
111
  </template>
@@ -0,0 +1,24 @@
1
+ import { computed, type Ref } from 'vue'
2
+
3
+ export function useRatingScale(
4
+ model: Ref<number>,
5
+ max: () => number,
6
+ stars: () => number | undefined,
7
+ ) {
8
+ const count = computed(() => stars() ?? max())
9
+ const scaled = computed(() => count.value !== max())
10
+
11
+ function toScore(value: number) {
12
+ return scaled.value ? Number(((value / count.value) * max()).toPrecision(15)) : value
13
+ }
14
+
15
+ const value = computed({
16
+ get: () =>
17
+ scaled.value ? Number(((model.value / max()) * count.value).toFixed(10)) : model.value,
18
+ set: (value: number) => {
19
+ model.value = toScore(value)
20
+ },
21
+ })
22
+
23
+ return { count, scaled, value, toScore }
24
+ }
@@ -85,7 +85,7 @@
85
85
  <ScrollArea :shadow="false" class="min-h-0 flex-1">
86
86
  <nav
87
87
  :aria-label="props.label ?? t.sidebar.navLabel"
88
- :class="cn('flex flex-col gap-1 py-2', !inDrawer && 'px-2.5')"
88
+ :class="cn('flex flex-col gap-1', inDrawer ? 'py-2' : 'px-2.5 py-2.5')"
89
89
  >
90
90
  <slot />
91
91
  </nav>