@microbit/ui 0.0.0-gray.ramp.90

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 (102) hide show
  1. package/LICENSE.md +53 -0
  2. package/README.md +222 -0
  3. package/lang/ui.ca.json +22 -0
  4. package/lang/ui.cy.json +22 -0
  5. package/lang/ui.de.json +22 -0
  6. package/lang/ui.en-us.json +22 -0
  7. package/lang/ui.en.json +22 -0
  8. package/lang/ui.es-es.json +22 -0
  9. package/lang/ui.fr.json +22 -0
  10. package/lang/ui.ga-ie.json +22 -0
  11. package/lang/ui.it.json +22 -0
  12. package/lang/ui.ja.json +22 -0
  13. package/lang/ui.ko.json +22 -0
  14. package/lang/ui.lol.json +22 -0
  15. package/lang/ui.nl.json +22 -0
  16. package/lang/ui.pl.json +22 -0
  17. package/lang/ui.pt-br.json +22 -0
  18. package/lang/ui.zh-cn.json +22 -0
  19. package/lang/ui.zh-tw.json +22 -0
  20. package/package.json +66 -0
  21. package/postcss-legacy-safari.cjs +96 -0
  22. package/reset.css +35 -0
  23. package/src/Avatar.recipe.ts +168 -0
  24. package/src/Avatar.tsx +276 -0
  25. package/src/Button.recipe.ts +192 -0
  26. package/src/Button.tsx +69 -0
  27. package/src/ButtonGroup.tsx +70 -0
  28. package/src/Card.recipe.ts +49 -0
  29. package/src/Card.tsx +56 -0
  30. package/src/Checkbox.recipe.ts +111 -0
  31. package/src/Checkbox.tsx +106 -0
  32. package/src/CloseButton.tsx +87 -0
  33. package/src/CloseIcon.tsx +40 -0
  34. package/src/Code.tsx +20 -0
  35. package/src/Collapse.tsx +180 -0
  36. package/src/ComboBox.tsx +192 -0
  37. package/src/Divider.tsx +85 -0
  38. package/src/Drawer.recipe.ts +98 -0
  39. package/src/Drawer.tsx +138 -0
  40. package/src/Fade.tsx +48 -0
  41. package/src/GridList.recipe.ts +58 -0
  42. package/src/GridList.tsx +81 -0
  43. package/src/Heading.recipe.ts +52 -0
  44. package/src/Heading.tsx +14 -0
  45. package/src/Icon.tsx +75 -0
  46. package/src/IconButton.tsx +44 -0
  47. package/src/Image.tsx +11 -0
  48. package/src/Input.recipe.ts +75 -0
  49. package/src/Input.tsx +44 -0
  50. package/src/InputGroup.tsx +62 -0
  51. package/src/Kbd.tsx +26 -0
  52. package/src/Link.tsx +23 -0
  53. package/src/LinkBox.tsx +88 -0
  54. package/src/LinkButton.tsx +80 -0
  55. package/src/List.tsx +31 -0
  56. package/src/ListBox.recipe.ts +43 -0
  57. package/src/ListBox.tsx +88 -0
  58. package/src/Menu.recipe.ts +124 -0
  59. package/src/Menu.tsx +291 -0
  60. package/src/Modal.recipe.ts +163 -0
  61. package/src/Modal.tsx +377 -0
  62. package/src/NativeSelect.tsx +94 -0
  63. package/src/NumberField.recipe.ts +67 -0
  64. package/src/NumberField.tsx +86 -0
  65. package/src/PopoverArrow.tsx +67 -0
  66. package/src/ProgressBar.tsx +61 -0
  67. package/src/Radio.recipe.ts +112 -0
  68. package/src/Radio.tsx +89 -0
  69. package/src/Select.recipe.ts +210 -0
  70. package/src/Select.tsx +153 -0
  71. package/src/SharedUIProvider.tsx +55 -0
  72. package/src/Skeleton.tsx +146 -0
  73. package/src/Slide.tsx +54 -0
  74. package/src/Slider.recipe.ts +102 -0
  75. package/src/Slider.tsx +163 -0
  76. package/src/Spinner.tsx +74 -0
  77. package/src/Svg.tsx +23 -0
  78. package/src/Switch.recipe.ts +107 -0
  79. package/src/Switch.tsx +62 -0
  80. package/src/Text.recipe.ts +29 -0
  81. package/src/Text.tsx +16 -0
  82. package/src/TextField.recipe.ts +54 -0
  83. package/src/TextField.tsx +91 -0
  84. package/src/Toast.recipe.ts +98 -0
  85. package/src/Toast.tsx +173 -0
  86. package/src/Tooltip.recipe.ts +37 -0
  87. package/src/Tooltip.tsx +76 -0
  88. package/src/UnmountCallback.tsx +18 -0
  89. package/src/VisuallyHidden.tsx +14 -0
  90. package/src/base-preset.ts +371 -0
  91. package/src/base-tokens.ts +1017 -0
  92. package/src/button-icon.ts +23 -0
  93. package/src/data-attrs.ts +16 -0
  94. package/src/dense-preset.ts +108 -0
  95. package/src/hooks/useBreakpointValue.ts +63 -0
  96. package/src/hooks/useClipboard.ts +64 -0
  97. package/src/hooks/useDisclosure.ts +32 -0
  98. package/src/hooks/useMediaQuery.ts +28 -0
  99. package/src/hooks/usePrevious.ts +18 -0
  100. package/src/index.ts +63 -0
  101. package/src/messages.ts +17 -0
  102. package/src/system.ts +45 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { HTMLAttributes, ReactNode } from "react";
7
+ import { css, cx } from "styled-system/css";
8
+ import { SystemStyleObject } from "styled-system/types";
9
+
10
+ /**
11
+ * The placeholder block, as an object rather than a precomputed class so a
12
+ * caller's `css` is merged into one `css()` call and its overrides win
13
+ * (playbook gotcha #8).
14
+ *
15
+ * The colours are Chakra's, through the same pair of custom properties, so a
16
+ * call site can retint one skeleton without knowing how the animation works.
17
+ */
18
+ const skeletonBase: SystemStyleObject = {
19
+ "--skeleton-start-color": "token(colors.gray.100)",
20
+ "--skeleton-end-color": "token(colors.gray.400)",
21
+ background: "var(--skeleton-start-color)",
22
+ borderColor: "var(--skeleton-end-color)",
23
+ opacity: 0.7,
24
+ borderRadius: "sm",
25
+ boxShadow: "none",
26
+ backgroundClip: "padding-box",
27
+ cursor: "default",
28
+ color: "transparent",
29
+ pointerEvents: "none",
30
+ userSelect: "none",
31
+ // Chakra hid the content rather than unmounting it, so a skeleton sized
32
+ // from real children keeps their dimensions.
33
+ "&::before, &::after, *": { visibility: "hidden" },
34
+ animation:
35
+ "skeletonFade var(--skeleton-speed, 0.8s) linear infinite alternate",
36
+ _motionReduce: { animation: "none" },
37
+ };
38
+
39
+ export interface SkeletonProps
40
+ extends Omit<HTMLAttributes<HTMLDivElement>, "color"> {
41
+ /** Show the children instead of the placeholder. */
42
+ isLoaded?: boolean;
43
+ /** Seconds per pulse (Chakra's `speed`, default 0.8). */
44
+ speed?: number;
45
+ children?: ReactNode;
46
+ /** Per-instance style overrides, merged after the base. */
47
+ css?: SystemStyleObject;
48
+ className?: string;
49
+ }
50
+
51
+ /**
52
+ * Skeleton — Chakra's loading placeholder: a block pulsing between two greys
53
+ * until its content is ready.
54
+ *
55
+ * Chakra faded the real content in over 0.4s when `isLoaded` turned true;
56
+ * here it simply appears. Wrap in `Fade` where that transition matters.
57
+ */
58
+ export const Skeleton = ({
59
+ isLoaded,
60
+ speed,
61
+ children,
62
+ css: cssProp,
63
+ className,
64
+ style,
65
+ ...rest
66
+ }: SkeletonProps) => {
67
+ if (isLoaded) {
68
+ return (
69
+ <div
70
+ {...rest}
71
+ style={style}
72
+ className={cx(cssProp ? css(cssProp) : undefined, className)}
73
+ >
74
+ {children}
75
+ </div>
76
+ );
77
+ }
78
+ return (
79
+ <div
80
+ {...rest}
81
+ style={
82
+ speed === undefined
83
+ ? style
84
+ : ({ ...style, "--skeleton-speed": `${speed}s` } as typeof style)
85
+ }
86
+ className={cx(css({ ...skeletonBase, ...cssProp }), className)}
87
+ >
88
+ {children}
89
+ </div>
90
+ );
91
+ };
92
+
93
+ export interface SkeletonTextProps extends SkeletonProps {
94
+ /** How many lines to draw (Chakra's default is 3). */
95
+ noOfLines?: number;
96
+ /** Gap between the lines. Any CSS length. */
97
+ spacing?: string;
98
+ /** Height of each line. Any CSS length. */
99
+ skeletonHeight?: string;
100
+ }
101
+
102
+ /**
103
+ * SkeletonText — a paragraph-shaped `Skeleton`: evenly spaced lines, the last
104
+ * one short, as Chakra drew them.
105
+ */
106
+ export const SkeletonText = ({
107
+ noOfLines = 3,
108
+ spacing = "0.5rem",
109
+ skeletonHeight = "0.5rem",
110
+ isLoaded,
111
+ speed,
112
+ children,
113
+ css: cssProp,
114
+ className,
115
+ ...rest
116
+ }: SkeletonTextProps) => {
117
+ if (isLoaded) {
118
+ return (
119
+ <div
120
+ {...rest}
121
+ className={cx(cssProp ? css(cssProp) : undefined, className)}
122
+ >
123
+ {children}
124
+ </div>
125
+ );
126
+ }
127
+ return (
128
+ <div
129
+ {...rest}
130
+ className={cx(cssProp ? css(cssProp) : undefined, className)}
131
+ >
132
+ {Array.from({ length: noOfLines }, (_, index) => (
133
+ <Skeleton
134
+ key={index}
135
+ speed={speed}
136
+ style={{
137
+ height: skeletonHeight,
138
+ // Chakra's shape: a last line at 80%, and no gap after it.
139
+ width: noOfLines > 1 && index === noOfLines - 1 ? "80%" : "100%",
140
+ marginBottom: index === noOfLines - 1 ? "0" : spacing,
141
+ }}
142
+ />
143
+ ))}
144
+ </div>
145
+ );
146
+ };
package/src/Slide.tsx 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,102 @@
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 (3.5-token thumb, 1-token
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
+ * Geometry matches Chakra's DOM: the root is thumb-height with NO horizontal
15
+ * padding, the track is absolutely centred inside it at full width, and the
16
+ * thumb overhangs the track ends at 0%/100%. Call sites position overlays
17
+ * (marks, value labels) against that root box, so Chakra-era offsets work
18
+ * unchanged. Sizes are tokens, not px, so apps with resized scales (e.g.
19
+ * python-editor's ×0.88) get their Chakra-equivalent geometry.
20
+ *
21
+ * The `mark` slot is hidden until the slider has focus (Chakra call sites did
22
+ * this with `_focusWithin` + a class); the reveal lives here so call sites
23
+ * only style the mark's look.
24
+ *
25
+ * Registered in the base preset (base-preset.ts). No variants, so it
26
+ * needs no `staticCss` entry.
27
+ */
28
+ export const slider = defineSlotRecipe({
29
+ className: "slider",
30
+ slots: ["root", "track", "filledTrack", "thumb", "mark"],
31
+ base: {
32
+ root: {
33
+ position: "relative",
34
+ display: "block",
35
+ width: "100%",
36
+ // Thumb-height, like Chakra's container; the track centres inside.
37
+ height: "3.5",
38
+ touchAction: "none",
39
+ "&:focus-within [data-part='mark']": {
40
+ display: "block",
41
+ },
42
+ "&[data-disabled]": {
43
+ opacity: 0.4,
44
+ cursor: "not-allowed",
45
+ },
46
+ },
47
+ track: {
48
+ position: "absolute",
49
+ top: "50%",
50
+ transform: "translateY(-50%)",
51
+ left: 0,
52
+ width: "100%",
53
+ height: "1",
54
+ overflow: "hidden",
55
+ borderRadius: "sm",
56
+ bg: "gray.200",
57
+ cursor: "pointer",
58
+ "[data-disabled] &": {
59
+ bg: "gray.300",
60
+ cursor: "default",
61
+ },
62
+ },
63
+ filledTrack: {
64
+ position: "absolute",
65
+ top: 0,
66
+ left: 0,
67
+ height: "100%",
68
+ bg: "sliderFilledTrack",
69
+ },
70
+ thumb: {
71
+ position: "absolute",
72
+ top: "50%",
73
+ width: "3.5",
74
+ height: "3.5",
75
+ display: "flex",
76
+ alignItems: "center",
77
+ justifyContent: "center",
78
+ outline: "none",
79
+ zIndex: 1,
80
+ borderRadius: "full",
81
+ bg: "white",
82
+ boxShadow: "base",
83
+ transitionProperty: "transform",
84
+ transitionDuration: "normal",
85
+ "&[data-dragging]": {
86
+ transform: "translate(-50%, -50%) scale(1.15)",
87
+ },
88
+ transform: "translate(-50%, -50%)",
89
+ "&[data-focus-visible]": {
90
+ focusShadow: "outline",
91
+ },
92
+ "&[data-disabled]": {
93
+ bg: "gray.300",
94
+ },
95
+ },
96
+ mark: {
97
+ display: "none",
98
+ position: "absolute",
99
+ whiteSpace: "nowrap",
100
+ },
101
+ },
102
+ });
package/src/Slider.tsx ADDED
@@ -0,0 +1,163 @@
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
+ * Additional positioned overlays rendered inside the slider root
42
+ * (always-visible value labels, threshold markers, ...). The root is
43
+ * position: relative; position children absolutely, typically with a
44
+ * percentage `left` for the track position.
45
+ */
46
+ children?: ReactNode;
47
+ /**
48
+ * Tooltip-styled bubble anchored above the thumb (Chakra's
49
+ * Tooltip-around-SliderThumb pattern). Rendered only while
50
+ * `isThumbTooltipOpen`; drive it from hover/focus, e.g. via
51
+ * `onThumbFocusChange` and mouse handlers on an enclosing element.
52
+ */
53
+ thumbTooltip?: ReactNode;
54
+ isThumbTooltipOpen?: boolean;
55
+ /** Thumb focus tracking (react-aria focus events). */
56
+ onThumbFocusChange?: (isFocused: boolean) => void;
57
+ }
58
+
59
+ /**
60
+ * Slider — react-aria-components <Slider> styled like Chakra's horizontal md
61
+ * slider.
62
+ */
63
+ export const Slider = ({
64
+ value,
65
+ onChange,
66
+ minValue = 0,
67
+ maxValue = 100,
68
+ "aria-label": ariaLabel,
69
+ isDisabled,
70
+ formatOptions,
71
+ css: cssProp,
72
+ trackCss,
73
+ filledTrackCss,
74
+ thumbCss,
75
+ mark,
76
+ markCss,
77
+ children,
78
+ thumbTooltip,
79
+ isThumbTooltipOpen,
80
+ onThumbFocusChange,
81
+ }: SliderProps) => {
82
+ const slots = slider();
83
+ const percent = ((value - minValue) / (maxValue - minValue)) * 100;
84
+ return (
85
+ <RACSlider
86
+ value={value}
87
+ onChange={onChange}
88
+ minValue={minValue}
89
+ maxValue={maxValue}
90
+ aria-label={ariaLabel}
91
+ isDisabled={isDisabled}
92
+ formatOptions={formatOptions}
93
+ className={cx(slots.root, cssProp ? css(cssProp) : undefined)}
94
+ >
95
+ {mark && (
96
+ <div
97
+ data-part="mark"
98
+ className={cx(slots.mark, markCss ? css(markCss) : undefined)}
99
+ style={{ left: `${percent}%` }}
100
+ >
101
+ {mark}
102
+ </div>
103
+ )}
104
+ <SliderTrack
105
+ className={cx(slots.track, trackCss ? css(trackCss) : undefined)}
106
+ >
107
+ <div
108
+ className={cx(
109
+ slots.filledTrack,
110
+ filledTrackCss ? css(filledTrackCss) : undefined,
111
+ )}
112
+ style={{ width: `${percent}%` }}
113
+ />
114
+ </SliderTrack>
115
+ {thumbTooltip && isThumbTooltipOpen && (
116
+ // Matches the shared Tooltip's look (tooltipBase) with a bottom
117
+ // arrow, anchored above the thumb.
118
+ <div
119
+ role="presentation"
120
+ className={css({
121
+ position: "absolute",
122
+ bottom: "calc(50% + token(sizes.3.5))",
123
+ transform: "translateX(-50%)",
124
+ bg: "gray.700",
125
+ color: "white",
126
+ px: "2",
127
+ py: "1",
128
+ borderRadius: "md",
129
+ fontSize: "sm",
130
+ fontWeight: "medium",
131
+ boxShadow: "md",
132
+ zIndex: "tooltip",
133
+ whiteSpace: "nowrap",
134
+ _after: {
135
+ content: '""',
136
+ position: "absolute",
137
+ // 1px into the box so subpixel edges can't antialias into a
138
+ // hairline seam (see PopoverArrow).
139
+ top: "calc(100% - 1px)",
140
+ left: "50%",
141
+ transform: "translateX(-50%)",
142
+ borderWidth: "4px",
143
+ borderStyle: "solid",
144
+ // Per-side: tokens don't resolve inside multi-value
145
+ // shorthands (they emit verbatim and the browser drops the
146
+ // invalid declaration).
147
+ borderColor: "transparent",
148
+ borderTopColor: "gray.700",
149
+ },
150
+ })}
151
+ style={{ left: `${percent}%` }}
152
+ >
153
+ {thumbTooltip}
154
+ </div>
155
+ )}
156
+ <SliderThumb
157
+ onFocusChange={onThumbFocusChange}
158
+ className={cx(slots.thumb, thumbCss ? css(thumbCss) : undefined)}
159
+ />
160
+ {children}
161
+ </RACSlider>
162
+ );
163
+ };
@@ -0,0 +1,74 @@
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 { dataAttrs } from "./data-attrs";
9
+ import { SystemStyleObject } from "styled-system/types";
10
+
11
+ export interface SpinnerProps {
12
+ /** Chakra Spinner sizes: sm 1rem, md 1.5rem (default). */
13
+ size?: "sm" | "md";
14
+ /**
15
+ * Revolution time (Chakra's `speed`, default 0.45s). Under
16
+ * prefers-reduced-motion the spin is slowed to ~3x this value.
17
+ */
18
+ speed?: string;
19
+ /** Per-instance style overrides, merged after the base. */
20
+ css?: SystemStyleObject;
21
+ className?: string;
22
+ /**
23
+ * Accessible name. Required: Chakra's Spinner announced a visually hidden
24
+ * "Loading..." by default, so a nameless spinner would regress on it.
25
+ */
26
+ "aria-label": string;
27
+ /** `data-*` attributes land on the spinner, for tests that wait on it. */
28
+ [key: `data-${string}`]: unknown;
29
+ }
30
+
31
+ /**
32
+ * Spinner — Chakra's border-based spinner (currentColor with transparent
33
+ * bottom/left quadrants).
34
+ */
35
+ export const Spinner = ({
36
+ size = "md",
37
+ speed,
38
+ css: cssProp,
39
+ className,
40
+ "aria-label": ariaLabel,
41
+ ...rest
42
+ }: SpinnerProps) => (
43
+ <span
44
+ {...dataAttrs(rest)}
45
+ role="status"
46
+ aria-label={ariaLabel}
47
+ style={speed ? ({ "--spinner-speed": speed } as CSSProperties) : undefined}
48
+ className={cx(
49
+ // Single css() call so caller overrides of base properties (e.g. width)
50
+ // are deduped at merge time rather than racing on stylesheet order.
51
+ css(
52
+ {
53
+ display: "inline-block",
54
+ borderWidth: "2px",
55
+ borderStyle: "solid",
56
+ borderColor: "currentColor",
57
+ borderBottomColor: "transparent",
58
+ borderLeftColor: "transparent",
59
+ borderRadius: "full",
60
+ // The duration rides a variable so the reduced-motion slow-down
61
+ // scales with a per-instance `speed` rather than fighting it.
62
+ animation: "spin var(--spinner-speed, 0.45s) linear infinite",
63
+ _motionReduce: {
64
+ animationDuration: "calc(var(--spinner-speed, 0.45s) * 3.33)",
65
+ },
66
+ width: size === "sm" ? "1rem" : "1.5rem",
67
+ height: size === "sm" ? "1rem" : "1.5rem",
68
+ },
69
+ cssProp,
70
+ ),
71
+ className,
72
+ )}
73
+ />
74
+ );
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,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
+ });