@hina-ui/vue 1.1.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 (122) hide show
  1. package/dist/index.js +485 -262
  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-group/CheckboxGroup.vue.d.ts +32 -29
  8. package/dist/types/components/checkbox-group/CheckboxGroup.vue.d.ts.map +1 -1
  9. package/dist/types/components/collapsible/CollapsibleContent.vue.d.ts.map +1 -1
  10. package/dist/types/components/combobox/Combobox.vue.d.ts +38 -38
  11. package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
  12. package/dist/types/components/combobox/ComboboxList.vue.d.ts +22 -15
  13. package/dist/types/components/combobox/ComboboxList.vue.d.ts.map +1 -1
  14. package/dist/types/components/dialog/Dialog.vue.d.ts +22 -16
  15. package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
  16. package/dist/types/components/dialog/dialog.variants.d.ts +32 -2
  17. package/dist/types/components/dialog/dialog.variants.d.ts.map +1 -1
  18. package/dist/types/components/image/ImageGroup.vue.d.ts.map +1 -1
  19. package/dist/types/components/image/composables/useImageGroupState.d.ts +7 -0
  20. package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -0
  21. package/dist/types/components/input/Input.vue.d.ts +3 -3
  22. package/dist/types/components/input/Input.vue.d.ts.map +1 -1
  23. package/dist/types/components/input/InputBase.vue.d.ts +3 -3
  24. package/dist/types/components/input/InputBase.vue.d.ts.map +1 -1
  25. package/dist/types/components/input/input.variants.d.ts +52 -6
  26. package/dist/types/components/input/input.variants.d.ts.map +1 -1
  27. package/dist/types/components/input-group/InputGroup.vue.d.ts +3 -3
  28. package/dist/types/components/input-group/InputGroup.vue.d.ts.map +1 -1
  29. package/dist/types/components/input-group/context.d.ts +2 -1
  30. package/dist/types/components/input-group/context.d.ts.map +1 -1
  31. package/dist/types/components/input-group/input-group.variants.d.ts +13 -1
  32. package/dist/types/components/input-group/input-group.variants.d.ts.map +1 -1
  33. package/dist/types/components/listbox/Listbox.vue.d.ts +36 -29
  34. package/dist/types/components/listbox/Listbox.vue.d.ts.map +1 -1
  35. package/dist/types/components/listbox/ListboxOptionContent.vue.d.ts +31 -0
  36. package/dist/types/components/listbox/ListboxOptionContent.vue.d.ts.map +1 -0
  37. package/dist/types/components/listbox/listbox.variants.d.ts +19 -1
  38. package/dist/types/components/listbox/listbox.variants.d.ts.map +1 -1
  39. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts +41 -43
  40. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts.map +1 -1
  41. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts +27 -0
  42. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts.map +1 -0
  43. package/dist/types/components/multi-select/MultiSelect.vue.d.ts +39 -41
  44. package/dist/types/components/multi-select/MultiSelect.vue.d.ts.map +1 -1
  45. package/dist/types/components/number-input/NumberInput.vue.d.ts +3 -3
  46. package/dist/types/components/number-input/NumberInput.vue.d.ts.map +1 -1
  47. package/dist/types/components/number-input/number-input.variants.d.ts +10 -1
  48. package/dist/types/components/number-input/number-input.variants.d.ts.map +1 -1
  49. package/dist/types/components/password-input/PasswordInput.vue.d.ts +3 -3
  50. package/dist/types/components/password-input/PasswordInput.vue.d.ts.map +1 -1
  51. package/dist/types/components/radio-group/RadioGroup.vue.d.ts +32 -29
  52. package/dist/types/components/radio-group/RadioGroup.vue.d.ts.map +1 -1
  53. package/dist/types/components/scroll-area/scroll-restore.d.ts +0 -1
  54. package/dist/types/components/scroll-area/scroll-restore.d.ts.map +1 -1
  55. package/dist/types/components/search-input/SearchInput.vue.d.ts +3 -3
  56. package/dist/types/components/search-input/SearchInput.vue.d.ts.map +1 -1
  57. package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts +30 -30
  58. package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts.map +1 -1
  59. package/dist/types/components/select/Select.vue.d.ts +41 -37
  60. package/dist/types/components/select/Select.vue.d.ts.map +1 -1
  61. package/dist/types/components/select/SelectList.vue.d.ts +22 -15
  62. package/dist/types/components/select/SelectList.vue.d.ts.map +1 -1
  63. package/dist/types/components/select/composables/useClearTransition.d.ts +10 -0
  64. package/dist/types/components/select/composables/useClearTransition.d.ts.map +1 -0
  65. package/dist/types/components/select/select.variants.d.ts +16 -0
  66. package/dist/types/components/select/select.variants.d.ts.map +1 -1
  67. package/dist/types/components/select/types.d.ts +7 -7
  68. package/dist/types/components/select/types.d.ts.map +1 -1
  69. package/dist/types/components/textarea/Textarea.vue.d.ts.map +1 -1
  70. package/dist/types/components/textarea/composables/useTextareaSizing.d.ts +27 -0
  71. package/dist/types/components/textarea/composables/useTextareaSizing.d.ts.map +1 -0
  72. package/dist/types/components/textarea/textarea.variants.d.ts +9 -6
  73. package/dist/types/components/textarea/textarea.variants.d.ts.map +1 -1
  74. package/dist/types/lib/caret.d.ts +0 -4
  75. package/dist/types/lib/caret.d.ts.map +1 -1
  76. package/dist/types/lib/collapse.d.ts +2 -0
  77. package/dist/types/lib/collapse.d.ts.map +1 -1
  78. package/dist/types/lib/field-focus.d.ts +0 -4
  79. package/dist/types/lib/field-focus.d.ts.map +1 -1
  80. package/package.json +1 -1
  81. package/src/components/accordion/AccordionContent.vue +2 -11
  82. package/src/components/avatar/AvatarGroup.vue +3 -8
  83. package/src/components/avatar/utils/children.ts +7 -0
  84. package/src/components/checkbox-group/CheckboxGroup.vue +3 -3
  85. package/src/components/collapsible/CollapsibleContent.vue +2 -11
  86. package/src/components/combobox/Combobox.vue +3 -3
  87. package/src/components/combobox/ComboboxList.vue +3 -3
  88. package/src/components/dialog/Dialog.vue +70 -39
  89. package/src/components/dialog/dialog.variants.ts +9 -3
  90. package/src/components/image/ImageGroup.vue +2 -30
  91. package/src/components/image/composables/useImageGroupState.ts +35 -0
  92. package/src/components/input/Input.vue +3 -3
  93. package/src/components/input/InputBase.vue +5 -5
  94. package/src/components/input/input.variants.ts +13 -3
  95. package/src/components/input-group/InputGroup.vue +11 -4
  96. package/src/components/input-group/context.ts +2 -1
  97. package/src/components/input-group/input-group.variants.ts +8 -1
  98. package/src/components/listbox/Listbox.vue +26 -29
  99. package/src/components/listbox/ListboxOptionContent.vue +28 -0
  100. package/src/components/listbox/listbox.variants.ts +6 -1
  101. package/src/components/multi-combobox/MultiCombobox.vue +14 -52
  102. package/src/components/multi-combobox/composables/useMultiCombobox.ts +63 -0
  103. package/src/components/multi-select/MultiSelect.vue +3 -4
  104. package/src/components/number-input/NumberInput.vue +8 -7
  105. package/src/components/number-input/number-input.variants.ts +5 -1
  106. package/src/components/password-input/PasswordInput.vue +3 -3
  107. package/src/components/radio-group/RadioGroup.vue +3 -3
  108. package/src/components/scroll-area/scroll-restore.ts +0 -5
  109. package/src/components/search-input/SearchInput.vue +3 -3
  110. package/src/components/segmented-control/SegmentedControl.vue +3 -3
  111. package/src/components/select/Select.vue +68 -25
  112. package/src/components/select/SelectList.vue +3 -3
  113. package/src/components/select/composables/useClearTransition.ts +25 -0
  114. package/src/components/select/select.variants.ts +24 -0
  115. package/src/components/select/types.ts +10 -8
  116. package/src/components/textarea/Textarea.vue +2 -41
  117. package/src/components/textarea/composables/useTextareaSizing.ts +54 -0
  118. package/src/components/textarea/textarea.variants.ts +5 -4
  119. package/src/lib/caret.ts +0 -4
  120. package/src/lib/collapse.ts +21 -1
  121. package/src/lib/field-focus.ts +0 -4
  122. package/src/styles/interaction.css +4 -2
@@ -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
  ComboboxContent,
@@ -19,11 +19,11 @@
19
19
 
20
20
  defineOptions({ name: 'HnComboboxList' })
21
21
 
22
- const props = defineProps<{ options: SelectItems; keyboard?: boolean }>()
22
+ const props = defineProps<{ options: SelectItems<T>; keyboard?: boolean }>()
23
23
 
24
24
  const fresh = ref(false)
25
25
 
26
- defineSlots<{ option(props: { option: SelectOption }): unknown }>()
26
+ defineSlots<{ option?(props: { option: T }): unknown }>()
27
27
 
28
28
  const t = useUiLocale()
29
29
  </script>
@@ -15,8 +15,7 @@
15
15
  import Heading from '../heading/Heading.vue'
16
16
  import Text from '../text/Text.vue'
17
17
  import { cn } from '../../lib/cn'
18
- import { useUiLocale } from '../../locale'
19
- import { dialogWrapper, dialogCard } from './dialog.variants'
18
+ import { dialogWrapper, dialogCard, type DialogVariants } from './dialog.variants'
20
19
 
21
20
  defineOptions({ name: 'HnDialog' })
22
21
 
@@ -24,20 +23,30 @@
24
23
  defineProps<{
25
24
  title: string
26
25
  description?: string
27
- size?: 'sm' | 'md' | 'lg'
28
- placement?: 'center' | 'bottom'
26
+ size?: DialogVariants['size']
27
+ placement?: 'center' | 'top' | 'bottom'
28
+ header?: boolean
29
+ closable?: boolean
29
30
  locked?: boolean
30
31
  class?: string
31
32
  }>(),
32
- { size: 'md', locked: false },
33
+ { size: 'md', header: true, closable: true, locked: false },
33
34
  )
34
35
 
36
+ const slots = defineSlots<{
37
+ default?(): unknown
38
+ icon?(): unknown
39
+ title?(): unknown
40
+ body?(props: { close: () => void }): unknown
41
+ content?(props: { close: () => void }): unknown
42
+ footer?(props: { close: () => void }): unknown
43
+ }>()
44
+
35
45
  function guard(e: Event) {
36
46
  if (props.locked) e.preventDefault()
37
47
  }
38
48
 
39
49
  const open = defineModel<boolean>('open')
40
- const t = useUiLocale()
41
50
 
42
51
  function close() {
43
52
  open.value = false
@@ -51,50 +60,72 @@
51
60
  </DialogTrigger>
52
61
  <DialogPortal>
53
62
  <DialogOverlay class="hn-scrim" />
54
- <div
55
- :class="
56
- cn(
57
- 'pointer-events-none fixed inset-0 z-(--hn-z-overlay) grid',
58
- dialogWrapper({ placement: props.placement ?? 'auto' }),
59
- )
60
- "
61
- >
63
+ <div :class="dialogWrapper({ placement: props.placement ?? 'auto' })">
62
64
  <DialogContent as-child @escape-key-down="guard" @interact-outside="guard">
63
65
  <Card
66
+ v-bind="props.description ? {} : { 'aria-describedby': undefined }"
64
67
  :padded="false"
65
68
  :class="
66
69
  cn(
67
- 'pointer-events-auto flex w-full flex-col gap-4 shadow-lg outline-none',
68
- 'max-h-[calc(100dvh-2rem)] py-(--hn-panel-p)',
69
- dialogCard({ placement: props.placement ?? 'auto', size: props.size }),
70
+ dialogCard({
71
+ placement: props.placement ?? 'auto',
72
+ size: props.size,
73
+ padded: !slots.body,
74
+ fitViewport: true,
75
+ }),
70
76
  props.class,
71
77
  )
72
78
  "
73
79
  >
74
- <div class="flex shrink-0 items-start justify-between gap-4 px-(--hn-panel-p)">
75
- <div class="flex min-w-0 flex-col gap-1.5">
76
- <DialogTitle as-child>
77
- <Heading :level="2" size="lg">{{ props.title }}</Heading>
78
- </DialogTitle>
79
- <DialogDescription v-if="props.description" as-child>
80
- <Text tone="muted">{{ props.description }}</Text>
81
- </DialogDescription>
80
+ <template v-if="!props.header || slots.body">
81
+ <DialogTitle as-child>
82
+ <Heading :level="2" class="sr-only">{{ props.title }}</Heading>
83
+ </DialogTitle>
84
+ <DialogDescription v-if="props.description" class="sr-only">
85
+ {{ props.description }}
86
+ </DialogDescription>
87
+ </template>
88
+ <slot v-if="slots.body" name="body" :close="close" />
89
+ <template v-else>
90
+ <div
91
+ v-if="props.header"
92
+ class="flex shrink-0 items-start justify-between gap-4 px-(--hn-panel-p)"
93
+ >
94
+ <div class="flex min-w-0 flex-col gap-1.5">
95
+ <div class="flex min-w-0 items-center gap-2">
96
+ <span
97
+ v-if="slots.icon"
98
+ class="text-muted flex shrink-0 [&_svg]:size-5"
99
+ aria-hidden="true"
100
+ >
101
+ <slot name="icon" />
102
+ </span>
103
+ <DialogTitle as-child>
104
+ <Heading :level="2" size="lg" class="min-w-0">
105
+ <slot name="title">{{ props.title }}</slot>
106
+ </Heading>
107
+ </DialogTitle>
108
+ </div>
109
+ <DialogDescription v-if="props.description" as-child>
110
+ <Text tone="muted">{{ props.description }}</Text>
111
+ </DialogDescription>
112
+ </div>
113
+ <DialogClose v-if="props.closable" as-child>
114
+ <CloseButton :disabled="props.locked" class="-mt-1.5 -me-1.5 shrink-0" />
115
+ </DialogClose>
82
116
  </div>
83
- <DialogClose as-child>
84
- <CloseButton :disabled="props.locked" class="-mt-1.5 -me-1.5 shrink-0" />
85
- </DialogClose>
86
- </div>
87
- <ScrollArea v-if="$slots.content" class="min-h-0">
88
- <div class="px-(--hn-panel-p) py-1">
89
- <slot name="content" :close="close" />
117
+ <ScrollArea v-if="$slots.content" class="min-h-0">
118
+ <div class="px-(--hn-panel-p) py-1">
119
+ <slot name="content" :close="close" />
120
+ </div>
121
+ </ScrollArea>
122
+ <div
123
+ v-if="$slots.footer"
124
+ class="flex shrink-0 justify-end gap-(--hn-inline-gap) px-(--hn-panel-p)"
125
+ >
126
+ <slot name="footer" :close="close" />
90
127
  </div>
91
- </ScrollArea>
92
- <div
93
- v-if="$slots.footer"
94
- class="flex shrink-0 justify-end gap-(--hn-inline-gap) px-(--hn-panel-p)"
95
- >
96
- <slot name="footer" :close="close" />
97
- </div>
128
+ </template>
98
129
  </Card>
99
130
  </DialogContent>
100
131
  </div>
@@ -1,10 +1,11 @@
1
1
  import { tv, type VariantProps } from '../../lib/tv'
2
2
 
3
3
  export const dialogWrapper = tv({
4
- base: '',
4
+ base: 'pointer-events-none fixed inset-0 z-(--hn-z-overlay) grid',
5
5
  variants: {
6
6
  placement: {
7
7
  center: 'place-items-center p-4',
8
+ top: 'items-start justify-items-center p-4',
8
9
  bottom: 'items-end justify-items-center p-4',
9
10
  auto: 'place-items-center p-4 max-sm:items-end max-sm:justify-items-stretch',
10
11
  },
@@ -13,10 +14,13 @@ export const dialogWrapper = tv({
13
14
  })
14
15
 
15
16
  export const dialogCard = tv({
16
- base: '',
17
+ base: 'pointer-events-auto flex w-full flex-col shadow-lg outline-none',
17
18
  variants: {
19
+ padded: { true: 'gap-4 py-(--hn-panel-p)' },
20
+ fitViewport: { true: 'max-h-[calc(100dvh-2rem)]' },
18
21
  placement: {
19
22
  center: 'hn-anim-modal',
23
+ top: 'hn-anim-sheet-top',
20
24
  bottom: 'hn-anim-sheet-bottom',
21
25
  auto: 'hn-anim-modal max-sm:hn-anim-sheet-bottom max-sm:max-w-none',
22
26
  },
@@ -24,9 +28,11 @@ export const dialogCard = tv({
24
28
  sm: 'max-w-sm',
25
29
  md: 'max-w-md',
26
30
  lg: 'max-w-xl',
31
+ xl: 'max-w-2xl',
32
+ '2xl': 'max-w-4xl',
27
33
  },
28
34
  },
29
- defaultVariants: { placement: 'auto', size: 'md' },
35
+ defaultVariants: { placement: 'auto', size: 'md', padded: true },
30
36
  })
31
37
 
32
38
  export type DialogVariants = VariantProps<typeof dialogCard>
@@ -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
@@ -70,7 +70,7 @@
70
70
  :aria-busy="props.loading || undefined"
71
71
  :class="
72
72
  cn(
73
- group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
73
+ group ? inputEmbedded() : textInputHost({ variant: props.variant, size: props.size }),
74
74
  props.class,
75
75
  )
76
76
  "
@@ -2,14 +2,14 @@ import { tv, type VariantProps } from '../../lib/tv'
2
2
 
3
3
  export const inputHost = tv({
4
4
  base: [
5
- 'group/hn-field hn-field text-fg flex w-full min-w-0 cursor-text items-stretch overflow-hidden rounded-md border font-medium',
5
+ 'group/hn-field text-fg flex w-full min-w-0 cursor-text items-stretch 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
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,6 +71,7 @@ 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
77
  base: [
@@ -3,15 +3,15 @@
3
3
  import { cn } from '../../lib/cn'
4
4
  import { focusFieldFrom } from '../../lib/field-focus'
5
5
  import { useFieldControl } from '../form-field/context'
6
- import { inputHost, type InputVariants } from '../input/input.variants'
6
+ import { textInputHost, type TextInputVariants } from '../input/input.variants'
7
7
  import { provideInputGroup } from './context'
8
8
  import { inputGroup } from './input-group.variants'
9
9
 
10
10
  defineOptions({ name: 'HnInputGroup' })
11
11
 
12
12
  const props = defineProps<{
13
- variant?: InputVariants['variant']
14
- size?: InputVariants['size']
13
+ variant?: TextInputVariants['variant']
14
+ size?: TextInputVariants['size']
15
15
  disabled?: boolean
16
16
  invalid?: boolean
17
17
  class?: string
@@ -20,6 +20,7 @@
20
20
  const { disabled } = useFieldControl({ disabled: () => props.disabled })
21
21
 
22
22
  provideInputGroup({
23
+ variant: computed(() => props.variant),
23
24
  size: computed(() => props.size),
24
25
  disabled,
25
26
  invalid: computed(() => !!props.invalid),
@@ -31,7 +32,13 @@
31
32
  data-hn-input-group
32
33
  :data-invalid="props.invalid ? '' : undefined"
33
34
  :data-disabled="disabled ? '' : undefined"
34
- :class="cn(inputHost({ variant: props.variant, size: props.size }), inputGroup(), props.class)"
35
+ :class="
36
+ cn(
37
+ textInputHost({ variant: props.variant, size: props.size }),
38
+ inputGroup({ divided: props.variant !== 'bare' }),
39
+ props.class,
40
+ )
41
+ "
35
42
  @click="focusFieldFrom($event.currentTarget as HTMLElement, $event.target as HTMLElement)"
36
43
  >
37
44
  <slot />
@@ -1,7 +1,8 @@
1
1
  import { inject, provide, type ComputedRef, type InjectionKey } from 'vue'
2
- import type { InputVariants } from '../input/input.variants'
2
+ import type { InputVariants, TextInputVariants } from '../input/input.variants'
3
3
 
4
4
  export interface InputGroupContext {
5
+ variant?: ComputedRef<TextInputVariants['variant']>
5
6
  size: ComputedRef<InputVariants['size']>
6
7
  disabled: ComputedRef<boolean>
7
8
  invalid: ComputedRef<boolean>
@@ -2,10 +2,17 @@ import { tv } from '../../lib/tv'
2
2
 
3
3
  export const inputGroup = tv({
4
4
  base: [
5
- '[&>*+*]:border-s [&>*+*]:border-s-line',
6
5
  '[&>button]:h-auto [&>button]:rounded-none [&>button]:[--hn-press-scale:1]',
7
6
  '[&>button:focus-visible]:-outline-offset-2',
8
7
  ],
8
+ variants: {
9
+ divided: {
10
+ true: '[&>*+*]:border-s [&>*+*]:border-s-line',
11
+ },
12
+ },
13
+ defaultVariants: {
14
+ divided: true,
15
+ },
9
16
  })
10
17
 
11
18
  export const inputGroupAddon = tv({
@@ -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>