@brumaombra/ui-vintage 0.7.13 → 0.8.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 (191) hide show
  1. package/COMPONENTS.md +3602 -0
  2. package/README.md +49 -9
  3. package/package.json +29 -8
  4. package/skills/ui-vintage/SKILL.md +42 -0
  5. package/src/components/blog/BlogFAQSection.vue +2 -2
  6. package/src/components/blog/BlogHeaderSection.vue +2 -2
  7. package/src/components/blog/BlogInfoSection.vue +3 -3
  8. package/src/components/blog/BlogProgressBar.vue +7 -6
  9. package/src/components/blog/BlogSectionTitle.vue +1 -1
  10. package/src/components/blog/BlogTagsSection.vue +22 -0
  11. package/src/components/blog/CategoryCard.vue +1 -1
  12. package/src/components/blog/HeaderCarousel.vue +6 -6
  13. package/src/components/blog/PostCard.vue +2 -2
  14. package/src/components/blog/SocialShareSidebar.vue +1 -1
  15. package/src/components/blog/TableOfContents.vue +6 -6
  16. package/src/components/blog/index.ts +1 -0
  17. package/src/components/busy-indicator/busy-indicator-state.ts +3 -2
  18. package/src/components/chip/Chip.vue +9 -5
  19. package/src/components/confirm-dialog/ConfirmDialog.vue +17 -6
  20. package/src/components/confirm-dialog/confirm-dialog-state.ts +4 -3
  21. package/src/components/content/BlogList.vue +3 -3
  22. package/src/components/content/BlogTable.vue +7 -7
  23. package/src/components/content/CodeBlock.vue +4 -4
  24. package/src/components/content/Flow.vue +3 -3
  25. package/src/components/content/ProseHr.vue +2 -2
  26. package/src/components/content/Terminal.vue +4 -4
  27. package/src/components/dashboard-shell/DashboardShell.vue +3 -3
  28. package/src/components/empty-state-card/EmptyStateCard.vue +4 -4
  29. package/src/components/error-page/ErrorPage.vue +16 -14
  30. package/src/components/info-card/InfoCard.vue +1 -1
  31. package/src/components/landing/landing-navbar/LandingNavbar.vue +2 -1
  32. package/src/components/load-more-button/LoadMoreButton.vue +2 -2
  33. package/src/components/loading-state-card/LoadingStateCard.vue +4 -4
  34. package/src/components/message-dialog/MessageDialog.vue +4 -4
  35. package/src/components/message-toast/MessageToast.vue +222 -50
  36. package/src/components/message-toast/index.ts +2 -2
  37. package/src/components/message-toast/message-toast-state.ts +107 -81
  38. package/src/components/page-header/PageHeader.vue +1 -1
  39. package/src/components/progress-component/ProgressComponent.vue +4 -4
  40. package/src/components/single-value-card/SingleValueCard.vue +47 -11
  41. package/src/components/theme-selector/ThemeSelector.vue +20 -36
  42. package/src/components/theme-selector/index.ts +3 -1
  43. package/src/components/theme-selector/theme-transition.ts +86 -0
  44. package/src/components/ui/accordion/Accordion.vue +41 -26
  45. package/src/components/ui/alert-dialog/AlertDialogContent.vue +2 -2
  46. package/src/components/ui/alert-dialog/AlertDialogFooter.vue +1 -1
  47. package/src/components/ui/animated-number/AnimatedNumber.vue +88 -0
  48. package/src/components/ui/animated-number/index.ts +1 -0
  49. package/src/components/ui/avatar/Avatar.vue +66 -0
  50. package/src/components/ui/avatar/AvatarFallback.vue +31 -0
  51. package/src/components/ui/avatar/AvatarGroup.vue +75 -0
  52. package/src/components/ui/avatar/AvatarImage.vue +23 -0
  53. package/src/components/ui/avatar/index.ts +47 -0
  54. package/src/components/ui/badge/Badge.vue +13 -2
  55. package/src/components/ui/breadcrumb/Breadcrumb.vue +4 -1
  56. package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +7 -1
  57. package/src/components/ui/busy/Busy.vue +45 -20
  58. package/src/components/ui/button/Button.vue +20 -3
  59. package/src/components/ui/button/index.ts +12 -2
  60. package/src/components/ui/card/Card.vue +21 -8
  61. package/src/components/ui/card/CardText.vue +1 -1
  62. package/src/components/ui/checkbox/Checkbox.vue +48 -0
  63. package/src/components/ui/checkbox/index.ts +1 -0
  64. package/src/components/ui/collapsible/Collapsible.vue +22 -0
  65. package/src/components/ui/collapsible/CollapsibleContent.vue +22 -0
  66. package/src/components/ui/collapsible/CollapsibleTrigger.vue +19 -0
  67. package/src/components/ui/collapsible/index.ts +3 -0
  68. package/src/components/ui/combobox/Combobox.vue +24 -0
  69. package/src/components/ui/combobox/ComboboxAnchor.vue +39 -0
  70. package/src/components/ui/combobox/ComboboxEmpty.vue +21 -0
  71. package/src/components/ui/combobox/ComboboxGroup.vue +26 -0
  72. package/src/components/ui/combobox/ComboboxInput.vue +28 -0
  73. package/src/components/ui/combobox/ComboboxItem.vue +39 -0
  74. package/src/components/ui/combobox/ComboboxList.vue +43 -0
  75. package/src/components/ui/combobox/ComboboxSelect.vue +162 -0
  76. package/src/components/ui/combobox/ComboboxSeparator.vue +16 -0
  77. package/src/components/ui/combobox/ComboboxTrigger.vue +28 -0
  78. package/src/components/ui/combobox/index.ts +11 -0
  79. package/src/components/ui/combobox/types.ts +16 -0
  80. package/src/components/ui/command/CommandDialog.vue +18 -16
  81. package/src/components/ui/command/CommandGroup.vue +1 -1
  82. package/src/components/ui/command/CommandInput.vue +2 -2
  83. package/src/components/ui/command/CommandItem.vue +7 -5
  84. package/src/components/ui/data-table/DataTable.vue +230 -0
  85. package/src/components/ui/data-table/index.ts +20 -0
  86. package/src/components/ui/date-picker/DatePicker.vue +11 -15
  87. package/src/components/ui/date-time-picker/DateTimePicker.vue +2 -7
  88. package/src/components/ui/dialog/DialogContent.vue +9 -4
  89. package/src/components/ui/dialog/DialogOverlay.vue +1 -1
  90. package/src/components/ui/dialog/DialogScrollContent.vue +10 -5
  91. package/src/components/ui/dropdown-menu/DropdownMenu.vue +18 -0
  92. package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +34 -0
  93. package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -0
  94. package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +13 -0
  95. package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +34 -0
  96. package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +26 -0
  97. package/src/components/ui/dropdown-menu/DropdownMenuPortal.vue +13 -0
  98. package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +18 -0
  99. package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +32 -0
  100. package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +16 -0
  101. package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +15 -0
  102. package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -0
  103. package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +39 -0
  104. package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +36 -0
  105. package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +15 -0
  106. package/src/components/ui/dropdown-menu/classes.ts +24 -0
  107. package/src/components/ui/dropdown-menu/index.ts +15 -0
  108. package/src/components/ui/field/FieldDescription.vue +1 -1
  109. package/src/components/ui/field/FieldError.vue +4 -1
  110. package/src/components/ui/file-dropzone/FileDropzone.vue +250 -0
  111. package/src/components/ui/file-dropzone/index.ts +35 -0
  112. package/src/components/ui/hover-card/HoverCard.vue +21 -0
  113. package/src/components/ui/hover-card/HoverCardContent.vue +33 -0
  114. package/src/components/ui/hover-card/HoverCardTrigger.vue +13 -0
  115. package/src/components/ui/hover-card/index.ts +3 -0
  116. package/src/components/ui/input/Input.vue +5 -5
  117. package/src/components/ui/kbd/Kbd.vue +21 -0
  118. package/src/components/ui/kbd/KbdGroup.vue +72 -0
  119. package/src/components/ui/kbd/index.ts +2 -0
  120. package/src/components/ui/number-field/NumberField.vue +130 -0
  121. package/src/components/ui/number-field/index.ts +1 -0
  122. package/src/components/ui/pagination/Pagination.vue +27 -0
  123. package/src/components/ui/pagination/PaginationContent.vue +20 -0
  124. package/src/components/ui/pagination/PaginationEllipsis.vue +31 -0
  125. package/src/components/ui/pagination/PaginationFirst.vue +32 -0
  126. package/src/components/ui/pagination/PaginationItem.vue +41 -0
  127. package/src/components/ui/pagination/PaginationLast.vue +32 -0
  128. package/src/components/ui/pagination/PaginationNext.vue +32 -0
  129. package/src/components/ui/pagination/PaginationPrevious.vue +32 -0
  130. package/src/components/ui/pagination/SimplePagination.vue +49 -0
  131. package/src/components/ui/pagination/index.ts +9 -0
  132. package/src/components/ui/pin-input/PinInput.vue +48 -0
  133. package/src/components/ui/pin-input/PinInputInput.vue +49 -0
  134. package/src/components/ui/pin-input/context.ts +8 -0
  135. package/src/components/ui/pin-input/index.ts +2 -0
  136. package/src/components/ui/popover/PopoverContent.vue +1 -1
  137. package/src/components/ui/progress/Progress.vue +22 -7
  138. package/src/components/ui/radio-group/RadioGroup.vue +27 -0
  139. package/src/components/ui/radio-group/RadioGroupCard.vue +56 -0
  140. package/src/components/ui/radio-group/RadioGroupItem.vue +40 -0
  141. package/src/components/ui/radio-group/index.ts +3 -0
  142. package/src/components/ui/select/SelectContent.vue +1 -1
  143. package/src/components/ui/select/SelectItem.vue +2 -2
  144. package/src/components/ui/select/SelectItemContent.vue +1 -1
  145. package/src/components/ui/select/SelectTrigger.vue +6 -6
  146. package/src/components/ui/select/SelectValueContent.vue +1 -1
  147. package/src/components/ui/sheet/SheetContent.vue +5 -5
  148. package/src/components/ui/sheet/SheetOverlay.vue +1 -1
  149. package/src/components/ui/sidebar/Sidebar.vue +7 -5
  150. package/src/components/ui/sidebar/SidebarRail.vue +3 -1
  151. package/src/components/ui/sidebar/SidebarTrigger.vue +3 -1
  152. package/src/components/ui/skeleton/Skeleton.vue +4 -4
  153. package/src/components/ui/slider/Slider.vue +8 -6
  154. package/src/components/ui/spinner/Spinner.vue +47 -0
  155. package/src/components/ui/spinner/index.ts +1 -0
  156. package/src/components/ui/stepper/SimpleStepper.vue +56 -0
  157. package/src/components/ui/stepper/Stepper.vue +30 -0
  158. package/src/components/ui/stepper/StepperDescription.vue +20 -0
  159. package/src/components/ui/stepper/StepperIndicator.vue +41 -0
  160. package/src/components/ui/stepper/StepperItem.vue +21 -0
  161. package/src/components/ui/stepper/StepperSeparator.vue +34 -0
  162. package/src/components/ui/stepper/StepperTitle.vue +20 -0
  163. package/src/components/ui/stepper/StepperTrigger.vue +20 -0
  164. package/src/components/ui/stepper/index.ts +17 -0
  165. package/src/components/ui/switch/Switch.vue +8 -6
  166. package/src/components/ui/tabs/TabsContent.vue +1 -1
  167. package/src/components/ui/tabs/TabsList.vue +13 -5
  168. package/src/components/ui/tabs/TabsTrigger.vue +5 -3
  169. package/src/components/ui/tags-input/TagsInput.vue +49 -0
  170. package/src/components/ui/tags-input/TagsInputInput.vue +23 -0
  171. package/src/components/ui/tags-input/TagsInputItem.vue +35 -0
  172. package/src/components/ui/tags-input/TagsInputItemDelete.vue +31 -0
  173. package/src/components/ui/tags-input/TagsInputItemText.vue +21 -0
  174. package/src/components/ui/tags-input/index.ts +5 -0
  175. package/src/components/ui/textarea/Textarea.vue +4 -5
  176. package/src/components/ui/toggle-group/Toggle.vue +33 -0
  177. package/src/components/ui/toggle-group/ToggleGroup.vue +93 -0
  178. package/src/components/ui/toggle-group/ToggleGroupItem.vue +34 -0
  179. package/src/components/ui/toggle-group/index.ts +69 -0
  180. package/src/components/ui/tooltip/TooltipContent.vue +1 -1
  181. package/src/i18n/de.json +76 -0
  182. package/src/i18n/en.json +76 -0
  183. package/src/i18n/es.json +76 -0
  184. package/src/i18n/fr.json +76 -0
  185. package/src/i18n/it.json +76 -0
  186. package/src/i18n/ja.json +76 -0
  187. package/src/i18n/pt.json +76 -0
  188. package/src/i18n/ru.json +76 -0
  189. package/src/i18n/zh.json +76 -0
  190. package/src/lib/color-tokens.ts +14 -14
  191. package/src/styles.css +289 -5
@@ -0,0 +1,2 @@
1
+ export { default as PinInput } from './PinInput.vue';
2
+ export { default as PinInputInput } from './PinInputInput.vue';
@@ -26,7 +26,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
26
26
  <template>
27
27
  <PopoverPortal>
28
28
  <PopoverContent data-slot="popover-content" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
29
- 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded border border-border p-4 transition-colors duration-300 ease-in-out origin-(--reka-popover-content-transform-origin) outline-hidden',
29
+ 'uv-floating-motion bg-popover text-popover-foreground z-50 w-72 rounded border border-border p-4 shadow-elevated-lg outline-hidden',
30
30
  props.class
31
31
  )
32
32
  ">
@@ -1,24 +1,39 @@
1
1
  <script setup lang="ts">
2
2
  import type { ProgressRootProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
+ import { computed } from 'vue';
4
5
  import { reactiveOmit } from '@vueuse/core';
5
6
  import { ProgressIndicator, ProgressRoot } from 'reka-ui';
6
7
  import { cn } from '../../../lib/utils';
7
8
 
8
9
  // Props
9
- const props = withDefaults(defineProps<ProgressRootProps & { class?: HTMLAttributes['class'] }>(), {
10
- modelValue: 0
10
+ const props = withDefaults(defineProps<ProgressRootProps & {
11
+ indeterminate?: boolean;
12
+ class?: HTMLAttributes['class'];
13
+ }>(), {
14
+ modelValue: 0,
15
+ indeterminate: false
11
16
  });
12
17
 
13
- const delegatedProps = reactiveOmit(props, 'class');
18
+ const delegatedProps = reactiveOmit(props, 'class', 'indeterminate');
19
+
20
+ // Resolve the fill percentage against the configured max
21
+ const percentage = computed(() => {
22
+ const max = props.max ?? 100;
23
+ if (!max) return 0;
24
+ return Math.min(100, Math.max(0, ((props.modelValue ?? 0) / max) * 100));
25
+ });
14
26
  </script>
15
27
 
16
28
  <template>
17
29
  <ProgressRoot data-slot="progress" v-bind="delegatedProps" :class="cn(
18
- 'bg-(--bg-selected-light) dark:bg-(--bg-selected-dark) relative h-2 w-full overflow-hidden',
30
+ 'relative h-2 w-full overflow-hidden rounded-sm bg-muted',
19
31
  props.class
20
- )
21
- ">
22
- <ProgressIndicator data-slot="progress-indicator" class="bg-primary h-full w-full flex-1 transition-all" :style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%);`" />
32
+ )">
33
+ <!-- Indeterminate bar -->
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
+
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}%);`" />
23
38
  </ProgressRoot>
24
39
  </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import type { RadioGroupRootEmits, RadioGroupRootProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { RadioGroupRoot, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<RadioGroupRootProps & {
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+
13
+ // Emits
14
+ const emits = defineEmits<RadioGroupRootEmits>();
15
+
16
+ const delegatedProps = reactiveOmit(props, 'class');
17
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
18
+ </script>
19
+
20
+ <template>
21
+ <RadioGroupRoot v-slot="slotProps" data-slot="radio-group" v-bind="forwarded" :class="cn(
22
+ 'grid gap-3 aria-[orientation=horizontal]:flex aria-[orientation=horizontal]:flex-wrap aria-[orientation=horizontal]:items-center aria-[orientation=horizontal]:gap-x-5',
23
+ props.class
24
+ )">
25
+ <slot v-bind="slotProps" />
26
+ </RadioGroupRoot>
27
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import type { AcceptableValue } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { HugeiconsIcon } from '@hugeicons/vue';
5
+ import { RadioGroupIndicator, RadioGroupItem } from 'reka-ui';
6
+ import type { HugeiconsIconDefinition } from '../../../lib/common-types';
7
+ import { cn } from '../../../lib/utils';
8
+
9
+ // Props
10
+ const props = defineProps<{
11
+ value: AcceptableValue;
12
+ label: string;
13
+ description?: string;
14
+ icon?: HugeiconsIconDefinition;
15
+ disabled?: boolean;
16
+ id?: string;
17
+ class?: HTMLAttributes['class'];
18
+ }>();
19
+ </script>
20
+
21
+ <template>
22
+ <RadioGroupItem :id="props.id" :value="props.value" :disabled="props.disabled" data-slot="radio-group-card" :class="cn(
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
+ '[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
+ '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',
27
+ 'disabled:cursor-not-allowed disabled:opacity-60 disabled:active:scale-100',
28
+ props.class
29
+ )">
30
+ <!-- Icon -->
31
+ <span v-if="props.icon" class="flex size-9 shrink-0 items-center justify-center rounded border border-border bg-surface text-muted-foreground transition-colors duration-150 group-data-[state=checked]/radio-card:border-primary/40 group-data-[state=checked]/radio-card:bg-primary/10 group-data-[state=checked]/radio-card:text-primary">
32
+ <HugeiconsIcon :icon="props.icon" class="size-4.5" />
33
+ </span>
34
+
35
+ <!-- Label and description -->
36
+ <span class="flex min-w-0 flex-1 flex-col gap-0.5">
37
+ <span class="text-xs font-semibold text-foreground sm:text-sm">
38
+ <slot name="label">{{ props.label }}</slot>
39
+ </span>
40
+ <span v-if="props.description || $slots.description" class="text-xs text-muted-foreground">
41
+ <slot name="description">{{ props.description }}</slot>
42
+ </span>
43
+ </span>
44
+
45
+ <!-- Radio circle -->
46
+ <span aria-hidden="true" class="flex size-5 shrink-0 items-center justify-center rounded-full border border-input bg-secondary shadow-elevated-sm transition-colors duration-150 group-data-[state=checked]/radio-card:border-primary">
47
+ <RadioGroupIndicator force-mount data-slot="radio-group-indicator" :class="cn(
48
+ 'pointer-events-none flex items-center justify-center opacity-0 scale-0',
49
+ '[transition:scale_140ms_var(--ease-snappy),opacity_100ms_var(--ease-snappy)]',
50
+ 'data-[state=checked]:opacity-100 data-[state=checked]:scale-100 data-[state=checked]:[transition:scale_420ms_var(--ease-bounce),opacity_80ms_linear]'
51
+ )">
52
+ <span class="size-2.5 rounded-full bg-primary" />
53
+ </RadioGroupIndicator>
54
+ </span>
55
+ </RadioGroupItem>
56
+ </template>
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ import type { RadioGroupItemEmits, RadioGroupItemProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { RadioGroupIndicator, RadioGroupItem, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<RadioGroupItemProps & {
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+
13
+ // Emits
14
+ const emits = defineEmits<RadioGroupItemEmits>();
15
+
16
+ const delegatedProps = reactiveOmit(props, 'class');
17
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
18
+ </script>
19
+
20
+ <template>
21
+ <RadioGroupItem v-slot="slotProps" data-slot="radio-group-item" v-bind="forwarded" :class="cn(
22
+ 'peer relative inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full border border-input bg-secondary text-primary shadow-elevated-sm outline-none',
23
+ '[transition:scale_320ms_var(--ease-spring),border-color_150ms_var(--ease-snappy),box-shadow_150ms_var(--ease-snappy)]',
24
+ 'hover:border-border-strong active:scale-[0.86] focus-visible:ring-[3px] focus-visible:ring-ring/45',
25
+ 'data-[state=checked]:border-primary data-[state=checked]:hover:border-primary',
26
+ 'disabled:cursor-not-allowed disabled:opacity-60 disabled:active:scale-100 aria-invalid:border-destructive',
27
+ props.class
28
+ )">
29
+ <!-- Indicator (always mounted so the dot can pop in and shrink out) -->
30
+ <RadioGroupIndicator force-mount data-slot="radio-group-indicator" :class="cn(
31
+ 'pointer-events-none flex items-center justify-center opacity-0 scale-0',
32
+ '[transition:scale_140ms_var(--ease-snappy),opacity_100ms_var(--ease-snappy)]',
33
+ 'data-[state=checked]:opacity-100 data-[state=checked]:scale-100 data-[state=checked]:[transition:scale_420ms_var(--ease-bounce),opacity_80ms_linear]'
34
+ )">
35
+ <slot v-bind="slotProps">
36
+ <span class="size-2.5 rounded-full bg-primary" />
37
+ </slot>
38
+ </RadioGroupIndicator>
39
+ </RadioGroupItem>
40
+ </template>
@@ -0,0 +1,3 @@
1
+ export { default as RadioGroup } from './RadioGroup.vue';
2
+ export { default as RadioGroupItem } from './RadioGroupItem.vue';
3
+ export { default as RadioGroupCard } from './RadioGroupCard.vue';
@@ -28,7 +28,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
28
28
  <template>
29
29
  <SelectPortal>
30
30
  <SelectContent data-slot="select-content" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
31
- 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-32 overflow-x-hidden overflow-y-auto rounded border border-border transition-colors duration-300 ease-in-out max-sm:w-(--reka-select-trigger-width) max-sm:min-w-(--reka-select-trigger-width)',
31
+ 'uv-floating-motion bg-popover text-popover-foreground relative z-50 max-h-(--reka-select-content-available-height) min-w-32 overflow-x-hidden overflow-y-auto rounded border border-border shadow-elevated-lg max-sm:w-(--reka-select-trigger-width) max-sm:min-w-(--reka-select-trigger-width)',
32
32
  position === 'popper' &&
33
33
  'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
34
34
  props.class
@@ -16,14 +16,14 @@ 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 [&_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
  ">
23
23
  <span class="absolute right-2 flex size-3.5 items-center justify-center">
24
24
  <SelectItemIndicator>
25
25
  <slot name="indicator-icon">
26
- <HugeiconsIcon :icon="Tick02Icon" class="size-4" />
26
+ <HugeiconsIcon :icon="Tick02Icon" class="size-4 animate-uv-pop text-primary" />
27
27
  </slot>
28
28
  </SelectItemIndicator>
29
29
  </span>
@@ -36,7 +36,7 @@ const props = defineProps<{
36
36
 
37
37
  <!-- Description -->
38
38
  <span v-if="props.description" data-slot="select-item-content-description" :class="cn(
39
- 'font-regular text-[11px] sm:text-xs leading-4 sm:leading-5 text-(--text-secondary-light) dark:text-(--text-secondary-dark)',
39
+ 'font-regular text-[11px] sm:text-xs leading-4 sm:leading-5 text-muted-foreground',
40
40
  props.descriptionClass
41
41
  )
42
42
  ">
@@ -19,15 +19,15 @@ const forwardedProps = useForwardProps(delegatedProps);
19
19
 
20
20
  <template>
21
21
  <SelectTrigger data-slot="select-trigger" :data-size="size" v-bind="forwardedProps" :class="cn(
22
- 'data-placeholder:text-(--text-secondary-light) [&_svg:not([class*=\'text-\'])]:text-(--text-secondary-light) flex w-fit cursor-pointer items-center justify-between gap-2 rounded border border-(--border-light) bg-(--button-secondary-light) px-4 py-3 text-xs font-semibold whitespace-nowrap text-(--text-primary-light) transition-all duration-300 ease-in-out outline-none disabled:cursor-not-allowed disabled:opacity-60 data-[size=default]:h-13 data-[size=sm]:h-10 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 dark:border-(--border-dark) dark:bg-(--button-secondary-dark) dark:text-(--text-primary-dark) dark:data-placeholder:text-(--text-secondary-dark) dark:[&_svg:not([class*=\'text-\'])]:text-(--text-secondary-dark) sm:text-sm',
23
- 'focus-visible:border-(--border-hover-light) focus-visible:ring-0 dark:focus-visible:border-(--border-hover-dark)',
24
- 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
22
+ 'group/select-trigger uv-field flex w-fit cursor-pointer items-center justify-between gap-2 rounded border border-input bg-secondary px-4 py-3 text-xs font-semibold whitespace-nowrap text-foreground shadow-elevated-sm outline-none data-placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60 data-[size=default]:h-13 data-[size=sm]:h-10 sm:text-sm',
23
+ '*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2',
24
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 [&_svg:not([class*=\'text-\'])]:text-muted-foreground',
25
+ 'aria-invalid:animate-uv-shake',
25
26
  props.class
26
- )
27
- ">
27
+ )">
28
28
  <slot />
29
29
  <SelectIcon as-child>
30
- <HugeiconsIcon :icon="ArrowDown01Icon" class="size-4 opacity-50" />
30
+ <HugeiconsIcon :icon="ArrowDown01Icon" class="size-4 opacity-60 transition-transform duration-300 ease-spring group-data-[state=open]/select-trigger:rotate-180" />
31
31
  </SelectIcon>
32
32
  </SelectTrigger>
33
33
  </template>
@@ -44,7 +44,7 @@ const props = defineProps<SelectValueProps & {
44
44
 
45
45
  <!-- Description -->
46
46
  <span v-if="props.description" data-slot="select-value-content-description" :class="cn(
47
- 'min-w-0 whitespace-normal wrap-break-word text-[11px] leading-4 text-(--text-secondary-light) dark:text-(--text-secondary-dark) sm:text-xs sm:leading-5 max-sm:[display:-webkit-box] max-sm:overflow-hidden max-sm:[-webkit-box-orient:vertical] max-sm:[-webkit-line-clamp:3]',
47
+ 'min-w-0 whitespace-normal wrap-break-word text-[11px] leading-4 text-muted-foreground sm:text-xs sm:leading-5 max-sm:[display:-webkit-box] max-sm:overflow-hidden max-sm:[-webkit-box-orient:vertical] max-sm:[-webkit-line-clamp:3]',
48
48
  props.descriptionClass
49
49
  )
50
50
  ">
@@ -30,15 +30,15 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
30
30
  <DialogPortal>
31
31
  <SheetOverlay />
32
32
  <DialogContent data-slot="sheet-content" :class="cn(
33
- 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
33
+ 'bg-background fixed z-50 flex flex-col gap-4 shadow-elevated-xl outline-none [animation-fill-mode:both] data-[state=open]:[animation-duration:480ms] data-[state=open]:[animation-timing-function:var(--ease-spring)] data-[state=closed]:[animation-duration:200ms] data-[state=closed]:[animation-timing-function:var(--ease-snappy)]',
34
34
  side === 'right' &&
35
- 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
35
+ 'data-[state=open]:[animation-name:uv-sheet-in-right] data-[state=closed]:[animation-name:uv-sheet-out-right] inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
36
36
  side === 'left' &&
37
- 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
37
+ 'data-[state=open]:[animation-name:uv-sheet-in-left] data-[state=closed]:[animation-name:uv-sheet-out-left] inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
38
38
  side === 'top' &&
39
- 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
39
+ 'data-[state=open]:[animation-name:uv-sheet-in-top] data-[state=closed]:[animation-name:uv-sheet-out-top] inset-x-0 top-0 h-auto border-b',
40
40
  side === 'bottom' &&
41
- 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
41
+ 'data-[state=open]:[animation-name:uv-sheet-in-bottom] data-[state=closed]:[animation-name:uv-sheet-out-bottom] inset-x-0 bottom-0 h-auto border-t',
42
42
  props.class
43
43
  )
44
44
  " v-bind="{ ...$attrs, ...forwarded }">
@@ -13,7 +13,7 @@ const delegatedProps = reactiveOmit(props, 'class');
13
13
 
14
14
  <template>
15
15
  <DialogOverlay data-slot="sheet-overlay" :class="cn(
16
- 'fixed inset-0 z-50 bg-black/45 backdrop-blur-[2px] transition-[opacity,backdrop-filter] ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
16
+ 'uv-overlay-motion fixed inset-0 z-50 bg-black/40 backdrop-blur-[3px] dark:bg-black/60',
17
17
  props.class
18
18
  )
19
19
  " v-bind="delegatedProps">
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { SidebarProps } from '.';
3
+ import { useI18n } from 'vue-i18n';
3
4
  import { cn } from '../../../lib/utils';
4
5
  import { Sheet, SheetContent } from '../sheet';
5
6
  import SheetDescription from '../sheet/SheetDescription.vue';
@@ -19,6 +20,7 @@ const props = withDefaults(defineProps<SidebarProps>(), {
19
20
  compact: false
20
21
  });
21
22
 
23
+ const { t } = useI18n();
22
24
  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
23
25
 
24
26
  function handleMobileSidebarClick(event: MouseEvent) {
@@ -40,12 +42,12 @@ function handleMobileSidebarClick(event: MouseEvent) {
40
42
  </div>
41
43
 
42
44
  <Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
43
- <SheetContent data-sidebar="sidebar" data-slot="sidebar" data-mobile="true" :data-compact="props.compact ? 'true' : 'false'" :side="side" class="group/sidebar-wrapper border-sidebar-border bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" :style="{
45
+ <SheetContent data-sidebar="sidebar" data-slot="sidebar" data-mobile="true" :data-compact="props.compact ? 'true' : 'false'" :side="side" class="group/sidebar-wrapper border-sidebar-border bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 data-[state=open]:[animation-duration:320ms] data-[state=open]:[animation-timing-function:var(--ease-out-expo)] [&>button]:hidden" :style="{
44
46
  '--sidebar-width': SIDEBAR_WIDTH_MOBILE
45
47
  }">
46
48
  <SheetHeader class="sr-only">
47
- <SheetTitle>Sidebar</SheetTitle>
48
- <SheetDescription>Displays the mobile sidebar.</SheetDescription>
49
+ <SheetTitle>{{ t('uiVintage.sidebar.title') }}</SheetTitle>
50
+ <SheetDescription>{{ t('uiVintage.sidebar.description') }}</SheetDescription>
49
51
  </SheetHeader>
50
52
  <div class="flex h-full w-full flex-col" @click="handleMobileSidebarClick">
51
53
  <slot />
@@ -56,7 +58,7 @@ function handleMobileSidebarClick(event: MouseEvent) {
56
58
  <div v-else class="group/sidebar-wrapper group peer text-sidebar-foreground hidden md:block" data-slot="sidebar" :data-state="state" :data-collapsible="state === 'collapsed' ? collapsible : ''" :data-variant="variant" :data-side="side">
57
59
  <!-- This is what handles the sidebar gap on desktop -->
58
60
  <div :class="cn(
59
- 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
61
+ 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-300 ease-out-expo',
60
62
  'group-data-[collapsible=offcanvas]:w-0',
61
63
  'group-data-[side=right]:rotate-180',
62
64
  variant === 'floating' || variant === 'inset'
@@ -65,7 +67,7 @@ function handleMobileSidebarClick(event: MouseEvent) {
65
67
  )
66
68
  " />
67
69
  <div :class="cn(
68
- 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
70
+ 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-300 ease-out-expo md:flex',
69
71
  side === 'left'
70
72
  ? 'left-0 group-data-[collapsible=offcanvas]:-left-(--sidebar-width)'
71
73
  : 'right-0 group-data-[collapsible=offcanvas]:-right-(--sidebar-width)',
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes } from 'vue';
3
+ import { useI18n } from 'vue-i18n';
3
4
  import { cn } from '../../../lib/utils';
4
5
  import { useSidebar } from './utils';
5
6
 
@@ -8,11 +9,12 @@ const props = defineProps<{
8
9
  class?: HTMLAttributes['class'];
9
10
  }>();
10
11
 
12
+ const { t } = useI18n();
11
13
  const { toggleSidebar } = useSidebar();
12
14
  </script>
13
15
 
14
16
  <template>
15
- <button data-sidebar="rail" data-slot="sidebar-rail" aria-label="Toggle Sidebar" :tabindex="-1" title="Toggle Sidebar" :class="cn(
17
+ <button data-sidebar="rail" data-slot="sidebar-rail" :aria-label="t('uiVintage.sidebar.toggle')" :tabindex="-1" :title="t('uiVintage.sidebar.toggle')" :class="cn(
16
18
  'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-0.5 sm:flex',
17
19
  'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
18
20
  '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
@@ -2,6 +2,7 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import { PanelLeftIcon } from '@hugeicons/core-free-icons';
4
4
  import { HugeiconsIcon } from '@hugeicons/vue';
5
+ import { useI18n } from 'vue-i18n';
5
6
  import { cn } from '../../../lib/utils';
6
7
  import { Button } from '../button';
7
8
  import { useSidebar } from './utils';
@@ -11,12 +12,13 @@ const props = defineProps<{
11
12
  class?: HTMLAttributes['class'];
12
13
  }>();
13
14
 
15
+ const { t } = useI18n();
14
16
  const { toggleSidebar } = useSidebar();
15
17
  </script>
16
18
 
17
19
  <template>
18
20
  <Button data-sidebar="trigger" data-slot="sidebar-trigger" variant="secondary" size="icon" :class="cn(props.class)" @click="toggleSidebar">
19
21
  <HugeiconsIcon :icon="PanelLeftIcon" />
20
- <span class="sr-only">Toggle Sidebar</span>
22
+ <span class="sr-only">{{ t('uiVintage.sidebar.toggle') }}</span>
21
23
  </Button>
22
24
  </template>
@@ -10,8 +10,8 @@ const props = defineProps<{
10
10
 
11
11
  <template>
12
12
  <div data-slot="skeleton" :class="cn(
13
- props.class,
14
- 'animate-pulse rounded border border-(--border-light) bg-(--bg-surface-light) transition-colors duration-300 ease-in-out dark:border-(--border-dark) dark:bg-(--bg-surface-dark)'
15
- )
16
- " />
13
+ 'relative overflow-hidden rounded border border-border bg-surface',
14
+ 'after:absolute after:inset-0 after:animate-uv-shimmer after:bg-[linear-gradient(90deg,transparent,color-mix(in_oklab,var(--foreground)_6%,transparent),transparent)]',
15
+ props.class
16
+ )" />
17
17
  </template>
@@ -19,14 +19,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
19
19
 
20
20
  <template>
21
21
  <SliderRoot v-slot="{ modelValue }" data-slot="slider" :class="cn(
22
- 'relative flex w-full cursor-pointer touch-none items-center select-none data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
22
+ 'group/slider relative flex w-full cursor-pointer touch-none items-center py-2 select-none data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col data-[orientation=vertical]:px-2 data-[orientation=vertical]:py-0',
23
23
  props.class
24
- )
25
- " v-bind="forwarded">
26
- <SliderTrack data-slot="slider-track" class="bg-muted relative grow overflow-hidden rounded data-[orientation=horizontal]:h-2 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2">
27
- <SliderRange data-slot="slider-range" class="bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full" />
24
+ )" v-bind="forwarded">
25
+ <!-- Track -->
26
+ <SliderTrack data-slot="slider-track" class="relative grow overflow-hidden rounded bg-muted transition-[height,width] duration-200 ease-spring data-[orientation=horizontal]:h-2 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2 group-hover/slider:data-[orientation=horizontal]:h-2.5 group-hover/slider:data-[orientation=vertical]:w-2.5">
27
+ <!-- Filled range -->
28
+ <SliderRange data-slot="slider-range" class="absolute bg-primary bg-[linear-gradient(90deg,transparent,rgb(255_255_255/0.22))] data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full" />
28
29
  </SliderTrack>
29
30
 
30
- <SliderThumb v-for="(_, key) in modelValue" :key="key" data-slot="slider-thumb" class="bg-white border-primary ring-ring/50 block size-5 shrink-0 rounded border shadow-sm transition-[color,box-shadow,transform] hover:scale-105 hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" />
31
+ <!-- Thumbs -->
32
+ <SliderThumb v-for="(_, key) in modelValue" :key="key" data-slot="slider-thumb" class="block size-5 shrink-0 cursor-grab rounded border-2 border-primary bg-white shadow-elevated-md outline-none [transition:scale_300ms_var(--ease-spring),box-shadow_200ms] hover:scale-110 hover:shadow-[0_0_0_6px_color-mix(in_oklab,var(--primary)_16%,transparent)] focus-visible:shadow-[0_0_0_6px_color-mix(in_oklab,var(--primary)_24%,transparent)] active:scale-125 active:cursor-grabbing disabled:pointer-events-none disabled:opacity-50" />
31
33
  </SliderRoot>
32
34
  </template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue';
3
+ import { computed } from 'vue';
4
+ import { getUiVintageRuntimeMessage } from '../../../lib/i18n';
5
+ import { cn } from '../../../lib/utils';
6
+
7
+ // Props
8
+ const props = withDefaults(defineProps<{
9
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
10
+ label?: string;
11
+ class?: HTMLAttributes['class'];
12
+ }>(), {
13
+ size: 'md',
14
+ label: ''
15
+ });
16
+
17
+ const resolvedLabel = computed(() => props.label || getUiVintageRuntimeMessage('uiVintage.buttons.loading', 'Loading'));
18
+
19
+ // Size classes
20
+ const sizeClasses: Record<NonNullable<typeof props.size>, string> = {
21
+ xs: 'size-3',
22
+ sm: 'size-4',
23
+ md: 'size-5',
24
+ lg: 'size-8',
25
+ xl: 'size-12'
26
+ };
27
+ </script>
28
+
29
+ <template>
30
+ <span data-slot="spinner" role="status" :aria-label="resolvedLabel" :class="cn('relative inline-flex shrink-0 text-current', sizeClasses[props.size], props.class)">
31
+ <svg viewBox="0 0 24 24" fill="none" class="size-full animate-[spin_0.7s_linear_infinite]" aria-hidden="true">
32
+ <!-- Track -->
33
+ <circle cx="12" cy="12" r="9.5" stroke="currentColor" stroke-width="2.5" class="opacity-20" />
34
+
35
+ <!-- Arc -->
36
+ <circle cx="12" cy="12" r="9.5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-dasharray="60" class="origin-center animate-[uv-spinner-dash_1.4s_ease-in-out_infinite]" />
37
+ </svg>
38
+ </span>
39
+ </template>
40
+
41
+ <style>
42
+ @keyframes uv-spinner-dash {
43
+ 0% { stroke-dashoffset: 56; }
44
+ 50% { stroke-dashoffset: 16; transform: rotate(135deg); }
45
+ 100% { stroke-dashoffset: 56; transform: rotate(450deg); }
46
+ }
47
+ </style>
@@ -0,0 +1 @@
1
+ export { default as Spinner } from './Spinner.vue';
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue';
3
+ import type { StepperStep } from '.';
4
+ import { Tick02Icon } from '@hugeicons/core-free-icons';
5
+ import { HugeiconsIcon } from '@hugeicons/vue';
6
+ import { computed } from 'vue';
7
+ import { cn } from '../../../lib/utils';
8
+ import Stepper from './Stepper.vue';
9
+ import StepperDescription from './StepperDescription.vue';
10
+ import StepperIndicator from './StepperIndicator.vue';
11
+ import StepperItem from './StepperItem.vue';
12
+ import StepperSeparator from './StepperSeparator.vue';
13
+ import StepperTitle from './StepperTitle.vue';
14
+ import StepperTrigger from './StepperTrigger.vue';
15
+
16
+ // Props
17
+ const props = withDefaults(defineProps<{
18
+ steps: StepperStep[];
19
+ orientation?: 'horizontal' | 'vertical';
20
+ linear?: boolean;
21
+ class?: HTMLAttributes['class'];
22
+ }>(), {
23
+ orientation: 'horizontal',
24
+ linear: true,
25
+ });
26
+
27
+ // Active step (1-based, v-model)
28
+ const modelValue = defineModel<number>({ default: 1 });
29
+
30
+ const isVertical = computed(() => props.orientation === 'vertical');
31
+ </script>
32
+
33
+ <template>
34
+ <Stepper v-model="modelValue" :orientation="props.orientation" :linear="props.linear" :class="cn('w-full', isVertical ? 'gap-0' : 'items-start gap-2', props.class)">
35
+ <StepperItem v-for="(step, index) in props.steps" :key="index" :step="index + 1" :class="isVertical ? 'items-start gap-0 pb-8 last:pb-0' : 'flex-1 flex-col items-center gap-0'">
36
+ <!-- Step trigger -->
37
+ <StepperTrigger :class="isVertical ? 'items-start gap-4' : 'flex-col gap-2 text-center'">
38
+ <StepperIndicator v-slot="{ step: stepNumber, state }">
39
+ <!-- Custom icon until the step is completed -->
40
+ <HugeiconsIcon v-if="state !== 'completed' && step.icon" :icon="step.icon" class="size-4" />
41
+ <span v-else-if="state !== 'completed'">{{ stepNumber }}</span>
42
+ <HugeiconsIcon v-else :icon="Tick02Icon" class="size-4 animate-uv-pop" :stroke-width="2.5" />
43
+ </StepperIndicator>
44
+
45
+ <!-- Step text -->
46
+ <div :class="cn('flex flex-col gap-0.5', isVertical ? 'pt-2' : 'items-center px-1')">
47
+ <StepperTitle>{{ step.title }}</StepperTitle>
48
+ <StepperDescription v-if="step.description">{{ step.description }}</StepperDescription>
49
+ </div>
50
+ </StepperTrigger>
51
+
52
+ <!-- Connector to the next step -->
53
+ <StepperSeparator v-if="index < props.steps.length - 1" :class="isVertical ? 'absolute top-12 bottom-1 left-[21px]' : 'absolute top-[21px] right-[calc(-50%+18px)] left-[calc(50%+26px)]'" />
54
+ </StepperItem>
55
+ </Stepper>
56
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import type { StepperRootEmits, StepperRootProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { StepperRoot, useForwardPropsEmits } from 'reka-ui';
6
+ import { useI18n } from 'vue-i18n';
7
+ import { cn } from '../../../lib/utils';
8
+
9
+ // Props
10
+ const props = withDefaults(defineProps<StepperRootProps & {
11
+ class?: HTMLAttributes['class'];
12
+ }>(), {
13
+ orientation: 'horizontal',
14
+ linear: true,
15
+ });
16
+
17
+ // Emits
18
+ const emits = defineEmits<StepperRootEmits>();
19
+
20
+ const delegatedProps = reactiveOmit(props, 'class');
21
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
22
+
23
+ const { t } = useI18n();
24
+ </script>
25
+
26
+ <template>
27
+ <StepperRoot v-slot="slotProps" data-slot="stepper" :aria-label="t('uiVintage.stepper.label')" v-bind="forwarded" :class="cn('flex gap-2 data-[orientation=vertical]:flex-col', props.class)">
28
+ <slot v-bind="slotProps" />
29
+ </StepperRoot>
30
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import type { StepperDescriptionProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { StepperDescription } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<StepperDescriptionProps & {
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+
13
+ const delegatedProps = reactiveOmit(props, 'class');
14
+ </script>
15
+
16
+ <template>
17
+ <StepperDescription data-slot="stepper-description" v-bind="delegatedProps" :class="cn('text-xs text-muted-foreground', props.class)">
18
+ <slot />
19
+ </StepperDescription>
20
+ </template>