@microbit/ui 0.1.0-alpha.17 → 0.1.0-alpha.19
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/LICENSE.md +8 -0
- package/README.md +34 -11
- package/lang/ui.ca.json +36 -0
- package/lang/ui.cy.json +36 -0
- package/lang/ui.de.json +36 -0
- package/lang/ui.en-us.json +36 -0
- package/lang/ui.en.json +28 -0
- package/lang/ui.es-es.json +36 -0
- package/lang/ui.fr.json +36 -0
- package/lang/ui.ga-ie.json +36 -0
- package/lang/ui.it.json +36 -0
- package/lang/ui.ja.json +36 -0
- package/lang/ui.ko.json +36 -0
- package/lang/ui.lol.json +36 -0
- package/lang/ui.nl.json +36 -0
- package/lang/ui.pl.json +36 -0
- package/lang/ui.pt-br.json +36 -0
- package/lang/ui.zh-cn.json +36 -0
- package/lang/ui.zh-tw.json +36 -0
- package/package.json +1 -1
- package/src/Avatar.recipe.ts +3 -1
- package/src/Checkbox.recipe.ts +5 -0
- package/src/Checkbox.tsx +42 -2
- package/src/CheckboxGroup.tsx +6 -10
- package/src/ComboBox.tsx +38 -16
- package/src/Field.recipe.ts +113 -0
- package/src/Field.tsx +189 -0
- package/src/Input.recipe.ts +4 -2
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +25 -7
- package/src/NumberField.tsx +52 -22
- package/src/Radio.recipe.ts +4 -0
- package/src/Radio.tsx +1 -63
- package/src/RadioGroup.tsx +69 -0
- package/src/Select.recipe.ts +27 -17
- package/src/Select.tsx +38 -15
- package/src/Skeleton.tsx +5 -3
- package/src/Switch.recipe.ts +19 -0
- package/src/Switch.tsx +34 -3
- package/src/TextField.tsx +19 -8
- package/src/Toast.tsx +17 -2
- package/src/base-preset.ts +45 -10
- package/src/index.ts +3 -1
- package/src/FieldSupport.tsx +0 -68
- package/src/TextField.recipe.ts +0 -58
package/src/Select.recipe.ts
CHANGED
|
@@ -23,13 +23,16 @@ const transitionCommon =
|
|
|
23
23
|
* Apps restyle it through the `variant` group — classroom's `classroom`
|
|
24
24
|
* variant is the rounded pill its join form uses.
|
|
25
25
|
*
|
|
26
|
+
* The label is not a slot here: it comes from the `field` recipe, as every
|
|
27
|
+
* other labelled field's does. The only thing that costs is a per-`variant`
|
|
28
|
+
* label style — restyle the control and let the label match the family.
|
|
29
|
+
*
|
|
26
30
|
* Registered in the base preset (base-preset.ts).
|
|
27
31
|
*/
|
|
28
32
|
export const select = defineSlotRecipe({
|
|
29
33
|
className: "select",
|
|
30
34
|
slots: [
|
|
31
35
|
"root",
|
|
32
|
-
"label",
|
|
33
36
|
"trigger",
|
|
34
37
|
"value",
|
|
35
38
|
"indicator",
|
|
@@ -40,17 +43,11 @@ export const select = defineSlotRecipe({
|
|
|
40
43
|
"empty",
|
|
41
44
|
],
|
|
42
45
|
base: {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
label: {
|
|
49
|
-
fontSize: "md",
|
|
50
|
-
fontWeight: "medium",
|
|
51
|
-
marginEnd: "3",
|
|
52
|
-
mb: "2",
|
|
53
|
-
},
|
|
46
|
+
// No layout here: the component wears the `field` recipe's root alongside
|
|
47
|
+
// this slot, and that recipe is the single owner of field-root layout
|
|
48
|
+
// (else `labelPosition` would fight this slot over `flexDirection`). The
|
|
49
|
+
// slot stays for apps and variants to target.
|
|
50
|
+
root: {},
|
|
54
51
|
trigger: {
|
|
55
52
|
display: "flex",
|
|
56
53
|
alignItems: "center",
|
|
@@ -66,16 +63,16 @@ export const select = defineSlotRecipe({
|
|
|
66
63
|
transitionProperty: transitionCommon,
|
|
67
64
|
transitionDuration: "normal",
|
|
68
65
|
border: "1px solid",
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
// As the input recipe: the ramp's accessible outline stop, with hover
|
|
67
|
+
// stepping darker. The dropdown card below keeps the light gray.200 —
|
|
68
|
+
// it's a surface edge, not a form-control boundary.
|
|
69
|
+
borderColor: "gray.400",
|
|
71
70
|
bg: "white",
|
|
72
71
|
color: "inherit",
|
|
73
|
-
h: "10",
|
|
74
|
-
px: "4",
|
|
75
72
|
// As the input recipe, so a Select, a NativeSelect and a TextField in one
|
|
76
73
|
// form all tint together on hover. (react-aria's TextField has no hover
|
|
77
74
|
// effect, but matching the family beats matching their docs.)
|
|
78
|
-
_hover: { borderColor: "gray.
|
|
75
|
+
_hover: { borderColor: "gray.500" },
|
|
79
76
|
// `data-invalid` lands on the root — and, in a ComboBox, on the input —
|
|
80
77
|
// but never on the trigger: a RAC Button has no validity state, and our
|
|
81
78
|
// ComboBox control is a plain div. So it comes down from the parent.
|
|
@@ -207,4 +204,17 @@ export const select = defineSlotRecipe({
|
|
|
207
204
|
color: "gray.600",
|
|
208
205
|
},
|
|
209
206
|
},
|
|
207
|
+
variants: {
|
|
208
|
+
// The `input` recipe's size ladder, step for step, so a Select sits level
|
|
209
|
+
// with a TextField beside it at every size. The indicator is em-sized and
|
|
210
|
+
// scales free; the dropdown card keeps one density across sizes.
|
|
211
|
+
size: {
|
|
212
|
+
lg: { trigger: { fontSize: "lg", px: "4", h: "12", borderRadius: "md" } },
|
|
213
|
+
md: { trigger: { fontSize: "md", px: "4", h: "10", borderRadius: "md" } },
|
|
214
|
+
sm: { trigger: { fontSize: "sm", px: "3", h: "8", borderRadius: "sm" } },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
defaultVariants: {
|
|
218
|
+
size: "md",
|
|
219
|
+
},
|
|
210
220
|
});
|
package/src/Select.tsx
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { createContext, ReactNode, useContext } from "react";
|
|
7
7
|
import {
|
|
8
8
|
Button as RACButton,
|
|
9
|
-
Label as RACLabel,
|
|
10
9
|
ListBox as RACListBox,
|
|
11
10
|
ListBoxItem as RACListBoxItem,
|
|
12
11
|
ListBoxItemProps as RACListBoxItemProps,
|
|
@@ -16,16 +15,19 @@ import {
|
|
|
16
15
|
SelectProps as RACSelectProps,
|
|
17
16
|
SelectValue,
|
|
18
17
|
} from "react-aria-components";
|
|
18
|
+
import { useIntl } from "react-intl";
|
|
19
19
|
import { RiArrowDownSLine } from "react-icons/ri";
|
|
20
20
|
import { css, cx } from "styled-system/css";
|
|
21
|
-
import { select, SelectVariantProps } from "styled-system/recipes";
|
|
21
|
+
import { field, select, SelectVariantProps } from "styled-system/recipes";
|
|
22
22
|
import { SystemStyleObject } from "styled-system/types";
|
|
23
23
|
import {
|
|
24
|
-
|
|
24
|
+
FieldLabel,
|
|
25
|
+
FieldLayoutProps,
|
|
25
26
|
FieldSupport,
|
|
26
27
|
FieldSupportProps,
|
|
27
|
-
} from "./
|
|
28
|
+
} from "./Field";
|
|
28
29
|
import { Icon } from "./Icon";
|
|
30
|
+
import { uiMessage } from "./messages";
|
|
29
31
|
|
|
30
32
|
export type SelectSlots = ReturnType<typeof select>;
|
|
31
33
|
|
|
@@ -43,10 +45,16 @@ export interface SelectProps<T extends object>
|
|
|
43
45
|
"className" | "children" | "style" | "placeholder"
|
|
44
46
|
>,
|
|
45
47
|
SelectVariantProps,
|
|
46
|
-
FieldSupportProps
|
|
48
|
+
FieldSupportProps,
|
|
49
|
+
FieldLayoutProps {
|
|
47
50
|
/** Visible label. Use `aria-label` instead where the design has none. */
|
|
48
51
|
label?: ReactNode;
|
|
49
|
-
/**
|
|
52
|
+
/** Label style overrides. */
|
|
53
|
+
labelCss?: SystemStyleObject;
|
|
54
|
+
/**
|
|
55
|
+
* Shown in the trigger while nothing is chosen (Chakra's placeholder).
|
|
56
|
+
* Defaults to a translated "Select an item".
|
|
57
|
+
*/
|
|
50
58
|
placeholder?: string;
|
|
51
59
|
/** `SelectOption`s. */
|
|
52
60
|
children: ReactNode;
|
|
@@ -65,8 +73,8 @@ export interface SelectProps<T extends object>
|
|
|
65
73
|
* while positioning, which beats any class.
|
|
66
74
|
*/
|
|
67
75
|
maxHeight?: number;
|
|
68
|
-
/** Per-instance overrides for the trigger. */
|
|
69
|
-
|
|
76
|
+
/** Per-instance overrides for the trigger (the button the value sits in). */
|
|
77
|
+
triggerCss?: SystemStyleObject;
|
|
70
78
|
/** Per-instance overrides for the dropdown card. */
|
|
71
79
|
contentCss?: SystemStyleObject;
|
|
72
80
|
className?: string;
|
|
@@ -79,6 +87,7 @@ export interface SelectProps<T extends object>
|
|
|
79
87
|
*/
|
|
80
88
|
export const Select = <T extends object>({
|
|
81
89
|
label,
|
|
90
|
+
labelCss,
|
|
82
91
|
placeholder,
|
|
83
92
|
children,
|
|
84
93
|
indicator,
|
|
@@ -87,7 +96,8 @@ export const Select = <T extends object>({
|
|
|
87
96
|
helperText,
|
|
88
97
|
errorMessage,
|
|
89
98
|
helperTextCss,
|
|
90
|
-
|
|
99
|
+
labelPosition,
|
|
100
|
+
triggerCss,
|
|
91
101
|
contentCss,
|
|
92
102
|
className,
|
|
93
103
|
...props
|
|
@@ -95,25 +105,37 @@ export const Select = <T extends object>({
|
|
|
95
105
|
// splitVariantProps, not a hand-picked list: an app preset can add variant
|
|
96
106
|
// groups to the recipe and they have to reach it (playbook gotcha #37).
|
|
97
107
|
const [variantProps, rest] = select.splitVariantProps(props);
|
|
108
|
+
const intl = useIntl();
|
|
98
109
|
const slots = select(variantProps);
|
|
110
|
+
const fieldSlots = field({ size: variantProps.size, labelPosition });
|
|
99
111
|
return (
|
|
100
112
|
<SelectSlotProvider value={slots}>
|
|
101
113
|
<RACSelect
|
|
102
114
|
{...(rest as RACSelectProps<T>)}
|
|
103
|
-
className={cx(slots.root, className)}
|
|
115
|
+
className={cx(fieldSlots.root, slots.root, className)}
|
|
104
116
|
>
|
|
105
117
|
{label != null && (
|
|
106
|
-
<
|
|
118
|
+
<FieldLabel
|
|
119
|
+
size={variantProps.size}
|
|
120
|
+
labelPosition={labelPosition}
|
|
121
|
+
isRequired={props.isRequired}
|
|
122
|
+
css={labelCss}
|
|
123
|
+
>
|
|
107
124
|
{label}
|
|
108
|
-
|
|
109
|
-
</RACLabel>
|
|
125
|
+
</FieldLabel>
|
|
110
126
|
)}
|
|
111
127
|
<RACButton
|
|
112
|
-
className={cx(
|
|
128
|
+
className={cx(
|
|
129
|
+
slots.trigger,
|
|
130
|
+
triggerCss ? css(triggerCss) : undefined,
|
|
131
|
+
)}
|
|
113
132
|
>
|
|
114
133
|
<SelectValue className={slots.value}>
|
|
115
134
|
{({ isPlaceholder, defaultChildren }) =>
|
|
116
|
-
isPlaceholder
|
|
135
|
+
isPlaceholder
|
|
136
|
+
? placeholder ??
|
|
137
|
+
intl.formatMessage(uiMessage("ui.select-placeholder"))
|
|
138
|
+
: defaultChildren
|
|
117
139
|
}
|
|
118
140
|
</SelectValue>
|
|
119
141
|
{indicator !== null && (
|
|
@@ -136,6 +158,7 @@ export const Select = <T extends object>({
|
|
|
136
158
|
helperText={helperText}
|
|
137
159
|
errorMessage={errorMessage}
|
|
138
160
|
helperTextCss={helperTextCss}
|
|
161
|
+
labelPosition={labelPosition}
|
|
139
162
|
/>
|
|
140
163
|
</RACSelect>
|
|
141
164
|
</SelectSlotProvider>
|
package/src/Skeleton.tsx
CHANGED
|
@@ -12,12 +12,14 @@ import { SystemStyleObject } from "styled-system/types";
|
|
|
12
12
|
* caller's `css` is merged into one `css()` call and its overrides win
|
|
13
13
|
* (playbook gotcha #8).
|
|
14
14
|
*
|
|
15
|
-
* The colours
|
|
16
|
-
* call site can retint one skeleton without knowing how the animation
|
|
15
|
+
* The colours flow through the same pair of custom properties Chakra used,
|
|
16
|
+
* so a call site can retint one skeleton without knowing how the animation
|
|
17
|
+
* works.
|
|
17
18
|
*/
|
|
18
19
|
const skeletonBase: SystemStyleObject = {
|
|
19
20
|
"--skeleton-start-color": "token(colors.gray.100)",
|
|
20
|
-
|
|
21
|
+
// gray.350, the decorative-fill stop, as the Avatar default.
|
|
22
|
+
"--skeleton-end-color": "token(colors.gray.350)",
|
|
21
23
|
background: "var(--skeleton-start-color)",
|
|
22
24
|
borderColor: "var(--skeleton-end-color)",
|
|
23
25
|
opacity: 0.7,
|
package/src/Switch.recipe.ts
CHANGED
|
@@ -72,6 +72,24 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
74
|
variants: {
|
|
75
|
+
// `start` puts the label first and the control at the row's end — the
|
|
76
|
+
// settings-row pattern (a preference name beside its switch), the toggle
|
|
77
|
+
// counterpart of the field recipe's `labelPosition="side"`. Values are
|
|
78
|
+
// start/end rather than top/side because the default label is already
|
|
79
|
+
// beside the control, after it. The label keeps an end margin so a long
|
|
80
|
+
// translation can't butt against the track (the SelectFormControl
|
|
81
|
+
// lesson — see the field-chrome roadmap bullet).
|
|
82
|
+
labelPosition: {
|
|
83
|
+
end: {},
|
|
84
|
+
start: {
|
|
85
|
+
root: {
|
|
86
|
+
flexDirection: "row-reverse",
|
|
87
|
+
justifyContent: "space-between",
|
|
88
|
+
width: "100%",
|
|
89
|
+
},
|
|
90
|
+
label: { marginStart: "0", marginEnd: "3" },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
75
93
|
// Chakra's Switch size scale (track width x height; the selected-thumb
|
|
76
94
|
// translate is the difference between them).
|
|
77
95
|
size: {
|
|
@@ -103,5 +121,6 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
103
121
|
},
|
|
104
122
|
defaultVariants: {
|
|
105
123
|
size: "md",
|
|
124
|
+
labelPosition: "end",
|
|
106
125
|
},
|
|
107
126
|
});
|
package/src/Switch.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
-
import { ReactNode } from "react";
|
|
6
|
+
import { ReactNode, useId } from "react";
|
|
7
7
|
import {
|
|
8
8
|
Switch as RACSwitch,
|
|
9
9
|
SwitchProps as RACSwitchProps,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { css, cx } from "styled-system/css";
|
|
12
12
|
import { switchRecipe, SwitchRecipeVariantProps } from "styled-system/recipes";
|
|
13
13
|
import { SystemStyleObject } from "styled-system/types";
|
|
14
|
+
import { FieldHelperText } from "./Field";
|
|
14
15
|
|
|
15
16
|
export interface SwitchProps
|
|
16
17
|
extends Omit<RACSwitchProps, "className" | "children" | "style">,
|
|
@@ -19,24 +20,43 @@ export interface SwitchProps
|
|
|
19
20
|
css?: SystemStyleObject;
|
|
20
21
|
className?: string;
|
|
21
22
|
children?: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Help text below the switch, wired to its `aria-describedby` — the same
|
|
25
|
+
* chrome the labelled fields' `helperText` renders. With it the component
|
|
26
|
+
* gains a wrapping `<div>`, so the switch-plus-text moves as one block.
|
|
27
|
+
*/
|
|
28
|
+
helperText?: ReactNode;
|
|
29
|
+
/** Per-instance style overrides for the helper text. */
|
|
30
|
+
helperTextCss?: SystemStyleObject;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
34
|
* Switch — react-aria-components <Switch> styled like Chakra's switch.
|
|
26
35
|
* Children render as the label; pass `aria-label` for label-less switches.
|
|
36
|
+
* `labelPosition="start"` is the settings-row layout: label first, switch at
|
|
37
|
+
* the row's end.
|
|
27
38
|
*/
|
|
28
39
|
export const Switch = ({
|
|
29
40
|
size,
|
|
41
|
+
labelPosition,
|
|
30
42
|
css: cssProp,
|
|
31
43
|
className,
|
|
32
44
|
children,
|
|
45
|
+
helperText,
|
|
46
|
+
helperTextCss,
|
|
33
47
|
...rest
|
|
34
48
|
}: SwitchProps) => {
|
|
35
|
-
const slots = switchRecipe({ size });
|
|
36
|
-
|
|
49
|
+
const slots = switchRecipe({ size, labelPosition });
|
|
50
|
+
const helperId = useId();
|
|
51
|
+
const describedBy =
|
|
52
|
+
[rest["aria-describedby"], helperText != null ? helperId : undefined]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(" ") || undefined;
|
|
55
|
+
const switchElement = (
|
|
37
56
|
<RACSwitch
|
|
38
57
|
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
39
58
|
{...rest}
|
|
59
|
+
aria-describedby={describedBy}
|
|
40
60
|
>
|
|
41
61
|
{({ isSelected, isFocusVisible, isDisabled }) => {
|
|
42
62
|
const state = {
|
|
@@ -59,4 +79,15 @@ export const Switch = ({
|
|
|
59
79
|
}}
|
|
60
80
|
</RACSwitch>
|
|
61
81
|
);
|
|
82
|
+
if (helperText == null) {
|
|
83
|
+
return switchElement;
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
<div>
|
|
87
|
+
{switchElement}
|
|
88
|
+
<FieldHelperText id={helperId} css={helperTextCss}>
|
|
89
|
+
{helperText}
|
|
90
|
+
</FieldHelperText>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
62
93
|
};
|
package/src/TextField.tsx
CHANGED
|
@@ -6,16 +6,17 @@
|
|
|
6
6
|
import { FocusEvent, forwardRef, ReactNode } from "react";
|
|
7
7
|
import {
|
|
8
8
|
Input as RACInput,
|
|
9
|
-
Label as RACLabel,
|
|
10
9
|
TextField as RACTextField,
|
|
11
10
|
TextFieldProps as RACTextFieldProps,
|
|
12
11
|
} from "react-aria-components";
|
|
13
12
|
import { field, input, InputVariantProps } from "styled-system/recipes";
|
|
13
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
FieldLabel,
|
|
16
|
+
FieldLayoutProps,
|
|
16
17
|
FieldSupport,
|
|
17
18
|
FieldSupportProps,
|
|
18
|
-
} from "./
|
|
19
|
+
} from "./Field";
|
|
19
20
|
|
|
20
21
|
export interface TextFieldProps
|
|
21
22
|
extends Omit<
|
|
@@ -23,9 +24,12 @@ export interface TextFieldProps
|
|
|
23
24
|
"className" | "children" | "style" | "onFocus" | "onBlur"
|
|
24
25
|
>,
|
|
25
26
|
InputVariantProps,
|
|
26
|
-
FieldSupportProps
|
|
27
|
+
FieldSupportProps,
|
|
28
|
+
FieldLayoutProps {
|
|
27
29
|
/** Visible label (Chakra's FormLabel; asterisk added when `isRequired`). */
|
|
28
30
|
label: ReactNode;
|
|
31
|
+
/** Label style overrides. */
|
|
32
|
+
labelCss?: SystemStyleObject;
|
|
29
33
|
onFocus?: (e: FocusEvent<HTMLInputElement>) => void;
|
|
30
34
|
/** Input autocapitalize attribute (react-aria's TextField omits it). */
|
|
31
35
|
autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters";
|
|
@@ -40,9 +44,11 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
40
44
|
function TextField(
|
|
41
45
|
{
|
|
42
46
|
label,
|
|
47
|
+
labelCss,
|
|
43
48
|
helperText,
|
|
44
49
|
errorMessage,
|
|
45
50
|
helperTextCss,
|
|
51
|
+
labelPosition,
|
|
46
52
|
onFocus,
|
|
47
53
|
autoCapitalize,
|
|
48
54
|
...props
|
|
@@ -52,13 +58,17 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
52
58
|
// As Input: forward every recipe variant group, not just `size`, so a
|
|
53
59
|
// preset that adds one keeps working.
|
|
54
60
|
const [variantProps, rest] = input.splitVariantProps(props);
|
|
55
|
-
const slots = field();
|
|
61
|
+
const slots = field({ size: variantProps.size, labelPosition });
|
|
56
62
|
return (
|
|
57
63
|
<RACTextField {...rest} className={slots.root}>
|
|
58
|
-
<
|
|
64
|
+
<FieldLabel
|
|
65
|
+
size={variantProps.size}
|
|
66
|
+
labelPosition={labelPosition}
|
|
67
|
+
isRequired={rest.isRequired}
|
|
68
|
+
css={labelCss}
|
|
69
|
+
>
|
|
59
70
|
{label}
|
|
60
|
-
|
|
61
|
-
</RACLabel>
|
|
71
|
+
</FieldLabel>
|
|
62
72
|
<RACInput
|
|
63
73
|
ref={ref}
|
|
64
74
|
className={input(variantProps)}
|
|
@@ -69,6 +79,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
69
79
|
helperText={helperText}
|
|
70
80
|
errorMessage={errorMessage}
|
|
71
81
|
helperTextCss={helperTextCss}
|
|
82
|
+
labelPosition={labelPosition}
|
|
72
83
|
/>
|
|
73
84
|
</RACTextField>
|
|
74
85
|
);
|
package/src/Toast.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
-
import { ReactNode, useMemo } from "react";
|
|
6
|
+
import { ReactNode, useMemo, useSyncExternalStore } from "react";
|
|
7
7
|
import {
|
|
8
8
|
Button as RACButton,
|
|
9
9
|
UNSTABLE_Toast as RACToast,
|
|
@@ -55,6 +55,10 @@ const statusIcon: Record<ToastStatus, IconType> = {
|
|
|
55
55
|
error: RiErrorWarningFill,
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
// Module scope so useSyncExternalStore doesn't resubscribe every render.
|
|
59
|
+
const subscribeToQueue = (fn: () => void) => toastQueue.subscribe(fn);
|
|
60
|
+
const getVisibleCount = () => toastQueue.visibleToasts.length;
|
|
61
|
+
|
|
58
62
|
/**
|
|
59
63
|
* Mount once near the app root, inside the IntlProvider (the close button's
|
|
60
64
|
* label and the status announcements are react-intl messages).
|
|
@@ -63,8 +67,19 @@ const statusIcon: Record<ToastStatus, IconType> = {
|
|
|
63
67
|
export const ToastProvider = () => {
|
|
64
68
|
const intl = useIntl();
|
|
65
69
|
const slots = toastRecipe();
|
|
70
|
+
// The region's landmark label counts the visible toasts, so it has to
|
|
71
|
+
// track the queue.
|
|
72
|
+
const count = useSyncExternalStore(
|
|
73
|
+
subscribeToQueue,
|
|
74
|
+
getVisibleCount,
|
|
75
|
+
getVisibleCount,
|
|
76
|
+
);
|
|
66
77
|
return (
|
|
67
|
-
<RACToastRegion
|
|
78
|
+
<RACToastRegion
|
|
79
|
+
queue={toastQueue}
|
|
80
|
+
aria-label={intl.formatMessage(uiMessage("ui.toast-region"), { count })}
|
|
81
|
+
className={slots.region}
|
|
82
|
+
>
|
|
68
83
|
{({ toast }) => {
|
|
69
84
|
const status = toast.content.status ?? "info";
|
|
70
85
|
return (
|
package/src/base-preset.ts
CHANGED
|
@@ -42,9 +42,46 @@ import { switchRecipe } from "./Switch.recipe";
|
|
|
42
42
|
import { dialog } from "./Modal.recipe";
|
|
43
43
|
import { text } from "./Text.recipe";
|
|
44
44
|
import { tooltip } from "./Tooltip.recipe";
|
|
45
|
-
import { field } from "./
|
|
45
|
+
import { field } from "./Field.recipe";
|
|
46
46
|
import { toast } from "./Toast.recipe";
|
|
47
47
|
|
|
48
|
+
// The family gray ramp: pure neutrals as the library default. The two
|
|
49
|
+
// halves have different jobs and different override rules:
|
|
50
|
+
//
|
|
51
|
+
// - 10–300 are surface stops (panels, page backdrops, hairlines, subtle
|
|
52
|
+
// fills). Presets may freely override these values — pinning an app's
|
|
53
|
+
// existing surfaces or applying a brand tint — because a few bits of
|
|
54
|
+
// lightness here reads across a whole viewport.
|
|
55
|
+
// - 350 is the decorative/state fill stop (~2.1:1): avatar discs, skeleton
|
|
56
|
+
// pulse, pressed fills. Never text or boundaries.
|
|
57
|
+
// - 400–900 are ink stops (outlines, placeholders, text) with a contrast
|
|
58
|
+
// contract on white: 400 ≥ 3:1, the accessible form-outline stop
|
|
59
|
+
// (WCAG 1.4.11); 500 ≥ 4.5:1, text-safe secondary (placeholders, muted
|
|
60
|
+
// icons). Presets may re-tint these only luminance-matched — the
|
|
61
|
+
// contrast figures are the contract, hue is free.
|
|
62
|
+
//
|
|
63
|
+
// Override values, never names: raw var(--colors-gray-*) references and
|
|
64
|
+
// paired private presets depend on the names, so a rename is a breaking
|
|
65
|
+
// change to both and needs every app and paired preset moved in lockstep
|
|
66
|
+
// (as was done when Chakra's misnamed darker-than-50 stop `25` became
|
|
67
|
+
// `75`). And never override partially in a way that lets a stop fall
|
|
68
|
+
// through to a different grey system.
|
|
69
|
+
const gray = {
|
|
70
|
+
10: { value: "#fcfcfc" },
|
|
71
|
+
50: { value: "#f9f9f9" },
|
|
72
|
+
75: { value: "#f5f5f5" },
|
|
73
|
+
100: { value: "#f1f1f1" },
|
|
74
|
+
200: { value: "#e7e7e7" },
|
|
75
|
+
300: { value: "#d4d4d4" },
|
|
76
|
+
350: { value: "#b4b4b4" }, // ~2.1:1 — decorative fills only
|
|
77
|
+
400: { value: "#949494" }, // 3.05:1 — accessible outline stop
|
|
78
|
+
500: { value: "#767676" }, // 4.54:1 — text-safe secondary
|
|
79
|
+
600: { value: "#575757" },
|
|
80
|
+
700: { value: "#404040" },
|
|
81
|
+
800: { value: "#262626" },
|
|
82
|
+
900: { value: "#1a1a1a" },
|
|
83
|
+
};
|
|
84
|
+
|
|
48
85
|
/**
|
|
49
86
|
* The base preset: the complete, working micro:bit design system. The base
|
|
50
87
|
* token scales (base-tokens.ts), the micro:bit house style
|
|
@@ -92,16 +129,12 @@ export const basePreset = definePreset({
|
|
|
92
129
|
tokens: {
|
|
93
130
|
colors: {
|
|
94
131
|
...colors,
|
|
95
|
-
gray
|
|
96
|
-
...colors.gray,
|
|
97
|
-
// Very light grays the family's designs use below Chakra's 50.
|
|
98
|
-
10: { value: "#fcfcfc" },
|
|
99
|
-
25: { value: "#f5f5f5" },
|
|
100
|
-
},
|
|
132
|
+
gray,
|
|
101
133
|
// OSS default brand ramps (see the brand contract above). `brand`
|
|
102
|
-
// aliases Chakra blue; `brand2` Chakra's
|
|
103
|
-
//
|
|
104
|
-
//
|
|
134
|
+
// aliases Chakra blue; `brand2` stays Chakra's slate gray — a frozen
|
|
135
|
+
// legacy alias, deliberately decoupled from the neutral `gray` above
|
|
136
|
+
// so ml-trainer's OSS look and `statusBarBg`'s default don't move.
|
|
137
|
+
// Removing the slot is a follow-up needing an ml-trainer lockstep.
|
|
105
138
|
brand: colors.blue,
|
|
106
139
|
brand2: colors.gray,
|
|
107
140
|
},
|
|
@@ -304,9 +337,11 @@ export const basePreset = definePreset({
|
|
|
304
337
|
// as a runtime prop, so generate the breakpoint-prefixed variants too.
|
|
305
338
|
dialog: [{ size: ["*"], responsive: true }, { centered: ["*"] }],
|
|
306
339
|
drawer: ["*"],
|
|
340
|
+
field: ["*"],
|
|
307
341
|
gridList: ["*"],
|
|
308
342
|
listBox: ["*"],
|
|
309
343
|
input: ["*"],
|
|
344
|
+
numberField: ["*"],
|
|
310
345
|
radio: ["*"],
|
|
311
346
|
select: ["*"],
|
|
312
347
|
switchRecipe: ["*"],
|
package/src/index.ts
CHANGED
|
@@ -17,16 +17,18 @@ export * from "./ButtonGroup";
|
|
|
17
17
|
export * from "./Card";
|
|
18
18
|
export * from "./Checkbox";
|
|
19
19
|
export * from "./CheckboxGroup";
|
|
20
|
-
export * from "./
|
|
20
|
+
export * from "./Field";
|
|
21
21
|
export * from "./IconButton";
|
|
22
22
|
export * from "./Image";
|
|
23
23
|
export * from "./Input";
|
|
24
24
|
export * from "./InputGroup";
|
|
25
25
|
export * from "./LinkBox";
|
|
26
26
|
export * from "./NativeSelect";
|
|
27
|
+
export * from "./NativeSelectField";
|
|
27
28
|
export * from "./NumberField";
|
|
28
29
|
export * from "./ProgressBar";
|
|
29
30
|
export * from "./Radio";
|
|
31
|
+
export * from "./RadioGroup";
|
|
30
32
|
export * from "./Skeleton";
|
|
31
33
|
export * from "./Slide";
|
|
32
34
|
export * from "./Slider";
|
package/src/FieldSupport.tsx
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
-
*
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
*/
|
|
6
|
-
import { ReactNode } from "react";
|
|
7
|
-
import { FieldError, Text as RACText } from "react-aria-components";
|
|
8
|
-
import { css, cx } from "styled-system/css";
|
|
9
|
-
import { field } from "styled-system/recipes";
|
|
10
|
-
import { SystemStyleObject } from "styled-system/types";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The label/helper/error chrome every labelled form field shares — Chakra's
|
|
14
|
-
* FormControl parts, generalised out of TextField so Select, ComboBox,
|
|
15
|
-
* NumberField, RadioGroup and CheckboxGroup carry the same props
|
|
16
|
-
* (data-microbit-org's forms attach helper and error text to all of these).
|
|
17
|
-
*/
|
|
18
|
-
export interface FieldSupportProps {
|
|
19
|
-
/** Help text below the field (Chakra's FormHelperText). */
|
|
20
|
-
helperText?: ReactNode;
|
|
21
|
-
/** Shown below the field when invalid (Chakra's FormErrorMessage). */
|
|
22
|
-
errorMessage?: ReactNode;
|
|
23
|
-
/** Per-instance style overrides for the helper text. */
|
|
24
|
-
helperTextCss?: SystemStyleObject;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Helper text and error message for a react-aria field container. Render
|
|
29
|
-
* inside any RAC component with field validation context (TextField, Select,
|
|
30
|
-
* ComboBox, NumberField, RadioGroup, CheckboxGroup) — react-aria wires the
|
|
31
|
-
* description to the input's aria-describedby, and the error renders only
|
|
32
|
-
* while the field is invalid. Also exported for app-side composites built on
|
|
33
|
-
* RAC containers.
|
|
34
|
-
*/
|
|
35
|
-
export const FieldSupport = ({
|
|
36
|
-
helperText,
|
|
37
|
-
errorMessage,
|
|
38
|
-
helperTextCss,
|
|
39
|
-
}: FieldSupportProps) => {
|
|
40
|
-
const slots = field();
|
|
41
|
-
return (
|
|
42
|
-
<>
|
|
43
|
-
{helperText != null && (
|
|
44
|
-
<RACText
|
|
45
|
-
slot="description"
|
|
46
|
-
className={cx(
|
|
47
|
-
slots.helperText,
|
|
48
|
-
helperTextCss ? css(helperTextCss) : undefined,
|
|
49
|
-
)}
|
|
50
|
-
>
|
|
51
|
-
{helperText}
|
|
52
|
-
</RACText>
|
|
53
|
-
)}
|
|
54
|
-
<FieldError className={slots.errorMessage}>{errorMessage}</FieldError>
|
|
55
|
-
</>
|
|
56
|
-
);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The required-field asterisk (Chakra's FormLabel indicator). Render inside
|
|
61
|
-
* the field's label when `isRequired`; aria-hidden because react-aria already
|
|
62
|
-
* announces requiredness from the input itself.
|
|
63
|
-
*/
|
|
64
|
-
export const FieldRequiredIndicator = () => (
|
|
65
|
-
<span aria-hidden className={field().requiredIndicator}>
|
|
66
|
-
*
|
|
67
|
-
</span>
|
|
68
|
-
);
|