atlas-react-kit 1.2.0 → 1.2.2
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 +40 -40
- package/DESIGN_RULES.md +14 -40
- package/README.md +7 -6
- package/dist/alert.js +7 -6
- package/dist/calendar.js +5 -4
- package/dist/card.js +2 -1
- package/dist/combobox.d.ts +4 -3
- package/dist/combobox.js +8 -17
- package/dist/date-time-picker.js +2 -1
- package/dist/dialog.js +2 -1
- package/dist/dropdown-menu.js +7 -5
- package/dist/field.d.ts +6 -3
- package/dist/field.js +3 -2
- package/dist/form/checkbox-field.d.ts +3 -5
- package/dist/form/checkbox-field.js +13 -19
- package/dist/form/combobox-field.d.ts +2 -2
- package/dist/form/combobox-field.js +5 -6
- package/dist/form/control-field.d.ts +5 -5
- package/dist/form/control-field.js +6 -9
- package/dist/form/date-picker-field.d.ts +1 -1
- package/dist/form/date-picker-field.js +3 -7
- package/dist/form/date-time-picker-field.d.ts +1 -1
- package/dist/form/date-time-picker-field.js +3 -7
- package/dist/form/field-error.d.ts +13 -4
- package/dist/form/field-shell.d.ts +63 -47
- package/dist/form/field-shell.js +67 -35
- package/dist/form/focus-first-invalid.d.ts +7 -4
- package/dist/form/focus-first-invalid.js +10 -6
- package/dist/form/markdown-field.d.ts +1 -1
- package/dist/form/markdown-field.js +8 -4
- package/dist/form/select-field.d.ts +2 -8
- package/dist/form/select-field.js +5 -7
- package/dist/form/text-field.d.ts +2 -8
- package/dist/form/text-field.js +9 -6
- package/dist/form/toggle-group-field.d.ts +2 -2
- package/dist/form/toggle-group-field.js +5 -6
- package/dist/info-tip.d.ts +7 -4
- package/dist/info-tip.js +12 -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 +7 -1
- package/dist/internal/form-control.js +8 -1
- package/dist/internal/kanban-layout.js +2 -1
- package/dist/internal/listbox-navigation.d.ts +8 -8
- package/dist/internal/listbox-navigation.js +23 -23
- package/dist/internal/listbox-option.d.ts +16 -3
- package/dist/internal/listbox-option.js +22 -5
- package/dist/internal/overlay-styles.d.ts +15 -5
- package/dist/internal/overlay-styles.js +15 -5
- package/dist/internal/search.d.ts +4 -0
- package/dist/internal/search.js +6 -0
- package/dist/internal/styles.d.ts +21 -0
- package/dist/internal/styles.js +21 -0
- package/dist/kanban.js +3 -2
- package/dist/label.js +2 -1
- package/dist/lazy-calendar.js +3 -2
- package/dist/nav-link.js +2 -1
- package/dist/picker-shell.d.ts +3 -1
- package/dist/picker-shell.js +3 -3
- package/dist/popover.js +6 -5
- package/dist/search-picker.js +8 -16
- package/dist/select.js +5 -5
- package/dist/skip-link.js +2 -1
- package/dist/tabs.js +3 -2
- package/dist/toast.js +11 -10
- package/dist/toggle-group.d.ts +1 -1
- package/dist/toggle-group.js +36 -2
- package/dist/tooltip.js +2 -2
- package/package.json +1 -1
- package/skills/atlas-react-kit/SKILL.md +46 -42
package/dist/form/field-shell.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
+
import { useId, } from "react";
|
|
4
|
+
import { Slot } from "radix-ui";
|
|
5
|
+
import { fieldNameProps, pickerValueId } from "../internal/form-control.js";
|
|
3
6
|
import { Label } from "../label.js";
|
|
4
7
|
import { cn } from "../utils.js";
|
|
5
8
|
import { FieldError, getFieldErrorMessage, } from "./field-error.js";
|
|
@@ -13,27 +16,32 @@ import { FieldError, getFieldErrorMessage, } from "./field-error.js";
|
|
|
13
16
|
* `showError` is false, or the field is untouched (`isTouched: false`)
|
|
14
17
|
* and the form has not been submitted.
|
|
15
18
|
*/
|
|
16
|
-
|
|
19
|
+
function metaErrorText(meta, { submitted, showError = true } = {}) {
|
|
17
20
|
if (!meta || !showError)
|
|
18
21
|
return undefined;
|
|
19
22
|
if (meta.isTouched === false && !submitted)
|
|
20
23
|
return undefined;
|
|
21
24
|
return getFieldErrorMessage(meta.errors[0]);
|
|
22
25
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The error text a `form/*` field shows (`metaErrorText`) and its control
|
|
28
|
+
* id. The id is generated per field, so two forms that share a field name
|
|
29
|
+
* (login and sign-up `email`) never share an id, and each label, hint, and
|
|
30
|
+
* error links to its own control.
|
|
31
|
+
*/
|
|
32
|
+
export function useFormField(meta, timing) {
|
|
33
|
+
return { id: useId(), errorText: metaErrorText(meta, timing) };
|
|
26
34
|
}
|
|
27
35
|
/** Id of the label `FormFieldShell` renders for control `id`. */
|
|
28
|
-
|
|
36
|
+
function labelIdFor(id) {
|
|
29
37
|
return `${id}-label`;
|
|
30
38
|
}
|
|
31
39
|
/** Id of the hint `FormFieldShell` renders for control `id`. */
|
|
32
|
-
|
|
40
|
+
function descriptionIdFor(id) {
|
|
33
41
|
return `${id}-description`;
|
|
34
42
|
}
|
|
35
43
|
/** Id of the error text `FormFieldShell` renders for control `id`. */
|
|
36
|
-
|
|
44
|
+
function errorIdFor(id) {
|
|
37
45
|
return `${id}-error`;
|
|
38
46
|
}
|
|
39
47
|
/**
|
|
@@ -44,39 +52,63 @@ function requiredIdFor(id) {
|
|
|
44
52
|
return `${id}-required`;
|
|
45
53
|
}
|
|
46
54
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
55
|
+
* ARIA for a field's control, from the ids `FormFieldShell` renders:
|
|
56
|
+
* invalid while there is error text, described by the hint, the hidden
|
|
57
|
+
* "Required" text, and the error. `required: "described"` is for a button
|
|
58
|
+
* trigger, which cannot take `aria-required`; leave `required` out where
|
|
59
|
+
* the control carries native `required`. `labelledBy: "label"` names a
|
|
60
|
+
* group; `"label-value"` reads a picker "label, value".
|
|
49
61
|
*/
|
|
50
|
-
export function
|
|
51
|
-
const
|
|
52
|
-
|
|
62
|
+
export function fieldAria(id, errorText, { description, required, labelledBy, } = {}) {
|
|
63
|
+
const describedBy = [
|
|
64
|
+
description ? descriptionIdFor(id) : undefined,
|
|
65
|
+
required === "described" ? requiredIdFor(id) : undefined,
|
|
66
|
+
errorText ? errorIdFor(id) : undefined,
|
|
67
|
+
].filter(Boolean);
|
|
68
|
+
return {
|
|
69
|
+
"aria-invalid": errorText ? true : undefined,
|
|
70
|
+
"aria-describedby": describedBy.length ? describedBy.join(" ") : undefined,
|
|
71
|
+
"aria-required": required === true ? true : undefined,
|
|
72
|
+
...(labelledBy && {
|
|
73
|
+
"aria-labelledby": labelledBy === "label"
|
|
74
|
+
? labelIdFor(id)
|
|
75
|
+
: `${labelIdFor(id)} ${pickerValueId(id)}`,
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
53
78
|
}
|
|
54
79
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
80
|
+
* The date and date-time fields: `picker` (the trigger) in a
|
|
81
|
+
* `FormFieldShell`, given every prop of `field` the shell does not take. A
|
|
82
|
+
* button cannot carry `required`, so the trigger is described by the hidden
|
|
83
|
+
* "Required" text and reads "label, value".
|
|
59
84
|
*/
|
|
60
|
-
export function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
export function FormPickerField({ field, picker: Picker, }) {
|
|
86
|
+
const { fieldName, label, meta, submitted, showError, required, fieldClassName, ...pickerProps } = field;
|
|
87
|
+
const { id, errorText } = useFormField(meta, { submitted, showError });
|
|
88
|
+
return (_jsx(FormFieldShell, { id: id, label: label, errorText: errorText, required: required, className: fieldClassName, children: _jsx(Picker, { ...pickerProps, id: id, ...fieldNameProps(fieldName), ...fieldAria(id, errorText, {
|
|
89
|
+
required: required && "described",
|
|
90
|
+
labelledBy: "label-value",
|
|
91
|
+
}) }) }));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Every `form/*` field's wrapper (`data-slot="form-field"`): children with
|
|
95
|
+
* the field spacing, then the error text. `asChild` makes the one child the
|
|
96
|
+
* wrapper (the checkbox group's fieldset).
|
|
97
|
+
*/
|
|
98
|
+
export function FormFieldFrame({ id, errorText, asChild, className, children, ...props }) {
|
|
99
|
+
const Comp = asChild ? Slot.Root : "div";
|
|
100
|
+
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 })] }));
|
|
101
|
+
}
|
|
102
|
+
/** A field's hint, with id `descriptionIdFor(id)`. */
|
|
103
|
+
export function FormFieldDescription({ id, children, }) {
|
|
104
|
+
return (_jsx("p", { id: descriptionIdFor(id), "data-slot": "form-field-description", className: "text-xs text-muted-foreground", children: children }));
|
|
66
105
|
}
|
|
67
106
|
/**
|
|
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.
|
|
107
|
+
* Label, hint, control, and error in a `FormFieldFrame`. `labelAction` sits
|
|
108
|
+
* on the label's row; `labelFor={false}` when the control is not rendered
|
|
109
|
+
* (Markdown preview). `required` marks the label only.
|
|
78
110
|
*/
|
|
79
|
-
export function FormFieldShell({ id, label, labelAction, description, errorText, labelFor = true, required,
|
|
111
|
+
export function FormFieldShell({ id, label, labelAction, description, errorText, labelFor = true, required, children, ...props }) {
|
|
80
112
|
const labelNode = (_jsx(Label, { id: labelIdFor(id), htmlFor: labelFor ? id : undefined, required: required, children: label }));
|
|
81
|
-
return (_jsxs(
|
|
113
|
+
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
114
|
}
|
|
@@ -15,14 +15,17 @@ export type InvalidSubmitEvent = {
|
|
|
15
15
|
/**
|
|
16
16
|
* Invalid-submit handler that focuses the first field, in page order, whose
|
|
17
17
|
* meta has errors. It reads the form state, not the rendered `aria-invalid`,
|
|
18
|
-
* so it works before React has re-rendered. Fields are matched by
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* so it works before React has re-rendered. Fields are matched by the
|
|
19
|
+
* `data-field-name` each `form/*` field puts on its control: `FormTextField`
|
|
20
|
+
* and `FormMarkdownField` read `field.name`; the other fields take
|
|
21
|
+
* `fieldName={field.name}`. The lookup stays inside the form with id
|
|
22
|
+
* `formId`, so a second form with the same field names is never touched.
|
|
23
|
+
* The form library needs no focus API of its own.
|
|
21
24
|
*
|
|
22
25
|
* ```tsx
|
|
23
26
|
* const form = useForm({ onSubmitInvalid: focusFirstInvalid("order-form") })
|
|
24
27
|
* <form id="order-form" onSubmit={...}>
|
|
25
|
-
* <FormSelectField
|
|
28
|
+
* <FormSelectField fieldName={field.name} ... />
|
|
26
29
|
* ```
|
|
27
30
|
*/
|
|
28
31
|
export declare function focusFirstInvalid(formId: string): ({ formApi }: InvalidSubmitEvent) => void;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { FIELD_NAME_ATTRIBUTE } from "../internal/form-control.js";
|
|
1
2
|
/**
|
|
2
3
|
* Invalid-submit handler that focuses the first field, in page order, whose
|
|
3
4
|
* meta has errors. It reads the form state, not the rendered `aria-invalid`,
|
|
4
|
-
* so it works before React has re-rendered. Fields are matched by
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* so it works before React has re-rendered. Fields are matched by the
|
|
6
|
+
* `data-field-name` each `form/*` field puts on its control: `FormTextField`
|
|
7
|
+
* and `FormMarkdownField` read `field.name`; the other fields take
|
|
8
|
+
* `fieldName={field.name}`. The lookup stays inside the form with id
|
|
9
|
+
* `formId`, so a second form with the same field names is never touched.
|
|
10
|
+
* The form library needs no focus API of its own.
|
|
7
11
|
*
|
|
8
12
|
* ```tsx
|
|
9
13
|
* const form = useForm({ onSubmitInvalid: focusFirstInvalid("order-form") })
|
|
10
14
|
* <form id="order-form" onSubmit={...}>
|
|
11
|
-
* <FormSelectField
|
|
15
|
+
* <FormSelectField fieldName={field.name} ... />
|
|
12
16
|
* ```
|
|
13
17
|
*/
|
|
14
18
|
export function focusFirstInvalid(formId) {
|
|
@@ -19,8 +23,8 @@ export function focusFirstInvalid(formId) {
|
|
|
19
23
|
const invalid = new Set(Object.entries(formApi.state.fieldMeta)
|
|
20
24
|
.filter(([, meta]) => meta?.errors.length)
|
|
21
25
|
.map(([name]) => name));
|
|
22
|
-
for (const element of form.
|
|
23
|
-
if (
|
|
26
|
+
for (const element of form.querySelectorAll(`[${FIELD_NAME_ATTRIBUTE}]`)) {
|
|
27
|
+
if (invalid.has(element.getAttribute(FIELD_NAME_ATTRIBUTE) ?? "")) {
|
|
24
28
|
element.focus();
|
|
25
29
|
return;
|
|
26
30
|
}
|
|
@@ -3,7 +3,7 @@ import type { TextFieldBaseProps } from "./text-field.js";
|
|
|
3
3
|
* GFM body field: Write is a textarea, Preview is the sanitized Markdown
|
|
4
4
|
* renderer. String-in/string-out — not a WYSIWYG document model.
|
|
5
5
|
*/
|
|
6
|
-
export declare function FormMarkdownField({ field,
|
|
6
|
+
export declare function FormMarkdownField({ field, label, placeholder, rows, showError, submitted, required, className, fieldClassName, previewClassName, }: TextFieldBaseProps & {
|
|
7
7
|
/** Write textarea placeholder. Default "Markdown supported". */
|
|
8
8
|
placeholder?: string;
|
|
9
9
|
/** Write textarea rows. Default 5. */
|
|
@@ -4,9 +4,10 @@ import { lazy, Suspense, useState } from "react";
|
|
|
4
4
|
import { Spinner } from "../spinner.js";
|
|
5
5
|
import { Textarea } from "../textarea.js";
|
|
6
6
|
import { ToggleGroup, ToggleGroupItem } from "../toggle-group.js";
|
|
7
|
+
import { fieldNameProps } from "../internal/form-control.js";
|
|
7
8
|
import { fieldSurface } from "../internal/styles.js";
|
|
8
9
|
import { cn, onInputChange } from "../utils.js";
|
|
9
|
-
import {
|
|
10
|
+
import { fieldAria, FormFieldShell, useFormField } from "./field-shell.js";
|
|
10
11
|
// The renderer (react-markdown + remark-gfm) is fetched the first time
|
|
11
12
|
// Preview is shown or hovered, so a form in Write mode stays light.
|
|
12
13
|
const loadMarkdown = () => import("../markdown.js");
|
|
@@ -16,14 +17,17 @@ const Markdown = lazy(() => loadMarkdown().then((module) => ({ default: module.M
|
|
|
16
17
|
* GFM body field: Write is a textarea, Preview is the sanitized Markdown
|
|
17
18
|
* renderer. String-in/string-out — not a WYSIWYG document model.
|
|
18
19
|
*/
|
|
19
|
-
export function FormMarkdownField({ field,
|
|
20
|
+
export function FormMarkdownField({ field, label, placeholder = "Markdown supported", rows = 5, showError, submitted, required, className, fieldClassName, previewClassName, }) {
|
|
21
|
+
const { id, errorText } = useFormField(field.state.meta, {
|
|
22
|
+
submitted,
|
|
23
|
+
showError,
|
|
24
|
+
});
|
|
20
25
|
const [mode, setMode] = useState("write");
|
|
21
26
|
const onChange = onInputChange(field.handleChange);
|
|
22
|
-
const errorText = metaErrorText(field.state.meta, { submitted, showError });
|
|
23
27
|
const trimmed = field.state.value.trim();
|
|
24
28
|
function handleModeChange(value) {
|
|
25
29
|
if (value === "write" || value === "preview")
|
|
26
30
|
setMode(value);
|
|
27
31
|
}
|
|
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,
|
|
32
|
+
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, ...fieldNameProps(field.name), 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
33
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
import type { FormFieldBaseProps, FormFieldMetaProps } from "./field-error.js";
|
|
3
3
|
type Props = FormFieldBaseProps & FormFieldMetaProps & {
|
|
4
4
|
/** The selected item's value, or `undefined` for none. */
|
|
@@ -7,12 +7,6 @@ type Props = FormFieldBaseProps & FormFieldMetaProps & {
|
|
|
7
7
|
onValueChange: (value: string) => void;
|
|
8
8
|
/** Trigger text while no item is selected. */
|
|
9
9
|
placeholder?: string;
|
|
10
|
-
/**
|
|
11
|
-
* Id of the trigger the label points at. Defaults to a generated id;
|
|
12
|
-
* pass the field name (`id={field.name}`) so a submit handler such as
|
|
13
|
-
* `focusFirstInvalid` can find the control.
|
|
14
|
-
*/
|
|
15
|
-
id?: string;
|
|
16
10
|
/** Requires a value (native form validation) and marks the label. */
|
|
17
11
|
required?: boolean;
|
|
18
12
|
/** Forwarded to the select trigger. */
|
|
@@ -23,5 +17,5 @@ type Props = FormFieldBaseProps & FormFieldMetaProps & {
|
|
|
23
17
|
children: ReactNode;
|
|
24
18
|
};
|
|
25
19
|
/** Select control with built-in field error text and invalid styling. */
|
|
26
|
-
export declare function FormSelectField({
|
|
20
|
+
export declare function FormSelectField({ fieldName, value, meta, submitted, showError, label, onValueChange, placeholder, required, className, fieldClassName, contentClassName, children, }: Props): import("react").JSX.Element;
|
|
27
21
|
export {};
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { fieldNameProps } from "../internal/form-control.js";
|
|
4
4
|
import { Select, SelectContent, SelectTrigger, SelectValue } from "../select.js";
|
|
5
|
-
import {
|
|
5
|
+
import { fieldAria, FormFieldShell, useFormField } from "./field-shell.js";
|
|
6
6
|
/** Select control with built-in field error text and invalid styling. */
|
|
7
|
-
export function FormSelectField({
|
|
8
|
-
const errorText =
|
|
9
|
-
|
|
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 })] }) }));
|
|
7
|
+
export function FormSelectField({ fieldName, value, meta, submitted, showError, label, onValueChange, placeholder, required, className, fieldClassName, contentClassName, children, }) {
|
|
8
|
+
const { id, errorText } = useFormField(meta, { submitted, showError });
|
|
9
|
+
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, ...fieldNameProps(fieldName), className: className, ...fieldAria(id, errorText, { required }), children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { className: contentClassName, children: children })] }) }));
|
|
12
10
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FieldMetaState, FormFieldBaseProps } from "./field-error.js";
|
|
2
2
|
export type TextFieldApi = {
|
|
3
|
-
/** Field name; the
|
|
3
|
+
/** Field name; put on the control as `data-field-name` for `focusFirstInvalid`. */
|
|
4
4
|
name: string;
|
|
5
5
|
state: {
|
|
6
6
|
/** Current text. */
|
|
@@ -20,12 +20,6 @@ export type TextFieldApi = {
|
|
|
20
20
|
export type TextFieldBaseProps = FormFieldBaseProps & {
|
|
21
21
|
/** The TanStack Form field (or the same shape) the control reads and writes. */
|
|
22
22
|
field: TextFieldApi;
|
|
23
|
-
/**
|
|
24
|
-
* Control id the label points at. Defaults to `field.name`; pass a unique
|
|
25
|
-
* id when two forms on one page share a field name (login and sign-up
|
|
26
|
-
* `email`), or the second label focuses the first input.
|
|
27
|
-
*/
|
|
28
|
-
id?: string;
|
|
29
23
|
/** Sets `required` on the text control and marks the label. */
|
|
30
24
|
required?: boolean;
|
|
31
25
|
/** Forwarded to the text control (the input or textarea). */
|
|
@@ -35,7 +29,7 @@ export type TextFieldBaseProps = FormFieldBaseProps & {
|
|
|
35
29
|
* Text input, or a textarea with `multiline`, bound to a TanStack Form
|
|
36
30
|
* `field`, with a label, optional hint, and the `form/*` error text.
|
|
37
31
|
*/
|
|
38
|
-
export declare function FormTextField({ field,
|
|
32
|
+
export declare function FormTextField({ field, label, placeholder, description, autoFocus, multiline, rows, inputType, showError, submitted, autoComplete, required, minLength, className, fieldClassName, }: TextFieldBaseProps & {
|
|
39
33
|
/** Text shown in the empty control. */
|
|
40
34
|
placeholder?: string;
|
|
41
35
|
/** Optional hint under the label (employer vs referrer, etc.). */
|
package/dist/form/text-field.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { Input } from "../input.js";
|
|
4
|
+
import { fieldNameProps } from "../internal/form-control.js";
|
|
4
5
|
import { Textarea } from "../textarea.js";
|
|
5
6
|
import { onInputChange } from "../utils.js";
|
|
6
|
-
import {
|
|
7
|
+
import { fieldAria, FormFieldShell, useFormField } from "./field-shell.js";
|
|
7
8
|
/**
|
|
8
9
|
* Text input, or a textarea with `multiline`, bound to a TanStack Form
|
|
9
10
|
* `field`, with a label, optional hint, and the `form/*` error text.
|
|
10
11
|
*/
|
|
11
|
-
export function FormTextField({ field,
|
|
12
|
+
export function FormTextField({ field, label, placeholder, description, autoFocus, multiline, rows = 3, inputType = "text", showError, submitted, autoComplete, required, minLength, className, fieldClassName, }) {
|
|
13
|
+
const { id, errorText } = useFormField(field.state.meta, {
|
|
14
|
+
submitted,
|
|
15
|
+
showError,
|
|
16
|
+
});
|
|
12
17
|
const onChange = onInputChange(field.handleChange);
|
|
13
|
-
const errorText = metaErrorText(field.state.meta, { submitted, showError });
|
|
14
|
-
const descriptionId = description ? descriptionIdFor(id) : undefined;
|
|
15
18
|
const controlProps = {
|
|
16
19
|
id,
|
|
20
|
+
...fieldNameProps(field.name),
|
|
17
21
|
value: field.state.value,
|
|
18
22
|
onChange,
|
|
19
23
|
onBlur: field.handleBlur,
|
|
@@ -22,8 +26,7 @@ export function FormTextField({ field, id = field.name, label, placeholder, desc
|
|
|
22
26
|
required,
|
|
23
27
|
minLength,
|
|
24
28
|
className,
|
|
25
|
-
|
|
26
|
-
"aria-describedby": describedBy(id, errorText, descriptionId),
|
|
29
|
+
...fieldAria(id, errorText, { description }),
|
|
27
30
|
};
|
|
28
31
|
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
32
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
import type { FormFieldBaseProps, FormFieldDescriptionProps, FormFieldMetaProps } from "./field-error.js";
|
|
3
3
|
export type ToggleGroupFieldOption<T extends string> = {
|
|
4
4
|
/** Value passed to `onValueChange` when this item is pressed. */
|
|
@@ -26,5 +26,5 @@ type Props<T extends string> = FormFieldBaseProps & FormFieldMetaProps & FormFie
|
|
|
26
26
|
* One choice from a few as a joined outline toggle group (a segmented
|
|
27
27
|
* control), with a visible label and the `form/*` error wiring.
|
|
28
28
|
*/
|
|
29
|
-
export declare function FormToggleGroupField<T extends string>({ label, description, value, onValueChange, options, meta, submitted, showError, required, disabled, size, className, fieldClassName, }: Props<T>): import("react").JSX.Element;
|
|
29
|
+
export declare function FormToggleGroupField<T extends string>({ fieldName, label, description, value, onValueChange, options, meta, submitted, showError, required, disabled, size, className, fieldClassName, }: Props<T>): import("react").JSX.Element;
|
|
30
30
|
export {};
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { fieldNameProps } from "../internal/form-control.js";
|
|
4
4
|
import { ToggleGroup, ToggleGroupItem } from "../toggle-group.js";
|
|
5
|
-
import {
|
|
5
|
+
import { fieldAria, FormFieldShell, useFormField } 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.
|
|
9
9
|
*/
|
|
10
|
-
export function FormToggleGroupField({ label, description, value, onValueChange, options, meta, submitted, showError, required, disabled, size = "sm", className, fieldClassName, }) {
|
|
11
|
-
const id =
|
|
12
|
-
const errorText = metaErrorText(meta, { submitted, showError });
|
|
10
|
+
export function FormToggleGroupField({ fieldName, label, description, value, onValueChange, options, meta, submitted, showError, required, disabled, size = "sm", className, fieldClassName, }) {
|
|
11
|
+
const { id, errorText } = useFormField(meta, { submitted, showError });
|
|
13
12
|
function handleValueChange(next) {
|
|
14
13
|
const option = options.find((o) => o.value === next);
|
|
15
14
|
if (option)
|
|
16
15
|
onValueChange(option.value);
|
|
17
16
|
}
|
|
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,
|
|
17
|
+
return (_jsx(FormFieldShell, { id: id, label: label, labelFor: false, description: description, errorText: errorText, required: required, className: fieldClassName, children: _jsx(ToggleGroup, { id: id, ...fieldNameProps(fieldName), 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
18
|
}
|
package/dist/info-tip.d.ts
CHANGED
|
@@ -11,8 +11,11 @@ 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. `box` is a 44px button; after a label (any earlier
|
|
17
|
+
* sibling) its glyph sits that same quarter em from the text, so leave the
|
|
18
|
+
* gap off the parent. Default `box`.
|
|
16
19
|
*/
|
|
17
20
|
layout?: InfoTipLayout;
|
|
18
21
|
/** Panel side. Default `top`; Radix still flips on collision. */
|
|
@@ -29,8 +32,8 @@ export type InfoTipProps = {
|
|
|
29
32
|
contentClassName?: string;
|
|
30
33
|
} & Omit<React.ComponentProps<"button">, "title" | "children" | "type" | "onMouseEnter" | "onMouseLeave" | "onClick">;
|
|
31
34
|
/**
|
|
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
|
|
35
|
+
* Longer help than a Tooltip: a Popover with a 20px glyph (1em inline) and
|
|
36
|
+
* a 44px hit area. Hover opens only on a fine pointer; click, tap, and keyboard always
|
|
34
37
|
* toggle it. Not a glossary or "coming soon" chip — pass that copy in.
|
|
35
38
|
*/
|
|
36
39
|
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,15 @@ 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
|
+
// Box: the 44px target leaves 12px either side of the 20px
|
|
58
|
+
// glyph. After a label, pull it back over that padding so the
|
|
59
|
+
// glyph sits the same quarter em from the text as it does inline.
|
|
60
|
+
inline
|
|
61
|
+
? cn("ml-[0.25em] size-[1em] align-[-0.125em]", hitArea44)
|
|
62
|
+
: "size-11 not-first:ml-[calc(0.25em-0.75rem)]", className), onMouseEnter: openFromHover, onMouseLeave: closeFromHover, onClick: (event) => {
|
|
55
63
|
// Hover already opened the panel; this click pins it instead of
|
|
56
64
|
// toggling it closed (Playwright and real mice both hover before
|
|
57
65
|
// they click). Preventing default skips Radix's toggle.
|
|
@@ -60,7 +68,7 @@ function InfoTip({ label, title, description, children, layout = "box", side = "
|
|
|
60
68
|
cancelClose();
|
|
61
69
|
openedBy.current = "click";
|
|
62
70
|
}
|
|
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,
|
|
71
|
+
}, 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
72
|
// A pointer passing over the glyph must not take focus from where
|
|
65
73
|
// the user is typing, nor hand it to the trigger on close.
|
|
66
74
|
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,11 +12,17 @@ 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. */
|
|
19
19
|
export declare const PICKER_COLLISION_PADDING = 16;
|
|
20
|
+
/** Carries a field's name on its control, for `focusFirstInvalid`. */
|
|
21
|
+
export declare const FIELD_NAME_ATTRIBUTE = "data-field-name";
|
|
22
|
+
/** The props that mark a control with its field's name. */
|
|
23
|
+
export declare function fieldNameProps(name: string | undefined): {
|
|
24
|
+
"data-field-name": string | undefined;
|
|
25
|
+
};
|
|
20
26
|
/**
|
|
21
27
|
* Id of a picker trigger's value text. A form field lists it after the label
|
|
22
28
|
* id in `aria-labelledby` so the trigger reads "label, value".
|
|
@@ -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,11 +13,17 @@ 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. */
|
|
19
20
|
export const PICKER_COLLISION_PADDING = 16;
|
|
21
|
+
/** Carries a field's name on its control, for `focusFirstInvalid`. */
|
|
22
|
+
export const FIELD_NAME_ATTRIBUTE = "data-field-name";
|
|
23
|
+
/** The props that mark a control with its field's name. */
|
|
24
|
+
export function fieldNameProps(name) {
|
|
25
|
+
return { [FIELD_NAME_ATTRIBUTE]: name };
|
|
26
|
+
}
|
|
20
27
|
/**
|
|
21
28
|
* Id of a picker trigger's value text. A form field lists it after the label
|
|
22
29
|
* id in `aria-labelledby` so the trigger reads "label, value".
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { clamp } from "./math.js";
|
|
1
2
|
/** Drop zone ids for the drag-time action rail. */
|
|
2
3
|
export const KANBAN_COMPLETE_ID = "__kanban_complete__";
|
|
3
4
|
export const KANBAN_DELETE_ID = "__kanban_delete__";
|
|
@@ -45,7 +46,7 @@ export function resolveKanbanDrop(input, groups) {
|
|
|
45
46
|
return null;
|
|
46
47
|
const columnItems = groups[toColumnId] ?? [];
|
|
47
48
|
const remaining = columnItems.filter((id) => id !== itemId);
|
|
48
|
-
const index =
|
|
49
|
+
const index = clamp(rawIndex ?? remaining.length, 0, remaining.length);
|
|
49
50
|
if (initialGroup === toColumnId && index === columnItems.indexOf(itemId)) {
|
|
50
51
|
return null;
|
|
51
52
|
}
|
|
@@ -12,19 +12,19 @@ export declare function listboxOptionId(listId: string, index: number): string;
|
|
|
12
12
|
export declare function scrollOptionIntoView(node: HTMLElement | null): void;
|
|
13
13
|
/**
|
|
14
14
|
* Shared keydown for a search input that drives a listbox: navigation keys
|
|
15
|
-
* move the highlight
|
|
16
|
-
* the key is handled (or an IME is composing, when every key
|
|
17
|
-
* IME), so the caller can stop; `false` leaves the key to the
|
|
18
|
-
* example SearchPicker's Escape).
|
|
15
|
+
* move the highlight over `items`, Enter picks the highlighted item. Returns
|
|
16
|
+
* `true` when the key is handled (or an IME is composing, when every key
|
|
17
|
+
* belongs to the IME), so the caller can stop; `false` leaves the key to the
|
|
18
|
+
* caller (for example SearchPicker's Escape).
|
|
19
19
|
*/
|
|
20
|
-
export declare function handleListboxKey(event: {
|
|
20
|
+
export declare function handleListboxKey<T>(event: {
|
|
21
21
|
key: string;
|
|
22
22
|
keyCode: number;
|
|
23
23
|
nativeEvent: KeyboardEvent;
|
|
24
24
|
preventDefault: () => void;
|
|
25
|
-
}, {
|
|
25
|
+
}, { items, highlight, setHighlight, onPick, }: {
|
|
26
|
+
items: readonly T[];
|
|
26
27
|
highlight: number;
|
|
27
|
-
length: number;
|
|
28
28
|
setHighlight: (index: number) => void;
|
|
29
|
-
|
|
29
|
+
onPick: (item: T) => void;
|
|
30
30
|
}): boolean;
|