@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.
- package/COMPONENTS.md +3643 -0
- package/README.md +50 -10
- package/package.json +30 -8
- package/skills/ui-vintage/SKILL.md +42 -0
- package/src/components/blog/BlogFAQSection.vue +2 -2
- package/src/components/blog/BlogHeaderSection.vue +2 -2
- package/src/components/blog/BlogInfoSection.vue +3 -3
- package/src/components/blog/BlogProgressBar.vue +7 -6
- package/src/components/blog/BlogSectionTitle.vue +1 -1
- package/src/components/blog/BlogTagsSection.vue +22 -0
- package/src/components/blog/CategoryCard.vue +1 -1
- package/src/components/blog/HeaderCarousel.vue +6 -6
- package/src/components/blog/PostCard.vue +2 -2
- package/src/components/blog/SocialShareSidebar.vue +1 -1
- package/src/components/blog/TableOfContents.vue +6 -6
- package/src/components/blog/index.ts +1 -0
- package/src/components/busy-indicator/busy-indicator-state.ts +3 -2
- package/src/components/chip/Chip.vue +9 -5
- package/src/components/confirm-dialog/ConfirmDialog.vue +17 -6
- package/src/components/confirm-dialog/confirm-dialog-state.ts +4 -3
- package/src/components/content/BlogList.vue +3 -3
- package/src/components/content/BlogTable.vue +7 -7
- package/src/components/content/CodeBlock.vue +4 -4
- package/src/components/content/Flow.vue +3 -3
- package/src/components/content/ProseHr.vue +2 -2
- package/src/components/content/Terminal.vue +4 -4
- package/src/components/cookie-consent/CookieConsent.vue +82 -0
- package/src/components/cookie-consent/index.ts +3 -0
- package/src/components/cookie-consent/useCookieConsent.ts +55 -0
- package/src/components/dashboard-shell/DashboardShell.vue +3 -3
- package/src/components/empty-state-card/EmptyStateCard.vue +4 -4
- package/src/components/error-page/ErrorPage.vue +16 -14
- package/src/components/info-card/InfoCard.vue +1 -1
- package/src/components/landing/landing-navbar/LandingNavbar.vue +2 -1
- package/src/components/load-more-button/LoadMoreButton.vue +2 -2
- package/src/components/loading-state-card/LoadingStateCard.vue +4 -4
- package/src/components/message-dialog/MessageDialog.vue +4 -4
- package/src/components/message-toast/MessageToast.vue +222 -50
- package/src/components/message-toast/index.ts +2 -2
- package/src/components/message-toast/message-toast-state.ts +107 -81
- package/src/components/page-header/PageHeader.vue +1 -1
- package/src/components/progress-component/ProgressComponent.vue +4 -4
- package/src/components/single-value-card/SingleValueCard.vue +47 -11
- package/src/components/theme-selector/ThemeSelector.vue +20 -36
- package/src/components/theme-selector/index.ts +3 -1
- package/src/components/theme-selector/theme-transition.ts +86 -0
- package/src/components/ui/accordion/Accordion.vue +41 -26
- package/src/components/ui/alert-dialog/AlertDialogContent.vue +2 -2
- package/src/components/ui/alert-dialog/AlertDialogFooter.vue +1 -1
- package/src/components/ui/animated-number/AnimatedNumber.vue +102 -0
- package/src/components/ui/animated-number/index.ts +1 -0
- package/src/components/ui/avatar/Avatar.vue +66 -0
- package/src/components/ui/avatar/AvatarFallback.vue +31 -0
- package/src/components/ui/avatar/AvatarGroup.vue +75 -0
- package/src/components/ui/avatar/AvatarImage.vue +23 -0
- package/src/components/ui/avatar/index.ts +47 -0
- package/src/components/ui/badge/Badge.vue +13 -2
- package/src/components/ui/breadcrumb/Breadcrumb.vue +4 -1
- package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +7 -1
- package/src/components/ui/busy/Busy.vue +45 -20
- package/src/components/ui/button/Button.vue +20 -3
- package/src/components/ui/button/index.ts +12 -2
- package/src/components/ui/card/Card.vue +21 -8
- package/src/components/ui/card/CardText.vue +1 -1
- package/src/components/ui/checkbox/Checkbox.vue +48 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/collapsible/Collapsible.vue +22 -0
- package/src/components/ui/collapsible/CollapsibleContent.vue +22 -0
- package/src/components/ui/collapsible/CollapsibleTrigger.vue +19 -0
- package/src/components/ui/collapsible/index.ts +3 -0
- package/src/components/ui/combobox/Combobox.vue +24 -0
- package/src/components/ui/combobox/ComboboxAnchor.vue +39 -0
- package/src/components/ui/combobox/ComboboxEmpty.vue +21 -0
- package/src/components/ui/combobox/ComboboxGroup.vue +26 -0
- package/src/components/ui/combobox/ComboboxInput.vue +28 -0
- package/src/components/ui/combobox/ComboboxItem.vue +39 -0
- package/src/components/ui/combobox/ComboboxList.vue +43 -0
- package/src/components/ui/combobox/ComboboxSelect.vue +162 -0
- package/src/components/ui/combobox/ComboboxSeparator.vue +16 -0
- package/src/components/ui/combobox/ComboboxTrigger.vue +28 -0
- package/src/components/ui/combobox/index.ts +11 -0
- package/src/components/ui/combobox/types.ts +16 -0
- package/src/components/ui/command/CommandDialog.vue +18 -16
- package/src/components/ui/command/CommandGroup.vue +1 -1
- package/src/components/ui/command/CommandInput.vue +2 -2
- package/src/components/ui/command/CommandItem.vue +7 -5
- package/src/components/ui/data-table/DataTable.vue +230 -0
- package/src/components/ui/data-table/index.ts +20 -0
- package/src/components/ui/date-picker/DatePicker.vue +11 -15
- package/src/components/ui/date-time-picker/DateTimePicker.vue +2 -7
- package/src/components/ui/dialog/DialogContent.vue +9 -4
- package/src/components/ui/dialog/DialogOverlay.vue +1 -1
- package/src/components/ui/dialog/DialogScrollContent.vue +10 -5
- package/src/components/ui/dropdown-menu/DropdownMenu.vue +18 -0
- package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +34 -0
- package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -0
- package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +13 -0
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +34 -0
- package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +26 -0
- package/src/components/ui/dropdown-menu/DropdownMenuPortal.vue +13 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +18 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +32 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +16 -0
- package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +15 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +39 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +36 -0
- package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +15 -0
- package/src/components/ui/dropdown-menu/classes.ts +24 -0
- package/src/components/ui/dropdown-menu/index.ts +15 -0
- package/src/components/ui/field/FieldDescription.vue +1 -1
- package/src/components/ui/field/FieldError.vue +4 -1
- package/src/components/ui/file-dropzone/FileDropzone.vue +250 -0
- package/src/components/ui/file-dropzone/index.ts +35 -0
- package/src/components/ui/hover-card/HoverCard.vue +21 -0
- package/src/components/ui/hover-card/HoverCardContent.vue +33 -0
- package/src/components/ui/hover-card/HoverCardTrigger.vue +13 -0
- package/src/components/ui/hover-card/index.ts +3 -0
- package/src/components/ui/input/Input.vue +5 -5
- package/src/components/ui/kbd/Kbd.vue +21 -0
- package/src/components/ui/kbd/KbdGroup.vue +72 -0
- package/src/components/ui/kbd/index.ts +2 -0
- package/src/components/ui/number-field/NumberField.vue +130 -0
- package/src/components/ui/number-field/index.ts +1 -0
- package/src/components/ui/pagination/Pagination.vue +27 -0
- package/src/components/ui/pagination/PaginationContent.vue +20 -0
- package/src/components/ui/pagination/PaginationEllipsis.vue +31 -0
- package/src/components/ui/pagination/PaginationFirst.vue +32 -0
- package/src/components/ui/pagination/PaginationItem.vue +41 -0
- package/src/components/ui/pagination/PaginationLast.vue +32 -0
- package/src/components/ui/pagination/PaginationNext.vue +32 -0
- package/src/components/ui/pagination/PaginationPrevious.vue +32 -0
- package/src/components/ui/pagination/SimplePagination.vue +49 -0
- package/src/components/ui/pagination/index.ts +9 -0
- package/src/components/ui/pin-input/PinInput.vue +48 -0
- package/src/components/ui/pin-input/PinInputInput.vue +49 -0
- package/src/components/ui/pin-input/context.ts +8 -0
- package/src/components/ui/pin-input/index.ts +2 -0
- package/src/components/ui/popover/PopoverContent.vue +1 -1
- package/src/components/ui/progress/Progress.vue +22 -7
- package/src/components/ui/radio-group/RadioGroup.vue +27 -0
- package/src/components/ui/radio-group/RadioGroupCard.vue +56 -0
- package/src/components/ui/radio-group/RadioGroupItem.vue +40 -0
- package/src/components/ui/radio-group/index.ts +3 -0
- package/src/components/ui/select/SelectContent.vue +1 -1
- package/src/components/ui/select/SelectItem.vue +2 -2
- package/src/components/ui/select/SelectItemContent.vue +1 -1
- package/src/components/ui/select/SelectTrigger.vue +6 -6
- package/src/components/ui/select/SelectValueContent.vue +1 -1
- package/src/components/ui/sheet/SheetContent.vue +5 -5
- package/src/components/ui/sheet/SheetOverlay.vue +1 -1
- package/src/components/ui/sidebar/Sidebar.vue +7 -5
- package/src/components/ui/sidebar/SidebarRail.vue +3 -1
- package/src/components/ui/sidebar/SidebarTrigger.vue +3 -1
- package/src/components/ui/skeleton/Skeleton.vue +4 -4
- package/src/components/ui/slider/Slider.vue +8 -6
- package/src/components/ui/spinner/Spinner.vue +47 -0
- package/src/components/ui/spinner/index.ts +1 -0
- package/src/components/ui/stepper/SimpleStepper.vue +56 -0
- package/src/components/ui/stepper/Stepper.vue +30 -0
- package/src/components/ui/stepper/StepperDescription.vue +20 -0
- package/src/components/ui/stepper/StepperIndicator.vue +41 -0
- package/src/components/ui/stepper/StepperItem.vue +21 -0
- package/src/components/ui/stepper/StepperSeparator.vue +34 -0
- package/src/components/ui/stepper/StepperTitle.vue +20 -0
- package/src/components/ui/stepper/StepperTrigger.vue +20 -0
- package/src/components/ui/stepper/index.ts +17 -0
- package/src/components/ui/switch/Switch.vue +8 -6
- package/src/components/ui/tabs/TabsContent.vue +1 -1
- package/src/components/ui/tabs/TabsList.vue +13 -5
- package/src/components/ui/tabs/TabsTrigger.vue +5 -3
- package/src/components/ui/tags-input/TagsInput.vue +49 -0
- package/src/components/ui/tags-input/TagsInputInput.vue +23 -0
- package/src/components/ui/tags-input/TagsInputItem.vue +35 -0
- package/src/components/ui/tags-input/TagsInputItemDelete.vue +31 -0
- package/src/components/ui/tags-input/TagsInputItemText.vue +21 -0
- package/src/components/ui/tags-input/index.ts +5 -0
- package/src/components/ui/textarea/Textarea.vue +4 -5
- package/src/components/ui/toggle-group/Toggle.vue +33 -0
- package/src/components/ui/toggle-group/ToggleGroup.vue +93 -0
- package/src/components/ui/toggle-group/ToggleGroupItem.vue +34 -0
- package/src/components/ui/toggle-group/index.ts +69 -0
- package/src/components/ui/tooltip/TooltipContent.vue +1 -1
- package/src/i18n/de.json +83 -0
- package/src/i18n/en.json +83 -0
- package/src/i18n/es.json +83 -0
- package/src/i18n/fr.json +83 -0
- package/src/i18n/it.json +83 -0
- package/src/i18n/ja.json +83 -0
- package/src/i18n/pt.json +83 -0
- package/src/i18n/ru.json +83 -0
- package/src/i18n/zh.json +83 -0
- package/src/lib/color-tokens.ts +14 -14
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 =
|
|
13
|
+
const props = defineProps<DialogRootProps & {
|
|
15
14
|
title?: string;
|
|
16
15
|
description?: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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>{{
|
|
34
|
-
<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-
|
|
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-
|
|
28
|
-
<HugeiconsIcon :icon="Search01Icon" class="size-4 shrink-0
|
|
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
|
-
//
|
|
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 ??
|
|
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-
|
|
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
|
}
|