@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
@@ -1,12 +1,17 @@
1
1
  <script setup lang="ts">
2
- import { computed, onMounted, shallowRef, watch } from 'vue'
3
- import type { ThemedToken } from 'shiki/core'
2
+ import { computed } from 'vue'
4
3
  import { cn } from '../../lib/cn'
5
- import { devWarn } from '../../lib/dev'
6
4
  import { useUiLocale } from '../../locale'
7
5
  import CopyButton from '../copy-button/CopyButton.vue'
8
6
  import ScrollArea from '../scroll-area/ScrollArea.vue'
9
- import { tokenize } from './highlighter'
7
+ import { useCodeHighlight } from './composables/useCodeHighlight'
8
+ import {
9
+ codeBlock,
10
+ codeBlockArea,
11
+ codeBlockContent,
12
+ codeBlockActions,
13
+ codeBlockLabel,
14
+ } from './code-block.variants'
10
15
 
11
16
  defineOptions({ name: 'HnCodeBlock' })
12
17
 
@@ -25,38 +30,18 @@
25
30
  const t = useUiLocale()
26
31
  const tag = computed(() => props.label ?? props.lang)
27
32
 
28
- const tokens = shallowRef<ThemedToken[][] | null>(null)
29
-
30
- onMounted(() => {
31
- watch(
32
- () => [props.code, props.lang, props.html] as const,
33
- async ([code, lang, html]) => {
34
- if (html || !lang) {
35
- tokens.value = null
36
- return
37
- }
38
- try {
39
- const result = await tokenize(code, lang)
40
- if (code === props.code && lang === props.lang) tokens.value = result
41
- } catch (error) {
42
- tokens.value = null
43
- devWarn('CodeBlock', `着色管线失败,已退回素文本(lang="${lang}"):${String(error)}`, lang)
44
- }
45
- },
46
- { immediate: true },
47
- )
48
- })
33
+ const tokens = useCodeHighlight(props)
49
34
  </script>
50
35
 
51
36
  <template>
52
- <div :data-lang="props.lang" :class="cn('relative', props.class)">
53
- <ScrollArea direction="horizontal" focusable :label="tag" class="hn-pre">
37
+ <div :data-lang="props.lang" :class="cn(codeBlock(), props.class)">
38
+ <ScrollArea direction="both" focusable :label="tag" :class="codeBlockArea()">
54
39
  <pre
55
- class="m-0"
40
+ :class="codeBlockContent()"
56
41
  ><code v-if="props.html" v-html="props.html"></code><code v-else-if="tokens"><template v-for="(line, i) of tokens" :key="i">{{ i ? '\n' : '' }}<span v-for="(tk, j) of line" :key="j" :style="tk.htmlStyle">{{ tk.content }}</span></template></code><code v-else>{{ code }}</code></pre>
57
42
  </ScrollArea>
58
- <div class="absolute top-2 end-2 flex items-center gap-2">
59
- <span v-if="tag" class="text-faint font-mono text-xs select-none">{{ tag }}</span>
43
+ <div :class="codeBlockActions()">
44
+ <span v-if="tag" :class="codeBlockLabel()">{{ tag }}</span>
60
45
  <CopyButton v-if="copyable" :text="props.code" :label="t.codeblock.copy" />
61
46
  </div>
62
47
  </div>
@@ -0,0 +1,21 @@
1
+ import { tv } from '../../lib/tv'
2
+
3
+ export const codeBlock = tv({
4
+ base: 'relative flex min-h-0 min-w-0 flex-col',
5
+ })
6
+
7
+ export const codeBlockArea = tv({
8
+ base: 'hn-pre min-h-0 grow p-0',
9
+ })
10
+
11
+ export const codeBlockContent = tv({
12
+ base: 'hn-pre-content m-0 w-max min-w-full',
13
+ })
14
+
15
+ export const codeBlockActions = tv({
16
+ base: 'absolute top-2 end-2 flex items-center gap-2',
17
+ })
18
+
19
+ export const codeBlockLabel = tv({
20
+ base: 'text-faint font-mono text-xs select-none',
21
+ })
@@ -0,0 +1,30 @@
1
+ import { onMounted, shallowRef, watch } from 'vue'
2
+ import type { ThemedToken } from 'shiki/core'
3
+ import { devWarn } from '../../../lib/dev'
4
+ import { tokenize } from '../highlighter'
5
+
6
+ export function useCodeHighlight(props: { code: string; lang?: string; html?: string }) {
7
+ const tokens = shallowRef<ThemedToken[][] | null>(null)
8
+
9
+ onMounted(() => {
10
+ watch(
11
+ () => [props.code, props.lang, props.html] as const,
12
+ async ([code, lang, html]) => {
13
+ if (html || !lang) {
14
+ tokens.value = null
15
+ return
16
+ }
17
+ try {
18
+ const result = await tokenize(code, lang)
19
+ if (code === props.code && lang === props.lang) tokens.value = result
20
+ } catch (error) {
21
+ tokens.value = null
22
+ devWarn('CodeBlock', `着色管线失败,已退回素文本(lang="${lang}"):${String(error)}`, lang)
23
+ }
24
+ },
25
+ { immediate: true },
26
+ )
27
+ })
28
+
29
+ return tokens
30
+ }
@@ -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()
@@ -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>
@@ -5,24 +5,32 @@
5
5
  DropdownMenuTrigger,
6
6
  DropdownMenuPortal,
7
7
  DropdownMenuContent,
8
+ type DropdownMenuContentEmits,
8
9
  } from 'reka-ui'
10
+ import { useAnchoredOverlay } from '../../lib/anchored-overlay'
9
11
  import Card from '../card/Card.vue'
10
12
  import { cn } from '../../lib/cn'
11
13
 
12
- defineOptions({ name: 'HnDropdownMenu' })
14
+ defineOptions({ name: 'HnDropdownMenu', inheritAttrs: false })
13
15
 
14
16
  const props = withDefaults(
15
17
  defineProps<{
16
18
  label?: string
19
+ anchor?: HTMLElement | null
20
+ modal?: boolean
21
+ dir?: 'ltr' | 'rtl'
17
22
  side?: 'top' | 'right' | 'bottom' | 'left'
18
23
  align?: 'start' | 'center' | 'end'
19
24
  sideOffset?: number
20
25
  class?: string
21
26
  }>(),
22
- { side: 'bottom', align: 'center', sideOffset: 8 },
27
+ { side: 'bottom', align: 'center', sideOffset: 8, modal: true },
23
28
  )
24
29
 
25
30
  const open = defineModel<boolean>('open')
31
+ const emit = defineEmits<DropdownMenuContentEmits>()
32
+ defineSlots<{ default?(): unknown; content?(): unknown }>()
33
+ const { trigger, reference, visible, events } = useAnchoredOverlay(props, open, emit)
26
34
 
27
35
  const pressOrigin = computed(() => {
28
36
  if (props.side === 'left') return 'right'
@@ -34,8 +42,10 @@
34
42
  </script>
35
43
 
36
44
  <template>
37
- <DropdownMenuRoot v-model:open="open" modal>
45
+ <DropdownMenuRoot v-model:open="visible" :modal="props.modal" :dir="props.dir">
38
46
  <DropdownMenuTrigger
47
+ v-if="$slots.default"
48
+ ref="trigger"
39
49
  as-child
40
50
  class="group/hn-disclosure"
41
51
  :style="{ transformOrigin: pressOrigin }"
@@ -44,8 +54,10 @@
44
54
  </DropdownMenuTrigger>
45
55
  <DropdownMenuPortal>
46
56
  <DropdownMenuContent
57
+ v-if="reference"
58
+ v-bind="{ 'aria-label': props.label, ...$attrs, ...events }"
59
+ :reference="reference"
47
60
  as-child
48
- :aria-label="props.label"
49
61
  :side="props.side"
50
62
  :align="props.align"
51
63
  :side-offset="props.sideOffset"
@@ -1,21 +1,21 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue'
3
2
  import { Motion } from 'motion-v'
4
3
  import { cn } from '../../lib/cn'
5
- import { TRANSITION, prefersReducedMotion } from '../../motion'
4
+ import { useHighlightMotion } from './composables/useHighlightMotion'
6
5
 
7
6
  defineOptions({ name: 'HnHighlight' })
8
7
 
9
8
  const props = withDefaults(
10
9
  defineProps<{
11
10
  id?: string
11
+ axis?: 'x' | 'y' | 'both'
12
12
  as?: 'div' | 'li'
13
13
  class?: string
14
14
  }>(),
15
- { id: undefined, as: 'div' },
15
+ { id: undefined, axis: 'both', as: 'div' },
16
16
  )
17
17
 
18
- const transition = computed(() => (prefersReducedMotion() ? { duration: 0 } : TRANSITION.layout))
18
+ const { transition, transformTemplate } = useHighlightMotion(() => props.axis)
19
19
  </script>
20
20
 
21
21
  <template>
@@ -26,6 +26,7 @@
26
26
  aria-hidden="true"
27
27
  data-hn-highlight
28
28
  :transition="transition"
29
+ :transform-template="transformTemplate"
29
30
  :class="cn('pointer-events-none', props.class)"
30
31
  />
31
32
  </template>
@@ -0,0 +1,22 @@
1
+ import { computed } from 'vue'
2
+ import type { MotionProps } from 'motion-v'
3
+ import { TRANSITION, prefersReducedMotion } from '../../../motion'
4
+
5
+ export function useHighlightMotion(axis: () => 'x' | 'y' | 'both') {
6
+ const transition = computed(() => (prefersReducedMotion() ? { duration: 0 } : TRANSITION.layout))
7
+ const transformTemplate = computed<MotionProps['transformTemplate']>(() => {
8
+ const direction = axis()
9
+ if (direction === 'both') return undefined
10
+
11
+ return (_, generated) => {
12
+ if (!generated || generated === 'none' || typeof DOMMatrix === 'undefined') return generated
13
+
14
+ const matrix = new DOMMatrix(generated)
15
+ if (direction === 'x') matrix.m42 = 0
16
+ else matrix.m41 = 0
17
+ return matrix.toString()
18
+ }
19
+ })
20
+
21
+ return { transition, transformTemplate }
22
+ }
@@ -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: [