@hina-ui/vue 1.1.0 → 1.3.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.
Files changed (175) hide show
  1. package/dist/index.js +1062 -528
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/accordion/AccordionContent.vue.d.ts.map +1 -1
  4. package/dist/types/components/alert-dialog/AlertDialog.vue.d.ts +4 -0
  5. package/dist/types/components/alert-dialog/AlertDialog.vue.d.ts.map +1 -1
  6. package/dist/types/components/alert-dialog/alert-dialog.variants.d.ts +5 -0
  7. package/dist/types/components/alert-dialog/alert-dialog.variants.d.ts.map +1 -0
  8. package/dist/types/components/alert-dialog/composables/useAlertDialogConfirm.d.ts +9 -0
  9. package/dist/types/components/alert-dialog/composables/useAlertDialogConfirm.d.ts.map +1 -0
  10. package/dist/types/components/alert-dialog/composables/useConfirmDelay.d.ts +3 -0
  11. package/dist/types/components/alert-dialog/composables/useConfirmDelay.d.ts.map +1 -0
  12. package/dist/types/components/anchor/Anchor.vue.d.ts.map +1 -1
  13. package/dist/types/components/avatar/AvatarGroup.vue.d.ts.map +1 -1
  14. package/dist/types/components/avatar/utils/children.d.ts +3 -0
  15. package/dist/types/components/avatar/utils/children.d.ts.map +1 -0
  16. package/dist/types/components/checkbox-group/CheckboxGroup.vue.d.ts +32 -29
  17. package/dist/types/components/checkbox-group/CheckboxGroup.vue.d.ts.map +1 -1
  18. package/dist/types/components/code-block/CodeBlock.vue.d.ts.map +1 -1
  19. package/dist/types/components/code-block/code-block.variants.d.ts +6 -0
  20. package/dist/types/components/code-block/code-block.variants.d.ts.map +1 -0
  21. package/dist/types/components/code-block/composables/useCodeHighlight.d.ts +7 -0
  22. package/dist/types/components/code-block/composables/useCodeHighlight.d.ts.map +1 -0
  23. package/dist/types/components/collapsible/CollapsibleContent.vue.d.ts.map +1 -1
  24. package/dist/types/components/combobox/Combobox.vue.d.ts +38 -38
  25. package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
  26. package/dist/types/components/combobox/ComboboxList.vue.d.ts +22 -15
  27. package/dist/types/components/combobox/ComboboxList.vue.d.ts.map +1 -1
  28. package/dist/types/components/dialog/Dialog.vue.d.ts +22 -16
  29. package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
  30. package/dist/types/components/dialog/dialog.variants.d.ts +32 -2
  31. package/dist/types/components/dialog/dialog.variants.d.ts.map +1 -1
  32. package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts +18 -6
  33. package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts.map +1 -1
  34. package/dist/types/components/highlight/Highlight.vue.d.ts +2 -0
  35. package/dist/types/components/highlight/Highlight.vue.d.ts.map +1 -1
  36. package/dist/types/components/highlight/composables/useHighlightMotion.d.ts +10 -0
  37. package/dist/types/components/highlight/composables/useHighlightMotion.d.ts.map +1 -0
  38. package/dist/types/components/image/ImageGroup.vue.d.ts.map +1 -1
  39. package/dist/types/components/image/composables/useImageGroupState.d.ts +7 -0
  40. package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -0
  41. package/dist/types/components/input/Input.vue.d.ts +3 -3
  42. package/dist/types/components/input/Input.vue.d.ts.map +1 -1
  43. package/dist/types/components/input/InputBase.vue.d.ts +3 -3
  44. package/dist/types/components/input/InputBase.vue.d.ts.map +1 -1
  45. package/dist/types/components/input/input.variants.d.ts +52 -6
  46. package/dist/types/components/input/input.variants.d.ts.map +1 -1
  47. package/dist/types/components/input-group/InputGroup.vue.d.ts +3 -3
  48. package/dist/types/components/input-group/InputGroup.vue.d.ts.map +1 -1
  49. package/dist/types/components/input-group/context.d.ts +2 -1
  50. package/dist/types/components/input-group/context.d.ts.map +1 -1
  51. package/dist/types/components/input-group/input-group.variants.d.ts +13 -1
  52. package/dist/types/components/input-group/input-group.variants.d.ts.map +1 -1
  53. package/dist/types/components/lightbox/LightboxThumbs.vue.d.ts.map +1 -1
  54. package/dist/types/components/listbox/Listbox.vue.d.ts +36 -29
  55. package/dist/types/components/listbox/Listbox.vue.d.ts.map +1 -1
  56. package/dist/types/components/listbox/ListboxOptionContent.vue.d.ts +31 -0
  57. package/dist/types/components/listbox/ListboxOptionContent.vue.d.ts.map +1 -0
  58. package/dist/types/components/listbox/listbox.variants.d.ts +19 -1
  59. package/dist/types/components/listbox/listbox.variants.d.ts.map +1 -1
  60. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts +41 -43
  61. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts.map +1 -1
  62. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts +27 -0
  63. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts.map +1 -0
  64. package/dist/types/components/multi-select/MultiSelect.vue.d.ts +39 -41
  65. package/dist/types/components/multi-select/MultiSelect.vue.d.ts.map +1 -1
  66. package/dist/types/components/number-input/NumberInput.vue.d.ts +3 -3
  67. package/dist/types/components/number-input/NumberInput.vue.d.ts.map +1 -1
  68. package/dist/types/components/number-input/number-input.variants.d.ts +10 -1
  69. package/dist/types/components/number-input/number-input.variants.d.ts.map +1 -1
  70. package/dist/types/components/password-input/PasswordInput.vue.d.ts +3 -3
  71. package/dist/types/components/password-input/PasswordInput.vue.d.ts.map +1 -1
  72. package/dist/types/components/popover/Popover.vue.d.ts +19 -6
  73. package/dist/types/components/popover/Popover.vue.d.ts.map +1 -1
  74. package/dist/types/components/popover/PopoverContent.vue.d.ts +29 -0
  75. package/dist/types/components/popover/PopoverContent.vue.d.ts.map +1 -0
  76. package/dist/types/components/popover/composables/usePopoverCloseFocus.d.ts +2 -0
  77. package/dist/types/components/popover/composables/usePopoverCloseFocus.d.ts.map +1 -0
  78. package/dist/types/components/radio-group/RadioGroup.vue.d.ts +32 -29
  79. package/dist/types/components/radio-group/RadioGroup.vue.d.ts.map +1 -1
  80. package/dist/types/components/scroll-area/ScrollArea.vue.d.ts.map +1 -1
  81. package/dist/types/components/scroll-area/composables/useScrollFocus.d.ts +17 -0
  82. package/dist/types/components/scroll-area/composables/useScrollFocus.d.ts.map +1 -0
  83. package/dist/types/components/scroll-area/scroll-restore.d.ts +0 -1
  84. package/dist/types/components/scroll-area/scroll-restore.d.ts.map +1 -1
  85. package/dist/types/components/search-input/SearchInput.vue.d.ts +3 -3
  86. package/dist/types/components/search-input/SearchInput.vue.d.ts.map +1 -1
  87. package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts +30 -30
  88. package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts.map +1 -1
  89. package/dist/types/components/select/Select.vue.d.ts +41 -37
  90. package/dist/types/components/select/Select.vue.d.ts.map +1 -1
  91. package/dist/types/components/select/SelectList.vue.d.ts +22 -15
  92. package/dist/types/components/select/SelectList.vue.d.ts.map +1 -1
  93. package/dist/types/components/select/composables/useClearTransition.d.ts +10 -0
  94. package/dist/types/components/select/composables/useClearTransition.d.ts.map +1 -0
  95. package/dist/types/components/select/select.variants.d.ts +16 -0
  96. package/dist/types/components/select/select.variants.d.ts.map +1 -1
  97. package/dist/types/components/select/types.d.ts +7 -7
  98. package/dist/types/components/select/types.d.ts.map +1 -1
  99. package/dist/types/components/tabs/TabsTrigger.vue.d.ts.map +1 -1
  100. package/dist/types/components/textarea/Textarea.vue.d.ts.map +1 -1
  101. package/dist/types/components/textarea/composables/useTextareaSizing.d.ts +27 -0
  102. package/dist/types/components/textarea/composables/useTextareaSizing.d.ts.map +1 -0
  103. package/dist/types/components/textarea/textarea.variants.d.ts +9 -6
  104. package/dist/types/components/textarea/textarea.variants.d.ts.map +1 -1
  105. package/dist/types/lib/anchored-overlay.d.ts +23 -0
  106. package/dist/types/lib/anchored-overlay.d.ts.map +1 -0
  107. package/dist/types/lib/caret.d.ts +0 -4
  108. package/dist/types/lib/caret.d.ts.map +1 -1
  109. package/dist/types/lib/collapse.d.ts +2 -0
  110. package/dist/types/lib/collapse.d.ts.map +1 -1
  111. package/dist/types/lib/field-focus.d.ts +0 -4
  112. package/dist/types/lib/field-focus.d.ts.map +1 -1
  113. package/package.json +1 -1
  114. package/src/components/accordion/AccordionContent.vue +2 -11
  115. package/src/components/alert-dialog/AlertDialog.vue +24 -36
  116. package/src/components/alert-dialog/alert-dialog.variants.ts +17 -0
  117. package/src/components/alert-dialog/composables/useAlertDialogConfirm.ts +29 -0
  118. package/src/components/alert-dialog/composables/useConfirmDelay.ts +37 -0
  119. package/src/components/anchor/Anchor.vue +1 -0
  120. package/src/components/avatar/AvatarGroup.vue +3 -8
  121. package/src/components/avatar/utils/children.ts +7 -0
  122. package/src/components/checkbox-group/CheckboxGroup.vue +3 -3
  123. package/src/components/code-block/CodeBlock.vue +15 -30
  124. package/src/components/code-block/code-block.variants.ts +21 -0
  125. package/src/components/code-block/composables/useCodeHighlight.ts +30 -0
  126. package/src/components/collapsible/CollapsibleContent.vue +2 -11
  127. package/src/components/combobox/Combobox.vue +3 -3
  128. package/src/components/combobox/ComboboxList.vue +3 -3
  129. package/src/components/dialog/Dialog.vue +70 -39
  130. package/src/components/dialog/dialog.variants.ts +9 -3
  131. package/src/components/dropdown-menu/DropdownMenu.vue +16 -4
  132. package/src/components/highlight/Highlight.vue +5 -4
  133. package/src/components/highlight/composables/useHighlightMotion.ts +22 -0
  134. package/src/components/image/ImageGroup.vue +2 -30
  135. package/src/components/image/composables/useImageGroupState.ts +35 -0
  136. package/src/components/input/Input.vue +3 -3
  137. package/src/components/input/InputBase.vue +5 -5
  138. package/src/components/input/input.variants.ts +13 -3
  139. package/src/components/input-group/InputGroup.vue +11 -4
  140. package/src/components/input-group/context.ts +2 -1
  141. package/src/components/input-group/input-group.variants.ts +8 -1
  142. package/src/components/lightbox/LightboxThumbs.vue +1 -0
  143. package/src/components/listbox/Listbox.vue +26 -29
  144. package/src/components/listbox/ListboxOptionContent.vue +28 -0
  145. package/src/components/listbox/listbox.variants.ts +6 -1
  146. package/src/components/multi-combobox/MultiCombobox.vue +14 -52
  147. package/src/components/multi-combobox/composables/useMultiCombobox.ts +63 -0
  148. package/src/components/multi-select/MultiSelect.vue +3 -4
  149. package/src/components/number-input/NumberInput.vue +8 -7
  150. package/src/components/number-input/number-input.variants.ts +5 -1
  151. package/src/components/password-input/PasswordInput.vue +3 -3
  152. package/src/components/popover/Popover.vue +21 -5
  153. package/src/components/popover/PopoverContent.vue +25 -0
  154. package/src/components/popover/composables/usePopoverCloseFocus.ts +17 -0
  155. package/src/components/radio-group/RadioGroup.vue +3 -3
  156. package/src/components/scroll-area/ScrollArea.vue +7 -5
  157. package/src/components/scroll-area/composables/useScrollFocus.ts +39 -0
  158. package/src/components/scroll-area/scroll-restore.ts +0 -5
  159. package/src/components/search-input/SearchInput.vue +3 -3
  160. package/src/components/segmented-control/SegmentedControl.vue +9 -4
  161. package/src/components/select/Select.vue +68 -25
  162. package/src/components/select/SelectList.vue +3 -3
  163. package/src/components/select/composables/useClearTransition.ts +25 -0
  164. package/src/components/select/select.variants.ts +24 -0
  165. package/src/components/select/types.ts +10 -8
  166. package/src/components/tabs/TabsTrigger.vue +6 -1
  167. package/src/components/textarea/Textarea.vue +2 -41
  168. package/src/components/textarea/composables/useTextareaSizing.ts +54 -0
  169. package/src/components/textarea/textarea.variants.ts +5 -4
  170. package/src/lib/anchored-overlay.ts +110 -0
  171. package/src/lib/caret.ts +0 -4
  172. package/src/lib/collapse.ts +21 -1
  173. package/src/lib/field-focus.ts +0 -4
  174. package/src/styles/interaction.css +4 -2
  175. package/src/styles/typography.css +9 -4
@@ -3,15 +3,15 @@
3
3
  import { cn } from '../../lib/cn'
4
4
  import { focusFieldFrom } from '../../lib/field-focus'
5
5
  import { useFieldControl } from '../form-field/context'
6
- import { inputHost, type InputVariants } from '../input/input.variants'
6
+ import { textInputHost, type TextInputVariants } from '../input/input.variants'
7
7
  import { provideInputGroup } from './context'
8
8
  import { inputGroup } from './input-group.variants'
9
9
 
10
10
  defineOptions({ name: 'HnInputGroup' })
11
11
 
12
12
  const props = defineProps<{
13
- variant?: InputVariants['variant']
14
- size?: InputVariants['size']
13
+ variant?: TextInputVariants['variant']
14
+ size?: TextInputVariants['size']
15
15
  disabled?: boolean
16
16
  invalid?: boolean
17
17
  class?: string
@@ -20,6 +20,7 @@
20
20
  const { disabled } = useFieldControl({ disabled: () => props.disabled })
21
21
 
22
22
  provideInputGroup({
23
+ variant: computed(() => props.variant),
23
24
  size: computed(() => props.size),
24
25
  disabled,
25
26
  invalid: computed(() => !!props.invalid),
@@ -31,7 +32,13 @@
31
32
  data-hn-input-group
32
33
  :data-invalid="props.invalid ? '' : undefined"
33
34
  :data-disabled="disabled ? '' : undefined"
34
- :class="cn(inputHost({ variant: props.variant, size: props.size }), inputGroup(), props.class)"
35
+ :class="
36
+ cn(
37
+ textInputHost({ variant: props.variant, size: props.size }),
38
+ inputGroup({ divided: props.variant !== 'bare' }),
39
+ props.class,
40
+ )
41
+ "
35
42
  @click="focusFieldFrom($event.currentTarget as HTMLElement, $event.target as HTMLElement)"
36
43
  >
37
44
  <slot />
@@ -1,7 +1,8 @@
1
1
  import { inject, provide, type ComputedRef, type InjectionKey } from 'vue'
2
- import type { InputVariants } from '../input/input.variants'
2
+ import type { InputVariants, TextInputVariants } from '../input/input.variants'
3
3
 
4
4
  export interface InputGroupContext {
5
+ variant?: ComputedRef<TextInputVariants['variant']>
5
6
  size: ComputedRef<InputVariants['size']>
6
7
  disabled: ComputedRef<boolean>
7
8
  invalid: ComputedRef<boolean>
@@ -2,10 +2,17 @@ import { tv } from '../../lib/tv'
2
2
 
3
3
  export const inputGroup = tv({
4
4
  base: [
5
- '[&>*+*]:border-s [&>*+*]:border-s-line',
6
5
  '[&>button]:h-auto [&>button]:rounded-none [&>button]:[--hn-press-scale:1]',
7
6
  '[&>button:focus-visible]:-outline-offset-2',
8
7
  ],
8
+ variants: {
9
+ divided: {
10
+ true: '[&>*+*]:border-s [&>*+*]:border-s-line',
11
+ },
12
+ },
13
+ defaultVariants: {
14
+ divided: true,
15
+ },
9
16
  })
10
17
 
11
18
  export const inputGroupAddon = tv({
@@ -65,6 +65,7 @@
65
65
  <Highlight
66
66
  v-if="at === props.index"
67
67
  :id="highlightId"
68
+ axis="x"
68
69
  class="ring-accent absolute inset-0 rounded-md ring-2 ring-inset"
69
70
  />
70
71
  </button>
@@ -1,11 +1,9 @@
1
- <script setup lang="ts">
2
- import { Check } from '@lucide/vue'
1
+ <script setup lang="ts" generic="T extends SelectOption = SelectOption">
3
2
  import {
4
3
  ListboxContent,
5
4
  ListboxGroup,
6
5
  ListboxGroupLabel,
7
6
  ListboxItem,
8
- ListboxItemIndicator,
9
7
  ListboxRoot,
10
8
  } from 'reka-ui'
11
9
  import { cn } from '../../lib/cn'
@@ -14,25 +12,30 @@
14
12
  import ScrollArea from '../scroll-area/ScrollArea.vue'
15
13
  import { selectEmpty, selectItem, selectLabel } from '../select/select.variants'
16
14
  import { isOptionGroup, type SelectItems, type SelectOption } from '../select/types'
15
+ import ListboxOptionContent from './ListboxOptionContent.vue'
17
16
  import { listbox, listboxContent, type ListboxVariants } from './listbox.variants'
18
17
 
19
18
  defineOptions({ name: 'HnListbox', inheritAttrs: false })
20
19
 
21
20
  const props = withDefaults(
22
21
  defineProps<{
23
- options: SelectItems
22
+ options: SelectItems<T>
24
23
  multiple?: boolean
25
24
  maxHeight?: string
25
+ padded?: boolean
26
26
  variant?: ListboxVariants['variant']
27
27
  disabled?: boolean
28
28
  class?: string
29
29
  }>(),
30
- { maxHeight: '20rem' },
30
+ { maxHeight: '20rem', padded: true },
31
31
  )
32
32
 
33
33
  const model = defineModel<string | number | null | Array<string | number>>()
34
34
 
35
- defineSlots<{ option(props: { option: SelectOption }): unknown }>()
35
+ const slots = defineSlots<{
36
+ option?(props: { option: T; selected: boolean }): unknown
37
+ trailing?(props: { option: T; selected: boolean }): unknown
38
+ }>()
36
39
 
37
40
  const t = useUiLocale()
38
41
 
@@ -55,7 +58,7 @@
55
58
  :class="cn(listbox({ variant: props.variant }), props.class)"
56
59
  >
57
60
  <ScrollArea :style="{ maxHeight: props.maxHeight }">
58
- <ListboxContent v-bind="$attrs" :class="listboxContent()">
61
+ <ListboxContent v-bind="$attrs" :class="listboxContent({ padded: props.padded })">
59
62
  <template
60
63
  v-for="item in props.options"
61
64
  :key="isOptionGroup(item) ? item.label : item.value"
@@ -69,31 +72,25 @@
69
72
  :disabled="option.disabled"
70
73
  :class="selectItem()"
71
74
  >
72
- <span class="min-w-0 flex-1">
73
- <slot name="option" :option="option">
74
- <span class="block truncate">{{ option.label }}</span>
75
- <span v-if="option.description" class="text-muted block truncate text-xs">
76
- {{ option.description }}
77
- </span>
78
- </slot>
79
- </span>
80
- <span class="flex size-4 shrink-0 items-center justify-center">
81
- <ListboxItemIndicator><Check /></ListboxItemIndicator>
82
- </span>
75
+ <ListboxOptionContent :option="option">
76
+ <template v-if="slots.option" #option="slotProps">
77
+ <slot name="option" v-bind="slotProps" />
78
+ </template>
79
+ <template v-if="slots.trailing" #trailing="slotProps">
80
+ <slot name="trailing" v-bind="slotProps" />
81
+ </template>
82
+ </ListboxOptionContent>
83
83
  </ListboxItem>
84
84
  </ListboxGroup>
85
85
  <ListboxItem v-else :value="item.value" :disabled="item.disabled" :class="selectItem()">
86
- <span class="min-w-0 flex-1">
87
- <slot name="option" :option="item">
88
- <span class="block truncate">{{ item.label }}</span>
89
- <span v-if="item.description" class="text-muted block truncate text-xs">
90
- {{ item.description }}
91
- </span>
92
- </slot>
93
- </span>
94
- <span class="flex size-4 shrink-0 items-center justify-center">
95
- <ListboxItemIndicator><Check /></ListboxItemIndicator>
96
- </span>
86
+ <ListboxOptionContent :option="item">
87
+ <template v-if="slots.option" #option="slotProps">
88
+ <slot name="option" v-bind="slotProps" />
89
+ </template>
90
+ <template v-if="slots.trailing" #trailing="slotProps">
91
+ <slot name="trailing" v-bind="slotProps" />
92
+ </template>
93
+ </ListboxOptionContent>
97
94
  </ListboxItem>
98
95
  </template>
99
96
  <div v-if="props.options.length === 0" :class="selectEmpty()">{{ t.select.empty }}</div>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts" generic="T extends SelectOption = SelectOption">
2
+ import { Check } from '@lucide/vue'
3
+ import { injectListboxItemContext, ListboxItemIndicator } from 'reka-ui'
4
+ import type { SelectOption } from '../select/types'
5
+
6
+ defineOptions({ name: 'HnListboxOptionContent', inheritAttrs: false })
7
+ const props = defineProps<{ option: T }>()
8
+ const slots = defineSlots<{
9
+ option?(props: { option: T; selected: boolean }): unknown
10
+ trailing?(props: { option: T; selected: boolean }): unknown
11
+ }>()
12
+ const { isSelected: selected } = injectListboxItemContext()
13
+ </script>
14
+
15
+ <template>
16
+ <span class="min-w-0 flex-1">
17
+ <slot name="option" :option="props.option" :selected="selected">
18
+ <span class="block truncate">{{ props.option.label }}</span>
19
+ <span v-if="props.option.description" class="text-muted block truncate text-xs">
20
+ {{ props.option.description }}
21
+ </span>
22
+ </slot>
23
+ </span>
24
+ <slot v-if="slots.trailing" name="trailing" :option="props.option" :selected="selected" />
25
+ <span v-else class="flex size-4 shrink-0 items-center justify-center">
26
+ <ListboxItemIndicator><Check /></ListboxItemIndicator>
27
+ </span>
28
+ </template>
@@ -9,6 +9,7 @@ export const listbox = tv({
9
9
  variant: {
10
10
  primary: 'border-line bg-surface border shadow-sm',
11
11
  secondary: 'bg-inset',
12
+ bare: 'rounded-none border-0 bg-transparent shadow-none',
12
13
  },
13
14
  },
14
15
  defaultVariants: {
@@ -17,7 +18,11 @@ export const listbox = tv({
17
18
  })
18
19
 
19
20
  export const listboxContent = tv({
20
- base: 'flex flex-col p-1 outline-none',
21
+ base: 'flex flex-col outline-none',
22
+ variants: {
23
+ padded: { true: 'p-1', false: 'p-0' },
24
+ },
25
+ defaultVariants: { padded: true },
21
26
  })
22
27
 
23
28
  export type ListboxVariants = VariantProps<typeof listbox>
@@ -1,9 +1,8 @@
1
- <script setup lang="ts">
2
- import { computed, reactive, ref, shallowRef, watch } from 'vue'
1
+ <script setup lang="ts" generic="T extends SelectOption = SelectOption">
2
+ import { computed } from 'vue'
3
3
  import { ComboboxAnchor, ComboboxInput, ComboboxRoot, ComboboxTrigger } from 'reka-ui'
4
4
  import { cn } from '../../lib/cn'
5
5
  import { useFieldControl } from '../form-field/context'
6
- import { focusFieldFrom } from '../../lib/field-focus'
7
6
  import { useUiLocale } from '../../locale'
8
7
  import { buttonIconBox } from '../button/button.variants'
9
8
  import IconSlot from '../button/IconSlot.vue'
@@ -19,7 +18,8 @@
19
18
  inputIndicator,
20
19
  type InputVariants,
21
20
  } from '../input/input.variants'
22
- import { flattenOptions, type SelectItems, type SelectOption } from '../select/types'
21
+ import type { SelectItems, SelectOption } from '../select/types'
22
+ import { useMultiCombobox } from './composables/useMultiCombobox'
23
23
  import {
24
24
  tagsInputChip,
25
25
  tagsInputControl,
@@ -32,7 +32,7 @@
32
32
 
33
33
  const props = withDefaults(
34
34
  defineProps<{
35
- options: SelectItems
35
+ options: SelectItems<T>
36
36
  placeholder?: string
37
37
  ignoreFilter?: boolean
38
38
  loading?: boolean
@@ -52,11 +52,9 @@
52
52
  const search = defineModel<string>('search', { default: '' })
53
53
  const open = defineModel<boolean>('open', { default: false })
54
54
 
55
- defineSlots<{ option(props: { option: SelectOption }): unknown }>()
55
+ defineSlots<{ option?(props: { option: T }): unknown }>()
56
56
 
57
57
  const t = useUiLocale()
58
- const keyboard = ref(false)
59
- const input = shallowRef<{ $el: HTMLInputElement } | null>(null)
60
58
 
61
59
  const {
62
60
  id: fieldId,
@@ -68,52 +66,16 @@
68
66
  disabled: () => props.disabled,
69
67
  })
70
68
 
71
- const labels = reactive(new Map<string | number, string>())
72
- watch(
73
- () => props.options,
74
- options => {
75
- for (const option of flattenOptions(options)) labels.set(option.value, option.label)
76
- },
77
- { immediate: true, deep: true },
78
- )
79
-
80
- const selected = computed(() =>
81
- model.value.map(value => ({ value, label: labels.get(value) ?? String(value) })),
82
- )
69
+ const { selected, keyboard, input, remove, clear, onHostClick, onInputKeydown } =
70
+ useMultiCombobox({
71
+ options: () => props.options,
72
+ model,
73
+ open,
74
+ disabled: () => disabled.value,
75
+ onClear: () => emit('clear'),
76
+ })
83
77
  const chipSize = computed(() => (props.size === 'sm' ? 'sm' : 'md'))
84
78
  const clearing = computed(() => props.clearable && selected.value.length > 0 && !disabled.value)
85
-
86
- watch(open, value => {
87
- if (!value) keyboard.value = false
88
- })
89
-
90
- function remove(value: string | number) {
91
- if (disabled.value) return
92
- model.value = model.value.filter(item => item !== value)
93
- }
94
-
95
- function clear() {
96
- if (!model.value.length) return
97
- model.value = []
98
- emit('clear')
99
- input.value?.$el.focus()
100
- }
101
-
102
- function onHostClick(event: MouseEvent) {
103
- const focused = focusFieldFrom(event.currentTarget as HTMLElement, event.target as HTMLElement)
104
- if (focused && !open.value) open.value = true
105
- }
106
-
107
- function onInputKeydown(event: KeyboardEvent) {
108
- keyboard.value = true
109
- if (
110
- event.key !== 'Backspace' ||
111
- (event.target as HTMLInputElement).value !== '' ||
112
- !model.value.length
113
- )
114
- return
115
- model.value = model.value.slice(0, -1)
116
- }
117
79
  </script>
118
80
 
119
81
  <template>
@@ -0,0 +1,63 @@
1
+ import { computed, reactive, ref, shallowRef, watch, type Ref } from 'vue'
2
+ import { focusFieldFrom } from '../../../lib/field-focus'
3
+ import { flattenOptions, type SelectItems, type SelectOption } from '../../select/types'
4
+
5
+ interface MultiComboboxOptions<T extends SelectOption> {
6
+ options: () => SelectItems<T>
7
+ model: Ref<Array<string | number>>
8
+ open: Ref<boolean>
9
+ disabled: () => boolean
10
+ onClear: () => void
11
+ }
12
+
13
+ export function useMultiCombobox<T extends SelectOption>(options: MultiComboboxOptions<T>) {
14
+ const { model, open } = options
15
+ const keyboard = ref(false)
16
+ const input = shallowRef<{ $el: HTMLInputElement } | null>(null)
17
+ const labels = reactive(new Map<string | number, string>())
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
+ )
25
+
26
+ const selected = computed(() =>
27
+ model.value.map(value => ({ value, label: labels.get(value) ?? String(value) })),
28
+ )
29
+
30
+ watch(open, value => {
31
+ if (!value) keyboard.value = false
32
+ })
33
+
34
+ function remove(value: string | number) {
35
+ if (options.disabled()) return
36
+ model.value = model.value.filter(item => item !== value)
37
+ }
38
+
39
+ function clear() {
40
+ if (!model.value.length) return
41
+ model.value = []
42
+ options.onClear()
43
+ input.value?.$el.focus()
44
+ }
45
+
46
+ function onHostClick(event: MouseEvent) {
47
+ const focused = focusFieldFrom(event.currentTarget as HTMLElement, event.target as HTMLElement)
48
+ if (focused && !open.value) open.value = true
49
+ }
50
+
51
+ function onInputKeydown(event: KeyboardEvent) {
52
+ keyboard.value = true
53
+ if (
54
+ event.key !== 'Backspace' ||
55
+ (event.target as HTMLInputElement).value !== '' ||
56
+ !model.value.length
57
+ )
58
+ return
59
+ model.value = model.value.slice(0, -1)
60
+ }
61
+
62
+ return { selected, keyboard, input, remove, clear, onHostClick, onInputKeydown }
63
+ }
@@ -1,4 +1,4 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="T extends SelectOption = SelectOption">
2
2
  import { computed, ref } from 'vue'
3
3
  import { SelectRoot, SelectTrigger } from 'reka-ui'
4
4
  import { cn } from '../../lib/cn'
@@ -25,7 +25,7 @@
25
25
 
26
26
  const props = withDefaults(
27
27
  defineProps<{
28
- options: SelectItems
28
+ options: SelectItems<T>
29
29
  placeholder?: string
30
30
  name?: string
31
31
  required?: boolean
@@ -45,7 +45,7 @@
45
45
  const model = defineModel<Array<string | number>>({ default: () => [] })
46
46
  const open = defineModel<boolean>('open', { default: false })
47
47
 
48
- defineSlots<{ option(props: { option: SelectOption }): unknown }>()
48
+ defineSlots<{ option?(props: { option: T }): unknown }>()
49
49
 
50
50
  const t = useUiLocale()
51
51
  const group = injectInputGroup()
@@ -157,7 +157,6 @@
157
157
  <DisclosureIcon />
158
158
  </span>
159
159
  </SelectTrigger>
160
- <!-- Reka 2.10 BubbleSelect assigns arrays to select.value, which loses multiple values. -->
161
160
  <VisuallyHidden v-if="props.name" as-child>
162
161
  <select
163
162
  v-model="model"
@@ -16,8 +16,8 @@
16
16
  import {
17
17
  inputControl,
18
18
  inputEmbedded,
19
- inputHost,
20
- type InputVariants,
19
+ textInputHost,
20
+ type TextInputVariants,
21
21
  } from '../input/input.variants'
22
22
  import { numberInputStep, numberInputStepper } from './number-input.variants'
23
23
 
@@ -34,8 +34,8 @@
34
34
  formatOptions?: Intl.NumberFormatOptions
35
35
  locale?: string
36
36
  controls?: boolean
37
- variant?: InputVariants['variant']
38
- size?: InputVariants['size']
37
+ variant?: TextInputVariants['variant']
38
+ size?: TextInputVariants['size']
39
39
  disabled?: boolean
40
40
  readonly?: boolean
41
41
  invalid?: boolean
@@ -47,6 +47,7 @@
47
47
 
48
48
  const t = useUiLocale()
49
49
  const group = injectInputGroup()
50
+ const divided = computed(() => (group ? group.variant?.value : props.variant) !== 'bare')
50
51
  const size = computed(() => (group ? group.size.value : props.size))
51
52
  const {
52
53
  id: fieldId,
@@ -83,7 +84,7 @@
83
84
  :data-disabled="disabled ? '' : undefined"
84
85
  :class="
85
86
  cn(
86
- group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
87
+ group ? inputEmbedded() : textInputHost({ variant: props.variant, size: props.size }),
87
88
  props.class,
88
89
  )
89
90
  "
@@ -95,13 +96,13 @@
95
96
  :aria-describedby="describedBy"
96
97
  :class="cn(inputControl(), 'tabular-nums')"
97
98
  />
98
- <div v-if="props.controls" :class="numberInputStepper({ size })">
99
+ <div v-if="props.controls" :class="numberInputStepper({ size, divided })">
99
100
  <NumberFieldIncrement :aria-label="t.numberInput.increase" :class="numberInputStep()">
100
101
  <ChevronUp />
101
102
  </NumberFieldIncrement>
102
103
  <NumberFieldDecrement
103
104
  :aria-label="t.numberInput.decrease"
104
- :class="numberInputStep({ divided: true })"
105
+ :class="numberInputStep({ divided })"
105
106
  >
106
107
  <ChevronDown />
107
108
  </NumberFieldDecrement>
@@ -1,8 +1,11 @@
1
1
  import { tv } from '../../lib/tv'
2
2
 
3
3
  export const numberInputStepper = tv({
4
- base: 'border-line flex shrink-0 flex-col border-s',
4
+ base: 'flex shrink-0 flex-col',
5
5
  variants: {
6
+ divided: {
7
+ true: 'border-line border-s',
8
+ },
6
9
  size: {
7
10
  sm: '[&_svg]:size-3',
8
11
  md: '[&_svg]:size-3',
@@ -11,6 +14,7 @@ export const numberInputStepper = tv({
11
14
  },
12
15
  defaultVariants: {
13
16
  size: 'md',
17
+ divided: true,
14
18
  },
15
19
  })
16
20
 
@@ -4,13 +4,13 @@
4
4
  import { useUiLocale } from '../../locale'
5
5
  import InputBase from '../input/InputBase.vue'
6
6
  import InputAction from '../input/InputAction.vue'
7
- import type { InputVariants } from '../input/input.variants'
7
+ import type { TextInputVariants } from '../input/input.variants'
8
8
 
9
9
  defineOptions({ name: 'HnPasswordInput', inheritAttrs: false })
10
10
 
11
11
  const props = defineProps<{
12
- variant?: InputVariants['variant']
13
- size?: InputVariants['size']
12
+ variant?: TextInputVariants['variant']
13
+ size?: TextInputVariants['size']
14
14
  disabled?: boolean
15
15
  invalid?: boolean
16
16
  class?: string
@@ -1,23 +1,32 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
- import { PopoverRoot, PopoverTrigger, PopoverPortal, PopoverContent } from 'reka-ui'
3
+ import { PopoverRoot, PopoverTrigger, PopoverPortal, type PopoverContentEmits } from 'reka-ui'
4
+ import PopoverContent from './PopoverContent.vue'
5
+ import { useAnchoredOverlay } from '../../lib/anchored-overlay'
4
6
  import Card from '../card/Card.vue'
5
7
  import { cn } from '../../lib/cn'
6
8
 
7
- defineOptions({ name: 'HnPopover' })
9
+ defineOptions({ name: 'HnPopover', inheritAttrs: false })
8
10
 
9
11
  const props = withDefaults(
10
12
  defineProps<{
13
+ anchor?: HTMLElement | null
14
+ modal?: boolean
11
15
  side?: 'top' | 'right' | 'bottom' | 'left'
12
16
  align?: 'start' | 'center' | 'end'
13
17
  sideOffset?: number
14
18
  padded?: boolean
15
19
  class?: string
16
20
  }>(),
17
- { side: 'bottom', align: 'center', sideOffset: 8, padded: true },
21
+ { side: 'bottom', align: 'center', sideOffset: 8, padded: true, modal: true },
18
22
  )
19
23
 
20
24
  const open = defineModel<boolean>('open')
25
+ const emit = defineEmits<PopoverContentEmits>()
26
+ defineSlots<{ default?(): unknown; content?(): unknown }>()
27
+ const { trigger, reference, visible, events } = useAnchoredOverlay(props, open, emit, event =>
28
+ emit('openAutoFocus', event),
29
+ )
21
30
 
22
31
  const pressOrigin = computed(() => {
23
32
  if (props.side === 'left') return 'right'
@@ -29,12 +38,19 @@
29
38
  </script>
30
39
 
31
40
  <template>
32
- <PopoverRoot v-model:open="open" modal>
33
- <PopoverTrigger as-child :style="{ transformOrigin: pressOrigin }">
41
+ <PopoverRoot v-model:open="visible" :modal="props.modal">
42
+ <PopoverTrigger
43
+ v-if="$slots.default"
44
+ ref="trigger"
45
+ as-child
46
+ :style="{ transformOrigin: pressOrigin }"
47
+ >
34
48
  <slot />
35
49
  </PopoverTrigger>
36
50
  <PopoverPortal>
37
51
  <PopoverContent
52
+ v-bind="{ ...$attrs, ...events }"
53
+ :reference="reference"
38
54
  as-child
39
55
  :side="props.side"
40
56
  :align="props.align"
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ PopoverContent,
4
+ useForwardPropsEmits,
5
+ type PopoverContentProps,
6
+ type PopoverContentEmits,
7
+ } from 'reka-ui'
8
+ import { usePopoverCloseFocus } from './composables/usePopoverCloseFocus'
9
+
10
+ defineOptions({ name: 'HnPopoverContent' })
11
+
12
+ const props = defineProps<PopoverContentProps>()
13
+ const emit = defineEmits<PopoverContentEmits>()
14
+ const forwarded = useForwardPropsEmits(props, emit)
15
+ const closeAutoFocus = usePopoverCloseFocus(emit)
16
+ </script>
17
+
18
+ <template>
19
+ <PopoverContent
20
+ v-bind="{ ...forwarded, onCloseAutoFocus: closeAutoFocus }"
21
+ :reference="props.reference"
22
+ >
23
+ <slot />
24
+ </PopoverContent>
25
+ </template>
@@ -0,0 +1,17 @@
1
+ import { nextTick } from 'vue'
2
+ import { injectPopoverRootContext } from 'reka-ui'
3
+
4
+ export function usePopoverCloseFocus(emit: (event: 'closeAutoFocus', value: Event) => void) {
5
+ const root = injectPopoverRootContext()
6
+
7
+ return (event: Event) => {
8
+ emit('closeAutoFocus', event)
9
+ if (!event.defaultPrevented || !root.triggerElement.value) return
10
+
11
+ const trigger = root.triggerElement.value
12
+ root.triggerElement.value = undefined
13
+ nextTick(() => {
14
+ root.triggerElement.value ??= trigger
15
+ })
16
+ }
17
+ }
@@ -1,4 +1,4 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="T extends SelectOption = SelectOption">
2
2
  import { RadioGroupItem, RadioGroupRoot } from 'reka-ui'
3
3
  import { cn } from '../../lib/cn'
4
4
  import { useFieldControl } from '../form-field/context'
@@ -22,7 +22,7 @@
22
22
  defineOptions({ name: 'HnRadioGroup' })
23
23
 
24
24
  const props = defineProps<{
25
- options: SelectOption[]
25
+ options: T[]
26
26
  orientation?: CheckboxGroupVariants['orientation']
27
27
  size?: CheckboxVariants['size']
28
28
  controlPlacement?: CheckboxVariants['controlPlacement']
@@ -39,7 +39,7 @@
39
39
  disabled: () => props.disabled,
40
40
  })
41
41
 
42
- defineSlots<{ option(props: { option: SelectOption }): unknown }>()
42
+ defineSlots<{ option?(props: { option: T }): unknown }>()
43
43
  </script>
44
44
 
45
45
  <template>