@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
@@ -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()
@@ -105,6 +105,7 @@
105
105
  :tabindex="disabled ? -1 : 0"
106
106
  :aria-disabled="disabled || undefined"
107
107
  :data-invalid="invalid ? '' : undefined"
108
+ :data-disabled="disabled ? '' : undefined"
108
109
  :aria-invalid="invalid || undefined"
109
110
  :class="
110
111
  cn(
@@ -156,7 +157,6 @@
156
157
  <DisclosureIcon />
157
158
  </span>
158
159
  </SelectTrigger>
159
- <!-- Reka 2.10 BubbleSelect assigns arrays to select.value, which loses multiple values. -->
160
160
  <VisuallyHidden v-if="props.name" as-child>
161
161
  <select
162
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,
@@ -80,9 +81,10 @@
80
81
  :disabled="disabled"
81
82
  data-hn-number-input
82
83
  :data-invalid="invalid ? '' : undefined"
84
+ :data-disabled="disabled ? '' : undefined"
83
85
  :class="
84
86
  cn(
85
- group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
87
+ group ? inputEmbedded() : textInputHost({ variant: props.variant, size: props.size }),
86
88
  props.class,
87
89
  )
88
90
  "
@@ -94,13 +96,13 @@
94
96
  :aria-describedby="describedBy"
95
97
  :class="cn(inputControl(), 'tabular-nums')"
96
98
  />
97
- <div v-if="props.controls" :class="numberInputStepper({ size })">
99
+ <div v-if="props.controls" :class="numberInputStepper({ size, divided })">
98
100
  <NumberFieldIncrement :aria-label="t.numberInput.increase" :class="numberInputStep()">
99
101
  <ChevronUp />
100
102
  </NumberFieldIncrement>
101
103
  <NumberFieldDecrement
102
104
  :aria-label="t.numberInput.decrease"
103
- :class="numberInputStep({ divided: true })"
105
+ :class="numberInputStep({ divided })"
104
106
  >
105
107
  <ChevronDown />
106
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,9 +1,10 @@
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'
5
5
  import {
6
6
  checkbox,
7
+ checkboxControl,
7
8
  checkboxBox,
8
9
  checkboxDescription,
9
10
  checkboxTitle,
@@ -12,6 +13,7 @@
12
13
  } from '../checkbox/checkbox.variants'
13
14
  import {
14
15
  checkboxGroup,
16
+ checkboxGroupItem,
15
17
  type CheckboxGroupVariants,
16
18
  } from '../checkbox-group/checkbox-group.variants'
17
19
  import type { SelectOption } from '../select/types'
@@ -20,9 +22,11 @@
20
22
  defineOptions({ name: 'HnRadioGroup' })
21
23
 
22
24
  const props = defineProps<{
23
- options: SelectOption[]
25
+ options: T[]
24
26
  orientation?: CheckboxGroupVariants['orientation']
25
27
  size?: CheckboxVariants['size']
28
+ controlPlacement?: CheckboxVariants['controlPlacement']
29
+ block?: boolean
26
30
  disabled?: boolean
27
31
  invalid?: boolean
28
32
  class?: string
@@ -35,7 +39,7 @@
35
39
  disabled: () => props.disabled,
36
40
  })
37
41
 
38
- defineSlots<{ option(props: { option: SelectOption }): unknown }>()
42
+ defineSlots<{ option?(props: { option: T }): unknown }>()
39
43
  </script>
40
44
 
41
45
  <template>
@@ -46,7 +50,7 @@
46
50
  :aria-describedby="describedBy"
47
51
  :aria-invalid="invalid || undefined"
48
52
  data-hn-radio-group
49
- :class="cn(checkboxGroup({ orientation: props.orientation }), props.class)"
53
+ :class="cn(checkboxGroup({ orientation: props.orientation, block: props.block }), props.class)"
50
54
  >
51
55
  <label
52
56
  v-for="option in props.options"
@@ -54,7 +58,16 @@
54
58
  data-hn-radio
55
59
  data-hn-state-group
56
60
  :data-disabled="disabled || option.disabled ? '' : undefined"
57
- :class="checkbox({ size: props.size })"
61
+ :class="
62
+ cn(
63
+ checkbox({
64
+ size: props.size,
65
+ controlPlacement: props.controlPlacement,
66
+ block: props.block,
67
+ }),
68
+ checkboxGroupItem({ orientation: props.orientation, block: props.block }),
69
+ )
70
+ "
58
71
  >
59
72
  <RadioGroupItem
60
73
  v-slot="{ checked }"
@@ -62,7 +75,12 @@
62
75
  :disabled="option.disabled"
63
76
  :aria-invalid="invalid || undefined"
64
77
  :data-invalid="invalid ? '' : undefined"
65
- :class="checkboxBox({ shape: 'round', size: props.size })"
78
+ :class="
79
+ cn(
80
+ checkboxBox({ shape: 'round', size: props.size }),
81
+ checkboxControl({ controlPlacement: props.controlPlacement }),
82
+ )
83
+ "
66
84
  >
67
85
  <Transition
68
86
  enter-active-class="hn-transition-press"
@@ -73,12 +91,15 @@
73
91
  <span v-if="checked" :class="radioDot()" />
74
92
  </Transition>
75
93
  </RadioGroupItem>
76
- <span :class="checkboxTitle()">
94
+ <span :class="checkboxTitle({ controlPlacement: props.controlPlacement })">
77
95
  <span :class="checkboxTitleText()">
78
96
  <slot name="option" :option="option">{{ option.label }}</slot>
79
97
  </span>
80
98
  </span>
81
- <span v-if="option.description" :class="checkboxDescription({ size: props.size })">
99
+ <span
100
+ v-if="option.description"
101
+ :class="checkboxDescription({ size: props.size, controlPlacement: props.controlPlacement })"
102
+ >
82
103
  {{ option.description }}
83
104
  </span>
84
105
  </label>
@@ -37,7 +37,6 @@ export type ScrollRecord = Record<string, number>
37
37
 
38
38
  export interface ScrollRestorerPorts {
39
39
  getViewport: () => HTMLElement | null
40
- /** Where the first-paint script left the scroller, read once and cleared. */
41
40
  takeFirstPaintTop: () => number | undefined
42
41
  readState: () => unknown
43
42
  writeState: (state: Record<string, unknown>) => void
@@ -110,10 +109,6 @@ export function createScrollRestorer(key: string, ports: ScrollRestorerPorts): S
110
109
  const firstPaintTop = ports.takeFirstPaintTop()
111
110
 
112
111
  if (initial) {
113
- // The first-paint script already put the scroller where this entry
114
- // wants it, so anything else means the reader moved it while the page
115
- // was still hydrating — the wheel listener that would have cancelled a
116
- // restore is only attached from here on.
117
112
  if (saved === undefined) return
118
113
  const el = ports.getViewport()
119
114
  if (firstPaintTop !== undefined && el && el.scrollTop !== firstPaintTop) return
@@ -2,14 +2,14 @@
2
2
  import { Search } from '@lucide/vue'
3
3
  import { cn } from '../../lib/cn'
4
4
  import InputBase from '../input/InputBase.vue'
5
- import type { InputVariants } from '../input/input.variants'
5
+ import type { TextInputVariants } from '../input/input.variants'
6
6
 
7
7
  defineOptions({ name: 'HnSearchInput', inheritAttrs: false })
8
8
 
9
9
  const props = withDefaults(
10
10
  defineProps<{
11
- variant?: InputVariants['variant']
12
- size?: InputVariants['size']
11
+ variant?: TextInputVariants['variant']
12
+ size?: TextInputVariants['size']
13
13
  clearable?: boolean
14
14
  loading?: boolean
15
15
  disabled?: boolean
@@ -1,4 +1,4 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="T extends SelectOption = SelectOption">
2
2
  import { ToggleGroupItem, ToggleGroupRoot, type AcceptableValue } from 'reka-ui'
3
3
  import { computed, useId, useSlots } from 'vue'
4
4
  import { cn } from '../../lib/cn'
@@ -18,7 +18,7 @@
18
18
 
19
19
  const props = withDefaults(
20
20
  defineProps<{
21
- options: SelectOption[]
21
+ options: T[]
22
22
  size?: SegmentedItemVariants['size']
23
23
  orientation?: SegmentedControlVariants['orientation']
24
24
  block?: boolean
@@ -30,7 +30,7 @@
30
30
 
31
31
  const model = defineModel<string | number>()
32
32
 
33
- defineSlots<{ option(props: { option: SelectOption }): unknown }>()
33
+ defineSlots<{ option?(props: { option: T }): unknown }>()
34
34
 
35
35
  const slots = useSlots()
36
36
 
@@ -1,8 +1,10 @@
1
- <script setup lang="ts">
2
- import { computed, ref } from 'vue'
3
- import { SelectRoot, SelectTrigger } from 'reka-ui'
1
+ <script setup lang="ts" generic="T extends SelectOption = SelectOption">
2
+ import { computed, ref, shallowRef } from 'vue'
3
+ import { SelectRoot, SelectTrigger, useDirection } from 'reka-ui'
4
4
  import { cn } from '../../lib/cn'
5
5
  import { useUiLocale } from '../../locale'
6
+ import { X } from '@lucide/vue'
7
+ import InputAction from '../input/InputAction.vue'
6
8
  import DisclosureIcon from '../disclosure-icon/DisclosureIcon.vue'
7
9
  import { useFieldControl } from '../form-field/context'
8
10
  import { injectInputGroup } from '../input-group/context'
@@ -13,14 +15,16 @@
13
15
  type InputVariants,
14
16
  } from '../input/input.variants'
15
17
  import SelectList from './SelectList.vue'
16
- import { selectTrigger } from './select.variants'
18
+ import { selectButton, selectClearSlot, selectHost, selectValue } from './select.variants'
19
+ import { useClearTransition } from './composables/useClearTransition'
17
20
  import { flattenOptions, type SelectItems, type SelectOption } from './types'
18
21
 
19
22
  defineOptions({ name: 'HnSelect', inheritAttrs: false })
20
23
 
21
24
  const props = defineProps<{
22
- options: SelectItems
25
+ options: SelectItems<T>
23
26
  placeholder?: string
27
+ clearable?: boolean
24
28
  name?: string
25
29
  required?: boolean
26
30
  autocomplete?: string
@@ -31,17 +35,22 @@
31
35
  class?: string
32
36
  }>()
33
37
 
38
+ const emit = defineEmits<{ clear: [] }>()
39
+
34
40
  const model = defineModel<string | number | null>()
35
41
  const open = defineModel<boolean>('open', { default: false })
36
42
 
37
43
  defineSlots<{
38
- value(props: { option: SelectOption }): unknown
39
- option(props: { option: SelectOption }): unknown
44
+ value?(props: { option: T }): unknown
45
+ option?(props: { option: T }): unknown
40
46
  }>()
41
47
 
42
48
  const t = useUiLocale()
43
49
  const group = injectInputGroup()
50
+ const direction = useDirection()
44
51
  const keyboard = ref(false)
52
+ const host = shallowRef<HTMLElement | null>(null)
53
+ const trigger = shallowRef<{ $el: HTMLElement } | null>(null)
45
54
 
46
55
  const {
47
56
  id: fieldId,
@@ -55,6 +64,18 @@
55
64
  const selected = computed(() =>
56
65
  flattenOptions(props.options).find(option => option.value === model.value),
57
66
  )
67
+ const clearing = computed(
68
+ () => !!props.clearable && model.value != null && model.value !== '' && !disabled.value,
69
+ )
70
+
71
+ const { reserved: clearSpace, hooks: clearTransition } = useClearTransition(() => clearing.value)
72
+
73
+ function clear() {
74
+ if (disabled.value || model.value == null || model.value === '') return
75
+ model.value = null
76
+ emit('clear')
77
+ trigger.value?.$el.focus()
78
+ }
58
79
  </script>
59
80
 
60
81
  <template>
@@ -66,33 +87,56 @@
66
87
  :required="props.required"
67
88
  :autocomplete="props.autocomplete"
68
89
  >
69
- <SelectTrigger
70
- v-bind="$attrs"
71
- :id="fieldId"
72
- :aria-describedby="describedBy"
90
+ <div
91
+ ref="host"
92
+ :dir="direction"
73
93
  data-hn-select
74
- @keydown="keyboard = true"
75
- @pointerdown="keyboard = false"
76
94
  :data-invalid="invalid ? '' : undefined"
77
- :aria-invalid="invalid || undefined"
95
+ :data-disabled="disabled ? '' : undefined"
78
96
  :class="
79
97
  cn(
80
98
  group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
81
- selectTrigger(),
99
+ selectHost(),
82
100
  props.class,
83
101
  )
84
102
  "
85
103
  >
86
- <span class="min-w-0 flex-1 truncate">
87
- <template v-if="selected">
88
- <slot name="value" :option="selected">{{ selected.label }}</slot>
89
- </template>
90
- <template v-else>{{ props.placeholder ?? t.select.placeholder }}</template>
91
- </span>
92
- <span :class="inputAdornment()">
93
- <DisclosureIcon />
94
- </span>
95
- </SelectTrigger>
104
+ <SelectTrigger
105
+ ref="trigger"
106
+ v-bind="$attrs"
107
+ :reference="host ?? undefined"
108
+ :id="fieldId"
109
+ :aria-describedby="describedBy"
110
+ :aria-invalid="invalid || undefined"
111
+ data-hn-select-trigger
112
+ @keydown="keyboard = true"
113
+ @pointerdown="keyboard = false"
114
+ :class="selectButton()"
115
+ >
116
+ <span :class="selectValue({ clearing: clearSpace })">
117
+ <template v-if="selected">
118
+ <slot name="value" :option="selected">{{ selected.label }}</slot>
119
+ </template>
120
+ <template v-else>{{ props.placeholder ?? t.select.placeholder }}</template>
121
+ </span>
122
+ <span :class="inputAdornment()">
123
+ <DisclosureIcon />
124
+ </span>
125
+ </SelectTrigger>
126
+ <Transition
127
+ enter-active-class="hn-transition-base"
128
+ enter-from-class="scale-90 opacity-0"
129
+ leave-active-class="hn-transition"
130
+ leave-to-class="scale-90 opacity-0"
131
+ v-on="clearTransition"
132
+ >
133
+ <span v-if="clearing" :class="selectClearSlot()">
134
+ <InputAction data-hn-select-clear :label="t.common.clear" @click="clear">
135
+ <X />
136
+ </InputAction>
137
+ </span>
138
+ </Transition>
139
+ </div>
96
140
  <SelectList :options="props.options" :keyboard="keyboard">
97
141
  <template #option="slotProps">
98
142
  <slot name="option" v-bind="slotProps" />
@@ -1,4 +1,4 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="T extends SelectOption = SelectOption">
2
2
  import { Check } from '@lucide/vue'
3
3
  import {
4
4
  SelectContent,
@@ -25,11 +25,11 @@
25
25
 
26
26
  defineOptions({ name: 'HnSelectList' })
27
27
 
28
- const props = defineProps<{ options: SelectItems; keyboard?: boolean }>()
28
+ const props = defineProps<{ options: SelectItems<T>; keyboard?: boolean }>()
29
29
 
30
30
  const fresh = ref(false)
31
31
 
32
- defineSlots<{ option(props: { option: SelectOption }): unknown }>()
32
+ defineSlots<{ option?(props: { option: T }): unknown }>()
33
33
 
34
34
  const t = useUiLocale()
35
35
  </script>
@@ -0,0 +1,25 @@
1
+ import { computed, ref } from 'vue'
2
+
3
+ export function useClearTransition(visible: () => boolean) {
4
+ const leaving = ref(false)
5
+ const reserved = computed(() => visible() || leaving.value)
6
+
7
+ function beforeEnter(element: Element) {
8
+ leaving.value = false
9
+ element.removeAttribute('inert')
10
+ }
11
+
12
+ function beforeLeave(element: Element) {
13
+ leaving.value = true
14
+ element.setAttribute('inert', '')
15
+ }
16
+
17
+ function finishLeave() {
18
+ leaving.value = false
19
+ }
20
+
21
+ return {
22
+ reserved,
23
+ hooks: { beforeEnter, beforeLeave, afterLeave: finishLeave, leaveCancelled: finishLeave },
24
+ }
25
+ }
@@ -1,4 +1,5 @@
1
1
  import { tv } from '../../lib/tv'
2
+ import { inputActionSlot } from '../input/input.variants'
2
3
 
3
4
  export const selectTrigger = tv({
4
5
  base: [
@@ -9,6 +10,14 @@ export const selectTrigger = tv({
9
10
  ],
10
11
  })
11
12
 
13
+ export const selectButton = tv({
14
+ base: [
15
+ 'group/hn-disclosure flex h-full min-w-0 flex-1 cursor-pointer items-center bg-transparent ps-[var(--hn-input-px)] pe-0 text-start text-inherit outline-none [font:inherit]',
16
+ 'data-placeholder:text-faint data-placeholder:font-normal disabled:cursor-not-allowed',
17
+ '[&_svg]:size-[var(--hn-input-icon)] [&_svg]:shrink-0',
18
+ ],
19
+ })
20
+
12
21
  export const selectContent = tv({
13
22
  base: [
14
23
  'hn-anim-pop z-(--hn-z-overlay) flex w-[var(--reka-select-trigger-width)] min-w-40 flex-col overflow-hidden shadow-md outline-none',
@@ -40,3 +49,18 @@ export const selectEmpty = tv({
40
49
  export const selectListBody = tv({
41
50
  base: 'flex flex-col p-1',
42
51
  })
52
+
53
+ export const selectHost = tv({
54
+ base: 'relative cursor-pointer',
55
+ })
56
+
57
+ export const selectValue = tv({
58
+ base: 'min-w-0 flex-1 truncate',
59
+ variants: {
60
+ clearing: { true: 'pe-[var(--hn-input-h)]' },
61
+ },
62
+ })
63
+
64
+ export const selectClearSlot = tv({
65
+ base: [inputActionSlot(), 'absolute inset-y-0 end-[var(--hn-input-h)]'],
66
+ })
@@ -1,21 +1,23 @@
1
- export interface SelectOption {
1
+ export type SelectOption<T extends object = object> = {
2
2
  value: string | number
3
3
  label: string
4
4
  description?: string
5
5
  disabled?: boolean
6
- }
6
+ } & T
7
7
 
8
- export interface SelectOptionGroup {
8
+ export interface SelectOptionGroup<T extends SelectOption = SelectOption> {
9
9
  label: string
10
- options: SelectOption[]
10
+ options: T[]
11
11
  }
12
12
 
13
- export type SelectItems = Array<SelectOption | SelectOptionGroup>
13
+ export type SelectItems<T extends SelectOption = SelectOption> = Array<T | SelectOptionGroup<T>>
14
14
 
15
- export function isOptionGroup(item: SelectOption | SelectOptionGroup): item is SelectOptionGroup {
16
- return 'options' in item
15
+ export function isOptionGroup<T extends SelectOption>(
16
+ item: T | SelectOptionGroup<T>,
17
+ ): item is SelectOptionGroup<T> {
18
+ return !('value' in item) && 'options' in item
17
19
  }
18
20
 
19
- export function flattenOptions(items: SelectItems): SelectOption[] {
21
+ export function flattenOptions<T extends SelectOption>(items: SelectItems<T>): T[] {
20
22
  return items.flatMap(item => (isOptionGroup(item) ? item.options : [item]))
21
23
  }
@@ -5,9 +5,11 @@
5
5
  import { useFieldControl } from '../form-field/context'
6
6
  import {
7
7
  checkbox,
8
+ checkboxControl,
8
9
  checkboxDescription,
9
10
  checkboxTitle,
10
11
  checkboxTitleText,
12
+ type CheckboxVariants,
11
13
  } from '../checkbox/checkbox.variants'
12
14
  import { switchThumb, switchTrack, type SwitchVariants } from './switch.variants'
13
15
 
@@ -16,6 +18,8 @@
16
18
  const props = defineProps<{
17
19
  size?: SwitchVariants['size']
18
20
  description?: string
21
+ controlPlacement?: CheckboxVariants['controlPlacement']
22
+ block?: boolean
19
23
  disabled?: boolean
20
24
  invalid?: boolean
21
25
  class?: string
@@ -41,7 +45,17 @@
41
45
  data-hn-switch
42
46
  data-hn-state-group
43
47
  :data-disabled="disabled ? '' : undefined"
44
- :class="cn(checkbox({ size: props.size }), props.class)"
48
+ :class="
49
+ cn(
50
+ checkbox({
51
+ size: props.size,
52
+ controlPlacement: props.controlPlacement,
53
+ block: props.block,
54
+ bare: !slots.default && !props.description,
55
+ }),
56
+ props.class,
57
+ )
58
+ "
45
59
  >
46
60
  <SwitchRoot
47
61
  v-bind="$attrs"
@@ -52,14 +66,22 @@
52
66
  :aria-describedby="describedBy"
53
67
  :data-invalid="invalid ? '' : undefined"
54
68
  :data-hn-on="model ? '' : undefined"
55
- :class="switchTrack({ size: props.size })"
69
+ :class="
70
+ cn(
71
+ switchTrack({ size: props.size }),
72
+ checkboxControl({ controlPlacement: props.controlPlacement }),
73
+ )
74
+ "
56
75
  >
57
76
  <span data-hn-thumb aria-hidden="true" :class="switchThumb()" />
58
77
  </SwitchRoot>
59
- <span v-if="slots.default" :class="checkboxTitle()">
78
+ <span v-if="slots.default" :class="checkboxTitle({ controlPlacement: props.controlPlacement })">
60
79
  <span :class="checkboxTitleText()"><slot /></span>
61
80
  </span>
62
- <span v-if="props.description" :class="checkboxDescription({ size: props.size })">
81
+ <span
82
+ v-if="props.description"
83
+ :class="checkboxDescription({ size: props.size, controlPlacement: props.controlPlacement })"
84
+ >
63
85
  {{ props.description }}
64
86
  </span>
65
87
  </label>
@@ -78,6 +78,7 @@
78
78
  <div
79
79
  data-hn-tags-input
80
80
  :data-invalid="invalid ? '' : undefined"
81
+ :data-disabled="disabled ? '' : undefined"
81
82
  @click="focusFieldFrom($event.currentTarget as HTMLElement, $event.target as HTMLElement)"
82
83
  :class="
83
84
  cn(