@hina-ui/vue 1.4.0 → 1.6.0
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 +1222 -599
- package/dist/index.js.map +1 -1
- package/dist/types/components/app-shell/AppShell.vue.d.ts +1 -0
- package/dist/types/components/app-shell/AppShell.vue.d.ts.map +1 -1
- package/dist/types/components/combobox/Combobox.vue.d.ts +2 -1
- package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
- package/dist/types/components/combobox/composables/useCombobox.d.ts +25 -0
- package/dist/types/components/combobox/composables/useCombobox.d.ts.map +1 -0
- package/dist/types/components/combobox/composables/useOptionLabels.d.ts +3 -0
- package/dist/types/components/combobox/composables/useOptionLabels.d.ts.map +1 -0
- package/dist/types/components/dialog/Dialog.vue.d.ts +3 -1
- package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
- package/dist/types/components/drawer/Drawer.vue.d.ts +21 -14
- package/dist/types/components/drawer/Drawer.vue.d.ts.map +1 -1
- package/dist/types/components/drawer/drawer.variants.d.ts +10 -1
- package/dist/types/components/drawer/drawer.variants.d.ts.map +1 -1
- package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts +4 -1
- package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts.map +1 -1
- package/dist/types/components/hover-card/HoverCard.vue.d.ts +4 -1
- package/dist/types/components/hover-card/HoverCard.vue.d.ts.map +1 -1
- package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts +6 -4
- package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts.map +1 -1
- package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts +9 -4
- package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts.map +1 -1
- package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts +1 -0
- package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts.map +1 -1
- package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts +2 -1
- package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts.map +1 -1
- package/dist/types/components/nav-link/NavLink.vue.d.ts.map +1 -1
- package/dist/types/components/nav-link/nav-link.variants.d.ts +1 -18
- package/dist/types/components/nav-link/nav-link.variants.d.ts.map +1 -1
- package/dist/types/components/popover/Popover.vue.d.ts +4 -1
- package/dist/types/components/popover/Popover.vue.d.ts.map +1 -1
- package/dist/types/components/sheet/Sheet.vue.d.ts +21 -14
- package/dist/types/components/sheet/Sheet.vue.d.ts.map +1 -1
- package/dist/types/components/sheet/sheet.variants.d.ts +54 -3
- package/dist/types/components/sheet/sheet.variants.d.ts.map +1 -1
- package/dist/types/components/sidebar/Sidebar.vue.d.ts +15 -11
- package/dist/types/components/sidebar/Sidebar.vue.d.ts.map +1 -1
- package/dist/types/components/sidebar/SidebarGroup.vue.d.ts.map +1 -1
- package/dist/types/components/sidebar/SidebarLabel.vue.d.ts +20 -0
- package/dist/types/components/sidebar/SidebarLabel.vue.d.ts.map +1 -0
- package/dist/types/components/sidebar/sidebar.variants.d.ts +46 -0
- package/dist/types/components/sidebar/sidebar.variants.d.ts.map +1 -0
- package/dist/types/components/tree/Tree.vue.d.ts +37 -0
- package/dist/types/components/tree/Tree.vue.d.ts.map +1 -0
- package/dist/types/components/tree/TreeCheck.vue.d.ts +8 -0
- package/dist/types/components/tree/TreeCheck.vue.d.ts.map +1 -0
- package/dist/types/components/tree/composables/useTree.d.ts +23 -0
- package/dist/types/components/tree/composables/useTree.d.ts.map +1 -0
- package/dist/types/components/tree/tree.variants.d.ts +5 -0
- package/dist/types/components/tree/tree.variants.d.ts.map +1 -0
- package/dist/types/components/tree/types.d.ts +18 -0
- package/dist/types/components/tree/types.d.ts.map +1 -0
- package/dist/types/components/tree/utils/selection.d.ts +16 -0
- package/dist/types/components/tree/utils/selection.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/anchored-overlay.d.ts +12 -2
- package/dist/types/lib/anchored-overlay.d.ts.map +1 -1
- package/dist/types/lib/overlay-anchor.d.ts +16 -0
- package/dist/types/lib/overlay-anchor.d.ts.map +1 -0
- package/dist/types/lib/scroll-viewport.d.ts +9 -0
- package/dist/types/lib/scroll-viewport.d.ts.map +1 -0
- 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 +3 -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/app-shell/AppShell.vue +2 -1
- package/src/components/combobox/Combobox.vue +14 -23
- package/src/components/combobox/composables/useCombobox.ts +63 -0
- package/src/components/combobox/composables/useOptionLabels.ts +20 -0
- package/src/components/dialog/Dialog.vue +5 -1
- package/src/components/drawer/Drawer.vue +69 -32
- package/src/components/drawer/drawer.variants.ts +6 -2
- package/src/components/dropdown-menu/DropdownMenu.vue +18 -5
- package/src/components/hover-card/HoverCard.vue +6 -1
- package/src/components/hover-card/HoverCardAnchor.vue +3 -1
- package/src/components/hover-card/composables/useHoverCardAnchor.ts +33 -42
- package/src/components/multi-combobox/MultiCombobox.vue +2 -0
- package/src/components/multi-combobox/composables/useMultiCombobox.ts +5 -10
- package/src/components/nav-link/NavLink.vue +7 -1
- package/src/components/nav-link/nav-link.variants.ts +1 -10
- package/src/components/popover/Popover.vue +17 -7
- package/src/components/sheet/Sheet.vue +67 -24
- package/src/components/sheet/sheet.variants.ts +17 -4
- package/src/components/sidebar/Sidebar.vue +43 -33
- package/src/components/sidebar/SidebarGroup.vue +12 -11
- package/src/components/sidebar/SidebarLabel.vue +24 -0
- package/src/components/sidebar/sidebar.variants.ts +36 -0
- package/src/components/tree/Tree.vue +132 -0
- package/src/components/tree/TreeCheck.vue +33 -0
- package/src/components/tree/composables/useTree.ts +81 -0
- package/src/components/tree/tree.variants.ts +22 -0
- package/src/components/tree/types.ts +20 -0
- package/src/components/tree/utils/selection.ts +78 -0
- package/src/index.ts +4 -0
- package/src/lib/anchored-overlay.ts +28 -19
- package/src/lib/overlay-anchor.ts +65 -0
- package/src/lib/scroll-viewport.ts +8 -0
- package/src/locale/en-US.ts +1 -0
- package/src/locale/index.ts +1 -0
- package/src/locale/types.ts +3 -0
- package/src/locale/zh-CN.ts +1 -0
- package/src/styles/motion.css +40 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed
|
|
2
|
+
import { computed } from 'vue'
|
|
3
3
|
import {
|
|
4
4
|
DialogClose,
|
|
5
5
|
DialogContent,
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
DialogTitle,
|
|
11
11
|
DialogTrigger,
|
|
12
12
|
} from 'reka-ui'
|
|
13
|
+
import { useOverlayPortal } from '../../lib/overlay-portal'
|
|
14
|
+
import { useScrollViewport } from '../../lib/scroll-viewport'
|
|
13
15
|
import { cn } from '../../lib/cn'
|
|
14
|
-
import { useUiLocale } from '../../locale'
|
|
15
16
|
import Card from '../card/Card.vue'
|
|
16
17
|
import CloseButton from '../close-button/CloseButton.vue'
|
|
17
18
|
import Heading from '../heading/Heading.vue'
|
|
@@ -26,16 +27,29 @@
|
|
|
26
27
|
defineProps<{
|
|
27
28
|
title: string
|
|
28
29
|
description?: string
|
|
30
|
+
header?: boolean
|
|
31
|
+
closable?: boolean
|
|
29
32
|
handle?: boolean
|
|
30
33
|
locked?: boolean
|
|
31
34
|
class?: string
|
|
32
35
|
}>(),
|
|
33
|
-
{ handle: true, locked: false },
|
|
36
|
+
{ header: true, closable: true, handle: true, locked: false },
|
|
34
37
|
)
|
|
35
38
|
|
|
39
|
+
const slots = defineSlots<{
|
|
40
|
+
default?(): unknown
|
|
41
|
+
icon?(): unknown
|
|
42
|
+
title?(): unknown
|
|
43
|
+
body?(props: { close: () => void }): unknown
|
|
44
|
+
content?(props: { close: () => void }): unknown
|
|
45
|
+
footer?(props: { close: () => void }): unknown
|
|
46
|
+
}>()
|
|
47
|
+
|
|
48
|
+
const { scrollArea, viewport } = useScrollViewport()
|
|
49
|
+
defineExpose({ viewport })
|
|
50
|
+
|
|
36
51
|
const open = defineModel<boolean>('open')
|
|
37
|
-
const
|
|
38
|
-
const panel = shallowRef<{ $el: HTMLElement } | null>(null)
|
|
52
|
+
const { content: panel, present } = useOverlayPortal(open)
|
|
39
53
|
|
|
40
54
|
const { dragging, offset, onPointerDown } = useDragToDismiss(() => panel.value?.$el ?? null, {
|
|
41
55
|
enabled: () => !props.locked,
|
|
@@ -63,54 +77,83 @@
|
|
|
63
77
|
<DialogTrigger v-if="$slots.default" as-child>
|
|
64
78
|
<slot />
|
|
65
79
|
</DialogTrigger>
|
|
66
|
-
<DialogPortal>
|
|
80
|
+
<DialogPortal v-if="present">
|
|
67
81
|
<DialogOverlay class="hn-scrim" />
|
|
68
82
|
<DialogContent as-child @escape-key-down="guard" @interact-outside="guard">
|
|
69
83
|
<Card
|
|
70
84
|
ref="panel"
|
|
85
|
+
v-bind="props.description ? {} : { 'aria-describedby': undefined }"
|
|
71
86
|
data-hn-sheet
|
|
72
87
|
:data-dragging="dragging ? '' : undefined"
|
|
73
88
|
:padded="false"
|
|
74
89
|
:style="style"
|
|
75
|
-
:class="
|
|
90
|
+
:class="
|
|
91
|
+
cn(sheetPanel({ grip: props.header || props.handle, padded: !slots.body }), props.class)
|
|
92
|
+
"
|
|
76
93
|
>
|
|
94
|
+
<template v-if="!props.header || slots.body">
|
|
95
|
+
<DialogTitle as-child>
|
|
96
|
+
<Heading :level="2" class="sr-only">{{ props.title }}</Heading>
|
|
97
|
+
</DialogTitle>
|
|
98
|
+
<DialogDescription v-if="props.description" class="sr-only">
|
|
99
|
+
{{ props.description }}
|
|
100
|
+
</DialogDescription>
|
|
101
|
+
</template>
|
|
77
102
|
<div
|
|
103
|
+
v-if="props.handle || (props.header && !slots.body)"
|
|
78
104
|
data-hn-sheet-grip
|
|
79
105
|
:data-dragging="dragging ? '' : undefined"
|
|
80
|
-
:class="sheetGrip()"
|
|
106
|
+
:class="sheetGrip({ standalone: !!slots.body })"
|
|
81
107
|
@pointerdown="onPointerDown"
|
|
82
108
|
>
|
|
83
109
|
<div
|
|
84
110
|
v-if="props.handle"
|
|
85
111
|
aria-hidden="true"
|
|
86
112
|
:data-disabled="props.locked ? '' : undefined"
|
|
87
|
-
:class="sheetHandle()"
|
|
113
|
+
:class="sheetHandle({ header: props.header && !slots.body })"
|
|
88
114
|
/>
|
|
89
|
-
<div
|
|
115
|
+
<div
|
|
116
|
+
v-if="props.header && !slots.body"
|
|
117
|
+
class="flex items-start justify-between gap-4 px-(--hn-panel-p)"
|
|
118
|
+
>
|
|
90
119
|
<div class="flex min-w-0 flex-col gap-1.5">
|
|
91
|
-
<
|
|
92
|
-
<
|
|
93
|
-
|
|
120
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
121
|
+
<span
|
|
122
|
+
v-if="slots.icon"
|
|
123
|
+
class="text-muted flex shrink-0 [&_svg]:size-5"
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
>
|
|
126
|
+
<slot name="icon" />
|
|
127
|
+
</span>
|
|
128
|
+
<DialogTitle as-child>
|
|
129
|
+
<Heading :level="2" size="lg" class="min-w-0">
|
|
130
|
+
<slot name="title">{{ props.title }}</slot>
|
|
131
|
+
</Heading>
|
|
132
|
+
</DialogTitle>
|
|
133
|
+
</div>
|
|
94
134
|
<DialogDescription v-if="props.description" as-child>
|
|
95
135
|
<Text tone="muted">{{ props.description }}</Text>
|
|
96
136
|
</DialogDescription>
|
|
97
137
|
</div>
|
|
98
|
-
<DialogClose v-if="!props.handle" as-child>
|
|
138
|
+
<DialogClose v-if="!props.handle && props.closable" as-child>
|
|
99
139
|
<CloseButton :disabled="props.locked" class="-mt-1.5 -me-1.5 shrink-0" />
|
|
100
140
|
</DialogClose>
|
|
101
141
|
</div>
|
|
102
142
|
</div>
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
143
|
+
<slot v-if="slots.body" name="body" :close="close" />
|
|
144
|
+
<template v-else>
|
|
145
|
+
<ScrollArea v-if="$slots.content" ref="scrollArea" class="min-h-0 grow">
|
|
146
|
+
<div class="px-(--hn-panel-p) py-1">
|
|
147
|
+
<slot name="content" :close="close" />
|
|
148
|
+
</div>
|
|
149
|
+
</ScrollArea>
|
|
150
|
+
<div
|
|
151
|
+
v-if="$slots.footer"
|
|
152
|
+
class="flex shrink-0 justify-end gap-(--hn-inline-gap) px-(--hn-panel-p)"
|
|
153
|
+
>
|
|
154
|
+
<slot name="footer" :close="close" />
|
|
106
155
|
</div>
|
|
107
|
-
</
|
|
108
|
-
<div
|
|
109
|
-
v-if="$slots.footer"
|
|
110
|
-
class="flex shrink-0 justify-end gap-(--hn-inline-gap) px-(--hn-panel-p)"
|
|
111
|
-
>
|
|
112
|
-
<slot name="footer" :close="close" />
|
|
113
|
-
</div>
|
|
156
|
+
</template>
|
|
114
157
|
</Card>
|
|
115
158
|
</DialogContent>
|
|
116
159
|
</DialogPortal>
|
|
@@ -3,17 +3,30 @@ import { tv } from '../../lib/tv'
|
|
|
3
3
|
export const sheetPanel = tv({
|
|
4
4
|
base: [
|
|
5
5
|
'hn-anim-sheet-bottom hn-transition pointer-events-auto fixed inset-x-0 bottom-0 z-(--hn-z-overlay)',
|
|
6
|
-
'mx-auto flex max-h-[calc(100dvh-3rem)] w-full flex-col
|
|
7
|
-
'rounded-t-xl rounded-b-none border-b-0
|
|
8
|
-
'pb-[max(var(--hn-panel-p),env(safe-area-inset-bottom))]',
|
|
6
|
+
'mx-auto flex max-h-[calc(100dvh-3rem)] w-full flex-col sm:max-w-xl',
|
|
7
|
+
'rounded-t-xl rounded-b-none border-b-0 shadow-lg outline-none',
|
|
9
8
|
'data-dragging:transition-none',
|
|
10
9
|
],
|
|
10
|
+
variants: {
|
|
11
|
+
padded: { true: 'gap-4 pb-[max(var(--hn-panel-p),env(safe-area-inset-bottom))]' },
|
|
12
|
+
grip: { true: '', false: '' },
|
|
13
|
+
},
|
|
14
|
+
compoundVariants: [
|
|
15
|
+
{ padded: true, grip: true, class: 'pt-2' },
|
|
16
|
+
{ padded: true, grip: false, class: 'pt-(--hn-panel-p)' },
|
|
17
|
+
],
|
|
18
|
+
defaultVariants: { grip: true, padded: true },
|
|
11
19
|
})
|
|
12
20
|
|
|
13
21
|
export const sheetGrip = tv({
|
|
14
22
|
base: 'flex shrink-0 cursor-grab touch-none flex-col select-none data-dragging:cursor-grabbing',
|
|
23
|
+
variants: { standalone: { true: 'py-2' } },
|
|
15
24
|
})
|
|
16
25
|
|
|
17
26
|
export const sheetHandle = tv({
|
|
18
|
-
base: 'bg-line-strong hn-transition-base mx-auto
|
|
27
|
+
base: 'bg-line-strong hn-transition-base mx-auto h-1.5 w-10 rounded-full data-disabled:opacity-50',
|
|
28
|
+
variants: {
|
|
29
|
+
header: { true: 'mb-3', false: '' },
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: { header: true },
|
|
19
32
|
})
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
|
-
import { Motion } from 'motion-v'
|
|
4
3
|
import { cn } from '../../lib/cn'
|
|
5
|
-
import { TRANSITION, prefersReducedMotion } from '../../motion'
|
|
6
4
|
import { useUiLocale } from '../../locale'
|
|
7
5
|
import ScrollArea from '../scroll-area/ScrollArea.vue'
|
|
8
|
-
import { useSidebar } from './context'
|
|
6
|
+
import { useSidebar, type SidebarState } from './context'
|
|
7
|
+
import SidebarLabel from './SidebarLabel.vue'
|
|
8
|
+
import {
|
|
9
|
+
sidebarRoot,
|
|
10
|
+
sidebarRegion,
|
|
11
|
+
sidebarBrand,
|
|
12
|
+
sidebarIcon,
|
|
13
|
+
sidebarWordmark,
|
|
14
|
+
} from './sidebar.variants'
|
|
9
15
|
|
|
10
16
|
defineOptions({ name: 'HnSidebar' })
|
|
11
17
|
|
|
@@ -14,54 +20,58 @@
|
|
|
14
20
|
class?: string
|
|
15
21
|
}>()
|
|
16
22
|
|
|
23
|
+
defineSlots<{
|
|
24
|
+
default?(): unknown
|
|
25
|
+
header?(props: { state: SidebarState }): unknown
|
|
26
|
+
icon?(props: { state: SidebarState }): unknown
|
|
27
|
+
wordmark?(props: { state: SidebarState }): unknown
|
|
28
|
+
footer?(props: { state: SidebarState }): unknown
|
|
29
|
+
}>()
|
|
30
|
+
|
|
17
31
|
const t = useUiLocale()
|
|
18
32
|
const sidebar = useSidebar()
|
|
19
33
|
const state = computed(() => sidebar?.state.value ?? 'expanded')
|
|
20
34
|
const inDrawer = computed(() => sidebar?.inDrawer ?? false)
|
|
21
|
-
|
|
22
|
-
const width = computed(() => {
|
|
23
|
-
if (state.value === 'rail') return 56
|
|
24
|
-
if (state.value === 'hidden') return 0
|
|
25
|
-
return 256
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const transition = computed(() => (prefersReducedMotion() ? { duration: 0 } : TRANSITION.layout))
|
|
29
35
|
</script>
|
|
30
36
|
|
|
31
37
|
<template>
|
|
32
|
-
<
|
|
33
|
-
as="aside"
|
|
34
|
-
:initial="false"
|
|
35
|
-
:animate="inDrawer ? undefined : { width }"
|
|
36
|
-
:transition="transition"
|
|
38
|
+
<aside
|
|
37
39
|
:data-state="state"
|
|
38
|
-
:
|
|
39
|
-
|
|
40
|
-
'flex h-full min-h-0 shrink-0 flex-col overflow-hidden',
|
|
41
|
-
inDrawer
|
|
42
|
-
? 'w-full'
|
|
43
|
-
: 'border-line border-e [transition:border-color_var(--hn-duration-base)_var(--hn-ease-move)]',
|
|
44
|
-
!inDrawer && state === 'hidden' && 'border-e-transparent',
|
|
45
|
-
props.class,
|
|
46
|
-
)
|
|
47
|
-
"
|
|
40
|
+
:inert="state === 'hidden'"
|
|
41
|
+
:class="cn(sidebarRoot({ inDrawer }), props.class)"
|
|
48
42
|
>
|
|
49
|
-
<div v-if="$slots.header" :class="
|
|
43
|
+
<div v-if="$slots.header" :class="sidebarRegion({ inDrawer })">
|
|
50
44
|
<slot name="header" :state="state" />
|
|
51
45
|
</div>
|
|
46
|
+
<div
|
|
47
|
+
v-else-if="$slots.icon || $slots.wordmark"
|
|
48
|
+
class="hn-collapse shrink-0"
|
|
49
|
+
:data-state="state === 'rail' && !$slots.icon ? 'closed' : 'open'"
|
|
50
|
+
:inert="state === 'rail' && !$slots.icon"
|
|
51
|
+
>
|
|
52
|
+
<div class="hn-collapse-body">
|
|
53
|
+
<div :class="sidebarRegion({ inDrawer })">
|
|
54
|
+
<div :class="sidebarBrand()">
|
|
55
|
+
<div v-if="$slots.icon" :class="sidebarIcon()">
|
|
56
|
+
<slot name="icon" :state="state" />
|
|
57
|
+
</div>
|
|
58
|
+
<SidebarLabel v-if="$slots.wordmark" as="div" :class="sidebarWordmark()">
|
|
59
|
+
<slot name="wordmark" :state="state" />
|
|
60
|
+
</SidebarLabel>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
52
65
|
<ScrollArea class="min-h-0 flex-1">
|
|
53
66
|
<nav
|
|
54
67
|
:aria-label="props.label ?? t.sidebar.navLabel"
|
|
55
|
-
:class="cn('flex flex-col gap-1', !inDrawer && 'px-2.5
|
|
68
|
+
:class="cn('flex flex-col gap-1 py-2', !inDrawer && 'px-2.5')"
|
|
56
69
|
>
|
|
57
70
|
<slot />
|
|
58
71
|
</nav>
|
|
59
72
|
</ScrollArea>
|
|
60
|
-
<div
|
|
61
|
-
v-if="$slots.footer"
|
|
62
|
-
:class="cn('border-line shrink-0 border-t', !inDrawer && 'px-3 py-3')"
|
|
63
|
-
>
|
|
73
|
+
<div v-if="$slots.footer" :class="sidebarRegion({ inDrawer, footer: true })">
|
|
64
74
|
<slot name="footer" :state="state" />
|
|
65
75
|
</div>
|
|
66
|
-
</
|
|
76
|
+
</aside>
|
|
67
77
|
</template>
|
|
@@ -31,8 +31,13 @@
|
|
|
31
31
|
:class="cn(props.class)"
|
|
32
32
|
@update:open="v => (rail ? undefined : (open = v))"
|
|
33
33
|
>
|
|
34
|
-
<div class="
|
|
35
|
-
<div
|
|
34
|
+
<div class="relative h-(--hn-control-h-sm)">
|
|
35
|
+
<div
|
|
36
|
+
class="hn-sidebar-label"
|
|
37
|
+
:data-collapsed="rail ? '' : undefined"
|
|
38
|
+
:aria-hidden="rail ? 'true' : undefined"
|
|
39
|
+
:inert="rail"
|
|
40
|
+
>
|
|
36
41
|
<CollapsibleTrigger as-child>
|
|
37
42
|
<Button
|
|
38
43
|
variant="ghost"
|
|
@@ -49,15 +54,11 @@
|
|
|
49
54
|
</Button>
|
|
50
55
|
</CollapsibleTrigger>
|
|
51
56
|
</div>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
>
|
|
58
|
-
<div class="hn-collapse-body">
|
|
59
|
-
<div class="border-line mx-2 my-1.5 border-t" />
|
|
60
|
-
</div>
|
|
57
|
+
<div
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
class="hn-sidebar-label border-line pointer-events-none absolute inset-x-2 top-1/2 border-t"
|
|
60
|
+
:data-collapsed="rail ? undefined : ''"
|
|
61
|
+
/>
|
|
61
62
|
</div>
|
|
62
63
|
<CollapsibleContent>
|
|
63
64
|
<div class="flex flex-col gap-0.5 pt-1">
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { cn } from '../../lib/cn'
|
|
4
|
+
import { useSidebar } from './context'
|
|
5
|
+
import { sidebarLabel } from './sidebar.variants'
|
|
6
|
+
|
|
7
|
+
defineOptions({ name: 'HnSidebarLabel' })
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<{ as?: string; class?: string }>(), { as: 'span' })
|
|
10
|
+
const sidebar = useSidebar()
|
|
11
|
+
const collapsed = computed(() => sidebar?.state.value === 'rail')
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<component
|
|
16
|
+
:is="props.as"
|
|
17
|
+
:data-collapsed="collapsed ? '' : undefined"
|
|
18
|
+
:aria-hidden="collapsed ? 'true' : undefined"
|
|
19
|
+
:inert="collapsed"
|
|
20
|
+
:class="cn(sidebarLabel(), props.class)"
|
|
21
|
+
>
|
|
22
|
+
<slot />
|
|
23
|
+
</component>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { tv } from '../../lib/tv'
|
|
2
|
+
|
|
3
|
+
export const sidebarRoot = tv({
|
|
4
|
+
base: 'flex h-full min-h-0 shrink-0 flex-col overflow-hidden',
|
|
5
|
+
variants: {
|
|
6
|
+
inDrawer: {
|
|
7
|
+
true: 'w-full',
|
|
8
|
+
false:
|
|
9
|
+
'hn-sidebar border-line w-64 border-e data-[state=rail]:w-14 data-[state=hidden]:w-0 data-[state=hidden]:border-e-transparent',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const sidebarRegion = tv({
|
|
15
|
+
base: 'shrink-0 py-3',
|
|
16
|
+
variants: {
|
|
17
|
+
inDrawer: { true: 'w-full', false: 'w-[calc(16rem-1px)] px-3' },
|
|
18
|
+
footer: { true: 'border-line border-t' },
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const sidebarLabel = tv({
|
|
23
|
+
base: 'hn-sidebar-label min-w-0',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const sidebarBrand = tv({
|
|
27
|
+
base: 'flex min-h-8 items-center gap-2',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const sidebarIcon = tv({
|
|
31
|
+
base: 'flex size-8 shrink-0 items-center justify-center overflow-hidden [&>*]:size-full [&_img]:size-full [&_img]:object-contain [&_svg]:size-full',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const sidebarWordmark = tv({
|
|
35
|
+
base: 'flex flex-1 items-center [&_img]:max-h-8 [&_img]:max-w-full [&_img]:object-contain [&_svg]:max-h-8 [&_svg]:max-w-full',
|
|
36
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { TreeItem, TreeRoot } from 'reka-ui'
|
|
3
|
+
import { cn } from '../../lib/cn'
|
|
4
|
+
import { useUiLocale } from '../../locale'
|
|
5
|
+
import { useFieldControl } from '../form-field/context'
|
|
6
|
+
import DisclosureIcon from '../disclosure-icon/DisclosureIcon.vue'
|
|
7
|
+
import TreeCheck from './TreeCheck.vue'
|
|
8
|
+
import { useTree } from './composables/useTree'
|
|
9
|
+
import { tree, treeEmpty, treeRow, treeToggle } from './tree.variants'
|
|
10
|
+
import type { TreeNode, TreeNodeSlot, TreeValue } from './types'
|
|
11
|
+
|
|
12
|
+
defineOptions({ name: 'HnTree', inheritAttrs: false })
|
|
13
|
+
const props = withDefaults(
|
|
14
|
+
defineProps<{
|
|
15
|
+
items: TreeNode[]
|
|
16
|
+
multiple?: boolean
|
|
17
|
+
defaultExpanded?: TreeValue[]
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
invalid?: boolean
|
|
20
|
+
class?: string
|
|
21
|
+
}>(),
|
|
22
|
+
{ defaultExpanded: () => [] },
|
|
23
|
+
)
|
|
24
|
+
const model = defineModel<TreeValue | TreeValue[] | null>()
|
|
25
|
+
const expandedModel = defineModel<TreeValue[]>('expanded')
|
|
26
|
+
defineSlots<{
|
|
27
|
+
node?(props: TreeNodeSlot): unknown
|
|
28
|
+
trailing?(props: TreeNodeSlot): unknown
|
|
29
|
+
empty?(): unknown
|
|
30
|
+
}>()
|
|
31
|
+
const t = useUiLocale()
|
|
32
|
+
const { id, labelledBy, describedBy, disabled, invalid } = useFieldControl({
|
|
33
|
+
disabled: () => props.disabled,
|
|
34
|
+
invalid: () => props.invalid,
|
|
35
|
+
})
|
|
36
|
+
const { selected, expanded, key, getChildren, state, select, toggle, keepRowClick } = useTree(
|
|
37
|
+
props,
|
|
38
|
+
model,
|
|
39
|
+
expandedModel,
|
|
40
|
+
disabled,
|
|
41
|
+
)
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div
|
|
46
|
+
v-if="props.items.length === 0"
|
|
47
|
+
v-bind="$attrs"
|
|
48
|
+
:id="id"
|
|
49
|
+
role="status"
|
|
50
|
+
:aria-labelledby="labelledBy"
|
|
51
|
+
:aria-describedby="describedBy"
|
|
52
|
+
data-hn-tree
|
|
53
|
+
:class="cn(treeEmpty(), props.class)"
|
|
54
|
+
>
|
|
55
|
+
<slot name="empty">{{ t.tree.empty }}</slot>
|
|
56
|
+
</div>
|
|
57
|
+
<TreeRoot
|
|
58
|
+
v-else
|
|
59
|
+
v-bind="$attrs"
|
|
60
|
+
:id="id"
|
|
61
|
+
v-slot="{ flattenItems }"
|
|
62
|
+
v-model:expanded="expanded"
|
|
63
|
+
:items="props.items"
|
|
64
|
+
:get-key="key"
|
|
65
|
+
:get-children="getChildren"
|
|
66
|
+
:model-value="props.multiple ? selected : selected[0]"
|
|
67
|
+
:multiple="props.multiple"
|
|
68
|
+
:disabled="disabled"
|
|
69
|
+
:aria-labelledby="labelledBy"
|
|
70
|
+
:aria-describedby="describedBy"
|
|
71
|
+
:aria-invalid="invalid || undefined"
|
|
72
|
+
:aria-disabled="disabled || undefined"
|
|
73
|
+
data-hn-tree
|
|
74
|
+
:data-disabled="disabled ? '' : undefined"
|
|
75
|
+
:data-invalid="invalid ? '' : undefined"
|
|
76
|
+
:class="cn(tree(), props.class)"
|
|
77
|
+
>
|
|
78
|
+
<TreeItem
|
|
79
|
+
v-for="item in flattenItems"
|
|
80
|
+
:key="item._id"
|
|
81
|
+
v-slot="{ isExpanded }"
|
|
82
|
+
v-bind="item.bind"
|
|
83
|
+
:disabled="state(item.value).disabled"
|
|
84
|
+
as-child
|
|
85
|
+
@select="select"
|
|
86
|
+
@toggle="keepRowClick"
|
|
87
|
+
>
|
|
88
|
+
<li
|
|
89
|
+
:aria-selected="props.multiple ? undefined : state(item.value).selected"
|
|
90
|
+
:aria-checked="
|
|
91
|
+
props.multiple
|
|
92
|
+
? state(item.value).indeterminate
|
|
93
|
+
? 'mixed'
|
|
94
|
+
: state(item.value).selected
|
|
95
|
+
: undefined
|
|
96
|
+
"
|
|
97
|
+
:style="{ '--hn-tree-level': item.level - 1 }"
|
|
98
|
+
:class="treeRow()"
|
|
99
|
+
>
|
|
100
|
+
<span
|
|
101
|
+
v-if="item.hasChildren"
|
|
102
|
+
:class="treeToggle()"
|
|
103
|
+
aria-hidden="true"
|
|
104
|
+
data-hn-tree-toggle
|
|
105
|
+
@click.stop="toggle(item.value)"
|
|
106
|
+
>
|
|
107
|
+
<DisclosureIcon direction="end" :open="isExpanded" />
|
|
108
|
+
</span>
|
|
109
|
+
<span v-else class="size-5 shrink-0" aria-hidden="true" />
|
|
110
|
+
<TreeCheck
|
|
111
|
+
v-if="props.multiple"
|
|
112
|
+
:selected="state(item.value).selected"
|
|
113
|
+
:indeterminate="state(item.value).indeterminate"
|
|
114
|
+
/>
|
|
115
|
+
<span class="min-w-0 flex-1">
|
|
116
|
+
<slot name="node" :node="item.value" :expanded="isExpanded" v-bind="state(item.value)">
|
|
117
|
+
<span class="block truncate">{{ item.value.label }}</span>
|
|
118
|
+
<span v-if="item.value.description" class="text-muted block truncate text-xs">
|
|
119
|
+
{{ item.value.description }}
|
|
120
|
+
</span>
|
|
121
|
+
</slot>
|
|
122
|
+
</span>
|
|
123
|
+
<slot
|
|
124
|
+
name="trailing"
|
|
125
|
+
:node="item.value"
|
|
126
|
+
:expanded="isExpanded"
|
|
127
|
+
v-bind="state(item.value)"
|
|
128
|
+
/>
|
|
129
|
+
</li>
|
|
130
|
+
</TreeItem>
|
|
131
|
+
</TreeRoot>
|
|
132
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Check, Minus } from '@lucide/vue'
|
|
3
|
+
import { checkboxBox } from '../checkbox/checkbox.variants'
|
|
4
|
+
|
|
5
|
+
defineOptions({ name: 'HnTreeCheck' })
|
|
6
|
+
const props = defineProps<{ selected: boolean; indeterminate: boolean }>()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<span
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
:data-state="props.indeterminate ? 'indeterminate' : props.selected ? 'checked' : 'unchecked'"
|
|
13
|
+
:class="checkboxBox()"
|
|
14
|
+
class="pointer-events-none"
|
|
15
|
+
>
|
|
16
|
+
<Transition
|
|
17
|
+
enter-active-class="hn-transition-press"
|
|
18
|
+
enter-from-class="scale-50 opacity-0"
|
|
19
|
+
leave-active-class="hn-transition"
|
|
20
|
+
leave-to-class="scale-50 opacity-0"
|
|
21
|
+
>
|
|
22
|
+
<Check v-if="props.selected" />
|
|
23
|
+
</Transition>
|
|
24
|
+
<Transition
|
|
25
|
+
enter-active-class="hn-transition-press"
|
|
26
|
+
enter-from-class="scale-50 opacity-0"
|
|
27
|
+
leave-active-class="hn-transition"
|
|
28
|
+
leave-to-class="scale-50 opacity-0"
|
|
29
|
+
>
|
|
30
|
+
<Minus v-if="props.indeterminate" />
|
|
31
|
+
</Transition>
|
|
32
|
+
</span>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { computed, type Ref } from 'vue'
|
|
2
|
+
import type { TreeNode, TreeNodeState, TreeValue } from '../types'
|
|
3
|
+
import { checkedState, indexTree, toggleChecked } from '../utils/selection'
|
|
4
|
+
|
|
5
|
+
interface Options {
|
|
6
|
+
items: TreeNode[]
|
|
7
|
+
multiple?: boolean
|
|
8
|
+
defaultExpanded: TreeValue[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useTree(
|
|
12
|
+
props: Options,
|
|
13
|
+
model: Ref<TreeValue | TreeValue[] | null | undefined>,
|
|
14
|
+
expandedModel: Ref<TreeValue[] | undefined>,
|
|
15
|
+
disabled: Ref<boolean>,
|
|
16
|
+
) {
|
|
17
|
+
const initialExpanded = [...props.defaultExpanded]
|
|
18
|
+
const index = computed(() => indexTree(props.items))
|
|
19
|
+
const states = computed(() => {
|
|
20
|
+
if (props.multiple)
|
|
21
|
+
return checkedState(index.value, Array.isArray(model.value) ? model.value : [])
|
|
22
|
+
return new Map(
|
|
23
|
+
[...index.value.entries].map(([value, entry]) => [
|
|
24
|
+
value,
|
|
25
|
+
{
|
|
26
|
+
selected: value === model.value,
|
|
27
|
+
indeterminate: false,
|
|
28
|
+
disabled: entry.disabled,
|
|
29
|
+
},
|
|
30
|
+
]),
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
const selected = computed(() =>
|
|
34
|
+
[...index.value.entries.values()]
|
|
35
|
+
.filter(entry => states.value.get(entry.node.value)?.selected)
|
|
36
|
+
.map(entry => entry.node),
|
|
37
|
+
)
|
|
38
|
+
const keys = computed(
|
|
39
|
+
() => new Map([...index.value.entries].map(([value, entry]) => [key(entry.node), value])),
|
|
40
|
+
)
|
|
41
|
+
const expanded = computed({
|
|
42
|
+
get: () => (expandedModel.value ?? initialExpanded).map(value => key({ value })),
|
|
43
|
+
set: value => {
|
|
44
|
+
expandedModel.value = value.flatMap(key =>
|
|
45
|
+
keys.value.has(key) ? [keys.value.get(key)!] : [],
|
|
46
|
+
)
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
function key(node: Pick<TreeNode, 'value'>) {
|
|
51
|
+
return `${typeof node.value}:${node.value}`
|
|
52
|
+
}
|
|
53
|
+
function getChildren(node: TreeNode) {
|
|
54
|
+
return node.children?.length ? node.children : undefined
|
|
55
|
+
}
|
|
56
|
+
function state(node: TreeNode): TreeNodeState {
|
|
57
|
+
const value = states.value.get(node.value)!
|
|
58
|
+
return { ...value, disabled: disabled.value || value.disabled }
|
|
59
|
+
}
|
|
60
|
+
function select(event: CustomEvent<{ value?: TreeNode }>) {
|
|
61
|
+
event.preventDefault()
|
|
62
|
+
const node = event.detail.value
|
|
63
|
+
if (!node || state(node).disabled) return
|
|
64
|
+
model.value = props.multiple
|
|
65
|
+
? toggleChecked(index.value, Array.isArray(model.value) ? model.value : [], node.value)
|
|
66
|
+
: model.value === node.value
|
|
67
|
+
? null
|
|
68
|
+
: node.value
|
|
69
|
+
}
|
|
70
|
+
function toggle(node: TreeNode) {
|
|
71
|
+
if (state(node).disabled || !getChildren(node)) return
|
|
72
|
+
const value = key(node)
|
|
73
|
+
expanded.value = expanded.value.includes(value)
|
|
74
|
+
? expanded.value.filter(key => key !== value)
|
|
75
|
+
: [...expanded.value, value]
|
|
76
|
+
}
|
|
77
|
+
function keepRowClick(event: CustomEvent<{ originalEvent: Event }>) {
|
|
78
|
+
if (event.detail.originalEvent.type === 'click') event.preventDefault()
|
|
79
|
+
}
|
|
80
|
+
return { selected, expanded, key, getChildren, state, select, toggle, keepRowClick }
|
|
81
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { tv } from '../../lib/tv'
|
|
2
|
+
|
|
3
|
+
export const tree = tv({
|
|
4
|
+
base: 'flex w-full min-w-0 flex-col outline-none',
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
export const treeRow = tv({
|
|
8
|
+
base: [
|
|
9
|
+
'hn-interactive hn-state-layer hn-press-none relative isolate flex w-full cursor-pointer',
|
|
10
|
+
'items-center gap-(--hn-control-gap) rounded-md py-2 pe-2.5 text-start text-sm select-none',
|
|
11
|
+
'ps-[calc(0.375rem+var(--hn-tree-level)*1.25rem)] text-fg outline-none',
|
|
12
|
+
'data-disabled:pointer-events-none data-disabled:opacity-50',
|
|
13
|
+
],
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const treeToggle = tv({
|
|
17
|
+
base: 'text-muted flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm rtl:-scale-x-100 [&>span>svg]:size-3.5',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const treeEmpty = tv({
|
|
21
|
+
base: 'text-muted px-2.5 py-2 text-sm',
|
|
22
|
+
})
|