@hina-ui/vue 1.7.7 → 1.7.8

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 (104) hide show
  1. package/dist/index.js +1318 -554
  2. package/dist/index.js.map +1 -1
  3. package/dist/shared/behavior/anchor-follow.ts +20 -11
  4. package/dist/shared/lib/command-palette/match.ts +9 -0
  5. package/dist/shared/lib/line-clamp.ts +76 -0
  6. package/dist/shared/lib/scroll-restore.ts +145 -0
  7. package/dist/shared/locale/en-US.ts +4 -0
  8. package/dist/shared/locale/index.ts +2 -0
  9. package/dist/shared/locale/ja-JP.ts +229 -0
  10. package/dist/shared/locale/types.ts +4 -0
  11. package/dist/shared/locale/zh-CN.ts +4 -0
  12. package/dist/shared/styles/density.css +4 -0
  13. package/dist/shared/styles/line-clamp.css +98 -0
  14. package/dist/shared/styles/tokens.css +1 -0
  15. package/dist/shared/styles/typography.css +18 -0
  16. package/dist/shared/variants/button.ts +2 -0
  17. package/dist/shared/variants/command-palette.ts +4 -0
  18. package/dist/shared/variants/line-clamp.ts +13 -0
  19. package/dist/shared/variants/tag.ts +1 -0
  20. package/dist/types/shared/src/behavior/anchor-follow.d.ts.map +1 -1
  21. package/dist/types/shared/src/lib/command-palette/match.d.ts +5 -0
  22. package/dist/types/shared/src/lib/command-palette/match.d.ts.map +1 -1
  23. package/dist/types/shared/src/lib/line-clamp.d.ts +6 -0
  24. package/dist/types/shared/src/lib/line-clamp.d.ts.map +1 -0
  25. package/dist/types/shared/src/lib/scroll-restore.d.ts +14 -0
  26. package/dist/types/shared/src/lib/scroll-restore.d.ts.map +1 -1
  27. package/dist/types/shared/src/locale/en-US.d.ts.map +1 -1
  28. package/dist/types/shared/src/locale/index.d.ts +1 -0
  29. package/dist/types/shared/src/locale/index.d.ts.map +1 -1
  30. package/dist/types/shared/src/locale/ja-JP.d.ts +3 -0
  31. package/dist/types/shared/src/locale/ja-JP.d.ts.map +1 -0
  32. package/dist/types/shared/src/locale/types.d.ts +4 -0
  33. package/dist/types/shared/src/locale/types.d.ts.map +1 -1
  34. package/dist/types/shared/src/locale/zh-CN.d.ts.map +1 -1
  35. package/dist/types/shared/src/variants/button.d.ts +6 -0
  36. package/dist/types/shared/src/variants/button.d.ts.map +1 -1
  37. package/dist/types/shared/src/variants/command-palette.d.ts +1 -0
  38. package/dist/types/shared/src/variants/command-palette.d.ts.map +1 -1
  39. package/dist/types/shared/src/variants/line-clamp.d.ts +4 -0
  40. package/dist/types/shared/src/variants/line-clamp.d.ts.map +1 -0
  41. package/dist/types/shared/src/variants/tag.d.ts +3 -0
  42. package/dist/types/shared/src/variants/tag.d.ts.map +1 -1
  43. package/dist/types/shared/src/variants/toggle.d.ts +7 -0
  44. package/dist/types/shared/src/variants/toggle.d.ts.map +1 -1
  45. package/dist/types/vue/src/components/app-shell/AppShell.vue.d.ts +2 -2
  46. package/dist/types/vue/src/components/app-shell/AppShell.vue.d.ts.map +1 -1
  47. package/dist/types/vue/src/components/close-button/CloseButton.vue.d.ts +1 -1
  48. package/dist/types/vue/src/components/command-palette/CommandPalette.vue.d.ts +40 -34
  49. package/dist/types/vue/src/components/command-palette/CommandPalette.vue.d.ts.map +1 -1
  50. package/dist/types/vue/src/components/command-palette/CommandPaletteInput.vue.d.ts +8 -0
  51. package/dist/types/vue/src/components/command-palette/CommandPaletteInput.vue.d.ts.map +1 -0
  52. package/dist/types/vue/src/components/command-palette/CommandPaletteItem.vue.d.ts +21 -7
  53. package/dist/types/vue/src/components/command-palette/CommandPaletteItem.vue.d.ts.map +1 -1
  54. package/dist/types/vue/src/components/command-palette/CommandPalettePanel.vue.d.ts +37 -31
  55. package/dist/types/vue/src/components/command-palette/CommandPalettePanel.vue.d.ts.map +1 -1
  56. package/dist/types/vue/src/components/command-palette/composables/useCommandItems.d.ts +6 -6
  57. package/dist/types/vue/src/components/command-palette/composables/useCommandItems.d.ts.map +1 -1
  58. package/dist/types/vue/src/components/command-palette/context.d.ts +11 -0
  59. package/dist/types/vue/src/components/command-palette/context.d.ts.map +1 -0
  60. package/dist/types/vue/src/components/command-palette/types.d.ts +16 -5
  61. package/dist/types/vue/src/components/command-palette/types.d.ts.map +1 -1
  62. package/dist/types/vue/src/components/command-palette/utils/match.d.ts +3 -3
  63. package/dist/types/vue/src/components/command-palette/utils/match.d.ts.map +1 -1
  64. package/dist/types/vue/src/components/copy-button/CopyButton.vue.d.ts +1 -1
  65. package/dist/types/vue/src/components/description-list/DescriptionList.vue.d.ts +1 -0
  66. package/dist/types/vue/src/components/description-list/DescriptionList.vue.d.ts.map +1 -1
  67. package/dist/types/vue/src/components/dialog/Dialog.vue.d.ts +3 -0
  68. package/dist/types/vue/src/components/dialog/Dialog.vue.d.ts.map +1 -1
  69. package/dist/types/vue/src/components/drawer/Drawer.vue.d.ts +3 -0
  70. package/dist/types/vue/src/components/drawer/Drawer.vue.d.ts.map +1 -1
  71. package/dist/types/vue/src/components/form/Form.vue.d.ts +5 -0
  72. package/dist/types/vue/src/components/form/Form.vue.d.ts.map +1 -1
  73. package/dist/types/vue/src/components/form/scope.d.ts +4 -0
  74. package/dist/types/vue/src/components/form/scope.d.ts.map +1 -0
  75. package/dist/types/vue/src/components/line-clamp/LineClamp.vue.d.ts +28 -0
  76. package/dist/types/vue/src/components/line-clamp/LineClamp.vue.d.ts.map +1 -0
  77. package/dist/types/vue/src/components/line-clamp/line-clamp.variants.d.ts +2 -0
  78. package/dist/types/vue/src/components/line-clamp/line-clamp.variants.d.ts.map +1 -0
  79. package/dist/types/vue/src/components/sheet/Sheet.vue.d.ts +3 -0
  80. package/dist/types/vue/src/components/sheet/Sheet.vue.d.ts.map +1 -1
  81. package/dist/types/vue/src/index.d.ts +6 -4
  82. package/dist/types/vue/src/index.d.ts.map +1 -1
  83. package/dist/types/vue/src/locale/index.d.ts +1 -1
  84. package/dist/types/vue/src/locale/index.d.ts.map +1 -1
  85. package/package.json +1 -1
  86. package/src/components/app-shell/AppShell.vue +54 -4
  87. package/src/components/command-palette/CommandPalette.vue +53 -16
  88. package/src/components/command-palette/CommandPaletteInput.vue +32 -0
  89. package/src/components/command-palette/CommandPaletteItem.vue +7 -5
  90. package/src/components/command-palette/CommandPalettePanel.vue +95 -34
  91. package/src/components/command-palette/composables/useCommandItems.ts +16 -14
  92. package/src/components/command-palette/context.ts +28 -0
  93. package/src/components/command-palette/types.ts +21 -5
  94. package/src/components/command-palette/utils/match.ts +6 -3
  95. package/src/components/description-list/DescriptionList.vue +8 -2
  96. package/src/components/dialog/Dialog.vue +13 -8
  97. package/src/components/drawer/Drawer.vue +13 -8
  98. package/src/components/form/Form.vue +14 -1
  99. package/src/components/form/scope.ts +39 -0
  100. package/src/components/line-clamp/LineClamp.vue +105 -0
  101. package/src/components/line-clamp/line-clamp.variants.ts +1 -0
  102. package/src/components/sheet/Sheet.vue +14 -10
  103. package/src/index.ts +15 -2
  104. package/src/locale/index.ts +1 -1
@@ -1,5 +1,14 @@
1
1
  <script setup lang="ts">
2
- import { computed, getCurrentInstance, shallowRef, watch } from 'vue'
2
+ import {
3
+ computed,
4
+ getCurrentInstance,
5
+ h,
6
+ inject,
7
+ onMounted,
8
+ shallowRef,
9
+ ssrContextKey,
10
+ watch,
11
+ } from 'vue'
3
12
  import { cn } from '../../lib/cn'
4
13
  import { useUiLocale } from '../../locale'
5
14
  import ScrollArea from '../scroll-area/ScrollArea.vue'
@@ -7,6 +16,10 @@
7
16
  import Drawer from '../drawer/Drawer.vue'
8
17
  import DrawerScope from '../sidebar/DrawerScope'
9
18
  import { provideSidebar, type SidebarState } from '../sidebar/context'
19
+ import {
20
+ createScrollRestoreSession,
21
+ scrollRestoreScript,
22
+ } from '../../../../shared/src/lib/scroll-restore'
10
23
  import { useDesktopQuery } from './composables/useDesktopQuery'
11
24
  import { useSidebarScrollUpdates } from './composables/useSidebarScrollUpdates'
12
25
 
@@ -56,6 +69,32 @@
56
69
  onTransitionRun,
57
70
  })
58
71
 
72
+ const RestoreScript = () => h('script', { innerHTML: scrollRestoreScript() })
73
+ const scripted = shallowRef(!!inject(ssrContextKey, null) || !!getCurrentInstance()?.vnode.el)
74
+ onMounted(() => {
75
+ scripted.value = false
76
+ })
77
+
78
+ let restoredKey: string | undefined
79
+ watch(
80
+ () => [props.restoreKey, main.value?.instance] as const,
81
+ ([key, instance], _previous, onCleanup) => {
82
+ if (!key || !instance) return
83
+ const { viewport, target } = instance.elements()
84
+ const session = createScrollRestoreSession({
85
+ key,
86
+ viewport,
87
+ target,
88
+ initial: restoredKey === undefined || restoredKey === key,
89
+ onUpdated: listener => instance.on('updated', listener),
90
+ onScroll: listener => instance.on('scroll', listener),
91
+ })
92
+ restoredKey = key
93
+ onCleanup(() => session.dispose())
94
+ },
95
+ { immediate: true, flush: 'post' },
96
+ )
97
+
59
98
  type Navigable = { currentRoute?: { value?: { fullPath?: string } } }
60
99
  const router = getCurrentInstance()?.appContext.config.globalProperties.$router as
61
100
  Navigable | undefined
@@ -70,7 +109,14 @@
70
109
 
71
110
  <template>
72
111
  <TooltipProvider>
73
- <div :class="cn('bg-canvas text-fg flex h-screen flex-col overflow-hidden', props.class)">
112
+ <div
113
+ :class="
114
+ cn(
115
+ 'bg-canvas text-fg flex h-screen flex-col overflow-hidden [--hn-app-shell-header-h:calc(3.5rem+1px)]',
116
+ props.class,
117
+ )
118
+ "
119
+ >
74
120
  <div v-if="$slots.banner" class="shrink-0">
75
121
  <slot name="banner" />
76
122
  </div>
@@ -79,8 +125,11 @@
79
125
  <slot name="sidebar" />
80
126
  </div>
81
127
  <div class="flex min-h-0 min-w-0 flex-1 flex-col">
82
- <header v-if="$slots.header" class="border-line bg-canvas shrink-0 border-b">
83
- <div class="flex h-14 items-center gap-3 px-4 sm:px-6">
128
+ <header
129
+ v-if="$slots.header"
130
+ class="border-line bg-canvas h-(--hn-app-shell-header-h) shrink-0 border-b"
131
+ >
132
+ <div class="flex h-full items-center gap-3 px-4 sm:px-6">
84
133
  <slot name="header" />
85
134
  </div>
86
135
  </header>
@@ -88,6 +137,7 @@
88
137
  <ScrollArea ref="main" :data-scroll-restore="props.restoreKey" class="h-full">
89
138
  <slot />
90
139
  </ScrollArea>
140
+ <RestoreScript v-if="props.restoreKey && scripted" />
91
141
  </main>
92
142
  </div>
93
143
  </div>
@@ -1,4 +1,4 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="T = unknown">
2
2
  import {
3
3
  DialogContent,
4
4
  DialogOverlay,
@@ -14,22 +14,40 @@
14
14
  import CommandPalettePanel from './CommandPalettePanel.vue'
15
15
  import { useHotkey } from './composables/useHotkey'
16
16
  import { commandWrapper } from './command-palette.variants'
17
- import type { CommandItem, CommandItems } from './types'
17
+ import type {
18
+ CommandEmptySlotProps,
19
+ CommandItem,
20
+ CommandItems,
21
+ CommandItemSlotProps,
22
+ } from './types'
18
23
 
19
24
  defineOptions({ name: 'HnCommandPalette', inheritAttrs: false })
20
25
 
21
- const props = defineProps<{
22
- items: CommandItems
23
- virtualize?: VirtualizeOptions
24
- placeholder?: string
25
- label?: string
26
- hotkey?: string
27
- ignoreFilter?: boolean
28
- inline?: boolean
29
- class?: string
30
- }>()
26
+ const props = withDefaults(
27
+ defineProps<{
28
+ items: CommandItems<T>
29
+ virtualize?: VirtualizeOptions
30
+ placeholder?: string
31
+ label?: string
32
+ hotkey?: string
33
+ ignoreFilter?: boolean
34
+ loading?: boolean
35
+ closeOnSelect?: boolean
36
+ inline?: boolean
37
+ class?: string
38
+ }>(),
39
+ { closeOnSelect: true },
40
+ )
31
41
 
32
- const emit = defineEmits<{ select: [item: CommandItem] }>()
42
+ const emit = defineEmits<{ select: [item: CommandItem<T>] }>()
43
+
44
+ defineSlots<{
45
+ default?(): unknown
46
+ input?(): unknown
47
+ item?(props: CommandItemSlotProps<T>): unknown
48
+ loading?(): unknown
49
+ empty?(props: CommandEmptySlotProps): unknown
50
+ }>()
33
51
 
34
52
  const open = defineModel<boolean>('open', { default: false })
35
53
  const search = defineModel<string>('search', { default: '' })
@@ -49,10 +67,10 @@
49
67
  if (!value) search.value = ''
50
68
  })
51
69
 
52
- function select(item: CommandItem) {
70
+ function select(item: CommandItem<T>) {
53
71
  item.onSelect?.()
54
72
  emit('select', item)
55
- if (!props.inline) open.value = false
73
+ if (!props.inline && (item.closeOnSelect ?? props.closeOnSelect)) open.value = false
56
74
  }
57
75
  </script>
58
76
 
@@ -67,9 +85,19 @@
67
85
  :label="label"
68
86
  :placeholder="props.placeholder"
69
87
  :ignore-filter="props.ignoreFilter"
88
+ :loading="props.loading"
70
89
  :class="props.class"
71
90
  @select="select"
72
- />
91
+ >
92
+ <template v-if="$slots.input" #input><slot name="input" /></template>
93
+ <template v-if="$slots.item" #item="slotProps">
94
+ <slot name="item" v-bind="slotProps" />
95
+ </template>
96
+ <template v-if="$slots.loading" #loading><slot name="loading" /></template>
97
+ <template v-if="$slots.empty" #empty="slotProps">
98
+ <slot name="empty" v-bind="slotProps" />
99
+ </template>
100
+ </CommandPalettePanel>
73
101
  <DialogRoot v-else v-model:open="open">
74
102
  <DialogTrigger v-if="$slots.default" as-child>
75
103
  <slot />
@@ -87,12 +115,21 @@
87
115
  :label="label"
88
116
  :placeholder="props.placeholder"
89
117
  :ignore-filter="props.ignoreFilter"
118
+ :loading="props.loading"
90
119
  :class="props.class"
91
120
  @select="select"
92
121
  >
93
122
  <VisuallyHidden>
94
123
  <DialogTitle>{{ label }}</DialogTitle>
95
124
  </VisuallyHidden>
125
+ <template v-if="$slots.input" #input><slot name="input" /></template>
126
+ <template v-if="$slots.item" #item="slotProps">
127
+ <slot name="item" v-bind="slotProps" />
128
+ </template>
129
+ <template v-if="$slots.loading" #loading><slot name="loading" /></template>
130
+ <template v-if="$slots.empty" #empty="slotProps">
131
+ <slot name="empty" v-bind="slotProps" />
132
+ </template>
96
133
  </CommandPalettePanel>
97
134
  </DialogContent>
98
135
  </div>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ import { ListboxFilter } from 'reka-ui'
3
+ import { shallowRef, watchEffect } from 'vue'
4
+ import { cn } from '../../lib/cn'
5
+ import { commandInput } from './command-palette.variants'
6
+ import { injectCommandPalette } from './context'
7
+
8
+ defineOptions({ name: 'HnCommandPaletteInput' })
9
+
10
+ const props = defineProps<{
11
+ placeholder?: string
12
+ class?: string
13
+ }>()
14
+
15
+ const context = injectCommandPalette()
16
+ const filter = shallowRef<{ $el: HTMLElement }>()
17
+
18
+ watchEffect(() => {
19
+ context.input.value = filter.value?.$el
20
+ })
21
+ </script>
22
+
23
+ <template>
24
+ <ListboxFilter
25
+ ref="filter"
26
+ v-model="context.search.value"
27
+ :auto-focus="context.autoFocus.value"
28
+ :placeholder="props.placeholder ?? context.placeholder.value"
29
+ :aria-label="context.label.value"
30
+ :class="props.class ? cn(commandInput(), props.class) : commandInput()"
31
+ />
32
+ </template>
@@ -1,4 +1,4 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="T = unknown">
2
2
  import { ListboxItem } from 'reka-ui'
3
3
  import { computed } from 'vue'
4
4
  import Kbd from '../kbd/Kbd.vue'
@@ -8,8 +8,9 @@
8
8
 
9
9
  defineOptions({ name: 'HnCommandPaletteItem' })
10
10
 
11
- const props = defineProps<{ match: CommandMatch }>()
11
+ const props = defineProps<{ match: CommandMatch<T> }>()
12
12
  const emit = defineEmits<{ select: [] }>()
13
+ defineSlots<{ default?(): unknown }>()
13
14
 
14
15
  const parts = computed(() => {
15
16
  const { item, start, end } = props.match
@@ -26,13 +27,14 @@
26
27
  :class="selectItem()"
27
28
  @select="emit('select')"
28
29
  >
30
+ <slot v-if="$slots.default" />
29
31
  <component
30
32
  :is="props.match.item.icon"
31
- v-if="props.match.item.icon"
33
+ v-else-if="props.match.item.icon"
32
34
  aria-hidden="true"
33
35
  class="text-muted"
34
36
  />
35
- <span :class="commandItemBody()">
37
+ <span v-if="!$slots.default" :class="commandItemBody()">
36
38
  <span class="truncate">
37
39
  <span v-text="parts[0]" />
38
40
  <span v-if="parts[1]" :class="commandItemMatch()" v-text="parts[1]" />
@@ -42,7 +44,7 @@
42
44
  {{ props.match.item.description }}
43
45
  </span>
44
46
  </span>
45
- <span v-if="props.match.item.kbd?.length" :class="commandItemHint()">
47
+ <span v-if="!$slots.default && props.match.item.kbd?.length" :class="commandItemHint()">
46
48
  <Kbd v-for="key in props.match.item.kbd" :key="key">{{ key }}</Kbd>
47
49
  </span>
48
50
  </ListboxItem>
@@ -1,13 +1,7 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="T = unknown">
2
2
  import { Search } from '@lucide/vue'
3
- import {
4
- ListboxContent,
5
- ListboxFilter,
6
- ListboxGroup,
7
- ListboxGroupLabel,
8
- ListboxRoot,
9
- } from 'reka-ui'
10
- import { shallowRef } from 'vue'
3
+ import { ListboxContent, ListboxGroup, ListboxGroupLabel, ListboxRoot } from 'reka-ui'
4
+ import { computed, nextTick, shallowRef } from 'vue'
11
5
  import { cn } from '../../lib/cn'
12
6
  import { useUiLocale } from '../../locale'
13
7
  import Card from '../card/Card.vue'
@@ -15,38 +9,77 @@
15
9
  import VirtualChoices from '../virtual-list/VirtualChoices.vue'
16
10
  import type { VirtualizeOptions } from '../../lib/virtual/types'
17
11
  import { selectLabel, selectListBody } from '../select/select.variants'
12
+ import CommandPaletteInput from './CommandPaletteInput.vue'
18
13
  import CommandPaletteItem from './CommandPaletteItem.vue'
19
14
  import {
20
15
  commandCard,
21
16
  commandEmpty,
22
- commandInput,
23
17
  commandInputRow,
24
18
  commandList,
19
+ commandStatus,
25
20
  } from './command-palette.variants'
26
- import type { CommandItem, CommandItems } from './types'
21
+ import { provideCommandPalette } from './context'
22
+ import type {
23
+ CommandEmptySlotProps,
24
+ CommandItem,
25
+ CommandItems,
26
+ CommandItemSlotProps,
27
+ } from './types'
27
28
  import { useCommandItems } from './composables/useCommandItems'
29
+ import { commandMatchRange } from './utils/match'
28
30
 
29
31
  defineOptions({ name: 'HnCommandPalettePanel', inheritAttrs: false })
30
32
 
31
33
  const props = defineProps<{
32
- items: CommandItems
34
+ items: CommandItems<T>
33
35
  virtualize?: VirtualizeOptions
34
36
  label: string
35
37
  placeholder?: string
36
38
  ignoreFilter?: boolean
39
+ loading?: boolean
37
40
  autoFocus?: boolean
38
41
  inline?: boolean
39
42
  class?: string
40
43
  }>()
41
44
 
42
- const emit = defineEmits<{ select: [item: CommandItem] }>()
45
+ const emit = defineEmits<{ select: [item: CommandItem<T>] }>()
46
+
47
+ defineSlots<{
48
+ default?(): unknown
49
+ input?(): unknown
50
+ item?(props: CommandItemSlotProps<T>): unknown
51
+ loading?(): unknown
52
+ empty?(props: CommandEmptySlotProps): unknown
53
+ }>()
43
54
 
44
55
  const search = defineModel<string>('search', { default: '' })
45
56
 
46
57
  const t = useUiLocale()
47
58
 
48
- const input = shallowRef<{ $el: HTMLElement }>()
59
+ const input = shallowRef<HTMLElement>()
49
60
  const { sections, options } = useCommandItems(props, () => search.value)
61
+
62
+ function select(item: CommandItem<T>) {
63
+ const field = input.value
64
+ const panel = field?.closest('[data-hn-command-palette]')
65
+ const inside = !!panel?.contains(document.activeElement)
66
+ emit('select', item)
67
+ if (!field || !panel || !inside) return
68
+ void nextTick(() => {
69
+ const active = document.activeElement
70
+ if (!field.isConnected) return
71
+ if (!active || active === document.body || panel.contains(active))
72
+ field.focus({ preventScroll: true })
73
+ })
74
+ }
75
+
76
+ provideCommandPalette({
77
+ search,
78
+ label: computed(() => props.label),
79
+ placeholder: computed(() => props.placeholder ?? t.value.command.placeholder),
80
+ autoFocus: computed(() => props.autoFocus),
81
+ input,
82
+ })
50
83
  </script>
51
84
 
52
85
  <template>
@@ -58,22 +91,21 @@
58
91
  >
59
92
  <slot />
60
93
  <ListboxRoot selection-behavior="replace" highlight-on-hover class="flex min-h-0 flex-col">
61
- <div :class="commandInputRow()">
94
+ <slot v-if="$slots.input" name="input" />
95
+ <div v-else :class="commandInputRow()">
62
96
  <Search aria-hidden="true" />
63
- <ListboxFilter
64
- ref="input"
65
- v-model="search"
66
- :auto-focus="props.autoFocus"
67
- :placeholder="props.placeholder ?? t.command.placeholder"
68
- :aria-label="props.label"
69
- :class="commandInput()"
70
- />
97
+ <CommandPaletteInput />
71
98
  </div>
72
- <ListboxContent v-if="props.virtualize" as-child :aria-label="props.label">
99
+ <ListboxContent
100
+ v-if="props.virtualize"
101
+ as-child
102
+ :aria-label="props.label"
103
+ :aria-busy="props.loading || undefined"
104
+ >
73
105
  <VirtualChoices
74
106
  :options="options"
75
107
  :virtualize="props.virtualize"
76
- :input="input?.$el"
108
+ :input="input"
77
109
  kind="listbox"
78
110
  :class="commandList()"
79
111
  >
@@ -81,14 +113,29 @@
81
113
  <CommandPaletteItem
82
114
  v-bind="attrs"
83
115
  :match="option.match"
84
- @select="emit('select', option.match.item)"
85
- />
116
+ @select="select(option.match.item)"
117
+ >
118
+ <template v-if="$slots.item" #default>
119
+ <slot
120
+ name="item"
121
+ :item="option.match.item"
122
+ :match="commandMatchRange(option.match)"
123
+ />
124
+ </template>
125
+ </CommandPaletteItem>
126
+ </template>
127
+ <template #empty>
128
+ <slot v-if="props.loading" name="loading">{{ t.common.loading }}</slot>
129
+ <slot v-else name="empty" :search="search">{{ t.command.empty }}</slot>
86
130
  </template>
87
- <template #empty>{{ t.command.empty }}</template>
88
131
  </VirtualChoices>
89
132
  </ListboxContent>
90
133
  <ScrollArea v-else :class="commandList()">
91
- <ListboxContent :aria-label="props.label" :class="selectListBody()">
134
+ <ListboxContent
135
+ :aria-label="props.label"
136
+ :aria-busy="props.loading || undefined"
137
+ :class="selectListBody()"
138
+ >
92
139
  <template v-for="section in sections" :key="section.key">
93
140
  <ListboxGroup v-if="section.label">
94
141
  <ListboxGroupLabel :class="selectLabel()">{{ section.label }}</ListboxGroupLabel>
@@ -96,21 +143,35 @@
96
143
  v-for="match in section.matches"
97
144
  :key="match.item.id"
98
145
  :match="match"
99
- @select="emit('select', match.item)"
100
- />
146
+ @select="select(match.item)"
147
+ >
148
+ <template v-if="$slots.item" #default>
149
+ <slot name="item" :item="match.item" :match="commandMatchRange(match)" />
150
+ </template>
151
+ </CommandPaletteItem>
101
152
  </ListboxGroup>
102
153
  <template v-else>
103
154
  <CommandPaletteItem
104
155
  v-for="match in section.matches"
105
156
  :key="match.item.id"
106
157
  :match="match"
107
- @select="emit('select', match.item)"
108
- />
158
+ @select="select(match.item)"
159
+ >
160
+ <template v-if="$slots.item" #default>
161
+ <slot name="item" :item="match.item" :match="commandMatchRange(match)" />
162
+ </template>
163
+ </CommandPaletteItem>
109
164
  </template>
110
165
  </template>
111
- <div v-if="sections.length === 0" :class="commandEmpty()">{{ t.command.empty }}</div>
112
166
  </ListboxContent>
167
+ <div v-if="sections.length === 0" role="status" :class="commandEmpty()">
168
+ <slot v-if="props.loading" name="loading">{{ t.common.loading }}</slot>
169
+ <slot v-else name="empty" :search="search">{{ t.command.empty }}</slot>
170
+ </div>
113
171
  </ScrollArea>
172
+ <div v-if="props.loading && sections.length > 0" role="status" :class="commandStatus()">
173
+ <slot name="loading">{{ t.common.loading }}</slot>
174
+ </div>
114
175
  </ListboxRoot>
115
176
  </Card>
116
177
  </template>
@@ -4,24 +4,26 @@ 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
- type CommandOption = SelectOption & { match: CommandMatch }
7
+ type CommandOption<T> = SelectOption & { match: CommandMatch<T> }
8
8
 
9
- export function useCommandItems(
10
- props: { items: CommandItems; ignoreFilter?: boolean },
9
+ export function useCommandItems<T>(
10
+ props: { items: CommandItems<T>; ignoreFilter?: boolean },
11
11
  search: () => string,
12
12
  ) {
13
13
  const sections = computed(() => filterCommands(props.items, props.ignoreFilter ? '' : search()))
14
- const options = computed<SelectItems<CommandOption>>(() =>
15
- sections.value.flatMap<CommandOption | { label: string; options: CommandOption[] }>(section => {
16
- const options = section.matches.map(match => ({
17
- value: match.item.id,
18
- label: match.item.label,
19
- disabled: match.item.disabled,
20
- description: match.item.description,
21
- match,
22
- }))
23
- return section.label ? [{ label: section.label, options }] : options
24
- }),
14
+ 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
+ ),
25
27
  )
26
28
  return { sections, options }
27
29
  }
@@ -0,0 +1,28 @@
1
+ import {
2
+ inject,
3
+ provide,
4
+ type ComputedRef,
5
+ type InjectionKey,
6
+ type Ref,
7
+ type ShallowRef,
8
+ } from 'vue'
9
+
10
+ export interface CommandPaletteContext {
11
+ search: Ref<string>
12
+ label: ComputedRef<string>
13
+ placeholder: ComputedRef<string>
14
+ autoFocus: ComputedRef<boolean | undefined>
15
+ input: ShallowRef<HTMLElement | undefined>
16
+ }
17
+
18
+ const COMMAND_PALETTE_KEY = Symbol('hn-command-palette') as InjectionKey<CommandPaletteContext>
19
+
20
+ export function provideCommandPalette(context: CommandPaletteContext) {
21
+ provide(COMMAND_PALETTE_KEY, context)
22
+ }
23
+
24
+ export function injectCommandPalette() {
25
+ const context = inject(COMMAND_PALETTE_KEY, null)
26
+ if (!context) throw new Error('CommandPaletteInput must be used inside CommandPalette')
27
+ return context
28
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Component } from 'vue'
2
+ import type { CommandMatchRange } from '../../../../shared/src/lib/command-palette/match'
2
3
 
3
- export interface CommandItem {
4
+ export interface CommandItem<T = unknown> {
4
5
  id: string
5
6
  label: string
6
7
  description?: string
@@ -8,16 +9,31 @@ export interface CommandItem {
8
9
  icon?: Component
9
10
  kbd?: string[]
10
11
  disabled?: boolean
12
+ closeOnSelect?: boolean
13
+ data?: T
11
14
  onSelect?: () => void
12
15
  }
13
16
 
14
- export interface CommandGroup {
17
+ export interface CommandGroup<T = unknown> {
15
18
  label: string
16
- items: CommandItem[]
19
+ items: CommandItem<T>[]
17
20
  }
18
21
 
19
- export type CommandItems = Array<CommandItem | CommandGroup>
22
+ export type CommandItems<T = unknown> = Array<CommandItem<T> | CommandGroup<T>>
20
23
 
21
- export function isCommandGroup(entry: CommandItem | CommandGroup): entry is CommandGroup {
24
+ export type CommandItemMatch = CommandMatchRange
25
+
26
+ export interface CommandItemSlotProps<T = unknown> {
27
+ item: CommandItem<T>
28
+ match: CommandItemMatch | null
29
+ }
30
+
31
+ export interface CommandEmptySlotProps {
32
+ search: string
33
+ }
34
+
35
+ export function isCommandGroup<T>(
36
+ entry: CommandItem<T> | CommandGroup<T>,
37
+ ): entry is CommandGroup<T> {
22
38
  return 'items' in entry
23
39
  }
@@ -1,6 +1,9 @@
1
1
  import type { CommandItem } from '../types'
2
2
  import type * as Shared from '../../../../../shared/src/lib/command-palette/match'
3
3
 
4
- export { filterCommands } from '../../../../../shared/src/lib/command-palette/match'
5
- export type CommandMatch = Shared.CommandMatch<CommandItem>
6
- export type CommandSection = Shared.CommandSection<CommandItem>
4
+ export {
5
+ commandMatchRange,
6
+ filterCommands,
7
+ } from '../../../../../shared/src/lib/command-palette/match'
8
+ export type CommandMatch<T = unknown> = Shared.CommandMatch<CommandItem<T>>
9
+ export type CommandSection<T = unknown> = Shared.CommandSection<CommandItem<T>>
@@ -3,11 +3,17 @@
3
3
 
4
4
  defineOptions({ name: 'HnDescriptionList' })
5
5
 
6
- const props = defineProps<{ class?: string }>()
6
+ const props = defineProps<{
7
+ orientation?: 'vertical' | 'horizontal'
8
+ class?: string
9
+ }>()
7
10
  </script>
8
11
 
9
12
  <template>
10
- <dl :class="cn('hn-dl', props.class)">
13
+ <dl
14
+ :data-orientation="props.orientation === 'horizontal' ? 'horizontal' : undefined"
15
+ :class="cn('hn-dl', props.class)"
16
+ >
11
17
  <slot />
12
18
  </dl>
13
19
  </template>