atlas-react-kit 0.11.2 → 0.13.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.
Files changed (42) hide show
  1. package/COMPONENTS.md +176 -83
  2. package/DESIGN_RULES.md +14 -1
  3. package/README.md +3 -3
  4. package/dist/alert.d.ts +1 -1
  5. package/dist/badge.d.ts +2 -2
  6. package/dist/button.d.ts +2 -2
  7. package/dist/date-picker.d.ts +3 -9
  8. package/dist/date-picker.js +8 -38
  9. package/dist/date-time-picker.d.ts +1 -1
  10. package/dist/date-time-picker.js +3 -3
  11. package/dist/error-boundary.d.ts +1 -1
  12. package/dist/field.d.ts +1 -1
  13. package/dist/form/control-field.d.ts +31 -0
  14. package/dist/form/control-field.js +22 -0
  15. package/dist/form/date-picker-field.d.ts +9 -3
  16. package/dist/form/date-picker-field.js +3 -3
  17. package/dist/form/date-time-picker-field.d.ts +10 -4
  18. package/dist/form/date-time-picker-field.js +3 -3
  19. package/dist/form/field-error.d.ts +18 -0
  20. package/dist/form/field-shell.d.ts +29 -10
  21. package/dist/form/field-shell.js +37 -11
  22. package/dist/form/markdown-field.d.ts +4 -2
  23. package/dist/form/markdown-field.js +3 -3
  24. package/dist/form/select-field.d.ts +5 -3
  25. package/dist/form/select-field.js +3 -3
  26. package/dist/form/text-field.d.ts +3 -4
  27. package/dist/form/text-field.js +6 -6
  28. package/dist/internal/date-like.d.ts +31 -0
  29. package/dist/internal/date-like.js +33 -0
  30. package/dist/label.d.ts +16 -1
  31. package/dist/label.js +7 -3
  32. package/dist/nav-link.d.ts +22 -0
  33. package/dist/nav-link.js +31 -0
  34. package/dist/next/nav-link.d.ts +11 -0
  35. package/dist/next/nav-link.js +14 -0
  36. package/dist/picker-shell.d.ts +48 -6
  37. package/dist/picker-shell.js +28 -0
  38. package/dist/toast.d.ts +4 -2
  39. package/dist/toast.js +18 -3
  40. package/dist/toggle.d.ts +1 -1
  41. package/package.json +17 -5
  42. package/skills/atlas-react-kit/SKILL.md +2 -0
@@ -1,42 +1,12 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { format, isSameDay, parseISO } from "date-fns";
4
+ import { format, isSameDay } from "date-fns";
5
+ import { toDate, } from "./internal/date-like.js";
5
6
  import { MD_MEDIA_QUERY, useMediaQuery } from "./internal/use-media-query.js";
6
7
  import { LazyCalendar } from "./lazy-calendar.js";
7
- import { PickerClearFooter, PickerShell, } from "./picker-shell.js";
8
- /**
9
- * Coerce a date-like value to a local `Date`.
10
- * Plain calendar dates (`yyyy-MM-dd`) parse as local midnight so they do not
11
- * shift when the runtime timezone is behind UTC.
12
- */
13
- export function toDate(value) {
14
- if (value == null || value === "")
15
- return undefined;
16
- if (value instanceof Date) {
17
- return Number.isNaN(value.getTime()) ? undefined : value;
18
- }
19
- if (typeof value === "number") {
20
- const date = new Date(value);
21
- return Number.isNaN(date.getTime()) ? undefined : date;
22
- }
23
- const trimmed = value.trim();
24
- const ymd = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
25
- if (ymd) {
26
- const year = Number(ymd[1]);
27
- const month = Number(ymd[2]);
28
- const day = Number(ymd[3]);
29
- const date = new Date(year, month - 1, day);
30
- if (date.getFullYear() !== year ||
31
- date.getMonth() !== month - 1 ||
32
- date.getDate() !== day) {
33
- return undefined;
34
- }
35
- return date;
36
- }
37
- const parsed = parseISO(trimmed);
38
- return Number.isNaN(parsed.getTime()) ? undefined : parsed;
39
- }
8
+ import { PickerClearFooter, PickerShell, limitCalendarProps, } from "./picker-shell.js";
9
+ export { toDate };
40
10
  /** Format a Date as a timezone-safe calendar date (`yyyy-MM-dd`). */
41
11
  export function formatLocalDate(date) {
42
12
  return format(date, "yyyy-MM-dd");
@@ -52,15 +22,15 @@ function formatRangeTriggerLabel(from, to, placeholder) {
52
22
  return format(from, "PPP");
53
23
  return `${format(from, "PPP")} – ${format(to, "PPP")}`;
54
24
  }
55
- function DatePickerSingleButton({ value, onChange, placeholder = "Pick a date", numberOfMonths, defaultMonth, calendarProps, ...shell }) {
25
+ function DatePickerSingleButton({ value, onChange, placeholder = "Pick a date", numberOfMonths, defaultMonth, calendarProps, minDate, maxDate, ...shell }) {
56
26
  const [open, setOpen] = React.useState(false);
57
27
  const date = toDate(value);
58
- return (_jsx(PickerShell, { ...shell, empty: !date, label: date ? format(date, "PPP") : placeholder, dialogLabel: "Choose date", open: open, onOpenChange: setOpen, children: _jsx(LazyCalendar, { ...calendarProps, mode: "single", autoFocus: true, selected: date, defaultMonth: defaultMonth ?? date, numberOfMonths: numberOfMonths, onSelect: (next) => {
28
+ return (_jsx(PickerShell, { ...shell, empty: !date, label: date ? format(date, "PPP") : placeholder, dialogLabel: "Choose date", open: open, onOpenChange: setOpen, children: _jsx(LazyCalendar, { ...limitCalendarProps(calendarProps, minDate, maxDate), mode: "single", autoFocus: true, selected: date, defaultMonth: defaultMonth ?? date, numberOfMonths: numberOfMonths, onSelect: (next) => {
59
29
  onChange(next);
60
30
  setOpen(false);
61
31
  } }) }));
62
32
  }
63
- function DatePickerRangeButton({ value, onChange, placeholder = "Pick a date range", numberOfMonths, defaultMonth, calendarProps, ...shell }) {
33
+ function DatePickerRangeButton({ value, onChange, placeholder = "Pick a date range", numberOfMonths, defaultMonth, calendarProps, minDate, maxDate, ...shell }) {
64
34
  const [open, setOpen] = React.useState(false);
65
35
  const mdUp = useMediaQuery(MD_MEDIA_QUERY);
66
36
  const months = numberOfMonths ?? (mdUp ? 2 : 1);
@@ -76,7 +46,7 @@ function DatePickerRangeButton({ value, onChange, placeholder = "Pick a date ran
76
46
  restartRef.current = Boolean(from && to);
77
47
  setOpen(next);
78
48
  };
79
- return (_jsxs(PickerShell, { ...shell, empty: !from, label: formatRangeTriggerLabel(from, to, placeholder), dialogLabel: "Choose dates", open: open, onOpenChange: onOpenChange, children: [_jsx(LazyCalendar, { ...calendarProps, mode: "range", autoFocus: true, selected: selected, defaultMonth: defaultMonth ?? from ?? to, numberOfMonths: months, onSelect: (next, day) => {
49
+ return (_jsxs(PickerShell, { ...shell, empty: !from, label: formatRangeTriggerLabel(from, to, placeholder), dialogLabel: "Choose dates", open: open, onOpenChange: onOpenChange, children: [_jsx(LazyCalendar, { ...limitCalendarProps(calendarProps, minDate, maxDate), mode: "range", autoFocus: true, selected: selected, defaultMonth: defaultMonth ?? from ?? to, numberOfMonths: months, onSelect: (next, day) => {
80
50
  const range = restartRef.current ? { from: day, to: day } : next;
81
51
  restartRef.current = false;
82
52
  onChange(range);
@@ -25,4 +25,4 @@ export declare function resolveTimeStep(step: number | undefined): number;
25
25
  * stays on-screen at a 390px width. `className` styles the trigger,
26
26
  * `contentClassName` the popover, and `calendarProps` reaches Day Picker.
27
27
  */
28
- export declare function DateTimePickerButton({ value, onChange, timeStep, placeholder, defaultMonth, calendarProps, ...shell }: DateTimePickerButtonProps): React.JSX.Element;
28
+ export declare function DateTimePickerButton({ value, onChange, timeStep, placeholder, defaultMonth, calendarProps, minDate, maxDate, ...shell }: DateTimePickerButtonProps): React.JSX.Element;
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { format } from "date-fns";
5
5
  import { LazyCalendar } from "./lazy-calendar.js";
6
- import { PickerClearFooter, PickerShell, } from "./picker-shell.js";
6
+ import { PickerClearFooter, PickerShell, limitCalendarProps, } from "./picker-shell.js";
7
7
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./select.js";
8
8
  /** Default clock increment, in minutes. Must divide 60. */
9
9
  export const DEFAULT_TIME_STEP = 15;
@@ -68,14 +68,14 @@ function preventDismissForPortaledSelect(event) {
68
68
  * stays on-screen at a 390px width. `className` styles the trigger,
69
69
  * `contentClassName` the popover, and `calendarProps` reaches Day Picker.
70
70
  */
71
- export function DateTimePickerButton({ value, onChange, timeStep, placeholder = "Pick a date and time", defaultMonth, calendarProps, ...shell }) {
71
+ export function DateTimePickerButton({ value, onChange, timeStep, placeholder = "Pick a date and time", defaultMonth, calendarProps, minDate, maxDate, ...shell }) {
72
72
  const [open, setOpen] = React.useState(false);
73
73
  const date = value ?? null;
74
74
  const step = resolveTimeStep(timeStep);
75
75
  const empty = date == null;
76
76
  const hourId = React.useId();
77
77
  const minuteId = React.useId();
78
- return (_jsxs(PickerShell, { ...shell, empty: empty, label: date ? format(date, "PPP p") : placeholder, dialogLabel: "Choose date and time", open: open, onOpenChange: setOpen, onInteractOutside: preventDismissForPortaledSelect, children: [_jsx(LazyCalendar, { ...calendarProps, mode: "single", autoFocus: true, selected: date ?? undefined, defaultMonth: defaultMonth ?? date ?? undefined, onSelect: (next) => {
78
+ return (_jsxs(PickerShell, { ...shell, empty: empty, label: date ? format(date, "PPP p") : placeholder, dialogLabel: "Choose date and time", open: open, onOpenChange: setOpen, onInteractOutside: preventDismissForPortaledSelect, children: [_jsx(LazyCalendar, { ...limitCalendarProps(calendarProps, minDate, maxDate), mode: "single", autoFocus: true, selected: date ?? undefined, defaultMonth: defaultMonth ?? date ?? undefined, onSelect: (next) => {
79
79
  // Re-clicking a selected day would clear it; require Clear instead
80
80
  // so a time already set is not dropped by an accidental second tap.
81
81
  if (!next)
@@ -31,6 +31,6 @@ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBo
31
31
  componentDidCatch(error: Error, info: ErrorInfo): void;
32
32
  retry: () => void;
33
33
  reload: () => void;
34
- render(): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
34
+ render(): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
35
35
  }
36
36
  export {};
package/dist/field.d.ts CHANGED
@@ -19,7 +19,7 @@ declare function FieldGroup({ className, ...props }: React.ComponentProps<"div">
19
19
  * content in a row.
20
20
  */
21
21
  declare const fieldVariants: (props?: ({
22
- orientation?: "horizontal" | "vertical" | "responsive" | null | undefined;
22
+ orientation?: "horizontal" | "responsive" | "vertical" | null | undefined;
23
23
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
24
24
  declare function Field({ className, orientation, ...props }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>): import("react").JSX.Element;
25
25
  declare function FieldContent({ className, ...props }: React.ComponentProps<"div">): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ import type { ErrorTimingProps, FieldMetaState } from "./field-error.js";
3
+ /** Props `FormControlField` hands its control: spread them onto it. */
4
+ export type FormControlProps = {
5
+ id: string;
6
+ "aria-invalid": true | undefined;
7
+ "aria-describedby": string | undefined;
8
+ "aria-required": true | undefined;
9
+ };
10
+ type Props = ErrorTimingProps & Omit<ComponentProps<"div">, "id" | "children"> & {
11
+ /** Control id the label points at. Defaults to a generated id. */
12
+ id?: string;
13
+ label: string;
14
+ /** Hint between the label and the control, read with the control. */
15
+ description?: ReactNode;
16
+ /** Marks the label and passes `aria-required` to the control. */
17
+ required?: boolean;
18
+ /** Error meta, the same shape the other `form/*` fields take. */
19
+ meta?: FieldMetaState;
20
+ /** Renders the control from the id and ARIA props it should carry. */
21
+ children: (control: FormControlProps) => ReactNode;
22
+ };
23
+ /**
24
+ * The `form/*` field layout for a control the kit does not ship (a
25
+ * combobox, a segmented picker): label with the required marker, optional
26
+ * hint, the control, and its error, with the same spacing, ids, and error
27
+ * timing as `FormTextField`. `className` and other div props go to the
28
+ * wrapper (`data-slot="form-field"`).
29
+ */
30
+ export declare function FormControlField({ id: idProp, label, description, required, meta, submitted, showError, children, ...props }: Props): import("react").JSX.Element;
31
+ export {};
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { describedBy, descriptionIdFor, FormFieldShell, invalidWhen, metaErrorText, } from "./field-shell.js";
5
+ /**
6
+ * The `form/*` field layout for a control the kit does not ship (a
7
+ * combobox, a segmented picker): label with the required marker, optional
8
+ * hint, the control, and its error, with the same spacing, ids, and error
9
+ * timing as `FormTextField`. `className` and other div props go to the
10
+ * wrapper (`data-slot="form-field"`).
11
+ */
12
+ export function FormControlField({ id: idProp, label, description, required, meta, submitted, showError, children, ...props }) {
13
+ const generatedId = useId();
14
+ const id = idProp ?? generatedId;
15
+ const errorText = metaErrorText(meta, { submitted, showError });
16
+ return (_jsx(FormFieldShell, { ...props, id: id, label: label, description: description, errorText: errorText, required: required, children: children({
17
+ id,
18
+ "aria-invalid": invalidWhen(errorText),
19
+ "aria-describedby": describedBy(id, errorText, description ? descriptionIdFor(id) : undefined),
20
+ "aria-required": required || undefined,
21
+ }) }));
22
+ }
@@ -1,12 +1,18 @@
1
1
  import { type DateLike, type DatePickerCalendarProps } from "../date-picker.js";
2
- import type { FieldMetaState } from "./field-error.js";
3
- type Props = {
2
+ import type { ErrorTimingProps, FieldMetaState } from "./field-error.js";
3
+ type Props = ErrorTimingProps & {
4
4
  value: DateLike;
5
5
  onValueChange: (date: Date | undefined) => void;
6
6
  label: string;
7
7
  meta?: FieldMetaState;
8
8
  placeholder?: string;
9
9
  disabled?: boolean;
10
+ /** Marks the label and adds "Required" to the trigger's description. */
11
+ required?: boolean;
12
+ /** Earliest day that can be picked; earlier days are disabled. */
13
+ minDate?: DateLike;
14
+ /** Latest day that can be picked; later days are disabled. */
15
+ maxDate?: DateLike;
10
16
  /** Forwarded to the date picker trigger. */
11
17
  className?: string;
12
18
  /** Classes for the field wrapper (label, trigger, and error). */
@@ -17,5 +23,5 @@ type Props = {
17
23
  calendarProps?: DatePickerCalendarProps;
18
24
  };
19
25
  /** Date picker with built-in field error text and invalid styling. */
20
- export declare function FormDatePickerField({ onValueChange, label, meta, fieldClassName, ...picker }: Props): import("react").JSX.Element;
26
+ export declare function FormDatePickerField({ onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }: Props): import("react").JSX.Element;
21
27
  export {};
@@ -4,8 +4,8 @@ import { useId } from "react";
4
4
  import { DatePickerButton, } from "../date-picker.js";
5
5
  import { FormFieldShell, metaErrorText, pickerFieldAria, } from "./field-shell.js";
6
6
  /** Date picker with built-in field error text and invalid styling. */
7
- export function FormDatePickerField({ onValueChange, label, meta, fieldClassName, ...picker }) {
8
- const errorText = metaErrorText(meta);
7
+ export function FormDatePickerField({ onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }) {
8
+ const errorText = metaErrorText(meta, { submitted, showError });
9
9
  const id = useId();
10
- return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, className: fieldClassName, children: _jsx(DatePickerButton, { ...picker, id: id, onChange: onValueChange, ...pickerFieldAria(id, errorText) }) }));
10
+ return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsx(DatePickerButton, { ...picker, id: id, onChange: onValueChange, ...pickerFieldAria(id, errorText, required) }) }));
11
11
  }
@@ -1,12 +1,18 @@
1
- import type { DatePickerCalendarProps } from "../date-picker.js";
2
- import type { FieldMetaState } from "./field-error.js";
3
- type Props = {
1
+ import type { DateLike, DatePickerCalendarProps } from "../date-picker.js";
2
+ import type { ErrorTimingProps, FieldMetaState } from "./field-error.js";
3
+ type Props = ErrorTimingProps & {
4
4
  value: Date | null;
5
5
  onValueChange: (date: Date | null) => void;
6
6
  label: string;
7
7
  meta?: FieldMetaState;
8
8
  placeholder?: string;
9
9
  disabled?: boolean;
10
+ /** Marks the label and adds "Required" to the trigger's description. */
11
+ required?: boolean;
12
+ /** Earliest day that can be picked; earlier days are disabled. */
13
+ minDate?: DateLike;
14
+ /** Latest day that can be picked; later days are disabled. */
15
+ maxDate?: DateLike;
10
16
  /** Minute increment forwarded to the picker. */
11
17
  timeStep?: number;
12
18
  /** Forwarded to the date-time picker trigger. */
@@ -19,5 +25,5 @@ type Props = {
19
25
  calendarProps?: DatePickerCalendarProps;
20
26
  };
21
27
  /** Date-time picker with built-in field error text and invalid styling. */
22
- export declare function FormDateTimePickerField({ onValueChange, label, meta, fieldClassName, ...picker }: Props): import("react").JSX.Element;
28
+ export declare function FormDateTimePickerField({ onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }: Props): import("react").JSX.Element;
23
29
  export {};
@@ -4,8 +4,8 @@ import { useId } from "react";
4
4
  import { DateTimePickerButton } from "../date-time-picker.js";
5
5
  import { FormFieldShell, metaErrorText, pickerFieldAria, } from "./field-shell.js";
6
6
  /** Date-time picker with built-in field error text and invalid styling. */
7
- export function FormDateTimePickerField({ onValueChange, label, meta, fieldClassName, ...picker }) {
8
- const errorText = metaErrorText(meta);
7
+ export function FormDateTimePickerField({ onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }) {
8
+ const errorText = metaErrorText(meta, { submitted, showError });
9
9
  const id = useId();
10
- return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, className: fieldClassName, children: _jsx(DateTimePickerButton, { ...picker, id: id, onChange: onValueChange, ...pickerFieldAria(id, errorText) }) }));
10
+ return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsx(DateTimePickerButton, { ...picker, id: id, onChange: onValueChange, ...pickerFieldAria(id, errorText, required) }) }));
11
11
  }
@@ -3,6 +3,24 @@ export type FieldErrorMessage = string | {
3
3
  } | undefined;
4
4
  export type FieldMetaState = {
5
5
  errors: FieldErrorMessage[];
6
+ /**
7
+ * Whether the person has changed or left the field (TanStack Form's
8
+ * `meta.isTouched`). When it is `false`, the `form/*` fields hold back
9
+ * the error until the field is touched or their `submitted` prop is set.
10
+ * Leave it out to show errors as soon as they exist.
11
+ */
12
+ isTouched?: boolean;
13
+ };
14
+ /** When a field shows its error: the `form/*` fields' shared timing props. */
15
+ export type ErrorTimingProps = {
16
+ /**
17
+ * Set once the form has been submitted (TanStack Form:
18
+ * `form.state.submissionAttempts > 0`), so an untouched field shows its
19
+ * error too. Only matters when `meta.isTouched` is passed.
20
+ */
21
+ submitted?: boolean;
22
+ /** When false, never shows error text or invalid styling. Default true. */
23
+ showError?: boolean;
6
24
  };
7
25
  /** First TanStack Form error message for a field, if any. */
8
26
  export declare function getFieldErrorMessage(message: FieldErrorMessage): string | undefined;
@@ -1,17 +1,29 @@
1
- import type { ReactNode } from "react";
2
- import { type FieldMetaState } from "./field-error.js";
1
+ import type { ComponentProps, ReactNode } from "react";
2
+ import { type ErrorTimingProps, type FieldMetaState } from "./field-error.js";
3
3
  /**
4
4
  * Not a public entry. The layout and error wiring every `form/*` field
5
- * shares, so a field only says which control it renders.
5
+ * shares, so a field only says which control it renders. Custom controls
6
+ * get the same shell through the public `FormControlField`.
6
7
  */
7
- /** First error in `meta`, or `undefined` when there is no meta or no error. */
8
- export declare function metaErrorText(meta: FieldMetaState | undefined): string | undefined;
8
+ /**
9
+ * First error in `meta`, or `undefined` when there is no meta, no error,
10
+ * `showError` is false, or the field is untouched (`isTouched: false`)
11
+ * and the form has not been submitted.
12
+ */
13
+ export declare function metaErrorText(meta: FieldMetaState | undefined, { submitted, showError }?: ErrorTimingProps): string | undefined;
9
14
  /** `aria-invalid` for the control: `true` while there is error text. */
10
15
  export declare function invalidWhen(errorText: string | undefined): true | undefined;
11
16
  /** Id of the label `FormFieldShell` renders for control `id`. */
12
17
  export declare function labelIdFor(id: string): string;
18
+ /** Id of the hint `FormFieldShell` renders for control `id`. */
19
+ export declare function descriptionIdFor(id: string): string;
13
20
  /** Id of the error text `FormFieldShell` renders for control `id`. */
14
21
  export declare function errorIdFor(id: string): string;
22
+ /**
23
+ * Id of the hidden "Required" text `FormFieldShell` renders for a required
24
+ * control `id`, for controls that cannot carry `required` themselves.
25
+ */
26
+ export declare function requiredIdFor(id: string): string;
15
27
  /**
16
28
  * `aria-describedby` for the control: the given hint ids, plus the error
17
29
  * text while there is one, or `undefined` when there is nothing to read.
@@ -20,9 +32,10 @@ export declare function describedBy(id: string, errorText: string | undefined, .
20
32
  /**
21
33
  * Trigger ARIA for the date and date-time fields: invalid while there is
22
34
  * error text, named "label, value" (a `<label for>` alone would hide the
23
- * picked value), and described by the error.
35
+ * picked value), and described by "Required" (a button cannot take
36
+ * `aria-required`) and the error.
24
37
  */
25
- export declare function pickerFieldAria(id: string, errorText: string | undefined): {
38
+ export declare function pickerFieldAria(id: string, errorText: string | undefined, required?: boolean): {
26
39
  "aria-invalid": boolean | undefined;
27
40
  "aria-labelledby": string;
28
41
  "aria-describedby": string | undefined;
@@ -32,14 +45,20 @@ export declare function pickerFieldAria(id: string, errorText: string | undefine
32
45
  * `labelAction` sits on the label's row (the Markdown field's mode toggle).
33
46
  * Set `labelFor` to false when the control is not rendered (Markdown
34
47
  * preview), so the label does not point at a missing element.
35
- * `className` styles the wrapper (`data-slot="form-field"`), e.g. a width.
48
+ * `description` is a hint between the label and the control, with id
49
+ * `descriptionIdFor(id)`. `className` and other div props go to the
50
+ * wrapper (`data-slot="form-field"`), e.g. a width.
51
+ * `required` marks the label; the control still sets its own `required`.
52
+ * The error sits below the control and takes no space while there is none,
53
+ * so put fields in a grid row with `items-start` to keep controls aligned.
36
54
  */
37
- export declare function FormFieldShell({ id, label, labelAction, errorText, labelFor, className, children, }: {
55
+ export declare function FormFieldShell({ id, label, labelAction, description, errorText, labelFor, required, className, children, ...props }: Omit<ComponentProps<"div">, "id" | "children"> & {
38
56
  id: string;
39
57
  label: string;
40
58
  labelAction?: ReactNode;
59
+ description?: ReactNode;
41
60
  errorText: string | undefined;
42
61
  labelFor?: boolean;
43
- className?: string;
62
+ required?: boolean;
44
63
  children: ReactNode;
45
64
  }): import("react").JSX.Element;
@@ -5,11 +5,20 @@ import { cn } from "../utils.js";
5
5
  import { FieldError, getFieldErrorMessage, } from "./field-error.js";
6
6
  /**
7
7
  * Not a public entry. The layout and error wiring every `form/*` field
8
- * shares, so a field only says which control it renders.
8
+ * shares, so a field only says which control it renders. Custom controls
9
+ * get the same shell through the public `FormControlField`.
9
10
  */
10
- /** First error in `meta`, or `undefined` when there is no meta or no error. */
11
- export function metaErrorText(meta) {
12
- return meta ? getFieldErrorMessage(meta.errors[0]) : undefined;
11
+ /**
12
+ * First error in `meta`, or `undefined` when there is no meta, no error,
13
+ * `showError` is false, or the field is untouched (`isTouched: false`)
14
+ * and the form has not been submitted.
15
+ */
16
+ export function metaErrorText(meta, { submitted, showError = true } = {}) {
17
+ if (!meta || !showError)
18
+ return undefined;
19
+ if (meta.isTouched === false && !submitted)
20
+ return undefined;
21
+ return getFieldErrorMessage(meta.errors[0]);
13
22
  }
14
23
  /** `aria-invalid` for the control: `true` while there is error text. */
15
24
  export function invalidWhen(errorText) {
@@ -19,10 +28,21 @@ export function invalidWhen(errorText) {
19
28
  export function labelIdFor(id) {
20
29
  return `${id}-label`;
21
30
  }
31
+ /** Id of the hint `FormFieldShell` renders for control `id`. */
32
+ export function descriptionIdFor(id) {
33
+ return `${id}-description`;
34
+ }
22
35
  /** Id of the error text `FormFieldShell` renders for control `id`. */
23
36
  export function errorIdFor(id) {
24
37
  return `${id}-error`;
25
38
  }
39
+ /**
40
+ * Id of the hidden "Required" text `FormFieldShell` renders for a required
41
+ * control `id`, for controls that cannot carry `required` themselves.
42
+ */
43
+ export function requiredIdFor(id) {
44
+ return `${id}-required`;
45
+ }
26
46
  /**
27
47
  * `aria-describedby` for the control: the given hint ids, plus the error
28
48
  * text while there is one, or `undefined` when there is nothing to read.
@@ -34,13 +54,14 @@ export function describedBy(id, errorText, ...hintIds) {
34
54
  /**
35
55
  * Trigger ARIA for the date and date-time fields: invalid while there is
36
56
  * error text, named "label, value" (a `<label for>` alone would hide the
37
- * picked value), and described by the error.
57
+ * picked value), and described by "Required" (a button cannot take
58
+ * `aria-required`) and the error.
38
59
  */
39
- export function pickerFieldAria(id, errorText) {
60
+ export function pickerFieldAria(id, errorText, required) {
40
61
  return {
41
62
  "aria-invalid": invalidWhen(errorText),
42
63
  "aria-labelledby": `${labelIdFor(id)} ${pickerValueId(id)}`,
43
- "aria-describedby": describedBy(id, errorText),
64
+ "aria-describedby": describedBy(id, errorText, required ? requiredIdFor(id) : undefined),
44
65
  };
45
66
  }
46
67
  /**
@@ -48,9 +69,14 @@ export function pickerFieldAria(id, errorText) {
48
69
  * `labelAction` sits on the label's row (the Markdown field's mode toggle).
49
70
  * Set `labelFor` to false when the control is not rendered (Markdown
50
71
  * preview), so the label does not point at a missing element.
51
- * `className` styles the wrapper (`data-slot="form-field"`), e.g. a width.
72
+ * `description` is a hint between the label and the control, with id
73
+ * `descriptionIdFor(id)`. `className` and other div props go to the
74
+ * wrapper (`data-slot="form-field"`), e.g. a width.
75
+ * `required` marks the label; the control still sets its own `required`.
76
+ * The error sits below the control and takes no space while there is none,
77
+ * so put fields in a grid row with `items-start` to keep controls aligned.
52
78
  */
53
- export function FormFieldShell({ id, label, labelAction, errorText, labelFor = true, className, children, }) {
54
- const labelNode = (_jsx(Label, { id: labelIdFor(id), htmlFor: labelFor ? id : undefined, children: label }));
55
- return (_jsxs("div", { "data-slot": "form-field", className: cn("flex flex-col gap-1.5", className), children: [labelAction ? (_jsxs("div", { "data-slot": "form-field-header", className: "flex items-center justify-between gap-2", children: [labelNode, labelAction] })) : (labelNode), children, _jsx(FieldError, { id: errorIdFor(id), text: errorText })] }));
79
+ export function FormFieldShell({ id, label, labelAction, description, errorText, labelFor = true, required, className, children, ...props }) {
80
+ const labelNode = (_jsx(Label, { id: labelIdFor(id), htmlFor: labelFor ? id : undefined, required: required, children: label }));
81
+ return (_jsxs("div", { "data-slot": "form-field", className: cn("flex flex-col gap-1.5", className), ...props, children: [labelAction ? (_jsxs("div", { "data-slot": "form-field-header", className: "flex items-center justify-between gap-2", children: [labelNode, labelAction] })) : (labelNode), description ? (_jsx("p", { id: descriptionIdFor(id), "data-slot": "form-field-description", className: "text-xs text-muted-foreground", children: description })) : null, children, required ? (_jsx("span", { id: requiredIdFor(id), hidden: true, children: "Required" })) : null, _jsx(FieldError, { id: errorIdFor(id), text: errorText })] }));
56
82
  }
@@ -1,9 +1,10 @@
1
+ import type { ErrorTimingProps } from "./field-error.js";
1
2
  import type { TextFieldApi } from "./text-field.js";
2
3
  /**
3
4
  * GFM body field: Write is a textarea, Preview is the sanitized Markdown
4
5
  * renderer. String-in/string-out — not a WYSIWYG document model.
5
6
  */
6
- export declare function FormMarkdownField({ field, id, label, placeholder, rows, showError, className, fieldClassName, previewClassName, }: {
7
+ export declare function FormMarkdownField({ field, id, label, placeholder, rows, showError, submitted, required, className, fieldClassName, previewClassName, }: ErrorTimingProps & {
7
8
  field: TextFieldApi;
8
9
  /**
9
10
  * Control id the label points at. Defaults to `field.name`; pass a unique
@@ -13,7 +14,8 @@ export declare function FormMarkdownField({ field, id, label, placeholder, rows,
13
14
  label: string;
14
15
  placeholder?: string;
15
16
  rows?: number;
16
- showError?: boolean;
17
+ /** Sets `required` on the Write textarea and marks the label. */
18
+ required?: boolean;
17
19
  /** Forwarded to the Write textarea. */
18
20
  className?: string;
19
21
  /** Classes for the field wrapper (label row, body, and error). */
@@ -16,14 +16,14 @@ const Markdown = lazy(() => loadMarkdown().then((module) => ({ default: module.M
16
16
  * GFM body field: Write is a textarea, Preview is the sanitized Markdown
17
17
  * renderer. String-in/string-out — not a WYSIWYG document model.
18
18
  */
19
- export function FormMarkdownField({ field, id = field.name, label, placeholder = "Markdown supported", rows = 5, showError = true, className, fieldClassName, previewClassName, }) {
19
+ export function FormMarkdownField({ field, id = field.name, label, placeholder = "Markdown supported", rows = 5, showError, submitted, required, className, fieldClassName, previewClassName, }) {
20
20
  const [mode, setMode] = useState("write");
21
21
  const onChange = onInputChange(field.handleChange);
22
- const errorText = showError ? metaErrorText(field.state.meta) : undefined;
22
+ const errorText = metaErrorText(field.state.meta, { submitted, showError });
23
23
  const trimmed = field.state.value.trim();
24
24
  function handleModeChange(value) {
25
25
  if (value === "write" || value === "preview")
26
26
  setMode(value);
27
27
  }
28
- return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, labelFor: mode === "write", className: fieldClassName, labelAction: _jsxs(ToggleGroup, { type: "single", value: mode, onValueChange: handleModeChange, variant: "outline", size: "sm", spacing: 0, "aria-label": `${label} mode`, children: [_jsx(ToggleGroupItem, { value: "write", children: "Write" }), _jsx(ToggleGroupItem, { value: "preview", onPointerEnter: preloadMarkdown, onFocus: preloadMarkdown, children: "Preview" })] }), children: mode === "write" ? (_jsx(Textarea, { id: id, value: field.state.value, onChange: onChange, onBlur: field.handleBlur, placeholder: placeholder, rows: rows, className: className, "aria-invalid": invalidWhen(errorText), "aria-describedby": describedBy(id, errorText) })) : (_jsx("div", { "data-slot": "form-markdown-preview", className: cn(fieldSurface, "min-h-16", previewClassName), "aria-live": "polite", children: trimmed ? (_jsx(Suspense, { fallback: _jsx(Spinner, { "aria-label": "Loading preview" }), children: _jsx(Markdown, { content: trimmed }) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing to preview" })) })) }));
28
+ return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, labelFor: mode === "write", required: required, className: fieldClassName, labelAction: _jsxs(ToggleGroup, { type: "single", value: mode, onValueChange: handleModeChange, variant: "outline", size: "sm", spacing: 0, "aria-label": `${label} mode`, children: [_jsx(ToggleGroupItem, { value: "write", children: "Write" }), _jsx(ToggleGroupItem, { value: "preview", onPointerEnter: preloadMarkdown, onFocus: preloadMarkdown, children: "Preview" })] }), children: mode === "write" ? (_jsx(Textarea, { id: id, value: field.state.value, onChange: onChange, onBlur: field.handleBlur, placeholder: placeholder, rows: rows, required: required, className: className, "aria-invalid": invalidWhen(errorText), "aria-describedby": describedBy(id, errorText) })) : (_jsx("div", { "data-slot": "form-markdown-preview", className: cn(fieldSurface, "min-h-16", previewClassName), "aria-live": "polite", children: trimmed ? (_jsx(Suspense, { fallback: _jsx(Spinner, { "aria-label": "Loading preview" }), children: _jsx(Markdown, { content: trimmed }) })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing to preview" })) })) }));
29
29
  }
@@ -1,11 +1,13 @@
1
1
  import { type ReactNode } from "react";
2
- import type { FieldMetaState } from "./field-error.js";
3
- type Props = {
2
+ import type { ErrorTimingProps, FieldMetaState } from "./field-error.js";
3
+ type Props = ErrorTimingProps & {
4
4
  value: string | undefined;
5
5
  meta?: FieldMetaState;
6
6
  label: string;
7
7
  onValueChange: (value: string) => void;
8
8
  placeholder?: string;
9
+ /** Requires a value (native form validation) and marks the label. */
10
+ required?: boolean;
9
11
  /** Forwarded to the select trigger. */
10
12
  className?: string;
11
13
  /** Classes for the field wrapper (label, trigger, and error). */
@@ -15,5 +17,5 @@ type Props = {
15
17
  children: ReactNode;
16
18
  };
17
19
  /** Select control with built-in field error text and invalid styling. */
18
- export declare function FormSelectField({ value, meta, label, onValueChange, placeholder, className, fieldClassName, contentClassName, children, }: Props): import("react").JSX.Element;
20
+ export declare function FormSelectField({ value, meta, submitted, showError, label, onValueChange, placeholder, required, className, fieldClassName, contentClassName, children, }: Props): import("react").JSX.Element;
19
21
  export {};
@@ -4,8 +4,8 @@ import { useId } from "react";
4
4
  import { Select, SelectContent, SelectTrigger, SelectValue } from "../select.js";
5
5
  import { describedBy, FormFieldShell, invalidWhen, metaErrorText, } from "./field-shell.js";
6
6
  /** Select control with built-in field error text and invalid styling. */
7
- export function FormSelectField({ value, meta, label, onValueChange, placeholder, className, fieldClassName, contentClassName, children, }) {
8
- const errorText = metaErrorText(meta);
7
+ export function FormSelectField({ value, meta, submitted, showError, label, onValueChange, placeholder, required, className, fieldClassName, contentClassName, children, }) {
8
+ const errorText = metaErrorText(meta, { submitted, showError });
9
9
  const id = useId();
10
- return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, className: fieldClassName, children: _jsxs(Select, { value: value, onValueChange: onValueChange, children: [_jsx(SelectTrigger, { id: id, className: className, "aria-invalid": invalidWhen(errorText), "aria-describedby": describedBy(id, errorText), children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { className: contentClassName, children: children })] }) }));
10
+ return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsxs(Select, { value: value, onValueChange: onValueChange, required: required, children: [_jsx(SelectTrigger, { id: id, className: className, "aria-required": required || undefined, "aria-invalid": invalidWhen(errorText), "aria-describedby": describedBy(id, errorText), children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { className: contentClassName, children: children })] }) }));
11
11
  }
@@ -1,4 +1,4 @@
1
- import type { FieldMetaState } from "./field-error.js";
1
+ import type { ErrorTimingProps, FieldMetaState } from "./field-error.js";
2
2
  export type TextFieldApi = {
3
3
  name: string;
4
4
  state: {
@@ -8,7 +8,7 @@ export type TextFieldApi = {
8
8
  handleChange: (value: string) => void;
9
9
  handleBlur: () => void;
10
10
  };
11
- export declare function FormTextField({ field, id, label, placeholder, description, autoFocus, multiline, rows, inputType, showError, autoComplete, required, minLength, className, fieldClassName, }: {
11
+ export declare function FormTextField({ field, id, label, placeholder, description, autoFocus, multiline, rows, inputType, showError, submitted, autoComplete, required, minLength, className, fieldClassName, }: ErrorTimingProps & {
12
12
  field: TextFieldApi;
13
13
  /**
14
14
  * Control id the label points at. Defaults to `field.name`; pass a unique
@@ -25,9 +25,8 @@ export declare function FormTextField({ field, id, label, placeholder, descripti
25
25
  /** Textarea rows when `multiline` is set. */
26
26
  rows?: number;
27
27
  inputType?: "text" | "email" | "password" | "date" | "datetime-local" | "time" | "number";
28
- /** When true (default), shows meta.errors and aria-invalid styling. */
29
- showError?: boolean;
30
28
  autoComplete?: string;
29
+ /** Sets `required` on the control and marks the label. */
31
30
  required?: boolean;
32
31
  minLength?: number;
33
32
  /** Forwarded to the input or textarea. */
@@ -1,13 +1,13 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { Input } from "../input.js";
4
4
  import { Textarea } from "../textarea.js";
5
5
  import { onInputChange } from "../utils.js";
6
- import { describedBy, FormFieldShell, invalidWhen, metaErrorText, } from "./field-shell.js";
7
- export function FormTextField({ field, id = field.name, label, placeholder, description, autoFocus, multiline, rows = 3, inputType = "text", showError = true, autoComplete, required, minLength, className, fieldClassName, }) {
6
+ import { describedBy, descriptionIdFor, FormFieldShell, invalidWhen, metaErrorText, } from "./field-shell.js";
7
+ export function FormTextField({ field, id = field.name, label, placeholder, description, autoFocus, multiline, rows = 3, inputType = "text", showError, submitted, autoComplete, required, minLength, className, fieldClassName, }) {
8
8
  const onChange = onInputChange(field.handleChange);
9
- const errorText = showError ? metaErrorText(field.state.meta) : undefined;
10
- const descriptionId = description ? `${id}-description` : undefined;
9
+ const errorText = metaErrorText(field.state.meta, { submitted, showError });
10
+ const descriptionId = description ? descriptionIdFor(id) : undefined;
11
11
  const controlProps = {
12
12
  id,
13
13
  value: field.state.value,
@@ -21,5 +21,5 @@ export function FormTextField({ field, id = field.name, label, placeholder, desc
21
21
  "aria-invalid": invalidWhen(errorText),
22
22
  "aria-describedby": describedBy(id, errorText, descriptionId),
23
23
  };
24
- return (_jsxs(FormFieldShell, { id: id, label: label, errorText: errorText, className: fieldClassName, children: [description ? (_jsx("p", { id: descriptionId, "data-slot": "form-field-description", className: "text-xs text-muted-foreground", children: description })) : null, multiline ? (_jsx(Textarea, { ...controlProps, rows: rows })) : (_jsx(Input, { ...controlProps, type: inputType, autoFocus: autoFocus }))] }));
24
+ return (_jsx(FormFieldShell, { id: id, label: label, description: description, errorText: errorText, required: required, className: fieldClassName, children: multiline ? (_jsx(Textarea, { ...controlProps, rows: rows })) : (_jsx(Input, { ...controlProps, type: inputType, autoFocus: autoFocus })) }));
25
25
  }
@@ -0,0 +1,31 @@
1
+ /** A date the pickers accept: a `Date`, `yyyy-MM-dd`, ISO text, or a timestamp. */
2
+ export type DateLike = Date | string | number | null | undefined;
3
+ /**
4
+ * Coerce a date-like value to a local `Date`.
5
+ * Plain calendar dates (`yyyy-MM-dd`) parse as local midnight so they do not
6
+ * shift when the runtime timezone is behind UTC.
7
+ */
8
+ export declare function toDate(value: DateLike): Date | undefined;
9
+ /**
10
+ * A from/to pair of days. `to` is unset while a range is half picked. Same
11
+ * shape as Day Picker's, so either can be passed where the other is expected.
12
+ */
13
+ export type DateRange = {
14
+ from: Date | undefined;
15
+ to?: Date | undefined;
16
+ };
17
+ /**
18
+ * Matches calendar days, for `disabled` and `hidden`: `true` for every day,
19
+ * one date or a list, a range, days `before` / `after` a date (both: the days
20
+ * outside them), weekdays (`0` is Sunday), or a predicate.
21
+ */
22
+ export type DateMatcher = boolean | Date | Date[] | DateRange | {
23
+ before: Date;
24
+ } | {
25
+ after: Date;
26
+ } | {
27
+ before: Date;
28
+ after: Date;
29
+ } | {
30
+ dayOfWeek: number | number[];
31
+ } | ((date: Date) => boolean);