@hina-ui/vue 1.7.1 → 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 (127) hide show
  1. package/dist/index.js +1933 -510
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/anchor/composables/useScrollSpy.d.ts.map +1 -1
  4. package/dist/types/components/data-table/DataTable.vue.d.ts.map +1 -1
  5. package/dist/types/components/data-table/column-sizing-styles.d.ts +4 -0
  6. package/dist/types/components/data-table/column-sizing-styles.d.ts.map +1 -0
  7. package/dist/types/components/data-table/composables/useTableColumns.d.ts +1 -0
  8. package/dist/types/components/data-table/composables/useTableColumns.d.ts.map +1 -1
  9. package/dist/types/components/navigation-menu/NavigationMenu.vue.d.ts +33 -0
  10. package/dist/types/components/navigation-menu/NavigationMenu.vue.d.ts.map +1 -0
  11. package/dist/types/components/navigation-menu/NavigationMenuContent.vue.d.ts +29 -0
  12. package/dist/types/components/navigation-menu/NavigationMenuContent.vue.d.ts.map +1 -0
  13. package/dist/types/components/navigation-menu/NavigationMenuItem.vue.d.ts +16 -0
  14. package/dist/types/components/navigation-menu/NavigationMenuItem.vue.d.ts.map +1 -0
  15. package/dist/types/components/navigation-menu/NavigationMenuLink.vue.d.ts +33 -0
  16. package/dist/types/components/navigation-menu/NavigationMenuLink.vue.d.ts.map +1 -0
  17. package/dist/types/components/navigation-menu/NavigationMenuTrigger.vue.d.ts +19 -0
  18. package/dist/types/components/navigation-menu/NavigationMenuTrigger.vue.d.ts.map +1 -0
  19. package/dist/types/components/navigation-menu/composables/useNavigationMenuKeyboard.d.ts +4 -0
  20. package/dist/types/components/navigation-menu/composables/useNavigationMenuKeyboard.d.ts.map +1 -0
  21. package/dist/types/components/navigation-menu/composables/useNavigationMenuLayout.d.ts +12 -0
  22. package/dist/types/components/navigation-menu/composables/useNavigationMenuLayout.d.ts.map +1 -0
  23. package/dist/types/components/navigation-menu/context.d.ts +10 -0
  24. package/dist/types/components/navigation-menu/context.d.ts.map +1 -0
  25. package/dist/types/components/navigation-menu/navigation-menu.variants.d.ts +112 -0
  26. package/dist/types/components/navigation-menu/navigation-menu.variants.d.ts.map +1 -0
  27. package/dist/types/components/navigation-menu/types.d.ts +17 -0
  28. package/dist/types/components/navigation-menu/types.d.ts.map +1 -0
  29. package/dist/types/components/rating/Rating.vue.d.ts +1 -0
  30. package/dist/types/components/rating/Rating.vue.d.ts.map +1 -1
  31. package/dist/types/components/rating/composables/useRatingScale.d.ts +8 -0
  32. package/dist/types/components/rating/composables/useRatingScale.d.ts.map +1 -0
  33. package/dist/types/components/stepper/Stepper.vue.d.ts +36 -0
  34. package/dist/types/components/stepper/Stepper.vue.d.ts.map +1 -0
  35. package/dist/types/components/stepper/StepperStep.vue.d.ts +33 -0
  36. package/dist/types/components/stepper/StepperStep.vue.d.ts.map +1 -0
  37. package/dist/types/components/stepper/composables/useStepper.d.ts +13 -0
  38. package/dist/types/components/stepper/composables/useStepper.d.ts.map +1 -0
  39. package/dist/types/components/stepper/stepper.variants.d.ts +58 -0
  40. package/dist/types/components/stepper/stepper.variants.d.ts.map +1 -0
  41. package/dist/types/components/stepper/types.d.ts +45 -0
  42. package/dist/types/components/stepper/types.d.ts.map +1 -0
  43. package/dist/types/components/table/table.variants.d.ts +1 -1
  44. package/dist/types/components/table/table.variants.d.ts.map +1 -1
  45. package/dist/types/components/timeline/Timeline.vue.d.ts +36 -0
  46. package/dist/types/components/timeline/Timeline.vue.d.ts.map +1 -0
  47. package/dist/types/components/timeline/timeline.variants.d.ts +51 -0
  48. package/dist/types/components/timeline/timeline.variants.d.ts.map +1 -0
  49. package/dist/types/components/timeline/types.d.ts +18 -0
  50. package/dist/types/components/timeline/types.d.ts.map +1 -0
  51. package/dist/types/components/toolbar/Toolbar.vue.d.ts +20 -0
  52. package/dist/types/components/toolbar/Toolbar.vue.d.ts.map +1 -0
  53. package/dist/types/components/toolbar/ToolbarButton.vue.d.ts +18 -0
  54. package/dist/types/components/toolbar/ToolbarButton.vue.d.ts.map +1 -0
  55. package/dist/types/components/toolbar/ToolbarControl.vue.d.ts +22 -0
  56. package/dist/types/components/toolbar/ToolbarControl.vue.d.ts.map +1 -0
  57. package/dist/types/components/toolbar/ToolbarLink.vue.d.ts +24 -0
  58. package/dist/types/components/toolbar/ToolbarLink.vue.d.ts.map +1 -0
  59. package/dist/types/components/toolbar/ToolbarSeparator.vue.d.ts +10 -0
  60. package/dist/types/components/toolbar/ToolbarSeparator.vue.d.ts.map +1 -0
  61. package/dist/types/components/toolbar/ToolbarToggleGroup.vue.d.ts +31 -0
  62. package/dist/types/components/toolbar/ToolbarToggleGroup.vue.d.ts.map +1 -0
  63. package/dist/types/components/toolbar/ToolbarToggleItem.vue.d.ts +21 -0
  64. package/dist/types/components/toolbar/ToolbarToggleItem.vue.d.ts.map +1 -0
  65. package/dist/types/components/toolbar/context.d.ts +13 -0
  66. package/dist/types/components/toolbar/context.d.ts.map +1 -0
  67. package/dist/types/components/toolbar/toolbar.variants.d.ts +64 -0
  68. package/dist/types/components/toolbar/toolbar.variants.d.ts.map +1 -0
  69. package/dist/types/components/toolbar/types.d.ts +28 -0
  70. package/dist/types/components/toolbar/types.d.ts.map +1 -0
  71. package/dist/types/index.d.ts +17 -0
  72. package/dist/types/index.d.ts.map +1 -1
  73. package/dist/types/locale/en-US.d.ts.map +1 -1
  74. package/dist/types/locale/index.d.ts.map +1 -1
  75. package/dist/types/locale/types.d.ts +7 -0
  76. package/dist/types/locale/types.d.ts.map +1 -1
  77. package/dist/types/locale/zh-CN.d.ts.map +1 -1
  78. package/package.json +1 -1
  79. package/src/components/anchor/composables/useScrollSpy.ts +8 -11
  80. package/src/components/data-table/DataTable.vue +1 -0
  81. package/src/components/data-table/DataTableHead.vue +1 -1
  82. package/src/components/data-table/column-sizing-styles.ts +40 -0
  83. package/src/components/data-table/composables/useTableColumns.ts +7 -9
  84. package/src/components/navigation-menu/NavigationMenu.vue +84 -0
  85. package/src/components/navigation-menu/NavigationMenuContent.vue +25 -0
  86. package/src/components/navigation-menu/NavigationMenuItem.vue +13 -0
  87. package/src/components/navigation-menu/NavigationMenuLink.vue +82 -0
  88. package/src/components/navigation-menu/NavigationMenuTrigger.vue +44 -0
  89. package/src/components/navigation-menu/composables/useNavigationMenuKeyboard.ts +33 -0
  90. package/src/components/navigation-menu/composables/useNavigationMenuLayout.ts +72 -0
  91. package/src/components/navigation-menu/context.ts +19 -0
  92. package/src/components/navigation-menu/navigation-menu.variants.ts +82 -0
  93. package/src/components/navigation-menu/types.ts +17 -0
  94. package/src/components/rating/Rating.vue +22 -8
  95. package/src/components/rating/composables/useRatingScale.ts +24 -0
  96. package/src/components/sidebar/Sidebar.vue +1 -1
  97. package/src/components/stepper/Stepper.vue +95 -0
  98. package/src/components/stepper/StepperStep.vue +102 -0
  99. package/src/components/stepper/composables/useStepper.ts +112 -0
  100. package/src/components/stepper/stepper.variants.ts +39 -0
  101. package/src/components/stepper/types.ts +51 -0
  102. package/src/components/table/table.variants.ts +1 -2
  103. package/src/components/timeline/Timeline.vue +109 -0
  104. package/src/components/timeline/timeline.variants.ts +31 -0
  105. package/src/components/timeline/types.ts +20 -0
  106. package/src/components/toolbar/Toolbar.vue +45 -0
  107. package/src/components/toolbar/ToolbarButton.vue +26 -0
  108. package/src/components/toolbar/ToolbarControl.vue +46 -0
  109. package/src/components/toolbar/ToolbarLink.vue +40 -0
  110. package/src/components/toolbar/ToolbarSeparator.vue +20 -0
  111. package/src/components/toolbar/ToolbarToggleGroup.vue +42 -0
  112. package/src/components/toolbar/ToolbarToggleItem.vue +34 -0
  113. package/src/components/toolbar/context.ts +29 -0
  114. package/src/components/toolbar/toolbar.variants.ts +37 -0
  115. package/src/components/toolbar/types.ts +30 -0
  116. package/src/index.ts +45 -0
  117. package/src/locale/en-US.ts +7 -0
  118. package/src/locale/index.ts +1 -0
  119. package/src/locale/types.ts +7 -0
  120. package/src/locale/zh-CN.ts +7 -0
  121. package/src/styles/data-table.css +0 -1
  122. package/src/styles/interaction.css +1 -0
  123. package/src/styles/motion.css +66 -0
  124. package/src/styles/stepper.css +41 -0
  125. package/src/styles/timeline.css +144 -0
  126. package/src/styles/tokens.css +2 -0
  127. package/src/styles/typography.css +1 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hina-ui/vue",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "An elegant, restrained Vue 3 component library built on Reka UI and Tailwind CSS v4.",
5
5
  "keywords": [
6
6
  "vue",
@@ -11,7 +11,7 @@ export interface SpyEntry {
11
11
  export function useScrollSpy(entries: () => SpyEntry[]) {
12
12
  const visible = shallowRef<ReadonlySet<string>>(new Set())
13
13
  const targets = shallowRef<Array<HTMLElement | null>>([])
14
- const seen = new Set<string>()
14
+ const intersecting = new Set<string>()
15
15
 
16
16
  const covered = computed(() => entries().filter(entry => visible.value.has(entry.id)))
17
17
  const current = computed(() => covered.value[0]?.id)
@@ -23,18 +23,15 @@ export function useScrollSpy(entries: () => SpyEntry[]) {
23
23
  useIntersectionObserver(
24
24
  targets,
25
25
  observed => {
26
- const next = new Set(visible.value)
27
26
  for (const entry of observed) {
28
- if (entry.isIntersecting) {
29
- seen.add(entry.target.id)
30
- next.add(entry.target.id)
31
- } else if (seen.has(entry.target.id)) {
32
- next.delete(entry.target.id)
33
- }
27
+ if (entry.isIntersecting && entry.intersectionRatio > 0) intersecting.add(entry.target.id)
28
+ else intersecting.delete(entry.target.id)
34
29
  }
35
- if (next.size) visible.value = next
30
+ visible.value = intersecting.size
31
+ ? new Set(intersecting)
32
+ : new Set(current.value ? [current.value] : [])
36
33
  },
37
- { rootMargin: '0px 0px -15% 0px' },
34
+ { rootMargin: '0px 0px -15% 0px', threshold: [0, Number.EPSILON] },
38
35
  )
39
36
 
40
37
  function targetOf(id: string) {
@@ -42,7 +39,7 @@ export function useScrollSpy(entries: () => SpyEntry[]) {
42
39
  }
43
40
 
44
41
  function locate() {
45
- seen.clear()
42
+ intersecting.clear()
46
43
  targets.value = entries().map(({ id }) => targetOf(id))
47
44
  }
48
45
 
@@ -168,6 +168,7 @@
168
168
  :class="
169
169
  cn(
170
170
  tableWrapper({ variant: props.variant, hover: false }),
171
+ layout.constrained.value && '@container',
171
172
  (props.fill || props.height) && 'h-full',
172
173
  props.stickyHeader && '[&_thead_th]:sticky [&_thead_th]:z-[3]',
173
174
  props.stickyFooter &&
@@ -65,7 +65,7 @@
65
65
  :style="header.style"
66
66
  :class="
67
67
  cn(
68
- 'hn-table-header relative',
68
+ 'hn-table-header relative p-0',
69
69
  header.column.headerClass,
70
70
  drag.kind.value === 'column' && drag.key.value === header.column.key && 'opacity-40',
71
71
  )
@@ -0,0 +1,40 @@
1
+ import type { CSSProperties } from 'vue'
2
+ import { allocateWidths, columnBounds } from './column-sizing'
3
+ import type { DataTableColumn } from './types'
4
+
5
+ export function columnSizingStyles<T extends object>(
6
+ columns: DataTableColumn<T>[],
7
+ overrides: Record<string, number>,
8
+ controls: number,
9
+ ): CSSProperties {
10
+ const initial = allocateWidths(columns, overrides, 0)
11
+ const total = Object.values(initial).reduce((sum, width) => sum + width, controls)
12
+ const flexible = columns.filter(
13
+ column =>
14
+ !column.pin &&
15
+ overrides[column.key] === undefined &&
16
+ initial[column.key]! < columnBounds(column).max,
17
+ )
18
+ const capacities = flexible
19
+ .map(column => columnBounds(column).max - initial[column.key]!)
20
+ .sort((a, b) => a - b)
21
+ let consumed = 0
22
+ const shares = capacities.map((capacity, index) => {
23
+ const share = `calc((100cqi - ${total + consumed}px) / ${capacities.length - index})`
24
+ consumed += capacity
25
+ return share
26
+ })
27
+ const keys = new Set(flexible.map(column => column.key))
28
+ return {
29
+ width: shares.length ? `clamp(${total}px, 100%, ${total + consumed}px)` : `${total}px`,
30
+ '--hn-table-column-grow': shares.length ? `max(0px, ${shares.join(', ')})` : '0px',
31
+ ...Object.fromEntries(
32
+ columns.map((column, index) => [
33
+ `--hn-table-column-${index}`,
34
+ keys.has(column.key)
35
+ ? `min(${columnBounds(column).max}px, calc(${initial[column.key]}px + var(--hn-table-column-grow)))`
36
+ : `${initial[column.key]}px`,
37
+ ]),
38
+ ),
39
+ }
40
+ }
@@ -9,6 +9,7 @@ import {
9
9
  } from 'vue'
10
10
  import { cssSize } from '../utils'
11
11
  import { allocateWidths, pixelWidth } from '../column-sizing'
12
+ import { columnSizingStyles } from '../column-sizing-styles'
12
13
  import { useTableResize } from './useTableResize'
13
14
  import { useTableResizeHandles } from './useTableResizeHandles'
14
15
  import type { DataTableColumn, DataTableHeader, DataTableProps } from '../types'
@@ -200,15 +201,11 @@ export function useTableColumns<T extends object>(
200
201
  '--hn-table-viewport-width': available.value ? `${available.value}px` : undefined,
201
202
  tableLayout: constrained.value ? 'fixed' : 'auto',
202
203
  ...(constrained.value
203
- ? {
204
- ...Object.fromEntries(
205
- ctl.visibleColumns.value.map((column, index) => [
206
- `--hn-table-column-${index}`,
207
- `${widths.value[column.key]}px`,
208
- ]),
209
- ),
210
- width: `${ctl.visibleColumns.value.reduce((sum, column) => sum + width(column), leading.value * 48 + trailing.value * 72)}px`,
211
- }
204
+ ? columnSizingStyles(
205
+ ctl.visibleColumns.value,
206
+ active.value ?? models.columnWidths.value,
207
+ leading.value * 48 + trailing.value * 72,
208
+ )
212
209
  : {}),
213
210
  }))
214
211
  function moveColumn(key: string, target: string) {
@@ -240,6 +237,7 @@ export function useTableColumns<T extends object>(
240
237
  observer?.disconnect()
241
238
  })
242
239
  return {
240
+ constrained,
243
241
  headerRows,
244
242
  tableStyle,
245
243
  cellStyle,
@@ -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
+ }