@brumaombra/ui-vintage 0.7.13 → 0.8.0
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 +3602 -0
- package/README.md +49 -9
- package/package.json +29 -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/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 +88 -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 +76 -0
- package/src/i18n/en.json +76 -0
- package/src/i18n/es.json +76 -0
- package/src/i18n/fr.json +76 -0
- package/src/i18n/it.json +76 -0
- package/src/i18n/ja.json +76 -0
- package/src/i18n/pt.json +76 -0
- package/src/i18n/ru.json +76 -0
- package/src/i18n/zh.json +76 -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 { HTMLAttributes } from 'vue';
|
|
3
|
+
import { cn } from '../../../lib/utils';
|
|
4
|
+
|
|
5
|
+
// Props
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
class?: HTMLAttributes['class'];
|
|
8
|
+
pressed?: boolean;
|
|
9
|
+
}>();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<kbd data-slot="kbd" :data-pressed="props.pressed ? '' : undefined" :class="cn(
|
|
14
|
+
'inline-flex h-5 min-w-5 select-none items-center justify-center gap-1 rounded-sm border border-b-2 border-border bg-surface px-1.5 font-mono text-[11px] leading-none font-semibold text-muted-foreground shadow-elevated-sm transition-[translate,border-width,box-shadow] duration-100 ease-snappy [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-3',
|
|
15
|
+
'data-pressed:translate-y-px data-pressed:border-b data-pressed:shadow-none',
|
|
16
|
+
props.class
|
|
17
|
+
)
|
|
18
|
+
">
|
|
19
|
+
<slot />
|
|
20
|
+
</kbd>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'vue';
|
|
3
|
+
import { onMounted, ref } from 'vue';
|
|
4
|
+
import { cn } from '../../../lib/utils';
|
|
5
|
+
import Kbd from './Kbd.vue';
|
|
6
|
+
|
|
7
|
+
// Props
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
class?: HTMLAttributes['class'];
|
|
10
|
+
keys?: string[];
|
|
11
|
+
pressed?: boolean;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
// Default to non-Mac glyphs so SSR output is stable, then refine on the client
|
|
15
|
+
const isMac = ref(false);
|
|
16
|
+
|
|
17
|
+
onMounted(() => {
|
|
18
|
+
isMac.value = /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Map special key names to their display glyph
|
|
22
|
+
const formatKey = (key: string) => {
|
|
23
|
+
switch (key.toLowerCase()) {
|
|
24
|
+
case 'mod':
|
|
25
|
+
return isMac.value ? '⌘' : 'Ctrl';
|
|
26
|
+
case 'cmd':
|
|
27
|
+
case 'meta':
|
|
28
|
+
return isMac.value ? '⌘' : 'Win';
|
|
29
|
+
case 'ctrl':
|
|
30
|
+
case 'control':
|
|
31
|
+
return isMac.value ? '⌃' : 'Ctrl';
|
|
32
|
+
case 'shift':
|
|
33
|
+
return '⇧';
|
|
34
|
+
case 'alt':
|
|
35
|
+
case 'option':
|
|
36
|
+
return isMac.value ? '⌥' : 'Alt';
|
|
37
|
+
case 'enter':
|
|
38
|
+
case 'return':
|
|
39
|
+
return '↵';
|
|
40
|
+
case 'esc':
|
|
41
|
+
case 'escape':
|
|
42
|
+
return 'Esc';
|
|
43
|
+
case 'tab':
|
|
44
|
+
return 'Tab';
|
|
45
|
+
case 'backspace':
|
|
46
|
+
return '⌫';
|
|
47
|
+
case 'up':
|
|
48
|
+
return '↑';
|
|
49
|
+
case 'down':
|
|
50
|
+
return '↓';
|
|
51
|
+
case 'left':
|
|
52
|
+
return '←';
|
|
53
|
+
case 'right':
|
|
54
|
+
return '→';
|
|
55
|
+
case 'space':
|
|
56
|
+
return 'Space';
|
|
57
|
+
default:
|
|
58
|
+
return key.length === 1 ? key.toUpperCase() : key;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<span data-slot="kbd-group" :class="cn('inline-flex items-center gap-1', props.class)">
|
|
65
|
+
<slot>
|
|
66
|
+
<!-- Keys rendered from the keys prop -->
|
|
67
|
+
<Kbd v-for="(key, index) in props.keys ?? []" :key="`${key}-${index}`" :pressed="props.pressed">
|
|
68
|
+
{{ formatKey(key) }}
|
|
69
|
+
</Kbd>
|
|
70
|
+
</slot>
|
|
71
|
+
</span>
|
|
72
|
+
</template>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { NumberFieldRootEmits, NumberFieldRootProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { computed, nextTick, ref, watch } from 'vue';
|
|
5
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
6
|
+
import { MinusSignIcon, PlusSignIcon } from '@hugeicons/core-free-icons';
|
|
7
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
8
|
+
import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFieldRoot, useForwardPropsEmits } from 'reka-ui';
|
|
9
|
+
import { useI18n } from 'vue-i18n';
|
|
10
|
+
import { cn } from '../../../lib/utils';
|
|
11
|
+
|
|
12
|
+
// Props
|
|
13
|
+
const props = defineProps<NumberFieldRootProps & {
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
decrementLabel?: string;
|
|
16
|
+
incrementLabel?: string;
|
|
17
|
+
class?: HTMLAttributes['class'];
|
|
18
|
+
inputClass?: HTMLAttributes['class'];
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
// Emits
|
|
22
|
+
const emits = defineEmits<NumberFieldRootEmits>();
|
|
23
|
+
|
|
24
|
+
const delegatedProps = reactiveOmit(props, 'class', 'inputClass', 'placeholder', 'decrementLabel', 'incrementLabel');
|
|
25
|
+
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
26
|
+
|
|
27
|
+
const { t } = useI18n();
|
|
28
|
+
const resolvedDecrementLabel = computed(() => props.decrementLabel || t('uiVintage.numberField.decrement'));
|
|
29
|
+
const resolvedIncrementLabel = computed(() => props.incrementLabel || t('uiVintage.numberField.increment'));
|
|
30
|
+
|
|
31
|
+
const viewportRef = ref<HTMLElement | null>(null);
|
|
32
|
+
const ghostRef = ref<HTMLElement | null>(null);
|
|
33
|
+
const ghostText = ref('');
|
|
34
|
+
const lastValue = ref<number | null | undefined>(props.modelValue ?? props.defaultValue);
|
|
35
|
+
let ghostAnimation: Animation | null = null;
|
|
36
|
+
|
|
37
|
+
// Keep the reference value in sync with external model updates
|
|
38
|
+
watch(() => props.modelValue, (value) => {
|
|
39
|
+
lastValue.value = value;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Slide the new digits in (and the old ones out) in the direction of the change
|
|
43
|
+
const animateChange = async (value: number) => {
|
|
44
|
+
const previous = lastValue.value;
|
|
45
|
+
lastValue.value = value;
|
|
46
|
+
|
|
47
|
+
const input = viewportRef.value?.querySelector('input');
|
|
48
|
+
if (!input || typeof window === 'undefined' || previous === null || previous === undefined || Number.isNaN(previous) || Number.isNaN(value)) return;
|
|
49
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
50
|
+
|
|
51
|
+
// The DOM still shows the previous text at this point
|
|
52
|
+
const previousText = input.value;
|
|
53
|
+
const direction = value > previous ? 1 : -1;
|
|
54
|
+
ghostAnimation?.cancel();
|
|
55
|
+
ghostText.value = previousText;
|
|
56
|
+
await nextTick();
|
|
57
|
+
|
|
58
|
+
// Skip when the text did not visibly change (e.g. a typed value committed on blur)
|
|
59
|
+
if (input.value === previousText) {
|
|
60
|
+
ghostText.value = '';
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const options: KeyframeAnimationOptions = { duration: 320, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' };
|
|
65
|
+
input.animate([
|
|
66
|
+
{ transform: `translateY(${direction * 60}%)`, opacity: 0, filter: 'blur(2px)' },
|
|
67
|
+
{ transform: 'translateY(0)', opacity: 1, filter: 'blur(0)' }
|
|
68
|
+
], options);
|
|
69
|
+
|
|
70
|
+
if (ghostRef.value) {
|
|
71
|
+
ghostAnimation = ghostRef.value.animate([
|
|
72
|
+
{ transform: 'translateY(0)', opacity: 1, filter: 'blur(0)' },
|
|
73
|
+
{ transform: `translateY(${direction * -60}%)`, opacity: 0, filter: 'blur(2px)' }
|
|
74
|
+
], { ...options, duration: 220, fill: 'forwards' });
|
|
75
|
+
ghostAnimation.onfinish = () => {
|
|
76
|
+
ghostText.value = '';
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<NumberFieldRoot v-slot="slotProps" data-slot="number-field" v-bind="forwarded" :class="cn(
|
|
84
|
+
'uv-field flex h-13 w-full items-center gap-1 rounded border border-input bg-secondary px-1.5 text-foreground shadow-elevated-sm',
|
|
85
|
+
'data-disabled:cursor-not-allowed data-disabled:opacity-60 aria-invalid:animate-uv-shake',
|
|
86
|
+
props.class
|
|
87
|
+
)" @update:model-value="animateChange">
|
|
88
|
+
<!-- Decrement button -->
|
|
89
|
+
<NumberFieldDecrement data-slot="number-field-decrement" :aria-label="resolvedDecrementLabel" :class="cn(
|
|
90
|
+
'inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground outline-none',
|
|
91
|
+
'[transition:scale_320ms_var(--ease-spring),background-color_150ms_var(--ease-snappy),color_150ms_var(--ease-snappy),opacity_150ms_var(--ease-snappy)]',
|
|
92
|
+
'hover:bg-accent hover:text-foreground active:scale-[0.84] data-pressed:scale-[0.84] focus-visible:ring-[3px] focus-visible:ring-ring/45',
|
|
93
|
+
'disabled:pointer-events-none disabled:opacity-35 data-disabled:pointer-events-none data-disabled:opacity-35'
|
|
94
|
+
)">
|
|
95
|
+
<slot name="decrement-icon">
|
|
96
|
+
<HugeiconsIcon :icon="MinusSignIcon" class="size-4" />
|
|
97
|
+
</slot>
|
|
98
|
+
</NumberFieldDecrement>
|
|
99
|
+
|
|
100
|
+
<!-- Value viewport (clips the sliding digits) -->
|
|
101
|
+
<div ref="viewportRef" class="relative flex h-full min-w-0 flex-1 items-center overflow-hidden">
|
|
102
|
+
<NumberFieldInput data-slot="number-field-input" :placeholder="props.placeholder" :class="cn(
|
|
103
|
+
'h-full w-full min-w-0 bg-transparent text-center text-xs font-semibold tabular-nums text-foreground outline-none sm:text-sm',
|
|
104
|
+
'selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground placeholder:opacity-60 disabled:cursor-not-allowed',
|
|
105
|
+
props.inputClass
|
|
106
|
+
)" />
|
|
107
|
+
|
|
108
|
+
<!-- Outgoing value ghost (decorative) -->
|
|
109
|
+
<span v-if="ghostText" ref="ghostRef" aria-hidden="true" :class="cn(
|
|
110
|
+
'pointer-events-none absolute inset-0 flex items-center justify-center text-xs font-semibold tabular-nums text-foreground sm:text-sm',
|
|
111
|
+
props.inputClass
|
|
112
|
+
)">{{ ghostText }}</span>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<!-- Increment button -->
|
|
116
|
+
<NumberFieldIncrement data-slot="number-field-increment" :aria-label="resolvedIncrementLabel" :class="cn(
|
|
117
|
+
'inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground outline-none',
|
|
118
|
+
'[transition:scale_320ms_var(--ease-spring),background-color_150ms_var(--ease-snappy),color_150ms_var(--ease-snappy),opacity_150ms_var(--ease-snappy)]',
|
|
119
|
+
'hover:bg-accent hover:text-foreground active:scale-[0.84] data-pressed:scale-[0.84] focus-visible:ring-[3px] focus-visible:ring-ring/45',
|
|
120
|
+
'disabled:pointer-events-none disabled:opacity-35 data-disabled:pointer-events-none data-disabled:opacity-35'
|
|
121
|
+
)">
|
|
122
|
+
<slot name="increment-icon">
|
|
123
|
+
<HugeiconsIcon :icon="PlusSignIcon" class="size-4" />
|
|
124
|
+
</slot>
|
|
125
|
+
</NumberFieldIncrement>
|
|
126
|
+
|
|
127
|
+
<!-- Extra content (e.g. hidden inputs) -->
|
|
128
|
+
<slot v-bind="slotProps" />
|
|
129
|
+
</NumberFieldRoot>
|
|
130
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as NumberField } from './NumberField.vue';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PaginationRootEmits, PaginationRootProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
5
|
+
import { PaginationRoot, useForwardPropsEmits } from 'reka-ui';
|
|
6
|
+
import { useI18n } from 'vue-i18n';
|
|
7
|
+
import { cn } from '../../../lib/utils';
|
|
8
|
+
|
|
9
|
+
// Props
|
|
10
|
+
const props = defineProps<PaginationRootProps & {
|
|
11
|
+
class?: HTMLAttributes['class'];
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
// Emits
|
|
15
|
+
const emits = defineEmits<PaginationRootEmits>();
|
|
16
|
+
|
|
17
|
+
const delegatedProps = reactiveOmit(props, 'class');
|
|
18
|
+
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
19
|
+
|
|
20
|
+
const { t } = useI18n();
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<PaginationRoot v-slot="slotProps" data-slot="pagination" :aria-label="t('uiVintage.pagination.label')" v-bind="forwarded" :class="cn('mx-auto flex w-full justify-center', props.class)">
|
|
25
|
+
<slot v-bind="slotProps" />
|
|
26
|
+
</PaginationRoot>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PaginationListProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
5
|
+
import { PaginationList } from 'reka-ui';
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
|
|
8
|
+
// Props
|
|
9
|
+
const props = defineProps<PaginationListProps & {
|
|
10
|
+
class?: HTMLAttributes['class'];
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
const delegatedProps = reactiveOmit(props, 'class');
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<PaginationList v-slot="slotProps" data-slot="pagination-content" v-bind="delegatedProps" :class="cn('flex flex-row flex-wrap items-center justify-center gap-1.5', props.class)">
|
|
18
|
+
<slot v-bind="slotProps" />
|
|
19
|
+
</PaginationList>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PaginationEllipsisProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { MoreHorizontalIcon } from '@hugeicons/core-free-icons';
|
|
5
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
6
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
7
|
+
import { PaginationEllipsis } from 'reka-ui';
|
|
8
|
+
import { useI18n } from 'vue-i18n';
|
|
9
|
+
import { cn } from '../../../lib/utils';
|
|
10
|
+
|
|
11
|
+
// Props
|
|
12
|
+
const props = defineProps<PaginationEllipsisProps & {
|
|
13
|
+
class?: HTMLAttributes['class'];
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const delegatedProps = reactiveOmit(props, 'class');
|
|
17
|
+
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<PaginationEllipsis data-slot="pagination-ellipsis" v-bind="delegatedProps" :class="cn('flex size-10 items-center justify-center text-muted-foreground', props.class)">
|
|
23
|
+
<!-- Default slot content -->
|
|
24
|
+
<slot>
|
|
25
|
+
<HugeiconsIcon :icon="MoreHorizontalIcon" class="size-4" aria-hidden="true" />
|
|
26
|
+
</slot>
|
|
27
|
+
|
|
28
|
+
<!-- Screen reader text for accessibility -->
|
|
29
|
+
<span class="sr-only">{{ t('uiVintage.pagination.more') }}</span>
|
|
30
|
+
</PaginationEllipsis>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PaginationFirstProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import type { ButtonVariants } from '../button';
|
|
5
|
+
import { ArrowLeftDoubleIcon } from '@hugeicons/core-free-icons';
|
|
6
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
7
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
8
|
+
import { PaginationFirst } from 'reka-ui';
|
|
9
|
+
import { useI18n } from 'vue-i18n';
|
|
10
|
+
import { cn } from '../../../lib/utils';
|
|
11
|
+
import { buttonVariants } from '../button';
|
|
12
|
+
|
|
13
|
+
// Props
|
|
14
|
+
const props = withDefaults(defineProps<PaginationFirstProps & {
|
|
15
|
+
size?: ButtonVariants['size'];
|
|
16
|
+
class?: HTMLAttributes['class'];
|
|
17
|
+
}>(), {
|
|
18
|
+
size: 'icon-sm',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const delegatedProps = reactiveOmit(props, 'class', 'size');
|
|
22
|
+
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<PaginationFirst data-slot="pagination-first" :aria-label="t('uiVintage.pagination.first')" v-bind="delegatedProps" :class="cn(buttonVariants({ variant: 'secondary', size: props.size }), 'group/pagination-nav', props.class)">
|
|
28
|
+
<slot>
|
|
29
|
+
<HugeiconsIcon :icon="ArrowLeftDoubleIcon" class="size-4 transition-transform duration-220 ease-spring group-hover/pagination-nav:-translate-x-0.5" aria-hidden="true" />
|
|
30
|
+
</slot>
|
|
31
|
+
</PaginationFirst>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PaginationListItemProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import type { ButtonVariants } from '../button';
|
|
5
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
6
|
+
import { PaginationListItem } from 'reka-ui';
|
|
7
|
+
import { useI18n } from 'vue-i18n';
|
|
8
|
+
import { cn } from '../../../lib/utils';
|
|
9
|
+
import { buttonVariants } from '../button';
|
|
10
|
+
|
|
11
|
+
// Props
|
|
12
|
+
const props = withDefaults(defineProps<PaginationListItemProps & {
|
|
13
|
+
size?: ButtonVariants['size'];
|
|
14
|
+
class?: HTMLAttributes['class'];
|
|
15
|
+
}>(), {
|
|
16
|
+
size: 'icon-sm',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const delegatedProps = reactiveOmit(props, 'class', 'size');
|
|
20
|
+
|
|
21
|
+
const { t } = useI18n();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<PaginationListItem data-slot="pagination-item" :aria-label="t('uiVintage.pagination.page', { page: props.value })" v-bind="delegatedProps" :class="cn(
|
|
26
|
+
buttonVariants({ variant: 'secondary', size: props.size }),
|
|
27
|
+
'tabular-nums',
|
|
28
|
+
'data-selected:border-primary data-selected:bg-primary data-selected:text-primary-foreground data-selected:shadow-glow data-selected:hover:border-primary data-selected:hover:bg-primary-hover',
|
|
29
|
+
'data-selected:animate-[uv-pagination-pop_380ms_var(--ease-bounce)]',
|
|
30
|
+
props.class
|
|
31
|
+
)">
|
|
32
|
+
<slot>{{ props.value }}</slot>
|
|
33
|
+
</PaginationListItem>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
@keyframes uv-pagination-pop {
|
|
38
|
+
0% { transform: scale(0.82); }
|
|
39
|
+
100% { transform: scale(1); }
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PaginationLastProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import type { ButtonVariants } from '../button';
|
|
5
|
+
import { ArrowRightDoubleIcon } from '@hugeicons/core-free-icons';
|
|
6
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
7
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
8
|
+
import { PaginationLast } from 'reka-ui';
|
|
9
|
+
import { useI18n } from 'vue-i18n';
|
|
10
|
+
import { cn } from '../../../lib/utils';
|
|
11
|
+
import { buttonVariants } from '../button';
|
|
12
|
+
|
|
13
|
+
// Props
|
|
14
|
+
const props = withDefaults(defineProps<PaginationLastProps & {
|
|
15
|
+
size?: ButtonVariants['size'];
|
|
16
|
+
class?: HTMLAttributes['class'];
|
|
17
|
+
}>(), {
|
|
18
|
+
size: 'icon-sm',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const delegatedProps = reactiveOmit(props, 'class', 'size');
|
|
22
|
+
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<PaginationLast data-slot="pagination-last" :aria-label="t('uiVintage.pagination.last')" v-bind="delegatedProps" :class="cn(buttonVariants({ variant: 'secondary', size: props.size }), 'group/pagination-nav', props.class)">
|
|
28
|
+
<slot>
|
|
29
|
+
<HugeiconsIcon :icon="ArrowRightDoubleIcon" class="size-4 transition-transform duration-220 ease-spring group-hover/pagination-nav:translate-x-0.5" aria-hidden="true" />
|
|
30
|
+
</slot>
|
|
31
|
+
</PaginationLast>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PaginationNextProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import type { ButtonVariants } from '../button';
|
|
5
|
+
import { ArrowRight01Icon } from '@hugeicons/core-free-icons';
|
|
6
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
7
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
8
|
+
import { PaginationNext } from 'reka-ui';
|
|
9
|
+
import { useI18n } from 'vue-i18n';
|
|
10
|
+
import { cn } from '../../../lib/utils';
|
|
11
|
+
import { buttonVariants } from '../button';
|
|
12
|
+
|
|
13
|
+
// Props
|
|
14
|
+
const props = withDefaults(defineProps<PaginationNextProps & {
|
|
15
|
+
size?: ButtonVariants['size'];
|
|
16
|
+
class?: HTMLAttributes['class'];
|
|
17
|
+
}>(), {
|
|
18
|
+
size: 'icon-sm',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const delegatedProps = reactiveOmit(props, 'class', 'size');
|
|
22
|
+
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<PaginationNext data-slot="pagination-next" :aria-label="t('uiVintage.pagination.next')" v-bind="delegatedProps" :class="cn(buttonVariants({ variant: 'secondary', size: props.size }), 'group/pagination-nav', props.class)">
|
|
28
|
+
<slot>
|
|
29
|
+
<HugeiconsIcon :icon="ArrowRight01Icon" class="size-4 transition-transform duration-220 ease-spring group-hover/pagination-nav:translate-x-0.5" aria-hidden="true" />
|
|
30
|
+
</slot>
|
|
31
|
+
</PaginationNext>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PaginationPrevProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import type { ButtonVariants } from '../button';
|
|
5
|
+
import { ArrowLeft01Icon } from '@hugeicons/core-free-icons';
|
|
6
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
7
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
8
|
+
import { PaginationPrev } from 'reka-ui';
|
|
9
|
+
import { useI18n } from 'vue-i18n';
|
|
10
|
+
import { cn } from '../../../lib/utils';
|
|
11
|
+
import { buttonVariants } from '../button';
|
|
12
|
+
|
|
13
|
+
// Props
|
|
14
|
+
const props = withDefaults(defineProps<PaginationPrevProps & {
|
|
15
|
+
size?: ButtonVariants['size'];
|
|
16
|
+
class?: HTMLAttributes['class'];
|
|
17
|
+
}>(), {
|
|
18
|
+
size: 'icon-sm',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const delegatedProps = reactiveOmit(props, 'class', 'size');
|
|
22
|
+
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<PaginationPrev data-slot="pagination-previous" :aria-label="t('uiVintage.pagination.previous')" v-bind="delegatedProps" :class="cn(buttonVariants({ variant: 'secondary', size: props.size }), 'group/pagination-nav', props.class)">
|
|
28
|
+
<slot>
|
|
29
|
+
<HugeiconsIcon :icon="ArrowLeft01Icon" class="size-4 transition-transform duration-220 ease-spring group-hover/pagination-nav:-translate-x-0.5" aria-hidden="true" />
|
|
30
|
+
</slot>
|
|
31
|
+
</PaginationPrev>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'vue';
|
|
3
|
+
import Pagination from './Pagination.vue';
|
|
4
|
+
import PaginationContent from './PaginationContent.vue';
|
|
5
|
+
import PaginationEllipsis from './PaginationEllipsis.vue';
|
|
6
|
+
import PaginationFirst from './PaginationFirst.vue';
|
|
7
|
+
import PaginationItem from './PaginationItem.vue';
|
|
8
|
+
import PaginationLast from './PaginationLast.vue';
|
|
9
|
+
import PaginationNext from './PaginationNext.vue';
|
|
10
|
+
import PaginationPrevious from './PaginationPrevious.vue';
|
|
11
|
+
|
|
12
|
+
// Props
|
|
13
|
+
const props = withDefaults(defineProps<{
|
|
14
|
+
total: number;
|
|
15
|
+
itemsPerPage?: number;
|
|
16
|
+
siblingCount?: number;
|
|
17
|
+
showEdges?: boolean;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
class?: HTMLAttributes['class'];
|
|
20
|
+
}>(), {
|
|
21
|
+
itemsPerPage: 10,
|
|
22
|
+
siblingCount: 1,
|
|
23
|
+
showEdges: true,
|
|
24
|
+
disabled: false,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Current page (1-based, v-model:page)
|
|
28
|
+
const page = defineModel<number>('page', { default: 1 });
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<Pagination v-model:page="page" :total="props.total" :items-per-page="props.itemsPerPage" :sibling-count="props.siblingCount" :show-edges="props.showEdges" :disabled="props.disabled" :class="props.class">
|
|
33
|
+
<PaginationContent v-slot="{ items }">
|
|
34
|
+
<!-- Jump to start / previous -->
|
|
35
|
+
<PaginationFirst class="hidden sm:inline-flex" />
|
|
36
|
+
<PaginationPrevious />
|
|
37
|
+
|
|
38
|
+
<!-- Page numbers and gaps -->
|
|
39
|
+
<template v-for="(item, index) in items" :key="item.type === 'page' ? `page-${item.value}` : `ellipsis-${index}`">
|
|
40
|
+
<PaginationItem v-if="item.type === 'page'" :value="item.value" />
|
|
41
|
+
<PaginationEllipsis v-else />
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<!-- Next / jump to end -->
|
|
45
|
+
<PaginationNext />
|
|
46
|
+
<PaginationLast class="hidden sm:inline-flex" />
|
|
47
|
+
</PaginationContent>
|
|
48
|
+
</Pagination>
|
|
49
|
+
</template>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as Pagination } from './Pagination.vue';
|
|
2
|
+
export { default as PaginationContent } from './PaginationContent.vue';
|
|
3
|
+
export { default as PaginationEllipsis } from './PaginationEllipsis.vue';
|
|
4
|
+
export { default as PaginationFirst } from './PaginationFirst.vue';
|
|
5
|
+
export { default as PaginationItem } from './PaginationItem.vue';
|
|
6
|
+
export { default as PaginationLast } from './PaginationLast.vue';
|
|
7
|
+
export { default as PaginationNext } from './PaginationNext.vue';
|
|
8
|
+
export { default as PaginationPrevious } from './PaginationPrevious.vue';
|
|
9
|
+
export { default as SimplePagination } from './SimplePagination.vue';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts" generic="Type extends 'text' | 'number' = 'text'">
|
|
2
|
+
import type { PinInputRootEmits, PinInputRootProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { computed, provide } from 'vue';
|
|
5
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
6
|
+
import { PinInputRoot, useForwardPropsEmits } from 'reka-ui';
|
|
7
|
+
import { cn } from '../../../lib/utils';
|
|
8
|
+
import { pinInputContextKey } from './context';
|
|
9
|
+
import PinInputInput from './PinInputInput.vue';
|
|
10
|
+
|
|
11
|
+
// Props
|
|
12
|
+
const props = defineProps<PinInputRootProps<Type> & {
|
|
13
|
+
length?: number;
|
|
14
|
+
invalid?: boolean;
|
|
15
|
+
separatorAfter?: number;
|
|
16
|
+
class?: HTMLAttributes['class'];
|
|
17
|
+
inputClass?: HTMLAttributes['class'];
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
// Emits
|
|
21
|
+
const emits = defineEmits<PinInputRootEmits<Type>>();
|
|
22
|
+
|
|
23
|
+
const delegatedProps = reactiveOmit(props, 'class', 'inputClass', 'length', 'invalid', 'separatorAfter');
|
|
24
|
+
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
25
|
+
|
|
26
|
+
// Share the invalid state with the cells (also when they are passed through the slot)
|
|
27
|
+
provide(pinInputContextKey, {
|
|
28
|
+
invalid: computed(() => !!props.invalid)
|
|
29
|
+
});
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<PinInputRoot v-slot="slotProps" data-slot="pin-input" v-bind="forwarded" :aria-invalid="props.invalid || undefined" :class="cn(
|
|
34
|
+
'flex items-center gap-2 has-disabled:opacity-60',
|
|
35
|
+
props.invalid && 'animate-uv-shake',
|
|
36
|
+
props.class
|
|
37
|
+
)">
|
|
38
|
+
<slot v-bind="slotProps">
|
|
39
|
+
<!-- Auto-rendered cells -->
|
|
40
|
+
<template v-for="index in props.length ?? 0" :key="index">
|
|
41
|
+
<PinInputInput :index="index - 1" :class="props.inputClass" />
|
|
42
|
+
|
|
43
|
+
<!-- Separator -->
|
|
44
|
+
<span v-if="props.separatorAfter === index - 1 && index < (props.length ?? 0)" data-slot="pin-input-separator" aria-hidden="true" class="mx-0.5 h-0.5 w-3 shrink-0 rounded-full bg-border-strong" />
|
|
45
|
+
</template>
|
|
46
|
+
</slot>
|
|
47
|
+
</PinInputRoot>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PinInputInputProps } from 'reka-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'vue';
|
|
4
|
+
import { computed, inject } from 'vue';
|
|
5
|
+
import { reactiveOmit } from '@vueuse/core';
|
|
6
|
+
import { injectPinInputRootContext, PinInputInput, useForwardProps } from 'reka-ui';
|
|
7
|
+
import { cn } from '../../../lib/utils';
|
|
8
|
+
import { pinInputContextKey } from './context';
|
|
9
|
+
|
|
10
|
+
defineOptions({ inheritAttrs: false });
|
|
11
|
+
|
|
12
|
+
// Props
|
|
13
|
+
const props = defineProps<PinInputInputProps & {
|
|
14
|
+
class?: HTMLAttributes['class'];
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const delegatedProps = reactiveOmit(props, 'class');
|
|
18
|
+
const forwardedProps = useForwardProps(delegatedProps);
|
|
19
|
+
|
|
20
|
+
const rootContext = injectPinInputRootContext();
|
|
21
|
+
const pinInputContext = inject(pinInputContextKey, null);
|
|
22
|
+
|
|
23
|
+
// Current character of this cell
|
|
24
|
+
const cellValue = computed(() => {
|
|
25
|
+
const value = rootContext.currentModelValue.value[props.index];
|
|
26
|
+
return value === undefined || value === null ? '' : String(value);
|
|
27
|
+
});
|
|
28
|
+
const isFilled = computed(() => cellValue.value !== '');
|
|
29
|
+
const displayValue = computed(() => (rootContext.mask.value ? '•' : cellValue.value));
|
|
30
|
+
const isInvalid = computed(() => pinInputContext?.invalid.value || undefined);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<span data-slot="pin-input-cell" class="relative inline-flex shrink-0">
|
|
35
|
+
<!-- Native input (text is transparent, the overlay below renders the character) -->
|
|
36
|
+
<PinInputInput data-slot="pin-input-input" v-bind="{ ...$attrs, ...forwardedProps }" :aria-invalid="isInvalid" :data-filled="isFilled ? '' : undefined" :class="cn(
|
|
37
|
+
'uv-field size-12 rounded border border-input bg-secondary text-center text-lg font-bold text-transparent caret-primary shadow-elevated-sm outline-none sm:size-13',
|
|
38
|
+
'placeholder:text-muted-foreground placeholder:opacity-50 selection:bg-primary/25',
|
|
39
|
+
'data-filled:border-primary/60 data-filled:bg-primary/5',
|
|
40
|
+
'disabled:cursor-not-allowed disabled:opacity-60',
|
|
41
|
+
props.class
|
|
42
|
+
)" />
|
|
43
|
+
|
|
44
|
+
<!-- Animated character (re-keyed on every value so it pops) -->
|
|
45
|
+
<span v-if="isFilled" :key="cellValue" aria-hidden="true" class="pointer-events-none absolute inset-0 flex animate-uv-pop items-center justify-center text-lg font-bold text-foreground">
|
|
46
|
+
{{ displayValue }}
|
|
47
|
+
</span>
|
|
48
|
+
</span>
|
|
49
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue';
|
|
2
|
+
|
|
3
|
+
// Shared state provided by PinInput to its cells
|
|
4
|
+
export interface PinInputContext {
|
|
5
|
+
invalid: ComputedRef<boolean>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const pinInputContextKey: InjectionKey<PinInputContext> = Symbol('uvPinInput');
|