@hina-ui/vue 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/index.js +769 -331
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/components/accordion/AccordionContent.vue.d.ts.map +1 -1
  4. package/dist/types/components/avatar/AvatarGroup.vue.d.ts.map +1 -1
  5. package/dist/types/components/avatar/utils/children.d.ts +3 -0
  6. package/dist/types/components/avatar/utils/children.d.ts.map +1 -0
  7. package/dist/types/components/checkbox/Checkbox.vue.d.ts +2 -0
  8. package/dist/types/components/checkbox/Checkbox.vue.d.ts.map +1 -1
  9. package/dist/types/components/checkbox/checkbox.variants.d.ts +75 -2
  10. package/dist/types/components/checkbox/checkbox.variants.d.ts.map +1 -1
  11. package/dist/types/components/checkbox-group/CheckboxGroup.vue.d.ts +32 -27
  12. package/dist/types/components/checkbox-group/CheckboxGroup.vue.d.ts.map +1 -1
  13. package/dist/types/components/checkbox-group/checkbox-group.variants.d.ts +34 -0
  14. package/dist/types/components/checkbox-group/checkbox-group.variants.d.ts.map +1 -1
  15. package/dist/types/components/collapsible/CollapsibleContent.vue.d.ts.map +1 -1
  16. package/dist/types/components/combobox/Combobox.vue.d.ts +38 -38
  17. package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
  18. package/dist/types/components/combobox/ComboboxList.vue.d.ts +22 -15
  19. package/dist/types/components/combobox/ComboboxList.vue.d.ts.map +1 -1
  20. package/dist/types/components/date-picker/DatePicker.vue.d.ts.map +1 -1
  21. package/dist/types/components/date-range-picker/DateRangePicker.vue.d.ts.map +1 -1
  22. package/dist/types/components/date-time-picker/DateTimePicker.vue.d.ts.map +1 -1
  23. package/dist/types/components/dialog/Dialog.vue.d.ts +22 -16
  24. package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
  25. package/dist/types/components/dialog/dialog.variants.d.ts +32 -2
  26. package/dist/types/components/dialog/dialog.variants.d.ts.map +1 -1
  27. package/dist/types/components/form-field/FormField.vue.d.ts +7 -4
  28. package/dist/types/components/form-field/FormField.vue.d.ts.map +1 -1
  29. package/dist/types/components/form-field/form-field.variants.d.ts +73 -1
  30. package/dist/types/components/form-field/form-field.variants.d.ts.map +1 -1
  31. package/dist/types/components/form-field/types.d.ts +6 -0
  32. package/dist/types/components/form-field/types.d.ts.map +1 -0
  33. package/dist/types/components/form-layout/FormLayout.vue.d.ts +2 -1
  34. package/dist/types/components/form-layout/FormLayout.vue.d.ts.map +1 -1
  35. package/dist/types/components/form-layout/context.d.ts +7 -0
  36. package/dist/types/components/form-layout/context.d.ts.map +1 -0
  37. package/dist/types/components/image/ImageGroup.vue.d.ts.map +1 -1
  38. package/dist/types/components/image/composables/useImageGroupState.d.ts +7 -0
  39. package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -0
  40. package/dist/types/components/input/Input.vue.d.ts +3 -3
  41. package/dist/types/components/input/Input.vue.d.ts.map +1 -1
  42. package/dist/types/components/input/InputBase.vue.d.ts +3 -3
  43. package/dist/types/components/input/InputBase.vue.d.ts.map +1 -1
  44. package/dist/types/components/input/input.variants.d.ts +53 -7
  45. package/dist/types/components/input/input.variants.d.ts.map +1 -1
  46. package/dist/types/components/input-group/InputGroup.vue.d.ts +3 -3
  47. package/dist/types/components/input-group/InputGroup.vue.d.ts.map +1 -1
  48. package/dist/types/components/input-group/context.d.ts +2 -1
  49. package/dist/types/components/input-group/context.d.ts.map +1 -1
  50. package/dist/types/components/input-group/input-group.variants.d.ts +13 -1
  51. package/dist/types/components/input-group/input-group.variants.d.ts.map +1 -1
  52. package/dist/types/components/listbox/Listbox.vue.d.ts +36 -29
  53. package/dist/types/components/listbox/Listbox.vue.d.ts.map +1 -1
  54. package/dist/types/components/listbox/ListboxOptionContent.vue.d.ts +31 -0
  55. package/dist/types/components/listbox/ListboxOptionContent.vue.d.ts.map +1 -0
  56. package/dist/types/components/listbox/listbox.variants.d.ts +19 -1
  57. package/dist/types/components/listbox/listbox.variants.d.ts.map +1 -1
  58. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts +41 -43
  59. package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts.map +1 -1
  60. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts +27 -0
  61. package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts.map +1 -0
  62. package/dist/types/components/multi-select/MultiSelect.vue.d.ts +39 -41
  63. package/dist/types/components/multi-select/MultiSelect.vue.d.ts.map +1 -1
  64. package/dist/types/components/number-input/NumberInput.vue.d.ts +3 -3
  65. package/dist/types/components/number-input/NumberInput.vue.d.ts.map +1 -1
  66. package/dist/types/components/number-input/number-input.variants.d.ts +10 -1
  67. package/dist/types/components/number-input/number-input.variants.d.ts.map +1 -1
  68. package/dist/types/components/password-input/PasswordInput.vue.d.ts +3 -3
  69. package/dist/types/components/password-input/PasswordInput.vue.d.ts.map +1 -1
  70. package/dist/types/components/radio-group/RadioGroup.vue.d.ts +32 -27
  71. package/dist/types/components/radio-group/RadioGroup.vue.d.ts.map +1 -1
  72. package/dist/types/components/scroll-area/scroll-restore.d.ts +0 -1
  73. package/dist/types/components/scroll-area/scroll-restore.d.ts.map +1 -1
  74. package/dist/types/components/search-input/SearchInput.vue.d.ts +3 -3
  75. package/dist/types/components/search-input/SearchInput.vue.d.ts.map +1 -1
  76. package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts +30 -30
  77. package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts.map +1 -1
  78. package/dist/types/components/select/Select.vue.d.ts +41 -37
  79. package/dist/types/components/select/Select.vue.d.ts.map +1 -1
  80. package/dist/types/components/select/SelectList.vue.d.ts +22 -15
  81. package/dist/types/components/select/SelectList.vue.d.ts.map +1 -1
  82. package/dist/types/components/select/composables/useClearTransition.d.ts +10 -0
  83. package/dist/types/components/select/composables/useClearTransition.d.ts.map +1 -0
  84. package/dist/types/components/select/select.variants.d.ts +16 -0
  85. package/dist/types/components/select/select.variants.d.ts.map +1 -1
  86. package/dist/types/components/select/types.d.ts +7 -7
  87. package/dist/types/components/select/types.d.ts.map +1 -1
  88. package/dist/types/components/switch/Switch.vue.d.ts +3 -0
  89. package/dist/types/components/switch/Switch.vue.d.ts.map +1 -1
  90. package/dist/types/components/tags-input/TagsInput.vue.d.ts.map +1 -1
  91. package/dist/types/components/textarea/Textarea.vue.d.ts.map +1 -1
  92. package/dist/types/components/textarea/composables/useTextareaSizing.d.ts +27 -0
  93. package/dist/types/components/textarea/composables/useTextareaSizing.d.ts.map +1 -0
  94. package/dist/types/components/textarea/textarea.variants.d.ts +9 -6
  95. package/dist/types/components/textarea/textarea.variants.d.ts.map +1 -1
  96. package/dist/types/components/tree-select/TreeSelect.vue.d.ts.map +1 -1
  97. package/dist/types/index.d.ts +1 -0
  98. package/dist/types/index.d.ts.map +1 -1
  99. package/dist/types/lib/caret.d.ts +0 -4
  100. package/dist/types/lib/caret.d.ts.map +1 -1
  101. package/dist/types/lib/collapse.d.ts +2 -0
  102. package/dist/types/lib/collapse.d.ts.map +1 -1
  103. package/dist/types/lib/field-focus.d.ts +0 -4
  104. package/dist/types/lib/field-focus.d.ts.map +1 -1
  105. package/package.json +1 -1
  106. package/src/components/accordion/AccordionContent.vue +2 -11
  107. package/src/components/avatar/AvatarGroup.vue +3 -8
  108. package/src/components/avatar/utils/children.ts +7 -0
  109. package/src/components/checkbox/Checkbox.vue +25 -4
  110. package/src/components/checkbox/checkbox.variants.ts +34 -3
  111. package/src/components/checkbox-group/CheckboxGroup.vue +14 -5
  112. package/src/components/checkbox-group/checkbox-group.variants.ts +9 -0
  113. package/src/components/collapsible/CollapsibleContent.vue +2 -11
  114. package/src/components/combobox/Combobox.vue +4 -3
  115. package/src/components/combobox/ComboboxList.vue +3 -3
  116. package/src/components/date-picker/DatePicker.vue +1 -0
  117. package/src/components/date-range-picker/DateRangePicker.vue +1 -0
  118. package/src/components/date-time-picker/DateTimePicker.vue +1 -0
  119. package/src/components/dialog/Dialog.vue +70 -39
  120. package/src/components/dialog/dialog.variants.ts +9 -3
  121. package/src/components/form-field/FormField.vue +82 -37
  122. package/src/components/form-field/form-field.variants.ts +39 -0
  123. package/src/components/form-field/types.ts +5 -0
  124. package/src/components/form-layout/FormLayout.vue +13 -7
  125. package/src/components/form-layout/context.ts +19 -0
  126. package/src/components/image/ImageGroup.vue +2 -30
  127. package/src/components/image/composables/useImageGroupState.ts +35 -0
  128. package/src/components/input/Input.vue +3 -3
  129. package/src/components/input/InputBase.vue +6 -5
  130. package/src/components/input/input.variants.ts +19 -5
  131. package/src/components/input-group/InputGroup.vue +16 -5
  132. package/src/components/input-group/context.ts +2 -1
  133. package/src/components/input-group/input-group.variants.ts +8 -1
  134. package/src/components/listbox/Listbox.vue +26 -29
  135. package/src/components/listbox/ListboxOptionContent.vue +28 -0
  136. package/src/components/listbox/listbox.variants.ts +6 -1
  137. package/src/components/multi-combobox/MultiCombobox.vue +15 -52
  138. package/src/components/multi-combobox/composables/useMultiCombobox.ts +63 -0
  139. package/src/components/multi-select/MultiSelect.vue +4 -4
  140. package/src/components/number-input/NumberInput.vue +9 -7
  141. package/src/components/number-input/number-input.variants.ts +5 -1
  142. package/src/components/password-input/PasswordInput.vue +3 -3
  143. package/src/components/radio-group/RadioGroup.vue +29 -8
  144. package/src/components/scroll-area/scroll-restore.ts +0 -5
  145. package/src/components/search-input/SearchInput.vue +3 -3
  146. package/src/components/segmented-control/SegmentedControl.vue +3 -3
  147. package/src/components/select/Select.vue +69 -25
  148. package/src/components/select/SelectList.vue +3 -3
  149. package/src/components/select/composables/useClearTransition.ts +25 -0
  150. package/src/components/select/select.variants.ts +24 -0
  151. package/src/components/select/types.ts +10 -8
  152. package/src/components/switch/Switch.vue +26 -4
  153. package/src/components/tags-input/TagsInput.vue +1 -0
  154. package/src/components/textarea/Textarea.vue +3 -41
  155. package/src/components/textarea/composables/useTextareaSizing.ts +54 -0
  156. package/src/components/textarea/textarea.variants.ts +6 -5
  157. package/src/components/tree-select/TreeSelect.vue +1 -0
  158. package/src/index.ts +1 -0
  159. package/src/lib/caret.ts +0 -4
  160. package/src/lib/collapse.ts +21 -1
  161. package/src/lib/field-focus.ts +0 -4
  162. package/src/styles/interaction.css +6 -4
@@ -2,10 +2,16 @@ import { tv, type VariantProps } from '../../lib/tv'
2
2
 
3
3
  export const checkbox = tv({
4
4
  base: [
5
- 'text-fg grid w-fit max-w-full cursor-pointer grid-cols-[auto_minmax(0,1fr)] items-center gap-x-[var(--hn-field-gap)] select-none',
5
+ 'text-fg grid w-fit max-w-full cursor-pointer items-center gap-x-[var(--hn-field-gap)] select-none',
6
6
  'data-disabled:cursor-not-allowed data-disabled:opacity-50',
7
7
  ],
8
8
  variants: {
9
+ controlPlacement: {
10
+ start: 'grid-cols-[auto_minmax(0,1fr)]',
11
+ end: 'grid-cols-[minmax(0,1fr)_auto]',
12
+ },
13
+ block: { true: 'w-full' },
14
+ bare: { true: 'gap-x-0' },
9
15
  size: {
10
16
  sm: 'text-sm',
11
17
  md: 'text-base',
@@ -14,7 +20,20 @@ export const checkbox = tv({
14
20
  },
15
21
  defaultVariants: {
16
22
  size: 'md',
23
+ controlPlacement: 'start',
24
+ block: false,
25
+ },
26
+ })
27
+
28
+ export const checkboxControl = tv({
29
+ base: 'row-start-1',
30
+ variants: {
31
+ controlPlacement: {
32
+ start: 'col-start-1 justify-self-start',
33
+ end: 'col-start-2 justify-self-end',
34
+ },
17
35
  },
36
+ defaultVariants: { controlPlacement: 'start' },
18
37
  })
19
38
 
20
39
  export const checkboxBox = tv({
@@ -46,7 +65,14 @@ export const checkboxBox = tv({
46
65
  })
47
66
 
48
67
  export const checkboxTitle = tv({
49
- base: 'flex min-h-[1lh] min-w-0 items-center',
68
+ base: 'row-start-1 flex min-h-[1lh] min-w-0 items-center',
69
+ variants: {
70
+ controlPlacement: {
71
+ start: 'col-start-2',
72
+ end: 'col-start-1',
73
+ },
74
+ },
75
+ defaultVariants: { controlPlacement: 'start' },
50
76
  })
51
77
 
52
78
  export const checkboxTitleText = tv({
@@ -54,8 +80,12 @@ export const checkboxTitleText = tv({
54
80
  })
55
81
 
56
82
  export const checkboxDescription = tv({
57
- base: 'text-muted col-start-2 block',
83
+ base: 'text-muted row-start-2 block',
58
84
  variants: {
85
+ controlPlacement: {
86
+ start: 'col-start-2',
87
+ end: 'col-start-1',
88
+ },
59
89
  size: {
60
90
  sm: 'text-xs',
61
91
  md: 'text-sm',
@@ -64,6 +94,7 @@ export const checkboxDescription = tv({
64
94
  },
65
95
  defaultVariants: {
66
96
  size: 'md',
97
+ controlPlacement: 'start',
67
98
  },
68
99
  })
69
100
 
@@ -1,18 +1,24 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="T extends SelectOption = SelectOption">
2
2
  import { CheckboxGroupRoot } from 'reka-ui'
3
3
  import { cn } from '../../lib/cn'
4
4
  import { shieldFormField, useFieldControl } from '../form-field/context'
5
5
  import Checkbox from '../checkbox/Checkbox.vue'
6
6
  import type { CheckboxVariants } from '../checkbox/checkbox.variants'
7
7
  import type { SelectOption } from '../select/types'
8
- import { checkboxGroup, type CheckboxGroupVariants } from './checkbox-group.variants'
8
+ import {
9
+ checkboxGroup,
10
+ checkboxGroupItem,
11
+ type CheckboxGroupVariants,
12
+ } from './checkbox-group.variants'
9
13
 
10
14
  defineOptions({ name: 'HnCheckboxGroup' })
11
15
 
12
16
  const props = defineProps<{
13
- options: SelectOption[]
17
+ options: T[]
14
18
  orientation?: CheckboxGroupVariants['orientation']
15
19
  size?: CheckboxVariants['size']
20
+ controlPlacement?: CheckboxVariants['controlPlacement']
21
+ block?: boolean
16
22
  disabled?: boolean
17
23
  invalid?: boolean
18
24
  class?: string
@@ -26,7 +32,7 @@
26
32
  })
27
33
  shieldFormField()
28
34
 
29
- defineSlots<{ option(props: { option: SelectOption }): unknown }>()
35
+ defineSlots<{ option?(props: { option: T }): unknown }>()
30
36
  </script>
31
37
 
32
38
  <template>
@@ -41,13 +47,16 @@
41
47
  data-hn-checkbox-group
42
48
  :data-orientation="props.orientation ?? 'vertical'"
43
49
  :data-disabled="disabled ? '' : undefined"
44
- :class="cn(checkboxGroup({ orientation: props.orientation }), props.class)"
50
+ :class="cn(checkboxGroup({ orientation: props.orientation, block: props.block }), props.class)"
45
51
  >
46
52
  <Checkbox
47
53
  v-for="option in props.options"
48
54
  :key="option.value"
49
55
  :value="option.value"
50
56
  :size="props.size"
57
+ :control-placement="props.controlPlacement"
58
+ :block="props.block"
59
+ :class="checkboxGroupItem({ orientation: props.orientation, block: props.block })"
51
60
  :disabled="disabled || option.disabled"
52
61
  :invalid="invalid"
53
62
  :description="option.description"
@@ -3,6 +3,7 @@ import { tv, type VariantProps } from '../../lib/tv'
3
3
  export const checkboxGroup = tv({
4
4
  base: 'flex min-w-0',
5
5
  variants: {
6
+ block: { true: 'w-full' },
6
7
  orientation: {
7
8
  vertical: 'flex-col gap-2',
8
9
  horizontal: 'flex-row flex-wrap gap-x-[var(--hn-inline-gap)] gap-y-2',
@@ -13,4 +14,12 @@ export const checkboxGroup = tv({
13
14
  },
14
15
  })
15
16
 
17
+ export const checkboxGroupItem = tv({
18
+ variants: {
19
+ orientation: { vertical: '', horizontal: '' },
20
+ block: { true: '' },
21
+ },
22
+ compoundVariants: [{ orientation: 'horizontal', block: true, class: 'flex-1' }],
23
+ })
24
+
16
25
  export type CheckboxGroupVariants = VariantProps<typeof checkboxGroup>
@@ -1,22 +1,13 @@
1
1
  <script setup lang="ts">
2
- import { onBeforeUpdate, onMounted, shallowRef, type ComponentPublicInstance } from 'vue'
3
2
  import { CollapsibleContent as RekaCollapsibleContent } from 'reka-ui'
4
3
  import { cn } from '../../lib/cn'
5
- import { writeCollapseGap } from '../../lib/collapse'
4
+ import { useCollapseGap } from '../../lib/collapse'
6
5
 
7
6
  defineOptions({ name: 'HnCollapsibleContent' })
8
7
 
9
8
  const props = defineProps<{ class?: string }>()
10
9
 
11
- const content = shallowRef<ComponentPublicInstance | null>(null)
12
-
13
- function measure() {
14
- const el = content.value?.$el as Element | undefined
15
- if (el instanceof Element) writeCollapseGap(el, el.parentElement)
16
- }
17
-
18
- onMounted(measure)
19
- onBeforeUpdate(measure)
10
+ const content = useCollapseGap()
20
11
  </script>
21
12
 
22
13
  <template>
@@ -1,4 +1,4 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts" generic="T extends SelectOption = SelectOption">
2
2
  import { computed, ref, shallowRef, watch } from 'vue'
3
3
  import { ComboboxAnchor, ComboboxInput, ComboboxRoot, ComboboxTrigger } from 'reka-ui'
4
4
  import { cn } from '../../lib/cn'
@@ -24,7 +24,7 @@
24
24
  defineOptions({ name: 'HnCombobox', inheritAttrs: false })
25
25
 
26
26
  const props = defineProps<{
27
- options: SelectItems
27
+ options: SelectItems<T>
28
28
  placeholder?: string
29
29
  ignoreFilter?: boolean
30
30
  clearable?: boolean
@@ -40,7 +40,7 @@
40
40
  const search = defineModel<string>('search', { default: '' })
41
41
  const open = defineModel<boolean>('open', { default: false })
42
42
 
43
- defineSlots<{ option(props: { option: SelectOption }): unknown }>()
43
+ defineSlots<{ option?(props: { option: T }): unknown }>()
44
44
 
45
45
  const t = useUiLocale()
46
46
  const group = injectInputGroup()
@@ -93,6 +93,7 @@
93
93
  <div
94
94
  data-hn-combobox
95
95
  :data-invalid="invalid ? '' : undefined"
96
+ :data-disabled="disabled ? '' : undefined"
96
97
  :class="
97
98
  cn(
98
99
  group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
@@ -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>
@@ -73,6 +73,7 @@
73
73
  ref="host"
74
74
  data-hn-date-picker
75
75
  :data-invalid="invalid ? '' : undefined"
76
+ :data-disabled="disabled ? '' : undefined"
76
77
  :class="
77
78
  cn(
78
79
  outer ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
@@ -75,6 +75,7 @@
75
75
  ref="host"
76
76
  data-hn-date-range-picker
77
77
  :data-invalid="invalid ? '' : undefined"
78
+ :data-disabled="disabled ? '' : undefined"
78
79
  :class="
79
80
  cn(
80
81
  outer ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
@@ -96,6 +96,7 @@
96
96
  ref="host"
97
97
  data-hn-date-time-picker
98
98
  :data-invalid="invalid ? '' : undefined"
99
+ :data-disabled="disabled ? '' : undefined"
99
100
  :class="
100
101
  cn(
101
102
  outer ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
@@ -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>
@@ -6,8 +6,13 @@
6
6
  import VisuallyHidden from '../visually-hidden/VisuallyHidden.vue'
7
7
  import { injectForm } from '../form/context'
8
8
  import { provideFormField } from './context'
9
+ import { useFormFieldLayout } from '../form-layout/context'
10
+ import type { FormFieldLayoutProps } from './types'
9
11
  import {
10
12
  formFieldDescription,
13
+ formFieldLayout,
14
+ formFieldContent,
15
+ formFieldControl,
11
16
  formFieldLabel,
12
17
  formFieldMark,
13
18
  formFieldMessage,
@@ -16,16 +21,19 @@
16
21
 
17
22
  defineOptions({ name: 'HnFormField' })
18
23
 
19
- const props = defineProps<{
20
- name?: string
21
- label?: string
22
- description?: string
23
- error?: string
24
- required?: boolean
25
- disabled?: boolean
26
- class?: string
27
- }>()
24
+ const props = defineProps<
25
+ FormFieldLayoutProps & {
26
+ name?: string
27
+ label?: string
28
+ description?: string
29
+ error?: string
30
+ required?: boolean
31
+ disabled?: boolean
32
+ class?: string
33
+ }
34
+ >()
28
35
 
36
+ const layout = useFormFieldLayout(props)
29
37
  const slots = useSlots()
30
38
  const t = useUiLocale()
31
39
  const form = injectForm()
@@ -45,6 +53,19 @@
45
53
  const invalid = computed(() => !!message.value)
46
54
  const disabled = computed(() => !!props.disabled || !!form?.disabled.value)
47
55
  const described = computed(() => !!props.description || !!slots.description)
56
+ const headed = computed(() => !!props.label || !!slots.label)
57
+ const descriptionWithLabel = computed(
58
+ () => described.value && layout.value.descriptionPlacement === 'label',
59
+ )
60
+ const contentOrientation = computed(() =>
61
+ headed.value || descriptionWithLabel.value ? layout.value.orientation : 'vertical',
62
+ )
63
+ const layoutStyle = computed(() => ({
64
+ '--hn-form-label-width':
65
+ typeof layout.value.labelWidth === 'number'
66
+ ? `${layout.value.labelWidth}px`
67
+ : layout.value.labelWidth,
68
+ }))
48
69
  const describedBy = computed(() => {
49
70
  const ids = []
50
71
  if (described.value) ids.push(descriptionId)
@@ -65,39 +86,63 @@
65
86
  data-hn-form-field
66
87
  :data-invalid="invalid ? '' : undefined"
67
88
  :data-disabled="disabled ? '' : undefined"
68
- :class="cn(formFieldRoot(), props.class)"
89
+ :data-orientation="layout.orientation"
90
+ :class="cn(formFieldRoot({ orientation: layout.orientation }), props.class)"
91
+ :style="layoutStyle"
69
92
  @focusout="onFocusOut"
70
93
  >
71
- <label
72
- v-if="props.label || slots.label"
73
- :id="labelId"
74
- :for="controlId"
75
- :class="formFieldLabel({ disabled })"
76
- >
77
- <slot name="label">{{ props.label }}</slot>
78
- <span v-if="props.required" :class="formFieldMark()" aria-hidden="true">*</span>
79
- <VisuallyHidden v-if="props.required">{{ t.form.required }}</VisuallyHidden>
80
- </label>
81
- <slot />
82
- <p v-if="described" :id="descriptionId" :class="formFieldDescription()">
83
- <slot name="description">{{ props.description }}</slot>
84
- </p>
85
- <Transition
86
- enter-from-class="hn-collapse-closed"
87
- enter-to-class="hn-collapse-open"
88
- leave-from-class="hn-collapse-open"
89
- leave-to-class="hn-collapse-closed"
90
- v-on="hooks"
91
- >
94
+ <div data-hn-form-field-layout :class="formFieldLayout({ orientation: contentOrientation })">
92
95
  <div
93
- v-if="message"
94
- data-hn-form-field-message
95
- class="hn-collapse [--hn-collapse-out:var(--hn-duration-fast)]"
96
+ v-if="headed || descriptionWithLabel"
97
+ data-hn-form-field-header
98
+ :class="formFieldContent()"
96
99
  >
97
- <div class="hn-collapse-body">
98
- <p :id="messageId" :class="formFieldMessage()" aria-live="polite">{{ message }}</p>
100
+ <label
101
+ v-if="headed"
102
+ :id="labelId"
103
+ :for="controlId"
104
+ :class="formFieldLabel({ disabled, orientation: contentOrientation })"
105
+ >
106
+ <slot name="label">{{ props.label }}</slot>
107
+ <span v-if="props.required" :class="formFieldMark()" aria-hidden="true">*</span>
108
+ <VisuallyHidden v-if="props.required">{{ t.form.required }}</VisuallyHidden>
109
+ </label>
110
+ <p v-if="descriptionWithLabel" :id="descriptionId" :class="formFieldDescription()">
111
+ <slot name="description">{{ props.description }}</slot>
112
+ </p>
113
+ </div>
114
+ <div data-hn-form-field-body :class="formFieldContent()">
115
+ <div
116
+ data-hn-form-field-control
117
+ :class="formFieldControl({ orientation: contentOrientation })"
118
+ >
119
+ <slot />
99
120
  </div>
121
+ <p
122
+ v-if="described && !descriptionWithLabel"
123
+ :id="descriptionId"
124
+ :class="formFieldDescription()"
125
+ >
126
+ <slot name="description">{{ props.description }}</slot>
127
+ </p>
128
+ <Transition
129
+ enter-from-class="hn-collapse-closed"
130
+ enter-to-class="hn-collapse-open"
131
+ leave-from-class="hn-collapse-open"
132
+ leave-to-class="hn-collapse-closed"
133
+ v-on="hooks"
134
+ >
135
+ <div
136
+ v-if="message"
137
+ data-hn-form-field-message
138
+ class="hn-collapse [--hn-collapse-out:var(--hn-duration-fast)]"
139
+ >
140
+ <div class="hn-collapse-body">
141
+ <p :id="messageId" :class="formFieldMessage()" aria-live="polite">{{ message }}</p>
142
+ </div>
143
+ </div>
144
+ </Transition>
100
145
  </div>
101
- </Transition>
146
+ </div>
102
147
  </div>
103
148
  </template>
@@ -1,12 +1,51 @@
1
1
  import { tv } from '../../lib/tv'
2
2
 
3
3
  export const formFieldRoot = tv({
4
+ base: 'min-w-0',
5
+ variants: {
6
+ orientation: {
7
+ vertical: '',
8
+ horizontal: '',
9
+ responsive: '@container/hn-form-field',
10
+ },
11
+ },
12
+ })
13
+
14
+ export const formFieldLayout = tv({
15
+ base: 'grid min-w-0 grid-cols-1 items-start gap-x-6 gap-y-1.5',
16
+ variants: {
17
+ orientation: {
18
+ vertical: '',
19
+ horizontal: 'grid-cols-[var(--hn-form-label-width)_minmax(0,1fr)]',
20
+ responsive: '@min-[32rem]/hn-form-field:grid-cols-[var(--hn-form-label-width)_minmax(0,1fr)]',
21
+ },
22
+ },
23
+ })
24
+
25
+ export const formFieldContent = tv({
4
26
  base: 'flex min-w-0 flex-col gap-1.5',
5
27
  })
6
28
 
29
+ export const formFieldControl = tv({
30
+ base: 'min-w-0',
31
+ variants: {
32
+ orientation: {
33
+ vertical: '',
34
+ horizontal: 'grid min-h-[var(--hn-control-h-md)] items-center',
35
+ responsive:
36
+ '@min-[32rem]/hn-form-field:grid @min-[32rem]/hn-form-field:min-h-[var(--hn-control-h-md)] @min-[32rem]/hn-form-field:items-center',
37
+ },
38
+ },
39
+ })
40
+
7
41
  export const formFieldLabel = tv({
8
42
  base: 'text-fg inline-flex items-center gap-1 text-sm font-medium',
9
43
  variants: {
44
+ orientation: {
45
+ vertical: '',
46
+ horizontal: 'min-h-[var(--hn-control-h-md)]',
47
+ responsive: '@min-[32rem]/hn-form-field:min-h-[var(--hn-control-h-md)]',
48
+ },
10
49
  disabled: {
11
50
  true: 'text-disabled',
12
51
  },
@@ -0,0 +1,5 @@
1
+ export interface FormFieldLayoutProps {
2
+ orientation?: 'vertical' | 'horizontal' | 'responsive'
3
+ descriptionPlacement?: 'label' | 'control'
4
+ labelWidth?: string | number
5
+ }