@microbit/ui 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +40 -0
- package/README.md +279 -26
- package/lang/ui.ar.json +62 -0
- package/lang/ui.ca.json +43 -3
- package/lang/ui.cy.json +62 -0
- package/lang/ui.de.json +62 -0
- package/lang/ui.en-us.json +40 -0
- package/lang/ui.en.json +40 -0
- package/lang/ui.es-es.json +43 -3
- package/lang/ui.fr.json +44 -4
- package/lang/ui.ga-ie.json +62 -0
- package/lang/ui.it.json +62 -0
- package/lang/ui.ja.json +44 -4
- package/lang/ui.ko.json +44 -4
- package/lang/ui.lo.json +62 -0
- package/lang/ui.lol.json +40 -0
- package/lang/ui.nl.json +44 -4
- package/lang/ui.pl.json +44 -4
- package/lang/ui.pt-br.json +44 -4
- package/lang/ui.vi.json +62 -0
- package/lang/ui.zh-cn.json +62 -0
- package/lang/ui.zh-tw.json +44 -4
- package/package.json +19 -10
- package/postcss-legacy-safari.cjs +96 -0
- package/reset.css +35 -0
- package/src/Avatar.recipe.ts +191 -0
- package/src/Avatar.tsx +278 -0
- package/src/Breadcrumb.recipe.ts +45 -0
- package/src/Breadcrumb.tsx +114 -0
- package/src/Button.recipe.ts +88 -50
- package/src/Button.tsx +68 -30
- package/src/ButtonGroup.tsx +37 -15
- package/src/Card.recipe.ts +1 -2
- package/src/Card.tsx +2 -1
- package/src/Checkbox.recipe.ts +49 -14
- package/src/Checkbox.tsx +111 -32
- package/src/CheckboxGroup.tsx +70 -0
- package/src/CloseButton.tsx +3 -3
- package/src/CloseIcon.tsx +6 -4
- package/src/Code.tsx +20 -0
- package/src/Collapse.tsx +179 -0
- package/src/ComboBox.tsx +246 -0
- package/src/Divider.tsx +72 -8
- package/src/Drawer.recipe.ts +21 -10
- package/src/Drawer.tsx +3 -4
- package/src/ExternalLink.tsx +43 -0
- package/src/Fade.tsx +62 -0
- package/src/Field.recipe.ts +114 -0
- package/src/Field.tsx +187 -0
- package/src/GridList.recipe.ts +57 -0
- package/src/GridList.tsx +81 -0
- package/src/Heading.recipe.ts +20 -1
- package/src/Heading.tsx +3 -3
- package/src/Icon.tsx +23 -7
- package/src/IconButton.tsx +8 -13
- package/src/Image.tsx +1 -1
- package/src/Input.recipe.ts +41 -28
- package/src/Input.tsx +23 -7
- package/src/InputGroup.tsx +26 -12
- package/src/Kbd.tsx +26 -0
- package/src/Link.tsx +3 -3
- package/src/LinkBox.tsx +2 -3
- package/src/LinkButton.tsx +81 -0
- package/src/List.tsx +8 -6
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +51 -17
- package/src/Menu.tsx +117 -2
- package/src/Modal.recipe.ts +17 -9
- package/src/Modal.tsx +137 -24
- package/src/MoreMenuButton.tsx +63 -0
- package/src/NativeSelect.tsx +41 -14
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +108 -0
- package/src/NumberField.tsx +138 -0
- package/src/PopoverArrow.tsx +19 -5
- package/src/ProgressBar.tsx +3 -5
- package/src/Radio.recipe.ts +108 -0
- package/src/Radio.tsx +62 -0
- package/src/RadioGroup.tsx +68 -0
- package/src/Select.recipe.ts +211 -0
- package/src/Select.tsx +194 -0
- package/src/SharedUIProvider.tsx +63 -7
- package/src/Skeleton.tsx +146 -0
- package/src/Slide.tsx +2 -2
- package/src/Slider.recipe.ts +27 -16
- package/src/Slider.tsx +78 -6
- package/src/Spinner.tsx +11 -9
- package/src/Svg.tsx +2 -3
- package/src/Switch.recipe.ts +57 -11
- package/src/Switch.tsx +39 -7
- package/src/Text.recipe.ts +26 -0
- package/src/Text.tsx +6 -2
- package/src/TextField.tsx +54 -40
- package/src/Toast.recipe.ts +52 -15
- package/src/Toast.tsx +151 -46
- package/src/Tooltip.recipe.ts +32 -0
- package/src/Tooltip.tsx +68 -28
- package/src/TooltipButton.tsx +229 -0
- package/src/UnmountCallback.tsx +2 -2
- package/src/VisuallyHidden.tsx +1 -1
- package/src/base-preset.ts +336 -84
- package/src/{chakra-tokens.ts → base-tokens.ts} +7 -7
- package/src/button-icon.ts +22 -0
- package/src/data-attrs.ts +16 -0
- package/src/dense-preset.ts +107 -0
- package/src/hooks/useBreakpointValue.ts +4 -4
- package/src/hooks/useClipboard.ts +63 -0
- package/src/hooks/useDisclosure.ts +31 -0
- package/src/hooks/useMediaQuery.ts +27 -0
- package/src/hooks/usePrevious.ts +15 -0
- package/src/index.ts +29 -3
- package/src/rac-locale.ts +33 -0
- package/src/system.ts +22 -1
- package/src/TextField.recipe.ts +0 -54
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { defineSlotRecipe } from "@pandacss/dev";
|
|
7
|
+
|
|
8
|
+
// The common transition-property list, inlined (Panda has no
|
|
9
|
+
// transitionProperty token category).
|
|
10
|
+
// No box-shadow: nothing in this recipe uses one, and the focus ring's
|
|
11
|
+
// layers must never fade in.
|
|
12
|
+
const transitionCommon =
|
|
13
|
+
"background-color, border-color, color, fill, stroke, opacity, transform";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Select slot recipe — the dropdown pair, shared by `Select` (a listbox behind
|
|
17
|
+
* a button) and `ComboBox` (a listbox behind a text input). One recipe because
|
|
18
|
+
* the two differ only in what the control is: keeping them together is what
|
|
19
|
+
* stops a searchable and a non-searchable picker drifting apart visually.
|
|
20
|
+
*
|
|
21
|
+
* `trigger` matches the `input` recipe's outline field so a select sits level
|
|
22
|
+
* with a TextField beside it; `content` matches the `menu` recipe's card so
|
|
23
|
+
* every dropdown surface in the family agrees.
|
|
24
|
+
*
|
|
25
|
+
* Apps restyle it through the `variant` group — classroom's `classroom`
|
|
26
|
+
* variant is the rounded pill its join form uses.
|
|
27
|
+
*
|
|
28
|
+
* The label is not a slot here: it comes from the `field` recipe, as every
|
|
29
|
+
* other labelled field's does. The only thing that costs is a per-`variant`
|
|
30
|
+
* label style — restyle the control and let the label match the family.
|
|
31
|
+
*
|
|
32
|
+
* Registered in the base preset (base-preset.ts).
|
|
33
|
+
*/
|
|
34
|
+
export const select = defineSlotRecipe({
|
|
35
|
+
className: "select",
|
|
36
|
+
slots: [
|
|
37
|
+
"root",
|
|
38
|
+
"trigger",
|
|
39
|
+
"value",
|
|
40
|
+
"indicator",
|
|
41
|
+
"content",
|
|
42
|
+
"list",
|
|
43
|
+
"option",
|
|
44
|
+
"optionIndicator",
|
|
45
|
+
"empty",
|
|
46
|
+
],
|
|
47
|
+
base: {
|
|
48
|
+
// No layout here: the component wears the `field` recipe's root alongside
|
|
49
|
+
// this slot, and that recipe is the single owner of field-root layout
|
|
50
|
+
// (else `labelPosition` would fight this slot over `flexDirection`). The
|
|
51
|
+
// slot stays for apps and variants to target.
|
|
52
|
+
root: {},
|
|
53
|
+
trigger: {
|
|
54
|
+
display: "flex",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
justifyContent: "space-between",
|
|
57
|
+
gap: "2",
|
|
58
|
+
width: "100%",
|
|
59
|
+
minWidth: 0,
|
|
60
|
+
outline: "none",
|
|
61
|
+
appearance: "none",
|
|
62
|
+
font: "inherit",
|
|
63
|
+
textAlign: "start",
|
|
64
|
+
cursor: "pointer",
|
|
65
|
+
transitionProperty: transitionCommon,
|
|
66
|
+
transitionDuration: "normal",
|
|
67
|
+
border: "2px solid",
|
|
68
|
+
// As the input recipe (rationale there). The dropdown card below
|
|
69
|
+
// keeps gray.200 — a surface edge, not a form-control boundary.
|
|
70
|
+
borderColor: "gray.300",
|
|
71
|
+
bg: "white",
|
|
72
|
+
color: "inherit",
|
|
73
|
+
// As the input recipe, so mixed fields tint together on hover.
|
|
74
|
+
_hover: { borderColor: "gray.500" },
|
|
75
|
+
// `data-invalid` lands on the root — and, in a ComboBox, on the input —
|
|
76
|
+
// but never on the trigger: a RAC Button has no validity state, and our
|
|
77
|
+
// ComboBox control is a plain div. So it comes down from the parent.
|
|
78
|
+
// `> &` rather than a descendant selector, so an app's own invalid form
|
|
79
|
+
// wrapper cannot paint every control inside it red. Repeated `&`: the
|
|
80
|
+
// hover < focused < invalid specificity ladder, as the input recipe.
|
|
81
|
+
"&&:is([data-focused], :has(input:focus))": {
|
|
82
|
+
borderColor: "focusBorder",
|
|
83
|
+
},
|
|
84
|
+
// Border colour alone, as the input recipe.
|
|
85
|
+
"[data-invalid] > &&&": {
|
|
86
|
+
borderColor: "danger.500",
|
|
87
|
+
},
|
|
88
|
+
// Keyboard-only ring, two cases. Select's button: RAC's attribute.
|
|
89
|
+
// ComboBox: the input's RAC attributes are stripped by react-aria's
|
|
90
|
+
// synthetic blur during virtual focus, so its case combines the two
|
|
91
|
+
// signals that survive — native :focus plus the global-modality
|
|
92
|
+
// attribute the component renders (see ComboBox.tsx). No flicker
|
|
93
|
+
// during list navigation; can't match Select (no input).
|
|
94
|
+
"&&&[data-focus-visible], &&&[data-keyboard-modality]:has(input:focus)": {
|
|
95
|
+
focusRing: "outline",
|
|
96
|
+
},
|
|
97
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
98
|
+
},
|
|
99
|
+
// Whatever shows the current value: Select's SelectValue, ComboBox's
|
|
100
|
+
// input. One slot for both, so an app restyling the placeholder (say)
|
|
101
|
+
// does not have to know which kind of control it is looking at.
|
|
102
|
+
value: {
|
|
103
|
+
flex: "1",
|
|
104
|
+
minWidth: 0,
|
|
105
|
+
overflow: "hidden",
|
|
106
|
+
textOverflow: "ellipsis",
|
|
107
|
+
whiteSpace: "nowrap",
|
|
108
|
+
outline: "none",
|
|
109
|
+
bg: "transparent",
|
|
110
|
+
color: "inherit",
|
|
111
|
+
font: "inherit",
|
|
112
|
+
// RAC sets data-placeholder on SelectValue when nothing is chosen; the
|
|
113
|
+
// ComboBox input uses the real placeholder attribute.
|
|
114
|
+
"&[data-placeholder]": { color: "gray.500" },
|
|
115
|
+
_placeholder: { color: "gray.500" },
|
|
116
|
+
},
|
|
117
|
+
indicator: {
|
|
118
|
+
display: "inline-flex",
|
|
119
|
+
flexShrink: 0,
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
justifyContent: "center",
|
|
122
|
+
fontSize: "1.25em",
|
|
123
|
+
color: "inherit",
|
|
124
|
+
// No pointer-events:none here: in a ComboBox this slot is the button
|
|
125
|
+
// that opens the list. Select's is an aria-hidden span inside the
|
|
126
|
+
// trigger, so it needs no help being inert.
|
|
127
|
+
background: "transparent",
|
|
128
|
+
border: "none",
|
|
129
|
+
cursor: "pointer",
|
|
130
|
+
// No focus styling, deliberately: react-aria keeps a ComboBox's toggle
|
|
131
|
+
// button out of the tab order (the input owns the keyboard), so a ring
|
|
132
|
+
// here would only ever be reachable programmatically, and would suggest
|
|
133
|
+
// the chevron is a tab stop. The whole control shows focus instead.
|
|
134
|
+
outline: "none",
|
|
135
|
+
},
|
|
136
|
+
content: {
|
|
137
|
+
// Line the card up with the control, as a select should. `Select` gets
|
|
138
|
+
// this from RAC, whose trigger is the button it measures; `ComboBox`
|
|
139
|
+
// measures its own control and sets the width inline, because RAC's var
|
|
140
|
+
// is the *input's* width there.
|
|
141
|
+
minWidth: "var(--trigger-width)",
|
|
142
|
+
display: "flex",
|
|
143
|
+
flexDirection: "column",
|
|
144
|
+
bg: "white",
|
|
145
|
+
color: "inherit",
|
|
146
|
+
py: "2",
|
|
147
|
+
zIndex: "popover",
|
|
148
|
+
borderRadius: "md",
|
|
149
|
+
borderWidth: "1px",
|
|
150
|
+
borderColor: "gray.200",
|
|
151
|
+
boxShadow: "sm",
|
|
152
|
+
// Matches the menu recipe's fade/scale.
|
|
153
|
+
transformOrigin: "top",
|
|
154
|
+
opacity: 1,
|
|
155
|
+
transform: "scale(1)",
|
|
156
|
+
transition: "opacity 0.1s ease-out, transform 0.1s ease-out",
|
|
157
|
+
"&[data-entering]": { opacity: 0, transform: "scale(0.95)" },
|
|
158
|
+
"&[data-exiting]": { opacity: 0, transform: "scale(0.95)" },
|
|
159
|
+
_motionReduce: { transition: "none" },
|
|
160
|
+
},
|
|
161
|
+
list: {
|
|
162
|
+
outline: "none",
|
|
163
|
+
overflowY: "auto",
|
|
164
|
+
},
|
|
165
|
+
option: {
|
|
166
|
+
display: "flex",
|
|
167
|
+
alignItems: "center",
|
|
168
|
+
gap: "2",
|
|
169
|
+
py: "1.5",
|
|
170
|
+
px: "3",
|
|
171
|
+
cursor: "pointer",
|
|
172
|
+
color: "inherit",
|
|
173
|
+
outline: "none",
|
|
174
|
+
// As the menu recipe: the highlight is focus indication (no
|
|
175
|
+
// transition — it snaps with the ring, which keyboard nav adds;
|
|
176
|
+
// inset, the rows being full-bleed).
|
|
177
|
+
"&[data-focused]": { bg: "gray.100" },
|
|
178
|
+
"&[data-focus-visible]": { focusRing: "outlineInset" },
|
|
179
|
+
"&[data-pressed]": { bg: "gray.200" },
|
|
180
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
181
|
+
},
|
|
182
|
+
optionIndicator: {
|
|
183
|
+
display: "inline-flex",
|
|
184
|
+
flexShrink: 0,
|
|
185
|
+
alignItems: "center",
|
|
186
|
+
justifyContent: "center",
|
|
187
|
+
marginStart: "auto",
|
|
188
|
+
fontSize: "0.8em",
|
|
189
|
+
opacity: 0,
|
|
190
|
+
"[data-selected] &": { opacity: 1 },
|
|
191
|
+
},
|
|
192
|
+
empty: {
|
|
193
|
+
px: "3",
|
|
194
|
+
py: "2",
|
|
195
|
+
color: "gray.600",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
variants: {
|
|
199
|
+
// The `input` recipe's size ladder, step for step, so a Select sits level
|
|
200
|
+
// with a TextField beside it at every size. The indicator is em-sized and
|
|
201
|
+
// scales free; the dropdown card keeps one density across sizes.
|
|
202
|
+
size: {
|
|
203
|
+
lg: { trigger: { fontSize: "lg", px: "4", h: "12", borderRadius: "md" } },
|
|
204
|
+
md: { trigger: { fontSize: "md", px: "4", h: "10", borderRadius: "md" } },
|
|
205
|
+
sm: { trigger: { fontSize: "sm", px: "3", h: "8", borderRadius: "sm" } },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
defaultVariants: {
|
|
209
|
+
size: "md",
|
|
210
|
+
},
|
|
211
|
+
});
|
package/src/Select.tsx
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { createContext, ReactNode, useContext } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Button as RACButton,
|
|
9
|
+
ListBox as RACListBox,
|
|
10
|
+
ListBoxItem as RACListBoxItem,
|
|
11
|
+
ListBoxItemProps as RACListBoxItemProps,
|
|
12
|
+
Popover,
|
|
13
|
+
PopoverProps,
|
|
14
|
+
Select as RACSelect,
|
|
15
|
+
SelectProps as RACSelectProps,
|
|
16
|
+
SelectValue,
|
|
17
|
+
} from "react-aria-components";
|
|
18
|
+
import { useIntl } from "react-intl";
|
|
19
|
+
import { RiArrowDownSLine } from "react-icons/ri";
|
|
20
|
+
import { css, cx } from "styled-system/css";
|
|
21
|
+
import { field, select, SelectVariantProps } from "styled-system/recipes";
|
|
22
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
23
|
+
import {
|
|
24
|
+
FieldLabel,
|
|
25
|
+
FieldLayoutProps,
|
|
26
|
+
FieldSupport,
|
|
27
|
+
FieldSupportProps,
|
|
28
|
+
} from "./Field";
|
|
29
|
+
import { Icon } from "./Icon";
|
|
30
|
+
import { uiMessage } from "./messages";
|
|
31
|
+
|
|
32
|
+
export type SelectSlots = ReturnType<typeof select>;
|
|
33
|
+
|
|
34
|
+
// Options are children, so they can't see the variant their Select was given.
|
|
35
|
+
// The parent hands its resolved slots down, as Modal does for its own slots.
|
|
36
|
+
const SlotContext = createContext<SelectSlots>(select({}));
|
|
37
|
+
|
|
38
|
+
export const useSelectSlots = () => useContext(SlotContext);
|
|
39
|
+
|
|
40
|
+
export const SelectSlotProvider = SlotContext.Provider;
|
|
41
|
+
|
|
42
|
+
export interface SelectProps<T extends object>
|
|
43
|
+
extends Omit<
|
|
44
|
+
RACSelectProps<T>,
|
|
45
|
+
"className" | "children" | "style" | "placeholder"
|
|
46
|
+
>,
|
|
47
|
+
SelectVariantProps,
|
|
48
|
+
FieldSupportProps,
|
|
49
|
+
FieldLayoutProps {
|
|
50
|
+
/** Visible label. Use `aria-label` instead where the design has none. */
|
|
51
|
+
label?: ReactNode;
|
|
52
|
+
/** Label style overrides. */
|
|
53
|
+
labelCss?: SystemStyleObject;
|
|
54
|
+
/**
|
|
55
|
+
* Shown in the trigger while nothing is chosen. Defaults to a translated
|
|
56
|
+
* "Select an item".
|
|
57
|
+
*/
|
|
58
|
+
placeholder?: string;
|
|
59
|
+
/** `SelectOption`s. */
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
/**
|
|
62
|
+
* Replaces the chevron; `null` removes it. Rarely right on a Select — the
|
|
63
|
+
* chevron is the only thing marking its trigger as a dropdown rather than
|
|
64
|
+
* a label, where a ComboBox's text input speaks for itself (which is why
|
|
65
|
+
* classroom's chevron-less autocomplete is a ComboBox).
|
|
66
|
+
*/
|
|
67
|
+
indicator?: ReactNode | null;
|
|
68
|
+
/** Placement of the dropdown relative to the trigger. */
|
|
69
|
+
placement?: PopoverProps["placement"];
|
|
70
|
+
/**
|
|
71
|
+
* Cap the dropdown's height. A prop rather than a `contentCss` rule because
|
|
72
|
+
* RAC writes its own max-height inline while positioning, which beats any
|
|
73
|
+
* class.
|
|
74
|
+
*/
|
|
75
|
+
maxHeight?: number;
|
|
76
|
+
/** Per-instance overrides for the trigger (the button the value sits in). */
|
|
77
|
+
triggerCss?: SystemStyleObject;
|
|
78
|
+
/** Per-instance overrides for the dropdown card. */
|
|
79
|
+
contentCss?: SystemStyleObject;
|
|
80
|
+
className?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Select — a listbox behind a button, for choosing one of a known set. Use
|
|
85
|
+
* ComboBox where the user should be able to type to filter.
|
|
86
|
+
*/
|
|
87
|
+
export const Select = <T extends object>({
|
|
88
|
+
label,
|
|
89
|
+
labelCss,
|
|
90
|
+
placeholder,
|
|
91
|
+
children,
|
|
92
|
+
indicator,
|
|
93
|
+
placement = "bottom start",
|
|
94
|
+
maxHeight,
|
|
95
|
+
helperText,
|
|
96
|
+
errorMessage,
|
|
97
|
+
helperTextCss,
|
|
98
|
+
labelPosition,
|
|
99
|
+
triggerCss,
|
|
100
|
+
contentCss,
|
|
101
|
+
className,
|
|
102
|
+
...props
|
|
103
|
+
}: SelectProps<T>) => {
|
|
104
|
+
// splitVariantProps, not a hand-picked list: an app preset can add variant
|
|
105
|
+
// groups to the recipe and they have to reach it.
|
|
106
|
+
const [variantProps, rest] = select.splitVariantProps(props);
|
|
107
|
+
const intl = useIntl();
|
|
108
|
+
const slots = select(variantProps);
|
|
109
|
+
const fieldSlots = field({ size: variantProps.size, labelPosition });
|
|
110
|
+
return (
|
|
111
|
+
<SelectSlotProvider value={slots}>
|
|
112
|
+
<RACSelect
|
|
113
|
+
{...(rest as RACSelectProps<T>)}
|
|
114
|
+
className={cx(fieldSlots.root, slots.root, className)}
|
|
115
|
+
>
|
|
116
|
+
{label != null && (
|
|
117
|
+
<FieldLabel
|
|
118
|
+
size={variantProps.size}
|
|
119
|
+
labelPosition={labelPosition}
|
|
120
|
+
isRequired={props.isRequired}
|
|
121
|
+
css={labelCss}
|
|
122
|
+
>
|
|
123
|
+
{label}
|
|
124
|
+
</FieldLabel>
|
|
125
|
+
)}
|
|
126
|
+
<RACButton
|
|
127
|
+
className={cx(
|
|
128
|
+
slots.trigger,
|
|
129
|
+
triggerCss ? css(triggerCss) : undefined,
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
<SelectValue className={slots.value}>
|
|
133
|
+
{({ isPlaceholder, defaultChildren }) =>
|
|
134
|
+
isPlaceholder
|
|
135
|
+
? placeholder ??
|
|
136
|
+
intl.formatMessage(uiMessage("ui.select-placeholder"))
|
|
137
|
+
: defaultChildren
|
|
138
|
+
}
|
|
139
|
+
</SelectValue>
|
|
140
|
+
{indicator !== null && (
|
|
141
|
+
<span className={slots.indicator} aria-hidden>
|
|
142
|
+
{indicator ?? <Icon as={RiArrowDownSLine} />}
|
|
143
|
+
</span>
|
|
144
|
+
)}
|
|
145
|
+
</RACButton>
|
|
146
|
+
<Popover
|
|
147
|
+
placement={placement}
|
|
148
|
+
maxHeight={maxHeight}
|
|
149
|
+
className={cx(
|
|
150
|
+
slots.content,
|
|
151
|
+
contentCss ? css(contentCss) : undefined,
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
<RACListBox className={slots.list}>{children}</RACListBox>
|
|
155
|
+
</Popover>
|
|
156
|
+
<FieldSupport
|
|
157
|
+
helperText={helperText}
|
|
158
|
+
errorMessage={errorMessage}
|
|
159
|
+
helperTextCss={helperTextCss}
|
|
160
|
+
labelPosition={labelPosition}
|
|
161
|
+
/>
|
|
162
|
+
</RACSelect>
|
|
163
|
+
</SelectSlotProvider>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export interface SelectOptionProps
|
|
168
|
+
extends Omit<RACListBoxItemProps, "className" | "children" | "style"> {
|
|
169
|
+
children?: ReactNode;
|
|
170
|
+
css?: SystemStyleObject;
|
|
171
|
+
className?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** A row in a `Select` or `ComboBox` list. */
|
|
175
|
+
export const SelectOption = ({
|
|
176
|
+
children,
|
|
177
|
+
css: cssProp,
|
|
178
|
+
className,
|
|
179
|
+
...rest
|
|
180
|
+
}: SelectOptionProps) => {
|
|
181
|
+
const slots = useSelectSlots();
|
|
182
|
+
return (
|
|
183
|
+
<RACListBoxItem
|
|
184
|
+
{...rest}
|
|
185
|
+
className={cx(
|
|
186
|
+
slots.option,
|
|
187
|
+
cssProp ? css(cssProp) : undefined,
|
|
188
|
+
className,
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
{children}
|
|
192
|
+
</RACListBoxItem>
|
|
193
|
+
);
|
|
194
|
+
};
|
package/src/SharedUIProvider.tsx
CHANGED
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
ReactNode,
|
|
9
|
+
useContext,
|
|
10
|
+
useEffect,
|
|
11
|
+
useMemo,
|
|
12
|
+
} from "react";
|
|
13
|
+
import { isRTL } from "react-aria";
|
|
14
|
+
import { I18nProvider } from "react-aria-components";
|
|
15
|
+
import { IntlContext } from "react-intl";
|
|
16
|
+
import { racLocale } from "./rac-locale";
|
|
7
17
|
|
|
8
18
|
/**
|
|
9
19
|
* Registers the close function of the currently open dismissable overlay, or
|
|
@@ -21,27 +31,73 @@ const SharedUIContext = createContext<SharedUIContextValue | null>(null);
|
|
|
21
31
|
|
|
22
32
|
export interface SharedUIProviderProps {
|
|
23
33
|
overlayCloseRegistrar?: OverlayCloseRegistrar;
|
|
34
|
+
/**
|
|
35
|
+
* Locale for react-aria's own built-in strings. Defaults to the surrounding
|
|
36
|
+
* IntlProvider's locale, which is what apps want: pass this only where the
|
|
37
|
+
* two must differ.
|
|
38
|
+
*/
|
|
39
|
+
locale?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Keeps `<html lang>` and `<html dir>` in step with the locale, so assistive
|
|
42
|
+
* tech announces the page in the app's language and the layout runs the
|
|
43
|
+
* right way round. On by default; set false where the app doesn't own the
|
|
44
|
+
* document it's mounted in (an embedded widget), or when mounting more than
|
|
45
|
+
* one provider per page. The static `lang="en"` in index.html remains the
|
|
46
|
+
* pre-hydration default.
|
|
47
|
+
*
|
|
48
|
+
* Turning it off in an RTL locale means the app must set `dir` itself above
|
|
49
|
+
* the components, or the UI half-mirrors: react-aria mirrors from the locale
|
|
50
|
+
* in JS whatever the DOM says, while logical properties and `_rtl` rules
|
|
51
|
+
* read `dir` (see the README's RTL note).
|
|
52
|
+
*
|
|
53
|
+
* Correct only when the IntlProvider locale is the language the app
|
|
54
|
+
* actually renders in — an app falling back wholesale to its English
|
|
55
|
+
* catalog should claim `en` there, not the user's language setting (see
|
|
56
|
+
* the README's react-intl step).
|
|
57
|
+
*/
|
|
58
|
+
setDocumentLang?: boolean;
|
|
24
59
|
children: ReactNode;
|
|
25
60
|
}
|
|
26
61
|
|
|
27
62
|
/**
|
|
28
|
-
* SharedUIProvider — the app-side installation point for
|
|
29
|
-
* integrations
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
63
|
+
* SharedUIProvider — the app-side installation point for shared-ui
|
|
64
|
+
* integrations: the optional overlay-close registrar, and the locale for
|
|
65
|
+
* react-aria's built-in strings.
|
|
66
|
+
*
|
|
67
|
+
* This package's own strings come from react-intl, so an IntlProvider must be
|
|
68
|
+
* mounted above shared-ui components (see the package README). react-aria
|
|
69
|
+
* translates its built-in strings itself, from its own bundled catalogs, and
|
|
70
|
+
* without this provider it picks the locale from the browser rather than from
|
|
71
|
+
* the app's language setting — mount it inside the IntlProvider so the two
|
|
72
|
+
* agree.
|
|
33
73
|
*/
|
|
34
74
|
export const SharedUIProvider = ({
|
|
35
75
|
overlayCloseRegistrar,
|
|
76
|
+
locale,
|
|
77
|
+
setDocumentLang = true,
|
|
36
78
|
children,
|
|
37
79
|
}: SharedUIProviderProps) => {
|
|
80
|
+
// Read the context rather than calling useIntl(), which throws when there is
|
|
81
|
+
// no IntlProvider: react-aria falls back to the browser locale, as before.
|
|
82
|
+
const intlLocale = useContext(IntlContext)?.locale;
|
|
83
|
+
const appLocale = locale ?? intlLocale;
|
|
84
|
+
// The tag react-aria gets, so the document's direction and the components'
|
|
85
|
+
// cannot disagree; also the only form isRTL can be handed safely, as it
|
|
86
|
+
// throws on a malformed tag.
|
|
87
|
+
const sanitizedLocale = racLocale(appLocale);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (setDocumentLang && appLocale && sanitizedLocale) {
|
|
90
|
+
document.documentElement.lang = appLocale;
|
|
91
|
+
document.documentElement.dir = isRTL(sanitizedLocale) ? "rtl" : "ltr";
|
|
92
|
+
}
|
|
93
|
+
}, [setDocumentLang, appLocale, sanitizedLocale]);
|
|
38
94
|
const value = useMemo(
|
|
39
95
|
() => ({ overlayCloseRegistrar }),
|
|
40
96
|
[overlayCloseRegistrar],
|
|
41
97
|
);
|
|
42
98
|
return (
|
|
43
99
|
<SharedUIContext.Provider value={value}>
|
|
44
|
-
{children}
|
|
100
|
+
<I18nProvider locale={sanitizedLocale}>{children}</I18nProvider>
|
|
45
101
|
</SharedUIContext.Provider>
|
|
46
102
|
);
|
|
47
103
|
};
|
package/src/Skeleton.tsx
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { HTMLAttributes, ReactNode } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The placeholder block, as an object rather than a precomputed class so a
|
|
12
|
+
* caller's `css` is merged into one `css()` call and its overrides win.
|
|
13
|
+
*
|
|
14
|
+
* The colours flow through a pair of custom properties, so a call site can
|
|
15
|
+
* retint one skeleton without knowing how the animation works.
|
|
16
|
+
*/
|
|
17
|
+
const skeletonBase: SystemStyleObject = {
|
|
18
|
+
"--skeleton-start-color": "token(colors.gray.100)",
|
|
19
|
+
// gray.350, the decorative-fill stop, as the Avatar default.
|
|
20
|
+
"--skeleton-end-color": "token(colors.gray.350)",
|
|
21
|
+
background: "var(--skeleton-start-color)",
|
|
22
|
+
borderColor: "var(--skeleton-end-color)",
|
|
23
|
+
opacity: 0.7,
|
|
24
|
+
borderRadius: "sm",
|
|
25
|
+
boxShadow: "none",
|
|
26
|
+
backgroundClip: "padding-box",
|
|
27
|
+
cursor: "default",
|
|
28
|
+
color: "transparent",
|
|
29
|
+
pointerEvents: "none",
|
|
30
|
+
userSelect: "none",
|
|
31
|
+
// The content is hidden rather than unmounted, so a skeleton sized from
|
|
32
|
+
// real children keeps their dimensions.
|
|
33
|
+
"&::before, &::after, *": { visibility: "hidden" },
|
|
34
|
+
animation:
|
|
35
|
+
"skeletonFade var(--skeleton-speed, 0.8s) linear infinite alternate",
|
|
36
|
+
_motionReduce: { animation: "none" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface SkeletonProps
|
|
40
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, "color"> {
|
|
41
|
+
/** Show the children instead of the placeholder. */
|
|
42
|
+
isLoaded?: boolean;
|
|
43
|
+
/** Seconds per pulse (default 0.8). */
|
|
44
|
+
speed?: number;
|
|
45
|
+
children?: ReactNode;
|
|
46
|
+
/** Per-instance style overrides, merged after the base. */
|
|
47
|
+
css?: SystemStyleObject;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Skeleton — a loading placeholder: a block pulsing between two greys until
|
|
53
|
+
* its content is ready.
|
|
54
|
+
*
|
|
55
|
+
* Content simply appears when `isLoaded` turns true; wrap in `Fade` where a
|
|
56
|
+
* transition matters.
|
|
57
|
+
*/
|
|
58
|
+
export const Skeleton = ({
|
|
59
|
+
isLoaded,
|
|
60
|
+
speed,
|
|
61
|
+
children,
|
|
62
|
+
css: cssProp,
|
|
63
|
+
className,
|
|
64
|
+
style,
|
|
65
|
+
...rest
|
|
66
|
+
}: SkeletonProps) => {
|
|
67
|
+
if (isLoaded) {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
{...rest}
|
|
71
|
+
style={style}
|
|
72
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
{...rest}
|
|
81
|
+
style={
|
|
82
|
+
speed === undefined
|
|
83
|
+
? style
|
|
84
|
+
: ({ ...style, "--skeleton-speed": `${speed}s` } as typeof style)
|
|
85
|
+
}
|
|
86
|
+
className={cx(css({ ...skeletonBase, ...cssProp }), className)}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export interface SkeletonTextProps extends SkeletonProps {
|
|
94
|
+
/** How many lines to draw (default 3). */
|
|
95
|
+
noOfLines?: number;
|
|
96
|
+
/** Gap between the lines. Any CSS length. */
|
|
97
|
+
spacing?: string;
|
|
98
|
+
/** Height of each line. Any CSS length. */
|
|
99
|
+
skeletonHeight?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* SkeletonText — a paragraph-shaped `Skeleton`: evenly spaced lines, the last
|
|
104
|
+
* one short.
|
|
105
|
+
*/
|
|
106
|
+
export const SkeletonText = ({
|
|
107
|
+
noOfLines = 3,
|
|
108
|
+
spacing = "0.5rem",
|
|
109
|
+
skeletonHeight = "0.5rem",
|
|
110
|
+
isLoaded,
|
|
111
|
+
speed,
|
|
112
|
+
children,
|
|
113
|
+
css: cssProp,
|
|
114
|
+
className,
|
|
115
|
+
...rest
|
|
116
|
+
}: SkeletonTextProps) => {
|
|
117
|
+
if (isLoaded) {
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
{...rest}
|
|
121
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
122
|
+
>
|
|
123
|
+
{children}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
{...rest}
|
|
130
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
131
|
+
>
|
|
132
|
+
{Array.from({ length: noOfLines }, (_, index) => (
|
|
133
|
+
<Skeleton
|
|
134
|
+
key={index}
|
|
135
|
+
speed={speed}
|
|
136
|
+
style={{
|
|
137
|
+
height: skeletonHeight,
|
|
138
|
+
// The last line is drawn at 80%, with no gap after it.
|
|
139
|
+
width: noOfLines > 1 && index === noOfLines - 1 ? "80%" : "100%",
|
|
140
|
+
marginBottom: index === noOfLines - 1 ? "0" : spacing,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
package/src/Slide.tsx
CHANGED
|
@@ -17,8 +17,8 @@ export interface SlideProps {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Slide —
|
|
21
|
-
*
|
|
20
|
+
* Slide — a slide-in transition, bottom edge only (add other directions when
|
|
21
|
+
* a use appears): a full-width panel pinned to the viewport bottom,
|
|
22
22
|
* translated offscreen when closed. Content stays mounted throughout, so
|
|
23
23
|
* closing doesn't drop in-flight state and the exit animates.
|
|
24
24
|
*/
|