@hina-ui/vue 1.0.0 → 1.2.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 (162) hide show
  1. package/dist/index.js +769 -331
  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/avatar/AvatarGroup.vue.d.ts.map +1 -1
  5. package/dist/types/components/avatar/utils/children.d.ts +3 -0
  6. package/dist/types/components/avatar/utils/children.d.ts.map +1 -0
  7. package/dist/types/components/checkbox/Checkbox.vue.d.ts +2 -0
  8. package/dist/types/components/checkbox/Checkbox.vue.d.ts.map +1 -1
  9. package/dist/types/components/checkbox/checkbox.variants.d.ts +75 -2
  10. package/dist/types/components/checkbox/checkbox.variants.d.ts.map +1 -1
  11. package/dist/types/components/checkbox-group/CheckboxGroup.vue.d.ts +32 -27
  12. package/dist/types/components/checkbox-group/CheckboxGroup.vue.d.ts.map +1 -1
  13. package/dist/types/components/checkbox-group/checkbox-group.variants.d.ts +34 -0
  14. package/dist/types/components/checkbox-group/checkbox-group.variants.d.ts.map +1 -1
  15. package/dist/types/components/collapsible/CollapsibleContent.vue.d.ts.map +1 -1
  16. package/dist/types/components/combobox/Combobox.vue.d.ts +38 -38
  17. package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
  18. package/dist/types/components/combobox/ComboboxList.vue.d.ts +22 -15
  19. package/dist/types/components/combobox/ComboboxList.vue.d.ts.map +1 -1
  20. package/dist/types/components/date-picker/DatePicker.vue.d.ts.map +1 -1
  21. package/dist/types/components/date-range-picker/DateRangePicker.vue.d.ts.map +1 -1
  22. package/dist/types/components/date-time-picker/DateTimePicker.vue.d.ts.map +1 -1
  23. package/dist/types/components/dialog/Dialog.vue.d.ts +22 -16
  24. package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
  25. package/dist/types/components/dialog/dialog.variants.d.ts +32 -2
  26. package/dist/types/components/dialog/dialog.variants.d.ts.map +1 -1
  27. package/dist/types/components/form-field/FormField.vue.d.ts +7 -4
  28. package/dist/types/components/form-field/FormField.vue.d.ts.map +1 -1
  29. package/dist/types/components/form-field/form-field.variants.d.ts +73 -1
  30. package/dist/types/components/form-field/form-field.variants.d.ts.map +1 -1
  31. package/dist/types/components/form-field/types.d.ts +6 -0
  32. package/dist/types/components/form-field/types.d.ts.map +1 -0
  33. package/dist/types/components/form-layout/FormLayout.vue.d.ts +2 -1
  34. package/dist/types/components/form-layout/FormLayout.vue.d.ts.map +1 -1
  35. package/dist/types/components/form-layout/context.d.ts +7 -0
  36. package/dist/types/components/form-layout/context.d.ts.map +1 -0
  37. package/dist/types/components/image/ImageGroup.vue.d.ts.map +1 -1
  38. package/dist/types/components/image/composables/useImageGroupState.d.ts +7 -0
  39. package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -0
  40. package/dist/types/components/input/Input.vue.d.ts +3 -3
  41. package/dist/types/components/input/Input.vue.d.ts.map +1 -1
  42. package/dist/types/components/input/InputBase.vue.d.ts +3 -3
  43. package/dist/types/components/input/InputBase.vue.d.ts.map +1 -1
  44. package/dist/types/components/input/input.variants.d.ts +53 -7
  45. package/dist/types/components/input/input.variants.d.ts.map +1 -1
  46. package/dist/types/components/input-group/InputGroup.vue.d.ts +3 -3
  47. package/dist/types/components/input-group/InputGroup.vue.d.ts.map +1 -1
  48. package/dist/types/components/input-group/context.d.ts +2 -1
  49. package/dist/types/components/input-group/context.d.ts.map +1 -1
  50. package/dist/types/components/input-group/input-group.variants.d.ts +13 -1
  51. package/dist/types/components/input-group/input-group.variants.d.ts.map +1 -1
  52. package/dist/types/components/listbox/Listbox.vue.d.ts +36 -29
  53. package/dist/types/components/listbox/Listbox.vue.d.ts.map +1 -1
  54. package/dist/types/components/listbox/ListboxOptionContent.vue.d.ts +31 -0
  55. package/dist/types/components/listbox/ListboxOptionContent.vue.d.ts.map +1 -0
  56. package/dist/types/components/listbox/listbox.variants.d.ts +19 -1
  57. package/dist/types/components/listbox/listbox.variants.d.ts.map +1 -1
  58. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts +41 -43
  59. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts.map +1 -1
  60. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts +27 -0
  61. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts.map +1 -0
  62. package/dist/types/components/multi-select/MultiSelect.vue.d.ts +39 -41
  63. package/dist/types/components/multi-select/MultiSelect.vue.d.ts.map +1 -1
  64. package/dist/types/components/number-input/NumberInput.vue.d.ts +3 -3
  65. package/dist/types/components/number-input/NumberInput.vue.d.ts.map +1 -1
  66. package/dist/types/components/number-input/number-input.variants.d.ts +10 -1
  67. package/dist/types/components/number-input/number-input.variants.d.ts.map +1 -1
  68. package/dist/types/components/password-input/PasswordInput.vue.d.ts +3 -3
  69. package/dist/types/components/password-input/PasswordInput.vue.d.ts.map +1 -1
  70. package/dist/types/components/radio-group/RadioGroup.vue.d.ts +32 -27
  71. package/dist/types/components/radio-group/RadioGroup.vue.d.ts.map +1 -1
  72. package/dist/types/components/scroll-area/scroll-restore.d.ts +0 -1
  73. package/dist/types/components/scroll-area/scroll-restore.d.ts.map +1 -1
  74. package/dist/types/components/search-input/SearchInput.vue.d.ts +3 -3
  75. package/dist/types/components/search-input/SearchInput.vue.d.ts.map +1 -1
  76. package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts +30 -30
  77. package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts.map +1 -1
  78. package/dist/types/components/select/Select.vue.d.ts +41 -37
  79. package/dist/types/components/select/Select.vue.d.ts.map +1 -1
  80. package/dist/types/components/select/SelectList.vue.d.ts +22 -15
  81. package/dist/types/components/select/SelectList.vue.d.ts.map +1 -1
  82. package/dist/types/components/select/composables/useClearTransition.d.ts +10 -0
  83. package/dist/types/components/select/composables/useClearTransition.d.ts.map +1 -0
  84. package/dist/types/components/select/select.variants.d.ts +16 -0
  85. package/dist/types/components/select/select.variants.d.ts.map +1 -1
  86. package/dist/types/components/select/types.d.ts +7 -7
  87. package/dist/types/components/select/types.d.ts.map +1 -1
  88. package/dist/types/components/switch/Switch.vue.d.ts +3 -0
  89. package/dist/types/components/switch/Switch.vue.d.ts.map +1 -1
  90. package/dist/types/components/tags-input/TagsInput.vue.d.ts.map +1 -1
  91. package/dist/types/components/textarea/Textarea.vue.d.ts.map +1 -1
  92. package/dist/types/components/textarea/composables/useTextareaSizing.d.ts +27 -0
  93. package/dist/types/components/textarea/composables/useTextareaSizing.d.ts.map +1 -0
  94. package/dist/types/components/textarea/textarea.variants.d.ts +9 -6
  95. package/dist/types/components/textarea/textarea.variants.d.ts.map +1 -1
  96. package/dist/types/components/tree-select/TreeSelect.vue.d.ts.map +1 -1
  97. package/dist/types/index.d.ts +1 -0
  98. package/dist/types/index.d.ts.map +1 -1
  99. package/dist/types/lib/caret.d.ts +0 -4
  100. package/dist/types/lib/caret.d.ts.map +1 -1
  101. package/dist/types/lib/collapse.d.ts +2 -0
  102. package/dist/types/lib/collapse.d.ts.map +1 -1
  103. package/dist/types/lib/field-focus.d.ts +0 -4
  104. package/dist/types/lib/field-focus.d.ts.map +1 -1
  105. package/package.json +1 -1
  106. package/src/components/accordion/AccordionContent.vue +2 -11
  107. package/src/components/avatar/AvatarGroup.vue +3 -8
  108. package/src/components/avatar/utils/children.ts +7 -0
  109. package/src/components/checkbox/Checkbox.vue +25 -4
  110. package/src/components/checkbox/checkbox.variants.ts +34 -3
  111. package/src/components/checkbox-group/CheckboxGroup.vue +14 -5
  112. package/src/components/checkbox-group/checkbox-group.variants.ts +9 -0
  113. package/src/components/collapsible/CollapsibleContent.vue +2 -11
  114. package/src/components/combobox/Combobox.vue +4 -3
  115. package/src/components/combobox/ComboboxList.vue +3 -3
  116. package/src/components/date-picker/DatePicker.vue +1 -0
  117. package/src/components/date-range-picker/DateRangePicker.vue +1 -0
  118. package/src/components/date-time-picker/DateTimePicker.vue +1 -0
  119. package/src/components/dialog/Dialog.vue +70 -39
  120. package/src/components/dialog/dialog.variants.ts +9 -3
  121. package/src/components/form-field/FormField.vue +82 -37
  122. package/src/components/form-field/form-field.variants.ts +39 -0
  123. package/src/components/form-field/types.ts +5 -0
  124. package/src/components/form-layout/FormLayout.vue +13 -7
  125. package/src/components/form-layout/context.ts +19 -0
  126. package/src/components/image/ImageGroup.vue +2 -30
  127. package/src/components/image/composables/useImageGroupState.ts +35 -0
  128. package/src/components/input/Input.vue +3 -3
  129. package/src/components/input/InputBase.vue +6 -5
  130. package/src/components/input/input.variants.ts +19 -5
  131. package/src/components/input-group/InputGroup.vue +16 -5
  132. package/src/components/input-group/context.ts +2 -1
  133. package/src/components/input-group/input-group.variants.ts +8 -1
  134. package/src/components/listbox/Listbox.vue +26 -29
  135. package/src/components/listbox/ListboxOptionContent.vue +28 -0
  136. package/src/components/listbox/listbox.variants.ts +6 -1
  137. package/src/components/multi-combobox/MultiCombobox.vue +15 -52
  138. package/src/components/multi-combobox/composables/useMultiCombobox.ts +63 -0
  139. package/src/components/multi-select/MultiSelect.vue +4 -4
  140. package/src/components/number-input/NumberInput.vue +9 -7
  141. package/src/components/number-input/number-input.variants.ts +5 -1
  142. package/src/components/password-input/PasswordInput.vue +3 -3
  143. package/src/components/radio-group/RadioGroup.vue +29 -8
  144. package/src/components/scroll-area/scroll-restore.ts +0 -5
  145. package/src/components/search-input/SearchInput.vue +3 -3
  146. package/src/components/segmented-control/SegmentedControl.vue +3 -3
  147. package/src/components/select/Select.vue +69 -25
  148. package/src/components/select/SelectList.vue +3 -3
  149. package/src/components/select/composables/useClearTransition.ts +25 -0
  150. package/src/components/select/select.variants.ts +24 -0
  151. package/src/components/select/types.ts +10 -8
  152. package/src/components/switch/Switch.vue +26 -4
  153. package/src/components/tags-input/TagsInput.vue +1 -0
  154. package/src/components/textarea/Textarea.vue +3 -41
  155. package/src/components/textarea/composables/useTextareaSizing.ts +54 -0
  156. package/src/components/textarea/textarea.variants.ts +6 -5
  157. package/src/components/tree-select/TreeSelect.vue +1 -0
  158. package/src/index.ts +1 -0
  159. package/src/lib/caret.ts +0 -4
  160. package/src/lib/collapse.ts +21 -1
  161. package/src/lib/field-focus.ts +0 -4
  162. package/src/styles/interaction.css +6 -4
@@ -2,6 +2,8 @@
2
2
  import { computed, shallowRef, useId, useSlots } from 'vue'
3
3
  import { cn } from '../../lib/cn'
4
4
  import { injectForm, provideForm } from '../form/context'
5
+ import type { FormFieldLayoutProps } from '../form-field/types'
6
+ import { provideFormLayout, useFormFieldLayout } from './context'
5
7
  import type { FormErrors } from '../form/standard-schema'
6
8
  import {
7
9
  formLayoutDescription,
@@ -14,16 +16,20 @@
14
16
  defineOptions({ name: 'HnFormLayout' })
15
17
 
16
18
  const props = withDefaults(
17
- defineProps<{
18
- legend?: string
19
- description?: string
20
- columns?: FormLayoutVariants['columns']
21
- disabled?: boolean
22
- class?: string
23
- }>(),
19
+ defineProps<
20
+ FormFieldLayoutProps & {
21
+ legend?: string
22
+ description?: string
23
+ columns?: FormLayoutVariants['columns']
24
+ disabled?: boolean
25
+ class?: string
26
+ }
27
+ >(),
24
28
  { columns: 1 },
25
29
  )
26
30
 
31
+ provideFormLayout(useFormFieldLayout(props))
32
+
27
33
  const slots = useSlots()
28
34
  const form = injectForm()
29
35
  const descriptionId = useId()
@@ -0,0 +1,19 @@
1
+ import { computed, inject, provide, type ComputedRef, type InjectionKey } from 'vue'
2
+ import type { FormFieldLayoutProps } from '../form-field/types'
3
+
4
+ type FieldLayout = ComputedRef<Required<FormFieldLayoutProps>>
5
+ const FORM_LAYOUT_KEY: InjectionKey<FieldLayout> = Symbol('hn-form-layout')
6
+
7
+ export function useFormFieldLayout(props: FormFieldLayoutProps): FieldLayout {
8
+ const parent = inject(FORM_LAYOUT_KEY, null)
9
+ return computed(() => ({
10
+ orientation: props.orientation ?? parent?.value.orientation ?? 'vertical',
11
+ descriptionPlacement:
12
+ props.descriptionPlacement ?? parent?.value.descriptionPlacement ?? 'control',
13
+ labelWidth: props.labelWidth ?? parent?.value.labelWidth ?? '10rem',
14
+ }))
15
+ }
16
+
17
+ export function provideFormLayout(layout: FieldLayout) {
18
+ provide(FORM_LAYOUT_KEY, layout)
19
+ }
@@ -1,40 +1,12 @@
1
1
  <script setup lang="ts">
2
- import { computed, reactive, shallowRef } from 'vue'
3
2
  import Lightbox from '../lightbox/Lightbox.vue'
4
- import type { LightboxItem } from '../lightbox/types'
5
- import { provideImageGroup } from './context'
3
+ import { useImageGroupState } from './composables/useImageGroupState'
6
4
 
7
5
  defineOptions({ name: 'HnImageGroup' })
8
6
 
9
7
  const props = defineProps<{ loop?: boolean }>()
10
8
 
11
- const registry = reactive(new Map<string, () => LightboxItem>())
12
- const open = shallowRef(false)
13
- const index = shallowRef(0)
14
-
15
- function byDom(a: LightboxItem, b: LightboxItem): number {
16
- const first = a.source?.()
17
- const second = b.source?.()
18
- if (!first || !second || first === second) return 0
19
- return first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
20
- }
21
-
22
- const items = computed(() => [...registry.values()].map(read => read()).sort(byDom))
23
-
24
- provideImageGroup({
25
- register: (id, item) => {
26
- registry.set(id, item)
27
- },
28
- unregister: id => {
29
- registry.delete(id)
30
- },
31
- open: id => {
32
- const at = items.value.findIndex(item => item.id === id)
33
- if (at < 0) return
34
- index.value = at
35
- open.value = true
36
- },
37
- })
9
+ const { items, open, index } = useImageGroupState()
38
10
  </script>
39
11
 
40
12
  <template>
@@ -0,0 +1,35 @@
1
+ import { computed, reactive, shallowRef } from 'vue'
2
+ import type { LightboxItem } from '../../lightbox/types'
3
+ import { provideImageGroup } from '../context'
4
+
5
+ export function useImageGroupState() {
6
+ const registry = reactive(new Map<string, () => LightboxItem>())
7
+ const open = shallowRef(false)
8
+ const index = shallowRef(0)
9
+
10
+ function byDom(a: LightboxItem, b: LightboxItem): number {
11
+ const first = a.source?.()
12
+ const second = b.source?.()
13
+ if (!first || !second || first === second) return 0
14
+ return first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
15
+ }
16
+
17
+ const items = computed(() => [...registry.values()].map(read => read()).sort(byDom))
18
+
19
+ provideImageGroup({
20
+ register: (id, item) => {
21
+ registry.set(id, item)
22
+ },
23
+ unregister: id => {
24
+ registry.delete(id)
25
+ },
26
+ open: id => {
27
+ const at = items.value.findIndex(item => item.id === id)
28
+ if (at < 0) return
29
+ index.value = at
30
+ open.value = true
31
+ },
32
+ })
33
+
34
+ return { items, open, index }
35
+ }
@@ -1,13 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import { shallowRef } from 'vue'
3
3
  import InputBase from './InputBase.vue'
4
- import type { InputVariants } from './input.variants'
4
+ import type { TextInputVariants } from './input.variants'
5
5
 
6
6
  defineOptions({ name: 'HnInput', inheritAttrs: false })
7
7
 
8
8
  const props = defineProps<{
9
- variant?: InputVariants['variant']
10
- size?: InputVariants['size']
9
+ variant?: TextInputVariants['variant']
10
+ size?: TextInputVariants['size']
11
11
  clearable?: boolean
12
12
  loading?: boolean
13
13
  disabled?: boolean
@@ -14,15 +14,15 @@
14
14
  inputAdornment,
15
15
  inputControl,
16
16
  inputEmbedded,
17
- inputHost,
18
- type InputVariants,
17
+ textInputHost,
18
+ type TextInputVariants,
19
19
  } from './input.variants'
20
20
 
21
21
  defineOptions({ name: 'HnInputBase', inheritAttrs: false })
22
22
 
23
23
  const props = defineProps<{
24
- variant?: InputVariants['variant']
25
- size?: InputVariants['size']
24
+ variant?: TextInputVariants['variant']
25
+ size?: TextInputVariants['size']
26
26
  clearable?: boolean
27
27
  loading?: boolean
28
28
  disabled?: boolean
@@ -66,10 +66,11 @@
66
66
  <div
67
67
  data-hn-input
68
68
  :data-invalid="invalid ? '' : undefined"
69
+ :data-disabled="disabled ? '' : undefined"
69
70
  :aria-busy="props.loading || undefined"
70
71
  :class="
71
72
  cn(
72
- group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
73
+ group ? inputEmbedded() : textInputHost({ variant: props.variant, size: props.size }),
73
74
  props.class,
74
75
  )
75
76
  "
@@ -2,14 +2,14 @@ import { tv, type VariantProps } from '../../lib/tv'
2
2
 
3
3
  export const inputHost = tv({
4
4
  base: [
5
- 'hn-field text-fg flex w-full min-w-0 cursor-text items-stretch overflow-hidden rounded-md border font-medium',
6
- 'has-disabled:cursor-not-allowed has-disabled:opacity-50',
5
+ 'group/hn-field text-fg flex w-full min-w-0 cursor-text items-stretch overflow-hidden rounded-md border font-medium',
6
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-50',
7
7
  ],
8
8
  variants: {
9
9
  variant: {
10
10
  primary:
11
- 'border-line [--hn-field-bg:var(--hn-surface)] [--hn-field-shadow:var(--hn-shadow-sm)]',
12
- secondary: 'border-transparent [--hn-field-bg:var(--hn-bg-inset)]',
11
+ 'hn-field border-line [--hn-field-bg:var(--hn-surface)] [--hn-field-shadow:var(--hn-shadow-sm)]',
12
+ secondary: 'hn-field border-transparent [--hn-field-bg:var(--hn-bg-inset)]',
13
13
  },
14
14
  size: {
15
15
  sm: 'h-[var(--hn-control-h-sm)] text-sm [--hn-input-h:var(--hn-control-h-sm)] [--hn-input-px:var(--hn-control-px-sm)] [--hn-input-icon:0.875rem]',
@@ -23,6 +23,15 @@ export const inputHost = tv({
23
23
  },
24
24
  })
25
25
 
26
+ export const textInputHost = tv({
27
+ extend: inputHost,
28
+ variants: {
29
+ variant: {
30
+ bare: 'rounded-none border-0 bg-transparent shadow-none outline-none',
31
+ },
32
+ },
33
+ })
34
+
26
35
  export const inputControl = tv({
27
36
  base: [
28
37
  'h-full min-w-0 flex-1 bg-transparent px-[var(--hn-input-px)] text-inherit outline-none [font:inherit]',
@@ -62,7 +71,12 @@ export const inputActionSlot = tv({
62
71
  })
63
72
 
64
73
  export type InputVariants = VariantProps<typeof inputHost>
74
+ export type TextInputVariants = VariantProps<typeof textInputHost>
65
75
 
66
76
  export const inputEmbedded = tv({
67
- base: 'flex min-w-0 flex-1 items-stretch',
77
+ base: [
78
+ 'group/hn-field flex min-w-0 flex-1 items-stretch',
79
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-50',
80
+ 'group-data-disabled/hn-field:data-disabled:opacity-100',
81
+ ],
68
82
  })
@@ -2,23 +2,27 @@
2
2
  import { computed } from 'vue'
3
3
  import { cn } from '../../lib/cn'
4
4
  import { focusFieldFrom } from '../../lib/field-focus'
5
- import { inputHost, type InputVariants } from '../input/input.variants'
5
+ import { useFieldControl } from '../form-field/context'
6
+ import { textInputHost, type TextInputVariants } from '../input/input.variants'
6
7
  import { provideInputGroup } from './context'
7
8
  import { inputGroup } from './input-group.variants'
8
9
 
9
10
  defineOptions({ name: 'HnInputGroup' })
10
11
 
11
12
  const props = defineProps<{
12
- variant?: InputVariants['variant']
13
- size?: InputVariants['size']
13
+ variant?: TextInputVariants['variant']
14
+ size?: TextInputVariants['size']
14
15
  disabled?: boolean
15
16
  invalid?: boolean
16
17
  class?: string
17
18
  }>()
18
19
 
20
+ const { disabled } = useFieldControl({ disabled: () => props.disabled })
21
+
19
22
  provideInputGroup({
23
+ variant: computed(() => props.variant),
20
24
  size: computed(() => props.size),
21
- disabled: computed(() => !!props.disabled),
25
+ disabled,
22
26
  invalid: computed(() => !!props.invalid),
23
27
  })
24
28
  </script>
@@ -27,7 +31,14 @@
27
31
  <div
28
32
  data-hn-input-group
29
33
  :data-invalid="props.invalid ? '' : undefined"
30
- :class="cn(inputHost({ variant: props.variant, size: props.size }), inputGroup(), props.class)"
34
+ :data-disabled="disabled ? '' : undefined"
35
+ :class="
36
+ cn(
37
+ textInputHost({ variant: props.variant, size: props.size }),
38
+ inputGroup({ divided: props.variant !== 'bare' }),
39
+ props.class,
40
+ )
41
+ "
31
42
  @click="focusFieldFrom($event.currentTarget as HTMLElement, $event.target as HTMLElement)"
32
43
  >
33
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({
@@ -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>
@@ -130,6 +92,7 @@
130
92
  <div
131
93
  data-hn-multi-combobox
132
94
  :data-invalid="invalid ? '' : undefined"
95
+ :data-disabled="disabled ? '' : undefined"
133
96
  :aria-busy="props.loading || undefined"
134
97
  :class="
135
98
  cn(
@@ -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
+ }