@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,41 @@
1
+ <script setup lang="ts">
2
+ import type { StepperIndicatorProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { Tick02Icon } from '@hugeicons/core-free-icons';
5
+ import { HugeiconsIcon } from '@hugeicons/vue';
6
+ import { reactiveOmit } from '@vueuse/core';
7
+ import { injectStepperItemContext, StepperIndicator } from 'reka-ui';
8
+ import { computed } from 'vue';
9
+ import { cn } from '../../../lib/utils';
10
+
11
+ // Props
12
+ const props = defineProps<StepperIndicatorProps & {
13
+ class?: HTMLAttributes['class'];
14
+ }>();
15
+
16
+ const delegatedProps = reactiveOmit(props, 'class');
17
+
18
+ // State of the parent step
19
+ const itemContext = injectStepperItemContext();
20
+ const state = computed(() => itemContext.state.value);
21
+ </script>
22
+
23
+ <template>
24
+ <StepperIndicator v-slot="{ step }" data-slot="stepper-indicator" :data-state="state" v-bind="delegatedProps" :class="cn(
25
+ 'relative inline-flex size-9 shrink-0 items-center justify-center rounded-full border-2 border-border bg-card text-sm font-bold text-muted-foreground ring-0 ring-primary/15 tabular-nums',
26
+ '[transition:color_150ms,background-color_220ms,border-color_220ms,box-shadow_380ms_var(--ease-spring),transform_380ms_var(--ease-spring)]',
27
+ 'group-hover/stepper-trigger:border-border-strong group-active/stepper-trigger:scale-95 group-active/stepper-trigger:duration-75',
28
+ 'data-[state=active]:border-primary data-[state=active]:text-primary data-[state=active]:ring-4 data-[state=active]:group-hover/stepper-trigger:border-primary',
29
+ 'data-[state=completed]:border-primary data-[state=completed]:bg-primary data-[state=completed]:text-primary-foreground data-[state=completed]:group-hover/stepper-trigger:border-primary',
30
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
31
+ props.class
32
+ )">
33
+ <slot :step="step" :state="state">
34
+ <!-- Completed check -->
35
+ <HugeiconsIcon v-if="state === 'completed'" :icon="Tick02Icon" class="size-4 animate-uv-pop" :stroke-width="2.5" />
36
+
37
+ <!-- Step number -->
38
+ <span v-else>{{ step }}</span>
39
+ </slot>
40
+ </StepperIndicator>
41
+ </template>
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import type { StepperItemProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { StepperItem, useForwardProps } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<StepperItemProps & {
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+
13
+ const delegatedProps = reactiveOmit(props, 'class');
14
+ const forwardedProps = useForwardProps(delegatedProps);
15
+ </script>
16
+
17
+ <template>
18
+ <StepperItem v-slot="slotProps" data-slot="stepper-item" v-bind="forwardedProps" :class="cn('group/stepper-item relative flex items-center gap-2 data-disabled:pointer-events-none data-disabled:opacity-50', props.class)">
19
+ <slot v-bind="slotProps" />
20
+ </StepperItem>
21
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import type { StepperSeparatorProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { injectStepperItemContext, injectStepperRootContext, StepperSeparator } from 'reka-ui';
6
+ import { computed } from 'vue';
7
+ import { cn } from '../../../lib/utils';
8
+
9
+ // Props
10
+ const props = defineProps<StepperSeparatorProps & {
11
+ class?: HTMLAttributes['class'];
12
+ }>();
13
+
14
+ const delegatedProps = reactiveOmit(props, 'class');
15
+
16
+ // Orientation and completion come from the stepper
17
+ const rootContext = injectStepperRootContext();
18
+ const itemContext = injectStepperItemContext();
19
+ const isVertical = computed(() => rootContext.orientation.value === 'vertical');
20
+ const isCompleted = computed(() => itemContext.state.value === 'completed');
21
+
22
+ // Fill grows along the track from its start
23
+ const fillClass = computed(() => {
24
+ if (isVertical.value) return cn('origin-top', isCompleted.value ? 'scale-y-100' : 'scale-y-0');
25
+ return cn('origin-left rtl:origin-right', isCompleted.value ? 'scale-x-100' : 'scale-x-0');
26
+ });
27
+ </script>
28
+
29
+ <template>
30
+ <StepperSeparator data-slot="stepper-separator" v-bind="delegatedProps" :class="cn('relative shrink-0 overflow-hidden rounded-full bg-border', isVertical ? 'w-0.5' : 'h-0.5', props.class)">
31
+ <!-- Progress fill -->
32
+ <span aria-hidden="true" :class="cn('absolute inset-0 rounded-full bg-primary transition-transform duration-500 ease-out-expo', fillClass)" />
33
+ </StepperSeparator>
34
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import type { StepperTitleProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { StepperTitle } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<StepperTitleProps & {
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+
13
+ const delegatedProps = reactiveOmit(props, 'class');
14
+ </script>
15
+
16
+ <template>
17
+ <StepperTitle data-slot="stepper-title" v-bind="delegatedProps" :class="cn('text-xs font-semibold text-foreground transition-colors duration-150 group-data-[state=inactive]/stepper-item:text-muted-foreground sm:text-sm', props.class)">
18
+ <slot />
19
+ </StepperTitle>
20
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import type { StepperTriggerProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { StepperTrigger } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<StepperTriggerProps & {
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+
13
+ const delegatedProps = reactiveOmit(props, 'class');
14
+ </script>
15
+
16
+ <template>
17
+ <StepperTrigger data-slot="stepper-trigger" v-bind="delegatedProps" :class="cn('group/stepper-trigger flex cursor-pointer items-center gap-3 rounded p-1 text-left outline-none transition-colors duration-150 focus-visible:ring-[3px] focus-visible:ring-ring/45 data-disabled:cursor-not-allowed', props.class)">
18
+ <slot />
19
+ </StepperTrigger>
20
+ </template>
@@ -0,0 +1,17 @@
1
+ import type { HugeiconsIconDefinition } from '../../../lib/common-types';
2
+
3
+ export { default as SimpleStepper } from './SimpleStepper.vue';
4
+ export { default as Stepper } from './Stepper.vue';
5
+ export { default as StepperDescription } from './StepperDescription.vue';
6
+ export { default as StepperIndicator } from './StepperIndicator.vue';
7
+ export { default as StepperItem } from './StepperItem.vue';
8
+ export { default as StepperSeparator } from './StepperSeparator.vue';
9
+ export { default as StepperTitle } from './StepperTitle.vue';
10
+ export { default as StepperTrigger } from './StepperTrigger.vue';
11
+
12
+ // Step definition used by SimpleStepper
13
+ export type StepperStep = {
14
+ title: string;
15
+ description?: string;
16
+ icon?: HugeiconsIconDefinition;
17
+ };
@@ -19,14 +19,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
19
19
 
20
20
  <template>
21
21
  <SwitchRoot v-slot="slotProps" data-slot="switch" v-bind="forwarded" :class="cn(
22
- 'peer relative inline-flex h-7 w-12 shrink-0 cursor-pointer items-center rounded border border-(--border-light) bg-(--button-secondary-light) transition-all duration-200 outline-none focus-visible:border-(--border-hover-light) focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-60 data-[state=checked]:border-(--button-primary-light) data-[state=checked]:bg-(--bg-selected-light) dark:border-(--border-dark) dark:bg-(--button-secondary-dark) dark:focus-visible:border-(--border-hover-dark) dark:data-[state=checked]:border-(--button-primary-dark) dark:data-[state=checked]:bg-(--bg-selected-dark)',
22
+ 'group/switch peer relative inline-flex h-7 w-12 shrink-0 cursor-pointer items-center rounded border border-input bg-secondary shadow-elevated-sm outline-none',
23
+ '[transition:background-color_200ms,border-color_200ms,box-shadow_250ms] hover:border-border-strong focus-visible:ring-[3px] focus-visible:ring-ring/45 disabled:cursor-not-allowed disabled:opacity-60',
24
+ 'data-[state=checked]:border-primary data-[state=checked]:bg-accent data-[state=checked]:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_14%,transparent)]',
23
25
  props.class
24
- )
25
- ">
26
+ )">
26
27
  <SwitchThumb data-slot="switch-thumb" :class="cn(
27
- 'pointer-events-none absolute left-1 top-1/2 block h-4 w-4 -translate-y-1/2 rounded-[0.2rem] bg-(--text-secondary-light) ring-0 transition-all duration-200 data-[state=checked]:translate-x-6 data-[state=checked]:bg-(--button-primary-light) dark:bg-(--text-secondary-dark) dark:data-[state=checked]:bg-(--button-primary-dark)'
28
- )
29
- ">
28
+ 'pointer-events-none absolute left-1 top-1/2 flex h-4 w-4 -translate-y-1/2 items-center justify-center rounded-[0.2rem] bg-muted-foreground ring-0',
29
+ '[transition:translate_380ms_var(--ease-spring),width_220ms_var(--ease-spring),background-color_200ms]',
30
+ 'group-active/switch:w-5 data-[state=checked]:translate-x-6 data-[state=checked]:bg-primary group-active/switch:data-[state=checked]:translate-x-5'
31
+ )">
30
32
  <slot name="thumb" v-bind="slotProps" />
31
33
  </SwitchThumb>
32
34
  </SwitchRoot>
@@ -14,7 +14,7 @@ const delegatedProps = reactiveOmit(props, 'class');
14
14
  </script>
15
15
 
16
16
  <template>
17
- <TabsContent data-slot="tabs-content" :class="cn('flex-1 outline-none', props.class)" v-bind="delegatedProps">
17
+ <TabsContent data-slot="tabs-content" :class="cn('flex-1 outline-none data-[state=active]:animate-uv-fade-up', props.class)" v-bind="delegatedProps">
18
18
  <slot />
19
19
  </TabsContent>
20
20
  </template>
@@ -2,23 +2,31 @@
2
2
  import type { TabsListProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
- import { TabsList } from 'reka-ui';
5
+ import { TabsIndicator, TabsList } from 'reka-ui';
6
6
  import { cn } from '../../../lib/utils';
7
7
 
8
8
  // Props
9
9
  const props = defineProps<TabsListProps & {
10
10
  class?: HTMLAttributes['class'];
11
+ indicatorClass?: HTMLAttributes['class'];
11
12
  }>();
12
13
 
13
- const delegatedProps = reactiveOmit(props, 'class');
14
+ const delegatedProps = reactiveOmit(props, 'class', 'indicatorClass');
14
15
  </script>
15
16
 
16
17
  <template>
17
18
  <TabsList data-slot="tabs-list" v-bind="delegatedProps" :class="cn(
18
- 'inline-flex h-auto w-fit items-center justify-center gap-1 rounded border border-border bg-card p-1 text-card-foreground transition-colors duration-300 ease-in-out',
19
+ 'group/tabs-list relative isolate inline-flex h-auto w-fit items-center justify-center gap-1 rounded border border-border bg-card p-1 text-card-foreground shadow-elevated-sm aria-[orientation=vertical]:flex-col aria-[orientation=vertical]:items-stretch',
19
20
  props.class
20
- )
21
- ">
21
+ )">
22
+ <!-- Sliding active indicator -->
23
+ <TabsIndicator data-slot="tabs-indicator" :class="cn(
24
+ 'absolute -z-10 rounded border border-primary bg-surface shadow-elevated-sm transition-[translate,width,height] duration-[380ms] ease-spring',
25
+ 'group-aria-[orientation=horizontal]/tabs-list:inset-y-1 group-aria-[orientation=horizontal]/tabs-list:left-0 group-aria-[orientation=horizontal]/tabs-list:w-(--reka-tabs-indicator-size) group-aria-[orientation=horizontal]/tabs-list:translate-x-(--reka-tabs-indicator-position)',
26
+ 'group-aria-[orientation=vertical]/tabs-list:inset-x-1 group-aria-[orientation=vertical]/tabs-list:top-0 group-aria-[orientation=vertical]/tabs-list:h-(--reka-tabs-indicator-size) group-aria-[orientation=vertical]/tabs-list:translate-y-(--reka-tabs-indicator-position)',
27
+ props.indicatorClass
28
+ )" />
29
+
22
30
  <slot />
23
31
  </TabsList>
24
32
  </template>
@@ -16,10 +16,12 @@ const forwardedProps = useForwardProps(delegatedProps);
16
16
 
17
17
  <template>
18
18
  <TabsTrigger data-slot="tabs-trigger" :class="cn(
19
- 'text-muted-foreground hover:text-foreground focus-visible:outline-none inline-flex min-h-9 flex-1 cursor-pointer items-center justify-center gap-1.5 rounded border border-transparent bg-transparent px-4 py-2 text-sm font-semibold whitespace-nowrap transition-colors duration-300 ease-in-out disabled:pointer-events-none disabled:opacity-50 data-[state=active]:cursor-pointer data-[state=active]:border-(--button-primary-light) data-[state=active]:bg-gray-50 data-[state=active]:text-(--button-primary-light) data-[state=active]:font-semibold data-[state=active]:outline-1 data-[state=active]:outline-border data-[state=active]:outline-offset-0 data-[state=active]:shadow-none dark:data-[state=active]:border-(--button-primary-dark) dark:data-[state=active]:bg-(--bg-card-dark) dark:data-[state=active]:text-(--button-primary-dark) [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
19
+ 'relative inline-flex min-h-9 flex-1 cursor-pointer items-center justify-center gap-1.5 rounded border border-transparent bg-transparent px-4 py-2 text-sm font-semibold whitespace-nowrap text-muted-foreground outline-none select-none',
20
+ '[transition:color_150ms,background-color_150ms,scale_300ms_var(--ease-spring)] hover:text-foreground active:scale-[0.97] focus-visible:ring-[3px] focus-visible:ring-ring/45 disabled:pointer-events-none disabled:opacity-50',
21
+ 'data-[state=active]:text-primary data-[state=inactive]:hover:bg-accent/60',
22
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 [&_svg]:transition-transform [&_svg]:duration-300 [&_svg]:ease-bounce data-[state=active]:[&_svg]:scale-110',
20
23
  props.class
21
- )
22
- " v-bind="forwardedProps">
24
+ )" v-bind="forwardedProps">
23
25
  <slot />
24
26
  </TabsTrigger>
25
27
  </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts" generic="T extends AcceptableInputValue = string">
2
+ import type { AcceptableInputValue, TagsInputRootEmits, TagsInputRootProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { computed } from 'vue';
5
+ import { reactiveOmit } from '@vueuse/core';
6
+ import { TagsInputRoot, useForwardPropsEmits } from 'reka-ui';
7
+ import { useI18n } from 'vue-i18n';
8
+ import { cn } from '../../../lib/utils';
9
+ import TagsInputInput from './TagsInputInput.vue';
10
+ import TagsInputItem from './TagsInputItem.vue';
11
+
12
+ // Props
13
+ const props = defineProps<TagsInputRootProps<T> & {
14
+ placeholder?: string;
15
+ class?: HTMLAttributes['class'];
16
+ inputClass?: HTMLAttributes['class'];
17
+ }>();
18
+
19
+ // Emits
20
+ const emits = defineEmits<TagsInputRootEmits<T>>();
21
+
22
+ const delegatedProps = reactiveOmit(props, 'class', 'inputClass', 'placeholder');
23
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
24
+
25
+ const { t } = useI18n();
26
+ const resolvedPlaceholder = computed(() => props.placeholder ?? t('uiVintage.tagsInput.placeholder'));
27
+
28
+ // Stable key per tag (falls back to the index when duplicates are allowed)
29
+ const getItemKey = (item: AcceptableInputValue, index: number) => {
30
+ if (props.duplicate) return index;
31
+ return typeof item === 'object' ? JSON.stringify(item) : String(item);
32
+ };
33
+ </script>
34
+
35
+ <template>
36
+ <TagsInputRoot v-slot="slotProps" data-slot="tags-input" v-bind="forwarded" :class="cn(
37
+ 'uv-field flex min-h-13 w-full flex-wrap items-center gap-2 rounded border border-input bg-secondary px-3 py-2 text-xs font-semibold text-foreground shadow-elevated-sm sm:text-sm',
38
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-60 data-invalid:animate-uv-shake',
39
+ props.class
40
+ )">
41
+ <slot v-bind="slotProps">
42
+ <!-- Auto-rendered tags -->
43
+ <TagsInputItem v-for="(item, index) in slotProps.modelValue" :key="getItemKey(item, index)" :value="item" />
44
+
45
+ <!-- Text input -->
46
+ <TagsInputInput :placeholder="resolvedPlaceholder" :class="props.inputClass" />
47
+ </slot>
48
+ </TagsInputRoot>
49
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ import type { TagsInputInputProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { TagsInputInput, useForwardProps } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<TagsInputInputProps & {
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+
13
+ const delegatedProps = reactiveOmit(props, 'class');
14
+ const forwardedProps = useForwardProps(delegatedProps);
15
+ </script>
16
+
17
+ <template>
18
+ <TagsInputInput data-slot="tags-input-input" v-bind="forwardedProps" :class="cn(
19
+ 'h-7 min-w-24 flex-1 bg-transparent px-1 text-xs font-semibold text-foreground outline-none sm:text-sm',
20
+ 'placeholder:text-muted-foreground placeholder:opacity-60 disabled:cursor-not-allowed',
21
+ props.class
22
+ )" />
23
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ import type { TagsInputItemProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { TagsInputItem, useForwardProps } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+ import TagsInputItemDelete from './TagsInputItemDelete.vue';
8
+ import TagsInputItemText from './TagsInputItemText.vue';
9
+
10
+ // Props
11
+ const props = defineProps<TagsInputItemProps & {
12
+ class?: HTMLAttributes['class'];
13
+ }>();
14
+
15
+ const delegatedProps = reactiveOmit(props, 'class');
16
+ const forwardedProps = useForwardProps(delegatedProps);
17
+ </script>
18
+
19
+ <template>
20
+ <TagsInputItem data-slot="tags-input-item" v-bind="forwardedProps" :class="cn(
21
+ 'group/tag inline-flex h-7 max-w-full animate-uv-pop items-center gap-1.5 rounded border border-border bg-surface py-1 pr-1 pl-2.5 text-xs font-semibold text-foreground outline-none',
22
+ 'transition-[border-color,background-color,box-shadow] duration-150 ease-snappy',
23
+ 'data-[state=active]:border-primary data-[state=active]:bg-primary/5 data-[state=active]:ring-[3px] data-[state=active]:ring-primary/15',
24
+ 'data-disabled:opacity-60',
25
+ props.class
26
+ )">
27
+ <slot>
28
+ <!-- Tag text -->
29
+ <TagsInputItemText />
30
+
31
+ <!-- Delete button -->
32
+ <TagsInputItemDelete />
33
+ </slot>
34
+ </TagsInputItem>
35
+ </template>
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ import type { TagsInputItemDeleteProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { Cancel01Icon } from '@hugeicons/core-free-icons';
6
+ import { HugeiconsIcon } from '@hugeicons/vue';
7
+ import { TagsInputItemDelete, useForwardProps } from 'reka-ui';
8
+ import { cn } from '../../../lib/utils';
9
+
10
+ // Props
11
+ const props = defineProps<TagsInputItemDeleteProps & {
12
+ class?: HTMLAttributes['class'];
13
+ }>();
14
+
15
+ const delegatedProps = reactiveOmit(props, 'class');
16
+ const forwardedProps = useForwardProps(delegatedProps);
17
+ </script>
18
+
19
+ <template>
20
+ <TagsInputItemDelete data-slot="tags-input-item-delete" v-bind="forwardedProps" :class="cn(
21
+ 'inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground outline-none',
22
+ '[transition:rotate_320ms_var(--ease-spring),scale_320ms_var(--ease-spring),background-color_150ms_var(--ease-snappy),color_150ms_var(--ease-snappy)]',
23
+ 'hover:rotate-90 hover:bg-accent hover:text-foreground active:scale-[0.8] focus-visible:ring-[3px] focus-visible:ring-ring/45',
24
+ 'group-data-[state=active]/tag:text-primary data-disabled:pointer-events-none',
25
+ props.class
26
+ )">
27
+ <slot>
28
+ <HugeiconsIcon :icon="Cancel01Icon" class="size-3" />
29
+ </slot>
30
+ </TagsInputItemDelete>
31
+ </template>
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import type { TagsInputItemTextProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { TagsInputItemText, useForwardProps } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<TagsInputItemTextProps & {
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+
13
+ const delegatedProps = reactiveOmit(props, 'class');
14
+ const forwardedProps = useForwardProps(delegatedProps);
15
+ </script>
16
+
17
+ <template>
18
+ <TagsInputItemText data-slot="tags-input-item-text" v-bind="forwardedProps" :class="cn('truncate leading-none', props.class)">
19
+ <slot />
20
+ </TagsInputItemText>
21
+ </template>
@@ -0,0 +1,5 @@
1
+ export { default as TagsInput } from './TagsInput.vue';
2
+ export { default as TagsInputItem } from './TagsInputItem.vue';
3
+ export { default as TagsInputItemText } from './TagsInputItemText.vue';
4
+ export { default as TagsInputItemDelete } from './TagsInputItemDelete.vue';
5
+ export { default as TagsInputInput } from './TagsInputInput.vue';
@@ -23,10 +23,9 @@ const modelValue = useVModel(props, 'modelValue', emits, {
23
23
 
24
24
  <template>
25
25
  <textarea v-model="modelValue" data-slot="textarea" :class="cn(
26
- 'selection:bg-primary selection:text-primary-foreground flex field-sizing-content min-h-30 w-full min-w-0 rounded border border-(--border-light) bg-(--button-secondary-light) px-4 py-3 text-xs font-semibold text-(--text-primary-light) outline-none transition-all duration-300 ease-in-out placeholder:text-(--text-secondary-light) placeholder:opacity-60 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-60 dark:border-(--border-dark) dark:bg-(--button-secondary-dark) dark:text-(--text-primary-dark) dark:placeholder:text-(--text-secondary-dark) sm:text-sm',
27
- 'focus-visible:border-(--border-hover-light) focus-visible:ring-0 dark:focus-visible:border-(--border-hover-dark)',
28
- 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
26
+ 'uv-field flex field-sizing-content min-h-30 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
+ 'selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground placeholder:opacity-60',
28
+ 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-60 aria-invalid:animate-uv-shake',
29
29
  props.class
30
- )
31
- " />
30
+ )" />
32
31
  </template>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import type { ToggleEmits, ToggleProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import type { ToggleVariants } from '.';
5
+ import { reactiveOmit } from '@vueuse/core';
6
+ import { Toggle, useForwardPropsEmits } from 'reka-ui';
7
+ import { cn } from '../../../lib/utils';
8
+ import { toggleVariants } from '.';
9
+
10
+ // Props
11
+ const props = withDefaults(defineProps<ToggleProps & {
12
+ variant?: ToggleVariants['variant'];
13
+ size?: ToggleVariants['size'];
14
+ class?: HTMLAttributes['class'];
15
+ }>(), {
16
+ variant: 'default',
17
+ size: 'default',
18
+ disabled: false,
19
+ modelValue: undefined,
20
+ });
21
+
22
+ // Emits
23
+ const emits = defineEmits<ToggleEmits>();
24
+
25
+ const delegatedProps = reactiveOmit(props, 'class', 'variant', 'size');
26
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
27
+ </script>
28
+
29
+ <template>
30
+ <Toggle v-slot="slotProps" data-slot="toggle" :data-variant="props.variant" :data-size="props.size" v-bind="forwarded" :class="cn(toggleVariants({ variant: props.variant, size: props.size }), props.class)">
31
+ <slot v-bind="slotProps" />
32
+ </Toggle>
33
+ </template>
@@ -0,0 +1,93 @@
1
+ <script setup lang="ts">
2
+ import type { ToggleGroupRootEmits, ToggleGroupRootProps } from 'reka-ui';
3
+ import type { CSSProperties, HTMLAttributes } from 'vue';
4
+ import type { ToggleGroupSize } from '.';
5
+ import { reactiveOmit, unrefElement, useMutationObserver, useResizeObserver } from '@vueuse/core';
6
+ import { ToggleGroupRoot, useForwardPropsEmits } from 'reka-ui';
7
+ import { computed, nextTick, onMounted, provide, ref, shallowRef, toRef } from 'vue';
8
+ import { cn } from '../../../lib/utils';
9
+ import { toggleGroupContextKey } from '.';
10
+
11
+ // Props
12
+ const props = withDefaults(defineProps<ToggleGroupRootProps & {
13
+ size?: ToggleGroupSize;
14
+ highlight?: boolean;
15
+ class?: HTMLAttributes['class'];
16
+ }>(), {
17
+ size: 'default',
18
+ highlight: false,
19
+ disabled: false,
20
+ rovingFocus: true,
21
+ loop: true,
22
+ });
23
+
24
+ // Emits
25
+ const emits = defineEmits<ToggleGroupRootEmits>();
26
+
27
+ const delegatedProps = reactiveOmit(props, 'class', 'size', 'highlight');
28
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
29
+
30
+ // Single selection gets the sliding indicator (mirrors reka's type inference)
31
+ const isSingle = computed(() => {
32
+ if (props.type) return props.type === 'single';
33
+ return !Array.isArray(props.modelValue ?? props.defaultValue);
34
+ });
35
+
36
+ // Share size and indicator mode with the items
37
+ provide(toggleGroupContextKey, { size: toRef(props, 'size'), indicator: isSingle });
38
+
39
+ // Sliding indicator geometry
40
+ const rootRef = ref();
41
+ const activeItem = shallowRef<HTMLElement | null>(null);
42
+ const rect = ref({ x: 0, y: 0, width: 0, height: 0 });
43
+ const visible = ref(false);
44
+ const animated = ref(false);
45
+
46
+ // Measure the active item (offset* ignores the press scale transform)
47
+ function measure() {
48
+ const root = unrefElement(rootRef) as HTMLElement | null | undefined;
49
+ if (!root || !isSingle.value) return;
50
+ const item = root.querySelector<HTMLElement>('[data-slot="toggle-group-item"][data-state="on"]');
51
+ activeItem.value = item;
52
+ if (!item) {
53
+ visible.value = false;
54
+ return;
55
+ }
56
+ rect.value = { x: item.offsetLeft, y: item.offsetTop, width: item.offsetWidth, height: item.offsetHeight };
57
+ visible.value = true;
58
+ }
59
+
60
+ // Re-measure when selection changes or when the layout moves
61
+ useMutationObserver(rootRef, measure, { subtree: true, attributes: true, attributeFilter: ['data-state'] });
62
+ useResizeObserver(rootRef, measure);
63
+ useResizeObserver(activeItem, measure);
64
+
65
+ // First placement is instant, later moves slide
66
+ onMounted(async () => {
67
+ measure();
68
+ await nextTick();
69
+ requestAnimationFrame(() => {
70
+ animated.value = true;
71
+ });
72
+ });
73
+
74
+ const indicatorStyle = computed<CSSProperties>(() => ({
75
+ width: `${rect.value.width}px`,
76
+ height: `${rect.value.height}px`,
77
+ transform: `translate(${rect.value.x}px, ${rect.value.y}px) scale(${visible.value ? 1 : 0.9})`,
78
+ opacity: visible.value ? 1 : 0,
79
+ }));
80
+ </script>
81
+
82
+ <template>
83
+ <ToggleGroupRoot ref="rootRef" v-slot="slotProps" data-slot="toggle-group" :data-size="props.size" v-bind="forwarded" :class="cn('relative inline-flex w-fit items-center gap-1 rounded border border-border bg-card p-1 shadow-elevated-sm data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch', props.class)">
84
+ <!-- Sliding indicator behind the active item -->
85
+ <span v-if="isSingle" data-slot="toggle-group-indicator" aria-hidden="true" :style="indicatorStyle" :class="cn(
86
+ 'pointer-events-none absolute top-0 left-0 z-0 rounded-sm border',
87
+ animated && '[transition:transform_380ms_var(--ease-spring),width_380ms_var(--ease-spring),height_380ms_var(--ease-spring),opacity_180ms_var(--ease-snappy)]',
88
+ props.highlight ? 'border-primary/40 bg-primary/10 shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_10%,transparent)]' : 'border-border-strong bg-surface shadow-elevated-sm'
89
+ )" />
90
+
91
+ <slot v-bind="slotProps" />
92
+ </ToggleGroupRoot>
93
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import type { ToggleGroupItemProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import type { ToggleGroupSize } from '.';
5
+ import { reactiveOmit } from '@vueuse/core';
6
+ import { ToggleGroupItem, useForwardProps } from 'reka-ui';
7
+ import { computed, inject } from 'vue';
8
+ import { cn } from '../../../lib/utils';
9
+ import { toggleGroupContextKey, toggleGroupItemVariants } from '.';
10
+
11
+ // Props
12
+ const props = defineProps<ToggleGroupItemProps & {
13
+ size?: ToggleGroupSize;
14
+ class?: HTMLAttributes['class'];
15
+ }>();
16
+
17
+ const delegatedProps = reactiveOmit(props, 'class', 'size');
18
+ const forwardedProps = useForwardProps(delegatedProps);
19
+
20
+ // Size and indicator mode come from the group
21
+ const context = inject(toggleGroupContextKey, null);
22
+ const size = computed(() => props.size ?? context?.size.value ?? 'default');
23
+ const hasIndicator = computed(() => context?.indicator.value ?? false);
24
+ </script>
25
+
26
+ <template>
27
+ <ToggleGroupItem v-slot="slotProps" data-slot="toggle-group-item" :data-size="size" v-bind="forwardedProps" :class="cn(
28
+ toggleGroupItemVariants({ size }),
29
+ !hasIndicator && 'hover:bg-accent data-[state=on]:bg-accent',
30
+ props.class
31
+ )">
32
+ <slot v-bind="slotProps" />
33
+ </ToggleGroupItem>
34
+ </template>