@brumaombra/ui-vintage 0.7.13 → 0.8.1

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 (194) hide show
  1. package/COMPONENTS.md +3643 -0
  2. package/README.md +50 -10
  3. package/package.json +30 -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/cookie-consent/CookieConsent.vue +82 -0
  28. package/src/components/cookie-consent/index.ts +3 -0
  29. package/src/components/cookie-consent/useCookieConsent.ts +55 -0
  30. package/src/components/dashboard-shell/DashboardShell.vue +3 -3
  31. package/src/components/empty-state-card/EmptyStateCard.vue +4 -4
  32. package/src/components/error-page/ErrorPage.vue +16 -14
  33. package/src/components/info-card/InfoCard.vue +1 -1
  34. package/src/components/landing/landing-navbar/LandingNavbar.vue +2 -1
  35. package/src/components/load-more-button/LoadMoreButton.vue +2 -2
  36. package/src/components/loading-state-card/LoadingStateCard.vue +4 -4
  37. package/src/components/message-dialog/MessageDialog.vue +4 -4
  38. package/src/components/message-toast/MessageToast.vue +222 -50
  39. package/src/components/message-toast/index.ts +2 -2
  40. package/src/components/message-toast/message-toast-state.ts +107 -81
  41. package/src/components/page-header/PageHeader.vue +1 -1
  42. package/src/components/progress-component/ProgressComponent.vue +4 -4
  43. package/src/components/single-value-card/SingleValueCard.vue +47 -11
  44. package/src/components/theme-selector/ThemeSelector.vue +20 -36
  45. package/src/components/theme-selector/index.ts +3 -1
  46. package/src/components/theme-selector/theme-transition.ts +86 -0
  47. package/src/components/ui/accordion/Accordion.vue +41 -26
  48. package/src/components/ui/alert-dialog/AlertDialogContent.vue +2 -2
  49. package/src/components/ui/alert-dialog/AlertDialogFooter.vue +1 -1
  50. package/src/components/ui/animated-number/AnimatedNumber.vue +102 -0
  51. package/src/components/ui/animated-number/index.ts +1 -0
  52. package/src/components/ui/avatar/Avatar.vue +66 -0
  53. package/src/components/ui/avatar/AvatarFallback.vue +31 -0
  54. package/src/components/ui/avatar/AvatarGroup.vue +75 -0
  55. package/src/components/ui/avatar/AvatarImage.vue +23 -0
  56. package/src/components/ui/avatar/index.ts +47 -0
  57. package/src/components/ui/badge/Badge.vue +13 -2
  58. package/src/components/ui/breadcrumb/Breadcrumb.vue +4 -1
  59. package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +7 -1
  60. package/src/components/ui/busy/Busy.vue +45 -20
  61. package/src/components/ui/button/Button.vue +20 -3
  62. package/src/components/ui/button/index.ts +12 -2
  63. package/src/components/ui/card/Card.vue +21 -8
  64. package/src/components/ui/card/CardText.vue +1 -1
  65. package/src/components/ui/checkbox/Checkbox.vue +48 -0
  66. package/src/components/ui/checkbox/index.ts +1 -0
  67. package/src/components/ui/collapsible/Collapsible.vue +22 -0
  68. package/src/components/ui/collapsible/CollapsibleContent.vue +22 -0
  69. package/src/components/ui/collapsible/CollapsibleTrigger.vue +19 -0
  70. package/src/components/ui/collapsible/index.ts +3 -0
  71. package/src/components/ui/combobox/Combobox.vue +24 -0
  72. package/src/components/ui/combobox/ComboboxAnchor.vue +39 -0
  73. package/src/components/ui/combobox/ComboboxEmpty.vue +21 -0
  74. package/src/components/ui/combobox/ComboboxGroup.vue +26 -0
  75. package/src/components/ui/combobox/ComboboxInput.vue +28 -0
  76. package/src/components/ui/combobox/ComboboxItem.vue +39 -0
  77. package/src/components/ui/combobox/ComboboxList.vue +43 -0
  78. package/src/components/ui/combobox/ComboboxSelect.vue +162 -0
  79. package/src/components/ui/combobox/ComboboxSeparator.vue +16 -0
  80. package/src/components/ui/combobox/ComboboxTrigger.vue +28 -0
  81. package/src/components/ui/combobox/index.ts +11 -0
  82. package/src/components/ui/combobox/types.ts +16 -0
  83. package/src/components/ui/command/CommandDialog.vue +18 -16
  84. package/src/components/ui/command/CommandGroup.vue +1 -1
  85. package/src/components/ui/command/CommandInput.vue +2 -2
  86. package/src/components/ui/command/CommandItem.vue +7 -5
  87. package/src/components/ui/data-table/DataTable.vue +230 -0
  88. package/src/components/ui/data-table/index.ts +20 -0
  89. package/src/components/ui/date-picker/DatePicker.vue +11 -15
  90. package/src/components/ui/date-time-picker/DateTimePicker.vue +2 -7
  91. package/src/components/ui/dialog/DialogContent.vue +9 -4
  92. package/src/components/ui/dialog/DialogOverlay.vue +1 -1
  93. package/src/components/ui/dialog/DialogScrollContent.vue +10 -5
  94. package/src/components/ui/dropdown-menu/DropdownMenu.vue +18 -0
  95. package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +34 -0
  96. package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -0
  97. package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +13 -0
  98. package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +34 -0
  99. package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +26 -0
  100. package/src/components/ui/dropdown-menu/DropdownMenuPortal.vue +13 -0
  101. package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +18 -0
  102. package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +32 -0
  103. package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +16 -0
  104. package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +15 -0
  105. package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -0
  106. package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +39 -0
  107. package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +36 -0
  108. package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +15 -0
  109. package/src/components/ui/dropdown-menu/classes.ts +24 -0
  110. package/src/components/ui/dropdown-menu/index.ts +15 -0
  111. package/src/components/ui/field/FieldDescription.vue +1 -1
  112. package/src/components/ui/field/FieldError.vue +4 -1
  113. package/src/components/ui/file-dropzone/FileDropzone.vue +250 -0
  114. package/src/components/ui/file-dropzone/index.ts +35 -0
  115. package/src/components/ui/hover-card/HoverCard.vue +21 -0
  116. package/src/components/ui/hover-card/HoverCardContent.vue +33 -0
  117. package/src/components/ui/hover-card/HoverCardTrigger.vue +13 -0
  118. package/src/components/ui/hover-card/index.ts +3 -0
  119. package/src/components/ui/input/Input.vue +5 -5
  120. package/src/components/ui/kbd/Kbd.vue +21 -0
  121. package/src/components/ui/kbd/KbdGroup.vue +72 -0
  122. package/src/components/ui/kbd/index.ts +2 -0
  123. package/src/components/ui/number-field/NumberField.vue +130 -0
  124. package/src/components/ui/number-field/index.ts +1 -0
  125. package/src/components/ui/pagination/Pagination.vue +27 -0
  126. package/src/components/ui/pagination/PaginationContent.vue +20 -0
  127. package/src/components/ui/pagination/PaginationEllipsis.vue +31 -0
  128. package/src/components/ui/pagination/PaginationFirst.vue +32 -0
  129. package/src/components/ui/pagination/PaginationItem.vue +41 -0
  130. package/src/components/ui/pagination/PaginationLast.vue +32 -0
  131. package/src/components/ui/pagination/PaginationNext.vue +32 -0
  132. package/src/components/ui/pagination/PaginationPrevious.vue +32 -0
  133. package/src/components/ui/pagination/SimplePagination.vue +49 -0
  134. package/src/components/ui/pagination/index.ts +9 -0
  135. package/src/components/ui/pin-input/PinInput.vue +48 -0
  136. package/src/components/ui/pin-input/PinInputInput.vue +49 -0
  137. package/src/components/ui/pin-input/context.ts +8 -0
  138. package/src/components/ui/pin-input/index.ts +2 -0
  139. package/src/components/ui/popover/PopoverContent.vue +1 -1
  140. package/src/components/ui/progress/Progress.vue +22 -7
  141. package/src/components/ui/radio-group/RadioGroup.vue +27 -0
  142. package/src/components/ui/radio-group/RadioGroupCard.vue +56 -0
  143. package/src/components/ui/radio-group/RadioGroupItem.vue +40 -0
  144. package/src/components/ui/radio-group/index.ts +3 -0
  145. package/src/components/ui/select/SelectContent.vue +1 -1
  146. package/src/components/ui/select/SelectItem.vue +2 -2
  147. package/src/components/ui/select/SelectItemContent.vue +1 -1
  148. package/src/components/ui/select/SelectTrigger.vue +6 -6
  149. package/src/components/ui/select/SelectValueContent.vue +1 -1
  150. package/src/components/ui/sheet/SheetContent.vue +5 -5
  151. package/src/components/ui/sheet/SheetOverlay.vue +1 -1
  152. package/src/components/ui/sidebar/Sidebar.vue +7 -5
  153. package/src/components/ui/sidebar/SidebarRail.vue +3 -1
  154. package/src/components/ui/sidebar/SidebarTrigger.vue +3 -1
  155. package/src/components/ui/skeleton/Skeleton.vue +4 -4
  156. package/src/components/ui/slider/Slider.vue +8 -6
  157. package/src/components/ui/spinner/Spinner.vue +47 -0
  158. package/src/components/ui/spinner/index.ts +1 -0
  159. package/src/components/ui/stepper/SimpleStepper.vue +56 -0
  160. package/src/components/ui/stepper/Stepper.vue +30 -0
  161. package/src/components/ui/stepper/StepperDescription.vue +20 -0
  162. package/src/components/ui/stepper/StepperIndicator.vue +41 -0
  163. package/src/components/ui/stepper/StepperItem.vue +21 -0
  164. package/src/components/ui/stepper/StepperSeparator.vue +34 -0
  165. package/src/components/ui/stepper/StepperTitle.vue +20 -0
  166. package/src/components/ui/stepper/StepperTrigger.vue +20 -0
  167. package/src/components/ui/stepper/index.ts +17 -0
  168. package/src/components/ui/switch/Switch.vue +8 -6
  169. package/src/components/ui/tabs/TabsContent.vue +1 -1
  170. package/src/components/ui/tabs/TabsList.vue +13 -5
  171. package/src/components/ui/tabs/TabsTrigger.vue +5 -3
  172. package/src/components/ui/tags-input/TagsInput.vue +49 -0
  173. package/src/components/ui/tags-input/TagsInputInput.vue +23 -0
  174. package/src/components/ui/tags-input/TagsInputItem.vue +35 -0
  175. package/src/components/ui/tags-input/TagsInputItemDelete.vue +31 -0
  176. package/src/components/ui/tags-input/TagsInputItemText.vue +21 -0
  177. package/src/components/ui/tags-input/index.ts +5 -0
  178. package/src/components/ui/textarea/Textarea.vue +4 -5
  179. package/src/components/ui/toggle-group/Toggle.vue +33 -0
  180. package/src/components/ui/toggle-group/ToggleGroup.vue +93 -0
  181. package/src/components/ui/toggle-group/ToggleGroupItem.vue +34 -0
  182. package/src/components/ui/toggle-group/index.ts +69 -0
  183. package/src/components/ui/tooltip/TooltipContent.vue +1 -1
  184. package/src/i18n/de.json +83 -0
  185. package/src/i18n/en.json +83 -0
  186. package/src/i18n/es.json +83 -0
  187. package/src/i18n/fr.json +83 -0
  188. package/src/i18n/it.json +83 -0
  189. package/src/i18n/ja.json +83 -0
  190. package/src/i18n/pt.json +83 -0
  191. package/src/i18n/ru.json +83 -0
  192. package/src/i18n/zh.json +83 -0
  193. package/src/lib/color-tokens.ts +14 -14
  194. package/src/styles.css +289 -5
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes, VNode } from 'vue';
3
+ import type { AvatarShape, AvatarSize } from '.';
4
+ import { Comment, Fragment, Text, useSlots } from 'vue';
5
+ import { cn } from '../../../lib/utils';
6
+ import { avatarShapeClasses, avatarSizeClasses, avatarTextClasses } from '.';
7
+
8
+ // Props
9
+ const props = withDefaults(defineProps<{
10
+ max?: number;
11
+ size?: AvatarSize;
12
+ shape?: AvatarShape;
13
+ class?: HTMLAttributes['class'];
14
+ }>(), {
15
+ size: 'md',
16
+ shape: 'circle',
17
+ });
18
+
19
+ const slots = useSlots();
20
+
21
+ // Flatten slot vnodes (v-for fragments) and drop comments / whitespace text
22
+ function flatten(nodes: VNode[]): VNode[] {
23
+ const result: VNode[] = [];
24
+ for (const node of nodes) {
25
+ if (node.type === Comment) continue;
26
+ if (node.type === Text && typeof node.children === 'string' && !node.children.trim()) continue;
27
+ if (node.type === Fragment && Array.isArray(node.children)) {
28
+ result.push(...flatten(node.children as VNode[]));
29
+ continue;
30
+ }
31
+ result.push(node);
32
+ }
33
+ return result;
34
+ }
35
+
36
+ // Children are read at render time so slot changes are always reflected
37
+ function getChildren() {
38
+ return flatten(slots.default?.() ?? []);
39
+ }
40
+
41
+ // Whether a max limit applies
42
+ function hasMax() {
43
+ return props.max !== undefined && props.max >= 0;
44
+ }
45
+
46
+ // Avatars that fit within max
47
+ function getVisible() {
48
+ const children = getChildren();
49
+ return hasMax() ? children.slice(0, props.max) : children;
50
+ }
51
+
52
+ // Number of avatars hidden behind the +N bubble
53
+ function getOverflow() {
54
+ return hasMax() ? Math.max(0, getChildren().length - (props.max ?? 0)) : 0;
55
+ }
56
+
57
+ // Renders a vnode as-is
58
+ const VNodeRenderer = (renderProps: { node: VNode }) => renderProps.node;
59
+ </script>
60
+
61
+ <template>
62
+ <div data-slot="avatar-group" :class="cn(
63
+ 'flex items-center -space-x-2 hover:-space-x-0.5',
64
+ '*:ring-2 *:ring-background *:transition-[margin,translate] *:duration-380 *:ease-spring *:hover:-translate-y-0.5',
65
+ props.class
66
+ )">
67
+ <!-- Visible avatars -->
68
+ <VNodeRenderer v-for="(node, index) in getVisible()" :key="node.key ?? index" :node="node" />
69
+
70
+ <!-- Overflow bubble -->
71
+ <span v-if="getOverflow() > 0" data-slot="avatar-group-overflow" :class="cn('relative inline-flex shrink-0 select-none items-center justify-center bg-surface font-bold text-muted-foreground', avatarSizeClasses[props.size], avatarShapeClasses[props.shape], avatarTextClasses[props.size])">
72
+ +{{ getOverflow() }}
73
+ </span>
74
+ </div>
75
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ import type { AvatarImageEmits, AvatarImageProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { AvatarImage, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<AvatarImageProps & {
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+
13
+ // Emits
14
+ const emits = defineEmits<AvatarImageEmits>();
15
+
16
+ const delegatedProps = reactiveOmit(props, 'class');
17
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
18
+ </script>
19
+
20
+ <template>
21
+ <!-- Reka reveals the image only once loaded, which plays the zoom-in entrance -->
22
+ <AvatarImage data-slot="avatar-image" v-bind="forwarded" :class="cn('aspect-square size-full object-cover animate-[uv-float-in_420ms_var(--ease-out-expo)_both]', props.class)" />
23
+ </template>
@@ -0,0 +1,47 @@
1
+ import type { InjectionKey, Ref } from 'vue';
2
+
3
+ export { default as Avatar } from './Avatar.vue';
4
+ export { default as AvatarFallback } from './AvatarFallback.vue';
5
+ export { default as AvatarGroup } from './AvatarGroup.vue';
6
+ export { default as AvatarImage } from './AvatarImage.vue';
7
+
8
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
9
+ export type AvatarShape = 'circle' | 'square';
10
+ export type AvatarStatus = 'online' | 'offline' | 'busy' | 'away';
11
+
12
+ // Box size per avatar size (24/32/40/48/64px)
13
+ export const avatarSizeClasses: Record<AvatarSize, string> = {
14
+ xs: 'size-6',
15
+ sm: 'size-8',
16
+ md: 'size-10',
17
+ lg: 'size-12',
18
+ xl: 'size-16',
19
+ };
20
+
21
+ // Initials text size per avatar size
22
+ export const avatarTextClasses: Record<AvatarSize, string> = {
23
+ xs: 'text-[10px]',
24
+ sm: 'text-xs',
25
+ md: 'text-sm',
26
+ lg: 'text-base',
27
+ xl: 'text-lg',
28
+ };
29
+
30
+ // Corner radius per avatar shape
31
+ export const avatarShapeClasses: Record<AvatarShape, string> = {
32
+ circle: 'rounded-full',
33
+ square: 'rounded',
34
+ };
35
+
36
+ // Context shared by an avatar with its image and fallback
37
+ export const avatarContextKey: InjectionKey<{ size: Ref<AvatarSize>; shape: Ref<AvatarShape> }> = Symbol('uv-avatar');
38
+
39
+ // Compute up to 2 initials from a full name
40
+ export function getInitials(name: string | null | undefined): string {
41
+ const words = (name ?? '').trim().split(/\s+/).filter(Boolean);
42
+ if (words.length === 0) return '';
43
+
44
+ // Single word: first two characters, otherwise first letter of the first and last word
45
+ if (words.length === 1) return Array.from(words[0]!).slice(0, 2).join('').toUpperCase();
46
+ return (Array.from(words[0]!)[0]! + Array.from(words[words.length - 1]!)[0]!).toUpperCase();
47
+ }
@@ -10,15 +10,26 @@ const props = withDefaults(defineProps<{
10
10
  text: string;
11
11
  icon?: HugeiconsIconDefinition | null;
12
12
  color?: ToneColor;
13
+ dot?: boolean;
14
+ pulse?: boolean;
13
15
  class?: HTMLAttributes['class'];
14
16
  }>(), {
15
17
  icon: null,
16
- color: 'gray'
18
+ color: 'gray',
19
+ dot: false,
20
+ pulse: false
17
21
  });
18
22
  </script>
19
23
 
20
24
  <template>
21
- <span data-slot="badge" :class="cn('inline-flex w-fit items-center justify-center gap-1.5 whitespace-nowrap rounded border px-2 py-0.5 text-xs font-semibold transition-colors duration-300 ease-in-out', getSurfaceToneClasses(props.color), props.class)">
25
+ <span data-slot="badge" :class="cn('inline-flex w-fit items-center justify-center gap-1.5 whitespace-nowrap rounded border px-2 py-0.5 text-xs font-semibold transition-colors duration-150', getSurfaceToneClasses(props.color), props.class)">
26
+ <!-- Status dot -->
27
+ <span v-if="props.dot || props.pulse" class="relative flex size-1.5 shrink-0">
28
+ <span v-if="props.pulse" class="absolute inset-0 animate-uv-ping-soft rounded-full bg-current" />
29
+ <span class="relative size-1.5 rounded-full bg-current" />
30
+ </span>
31
+
32
+ <!-- Icon -->
22
33
  <HugeiconsIcon v-if="props.icon" :icon="props.icon" class="size-3.5 shrink-0" :stroke-width="1.8" />
23
34
  {{ props.text }}
24
35
  </span>
@@ -1,14 +1,17 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes } from 'vue';
3
+ import { useI18n } from 'vue-i18n';
3
4
 
4
5
  // Props
5
6
  const props = defineProps<{
6
7
  class?: HTMLAttributes['class'];
7
8
  }>();
9
+
10
+ const { t } = useI18n();
8
11
  </script>
9
12
 
10
13
  <template>
11
- <nav aria-label="breadcrumb" data-slot="breadcrumb" :class="props.class">
14
+ <nav :aria-label="t('uiVintage.common.breadcrumb')" data-slot="breadcrumb" :class="props.class">
12
15
  <slot />
13
16
  </nav>
14
17
  </template>
@@ -2,19 +2,25 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import { MoreHorizontalIcon } 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
 
7
8
  // Props
8
9
  const props = defineProps<{
9
10
  class?: HTMLAttributes['class'];
10
11
  }>();
12
+
13
+ const { t } = useI18n();
11
14
  </script>
12
15
 
13
16
  <template>
14
17
  <span data-slot="breadcrumb-ellipsis" role="presentation" aria-hidden="true" :class="cn('flex size-9 items-center justify-center', props.class)">
18
+ <!-- Default slot content -->
15
19
  <slot>
16
20
  <HugeiconsIcon :icon="MoreHorizontalIcon" class="size-4" />
17
21
  </slot>
18
- <span class="sr-only">More</span>
22
+
23
+ <!-- Screen reader text for accessibility -->
24
+ <span class="sr-only">{{ t('uiVintage.common.more') }}</span>
19
25
  </span>
20
26
  </template>
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
- import type { HTMLAttributes } from 'vue';
3
- import { Loading03Icon } from '@hugeicons/core-free-icons';
4
- import { HugeiconsIcon } from '@hugeicons/vue';
2
+ import { computed, type HTMLAttributes } from 'vue';
3
+ import { getUiVintageRuntimeMessage } from '../../../lib/i18n';
5
4
  import { cn } from '../../../lib/utils';
5
+ import { Spinner } from '../spinner';
6
6
 
7
7
  // Props
8
8
  const props = withDefaults(defineProps<{
@@ -11,27 +11,32 @@ const props = withDefaults(defineProps<{
11
11
  class?: HTMLAttributes['class'];
12
12
  overlayClass?: HTMLAttributes['class'];
13
13
  }>(), {
14
- show: false,
15
- label: 'Loading...'
14
+ show: false
16
15
  });
16
+
17
+ const resolvedLabel = computed(() => props.label || getUiVintageRuntimeMessage('uiVintage.common.loading.title', 'Loading...'));
17
18
  </script>
18
19
 
19
20
  <template>
20
- <Transition name="busy-fade">
21
+ <Transition name="uv-busy">
21
22
  <div v-if="props.show" data-slot="busy-overlay" :class="cn(
22
- 'fixed inset-0 z-50 flex items-center justify-center bg-black/45 backdrop-blur-[2px] transition-[opacity,backdrop-filter] duration-300 ease-out',
23
+ 'fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-[3px] dark:bg-black/60',
23
24
  props.overlayClass,
24
- )
25
- ">
25
+ )">
26
26
  <div data-slot="busy-content" role="status" aria-live="polite" aria-busy="true" :class="cn(
27
- 'bg-card text-card-foreground flex w-72 flex-col items-center justify-center gap-4 rounded border border-border px-8 py-7 text-center shadow-2xl',
27
+ 'uv-busy-card bg-card text-card-foreground flex w-72 flex-col items-center justify-center gap-4 rounded border border-border px-8 py-7 text-center shadow-elevated-xl',
28
28
  props.class,
29
- )
30
- ">
29
+ )">
31
30
  <slot>
32
- <HugeiconsIcon data-slot="busy-spinner" :icon="Loading03Icon" class="size-10 animate-spin text-primary" />
33
- <span v-if="props.label" data-slot="busy-label" class="text-xs font-semibold text-foreground sm:text-sm">
34
- {{ props.label }}
31
+ <!-- Spinner with a soft glow -->
32
+ <div class="relative flex items-center justify-center">
33
+ <span class="absolute size-14 rounded-full bg-primary/15 blur-md" />
34
+ <Spinner data-slot="busy-spinner" size="xl" class="text-primary" />
35
+ </div>
36
+
37
+ <!-- Label -->
38
+ <span v-if="resolvedLabel" data-slot="busy-label" class="text-xs font-semibold text-foreground sm:text-sm">
39
+ {{ resolvedLabel }}
35
40
  </span>
36
41
  </slot>
37
42
  </div>
@@ -40,13 +45,33 @@ const props = withDefaults(defineProps<{
40
45
  </template>
41
46
 
42
47
  <style scoped>
43
- .busy-fade-enter-active,
44
- .busy-fade-leave-active {
45
- transition: opacity 0.3s ease-out;
48
+ .uv-busy-enter-active {
49
+ transition: opacity 0.25s var(--ease-out-expo);
50
+ }
51
+
52
+ .uv-busy-leave-active {
53
+ transition: opacity 0.18s var(--ease-snappy);
46
54
  }
47
55
 
48
- .busy-fade-enter-from,
49
- .busy-fade-leave-to {
56
+ .uv-busy-enter-from,
57
+ .uv-busy-leave-to {
50
58
  opacity: 0;
51
59
  }
60
+
61
+ .uv-busy-enter-active .uv-busy-card {
62
+ transition: transform 0.45s var(--ease-spring), filter 0.3s var(--ease-out-expo);
63
+ }
64
+
65
+ .uv-busy-leave-active .uv-busy-card {
66
+ transition: transform 0.18s var(--ease-snappy);
67
+ }
68
+
69
+ .uv-busy-enter-from .uv-busy-card {
70
+ transform: translateY(10px) scale(0.92);
71
+ filter: blur(4px);
72
+ }
73
+
74
+ .uv-busy-leave-to .uv-busy-card {
75
+ transform: scale(0.96);
76
+ }
52
77
  </style>
@@ -4,20 +4,37 @@ import type { HTMLAttributes } from 'vue';
4
4
  import type { ButtonVariants } from '.';
5
5
  import { Primitive } from 'reka-ui';
6
6
  import { cn } from '../../../lib/utils';
7
+ import { Spinner } from '../spinner';
7
8
  import { buttonVariants } from '.';
8
9
 
9
10
  // Props
10
11
  const props = withDefaults(defineProps<PrimitiveProps & {
11
12
  variant?: ButtonVariants['variant'];
12
13
  size?: ButtonVariants['size'];
14
+ loading?: boolean;
13
15
  class?: HTMLAttributes['class'];
14
16
  }>(), {
15
- as: 'button'
17
+ as: 'button',
18
+ loading: false
16
19
  });
17
20
  </script>
18
21
 
19
22
  <template>
20
- <Primitive data-slot="button" :data-variant="variant" :data-size="size" :as="as" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)">
21
- <slot />
23
+ <Primitive data-slot="button" :data-variant="variant" :data-size="size" :data-loading="props.loading ? '' : undefined" :aria-busy="props.loading || undefined" :disabled="props.loading || undefined" :as="as" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)">
24
+ <!-- As-child buttons delegate rendering to the slotted element -->
25
+ <slot v-if="asChild" />
26
+
27
+ <template v-else>
28
+ <!-- Content (kept in the layout while loading so the width never jumps) -->
29
+ <span v-if="props.loading" class="invisible contents">
30
+ <slot />
31
+ </span>
32
+ <slot v-else />
33
+
34
+ <!-- Loading indicator -->
35
+ <span v-if="props.loading" class="absolute inset-0 flex items-center justify-center">
36
+ <Spinner size="sm" />
37
+ </span>
38
+ </template>
22
39
  </Primitive>
23
40
  </template>
@@ -28,12 +28,22 @@ export type ButtonVariants = {
28
28
  | undefined;
29
29
  };
30
30
 
31
+ // Sheen sweep that crosses solid buttons on hover
32
+ const sheenClasses = "before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:-translate-x-full before:bg-[linear-gradient(105deg,transparent_35%,rgb(255_255_255/0.38)_50%,transparent_65%)] before:transition-transform before:duration-700 before:ease-out-expo hover:before:translate-x-full";
33
+
31
34
  export const buttonVariants: (props?: ButtonVariants & ClassProp) => string = cva(
32
- "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded text-xs font-semibold transition-all duration-300 ease-in-out disabled:pointer-events-none disabled:opacity-50 enabled:active:scale-[0.98] sm:text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
35
+ [
36
+ "relative isolate inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 overflow-hidden whitespace-nowrap rounded text-xs font-semibold outline-none sm:text-sm",
37
+ "[transition:color_150ms,background-color_150ms,border-color_150ms,box-shadow_220ms,opacity_150ms,scale_380ms_var(--ease-spring)]",
38
+ "enabled:active:scale-[0.97] enabled:active:duration-75 disabled:pointer-events-none disabled:opacity-50 data-loading:cursor-wait data-loading:disabled:opacity-100",
39
+ "focus-visible:ring-[3px] focus-visible:ring-ring/45",
40
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
41
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
42
+ ],
33
43
  {
34
44
  variants: {
35
45
  variant: {
36
- primary: getButtonVariantClasses('primary'),
46
+ primary: `${getButtonVariantClasses('primary')} ${sheenClasses}`,
37
47
  secondary: getButtonVariantClasses('secondary'),
38
48
  ghost: getButtonVariantClasses('ghost'),
39
49
  link: getButtonVariantClasses('link'),
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue';
2
+ import { computed, ref } from 'vue';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import {
5
5
  getSurfaceToneClasses,
@@ -10,30 +10,43 @@ import { cn } from '../../../lib/utils';
10
10
  type CardColor = 'default' | ToneColor;
11
11
 
12
12
  // Props
13
- const props = defineProps<{
13
+ const props = withDefaults(defineProps<{
14
14
  color?: CardColor;
15
+ interactive?: boolean;
15
16
  class?: HTMLAttributes['class'];
16
- }>();
17
+ }>(), {
18
+ interactive: false
19
+ });
20
+
21
+ const cardRef = ref<HTMLElement | null>(null);
17
22
 
18
23
  // Get the appropriate classes
19
24
  const colorClasses = computed(() => {
20
25
  // If color is not specified or is "default", use the default card styles
21
26
  if (!props.color || props.color === 'default') {
22
- return 'border border-border bg-card text-card-foreground';
27
+ return 'border border-border bg-card text-card-foreground shadow-elevated-sm';
23
28
  }
24
29
 
25
30
  // For other colors, use the corresponding surface tone classes
26
31
  return getSurfaceToneClasses(props.color);
27
32
  });
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
+ };
28
41
  </script>
29
42
 
30
43
  <template>
31
- <div data-slot="card" :class="cn(
32
- 'flex flex-col gap-4 sm:gap-6! rounded py-6 transition-colors duration-300 ease-in-out',
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',
33
46
  colorClasses,
47
+ props.interactive && 'cursor-pointer hover:-translate-y-0.5 hover:border-border-strong 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',
34
48
  props.class,
35
- )
36
- ">
49
+ )" @pointermove="handlePointerMove">
37
50
  <slot />
38
51
  </div>
39
52
  </template>
@@ -10,7 +10,7 @@ const props = defineProps<{
10
10
 
11
11
  <template>
12
12
  <p data-slot="card-text" :class="cn(
13
- 'text-xs sm:text-sm text-(--text-secondary-light) dark:text-(--text-secondary-dark)',
13
+ 'text-xs sm:text-sm text-muted-foreground',
14
14
  props.class,
15
15
  )
16
16
  ">
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<CheckboxRootProps & {
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+
13
+ // Emits
14
+ const emits = defineEmits<CheckboxRootEmits>();
15
+
16
+ const delegatedProps = reactiveOmit(props, 'class');
17
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
18
+ </script>
19
+
20
+ <template>
21
+ <CheckboxRoot v-slot="slotProps" data-slot="checkbox" v-bind="forwarded" :class="cn(
22
+ '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
+ '[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
+ '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]:bg-primary data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-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 mark can draw in and out) -->
30
+ <CheckboxIndicator force-mount data-slot="checkbox-indicator" class="pointer-events-none flex size-full items-center justify-center">
31
+ <slot v-bind="slotProps">
32
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5" aria-hidden="true">
33
+ <!-- Check mark: draws itself via stroke-dashoffset -->
34
+ <path d="M5 12.5l4.5 4.5L19 7.5" pathLength="1" :class="cn(
35
+ 'opacity-0 [stroke-dasharray:1_2] [stroke-dashoffset:1] [transition:stroke-dashoffset_120ms_var(--ease-snappy),opacity_90ms_var(--ease-snappy)_30ms]',
36
+ 'group-data-[state=checked]/checkbox:opacity-100 group-data-[state=checked]/checkbox:[stroke-dashoffset:0] group-data-[state=checked]/checkbox:[transition:stroke-dashoffset_380ms_var(--ease-out-expo)_40ms,opacity_60ms_linear_40ms]'
37
+ )" />
38
+
39
+ <!-- Indeterminate dash: sweeps in from the left -->
40
+ <path d="M6.5 12h11" pathLength="1" :class="cn(
41
+ 'opacity-0 [stroke-dasharray:1_2] [stroke-dashoffset:1] [transition:stroke-dashoffset_120ms_var(--ease-snappy),opacity_90ms_var(--ease-snappy)_30ms]',
42
+ 'group-data-[state=indeterminate]/checkbox:opacity-100 group-data-[state=indeterminate]/checkbox:[stroke-dashoffset:0] group-data-[state=indeterminate]/checkbox:[transition:stroke-dashoffset_320ms_var(--ease-out-expo)_40ms,opacity_60ms_linear_40ms]'
43
+ )" />
44
+ </svg>
45
+ </slot>
46
+ </CheckboxIndicator>
47
+ </CheckboxRoot>
48
+ </template>
@@ -0,0 +1 @@
1
+ export { default as Checkbox } from './Checkbox.vue';
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { CollapsibleRootEmits, CollapsibleRootProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { CollapsibleRoot, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<CollapsibleRootProps & { class?: HTMLAttributes['class'] }>();
10
+
11
+ // Emits
12
+ const emits = defineEmits<CollapsibleRootEmits>();
13
+
14
+ const delegatedProps = reactiveOmit(props, 'class');
15
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
16
+ </script>
17
+
18
+ <template>
19
+ <CollapsibleRoot v-slot="slotProps" data-slot="collapsible" v-bind="forwarded" :class="cn(props.class)">
20
+ <slot v-bind="slotProps" />
21
+ </CollapsibleRoot>
22
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { CollapsibleContentEmits, CollapsibleContentProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { CollapsibleContent, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<CollapsibleContentProps & { class?: HTMLAttributes['class'] }>();
10
+
11
+ // Emits
12
+ const emits = defineEmits<CollapsibleContentEmits>();
13
+
14
+ const delegatedProps = reactiveOmit(props, 'class');
15
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
16
+ </script>
17
+
18
+ <template>
19
+ <CollapsibleContent data-slot="collapsible-content" v-bind="forwarded" :class="cn('uv-collapsible-motion', props.class)">
20
+ <slot />
21
+ </CollapsibleContent>
22
+ </template>
@@ -0,0 +1,19 @@
1
+ <script setup lang="ts">
2
+ import type { CollapsibleTriggerProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { CollapsibleTrigger, useForwardProps } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<CollapsibleTriggerProps & { class?: HTMLAttributes['class'] }>();
10
+
11
+ const delegatedProps = reactiveOmit(props, 'class');
12
+ const forwardedProps = useForwardProps(delegatedProps);
13
+ </script>
14
+
15
+ <template>
16
+ <CollapsibleTrigger data-slot="collapsible-trigger" v-bind="forwardedProps" :class="cn(props.class)">
17
+ <slot />
18
+ </CollapsibleTrigger>
19
+ </template>
@@ -0,0 +1,3 @@
1
+ export { default as Collapsible } from './Collapsible.vue';
2
+ export { default as CollapsibleContent } from './CollapsibleContent.vue';
3
+ export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue';
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ import type { ComboboxRootEmits, ComboboxRootProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { ComboboxRoot, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
10
+ openOnClick: true
11
+ });
12
+
13
+ // Emits
14
+ const emits = defineEmits<ComboboxRootEmits>();
15
+
16
+ const delegatedProps = reactiveOmit(props, 'class');
17
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
18
+ </script>
19
+
20
+ <template>
21
+ <ComboboxRoot v-slot="slotProps" data-slot="combobox" v-bind="forwarded" :class="cn('relative', props.class)">
22
+ <slot v-bind="slotProps" />
23
+ </ComboboxRoot>
24
+ </template>
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import type { ComboboxAnchorProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { ComboboxAnchor, injectComboboxRootContext, useForwardProps } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = withDefaults(defineProps<ComboboxAnchorProps & {
10
+ class?: HTMLAttributes['class'];
11
+ size?: 'sm' | 'default';
12
+ }>(), { size: 'default' });
13
+
14
+ const delegatedProps = reactiveOmit(props, 'class', 'size');
15
+ const forwardedProps = useForwardProps(delegatedProps);
16
+
17
+ const rootContext = injectComboboxRootContext();
18
+
19
+ // Clicking the empty area of the field focuses the search input and opens the list
20
+ const handleClick = (event: MouseEvent) => {
21
+ if (rootContext.disabled.value) return;
22
+ const target = event.target as HTMLElement | null;
23
+ if (target?.closest('button, input, a')) return;
24
+ rootContext.inputElement.value?.focus();
25
+ if (!rootContext.open.value) rootContext.onOpenChange(true);
26
+ };
27
+ </script>
28
+
29
+ <template>
30
+ <ComboboxAnchor data-slot="combobox-anchor" :data-size="props.size" :data-state="rootContext.open.value ? 'open' : 'closed'" :data-disabled="rootContext.disabled.value ? '' : undefined" v-bind="forwardedProps" :class="cn(
31
+ 'group/combobox-anchor uv-field flex w-full cursor-text items-center gap-2 rounded border border-input bg-secondary px-4 py-1.5 text-xs font-semibold text-foreground shadow-elevated-sm sm:text-sm',
32
+ 'data-[size=default]:min-h-13 data-[size=sm]:min-h-10 data-disabled:cursor-not-allowed data-disabled:opacity-60',
33
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 [&_svg:not([class*=\'text-\'])]:text-muted-foreground',
34
+ props.class
35
+ )
36
+ " @click="handleClick">
37
+ <slot />
38
+ </ComboboxAnchor>
39
+ </template>