@brumaombra/ui-vintage 0.8.2 → 0.9.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 (134) hide show
  1. package/AGENTS.md +41 -0
  2. package/COMPONENTS.md +88 -17
  3. package/README.md +296 -262
  4. package/module.mjs +5 -11
  5. package/package.json +30 -12
  6. package/src/components/blog/BlogContentRenderer.vue +1 -1
  7. package/src/components/blog/BlogHeaderSection.vue +93 -19
  8. package/src/components/blog/BlogInfoSection.vue +39 -36
  9. package/src/components/blog/BlogPostHeader.vue +181 -0
  10. package/src/components/blog/BlogProgressBar.vue +3 -5
  11. package/src/components/blog/BlogSectionTitle.vue +8 -6
  12. package/src/components/blog/BlogTagsSection.vue +16 -7
  13. package/src/components/blog/CategoryCard.vue +18 -23
  14. package/src/components/blog/HeaderCarousel.vue +15 -6
  15. package/src/components/blog/PostCard.vue +13 -20
  16. package/src/components/blog/SocialShareSidebar.vue +24 -14
  17. package/src/components/blog/TableOfContents.vue +61 -28
  18. package/src/components/blog/index.ts +1 -0
  19. package/src/components/card-grid/CardGrid.vue +2 -2
  20. package/src/components/confirm-dialog/ConfirmDialog.vue +5 -0
  21. package/src/components/confirm-dialog/confirm-dialog-state.ts +22 -22
  22. package/src/components/content/BlogList.vue +28 -52
  23. package/src/components/content/BlogTable.vue +26 -30
  24. package/src/components/content/CodeBlock.vue +96 -22
  25. package/src/components/content/Flow.vue +29 -35
  26. package/src/components/content/ProseHr.vue +5 -5
  27. package/src/components/content/ProsePre.vue +9 -4
  28. package/src/components/content/Terminal.vue +42 -20
  29. package/src/components/cookie-consent/CookieConsent.vue +1 -1
  30. package/src/components/dashboard-shell/DashboardShell.vue +24 -7
  31. package/src/components/empty-state-card/EmptyStateCard.vue +2 -2
  32. package/src/components/error-page/ErrorPage.vue +3 -3
  33. package/src/components/info-card/InfoCard.vue +4 -4
  34. package/src/components/landing/landing-footer/LandingFooter.vue +16 -11
  35. package/src/components/landing/landing-navbar/LandingNavbar.vue +22 -6
  36. package/src/components/landing/landing-shell/LandingShell.vue +2 -7
  37. package/src/components/language-flag/index.ts +1 -0
  38. package/src/components/language-selector/LanguageSelector.vue +18 -10
  39. package/src/components/load-more-button/LoadMoreButton.vue +7 -12
  40. package/src/components/loading-state-card/LoadingStateCard.vue +2 -2
  41. package/src/components/message-dialog/MessageDialog.vue +11 -11
  42. package/src/components/message-dialog/message-dialog-state.ts +1 -4
  43. package/src/components/message-toast/MessageToast.vue +24 -9
  44. package/src/components/message-toast/message-toast-state.ts +20 -0
  45. package/src/components/page-header/PageHeader.vue +1 -1
  46. package/src/components/progress-component/ProgressComponent.vue +7 -3
  47. package/src/components/single-value-card/SingleValueCard.vue +37 -39
  48. package/src/components/slider-form-component/SliderFormComponent.vue +1 -1
  49. package/src/components/stat-strip/StatStrip.vue +36 -0
  50. package/src/components/stat-strip/index.ts +2 -0
  51. package/src/components/switch-form-component/SwitchFormComponent.vue +5 -5
  52. package/src/components/text-link/TextLink.vue +1 -1
  53. package/src/components/theme-selector/ThemeSelector.vue +2 -2
  54. package/src/components/theme-selector/theme-transition.ts +15 -3
  55. package/src/components/ui/accordion/Accordion.vue +18 -18
  56. package/src/components/ui/alert/AlertTitle.vue +1 -1
  57. package/src/components/ui/alert/index.ts +1 -1
  58. package/src/components/ui/avatar/AvatarFallback.vue +2 -1
  59. package/src/components/ui/avatar/AvatarGroup.vue +13 -3
  60. package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +2 -2
  61. package/src/components/ui/busy/Busy.vue +5 -7
  62. package/src/components/ui/button/Button.vue +4 -2
  63. package/src/components/ui/button/index.ts +1 -4
  64. package/src/components/ui/calendar/Calendar.vue +12 -7
  65. package/src/components/ui/calendar/CalendarCell.vue +1 -1
  66. package/src/components/ui/calendar/CalendarCellTrigger.vue +1 -1
  67. package/src/components/ui/calendar/CalendarHeadCell.vue +1 -1
  68. package/src/components/ui/calendar/CalendarNextButton.vue +1 -1
  69. package/src/components/ui/calendar/CalendarPrevButton.vue +1 -1
  70. package/src/components/ui/card/Card.vue +5 -15
  71. package/src/components/ui/checkbox/Checkbox.vue +5 -1
  72. package/src/components/ui/combobox/ComboboxInput.vue +5 -1
  73. package/src/components/ui/command/CommandGroup.vue +17 -3
  74. package/src/components/ui/command/CommandInput.vue +1 -1
  75. package/src/components/ui/command/CommandList.vue +5 -1
  76. package/src/components/ui/data-table/DataTable.vue +11 -3
  77. package/src/components/ui/date-picker/DatePicker.vue +7 -1
  78. package/src/components/ui/date-time-picker/DateTimePicker.vue +5 -1
  79. package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  80. package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +1 -1
  81. package/src/components/ui/dropdown-menu/classes.ts +2 -0
  82. package/src/components/ui/field/Field.vue +4 -0
  83. package/src/components/ui/field/FieldLabel.vue +6 -2
  84. package/src/components/ui/field/field-context.ts +62 -0
  85. package/src/components/ui/field/index.ts +1 -0
  86. package/src/components/ui/file-dropzone/FileDropzone.vue +4 -4
  87. package/src/components/ui/input/Input.vue +5 -1
  88. package/src/components/ui/native-select/NativeSelect.vue +6 -2
  89. package/src/components/ui/number-field/NumberField.vue +5 -1
  90. package/src/components/ui/pagination/PaginationItem.vue +1 -1
  91. package/src/components/ui/pin-input/PinInputInput.vue +2 -2
  92. package/src/components/ui/progress/Progress.vue +2 -2
  93. package/src/components/ui/radio-group/RadioGroupCard.vue +1 -1
  94. package/src/components/ui/select/SelectItem.vue +1 -1
  95. package/src/components/ui/select/SelectItemContent.vue +1 -1
  96. package/src/components/ui/select/SelectTrigger.vue +5 -1
  97. package/src/components/ui/sidebar/Sidebar.vue +1 -1
  98. package/src/components/ui/sidebar/SidebarGroupAction.vue +1 -1
  99. package/src/components/ui/sidebar/SidebarGroupLabel.vue +1 -1
  100. package/src/components/ui/sidebar/SidebarInset.vue +1 -1
  101. package/src/components/ui/sidebar/SidebarMenuAction.vue +1 -1
  102. package/src/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
  103. package/src/components/ui/sidebar/SidebarMenuSkeleton.vue +2 -2
  104. package/src/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
  105. package/src/components/ui/sidebar/SidebarProvider.vue +17 -6
  106. package/src/components/ui/sidebar/index.ts +2 -2
  107. package/src/components/ui/sidebar/utils.ts +1 -1
  108. package/src/components/ui/slider/Slider.vue +5 -4
  109. package/src/components/ui/spinner/Spinner.vue +3 -2
  110. package/src/components/ui/stepper/StepperDescription.vue +5 -3
  111. package/src/components/ui/stepper/StepperIndicator.vue +2 -2
  112. package/src/components/ui/stepper/StepperTitle.vue +5 -3
  113. package/src/components/ui/switch/Switch.vue +6 -2
  114. package/src/components/ui/tabs/TabsList.vue +1 -1
  115. package/src/components/ui/tabs/TabsTrigger.vue +1 -1
  116. package/src/components/ui/textarea/Textarea.vue +5 -1
  117. package/src/components/ui/time-picker/TimePicker.vue +2 -2
  118. package/src/components/ui/toggle-group/ToggleGroup.vue +1 -1
  119. package/src/components/ui/tooltip/TooltipContent.vue +1 -1
  120. package/src/i18n/de.json +16 -6
  121. package/src/i18n/en.json +16 -6
  122. package/src/i18n/es.json +16 -6
  123. package/src/i18n/fr.json +16 -6
  124. package/src/i18n/it.json +16 -6
  125. package/src/i18n/ja.json +16 -6
  126. package/src/i18n/pt.json +16 -6
  127. package/src/i18n/ru.json +16 -6
  128. package/src/i18n/zh.json +16 -6
  129. package/src/lib/color-tokens.ts +1 -1
  130. package/src/lib/i18n.ts +32 -4
  131. package/src/lib/utils.ts +21 -1
  132. package/src/runtime/ui-vintage-i18n-plugin.ts +11 -1
  133. package/src/styles.css +12 -16
  134. package/skills/ui-vintage/SKILL.md +0 -42
@@ -26,7 +26,7 @@ const forwardedProps = useForwardProps(delegatedProps);
26
26
  // Disabled
27
27
  'data-disabled:text-muted-foreground data-disabled:opacity-50',
28
28
  // Unavailable
29
- 'data-unavailable:text-destructive-foreground data-unavailable:line-through',
29
+ 'data-unavailable:text-destructive data-unavailable:line-through',
30
30
  // Outside months
31
31
  'data-outside-view:text-muted-foreground',
32
32
  props.class,
@@ -15,7 +15,7 @@ const forwardedProps = useForwardProps(delegatedProps);
15
15
 
16
16
  <template>
17
17
  <CalendarHeadCell data-slot="calendar-head-cell" :class="cn(
18
- 'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem]',
18
+ 'text-muted-foreground rounded flex-1 font-normal text-[0.8rem]',
19
19
  props.class,
20
20
  )
21
21
  " v-bind="forwardedProps">
@@ -22,7 +22,7 @@ const forwardedProps = useForwardProps(delegatedProps);
22
22
  :class="
23
23
  cn(
24
24
  buttonVariants({ variant: 'secondary' }),
25
- 'size-7 min-h-10 bg-transparent p-0 text-(--text-secondary-light) hover:text-(--text-primary-light) dark:text-(--text-secondary-dark) dark:hover:text-(--text-primary-dark)',
25
+ 'size-7 min-h-10 bg-transparent p-0 text-muted-foreground hover:text-foreground',
26
26
  props.class,
27
27
  )
28
28
  "
@@ -22,7 +22,7 @@ const forwardedProps = useForwardProps(delegatedProps);
22
22
  :class="
23
23
  cn(
24
24
  buttonVariants({ variant: 'secondary' }),
25
- 'size-7 min-h-10 bg-transparent p-0 text-(--text-secondary-light) hover:text-(--text-primary-light) dark:text-(--text-secondary-dark) dark:hover:text-(--text-primary-dark)',
25
+ 'size-7 min-h-10 bg-transparent p-0 text-muted-foreground hover:text-foreground',
26
26
  props.class,
27
27
  )
28
28
  "
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref } from 'vue';
2
+ import { computed } from 'vue';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import {
5
5
  getSurfaceToneClasses,
@@ -18,8 +18,6 @@ const props = withDefaults(defineProps<{
18
18
  interactive: false
19
19
  });
20
20
 
21
- const cardRef = ref<HTMLElement | null>(null);
22
-
23
21
  // Get the appropriate classes
24
22
  const colorClasses = computed(() => {
25
23
  // If color is not specified or is "default", use the default card styles
@@ -30,23 +28,15 @@ const colorClasses = computed(() => {
30
28
  // For other colors, use the corresponding surface tone classes
31
29
  return getSurfaceToneClasses(props.color);
32
30
  });
33
-
34
- // Track the pointer so interactive cards can render a spotlight under the cursor
35
- const handlePointerMove = (event: PointerEvent) => {
36
- if (!props.interactive || !cardRef.value) return;
37
- const rect = cardRef.value.getBoundingClientRect();
38
- cardRef.value.style.setProperty('--uv-spot-x', `${event.clientX - rect.left}px`);
39
- cardRef.value.style.setProperty('--uv-spot-y', `${event.clientY - rect.top}px`);
40
- };
41
31
  </script>
42
32
 
43
33
  <template>
44
- <div ref="cardRef" data-slot="card" :data-interactive="props.interactive ? '' : undefined" :class="cn(
45
- 'relative flex flex-col gap-4 sm:gap-6! rounded py-6 transition-[border-color,box-shadow,background-color,translate] duration-300 ease-out-expo',
34
+ <div data-slot="card" :data-interactive="props.interactive ? '' : undefined" :class="cn(
35
+ 'relative flex flex-col gap-4 sm:gap-6 rounded py-6 transition-[border-color,box-shadow,background-color,translate] duration-300 ease-out-expo',
46
36
  colorClasses,
47
- props.interactive && 'cursor-pointer hover:-translate-y-0.5 hover:border-primary/25 hover:shadow-elevated-lg active:translate-y-0 active:duration-100 before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:opacity-0 before:transition-opacity before:duration-300 before:bg-[radial-gradient(360px_circle_at_var(--uv-spot-x,50%)_var(--uv-spot-y,50%),color-mix(in_oklab,var(--primary)_9%,transparent),transparent_70%)] hover:before:opacity-100',
37
+ props.interactive && 'cursor-pointer hover:-translate-y-0.5 hover:shadow-elevated-md active:translate-y-0 active:duration-100',
48
38
  props.class,
49
- )" @pointermove="handlePointerMove">
39
+ )">
50
40
  <slot />
51
41
  </div>
52
42
  </template>
@@ -4,6 +4,7 @@ import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
5
  import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui';
6
6
  import { cn } from '../../../lib/utils';
7
+ import { useFieldControlId } from '../field/field-context';
7
8
 
8
9
  // Props
9
10
  const props = defineProps<CheckboxRootProps & {
@@ -15,10 +16,13 @@ const emits = defineEmits<CheckboxRootEmits>();
15
16
 
16
17
  const delegatedProps = reactiveOmit(props, 'class');
17
18
  const forwarded = useForwardPropsEmits(delegatedProps, emits);
19
+
20
+ // Let a surrounding Field label point to this control
21
+ const fieldControlId = useFieldControlId(() => props.id);
18
22
  </script>
19
23
 
20
24
  <template>
21
- <CheckboxRoot v-slot="slotProps" data-slot="checkbox" v-bind="forwarded" :class="cn(
25
+ <CheckboxRoot v-slot="slotProps" data-slot="checkbox" v-bind="forwarded" :id="fieldControlId" :class="cn(
22
26
  'group/checkbox peer relative inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm border border-input bg-secondary text-primary-foreground shadow-elevated-sm outline-none',
23
27
  '[transition:scale_320ms_var(--ease-spring),background-color_150ms_var(--ease-snappy),border-color_150ms_var(--ease-snappy),box-shadow_150ms_var(--ease-snappy)]',
24
28
  'hover:border-border-strong active:scale-[0.86] focus-visible:ring-[3px] focus-visible:ring-ring/45',
@@ -4,6 +4,7 @@ import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
5
  import { ComboboxInput, useForwardPropsEmits } from 'reka-ui';
6
6
  import { cn } from '../../../lib/utils';
7
+ import { useFieldControlId } from '../field/field-context';
7
8
 
8
9
  defineOptions({
9
10
  inheritAttrs: false
@@ -17,10 +18,13 @@ const emits = defineEmits<ComboboxInputEmits>();
17
18
 
18
19
  const delegatedProps = reactiveOmit(props, 'class');
19
20
  const forwarded = useForwardPropsEmits(delegatedProps, emits);
21
+
22
+ // Let a surrounding Field label point to this control
23
+ const fieldControlId = useFieldControlId();
20
24
  </script>
21
25
 
22
26
  <template>
23
- <ComboboxInput data-slot="combobox-input" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
27
+ <ComboboxInput :id="fieldControlId" data-slot="combobox-input" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
24
28
  'h-9 min-w-16 flex-1 truncate bg-transparent font-semibold text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed',
25
29
  props.class
26
30
  )
@@ -11,10 +11,24 @@ import { provideCommandGroupContext, useCommand } from '.';
11
11
  const props = defineProps<ListboxGroupProps & {
12
12
  class?: HTMLAttributes['class'];
13
13
  heading?: string;
14
+ stagger?: boolean;
14
15
  }>();
15
16
 
16
- // Omit local class from delegated props
17
- const delegatedProps = reactiveOmit(props, 'class');
17
+ // Omit local props from delegated props
18
+ const delegatedProps = reactiveOmit(props, 'class', 'stagger');
19
+
20
+ // Items float in one after another when the group mounts (same timing as the dropdown menu)
21
+ const staggerClass = [
22
+ '[&>*]:animate-[uv-float-in_240ms_var(--ease-out-expo)_both] [&>*]:[--uv-float-offset:-3px]',
23
+ '[&>*:nth-child(2)]:[animation-delay:16ms]',
24
+ '[&>*:nth-child(3)]:[animation-delay:32ms]',
25
+ '[&>*:nth-child(4)]:[animation-delay:48ms]',
26
+ '[&>*:nth-child(5)]:[animation-delay:64ms]',
27
+ '[&>*:nth-child(6)]:[animation-delay:80ms]',
28
+ '[&>*:nth-child(7)]:[animation-delay:96ms]',
29
+ '[&>*:nth-child(8)]:[animation-delay:112ms]',
30
+ '[&>*:nth-child(n+9)]:[animation-delay:128ms]'
31
+ ].join(' ');
18
32
 
19
33
  const { allGroups, filterState } = useCommand();
20
34
  const id = useId();
@@ -37,7 +51,7 @@ onUnmounted(() => {
37
51
  <ListboxGroupLabel v-if="heading" data-slot="command-group-heading" class="border-b border-border px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
38
52
  {{ heading }}
39
53
  </ListboxGroupLabel>
40
- <div class="p-2">
54
+ <div :class="cn('p-2', props.stagger && staggerClass)">
41
55
  <slot />
42
56
  </div>
43
57
  </ListboxGroup>
@@ -27,7 +27,7 @@ const { filterState } = useCommand();
27
27
  <div data-slot="command-input-wrapper" class="flex h-12 items-center gap-3 border-b border-border px-4">
28
28
  <HugeiconsIcon :icon="Search01Icon" class="size-4 shrink-0 text-muted-foreground" />
29
29
  <ListboxFilter v-bind="{ ...forwardedProps, ...$attrs }" v-model="filterState.search" data-slot="command-input" auto-focus :class="cn(
30
- 'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
30
+ 'placeholder:text-muted-foreground flex h-10 w-full rounded bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
31
31
  props.class,
32
32
  )
33
33
  " />
@@ -3,6 +3,7 @@ import type { ListboxContentProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
5
  import { ListboxContent, useForwardProps } from 'reka-ui';
6
+ import { useI18n } from 'vue-i18n';
6
7
  import { cn } from '../../../lib/utils';
7
8
 
8
9
  // Props
@@ -13,10 +14,13 @@ const delegatedProps = reactiveOmit(props, 'class');
13
14
 
14
15
  // Forward props
15
16
  const forwarded = useForwardProps(delegatedProps);
17
+
18
+ // Name the list of results for screen readers (an aria-label passed by the app wins)
19
+ const { t } = useI18n();
16
20
  </script>
17
21
 
18
22
  <template>
19
- <ListboxContent data-slot="command-list" v-bind="forwarded" :class="cn(
23
+ <ListboxContent data-slot="command-list" :aria-label="t('uiVintage.commandDialog.title')" v-bind="forwarded" :class="cn(
20
24
  'max-h-75 scroll-py-1 overflow-x-hidden overflow-y-auto',
21
25
  props.class,
22
26
  )
@@ -41,7 +41,7 @@ const props = withDefaults(defineProps<{
41
41
  const emits = defineEmits<{
42
42
  'update:selected': [value: PropertyKey[]];
43
43
  'update:sort': [value: DataTableSort | null];
44
- 'row-click': [row: T, event: MouseEvent];
44
+ 'row-click': [row: T, event: MouseEvent | KeyboardEvent];
45
45
  }>();
46
46
 
47
47
  // Slots
@@ -132,6 +132,14 @@ const handleRowClick = (row: T, event: MouseEvent) => {
132
132
  if ((event.target as HTMLElement).closest('button, a, input, [role=checkbox]')) return;
133
133
  emits('row-click', row, event);
134
134
  };
135
+
136
+ // Let keyboard users activate a focused clickable row with Enter or Space
137
+ const handleRowKeydown = (row: T, event: KeyboardEvent) => {
138
+ if (!props.rowClickable || event.target !== event.currentTarget) return;
139
+ if (event.key !== 'Enter' && event.key !== ' ') return;
140
+ event.preventDefault();
141
+ emits('row-click', row, event);
142
+ };
135
143
  </script>
136
144
 
137
145
  <template>
@@ -178,7 +186,7 @@ const handleRowClick = (row: T, event: MouseEvent) => {
178
186
  <td :colspan="columnCount" class="px-4 py-12">
179
187
  <slot name="empty">
180
188
  <div class="flex flex-col items-center gap-2 text-center text-muted-foreground">
181
- <HugeiconsIcon :icon="PackageIcon" class="size-8 animate-uv-float opacity-40" />
189
+ <HugeiconsIcon :icon="PackageIcon" class="size-8 opacity-40" />
182
190
  <span class="text-xs sm:text-sm">{{ resolvedEmptyText }}</span>
183
191
  </div>
184
192
  </slot>
@@ -188,7 +196,7 @@ const handleRowClick = (row: T, event: MouseEvent) => {
188
196
 
189
197
  <!-- Rows (animated when sorted, added, or removed) -->
190
198
  <TransitionGroup v-else tag="tbody" name="uv-data-table-row">
191
- <tr v-for="(row, index) in sortedRows" :key="getRowKey(row)" :data-state="selectedSet.has(getRowKey(row)) ? 'selected' : undefined" :class="cn('border-b border-border transition-colors duration-150 last:border-b-0 hover:bg-surface/70 data-[state=selected]:bg-primary/5', props.rowClickable && 'cursor-pointer')" @click="handleRowClick(row, $event)">
199
+ <tr v-for="(row, index) in sortedRows" :key="getRowKey(row)" :data-state="selectedSet.has(getRowKey(row)) ? 'selected' : undefined" :class="cn('border-b border-border transition-colors duration-150 last:border-b-0 hover:bg-surface/70 data-[state=selected]:bg-primary/5', props.rowClickable && 'cursor-pointer focus-visible:bg-surface/70 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring')" :tabindex="props.rowClickable ? 0 : undefined" @click="handleRowClick(row, $event)" @keydown="handleRowKeydown(row, $event)">
192
200
  <!-- Row selection -->
193
201
  <td v-if="props.selectable" class="px-4 py-3">
194
202
  <Checkbox :model-value="selectedSet.has(getRowKey(row))" :aria-label="t('uiVintage.dataTable.selectRow')" @update:model-value="toggleRow(row)" />
@@ -7,6 +7,7 @@ import type { DateValue } from 'reka-ui';
7
7
  import { computed, ref } from 'vue';
8
8
  import { useI18n } from 'vue-i18n';
9
9
  import { cn } from '../../../lib/utils';
10
+ import { useFieldControlId, useFieldLabelId } from '../field/field-context';
10
11
  import { Button } from '../button';
11
12
  import { Calendar } from '../calendar';
12
13
  import { Popover, PopoverContent, PopoverTrigger } from '../popover';
@@ -52,13 +53,18 @@ const defaultPlaceholder = computed(
52
53
  const formatter = computed(() => new DateFormatter(locale.value || 'en-US', {
53
54
  dateStyle: 'long',
54
55
  }));
56
+
57
+ // Let a surrounding Field label point to the trigger, and name it "label + selected date" so the value is not lost
58
+ const fieldControlId = useFieldControlId();
59
+ const fieldLabelId = useFieldLabelId();
60
+ const triggerLabelledBy = computed(() => fieldLabelId.value && fieldControlId.value ? `${fieldLabelId.value} ${fieldControlId.value}` : undefined);
55
61
  </script>
56
62
 
57
63
  <template>
58
64
  <Popover v-slot="{ close }">
59
65
  <!-- Date picker trigger -->
60
66
  <PopoverTrigger as-child>
61
- <Button variant="secondary" :disabled="disabled" :class="cn('h-13 w-60 justify-start text-left font-normal enabled:active:scale-100', !date && 'text-muted-foreground', props.class,)">
67
+ <Button :id="fieldControlId" :aria-labelledby="triggerLabelledBy" variant="secondary" :disabled="disabled" :class="cn('h-13 w-60 justify-start text-left font-normal enabled:active:scale-100', !date && 'text-muted-foreground', props.class,)">
62
68
  <HugeiconsIcon :icon="Calendar03Icon" class="size-4" />
63
69
  {{ date ? formatter.format(date.toDate(getLocalTimeZone())) : resolvedPlaceholder }}
64
70
  </Button>
@@ -3,6 +3,7 @@ import type { HTMLAttributes } from 'vue';
3
3
  import { fromDate, getLocalTimeZone, toCalendarDate, } from '@internationalized/date';
4
4
  import type { DateValue } from 'reka-ui';
5
5
  import { computed, ref } from 'vue';
6
+ import { useI18n } from 'vue-i18n';
6
7
  import { cn } from '../../../lib/utils';
7
8
  import { DatePicker } from '../date-picker';
8
9
  import { TimePicker } from '../time-picker';
@@ -154,6 +155,9 @@ const maxTimeValue = computed(() => {
154
155
  ? toTimeString(props.maxDate)
155
156
  : undefined;
156
157
  });
158
+
159
+ // The time input sits next to the date button, which takes the field label
160
+ const { t } = useI18n();
157
161
  </script>
158
162
 
159
163
  <template>
@@ -163,7 +167,7 @@ const maxTimeValue = computed(() => {
163
167
  </div>
164
168
 
165
169
  <div>
166
- <TimePicker v-model="timeValue" :disabled="props.disabled" :step="props.step" :min="minTimeValue" :max="maxTimeValue" :class="cn('w-full', props.timePickerClass)" />
170
+ <TimePicker v-model="timeValue" :aria-label="t('uiVintage.datePicker.time')" :disabled="props.disabled" :step="props.step" :min="minTimeValue" :max="maxTimeValue" :class="cn('w-full', props.timePickerClass)" />
167
171
  </div>
168
172
  </div>
169
173
  </template>
@@ -25,7 +25,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
25
25
  <template>
26
26
  <DropdownMenuItem data-slot="dropdown-menu-item" :data-inset="props.inset ? '' : undefined" :data-variant="props.variant" v-bind="forwarded" :class="cn(
27
27
  dropdownMenuItemClass,
28
- props.variant === 'destructive' && 'text-destructive data-highlighted:bg-destructive/10 data-highlighted:text-destructive',
28
+ props.variant === 'destructive' && 'text-destructive before:bg-destructive data-highlighted:bg-destructive/10 data-highlighted:text-destructive',
29
29
  props.class
30
30
  )
31
31
  ">
@@ -22,7 +22,7 @@ const forwardedProps = useForwardProps(delegatedProps);
22
22
  <DropdownMenuSubTrigger data-slot="dropdown-menu-sub-trigger" :data-inset="props.inset ? '' : undefined" v-bind="forwardedProps" :class="cn(
23
23
  'group/dropdown-sub-trigger',
24
24
  dropdownMenuItemClass,
25
- 'data-[state=open]:bg-accent data-[state=open]:text-foreground',
25
+ 'data-[state=open]:bg-accent data-[state=open]:text-foreground data-[state=open]:before:scale-y-100',
26
26
  props.class
27
27
  )
28
28
  ">
@@ -19,6 +19,8 @@ export const dropdownMenuItemClass = [
19
19
  'relative flex cursor-pointer items-center gap-3 rounded px-3 py-2 text-xs font-semibold text-muted-foreground outline-hidden select-none transition-colors duration-150 sm:text-sm',
20
20
  'data-highlighted:bg-accent data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-9',
21
21
  '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
22
+ // A primary bar springs onto the highlighted item, like in the command palette
23
+ 'before:absolute before:inset-y-1.5 before:left-0 before:w-0.5 before:scale-y-0 before:rounded-full before:bg-primary before:transition-transform before:duration-300 before:ease-spring data-highlighted:before:scale-y-100',
22
24
  // Leading icons nudge slightly when the item is highlighted
23
25
  '[&>svg]:transition-transform [&>svg]:duration-300 [&>svg]:ease-spring [&[data-highlighted]>svg]:translate-x-0.5 [&[data-highlighted]>svg]:scale-110'
24
26
  ].join(' ');
@@ -3,12 +3,16 @@ import type { HTMLAttributes } from 'vue';
3
3
  import type { FieldVariants } from '.';
4
4
  import { cn } from '../../../lib/utils';
5
5
  import { fieldVariants } from '.';
6
+ import { provideFieldContext } from './field-context';
6
7
 
7
8
  // Props
8
9
  const props = defineProps<{
9
10
  class?: HTMLAttributes['class'];
10
11
  orientation?: FieldVariants['orientation'];
11
12
  }>();
13
+
14
+ // Let FieldLabel point to the control placed in this field
15
+ provideFieldContext();
12
16
  </script>
13
17
 
14
18
  <template>
@@ -2,17 +2,21 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import { cn } from '../../../lib/utils';
4
4
  import { Label } from '../label';
5
+ import { useFieldLabel } from './field-context';
5
6
 
6
7
  // Props
7
8
  const props = defineProps<{
8
9
  class?: HTMLAttributes['class'];
9
10
  }>();
11
+
12
+ // Point to the control of the surrounding Field (an explicit for attribute still wins)
13
+ const { labelId, target: labelTarget } = useFieldLabel();
10
14
  </script>
11
15
 
12
16
  <template>
13
- <Label data-slot="field-label" :class="cn(
17
+ <Label :id="labelId" data-slot="field-label" :for="labelTarget" :class="cn(
14
18
  'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
15
- 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-4',
19
+ 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded has-[>[data-slot=field]]:border *:data-[slot=field]:p-4',
16
20
  'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
17
21
  props.class
18
22
  )
@@ -0,0 +1,62 @@
1
+ import type { InjectionKey, Ref } from 'vue';
2
+ import { computed, inject, onBeforeUnmount, provide, ref, useAttrs, useId } from 'vue';
3
+
4
+ interface FieldContext {
5
+ // Id of the control the field label points to (undefined until a control registers)
6
+ controlId: Ref<string | undefined>;
7
+ // Id of the field label, for controls that combine it with their own text
8
+ labelId: Ref<string | undefined>;
9
+ }
10
+
11
+ const fieldContextKey: InjectionKey<FieldContext> = Symbol('uv-field');
12
+
13
+ // Share the label target with the label and the control inside a Field
14
+ export const provideFieldContext = () => {
15
+ const context: FieldContext = { controlId: ref(undefined), labelId: ref(undefined) };
16
+ provide(fieldContextKey, context);
17
+ return context;
18
+ };
19
+
20
+ // Id the FieldLabel should point to, if the field has a control, and the id the label registers for itself
21
+ export const useFieldLabel = () => {
22
+ const field = inject(fieldContextKey, null);
23
+ const labelId = `uv-field-label-${useId()}`;
24
+ if (field && !field.labelId.value) {
25
+ field.labelId.value = labelId;
26
+ onBeforeUnmount(() => {
27
+ if (field.labelId.value === labelId) field.labelId.value = undefined;
28
+ });
29
+ }
30
+ return {
31
+ labelId: field ? labelId : undefined,
32
+ target: computed(() => field?.controlId.value)
33
+ };
34
+ };
35
+
36
+ // Id of the surrounding Field label, if any
37
+ export const useFieldLabelId = () => {
38
+ const field = inject(fieldContextKey, null);
39
+ return computed(() => field?.labelId.value);
40
+ };
41
+
42
+ // Give the first control inside a Field an id its FieldLabel points to (an explicit id on the control wins)
43
+ export const useFieldControlId = (explicitId?: () => string | undefined) => {
44
+ const field = inject(fieldContextKey, null);
45
+ const attrs = useAttrs();
46
+ const generatedId = `uv-field-control-${useId()}`;
47
+
48
+ // Only the first control claims the label, so composite controls (like DateTimePicker) don't fight over it
49
+ if (!field || field.controlId.value) {
50
+ return computed(() => explicitId?.());
51
+ }
52
+
53
+ const id = (explicitId?.() ?? (attrs.id as string | undefined)) || generatedId;
54
+ field.controlId.value = id;
55
+
56
+ // Release the label when the control goes away
57
+ onBeforeUnmount(() => {
58
+ if (field.controlId.value === id) field.controlId.value = undefined;
59
+ });
60
+
61
+ return computed(() => id);
62
+ };
@@ -29,6 +29,7 @@ export const fieldVariants: (props?: FieldVariants & ClassProp) => string = cva(
29
29
  }
30
30
  );
31
31
 
32
+ export { useFieldControlId } from './field-context';
32
33
  export { default as Field } from './Field.vue';
33
34
  export { default as FieldContent } from './FieldContent.vue';
34
35
  export { default as FieldDescription } from './FieldDescription.vue';
@@ -175,16 +175,16 @@ onBeforeUnmount(() => clearTimeout(rejectTimer));
175
175
  'group/dropzone relative isolate flex min-h-44 cursor-pointer select-none flex-col items-center justify-center gap-3 rounded border-2 border-dashed border-border bg-surface/50 px-6 py-8 text-center outline-none',
176
176
  '[transition:border-color_150ms,background-color_220ms,box-shadow_220ms,transform_420ms_var(--ease-spring)]',
177
177
  'hover:border-border-strong hover:bg-surface focus-visible:ring-[3px] focus-visible:ring-ring/45 active:scale-[0.99] active:duration-75',
178
- 'data-dragging:scale-[1.01] data-dragging:border-transparent data-dragging:bg-primary/5 data-dragging:shadow-[0_0_0_4px_color-mix(in_oklab,var(--primary)_12%,transparent)] data-dragging:hover:border-transparent data-dragging:hover:bg-primary/5',
178
+ 'data-dragging:scale-[1.01] data-dragging:border-transparent data-dragging:bg-primary/5 data-dragging:hover:border-transparent data-dragging:hover:bg-primary/5',
179
179
  'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:border-border data-disabled:hover:bg-surface/50 data-disabled:active:scale-100',
180
180
  isRejected && 'animate-uv-shake border-destructive/70'
181
181
  )" @click="openPicker" @keydown.enter.prevent="openPicker" @keydown.space.prevent="openPicker" @dragenter.prevent="onDragEnter" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
182
182
  <!-- Marching dashed border while dragging -->
183
183
  <span aria-hidden="true" class="uv-dropzone-ants pointer-events-none absolute -inset-0.5 rounded opacity-0 transition-opacity duration-150 group-data-dragging/dropzone:opacity-100" />
184
184
 
185
- <!-- Upload icon (floats up and grows while dragging) -->
186
- <span aria-hidden="true" class="relative flex size-14 items-center justify-center rounded border border-border bg-card text-muted-foreground shadow-elevated-sm transition-[translate,scale,color,border-color,box-shadow] duration-420 ease-bounce group-hover/dropzone:-translate-y-0.5 group-hover/dropzone:text-foreground group-data-dragging/dropzone:-translate-y-2 group-data-dragging/dropzone:scale-115 group-data-dragging/dropzone:border-primary/50 group-data-dragging/dropzone:text-primary group-data-dragging/dropzone:shadow-glow">
187
- <HugeiconsIcon :icon="CloudUploadIcon" class="size-6 group-data-dragging/dropzone:animate-uv-float" />
185
+ <!-- Upload icon (lifts and grows while dragging) -->
186
+ <span aria-hidden="true" class="relative flex size-14 items-center justify-center rounded border border-border bg-card text-muted-foreground shadow-elevated-sm transition-[translate,scale,color,border-color,box-shadow] duration-420 ease-bounce group-hover/dropzone:-translate-y-0.5 group-hover/dropzone:text-foreground group-data-dragging/dropzone:-translate-y-2 group-data-dragging/dropzone:scale-115 group-data-dragging/dropzone:border-primary/50 group-data-dragging/dropzone:text-primary group-data-dragging/dropzone:shadow-elevated-md">
187
+ <HugeiconsIcon :icon="CloudUploadIcon" class="size-6" />
188
188
  </span>
189
189
 
190
190
  <!-- Texts -->
@@ -2,6 +2,7 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import { useVModel } from '@vueuse/core';
4
4
  import { cn } from '../../../lib/utils';
5
+ import { useFieldControlId } from '../field/field-context';
5
6
 
6
7
  // Props
7
8
  const props = defineProps<{
@@ -19,10 +20,13 @@ const modelValue = useVModel(props, 'modelValue', emits, {
19
20
  passive: true,
20
21
  defaultValue: props.defaultValue
21
22
  });
23
+
24
+ // Let a surrounding Field label point to this control
25
+ const fieldControlId = useFieldControlId();
22
26
  </script>
23
27
 
24
28
  <template>
25
- <input v-model="modelValue" data-slot="input" :class="cn(
29
+ <input :id="fieldControlId" v-model="modelValue" data-slot="input" :class="cn(
26
30
  'uv-field h-13 w-full min-w-0 rounded border border-input bg-secondary px-4 py-3 text-xs font-semibold text-foreground shadow-elevated-sm outline-none sm:text-sm',
27
31
  'selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground placeholder:opacity-60 placeholder:transition-opacity focus-visible:placeholder:opacity-40',
28
32
  'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-semibold file:text-foreground',
@@ -5,6 +5,7 @@ import { reactiveOmit, useVModel } from '@vueuse/core';
5
5
  import { ArrowDown01Icon } from '@hugeicons/core-free-icons';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { cn } from '../../../lib/utils';
8
+ import { useFieldControlId } from '../field/field-context';
8
9
 
9
10
  defineOptions({
10
11
  inheritAttrs: false
@@ -24,12 +25,15 @@ const modelValue = useVModel(props, 'modelValue', emit, {
24
25
  });
25
26
 
26
27
  const delegatedProps = reactiveOmit(props, 'class');
28
+
29
+ // Let a surrounding Field label point to this control
30
+ const fieldControlId = useFieldControlId();
27
31
  </script>
28
32
 
29
33
  <template>
30
34
  <div class="group/native-select relative w-fit has-[select:disabled]:opacity-50" data-slot="native-select-wrapper">
31
- <select v-bind="{ ...$attrs, ...delegatedProps }" v-model="modelValue" data-slot="native-select" :class="cn(
32
- 'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
35
+ <select :id="fieldControlId" v-bind="{ ...$attrs, ...delegatedProps }" v-model="modelValue" data-slot="native-select" :class="cn(
36
+ 'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded border bg-transparent px-3 py-2 pr-9 text-sm shadow-elevated-sm transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
33
37
  'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
34
38
  'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
35
39
  props.class
@@ -8,6 +8,7 @@ import { HugeiconsIcon } from '@hugeicons/vue';
8
8
  import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFieldRoot, useForwardPropsEmits } from 'reka-ui';
9
9
  import { useI18n } from 'vue-i18n';
10
10
  import { cn } from '../../../lib/utils';
11
+ import { useFieldControlId } from '../field/field-context';
11
12
 
12
13
  // Props
13
14
  const props = defineProps<NumberFieldRootProps & {
@@ -77,10 +78,13 @@ const animateChange = async (value: number) => {
77
78
  };
78
79
  }
79
80
  };
81
+
82
+ // Let a surrounding Field label point to this control
83
+ const fieldControlId = useFieldControlId(() => props.id);
80
84
  </script>
81
85
 
82
86
  <template>
83
- <NumberFieldRoot v-slot="slotProps" data-slot="number-field" v-bind="forwarded" :class="cn(
87
+ <NumberFieldRoot v-slot="slotProps" data-slot="number-field" v-bind="forwarded" :id="fieldControlId" :class="cn(
84
88
  'uv-field flex h-13 w-full items-center gap-1 rounded border border-input bg-secondary px-1.5 text-foreground shadow-elevated-sm',
85
89
  'data-disabled:cursor-not-allowed data-disabled:opacity-60 aria-invalid:animate-uv-shake',
86
90
  props.class
@@ -25,7 +25,7 @@ const { t } = useI18n();
25
25
  <PaginationListItem data-slot="pagination-item" :aria-label="t('uiVintage.pagination.page', { page: props.value })" v-bind="delegatedProps" :class="cn(
26
26
  buttonVariants({ variant: 'secondary', size: props.size }),
27
27
  'tabular-nums',
28
- 'data-selected:border-primary data-selected:bg-primary data-selected:text-primary-foreground data-selected:shadow-glow data-selected:hover:border-primary data-selected:hover:bg-primary-hover',
28
+ 'data-selected:border-primary data-selected:bg-primary data-selected:text-primary-foreground data-selected:hover:border-primary data-selected:hover:bg-primary-hover',
29
29
  'data-selected:animate-[uv-pagination-pop_380ms_var(--ease-bounce)]',
30
30
  props.class
31
31
  )">
@@ -34,7 +34,7 @@ const isInvalid = computed(() => pinInputContext?.invalid.value || undefined);
34
34
  <span data-slot="pin-input-cell" class="relative inline-flex shrink-0">
35
35
  <!-- Native input (text is transparent, the overlay below renders the character) -->
36
36
  <PinInputInput data-slot="pin-input-input" v-bind="{ ...$attrs, ...forwardedProps }" :aria-invalid="isInvalid" :data-filled="isFilled ? '' : undefined" :class="cn(
37
- 'uv-field size-12 rounded border border-input bg-secondary text-center text-lg font-bold text-transparent caret-primary shadow-elevated-sm outline-none sm:size-13',
37
+ 'uv-field size-12 rounded border border-input bg-secondary text-center text-lg font-semibold text-transparent caret-primary shadow-elevated-sm outline-none sm:size-13',
38
38
  'placeholder:text-muted-foreground placeholder:opacity-50 selection:bg-primary/25',
39
39
  'data-filled:border-primary/60 data-filled:bg-primary/5',
40
40
  'disabled:cursor-not-allowed disabled:opacity-60',
@@ -42,7 +42,7 @@ const isInvalid = computed(() => pinInputContext?.invalid.value || undefined);
42
42
  )" />
43
43
 
44
44
  <!-- Animated character (re-keyed on every value so it pops) -->
45
- <span v-if="isFilled" :key="cellValue" aria-hidden="true" class="pointer-events-none absolute inset-0 flex animate-uv-pop items-center justify-center text-lg font-bold text-foreground">
45
+ <span v-if="isFilled" :key="cellValue" aria-hidden="true" class="pointer-events-none absolute inset-0 flex animate-uv-pop items-center justify-center text-lg font-semibold text-foreground">
46
46
  {{ displayValue }}
47
47
  </span>
48
48
  </span>
@@ -26,7 +26,7 @@ const percentage = computed(() => {
26
26
  </script>
27
27
 
28
28
  <template>
29
- <ProgressRoot data-slot="progress" v-bind="delegatedProps" :class="cn(
29
+ <ProgressRoot data-slot="progress" v-bind="delegatedProps" :model-value="props.indeterminate ? null : props.modelValue" :class="cn(
30
30
  'relative h-2 w-full overflow-hidden rounded-sm bg-muted',
31
31
  props.class
32
32
  )">
@@ -34,6 +34,6 @@ const percentage = computed(() => {
34
34
  <div v-if="props.indeterminate" data-slot="progress-indicator" class="absolute inset-y-0 left-0 w-full origin-left animate-uv-indeterminate rounded-sm bg-primary" />
35
35
 
36
36
  <!-- Determinate bar -->
37
- <ProgressIndicator v-else data-slot="progress-indicator" class="relative h-full w-full flex-1 overflow-hidden rounded-sm bg-primary transition-transform duration-700 ease-out-expo after:absolute after:inset-0 after:animate-uv-shimmer after:bg-[linear-gradient(90deg,transparent,rgb(255_255_255/0.35),transparent)]" :style="`transform: translateX(-${100 - percentage}%);`" />
37
+ <ProgressIndicator v-else data-slot="progress-indicator" class="relative h-full w-full flex-1 overflow-hidden rounded-sm bg-primary transition-transform duration-700 ease-out-expo" :style="`transform: translateX(-${100 - percentage}%);`" />
38
38
  </ProgressRoot>
39
39
  </template>
@@ -23,7 +23,7 @@ const props = defineProps<{
23
23
  'group/radio-card relative flex w-full cursor-pointer items-center gap-3 rounded border border-border bg-card px-4 py-3 text-left shadow-elevated-sm outline-none',
24
24
  '[transition:scale_320ms_var(--ease-spring),border-color_150ms_var(--ease-snappy),background-color_150ms_var(--ease-snappy),box-shadow_200ms_var(--ease-snappy)]',
25
25
  'hover:border-border-strong active:scale-[0.985] focus-visible:ring-[3px] focus-visible:ring-ring/45',
26
- 'data-[state=checked]:border-primary data-[state=checked]:bg-primary/5 data-[state=checked]:shadow-glow data-[state=checked]:hover:border-primary',
26
+ 'data-[state=checked]:border-primary data-[state=checked]:bg-primary/5 data-[state=checked]:hover:border-primary',
27
27
  'disabled:cursor-not-allowed disabled:opacity-60 disabled:active:scale-100',
28
28
  props.class
29
29
  )">
@@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps);
16
16
 
17
17
  <template>
18
18
  <SelectItem data-slot="select-item" v-bind="forwardedProps" :class="cn(
19
- 'focus:bg-accent focus:text-accent-foreground data-[state=checked]:text-foreground transition-colors duration-150[&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-2.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
19
+ 'focus:bg-accent focus:text-accent-foreground data-[state=checked]:text-foreground transition-colors duration-150 [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-2.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
20
20
  props.class
21
21
  )
22
22
  ">