@tanishqkancharla/maui 0.0.52 → 0.0.53
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/dist/components/Input.d.ts +9 -2
- package/dist/components/Input.js +34 -8
- package/dist/maui.d.ts +2 -2
- package/dist/maui.js +1 -1
- package/package.json +1 -1
- package/skills/maui/references/components/form-controls.md +11 -1
- package/skills/maui/references/tokens.md +2 -0
- package/src/components/Input.tsx +74 -10
- package/src/maui.ts +2 -0
- package/src/pages/FormControlsPage.tsx +12 -0
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import { type Ref } from "react";
|
|
1
2
|
import { AriaNumberFieldProps, AriaSearchFieldProps, AriaTextFieldOptions } from "react-aria";
|
|
2
3
|
type KeyboardHintProps = {
|
|
3
4
|
/** A single-character shortcut displayed at the inline end while unfocused. */
|
|
4
5
|
keyboardHint?: string;
|
|
5
6
|
};
|
|
6
|
-
|
|
7
|
+
type BaseTextFieldProps = AriaTextFieldOptions<"input"> & KeyboardHintProps;
|
|
8
|
+
export type TextFieldSize = "default" | "sm";
|
|
9
|
+
export type TextFieldProps = BaseTextFieldProps & {
|
|
10
|
+
/** `sm` matches `Button size="sm"` for dense rows such as tree items. */
|
|
11
|
+
size?: TextFieldSize;
|
|
12
|
+
ref?: Ref<HTMLInputElement>;
|
|
13
|
+
};
|
|
7
14
|
export type SearchFieldProps = AriaSearchFieldProps & KeyboardHintProps & {
|
|
8
15
|
"aria-keyshortcuts"?: string;
|
|
9
16
|
};
|
|
10
17
|
export declare function TextField(props: TextFieldProps): import("react").JSX.Element;
|
|
11
18
|
export declare function SearchField(props: SearchFieldProps): import("react").JSX.Element;
|
|
12
19
|
export declare function NumberField(props: AriaNumberFieldProps): import("react").JSX.Element;
|
|
13
|
-
export declare function QuietTextField(props:
|
|
20
|
+
export declare function QuietTextField(props: BaseTextFieldProps): import("react").JSX.Element;
|
|
14
21
|
export {};
|
package/dist/components/Input.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useState } from "react";
|
|
3
|
-
import { mergeProps, useButton, useLocale, useNumberField, useSearchField, useTextField, } from "react-aria";
|
|
3
|
+
import { mergeProps, useButton, useLocale, useNumberField, useObjectRef, useSearchField, useTextField, } from "react-aria";
|
|
4
4
|
import { useNumberFieldState, useSearchFieldState } from "react-stately";
|
|
5
5
|
import { defineVars, style, useStyles } from "purse-styles";
|
|
6
6
|
import { LARGE_SCALE } from "../theme/dataScale";
|
|
@@ -38,6 +38,20 @@ const inputClass = style(inputText, focusRing("&:focus-visible", shadowVars.cont
|
|
|
38
38
|
color: colors.gray[8],
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
|
+
const smallInputClass = style(spacing.padding({ x: 3 }), {
|
|
42
|
+
height: controlSize.smHeight,
|
|
43
|
+
paddingBlock: 0,
|
|
44
|
+
});
|
|
45
|
+
const fieldClass = style({
|
|
46
|
+
display: "flex",
|
|
47
|
+
flexDirection: "column",
|
|
48
|
+
gap: spacing.value(2),
|
|
49
|
+
width: "100%",
|
|
50
|
+
minWidth: 0,
|
|
51
|
+
});
|
|
52
|
+
const errorClass = style(text({ size: "xs", fontWeight: 400, color: "highContrast" }), {
|
|
53
|
+
color: "light-dark(#ce2c31, #e5484d)",
|
|
54
|
+
});
|
|
41
55
|
const inputFrameClass = style({
|
|
42
56
|
position: "relative",
|
|
43
57
|
width: "100%",
|
|
@@ -67,16 +81,28 @@ function KeyboardHint(props) {
|
|
|
67
81
|
return (_jsx(Kbd, { variant: "quiet", className: className, "aria-hidden": "true", children: props.children }));
|
|
68
82
|
}
|
|
69
83
|
export function TextField(props) {
|
|
70
|
-
const { keyboardHint, ...fieldProps } = props;
|
|
71
|
-
const ref =
|
|
72
|
-
const { inputProps } = useTextField(fieldProps, ref);
|
|
84
|
+
const { keyboardHint, size = "default", ref: forwardedRef, ...fieldProps } = props;
|
|
85
|
+
const ref = useObjectRef(forwardedRef);
|
|
86
|
+
const { inputProps, errorMessageProps, isInvalid, validationErrors, validationDetails, } = useTextField(fieldProps, ref);
|
|
73
87
|
const { isFocused, focusProps } = useKeyboardHint();
|
|
74
88
|
const frameClassName = useStyles(inputFrameClass);
|
|
75
|
-
const
|
|
89
|
+
const fieldClassName = useStyles(fieldClass);
|
|
90
|
+
const errorClassName = useStyles(errorClass);
|
|
91
|
+
const className = useStyles(inputClass, size === "sm" && smallInputClass, keyboardHint ? inputWithHintClass : undefined);
|
|
92
|
+
const errorMessage = typeof fieldProps.errorMessage === "function"
|
|
93
|
+
? fieldProps.errorMessage({
|
|
94
|
+
isInvalid,
|
|
95
|
+
validationErrors,
|
|
96
|
+
validationDetails,
|
|
97
|
+
})
|
|
98
|
+
: fieldProps.errorMessage;
|
|
76
99
|
const input = (_jsx("input", { className: className, ref: ref, ...mergeProps(inputProps, focusProps) }));
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
const control = keyboardHint ? (_jsxs("div", { className: frameClassName, children: [input, !isFocused ? _jsx(KeyboardHint, { children: keyboardHint }) : null] })) : (input);
|
|
101
|
+
const showError = isInvalid && errorMessage != null && errorMessage !== "";
|
|
102
|
+
// Keep one wrapper in every state so the input is not remounted, and does
|
|
103
|
+
// not lose focus, when the error appears or disappears while typing. Spans
|
|
104
|
+
// keep the field valid inside a <label>.
|
|
105
|
+
return (_jsxs("span", { className: fieldClassName, children: [control, showError ? (_jsx("span", { ...errorMessageProps, className: errorClassName, children: errorMessage })) : null] }));
|
|
80
106
|
}
|
|
81
107
|
const searchFieldSize = defineVars({
|
|
82
108
|
clearSize: { default: "16px", [LARGE_SCALE]: "40px" },
|
package/dist/maui.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { flex, flexItem, grid, gridItem, type AlignItems, type JustifyContent }
|
|
|
15
15
|
export { text, monospace, baseTextStyle, fontFamily, monoFontFamily, monoFontStyle, type TextSize, type TextWeight, type TextColor, type TextOptions, } from "./tokens/text";
|
|
16
16
|
export { avatar } from "./tokens/avatar";
|
|
17
17
|
export { motion, motionDurationMs, motionEasing, motionStreamDurationMs, overlayMotion, } from "./tokens/motion";
|
|
18
|
-
export { iconSizeValues, sizingTokens as sizing, type IconSize, } from "./tokens/sizing";
|
|
18
|
+
export { controlSize, iconSizeValues, sizingTokens as sizing, type IconSize, } from "./tokens/sizing";
|
|
19
19
|
export { prose, proseRhythm, proseHtml, proseStreamingMarkers, type ProseSize, } from "./tokens/prose";
|
|
20
20
|
export { visuallyHidden } from "./tokens/visuallyHidden";
|
|
21
21
|
export { Avatar } from "./components/Avatar";
|
|
@@ -27,7 +27,7 @@ export { Checkbox } from "./components/Checkbox";
|
|
|
27
27
|
export { Switch } from "./components/Switch";
|
|
28
28
|
export { Slider } from "./components/Slider";
|
|
29
29
|
export { RadioOptionGroup, RadioOption } from "./components/Radio";
|
|
30
|
-
export { TextField, SearchField, NumberField, QuietTextField, type TextFieldProps, type SearchFieldProps, } from "./components/Input";
|
|
30
|
+
export { TextField, SearchField, NumberField, QuietTextField, type TextFieldProps, type TextFieldSize, type SearchFieldProps, } from "./components/Input";
|
|
31
31
|
export { Select, SelectItem, type SelectProps, type SelectItemProps, } from "./components/Select";
|
|
32
32
|
export { Dialog, DialogActions, DialogBody, DialogTitle, type DialogActionsProps, type DialogBodyProps, type DialogProps, type DialogSize, type DialogTitleProps, } from "./components/Dialog";
|
|
33
33
|
export { Tooltip } from "./components/Tooltip";
|
package/dist/maui.js
CHANGED
|
@@ -17,7 +17,7 @@ export { flex, flexItem, grid, gridItem } from "./tokens/layout";
|
|
|
17
17
|
export { text, monospace, baseTextStyle, fontFamily, monoFontFamily, monoFontStyle, } from "./tokens/text";
|
|
18
18
|
export { avatar } from "./tokens/avatar";
|
|
19
19
|
export { motion, motionDurationMs, motionEasing, motionStreamDurationMs, overlayMotion, } from "./tokens/motion";
|
|
20
|
-
export { iconSizeValues, sizingTokens as sizing, } from "./tokens/sizing";
|
|
20
|
+
export { controlSize, iconSizeValues, sizingTokens as sizing, } from "./tokens/sizing";
|
|
21
21
|
export { prose, proseRhythm, proseHtml, proseStreamingMarkers, } from "./tokens/prose";
|
|
22
22
|
export { visuallyHidden } from "./tokens/visuallyHidden";
|
|
23
23
|
// Components
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Gallery: `/components/form-controls`. Import from `"maui"`. [Select](select.md) has its own page.
|
|
4
4
|
|
|
5
|
-
All fields are 28px tall, full width of the parent, `shadow.control` except `QuietTextField`. They take React Aria field props (`value`, `onChange`, `placeholder`, `isDisabled`, `isInvalid`, `aria-label`, `id`, …).
|
|
5
|
+
All fields are 28px tall by default, full width of the parent, `shadow.control` except `QuietTextField`. They take React Aria field props (`value`, `onChange`, `placeholder`, `isDisabled`, `isInvalid`, `aria-label`, `id`, …).
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
8
|
<TextField aria-label="Title" placeholder="Title" value={v} onChange={setV} keyboardHint="T" />
|
|
@@ -13,6 +13,16 @@ All fields are 28px tall, full width of the parent, `shadow.control` except `Qui
|
|
|
13
13
|
|
|
14
14
|
`NumberField` uses tabular numerals so the value does not shift as you step. Invalid (unfocused) adds a red 1px ring on top of `shadow.control`. Placeholders are italic `gray[8]`. Cap width in the parent (`maxWidth: 240px` is the gallery default).
|
|
15
15
|
|
|
16
|
+
`TextField` also accepts:
|
|
17
|
+
|
|
18
|
+
- `size="sm"` — 24px tall with tighter padding, matching `Button size="sm"`, for dense rows such as tree items.
|
|
19
|
+
- `ref` — the underlying `<input>`, for focus, blur, or selection.
|
|
20
|
+
- `errorMessage` — shown below the field while `isInvalid` is true and linked to the input with `aria-describedby`.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<TextField aria-label="Name" size="sm" value={name} onChange={setName} isInvalid={invalid} errorMessage="Use a name without slashes." />
|
|
24
|
+
```
|
|
25
|
+
|
|
16
26
|
`TextField`, `SearchField`, and `QuietTextField` accept `keyboardHint`, a single-character shortcut rendered at the inline end while the field is unfocused. The hint is presentational; the consuming app owns the keyboard handler. `SearchField` hides it when the query is non-empty so the clear button can use the same space.
|
|
17
27
|
|
|
18
28
|
## `Checkbox`
|
|
@@ -225,6 +225,8 @@ ease-in-out. Streaming markdown uses `motionStreamDurationMs`.
|
|
|
225
225
|
|
|
226
226
|
Stroke/fill is `currentColor`. Pass `size` on the icon component; optional `width` / `height` override the box.
|
|
227
227
|
|
|
228
|
+
`controlSize.height` (28px) and `controlSize.smHeight` (24px) are the control heights used by `Button` and `TextField`. Both become 40px at large scale. Use them to size a slot that must line up with a control, such as an icon next to a `TextField size="sm"`.
|
|
229
|
+
|
|
228
230
|
## Avatar tokens
|
|
229
231
|
|
|
230
232
|
`avatar.green` / `avatar.orange` / `avatar.pink` each have `background` and `foreground`. The `Avatar` component also uses `accent[4]`/`[11]`. You rarely need these tokens directly.
|
package/src/components/Input.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useState } from "react"
|
|
1
|
+
import { useRef, useState, type Ref } from "react"
|
|
2
2
|
import {
|
|
3
3
|
AriaNumberFieldProps,
|
|
4
4
|
AriaSearchFieldProps,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
useButton,
|
|
8
8
|
useLocale,
|
|
9
9
|
useNumberField,
|
|
10
|
+
useObjectRef,
|
|
10
11
|
useSearchField,
|
|
11
12
|
useTextField,
|
|
12
13
|
} from "react-aria"
|
|
@@ -61,12 +62,38 @@ const inputClass = style(
|
|
|
61
62
|
},
|
|
62
63
|
)
|
|
63
64
|
|
|
65
|
+
const smallInputClass = style(spacing.padding({ x: 3 }), {
|
|
66
|
+
height: controlSize.smHeight,
|
|
67
|
+
paddingBlock: 0,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const fieldClass = style({
|
|
71
|
+
display: "flex",
|
|
72
|
+
flexDirection: "column",
|
|
73
|
+
gap: spacing.value(2),
|
|
74
|
+
width: "100%",
|
|
75
|
+
minWidth: 0,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const errorClass = style(
|
|
79
|
+
text({ size: "xs", fontWeight: 400, color: "highContrast" }),
|
|
80
|
+
{
|
|
81
|
+
color: "light-dark(#ce2c31, #e5484d)",
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
|
|
64
85
|
type KeyboardHintProps = {
|
|
65
86
|
/** A single-character shortcut displayed at the inline end while unfocused. */
|
|
66
87
|
keyboardHint?: string
|
|
67
88
|
}
|
|
68
89
|
|
|
69
|
-
|
|
90
|
+
type BaseTextFieldProps = AriaTextFieldOptions<"input"> & KeyboardHintProps
|
|
91
|
+
export type TextFieldSize = "default" | "sm"
|
|
92
|
+
export type TextFieldProps = BaseTextFieldProps & {
|
|
93
|
+
/** `sm` matches `Button size="sm"` for dense rows such as tree items. */
|
|
94
|
+
size?: TextFieldSize
|
|
95
|
+
ref?: Ref<HTMLInputElement>
|
|
96
|
+
}
|
|
70
97
|
export type SearchFieldProps = AriaSearchFieldProps &
|
|
71
98
|
KeyboardHintProps & {
|
|
72
99
|
"aria-keyshortcuts"?: string
|
|
@@ -113,15 +140,37 @@ function KeyboardHint(props: { children: string }) {
|
|
|
113
140
|
}
|
|
114
141
|
|
|
115
142
|
export function TextField(props: TextFieldProps) {
|
|
116
|
-
const {
|
|
117
|
-
|
|
118
|
-
|
|
143
|
+
const {
|
|
144
|
+
keyboardHint,
|
|
145
|
+
size = "default",
|
|
146
|
+
ref: forwardedRef,
|
|
147
|
+
...fieldProps
|
|
148
|
+
} = props
|
|
149
|
+
const ref = useObjectRef(forwardedRef)
|
|
150
|
+
const {
|
|
151
|
+
inputProps,
|
|
152
|
+
errorMessageProps,
|
|
153
|
+
isInvalid,
|
|
154
|
+
validationErrors,
|
|
155
|
+
validationDetails,
|
|
156
|
+
} = useTextField(fieldProps, ref)
|
|
119
157
|
const { isFocused, focusProps } = useKeyboardHint()
|
|
120
158
|
const frameClassName = useStyles(inputFrameClass)
|
|
159
|
+
const fieldClassName = useStyles(fieldClass)
|
|
160
|
+
const errorClassName = useStyles(errorClass)
|
|
121
161
|
const className = useStyles(
|
|
122
162
|
inputClass,
|
|
163
|
+
size === "sm" && smallInputClass,
|
|
123
164
|
keyboardHint ? inputWithHintClass : undefined,
|
|
124
165
|
)
|
|
166
|
+
const errorMessage =
|
|
167
|
+
typeof fieldProps.errorMessage === "function"
|
|
168
|
+
? fieldProps.errorMessage({
|
|
169
|
+
isInvalid,
|
|
170
|
+
validationErrors,
|
|
171
|
+
validationDetails,
|
|
172
|
+
})
|
|
173
|
+
: fieldProps.errorMessage
|
|
125
174
|
const input = (
|
|
126
175
|
<input
|
|
127
176
|
className={className}
|
|
@@ -129,14 +178,29 @@ export function TextField(props: TextFieldProps) {
|
|
|
129
178
|
{...mergeProps(inputProps, focusProps)}
|
|
130
179
|
/>
|
|
131
180
|
)
|
|
132
|
-
|
|
133
|
-
if (!keyboardHint) return input
|
|
134
|
-
|
|
135
|
-
return (
|
|
181
|
+
const control = keyboardHint ? (
|
|
136
182
|
<div className={frameClassName}>
|
|
137
183
|
{input}
|
|
138
184
|
{!isFocused ? <KeyboardHint>{keyboardHint}</KeyboardHint> : null}
|
|
139
185
|
</div>
|
|
186
|
+
) : (
|
|
187
|
+
input
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const showError = isInvalid && errorMessage != null && errorMessage !== ""
|
|
191
|
+
|
|
192
|
+
// Keep one wrapper in every state so the input is not remounted, and does
|
|
193
|
+
// not lose focus, when the error appears or disappears while typing. Spans
|
|
194
|
+
// keep the field valid inside a <label>.
|
|
195
|
+
return (
|
|
196
|
+
<span className={fieldClassName}>
|
|
197
|
+
{control}
|
|
198
|
+
{showError ? (
|
|
199
|
+
<span {...errorMessageProps} className={errorClassName}>
|
|
200
|
+
{errorMessage}
|
|
201
|
+
</span>
|
|
202
|
+
) : null}
|
|
203
|
+
</span>
|
|
140
204
|
)
|
|
141
205
|
}
|
|
142
206
|
|
|
@@ -362,7 +426,7 @@ const quietInputClass = style(
|
|
|
362
426
|
},
|
|
363
427
|
)
|
|
364
428
|
|
|
365
|
-
export function QuietTextField(props:
|
|
429
|
+
export function QuietTextField(props: BaseTextFieldProps) {
|
|
366
430
|
const { keyboardHint, ...fieldProps } = props
|
|
367
431
|
const ref = useRef<HTMLInputElement>(null)
|
|
368
432
|
const { inputProps } = useTextField(fieldProps, ref)
|
package/src/maui.ts
CHANGED
|
@@ -41,6 +41,7 @@ export {
|
|
|
41
41
|
overlayMotion,
|
|
42
42
|
} from "./tokens/motion"
|
|
43
43
|
export {
|
|
44
|
+
controlSize,
|
|
44
45
|
iconSizeValues,
|
|
45
46
|
sizingTokens as sizing,
|
|
46
47
|
type IconSize,
|
|
@@ -82,6 +83,7 @@ export {
|
|
|
82
83
|
NumberField,
|
|
83
84
|
QuietTextField,
|
|
84
85
|
type TextFieldProps,
|
|
86
|
+
type TextFieldSize,
|
|
85
87
|
type SearchFieldProps,
|
|
86
88
|
} from "./components/Input"
|
|
87
89
|
export {
|
|
@@ -17,6 +17,7 @@ export function FormControlsPage() {
|
|
|
17
17
|
const [switchState, setSwitchState] = useState(false)
|
|
18
18
|
const [checkboxState, setCheckboxState] = useState(false)
|
|
19
19
|
const [textValue, setTextValue] = useState("")
|
|
20
|
+
const [smallTextValue, setSmallTextValue] = useState("notes/plan.md")
|
|
20
21
|
const [searchValue, setSearchValue] = useState("")
|
|
21
22
|
const [numberValue, setNumberValue] = useState(3)
|
|
22
23
|
const [radioValue, setRadioValue] = useState("one")
|
|
@@ -37,6 +38,17 @@ export function FormControlsPage() {
|
|
|
37
38
|
onChange={setTextValue}
|
|
38
39
|
/>
|
|
39
40
|
</Flex>
|
|
41
|
+
<Flex column gap={3} style={{ maxWidth: "240px" }}>
|
|
42
|
+
<Label htmlFor="small-text-field">Small text field with error</Label>
|
|
43
|
+
<TextField
|
|
44
|
+
id="small-text-field"
|
|
45
|
+
size="sm"
|
|
46
|
+
value={smallTextValue}
|
|
47
|
+
onChange={setSmallTextValue}
|
|
48
|
+
isInvalid={smallTextValue.includes("/")}
|
|
49
|
+
errorMessage="Use a name without slashes."
|
|
50
|
+
/>
|
|
51
|
+
</Flex>
|
|
40
52
|
<Flex column gap={3} style={{ maxWidth: "240px" }}>
|
|
41
53
|
<Label htmlFor="search-field">Search field</Label>
|
|
42
54
|
<SearchField
|