@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,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
|
|
59
|
+
<span v-for="index in count" :key="index" :class="ratingItem()">
|
|
53
60
|
<Star :class="ratingStar()" />
|
|
54
|
-
<span :class="ratingFill()" :style="{ width: starFill(
|
|
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
|
-
|
|
70
|
-
:length="
|
|
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'
|
|
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>
|