@parallelworks/ui 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/dist/ai/components/ChatInput.js +1 -1
- package/dist/components/ColorInput.d.ts +36 -0
- package/dist/components/ColorInput.js +82 -0
- package/dist/components/Combobox.d.ts +5 -2
- package/dist/components/Combobox.js +12 -11
- package/dist/components/CreateModal.js +2 -2
- package/dist/components/DatePicker.d.ts +5 -1
- package/dist/components/DatePicker.js +7 -7
- package/dist/components/Field.d.ts +19 -5
- package/dist/components/Field.js +21 -4
- package/dist/components/Input.d.ts +11 -1
- package/dist/components/Input.js +9 -6
- package/dist/components/MenuEntries.js +1 -1
- package/dist/components/NumberInput.d.ts +1 -0
- package/dist/components/OneTimeCodeInput.d.ts +23 -0
- package/dist/components/OneTimeCodeInput.js +39 -0
- package/dist/components/Provider.d.ts +12 -3
- package/dist/components/Provider.js +14 -2
- package/dist/components/SearchInput.d.ts +1 -0
- package/dist/components/Section.d.ts +7 -2
- package/dist/components/Section.js +58 -39
- package/dist/components/SegmentedControl.d.ts +4 -2
- package/dist/components/SegmentedControl.js +4 -2
- package/dist/components/Select.d.ts +2 -1
- package/dist/components/Select.js +1 -1
- package/dist/components/Tabs.js +1 -0
- package/dist/components/ThemeEditor.js +15 -52
- package/dist/components/ToggleGroup.d.ts +42 -0
- package/dist/components/ToggleGroup.js +69 -0
- package/dist/components/fieldBox.js +1 -1
- package/dist/form/Fields/BooleanField.js +1 -1
- package/dist/form/Fields/ColorField.js +6 -10
- package/dist/form/Fields/DropdownField.js +1 -1
- package/dist/form/Fields/DurationField.js +1 -1
- package/dist/form/Fields/MultiDropdownField.js +1 -1
- package/dist/form/Fields/StringField.js +1 -1
- package/dist/form/Fields/TextAreaField.js +1 -1
- package/dist/form/Form.js +1 -1
- package/dist/graph/GraphEditorDialogs.js +1 -1
- package/dist/graph/InputDialog.js +2 -2
- package/dist/graph/WorkflowSettingsDialog.js +1 -1
- package/dist/graph/editorFields.js +2 -2
- package/dist/graph/inputRefs.js +1 -1
- package/dist/graph/stepWith.js +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +14 -10
- package/dist/list/ListViewControls.js +1 -1
- package/dist/list/listSearch.js +1 -1
- package/dist/styles/theme.css +1 -1
- package/dist/styles.css +1 -1
- package/dist/theme/derive.d.ts +3 -0
- package/dist/theme/derive.js +11 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,9 +32,14 @@ Each need has one component. Reach for it before styling markup by hand.
|
|
|
32
32
|
| Work under way | `Loader`; `Skeleton` in a `LoadingRegion` for the shape of content still loading |
|
|
33
33
|
| Nothing to show yet, or nothing matches | `EmptyState` |
|
|
34
34
|
| A text field | `Input`, `Textarea`, or a control framed by `Field`, whose `actions` hold buttons that act on it |
|
|
35
|
-
| A number, a date | `NumberInput`, `DatePicker` |
|
|
35
|
+
| A number, a date, a color | `NumberInput`, `DatePicker`, `ColorInput` |
|
|
36
|
+
| A one-time code, such as a sign-in check | `OneTimeCodeInput` |
|
|
37
|
+
| A labeled value nobody edits, among fields | `FieldValue` in a `Field`; `DescriptionList` for a set of them alone |
|
|
38
|
+
| An error beside a control outside a `Field` | `ErrorText` |
|
|
39
|
+
| A dense field in a toolbar or a table cell | any field control with `size="sm"`; a width in `className` wins, and `cx` merges classes the same way |
|
|
36
40
|
| On or off | `Switch`; `Checkbox` in a list of things to tick, several of which may be on |
|
|
37
41
|
| One of a few choices, all shown | `RadioGroup` in a form; `SegmentedControl` in a toolbar, or as a field's control in a `Field group` |
|
|
42
|
+
| Narrowing a list by values, such as statuses: several at once, or one or none | `ToggleGroup` |
|
|
38
43
|
| Narrowing what is shown by typing | `SearchInput`; `ListSearchControl` on a list page's toolbar |
|
|
39
44
|
| A value from a list | `Select`, or `Combobox` when the list needs filtering |
|
|
40
45
|
| Free text with suggestions, or tags | `Autocomplete`; `Combobox` with `multiple creatable` |
|
|
@@ -78,6 +83,12 @@ Some pairs look alike and are split on purpose:
|
|
|
78
83
|
thing, each with its own panel or route. A `SegmentedControl` sets a value,
|
|
79
84
|
such as a time range, that changes what one view shows. `Tabs` with
|
|
80
85
|
`variant="segmented"` look the same, so the choice rests on what pressing does.
|
|
86
|
+
- **SegmentedControl or ToggleGroup.** A `SegmentedControl` holds a setting
|
|
87
|
+
that always has exactly one value, such as a time range or which view of the
|
|
88
|
+
data to show. A `ToggleGroup` filters a list by values and may be off: its
|
|
89
|
+
options are pressed independently, or one or none with `multiple={false}`,
|
|
90
|
+
and `all` leads with an All option for "no filter". Each option can carry a
|
|
91
|
+
status dot or icon and a count.
|
|
81
92
|
- **SearchInput or ListSearchControl.** A `SearchInput` is a box that is
|
|
82
93
|
always shown and narrows what sits beside it, such as a panel's rows or a
|
|
83
94
|
log's lines. `ListSearchControl` belongs to a list page's toolbar: it folds to
|
|
@@ -261,7 +261,7 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
|
|
|
261
261
|
"aria-label": inputLabel ?? effectivePlaceholder,
|
|
262
262
|
disabled,
|
|
263
263
|
rows: 1,
|
|
264
|
-
className: cx("theme-text block w-full resize-none overflow-hidden", "bg-transparent border-0 px-4", toolbar ? "pt-3.5 pb-1" : "pt-3 pb-1", "placeholder:theme-
|
|
264
|
+
className: cx("theme-text block w-full resize-none overflow-hidden", "bg-transparent border-0 px-4", toolbar ? "pt-3.5 pb-1" : "pt-3 pb-1", "placeholder:text-(--theme-placeholder)", "focus:outline-none focus:ring-0", "disabled:opacity-50 disabled:cursor-not-allowed", "text-[1rem] leading-relaxed")
|
|
265
265
|
})
|
|
266
266
|
}), !toolbar && action]
|
|
267
267
|
}),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type FieldBoxSize } from './fieldBox.js';
|
|
3
|
+
export interface ColorInputProps {
|
|
4
|
+
/** A CSS color the theme reads: a hex, `oklch()` or `color(display-p3 …)`. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Called with each color that parses, as typed or as picked: a picked one is a hex. */
|
|
7
|
+
onValueChange: (color: string) => void;
|
|
8
|
+
/** Without a label the row stands alone; name it with `aria-label`. */
|
|
9
|
+
label?: ReactNode;
|
|
10
|
+
/** Beside the label and outside it, so it stays out of the field's name. */
|
|
11
|
+
hint?: ReactNode;
|
|
12
|
+
/** Help under the field. */
|
|
13
|
+
description?: ReactNode;
|
|
14
|
+
error?: ReactNode;
|
|
15
|
+
/** `sm` for dense places such as toolbars and table cells. */
|
|
16
|
+
size?: FieldBoxSize | undefined;
|
|
17
|
+
/** The hex field's id, which a label names. */
|
|
18
|
+
id?: string | undefined;
|
|
19
|
+
/** Submits the color under this name with a form. */
|
|
20
|
+
name?: string | undefined;
|
|
21
|
+
disabled?: boolean | undefined;
|
|
22
|
+
required?: boolean | undefined;
|
|
23
|
+
'aria-label'?: string | undefined;
|
|
24
|
+
'aria-labelledby'?: string | undefined;
|
|
25
|
+
'aria-describedby'?: string | undefined;
|
|
26
|
+
'aria-invalid'?: boolean | undefined;
|
|
27
|
+
'aria-required'?: boolean | undefined;
|
|
28
|
+
/** On the row that holds the swatch and the hex field: its width or margin. */
|
|
29
|
+
className?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A color as a swatch that opens the system picker, beside a field its hex can be typed or pasted
|
|
33
|
+
* in. A half-typed color stays in the field, marked invalid, until it parses; leaving the field
|
|
34
|
+
* puts back the last color that did.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ColorInput({ value, onValueChange, label, hint, description, error, size, id, name, disabled, required, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, className, }: ColorInputProps): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { cx } from "../cx.js";
|
|
2
|
+
import { useStrings } from "./Provider.js";
|
|
3
|
+
import { parseColor, toHex } from "../theme/color.js";
|
|
4
|
+
import { ErrorText } from "./Field.js";
|
|
5
|
+
import { fieldBox } from "./fieldBox.js";
|
|
6
|
+
import { framed } from "./Input.js";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { useId, useState } from "react";
|
|
9
|
+
//#region src/components/ColorInput.tsx
|
|
10
|
+
var swatchClassName = cx("shrink-0 cursor-pointer p-0.5", "[&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded [&::-webkit-color-swatch]:border-0", "[&::-moz-color-swatch]:rounded [&::-moz-color-swatch]:border-0");
|
|
11
|
+
/**
|
|
12
|
+
* A color as a swatch that opens the system picker, beside a field its hex can be typed or pasted
|
|
13
|
+
* in. A half-typed color stays in the field, marked invalid, until it parses; leaving the field
|
|
14
|
+
* puts back the last color that did.
|
|
15
|
+
*/
|
|
16
|
+
function ColorInput({ value, onValueChange, label, hint, description, error, size = "md", id, name, disabled, required, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, className }) {
|
|
17
|
+
const t = useStrings().colorInput;
|
|
18
|
+
const [draft, setDraft] = useState(null);
|
|
19
|
+
const errorId = useId();
|
|
20
|
+
const parsed = parseColor(value);
|
|
21
|
+
const unparsed = draft !== null && parseColor(draft) === null;
|
|
22
|
+
const named = typeof label === "string" ? label : ariaLabel;
|
|
23
|
+
const ownError = unparsed && label === void 0;
|
|
24
|
+
return framed({
|
|
25
|
+
label,
|
|
26
|
+
hint,
|
|
27
|
+
description,
|
|
28
|
+
error: error ?? (unparsed ? t.invalid : void 0)
|
|
29
|
+
}, {
|
|
30
|
+
id,
|
|
31
|
+
required,
|
|
32
|
+
disabled
|
|
33
|
+
}, (control) => /* @__PURE__ */ jsxs("div", {
|
|
34
|
+
className: cx("flex w-full flex-col gap-1", className),
|
|
35
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
36
|
+
className: cx("flex items-center", size === "sm" ? "gap-1.5" : "gap-2"),
|
|
37
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
38
|
+
type: "color",
|
|
39
|
+
"aria-label": t.pick(named),
|
|
40
|
+
value: parsed ? toHex(parsed) : "#000000",
|
|
41
|
+
disabled,
|
|
42
|
+
onChange: (e) => {
|
|
43
|
+
setDraft(null);
|
|
44
|
+
onValueChange(e.target.value);
|
|
45
|
+
},
|
|
46
|
+
className: cx(fieldBox({ size }), swatchClassName, size === "sm" ? "w-8" : "w-10")
|
|
47
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
48
|
+
id,
|
|
49
|
+
name,
|
|
50
|
+
"aria-label": ariaLabel,
|
|
51
|
+
"aria-labelledby": ariaLabelledBy,
|
|
52
|
+
...control,
|
|
53
|
+
"aria-describedby": [
|
|
54
|
+
ariaDescribedBy,
|
|
55
|
+
control["aria-describedby"],
|
|
56
|
+
ownError && errorId
|
|
57
|
+
].filter(Boolean).join(" ") || void 0,
|
|
58
|
+
"aria-invalid": control["aria-invalid"] ?? (ariaInvalid || unparsed || void 0),
|
|
59
|
+
"aria-required": control["aria-required"] ?? (ariaRequired || void 0),
|
|
60
|
+
required,
|
|
61
|
+
disabled,
|
|
62
|
+
spellCheck: false,
|
|
63
|
+
autoComplete: "off",
|
|
64
|
+
value: draft ?? value,
|
|
65
|
+
onChange: (e) => {
|
|
66
|
+
const text = e.target.value;
|
|
67
|
+
if (parseColor(text)) {
|
|
68
|
+
setDraft(null);
|
|
69
|
+
onValueChange(text.trim().toLowerCase());
|
|
70
|
+
} else setDraft(text);
|
|
71
|
+
},
|
|
72
|
+
onBlur: () => setDraft(null),
|
|
73
|
+
className: cx(fieldBox({ size }), "font-mono")
|
|
74
|
+
})]
|
|
75
|
+
}), ownError && /* @__PURE__ */ jsx(ErrorText, {
|
|
76
|
+
id: errorId,
|
|
77
|
+
children: t.invalid
|
|
78
|
+
})]
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
export { ColorInput };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from 'react';
|
|
2
2
|
import { type ComboboxItems } from './comboboxItems.js';
|
|
3
|
+
import { type FieldBoxSize } from './fieldBox.js';
|
|
3
4
|
export type { ComboboxGroup, ComboboxItem, ComboboxItems, ComboboxLeaf, } from './comboboxItems.js';
|
|
4
5
|
interface FieldProps {
|
|
5
6
|
id?: string | undefined;
|
|
@@ -11,7 +12,9 @@ interface FieldProps {
|
|
|
11
12
|
'aria-describedby'?: string | undefined;
|
|
12
13
|
'aria-invalid'?: boolean | undefined;
|
|
13
14
|
'aria-required'?: boolean | undefined;
|
|
14
|
-
/**
|
|
15
|
+
/** `sm` for dense places such as toolbars and table cells. */
|
|
16
|
+
size?: FieldBoxSize | undefined;
|
|
17
|
+
/** Classes for the field box: border, background, width. */
|
|
15
18
|
className?: string | undefined;
|
|
16
19
|
}
|
|
17
20
|
type SelectionProps<Value> = {
|
|
@@ -54,4 +57,4 @@ export interface AutocompleteProps extends FieldProps {
|
|
|
54
57
|
/** A pick-from-the-list field: one value, or several with `multiple`. Typing filters the list. */
|
|
55
58
|
export declare function Combobox<Value = string>(props: ComboboxProps<Value>): import("react").JSX.Element;
|
|
56
59
|
/** A free-text field that suggests values as you type. */
|
|
57
|
-
export declare function Autocomplete({ items, value, defaultValue, onValueChange, className, placeholder, disabled, required, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, }: AutocompleteProps): import("react").JSX.Element;
|
|
60
|
+
export declare function Autocomplete({ items, value, defaultValue, onValueChange, className, placeholder, disabled, required, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, size, }: AutocompleteProps): import("react").JSX.Element;
|
|
@@ -4,8 +4,8 @@ import { panelClassName, panelItemClassName, positionerClassName } from "./float
|
|
|
4
4
|
import { Tooltip } from "./Tooltip.js";
|
|
5
5
|
import { useStrings } from "./Provider.js";
|
|
6
6
|
import { useQueryMatch } from "./search.js";
|
|
7
|
-
import { isComboboxGroup } from "./comboboxItems.js";
|
|
8
7
|
import { fieldBox } from "./fieldBox.js";
|
|
8
|
+
import { isComboboxGroup } from "./comboboxItems.js";
|
|
9
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { useCallback, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
11
11
|
import { Combobox } from "@base-ui/react/combobox";
|
|
@@ -68,17 +68,18 @@ function useEntryFilter() {
|
|
|
68
68
|
return useCallback((entry, query) => entry.created === true || match(query, entry.label, typeof entry.value === "string" ? entry.value : void 0, entry.group), [match]);
|
|
69
69
|
}
|
|
70
70
|
var entryLabel = (entry) => entry?.label ?? "";
|
|
71
|
-
var fieldClassName = cx(fieldBox({
|
|
71
|
+
var fieldClassName = (size) => cx(fieldBox({
|
|
72
|
+
size,
|
|
72
73
|
within: true,
|
|
73
74
|
multiline: true,
|
|
74
75
|
padded: false
|
|
75
76
|
}), "relative flex items-center gap-1");
|
|
76
|
-
var inputClassName = "
|
|
77
|
+
var inputClassName = (size) => cx("min-w-12 flex-1 self-stretch border-0 bg-transparent p-0 text-inherit outline-none placeholder:text-(--theme-placeholder) disabled:cursor-not-allowed", size === "sm" ? "pl-2" : "pl-3");
|
|
77
78
|
var triggerClassName = "flex shrink-0 items-center self-stretch px-2 text-(--theme-muted-text-color) disabled:cursor-not-allowed";
|
|
78
79
|
var popupInputClassName = fieldBox({ size: "sm" });
|
|
79
80
|
/** A pick-from-the-list field: one value, or several with `multiple`. Typing filters the list. */
|
|
80
81
|
function Combobox$1(props) {
|
|
81
|
-
const { items, isItemEqualToValue = dequal, itemToStringLabel = String, className, placeholder, disabled, required, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, trigger, triggerTooltip } = props;
|
|
82
|
+
const { items, isItemEqualToValue = dequal, itemToStringLabel = String, className, placeholder, disabled, required, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, trigger, triggerTooltip, size = "md" } = props;
|
|
82
83
|
const entries = useEntries(items);
|
|
83
84
|
const filter = useEntryFilter();
|
|
84
85
|
const strings = useStrings();
|
|
@@ -122,7 +123,7 @@ function Combobox$1(props) {
|
|
|
122
123
|
"aria-invalid": ariaInvalid || void 0,
|
|
123
124
|
"aria-required": ariaRequired || required || void 0,
|
|
124
125
|
autoComplete: "off",
|
|
125
|
-
className: trigger ? popupInputClassName : inputClassName
|
|
126
|
+
className: trigger ? popupInputClassName : inputClassName(size)
|
|
126
127
|
});
|
|
127
128
|
const itemRow = (entry, rowProps) => /* @__PURE__ */ jsxs(Combobox.Item, {
|
|
128
129
|
value: entry,
|
|
@@ -158,9 +159,9 @@ function Combobox$1(props) {
|
|
|
158
159
|
renderItem: itemRow
|
|
159
160
|
})] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Combobox.InputGroup, {
|
|
160
161
|
"data-testid": "combobox",
|
|
161
|
-
className: cx(fieldClassName, className),
|
|
162
|
+
className: cx(fieldClassName(size), className),
|
|
162
163
|
children: [props.multiple ? /* @__PURE__ */ jsxs(Combobox.Chips, {
|
|
163
|
-
className: "flex min-w-0 flex-1 flex-wrap items-center gap-1
|
|
164
|
+
className: cx("flex min-w-0 flex-1 flex-wrap items-center gap-1 pl-1", size === "sm" ? "py-0.5" : "py-1"),
|
|
164
165
|
children: [/* @__PURE__ */ jsx(Combobox.Value, { children: (values) => values.map((entry) => /* @__PURE__ */ jsxs(Combobox.Chip, {
|
|
165
166
|
"aria-label": entry.label,
|
|
166
167
|
className: "flex items-center gap-1 rounded bg-(--theme-muted-panel-bg) py-0.5 pr-0.5 pl-1.5 text-xs outline-none data-highlighted:bg-(--theme-hover)",
|
|
@@ -171,7 +172,7 @@ function Combobox$1(props) {
|
|
|
171
172
|
})]
|
|
172
173
|
}, entry.label)) }), input]
|
|
173
174
|
}) : /* @__PURE__ */ jsxs(Fragment, { children: [selectedIcon && /* @__PURE__ */ jsx("span", {
|
|
174
|
-
className: "flex shrink-0 items-center pl-3",
|
|
175
|
+
className: cx("flex shrink-0 items-center", size === "sm" ? "pl-2" : "pl-3"),
|
|
175
176
|
children: selectedIcon
|
|
176
177
|
}), input] }), /* @__PURE__ */ jsx(Combobox.Trigger, {
|
|
177
178
|
"aria-label": t.showOptions,
|
|
@@ -216,7 +217,7 @@ function Combobox$1(props) {
|
|
|
216
217
|
});
|
|
217
218
|
}
|
|
218
219
|
/** A free-text field that suggests values as you type. */
|
|
219
|
-
function Autocomplete$1({ items, value, defaultValue, onValueChange, className, placeholder, disabled, required, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired }) {
|
|
220
|
+
function Autocomplete$1({ items, value, defaultValue, onValueChange, className, placeholder, disabled, required, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, size = "md" }) {
|
|
220
221
|
const entries = useEntries(items);
|
|
221
222
|
const filter = useEntryFilter();
|
|
222
223
|
const t = useStrings().dropdown;
|
|
@@ -234,7 +235,7 @@ function Autocomplete$1({ items, value, defaultValue, onValueChange, className,
|
|
|
234
235
|
virtualized: true,
|
|
235
236
|
children: [/* @__PURE__ */ jsxs(Autocomplete.InputGroup, {
|
|
236
237
|
"data-testid": "combobox",
|
|
237
|
-
className: cx(fieldClassName, className),
|
|
238
|
+
className: cx(fieldClassName(size), className),
|
|
238
239
|
children: [/* @__PURE__ */ jsx(Autocomplete.Input, {
|
|
239
240
|
id,
|
|
240
241
|
placeholder,
|
|
@@ -244,7 +245,7 @@ function Autocomplete$1({ items, value, defaultValue, onValueChange, className,
|
|
|
244
245
|
"aria-invalid": ariaInvalid || void 0,
|
|
245
246
|
"aria-required": ariaRequired || required || void 0,
|
|
246
247
|
autoComplete: "off",
|
|
247
|
-
className: inputClassName
|
|
248
|
+
className: inputClassName(size)
|
|
248
249
|
}), entries.flat.length > 0 && /* @__PURE__ */ jsx(Autocomplete.Trigger, {
|
|
249
250
|
"aria-label": t.showOptions,
|
|
250
251
|
"data-testid": "combobox-button",
|
|
@@ -124,7 +124,7 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, namePlaceho
|
|
|
124
124
|
placeholder: namePlaceholder,
|
|
125
125
|
"aria-label": namePlaceholder,
|
|
126
126
|
onAnimationEnd: () => setNameFlash(false),
|
|
127
|
-
className: cx("mt-2 -mb-1 -mx-1.5 w-[calc(100%+0.75rem)] rounded-md border-0 px-1.5 py-1 bg-transparent shadow-none text-lg font-medium text-(--theme-app) placeholder:text-(--theme-
|
|
127
|
+
className: cx("mt-2 -mb-1 -mx-1.5 w-[calc(100%+0.75rem)] rounded-md border-0 px-1.5 py-1 bg-transparent shadow-none text-lg font-medium text-(--theme-app) placeholder:text-(--theme-placeholder) focus:outline-none", nameFlash && "animate-field-flash", nameMissing ? "ring-1 ring-(--theme-error)" : "focus:ring-0")
|
|
128
128
|
}),
|
|
129
129
|
nameError && /* @__PURE__ */ jsx("p", {
|
|
130
130
|
className: "mt-1 text-xs text-(--theme-error)",
|
|
@@ -142,7 +142,7 @@ function CreateModal({ open, onClose, typeIcon, typeLabel, typeMenu, namePlaceho
|
|
|
142
142
|
placeholder: descriptionText,
|
|
143
143
|
"aria-label": descriptionText,
|
|
144
144
|
rows: 3,
|
|
145
|
-
className: "mt-1.5 w-full border-0 p-0 bg-transparent shadow-none resize-none text-sm text-(--theme-app) placeholder:text-(--theme-
|
|
145
|
+
className: "mt-1.5 w-full border-0 p-0 bg-transparent shadow-none resize-none text-sm text-(--theme-app) placeholder:text-(--theme-placeholder) focus:outline-none focus:ring-0"
|
|
146
146
|
}) : /* @__PURE__ */ jsx("div", { className: "mt-3" })
|
|
147
147
|
] }),
|
|
148
148
|
children && /* @__PURE__ */ jsx("div", {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FieldBoxSize } from './fieldBox.js';
|
|
1
2
|
export interface DatePickerProps {
|
|
2
3
|
/** As a native date input holds it: `yyyy-MM-dd`, or with `withTime` the local
|
|
3
4
|
* `yyyy-MM-ddTHH:mm` of `datetime-local`. Empty while nothing is chosen. */
|
|
@@ -21,6 +22,9 @@ export interface DatePickerProps {
|
|
|
21
22
|
'aria-describedby'?: string | undefined;
|
|
22
23
|
'aria-invalid'?: boolean | undefined;
|
|
23
24
|
'aria-required'?: boolean | undefined;
|
|
25
|
+
/** `sm` for dense places such as toolbars and table cells. */
|
|
26
|
+
size?: FieldBoxSize | undefined;
|
|
27
|
+
/** On the row that holds the date and the time: its width or margin. */
|
|
24
28
|
className?: string | undefined;
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
@@ -28,4 +32,4 @@ export interface DatePickerProps {
|
|
|
28
32
|
* date or datetime-local input holds it, so it reads the same in forms and URLs; months, weekdays
|
|
29
33
|
* and the shown date follow the reader's locale.
|
|
30
34
|
*/
|
|
31
|
-
export declare function DatePicker({ value, onValueChange, withTime, min, max, timeStep, required, disabled, placeholder, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, className, }: DatePickerProps): import("react").JSX.Element;
|
|
35
|
+
export declare function DatePicker({ value, onValueChange, withTime, min, max, timeStep, required, disabled, placeholder, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, size, className, }: DatePickerProps): import("react").JSX.Element;
|
|
@@ -65,13 +65,13 @@ var calendarClassNames = {
|
|
|
65
65
|
outside: "text-(--theme-muted-text-color) opacity-60",
|
|
66
66
|
disabled: "text-(--theme-muted-text-color) opacity-40"
|
|
67
67
|
};
|
|
68
|
-
var triggerClassName = cx(fieldBox(), "flex flex-1 min-w-
|
|
68
|
+
var triggerClassName = (size) => cx(fieldBox({ size }), "flex flex-1 cursor-pointer items-center text-left", size === "sm" ? "min-w-32 gap-1.5" : "min-w-40 gap-2");
|
|
69
69
|
/**
|
|
70
70
|
* A day from a calendar, and with `withTime` a time beside it. The value is a string as a native
|
|
71
71
|
* date or datetime-local input holds it, so it reads the same in forms and URLs; months, weekdays
|
|
72
72
|
* and the shown date follow the reader's locale.
|
|
73
73
|
*/
|
|
74
|
-
function DatePicker({ value, onValueChange, withTime = false, min, max, timeStep = 60, required = false, disabled = false, placeholder, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, className }) {
|
|
74
|
+
function DatePicker({ value, onValueChange, withTime = false, min, max, timeStep = 60, required = false, disabled = false, placeholder, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, size = "md", className }) {
|
|
75
75
|
const t = useStrings().datePicker;
|
|
76
76
|
const { common } = useStrings();
|
|
77
77
|
const [open, setOpen] = useState(false);
|
|
@@ -95,7 +95,7 @@ function DatePicker({ value, onValueChange, withTime = false, min, max, timeStep
|
|
|
95
95
|
};
|
|
96
96
|
const sameDay = (bound) => day !== void 0 && parseDay(bound)?.getTime() === day.getTime();
|
|
97
97
|
return /* @__PURE__ */ jsxs("div", {
|
|
98
|
-
className: cx("flex w-full items-center gap-2", className),
|
|
98
|
+
className: cx("flex w-full items-center", size === "sm" ? "gap-1.5" : "gap-2", className),
|
|
99
99
|
children: [/* @__PURE__ */ jsxs(Popover.Root, {
|
|
100
100
|
open,
|
|
101
101
|
onOpenChange: setOpen,
|
|
@@ -107,12 +107,12 @@ function DatePicker({ value, onValueChange, withTime = false, min, max, timeStep
|
|
|
107
107
|
"aria-describedby": ariaDescribedBy,
|
|
108
108
|
"aria-invalid": ariaInvalid || void 0,
|
|
109
109
|
"aria-required": ariaRequired || void 0,
|
|
110
|
-
className: triggerClassName,
|
|
110
|
+
className: triggerClassName(size),
|
|
111
111
|
children: [/* @__PURE__ */ jsx(CalendarIcon, {
|
|
112
112
|
"aria-hidden": "true",
|
|
113
|
-
className: "
|
|
113
|
+
className: cx("shrink-0 text-(--theme-muted-text-color)", size === "sm" ? "size-3" : "size-3.5")
|
|
114
114
|
}), /* @__PURE__ */ jsx("span", {
|
|
115
|
-
className: cx("truncate", !day && "text-(--theme-
|
|
115
|
+
className: cx("truncate", !day && "text-(--theme-placeholder)"),
|
|
116
116
|
children: day ? shownDay.format(day) : placeholder ?? t.pickDate
|
|
117
117
|
})]
|
|
118
118
|
}), /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
|
|
@@ -183,7 +183,7 @@ function DatePicker({ value, onValueChange, withTime = false, min, max, timeStep
|
|
|
183
183
|
const next = event.target.value;
|
|
184
184
|
if (next) emit(day ?? earliest ?? /* @__PURE__ */ new Date(), next);
|
|
185
185
|
},
|
|
186
|
-
className: cx(fieldBox(), "w-auto tabular-nums")
|
|
186
|
+
className: cx(fieldBox({ size }), "w-auto tabular-nums")
|
|
187
187
|
})]
|
|
188
188
|
});
|
|
189
189
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
1
|
+
import { type OutputHTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import type { FieldBoxSize } from './fieldBox.js';
|
|
2
3
|
/** What a field's control takes to be named, described and marked by its field. Spread it
|
|
3
4
|
* on the control: `{(control) => <input {...control} />}`. */
|
|
4
5
|
export interface FieldControlProps {
|
|
@@ -43,12 +44,15 @@ export interface FieldProps {
|
|
|
43
44
|
id?: string | undefined;
|
|
44
45
|
className?: string | undefined;
|
|
45
46
|
/** The control, or a function that places the control props on it. A Base UI control, such
|
|
46
|
-
* as Select, Combobox, NumberInput, Checkbox or Switch, needs only the `id`.
|
|
47
|
+
* as Select, Combobox, NumberInput, Checkbox or Switch, needs only the `id`. A value shown
|
|
48
|
+
* but not edited goes in a `FieldValue`. */
|
|
47
49
|
children: ReactNode | ((control: FieldControlProps) => ReactNode);
|
|
48
50
|
}
|
|
49
|
-
/** An error in the look a Field gives its own, for
|
|
50
|
-
* the whole of it, such as a row of a list editor.
|
|
51
|
-
|
|
51
|
+
/** An error in the look a Field gives its own, for a control outside a Field, or for a part of
|
|
52
|
+
* one rather than the whole of it, such as a row of a list editor. Give it an `id` and name it
|
|
53
|
+
* in the control's `aria-describedby` so the error is read with the control. */
|
|
54
|
+
export declare function ErrorText({ id, children }: {
|
|
55
|
+
id?: string | undefined;
|
|
52
56
|
children: ReactNode;
|
|
53
57
|
}): import("react").JSX.Element | null;
|
|
54
58
|
/** A control with its label, help and error, tied to it for assistive technology. One frame
|
|
@@ -68,3 +72,13 @@ export declare function RevealToggle({ revealed, onRevealedChange, disabled, }:
|
|
|
68
72
|
onRevealedChange: (revealed: boolean) => void;
|
|
69
73
|
disabled?: boolean | undefined;
|
|
70
74
|
}): import("react").JSX.Element;
|
|
75
|
+
export interface FieldValueProps extends OutputHTMLAttributes<HTMLOutputElement> {
|
|
76
|
+
/** Matches the height and text of the controls around it, as `size` does on a control. */
|
|
77
|
+
size?: FieldBoxSize | undefined;
|
|
78
|
+
}
|
|
79
|
+
/** A value a Field shows that nobody edits, such as one worked out from other fields. It is an
|
|
80
|
+
* `<output>`, which the Field's label names and which reads out when the value changes; list
|
|
81
|
+
* the controls it is worked out from in `htmlFor`. It keeps a control's height, so it lines
|
|
82
|
+
* up with the fields around it. For a set of names and values on their own, use
|
|
83
|
+
* DescriptionList. */
|
|
84
|
+
export declare function FieldValue({ size, className, children, ...props }: FieldValueProps): import("react").JSX.Element;
|
package/dist/components/Field.js
CHANGED
|
@@ -20,10 +20,12 @@ function useFieldControl() {
|
|
|
20
20
|
var labelClassName = "flex items-center gap-1 text-sm font-medium text-(--theme-app)";
|
|
21
21
|
var actionsClassName = "ml-auto flex shrink-0 items-center gap-1";
|
|
22
22
|
var errorTextClassName = "flex items-center gap-1.5 text-xs text-(--theme-error)";
|
|
23
|
-
/** An error in the look a Field gives its own, for
|
|
24
|
-
* the whole of it, such as a row of a list editor.
|
|
25
|
-
|
|
23
|
+
/** An error in the look a Field gives its own, for a control outside a Field, or for a part of
|
|
24
|
+
* one rather than the whole of it, such as a row of a list editor. Give it an `id` and name it
|
|
25
|
+
* in the control's `aria-describedby` so the error is read with the control. */
|
|
26
|
+
function ErrorText({ id, children }) {
|
|
26
27
|
return children ? /* @__PURE__ */ jsxs("div", {
|
|
28
|
+
id,
|
|
27
29
|
role: "alert",
|
|
28
30
|
className: errorTextClassName,
|
|
29
31
|
children: [/* @__PURE__ */ jsx(WarningIcon, {
|
|
@@ -155,5 +157,20 @@ function RevealToggle({ revealed, onRevealedChange, disabled }) {
|
|
|
155
157
|
})
|
|
156
158
|
});
|
|
157
159
|
}
|
|
160
|
+
/** A value a Field shows that nobody edits, such as one worked out from other fields. It is an
|
|
161
|
+
* `<output>`, which the Field's label names and which reads out when the value changes; list
|
|
162
|
+
* the controls it is worked out from in `htmlFor`. It keeps a control's height, so it lines
|
|
163
|
+
* up with the fields around it. For a set of names and values on their own, use
|
|
164
|
+
* DescriptionList. */
|
|
165
|
+
function FieldValue({ size = "md", className, children, ...props }) {
|
|
166
|
+
const control = useFieldControl();
|
|
167
|
+
return /* @__PURE__ */ jsx("output", {
|
|
168
|
+
id: control?.id,
|
|
169
|
+
"aria-describedby": control?.["aria-describedby"],
|
|
170
|
+
...props,
|
|
171
|
+
className: cx("flex min-w-0 items-center break-words text-(--theme-app)", size === "sm" ? "min-h-7 text-xs" : "min-h-9 text-sm", className),
|
|
172
|
+
children
|
|
173
|
+
});
|
|
174
|
+
}
|
|
158
175
|
//#endregion
|
|
159
|
-
export { ErrorText, Field$1 as Field, FieldOption, FieldRowContext, RevealToggle, useFieldControl };
|
|
176
|
+
export { ErrorText, Field$1 as Field, FieldOption, FieldRowContext, FieldValue, RevealToggle, useFieldControl };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { InputHTMLAttributes, ReactNode, TextareaHTMLAttributes } from 'react';
|
|
2
|
+
import { type FieldControlProps } from './Field.js';
|
|
3
|
+
import { type FieldBoxSize } from './fieldBox.js';
|
|
2
4
|
interface FramedProps {
|
|
3
5
|
/** Without a label the box stands alone; name it with `aria-label`. */
|
|
4
6
|
label?: ReactNode;
|
|
@@ -8,11 +10,19 @@ interface FramedProps {
|
|
|
8
10
|
description?: ReactNode;
|
|
9
11
|
error?: ReactNode;
|
|
10
12
|
mono?: boolean;
|
|
13
|
+
/** `sm` for dense places such as toolbars and table cells. */
|
|
14
|
+
size?: FieldBoxSize | undefined;
|
|
11
15
|
}
|
|
12
|
-
export interface InputProps extends InputHTMLAttributes<HTMLInputElement>, FramedProps {
|
|
16
|
+
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, FramedProps {
|
|
13
17
|
}
|
|
14
18
|
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement>, FramedProps {
|
|
15
19
|
}
|
|
20
|
+
/** The Field around a control when it has a label; the control alone when it doesn't. */
|
|
21
|
+
export declare function framed({ label, hint, description, error }: Omit<FramedProps, 'mono' | 'size'>, { id, required, disabled, }: {
|
|
22
|
+
id?: string | undefined;
|
|
23
|
+
required?: boolean | undefined;
|
|
24
|
+
disabled?: boolean | undefined;
|
|
25
|
+
}, control: (props: Partial<FieldControlProps>) => ReactNode): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
|
|
16
26
|
/** A single-line text field. `type="password"` adds a button that shows what was typed. */
|
|
17
27
|
export declare const Input: import("react").ForwardRefExoticComponent<InputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
18
28
|
/** A multi-line text field that grows by hand. */
|
package/dist/components/Input.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
|
-
import { fieldBox } from "./fieldBox.js";
|
|
3
2
|
import { Field, RevealToggle } from "./Field.js";
|
|
3
|
+
import { fieldBox } from "./fieldBox.js";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { forwardRef, useState } from "react";
|
|
6
6
|
//#region src/components/Input.tsx
|
|
@@ -19,7 +19,7 @@ function framed({ label, hint, description, error }, { id, required, disabled },
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
/** A single-line text field. `type="password"` adds a button that shows what was typed. */
|
|
22
|
-
var Input = forwardRef(({ label, hint, description, error, className, mono, type, ...props }, ref) => {
|
|
22
|
+
var Input = forwardRef(({ label, hint, description, error, className, mono, size, type, ...props }, ref) => {
|
|
23
23
|
const [revealed, setRevealed] = useState(false);
|
|
24
24
|
const secret = type === "password";
|
|
25
25
|
return framed({
|
|
@@ -35,7 +35,7 @@ var Input = forwardRef(({ label, hint, description, error, className, mono, type
|
|
|
35
35
|
"aria-invalid": control["aria-invalid"] ?? props["aria-invalid"],
|
|
36
36
|
"aria-describedby": [props["aria-describedby"], control["aria-describedby"]].filter(Boolean).join(" ") || void 0,
|
|
37
37
|
type: secret && revealed ? "text" : type,
|
|
38
|
-
className: cx(fieldBox(), mono && "font-mono", secret && "pr-9", className)
|
|
38
|
+
className: cx(fieldBox({ size }), mono && "font-mono", secret && "pr-9", className)
|
|
39
39
|
});
|
|
40
40
|
return secret ? /* @__PURE__ */ jsxs("div", {
|
|
41
41
|
className: "relative w-full",
|
|
@@ -49,7 +49,7 @@ var Input = forwardRef(({ label, hint, description, error, className, mono, type
|
|
|
49
49
|
});
|
|
50
50
|
Input.displayName = "Input";
|
|
51
51
|
/** A multi-line text field that grows by hand. */
|
|
52
|
-
var Textarea = forwardRef(({ label, hint, description, error, className, mono, ...props }, ref) => framed({
|
|
52
|
+
var Textarea = forwardRef(({ label, hint, description, error, className, mono, size, ...props }, ref) => framed({
|
|
53
53
|
label,
|
|
54
54
|
hint,
|
|
55
55
|
description,
|
|
@@ -60,8 +60,11 @@ var Textarea = forwardRef(({ label, hint, description, error, className, mono, .
|
|
|
60
60
|
...control,
|
|
61
61
|
"aria-invalid": control["aria-invalid"] ?? props["aria-invalid"],
|
|
62
62
|
"aria-describedby": [props["aria-describedby"], control["aria-describedby"]].filter(Boolean).join(" ") || void 0,
|
|
63
|
-
className: cx(fieldBox({
|
|
63
|
+
className: cx(fieldBox({
|
|
64
|
+
size,
|
|
65
|
+
multiline: true
|
|
66
|
+
}), size === "sm" ? "min-h-16" : "min-h-24", "resize-y", mono && "font-mono", className)
|
|
64
67
|
})));
|
|
65
68
|
Textarea.displayName = "Textarea";
|
|
66
69
|
//#endregion
|
|
67
|
-
export { Input, Textarea };
|
|
70
|
+
export { Input, Textarea, framed };
|
|
@@ -38,7 +38,7 @@ function MenuEntries({ entries, search, query = "" }) {
|
|
|
38
38
|
children: /* @__PURE__ */ jsx(Menu.Input, {
|
|
39
39
|
"aria-label": search.placeholder,
|
|
40
40
|
placeholder: search.placeholder,
|
|
41
|
-
className: "w-full rounded-md border border-(--theme-border) bg-(--theme-muted-panel-bg) px-2 py-1 text-[13px] text-(--theme-app) outline-none placeholder:text-(--theme-
|
|
41
|
+
className: "w-full rounded-md border border-(--theme-border) bg-(--theme-muted-panel-bg) px-2 py-1 text-[13px] text-(--theme-app) outline-none placeholder:text-(--theme-placeholder) focus:border-(--theme-accent)"
|
|
42
42
|
})
|
|
43
43
|
}),
|
|
44
44
|
/* @__PURE__ */ jsx(Menu.List, { children: /* @__PURE__ */ jsx(EntryRows, { entries: shown }) }),
|
|
@@ -15,6 +15,7 @@ export interface NumberInputProps extends Omit<NumberField.Root.Props, 'classNam
|
|
|
15
15
|
'aria-invalid'?: boolean | undefined;
|
|
16
16
|
/** Marks the field required for assistive technology without the native check `required` adds. */
|
|
17
17
|
'aria-required'?: boolean | undefined;
|
|
18
|
+
/** `sm` for dense places such as toolbars and table cells. */
|
|
18
19
|
size?: FieldBoxSize | undefined;
|
|
19
20
|
/** Applied to the box around the input and its steppers, such as a width. */
|
|
20
21
|
className?: string | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { OTPField } from '@base-ui/react/otp-field';
|
|
2
|
+
import { type FieldBoxSize } from './fieldBox.js';
|
|
3
|
+
export interface OneTimeCodeInputProps extends Omit<OTPField.Root.Props, 'className' | 'render' | 'children' | 'length' | 'style'> {
|
|
4
|
+
/** How many characters the code has. */
|
|
5
|
+
length?: number | undefined;
|
|
6
|
+
/** `sm` for dense places such as toolbars and table cells. */
|
|
7
|
+
size?: FieldBoxSize | undefined;
|
|
8
|
+
'aria-invalid'?: boolean | undefined;
|
|
9
|
+
'aria-required'?: boolean | undefined;
|
|
10
|
+
/** On the row of boxes: its margin or gap. */
|
|
11
|
+
className?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A one-time code, such as the six digits of a sign-in check, typed one character to a box. It
|
|
15
|
+
* fills from the browser's or phone's code autofill (`autocomplete="one-time-code"`) and from a
|
|
16
|
+
* paste of the whole code, moves on as each character is typed and back on Backspace, and opens a
|
|
17
|
+
* number pad for a numeric code. The value is the code as one string; `onValueComplete` fires once
|
|
18
|
+
* every box is filled, and `autoSubmit` submits the form then.
|
|
19
|
+
*
|
|
20
|
+
* Name it with a Field or a `<label>` for its `id`, which is the first box's, or with
|
|
21
|
+
* `aria-label`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function OneTimeCodeInput({ length, size, className, 'aria-label': ariaLabel, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, ...rootProps }: OneTimeCodeInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { cx } from "../cx.js";
|
|
2
|
+
import { useStrings } from "./Provider.js";
|
|
3
|
+
import { fieldBox } from "./fieldBox.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useId } from "react";
|
|
6
|
+
import { OTPField } from "@base-ui/react/otp-field";
|
|
7
|
+
//#region src/components/OneTimeCodeInput.tsx
|
|
8
|
+
/**
|
|
9
|
+
* A one-time code, such as the six digits of a sign-in check, typed one character to a box. It
|
|
10
|
+
* fills from the browser's or phone's code autofill (`autocomplete="one-time-code"`) and from a
|
|
11
|
+
* paste of the whole code, moves on as each character is typed and back on Backspace, and opens a
|
|
12
|
+
* number pad for a numeric code. The value is the code as one string; `onValueComplete` fires once
|
|
13
|
+
* every box is filled, and `autoSubmit` submits the form then.
|
|
14
|
+
*
|
|
15
|
+
* Name it with a Field or a `<label>` for its `id`, which is the first box's, or with
|
|
16
|
+
* `aria-label`.
|
|
17
|
+
*/
|
|
18
|
+
function OneTimeCodeInput({ length = 6, size = "md", className, "aria-label": ariaLabel, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, ...rootProps }) {
|
|
19
|
+
const t = useStrings().oneTimeCode;
|
|
20
|
+
const labelId = useId();
|
|
21
|
+
return /* @__PURE__ */ jsxs(OTPField.Root, {
|
|
22
|
+
length,
|
|
23
|
+
"aria-label": ariaLabel,
|
|
24
|
+
...rootProps,
|
|
25
|
+
className: cx("flex items-center", size === "sm" ? "gap-1" : "gap-2", className),
|
|
26
|
+
children: [ariaLabel && /* @__PURE__ */ jsx("span", {
|
|
27
|
+
id: labelId,
|
|
28
|
+
hidden: true,
|
|
29
|
+
children: ariaLabel
|
|
30
|
+
}), Array.from({ length }, (_, index) => /* @__PURE__ */ jsx(OTPField.Input, {
|
|
31
|
+
...index > 0 ? { "aria-label": t.character(index + 1, length) } : ariaLabel ? { "aria-labelledby": labelId } : {},
|
|
32
|
+
...ariaInvalid && { "aria-invalid": true },
|
|
33
|
+
...ariaRequired && { "aria-required": true },
|
|
34
|
+
className: cx(fieldBox({ size }), "px-0 text-center font-mono tabular-nums", size === "sm" ? "w-7" : "w-9 text-base")
|
|
35
|
+
}, index))]
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { OneTimeCodeInput };
|