@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,21 @@
1
+ <script setup lang="ts">
2
+ import type { ComboboxEmptyProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { ComboboxEmpty } from 'reka-ui';
6
+ import { useI18n } from 'vue-i18n';
7
+ import { cn } from '../../../lib/utils';
8
+
9
+ // Props
10
+ const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>();
11
+
12
+ const delegatedProps = reactiveOmit(props, 'class');
13
+
14
+ const { t } = useI18n();
15
+ </script>
16
+
17
+ <template>
18
+ <ComboboxEmpty data-slot="combobox-empty" v-bind="delegatedProps" :class="cn('animate-uv-fade-up px-3 py-6 text-center text-xs font-semibold text-muted-foreground sm:text-sm', props.class)">
19
+ <slot>{{ t('uiVintage.combobox.empty') }}</slot>
20
+ </ComboboxEmpty>
21
+ </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import type { ComboboxGroupProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { ComboboxGroup, ComboboxLabel } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<ComboboxGroupProps & {
10
+ class?: HTMLAttributes['class'];
11
+ heading?: string;
12
+ }>();
13
+
14
+ const delegatedProps = reactiveOmit(props, 'class', 'heading');
15
+ </script>
16
+
17
+ <template>
18
+ <ComboboxGroup data-slot="combobox-group" v-bind="delegatedProps" :class="cn('overflow-hidden', props.class)">
19
+ <!-- Heading -->
20
+ <ComboboxLabel v-if="props.heading || $slots.heading" data-slot="combobox-group-heading" class="px-3 pt-2 pb-1.5 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground select-none">
21
+ <slot name="heading">{{ props.heading }}</slot>
22
+ </ComboboxLabel>
23
+
24
+ <slot />
25
+ </ComboboxGroup>
26
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import type { ComboboxInputEmits, ComboboxInputProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { ComboboxInput, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ defineOptions({
9
+ inheritAttrs: false
10
+ });
11
+
12
+ // Props
13
+ const props = defineProps<ComboboxInputProps & { class?: HTMLAttributes['class'] }>();
14
+
15
+ // Emits
16
+ const emits = defineEmits<ComboboxInputEmits>();
17
+
18
+ const delegatedProps = reactiveOmit(props, 'class');
19
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
20
+ </script>
21
+
22
+ <template>
23
+ <ComboboxInput data-slot="combobox-input" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
24
+ 'h-9 min-w-16 flex-1 truncate bg-transparent font-semibold text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed',
25
+ props.class
26
+ )
27
+ " />
28
+ </template>
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import type { ComboboxItemEmits, ComboboxItemProps } 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 { ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits } from 'reka-ui';
8
+ import { cn } from '../../../lib/utils';
9
+
10
+ // Props
11
+ const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>();
12
+
13
+ // Emits
14
+ const emits = defineEmits<ComboboxItemEmits>();
15
+
16
+ const delegatedProps = reactiveOmit(props, 'class');
17
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
18
+ </script>
19
+
20
+ <template>
21
+ <ComboboxItem data-slot="combobox-item" v-bind="forwarded" :class="cn(
22
+ 'relative flex w-full cursor-pointer items-center gap-3 rounded py-2 pr-9 pl-3 text-left text-xs font-semibold text-muted-foreground outline-hidden select-none transition-colors duration-150 sm:text-sm',
23
+ 'data-highlighted:bg-accent data-highlighted:text-foreground data-[state=checked]:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
24
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
25
+ props.class
26
+ )
27
+ ">
28
+ <slot />
29
+
30
+ <!-- Trailing check indicator (pops in when selected) -->
31
+ <span class="pointer-events-none absolute right-3 flex size-4 items-center justify-center text-primary">
32
+ <ComboboxItemIndicator>
33
+ <slot name="indicator-icon">
34
+ <HugeiconsIcon :icon="Tick02Icon" class="size-4 animate-uv-pop" :stroke-width="2.5" />
35
+ </slot>
36
+ </ComboboxItemIndicator>
37
+ </span>
38
+ </ComboboxItem>
39
+ </template>
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ import type { ComboboxContentEmits, ComboboxContentProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { ComboboxContent, ComboboxPortal, ComboboxViewport, useForwardPropsEmits } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ defineOptions({
9
+ inheritAttrs: false
10
+ });
11
+
12
+ // Props
13
+ const props = withDefaults(defineProps<ComboboxContentProps & {
14
+ class?: HTMLAttributes['class'];
15
+ viewportClass?: HTMLAttributes['class'];
16
+ }>(), {
17
+ position: 'popper',
18
+ align: 'start',
19
+ sideOffset: 6,
20
+ collisionPadding: 8
21
+ });
22
+
23
+ // Emits
24
+ const emits = defineEmits<ComboboxContentEmits>();
25
+
26
+ const delegatedProps = reactiveOmit(props, 'class', 'viewportClass');
27
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
28
+ </script>
29
+
30
+ <template>
31
+ <ComboboxPortal>
32
+ <ComboboxContent data-slot="combobox-list" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
33
+ 'uv-floating-motion bg-popover text-popover-foreground z-50 flex w-(--reka-combobox-trigger-width) max-h-[min(var(--reka-combobox-content-available-height),20rem)] flex-col overflow-hidden rounded border border-border shadow-elevated-lg outline-hidden',
34
+ props.class
35
+ )
36
+ ">
37
+ <!-- Scrollable viewport -->
38
+ <ComboboxViewport data-slot="combobox-viewport" :class="cn('min-h-0 scroll-py-1 p-1', props.viewportClass)">
39
+ <slot />
40
+ </ComboboxViewport>
41
+ </ComboboxContent>
42
+ </ComboboxPortal>
43
+ </template>
@@ -0,0 +1,162 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue';
3
+ import type { AcceptableValue } from 'reka-ui';
4
+ import { computed, ref } from 'vue';
5
+ import { Cancel01Icon, Search01Icon } from '@hugeicons/core-free-icons';
6
+ import { HugeiconsIcon } from '@hugeicons/vue';
7
+ import { useI18n } from 'vue-i18n';
8
+ import { cn } from '../../../lib/utils';
9
+ import { SelectItemContent } from '../select';
10
+ import Combobox from './Combobox.vue';
11
+ import ComboboxAnchor from './ComboboxAnchor.vue';
12
+ import ComboboxEmpty from './ComboboxEmpty.vue';
13
+ import ComboboxInput from './ComboboxInput.vue';
14
+ import ComboboxItem from './ComboboxItem.vue';
15
+ import ComboboxList from './ComboboxList.vue';
16
+ import ComboboxTrigger from './ComboboxTrigger.vue';
17
+ import type { ComboboxSelectModelValue, ComboboxSelectOption, ComboboxSelectValue } from './types';
18
+
19
+ // Props
20
+ const props = withDefaults(defineProps<{
21
+ modelValue?: ComboboxSelectModelValue;
22
+ options: ComboboxSelectOption[];
23
+ placeholder?: string;
24
+ searchPlaceholder?: string;
25
+ emptyText?: string;
26
+ multiple?: boolean;
27
+ disabled?: boolean;
28
+ size?: 'sm' | 'default';
29
+ class?: HTMLAttributes['class'];
30
+ contentClass?: HTMLAttributes['class'];
31
+ }>(), {
32
+ modelValue: undefined,
33
+ multiple: false,
34
+ disabled: false,
35
+ size: 'default'
36
+ });
37
+
38
+ // Emits
39
+ const emits = defineEmits<{
40
+ 'update:modelValue': [value: ComboboxSelectModelValue];
41
+ }>();
42
+
43
+ const { t } = useI18n();
44
+
45
+ const open = ref(false);
46
+ const searchTerm = ref('');
47
+
48
+ // Only animate the leading icon swap after the first interaction (no pop on page load)
49
+ const interacted = ref(false);
50
+
51
+ // Localized fallbacks, overridable via props
52
+ const placeholderText = computed(() => props.placeholder ?? t('uiVintage.combobox.placeholder'));
53
+ const searchPlaceholderText = computed(() => props.searchPlaceholder ?? t('uiVintage.combobox.searchPlaceholder'));
54
+ const emptyLabel = computed(() => props.emptyText ?? t('uiVintage.combobox.empty'));
55
+
56
+ // Normalized selection
57
+ const selectedValues = computed<ComboboxSelectValue[]>(() => {
58
+ if (Array.isArray(props.modelValue)) return props.modelValue;
59
+ return props.modelValue == null ? [] : [props.modelValue];
60
+ });
61
+
62
+ // Lookup options by value
63
+ const optionMap = computed(() => new Map(props.options.map(option => [option.value, option])));
64
+
65
+ const selectedOptions = computed(() => selectedValues.value
66
+ .map(value => optionMap.value.get(value))
67
+ .filter((option): option is ComboboxSelectOption => !!option));
68
+
69
+ const selectedOption = computed(() => (props.multiple ? undefined : selectedOptions.value[0]));
70
+
71
+ // Value handed to reka (array in multiple mode)
72
+ const rootValue = computed<AcceptableValue | AcceptableValue[] | undefined>(() => {
73
+ if (props.multiple) return selectedValues.value;
74
+ return selectedValues.value[0];
75
+ });
76
+
77
+ // Label shown inside the input for single selection
78
+ const displayValue = (value: unknown) => {
79
+ if (props.multiple || value == null || Array.isArray(value)) return '';
80
+ return optionMap.value.get(value as ComboboxSelectValue)?.label ?? '';
81
+ };
82
+
83
+ // Placeholder switches to a search prompt (or the current label) while open
84
+ const inputPlaceholder = computed(() => {
85
+ if (props.multiple) {
86
+ if (open.value || selectedOptions.value.length) return searchPlaceholderText.value;
87
+ return placeholderText.value;
88
+ }
89
+ if (open.value) return selectedOption.value?.label ?? searchPlaceholderText.value;
90
+ return placeholderText.value;
91
+ });
92
+
93
+ // Leading icon mirrors the selected option icon when closed
94
+ const showsOptionIcon = computed(() => !open.value && !!selectedOption.value?.icon);
95
+ const leadingIcon = computed(() => (showsOptionIcon.value && selectedOption.value?.icon) || Search01Icon);
96
+
97
+ const handleUpdate = (value: unknown) => {
98
+ if (props.multiple) {
99
+ emits('update:modelValue', Array.isArray(value) ? [...value] as ComboboxSelectValue[] : []);
100
+ return;
101
+ }
102
+ emits('update:modelValue', (value ?? null) as ComboboxSelectModelValue);
103
+ };
104
+
105
+ // Clear the text on open so the whole list is visible (single mode keeps the label as placeholder)
106
+ const handleOpenChange = (value: boolean) => {
107
+ open.value = value;
108
+ interacted.value = true;
109
+ if (value) searchTerm.value = '';
110
+ };
111
+
112
+ // Remove a tag in multiple mode
113
+ const removeValue = (value: ComboboxSelectValue) => {
114
+ if (props.disabled) return;
115
+ emits('update:modelValue', selectedValues.value.filter(item => item !== value));
116
+ };
117
+
118
+ // Backspace on an empty input removes the last tag
119
+ const handleKeydown = (event: KeyboardEvent) => {
120
+ if (!props.multiple || event.key !== 'Backspace' || searchTerm.value) return;
121
+ const last = selectedValues.value[selectedValues.value.length - 1];
122
+ if (last !== undefined) removeValue(last);
123
+ };
124
+ </script>
125
+
126
+ <template>
127
+ <Combobox :model-value="rootValue" :multiple="props.multiple" :disabled="props.disabled" :open="open" data-slot="combobox-select" :class="cn('w-full', props.class)" @update:model-value="handleUpdate" @update:open="handleOpenChange">
128
+ <!-- Field -->
129
+ <ComboboxAnchor :size="props.size" :class="props.multiple && selectedOptions.length ? 'py-2' : undefined">
130
+ <!-- Leading icon -->
131
+ <HugeiconsIcon :key="showsOptionIcon ? `option-${selectedOption?.value}` : 'search'" :icon="leadingIcon" :class="cn('size-4', interacted && 'animate-uv-pop')" />
132
+
133
+ <div class="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
134
+ <!-- Selected tags (multiple mode) -->
135
+ <TransitionGroup v-if="props.multiple" leave-active-class="transition-[opacity,scale] duration-150 ease-snappy" leave-to-class="scale-75 opacity-0">
136
+ <span v-for="option in selectedOptions" :key="option.value" data-slot="combobox-select-tag" class="inline-flex h-7 max-w-full animate-uv-pop items-center gap-1 rounded-sm border border-border bg-surface pr-1 pl-2 text-[11px] font-semibold text-foreground sm:text-xs">
137
+ <HugeiconsIcon v-if="option.icon" :icon="option.icon" class="size-3" />
138
+ <span class="truncate">{{ option.label }}</span>
139
+ <button type="button" :disabled="props.disabled" :aria-label="`${t('uiVintage.buttons.remove')} ${option.label}`" class="flex size-5 cursor-pointer items-center justify-center rounded-sm text-muted-foreground outline-none transition-[color,background-color,scale] duration-150 ease-spring hover:bg-accent hover:text-destructive focus-visible:ring-[3px] focus-visible:ring-ring/45 active:scale-90 disabled:pointer-events-none" @click.stop="removeValue(option.value)">
140
+ <HugeiconsIcon :icon="Cancel01Icon" class="size-3" />
141
+ </button>
142
+ </span>
143
+ </TransitionGroup>
144
+
145
+ <!-- Search input -->
146
+ <ComboboxInput v-model="searchTerm" :display-value="displayValue" :placeholder="inputPlaceholder" :disabled="props.disabled" @keydown="handleKeydown" />
147
+ </div>
148
+
149
+ <!-- Chevron trigger -->
150
+ <ComboboxTrigger />
151
+ </ComboboxAnchor>
152
+
153
+ <!-- Options list -->
154
+ <ComboboxList :class="props.contentClass">
155
+ <ComboboxEmpty>{{ emptyLabel }}</ComboboxEmpty>
156
+
157
+ <ComboboxItem v-for="option in props.options" :key="option.value" :value="option.value" :text-value="option.label" :disabled="option.disabled">
158
+ <SelectItemContent :icon="option.icon" :label="option.label" :description="option.description" />
159
+ </ComboboxItem>
160
+ </ComboboxList>
161
+ </Combobox>
162
+ </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import type { ComboboxSeparatorProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { ComboboxSeparator } from 'reka-ui';
6
+ import { cn } from '../../../lib/utils';
7
+
8
+ // Props
9
+ const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>();
10
+
11
+ const delegatedProps = reactiveOmit(props, 'class');
12
+ </script>
13
+
14
+ <template>
15
+ <ComboboxSeparator data-slot="combobox-separator" v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-border', props.class)" />
16
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import type { ComboboxTriggerProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
5
+ import { ArrowDown01Icon } from '@hugeicons/core-free-icons';
6
+ import { HugeiconsIcon } from '@hugeicons/vue';
7
+ import { ComboboxTrigger, useForwardProps } from 'reka-ui';
8
+ import { cn } from '../../../lib/utils';
9
+
10
+ // Props
11
+ const props = defineProps<ComboboxTriggerProps & { class?: HTMLAttributes['class'] }>();
12
+
13
+ const delegatedProps = reactiveOmit(props, 'class');
14
+ const forwardedProps = useForwardProps(delegatedProps);
15
+ </script>
16
+
17
+ <template>
18
+ <ComboboxTrigger data-slot="combobox-trigger" v-bind="forwardedProps" :class="cn(
19
+ 'group/combobox-trigger -mr-1 flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground outline-none transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60',
20
+ props.class
21
+ )
22
+ ">
23
+ <slot>
24
+ <!-- Chevron rotates with a spring while open -->
25
+ <HugeiconsIcon :icon="ArrowDown01Icon" class="size-4 text-current transition-transform duration-300 ease-spring group-data-[state=open]/combobox-trigger:rotate-180" />
26
+ </slot>
27
+ </ComboboxTrigger>
28
+ </template>
@@ -0,0 +1,11 @@
1
+ export { default as Combobox } from './Combobox.vue';
2
+ export { default as ComboboxAnchor } from './ComboboxAnchor.vue';
3
+ export { default as ComboboxEmpty } from './ComboboxEmpty.vue';
4
+ export { default as ComboboxGroup } from './ComboboxGroup.vue';
5
+ export { default as ComboboxInput } from './ComboboxInput.vue';
6
+ export { default as ComboboxItem } from './ComboboxItem.vue';
7
+ export { default as ComboboxList } from './ComboboxList.vue';
8
+ export { default as ComboboxSelect } from './ComboboxSelect.vue';
9
+ export { default as ComboboxSeparator } from './ComboboxSeparator.vue';
10
+ export { default as ComboboxTrigger } from './ComboboxTrigger.vue';
11
+ export type { ComboboxSelectModelValue, ComboboxSelectOption, ComboboxSelectValue } from './types';
@@ -0,0 +1,16 @@
1
+ import type { HugeiconsIconDefinition } from '../../../lib/common-types';
2
+
3
+ // Primitive value held by a ComboboxSelect option
4
+ export type ComboboxSelectValue = string | number;
5
+
6
+ // Option rendered by ComboboxSelect
7
+ export interface ComboboxSelectOption {
8
+ value: ComboboxSelectValue;
9
+ label: string;
10
+ description?: string;
11
+ icon?: HugeiconsIconDefinition;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ // v-model shape of ComboboxSelect (array when `multiple`)
16
+ export type ComboboxSelectModelValue = ComboboxSelectValue | ComboboxSelectValue[] | null | undefined;
@@ -1,37 +1,39 @@
1
1
  <script setup lang="ts">
2
2
  import type { DialogRootEmits, DialogRootProps } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { reactiveOmit } from '@vueuse/core';
3
5
  import { useForwardPropsEmits } from 'reka-ui';
4
- import {
5
- Dialog,
6
- DialogContent,
7
- DialogDescription,
8
- DialogHeader,
9
- DialogTitle,
10
- } from '../dialog';
6
+ import { computed } from 'vue';
7
+ import { useI18n } from 'vue-i18n';
8
+ import { cn } from '../../../lib/utils';
9
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '../dialog';
11
10
  import Command from './Command.vue';
12
11
 
13
12
  // Props
14
- const props = withDefaults(defineProps<DialogRootProps & {
13
+ const props = defineProps<DialogRootProps & {
15
14
  title?: string;
16
15
  description?: string;
17
- }>(), {
18
- title: 'Command Palette',
19
- description: 'Search for a command to run...'
20
- });
16
+ contentClass?: HTMLAttributes['class'];
17
+ }>();
18
+
19
+ const { t } = useI18n();
20
+ const resolvedTitle = computed(() => props.title || t('uiVintage.commandDialog.title'));
21
+ const resolvedDescription = computed(() => props.description || t('uiVintage.commandDialog.description'));
21
22
 
22
23
  // Emits
23
24
  const emits = defineEmits<DialogRootEmits>();
24
25
 
25
26
  // Forward props
26
- const forwarded = useForwardPropsEmits(props, emits);
27
+ const delegatedProps = reactiveOmit(props, 'title', 'description', 'contentClass');
28
+ const forwarded = useForwardPropsEmits(delegatedProps, emits);
27
29
  </script>
28
30
 
29
31
  <template>
30
32
  <Dialog v-slot="slotProps" v-bind="forwarded">
31
- <DialogContent class="overflow-hidden p-0">
33
+ <DialogContent :class="cn('top-[12vh] translate-y-0 gap-0 overflow-hidden p-0 sm:max-w-xl', props.contentClass)">
32
34
  <DialogHeader class="sr-only">
33
- <DialogTitle>{{ title }}</DialogTitle>
34
- <DialogDescription>{{ description }}</DialogDescription>
35
+ <DialogTitle>{{ resolvedTitle }}</DialogTitle>
36
+ <DialogDescription>{{ resolvedDescription }}</DialogDescription>
35
37
  </DialogHeader>
36
38
  <Command>
37
39
  <slot v-bind="slotProps" />
@@ -34,7 +34,7 @@ onUnmounted(() => {
34
34
 
35
35
  <template>
36
36
  <ListboxGroup v-bind="delegatedProps" :id="id" data-slot="command-group" :class="cn('text-foreground overflow-hidden', props.class)" :hidden="isRender ? undefined : true">
37
- <ListboxGroupLabel v-if="heading" data-slot="command-group-heading" class="border-b border-(--border-light) px-3 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-(--text-secondary-light) dark:border-(--border-dark) dark:text-(--text-secondary-dark)">
37
+ <ListboxGroupLabel v-if="heading" data-slot="command-group-heading" class="border-b border-border px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
38
38
  {{ heading }}
39
39
  </ListboxGroupLabel>
40
40
  <div class="p-2">
@@ -24,8 +24,8 @@ const { filterState } = useCommand();
24
24
  </script>
25
25
 
26
26
  <template>
27
- <div data-slot="command-input-wrapper" class="flex h-9 items-center gap-2 border-b px-3">
28
- <HugeiconsIcon :icon="Search01Icon" class="size-4 shrink-0 opacity-50" />
27
+ <div data-slot="command-input-wrapper" class="flex h-12 items-center gap-3 border-b border-border px-4">
28
+ <HugeiconsIcon :icon="Search01Icon" class="size-4 shrink-0 text-muted-foreground" />
29
29
  <ListboxFilter v-bind="{ ...forwardedProps, ...$attrs }" v-model="filterState.search" data-slot="command-input" auto-focus :class="cn(
30
30
  'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
31
31
  props.class,
@@ -44,10 +44,11 @@ const currentElement = useCurrentElement(itemRef);
44
44
  onMounted(() => {
45
45
  if (!(currentElement.value instanceof HTMLElement)) return;
46
46
 
47
- // textValue to perform filter
47
+ // Index both the rendered text and a string value (used as extra search keywords)
48
+ const keywords = typeof props.value === 'string' ? props.value : '';
48
49
  allItems.value.set(
49
50
  id,
50
- currentElement.value.textContent ?? props.value?.toString() ?? '',
51
+ `${currentElement.value.textContent ?? ''} ${keywords}`.trim(),
51
52
  );
52
53
 
53
54
  const groupId = groupContext?.id;
@@ -66,10 +67,11 @@ onUnmounted(() => {
66
67
 
67
68
  <template>
68
69
  <ListboxItem v-if="isRender" v-bind="forwarded" :id="id" ref="itemRef" data-slot="command-item" :class="cn(
69
- 'relative flex cursor-pointer items-center gap-3 rounded px-3 py-2 text-left text-sm font-semibold text-(--text-secondary-light) outline-hidden transition-colors duration-200 select-none data-highlighted:bg-(--bg-selected-light) data-highlighted:text-(--text-primary-light) data-disabled:pointer-events-none data-disabled:opacity-50 dark:text-(--text-secondary-dark) dark:data-highlighted:bg-(--bg-selected-dark) dark:data-highlighted:text-(--text-primary-dark) [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 [&_svg:not([class*=\'text-\'])]:text-current',
70
+ 'relative flex cursor-pointer items-center gap-3 rounded px-3 py-2 text-left text-sm font-semibold text-muted-foreground outline-hidden transition-colors duration-150 select-none data-highlighted:bg-accent data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
71
+ 'before:absolute before:inset-y-1.5 before:left-0 before:w-0.5 before:scale-y-0 before:rounded-full before:bg-primary before:transition-transform before:duration-300 before:ease-spring data-highlighted:before:scale-y-100',
72
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 [&_svg:not([class*=\'text-\'])]:text-current',
70
73
  props.class,
71
- )
72
- " @select="
74
+ )" @select="
73
75
  () => {
74
76
  filterState.search = '';
75
77
  }