@brumaombra/ui-vintage 0.8.2 → 0.9.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/AGENTS.md +41 -0
- package/COMPONENTS.md +88 -17
- package/README.md +296 -262
- package/module.mjs +5 -11
- package/package.json +30 -12
- package/src/components/blog/BlogContentRenderer.vue +1 -1
- package/src/components/blog/BlogHeaderSection.vue +93 -19
- package/src/components/blog/BlogInfoSection.vue +39 -36
- package/src/components/blog/BlogPostHeader.vue +181 -0
- package/src/components/blog/BlogProgressBar.vue +3 -5
- package/src/components/blog/BlogSectionTitle.vue +8 -6
- package/src/components/blog/BlogTagsSection.vue +16 -7
- package/src/components/blog/CategoryCard.vue +18 -23
- package/src/components/blog/HeaderCarousel.vue +15 -6
- package/src/components/blog/PostCard.vue +13 -20
- package/src/components/blog/SocialShareSidebar.vue +24 -14
- package/src/components/blog/TableOfContents.vue +61 -28
- package/src/components/blog/index.ts +1 -0
- package/src/components/card-grid/CardGrid.vue +2 -2
- package/src/components/confirm-dialog/ConfirmDialog.vue +5 -0
- package/src/components/confirm-dialog/confirm-dialog-state.ts +22 -22
- package/src/components/content/BlogList.vue +28 -52
- package/src/components/content/BlogTable.vue +26 -30
- package/src/components/content/CodeBlock.vue +96 -22
- package/src/components/content/Flow.vue +29 -35
- package/src/components/content/ProseHr.vue +5 -5
- package/src/components/content/ProsePre.vue +9 -4
- package/src/components/content/Terminal.vue +42 -20
- package/src/components/cookie-consent/CookieConsent.vue +1 -1
- package/src/components/dashboard-shell/DashboardShell.vue +24 -7
- package/src/components/empty-state-card/EmptyStateCard.vue +2 -2
- package/src/components/error-page/ErrorPage.vue +3 -3
- package/src/components/info-card/InfoCard.vue +4 -4
- package/src/components/landing/landing-footer/LandingFooter.vue +16 -11
- package/src/components/landing/landing-navbar/LandingNavbar.vue +22 -6
- package/src/components/landing/landing-shell/LandingShell.vue +2 -7
- package/src/components/language-flag/index.ts +1 -0
- package/src/components/language-selector/LanguageSelector.vue +18 -10
- package/src/components/load-more-button/LoadMoreButton.vue +7 -12
- package/src/components/loading-state-card/LoadingStateCard.vue +2 -2
- package/src/components/message-dialog/MessageDialog.vue +11 -11
- package/src/components/message-dialog/message-dialog-state.ts +1 -4
- package/src/components/message-toast/MessageToast.vue +24 -9
- package/src/components/message-toast/message-toast-state.ts +20 -0
- package/src/components/page-header/PageHeader.vue +1 -1
- package/src/components/progress-component/ProgressComponent.vue +7 -3
- package/src/components/single-value-card/SingleValueCard.vue +37 -39
- package/src/components/slider-form-component/SliderFormComponent.vue +1 -1
- package/src/components/stat-strip/StatStrip.vue +36 -0
- package/src/components/stat-strip/index.ts +2 -0
- package/src/components/switch-form-component/SwitchFormComponent.vue +5 -5
- package/src/components/text-link/TextLink.vue +1 -1
- package/src/components/theme-selector/ThemeSelector.vue +2 -2
- package/src/components/theme-selector/theme-transition.ts +15 -3
- package/src/components/ui/accordion/Accordion.vue +18 -18
- package/src/components/ui/alert/AlertTitle.vue +1 -1
- package/src/components/ui/alert/index.ts +1 -1
- package/src/components/ui/avatar/AvatarFallback.vue +2 -1
- package/src/components/ui/avatar/AvatarGroup.vue +13 -3
- package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +2 -2
- package/src/components/ui/busy/Busy.vue +5 -7
- package/src/components/ui/button/Button.vue +4 -2
- package/src/components/ui/button/index.ts +1 -4
- package/src/components/ui/calendar/Calendar.vue +12 -7
- package/src/components/ui/calendar/CalendarCell.vue +1 -1
- package/src/components/ui/calendar/CalendarCellTrigger.vue +1 -1
- package/src/components/ui/calendar/CalendarHeadCell.vue +1 -1
- package/src/components/ui/calendar/CalendarNextButton.vue +1 -1
- package/src/components/ui/calendar/CalendarPrevButton.vue +1 -1
- package/src/components/ui/card/Card.vue +5 -15
- package/src/components/ui/checkbox/Checkbox.vue +5 -1
- package/src/components/ui/combobox/ComboboxInput.vue +5 -1
- package/src/components/ui/command/CommandGroup.vue +17 -3
- package/src/components/ui/command/CommandInput.vue +1 -1
- package/src/components/ui/command/CommandList.vue +5 -1
- package/src/components/ui/data-table/DataTable.vue +11 -3
- package/src/components/ui/date-picker/DatePicker.vue +7 -1
- package/src/components/ui/date-time-picker/DateTimePicker.vue +5 -1
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +1 -1
- package/src/components/ui/dropdown-menu/classes.ts +2 -0
- package/src/components/ui/field/Field.vue +4 -0
- package/src/components/ui/field/FieldLabel.vue +6 -2
- package/src/components/ui/field/field-context.ts +62 -0
- package/src/components/ui/field/index.ts +1 -0
- package/src/components/ui/file-dropzone/FileDropzone.vue +4 -4
- package/src/components/ui/input/Input.vue +5 -1
- package/src/components/ui/native-select/NativeSelect.vue +6 -2
- package/src/components/ui/number-field/NumberField.vue +5 -1
- package/src/components/ui/pagination/PaginationItem.vue +1 -1
- package/src/components/ui/pin-input/PinInputInput.vue +2 -2
- package/src/components/ui/progress/Progress.vue +2 -2
- package/src/components/ui/radio-group/RadioGroupCard.vue +1 -1
- package/src/components/ui/select/SelectItem.vue +1 -1
- package/src/components/ui/select/SelectItemContent.vue +1 -1
- package/src/components/ui/select/SelectTrigger.vue +5 -1
- package/src/components/ui/sidebar/Sidebar.vue +1 -1
- package/src/components/ui/sidebar/SidebarGroupAction.vue +1 -1
- package/src/components/ui/sidebar/SidebarGroupLabel.vue +1 -1
- package/src/components/ui/sidebar/SidebarInset.vue +1 -1
- package/src/components/ui/sidebar/SidebarMenuAction.vue +1 -1
- package/src/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
- package/src/components/ui/sidebar/SidebarMenuSkeleton.vue +2 -2
- package/src/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
- package/src/components/ui/sidebar/SidebarProvider.vue +17 -6
- package/src/components/ui/sidebar/index.ts +2 -2
- package/src/components/ui/sidebar/utils.ts +1 -1
- package/src/components/ui/slider/Slider.vue +5 -4
- package/src/components/ui/spinner/Spinner.vue +3 -2
- package/src/components/ui/stepper/StepperDescription.vue +5 -3
- package/src/components/ui/stepper/StepperIndicator.vue +2 -2
- package/src/components/ui/stepper/StepperTitle.vue +5 -3
- package/src/components/ui/switch/Switch.vue +6 -2
- package/src/components/ui/tabs/TabsList.vue +1 -1
- package/src/components/ui/tabs/TabsTrigger.vue +1 -1
- package/src/components/ui/textarea/Textarea.vue +5 -1
- package/src/components/ui/time-picker/TimePicker.vue +2 -2
- package/src/components/ui/toggle-group/ToggleGroup.vue +1 -1
- package/src/components/ui/tooltip/TooltipContent.vue +1 -1
- package/src/i18n/de.json +16 -6
- package/src/i18n/en.json +16 -6
- package/src/i18n/es.json +16 -6
- package/src/i18n/fr.json +16 -6
- package/src/i18n/it.json +16 -6
- package/src/i18n/ja.json +16 -6
- package/src/i18n/pt.json +16 -6
- package/src/i18n/ru.json +16 -6
- package/src/i18n/zh.json +16 -6
- package/src/lib/color-tokens.ts +1 -1
- package/src/lib/i18n.ts +32 -4
- package/src/lib/utils.ts +21 -1
- package/src/runtime/ui-vintage-i18n-plugin.ts +11 -1
- package/src/styles.css +12 -16
- package/skills/ui-vintage/SKILL.md +0 -42
|
@@ -26,7 +26,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
|
|
26
26
|
// Disabled
|
|
27
27
|
'data-disabled:text-muted-foreground data-disabled:opacity-50',
|
|
28
28
|
// Unavailable
|
|
29
|
-
'data-unavailable:text-destructive
|
|
29
|
+
'data-unavailable:text-destructive data-unavailable:line-through',
|
|
30
30
|
// Outside months
|
|
31
31
|
'data-outside-view:text-muted-foreground',
|
|
32
32
|
props.class,
|
|
@@ -15,7 +15,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
|
|
15
15
|
|
|
16
16
|
<template>
|
|
17
17
|
<CalendarHeadCell data-slot="calendar-head-cell" :class="cn(
|
|
18
|
-
'text-muted-foreground rounded
|
|
18
|
+
'text-muted-foreground rounded flex-1 font-normal text-[0.8rem]',
|
|
19
19
|
props.class,
|
|
20
20
|
)
|
|
21
21
|
" v-bind="forwardedProps">
|
|
@@ -22,7 +22,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
|
|
22
22
|
:class="
|
|
23
23
|
cn(
|
|
24
24
|
buttonVariants({ variant: 'secondary' }),
|
|
25
|
-
'size-7 min-h-10 bg-transparent p-0 text-
|
|
25
|
+
'size-7 min-h-10 bg-transparent p-0 text-muted-foreground hover:text-foreground',
|
|
26
26
|
props.class,
|
|
27
27
|
)
|
|
28
28
|
"
|
|
@@ -22,7 +22,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
|
|
22
22
|
:class="
|
|
23
23
|
cn(
|
|
24
24
|
buttonVariants({ variant: 'secondary' }),
|
|
25
|
-
'size-7 min-h-10 bg-transparent p-0 text-
|
|
25
|
+
'size-7 min-h-10 bg-transparent p-0 text-muted-foreground hover:text-foreground',
|
|
26
26
|
props.class,
|
|
27
27
|
)
|
|
28
28
|
"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed
|
|
2
|
+
import { computed } from 'vue';
|
|
3
3
|
import type { HTMLAttributes } from 'vue';
|
|
4
4
|
import {
|
|
5
5
|
getSurfaceToneClasses,
|
|
@@ -18,8 +18,6 @@ const props = withDefaults(defineProps<{
|
|
|
18
18
|
interactive: false
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
const cardRef = ref<HTMLElement | null>(null);
|
|
22
|
-
|
|
23
21
|
// Get the appropriate classes
|
|
24
22
|
const colorClasses = computed(() => {
|
|
25
23
|
// If color is not specified or is "default", use the default card styles
|
|
@@ -30,23 +28,15 @@ const colorClasses = computed(() => {
|
|
|
30
28
|
// For other colors, use the corresponding surface tone classes
|
|
31
29
|
return getSurfaceToneClasses(props.color);
|
|
32
30
|
});
|
|
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
|
-
};
|
|
41
31
|
</script>
|
|
42
32
|
|
|
43
33
|
<template>
|
|
44
|
-
<div
|
|
45
|
-
'relative flex flex-col gap-4 sm:gap-6
|
|
34
|
+
<div data-slot="card" :data-interactive="props.interactive ? '' : undefined" :class="cn(
|
|
35
|
+
'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',
|
|
46
36
|
colorClasses,
|
|
47
|
-
props.interactive && 'cursor-pointer hover:-translate-y-0.5 hover:
|
|
37
|
+
props.interactive && 'cursor-pointer hover:-translate-y-0.5 hover:shadow-elevated-md active:translate-y-0 active:duration-100',
|
|
48
38
|
props.class,
|
|
49
|
-
)"
|
|
39
|
+
)">
|
|
50
40
|
<slot />
|
|
51
41
|
</div>
|
|
52
42
|
</template>
|
|
@@ -4,6 +4,7 @@ import type { HTMLAttributes } from 'vue';
|
|
|
4
4
|
import { reactiveOmit } from '@vueuse/core';
|
|
5
5
|
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui';
|
|
6
6
|
import { cn } from '../../../lib/utils';
|
|
7
|
+
import { useFieldControlId } from '../field/field-context';
|
|
7
8
|
|
|
8
9
|
// Props
|
|
9
10
|
const props = defineProps<CheckboxRootProps & {
|
|
@@ -15,10 +16,13 @@ const emits = defineEmits<CheckboxRootEmits>();
|
|
|
15
16
|
|
|
16
17
|
const delegatedProps = reactiveOmit(props, 'class');
|
|
17
18
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
19
|
+
|
|
20
|
+
// Let a surrounding Field label point to this control
|
|
21
|
+
const fieldControlId = useFieldControlId(() => props.id);
|
|
18
22
|
</script>
|
|
19
23
|
|
|
20
24
|
<template>
|
|
21
|
-
<CheckboxRoot v-slot="slotProps" data-slot="checkbox" v-bind="forwarded" :class="cn(
|
|
25
|
+
<CheckboxRoot v-slot="slotProps" data-slot="checkbox" v-bind="forwarded" :id="fieldControlId" :class="cn(
|
|
22
26
|
'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
27
|
'[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
28
|
'hover:border-border-strong active:scale-[0.86] focus-visible:ring-[3px] focus-visible:ring-ring/45',
|
|
@@ -4,6 +4,7 @@ import type { HTMLAttributes } from 'vue';
|
|
|
4
4
|
import { reactiveOmit } from '@vueuse/core';
|
|
5
5
|
import { ComboboxInput, useForwardPropsEmits } from 'reka-ui';
|
|
6
6
|
import { cn } from '../../../lib/utils';
|
|
7
|
+
import { useFieldControlId } from '../field/field-context';
|
|
7
8
|
|
|
8
9
|
defineOptions({
|
|
9
10
|
inheritAttrs: false
|
|
@@ -17,10 +18,13 @@ const emits = defineEmits<ComboboxInputEmits>();
|
|
|
17
18
|
|
|
18
19
|
const delegatedProps = reactiveOmit(props, 'class');
|
|
19
20
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
21
|
+
|
|
22
|
+
// Let a surrounding Field label point to this control
|
|
23
|
+
const fieldControlId = useFieldControlId();
|
|
20
24
|
</script>
|
|
21
25
|
|
|
22
26
|
<template>
|
|
23
|
-
<ComboboxInput data-slot="combobox-input" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
|
|
27
|
+
<ComboboxInput :id="fieldControlId" data-slot="combobox-input" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
|
|
24
28
|
'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
29
|
props.class
|
|
26
30
|
)
|
|
@@ -11,10 +11,24 @@ import { provideCommandGroupContext, useCommand } from '.';
|
|
|
11
11
|
const props = defineProps<ListboxGroupProps & {
|
|
12
12
|
class?: HTMLAttributes['class'];
|
|
13
13
|
heading?: string;
|
|
14
|
+
stagger?: boolean;
|
|
14
15
|
}>();
|
|
15
16
|
|
|
16
|
-
// Omit local
|
|
17
|
-
const delegatedProps = reactiveOmit(props, 'class');
|
|
17
|
+
// Omit local props from delegated props
|
|
18
|
+
const delegatedProps = reactiveOmit(props, 'class', 'stagger');
|
|
19
|
+
|
|
20
|
+
// Items float in one after another when the group mounts (same timing as the dropdown menu)
|
|
21
|
+
const staggerClass = [
|
|
22
|
+
'[&>*]:animate-[uv-float-in_240ms_var(--ease-out-expo)_both] [&>*]:[--uv-float-offset:-3px]',
|
|
23
|
+
'[&>*:nth-child(2)]:[animation-delay:16ms]',
|
|
24
|
+
'[&>*:nth-child(3)]:[animation-delay:32ms]',
|
|
25
|
+
'[&>*:nth-child(4)]:[animation-delay:48ms]',
|
|
26
|
+
'[&>*:nth-child(5)]:[animation-delay:64ms]',
|
|
27
|
+
'[&>*:nth-child(6)]:[animation-delay:80ms]',
|
|
28
|
+
'[&>*:nth-child(7)]:[animation-delay:96ms]',
|
|
29
|
+
'[&>*:nth-child(8)]:[animation-delay:112ms]',
|
|
30
|
+
'[&>*:nth-child(n+9)]:[animation-delay:128ms]'
|
|
31
|
+
].join(' ');
|
|
18
32
|
|
|
19
33
|
const { allGroups, filterState } = useCommand();
|
|
20
34
|
const id = useId();
|
|
@@ -37,7 +51,7 @@ onUnmounted(() => {
|
|
|
37
51
|
<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
52
|
{{ heading }}
|
|
39
53
|
</ListboxGroupLabel>
|
|
40
|
-
<div class="p-2">
|
|
54
|
+
<div :class="cn('p-2', props.stagger && staggerClass)">
|
|
41
55
|
<slot />
|
|
42
56
|
</div>
|
|
43
57
|
</ListboxGroup>
|
|
@@ -27,7 +27,7 @@ const { filterState } = useCommand();
|
|
|
27
27
|
<div data-slot="command-input-wrapper" class="flex h-12 items-center gap-3 border-b border-border px-4">
|
|
28
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
|
-
'placeholder:text-muted-foreground flex h-10 w-full rounded
|
|
30
|
+
'placeholder:text-muted-foreground flex h-10 w-full rounded bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
|
31
31
|
props.class,
|
|
32
32
|
)
|
|
33
33
|
" />
|
|
@@ -3,6 +3,7 @@ import type { ListboxContentProps } from 'reka-ui';
|
|
|
3
3
|
import type { HTMLAttributes } from 'vue';
|
|
4
4
|
import { reactiveOmit } from '@vueuse/core';
|
|
5
5
|
import { ListboxContent, useForwardProps } from 'reka-ui';
|
|
6
|
+
import { useI18n } from 'vue-i18n';
|
|
6
7
|
import { cn } from '../../../lib/utils';
|
|
7
8
|
|
|
8
9
|
// Props
|
|
@@ -13,10 +14,13 @@ const delegatedProps = reactiveOmit(props, 'class');
|
|
|
13
14
|
|
|
14
15
|
// Forward props
|
|
15
16
|
const forwarded = useForwardProps(delegatedProps);
|
|
17
|
+
|
|
18
|
+
// Name the list of results for screen readers (an aria-label passed by the app wins)
|
|
19
|
+
const { t } = useI18n();
|
|
16
20
|
</script>
|
|
17
21
|
|
|
18
22
|
<template>
|
|
19
|
-
<ListboxContent data-slot="command-list" v-bind="forwarded" :class="cn(
|
|
23
|
+
<ListboxContent data-slot="command-list" :aria-label="t('uiVintage.commandDialog.title')" v-bind="forwarded" :class="cn(
|
|
20
24
|
'max-h-75 scroll-py-1 overflow-x-hidden overflow-y-auto',
|
|
21
25
|
props.class,
|
|
22
26
|
)
|
|
@@ -41,7 +41,7 @@ const props = withDefaults(defineProps<{
|
|
|
41
41
|
const emits = defineEmits<{
|
|
42
42
|
'update:selected': [value: PropertyKey[]];
|
|
43
43
|
'update:sort': [value: DataTableSort | null];
|
|
44
|
-
'row-click': [row: T, event: MouseEvent];
|
|
44
|
+
'row-click': [row: T, event: MouseEvent | KeyboardEvent];
|
|
45
45
|
}>();
|
|
46
46
|
|
|
47
47
|
// Slots
|
|
@@ -132,6 +132,14 @@ const handleRowClick = (row: T, event: MouseEvent) => {
|
|
|
132
132
|
if ((event.target as HTMLElement).closest('button, a, input, [role=checkbox]')) return;
|
|
133
133
|
emits('row-click', row, event);
|
|
134
134
|
};
|
|
135
|
+
|
|
136
|
+
// Let keyboard users activate a focused clickable row with Enter or Space
|
|
137
|
+
const handleRowKeydown = (row: T, event: KeyboardEvent) => {
|
|
138
|
+
if (!props.rowClickable || event.target !== event.currentTarget) return;
|
|
139
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
emits('row-click', row, event);
|
|
142
|
+
};
|
|
135
143
|
</script>
|
|
136
144
|
|
|
137
145
|
<template>
|
|
@@ -178,7 +186,7 @@ const handleRowClick = (row: T, event: MouseEvent) => {
|
|
|
178
186
|
<td :colspan="columnCount" class="px-4 py-12">
|
|
179
187
|
<slot name="empty">
|
|
180
188
|
<div class="flex flex-col items-center gap-2 text-center text-muted-foreground">
|
|
181
|
-
<HugeiconsIcon :icon="PackageIcon" class="size-8
|
|
189
|
+
<HugeiconsIcon :icon="PackageIcon" class="size-8 opacity-40" />
|
|
182
190
|
<span class="text-xs sm:text-sm">{{ resolvedEmptyText }}</span>
|
|
183
191
|
</div>
|
|
184
192
|
</slot>
|
|
@@ -188,7 +196,7 @@ const handleRowClick = (row: T, event: MouseEvent) => {
|
|
|
188
196
|
|
|
189
197
|
<!-- Rows (animated when sorted, added, or removed) -->
|
|
190
198
|
<TransitionGroup v-else tag="tbody" name="uv-data-table-row">
|
|
191
|
-
<tr v-for="(row, index) in sortedRows" :key="getRowKey(row)" :data-state="selectedSet.has(getRowKey(row)) ? 'selected' : undefined" :class="cn('border-b border-border transition-colors duration-150 last:border-b-0 hover:bg-surface/70 data-[state=selected]:bg-primary/5', props.rowClickable && 'cursor-pointer')" @click="handleRowClick(row, $event)">
|
|
199
|
+
<tr v-for="(row, index) in sortedRows" :key="getRowKey(row)" :data-state="selectedSet.has(getRowKey(row)) ? 'selected' : undefined" :class="cn('border-b border-border transition-colors duration-150 last:border-b-0 hover:bg-surface/70 data-[state=selected]:bg-primary/5', props.rowClickable && 'cursor-pointer focus-visible:bg-surface/70 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring')" :tabindex="props.rowClickable ? 0 : undefined" @click="handleRowClick(row, $event)" @keydown="handleRowKeydown(row, $event)">
|
|
192
200
|
<!-- Row selection -->
|
|
193
201
|
<td v-if="props.selectable" class="px-4 py-3">
|
|
194
202
|
<Checkbox :model-value="selectedSet.has(getRowKey(row))" :aria-label="t('uiVintage.dataTable.selectRow')" @update:model-value="toggleRow(row)" />
|
|
@@ -7,6 +7,7 @@ import type { DateValue } from 'reka-ui';
|
|
|
7
7
|
import { computed, ref } from 'vue';
|
|
8
8
|
import { useI18n } from 'vue-i18n';
|
|
9
9
|
import { cn } from '../../../lib/utils';
|
|
10
|
+
import { useFieldControlId, useFieldLabelId } from '../field/field-context';
|
|
10
11
|
import { Button } from '../button';
|
|
11
12
|
import { Calendar } from '../calendar';
|
|
12
13
|
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
|
|
@@ -52,13 +53,18 @@ const defaultPlaceholder = computed(
|
|
|
52
53
|
const formatter = computed(() => new DateFormatter(locale.value || 'en-US', {
|
|
53
54
|
dateStyle: 'long',
|
|
54
55
|
}));
|
|
56
|
+
|
|
57
|
+
// Let a surrounding Field label point to the trigger, and name it "label + selected date" so the value is not lost
|
|
58
|
+
const fieldControlId = useFieldControlId();
|
|
59
|
+
const fieldLabelId = useFieldLabelId();
|
|
60
|
+
const triggerLabelledBy = computed(() => fieldLabelId.value && fieldControlId.value ? `${fieldLabelId.value} ${fieldControlId.value}` : undefined);
|
|
55
61
|
</script>
|
|
56
62
|
|
|
57
63
|
<template>
|
|
58
64
|
<Popover v-slot="{ close }">
|
|
59
65
|
<!-- Date picker trigger -->
|
|
60
66
|
<PopoverTrigger as-child>
|
|
61
|
-
<Button variant="secondary" :disabled="disabled" :class="cn('h-13 w-60 justify-start text-left font-normal enabled:active:scale-100', !date && 'text-muted-foreground', props.class,)">
|
|
67
|
+
<Button :id="fieldControlId" :aria-labelledby="triggerLabelledBy" variant="secondary" :disabled="disabled" :class="cn('h-13 w-60 justify-start text-left font-normal enabled:active:scale-100', !date && 'text-muted-foreground', props.class,)">
|
|
62
68
|
<HugeiconsIcon :icon="Calendar03Icon" class="size-4" />
|
|
63
69
|
{{ date ? formatter.format(date.toDate(getLocalTimeZone())) : resolvedPlaceholder }}
|
|
64
70
|
</Button>
|
|
@@ -3,6 +3,7 @@ import type { HTMLAttributes } from 'vue';
|
|
|
3
3
|
import { fromDate, getLocalTimeZone, toCalendarDate, } from '@internationalized/date';
|
|
4
4
|
import type { DateValue } from 'reka-ui';
|
|
5
5
|
import { computed, ref } from 'vue';
|
|
6
|
+
import { useI18n } from 'vue-i18n';
|
|
6
7
|
import { cn } from '../../../lib/utils';
|
|
7
8
|
import { DatePicker } from '../date-picker';
|
|
8
9
|
import { TimePicker } from '../time-picker';
|
|
@@ -154,6 +155,9 @@ const maxTimeValue = computed(() => {
|
|
|
154
155
|
? toTimeString(props.maxDate)
|
|
155
156
|
: undefined;
|
|
156
157
|
});
|
|
158
|
+
|
|
159
|
+
// The time input sits next to the date button, which takes the field label
|
|
160
|
+
const { t } = useI18n();
|
|
157
161
|
</script>
|
|
158
162
|
|
|
159
163
|
<template>
|
|
@@ -163,7 +167,7 @@ const maxTimeValue = computed(() => {
|
|
|
163
167
|
</div>
|
|
164
168
|
|
|
165
169
|
<div>
|
|
166
|
-
<TimePicker v-model="timeValue" :disabled="props.disabled" :step="props.step" :min="minTimeValue" :max="maxTimeValue" :class="cn('w-full', props.timePickerClass)" />
|
|
170
|
+
<TimePicker v-model="timeValue" :aria-label="t('uiVintage.datePicker.time')" :disabled="props.disabled" :step="props.step" :min="minTimeValue" :max="maxTimeValue" :class="cn('w-full', props.timePickerClass)" />
|
|
167
171
|
</div>
|
|
168
172
|
</div>
|
|
169
173
|
</template>
|
|
@@ -25,7 +25,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
|
25
25
|
<template>
|
|
26
26
|
<DropdownMenuItem data-slot="dropdown-menu-item" :data-inset="props.inset ? '' : undefined" :data-variant="props.variant" v-bind="forwarded" :class="cn(
|
|
27
27
|
dropdownMenuItemClass,
|
|
28
|
-
props.variant === 'destructive' && 'text-destructive data-highlighted:bg-destructive/10 data-highlighted:text-destructive',
|
|
28
|
+
props.variant === 'destructive' && 'text-destructive before:bg-destructive data-highlighted:bg-destructive/10 data-highlighted:text-destructive',
|
|
29
29
|
props.class
|
|
30
30
|
)
|
|
31
31
|
">
|
|
@@ -22,7 +22,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
|
|
22
22
|
<DropdownMenuSubTrigger data-slot="dropdown-menu-sub-trigger" :data-inset="props.inset ? '' : undefined" v-bind="forwardedProps" :class="cn(
|
|
23
23
|
'group/dropdown-sub-trigger',
|
|
24
24
|
dropdownMenuItemClass,
|
|
25
|
-
'data-[state=open]:bg-accent data-[state=open]:text-foreground',
|
|
25
|
+
'data-[state=open]:bg-accent data-[state=open]:text-foreground data-[state=open]:before:scale-y-100',
|
|
26
26
|
props.class
|
|
27
27
|
)
|
|
28
28
|
">
|
|
@@ -19,6 +19,8 @@ export const dropdownMenuItemClass = [
|
|
|
19
19
|
'relative flex cursor-pointer items-center gap-3 rounded px-3 py-2 text-xs font-semibold text-muted-foreground outline-hidden select-none transition-colors duration-150 sm:text-sm',
|
|
20
20
|
'data-highlighted:bg-accent data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-9',
|
|
21
21
|
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
|
22
|
+
// A primary bar springs onto the highlighted item, like in the command palette
|
|
23
|
+
'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',
|
|
22
24
|
// Leading icons nudge slightly when the item is highlighted
|
|
23
25
|
'[&>svg]:transition-transform [&>svg]:duration-300 [&>svg]:ease-spring [&[data-highlighted]>svg]:translate-x-0.5 [&[data-highlighted]>svg]:scale-110'
|
|
24
26
|
].join(' ');
|
|
@@ -3,12 +3,16 @@ import type { HTMLAttributes } from 'vue';
|
|
|
3
3
|
import type { FieldVariants } from '.';
|
|
4
4
|
import { cn } from '../../../lib/utils';
|
|
5
5
|
import { fieldVariants } from '.';
|
|
6
|
+
import { provideFieldContext } from './field-context';
|
|
6
7
|
|
|
7
8
|
// Props
|
|
8
9
|
const props = defineProps<{
|
|
9
10
|
class?: HTMLAttributes['class'];
|
|
10
11
|
orientation?: FieldVariants['orientation'];
|
|
11
12
|
}>();
|
|
13
|
+
|
|
14
|
+
// Let FieldLabel point to the control placed in this field
|
|
15
|
+
provideFieldContext();
|
|
12
16
|
</script>
|
|
13
17
|
|
|
14
18
|
<template>
|
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
import type { HTMLAttributes } from 'vue';
|
|
3
3
|
import { cn } from '../../../lib/utils';
|
|
4
4
|
import { Label } from '../label';
|
|
5
|
+
import { useFieldLabel } from './field-context';
|
|
5
6
|
|
|
6
7
|
// Props
|
|
7
8
|
const props = defineProps<{
|
|
8
9
|
class?: HTMLAttributes['class'];
|
|
9
10
|
}>();
|
|
11
|
+
|
|
12
|
+
// Point to the control of the surrounding Field (an explicit for attribute still wins)
|
|
13
|
+
const { labelId, target: labelTarget } = useFieldLabel();
|
|
10
14
|
</script>
|
|
11
15
|
|
|
12
16
|
<template>
|
|
13
|
-
<Label data-slot="field-label" :class="cn(
|
|
17
|
+
<Label :id="labelId" data-slot="field-label" :for="labelTarget" :class="cn(
|
|
14
18
|
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
|
|
15
|
-
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded
|
|
19
|
+
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded has-[>[data-slot=field]]:border *:data-[slot=field]:p-4',
|
|
16
20
|
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
|
|
17
21
|
props.class
|
|
18
22
|
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue';
|
|
2
|
+
import { computed, inject, onBeforeUnmount, provide, ref, useAttrs, useId } from 'vue';
|
|
3
|
+
|
|
4
|
+
interface FieldContext {
|
|
5
|
+
// Id of the control the field label points to (undefined until a control registers)
|
|
6
|
+
controlId: Ref<string | undefined>;
|
|
7
|
+
// Id of the field label, for controls that combine it with their own text
|
|
8
|
+
labelId: Ref<string | undefined>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const fieldContextKey: InjectionKey<FieldContext> = Symbol('uv-field');
|
|
12
|
+
|
|
13
|
+
// Share the label target with the label and the control inside a Field
|
|
14
|
+
export const provideFieldContext = () => {
|
|
15
|
+
const context: FieldContext = { controlId: ref(undefined), labelId: ref(undefined) };
|
|
16
|
+
provide(fieldContextKey, context);
|
|
17
|
+
return context;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Id the FieldLabel should point to, if the field has a control, and the id the label registers for itself
|
|
21
|
+
export const useFieldLabel = () => {
|
|
22
|
+
const field = inject(fieldContextKey, null);
|
|
23
|
+
const labelId = `uv-field-label-${useId()}`;
|
|
24
|
+
if (field && !field.labelId.value) {
|
|
25
|
+
field.labelId.value = labelId;
|
|
26
|
+
onBeforeUnmount(() => {
|
|
27
|
+
if (field.labelId.value === labelId) field.labelId.value = undefined;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
labelId: field ? labelId : undefined,
|
|
32
|
+
target: computed(() => field?.controlId.value)
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Id of the surrounding Field label, if any
|
|
37
|
+
export const useFieldLabelId = () => {
|
|
38
|
+
const field = inject(fieldContextKey, null);
|
|
39
|
+
return computed(() => field?.labelId.value);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Give the first control inside a Field an id its FieldLabel points to (an explicit id on the control wins)
|
|
43
|
+
export const useFieldControlId = (explicitId?: () => string | undefined) => {
|
|
44
|
+
const field = inject(fieldContextKey, null);
|
|
45
|
+
const attrs = useAttrs();
|
|
46
|
+
const generatedId = `uv-field-control-${useId()}`;
|
|
47
|
+
|
|
48
|
+
// Only the first control claims the label, so composite controls (like DateTimePicker) don't fight over it
|
|
49
|
+
if (!field || field.controlId.value) {
|
|
50
|
+
return computed(() => explicitId?.());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const id = (explicitId?.() ?? (attrs.id as string | undefined)) || generatedId;
|
|
54
|
+
field.controlId.value = id;
|
|
55
|
+
|
|
56
|
+
// Release the label when the control goes away
|
|
57
|
+
onBeforeUnmount(() => {
|
|
58
|
+
if (field.controlId.value === id) field.controlId.value = undefined;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return computed(() => id);
|
|
62
|
+
};
|
|
@@ -29,6 +29,7 @@ export const fieldVariants: (props?: FieldVariants & ClassProp) => string = cva(
|
|
|
29
29
|
}
|
|
30
30
|
);
|
|
31
31
|
|
|
32
|
+
export { useFieldControlId } from './field-context';
|
|
32
33
|
export { default as Field } from './Field.vue';
|
|
33
34
|
export { default as FieldContent } from './FieldContent.vue';
|
|
34
35
|
export { default as FieldDescription } from './FieldDescription.vue';
|
|
@@ -175,16 +175,16 @@ onBeforeUnmount(() => clearTimeout(rejectTimer));
|
|
|
175
175
|
'group/dropzone relative isolate flex min-h-44 cursor-pointer select-none flex-col items-center justify-center gap-3 rounded border-2 border-dashed border-border bg-surface/50 px-6 py-8 text-center outline-none',
|
|
176
176
|
'[transition:border-color_150ms,background-color_220ms,box-shadow_220ms,transform_420ms_var(--ease-spring)]',
|
|
177
177
|
'hover:border-border-strong hover:bg-surface focus-visible:ring-[3px] focus-visible:ring-ring/45 active:scale-[0.99] active:duration-75',
|
|
178
|
-
'data-dragging:scale-[1.01] data-dragging:border-transparent data-dragging:bg-primary/5 data-dragging:
|
|
178
|
+
'data-dragging:scale-[1.01] data-dragging:border-transparent data-dragging:bg-primary/5 data-dragging:hover:border-transparent data-dragging:hover:bg-primary/5',
|
|
179
179
|
'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:border-border data-disabled:hover:bg-surface/50 data-disabled:active:scale-100',
|
|
180
180
|
isRejected && 'animate-uv-shake border-destructive/70'
|
|
181
181
|
)" @click="openPicker" @keydown.enter.prevent="openPicker" @keydown.space.prevent="openPicker" @dragenter.prevent="onDragEnter" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
|
|
182
182
|
<!-- Marching dashed border while dragging -->
|
|
183
183
|
<span aria-hidden="true" class="uv-dropzone-ants pointer-events-none absolute -inset-0.5 rounded opacity-0 transition-opacity duration-150 group-data-dragging/dropzone:opacity-100" />
|
|
184
184
|
|
|
185
|
-
<!-- Upload icon (
|
|
186
|
-
<span aria-hidden="true" class="relative flex size-14 items-center justify-center rounded border border-border bg-card text-muted-foreground shadow-elevated-sm transition-[translate,scale,color,border-color,box-shadow] duration-420 ease-bounce group-hover/dropzone:-translate-y-0.5 group-hover/dropzone:text-foreground group-data-dragging/dropzone:-translate-y-2 group-data-dragging/dropzone:scale-115 group-data-dragging/dropzone:border-primary/50 group-data-dragging/dropzone:text-primary group-data-dragging/dropzone:shadow-
|
|
187
|
-
<HugeiconsIcon :icon="CloudUploadIcon" class="size-6
|
|
185
|
+
<!-- Upload icon (lifts and grows while dragging) -->
|
|
186
|
+
<span aria-hidden="true" class="relative flex size-14 items-center justify-center rounded border border-border bg-card text-muted-foreground shadow-elevated-sm transition-[translate,scale,color,border-color,box-shadow] duration-420 ease-bounce group-hover/dropzone:-translate-y-0.5 group-hover/dropzone:text-foreground group-data-dragging/dropzone:-translate-y-2 group-data-dragging/dropzone:scale-115 group-data-dragging/dropzone:border-primary/50 group-data-dragging/dropzone:text-primary group-data-dragging/dropzone:shadow-elevated-md">
|
|
187
|
+
<HugeiconsIcon :icon="CloudUploadIcon" class="size-6" />
|
|
188
188
|
</span>
|
|
189
189
|
|
|
190
190
|
<!-- Texts -->
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { HTMLAttributes } from 'vue';
|
|
3
3
|
import { useVModel } from '@vueuse/core';
|
|
4
4
|
import { cn } from '../../../lib/utils';
|
|
5
|
+
import { useFieldControlId } from '../field/field-context';
|
|
5
6
|
|
|
6
7
|
// Props
|
|
7
8
|
const props = defineProps<{
|
|
@@ -19,10 +20,13 @@ const modelValue = useVModel(props, 'modelValue', emits, {
|
|
|
19
20
|
passive: true,
|
|
20
21
|
defaultValue: props.defaultValue
|
|
21
22
|
});
|
|
23
|
+
|
|
24
|
+
// Let a surrounding Field label point to this control
|
|
25
|
+
const fieldControlId = useFieldControlId();
|
|
22
26
|
</script>
|
|
23
27
|
|
|
24
28
|
<template>
|
|
25
|
-
<input v-model="modelValue" data-slot="input" :class="cn(
|
|
29
|
+
<input :id="fieldControlId" v-model="modelValue" data-slot="input" :class="cn(
|
|
26
30
|
'uv-field h-13 w-full min-w-0 rounded border border-input bg-secondary px-4 py-3 text-xs font-semibold text-foreground shadow-elevated-sm outline-none sm:text-sm',
|
|
27
31
|
'selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground placeholder:opacity-60 placeholder:transition-opacity focus-visible:placeholder:opacity-40',
|
|
28
32
|
'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-semibold file:text-foreground',
|
|
@@ -5,6 +5,7 @@ import { reactiveOmit, useVModel } from '@vueuse/core';
|
|
|
5
5
|
import { ArrowDown01Icon } from '@hugeicons/core-free-icons';
|
|
6
6
|
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
7
7
|
import { cn } from '../../../lib/utils';
|
|
8
|
+
import { useFieldControlId } from '../field/field-context';
|
|
8
9
|
|
|
9
10
|
defineOptions({
|
|
10
11
|
inheritAttrs: false
|
|
@@ -24,12 +25,15 @@ const modelValue = useVModel(props, 'modelValue', emit, {
|
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
const delegatedProps = reactiveOmit(props, 'class');
|
|
28
|
+
|
|
29
|
+
// Let a surrounding Field label point to this control
|
|
30
|
+
const fieldControlId = useFieldControlId();
|
|
27
31
|
</script>
|
|
28
32
|
|
|
29
33
|
<template>
|
|
30
34
|
<div class="group/native-select relative w-fit has-[select:disabled]:opacity-50" data-slot="native-select-wrapper">
|
|
31
|
-
<select v-bind="{ ...$attrs, ...delegatedProps }" v-model="modelValue" data-slot="native-select" :class="cn(
|
|
32
|
-
'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded
|
|
35
|
+
<select :id="fieldControlId" v-bind="{ ...$attrs, ...delegatedProps }" v-model="modelValue" data-slot="native-select" :class="cn(
|
|
36
|
+
'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded border bg-transparent px-3 py-2 pr-9 text-sm shadow-elevated-sm transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
|
|
33
37
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
|
34
38
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
35
39
|
props.class
|
|
@@ -8,6 +8,7 @@ import { HugeiconsIcon } from '@hugeicons/vue';
|
|
|
8
8
|
import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFieldRoot, useForwardPropsEmits } from 'reka-ui';
|
|
9
9
|
import { useI18n } from 'vue-i18n';
|
|
10
10
|
import { cn } from '../../../lib/utils';
|
|
11
|
+
import { useFieldControlId } from '../field/field-context';
|
|
11
12
|
|
|
12
13
|
// Props
|
|
13
14
|
const props = defineProps<NumberFieldRootProps & {
|
|
@@ -77,10 +78,13 @@ const animateChange = async (value: number) => {
|
|
|
77
78
|
};
|
|
78
79
|
}
|
|
79
80
|
};
|
|
81
|
+
|
|
82
|
+
// Let a surrounding Field label point to this control
|
|
83
|
+
const fieldControlId = useFieldControlId(() => props.id);
|
|
80
84
|
</script>
|
|
81
85
|
|
|
82
86
|
<template>
|
|
83
|
-
<NumberFieldRoot v-slot="slotProps" data-slot="number-field" v-bind="forwarded" :class="cn(
|
|
87
|
+
<NumberFieldRoot v-slot="slotProps" data-slot="number-field" v-bind="forwarded" :id="fieldControlId" :class="cn(
|
|
84
88
|
'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
89
|
'data-disabled:cursor-not-allowed data-disabled:opacity-60 aria-invalid:animate-uv-shake',
|
|
86
90
|
props.class
|
|
@@ -25,7 +25,7 @@ const { t } = useI18n();
|
|
|
25
25
|
<PaginationListItem data-slot="pagination-item" :aria-label="t('uiVintage.pagination.page', { page: props.value })" v-bind="delegatedProps" :class="cn(
|
|
26
26
|
buttonVariants({ variant: 'secondary', size: props.size }),
|
|
27
27
|
'tabular-nums',
|
|
28
|
-
'data-selected:border-primary data-selected:bg-primary data-selected:text-primary-foreground data-selected:
|
|
28
|
+
'data-selected:border-primary data-selected:bg-primary data-selected:text-primary-foreground data-selected:hover:border-primary data-selected:hover:bg-primary-hover',
|
|
29
29
|
'data-selected:animate-[uv-pagination-pop_380ms_var(--ease-bounce)]',
|
|
30
30
|
props.class
|
|
31
31
|
)">
|
|
@@ -34,7 +34,7 @@ const isInvalid = computed(() => pinInputContext?.invalid.value || undefined);
|
|
|
34
34
|
<span data-slot="pin-input-cell" class="relative inline-flex shrink-0">
|
|
35
35
|
<!-- Native input (text is transparent, the overlay below renders the character) -->
|
|
36
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-
|
|
37
|
+
'uv-field size-12 rounded border border-input bg-secondary text-center text-lg font-semibold text-transparent caret-primary shadow-elevated-sm outline-none sm:size-13',
|
|
38
38
|
'placeholder:text-muted-foreground placeholder:opacity-50 selection:bg-primary/25',
|
|
39
39
|
'data-filled:border-primary/60 data-filled:bg-primary/5',
|
|
40
40
|
'disabled:cursor-not-allowed disabled:opacity-60',
|
|
@@ -42,7 +42,7 @@ const isInvalid = computed(() => pinInputContext?.invalid.value || undefined);
|
|
|
42
42
|
)" />
|
|
43
43
|
|
|
44
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-
|
|
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-semibold text-foreground">
|
|
46
46
|
{{ displayValue }}
|
|
47
47
|
</span>
|
|
48
48
|
</span>
|
|
@@ -26,7 +26,7 @@ const percentage = computed(() => {
|
|
|
26
26
|
</script>
|
|
27
27
|
|
|
28
28
|
<template>
|
|
29
|
-
<ProgressRoot data-slot="progress" v-bind="delegatedProps" :class="cn(
|
|
29
|
+
<ProgressRoot data-slot="progress" v-bind="delegatedProps" :model-value="props.indeterminate ? null : props.modelValue" :class="cn(
|
|
30
30
|
'relative h-2 w-full overflow-hidden rounded-sm bg-muted',
|
|
31
31
|
props.class
|
|
32
32
|
)">
|
|
@@ -34,6 +34,6 @@ const percentage = computed(() => {
|
|
|
34
34
|
<div v-if="props.indeterminate" data-slot="progress-indicator" class="absolute inset-y-0 left-0 w-full origin-left animate-uv-indeterminate rounded-sm bg-primary" />
|
|
35
35
|
|
|
36
36
|
<!-- Determinate bar -->
|
|
37
|
-
<ProgressIndicator v-else data-slot="progress-indicator" class="relative h-full w-full flex-1 overflow-hidden rounded-sm bg-primary transition-transform duration-700 ease-out-expo
|
|
37
|
+
<ProgressIndicator v-else data-slot="progress-indicator" class="relative h-full w-full flex-1 overflow-hidden rounded-sm bg-primary transition-transform duration-700 ease-out-expo" :style="`transform: translateX(-${100 - percentage}%);`" />
|
|
38
38
|
</ProgressRoot>
|
|
39
39
|
</template>
|
|
@@ -23,7 +23,7 @@ const props = defineProps<{
|
|
|
23
23
|
'group/radio-card relative flex w-full cursor-pointer items-center gap-3 rounded border border-border bg-card px-4 py-3 text-left shadow-elevated-sm outline-none',
|
|
24
24
|
'[transition:scale_320ms_var(--ease-spring),border-color_150ms_var(--ease-snappy),background-color_150ms_var(--ease-snappy),box-shadow_200ms_var(--ease-snappy)]',
|
|
25
25
|
'hover:border-border-strong active:scale-[0.985] focus-visible:ring-[3px] focus-visible:ring-ring/45',
|
|
26
|
-
'data-[state=checked]:border-primary data-[state=checked]:bg-primary/5 data-[state=checked]:
|
|
26
|
+
'data-[state=checked]:border-primary data-[state=checked]:bg-primary/5 data-[state=checked]:hover:border-primary',
|
|
27
27
|
'disabled:cursor-not-allowed disabled:opacity-60 disabled:active:scale-100',
|
|
28
28
|
props.class
|
|
29
29
|
)">
|
|
@@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
18
|
<SelectItem data-slot="select-item" v-bind="forwardedProps" :class="cn(
|
|
19
|
-
'focus:bg-accent focus:text-accent-foreground data-[state=checked]:text-foreground transition-colors duration-150[&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-2.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
|
|
19
|
+
'focus:bg-accent focus:text-accent-foreground data-[state=checked]:text-foreground transition-colors duration-150 [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-2.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
|
|
20
20
|
props.class
|
|
21
21
|
)
|
|
22
22
|
">
|