@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,75 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes, VNode } from 'vue';
|
|
3
|
+
import type { AvatarShape, AvatarSize } from '.';
|
|
4
|
+
import { Comment, Fragment, Text, useSlots } from 'vue';
|
|
5
|
+
import { cn } from '../../../lib/utils';
|
|
6
|
+
import { avatarShapeClasses, avatarSizeClasses, avatarTextClasses } from '.';
|
|
7
|
+
|
|
8
|
+
// Props
|
|
9
|
+
const props = withDefaults(defineProps<{
|
|
10
|
+
max?: number;
|
|
11
|
+
size?: AvatarSize;
|
|
12
|
+
shape?: AvatarShape;
|
|
13
|
+
class?: HTMLAttributes['class'];
|
|
14
|
+
}>(), {
|
|
15
|
+
size: 'md',
|
|
16
|
+
shape: 'circle',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const slots = useSlots();
|
|
20
|
+
|
|
21
|
+
// Flatten slot vnodes (v-for fragments) and drop comments / whitespace text
|
|
22
|
+
function flatten(nodes: VNode[]): VNode[] {
|
|
23
|
+
const result: VNode[] = [];
|
|
24
|
+
for (const node of nodes) {
|
|
25
|
+
if (node.type === Comment) continue;
|
|
26
|
+
if (node.type === Text && typeof node.children === 'string' && !node.children.trim()) continue;
|
|
27
|
+
if (node.type === Fragment && Array.isArray(node.children)) {
|
|
28
|
+
result.push(...flatten(node.children as VNode[]));
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
result.push(node);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Children are read at render time so slot changes are always reflected
|
|
37
|
+
function getChildren() {
|
|
38
|
+
return flatten(slots.default?.() ?? []);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Whether a max limit applies
|
|
42
|
+
function hasMax() {
|
|
43
|
+
return props.max !== undefined && props.max >= 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Avatars that fit within max
|
|
47
|
+
function getVisible() {
|
|
48
|
+
const children = getChildren();
|
|
49
|
+
return hasMax() ? children.slice(0, props.max) : children;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Number of avatars hidden behind the +N bubble
|
|
53
|
+
function getOverflow() {
|
|
54
|
+
return hasMax() ? Math.max(0, getChildren().length - (props.max ?? 0)) : 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Renders a vnode as-is
|
|
58
|
+
const VNodeRenderer = (renderProps: { node: VNode }) => renderProps.node;
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<div data-slot="avatar-group" :class="cn(
|
|
63
|
+
'flex items-center -space-x-2 hover:-space-x-0.5',
|
|
64
|
+
'*:ring-2 *:ring-background *:transition-[margin,translate] *:duration-380 *:ease-spring *:hover:-translate-y-0.5',
|
|
65
|
+
props.class
|
|
66
|
+
)">
|
|
67
|
+
<!-- Visible avatars -->
|
|
68
|
+
<VNodeRenderer v-for="(node, index) in getVisible()" :key="node.key ?? index" :node="node" />
|
|
69
|
+
|
|
70
|
+
<!-- Overflow bubble -->
|
|
71
|
+
<span v-if="getOverflow() > 0" data-slot="avatar-group-overflow" :class="cn('relative inline-flex shrink-0 select-none items-center justify-center bg-surface font-bold text-muted-foreground', avatarSizeClasses[props.size], avatarShapeClasses[props.shape], avatarTextClasses[props.size])">
|
|
72
|
+
+{{ getOverflow() }}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AvatarImageEmits, AvatarImageProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
5
|
+
import { AvatarImage, useForwardPropsEmits } from 'reka-ui';
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
|
|
8
|
+
// Props
|
|
9
|
+
const props = defineProps<AvatarImageProps & {
|
|
10
|
+
class?: HTMLAttributes['class'];
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
// Emits
|
|
14
|
+
const emits = defineEmits<AvatarImageEmits>();
|
|
15
|
+
|
|
16
|
+
const delegatedProps = reactiveOmit(props, 'class');
|
|
17
|
+
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<!-- Reka reveals the image only once loaded, which plays the zoom-in entrance -->
|
|
22
|
+
<AvatarImage data-slot="avatar-image" v-bind="forwarded" :class="cn('aspect-square size-full object-cover animate-[uv-float-in_420ms_var(--ease-out-expo)_both]', props.class)" />
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
export { default as Avatar } from './Avatar.vue';
|
|
4
|
+
export { default as AvatarFallback } from './AvatarFallback.vue';
|
|
5
|
+
export { default as AvatarGroup } from './AvatarGroup.vue';
|
|
6
|
+
export { default as AvatarImage } from './AvatarImage.vue';
|
|
7
|
+
|
|
8
|
+
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
9
|
+
export type AvatarShape = 'circle' | 'square';
|
|
10
|
+
export type AvatarStatus = 'online' | 'offline' | 'busy' | 'away';
|
|
11
|
+
|
|
12
|
+
// Box size per avatar size (24/32/40/48/64px)
|
|
13
|
+
export const avatarSizeClasses: Record<AvatarSize, string> = {
|
|
14
|
+
xs: 'size-6',
|
|
15
|
+
sm: 'size-8',
|
|
16
|
+
md: 'size-10',
|
|
17
|
+
lg: 'size-12',
|
|
18
|
+
xl: 'size-16',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Initials text size per avatar size
|
|
22
|
+
export const avatarTextClasses: Record<AvatarSize, string> = {
|
|
23
|
+
xs: 'text-[10px]',
|
|
24
|
+
sm: 'text-xs',
|
|
25
|
+
md: 'text-sm',
|
|
26
|
+
lg: 'text-base',
|
|
27
|
+
xl: 'text-lg',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Corner radius per avatar shape
|
|
31
|
+
export const avatarShapeClasses: Record<AvatarShape, string> = {
|
|
32
|
+
circle: 'rounded-full',
|
|
33
|
+
square: 'rounded',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Context shared by an avatar with its image and fallback
|
|
37
|
+
export const avatarContextKey: InjectionKey<{ size: Ref<AvatarSize>; shape: Ref<AvatarShape> }> = Symbol('uv-avatar');
|
|
38
|
+
|
|
39
|
+
// Compute up to 2 initials from a full name
|
|
40
|
+
export function getInitials(name: string | null | undefined): string {
|
|
41
|
+
const words = (name ?? '').trim().split(/\s+/).filter(Boolean);
|
|
42
|
+
if (words.length === 0) return '';
|
|
43
|
+
|
|
44
|
+
// Single word: first two characters, otherwise first letter of the first and last word
|
|
45
|
+
if (words.length === 1) return Array.from(words[0]!).slice(0, 2).join('').toUpperCase();
|
|
46
|
+
return (Array.from(words[0]!)[0]! + Array.from(words[words.length - 1]!)[0]!).toUpperCase();
|
|
47
|
+
}
|
|
@@ -10,15 +10,26 @@ const props = withDefaults(defineProps<{
|
|
|
10
10
|
text: string;
|
|
11
11
|
icon?: HugeiconsIconDefinition | null;
|
|
12
12
|
color?: ToneColor;
|
|
13
|
+
dot?: boolean;
|
|
14
|
+
pulse?: boolean;
|
|
13
15
|
class?: HTMLAttributes['class'];
|
|
14
16
|
}>(), {
|
|
15
17
|
icon: null,
|
|
16
|
-
color: 'gray'
|
|
18
|
+
color: 'gray',
|
|
19
|
+
dot: false,
|
|
20
|
+
pulse: false
|
|
17
21
|
});
|
|
18
22
|
</script>
|
|
19
23
|
|
|
20
24
|
<template>
|
|
21
|
-
<span data-slot="badge" :class="cn('inline-flex w-fit items-center justify-center gap-1.5 whitespace-nowrap rounded border px-2 py-0.5 text-xs font-semibold transition-colors duration-
|
|
25
|
+
<span data-slot="badge" :class="cn('inline-flex w-fit items-center justify-center gap-1.5 whitespace-nowrap rounded border px-2 py-0.5 text-xs font-semibold transition-colors duration-150', getSurfaceToneClasses(props.color), props.class)">
|
|
26
|
+
<!-- Status dot -->
|
|
27
|
+
<span v-if="props.dot || props.pulse" class="relative flex size-1.5 shrink-0">
|
|
28
|
+
<span v-if="props.pulse" class="absolute inset-0 animate-uv-ping-soft rounded-full bg-current" />
|
|
29
|
+
<span class="relative size-1.5 rounded-full bg-current" />
|
|
30
|
+
</span>
|
|
31
|
+
|
|
32
|
+
<!-- Icon -->
|
|
22
33
|
<HugeiconsIcon v-if="props.icon" :icon="props.icon" class="size-3.5 shrink-0" :stroke-width="1.8" />
|
|
23
34
|
{{ props.text }}
|
|
24
35
|
</span>
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'vue';
|
|
3
|
+
import { useI18n } from 'vue-i18n';
|
|
3
4
|
|
|
4
5
|
// Props
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
class?: HTMLAttributes['class'];
|
|
7
8
|
}>();
|
|
9
|
+
|
|
10
|
+
const { t } = useI18n();
|
|
8
11
|
</script>
|
|
9
12
|
|
|
10
13
|
<template>
|
|
11
|
-
<nav aria-label="breadcrumb" data-slot="breadcrumb" :class="props.class">
|
|
14
|
+
<nav :aria-label="t('uiVintage.common.breadcrumb')" data-slot="breadcrumb" :class="props.class">
|
|
12
15
|
<slot />
|
|
13
16
|
</nav>
|
|
14
17
|
</template>
|
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
import type { HTMLAttributes } from 'vue';
|
|
3
3
|
import { MoreHorizontalIcon } from '@hugeicons/core-free-icons';
|
|
4
4
|
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
5
|
+
import { useI18n } from 'vue-i18n';
|
|
5
6
|
import { cn } from '../../../lib/utils';
|
|
6
7
|
|
|
7
8
|
// Props
|
|
8
9
|
const props = defineProps<{
|
|
9
10
|
class?: HTMLAttributes['class'];
|
|
10
11
|
}>();
|
|
12
|
+
|
|
13
|
+
const { t } = useI18n();
|
|
11
14
|
</script>
|
|
12
15
|
|
|
13
16
|
<template>
|
|
14
17
|
<span data-slot="breadcrumb-ellipsis" role="presentation" aria-hidden="true" :class="cn('flex size-9 items-center justify-center', props.class)">
|
|
18
|
+
<!-- Default slot content -->
|
|
15
19
|
<slot>
|
|
16
20
|
<HugeiconsIcon :icon="MoreHorizontalIcon" class="size-4" />
|
|
17
21
|
</slot>
|
|
18
|
-
|
|
22
|
+
|
|
23
|
+
<!-- Screen reader text for accessibility -->
|
|
24
|
+
<span class="sr-only">{{ t('uiVintage.common.more') }}</span>
|
|
19
25
|
</span>
|
|
20
26
|
</template>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
4
|
-
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
2
|
+
import { computed, type HTMLAttributes } from 'vue';
|
|
3
|
+
import { getUiVintageRuntimeMessage } from '../../../lib/i18n';
|
|
5
4
|
import { cn } from '../../../lib/utils';
|
|
5
|
+
import { Spinner } from '../spinner';
|
|
6
6
|
|
|
7
7
|
// Props
|
|
8
8
|
const props = withDefaults(defineProps<{
|
|
@@ -11,27 +11,32 @@ const props = withDefaults(defineProps<{
|
|
|
11
11
|
class?: HTMLAttributes['class'];
|
|
12
12
|
overlayClass?: HTMLAttributes['class'];
|
|
13
13
|
}>(), {
|
|
14
|
-
show: false
|
|
15
|
-
label: 'Loading...'
|
|
14
|
+
show: false
|
|
16
15
|
});
|
|
16
|
+
|
|
17
|
+
const resolvedLabel = computed(() => props.label || getUiVintageRuntimeMessage('uiVintage.common.loading.title', 'Loading...'));
|
|
17
18
|
</script>
|
|
18
19
|
|
|
19
20
|
<template>
|
|
20
|
-
<Transition name="busy
|
|
21
|
+
<Transition name="uv-busy">
|
|
21
22
|
<div v-if="props.show" data-slot="busy-overlay" :class="cn(
|
|
22
|
-
'fixed inset-0 z-50 flex items-center justify-center bg-black/
|
|
23
|
+
'fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-[3px] dark:bg-black/60',
|
|
23
24
|
props.overlayClass,
|
|
24
|
-
)
|
|
25
|
-
">
|
|
25
|
+
)">
|
|
26
26
|
<div data-slot="busy-content" role="status" aria-live="polite" aria-busy="true" :class="cn(
|
|
27
|
-
'bg-card text-card-foreground flex w-72 flex-col items-center justify-center gap-4 rounded border border-border px-8 py-7 text-center shadow-
|
|
27
|
+
'uv-busy-card bg-card text-card-foreground flex w-72 flex-col items-center justify-center gap-4 rounded border border-border px-8 py-7 text-center shadow-elevated-xl',
|
|
28
28
|
props.class,
|
|
29
|
-
)
|
|
30
|
-
">
|
|
29
|
+
)">
|
|
31
30
|
<slot>
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
31
|
+
<!-- Spinner with a soft glow -->
|
|
32
|
+
<div class="relative flex items-center justify-center">
|
|
33
|
+
<span class="absolute size-14 rounded-full bg-primary/15 blur-md" />
|
|
34
|
+
<Spinner data-slot="busy-spinner" size="xl" class="text-primary" />
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Label -->
|
|
38
|
+
<span v-if="resolvedLabel" data-slot="busy-label" class="text-xs font-semibold text-foreground sm:text-sm">
|
|
39
|
+
{{ resolvedLabel }}
|
|
35
40
|
</span>
|
|
36
41
|
</slot>
|
|
37
42
|
</div>
|
|
@@ -40,13 +45,33 @@ const props = withDefaults(defineProps<{
|
|
|
40
45
|
</template>
|
|
41
46
|
|
|
42
47
|
<style scoped>
|
|
43
|
-
.busy-
|
|
44
|
-
.
|
|
45
|
-
|
|
48
|
+
.uv-busy-enter-active {
|
|
49
|
+
transition: opacity 0.25s var(--ease-out-expo);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.uv-busy-leave-active {
|
|
53
|
+
transition: opacity 0.18s var(--ease-snappy);
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
.busy-
|
|
49
|
-
.busy-
|
|
56
|
+
.uv-busy-enter-from,
|
|
57
|
+
.uv-busy-leave-to {
|
|
50
58
|
opacity: 0;
|
|
51
59
|
}
|
|
60
|
+
|
|
61
|
+
.uv-busy-enter-active .uv-busy-card {
|
|
62
|
+
transition: transform 0.45s var(--ease-spring), filter 0.3s var(--ease-out-expo);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.uv-busy-leave-active .uv-busy-card {
|
|
66
|
+
transition: transform 0.18s var(--ease-snappy);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.uv-busy-enter-from .uv-busy-card {
|
|
70
|
+
transform: translateY(10px) scale(0.92);
|
|
71
|
+
filter: blur(4px);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.uv-busy-leave-to .uv-busy-card {
|
|
75
|
+
transform: scale(0.96);
|
|
76
|
+
}
|
|
52
77
|
</style>
|
|
@@ -4,20 +4,37 @@ import type { HTMLAttributes } from 'vue';
|
|
|
4
4
|
import type { ButtonVariants } from '.';
|
|
5
5
|
import { Primitive } from 'reka-ui';
|
|
6
6
|
import { cn } from '../../../lib/utils';
|
|
7
|
+
import { Spinner } from '../spinner';
|
|
7
8
|
import { buttonVariants } from '.';
|
|
8
9
|
|
|
9
10
|
// Props
|
|
10
11
|
const props = withDefaults(defineProps<PrimitiveProps & {
|
|
11
12
|
variant?: ButtonVariants['variant'];
|
|
12
13
|
size?: ButtonVariants['size'];
|
|
14
|
+
loading?: boolean;
|
|
13
15
|
class?: HTMLAttributes['class'];
|
|
14
16
|
}>(), {
|
|
15
|
-
as: 'button'
|
|
17
|
+
as: 'button',
|
|
18
|
+
loading: false
|
|
16
19
|
});
|
|
17
20
|
</script>
|
|
18
21
|
|
|
19
22
|
<template>
|
|
20
|
-
<Primitive data-slot="button" :data-variant="variant" :data-size="size" :as="as" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)">
|
|
21
|
-
|
|
23
|
+
<Primitive data-slot="button" :data-variant="variant" :data-size="size" :data-loading="props.loading ? '' : undefined" :aria-busy="props.loading || undefined" :disabled="props.loading || undefined" :as="as" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)">
|
|
24
|
+
<!-- As-child buttons delegate rendering to the slotted element -->
|
|
25
|
+
<slot v-if="asChild" />
|
|
26
|
+
|
|
27
|
+
<template v-else>
|
|
28
|
+
<!-- Content (kept in the layout while loading so the width never jumps) -->
|
|
29
|
+
<span v-if="props.loading" class="invisible contents">
|
|
30
|
+
<slot />
|
|
31
|
+
</span>
|
|
32
|
+
<slot v-else />
|
|
33
|
+
|
|
34
|
+
<!-- Loading indicator -->
|
|
35
|
+
<span v-if="props.loading" class="absolute inset-0 flex items-center justify-center">
|
|
36
|
+
<Spinner size="sm" />
|
|
37
|
+
</span>
|
|
38
|
+
</template>
|
|
22
39
|
</Primitive>
|
|
23
40
|
</template>
|
|
@@ -28,12 +28,22 @@ export type ButtonVariants = {
|
|
|
28
28
|
| undefined;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
// Sheen sweep that crosses solid buttons on hover
|
|
32
|
+
const sheenClasses = "before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:-translate-x-full before:bg-[linear-gradient(105deg,transparent_35%,rgb(255_255_255/0.38)_50%,transparent_65%)] before:transition-transform before:duration-700 before:ease-out-expo hover:before:translate-x-full";
|
|
33
|
+
|
|
31
34
|
export const buttonVariants: (props?: ButtonVariants & ClassProp) => string = cva(
|
|
32
|
-
|
|
35
|
+
[
|
|
36
|
+
"relative isolate inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 overflow-hidden whitespace-nowrap rounded text-xs font-semibold outline-none sm:text-sm",
|
|
37
|
+
"[transition:color_150ms,background-color_150ms,border-color_150ms,box-shadow_220ms,opacity_150ms,scale_380ms_var(--ease-spring)]",
|
|
38
|
+
"enabled:active:scale-[0.97] enabled:active:duration-75 disabled:pointer-events-none disabled:opacity-50 data-loading:cursor-wait data-loading:disabled:opacity-100",
|
|
39
|
+
"focus-visible:ring-[3px] focus-visible:ring-ring/45",
|
|
40
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
41
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
42
|
+
],
|
|
33
43
|
{
|
|
34
44
|
variants: {
|
|
35
45
|
variant: {
|
|
36
|
-
primary: getButtonVariantClasses('primary')
|
|
46
|
+
primary: `${getButtonVariantClasses('primary')} ${sheenClasses}`,
|
|
37
47
|
secondary: getButtonVariantClasses('secondary'),
|
|
38
48
|
ghost: getButtonVariantClasses('ghost'),
|
|
39
49
|
link: getButtonVariantClasses('link'),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue';
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
3
|
import type { HTMLAttributes } from 'vue';
|
|
4
4
|
import {
|
|
5
5
|
getSurfaceToneClasses,
|
|
@@ -10,30 +10,43 @@ import { cn } from '../../../lib/utils';
|
|
|
10
10
|
type CardColor = 'default' | ToneColor;
|
|
11
11
|
|
|
12
12
|
// Props
|
|
13
|
-
const props = defineProps<{
|
|
13
|
+
const props = withDefaults(defineProps<{
|
|
14
14
|
color?: CardColor;
|
|
15
|
+
interactive?: boolean;
|
|
15
16
|
class?: HTMLAttributes['class'];
|
|
16
|
-
}>()
|
|
17
|
+
}>(), {
|
|
18
|
+
interactive: false
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const cardRef = ref<HTMLElement | null>(null);
|
|
17
22
|
|
|
18
23
|
// Get the appropriate classes
|
|
19
24
|
const colorClasses = computed(() => {
|
|
20
25
|
// If color is not specified or is "default", use the default card styles
|
|
21
26
|
if (!props.color || props.color === 'default') {
|
|
22
|
-
return 'border border-border bg-card text-card-foreground';
|
|
27
|
+
return 'border border-border bg-card text-card-foreground shadow-elevated-sm';
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
// For other colors, use the corresponding surface tone classes
|
|
26
31
|
return getSurfaceToneClasses(props.color);
|
|
27
32
|
});
|
|
33
|
+
|
|
34
|
+
// Track the pointer so interactive cards can render a spotlight under the cursor
|
|
35
|
+
const handlePointerMove = (event: PointerEvent) => {
|
|
36
|
+
if (!props.interactive || !cardRef.value) return;
|
|
37
|
+
const rect = cardRef.value.getBoundingClientRect();
|
|
38
|
+
cardRef.value.style.setProperty('--uv-spot-x', `${event.clientX - rect.left}px`);
|
|
39
|
+
cardRef.value.style.setProperty('--uv-spot-y', `${event.clientY - rect.top}px`);
|
|
40
|
+
};
|
|
28
41
|
</script>
|
|
29
42
|
|
|
30
43
|
<template>
|
|
31
|
-
<div data-slot="card" :class="cn(
|
|
32
|
-
'flex flex-col gap-4 sm:gap-6! rounded py-6 transition-
|
|
44
|
+
<div ref="cardRef" data-slot="card" :data-interactive="props.interactive ? '' : undefined" :class="cn(
|
|
45
|
+
'relative flex flex-col gap-4 sm:gap-6! rounded py-6 transition-[border-color,box-shadow,background-color,translate] duration-300 ease-out-expo',
|
|
33
46
|
colorClasses,
|
|
47
|
+
props.interactive && 'cursor-pointer hover:-translate-y-0.5 hover:border-border-strong hover:shadow-elevated-lg active:translate-y-0 active:duration-100 before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:opacity-0 before:transition-opacity before:duration-300 before:bg-[radial-gradient(360px_circle_at_var(--uv-spot-x,50%)_var(--uv-spot-y,50%),color-mix(in_oklab,var(--primary)_9%,transparent),transparent_70%)] hover:before:opacity-100',
|
|
34
48
|
props.class,
|
|
35
|
-
)
|
|
36
|
-
">
|
|
49
|
+
)" @pointermove="handlePointerMove">
|
|
37
50
|
<slot />
|
|
38
51
|
</div>
|
|
39
52
|
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
5
|
+
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui';
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
|
|
8
|
+
// Props
|
|
9
|
+
const props = defineProps<CheckboxRootProps & {
|
|
10
|
+
class?: HTMLAttributes['class'];
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
// Emits
|
|
14
|
+
const emits = defineEmits<CheckboxRootEmits>();
|
|
15
|
+
|
|
16
|
+
const delegatedProps = reactiveOmit(props, 'class');
|
|
17
|
+
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<CheckboxRoot v-slot="slotProps" data-slot="checkbox" v-bind="forwarded" :class="cn(
|
|
22
|
+
'group/checkbox peer relative inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm border border-input bg-secondary text-primary-foreground shadow-elevated-sm outline-none',
|
|
23
|
+
'[transition:scale_320ms_var(--ease-spring),background-color_150ms_var(--ease-snappy),border-color_150ms_var(--ease-snappy),box-shadow_150ms_var(--ease-snappy)]',
|
|
24
|
+
'hover:border-border-strong active:scale-[0.86] focus-visible:ring-[3px] focus-visible:ring-ring/45',
|
|
25
|
+
'data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary',
|
|
26
|
+
'disabled:cursor-not-allowed disabled:opacity-60 disabled:active:scale-100 aria-invalid:border-destructive',
|
|
27
|
+
props.class
|
|
28
|
+
)">
|
|
29
|
+
<!-- Indicator (always mounted so the mark can draw in and out) -->
|
|
30
|
+
<CheckboxIndicator force-mount data-slot="checkbox-indicator" class="pointer-events-none flex size-full items-center justify-center">
|
|
31
|
+
<slot v-bind="slotProps">
|
|
32
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5" aria-hidden="true">
|
|
33
|
+
<!-- Check mark: draws itself via stroke-dashoffset -->
|
|
34
|
+
<path d="M5 12.5l4.5 4.5L19 7.5" pathLength="1" :class="cn(
|
|
35
|
+
'opacity-0 [stroke-dasharray:1_2] [stroke-dashoffset:1] [transition:stroke-dashoffset_120ms_var(--ease-snappy),opacity_90ms_var(--ease-snappy)_30ms]',
|
|
36
|
+
'group-data-[state=checked]/checkbox:opacity-100 group-data-[state=checked]/checkbox:[stroke-dashoffset:0] group-data-[state=checked]/checkbox:[transition:stroke-dashoffset_380ms_var(--ease-out-expo)_40ms,opacity_60ms_linear_40ms]'
|
|
37
|
+
)" />
|
|
38
|
+
|
|
39
|
+
<!-- Indeterminate dash: sweeps in from the left -->
|
|
40
|
+
<path d="M6.5 12h11" pathLength="1" :class="cn(
|
|
41
|
+
'opacity-0 [stroke-dasharray:1_2] [stroke-dashoffset:1] [transition:stroke-dashoffset_120ms_var(--ease-snappy),opacity_90ms_var(--ease-snappy)_30ms]',
|
|
42
|
+
'group-data-[state=indeterminate]/checkbox:opacity-100 group-data-[state=indeterminate]/checkbox:[stroke-dashoffset:0] group-data-[state=indeterminate]/checkbox:[transition:stroke-dashoffset_320ms_var(--ease-out-expo)_40ms,opacity_60ms_linear_40ms]'
|
|
43
|
+
)" />
|
|
44
|
+
</svg>
|
|
45
|
+
</slot>
|
|
46
|
+
</CheckboxIndicator>
|
|
47
|
+
</CheckboxRoot>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Checkbox } from './Checkbox.vue';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
5
|
+
import { CollapsibleRoot, useForwardPropsEmits } from 'reka-ui';
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
|
|
8
|
+
// Props
|
|
9
|
+
const props = defineProps<CollapsibleRootProps & { class?: HTMLAttributes['class'] }>();
|
|
10
|
+
|
|
11
|
+
// Emits
|
|
12
|
+
const emits = defineEmits<CollapsibleRootEmits>();
|
|
13
|
+
|
|
14
|
+
const delegatedProps = reactiveOmit(props, 'class');
|
|
15
|
+
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<CollapsibleRoot v-slot="slotProps" data-slot="collapsible" v-bind="forwarded" :class="cn(props.class)">
|
|
20
|
+
<slot v-bind="slotProps" />
|
|
21
|
+
</CollapsibleRoot>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CollapsibleContentEmits, CollapsibleContentProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
5
|
+
import { CollapsibleContent, useForwardPropsEmits } from 'reka-ui';
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
|
|
8
|
+
// Props
|
|
9
|
+
const props = defineProps<CollapsibleContentProps & { class?: HTMLAttributes['class'] }>();
|
|
10
|
+
|
|
11
|
+
// Emits
|
|
12
|
+
const emits = defineEmits<CollapsibleContentEmits>();
|
|
13
|
+
|
|
14
|
+
const delegatedProps = reactiveOmit(props, 'class');
|
|
15
|
+
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<CollapsibleContent data-slot="collapsible-content" v-bind="forwarded" :class="cn('uv-collapsible-motion', props.class)">
|
|
20
|
+
<slot />
|
|
21
|
+
</CollapsibleContent>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CollapsibleTriggerProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
5
|
+
import { CollapsibleTrigger, useForwardProps } from 'reka-ui';
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
|
|
8
|
+
// Props
|
|
9
|
+
const props = defineProps<CollapsibleTriggerProps & { class?: HTMLAttributes['class'] }>();
|
|
10
|
+
|
|
11
|
+
const delegatedProps = reactiveOmit(props, 'class');
|
|
12
|
+
const forwardedProps = useForwardProps(delegatedProps);
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<CollapsibleTrigger data-slot="collapsible-trigger" v-bind="forwardedProps" :class="cn(props.class)">
|
|
17
|
+
<slot />
|
|
18
|
+
</CollapsibleTrigger>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ComboboxRootEmits, ComboboxRootProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
5
|
+
import { ComboboxRoot, useForwardPropsEmits } from 'reka-ui';
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
|
|
8
|
+
// Props
|
|
9
|
+
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
|
|
10
|
+
openOnClick: true
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Emits
|
|
14
|
+
const emits = defineEmits<ComboboxRootEmits>();
|
|
15
|
+
|
|
16
|
+
const delegatedProps = reactiveOmit(props, 'class');
|
|
17
|
+
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<ComboboxRoot v-slot="slotProps" data-slot="combobox" v-bind="forwarded" :class="cn('relative', props.class)">
|
|
22
|
+
<slot v-bind="slotProps" />
|
|
23
|
+
</ComboboxRoot>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ComboboxAnchorProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
5
|
+
import { ComboboxAnchor, injectComboboxRootContext, useForwardProps } from 'reka-ui';
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
|
|
8
|
+
// Props
|
|
9
|
+
const props = withDefaults(defineProps<ComboboxAnchorProps & {
|
|
10
|
+
class?: HTMLAttributes['class'];
|
|
11
|
+
size?: 'sm' | 'default';
|
|
12
|
+
}>(), { size: 'default' });
|
|
13
|
+
|
|
14
|
+
const delegatedProps = reactiveOmit(props, 'class', 'size');
|
|
15
|
+
const forwardedProps = useForwardProps(delegatedProps);
|
|
16
|
+
|
|
17
|
+
const rootContext = injectComboboxRootContext();
|
|
18
|
+
|
|
19
|
+
// Clicking the empty area of the field focuses the search input and opens the list
|
|
20
|
+
const handleClick = (event: MouseEvent) => {
|
|
21
|
+
if (rootContext.disabled.value) return;
|
|
22
|
+
const target = event.target as HTMLElement | null;
|
|
23
|
+
if (target?.closest('button, input, a')) return;
|
|
24
|
+
rootContext.inputElement.value?.focus();
|
|
25
|
+
if (!rootContext.open.value) rootContext.onOpenChange(true);
|
|
26
|
+
};
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<ComboboxAnchor data-slot="combobox-anchor" :data-size="props.size" :data-state="rootContext.open.value ? 'open' : 'closed'" :data-disabled="rootContext.disabled.value ? '' : undefined" v-bind="forwardedProps" :class="cn(
|
|
31
|
+
'group/combobox-anchor uv-field flex w-full cursor-text items-center gap-2 rounded border border-input bg-secondary px-4 py-1.5 text-xs font-semibold text-foreground shadow-elevated-sm sm:text-sm',
|
|
32
|
+
'data-[size=default]:min-h-13 data-[size=sm]:min-h-10 data-disabled:cursor-not-allowed data-disabled:opacity-60',
|
|
33
|
+
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 [&_svg:not([class*=\'text-\'])]:text-muted-foreground',
|
|
34
|
+
props.class
|
|
35
|
+
)
|
|
36
|
+
" @click="handleClick">
|
|
37
|
+
<slot />
|
|
38
|
+
</ComboboxAnchor>
|
|
39
|
+
</template>
|