@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
@@ -6,6 +6,7 @@
6
6
  import { useEdgeShadow } from './composables/useEdgeShadow'
7
7
  import { useWheelRedirect } from './composables/useWheelRedirect'
8
8
  import { useLayerLock } from './composables/useLayerLock'
9
+ import { useScrollFocus } from './composables/useScrollFocus'
9
10
 
10
11
  defineOptions({ name: 'HnScrollArea', inheritAttrs: false })
11
12
 
@@ -59,6 +60,10 @@
59
60
  }
60
61
 
61
62
  const { viewport, instance, onEvent } = useOverlayScrollbars(host, content, options)
63
+ const { hostFocus, viewportFocus } = useScrollFocus(host, viewport, () => ({
64
+ focusable: props.focusable,
65
+ label: props.label ?? t.value.scroll.regionLabel,
66
+ }))
62
67
  const { showXStart, showXEnd, showYStart, showYEnd, updateEdges } = useEdgeShadow(
63
68
  viewport,
64
69
  () => ({ direction: props.direction, shadow: props.shadow }),
@@ -83,14 +88,11 @@
83
88
  <div :class="cn('hn-scroll-area relative flex flex-col overflow-hidden', props.class)">
84
89
  <div
85
90
  ref="host"
86
- v-bind="$attrs"
91
+ v-bind="{ ...$attrs, ...hostFocus }"
87
92
  data-overlayscrollbars-initialize
88
- :tabindex="props.focusable ? 0 : undefined"
89
- :role="props.focusable ? 'region' : undefined"
90
- :aria-label="props.focusable ? (props.label ?? t.scroll.regionLabel) : undefined"
91
93
  class="w-full min-h-0 grow"
92
94
  >
93
- <div ref="content" data-overlayscrollbars-contents>
95
+ <div ref="content" v-bind="viewportFocus" data-overlayscrollbars-contents>
94
96
  <slot />
95
97
  </div>
96
98
  </div>
@@ -0,0 +1,39 @@
1
+ import { computed, nextTick, watch, type ShallowRef } from 'vue'
2
+
3
+ export function useScrollFocus(
4
+ host: ShallowRef<HTMLElement | undefined>,
5
+ viewport: ShallowRef<HTMLElement | undefined>,
6
+ options: () => { focusable: boolean; label: string },
7
+ ) {
8
+ function attributes(enabled: boolean) {
9
+ const { focusable, label } = options()
10
+ return {
11
+ tabindex: enabled && focusable ? 0 : undefined,
12
+ role: enabled && focusable ? 'region' : undefined,
13
+ 'aria-label': enabled && focusable ? label : undefined,
14
+ }
15
+ }
16
+
17
+ const hostFocus = computed(() => attributes(!viewport.value))
18
+ const viewportFocus = computed(() => attributes(!!viewport.value))
19
+
20
+ watch(
21
+ viewport,
22
+ async element => {
23
+ const target = host.value
24
+ if (!element || !target || !options().focusable) return
25
+ if (target.ownerDocument.activeElement !== target) return
26
+ await nextTick()
27
+ const active = target.ownerDocument.activeElement
28
+ if (
29
+ options().focusable &&
30
+ element.isConnected &&
31
+ (active === target || active === target.ownerDocument.body)
32
+ )
33
+ element.focus({ preventScroll: true })
34
+ },
35
+ { flush: 'sync' },
36
+ )
37
+
38
+ return { hostFocus, viewportFocus }
39
+ }
@@ -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
 
@@ -71,7 +71,12 @@
71
71
  :aria-label="slots.option ? option.label : undefined"
72
72
  :class="segmentedItem({ size: props.size, block: props.block })"
73
73
  >
74
- <Highlight v-if="option.value === current" :id="highlightId" :class="segmentedThumb()" />
74
+ <Highlight
75
+ v-if="option.value === current"
76
+ :id="highlightId"
77
+ :axis="props.orientation === 'vertical' ? 'y' : 'x'"
78
+ :class="segmentedThumb()"
79
+ />
75
80
  <Ripple />
76
81
  <slot name="option" :option="option">{{ option.label }}</slot>
77
82
  </ToggleGroupItem>
@@ -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,34 +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
95
  :data-disabled="disabled ? '' : undefined"
78
- :aria-invalid="invalid || undefined"
79
96
  :class="
80
97
  cn(
81
98
  group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
82
- selectTrigger(),
99
+ selectHost(),
83
100
  props.class,
84
101
  )
85
102
  "
86
103
  >
87
- <span class="min-w-0 flex-1 truncate">
88
- <template v-if="selected">
89
- <slot name="value" :option="selected">{{ selected.label }}</slot>
90
- </template>
91
- <template v-else>{{ props.placeholder ?? t.select.placeholder }}</template>
92
- </span>
93
- <span :class="inputAdornment()">
94
- <DisclosureIcon />
95
- </span>
96
- </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>
97
140
  <SelectList :options="props.options" :keyboard="keyboard">
98
141
  <template #option="slotProps">
99
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
  }
@@ -26,7 +26,12 @@
26
26
  :disabled="props.disabled"
27
27
  :class="cn(tabsTrigger({ variant, size, orientation }), props.class)"
28
28
  >
29
- <Highlight v-if="active" :id="highlightId" :class="tabsHighlight({ variant, orientation })" />
29
+ <Highlight
30
+ v-if="active"
31
+ :id="highlightId"
32
+ :axis="orientation === 'vertical' ? 'y' : 'x'"
33
+ :class="tabsHighlight({ variant, orientation })"
34
+ />
30
35
  <Ripple />
31
36
  <slot />
32
37
  </TabsTrigger>
@@ -1,8 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { computed, nextTick, onMounted, shallowRef, watch } from 'vue'
3
2
  import { cn } from '../../lib/cn'
4
3
  import { useFieldControl } from '../form-field/context'
5
- import { caretTop } from '../../lib/caret'
4
+ import { useTextareaSizing } from './composables/useTextareaSizing'
6
5
  import ScrollArea from '../scroll-area/ScrollArea.vue'
7
6
  import { textarea, textareaField, type TextareaVariants } from './textarea.variants'
8
7
 
@@ -34,45 +33,7 @@
34
33
 
35
34
  const model = defineModel<string>()
36
35
 
37
- const el = shallowRef<HTMLTextAreaElement | null>(null)
38
- const area = shallowRef<InstanceType<typeof ScrollArea>>()
39
-
40
- const bounds = computed(() => {
41
- const options = props.autosize === true ? {} : props.autosize || null
42
- return { min: options?.minRows ?? props.rows, max: options?.maxRows }
43
- })
44
-
45
- function fit() {
46
- const node = el.value
47
- if (!node) return
48
- node.style.height = 'auto'
49
- node.style.height = `${node.scrollHeight}px`
50
- if (document.activeElement === node) reveal(node)
51
- }
52
-
53
- function reveal(node: HTMLTextAreaElement) {
54
- const scroller =
55
- area.value?.viewport ?? node.closest<HTMLElement>('[data-overlayscrollbars-initialize]')
56
- if (!scroller) return
57
- const box = scroller.getBoundingClientRect()
58
- const top = node.getBoundingClientRect().top + caretTop(node)
59
- const bottom = top + parseFloat(getComputedStyle(node).lineHeight)
60
- if (bottom > box.bottom) scroller.scrollTop += bottom - box.bottom
61
- else if (top < box.top) scroller.scrollTop -= box.top - top
62
- }
63
-
64
- function focus(event: MouseEvent) {
65
- const node = el.value
66
- if (!node || node.disabled || event.target === node) return
67
- node.focus()
68
- node.setSelectionRange(node.value.length, node.value.length)
69
- }
70
-
71
- onMounted(fit)
72
- watch(
73
- () => [model.value, props.size, bounds.value.min],
74
- () => void nextTick(fit),
75
- )
36
+ const { el, area, bounds, fit, focus } = useTextareaSizing(props, model)
76
37
  </script>
77
38
 
78
39
  <template>
@@ -0,0 +1,54 @@
1
+ import { computed, nextTick, onMounted, shallowRef, watch, type Ref } from 'vue'
2
+ import { caretTop } from '../../../lib/caret'
3
+ import type { TextareaVariants } from '../textarea.variants'
4
+
5
+ interface TextareaSizingProps {
6
+ autosize?: boolean | { minRows?: number; maxRows?: number }
7
+ rows: number
8
+ size?: TextareaVariants['size']
9
+ variant?: TextareaVariants['variant']
10
+ }
11
+
12
+ export function useTextareaSizing(props: TextareaSizingProps, model: Ref<string | undefined>) {
13
+ const el = shallowRef<HTMLTextAreaElement | null>(null)
14
+ const area = shallowRef<{ viewport?: HTMLElement }>()
15
+
16
+ const bounds = computed(() => {
17
+ const options = props.autosize === true ? {} : props.autosize || null
18
+ return { min: options?.minRows ?? props.rows, max: options?.maxRows }
19
+ })
20
+
21
+ function fit() {
22
+ const node = el.value
23
+ if (!node) return
24
+ node.style.height = 'auto'
25
+ node.style.height = `${node.scrollHeight}px`
26
+ if (document.activeElement === node) reveal(node)
27
+ }
28
+
29
+ function reveal(node: HTMLTextAreaElement) {
30
+ const scroller =
31
+ area.value?.viewport ?? node.closest<HTMLElement>('[data-overlayscrollbars-initialize]')
32
+ if (!scroller) return
33
+ const box = scroller.getBoundingClientRect()
34
+ const top = node.getBoundingClientRect().top + caretTop(node)
35
+ const bottom = top + parseFloat(getComputedStyle(node).lineHeight)
36
+ if (bottom > box.bottom) scroller.scrollTop += bottom - box.bottom
37
+ else if (top < box.top) scroller.scrollTop -= box.top - top
38
+ }
39
+
40
+ function focus(event: MouseEvent) {
41
+ const node = el.value
42
+ if (!node || node.disabled || event.target === node) return
43
+ node.focus()
44
+ node.setSelectionRange(node.value.length, node.value.length)
45
+ }
46
+
47
+ onMounted(fit)
48
+ watch(
49
+ () => [model.value, props.size, props.variant, bounds.value.min],
50
+ () => void nextTick(fit),
51
+ )
52
+
53
+ return { el, area, bounds, fit, focus }
54
+ }
@@ -2,14 +2,15 @@ import { tv, type VariantProps } from '../../lib/tv'
2
2
 
3
3
  export const textarea = tv({
4
4
  base: [
5
- 'hn-field text-fg flex w-full min-w-0 cursor-text flex-col overflow-hidden rounded-md border font-medium',
5
+ 'text-fg flex w-full min-w-0 cursor-text flex-col overflow-hidden rounded-md border font-medium',
6
6
  'data-disabled:cursor-not-allowed data-disabled:opacity-50',
7
7
  ],
8
8
  variants: {
9
9
  variant: {
10
+ bare: 'rounded-none border-0 bg-transparent shadow-none outline-none [--hn-textarea-border-w:0px]',
10
11
  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)]',
12
+ 'hn-field border-line [--hn-field-bg:var(--hn-surface)] [--hn-field-shadow:var(--hn-shadow-sm)]',
13
+ secondary: 'hn-field border-transparent [--hn-field-bg:var(--hn-bg-inset)]',
13
14
  },
14
15
  size: {
15
16
  sm: 'text-sm [--hn-textarea-h:var(--hn-control-h-sm)] [--hn-textarea-px:var(--hn-control-px-sm)]',
@@ -39,7 +40,7 @@ export const textarea = tv({
39
40
  export const textareaField = tv({
40
41
  base: [
41
42
  'block w-full resize-none overflow-hidden bg-transparent text-inherit outline-none [font:inherit]',
42
- 'px-[var(--hn-textarea-px)] py-[calc((var(--hn-textarea-h)_-_1lh)/2_-_1px)]',
43
+ 'px-[var(--hn-textarea-px)] py-[calc((var(--hn-textarea-h)_-_1lh)/2_-_var(--hn-textarea-border-w,1px))]',
43
44
  'placeholder:text-faint placeholder:font-normal disabled:cursor-not-allowed',
44
45
  ],
45
46
  })