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.
- package/COMPONENTS.md +19 -23
- package/DESIGN_RULES.md +14 -40
- package/README.md +7 -6
- package/dist/alert.js +6 -5
- package/dist/calendar.js +5 -4
- package/dist/combobox.js +6 -11
- package/dist/date-time-picker.js +2 -1
- package/dist/dropdown-menu.js +2 -2
- package/dist/field.d.ts +6 -3
- package/dist/field.js +3 -2
- package/dist/form/checkbox-field.d.ts +1 -1
- package/dist/form/checkbox-field.js +8 -11
- package/dist/form/combobox-field.js +2 -2
- package/dist/form/control-field.js +2 -7
- package/dist/form/date-picker-field.js +5 -2
- package/dist/form/date-time-picker-field.js +5 -2
- package/dist/form/field-error.d.ts +3 -3
- package/dist/form/field-shell.d.ts +36 -31
- package/dist/form/field-shell.js +41 -34
- package/dist/form/markdown-field.js +2 -2
- package/dist/form/select-field.js +2 -2
- package/dist/form/text-field.js +2 -4
- package/dist/form/toggle-group-field.js +2 -2
- package/dist/info-tip.d.ts +5 -4
- package/dist/info-tip.js +9 -4
- package/dist/internal/active-link.d.ts +7 -0
- package/dist/internal/active-link.js +9 -0
- package/dist/internal/calendar-size.d.ts +5 -0
- package/dist/internal/calendar-size.js +5 -0
- package/dist/internal/form-control.d.ts +1 -1
- package/dist/internal/form-control.js +2 -1
- package/dist/internal/listbox-option.d.ts +16 -3
- package/dist/internal/listbox-option.js +22 -5
- package/dist/internal/overlay-styles.d.ts +10 -5
- package/dist/internal/overlay-styles.js +10 -5
- package/dist/internal/search.d.ts +4 -0
- package/dist/internal/search.js +6 -0
- package/dist/internal/styles.d.ts +16 -0
- package/dist/internal/styles.js +16 -0
- package/dist/kanban.js +2 -1
- package/dist/label.js +2 -1
- package/dist/lazy-calendar.js +3 -2
- package/dist/nav-link.js +2 -1
- package/dist/popover.js +4 -3
- package/dist/search-picker.js +5 -9
- package/dist/select.js +4 -4
- package/dist/skip-link.js +2 -1
- package/dist/tabs.js +3 -2
- package/dist/toast.js +11 -10
- package/dist/tooltip.js +2 -2
- package/package.json +1 -1
- 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 {
|
|
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,
|
|
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, ...
|
|
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,
|
|
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, ...
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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,
|
|
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. */
|
package/dist/form/field-shell.js
CHANGED
|
@@ -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
|
-
|
|
25
|
+
function labelIdFor(id) {
|
|
29
26
|
return `${id}-label`;
|
|
30
27
|
}
|
|
31
28
|
/** Id of the hint `FormFieldShell` renders for control `id`. */
|
|
32
|
-
|
|
29
|
+
function descriptionIdFor(id) {
|
|
33
30
|
return `${id}-description`;
|
|
34
31
|
}
|
|
35
32
|
/** Id of the error text `FormFieldShell` renders for control `id`. */
|
|
36
|
-
|
|
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
|
-
*
|
|
48
|
-
*
|
|
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
|
|
51
|
-
const
|
|
52
|
-
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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,
|
|
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(
|
|
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 {
|
|
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,
|
|
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 {
|
|
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,
|
|
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
|
}
|
package/dist/form/text-field.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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
|
}
|
package/dist/info-tip.d.ts
CHANGED
|
@@ -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`
|
|
15
|
-
*
|
|
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
|
|
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
|
|
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,
|
|
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"]';
|
|
@@ -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)]
|
|
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
|
|
20
|
-
*
|
|
21
|
-
* announce the empty state as
|
|
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
|
|
24
|
-
*
|
|
25
|
-
* announce the empty state as
|
|
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
|
|
3
|
-
* Select, Dialog, Tooltip) and the option rows
|
|
4
|
-
*
|
|
5
|
-
* and move the same everywhere. Tailwind only
|
|
6
|
-
* finds as literals, so every class appears here
|
|
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
|
|
3
|
-
* Select, Dialog, Tooltip) and the option rows
|
|
4
|
-
*
|
|
5
|
-
* and move the same everywhere. Tailwind only
|
|
6
|
-
* finds as literals, so every class appears here
|
|
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[];
|