@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.
Files changed (41) hide show
  1. package/dist/index.js +230 -59
  2. package/dist/index.js.map +1 -1
  3. package/dist/shared/behavior/form.ts +5 -9
  4. package/dist/shared/lib/command-palette/match.ts +17 -4
  5. package/dist/shared/lib/scroll-restore.ts +19 -1
  6. package/dist/shared/lib/virtual/choices.ts +2 -1
  7. package/dist/shared/variants/command-palette.ts +12 -0
  8. package/dist/shared/variants/virtual-list.ts +8 -0
  9. package/dist/types/shared/src/behavior/form.d.ts.map +1 -1
  10. package/dist/types/shared/src/lib/command-palette/match.d.ts +9 -0
  11. package/dist/types/shared/src/lib/command-palette/match.d.ts.map +1 -1
  12. package/dist/types/shared/src/lib/scroll-restore.d.ts +2 -0
  13. package/dist/types/shared/src/lib/scroll-restore.d.ts.map +1 -1
  14. package/dist/types/shared/src/lib/virtual/choices.d.ts +4 -0
  15. package/dist/types/shared/src/lib/virtual/choices.d.ts.map +1 -1
  16. package/dist/types/shared/src/variants/command-palette.d.ts +3 -0
  17. package/dist/types/shared/src/variants/command-palette.d.ts.map +1 -1
  18. package/dist/types/shared/src/variants/virtual-list.d.ts +2 -0
  19. package/dist/types/shared/src/variants/virtual-list.d.ts.map +1 -1
  20. package/dist/types/vue/src/components/command-palette/CommandPalette.vue.d.ts +2 -1
  21. package/dist/types/vue/src/components/command-palette/CommandPalette.vue.d.ts.map +1 -1
  22. package/dist/types/vue/src/components/command-palette/CommandPalettePanel.vue.d.ts +2 -1
  23. package/dist/types/vue/src/components/command-palette/CommandPalettePanel.vue.d.ts.map +1 -1
  24. package/dist/types/vue/src/components/command-palette/composables/useCommandItems.d.ts +6 -1
  25. package/dist/types/vue/src/components/command-palette/composables/useCommandItems.d.ts.map +1 -1
  26. package/dist/types/vue/src/components/command-palette/types.d.ts +3 -0
  27. package/dist/types/vue/src/components/command-palette/types.d.ts.map +1 -1
  28. package/dist/types/vue/src/components/command-palette/utils/match.d.ts +1 -1
  29. package/dist/types/vue/src/components/command-palette/utils/match.d.ts.map +1 -1
  30. package/dist/types/vue/src/components/virtual-list/VirtualChoices.vue.d.ts +4 -1
  31. package/dist/types/vue/src/components/virtual-list/VirtualChoices.vue.d.ts.map +1 -1
  32. package/dist/types/vue/src/index.d.ts +2 -2
  33. package/dist/types/vue/src/index.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/components/command-palette/CommandPalette.vue +8 -0
  36. package/src/components/command-palette/CommandPalettePanel.vue +85 -3
  37. package/src/components/command-palette/composables/useCommandItems.ts +19 -13
  38. package/src/components/command-palette/types.ts +4 -0
  39. package/src/components/command-palette/utils/match.ts +1 -0
  40. package/src/components/virtual-list/VirtualChoices.vue +39 -6
  41. 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> | { label: string; options: CommandOption<T>[] }>(
16
- section => {
17
- const options = section.matches.map(match => ({
18
- value: match.item.id,
19
- label: match.item.label,
20
- disabled: match.item.disabled,
21
- description: match.item.description,
22
- match,
23
- }))
24
- return section.label ? [{ label: section.label, options }] : options
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 { virtualListContent, virtualListItem } from './virtual-list.variants'
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 === false ? undefined : 'p-1'">
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="{ marginBlockStart: `${entry.gapBefore}px` }"
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 {