feat(forms): add useMaskedField and useMaskedInput composables for input masking

This commit is contained in:
2026-06-09 13:54:52 +07:00
parent 6de7c72fb3
commit 07937e26db
426 changed files with 12981 additions and 311 deletions
@@ -1,6 +1,12 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* A single `role="gridcell"` day container (`<td>`). Reflects the date's state
* (selected, disabled, unavailable, outside-view, today) as `data-*`
* attributes and `aria-*` for styling, and wraps the focusable
* `CalendarCellTrigger`.
*/
export interface CalendarCellProps extends PrimitiveProps {
/** The date this cell represents. */
date: Date;
@@ -1,6 +1,12 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* The focusable, clickable day button inside a `CalendarCell`. Selects its
* `day` on click/Enter/Space, drives roving focus and full arrow-key /
* Home-End / PageUp-Down keyboard navigation (paging the month when focus
* crosses the visible range), and exposes day state through its slot.
*/
export interface CalendarCellTriggerProps extends PrimitiveProps {
/** The day this trigger represents. */
day: Date;
@@ -1,6 +1,11 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* The `role="grid"` table for a single month. Provides grid context (the month
* it renders) to its head/body cells; render one per visible month when
* `numberOfMonths > 1`.
*/
export interface CalendarGridProps extends PrimitiveProps {
/** The month this grid represents. Defaults to the root placeholder's month. */
month?: Date;
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* The grid's `<tbody>` wrapper containing the week rows (`CalendarGridRow`) of
* day cells.
*/
export interface CalendarGridBodyProps extends PrimitiveProps {}
</script>
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* The grid's `<thead>` wrapper holding the row of weekday `CalendarHeadCell`
* labels.
*/
export interface CalendarGridHeadProps extends PrimitiveProps {}
</script>
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* A single table row (`<tr>`) representing one week of the month, or the
* weekday-label row inside the grid head.
*/
export interface CalendarGridRowProps extends PrimitiveProps {}
</script>
@@ -1,6 +1,11 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* A `scope="col"` weekday header cell (`<th>`). Renders the localized short
* label in its slot while exposing the full weekday name as the `aria-label`
* when a `day` is provided.
*/
export interface CalendarHeadCellProps extends PrimitiveProps {
/** The day this header cell represents — used for `aria-label`. */
day?: Date;
@@ -1,6 +1,10 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* Layout container for the calendar's top bar. Holds the `CalendarPrev`,
* `CalendarHeading`, and `CalendarNext` controls above the month grid(s).
*/
export interface CalendarHeaderProps extends PrimitiveProps {}
</script>
@@ -1,6 +1,12 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* Displays the currently visible month and year (e.g. "June 2026"), or a range
* when multiple months are shown. Marked `aria-hidden` since the grid already
* carries the full accessible label; expose the value via its default slot to
* customize the rendering.
*/
export interface CalendarHeadingProps extends PrimitiveProps {}
</script>
@@ -1,6 +1,11 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* Button that pages the calendar forward (by one month, or by
* `numberOfMonths` when paged navigation is enabled). Auto-disables when the
* next page would fall after `maxValue` or the calendar is disabled.
*/
export interface CalendarNextProps extends PrimitiveProps {
/** Override the root's `nextPage` for just this button. */
nextPage?: (placeholder: Date) => Date;
@@ -1,6 +1,11 @@
<script lang="ts">
import type { PrimitiveProps } from '../primitive';
/**
* Button that pages the calendar backward (by one month, or by
* `numberOfMonths` when paged navigation is enabled). Auto-disables when the
* previous page would fall before `minValue` or the calendar is disabled.
*/
export interface CalendarPrevProps extends PrimitiveProps {
/** Override the root's `prevPage` for just this button. */
prevPage?: (placeholder: Date) => Date;
@@ -2,6 +2,17 @@
import type { PrimitiveProps } from '../primitive';
import type { CalendarMonth, WeekDayFormat } from './utils';
/**
* A fully accessible, headless date calendar for picking a single day. The
* root owns the selected value and the displayed month ("placeholder"), builds
* the localized month grid(s), and wires up roving keyboard navigation,
* min/max bounds, and disabled/unavailable predicates. Use it to build an
* inline date picker or as the body of a popover/`DatePicker`.
*
* Compose it with `CalendarHeader` (`CalendarPrev` / `CalendarHeading` /
* `CalendarNext`) and one `CalendarGrid` per month. Supports `v-model` for the
* selected date and `v-model:placeholder` for the visible month.
*/
export interface CalendarRootProps extends PrimitiveProps {
/** Uncontrolled default selected date. */
defaultValue?: Date;
+110
View File
@@ -0,0 +1,110 @@
<script setup lang="ts">
import {
CalendarCell,
CalendarCellTrigger,
CalendarGrid,
CalendarGridBody,
CalendarGridHead,
CalendarGridRow,
CalendarHeadCell,
CalendarHeader,
CalendarHeading,
CalendarNext,
CalendarPrev,
CalendarRoot,
} from '@robonen/primitives';
import { ref } from 'vue';
const value = ref<Date>(new Date());
function formatSelected(date: Date | undefined) {
if (!date) return 'None';
return date.toLocaleDateString('en', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
}
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-3">
<CalendarRoot
v-slot="{ grid, weekDays }"
v-model="value"
class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 text-(--fg) shadow-sm"
>
<CalendarHeader class="mb-3 flex items-center justify-between gap-2">
<CalendarPrev
aria-label="Previous month"
class="inline-flex size-8 items-center justify-center rounded-lg border border-(--border) bg-(--bg) text-(--fg-muted) transition hover:bg-(--bg-inset) hover:text-(--fg) active:scale-95 cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
>
</CalendarPrev>
<CalendarHeading class="text-sm font-semibold tracking-tight" />
<CalendarNext
aria-label="Next month"
class="inline-flex size-8 items-center justify-center rounded-lg border border-(--border) bg-(--bg) text-(--fg-muted) transition hover:bg-(--bg-inset) hover:text-(--fg) active:scale-95 cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
>
</CalendarNext>
</CalendarHeader>
<CalendarGrid
v-for="month in grid"
:key="month.value.toString()"
:month="month.value"
class="w-full border-collapse select-none"
>
<CalendarGridHead>
<CalendarGridRow class="mb-1 flex">
<CalendarHeadCell
v-for="(weekday, i) in weekDays"
:key="weekday + i"
class="w-9 text-center text-xs font-medium text-(--fg-subtle)"
>
{{ weekday }}
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody>
<CalendarGridRow
v-for="(week, w) in month.weeks"
:key="w"
class="flex w-full"
>
<CalendarCell
v-for="day in week"
:key="day.toString()"
:date="day"
class="p-0.5"
>
<CalendarCellTrigger
v-slot="{ dayValue, selected, today }"
:day="day"
:month="month.value"
class="flex size-8 items-center justify-center rounded-lg text-sm tabular-nums transition outline-none cursor-pointer
focus-visible:ring-2 focus-visible:ring-(--ring)
hover:bg-(--bg-inset)
data-[selected]:bg-(--accent) data-[selected]:font-semibold data-[selected]:text-(--accent-fg) data-[selected]:hover:bg-(--accent-hover)
data-[outside-view]:text-(--fg-subtle) data-[outside-view]:opacity-50
data-[unavailable]:cursor-not-allowed data-[unavailable]:text-red-500 data-[unavailable]:line-through data-[unavailable]:hover:bg-transparent
data-[disabled]:cursor-not-allowed data-[disabled]:opacity-30"
>
<span
:class="[
today && !selected ? 'relative after:absolute after:bottom-1 after:left-1/2 after:size-1 after:-translate-x-1/2 after:rounded-full after:bg-(--accent)' : '',
]"
>
{{ dayValue }}
</span>
</CalendarCellTrigger>
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</CalendarRoot>
<p class="text-xs text-(--fg-muted)">
Selected:
<span class="font-medium text-(--fg)">{{ formatSelected(value) }}</span>
</p>
</div>
</template>