atlas-react-kit 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/COMPONENTS.md +19 -23
  2. package/DESIGN_RULES.md +14 -40
  3. package/README.md +7 -6
  4. package/dist/alert.js +6 -5
  5. package/dist/calendar.js +5 -4
  6. package/dist/combobox.js +6 -11
  7. package/dist/date-time-picker.js +2 -1
  8. package/dist/dropdown-menu.js +2 -2
  9. package/dist/field.d.ts +6 -3
  10. package/dist/field.js +3 -2
  11. package/dist/form/checkbox-field.d.ts +1 -1
  12. package/dist/form/checkbox-field.js +8 -11
  13. package/dist/form/combobox-field.js +2 -2
  14. package/dist/form/control-field.js +2 -7
  15. package/dist/form/date-picker-field.js +5 -2
  16. package/dist/form/date-time-picker-field.js +5 -2
  17. package/dist/form/field-error.d.ts +3 -3
  18. package/dist/form/field-shell.d.ts +36 -31
  19. package/dist/form/field-shell.js +41 -34
  20. package/dist/form/markdown-field.js +2 -2
  21. package/dist/form/select-field.js +2 -2
  22. package/dist/form/text-field.js +2 -4
  23. package/dist/form/toggle-group-field.js +2 -2
  24. package/dist/info-tip.d.ts +5 -4
  25. package/dist/info-tip.js +9 -4
  26. package/dist/internal/active-link.d.ts +7 -0
  27. package/dist/internal/active-link.js +9 -0
  28. package/dist/internal/calendar-size.d.ts +5 -0
  29. package/dist/internal/calendar-size.js +5 -0
  30. package/dist/internal/form-control.d.ts +1 -1
  31. package/dist/internal/form-control.js +2 -1
  32. package/dist/internal/listbox-option.d.ts +16 -3
  33. package/dist/internal/listbox-option.js +22 -5
  34. package/dist/internal/overlay-styles.d.ts +10 -5
  35. package/dist/internal/overlay-styles.js +10 -5
  36. package/dist/internal/search.d.ts +4 -0
  37. package/dist/internal/search.js +6 -0
  38. package/dist/internal/styles.d.ts +16 -0
  39. package/dist/internal/styles.js +16 -0
  40. package/dist/kanban.js +2 -1
  41. package/dist/label.js +2 -1
  42. package/dist/lazy-calendar.js +3 -2
  43. package/dist/nav-link.js +2 -1
  44. package/dist/popover.js +4 -3
  45. package/dist/search-picker.js +5 -9
  46. package/dist/select.js +4 -4
  47. package/dist/skip-link.js +2 -1
  48. package/dist/tabs.js +3 -2
  49. package/dist/toast.js +11 -10
  50. package/dist/tooltip.js +2 -2
  51. package/package.json +1 -1
  52. package/skills/atlas-react-kit/SKILL.md +46 -42
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useId } from "react";
4
- import { describedBy, descriptionIdFor, FormFieldShell, invalidWhen, metaErrorText, } from "./field-shell.js";
4
+ import { fieldAria, FormFieldShell, metaErrorText } from "./field-shell.js";
5
5
  /**
6
6
  * The `form/*` field layout for a control the kit does not ship (a
7
7
  * combobox, a segmented picker): label with the required marker, optional
@@ -13,10 +13,5 @@ export function FormControlField({ id: idProp, label, description, required, met
13
13
  const generatedId = useId();
14
14
  const id = idProp ?? generatedId;
15
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
- }) }));
16
+ return (_jsx(FormFieldShell, { ...props, id: id, label: label, description: description, errorText: errorText, required: required, children: children({ id, ...fieldAria(id, errorText, { description, required }) }) }));
22
17
  }
@@ -2,11 +2,14 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useId } from "react";
4
4
  import { DatePickerButton } from "../date-picker.js";
5
- import { FormFieldShell, metaErrorText, pickerFieldAria, } from "./field-shell.js";
5
+ import { fieldAria, FormFieldShell, metaErrorText, } from "./field-shell.js";
6
6
  /** Date picker with built-in field error text and invalid styling. */
7
7
  export function FormDatePickerField({ id: idProp, onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }) {
8
8
  const errorText = metaErrorText(meta, { submitted, showError });
9
9
  const generatedId = useId();
10
10
  const id = idProp ?? generatedId;
11
- return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsx(DatePickerButton, { ...picker, id: id, onValueChange: onValueChange, ...pickerFieldAria(id, errorText, required) }) }));
11
+ return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsx(DatePickerButton, { ...picker, id: id, onValueChange: onValueChange, ...fieldAria(id, errorText, {
12
+ required: required && "described",
13
+ labelledBy: "label-value",
14
+ }) }) }));
12
15
  }
@@ -2,11 +2,14 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useId } from "react";
4
4
  import { DateTimePickerButton } from "../date-time-picker.js";
5
- import { FormFieldShell, metaErrorText, pickerFieldAria, } from "./field-shell.js";
5
+ import { fieldAria, FormFieldShell, metaErrorText, } from "./field-shell.js";
6
6
  /** Date-time picker with built-in field error text and invalid styling. */
7
7
  export function FormDateTimePickerField({ id: idProp, onValueChange, label, meta, submitted, showError, required, fieldClassName, ...picker }) {
8
8
  const errorText = metaErrorText(meta, { submitted, showError });
9
9
  const generatedId = useId();
10
10
  const id = idProp ?? generatedId;
11
- return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsx(DateTimePickerButton, { ...picker, id: id, onValueChange: onValueChange, ...pickerFieldAria(id, errorText, required) }) }));
11
+ return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsx(DateTimePickerButton, { ...picker, id: id, onValueChange: onValueChange, ...fieldAria(id, errorText, {
12
+ required: required && "described",
13
+ labelledBy: "label-value",
14
+ }) }) }));
12
15
  }
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
- export type FieldErrorMessage = string | {
3
- message?: string;
4
- } | undefined;
2
+ import type { FieldErrorItem } from "../field.js";
3
+ /** An error as TanStack Form reports it: text, or an error with a `message`. */
4
+ export type FieldErrorMessage = string | FieldErrorItem;
5
5
  export type FieldMetaState = {
6
6
  /** Validation errors for the field; the `form/*` fields show the first. */
7
7
  errors: FieldErrorMessage[];
@@ -1,5 +1,6 @@
1
1
  import type { ComponentProps, ReactNode } from "react";
2
2
  import type { DateLike, DatePickerCalendarProps } from "../date-picker.js";
3
+ import type { AsChildProps } from "../internal/as-child.js";
3
4
  import { type ErrorTimingProps, type FieldMetaState, type FormFieldBaseProps, type FormFieldMetaProps } from "./field-error.js";
4
5
  /**
5
6
  * Not a public entry. The layout and error wiring every `form/*` field
@@ -12,19 +13,25 @@ import { type ErrorTimingProps, type FieldMetaState, type FormFieldBaseProps, ty
12
13
  * and the form has not been submitted.
13
14
  */
14
15
  export declare function metaErrorText(meta: FieldMetaState | undefined, { submitted, showError }?: ErrorTimingProps): string | undefined;
15
- /** `aria-invalid` for the control: `true` while there is error text. */
16
- export declare function invalidWhen(errorText: string | undefined): true | undefined;
17
- /** Id of the label `FormFieldShell` renders for control `id`. */
18
- export declare function labelIdFor(id: string): string;
19
- /** Id of the hint `FormFieldShell` renders for control `id`. */
20
- export declare function descriptionIdFor(id: string): string;
21
- /** Id of the error text `FormFieldShell` renders for control `id`. */
22
- export declare function errorIdFor(id: string): string;
23
16
  /**
24
- * `aria-describedby` for the control: the given hint ids, plus the error
25
- * text while there is one, or `undefined` when there is nothing to read.
17
+ * ARIA for a field's control, from the ids `FormFieldShell` renders:
18
+ * invalid while there is error text, described by the hint, the hidden
19
+ * "Required" text, and the error. `required: "described"` is for a button
20
+ * trigger, which cannot take `aria-required`; leave `required` out where
21
+ * the control carries native `required`. `labelledBy: "label"` names a
22
+ * group; `"label-value"` reads a picker "label, value".
26
23
  */
27
- export declare function describedBy(id: string, errorText: string | undefined, ...hintIds: Array<string | undefined>): string | undefined;
24
+ export declare function fieldAria(id: string, errorText: string | undefined, { description, required, labelledBy, }?: {
25
+ /** The field's hint; when set, the control is described by it. */
26
+ description?: ReactNode;
27
+ required?: boolean | "described";
28
+ labelledBy?: "label" | "label-value";
29
+ }): {
30
+ "aria-invalid": true | undefined;
31
+ "aria-describedby": string | undefined;
32
+ "aria-required": true | undefined;
33
+ "aria-labelledby"?: string | undefined;
34
+ };
28
35
  /**
29
36
  * Props the date and date-time fields share; each adds its own `value`,
30
37
  * `onValueChange`, and `placeholder`.
@@ -52,29 +59,27 @@ export type PickerFieldBaseProps = FormFieldBaseProps & FormFieldMetaProps & {
52
59
  calendarProps?: DatePickerCalendarProps;
53
60
  };
54
61
  /**
55
- * Trigger ARIA for the date and date-time fields: invalid while there is
56
- * error text, named "label, value" (a `<label for>` alone would hide the
57
- * picked value), and described by "Required" (a button cannot take
58
- * `aria-required`) and the error.
62
+ * Every `form/*` field's wrapper (`data-slot="form-field"`): children with
63
+ * the field spacing, then the error text. `asChild` makes the one child the
64
+ * wrapper (the checkbox group's fieldset).
59
65
  */
60
- export declare function pickerFieldAria(id: string, errorText: string | undefined, required?: boolean): {
61
- "aria-invalid": boolean | undefined;
62
- "aria-labelledby": string;
63
- "aria-describedby": string | undefined;
64
- };
66
+ export declare function FormFieldFrame({ id, errorText, asChild, className, children, ...props }: Omit<ComponentProps<"div">, "id"> & AsChildProps & {
67
+ /** Id of the control; the error id derives from it. */
68
+ id: string;
69
+ /** Error text under the children; `undefined` shows none. */
70
+ errorText: string | undefined;
71
+ }): import("react").JSX.Element;
72
+ /** A field's hint, with id `descriptionIdFor(id)`. */
73
+ export declare function FormFieldDescription({ id, children, }: {
74
+ id: string;
75
+ children: ReactNode;
76
+ }): import("react").JSX.Element;
65
77
  /**
66
- * Label, control, and error text stacked with the kit's field spacing.
67
- * `labelAction` sits on the label's row (the Markdown field's mode toggle).
68
- * Set `labelFor` to false when the control is not rendered (Markdown
69
- * preview), so the label does not point at a missing element.
70
- * `description` is a hint between the label and the control, with id
71
- * `descriptionIdFor(id)`. `className` and other div props go to the
72
- * wrapper (`data-slot="form-field"`), e.g. a width.
73
- * `required` marks the label; the control still sets its own `required`.
74
- * The error sits below the control and takes no space while there is none,
75
- * so put fields in a grid row with `items-start` to keep controls aligned.
78
+ * Label, hint, control, and error in a `FormFieldFrame`. `labelAction` sits
79
+ * on the label's row; `labelFor={false}` when the control is not rendered
80
+ * (Markdown preview). `required` marks the label only.
76
81
  */
77
- export declare function FormFieldShell({ id, label, labelAction, description, errorText, labelFor, required, className, children, ...props }: Omit<ComponentProps<"div">, "id" | "children"> & {
82
+ export declare function FormFieldShell({ id, label, labelAction, description, errorText, labelFor, required, children, ...props }: Omit<ComponentProps<"div">, "id" | "children"> & {
78
83
  /** Id of the control; the label, hint, and error ids derive from it. */
79
84
  id: string;
80
85
  /** Visible label text. */
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Slot } from "radix-ui";
2
3
  import { pickerValueId } from "../internal/form-control.js";
3
4
  import { Label } from "../label.js";
4
5
  import { cn } from "../utils.js";
@@ -20,20 +21,16 @@ export function metaErrorText(meta, { submitted, showError = true } = {}) {
20
21
  return undefined;
21
22
  return getFieldErrorMessage(meta.errors[0]);
22
23
  }
23
- /** `aria-invalid` for the control: `true` while there is error text. */
24
- export function invalidWhen(errorText) {
25
- return errorText ? true : undefined;
26
- }
27
24
  /** Id of the label `FormFieldShell` renders for control `id`. */
28
- export function labelIdFor(id) {
25
+ function labelIdFor(id) {
29
26
  return `${id}-label`;
30
27
  }
31
28
  /** Id of the hint `FormFieldShell` renders for control `id`. */
32
- export function descriptionIdFor(id) {
29
+ function descriptionIdFor(id) {
33
30
  return `${id}-description`;
34
31
  }
35
32
  /** Id of the error text `FormFieldShell` renders for control `id`. */
36
- export function errorIdFor(id) {
33
+ function errorIdFor(id) {
37
34
  return `${id}-error`;
38
35
  }
39
36
  /**
@@ -44,39 +41,49 @@ function requiredIdFor(id) {
44
41
  return `${id}-required`;
45
42
  }
46
43
  /**
47
- * `aria-describedby` for the control: the given hint ids, plus the error
48
- * text while there is one, or `undefined` when there is nothing to read.
44
+ * ARIA for a field's control, from the ids `FormFieldShell` renders:
45
+ * invalid while there is error text, described by the hint, the hidden
46
+ * "Required" text, and the error. `required: "described"` is for a button
47
+ * trigger, which cannot take `aria-required`; leave `required` out where
48
+ * the control carries native `required`. `labelledBy: "label"` names a
49
+ * group; `"label-value"` reads a picker "label, value".
49
50
  */
50
- export function describedBy(id, errorText, ...hintIds) {
51
- const ids = [...hintIds, errorText ? errorIdFor(id) : undefined].filter(Boolean);
52
- return ids.length ? ids.join(" ") : undefined;
51
+ export function fieldAria(id, errorText, { description, required, labelledBy, } = {}) {
52
+ const describedBy = [
53
+ description ? descriptionIdFor(id) : undefined,
54
+ required === "described" ? requiredIdFor(id) : undefined,
55
+ errorText ? errorIdFor(id) : undefined,
56
+ ].filter(Boolean);
57
+ return {
58
+ "aria-invalid": errorText ? true : undefined,
59
+ "aria-describedby": describedBy.length ? describedBy.join(" ") : undefined,
60
+ "aria-required": required === true ? true : undefined,
61
+ ...(labelledBy && {
62
+ "aria-labelledby": labelledBy === "label"
63
+ ? labelIdFor(id)
64
+ : `${labelIdFor(id)} ${pickerValueId(id)}`,
65
+ }),
66
+ };
53
67
  }
54
68
  /**
55
- * Trigger ARIA for the date and date-time fields: invalid while there is
56
- * error text, named "label, value" (a `<label for>` alone would hide the
57
- * picked value), and described by "Required" (a button cannot take
58
- * `aria-required`) and the error.
69
+ * Every `form/*` field's wrapper (`data-slot="form-field"`): children with
70
+ * the field spacing, then the error text. `asChild` makes the one child the
71
+ * wrapper (the checkbox group's fieldset).
59
72
  */
60
- export function pickerFieldAria(id, errorText, required) {
61
- return {
62
- "aria-invalid": invalidWhen(errorText),
63
- "aria-labelledby": `${labelIdFor(id)} ${pickerValueId(id)}`,
64
- "aria-describedby": describedBy(id, errorText, required ? requiredIdFor(id) : undefined),
65
- };
73
+ export function FormFieldFrame({ id, errorText, asChild, className, children, ...props }) {
74
+ const Comp = asChild ? Slot.Root : "div";
75
+ return (_jsxs(Comp, { "data-slot": "form-field", className: cn("flex flex-col gap-1.5", className), ...props, children: [_jsx(Slot.Slottable, { children: children }), _jsx(FieldError, { id: errorIdFor(id), text: errorText })] }));
76
+ }
77
+ /** A field's hint, with id `descriptionIdFor(id)`. */
78
+ export function FormFieldDescription({ id, children, }) {
79
+ return (_jsx("p", { id: descriptionIdFor(id), "data-slot": "form-field-description", className: "text-xs text-muted-foreground", children: children }));
66
80
  }
67
81
  /**
68
- * Label, control, and error text stacked with the kit's field spacing.
69
- * `labelAction` sits on the label's row (the Markdown field's mode toggle).
70
- * Set `labelFor` to false when the control is not rendered (Markdown
71
- * preview), so the label does not point at a missing element.
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.
82
+ * Label, hint, control, and error in a `FormFieldFrame`. `labelAction` sits
83
+ * on the label's row; `labelFor={false}` when the control is not rendered
84
+ * (Markdown preview). `required` marks the label only.
78
85
  */
79
- export function FormFieldShell({ id, label, labelAction, description, errorText, labelFor = true, required, className, children, ...props }) {
86
+ export function FormFieldShell({ id, label, labelAction, description, errorText, labelFor = true, required, children, ...props }) {
80
87
  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 })] }));
88
+ return (_jsxs(FormFieldFrame, { ...props, id: id, errorText: errorText, children: [labelAction ? (_jsxs("div", { "data-slot": "form-field-header", className: "flex items-center justify-between gap-2", children: [labelNode, labelAction] })) : (labelNode), description ? (_jsx(FormFieldDescription, { id: id, children: description })) : null, children, required ? (_jsx("span", { id: requiredIdFor(id), hidden: true, children: "Required" })) : null] }));
82
89
  }
@@ -6,7 +6,7 @@ import { Textarea } from "../textarea.js";
6
6
  import { ToggleGroup, ToggleGroupItem } from "../toggle-group.js";
7
7
  import { fieldSurface } from "../internal/styles.js";
8
8
  import { cn, onInputChange } from "../utils.js";
9
- import { describedBy, FormFieldShell, invalidWhen, metaErrorText, } from "./field-shell.js";
9
+ import { fieldAria, FormFieldShell, metaErrorText } from "./field-shell.js";
10
10
  // The renderer (react-markdown + remark-gfm) is fetched the first time
11
11
  // Preview is shown or hovered, so a form in Write mode stays light.
12
12
  const loadMarkdown = () => import("../markdown.js");
@@ -25,5 +25,5 @@ export function FormMarkdownField({ field, id = field.name, label, placeholder =
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", 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" })) })) }));
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, ...fieldAria(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
  }
@@ -2,11 +2,11 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId } from "react";
4
4
  import { Select, SelectContent, SelectTrigger, SelectValue } from "../select.js";
5
- import { describedBy, FormFieldShell, invalidWhen, metaErrorText, } from "./field-shell.js";
5
+ import { fieldAria, FormFieldShell, metaErrorText } from "./field-shell.js";
6
6
  /** Select control with built-in field error text and invalid styling. */
7
7
  export function FormSelectField({ id: idProp, value, meta, submitted, showError, label, onValueChange, placeholder, required, className, fieldClassName, contentClassName, children, }) {
8
8
  const errorText = metaErrorText(meta, { submitted, showError });
9
9
  const generatedId = useId();
10
10
  const id = idProp ?? generatedId;
11
- 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
+ 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, ...fieldAria(id, errorText, { required }), children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { className: contentClassName, children: children })] }) }));
12
12
  }
@@ -3,7 +3,7 @@ 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, descriptionIdFor, FormFieldShell, invalidWhen, metaErrorText, } from "./field-shell.js";
6
+ import { fieldAria, FormFieldShell, metaErrorText } from "./field-shell.js";
7
7
  /**
8
8
  * Text input, or a textarea with `multiline`, bound to a TanStack Form
9
9
  * `field`, with a label, optional hint, and the `form/*` error text.
@@ -11,7 +11,6 @@ import { describedBy, descriptionIdFor, FormFieldShell, invalidWhen, metaErrorTe
11
11
  export function FormTextField({ field, id = field.name, label, placeholder, description, autoFocus, multiline, rows = 3, inputType = "text", showError, submitted, autoComplete, required, minLength, className, fieldClassName, }) {
12
12
  const onChange = onInputChange(field.handleChange);
13
13
  const errorText = metaErrorText(field.state.meta, { submitted, showError });
14
- const descriptionId = description ? descriptionIdFor(id) : undefined;
15
14
  const controlProps = {
16
15
  id,
17
16
  value: field.state.value,
@@ -22,8 +21,7 @@ export function FormTextField({ field, id = field.name, label, placeholder, desc
22
21
  required,
23
22
  minLength,
24
23
  className,
25
- "aria-invalid": invalidWhen(errorText),
26
- "aria-describedby": describedBy(id, errorText, descriptionId),
24
+ ...fieldAria(id, errorText, { description }),
27
25
  };
28
26
  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 })) }));
29
27
  }
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useId } from "react";
4
4
  import { ToggleGroup, ToggleGroupItem } from "../toggle-group.js";
5
- import { describedBy, descriptionIdFor, FormFieldShell, invalidWhen, labelIdFor, metaErrorText, } from "./field-shell.js";
5
+ import { fieldAria, FormFieldShell, metaErrorText } from "./field-shell.js";
6
6
  /**
7
7
  * One choice from a few as a joined outline toggle group (a segmented
8
8
  * control), with a visible label and the `form/*` error wiring.
@@ -15,5 +15,5 @@ export function FormToggleGroupField({ label, description, value, onValueChange,
15
15
  if (option)
16
16
  onValueChange(option.value);
17
17
  }
18
- return (_jsx(FormFieldShell, { id: id, label: label, labelFor: false, description: description, errorText: errorText, required: required, className: fieldClassName, children: _jsx(ToggleGroup, { id: id, type: "single", value: value, onValueChange: handleValueChange, variant: "outline", size: size, spacing: 0, disabled: disabled, "aria-labelledby": labelIdFor(id), "aria-invalid": invalidWhen(errorText), "aria-describedby": describedBy(id, errorText, description ? descriptionIdFor(id) : undefined), className: className, children: options.map((option) => (_jsx(ToggleGroupItem, { value: option.value, children: option.label }, option.value))) }) }));
18
+ return (_jsx(FormFieldShell, { id: id, label: label, labelFor: false, description: description, errorText: errorText, required: required, className: fieldClassName, children: _jsx(ToggleGroup, { id: id, type: "single", value: value, onValueChange: handleValueChange, variant: "outline", size: size, spacing: 0, disabled: disabled, ...fieldAria(id, errorText, { description, labelledBy: "label" }), className: className, children: options.map((option) => (_jsx(ToggleGroupItem, { value: option.value, children: option.label }, option.value))) }) }));
19
19
  }
@@ -11,8 +11,9 @@ export type InfoTipProps = {
11
11
  /** Extra body after the title and description. */
12
12
  children?: React.ReactNode;
13
13
  /**
14
- * `inline` keeps the surrounding line height; the 44px hit area overflows.
15
- * Default `box`.
14
+ * `inline` sits in a line of text: the glyph is 1em, a quarter em after
15
+ * the word, on the text's cap height, and keeps the line height; the 44px
16
+ * hit area overflows. Default `box`.
16
17
  */
17
18
  layout?: InfoTipLayout;
18
19
  /** Panel side. Default `top`; Radix still flips on collision. */
@@ -29,8 +30,8 @@ export type InfoTipProps = {
29
30
  contentClassName?: string;
30
31
  } & Omit<React.ComponentProps<"button">, "title" | "children" | "type" | "onMouseEnter" | "onMouseLeave" | "onClick">;
31
32
  /**
32
- * Longer help than a Tooltip: a Popover with a 20px glyph and a 44px hit
33
- * area. Hover opens only on a fine pointer; click, tap, and keyboard always
33
+ * Longer help than a Tooltip: a Popover with a 20px glyph (1em inline) and
34
+ * a 44px hit area. Hover opens only on a fine pointer; click, tap, and keyboard always
34
35
  * toggle it. Not a glossary or "coming soon" chip — pass that copy in.
35
36
  */
36
37
  declare function InfoTip({ label, title, description, children, layout, side, align, badge, icon, className, contentClassName, ...props }: InfoTipProps): React.JSX.Element;
package/dist/info-tip.js CHANGED
@@ -7,8 +7,8 @@ import { focusRing, hitArea44, quietTextHover } from "./internal/styles.js";
7
7
  import { HOVER_MEDIA_QUERY, useMediaQuery } from "./internal/use-media-query.js";
8
8
  import { cn } from "./utils.js";
9
9
  /**
10
- * Longer help than a Tooltip: a Popover with a 20px glyph and a 44px hit
11
- * area. Hover opens only on a fine pointer; click, tap, and keyboard always
10
+ * Longer help than a Tooltip: a Popover with a 20px glyph (1em inline) and
11
+ * a 44px hit area. Hover opens only on a fine pointer; click, tap, and keyboard always
12
12
  * toggle it. Not a glossary or "coming soon" chip — pass that copy in.
13
13
  */
14
14
  function InfoTip({ label, title, description, children, layout = "box", side = "top", align = "center", badge, icon, className, contentClassName, ...props }) {
@@ -51,7 +51,12 @@ function InfoTip({ label, title, description, children, layout = "box", side = "
51
51
  if (next)
52
52
  openedBy.current = "click";
53
53
  setOpen(next);
54
- }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { ...props, type: "button", "aria-label": label, "data-slot": "info-tip-trigger", "data-layout": layout, className: cn(focusRing, "relative inline-flex shrink-0 items-center justify-center rounded-none text-muted-foreground", quietTextHover, inline ? cn("size-5 align-middle", hitArea44) : "size-11", className), onMouseEnter: openFromHover, onMouseLeave: closeFromHover, onClick: (event) => {
54
+ }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { ...props, type: "button", "aria-label": label, "data-slot": "info-tip-trigger", "data-layout": layout, className: cn(focusRing, "relative inline-flex shrink-0 items-center justify-center rounded-none text-muted-foreground", quietTextHover,
55
+ // Inline: 1em scales with the sentence; -0.125em centres the
56
+ // glyph on the cap height without growing the line box.
57
+ inline
58
+ ? cn("ml-[0.25em] size-[1em] align-[-0.125em]", hitArea44)
59
+ : "size-11", className), onMouseEnter: openFromHover, onMouseLeave: closeFromHover, onClick: (event) => {
55
60
  // Hover already opened the panel; this click pins it instead of
56
61
  // toggling it closed (Playwright and real mice both hover before
57
62
  // they click). Preventing default skips Radix's toggle.
@@ -60,7 +65,7 @@ function InfoTip({ label, title, description, children, layout = "box", side = "
60
65
  cancelClose();
61
66
  openedBy.current = "click";
62
67
  }
63
- }, children: icon ?? (_jsx(CircleHelpIcon, { className: "size-5", "data-icon": "info-tip", "aria-hidden": true })) }) }), _jsxs(PopoverContent, { side: side, align: align, className: cn("w-72", contentClassName), "aria-labelledby": title ? titleId : undefined, "aria-label": title ? undefined : label,
68
+ }, children: icon ?? (_jsx(CircleHelpIcon, { className: inline ? "size-full" : "size-5", "data-icon": "info-tip", "aria-hidden": true })) }) }), _jsxs(PopoverContent, { side: side, align: align, className: cn("w-72", contentClassName), "aria-labelledby": title ? titleId : undefined, "aria-label": title ? undefined : label,
64
69
  // A pointer passing over the glyph must not take focus from where
65
70
  // the user is typing, nor hand it to the trigger on close.
66
71
  onOpenAutoFocus: (event) => {
@@ -0,0 +1,7 @@
1
+ /** How NavLink and TabsNavLink mark the current page (`selectedState.active`). */
2
+ export declare function activeLinkAttributes(active: boolean): {
3
+ "data-state": string;
4
+ "aria-current": "page" | undefined;
5
+ };
6
+ /** A link marked by `activeLinkAttributes`, or an active Radix tab. */
7
+ export declare const activeLinkSelector = "[aria-current=\"page\"], [data-state=\"active\"]";
@@ -0,0 +1,9 @@
1
+ /** How NavLink and TabsNavLink mark the current page (`selectedState.active`). */
2
+ export function activeLinkAttributes(active) {
3
+ return {
4
+ "data-state": active ? "active" : "inactive",
5
+ "aria-current": active ? "page" : undefined,
6
+ };
7
+ }
8
+ /** A link marked by `activeLinkAttributes`, or an active Radix tab. */
9
+ export const activeLinkSelector = '[aria-current="page"], [data-state="active"]';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Calendar cell size and padding, shared with LazyCalendar's placeholder
3
+ * (here, not in calendar.tsx, so the placeholder skips Day Picker).
4
+ */
5
+ export declare const calendarSize = "[--cell-size:--spacing(8)] [--calendar-padding:--spacing(3)]";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Calendar cell size and padding, shared with LazyCalendar's placeholder
3
+ * (here, not in calendar.tsx, so the placeholder skips Day Picker).
4
+ */
5
+ export const calendarSize = "[--cell-size:--spacing(8)] [--calendar-padding:--spacing(3)]";
@@ -12,7 +12,7 @@ export declare const formControlTriggerClassName = "h-10 w-full border-input bg-
12
12
  * with muted text while empty (`data-empty`). Intrinsic padding makes one
13
13
  * line 40px tall while longer values wrap and consumer height classes work.
14
14
  */
15
- export declare const pickerTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background h-auto justify-start py-[calc((--spacing(10)_-_1lh_-_2px)/2)] data-[empty=true]:text-muted-foreground";
15
+ export declare const pickerTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background h-auto justify-start py-[calc((--spacing(10)_-_1lh_-_2px)/2)] data-placeholder:text-muted-foreground data-[empty=true]:text-muted-foreground";
16
16
  /** Calendar popover body: fits the viewport height and scrolls inside it. */
17
17
  export declare const pickerContentClassName = "max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-none p-0";
18
18
  /** Gutter used when a calendar popover collides with the viewport. */
@@ -1,3 +1,4 @@
1
+ import { emptyValueText } from "./styles.js";
1
2
  /**
2
3
  * Chrome that makes a Button-based picker trigger match Input / SelectTrigger:
3
4
  * 40px height, px-3, body text (16px below md so iOS does not zoom), border-input,
@@ -12,7 +13,7 @@ export const formControlTriggerClassName = "h-10 w-full border-input bg-backgrou
12
13
  * with muted text while empty (`data-empty`). Intrinsic padding makes one
13
14
  * line 40px tall while longer values wrap and consumer height classes work.
14
15
  */
15
- export const pickerTriggerClassName = `${formControlTriggerClassName} h-auto justify-start py-[calc((--spacing(10)_-_1lh_-_2px)/2)] data-[empty=true]:text-muted-foreground`;
16
+ export const pickerTriggerClassName = `${formControlTriggerClassName} h-auto justify-start py-[calc((--spacing(10)_-_1lh_-_2px)/2)] ${emptyValueText}`;
16
17
  /** Calendar popover body: fits the viewport height and scrolls inside it. */
17
18
  export const pickerContentClassName = "max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-none p-0";
18
19
  /** Gutter used when a calendar popover collides with the viewport. */
@@ -1,4 +1,16 @@
1
1
  import * as React from "react";
2
+ /**
3
+ * ARIA for the search input of a listbox; the input sets `aria-controls`
4
+ * itself, where the linter can see it.
5
+ */
6
+ export declare function listboxInputAria(listId: string, activeIndex: number | undefined): {
7
+ "aria-autocomplete": "list";
8
+ "aria-activedescendant": string | undefined;
9
+ };
10
+ /** The scrolling listbox; sr-only while `ListboxEmptyStatus` shows. */
11
+ export declare function Listbox({ empty, className, ...props }: Omit<React.ComponentProps<"ul">, "role"> & {
12
+ empty: boolean;
13
+ }): React.JSX.Element;
2
14
  type PickerOptionProps<T> = {
3
15
  item: T;
4
16
  getLabel: (item: T) => string;
@@ -16,9 +28,10 @@ type PickerOptionProps<T> = {
16
28
  declare function PickerOptionInner<T>({ item, getLabel, onSelect, onHighlight, showCheck, ...props }: PickerOptionProps<T>): React.JSX.Element;
17
29
  export declare const PickerOption: typeof PickerOptionInner;
18
30
  /**
19
- * The "No results" status. It sits outside the listbox, which may own only
20
- * options, and stays mounted (sr-only when not empty) so screen readers
21
- * announce the empty state as the query changes.
31
+ * The "No results" status (`children`, default "No results"). It sits
32
+ * outside the listbox, which may own only options, and stays mounted
33
+ * (sr-only when not empty) so screen readers announce the empty state as
34
+ * the query changes.
22
35
  */
23
36
  export declare function ListboxEmptyStatus({ empty, className, children, ...props }: Omit<React.ComponentProps<"div">, "role"> & {
24
37
  empty: boolean;
@@ -4,7 +4,23 @@ import { CheckIcon } from "lucide-react";
4
4
  import { cn } from "../utils.js";
5
5
  import { listboxOptionId, scrollOptionIntoView } from "./listbox-navigation.js";
6
6
  import { optionRow } from "./overlay-styles.js";
7
- import { listboxEmpty, optionState } from "./styles.js";
7
+ import { listboxEmpty, listboxScroll, optionState } from "./styles.js";
8
+ /**
9
+ * ARIA for the search input of a listbox; the input sets `aria-controls`
10
+ * itself, where the linter can see it.
11
+ */
12
+ export function listboxInputAria(listId, activeIndex) {
13
+ return {
14
+ "aria-autocomplete": "list",
15
+ "aria-activedescendant": activeIndex === undefined
16
+ ? undefined
17
+ : listboxOptionId(listId, activeIndex),
18
+ };
19
+ }
20
+ /** The scrolling listbox; sr-only while `ListboxEmptyStatus` shows. */
21
+ export function Listbox({ empty, className, ...props }) {
22
+ return (_jsx("ul", { role: "listbox", className: empty ? "sr-only" : cn(listboxScroll, className), ...props }));
23
+ }
8
24
  /**
9
25
  * One option of a search-driven listbox (Combobox, SearchPicker): a
10
26
  * non-focusable button in a presentational `li`, highlighted by state and
@@ -20,10 +36,11 @@ function PickerOptionInner({ item, getLabel, onSelect, onHighlight, showCheck, .
20
36
  }
21
37
  export const PickerOption = React.memo(PickerOptionInner);
22
38
  /**
23
- * The "No results" status. It sits outside the listbox, which may own only
24
- * options, and stays mounted (sr-only when not empty) so screen readers
25
- * announce the empty state as the query changes.
39
+ * The "No results" status (`children`, default "No results"). It sits
40
+ * outside the listbox, which may own only options, and stays mounted
41
+ * (sr-only when not empty) so screen readers announce the empty state as
42
+ * the query changes.
26
43
  */
27
- export function ListboxEmptyStatus({ empty, className, children, ...props }) {
44
+ export function ListboxEmptyStatus({ empty, className, children = "No results", ...props }) {
28
45
  return (_jsx("div", { role: "status", className: empty ? cn(listboxEmpty, className) : "sr-only", ...props, children: empty ? children : null }));
29
46
  }
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Classes shared by the kit's floating surfaces (Popover, DropdownMenu,
3
- * Select, Dialog, Tooltip) and the option rows inside them. Components add
4
- * these with `cn()` instead of retyping them, so the panels and rows look
5
- * and move the same everywhere. Tailwind only generates class names it
6
- * finds as literals, so every class appears here in full.
2
+ * Classes and placement defaults shared by the kit's floating surfaces
3
+ * (Popover, DropdownMenu, Select, Dialog, Tooltip) and the option rows
4
+ * inside them. Components use these instead of retyping them, so the
5
+ * panels and rows look, sit, and move the same everywhere. Tailwind only
6
+ * generates class names it finds as literals, so every class appears here
7
+ * in full.
7
8
  */
8
9
  /** The panel: square corners, popover colors, shadow, and a hairline ring. */
9
10
  export declare const overlaySurface = "rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10";
@@ -24,3 +25,7 @@ export declare const overlayFadeZoom = "duration-100 data-open:animate-in data-o
24
25
  export declare const optionRow = "relative flex w-full cursor-default items-center gap-2.5 rounded-none py-2 pl-3 text-left text-sm outline-hidden select-none";
25
26
  /** Group label in a menu or listbox (DropdownMenuLabel, SelectLabel). */
26
27
  export declare const menuLabel = "px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase";
28
+ /** Default gap in px between a trigger and its panel (Radix `sideOffset`). */
29
+ export declare const OVERLAY_SIDE_OFFSET = 4;
30
+ /** Default px a panel keeps from the viewport edge (Radix `collisionPadding`). */
31
+ export declare const OVERLAY_COLLISION_PADDING = 8;
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Classes shared by the kit's floating surfaces (Popover, DropdownMenu,
3
- * Select, Dialog, Tooltip) and the option rows inside them. Components add
4
- * these with `cn()` instead of retyping them, so the panels and rows look
5
- * and move the same everywhere. Tailwind only generates class names it
6
- * finds as literals, so every class appears here in full.
2
+ * Classes and placement defaults shared by the kit's floating surfaces
3
+ * (Popover, DropdownMenu, Select, Dialog, Tooltip) and the option rows
4
+ * inside them. Components use these instead of retyping them, so the
5
+ * panels and rows look, sit, and move the same everywhere. Tailwind only
6
+ * generates class names it finds as literals, so every class appears here
7
+ * in full.
7
8
  */
8
9
  /** The panel: square corners, popover colors, shadow, and a hairline ring. */
9
10
  export const overlaySurface = "rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10";
@@ -24,3 +25,7 @@ export const overlayFadeZoom = `${overlayFade} data-open:zoom-in-95 data-closed:
24
25
  export const optionRow = "relative flex w-full cursor-default items-center gap-2.5 rounded-none py-2 pl-3 text-left text-sm outline-hidden select-none";
25
26
  /** Group label in a menu or listbox (DropdownMenuLabel, SelectLabel). */
26
27
  export const menuLabel = "px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase";
28
+ /** Default gap in px between a trigger and its panel (Radix `sideOffset`). */
29
+ export const OVERLAY_SIDE_OFFSET = 4;
30
+ /** Default px a panel keeps from the viewport edge (Radix `collisionPadding`). */
31
+ export const OVERLAY_COLLISION_PADDING = 8;
@@ -9,4 +9,8 @@ export declare function createSearchIndex<T>(items: T[], getLabel: (item: T) =>
9
9
  item: T;
10
10
  label: string;
11
11
  }[];
12
+ /**
13
+ * The items whose folded label includes `needle` (a `normalizeSearchQuery`
14
+ * result), in order; every item when `needle` is empty.
15
+ */
12
16
  export declare function filterSearchIndex<T>(index: ReturnType<typeof createSearchIndex<T>>, needle: string): T[];