@hina-ui/vue 1.7.8 → 1.7.10
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 +230 -59
- package/dist/index.js.map +1 -1
- package/dist/shared/behavior/form.ts +5 -9
- package/dist/shared/lib/command-palette/match.ts +17 -4
- package/dist/shared/lib/scroll-restore.ts +19 -1
- package/dist/shared/lib/virtual/choices.ts +2 -1
- package/dist/shared/variants/command-palette.ts +12 -0
- package/dist/shared/variants/virtual-list.ts +8 -0
- package/dist/types/shared/src/behavior/form.d.ts.map +1 -1
- package/dist/types/shared/src/lib/command-palette/match.d.ts +9 -0
- package/dist/types/shared/src/lib/command-palette/match.d.ts.map +1 -1
- package/dist/types/shared/src/lib/scroll-restore.d.ts +2 -0
- package/dist/types/shared/src/lib/scroll-restore.d.ts.map +1 -1
- package/dist/types/shared/src/lib/virtual/choices.d.ts +4 -0
- package/dist/types/shared/src/lib/virtual/choices.d.ts.map +1 -1
- package/dist/types/shared/src/variants/command-palette.d.ts +3 -0
- package/dist/types/shared/src/variants/command-palette.d.ts.map +1 -1
- package/dist/types/shared/src/variants/virtual-list.d.ts +2 -0
- package/dist/types/shared/src/variants/virtual-list.d.ts.map +1 -1
- package/dist/types/vue/src/components/command-palette/CommandPalette.vue.d.ts +2 -1
- package/dist/types/vue/src/components/command-palette/CommandPalette.vue.d.ts.map +1 -1
- package/dist/types/vue/src/components/command-palette/CommandPalettePanel.vue.d.ts +2 -1
- package/dist/types/vue/src/components/command-palette/CommandPalettePanel.vue.d.ts.map +1 -1
- package/dist/types/vue/src/components/command-palette/composables/useCommandItems.d.ts +6 -1
- package/dist/types/vue/src/components/command-palette/composables/useCommandItems.d.ts.map +1 -1
- package/dist/types/vue/src/components/command-palette/types.d.ts +3 -0
- package/dist/types/vue/src/components/command-palette/types.d.ts.map +1 -1
- package/dist/types/vue/src/components/command-palette/utils/match.d.ts +1 -1
- package/dist/types/vue/src/components/command-palette/utils/match.d.ts.map +1 -1
- package/dist/types/vue/src/components/virtual-list/VirtualChoices.vue.d.ts +4 -1
- package/dist/types/vue/src/components/virtual-list/VirtualChoices.vue.d.ts.map +1 -1
- package/dist/types/vue/src/index.d.ts +2 -2
- package/dist/types/vue/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/command-palette/CommandPalette.vue +8 -0
- package/src/components/command-palette/CommandPalettePanel.vue +85 -3
- package/src/components/command-palette/composables/useCommandItems.ts +19 -13
- package/src/components/command-palette/types.ts +4 -0
- package/src/components/command-palette/utils/match.ts +1 -0
- package/src/components/virtual-list/VirtualChoices.vue +39 -6
- package/src/index.ts +2 -0
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
import {
|
|
15
15
|
commandCard,
|
|
16
16
|
commandEmpty,
|
|
17
|
+
commandGrid,
|
|
18
|
+
commandGridRows,
|
|
19
|
+
commandHeading,
|
|
17
20
|
commandInputRow,
|
|
18
21
|
commandList,
|
|
19
22
|
commandStatus,
|
|
@@ -21,12 +24,14 @@
|
|
|
21
24
|
import { provideCommandPalette } from './context'
|
|
22
25
|
import type {
|
|
23
26
|
CommandEmptySlotProps,
|
|
27
|
+
CommandGroup,
|
|
28
|
+
CommandHeadingSlotProps,
|
|
24
29
|
CommandItem,
|
|
25
30
|
CommandItems,
|
|
26
31
|
CommandItemSlotProps,
|
|
27
32
|
} from './types'
|
|
28
|
-
import { useCommandItems } from './composables/useCommandItems'
|
|
29
|
-
import { commandMatchRange } from './utils/match'
|
|
33
|
+
import { useCommandItems, type CommandOptionGroup } from './composables/useCommandItems'
|
|
34
|
+
import { commandMatchRange, layoutCommandSections } from './utils/match'
|
|
30
35
|
|
|
31
36
|
defineOptions({ name: 'HnCommandPalettePanel', inheritAttrs: false })
|
|
32
37
|
|
|
@@ -44,10 +49,11 @@
|
|
|
44
49
|
|
|
45
50
|
const emit = defineEmits<{ select: [item: CommandItem<T>] }>()
|
|
46
51
|
|
|
47
|
-
defineSlots<{
|
|
52
|
+
const slots = defineSlots<{
|
|
48
53
|
default?(): unknown
|
|
49
54
|
input?(): unknown
|
|
50
55
|
item?(props: CommandItemSlotProps<T>): unknown
|
|
56
|
+
heading?(props: CommandHeadingSlotProps<T>): unknown
|
|
51
57
|
loading?(): unknown
|
|
52
58
|
empty?(props: CommandEmptySlotProps): unknown
|
|
53
59
|
}>()
|
|
@@ -58,6 +64,16 @@
|
|
|
58
64
|
|
|
59
65
|
const input = shallowRef<HTMLElement>()
|
|
60
66
|
const { sections, options } = useCommandItems(props, () => search.value)
|
|
67
|
+
const layout = computed(() => (slots.heading ? layoutCommandSections(sections.value) : undefined))
|
|
68
|
+
|
|
69
|
+
function returnFocus() {
|
|
70
|
+
void nextTick(() => {
|
|
71
|
+
const field = input.value
|
|
72
|
+
const active = document.activeElement
|
|
73
|
+
if (field?.isConnected && (!active || active === document.body || active.contains(field)))
|
|
74
|
+
field.focus({ preventScroll: true })
|
|
75
|
+
})
|
|
76
|
+
}
|
|
61
77
|
|
|
62
78
|
function select(item: CommandItem<T>) {
|
|
63
79
|
const field = input.value
|
|
@@ -124,6 +140,16 @@
|
|
|
124
140
|
</template>
|
|
125
141
|
</CommandPaletteItem>
|
|
126
142
|
</template>
|
|
143
|
+
<template v-if="$slots.heading" #group="{ group }">
|
|
144
|
+
<div
|
|
145
|
+
data-hn-command-heading
|
|
146
|
+
:class="commandHeading()"
|
|
147
|
+
@mousedown.prevent
|
|
148
|
+
@click="returnFocus"
|
|
149
|
+
>
|
|
150
|
+
<slot name="heading" :group="(group as CommandOptionGroup<T>).group" />
|
|
151
|
+
</div>
|
|
152
|
+
</template>
|
|
127
153
|
<template #empty>
|
|
128
154
|
<slot v-if="props.loading" name="loading">{{ t.common.loading }}</slot>
|
|
129
155
|
<slot v-else name="empty" :search="search">{{ t.command.empty }}</slot>
|
|
@@ -131,7 +157,63 @@
|
|
|
131
157
|
</VirtualChoices>
|
|
132
158
|
</ListboxContent>
|
|
133
159
|
<ScrollArea v-else :class="commandList()">
|
|
160
|
+
<div v-if="layout" :class="commandGrid()">
|
|
161
|
+
<ListboxContent
|
|
162
|
+
:aria-label="props.label"
|
|
163
|
+
:aria-busy="props.loading || undefined"
|
|
164
|
+
:class="commandGridRows()"
|
|
165
|
+
:style="{ gridRow: `1 / span ${layout.rows}` }"
|
|
166
|
+
>
|
|
167
|
+
<template v-for="{ section, row, span } in layout.sections" :key="section.key">
|
|
168
|
+
<ListboxGroup
|
|
169
|
+
v-if="section.group"
|
|
170
|
+
:aria-label="section.group.label"
|
|
171
|
+
:aria-labelledby="undefined"
|
|
172
|
+
:class="commandGridRows()"
|
|
173
|
+
:style="{ gridRow: `${row} / span ${span}` }"
|
|
174
|
+
>
|
|
175
|
+
<CommandPaletteItem
|
|
176
|
+
v-for="(match, index) in section.matches"
|
|
177
|
+
:key="match.item.id"
|
|
178
|
+
:match="match"
|
|
179
|
+
:style="{ gridRow: index + 2 }"
|
|
180
|
+
@select="select(match.item)"
|
|
181
|
+
>
|
|
182
|
+
<template v-if="$slots.item" #default>
|
|
183
|
+
<slot name="item" :item="match.item" :match="commandMatchRange(match)" />
|
|
184
|
+
</template>
|
|
185
|
+
</CommandPaletteItem>
|
|
186
|
+
</ListboxGroup>
|
|
187
|
+
<template v-else>
|
|
188
|
+
<CommandPaletteItem
|
|
189
|
+
v-for="(match, index) in section.matches"
|
|
190
|
+
:key="match.item.id"
|
|
191
|
+
:match="match"
|
|
192
|
+
:style="{ gridRow: row + index }"
|
|
193
|
+
@select="select(match.item)"
|
|
194
|
+
>
|
|
195
|
+
<template v-if="$slots.item" #default>
|
|
196
|
+
<slot name="item" :item="match.item" :match="commandMatchRange(match)" />
|
|
197
|
+
</template>
|
|
198
|
+
</CommandPaletteItem>
|
|
199
|
+
</template>
|
|
200
|
+
</template>
|
|
201
|
+
</ListboxContent>
|
|
202
|
+
<template v-for="{ section, row } in layout.sections" :key="section.key">
|
|
203
|
+
<div
|
|
204
|
+
v-if="section.group"
|
|
205
|
+
data-hn-command-heading
|
|
206
|
+
:class="commandHeading()"
|
|
207
|
+
:style="{ gridRow: row, gridColumn: 1 }"
|
|
208
|
+
@mousedown.prevent
|
|
209
|
+
@click="returnFocus"
|
|
210
|
+
>
|
|
211
|
+
<slot name="heading" :group="section.group as CommandGroup<T>" />
|
|
212
|
+
</div>
|
|
213
|
+
</template>
|
|
214
|
+
</div>
|
|
134
215
|
<ListboxContent
|
|
216
|
+
v-else
|
|
135
217
|
:aria-label="props.label"
|
|
136
218
|
:aria-busy="props.loading || undefined"
|
|
137
219
|
:class="selectListBody()"
|
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
import { computed } from 'vue'
|
|
2
|
-
import type { CommandItems } from '../types'
|
|
2
|
+
import type { CommandGroup, CommandItems } from '../types'
|
|
3
3
|
import { filterCommands } from '../utils/match'
|
|
4
4
|
import type { CommandMatch } from '../utils/match'
|
|
5
5
|
import type { SelectItems, SelectOption } from '../../select/types'
|
|
6
6
|
|
|
7
7
|
type CommandOption<T> = SelectOption & { match: CommandMatch<T> }
|
|
8
8
|
|
|
9
|
+
export interface CommandOptionGroup<T> {
|
|
10
|
+
label: string
|
|
11
|
+
options: CommandOption<T>[]
|
|
12
|
+
group: CommandGroup<T>
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export function useCommandItems<T>(
|
|
10
16
|
props: { items: CommandItems<T>; ignoreFilter?: boolean },
|
|
11
17
|
search: () => string,
|
|
12
18
|
) {
|
|
13
19
|
const sections = computed(() => filterCommands(props.items, props.ignoreFilter ? '' : search()))
|
|
14
20
|
const options = computed<SelectItems<CommandOption<T>>>(() =>
|
|
15
|
-
sections.value.flatMap<CommandOption<T> |
|
|
16
|
-
section => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
),
|
|
21
|
+
sections.value.flatMap<CommandOption<T> | CommandOptionGroup<T>>(section => {
|
|
22
|
+
const options = section.matches.map(match => ({
|
|
23
|
+
value: match.item.id,
|
|
24
|
+
label: match.item.label,
|
|
25
|
+
disabled: match.item.disabled,
|
|
26
|
+
description: match.item.description,
|
|
27
|
+
match,
|
|
28
|
+
}))
|
|
29
|
+
return section.group
|
|
30
|
+
? [{ label: section.group.label, options, group: section.group as CommandGroup<T> }]
|
|
31
|
+
: options
|
|
32
|
+
}),
|
|
27
33
|
)
|
|
28
34
|
return { sections, options }
|
|
29
35
|
}
|
|
@@ -32,6 +32,10 @@ export interface CommandEmptySlotProps {
|
|
|
32
32
|
search: string
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface CommandHeadingSlotProps<T = unknown> {
|
|
36
|
+
group: CommandGroup<T>
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
export function isCommandGroup<T>(
|
|
36
40
|
entry: CommandItem<T> | CommandGroup<T>,
|
|
37
41
|
): entry is CommandGroup<T> {
|
|
@@ -4,6 +4,7 @@ import type * as Shared from '../../../../../shared/src/lib/command-palette/matc
|
|
|
4
4
|
export {
|
|
5
5
|
commandMatchRange,
|
|
6
6
|
filterCommands,
|
|
7
|
+
layoutCommandSections,
|
|
7
8
|
} from '../../../../../shared/src/lib/command-palette/match'
|
|
8
9
|
export type CommandMatch<T = unknown> = Shared.CommandMatch<CommandItem<T>>
|
|
9
10
|
export type CommandSection<T = unknown> = Shared.CommandSection<CommandItem<T>>
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts" generic="T extends SelectOption = SelectOption">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { cn } from '../../lib/cn'
|
|
2
4
|
import { useUiLocale } from '../../locale'
|
|
3
5
|
import { useVirtualChoices } from '../../lib/reka/useVirtualChoices'
|
|
4
6
|
import type { VirtualizeOptions } from '../../lib/virtual/types'
|
|
5
7
|
import ScrollArea from '../scroll-area/ScrollArea.vue'
|
|
6
8
|
import { selectEmpty, selectLabel } from '../select/select.variants'
|
|
7
|
-
import type { SelectItems, SelectOption } from '../select/types'
|
|
8
|
-
import {
|
|
9
|
+
import type { SelectItems, SelectOption, SelectOptionGroup } from '../select/types'
|
|
10
|
+
import {
|
|
11
|
+
virtualChoiceHeading,
|
|
12
|
+
virtualChoiceHeadings,
|
|
13
|
+
virtualListContent,
|
|
14
|
+
virtualListItem,
|
|
15
|
+
} from './virtual-list.variants'
|
|
9
16
|
|
|
10
17
|
defineOptions({ name: 'HnVirtualChoices', inheritAttrs: false })
|
|
11
18
|
const props = defineProps<{
|
|
@@ -20,10 +27,15 @@
|
|
|
20
27
|
const t = useUiLocale()
|
|
21
28
|
const { area, body, rows, entries, bodyStyle, measure, itemAttrs, labelId } =
|
|
22
29
|
useVirtualChoices(props)
|
|
23
|
-
defineSlots<{
|
|
30
|
+
const slots = defineSlots<{
|
|
24
31
|
default(props: { option: T; attrs: ReturnType<typeof itemAttrs> }): unknown
|
|
32
|
+
group?(props: { group: SelectOptionGroup<T> }): unknown
|
|
25
33
|
empty?(): unknown
|
|
26
34
|
}>()
|
|
35
|
+
const held = (index: number) => !!slots.group && !rows.value[index]!.option
|
|
36
|
+
const origin = computed(() =>
|
|
37
|
+
entries.value[0] ? entries.value[0].start - parseFloat(bodyStyle.value.paddingBlockStart) : 0,
|
|
38
|
+
)
|
|
27
39
|
</script>
|
|
28
40
|
|
|
29
41
|
<template>
|
|
@@ -34,7 +46,7 @@
|
|
|
34
46
|
maxHeight: typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight,
|
|
35
47
|
}"
|
|
36
48
|
>
|
|
37
|
-
<div :class="props.padded
|
|
49
|
+
<div :class="cn(slots.group && 'relative', props.padded !== false && 'p-1') || undefined">
|
|
38
50
|
<div
|
|
39
51
|
ref="body"
|
|
40
52
|
v-bind="$attrs"
|
|
@@ -45,17 +57,23 @@
|
|
|
45
57
|
<div
|
|
46
58
|
v-for="entry in entries"
|
|
47
59
|
:key="entry.key"
|
|
48
|
-
:ref="measure"
|
|
60
|
+
:ref="held(entry.index) ? undefined : measure"
|
|
49
61
|
:data-index="entry.index"
|
|
50
62
|
role="presentation"
|
|
51
63
|
:class="virtualListItem({ orientation: 'vertical' })"
|
|
52
|
-
:style="{
|
|
64
|
+
:style="{
|
|
65
|
+
marginBlockStart: `${entry.gapBefore}px`,
|
|
66
|
+
height: held(entry.index) ? `${entry.size}px` : undefined,
|
|
67
|
+
}"
|
|
53
68
|
>
|
|
54
69
|
<slot
|
|
55
70
|
v-if="rows[entry.index]!.option"
|
|
56
71
|
:option="rows[entry.index]!.option!"
|
|
57
72
|
:attrs="itemAttrs(entry.index)"
|
|
58
73
|
/>
|
|
74
|
+
<span v-else-if="slots.group" :id="labelId(entry.index)" hidden>
|
|
75
|
+
{{ rows[entry.index]!.label }}
|
|
76
|
+
</span>
|
|
59
77
|
<div v-else :id="labelId(entry.index)" :class="selectLabel()">
|
|
60
78
|
{{ rows[entry.index]!.label }}
|
|
61
79
|
</div>
|
|
@@ -64,6 +82,21 @@
|
|
|
64
82
|
<div v-if="!rows.length" :class="selectEmpty()">
|
|
65
83
|
<slot name="empty">{{ t.select.empty }}</slot>
|
|
66
84
|
</div>
|
|
85
|
+
<div v-else-if="slots.group" :class="virtualChoiceHeadings()">
|
|
86
|
+
<div class="relative">
|
|
87
|
+
<template v-for="entry in entries" :key="entry.key">
|
|
88
|
+
<div
|
|
89
|
+
v-if="rows[entry.index]!.source"
|
|
90
|
+
:ref="measure"
|
|
91
|
+
:data-index="entry.index"
|
|
92
|
+
:class="virtualChoiceHeading()"
|
|
93
|
+
:style="{ top: `${entry.start - origin}px` }"
|
|
94
|
+
>
|
|
95
|
+
<slot name="group" :group="rows[entry.index]!.source!" />
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
67
100
|
</div>
|
|
68
101
|
</ScrollArea>
|
|
69
102
|
</template>
|
package/src/index.ts
CHANGED
|
@@ -82,6 +82,7 @@ export type {
|
|
|
82
82
|
CommandItemMatch,
|
|
83
83
|
CommandItemSlotProps,
|
|
84
84
|
CommandEmptySlotProps,
|
|
85
|
+
CommandHeadingSlotProps,
|
|
85
86
|
} from './components/command-palette/types'
|
|
86
87
|
export { default as Listbox } from './components/listbox/Listbox.vue'
|
|
87
88
|
export type { ListboxVariants } from './components/listbox/listbox.variants'
|
|
@@ -307,6 +308,7 @@ export {
|
|
|
307
308
|
writeScrollRecord,
|
|
308
309
|
createScrollRestorer,
|
|
309
310
|
createScrollRestoreSession,
|
|
311
|
+
keepScrollPosition,
|
|
310
312
|
scrollRestoreScript,
|
|
311
313
|
} from './components/scroll-area/scroll-restore'
|
|
312
314
|
export type {
|