@microbit/ui 0.1.0-alpha.3

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 (67) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +118 -0
  3. package/lang/ui.ca.json +22 -0
  4. package/lang/ui.en-us.json +22 -0
  5. package/lang/ui.en.json +22 -0
  6. package/lang/ui.es-es.json +22 -0
  7. package/lang/ui.fr.json +22 -0
  8. package/lang/ui.ja.json +22 -0
  9. package/lang/ui.ko.json +22 -0
  10. package/lang/ui.lol.json +22 -0
  11. package/lang/ui.nl.json +22 -0
  12. package/lang/ui.pl.json +22 -0
  13. package/lang/ui.pt-br.json +22 -0
  14. package/lang/ui.zh-tw.json +22 -0
  15. package/package.json +54 -0
  16. package/src/Button.recipe.ts +162 -0
  17. package/src/Button.tsx +84 -0
  18. package/src/ButtonGroup.tsx +63 -0
  19. package/src/Card.recipe.ts +49 -0
  20. package/src/Card.tsx +56 -0
  21. package/src/Checkbox.recipe.ts +71 -0
  22. package/src/Checkbox.tsx +66 -0
  23. package/src/CloseButton.tsx +87 -0
  24. package/src/CloseIcon.tsx +40 -0
  25. package/src/Divider.tsx +21 -0
  26. package/src/Drawer.recipe.ts +98 -0
  27. package/src/Drawer.tsx +138 -0
  28. package/src/Heading.recipe.ts +52 -0
  29. package/src/Heading.tsx +14 -0
  30. package/src/Icon.tsx +55 -0
  31. package/src/IconButton.tsx +39 -0
  32. package/src/Image.tsx +11 -0
  33. package/src/Input.recipe.ts +63 -0
  34. package/src/Input.tsx +33 -0
  35. package/src/InputGroup.tsx +48 -0
  36. package/src/Link.tsx +23 -0
  37. package/src/LinkBox.tsx +88 -0
  38. package/src/List.tsx +27 -0
  39. package/src/Menu.recipe.ts +88 -0
  40. package/src/Menu.tsx +172 -0
  41. package/src/Modal.recipe.ts +163 -0
  42. package/src/Modal.tsx +265 -0
  43. package/src/NativeSelect.tsx +76 -0
  44. package/src/PopoverArrow.tsx +52 -0
  45. package/src/ProgressBar.tsx +61 -0
  46. package/src/SharedUIProvider.tsx +55 -0
  47. package/src/Slide.tsx +54 -0
  48. package/src/Slider.recipe.ts +91 -0
  49. package/src/Slider.tsx +99 -0
  50. package/src/Spinner.tsx +69 -0
  51. package/src/Svg.tsx +23 -0
  52. package/src/Switch.recipe.ts +80 -0
  53. package/src/Switch.tsx +60 -0
  54. package/src/Text.tsx +12 -0
  55. package/src/TextField.recipe.ts +54 -0
  56. package/src/TextField.tsx +72 -0
  57. package/src/Toast.recipe.ts +98 -0
  58. package/src/Toast.tsx +165 -0
  59. package/src/Tooltip.tsx +91 -0
  60. package/src/UnmountCallback.tsx +18 -0
  61. package/src/VisuallyHidden.tsx +14 -0
  62. package/src/base-preset.ts +278 -0
  63. package/src/chakra-tokens.ts +1014 -0
  64. package/src/hooks/useBreakpointValue.ts +63 -0
  65. package/src/index.ts +45 -0
  66. package/src/messages.ts +17 -0
  67. package/src/system.ts +36 -0
@@ -0,0 +1,52 @@
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
+ const arrowBase = css({
17
+ "& svg": { display: "block" },
18
+ "&[data-placement='bottom'] svg": { transform: "rotate(180deg)" },
19
+ "&[data-placement='right'] svg": { transform: "rotate(90deg)" },
20
+ "&[data-placement='left'] svg": { transform: "rotate(-90deg)" },
21
+ });
22
+
23
+ export interface PopoverArrowProps {
24
+ /**
25
+ * Base width of the visible triangle in px; it protrudes half this into
26
+ * the gap. Defaults to Chakra's tooltip arrow proportions (an 8px square
27
+ * rotated 45°).
28
+ */
29
+ size?: number;
30
+ /** Styles merged after the base, e.g. `{ "& svg": { fill: "white" } }`. */
31
+ css?: SystemStyleObject;
32
+ className?: string;
33
+ }
34
+
35
+ /**
36
+ * PopoverArrow — the notch pointing from a popover or tooltip at its
37
+ * trigger, oriented automatically for react-aria's resolved placement.
38
+ * Render inside a RAC Popover/Tooltip; set the fill via the css prop.
39
+ */
40
+ export const PopoverArrow = ({
41
+ size = 11.314,
42
+ css: cssProp,
43
+ className,
44
+ }: PopoverArrowProps) => (
45
+ <OverlayArrow
46
+ className={cx(arrowBase, cssProp ? css(cssProp) : undefined, className)}
47
+ >
48
+ <svg width={size} height={size} viewBox="0 0 12 12">
49
+ <path d="M0 0 L6 6 L12 0" />
50
+ </svg>
51
+ </OverlayArrow>
52
+ );
@@ -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
+ );
@@ -0,0 +1,55 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { createContext, ReactNode, useContext, useMemo } from "react";
7
+
8
+ /**
9
+ * Registers the close function of the currently open dismissable overlay, or
10
+ * clears it (null) when the overlay closes. Installed by apps that need to
11
+ * dismiss overlays from outside the component tree — e.g. the Android
12
+ * hardware back button. Only one overlay is open at a time.
13
+ */
14
+ export type OverlayCloseRegistrar = (close: (() => void) | null) => void;
15
+
16
+ interface SharedUIContextValue {
17
+ overlayCloseRegistrar?: OverlayCloseRegistrar;
18
+ }
19
+
20
+ const SharedUIContext = createContext<SharedUIContextValue | null>(null);
21
+
22
+ export interface SharedUIProviderProps {
23
+ overlayCloseRegistrar?: OverlayCloseRegistrar;
24
+ children: ReactNode;
25
+ }
26
+
27
+ /**
28
+ * SharedUIProvider — the app-side installation point for optional shared-ui
29
+ * integrations. Currently that is only the overlay-close registrar, so apps
30
+ * without one can omit the provider entirely. Localized strings come from
31
+ * react-intl: an IntlProvider must be mounted above shared-ui components
32
+ * (see the package README for merging this package's message catalogs).
33
+ */
34
+ export const SharedUIProvider = ({
35
+ overlayCloseRegistrar,
36
+ children,
37
+ }: SharedUIProviderProps) => {
38
+ const value = useMemo(
39
+ () => ({ overlayCloseRegistrar }),
40
+ [overlayCloseRegistrar],
41
+ );
42
+ return (
43
+ <SharedUIContext.Provider value={value}>
44
+ {children}
45
+ </SharedUIContext.Provider>
46
+ );
47
+ };
48
+
49
+ /**
50
+ * The app-installed overlay-close registrar, if any. Overlay components run
51
+ * controlled and register their close function while open so the app can
52
+ * dismiss them; without a registrar they manage open state internally.
53
+ */
54
+ export const useOverlayCloseRegistrar = (): OverlayCloseRegistrar | undefined =>
55
+ useContext(SharedUIContext)?.overlayCloseRegistrar;
package/src/Slide.tsx ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { ReactNode } from "react";
7
+ import { css, cx } from "styled-system/css";
8
+ import { SystemStyleObject } from "styled-system/types";
9
+
10
+ export interface SlideProps {
11
+ /** Slides in when true; sits translated offscreen when false. */
12
+ isOpen: boolean;
13
+ /** Per-instance style overrides (e.g. zIndex), merged after the base. */
14
+ css?: SystemStyleObject;
15
+ className?: string;
16
+ children: ReactNode;
17
+ }
18
+
19
+ /**
20
+ * Slide — Chakra's Slide transition, bottom edge only (add other directions
21
+ * when a use appears): a full-width panel pinned to the viewport bottom,
22
+ * translated offscreen when closed. Content stays mounted throughout, so
23
+ * closing doesn't drop in-flight state and the exit animates.
24
+ */
25
+ export const Slide = ({
26
+ isOpen,
27
+ css: cssProp,
28
+ className,
29
+ children,
30
+ }: SlideProps) => (
31
+ <div
32
+ data-open={isOpen ? "" : undefined}
33
+ className={cx(
34
+ css(
35
+ {
36
+ position: "fixed",
37
+ left: 0,
38
+ right: 0,
39
+ bottom: 0,
40
+ transform: "translateY(100%)",
41
+ "&[data-open]": { transform: "translateY(0)" },
42
+ transitionProperty: "transform",
43
+ transitionDuration: "0.25s",
44
+ transitionTimingFunction: "cubic-bezier(0, 0, 0.2, 1)",
45
+ _motionReduce: { transition: "none" },
46
+ },
47
+ cssProp,
48
+ ),
49
+ className,
50
+ )}
51
+ >
52
+ {children}
53
+ </div>
54
+ );
@@ -0,0 +1,91 @@
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
+ * Slider slot recipe — Chakra's horizontal md slider (14px thumb, 4px
10
+ * gray.200 track, blue filled track; light mode). Consumed by the shared-ui
11
+ * Slider, which maps the slots onto react-aria-components'
12
+ * Slider/SliderTrack/SliderThumb.
13
+ *
14
+ * The `mark` slot is hidden until the slider has focus (Chakra call sites did
15
+ * this with `_focusWithin` + a class); the reveal lives here so call sites
16
+ * only style the mark's look.
17
+ *
18
+ * Registered in the base preset (base-preset.ts). No variants, so it
19
+ * needs no `staticCss` entry.
20
+ */
21
+ export const slider = defineSlotRecipe({
22
+ className: "slider",
23
+ slots: ["root", "track", "filledTrack", "thumb", "mark"],
24
+ base: {
25
+ root: {
26
+ position: "relative",
27
+ display: "block",
28
+ width: "100%",
29
+ touchAction: "none",
30
+ // Half the thumb so it doesn't overflow the ends (Chakra container px).
31
+ px: "calc(14px / 2)",
32
+ "&:focus-within [data-part='mark']": {
33
+ display: "block",
34
+ },
35
+ "&[data-disabled]": {
36
+ opacity: 0.4,
37
+ cursor: "not-allowed",
38
+ },
39
+ },
40
+ track: {
41
+ position: "relative",
42
+ height: "4px",
43
+ overflow: "hidden",
44
+ borderRadius: "sm",
45
+ bg: "gray.200",
46
+ cursor: "pointer",
47
+ "[data-disabled] &": {
48
+ bg: "gray.300",
49
+ cursor: "default",
50
+ },
51
+ },
52
+ filledTrack: {
53
+ position: "absolute",
54
+ top: 0,
55
+ left: 0,
56
+ height: "100%",
57
+ bg: "sliderFilledTrack",
58
+ },
59
+ thumb: {
60
+ position: "absolute",
61
+ top: "50%",
62
+ width: "14px",
63
+ height: "14px",
64
+ display: "flex",
65
+ alignItems: "center",
66
+ justifyContent: "center",
67
+ outline: "none",
68
+ zIndex: 1,
69
+ borderRadius: "full",
70
+ bg: "white",
71
+ boxShadow: "base",
72
+ transitionProperty: "transform",
73
+ transitionDuration: "normal",
74
+ "&[data-dragging]": {
75
+ transform: "translate(-50%, -50%) scale(1.15)",
76
+ },
77
+ transform: "translate(-50%, -50%)",
78
+ "&[data-focus-visible]": {
79
+ focusShadow: "outline",
80
+ },
81
+ "&[data-disabled]": {
82
+ bg: "gray.300",
83
+ },
84
+ },
85
+ mark: {
86
+ display: "none",
87
+ position: "absolute",
88
+ whiteSpace: "nowrap",
89
+ },
90
+ },
91
+ });
package/src/Slider.tsx ADDED
@@ -0,0 +1,99 @@
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
+ Slider as RACSlider,
9
+ SliderThumb,
10
+ SliderTrack,
11
+ } from "react-aria-components";
12
+ import { css, cx } from "styled-system/css";
13
+ import { slider } from "styled-system/recipes";
14
+ import { SystemStyleObject } from "styled-system/types";
15
+
16
+ export interface SliderProps {
17
+ value: number;
18
+ onChange: (value: number) => void;
19
+ minValue?: number;
20
+ maxValue?: number;
21
+ "aria-label": string;
22
+ isDisabled?: boolean;
23
+ /**
24
+ * Number format for the value announced to assistive tech (e.g.
25
+ * `{ style: "unit", unit: "percent" }` so units are heard, not bare
26
+ * numbers).
27
+ */
28
+ formatOptions?: Intl.NumberFormatOptions;
29
+ /** Per-instance style overrides, merged after the recipe. */
30
+ css?: SystemStyleObject;
31
+ trackCss?: SystemStyleObject;
32
+ filledTrackCss?: SystemStyleObject;
33
+ thumbCss?: SystemStyleObject;
34
+ /**
35
+ * Positioned at the current value along the track and shown while the
36
+ * slider has focus (Chakra's SliderMark usage).
37
+ */
38
+ mark?: ReactNode;
39
+ markCss?: SystemStyleObject;
40
+ }
41
+
42
+ /**
43
+ * Slider — react-aria-components <Slider> styled like Chakra's horizontal md
44
+ * slider.
45
+ */
46
+ export const Slider = ({
47
+ value,
48
+ onChange,
49
+ minValue = 0,
50
+ maxValue = 100,
51
+ "aria-label": ariaLabel,
52
+ isDisabled,
53
+ formatOptions,
54
+ css: cssProp,
55
+ trackCss,
56
+ filledTrackCss,
57
+ thumbCss,
58
+ mark,
59
+ markCss,
60
+ }: SliderProps) => {
61
+ const slots = slider();
62
+ const percent = ((value - minValue) / (maxValue - minValue)) * 100;
63
+ return (
64
+ <RACSlider
65
+ value={value}
66
+ onChange={onChange}
67
+ minValue={minValue}
68
+ maxValue={maxValue}
69
+ aria-label={ariaLabel}
70
+ isDisabled={isDisabled}
71
+ formatOptions={formatOptions}
72
+ className={cx(slots.root, cssProp ? css(cssProp) : undefined)}
73
+ >
74
+ {mark && (
75
+ <div
76
+ data-part="mark"
77
+ className={cx(slots.mark, markCss ? css(markCss) : undefined)}
78
+ style={{ left: `${percent}%` }}
79
+ >
80
+ {mark}
81
+ </div>
82
+ )}
83
+ <SliderTrack
84
+ className={cx(slots.track, trackCss ? css(trackCss) : undefined)}
85
+ >
86
+ <div
87
+ className={cx(
88
+ slots.filledTrack,
89
+ filledTrackCss ? css(filledTrackCss) : undefined,
90
+ )}
91
+ style={{ width: `${percent}%` }}
92
+ />
93
+ </SliderTrack>
94
+ <SliderThumb
95
+ className={cx(slots.thumb, thumbCss ? css(thumbCss) : undefined)}
96
+ />
97
+ </RACSlider>
98
+ );
99
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { CSSProperties } from "react";
7
+ import { css, cx } from "styled-system/css";
8
+ import { SystemStyleObject } from "styled-system/types";
9
+
10
+ export interface SpinnerProps {
11
+ /** Chakra Spinner sizes: sm 1rem, md 1.5rem (default). */
12
+ size?: "sm" | "md";
13
+ /**
14
+ * Revolution time (Chakra's `speed`, default 0.45s). Under
15
+ * prefers-reduced-motion the spin is slowed to ~3x this value.
16
+ */
17
+ speed?: string;
18
+ /** Per-instance style overrides, merged after the base. */
19
+ css?: SystemStyleObject;
20
+ className?: string;
21
+ /**
22
+ * Accessible name. Required: Chakra's Spinner announced a visually hidden
23
+ * "Loading..." by default, so a nameless spinner would regress on it.
24
+ */
25
+ "aria-label": string;
26
+ }
27
+
28
+ /**
29
+ * Spinner — Chakra's border-based spinner (currentColor with transparent
30
+ * bottom/left quadrants).
31
+ */
32
+ export const Spinner = ({
33
+ size = "md",
34
+ speed,
35
+ css: cssProp,
36
+ className,
37
+ "aria-label": ariaLabel,
38
+ }: SpinnerProps) => (
39
+ <span
40
+ role="status"
41
+ aria-label={ariaLabel}
42
+ style={speed ? ({ "--spinner-speed": speed } as CSSProperties) : undefined}
43
+ className={cx(
44
+ // Single css() call so caller overrides of base properties (e.g. width)
45
+ // are deduped at merge time rather than racing on stylesheet order.
46
+ css(
47
+ {
48
+ display: "inline-block",
49
+ borderWidth: "2px",
50
+ borderStyle: "solid",
51
+ borderColor: "currentColor",
52
+ borderBottomColor: "transparent",
53
+ borderLeftColor: "transparent",
54
+ borderRadius: "full",
55
+ // The duration rides a variable so the reduced-motion slow-down
56
+ // scales with a per-instance `speed` rather than fighting it.
57
+ animation: "spin var(--spinner-speed, 0.45s) linear infinite",
58
+ _motionReduce: {
59
+ animationDuration: "calc(var(--spinner-speed, 0.45s) * 3.33)",
60
+ },
61
+ width: size === "sm" ? "1rem" : "1.5rem",
62
+ height: size === "sm" ? "1rem" : "1.5rem",
63
+ },
64
+ cssProp,
65
+ ),
66
+ className,
67
+ )}
68
+ />
69
+ );
package/src/Svg.tsx ADDED
@@ -0,0 +1,23 @@
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
+
8
+ /**
9
+ * Svg — a Panda-styled svg element with Chakra Icon's base sizing (1em,
10
+ * inline-block). For custom-path icons; react-icons glyphs use `Icon`.
11
+ * As a styled-factory component its call-site style props are extracted
12
+ * (unlike style props forwarded through a plain wrapper — gotcha #9).
13
+ */
14
+ export const Svg = styled("svg", {
15
+ base: {
16
+ width: "1em",
17
+ height: "1em",
18
+ display: "inline-block",
19
+ lineHeight: "1em",
20
+ flexShrink: 0,
21
+ verticalAlign: "middle",
22
+ },
23
+ });
@@ -0,0 +1,80 @@
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 md switch with the default blue colorScheme
10
+ * (light mode). Track is 1.875rem x 1rem with a 2px inset; the thumb slides by
11
+ * the track/thumb width difference when selected.
12
+ *
13
+ * State styling keys off data attributes stamped by the shared-ui Switch
14
+ * (react-aria provides the state via render props).
15
+ *
16
+ * Registered in the base preset (base-preset.ts). No variants, so it
17
+ * needs no `staticCss` entry.
18
+ */
19
+ export const switchRecipe = defineSlotRecipe({
20
+ className: "switch",
21
+ slots: ["root", "track", "thumb", "label"],
22
+ base: {
23
+ root: {
24
+ display: "inline-flex",
25
+ alignItems: "center",
26
+ verticalAlign: "top",
27
+ cursor: "pointer",
28
+ position: "relative",
29
+ "&[data-disabled]": { cursor: "not-allowed" },
30
+ },
31
+ track: {
32
+ display: "inline-flex",
33
+ flexShrink: 0,
34
+ justifyContent: "flex-start",
35
+ boxSizing: "content-box",
36
+ borderRadius: "full",
37
+ p: "0.5",
38
+ width: "1.875rem",
39
+ height: "4",
40
+ transitionProperty: "background-color",
41
+ transitionDuration: "fast",
42
+ bg: "gray.300",
43
+ "&[data-selected]": {
44
+ bg: "controlCheckedBg",
45
+ },
46
+ "&[data-focus-visible]": {
47
+ focusShadow: "outline",
48
+ },
49
+ "&[data-disabled]": { opacity: 0.4 },
50
+ // Forced-colors modes strip author backgrounds, flattening track and
51
+ // thumb to the same colour; a border keeps the track visible and the
52
+ // system SelectedItem pair conveys on/off.
53
+ _forcedColors: {
54
+ borderWidth: "1px",
55
+ borderStyle: "solid",
56
+ "&[data-selected]": { bg: "SelectedItem" },
57
+ },
58
+ },
59
+ thumb: {
60
+ bg: "white",
61
+ transitionProperty: "transform",
62
+ transitionDuration: "normal",
63
+ borderRadius: "inherit",
64
+ width: "4",
65
+ height: "4",
66
+ "&[data-selected]": {
67
+ transform: "translateX(0.875rem)",
68
+ },
69
+ _forcedColors: {
70
+ bg: "ButtonText",
71
+ "&[data-selected]": { bg: "SelectedItemText" },
72
+ },
73
+ },
74
+ label: {
75
+ userSelect: "none",
76
+ marginStart: "2",
77
+ "&[data-disabled]": { opacity: 0.4 },
78
+ },
79
+ },
80
+ });
package/src/Switch.tsx ADDED
@@ -0,0 +1,60 @@
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 } from "styled-system/recipes";
13
+ import { SystemStyleObject } from "styled-system/types";
14
+
15
+ export interface SwitchProps
16
+ extends Omit<RACSwitchProps, "className" | "children" | "style"> {
17
+ /** Per-instance style overrides for the root, merged after the recipe. */
18
+ css?: SystemStyleObject;
19
+ className?: string;
20
+ children?: ReactNode;
21
+ }
22
+
23
+ /**
24
+ * Switch — react-aria-components <Switch> styled like Chakra's md switch.
25
+ * Children render as the label; pass `aria-label` for label-less switches.
26
+ */
27
+ export const Switch = ({
28
+ css: cssProp,
29
+ className,
30
+ children,
31
+ ...rest
32
+ }: SwitchProps) => {
33
+ const slots = switchRecipe();
34
+ return (
35
+ <RACSwitch
36
+ className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
37
+ {...rest}
38
+ >
39
+ {({ isSelected, isFocusVisible, isDisabled }) => {
40
+ const state = {
41
+ "data-selected": isSelected || undefined,
42
+ "data-focus-visible": isFocusVisible || undefined,
43
+ "data-disabled": isDisabled || undefined,
44
+ };
45
+ return (
46
+ <>
47
+ <span className={slots.track} {...state} aria-hidden>
48
+ <span className={slots.thumb} {...state} />
49
+ </span>
50
+ {children != null && (
51
+ <span className={slots.label} {...state}>
52
+ {children}
53
+ </span>
54
+ )}
55
+ </>
56
+ );
57
+ }}
58
+ </RACSwitch>
59
+ );
60
+ };
package/src/Text.tsx ADDED
@@ -0,0 +1,12 @@
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
+
8
+ /**
9
+ * Text — a paragraph that accepts Panda style props. Use `as` to render a
10
+ * different element (`span`, `div`, `h2`). Replaces Chakra's <Text>.
11
+ */
12
+ export const Text = styled("p");