@hina-ui/vue 1.5.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 +831 -390
- 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/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/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 +3 -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 +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/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/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 +3 -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,8 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts" generic="T extends SelectOption = SelectOption">
|
|
2
|
-
import { computed
|
|
2
|
+
import { computed } from 'vue'
|
|
3
3
|
import { ComboboxAnchor, ComboboxInput, ComboboxRoot, ComboboxTrigger } from 'reka-ui'
|
|
4
4
|
import { cn } from '../../lib/cn'
|
|
5
|
-
import { focusFieldFrom } from '../../lib/field-focus'
|
|
6
5
|
import { useUiLocale } from '../../locale'
|
|
7
6
|
import { X } from '@lucide/vue'
|
|
8
7
|
import InputAction from '../input/InputAction.vue'
|
|
@@ -18,13 +17,15 @@
|
|
|
18
17
|
inputIndicator,
|
|
19
18
|
type InputVariants,
|
|
20
19
|
} from '../input/input.variants'
|
|
21
|
-
import {
|
|
20
|
+
import type { SelectItems, SelectOption } from '../select/types'
|
|
21
|
+
import { useCombobox } from './composables/useCombobox'
|
|
22
22
|
import ComboboxList from './ComboboxList.vue'
|
|
23
23
|
|
|
24
24
|
defineOptions({ name: 'HnCombobox', inheritAttrs: false })
|
|
25
25
|
|
|
26
26
|
const props = defineProps<{
|
|
27
27
|
options: SelectItems<T>
|
|
28
|
+
selectedOption?: T | null
|
|
28
29
|
placeholder?: string
|
|
29
30
|
ignoreFilter?: boolean
|
|
30
31
|
clearable?: boolean
|
|
@@ -44,8 +45,6 @@
|
|
|
44
45
|
|
|
45
46
|
const t = useUiLocale()
|
|
46
47
|
const group = injectInputGroup()
|
|
47
|
-
const keyboard = ref(false)
|
|
48
|
-
const input = shallowRef<{ $el: HTMLInputElement } | null>(null)
|
|
49
48
|
|
|
50
49
|
const size = computed(() => (group ? group.size.value : props.size))
|
|
51
50
|
const {
|
|
@@ -61,23 +60,14 @@
|
|
|
61
60
|
() => !!props.clearable && model.value != null && model.value !== '' && !disabled.value,
|
|
62
61
|
)
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
emit('clear')
|
|
72
|
-
input.value?.$el.focus()
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
watch(search, value => {
|
|
76
|
-
if (value === '' && model.value != null) model.value = null
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
watch(open, value => {
|
|
80
|
-
if (!value) keyboard.value = false
|
|
63
|
+
const { keyboard, input, displayValue, clear, onInput, onHostClick } = useCombobox({
|
|
64
|
+
options: () => props.options,
|
|
65
|
+
selectedOption: () => props.selectedOption,
|
|
66
|
+
model,
|
|
67
|
+
search,
|
|
68
|
+
open,
|
|
69
|
+
disabled: () => disabled.value,
|
|
70
|
+
onClear: () => emit('clear'),
|
|
81
71
|
})
|
|
82
72
|
</script>
|
|
83
73
|
|
|
@@ -100,7 +90,7 @@
|
|
|
100
90
|
props.class,
|
|
101
91
|
)
|
|
102
92
|
"
|
|
103
|
-
@click="
|
|
93
|
+
@click="onHostClick"
|
|
104
94
|
@keydown="keyboard = true"
|
|
105
95
|
>
|
|
106
96
|
<ComboboxInput
|
|
@@ -114,6 +104,7 @@
|
|
|
114
104
|
:disabled="disabled"
|
|
115
105
|
:aria-invalid="invalid || undefined"
|
|
116
106
|
:class="inputControl({ trailing: true })"
|
|
107
|
+
@input="onInput"
|
|
117
108
|
/>
|
|
118
109
|
<Transition
|
|
119
110
|
enter-active-class="hn-transition-base"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { computed, ref, shallowRef, watch, type Ref } from 'vue'
|
|
2
|
+
import { focusFieldFrom } from '../../../lib/field-focus'
|
|
3
|
+
import type { SelectItems, SelectOption } from '../../select/types'
|
|
4
|
+
import { useOptionLabels } from './useOptionLabels'
|
|
5
|
+
|
|
6
|
+
interface ComboboxOptions<T extends SelectOption> {
|
|
7
|
+
options: () => SelectItems<T>
|
|
8
|
+
selectedOption: () => T | null | undefined
|
|
9
|
+
model: Ref<string | number | null | undefined>
|
|
10
|
+
search: Ref<string>
|
|
11
|
+
open: Ref<boolean>
|
|
12
|
+
disabled: () => boolean
|
|
13
|
+
onClear: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useCombobox<T extends SelectOption>(options: ComboboxOptions<T>) {
|
|
17
|
+
const { model, search, open } = options
|
|
18
|
+
const labels = useOptionLabels(options.options, () => {
|
|
19
|
+
const option = options.selectedOption()
|
|
20
|
+
return option ? [option] : []
|
|
21
|
+
})
|
|
22
|
+
const label = computed(() => displayValue(model.value))
|
|
23
|
+
const editing = ref(false)
|
|
24
|
+
const keyboard = ref(false)
|
|
25
|
+
const input = shallowRef<{ $el: HTMLInputElement } | null>(null)
|
|
26
|
+
|
|
27
|
+
function displayValue(value: string | number | null | undefined) {
|
|
28
|
+
return value == null ? '' : (labels.get(value) ?? '')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clear() {
|
|
32
|
+
model.value = null
|
|
33
|
+
search.value = ''
|
|
34
|
+
options.onClear()
|
|
35
|
+
input.value?.$el.focus()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function onInput(event: Event) {
|
|
39
|
+
if (options.disabled()) return
|
|
40
|
+
editing.value = true
|
|
41
|
+
if ((event as InputEvent).isComposing) return
|
|
42
|
+
if ((event.target as HTMLInputElement).value === '' && model.value != null) model.value = null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function onHostClick(event: MouseEvent) {
|
|
46
|
+
focusFieldFrom(event.currentTarget as HTMLElement, event.target as HTMLElement)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
watch(open, value => {
|
|
50
|
+
if (!value) {
|
|
51
|
+
keyboard.value = false
|
|
52
|
+
editing.value = false
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
watch(model, () => (editing.value = false), { flush: 'sync' })
|
|
57
|
+
|
|
58
|
+
watch(label, value => {
|
|
59
|
+
if (!editing.value) search.value = value
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return { keyboard, input, displayValue, clear, onInput, onHostClick }
|
|
63
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { reactive, watch } from 'vue'
|
|
2
|
+
import { flattenOptions, type SelectItems, type SelectOption } from '../../select/types'
|
|
3
|
+
|
|
4
|
+
export function useOptionLabels<T extends SelectOption>(
|
|
5
|
+
options: () => SelectItems<T>,
|
|
6
|
+
selectedOptions: () => T[] | undefined,
|
|
7
|
+
) {
|
|
8
|
+
const labels = reactive(new Map<string | number, string>())
|
|
9
|
+
|
|
10
|
+
watch(
|
|
11
|
+
[options, selectedOptions],
|
|
12
|
+
([items, selected]) => {
|
|
13
|
+
for (const option of flattenOptions(items)) labels.set(option.value, option.label)
|
|
14
|
+
for (const option of selected ?? []) labels.set(option.value, option.label)
|
|
15
|
+
},
|
|
16
|
+
{ immediate: true, deep: true },
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return labels
|
|
20
|
+
}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
const props = withDefaults(
|
|
34
34
|
defineProps<{
|
|
35
35
|
options: SelectItems<T>
|
|
36
|
+
selectedOptions?: T[]
|
|
36
37
|
placeholder?: string
|
|
37
38
|
ignoreFilter?: boolean
|
|
38
39
|
loading?: boolean
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
const { selected, keyboard, input, remove, clear, onHostClick, onInputKeydown } =
|
|
70
71
|
useMultiCombobox({
|
|
71
72
|
options: () => props.options,
|
|
73
|
+
selectedOptions: () => props.selectedOptions,
|
|
72
74
|
model,
|
|
73
75
|
open,
|
|
74
76
|
disabled: () => disabled.value,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { computed,
|
|
1
|
+
import { computed, ref, shallowRef, watch, type Ref } from 'vue'
|
|
2
2
|
import { focusFieldFrom } from '../../../lib/field-focus'
|
|
3
|
-
import {
|
|
3
|
+
import type { SelectItems, SelectOption } from '../../select/types'
|
|
4
|
+
import { useOptionLabels } from '../../combobox/composables/useOptionLabels'
|
|
4
5
|
|
|
5
6
|
interface MultiComboboxOptions<T extends SelectOption> {
|
|
6
7
|
options: () => SelectItems<T>
|
|
8
|
+
selectedOptions: () => T[] | undefined
|
|
7
9
|
model: Ref<Array<string | number>>
|
|
8
10
|
open: Ref<boolean>
|
|
9
11
|
disabled: () => boolean
|
|
@@ -14,14 +16,7 @@ export function useMultiCombobox<T extends SelectOption>(options: MultiComboboxO
|
|
|
14
16
|
const { model, open } = options
|
|
15
17
|
const keyboard = ref(false)
|
|
16
18
|
const input = shallowRef<{ $el: HTMLInputElement } | null>(null)
|
|
17
|
-
const labels =
|
|
18
|
-
watch(
|
|
19
|
-
options.options,
|
|
20
|
-
options => {
|
|
21
|
-
for (const option of flattenOptions(options)) labels.set(option.value, option.label)
|
|
22
|
-
},
|
|
23
|
-
{ immediate: true, deep: true },
|
|
24
|
-
)
|
|
19
|
+
const labels = useOptionLabels(options.options, options.selectedOptions)
|
|
25
20
|
|
|
26
21
|
const selected = computed(() =>
|
|
27
22
|
model.value.map(value => ({ value, label: labels.get(value) ?? String(value) })),
|
|
@@ -42,7 +42,13 @@
|
|
|
42
42
|
:class="cn(navLink({ active: props.active }), props.class)"
|
|
43
43
|
>
|
|
44
44
|
<slot name="icon" />
|
|
45
|
-
<span
|
|
45
|
+
<span
|
|
46
|
+
:aria-hidden="rail ? 'true' : undefined"
|
|
47
|
+
:inert="rail"
|
|
48
|
+
:data-collapsed="rail ? '' : undefined"
|
|
49
|
+
data-hn-label
|
|
50
|
+
:class="navLinkLabel()"
|
|
51
|
+
>
|
|
46
52
|
<slot />
|
|
47
53
|
</span>
|
|
48
54
|
</Primitive>
|
|
@@ -17,14 +17,5 @@ export const navLink = tv({
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
export const navLinkLabel = tv({
|
|
20
|
-
base: 'shrink-0 whitespace-nowrap',
|
|
21
|
-
variants: {
|
|
22
|
-
rail: {
|
|
23
|
-
true: 'opacity-0 [transition:opacity_calc(var(--hn-duration-fast)/2)_var(--hn-ease-exit)]',
|
|
24
|
-
false: 'opacity-100 [transition:opacity_var(--hn-duration-fast)_var(--hn-ease-enter)]',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
defaultVariants: { rail: false },
|
|
20
|
+
base: 'hn-sidebar-label shrink-0 whitespace-nowrap',
|
|
28
21
|
})
|
|
29
|
-
|
|
30
|
-
export type NavLinkVariants = VariantProps<typeof navLink>
|
|
@@ -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>
|