@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
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
* Switch slot recipe — Chakra's switch with the default blue colorScheme
|
|
10
|
+
* (light mode) and its sm/md/lg size scale. The track has a 2px inset; the
|
|
11
|
+
* thumb matches the track height and slides by the track width/height
|
|
12
|
+
* difference when selected.
|
|
13
|
+
*
|
|
14
|
+
* State styling keys off data attributes stamped by the shared-ui Switch
|
|
15
|
+
* (react-aria provides the state via render props).
|
|
16
|
+
*
|
|
17
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
18
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
19
|
+
*/
|
|
20
|
+
export const switchRecipe = defineSlotRecipe({
|
|
21
|
+
className: "switch",
|
|
22
|
+
slots: ["root", "track", "thumb", "label"],
|
|
23
|
+
base: {
|
|
24
|
+
root: {
|
|
25
|
+
display: "inline-flex",
|
|
26
|
+
alignItems: "center",
|
|
27
|
+
verticalAlign: "top",
|
|
28
|
+
cursor: "pointer",
|
|
29
|
+
position: "relative",
|
|
30
|
+
"&[data-disabled]": { cursor: "not-allowed" },
|
|
31
|
+
},
|
|
32
|
+
track: {
|
|
33
|
+
display: "inline-flex",
|
|
34
|
+
flexShrink: 0,
|
|
35
|
+
justifyContent: "flex-start",
|
|
36
|
+
boxSizing: "content-box",
|
|
37
|
+
borderRadius: "full",
|
|
38
|
+
p: "0.5",
|
|
39
|
+
transitionProperty: "background-color",
|
|
40
|
+
transitionDuration: "fast",
|
|
41
|
+
bg: "gray.300",
|
|
42
|
+
"&[data-selected]": {
|
|
43
|
+
bg: "controlCheckedBg",
|
|
44
|
+
},
|
|
45
|
+
"&[data-focus-visible]": {
|
|
46
|
+
focusShadow: "outline",
|
|
47
|
+
},
|
|
48
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
49
|
+
// Forced-colors modes strip author backgrounds, flattening track and
|
|
50
|
+
// thumb to the same colour; a border keeps the track visible and the
|
|
51
|
+
// system SelectedItem pair conveys on/off.
|
|
52
|
+
_forcedColors: {
|
|
53
|
+
borderWidth: "1px",
|
|
54
|
+
borderStyle: "solid",
|
|
55
|
+
"&[data-selected]": { bg: "SelectedItem" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
thumb: {
|
|
59
|
+
bg: "white",
|
|
60
|
+
transitionProperty: "transform",
|
|
61
|
+
transitionDuration: "normal",
|
|
62
|
+
borderRadius: "inherit",
|
|
63
|
+
_forcedColors: {
|
|
64
|
+
bg: "ButtonText",
|
|
65
|
+
"&[data-selected]": { bg: "SelectedItemText" },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
label: {
|
|
69
|
+
userSelect: "none",
|
|
70
|
+
marginStart: "2",
|
|
71
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
variants: {
|
|
75
|
+
// Chakra's Switch size scale (track width x height; the selected-thumb
|
|
76
|
+
// translate is the difference between them).
|
|
77
|
+
size: {
|
|
78
|
+
sm: {
|
|
79
|
+
track: { width: "1.375rem", height: "3" },
|
|
80
|
+
thumb: {
|
|
81
|
+
width: "3",
|
|
82
|
+
height: "3",
|
|
83
|
+
"&[data-selected]": { transform: "translateX(0.625rem)" },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
md: {
|
|
87
|
+
track: { width: "1.875rem", height: "4" },
|
|
88
|
+
thumb: {
|
|
89
|
+
width: "4",
|
|
90
|
+
height: "4",
|
|
91
|
+
"&[data-selected]": { transform: "translateX(0.875rem)" },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
lg: {
|
|
95
|
+
track: { width: "2.875rem", height: "6" },
|
|
96
|
+
thumb: {
|
|
97
|
+
width: "6",
|
|
98
|
+
height: "6",
|
|
99
|
+
"&[data-selected]": { transform: "translateX(1.375rem)" },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
defaultVariants: {
|
|
105
|
+
size: "md",
|
|
106
|
+
},
|
|
107
|
+
});
|
package/src/Switch.tsx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Switch as RACSwitch,
|
|
9
|
+
SwitchProps as RACSwitchProps,
|
|
10
|
+
} from "react-aria-components";
|
|
11
|
+
import { css, cx } from "styled-system/css";
|
|
12
|
+
import { switchRecipe, SwitchRecipeVariantProps } from "styled-system/recipes";
|
|
13
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
14
|
+
|
|
15
|
+
export interface SwitchProps
|
|
16
|
+
extends Omit<RACSwitchProps, "className" | "children" | "style">,
|
|
17
|
+
SwitchRecipeVariantProps {
|
|
18
|
+
/** Per-instance style overrides for the root, merged after the recipe. */
|
|
19
|
+
css?: SystemStyleObject;
|
|
20
|
+
className?: string;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Switch — react-aria-components <Switch> styled like Chakra's switch.
|
|
26
|
+
* Children render as the label; pass `aria-label` for label-less switches.
|
|
27
|
+
*/
|
|
28
|
+
export const Switch = ({
|
|
29
|
+
size,
|
|
30
|
+
css: cssProp,
|
|
31
|
+
className,
|
|
32
|
+
children,
|
|
33
|
+
...rest
|
|
34
|
+
}: SwitchProps) => {
|
|
35
|
+
const slots = switchRecipe({ size });
|
|
36
|
+
return (
|
|
37
|
+
<RACSwitch
|
|
38
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
39
|
+
{...rest}
|
|
40
|
+
>
|
|
41
|
+
{({ isSelected, isFocusVisible, isDisabled }) => {
|
|
42
|
+
const state = {
|
|
43
|
+
"data-selected": isSelected || undefined,
|
|
44
|
+
"data-focus-visible": isFocusVisible || undefined,
|
|
45
|
+
"data-disabled": isDisabled || undefined,
|
|
46
|
+
};
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<span className={slots.track} {...state} aria-hidden>
|
|
50
|
+
<span className={slots.thumb} {...state} />
|
|
51
|
+
</span>
|
|
52
|
+
{children != null && (
|
|
53
|
+
<span className={slots.label} {...state}>
|
|
54
|
+
{children}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}}
|
|
60
|
+
</RACSwitch>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { defineRecipe } from "@pandacss/dev";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Text recipe — Chakra's <Text> had no styles of its own, but an app theme
|
|
10
|
+
* could give it sizes and a default size via `components.Text`
|
|
11
|
+
* (python-editor's did: sm/md with a default of md, which its shrunken
|
|
12
|
+
* fontSize scale renders at 0.9rem). A config recipe so an app preset can
|
|
13
|
+
* restore that: extend `defaultVariants` to set an app-wide default size.
|
|
14
|
+
* No default here — with no `size`, Text inherits like Chakra's default,
|
|
15
|
+
* which is what the other family apps expect.
|
|
16
|
+
*
|
|
17
|
+
* Registered in the base preset (base-preset.ts).
|
|
18
|
+
*/
|
|
19
|
+
export const text = defineRecipe({
|
|
20
|
+
className: "text",
|
|
21
|
+
jsx: ["Text"],
|
|
22
|
+
variants: {
|
|
23
|
+
size: {
|
|
24
|
+
sm: { fontSize: "sm" },
|
|
25
|
+
md: { fontSize: "md" },
|
|
26
|
+
lg: { fontSize: "lg" },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
package/src/Text.tsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { styled } from "styled-system/jsx";
|
|
7
|
+
import { text } from "styled-system/recipes";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Text — a paragraph that accepts Panda style props. Use `as` to render a
|
|
11
|
+
* different element (`span`, `div`, `h2`). Replaces Chakra's <Text>.
|
|
12
|
+
* Backed by the `text` config recipe: `size` picks a type-scale size, and an
|
|
13
|
+
* app preset can extend the recipe's `defaultVariants` to give all its Text
|
|
14
|
+
* a default size (Chakra `components.Text` parity — see Text.recipe.ts).
|
|
15
|
+
*/
|
|
16
|
+
export const Text = styled("p", text);
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
* Field slot recipe — Chakra's FormControl parts (FormLabel/FormHelperText/
|
|
10
|
+
* FormErrorMessage, light mode). Consumed by the shared-ui TextField, which
|
|
11
|
+
* maps the slots onto react-aria-components' TextField/Label/Text/FieldError;
|
|
12
|
+
* the input itself is styled by the `input` recipe (Input.recipe.ts).
|
|
13
|
+
*
|
|
14
|
+
* Registered in the base preset (base-preset.ts). No variants, so it
|
|
15
|
+
* needs no `staticCss` entry.
|
|
16
|
+
*/
|
|
17
|
+
export const field = defineSlotRecipe({
|
|
18
|
+
className: "field",
|
|
19
|
+
slots: ["root", "label", "requiredIndicator", "helperText", "errorMessage"],
|
|
20
|
+
base: {
|
|
21
|
+
root: {
|
|
22
|
+
display: "flex",
|
|
23
|
+
flexDirection: "column",
|
|
24
|
+
alignItems: "stretch",
|
|
25
|
+
width: "100%",
|
|
26
|
+
},
|
|
27
|
+
label: {
|
|
28
|
+
display: "block",
|
|
29
|
+
fontSize: "md",
|
|
30
|
+
fontWeight: "medium",
|
|
31
|
+
marginEnd: "3",
|
|
32
|
+
mb: "2",
|
|
33
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
34
|
+
},
|
|
35
|
+
requiredIndicator: {
|
|
36
|
+
marginStart: "1",
|
|
37
|
+
color: "danger.500",
|
|
38
|
+
},
|
|
39
|
+
helperText: {
|
|
40
|
+
mt: "2",
|
|
41
|
+
fontSize: "sm",
|
|
42
|
+
lineHeight: "normal",
|
|
43
|
+
color: "gray.600",
|
|
44
|
+
},
|
|
45
|
+
errorMessage: {
|
|
46
|
+
display: "flex",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
mt: "2",
|
|
49
|
+
fontSize: "sm",
|
|
50
|
+
lineHeight: "normal",
|
|
51
|
+
color: "danger.500",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { FocusEvent, forwardRef, ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
FieldError,
|
|
9
|
+
Input as RACInput,
|
|
10
|
+
Label as RACLabel,
|
|
11
|
+
Text as RACText,
|
|
12
|
+
TextField as RACTextField,
|
|
13
|
+
TextFieldProps as RACTextFieldProps,
|
|
14
|
+
} from "react-aria-components";
|
|
15
|
+
import { css, cx } from "styled-system/css";
|
|
16
|
+
import { field, input, InputVariantProps } from "styled-system/recipes";
|
|
17
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
18
|
+
|
|
19
|
+
export interface TextFieldProps
|
|
20
|
+
extends Omit<
|
|
21
|
+
RACTextFieldProps,
|
|
22
|
+
"className" | "children" | "style" | "onFocus" | "onBlur"
|
|
23
|
+
>,
|
|
24
|
+
InputVariantProps {
|
|
25
|
+
/** Visible label (Chakra's FormLabel; asterisk added when `isRequired`). */
|
|
26
|
+
label: ReactNode;
|
|
27
|
+
/** Help text below the input (Chakra's FormHelperText). */
|
|
28
|
+
helperText?: ReactNode;
|
|
29
|
+
/** Shown below the input when `isInvalid` (Chakra's FormErrorMessage). */
|
|
30
|
+
errorMessage?: ReactNode;
|
|
31
|
+
/** Per-instance style overrides for the helper text. */
|
|
32
|
+
helperTextCss?: SystemStyleObject;
|
|
33
|
+
onFocus?: (e: FocusEvent<HTMLInputElement>) => void;
|
|
34
|
+
/** Input autocapitalize attribute (react-aria's TextField omits it). */
|
|
35
|
+
autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* TextField — a labelled single-line text input, collapsing Chakra's
|
|
40
|
+
* FormControl/FormLabel/Input/FormHelperText/FormErrorMessage. The ref is
|
|
41
|
+
* forwarded to the input element.
|
|
42
|
+
*/
|
|
43
|
+
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
44
|
+
function TextField(
|
|
45
|
+
{
|
|
46
|
+
label,
|
|
47
|
+
helperText,
|
|
48
|
+
errorMessage,
|
|
49
|
+
helperTextCss,
|
|
50
|
+
onFocus,
|
|
51
|
+
autoCapitalize,
|
|
52
|
+
size,
|
|
53
|
+
...rest
|
|
54
|
+
},
|
|
55
|
+
ref,
|
|
56
|
+
) {
|
|
57
|
+
const slots = field();
|
|
58
|
+
return (
|
|
59
|
+
<RACTextField {...rest} className={slots.root}>
|
|
60
|
+
<RACLabel className={slots.label}>
|
|
61
|
+
{label}
|
|
62
|
+
{rest.isRequired ? (
|
|
63
|
+
<span aria-hidden className={slots.requiredIndicator}>
|
|
64
|
+
*
|
|
65
|
+
</span>
|
|
66
|
+
) : null}
|
|
67
|
+
</RACLabel>
|
|
68
|
+
<RACInput
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={input({ size })}
|
|
71
|
+
onFocus={onFocus}
|
|
72
|
+
autoCapitalize={autoCapitalize}
|
|
73
|
+
/>
|
|
74
|
+
{helperText && (
|
|
75
|
+
<RACText
|
|
76
|
+
slot="description"
|
|
77
|
+
className={cx(
|
|
78
|
+
slots.helperText,
|
|
79
|
+
helperTextCss ? css(helperTextCss) : undefined,
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{helperText}
|
|
83
|
+
</RACText>
|
|
84
|
+
)}
|
|
85
|
+
<FieldError className={slots.errorMessage}>{errorMessage}</FieldError>
|
|
86
|
+
</RACTextField>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
);
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
* Toast slot recipe — the Chakra-era solid Alert restyled as a toast (white
|
|
10
|
+
* text, teal for every status except error). Status colours come from the
|
|
11
|
+
* `toast*Bg` semantic tokens so brand presets can retune them without
|
|
12
|
+
* touching the recipe.
|
|
13
|
+
*
|
|
14
|
+
* Registered in the base preset (base-preset.ts). The status variant
|
|
15
|
+
* is chosen at runtime from the toast content, so it needs the preset's
|
|
16
|
+
* `staticCss` entry.
|
|
17
|
+
*/
|
|
18
|
+
export const toast = defineSlotRecipe({
|
|
19
|
+
className: "toast",
|
|
20
|
+
slots: ["region", "root", "icon", "title", "description", "closeButton"],
|
|
21
|
+
base: {
|
|
22
|
+
region: {
|
|
23
|
+
position: "fixed",
|
|
24
|
+
top: "4",
|
|
25
|
+
left: "50%",
|
|
26
|
+
transform: "translateX(-50%)",
|
|
27
|
+
zIndex: "toast",
|
|
28
|
+
display: "flex",
|
|
29
|
+
flexDirection: "column",
|
|
30
|
+
gap: "2",
|
|
31
|
+
pointerEvents: "none",
|
|
32
|
+
},
|
|
33
|
+
root: {
|
|
34
|
+
position: "relative",
|
|
35
|
+
display: "flex",
|
|
36
|
+
alignItems: "flex-start",
|
|
37
|
+
gap: "3",
|
|
38
|
+
p: "4",
|
|
39
|
+
// Leave room for the absolutely-positioned close button plus breathing
|
|
40
|
+
// space so its hover area never overlaps the title text (24px button at
|
|
41
|
+
// 4px inset spans ~4-28px from the right; content stops at 40px for a
|
|
42
|
+
// ~12px gap).
|
|
43
|
+
paddingRight: "10",
|
|
44
|
+
borderRadius: "md",
|
|
45
|
+
boxShadow: "lg",
|
|
46
|
+
color: "white",
|
|
47
|
+
maxW: "sm",
|
|
48
|
+
pointerEvents: "auto",
|
|
49
|
+
},
|
|
50
|
+
icon: {
|
|
51
|
+
fontSize: "1.25rem",
|
|
52
|
+
flexShrink: 0,
|
|
53
|
+
},
|
|
54
|
+
title: {
|
|
55
|
+
fontWeight: "bold",
|
|
56
|
+
},
|
|
57
|
+
description: {
|
|
58
|
+
mt: "1",
|
|
59
|
+
},
|
|
60
|
+
closeButton: {
|
|
61
|
+
// A sized box with the glyph centred (like Chakra's CloseButton size
|
|
62
|
+
// "sm": 24px box, 2xs glyph) gives padding around the X and a hover
|
|
63
|
+
// affordance.
|
|
64
|
+
position: "absolute",
|
|
65
|
+
top: "1",
|
|
66
|
+
insetEnd: "1",
|
|
67
|
+
color: "white",
|
|
68
|
+
display: "inline-flex",
|
|
69
|
+
alignItems: "center",
|
|
70
|
+
justifyContent: "center",
|
|
71
|
+
width: "6",
|
|
72
|
+
height: "6",
|
|
73
|
+
padding: "0",
|
|
74
|
+
fontSize: "2xs",
|
|
75
|
+
borderRadius: "md",
|
|
76
|
+
cursor: "pointer",
|
|
77
|
+
bg: "transparent",
|
|
78
|
+
border: "none",
|
|
79
|
+
outline: "none",
|
|
80
|
+
transitionProperty: "background-color, box-shadow",
|
|
81
|
+
transitionDuration: "normal",
|
|
82
|
+
// Chakra's CloseButton hover is a subtle dark overlay (blackAlpha) in
|
|
83
|
+
// light mode, not a bright highlight.
|
|
84
|
+
_hover: { bg: "blackAlpha.100" },
|
|
85
|
+
_active: { bg: "blackAlpha.200" },
|
|
86
|
+
_focusVisible: { focusShadow: "outline" },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
variants: {
|
|
90
|
+
status: {
|
|
91
|
+
info: { root: { bg: "toastInfoBg" } },
|
|
92
|
+
success: { root: { bg: "toastSuccessBg" } },
|
|
93
|
+
warning: { root: { bg: "toastWarningBg" } },
|
|
94
|
+
error: { root: { bg: "toastErrorBg" } },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
defaultVariants: { status: "info" },
|
|
98
|
+
});
|
package/src/Toast.tsx
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ReactNode, useMemo } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Button as RACButton,
|
|
9
|
+
UNSTABLE_Toast as RACToast,
|
|
10
|
+
UNSTABLE_ToastContent as RACToastContent,
|
|
11
|
+
UNSTABLE_ToastQueue as RACToastQueue,
|
|
12
|
+
UNSTABLE_ToastRegion as RACToastRegion,
|
|
13
|
+
} from "react-aria-components";
|
|
14
|
+
import { useIntl } from "react-intl";
|
|
15
|
+
import { IconType } from "react-icons/lib";
|
|
16
|
+
import {
|
|
17
|
+
RiAlertFill,
|
|
18
|
+
RiCheckboxCircleFill,
|
|
19
|
+
RiErrorWarningFill,
|
|
20
|
+
RiInformationFill,
|
|
21
|
+
} from "react-icons/ri";
|
|
22
|
+
import { toast as toastRecipe } from "styled-system/recipes";
|
|
23
|
+
import { CloseIcon } from "./CloseIcon";
|
|
24
|
+
import { Icon } from "./Icon";
|
|
25
|
+
import { uiMessage } from "./messages";
|
|
26
|
+
import { VisuallyHidden } from "./VisuallyHidden";
|
|
27
|
+
|
|
28
|
+
export type ToastStatus = "info" | "success" | "warning" | "error";
|
|
29
|
+
|
|
30
|
+
export interface ToastContent {
|
|
31
|
+
/** Dedup key: adding a toast whose id is already visible is a no-op. */
|
|
32
|
+
id?: string;
|
|
33
|
+
title?: ReactNode;
|
|
34
|
+
description?: ReactNode;
|
|
35
|
+
status?: ToastStatus;
|
|
36
|
+
isClosable?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Module-level queue shared by useToast() and the <ToastProvider/> region.
|
|
40
|
+
// (RAC's Toast API is still flagged UNSTABLE_*; the surface is small and behind
|
|
41
|
+
// this module, so a swap to a custom queue later is contained.)
|
|
42
|
+
export const toastQueue = new RACToastQueue<ToastContent>({
|
|
43
|
+
maxVisibleToasts: 5,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Status icon matching Chakra's AlertIcon (filled glyphs, coloured by the
|
|
47
|
+
// toast foreground = white here). Warning is a triangle, error a circle, as
|
|
48
|
+
// in Chakra — the glyph must distinguish them because the colours alone
|
|
49
|
+
// don't reliably. The icon is decorative; the status is announced via the
|
|
50
|
+
// visually hidden status text.
|
|
51
|
+
const statusIcon: Record<ToastStatus, IconType> = {
|
|
52
|
+
info: RiInformationFill,
|
|
53
|
+
success: RiCheckboxCircleFill,
|
|
54
|
+
warning: RiAlertFill,
|
|
55
|
+
error: RiErrorWarningFill,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mount once near the app root, inside the IntlProvider (the close button's
|
|
60
|
+
* label and the status announcements are react-intl messages).
|
|
61
|
+
* Renders the live region that announces and displays queued toasts.
|
|
62
|
+
*/
|
|
63
|
+
export const ToastProvider = () => {
|
|
64
|
+
const intl = useIntl();
|
|
65
|
+
const slots = toastRecipe();
|
|
66
|
+
return (
|
|
67
|
+
<RACToastRegion queue={toastQueue} className={slots.region}>
|
|
68
|
+
{({ toast }) => {
|
|
69
|
+
const status = toast.content.status ?? "info";
|
|
70
|
+
return (
|
|
71
|
+
<RACToast toast={toast} className={toastRecipe({ status }).root}>
|
|
72
|
+
<Icon as={statusIcon[status]} className={slots.icon} aria-hidden />
|
|
73
|
+
<RACToastContent>
|
|
74
|
+
{/* Colour and icon are the only visible status signals; say it
|
|
75
|
+
for assistive tech too. */}
|
|
76
|
+
<VisuallyHidden>
|
|
77
|
+
{intl.formatMessage(uiMessage(`ui.toast-status-${status}`))}{" "}
|
|
78
|
+
</VisuallyHidden>
|
|
79
|
+
{toast.content.title && (
|
|
80
|
+
<p className={slots.title}>{toast.content.title}</p>
|
|
81
|
+
)}
|
|
82
|
+
{toast.content.description && (
|
|
83
|
+
<div className={slots.description}>
|
|
84
|
+
{toast.content.description}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</RACToastContent>
|
|
88
|
+
{toast.content.isClosable && (
|
|
89
|
+
<RACButton
|
|
90
|
+
slot="close"
|
|
91
|
+
aria-label={intl.formatMessage(uiMessage("ui.close-action"))}
|
|
92
|
+
className={slots.closeButton}
|
|
93
|
+
>
|
|
94
|
+
<CloseIcon />
|
|
95
|
+
</RACButton>
|
|
96
|
+
)}
|
|
97
|
+
</RACToast>
|
|
98
|
+
);
|
|
99
|
+
}}
|
|
100
|
+
</RACToastRegion>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export interface ToastOptions extends ToastContent {
|
|
105
|
+
/**
|
|
106
|
+
* Auto-dismiss after ms; null means no auto-dismiss (Chakra's semantics).
|
|
107
|
+
* RAC enforces a 5000ms minimum for accessibility.
|
|
108
|
+
*/
|
|
109
|
+
duration?: number | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ToastFn {
|
|
113
|
+
(options: ToastOptions): void;
|
|
114
|
+
/** Whether a toast with this id is currently visible. */
|
|
115
|
+
isActive(id: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Replace a visible toast's content (Chakra's toast.update). The toast is
|
|
118
|
+
* re-added, so unlike Chakra it re-animates and restarts any timeout.
|
|
119
|
+
*/
|
|
120
|
+
update(id: string, options: ToastOptions): void;
|
|
121
|
+
/** Dismiss all visible toasts (Chakra's toast.closeAll). */
|
|
122
|
+
closeAll(): void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* useToast — imperative toast trigger matching the shape of Chakra's
|
|
127
|
+
* `useToast()` call sites: `toast({ title, description, status, duration })`.
|
|
128
|
+
*/
|
|
129
|
+
export const useToast = (): ToastFn =>
|
|
130
|
+
useMemo(() => {
|
|
131
|
+
const isActive = (id: string) =>
|
|
132
|
+
toastQueue.visibleToasts.some((t) => t.content.id === id);
|
|
133
|
+
const add = ({
|
|
134
|
+
id,
|
|
135
|
+
title,
|
|
136
|
+
description,
|
|
137
|
+
status,
|
|
138
|
+
isClosable,
|
|
139
|
+
duration,
|
|
140
|
+
}: ToastOptions) => {
|
|
141
|
+
if (id && isActive(id)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
toastQueue.add(
|
|
145
|
+
{
|
|
146
|
+
id,
|
|
147
|
+
title,
|
|
148
|
+
description,
|
|
149
|
+
status,
|
|
150
|
+
// A toast that never times out must be dismissable or it is
|
|
151
|
+
// permanent and unremovable; force the close button on.
|
|
152
|
+
isClosable: isClosable || duration == null,
|
|
153
|
+
},
|
|
154
|
+
{ timeout: duration ?? undefined },
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
const update = (id: string, options: ToastOptions) => {
|
|
158
|
+
const existing = toastQueue.visibleToasts.find(
|
|
159
|
+
(t) => t.content.id === id,
|
|
160
|
+
);
|
|
161
|
+
if (existing) {
|
|
162
|
+
toastQueue.close(existing.key);
|
|
163
|
+
}
|
|
164
|
+
add({ ...options, id });
|
|
165
|
+
};
|
|
166
|
+
const closeAll = () => {
|
|
167
|
+
// Copy first: closing mutates visibleToasts as we iterate.
|
|
168
|
+
[...toastQueue.visibleToasts].forEach((t) => toastQueue.close(t.key));
|
|
169
|
+
};
|
|
170
|
+
return Object.assign(add, { isActive, update, closeAll });
|
|
171
|
+
}, []);
|