@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,230 @@
1
+ <script setup lang="ts" generic="T extends Record<string, any>">
2
+ import type { HTMLAttributes } from 'vue';
3
+ import { computed } from 'vue';
4
+ import { useI18n } from 'vue-i18n';
5
+ import { ArrowUp01Icon, PackageIcon } from '@hugeicons/core-free-icons';
6
+ import { HugeiconsIcon } from '@hugeicons/vue';
7
+ import { cn } from '../../../lib/utils';
8
+ import { Checkbox } from '../checkbox';
9
+ import { Skeleton } from '../skeleton';
10
+ import type { DataTableColumn, DataTableSort } from '.';
11
+
12
+ // Props
13
+ const props = withDefaults(defineProps<{
14
+ columns: DataTableColumn<T>[];
15
+ rows: T[];
16
+ rowKey?: keyof T | ((row: T) => PropertyKey);
17
+ selectable?: boolean;
18
+ selected?: PropertyKey[];
19
+ sort?: DataTableSort | null;
20
+ manualSort?: boolean;
21
+ loading?: boolean;
22
+ loadingRows?: number;
23
+ emptyText?: string;
24
+ rowClickable?: boolean;
25
+ stickyHeader?: boolean;
26
+ class?: HTMLAttributes['class'];
27
+ }>(), {
28
+ rowKey: 'id' as never,
29
+ selectable: false,
30
+ selected: () => [],
31
+ sort: null,
32
+ manualSort: false,
33
+ loading: false,
34
+ loadingRows: 5,
35
+ emptyText: '',
36
+ rowClickable: false,
37
+ stickyHeader: false
38
+ });
39
+
40
+ // Emits
41
+ const emits = defineEmits<{
42
+ 'update:selected': [value: PropertyKey[]];
43
+ 'update:sort': [value: DataTableSort | null];
44
+ 'row-click': [row: T, event: MouseEvent];
45
+ }>();
46
+
47
+ // Slots
48
+ defineSlots<{
49
+ [key: `cell-${string}`]: (props: { row: T; value: unknown; index: number }) => unknown;
50
+ [key: `header-${string}`]: (props: { column: DataTableColumn<T> }) => unknown;
51
+ empty?: () => unknown;
52
+ }>();
53
+
54
+ const { t } = useI18n();
55
+ const resolvedEmptyText = computed(() => props.emptyText || t('uiVintage.common.empty.description'));
56
+
57
+ // Resolve a stable key for each row
58
+ const getRowKey = (row: T): PropertyKey => {
59
+ return typeof props.rowKey === 'function' ? props.rowKey(row) : row[props.rowKey as keyof T] as PropertyKey;
60
+ };
61
+
62
+ // Read the raw value of a cell
63
+ const getCellValue = (row: T, column: DataTableColumn<T>) => row[column.key];
64
+
65
+ // Format a cell for display when no slot is provided
66
+ const formatCell = (row: T, column: DataTableColumn<T>) => {
67
+ const value = getCellValue(row, column);
68
+ return column.format ? column.format(value, row) : value ?? '';
69
+ };
70
+
71
+ // Sort rows on the client unless sorting is delegated to the parent
72
+ const sortedRows = computed(() => {
73
+ if (!props.sort || props.manualSort) return props.rows;
74
+ const column = props.columns.find(item => item.key === props.sort?.key);
75
+ if (!column) return props.rows;
76
+ const direction = props.sort.direction === 'asc' ? 1 : -1;
77
+ const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
78
+
79
+ // Compare values with the column accessor (strings use natural ordering)
80
+ return [...props.rows].sort((first, second) => {
81
+ const firstValue = column.sortValue ? column.sortValue(first) : getCellValue(first, column);
82
+ const secondValue = column.sortValue ? column.sortValue(second) : getCellValue(second, column);
83
+ if (firstValue == null) return 1;
84
+ if (secondValue == null) return -1;
85
+ if (typeof firstValue === 'number' && typeof secondValue === 'number') return (firstValue - secondValue) * direction;
86
+ return collator.compare(String(firstValue), String(secondValue)) * direction;
87
+ });
88
+ });
89
+
90
+ // Cycle a column through ascending, descending, and unsorted
91
+ const toggleSort = (column: DataTableColumn<T>) => {
92
+ if (!column.sortable) return;
93
+ if (props.sort?.key !== column.key) {
94
+ emits('update:sort', { key: column.key, direction: 'asc' });
95
+ } else if (props.sort.direction === 'asc') {
96
+ emits('update:sort', { key: column.key, direction: 'desc' });
97
+ } else {
98
+ emits('update:sort', null);
99
+ }
100
+ };
101
+
102
+ // Selection helpers
103
+ const selectedSet = computed(() => new Set(props.selected));
104
+ const allSelected = computed(() => props.rows.length > 0 && props.rows.every(row => selectedSet.value.has(getRowKey(row))));
105
+ const someSelected = computed(() => props.rows.some(row => selectedSet.value.has(getRowKey(row))));
106
+ const headerCheckboxState = computed(() => allSelected.value ? true : someSelected.value ? 'indeterminate' : false);
107
+
108
+ // Toggle all rows
109
+ const toggleAll = () => {
110
+ emits('update:selected', allSelected.value ? [] : props.rows.map(getRowKey));
111
+ };
112
+
113
+ // Toggle a single row
114
+ const toggleRow = (row: T) => {
115
+ const key = getRowKey(row);
116
+ const next = new Set(props.selected);
117
+ if (next.has(key)) {
118
+ next.delete(key);
119
+ } else {
120
+ next.add(key);
121
+ }
122
+ emits('update:selected', [...next]);
123
+ };
124
+
125
+ // Alignment classes for header and body cells
126
+ const alignClasses = { left: 'text-left', center: 'text-center', right: 'text-right' } as const;
127
+ const justifyClasses = { left: 'justify-start', center: 'justify-center', right: 'justify-end' } as const;
128
+ const columnCount = computed(() => props.columns.length + (props.selectable ? 1 : 0));
129
+
130
+ // Emit row clicks (ignoring clicks on interactive children)
131
+ const handleRowClick = (row: T, event: MouseEvent) => {
132
+ if ((event.target as HTMLElement).closest('button, a, input, [role=checkbox]')) return;
133
+ emits('row-click', row, event);
134
+ };
135
+ </script>
136
+
137
+ <template>
138
+ <div data-slot="data-table" :class="cn('relative w-full overflow-auto rounded border border-border bg-card shadow-elevated-sm', props.class)">
139
+ <table class="w-full caption-bottom border-collapse text-sm">
140
+ <!-- Header -->
141
+ <thead :class="cn('bg-surface/80 backdrop-blur', props.stickyHeader && 'sticky top-0 z-10')">
142
+ <tr class="border-b border-border">
143
+ <!-- Select all -->
144
+ <th v-if="props.selectable" class="w-12 px-4 py-3">
145
+ <Checkbox :model-value="headerCheckboxState" :aria-label="t('uiVintage.dataTable.selectAll')" @update:model-value="toggleAll" />
146
+ </th>
147
+
148
+ <!-- Column headers -->
149
+ <th v-for="column in props.columns" :key="column.key" :style="column.width ? { width: column.width } : undefined" :aria-sort="props.sort?.key === column.key ? (props.sort.direction === 'asc' ? 'ascending' : 'descending') : undefined" :class="cn('h-11 px-4 text-xs font-semibold whitespace-nowrap text-muted-foreground', alignClasses[column.align ?? 'left'], column.headerClass)">
150
+ <slot :name="`header-${column.key}`" :column="column">
151
+ <button v-if="column.sortable" type="button" :class="cn('group/sort -mx-1.5 inline-flex cursor-pointer items-center gap-1.5 rounded-sm px-1.5 py-1 outline-none transition-colors duration-150 hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/45', props.sort?.key === column.key && 'text-foreground', justifyClasses[column.align ?? 'left'])" @click="toggleSort(column)">
152
+ {{ column.label }}
153
+ <HugeiconsIcon :icon="ArrowUp01Icon" :class="cn('size-3.5 transition-[rotate,opacity,color] duration-300 ease-spring', props.sort?.key === column.key ? 'text-primary opacity-100' : 'opacity-0 group-hover/sort:opacity-50', props.sort?.key === column.key && props.sort.direction === 'desc' && 'rotate-180')" />
154
+ </button>
155
+ <template v-else>
156
+ {{ column.label }}
157
+ </template>
158
+ </slot>
159
+ </th>
160
+ </tr>
161
+ </thead>
162
+
163
+ <!-- Loading rows -->
164
+ <tbody v-if="props.loading">
165
+ <tr v-for="index in props.loadingRows" :key="`loading-${index}`" class="border-b border-border last:border-b-0">
166
+ <td v-if="props.selectable" class="px-4 py-3">
167
+ <Skeleton class="size-5" />
168
+ </td>
169
+ <td v-for="column in props.columns" :key="column.key" class="px-4 py-3">
170
+ <Skeleton class="h-4" :style="{ width: `${45 + ((index * 17 + column.key.length * 11) % 45)}%` }" />
171
+ </td>
172
+ </tr>
173
+ </tbody>
174
+
175
+ <!-- Empty state -->
176
+ <tbody v-else-if="sortedRows.length === 0">
177
+ <tr>
178
+ <td :colspan="columnCount" class="px-4 py-12">
179
+ <slot name="empty">
180
+ <div class="flex flex-col items-center gap-2 text-center text-muted-foreground">
181
+ <HugeiconsIcon :icon="PackageIcon" class="size-8 animate-uv-float opacity-40" />
182
+ <span class="text-xs sm:text-sm">{{ resolvedEmptyText }}</span>
183
+ </div>
184
+ </slot>
185
+ </td>
186
+ </tr>
187
+ </tbody>
188
+
189
+ <!-- Rows (animated when sorted, added, or removed) -->
190
+ <TransitionGroup v-else tag="tbody" name="uv-data-table-row">
191
+ <tr v-for="(row, index) in sortedRows" :key="getRowKey(row)" :data-state="selectedSet.has(getRowKey(row)) ? 'selected' : undefined" :class="cn('border-b border-border transition-colors duration-150 last:border-b-0 hover:bg-surface/70 data-[state=selected]:bg-primary/5', props.rowClickable && 'cursor-pointer')" @click="handleRowClick(row, $event)">
192
+ <!-- Row selection -->
193
+ <td v-if="props.selectable" class="px-4 py-3">
194
+ <Checkbox :model-value="selectedSet.has(getRowKey(row))" :aria-label="t('uiVintage.dataTable.selectRow')" @update:model-value="toggleRow(row)" />
195
+ </td>
196
+
197
+ <!-- Cells -->
198
+ <td v-for="column in props.columns" :key="column.key" :class="cn('px-4 py-3 align-middle text-xs whitespace-nowrap md:text-sm', alignClasses[column.align ?? 'left'], column.class)">
199
+ <slot :name="`cell-${column.key}`" :row="row" :value="getCellValue(row, column)" :index="index">
200
+ {{ formatCell(row, column) }}
201
+ </slot>
202
+ </td>
203
+ </tr>
204
+ </TransitionGroup>
205
+ </table>
206
+ </div>
207
+ </template>
208
+
209
+ <style>
210
+ .uv-data-table-row-move {
211
+ transition: transform 0.45s var(--ease-spring);
212
+ }
213
+
214
+ .uv-data-table-row-enter-active {
215
+ transition: opacity 0.3s var(--ease-out-expo), transform 0.4s var(--ease-spring);
216
+ }
217
+
218
+ .uv-data-table-row-leave-active {
219
+ transition: opacity 0.18s var(--ease-snappy);
220
+ }
221
+
222
+ .uv-data-table-row-enter-from {
223
+ opacity: 0;
224
+ transform: translateY(-6px);
225
+ }
226
+
227
+ .uv-data-table-row-leave-to {
228
+ opacity: 0;
229
+ }
230
+ </style>
@@ -0,0 +1,20 @@
1
+ export { default as DataTable } from './DataTable.vue';
2
+
3
+ export type DataTableSortDirection = 'asc' | 'desc';
4
+
5
+ export interface DataTableSort {
6
+ key: string;
7
+ direction: DataTableSortDirection;
8
+ }
9
+
10
+ export interface DataTableColumn<T> {
11
+ key: string;
12
+ label: string;
13
+ sortable?: boolean;
14
+ align?: 'left' | 'center' | 'right';
15
+ width?: string;
16
+ class?: string;
17
+ headerClass?: string;
18
+ format?: (value: unknown, row: T) => string;
19
+ sortValue?: (row: T) => string | number | null | undefined;
20
+ }
@@ -5,6 +5,7 @@ import { Calendar03Icon } from '@hugeicons/core-free-icons';
5
5
  import { HugeiconsIcon } from '@hugeicons/vue';
6
6
  import type { DateValue } from 'reka-ui';
7
7
  import { computed, ref } from 'vue';
8
+ import { useI18n } from 'vue-i18n';
8
9
  import { cn } from '../../../lib/utils';
9
10
  import { Button } from '../button';
10
11
  import { Calendar } from '../calendar';
@@ -21,7 +22,6 @@ const props = withDefaults(defineProps<{
21
22
  class?: HTMLAttributes['class'];
22
23
  contentClass?: HTMLAttributes['class'];
23
24
  }>(), {
24
- placeholder: 'Pick a date',
25
25
  disabled: false
26
26
  });
27
27
 
@@ -30,6 +30,8 @@ const emits = defineEmits<{
30
30
  (e: 'update:modelValue', payload: DateValue | undefined): void;
31
31
  }>();
32
32
 
33
+ const { t, locale } = useI18n();
34
+ const resolvedPlaceholder = computed(() => props.placeholder || t('uiVintage.datePicker.placeholder'));
33
35
  const internalDate = ref<DateValue | undefined>(props.defaultValue);
34
36
  const isControlled = computed(() => props.modelValue !== undefined);
35
37
 
@@ -38,7 +40,6 @@ const date = computed({
38
40
  (isControlled.value ? props.modelValue : internalDate.value) as any,
39
41
  set: (value: DateValue | undefined) => {
40
42
  if (!isControlled.value) internalDate.value = value;
41
-
42
43
  emits('update:modelValue', value);
43
44
  },
44
45
  });
@@ -47,28 +48,23 @@ const defaultPlaceholder = computed(
47
48
  () => (date.value ?? today(getLocalTimeZone())) as any,
48
49
  );
49
50
 
50
- const formatter = new DateFormatter('en-US', {
51
+ // Format the selected date in the active locale
52
+ const formatter = computed(() => new DateFormatter(locale.value || 'en-US', {
51
53
  dateStyle: 'long',
52
- });
54
+ }));
53
55
  </script>
54
56
 
55
57
  <template>
56
58
  <Popover v-slot="{ close }">
59
+ <!-- Date picker trigger -->
57
60
  <PopoverTrigger as-child>
58
- <Button variant="secondary" :disabled="disabled" :class="cn(
59
- 'h-13 w-60 justify-start text-left font-normal enabled:active:scale-100',
60
- !date && 'text-muted-foreground',
61
- props.class,
62
- )
63
- ">
61
+ <Button variant="secondary" :disabled="disabled" :class="cn('h-13 w-60 justify-start text-left font-normal enabled:active:scale-100', !date && 'text-muted-foreground', props.class,)">
64
62
  <HugeiconsIcon :icon="Calendar03Icon" class="size-4" />
65
- {{
66
- date
67
- ? formatter.format(date.toDate(getLocalTimeZone()))
68
- : placeholder
69
- }}
63
+ {{ date ? formatter.format(date.toDate(getLocalTimeZone())) : resolvedPlaceholder }}
70
64
  </Button>
71
65
  </PopoverTrigger>
66
+
67
+ <!-- Date picker content -->
72
68
  <PopoverContent :class="cn('w-auto p-0', props.contentClass)" align="start">
73
69
  <Calendar v-model="date" :default-placeholder="defaultPlaceholder" :min-value="props.minValue" :max-value="props.maxValue" layout="month-and-year" initial-focus @update:model-value="close" />
74
70
  </PopoverContent>
@@ -25,8 +25,7 @@ const props = withDefaults(defineProps<{
25
25
  minDate: null,
26
26
  maxDate: null,
27
27
  step: 60,
28
- disabled: false,
29
- datePlaceholder: 'Pick a date'
28
+ disabled: false
30
29
  });
31
30
 
32
31
  // Emits
@@ -158,11 +157,7 @@ const maxTimeValue = computed(() => {
158
157
  </script>
159
158
 
160
159
  <template>
161
- <div :class="cn(
162
- 'grid grid-cols-1 gap-3 md:grid-cols-[minmax(0,1.35fr)_minmax(0,1fr)]',
163
- props.class,
164
- )
165
- ">
160
+ <div :class="cn('grid grid-cols-1 gap-3 md:grid-cols-[minmax(0,1.35fr)_minmax(0,1fr)]', props.class,)">
166
161
  <div>
167
162
  <DatePicker v-model="dateValue" :disabled="props.disabled" :placeholder="props.datePlaceholder" :min-value="minDateValue" :max-value="maxDateValue" :class="cn('w-full', props.datePickerClass)" :content-class="props.datePickerContentClass" />
168
163
  </div>
@@ -3,6 +3,9 @@ import type { DialogContentEmits, DialogContentProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
5
  import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui';
6
+ import { Cancel01Icon } from '@hugeicons/core-free-icons';
7
+ import { HugeiconsIcon } from '@hugeicons/vue';
8
+ import { useI18n } from 'vue-i18n';
6
9
  import { cn } from '../../../lib/utils';
7
10
  import { Button } from '../button';
8
11
  import DialogOverlay from './DialogOverlay.vue';
@@ -19,6 +22,8 @@ const props = withDefaults(defineProps<DialogContentProps & {
19
22
  showCloseButton: false
20
23
  });
21
24
 
25
+ const { t } = useI18n();
26
+
22
27
  // Emits
23
28
  const emits = defineEmits<DialogContentEmits>();
24
29
 
@@ -31,16 +36,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
31
36
  <DialogPortal>
32
37
  <DialogOverlay />
33
38
  <DialogContent data-slot="dialog-content" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
34
- 'bg-card text-card-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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded border border-border p-6 outline-none duration-200 sm:max-w-106.25',
39
+ 'uv-modal-motion bg-card text-card-foreground fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded border border-border p-6 shadow-elevated-xl outline-none sm:max-w-106.25',
35
40
  props.class
36
41
  )
37
42
  ">
38
43
  <slot />
39
44
 
40
45
  <DialogClose v-if="props.showCloseButton" data-slot="dialog-close" as-child>
41
- <Button variant="ghost" size="icon-sm" class="absolute top-4 right-4 p-0.5">
42
- <span aria-hidden="true">X</span>
43
- <span class="sr-only">Close</span>
46
+ <Button variant="ghost" size="icon-sm" class="group/close absolute top-3 right-3 size-8 p-0.5">
47
+ <HugeiconsIcon :icon="Cancel01Icon" aria-hidden="true" class="transition-transform duration-300 ease-spring group-hover/close:rotate-90" />
48
+ <span class="sr-only">{{ t('uiVintage.buttons.close') }}</span>
44
49
  </Button>
45
50
  </DialogClose>
46
51
  </DialogContent>
@@ -13,7 +13,7 @@ const delegatedProps = reactiveOmit(props, 'class');
13
13
 
14
14
  <template>
15
15
  <DialogOverlay data-slot="dialog-overlay" v-bind="delegatedProps" :class="cn(
16
- 'fixed inset-0 isolate 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 isolate z-50 bg-black/40 backdrop-blur-[3px] dark:bg-black/60',
17
17
  props.class
18
18
  )
19
19
  ">
@@ -3,6 +3,9 @@ import type { DialogContentEmits, DialogContentProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
5
  import { DialogClose, DialogContent, DialogOverlay, DialogPortal, useForwardPropsEmits } from 'reka-ui';
6
+ import { Cancel01Icon } from '@hugeicons/core-free-icons';
7
+ import { HugeiconsIcon } from '@hugeicons/vue';
8
+ import { useI18n } from 'vue-i18n';
6
9
  import { cn } from '../../../lib/utils';
7
10
  import { Button } from '../button';
8
11
 
@@ -18,6 +21,8 @@ const props = withDefaults(defineProps<DialogContentProps & {
18
21
  showCloseButton: false
19
22
  });
20
23
 
24
+ const { t } = useI18n();
25
+
21
26
  // Emits
22
27
  const emits = defineEmits<DialogContentEmits>();
23
28
 
@@ -28,9 +33,9 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
28
33
 
29
34
  <template>
30
35
  <DialogPortal>
31
- <DialogOverlay class="fixed inset-0 isolate z-50 grid place-items-center overflow-y-auto 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">
36
+ <DialogOverlay class="uv-overlay-motion fixed inset-0 isolate z-50 grid place-items-center overflow-y-auto bg-black/40 backdrop-blur-[3px] dark:bg-black/60">
32
37
  <DialogContent :class="cn(
33
- 'relative z-50 my-6 grid w-full max-w-[calc(100%-2rem)] gap-4 rounded border border-border bg-card p-6 text-card-foreground outline-none duration-200 sm:max-w-106.25',
38
+ 'uv-modal-motion relative z-50 my-6 grid w-full max-w-[calc(100%-2rem)] gap-4 rounded border border-border bg-card p-6 text-card-foreground shadow-elevated-xl outline-none sm:max-w-106.25',
34
39
  props.class
35
40
  )
36
41
  " v-bind="{ ...$attrs, ...forwarded }" @pointer-down-outside="
@@ -48,9 +53,9 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
48
53
  <slot />
49
54
 
50
55
  <DialogClose v-if="props.showCloseButton" data-slot="dialog-close" as-child>
51
- <Button variant="ghost" size="icon-sm" class="absolute top-4 right-4 p-0.5">
52
- <span aria-hidden="true">X</span>
53
- <span class="sr-only">Close</span>
56
+ <Button variant="ghost" size="icon-sm" class="group/close absolute top-3 right-3 size-8 p-0.5">
57
+ <HugeiconsIcon :icon="Cancel01Icon" aria-hidden="true" class="transition-transform duration-300 ease-spring group-hover/close:rotate-90" />
58
+ <span class="sr-only">{{ t('uiVintage.buttons.close') }}</span>
54
59
  </Button>
55
60
  </DialogClose>
56
61
  </DialogContent>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuRootEmits, DropdownMenuRootProps } from 'reka-ui';
3
+ import { DropdownMenuRoot, useForwardPropsEmits } from 'reka-ui';
4
+
5
+ // Props
6
+ const props = defineProps<DropdownMenuRootProps>();
7
+
8
+ // Emits
9
+ const emits = defineEmits<DropdownMenuRootEmits>();
10
+
11
+ const forwarded = useForwardPropsEmits(props, emits);
12
+ </script>
13
+
14
+ <template>
15
+ <DropdownMenuRoot v-slot="slotProps" data-slot="dropdown-menu" v-bind="forwarded">
16
+ <slot v-bind="slotProps" />
17
+ </DropdownMenuRoot>
18
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { Tick02Icon } from '@hugeicons/core-free-icons';
6
+ import { HugeiconsIcon } from '@hugeicons/vue';
7
+ import { DropdownMenuCheckboxItem, DropdownMenuItemIndicator, useForwardPropsEmits } from 'reka-ui';
8
+ import { cn } from '../../../lib/utils';
9
+ import { dropdownMenuItemClass } from './classes';
10
+
11
+ // Props
12
+ const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>();
13
+
14
+ // Emits
15
+ const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
16
+
17
+ const delegatedProps = reactiveOmit(props, 'class');
18
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
19
+ </script>
20
+
21
+ <template>
22
+ <DropdownMenuCheckboxItem data-slot="dropdown-menu-checkbox-item" v-bind="forwarded" :class="cn(dropdownMenuItemClass, 'pl-9 data-[state=checked]:text-foreground', props.class)">
23
+ <!-- Check indicator (pops in when checked) -->
24
+ <span class="pointer-events-none absolute left-3 flex size-4 items-center justify-center text-primary">
25
+ <DropdownMenuItemIndicator>
26
+ <slot name="indicator-icon">
27
+ <HugeiconsIcon :icon="Tick02Icon" class="size-4 animate-uv-pop" :stroke-width="2.5" />
28
+ </slot>
29
+ </DropdownMenuItemIndicator>
30
+ </span>
31
+
32
+ <slot />
33
+ </DropdownMenuCheckboxItem>
34
+ </template>
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuContentEmits, DropdownMenuContentProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { DropdownMenuContent, DropdownMenuPortal, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+ import { dropdownMenuStaggerClass, dropdownMenuSurfaceClass } from './classes';
8
+
9
+ defineOptions({
10
+ inheritAttrs: false
11
+ });
12
+
13
+ // Props
14
+ const props = withDefaults(defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(), {
15
+ align: 'start',
16
+ sideOffset: 6,
17
+ collisionPadding: 8
18
+ });
19
+
20
+ // Emits
21
+ const emits = defineEmits<DropdownMenuContentEmits>();
22
+
23
+ const delegatedProps = reactiveOmit(props, 'class');
24
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
25
+ </script>
26
+
27
+ <template>
28
+ <DropdownMenuPortal>
29
+ <DropdownMenuContent data-slot="dropdown-menu-content" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
30
+ dropdownMenuSurfaceClass,
31
+ 'max-h-(--reka-dropdown-menu-content-available-height)',
32
+ dropdownMenuStaggerClass,
33
+ props.class
34
+ )
35
+ ">
36
+ <slot />
37
+ </DropdownMenuContent>
38
+ </DropdownMenuPortal>
39
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuGroupProps } from 'reka-ui';
3
+ import { DropdownMenuGroup } from 'reka-ui';
4
+
5
+ // Props
6
+ const props = defineProps<DropdownMenuGroupProps>();
7
+ </script>
8
+
9
+ <template>
10
+ <DropdownMenuGroup data-slot="dropdown-menu-group" v-bind="props">
11
+ <slot />
12
+ </DropdownMenuGroup>
13
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuItemEmits, DropdownMenuItemProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { DropdownMenuItem, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+ import { dropdownMenuItemClass } from './classes';
8
+
9
+ // Props
10
+ const props = withDefaults(defineProps<DropdownMenuItemProps & {
11
+ class?: HTMLAttributes['class'];
12
+ inset?: boolean;
13
+ variant?: 'default' | 'destructive';
14
+ }>(), {
15
+ variant: 'default'
16
+ });
17
+
18
+ // Emits
19
+ const emits = defineEmits<DropdownMenuItemEmits>();
20
+
21
+ const delegatedProps = reactiveOmit(props, 'class', 'inset', 'variant');
22
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
23
+ </script>
24
+
25
+ <template>
26
+ <DropdownMenuItem data-slot="dropdown-menu-item" :data-inset="props.inset ? '' : undefined" :data-variant="props.variant" v-bind="forwarded" :class="cn(
27
+ dropdownMenuItemClass,
28
+ props.variant === 'destructive' && 'text-destructive data-highlighted:bg-destructive/10 data-highlighted:text-destructive',
29
+ props.class
30
+ )
31
+ ">
32
+ <slot />
33
+ </DropdownMenuItem>
34
+ </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuLabelProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { DropdownMenuLabel, useForwardProps } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<DropdownMenuLabelProps & {
10
+ class?: HTMLAttributes['class'];
11
+ inset?: boolean;
12
+ }>();
13
+
14
+ const delegatedProps = reactiveOmit(props, 'class', 'inset');
15
+ const forwardedProps = useForwardProps(delegatedProps);
16
+ </script>
17
+
18
+ <template>
19
+ <DropdownMenuLabel data-slot="dropdown-menu-label" :data-inset="props.inset ? '' : undefined" v-bind="forwardedProps" :class="cn(
20
+ 'px-3 pt-2 pb-1.5 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground select-none data-inset:pl-9',
21
+ props.class
22
+ )
23
+ ">
24
+ <slot />
25
+ </DropdownMenuLabel>
26
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuPortalProps } from 'reka-ui';
3
+ import { DropdownMenuPortal } from 'reka-ui';
4
+
5
+ // Props
6
+ const props = defineProps<DropdownMenuPortalProps>();
7
+ </script>
8
+
9
+ <template>
10
+ <DropdownMenuPortal v-bind="props">
11
+ <slot />
12
+ </DropdownMenuPortal>
13
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from 'reka-ui';
3
+ import { DropdownMenuRadioGroup, useForwardPropsEmits } from 'reka-ui';
4
+
5
+ // Props
6
+ const props = defineProps<DropdownMenuRadioGroupProps>();
7
+
8
+ // Emits
9
+ const emits = defineEmits<DropdownMenuRadioGroupEmits>();
10
+
11
+ const forwarded = useForwardPropsEmits(props, emits);
12
+ </script>
13
+
14
+ <template>
15
+ <DropdownMenuRadioGroup data-slot="dropdown-menu-radio-group" v-bind="forwarded">
16
+ <slot />
17
+ </DropdownMenuRadioGroup>
18
+ </template>