@nuxt/ui 3.0.0-alpha.11 → 3.0.0-alpha.13

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 (174) hide show
  1. package/.nuxt/ui/accordion.ts +2 -2
  2. package/.nuxt/ui/alert.ts +47 -37
  3. package/.nuxt/ui/avatar-group.ts +1 -1
  4. package/.nuxt/ui/avatar.ts +4 -4
  5. package/.nuxt/ui/badge.ts +37 -28
  6. package/.nuxt/ui/breadcrumb.ts +5 -5
  7. package/.nuxt/ui/button.ts +42 -43
  8. package/.nuxt/ui/calendar.ts +15 -15
  9. package/.nuxt/ui/card.ts +27 -1
  10. package/.nuxt/ui/carousel.ts +2 -2
  11. package/.nuxt/ui/checkbox.ts +18 -18
  12. package/.nuxt/ui/chip.ts +8 -8
  13. package/.nuxt/ui/color-picker.ts +2 -2
  14. package/.nuxt/ui/command-palette.ts +12 -12
  15. package/.nuxt/ui/container.ts +1 -1
  16. package/.nuxt/ui/context-menu.ts +32 -32
  17. package/.nuxt/ui/drawer.ts +72 -11
  18. package/.nuxt/ui/dropdown-menu.ts +33 -33
  19. package/.nuxt/ui/form-field.ts +6 -6
  20. package/.nuxt/ui/input-menu.ts +41 -41
  21. package/.nuxt/ui/input-number.ts +20 -20
  22. package/.nuxt/ui/input.ts +23 -23
  23. package/.nuxt/ui/kbd.ts +4 -4
  24. package/.nuxt/ui/link.ts +3 -3
  25. package/.nuxt/ui/modal.ts +5 -5
  26. package/.nuxt/ui/navigation-menu.ts +144 -77
  27. package/.nuxt/ui/pin-input.ts +20 -20
  28. package/.nuxt/ui/popover.ts +2 -2
  29. package/.nuxt/ui/progress.ts +17 -17
  30. package/.nuxt/ui/radio-group.ts +20 -20
  31. package/.nuxt/ui/select-menu.ts +33 -32
  32. package/.nuxt/ui/select.ts +31 -31
  33. package/.nuxt/ui/separator.ts +8 -8
  34. package/.nuxt/ui/skeleton.ts +1 -1
  35. package/.nuxt/ui/slideover.ts +4 -4
  36. package/.nuxt/ui/slider.ts +16 -16
  37. package/.nuxt/ui/stepper.ts +18 -18
  38. package/.nuxt/ui/switch.ts +20 -20
  39. package/.nuxt/ui/table.ts +16 -16
  40. package/.nuxt/ui/tabs.ts +33 -33
  41. package/.nuxt/ui/textarea.ts +23 -23
  42. package/.nuxt/ui/toast.ts +42 -32
  43. package/.nuxt/ui/toaster.ts +4 -4
  44. package/.nuxt/ui/tooltip.ts +2 -2
  45. package/.nuxt/ui.css +90 -0
  46. package/README.md +2 -2
  47. package/cli/package.json +2 -2
  48. package/cli/templates.mjs +7 -7
  49. package/dist/chunks/component-meta.cjs +2590 -2511
  50. package/dist/chunks/component-meta.mjs +2590 -2511
  51. package/dist/client/devtools/200.html +1 -1
  52. package/dist/client/devtools/404.html +1 -1
  53. package/dist/client/devtools/_nuxt/CG6Dc4jp.js +1 -0
  54. package/dist/client/devtools/_nuxt/{Dtz-o9GB.js → DrNJgD08.js} +1 -1
  55. package/dist/client/devtools/_nuxt/{DNZFyo26.js → ElqrUh1r.js} +1 -1
  56. package/dist/client/devtools/_nuxt/builds/latest.json +1 -1
  57. package/dist/client/devtools/_nuxt/builds/meta/255c8d0b-7281-4d74-b4b6-ec96e6721aa0.json +1 -0
  58. package/dist/client/devtools/_nuxt/entry.Hn8drx4g.css +1 -0
  59. package/dist/client/devtools/_nuxt/error-404.Dew7w9Ee.css +1 -0
  60. package/dist/client/devtools/_nuxt/error-500.Dep59i31.css +1 -0
  61. package/dist/client/devtools/_nuxt/mC2e7kOs.js +57 -0
  62. package/dist/client/devtools/index.html +1 -1
  63. package/dist/devtools/runtime/examples/CardExample.vue +3 -3
  64. package/dist/devtools/runtime/examples/CollapsibleExample.vue +1 -1
  65. package/dist/devtools/runtime/examples/ContainerExample.vue +1 -1
  66. package/dist/devtools/runtime/examples/ContextMenuExample.vue +1 -1
  67. package/dist/devtools/runtime/examples/PopoverExample.vue +1 -1
  68. package/dist/devtools/runtime/examples/TooltipExample.vue +1 -1
  69. package/dist/module.cjs +2 -1
  70. package/dist/module.json +1 -1
  71. package/dist/module.mjs +2 -1
  72. package/dist/runtime/components/Accordion.vue +2 -2
  73. package/dist/runtime/components/Alert.vue +15 -12
  74. package/dist/runtime/components/App.vue +3 -3
  75. package/dist/runtime/components/Avatar.vue +31 -10
  76. package/dist/runtime/components/AvatarGroup.vue +2 -2
  77. package/dist/runtime/components/Badge.vue +7 -3
  78. package/dist/runtime/components/Breadcrumb.vue +2 -2
  79. package/dist/runtime/components/Button.vue +2 -2
  80. package/dist/runtime/components/ButtonGroup.vue +2 -2
  81. package/dist/runtime/components/Calendar.vue +6 -3
  82. package/dist/runtime/components/Card.vue +8 -4
  83. package/dist/runtime/components/Carousel.vue +2 -2
  84. package/dist/runtime/components/Checkbox.vue +4 -4
  85. package/dist/runtime/components/Chip.vue +2 -2
  86. package/dist/runtime/components/Collapsible.vue +2 -2
  87. package/dist/runtime/components/ColorPicker.vue +46 -15
  88. package/dist/runtime/components/CommandPalette.vue +48 -41
  89. package/dist/runtime/components/Container.vue +2 -2
  90. package/dist/runtime/components/ContextMenu.vue +10 -2
  91. package/dist/runtime/components/ContextMenuContent.vue +4 -2
  92. package/dist/runtime/components/Drawer.vue +9 -3
  93. package/dist/runtime/components/DropdownMenu.vue +10 -2
  94. package/dist/runtime/components/DropdownMenuContent.vue +4 -2
  95. package/dist/runtime/components/Form.vue +46 -17
  96. package/dist/runtime/components/FormField.vue +12 -6
  97. package/dist/runtime/components/Input.vue +5 -4
  98. package/dist/runtime/components/InputMenu.vue +6 -5
  99. package/dist/runtime/components/InputNumber.vue +10 -6
  100. package/dist/runtime/components/Kbd.vue +2 -2
  101. package/dist/runtime/components/Link.vue +2 -2
  102. package/dist/runtime/components/Modal.vue +17 -3
  103. package/dist/runtime/components/NavigationMenu.vue +110 -67
  104. package/dist/runtime/components/Pagination.vue +2 -2
  105. package/dist/runtime/components/PinInput.vue +5 -4
  106. package/dist/runtime/components/Popover.vue +2 -2
  107. package/dist/runtime/components/Progress.vue +2 -2
  108. package/dist/runtime/components/RadioGroup.vue +4 -4
  109. package/dist/runtime/components/Select.vue +9 -7
  110. package/dist/runtime/components/SelectMenu.vue +67 -64
  111. package/dist/runtime/components/Separator.vue +2 -2
  112. package/dist/runtime/components/Skeleton.vue +2 -2
  113. package/dist/runtime/components/Slideover.vue +17 -3
  114. package/dist/runtime/components/Slider.vue +4 -4
  115. package/dist/runtime/components/Stepper.vue +3 -3
  116. package/dist/runtime/components/Switch.vue +4 -4
  117. package/dist/runtime/components/Table.vue +96 -18
  118. package/dist/runtime/components/Tabs.vue +2 -2
  119. package/dist/runtime/components/Textarea.vue +7 -5
  120. package/dist/runtime/components/Toast.vue +28 -16
  121. package/dist/runtime/components/Toaster.vue +4 -4
  122. package/dist/runtime/components/Tooltip.vue +2 -2
  123. package/dist/runtime/composables/useFormField.d.ts +6 -2
  124. package/dist/runtime/composables/useFormField.js +18 -11
  125. package/dist/runtime/composables/useToast.d.ts +4 -3
  126. package/dist/runtime/composables/useToast.js +20 -6
  127. package/dist/runtime/index.css +1 -1
  128. package/dist/runtime/locale/az.js +54 -0
  129. package/dist/runtime/locale/bn.js +54 -0
  130. package/dist/runtime/locale/es.js +2 -2
  131. package/dist/runtime/locale/he.d.ts +2 -0
  132. package/dist/runtime/locale/he.js +53 -0
  133. package/dist/runtime/locale/hi.d.ts +2 -0
  134. package/dist/runtime/locale/hi.js +54 -0
  135. package/dist/runtime/locale/hu.d.ts +2 -0
  136. package/dist/runtime/locale/hu.js +54 -0
  137. package/dist/runtime/locale/index.d.ts +8 -2
  138. package/dist/runtime/locale/index.js +8 -2
  139. package/dist/runtime/locale/km.d.ts +2 -0
  140. package/dist/runtime/locale/km.js +54 -0
  141. package/dist/runtime/locale/nb_no.d.ts +2 -0
  142. package/dist/runtime/locale/nb_no.js +54 -0
  143. package/dist/runtime/locale/zh_cn.d.ts +2 -0
  144. package/dist/runtime/locale/{zh_hans.js → zh_cn.js} +1 -1
  145. package/dist/runtime/locale/zh_tw.d.ts +2 -0
  146. package/dist/runtime/locale/{zh_hant.js → zh_tw.js} +1 -1
  147. package/dist/runtime/plugins/colors.js +8 -5
  148. package/dist/runtime/types/form.d.ts +21 -12
  149. package/dist/runtime/types/index.d.ts +1 -0
  150. package/dist/runtime/types/index.js +1 -0
  151. package/dist/runtime/types/utils.d.ts +5 -0
  152. package/dist/runtime/utils/form.js +3 -5
  153. package/dist/runtime/utils/tv.js +2 -2
  154. package/dist/runtime/vue/components/Link.vue +77 -18
  155. package/dist/shared/{ui.DCjJwb6l.cjs → ui.C5FzRWM5.cjs} +529 -366
  156. package/dist/shared/{ui.BKsd22-Q.mjs → ui.DCZM2pl8.mjs} +525 -366
  157. package/dist/unplugin.cjs +44 -20
  158. package/dist/unplugin.d.cts +10 -0
  159. package/dist/unplugin.d.mts +10 -0
  160. package/dist/unplugin.d.ts +10 -0
  161. package/dist/unplugin.mjs +43 -19
  162. package/dist/vite.cjs +3 -2
  163. package/dist/vite.d.cts +2 -0
  164. package/dist/vite.d.mts +2 -0
  165. package/dist/vite.d.ts +2 -0
  166. package/dist/vite.mjs +3 -2
  167. package/package.json +67 -41
  168. package/dist/client/devtools/_nuxt/CQ-XZFu7.js +0 -54
  169. package/dist/client/devtools/_nuxt/builds/meta/1f5fbd7b-9d97-480e-9ae4-e02b4ff7018b.json +0 -1
  170. package/dist/client/devtools/_nuxt/entry.qOdop8tq.css +0 -1
  171. package/dist/client/devtools/_nuxt/error-404.D-rS9BDk.css +0 -1
  172. package/dist/client/devtools/_nuxt/error-500.CfcCHs91.css +0 -1
  173. /package/dist/runtime/locale/{zh_hans.d.ts → az.d.ts} +0 -0
  174. /package/dist/runtime/locale/{zh_hant.d.ts → bn.d.ts} +0 -0
@@ -5,10 +5,11 @@ import type { AppConfig } from '@nuxt/schema'
5
5
  import _appConfig from '#build/app.config'
6
6
  import theme from '#build/ui/color-picker'
7
7
  import { tv } from '../utils/tv'
8
+ import type { HSLObject } from 'colortranslator'
8
9
 
9
- const appConfig = _appConfig as AppConfig & { ui: { colorPicker: Partial<typeof theme> } }
10
+ const appConfigColorPicker = _appConfig as AppConfig & { ui: { colorPicker: Partial<typeof theme> } }
10
11
 
11
- const colorPicker = tv({ extend: tv(theme), ...(appConfig.ui?.colorPicker || {}) })
12
+ const colorPicker = tv({ extend: tv(theme), ...(appConfigColorPicker.ui?.colorPicker || {}) })
12
13
 
13
14
  type ColorPickerVariants = VariantProps<typeof colorPicker>
14
15
 
@@ -18,6 +19,27 @@ type HSVColor = {
18
19
  v: number
19
20
  }
20
21
 
22
+ function HSLtoHSV(hsl: HSLObject): HSVColor {
23
+ const x = hsl.S * (hsl.L < 50 ? hsl.L : 100 - hsl.L)
24
+ const v = hsl.L + (x / 100)
25
+
26
+ return {
27
+ h: hsl.H,
28
+ s: hsl.L === 0 ? hsl.S : 2 * x / v,
29
+ v
30
+ }
31
+ }
32
+
33
+ function HSVtoHSL(hsv: HSVColor): HSLObject {
34
+ const x = (200 - hsv.s) * hsv.v / 100
35
+
36
+ return {
37
+ H: hsv.h,
38
+ S: x === 0 || x === 200 ? 0 : Math.round(hsv.s * hsv.v / (x <= 100 ? x : 200 - x)),
39
+ L: Math.round(x / 2)
40
+ }
41
+ }
42
+
21
43
  export type ColorPickerProps = {
22
44
  /**
23
45
  * The element or component this component should render as.
@@ -40,7 +62,7 @@ export type ColorPickerProps = {
40
62
  * Format of the color
41
63
  * @defaultValue 'hex'
42
64
  */
43
- format?: 'hex' | 'rgb' | 'hsl' | 'hwb'
65
+ format?: 'hex' | 'rgb' | 'hsl' | 'cmyk' | 'lab'
44
66
  size?: ColorPickerVariants['size']
45
67
  class?: any
46
68
  ui?: Partial<typeof colorPicker.slots>
@@ -52,7 +74,7 @@ import { ref, nextTick, computed, toValue } from 'vue'
52
74
  import { Primitive } from 'reka-ui'
53
75
  import { useEventListener, useElementBounding, watchThrottled, watchPausable } from '@vueuse/core'
54
76
  import { isClient } from '@vueuse/shared'
55
- import Color from 'color'
77
+ import { ColorTranslator } from 'colortranslator'
56
78
 
57
79
  const props = withDefaults(defineProps<ColorPickerProps>(), {
58
80
  format: 'hex',
@@ -64,28 +86,37 @@ const modelValue = defineModel<string>(undefined)
64
86
  const pickedColor = computed<HSVColor>({
65
87
  get() {
66
88
  try {
67
- const color = Color(modelValue.value || props.defaultValue)
68
- return color.hsv().object() as HSVColor
89
+ const color = new ColorTranslator(modelValue.value || props.defaultValue)
90
+
91
+ return HSLtoHSV(color.HSLObject)
69
92
  } catch (_) {
70
93
  return { h: 0, s: 0, v: 100 }
71
94
  }
72
95
  },
73
96
  set(value) {
74
- const color = Color.hsv(value.h, value.s, value.v)
97
+ const color = new ColorTranslator(HSVtoHSL(value), {
98
+ decimals: 2,
99
+ labUnit: 'percent',
100
+ cmykUnit: 'percent',
101
+ cmykFunction: 'cmyk'
102
+ })
75
103
 
76
104
  switch (props.format) {
77
105
  case 'rgb':
78
- modelValue.value = color.rgb().string()
106
+ modelValue.value = color.RGB
79
107
  break
80
108
  case 'hsl':
81
- modelValue.value = color.hsl().string()
109
+ modelValue.value = color.HSL
110
+ break
111
+ case 'cmyk':
112
+ modelValue.value = color.CMYK
82
113
  break
83
- case 'hwb':
84
- modelValue.value = color.hwb().string()
114
+ case 'lab':
115
+ modelValue.value = color.CIELab
85
116
  break
86
117
  case 'hex':
87
118
  default:
88
- modelValue.value = color.hex()
119
+ modelValue.value = color.HEX
89
120
  }
90
121
  }
91
122
  })
@@ -202,18 +233,18 @@ watchThrottled([selectorThumbPosition, trackThumbPosition], () => {
202
233
  nextTick(resumeWatchColor)
203
234
  }, { throttle: () => props.throttle })
204
235
 
205
- const trackThumbColor = computed(() => Color({
236
+ const trackThumbColor = computed(() => new ColorTranslator(HSVtoHSL({
206
237
  h: normalizeHue(trackThumbPosition.value.y),
207
238
  s: 100,
208
239
  v: 100
209
- }).hex())
240
+ })).HEX)
210
241
 
211
242
  const selectorStyle = computed(() => ({
212
243
  backgroundColor: trackThumbColor.value
213
244
  }))
214
245
 
215
246
  const selectorThumbStyle = computed(() => ({
216
- backgroundColor: Color(modelValue.value || props.defaultValue).hex(),
247
+ backgroundColor: new ColorTranslator(modelValue.value || props.defaultValue).HEX,
217
248
  left: `${selectorThumbPosition.value.x}%`,
218
249
  top: `${selectorThumbPosition.value.y}%`
219
250
  }))
@@ -8,14 +8,14 @@ import theme from '#build/ui/command-palette'
8
8
  import type { UseComponentIconsProps } from '../composables/useComponentIcons'
9
9
  import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
10
10
  import { tv } from '../utils/tv'
11
- import type { AvatarProps, ButtonProps, ChipProps, KbdProps, InputProps } from '../types'
11
+ import type { AvatarProps, ButtonProps, ChipProps, KbdProps, InputProps, LinkProps } from '../types'
12
12
  import type { DynamicSlots, PartialString } from '../types/utils'
13
13
 
14
- const appConfig = _appConfig as AppConfig & { ui: { commandPalette: Partial<typeof theme> } }
14
+ const appConfigCommandPalette = _appConfig as AppConfig & { ui: { commandPalette: Partial<typeof theme> } }
15
15
 
16
- const commandPalette = tv({ extend: tv(theme), ...(appConfig.ui?.commandPalette || {}) })
16
+ const commandPalette = tv({ extend: tv(theme), ...(appConfigCommandPalette.ui?.commandPalette || {}) })
17
17
 
18
- export interface CommandPaletteItem {
18
+ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
19
19
  prefix?: string
20
20
  label?: string
21
21
  suffix?: string
@@ -136,12 +136,15 @@ import { useAppConfig } from '#imports'
136
136
  import { useLocale } from '../composables/useLocale'
137
137
  import { omit, get } from '../utils'
138
138
  import { highlight } from '../utils/fuse'
139
+ import { pickLinkProps } from '../utils/link'
139
140
  import UIcon from './Icon.vue'
140
141
  import UAvatar from './Avatar.vue'
141
142
  import UButton from './Button.vue'
142
143
  import UChip from './Chip.vue'
143
- import UKbd from './Kbd.vue'
144
+ import ULinkBase from './LinkBase.vue'
145
+ import ULink from './Link.vue'
144
146
  import UInput from './Input.vue'
147
+ import UKbd from './Kbd.vue'
145
148
 
146
149
  const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
147
150
  modelValue: '',
@@ -281,47 +284,51 @@ const groups = computed(() => {
281
284
  :key="`group-${groupIndex}-${index}`"
282
285
  :value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml'])"
283
286
  :disabled="item.disabled"
284
- :class="ui.item({ class: props.ui?.item, active: item.active })"
287
+ as-child
285
288
  @select="item.onSelect"
286
289
  >
287
- <slot :name="item.slot || group.slot || 'item'" :item="item" :index="index">
288
- <slot :name="item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`" :item="item" :index="index">
289
- <UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, loading: true })" />
290
- <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, active: item.active })" />
291
- <UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar, active: item.active })" />
292
- <UChip
293
- v-else-if="item.chip"
294
- :size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
295
- inset
296
- standalone
297
- v-bind="item.chip"
298
- :class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip, active: item.active })"
299
- />
300
- </slot>
301
-
302
- <span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel({ class: props.ui?.itemLabel, active: item.active })">
303
- <slot :name="item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`" :item="item" :index="index">
304
- <span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
305
-
306
- <span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase, active: item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
307
-
308
- <span :class="ui.itemLabelSuffix({ class: props.ui?.itemLabelSuffix, active: item.active })" v-html="item.suffixHtml || item.suffix" />
309
- </slot>
310
- </span>
290
+ <ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
291
+ <ULinkBase v-bind="slotProps" :class="ui.item({ class: props.ui?.item, active: active || item.active })">
292
+ <slot :name="item.slot || group.slot || 'item'" :item="item" :index="index">
293
+ <slot :name="item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`" :item="item" :index="index">
294
+ <UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, loading: true })" />
295
+ <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, active: active || item.active })" />
296
+ <UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar, active: active || item.active })" />
297
+ <UChip
298
+ v-else-if="item.chip"
299
+ :size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
300
+ inset
301
+ standalone
302
+ v-bind="item.chip"
303
+ :class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip, active: active || item.active })"
304
+ />
305
+ </slot>
306
+
307
+ <span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel({ class: props.ui?.itemLabel, active: active || item.active })">
308
+ <slot :name="item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`" :item="item" :index="index">
309
+ <span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
310
+
311
+ <span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase, active: active || item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
312
+
313
+ <span :class="ui.itemLabelSuffix({ class: props.ui?.itemLabelSuffix, active: active || item.active })" v-html="item.suffixHtml || item.suffix" />
314
+ </slot>
315
+ </span>
311
316
 
312
- <span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
313
- <slot :name="item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`" :item="item" :index="index">
314
- <span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: props.ui?.itemTrailingKbds })">
315
- <UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
317
+ <span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
318
+ <slot :name="item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`" :item="item" :index="index">
319
+ <span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: props.ui?.itemTrailingKbds })">
320
+ <UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
321
+ </span>
322
+ <UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: props.ui?.itemTrailingHighlightedIcon })" />
323
+ </slot>
324
+
325
+ <ListboxItemIndicator as-child>
326
+ <UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
327
+ </ListboxItemIndicator>
316
328
  </span>
317
- <UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: props.ui?.itemTrailingHighlightedIcon })" />
318
329
  </slot>
319
-
320
- <ListboxItemIndicator as-child>
321
- <UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
322
- </ListboxItemIndicator>
323
- </span>
324
- </slot>
330
+ </ULinkBase>
331
+ </ULink>
325
332
  </ListboxItem>
326
333
  </ListboxGroup>
327
334
  </div>
@@ -5,9 +5,9 @@ import theme from '#build/ui/container'
5
5
  import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
6
6
  import { tv } from '../utils/tv'
7
7
 
8
- const appConfig = _appConfig as AppConfig & { ui: { container: Partial<typeof theme> } }
8
+ const appConfigContainer = _appConfig as AppConfig & { ui: { container: Partial<typeof theme> } }
9
9
 
10
- const container = tv({ extend: tv(theme), ...(appConfig.ui?.container || {}) })
10
+ const container = tv({ extend: tv(theme), ...(appConfigContainer.ui?.container || {}) })
11
11
 
12
12
  export interface ContainerProps {
13
13
  /**
@@ -9,9 +9,9 @@ import { tv } from '../utils/tv'
9
9
  import type { AvatarProps, KbdProps, LinkProps } from '../types'
10
10
  import type { DynamicSlots, PartialString } from '../types/utils'
11
11
 
12
- const appConfig = _appConfig as AppConfig & { ui: { contextMenu: Partial<typeof theme> } }
12
+ const appConfigContextMenu = _appConfig as AppConfig & { ui: { contextMenu: Partial<typeof theme> } }
13
13
 
14
- const contextMenu = tv({ extend: tv(theme), ...(appConfig.ui?.contextMenu || {}) })
14
+ const contextMenu = tv({ extend: tv(theme), ...(appConfigContextMenu.ui?.contextMenu || {}) })
15
15
 
16
16
  type ContextMenuVariants = VariantProps<typeof contextMenu>
17
17
 
@@ -51,6 +51,12 @@ export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'> {
51
51
  * @defaultValue appConfig.ui.icons.loading
52
52
  */
53
53
  loadingIcon?: string
54
+ /**
55
+ * The icon displayed when the item is an external link.
56
+ * Set to `false` to hide the external icon.
57
+ * @defaultValue appConfig.ui.icons.external
58
+ */
59
+ externalIcon?: boolean | string
54
60
  /** The content of the menu. */
55
61
  content?: Omit<ContextMenuContentProps, 'as' | 'asChild' | 'forceMount'>
56
62
  /**
@@ -152,6 +158,7 @@ import UContextMenuContent from './ContextMenuContent.vue'
152
158
  const props = withDefaults(defineProps<ContextMenuProps<T>>(), {
153
159
  portal: true,
154
160
  modal: true,
161
+ externalIcon: true,
155
162
  labelKey: 'label'
156
163
  })
157
164
  const emits = defineEmits<ContextMenuEmits>()
@@ -182,6 +189,7 @@ const ui = computed(() => contextMenu({
182
189
  :label-key="labelKey"
183
190
  :checked-icon="checkedIcon"
184
191
  :loading-icon="loadingIcon"
192
+ :external-icon="externalIcon"
185
193
  >
186
194
  <template v-for="(_, name) in proxySlots" #[name]="slotData: any">
187
195
  <slot :name="name" v-bind="slotData" />
@@ -13,6 +13,7 @@ interface ContextMenuContentProps<T> extends Omit<RekaContextMenuContentProps, '
13
13
  labelKey: string
14
14
  checkedIcon?: string
15
15
  loadingIcon?: string
16
+ externalIcon?: boolean | string
16
17
  class?: any
17
18
  ui: typeof _contextMenu
18
19
  uiOverride?: any
@@ -42,7 +43,7 @@ const emits = defineEmits<ContextMenuContentEmits>()
42
43
  const slots = defineSlots<ContextMenuSlots<T>>()
43
44
 
44
45
  const appConfig = useAppConfig()
45
- const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'class', 'ui'), emits)
46
+ const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
46
47
  const proxySlots = omit(slots, ['default']) as Record<string, ContextMenuSlots<T>[string]>
47
48
 
48
49
  const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ContextMenuItem, active?: boolean, index: number }>()
@@ -64,7 +65,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
64
65
  {{ get(item, props.labelKey as string) }}
65
66
  </slot>
66
67
 
67
- <UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, color: item?.color, active })" />
68
+ <UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, color: item?.color, active })" />
68
69
  </span>
69
70
 
70
71
  <span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
@@ -112,6 +113,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
112
113
  :label-key="labelKey"
113
114
  :checked-icon="checkedIcon"
114
115
  :loading-icon="loadingIcon"
116
+ :external-icon="externalIcon"
115
117
  v-bind="item.content"
116
118
  >
117
119
  <template v-for="(_, name) in proxySlots" #[name]="slotData: any">
@@ -7,9 +7,9 @@ import theme from '#build/ui/drawer'
7
7
  import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
8
8
  import { tv } from '../utils/tv'
9
9
 
10
- const appConfig = _appConfig as AppConfig & { ui: { drawer: Partial<typeof theme> } }
10
+ const appConfigDrawer = _appConfig as AppConfig & { ui: { drawer: Partial<typeof theme> } }
11
11
 
12
- const drawer = tv({ extend: tv(theme), ...(appConfig.ui?.drawer || {}) })
12
+ const drawer = tv({ extend: tv(theme), ...(appConfigDrawer.ui?.drawer || {}) })
13
13
 
14
14
  export interface DrawerProps extends Pick<DrawerRootProps, 'activeSnapPoint' | 'closeThreshold' | 'defaultOpen' | 'direction' | 'fadeFromIndex' | 'fixed' | 'modal' | 'nested' | 'direction' | 'open' | 'scrollLockTimeout' | 'shouldScaleBackground' | 'snapPoints'> {
15
15
  /**
@@ -19,6 +19,11 @@ export interface DrawerProps extends Pick<DrawerRootProps, 'activeSnapPoint' | '
19
19
  as?: any
20
20
  title?: string
21
21
  description?: string
22
+ /**
23
+ * Whether to inset the drawer from the edges.
24
+ * @defaultValue false
25
+ */
26
+ inset?: boolean
22
27
  /** The content of the drawer. */
23
28
  content?: Omit<DialogContentProps, 'as' | 'asChild' | 'forceMount'>
24
29
  /**
@@ -80,7 +85,8 @@ const rootProps = useForwardPropsEmits(reactivePick(props, 'activeSnapPoint', 'c
80
85
  const contentProps = toRef(() => props.content)
81
86
 
82
87
  const ui = computed(() => drawer({
83
- direction: props.direction
88
+ direction: props.direction,
89
+ inset: props.inset
84
90
  }))
85
91
  </script>
86
92
 
@@ -9,9 +9,9 @@ import { tv } from '../utils/tv'
9
9
  import type { AvatarProps, KbdProps, LinkProps } from '../types'
10
10
  import type { DynamicSlots, PartialString } from '../types/utils'
11
11
 
12
- const appConfig = _appConfig as AppConfig & { ui: { dropdownMenu: Partial<typeof theme> } }
12
+ const appConfigDropdownMenu = _appConfig as AppConfig & { ui: { dropdownMenu: Partial<typeof theme> } }
13
13
 
14
- const dropdownMenu = tv({ extend: tv(theme), ...(appConfig.ui?.dropdownMenu || {}) })
14
+ const dropdownMenu = tv({ extend: tv(theme), ...(appConfigDropdownMenu.ui?.dropdownMenu || {}) })
15
15
 
16
16
  type DropdownMenuVariants = VariantProps<typeof dropdownMenu>
17
17
 
@@ -51,6 +51,12 @@ export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'>
51
51
  * @defaultValue appConfig.ui.icons.loading
52
52
  */
53
53
  loadingIcon?: string
54
+ /**
55
+ * The icon displayed when the item is an external link.
56
+ * Set to `false` to hide the external icon.
57
+ * @defaultValue appConfig.ui.icons.external
58
+ */
59
+ externalIcon?: boolean | string
54
60
  /**
55
61
  * The content of the menu.
56
62
  * @defaultValue { side: 'bottom', sideOffset: 8, collisionPadding: 8 }
@@ -149,6 +155,7 @@ import UDropdownMenuContent from './DropdownMenuContent.vue'
149
155
  const props = withDefaults(defineProps<DropdownMenuProps<T>>(), {
150
156
  portal: true,
151
157
  modal: true,
158
+ externalIcon: true,
152
159
  labelKey: 'label'
153
160
  })
154
161
  const emits = defineEmits<DropdownMenuEmits>()
@@ -180,6 +187,7 @@ const ui = computed(() => dropdownMenu({
180
187
  :label-key="labelKey"
181
188
  :checked-icon="checkedIcon"
182
189
  :loading-icon="loadingIcon"
190
+ :external-icon="externalIcon"
183
191
  >
184
192
  <template v-for="(_, name) in proxySlots" #[name]="slotData: any">
185
193
  <slot :name="name" v-bind="slotData" />
@@ -14,6 +14,7 @@ interface DropdownMenuContentProps<T> extends Omit<RekaDropdownMenuContentProps,
14
14
  labelKey: string
15
15
  checkedIcon?: string
16
16
  loadingIcon?: string
17
+ externalIcon?: boolean | string
17
18
  class?: any
18
19
  ui: typeof _dropdownMenu
19
20
  uiOverride?: any
@@ -48,7 +49,7 @@ const emits = defineEmits<DropdownMenuContentEmits>()
48
49
  const slots = defineSlots<DropdownMenuContentSlots<T>>()
49
50
 
50
51
  const appConfig = useAppConfig()
51
- const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'class', 'ui'), emits)
52
+ const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
52
53
  const proxySlots = omit(slots, ['default']) as Record<string, DropdownMenuContentSlots<T>[string]>
53
54
 
54
55
  const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: DropdownMenuItem, active?: boolean, index: number }>()
@@ -70,7 +71,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
70
71
  {{ get(item, props.labelKey as string) }}
71
72
  </slot>
72
73
 
73
- <UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, color: item?.color, active })" />
74
+ <UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, color: item?.color, active })" />
74
75
  </span>
75
76
 
76
77
  <span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
@@ -121,6 +122,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
121
122
  :label-key="labelKey"
122
123
  :checked-icon="checkedIcon"
123
124
  :loading-icon="loadingIcon"
125
+ :external-icon="externalIcon"
124
126
  v-bind="item.content"
125
127
  >
126
128
  <template v-for="(_, name) in proxySlots" #[name]="slotData: any">
@@ -5,10 +5,11 @@ import theme from '#build/ui/form'
5
5
  import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
6
6
  import { tv } from '../utils/tv'
7
7
  import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId } from '../types/form'
8
+ import type { DeepReadonly } from 'vue'
8
9
 
9
- const appConfig = _appConfig as AppConfig & { ui: { form: Partial<typeof theme> } }
10
+ const appConfigForm = _appConfig as AppConfig & { ui: { form: Partial<typeof theme> } }
10
11
 
11
- const form = tv({ extend: tv(theme), ...(appConfig.ui?.form || {}) })
12
+ const form = tv({ extend: tv(theme), ...(appConfigForm.ui?.form || {}) })
12
13
 
13
14
  export interface FormProps<T extends object> {
14
15
  id?: string | number
@@ -52,7 +53,7 @@ defineSlots<FormSlots>()
52
53
 
53
54
  const formId = props.id ?? useId() as string
54
55
 
55
- const bus = useEventBus<FormEvent>(`form-${formId}`)
56
+ const bus = useEventBus<FormEvent<T>>(`form-${formId}`)
56
57
  const parentBus = inject(
57
58
  formBusInjectionKey,
58
59
  undefined
@@ -60,7 +61,7 @@ const parentBus = inject(
60
61
 
61
62
  provide(formBusInjectionKey, bus)
62
63
 
63
- const nestedForms = ref<Map<string | number, { validate: () => any }>>(new Map())
64
+ const nestedForms = ref<Map<string | number, { validate: typeof _validate }>>(new Map())
64
65
 
65
66
  onMounted(async () => {
66
67
  bus.on(async (event) => {
@@ -68,8 +69,24 @@ onMounted(async () => {
68
69
  nestedForms.value.set(event.formId, { validate: event.validate })
69
70
  } else if (event.type === 'detach') {
70
71
  nestedForms.value.delete(event.formId)
71
- } else if (props.validateOn?.includes(event.type as FormInputEvents)) {
72
- await _validate({ name: event.name, silent: true, nested: false })
72
+ } else if (props.validateOn?.includes(event.type)) {
73
+ if (event.type !== 'input') {
74
+ await _validate({ name: event.name, silent: true, nested: false })
75
+ } else if (event.eager || blurredFields.has(event.name)) {
76
+ await _validate({ name: event.name, silent: true, nested: false })
77
+ }
78
+ }
79
+
80
+ if (event.type === 'blur') {
81
+ blurredFields.add(event.name)
82
+ }
83
+
84
+ if (event.type === 'change' || event.type === 'input' || event.type === 'blur' || event.type === 'focus') {
85
+ touchedFields.add(event.name)
86
+ }
87
+
88
+ if (event.type === 'change' || event.type === 'input') {
89
+ dirtyFields.add(event.name)
73
90
  }
74
91
  })
75
92
  })
@@ -94,8 +111,12 @@ onUnmounted(() => {
94
111
  const errors = ref<FormErrorWithId[]>([])
95
112
  provide('form-errors', errors)
96
113
 
97
- const inputs = ref<Record<string, { id?: string, pattern?: RegExp }>>({})
98
- provide(formInputsInjectionKey, inputs)
114
+ const inputs = ref<{ [P in keyof T]?: { id?: string, pattern?: RegExp } }>({})
115
+ provide(formInputsInjectionKey, inputs as any)
116
+
117
+ const dirtyFields = new Set<keyof T>()
118
+ const touchedFields = new Set<keyof T>()
119
+ const blurredFields = new Set<keyof T>()
99
120
 
100
121
  function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
101
122
  return errs.map(err => ({
@@ -121,12 +142,12 @@ async function getErrors(): Promise<FormErrorWithId[]> {
121
142
  return resolveErrorIds(errs)
122
143
  }
123
144
 
124
- async function _validate(opts: { name?: string | string[], silent?: boolean, nested?: boolean } = { silent: false, nested: true }): Promise<T | false> {
125
- const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as string[]
145
+ async function _validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean } = { silent: false, nested: true, transform: false }): Promise<T | false> {
146
+ const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof T)[]
126
147
 
127
148
  const nestedValidatePromises = !names && opts.nested
128
149
  ? Array.from(nestedForms.value.values()).map(
129
- ({ validate }) => validate().then(() => undefined).catch((error: Error) => {
150
+ ({ validate }) => validate(opts).then(() => undefined).catch((error: Error) => {
130
151
  if (!(error instanceof FormValidationException)) {
131
152
  throw error
132
153
  }
@@ -151,13 +172,17 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes
151
172
  errors.value = await getErrors()
152
173
  }
153
174
 
154
- const childErrors = (await Promise.all(nestedValidatePromises)).filter(val => val)
175
+ const childErrors = (await Promise.all(nestedValidatePromises)).filter(val => val !== undefined)
155
176
 
156
177
  if (errors.value.length + childErrors.length > 0) {
157
178
  if (opts.silent) return false
158
179
  throw new FormValidationException(formId, errors.value, childErrors)
159
180
  }
160
181
 
182
+ if (opts.transform) {
183
+ Object.assign(props.state, transformedState.value)
184
+ }
185
+
161
186
  return props.state as T
162
187
  }
163
188
 
@@ -170,8 +195,7 @@ async function onSubmitWrapper(payload: Event) {
170
195
  const event = payload as FormSubmitEvent<any>
171
196
 
172
197
  try {
173
- await _validate({ nested: true })
174
- event.data = props.schema ? transformedState.value : props.state
198
+ event.data = await _validate({ nested: true, transform: true })
175
199
  await props.onSubmit?.(event)
176
200
  } catch (error) {
177
201
  if (!(error instanceof FormValidationException)) {
@@ -200,7 +224,7 @@ defineExpose<Form<T>>({
200
224
  validate: _validate,
201
225
  errors,
202
226
 
203
- setErrors(errs: FormError[], name?: string) {
227
+ setErrors(errs: FormError[], name?: keyof T) {
204
228
  if (name) {
205
229
  errors.value = errors.value
206
230
  .filter(error => error.name !== name)
@@ -214,7 +238,7 @@ defineExpose<Form<T>>({
214
238
  await onSubmitWrapper(new Event('submit'))
215
239
  },
216
240
 
217
- getErrors(name?: string) {
241
+ getErrors(name?: keyof T) {
218
242
  if (name) {
219
243
  return errors.value.filter(err => err.name === name)
220
244
  }
@@ -229,7 +253,12 @@ defineExpose<Form<T>>({
229
253
  }
230
254
  },
231
255
 
232
- disabled
256
+ disabled,
257
+ dirty: computed(() => !!dirtyFields.size),
258
+
259
+ dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof T>>,
260
+ blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof T>>,
261
+ touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof T>>
233
262
  })
234
263
  </script>
235
264