@microbit/ui 0.1.0-alpha.10
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 +53 -0
- package/README.md +220 -0
- package/lang/ui.ca.json +22 -0
- package/lang/ui.de.json +22 -0
- package/lang/ui.en-us.json +22 -0
- package/lang/ui.en.json +22 -0
- package/lang/ui.es-es.json +22 -0
- package/lang/ui.fr.json +22 -0
- package/lang/ui.ga-ie.json +22 -0
- package/lang/ui.ja.json +22 -0
- package/lang/ui.ko.json +22 -0
- package/lang/ui.lol.json +22 -0
- package/lang/ui.nl.json +22 -0
- package/lang/ui.pl.json +22 -0
- package/lang/ui.pt-br.json +22 -0
- package/lang/ui.zh-cn.json +22 -0
- package/lang/ui.zh-tw.json +22 -0
- package/package.json +58 -0
- package/postcss-legacy-safari.cjs +96 -0
- package/reset.css +35 -0
- package/src/Button.recipe.ts +162 -0
- package/src/Button.tsx +69 -0
- package/src/ButtonGroup.tsx +63 -0
- package/src/Card.recipe.ts +49 -0
- package/src/Card.tsx +56 -0
- package/src/Checkbox.recipe.ts +105 -0
- package/src/Checkbox.tsx +76 -0
- package/src/CloseButton.tsx +87 -0
- package/src/CloseIcon.tsx +40 -0
- package/src/Code.tsx +20 -0
- package/src/Collapse.tsx +180 -0
- package/src/Divider.tsx +52 -0
- package/src/Drawer.recipe.ts +98 -0
- package/src/Drawer.tsx +138 -0
- package/src/Fade.tsx +48 -0
- package/src/Heading.recipe.ts +52 -0
- package/src/Heading.tsx +14 -0
- package/src/Icon.tsx +55 -0
- package/src/IconButton.tsx +44 -0
- package/src/Image.tsx +11 -0
- package/src/Input.recipe.ts +72 -0
- package/src/Input.tsx +40 -0
- package/src/InputGroup.tsx +62 -0
- package/src/Kbd.tsx +26 -0
- package/src/Link.tsx +23 -0
- package/src/LinkBox.tsx +88 -0
- package/src/LinkButton.tsx +80 -0
- package/src/List.tsx +31 -0
- package/src/Menu.recipe.ts +119 -0
- package/src/Menu.tsx +261 -0
- package/src/Modal.recipe.ts +163 -0
- package/src/Modal.tsx +279 -0
- package/src/NativeSelect.tsx +94 -0
- package/src/NumberField.recipe.ts +67 -0
- package/src/NumberField.tsx +86 -0
- package/src/PopoverArrow.tsx +67 -0
- package/src/ProgressBar.tsx +61 -0
- package/src/Radio.recipe.ts +107 -0
- package/src/Radio.tsx +89 -0
- package/src/SharedUIProvider.tsx +55 -0
- package/src/Slide.tsx +54 -0
- package/src/Slider.recipe.ts +102 -0
- package/src/Slider.tsx +163 -0
- package/src/Spinner.tsx +69 -0
- package/src/Svg.tsx +23 -0
- package/src/Switch.recipe.ts +107 -0
- package/src/Switch.tsx +62 -0
- package/src/Text.recipe.ts +29 -0
- package/src/Text.tsx +16 -0
- package/src/TextField.recipe.ts +54 -0
- package/src/TextField.tsx +89 -0
- package/src/Toast.recipe.ts +98 -0
- package/src/Toast.tsx +171 -0
- package/src/Tooltip.tsx +91 -0
- package/src/UnmountCallback.tsx +18 -0
- package/src/VisuallyHidden.tsx +14 -0
- package/src/base-preset.ts +294 -0
- package/src/base-tokens.ts +1017 -0
- package/src/button-icon.ts +23 -0
- package/src/hooks/useBreakpointValue.ts +63 -0
- package/src/hooks/useClipboard.ts +64 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/usePrevious.ts +18 -0
- package/src/index.ts +55 -0
- package/src/messages.ts +17 -0
- package/src/system.ts +36 -0
package/src/Modal.tsx
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
CSSProperties,
|
|
9
|
+
ReactNode,
|
|
10
|
+
RefObject,
|
|
11
|
+
useContext,
|
|
12
|
+
} from "react";
|
|
13
|
+
import {
|
|
14
|
+
Button as RACButton,
|
|
15
|
+
Dialog,
|
|
16
|
+
Heading as RACHeading,
|
|
17
|
+
Modal as RACModal,
|
|
18
|
+
ModalOverlay,
|
|
19
|
+
} from "react-aria-components";
|
|
20
|
+
import { css, cx } from "styled-system/css";
|
|
21
|
+
import { dialog } from "styled-system/recipes";
|
|
22
|
+
import { ConditionalValue, SystemStyleObject } from "styled-system/types";
|
|
23
|
+
import { useIntl } from "react-intl";
|
|
24
|
+
import { CloseIcon } from "./CloseIcon";
|
|
25
|
+
import { uiMessage } from "./messages";
|
|
26
|
+
import { UnmountCallback } from "./UnmountCallback";
|
|
27
|
+
|
|
28
|
+
type DialogSlots = ReturnType<typeof dialog>;
|
|
29
|
+
|
|
30
|
+
const SlotContext = createContext<{ slots: DialogSlots; onClose: () => void }>({
|
|
31
|
+
slots: dialog({}),
|
|
32
|
+
onClose: () => undefined,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const useDialog = () => useContext(SlotContext);
|
|
36
|
+
|
|
37
|
+
export type ModalSize = ConditionalValue<
|
|
38
|
+
| "xs"
|
|
39
|
+
| "sm"
|
|
40
|
+
| "md"
|
|
41
|
+
| "lg"
|
|
42
|
+
| "xl"
|
|
43
|
+
| "2xl"
|
|
44
|
+
| "3xl"
|
|
45
|
+
| "4xl"
|
|
46
|
+
| "5xl"
|
|
47
|
+
| "6xl"
|
|
48
|
+
| "full"
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
export interface ModalProps {
|
|
52
|
+
isOpen: boolean;
|
|
53
|
+
onClose: () => void;
|
|
54
|
+
size?: ModalSize;
|
|
55
|
+
/** Allow closing by clicking the backdrop (default true; Escape always closes). */
|
|
56
|
+
isDismissable?: boolean;
|
|
57
|
+
/** Disable the enter/exit transitions (Chakra's motionPreset="none"). */
|
|
58
|
+
motionless?: boolean;
|
|
59
|
+
/** Prevent Escape closing the dialog (Chakra's closeOnEsc={false}). */
|
|
60
|
+
isKeyboardDismissDisabled?: boolean;
|
|
61
|
+
/** Style overrides for the dialog box (Chakra's ModalContent props). */
|
|
62
|
+
contentCss?: SystemStyleObject;
|
|
63
|
+
/**
|
|
64
|
+
* Inline styles for the dialog box, for runtime-computed positioning that
|
|
65
|
+
* Panda can't statically extract (e.g. a dialog aligned to a measured
|
|
66
|
+
* element). Prefer `contentCss` for static styles.
|
|
67
|
+
*/
|
|
68
|
+
contentStyle?: CSSProperties;
|
|
69
|
+
/**
|
|
70
|
+
* Style overrides for the backdrop (Chakra's ModalOverlay props), e.g. a
|
|
71
|
+
* transparent backdrop when something else provides the dimming.
|
|
72
|
+
*/
|
|
73
|
+
overlayCss?: SystemStyleObject;
|
|
74
|
+
/**
|
|
75
|
+
* Use "alertdialog" for confirmations that interrupt the user (Chakra's
|
|
76
|
+
* AlertDialog).
|
|
77
|
+
*/
|
|
78
|
+
role?: "dialog" | "alertdialog";
|
|
79
|
+
/** Vertically centre the dialog (Chakra's `isCentered`). */
|
|
80
|
+
isCentered?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Called after the dialog has fully closed (exit transition done and the
|
|
83
|
+
* dialog removed). Matches Chakra's `onCloseComplete`.
|
|
84
|
+
*/
|
|
85
|
+
onCloseComplete?: () => void;
|
|
86
|
+
/**
|
|
87
|
+
* Element to focus when the dialog closes, instead of the element that was
|
|
88
|
+
* focused when it opened. Matches Chakra's `finalFocusRef`.
|
|
89
|
+
*/
|
|
90
|
+
finalFocusRef?: RefObject<HTMLElement>;
|
|
91
|
+
/**
|
|
92
|
+
* Accessible name for dialogs without a ModalHeader (which otherwise
|
|
93
|
+
* provides the label). One of the two is required — react-aria warns in
|
|
94
|
+
* dev builds when a dialog has neither. An explicit `aria-label` wins
|
|
95
|
+
* over a ModalHeader.
|
|
96
|
+
*/
|
|
97
|
+
"aria-label"?: string;
|
|
98
|
+
children: ReactNode;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Modal — a focus-trapping dialog. Collapses Chakra's
|
|
103
|
+
* Modal/ModalOverlay/ModalContent into a single shell; place ModalHeader,
|
|
104
|
+
* ModalBody and ModalFooter inside.
|
|
105
|
+
*/
|
|
106
|
+
export const Modal = ({
|
|
107
|
+
isOpen,
|
|
108
|
+
onClose,
|
|
109
|
+
size,
|
|
110
|
+
isDismissable = true,
|
|
111
|
+
motionless,
|
|
112
|
+
isKeyboardDismissDisabled,
|
|
113
|
+
contentCss,
|
|
114
|
+
contentStyle,
|
|
115
|
+
overlayCss,
|
|
116
|
+
role,
|
|
117
|
+
isCentered,
|
|
118
|
+
onCloseComplete,
|
|
119
|
+
finalFocusRef,
|
|
120
|
+
"aria-label": ariaLabel,
|
|
121
|
+
children,
|
|
122
|
+
}: ModalProps) => {
|
|
123
|
+
const slots = dialog({ size, centered: isCentered });
|
|
124
|
+
const motionlessClass = motionless
|
|
125
|
+
? css({
|
|
126
|
+
transition: "none",
|
|
127
|
+
"&[data-entering]": { opacity: 1, transform: "none" },
|
|
128
|
+
"&[data-exiting]": { opacity: 1, transform: "none" },
|
|
129
|
+
})
|
|
130
|
+
: undefined;
|
|
131
|
+
const handleUnmount = () => {
|
|
132
|
+
onCloseComplete?.();
|
|
133
|
+
const el = finalFocusRef?.current;
|
|
134
|
+
if (el) {
|
|
135
|
+
// After RAC's own focus restoration, which also runs on unmount.
|
|
136
|
+
requestAnimationFrame(() => el.focus());
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
return (
|
|
140
|
+
<ModalOverlay
|
|
141
|
+
isOpen={isOpen}
|
|
142
|
+
onOpenChange={(open) => {
|
|
143
|
+
if (!open) {
|
|
144
|
+
onClose();
|
|
145
|
+
}
|
|
146
|
+
}}
|
|
147
|
+
isDismissable={isDismissable}
|
|
148
|
+
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
|
|
149
|
+
className={cx(
|
|
150
|
+
slots.overlay,
|
|
151
|
+
motionlessClass,
|
|
152
|
+
overlayCss ? css(overlayCss) : undefined,
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
<UnmountCallback callback={handleUnmount} />
|
|
156
|
+
<RACModal
|
|
157
|
+
style={contentStyle}
|
|
158
|
+
className={cx(
|
|
159
|
+
slots.content,
|
|
160
|
+
motionlessClass,
|
|
161
|
+
contentCss ? css(contentCss) : undefined,
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
<Dialog role={role} aria-label={ariaLabel} className={slots.inner}>
|
|
165
|
+
<SlotContext.Provider value={{ slots, onClose }}>
|
|
166
|
+
{children}
|
|
167
|
+
</SlotContext.Provider>
|
|
168
|
+
</Dialog>
|
|
169
|
+
</RACModal>
|
|
170
|
+
</ModalOverlay>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
interface SlotProps {
|
|
175
|
+
children?: ReactNode;
|
|
176
|
+
css?: SystemStyleObject;
|
|
177
|
+
className?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Modal title. Rendered as RAC's labelling heading for the dialog. */
|
|
181
|
+
export const ModalHeader = ({
|
|
182
|
+
children,
|
|
183
|
+
css: cssProp,
|
|
184
|
+
className,
|
|
185
|
+
level,
|
|
186
|
+
}: SlotProps & {
|
|
187
|
+
/** Heading element level (default 3, like RAC). */ level?: number;
|
|
188
|
+
}) => {
|
|
189
|
+
const { slots } = useDialog();
|
|
190
|
+
return (
|
|
191
|
+
<RACHeading
|
|
192
|
+
slot="title"
|
|
193
|
+
level={level}
|
|
194
|
+
className={cx(
|
|
195
|
+
slots.header,
|
|
196
|
+
cssProp ? css(cssProp) : undefined,
|
|
197
|
+
className,
|
|
198
|
+
)}
|
|
199
|
+
>
|
|
200
|
+
{children}
|
|
201
|
+
</RACHeading>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const ModalBody = ({ children, css: cssProp, className }: SlotProps) => {
|
|
206
|
+
const { slots } = useDialog();
|
|
207
|
+
return (
|
|
208
|
+
<div
|
|
209
|
+
className={cx(slots.body, cssProp ? css(cssProp) : undefined, className)}
|
|
210
|
+
>
|
|
211
|
+
{children}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const ModalFooter = ({
|
|
217
|
+
children,
|
|
218
|
+
css: cssProp,
|
|
219
|
+
className,
|
|
220
|
+
}: SlotProps) => {
|
|
221
|
+
const { slots } = useDialog();
|
|
222
|
+
return (
|
|
223
|
+
<div
|
|
224
|
+
className={cx(
|
|
225
|
+
slots.footer,
|
|
226
|
+
cssProp ? css(cssProp) : undefined,
|
|
227
|
+
className,
|
|
228
|
+
)}
|
|
229
|
+
>
|
|
230
|
+
{children}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export interface ModalCloseButtonProps {
|
|
236
|
+
/** Accessible name; defaults to the localized close label. */
|
|
237
|
+
"aria-label"?: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* ModalCloseButton — the X in the dialog's top corner (Chakra's
|
|
242
|
+
* ModalCloseButton at its default md size). Closes via the Modal's onClose.
|
|
243
|
+
*/
|
|
244
|
+
export const ModalCloseButton = ({
|
|
245
|
+
"aria-label": ariaLabel,
|
|
246
|
+
}: ModalCloseButtonProps) => {
|
|
247
|
+
const intl = useIntl();
|
|
248
|
+
const { slots, onClose } = useDialog();
|
|
249
|
+
return (
|
|
250
|
+
<RACButton
|
|
251
|
+
aria-label={ariaLabel ?? intl.formatMessage(uiMessage("ui.close-action"))}
|
|
252
|
+
onPress={onClose}
|
|
253
|
+
className={cx(
|
|
254
|
+
slots.closeTrigger,
|
|
255
|
+
css({
|
|
256
|
+
display: "inline-flex",
|
|
257
|
+
alignItems: "center",
|
|
258
|
+
justifyContent: "center",
|
|
259
|
+
width: "8",
|
|
260
|
+
height: "8",
|
|
261
|
+
fontSize: "xs",
|
|
262
|
+
borderRadius: "md",
|
|
263
|
+
cursor: "pointer",
|
|
264
|
+
bg: "transparent",
|
|
265
|
+
border: "none",
|
|
266
|
+
color: "inherit",
|
|
267
|
+
outline: "none",
|
|
268
|
+
transitionProperty: "background-color, box-shadow",
|
|
269
|
+
transitionDuration: "normal",
|
|
270
|
+
_hover: { bg: "blackAlpha.100" },
|
|
271
|
+
_active: { bg: "blackAlpha.200" },
|
|
272
|
+
_focusVisible: { focusShadow: "outline" },
|
|
273
|
+
}),
|
|
274
|
+
)}
|
|
275
|
+
>
|
|
276
|
+
<CloseIcon />
|
|
277
|
+
</RACButton>
|
|
278
|
+
);
|
|
279
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { forwardRef, SelectHTMLAttributes } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { input, InputVariantProps } from "styled-system/recipes";
|
|
9
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
10
|
+
|
|
11
|
+
export interface NativeSelectProps
|
|
12
|
+
// `size` is the recipe's size scale, as in Chakra; the native visible-rows
|
|
13
|
+
// attribute it shadows was unused.
|
|
14
|
+
extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "className" | "size">,
|
|
15
|
+
InputVariantProps {
|
|
16
|
+
/**
|
|
17
|
+
* Suppress the dropdown chevron, e.g. when an adjacent control provides the
|
|
18
|
+
* affordance. Without the chevron a bare <select> element is rendered (no
|
|
19
|
+
* wrapper), so it can participate directly in e.g. an attached ButtonGroup.
|
|
20
|
+
*/
|
|
21
|
+
hideChevron?: boolean;
|
|
22
|
+
/** Per-instance style overrides for the select, merged after the recipe. */
|
|
23
|
+
css?: SystemStyleObject;
|
|
24
|
+
/**
|
|
25
|
+
* Style overrides for the chevron wrapper, which is where width constraints
|
|
26
|
+
* belong (the select fills it). Chakra's Select worked the same way: layout
|
|
27
|
+
* props were split onto `.chakra-select__wrapper`, `width: 100%` otherwise.
|
|
28
|
+
* No wrapper is rendered with `hideChevron`; constrain the select directly.
|
|
29
|
+
*/
|
|
30
|
+
wrapperCss?: SystemStyleObject;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* NativeSelect — a native select styled like Chakra's Select field (outline).
|
|
36
|
+
* The recipe's `appearance: none` removes the platform chevron, so one is
|
|
37
|
+
* drawn back in by default (Chakra Select's glyph, `currentColor`).
|
|
38
|
+
*/
|
|
39
|
+
export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
40
|
+
function NativeSelect(
|
|
41
|
+
{ hideChevron = false, size, css: cssProp, wrapperCss, className, ...rest },
|
|
42
|
+
ref,
|
|
43
|
+
) {
|
|
44
|
+
const select = (
|
|
45
|
+
<select
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cx(
|
|
48
|
+
input({ size }),
|
|
49
|
+
css(
|
|
50
|
+
{ cursor: "pointer" },
|
|
51
|
+
// Room for the chevron overlay (Chakra Select's icon spacing,
|
|
52
|
+
// constant across sizes).
|
|
53
|
+
hideChevron ? undefined : { paddingRight: "8" },
|
|
54
|
+
cssProp,
|
|
55
|
+
),
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
{...rest}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
if (hideChevron) {
|
|
62
|
+
return select;
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
<span
|
|
66
|
+
className={css(
|
|
67
|
+
// Full-width like every other field (and Chakra's select wrapper);
|
|
68
|
+
// the select's own recipe width fills it. Constrain via wrapperCss.
|
|
69
|
+
{ position: "relative", display: "inline-flex", width: "100%" },
|
|
70
|
+
wrapperCss,
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{select}
|
|
74
|
+
{/* Chakra Select's chevron, fixed-size across field sizes. */}
|
|
75
|
+
<svg
|
|
76
|
+
viewBox="0 0 24 24"
|
|
77
|
+
aria-hidden
|
|
78
|
+
className={css({
|
|
79
|
+
position: "absolute",
|
|
80
|
+
right: "2",
|
|
81
|
+
top: "50%",
|
|
82
|
+
transform: "translateY(-50%)",
|
|
83
|
+
width: "5",
|
|
84
|
+
height: "5",
|
|
85
|
+
pointerEvents: "none",
|
|
86
|
+
fill: "currentColor",
|
|
87
|
+
})}
|
|
88
|
+
>
|
|
89
|
+
<path d="M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z" />
|
|
90
|
+
</svg>
|
|
91
|
+
</span>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
);
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* NumberField slot recipe — Chakra's NumberInput look: an outline input (the
|
|
10
|
+
* `input` recipe styles the input itself) with a right-hand stepper column of
|
|
11
|
+
* two stacked buttons. Consumed by the shared-ui NumberField
|
|
12
|
+
* (react-aria-components NumberField).
|
|
13
|
+
*
|
|
14
|
+
* Registered in the base preset (base-preset.ts). No variants, so it needs
|
|
15
|
+
* no `staticCss` entry.
|
|
16
|
+
*/
|
|
17
|
+
export const numberField = defineSlotRecipe({
|
|
18
|
+
className: "numberField",
|
|
19
|
+
slots: ["root", "group", "stepper", "stepperButton"],
|
|
20
|
+
base: {
|
|
21
|
+
root: {
|
|
22
|
+
display: "flex",
|
|
23
|
+
flexDirection: "column",
|
|
24
|
+
alignItems: "stretch",
|
|
25
|
+
},
|
|
26
|
+
group: {
|
|
27
|
+
position: "relative",
|
|
28
|
+
zIndex: 0,
|
|
29
|
+
},
|
|
30
|
+
// Chakra's NumberInputStepper: a column overlaying the input's right
|
|
31
|
+
// edge, inset by the input border.
|
|
32
|
+
stepper: {
|
|
33
|
+
display: "flex",
|
|
34
|
+
flexDirection: "column",
|
|
35
|
+
position: "absolute",
|
|
36
|
+
insetEnd: "0",
|
|
37
|
+
top: "0",
|
|
38
|
+
height: "calc(100% - 2px)",
|
|
39
|
+
margin: "1px",
|
|
40
|
+
width: "6",
|
|
41
|
+
zIndex: 1,
|
|
42
|
+
},
|
|
43
|
+
stepperButton: {
|
|
44
|
+
display: "flex",
|
|
45
|
+
alignItems: "center",
|
|
46
|
+
justifyContent: "center",
|
|
47
|
+
flex: 1,
|
|
48
|
+
cursor: "pointer",
|
|
49
|
+
lineHeight: "normal",
|
|
50
|
+
fontSize: "xs",
|
|
51
|
+
color: "inherit",
|
|
52
|
+
bg: "transparent",
|
|
53
|
+
borderStart: "1px solid",
|
|
54
|
+
borderColor: "gray.200",
|
|
55
|
+
transitionProperty: "background",
|
|
56
|
+
transitionDuration: "ultra-fast",
|
|
57
|
+
"&:last-child": {
|
|
58
|
+
borderTop: "1px solid",
|
|
59
|
+
borderTopColor: "gray.200",
|
|
60
|
+
marginTop: "-1px",
|
|
61
|
+
},
|
|
62
|
+
"&[data-hovered]": { bg: "gray.100" },
|
|
63
|
+
"&[data-pressed]": { bg: "gray.200" },
|
|
64
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { forwardRef, ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Button as RACButton,
|
|
9
|
+
Group as RACGroup,
|
|
10
|
+
Input as RACInput,
|
|
11
|
+
Label as RACLabel,
|
|
12
|
+
NumberField as RACNumberField,
|
|
13
|
+
NumberFieldProps as RACNumberFieldProps,
|
|
14
|
+
} from "react-aria-components";
|
|
15
|
+
import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
|
|
16
|
+
import { css, cx } from "styled-system/css";
|
|
17
|
+
import { field, input, numberField } from "styled-system/recipes";
|
|
18
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
19
|
+
import { Icon } from "./Icon";
|
|
20
|
+
|
|
21
|
+
export interface NumberFieldProps
|
|
22
|
+
extends Omit<RACNumberFieldProps, "className" | "children" | "style"> {
|
|
23
|
+
/** Visible label (optional; otherwise pass `aria-label`). */
|
|
24
|
+
label?: ReactNode;
|
|
25
|
+
/** Root style overrides (e.g. row layout for label-beside-field forms). */
|
|
26
|
+
css?: SystemStyleObject;
|
|
27
|
+
/** Label style overrides. */
|
|
28
|
+
labelCss?: SystemStyleObject;
|
|
29
|
+
/** Group (input + steppers) style overrides — set `width` here. */
|
|
30
|
+
groupCss?: SystemStyleObject;
|
|
31
|
+
/** Input style overrides (e.g. a smaller size than the recipe's md). */
|
|
32
|
+
inputCss?: SystemStyleObject;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* NumberField — react-aria-components <NumberField> styled like Chakra's
|
|
37
|
+
* NumberInput (outline input + right-hand stepper column). The ref is
|
|
38
|
+
* forwarded to the input element. Value clamping to min/maxValue is
|
|
39
|
+
* handled by react-aria; onChange receives NaN when the field is emptied.
|
|
40
|
+
*/
|
|
41
|
+
export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
42
|
+
function NumberField(
|
|
43
|
+
{ label, css: cssProp, labelCss, groupCss, inputCss, ...rest },
|
|
44
|
+
ref,
|
|
45
|
+
) {
|
|
46
|
+
const slots = numberField();
|
|
47
|
+
const fieldSlots = field();
|
|
48
|
+
return (
|
|
49
|
+
<RACNumberField
|
|
50
|
+
{...rest}
|
|
51
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined)}
|
|
52
|
+
>
|
|
53
|
+
{label != null && (
|
|
54
|
+
<RACLabel
|
|
55
|
+
className={cx(
|
|
56
|
+
fieldSlots.label,
|
|
57
|
+
labelCss ? css(labelCss) : undefined,
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
{label}
|
|
61
|
+
</RACLabel>
|
|
62
|
+
)}
|
|
63
|
+
<RACGroup
|
|
64
|
+
className={cx(slots.group, groupCss ? css(groupCss) : undefined)}
|
|
65
|
+
>
|
|
66
|
+
<RACInput
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={cx(
|
|
69
|
+
input(),
|
|
70
|
+
// Room for the stepper column.
|
|
71
|
+
css({ paddingEnd: "6" }, inputCss),
|
|
72
|
+
)}
|
|
73
|
+
/>
|
|
74
|
+
<div className={slots.stepper}>
|
|
75
|
+
<RACButton slot="increment" className={slots.stepperButton}>
|
|
76
|
+
<Icon as={RiArrowUpSFill} />
|
|
77
|
+
</RACButton>
|
|
78
|
+
<RACButton slot="decrement" className={slots.stepperButton}>
|
|
79
|
+
<Icon as={RiArrowDownSFill} />
|
|
80
|
+
</RACButton>
|
|
81
|
+
</div>
|
|
82
|
+
</RACGroup>
|
|
83
|
+
</RACNumberField>
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { OverlayArrow } from "react-aria-components";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
// react-aria positions the arrow and stamps the resolved side on the wrapper
|
|
11
|
+
// as data-placement, but leaves orienting the glyph to us. The svg is square
|
|
12
|
+
// with the triangle in the half nearest the overlay (tip at centre), so
|
|
13
|
+
// rotating about the centre keeps it flush against the overlay on every side
|
|
14
|
+
// — no translate fix-ups, whose signs depend on rotation direction and are
|
|
15
|
+
// easy to get wrong (they detached the tooltip arrow from its box).
|
|
16
|
+
//
|
|
17
|
+
// "Flush" still antialiases to a hairline seam when the overlay's edge lands
|
|
18
|
+
// on a subpixel boundary, so each placement also pulls the svg 1px into the
|
|
19
|
+
// overlay with a negative margin on the overlay-facing side (margins move the
|
|
20
|
+
// box before the rotate, so the bleed direction is unaffected by it).
|
|
21
|
+
const arrowBase = css({
|
|
22
|
+
"& svg": { display: "block" },
|
|
23
|
+
"&[data-placement='top'] svg": { marginTop: "-1px" },
|
|
24
|
+
"&[data-placement='bottom'] svg": {
|
|
25
|
+
transform: "rotate(180deg)",
|
|
26
|
+
marginBottom: "-1px",
|
|
27
|
+
},
|
|
28
|
+
"&[data-placement='right'] svg": {
|
|
29
|
+
transform: "rotate(90deg)",
|
|
30
|
+
marginRight: "-1px",
|
|
31
|
+
},
|
|
32
|
+
"&[data-placement='left'] svg": {
|
|
33
|
+
transform: "rotate(-90deg)",
|
|
34
|
+
marginLeft: "-1px",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export interface PopoverArrowProps {
|
|
39
|
+
/**
|
|
40
|
+
* Base width of the visible triangle in px; it protrudes half this into
|
|
41
|
+
* the gap. Defaults to Chakra's tooltip arrow proportions (an 8px square
|
|
42
|
+
* rotated 45°).
|
|
43
|
+
*/
|
|
44
|
+
size?: number;
|
|
45
|
+
/** Styles merged after the base, e.g. `{ "& svg": { fill: "white" } }`. */
|
|
46
|
+
css?: SystemStyleObject;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* PopoverArrow — the notch pointing from a popover or tooltip at its
|
|
52
|
+
* trigger, oriented automatically for react-aria's resolved placement.
|
|
53
|
+
* Render inside a RAC Popover/Tooltip; set the fill via the css prop.
|
|
54
|
+
*/
|
|
55
|
+
export const PopoverArrow = ({
|
|
56
|
+
size = 11.314,
|
|
57
|
+
css: cssProp,
|
|
58
|
+
className,
|
|
59
|
+
}: PopoverArrowProps) => (
|
|
60
|
+
<OverlayArrow
|
|
61
|
+
className={cx(arrowBase, cssProp ? css(cssProp) : undefined, className)}
|
|
62
|
+
>
|
|
63
|
+
<svg width={size} height={size} viewBox="0 0 12 12">
|
|
64
|
+
<path d="M0 0 L6 6 L12 0" />
|
|
65
|
+
</svg>
|
|
66
|
+
</OverlayArrow>
|
|
67
|
+
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ProgressBar as RACProgressBar } from "react-aria-components";
|
|
7
|
+
import { css } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
export interface ProgressBarProps {
|
|
11
|
+
/** 0–100. */
|
|
12
|
+
value: number;
|
|
13
|
+
"aria-label": string;
|
|
14
|
+
/** Track overrides (size, radius, width), merged after the base. */
|
|
15
|
+
css?: SystemStyleObject;
|
|
16
|
+
/** Fill overrides (colour), merged after the base. */
|
|
17
|
+
barCss?: SystemStyleObject;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* ProgressBar — react-aria-components <ProgressBar> styled like Chakra's md
|
|
22
|
+
* Progress (12px gray.100 track). Set the fill colour per call site via
|
|
23
|
+
* `barCss` (Chakra's colorScheme).
|
|
24
|
+
*/
|
|
25
|
+
export const ProgressBar = ({
|
|
26
|
+
value,
|
|
27
|
+
"aria-label": ariaLabel,
|
|
28
|
+
css: cssProp,
|
|
29
|
+
barCss,
|
|
30
|
+
}: ProgressBarProps) => (
|
|
31
|
+
<RACProgressBar
|
|
32
|
+
value={value}
|
|
33
|
+
aria-label={ariaLabel}
|
|
34
|
+
// Single css() calls so caller overrides of base properties (radius, fill
|
|
35
|
+
// colour) are deduped at merge time rather than racing on stylesheet order.
|
|
36
|
+
className={css(
|
|
37
|
+
{
|
|
38
|
+
width: "100%",
|
|
39
|
+
height: 3,
|
|
40
|
+
bg: "gray.100",
|
|
41
|
+
overflow: "hidden",
|
|
42
|
+
borderRadius: "sm",
|
|
43
|
+
},
|
|
44
|
+
cssProp,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{({ percentage }) => (
|
|
48
|
+
<div
|
|
49
|
+
className={css(
|
|
50
|
+
{
|
|
51
|
+
height: "100%",
|
|
52
|
+
bg: "progressFilledTrack",
|
|
53
|
+
transition: "width 0.2s",
|
|
54
|
+
},
|
|
55
|
+
barCss,
|
|
56
|
+
)}
|
|
57
|
+
style={{ width: `${percentage ?? 0}%` }}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</RACProgressBar>
|
|
61
|
+
);
|