@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.
Files changed (86) hide show
  1. package/LICENSE.md +53 -0
  2. package/README.md +220 -0
  3. package/lang/ui.ca.json +22 -0
  4. package/lang/ui.de.json +22 -0
  5. package/lang/ui.en-us.json +22 -0
  6. package/lang/ui.en.json +22 -0
  7. package/lang/ui.es-es.json +22 -0
  8. package/lang/ui.fr.json +22 -0
  9. package/lang/ui.ga-ie.json +22 -0
  10. package/lang/ui.ja.json +22 -0
  11. package/lang/ui.ko.json +22 -0
  12. package/lang/ui.lol.json +22 -0
  13. package/lang/ui.nl.json +22 -0
  14. package/lang/ui.pl.json +22 -0
  15. package/lang/ui.pt-br.json +22 -0
  16. package/lang/ui.zh-cn.json +22 -0
  17. package/lang/ui.zh-tw.json +22 -0
  18. package/package.json +58 -0
  19. package/postcss-legacy-safari.cjs +96 -0
  20. package/reset.css +35 -0
  21. package/src/Button.recipe.ts +162 -0
  22. package/src/Button.tsx +69 -0
  23. package/src/ButtonGroup.tsx +63 -0
  24. package/src/Card.recipe.ts +49 -0
  25. package/src/Card.tsx +56 -0
  26. package/src/Checkbox.recipe.ts +105 -0
  27. package/src/Checkbox.tsx +76 -0
  28. package/src/CloseButton.tsx +87 -0
  29. package/src/CloseIcon.tsx +40 -0
  30. package/src/Code.tsx +20 -0
  31. package/src/Collapse.tsx +180 -0
  32. package/src/Divider.tsx +52 -0
  33. package/src/Drawer.recipe.ts +98 -0
  34. package/src/Drawer.tsx +138 -0
  35. package/src/Fade.tsx +48 -0
  36. package/src/Heading.recipe.ts +52 -0
  37. package/src/Heading.tsx +14 -0
  38. package/src/Icon.tsx +55 -0
  39. package/src/IconButton.tsx +44 -0
  40. package/src/Image.tsx +11 -0
  41. package/src/Input.recipe.ts +72 -0
  42. package/src/Input.tsx +40 -0
  43. package/src/InputGroup.tsx +62 -0
  44. package/src/Kbd.tsx +26 -0
  45. package/src/Link.tsx +23 -0
  46. package/src/LinkBox.tsx +88 -0
  47. package/src/LinkButton.tsx +80 -0
  48. package/src/List.tsx +31 -0
  49. package/src/Menu.recipe.ts +119 -0
  50. package/src/Menu.tsx +261 -0
  51. package/src/Modal.recipe.ts +163 -0
  52. package/src/Modal.tsx +279 -0
  53. package/src/NativeSelect.tsx +94 -0
  54. package/src/NumberField.recipe.ts +67 -0
  55. package/src/NumberField.tsx +86 -0
  56. package/src/PopoverArrow.tsx +67 -0
  57. package/src/ProgressBar.tsx +61 -0
  58. package/src/Radio.recipe.ts +107 -0
  59. package/src/Radio.tsx +89 -0
  60. package/src/SharedUIProvider.tsx +55 -0
  61. package/src/Slide.tsx +54 -0
  62. package/src/Slider.recipe.ts +102 -0
  63. package/src/Slider.tsx +163 -0
  64. package/src/Spinner.tsx +69 -0
  65. package/src/Svg.tsx +23 -0
  66. package/src/Switch.recipe.ts +107 -0
  67. package/src/Switch.tsx +62 -0
  68. package/src/Text.recipe.ts +29 -0
  69. package/src/Text.tsx +16 -0
  70. package/src/TextField.recipe.ts +54 -0
  71. package/src/TextField.tsx +89 -0
  72. package/src/Toast.recipe.ts +98 -0
  73. package/src/Toast.tsx +171 -0
  74. package/src/Tooltip.tsx +91 -0
  75. package/src/UnmountCallback.tsx +18 -0
  76. package/src/VisuallyHidden.tsx +14 -0
  77. package/src/base-preset.ts +294 -0
  78. package/src/base-tokens.ts +1017 -0
  79. package/src/button-icon.ts +23 -0
  80. package/src/hooks/useBreakpointValue.ts +63 -0
  81. package/src/hooks/useClipboard.ts +64 -0
  82. package/src/hooks/useMediaQuery.ts +28 -0
  83. package/src/hooks/usePrevious.ts +18 -0
  84. package/src/index.ts +55 -0
  85. package/src/messages.ts +17 -0
  86. 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
+ }, []);