@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,192 @@
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
+ // Chakra's transition.property.common, inlined (Panda has no transitionProperty
9
+ // token category). Matches Chakra's Button base transition.
10
+ const transitionCommon =
11
+ "background-color, border-color, color, fill, stroke, opacity, box-shadow, transform";
12
+
13
+ /**
14
+ * Button recipe — Chakra's default Button base + sizes, with this app's
15
+ * borderRadius (`button` = 2rem) and variant set ported from
16
+ * `components/button.ts`. Interaction conditions (`_hover`/`_active`/
17
+ * `_disabled`/`_focusVisible`) are widened in the preset to also match
18
+ * react-aria-components' data attributes, so these Chakra-shaped variant
19
+ * objects work unchanged on RAC's <Button>.
20
+ *
21
+ * A config recipe (not a component cva): styles land in the `recipes` layer so
22
+ * call sites can override with plain style props, and presets extend the
23
+ * variants. This file holds the brand-independent variant set — the core
24
+ * Chakra variants plus the family-wide `language`/`toolbar` variants; a
25
+ * consuming app's preset extends it with app vocabulary (e.g. ml-trainer's
26
+ * `led`/`record*`/`secondary-disabled`). Brand divergence within a variant is
27
+ * token-driven (see the `button.*` and `languageText` semantic tokens).
28
+ *
29
+ * Registered in the base preset (base-preset.ts).
30
+ */
31
+ export const button = defineRecipe({
32
+ className: "btn",
33
+ jsx: ["Button", "IconButton"],
34
+ base: {
35
+ lineHeight: "1.2",
36
+ borderRadius: "button",
37
+ fontWeight: "semibold",
38
+ transitionProperty: transitionCommon,
39
+ transitionDuration: "normal",
40
+ display: "inline-flex",
41
+ appearance: "none",
42
+ alignItems: "center",
43
+ justifyContent: "center",
44
+ userSelect: "none",
45
+ position: "relative",
46
+ whiteSpace: "nowrap",
47
+ verticalAlign: "middle",
48
+ outline: "none",
49
+ _focusVisible: {
50
+ focusShadow: "outline",
51
+ },
52
+ _disabled: {
53
+ opacity: 0.4,
54
+ cursor: "not-allowed",
55
+ boxShadow: "none",
56
+ },
57
+ _hover: {
58
+ _disabled: {
59
+ bg: "initial",
60
+ },
61
+ },
62
+ },
63
+ variants: {
64
+ size: {
65
+ lg: { h: "12", minW: "12", fontSize: "lg", px: "6" },
66
+ md: { h: "10", minW: "10", fontSize: "md", px: "4" },
67
+ sm: { h: "8", minW: "8", fontSize: "sm", px: "3" },
68
+ xs: { h: "6", minW: "6", fontSize: "xs", px: "2" },
69
+ },
70
+ variant: {
71
+ // Chakra's unstyled reset + this app's border-radius removal.
72
+ unstyled: {
73
+ bg: "none",
74
+ color: "inherit",
75
+ display: "inline",
76
+ lineHeight: "inherit",
77
+ m: "0",
78
+ p: "0",
79
+ borderRadius: "unset",
80
+ },
81
+ // Chakra's link layout (no padding/height) + this app's colours.
82
+ link: {
83
+ padding: 0,
84
+ height: "auto",
85
+ lineHeight: "normal",
86
+ verticalAlign: "baseline",
87
+ borderWidth: "0",
88
+ color: "brand.600",
89
+ fontWeight: "normal",
90
+ bg: "transparent",
91
+ _hover: {
92
+ textDecoration: "underline",
93
+ },
94
+ },
95
+ // Colours come from the `button.*` semantic tokens so the family's two
96
+ // button idioms (brand-coloured vs black-on-white) share this recipe —
97
+ // see the token block in base-preset.ts.
98
+ secondary: {
99
+ borderWidth: "2px",
100
+ borderColor: "button.secondaryBorder",
101
+ color: "button.secondaryText",
102
+ bg: "transparent",
103
+ _hover: {
104
+ borderColor: "button.secondaryHoverBorder",
105
+ bg: "button.secondaryHoverBg",
106
+ },
107
+ _active: {
108
+ bg: "button.secondaryActiveBg",
109
+ borderColor: "button.secondaryActiveBorder",
110
+ },
111
+ },
112
+ ghost: {
113
+ color: "black",
114
+ bg: "transparent",
115
+ _hover: { bg: "blackAlpha.50" },
116
+ _active: { bg: "blackAlpha.100" },
117
+ },
118
+ // Chakra had no `plain` variant, so `variant="plain"` fell through to
119
+ // base-only styling: a transparent, colour-inheriting button. Used for the
120
+ // action-bar icon-button menu triggers (settings/help), which supply their
121
+ // own colour and shape via instance styles.
122
+ plain: {
123
+ bg: "transparent",
124
+ color: "inherit",
125
+ },
126
+ primary: {
127
+ color: "white",
128
+ bg: "button.primaryBg",
129
+ _hover: {
130
+ bg: "button.primaryHoverBg",
131
+ _disabled: { bg: "button.primaryBg" },
132
+ },
133
+ _active: { bg: "button.primaryActiveBg" },
134
+ },
135
+ // 600/700, matching what python-editor's Chakra outline + red
136
+ // colorScheme resolved to. (Extracted from ml-trainer at 500/600, but
137
+ // its one warning button tolerates the darkening; python-editor's
138
+ // "Reset project" was visibly lighter than its Chakra self.)
139
+ // NOTE (2026-08-02, from classroom): two Chakra `outline` shapes have
140
+ // no home here and are currently restated per call site in that app —
141
+ // worth considering as variants once a second consumer wants them.
142
+ // - a neutral outline (transparent, 1px gray.200, inherited text,
143
+ // gray.50/gray.100 hover/press): Chakra's default-colorScheme
144
+ // `outline`, and python-editor's *default* variant per the
145
+ // playbook's cross-app vocabulary, so likely already a 2-app shape.
146
+ // - an on-colour outline (white 2px + white text over a coloured bar,
147
+ // whiteAlpha hover/press): Chakra's `outline` + `whiteAlpha`.
148
+ // `warning` below is the *destructive* outline and is not a substitute
149
+ // for either; `warningSolid` did map exactly onto Chakra solid+red.
150
+ warning: {
151
+ borderWidth: "2px",
152
+ borderColor: "danger.600",
153
+ color: "danger.600",
154
+ bg: "transparent",
155
+ _hover: { borderColor: "danger.700", color: "danger.700" },
156
+ _active: { bg: "danger.50" },
157
+ },
158
+ // Chakra's built-in solid + red colorScheme (destructive confirm
159
+ // buttons). Same values as `record` today, but a separate variant so
160
+ // recording UI and destructive actions can diverge - hence danger
161
+ // tokens here, red.* literals there.
162
+ warningSolid: {
163
+ color: "white",
164
+ bg: "danger.500",
165
+ _hover: { bg: "danger.600", _disabled: { bg: "danger.500" } },
166
+ _active: { bg: "danger.700" },
167
+ },
168
+ // Family-wide variants (every censused app has language- and
169
+ // toolbar-class buttons).
170
+ toolbar: {
171
+ color: "black",
172
+ bg: "white",
173
+ _hover: { bg: "whiteAlpha.900", _disabled: { bg: "white" } },
174
+ _active: { bg: "whiteAlpha.800" },
175
+ _focusVisible: { focusShadow: "outlineDark" },
176
+ },
177
+ language: {
178
+ borderWidth: "2px",
179
+ borderColor: "gray.200",
180
+ color: "languageText",
181
+ // The hover wash reads as a highlight because the app's gray ramp
182
+ // carries its brand undertone (per-app tinted ramps) — an
183
+ // achromatic gray.100 here goes drab, a tinted one doesn't.
184
+ _hover: { color: "languageTextHover", bg: "gray.100" },
185
+ },
186
+ },
187
+ },
188
+ defaultVariants: {
189
+ variant: "secondary",
190
+ size: "md",
191
+ },
192
+ });
package/src/Button.tsx ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { forwardRef, ReactNode } from "react";
7
+ import {
8
+ Button as RACButton,
9
+ ButtonProps as RACButtonProps,
10
+ } from "react-aria-components";
11
+ import { css, cx } from "styled-system/css";
12
+ import { button, ButtonVariantProps } from "styled-system/recipes";
13
+ import { SystemStyleObject } from "styled-system/types";
14
+ import { buttonIcon } from "./button-icon";
15
+
16
+ export interface ButtonProps
17
+ extends Omit<RACButtonProps, "className" | "children">,
18
+ ButtonVariantProps {
19
+ /** Per-instance style overrides, merged after the recipe. */
20
+ css?: SystemStyleObject;
21
+ className?: string;
22
+ /** Icon rendered before the label, matching Chakra's `leftIcon`. */
23
+ leftIcon?: ReactNode;
24
+ /** Icon rendered after the label, matching Chakra's `rightIcon`. */
25
+ rightIcon?: ReactNode;
26
+ children?: ReactNode;
27
+ }
28
+
29
+ /**
30
+ * Button — react-aria-components <Button> styled with the `button` config
31
+ * recipe. The recipe's interaction states are driven by RAC's data attributes
32
+ * (see the preset's widened conditions), so disabled/hover/press/focus all work
33
+ * without extra wiring.
34
+ */
35
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
36
+ function Button(
37
+ {
38
+ variant,
39
+ size,
40
+ css: cssProp,
41
+ className,
42
+ leftIcon,
43
+ rightIcon,
44
+ children,
45
+ ...rest
46
+ },
47
+ ref,
48
+ ) {
49
+ return (
50
+ <RACButton
51
+ ref={ref}
52
+ className={cx(
53
+ button({ variant, size }),
54
+ cssProp ? css(cssProp) : undefined,
55
+ className,
56
+ )}
57
+ {...rest}
58
+ >
59
+ {leftIcon ? (
60
+ <span className={buttonIcon({ side: "left" })}>{leftIcon}</span>
61
+ ) : null}
62
+ {children}
63
+ {rightIcon ? (
64
+ <span className={buttonIcon({ side: "right" })}>{rightIcon}</span>
65
+ ) : null}
66
+ </RACButton>
67
+ );
68
+ },
69
+ );
@@ -0,0 +1,70 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { forwardRef, HTMLAttributes } from "react";
7
+ import { css, cx } from "styled-system/css";
8
+ import { SystemStyleObject } from "styled-system/types";
9
+
10
+ export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
11
+ /**
12
+ * Square the inner radii so adjacent buttons form one control (Chakra's
13
+ * `isAttached`). Otherwise buttons get a small gap.
14
+ */
15
+ isAttached?: boolean;
16
+ /** Per-instance style overrides. */
17
+ css?: SystemStyleObject;
18
+ className?: string;
19
+ }
20
+
21
+ /**
22
+ * ButtonGroup — lays out related buttons in a row. Works with any button
23
+ * elements (shared-ui or native). Replaces Chakra's ButtonGroup.
24
+ */
25
+ export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
26
+ function ButtonGroup({ isAttached, css: cssProp, className, ...rest }, ref) {
27
+ return (
28
+ <div
29
+ ref={ref}
30
+ className={cx(
31
+ css({
32
+ display: "inline-flex",
33
+ alignItems: "center",
34
+ // Buttons are position: relative, so without this an attached
35
+ // sibling paints over the focused button's focus-ring shadow.
36
+ // Chakra raised on :focus, but with the attached -1px overlap a
37
+ // raised button also paints over a seam its neighbour draws
38
+ // (e.g. a solid split button's white borderLeft), so raise only
39
+ // when the ring is actually shown.
40
+ "& > *": { _focusVisible: { zIndex: 1 } },
41
+ }),
42
+ isAttached
43
+ ? css({
44
+ gap: 0,
45
+ // :first-child/:last-child, not :first-of-type: attached
46
+ // groups can mix element types (e.g. select + button), and
47
+ // -of-type matches per element type.
48
+ // marginEnd -1px (Chakra parity): overlaps adjacent borders
49
+ // so two 1px inner edges read as a single 1px seam.
50
+ "& > *:first-child:not(:last-child)": {
51
+ borderEndRadius: 0,
52
+ marginEnd: "-1px",
53
+ },
54
+ "& > *:not(:first-child):not(:last-child)": {
55
+ borderRadius: 0,
56
+ marginEnd: "-1px",
57
+ },
58
+ "& > *:not(:first-child):last-child": {
59
+ borderStartRadius: 0,
60
+ },
61
+ })
62
+ : css({ gap: 2 }),
63
+ cssProp ? css(cssProp) : undefined,
64
+ className,
65
+ )}
66
+ {...rest}
67
+ />
68
+ );
69
+ },
70
+ );
@@ -0,0 +1,49 @@
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
+ * Card slot recipe — Chakra's Card at its default md size (light mode), with
10
+ * the `elevated` (default) and `outline` variants this app uses.
11
+ *
12
+ * Registered in the base preset (base-preset.ts); `variant` is
13
+ * forwarded as a runtime prop so the variants are generated via `staticCss`.
14
+ */
15
+ export const card = defineSlotRecipe({
16
+ className: "card",
17
+ slots: ["container", "body"],
18
+ base: {
19
+ container: {
20
+ display: "flex",
21
+ flexDirection: "column",
22
+ position: "relative",
23
+ minWidth: 0,
24
+ wordWrap: "break-word",
25
+ bg: "white",
26
+ borderRadius: "md",
27
+ color: "inherit",
28
+ },
29
+ body: {
30
+ padding: "5",
31
+ flex: "1 1 0%",
32
+ },
33
+ },
34
+ variants: {
35
+ variant: {
36
+ elevated: {
37
+ container: { boxShadow: "base" },
38
+ },
39
+ outline: {
40
+ container: {
41
+ borderWidth: "1px",
42
+ borderStyle: "solid",
43
+ borderColor: "gray.200",
44
+ },
45
+ },
46
+ },
47
+ },
48
+ defaultVariants: { variant: "elevated" },
49
+ });
package/src/Card.tsx ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { forwardRef, HTMLAttributes } from "react";
7
+ import { css, cx } from "styled-system/css";
8
+ import { card, CardVariantProps } from "styled-system/recipes";
9
+ import { SystemStyleObject } from "styled-system/types";
10
+
11
+ export interface CardProps
12
+ extends HTMLAttributes<HTMLDivElement>,
13
+ CardVariantProps {
14
+ /** Per-instance style overrides, merged after the recipe. */
15
+ css?: SystemStyleObject;
16
+ }
17
+
18
+ /**
19
+ * Card — Chakra's <Card> equivalent (md size). Place a CardBody inside.
20
+ */
21
+ export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
22
+ { variant, css: cssProp, className, ...rest },
23
+ ref,
24
+ ) {
25
+ return (
26
+ <div
27
+ ref={ref}
28
+ className={cx(
29
+ card({ variant }).container,
30
+ cssProp ? css(cssProp) : undefined,
31
+ className,
32
+ )}
33
+ {...rest}
34
+ />
35
+ );
36
+ });
37
+
38
+ export interface CardBodyProps extends HTMLAttributes<HTMLDivElement> {
39
+ css?: SystemStyleObject;
40
+ }
41
+
42
+ export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
43
+ function CardBody({ css: cssProp, className, ...rest }, ref) {
44
+ return (
45
+ <div
46
+ ref={ref}
47
+ className={cx(
48
+ card({}).body,
49
+ cssProp ? css(cssProp) : undefined,
50
+ className,
51
+ )}
52
+ {...rest}
53
+ />
54
+ );
55
+ },
56
+ );
@@ -0,0 +1,111 @@
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
+ * Checkbox slot recipe — Chakra's checkbox with the default blue colorScheme
10
+ * (light mode) and its sm/md/lg size scale. The control's
11
+ * `borderColor: inherit` picks up a `borderColor` set on the root, matching
12
+ * Chakra's convention for tinting the box from the call site.
13
+ *
14
+ * State styling keys off data attributes stamped on the control by the
15
+ * shared-ui Checkbox (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 checkbox = defineSlotRecipe({
21
+ className: "checkbox",
22
+ slots: ["root", "control", "icon", "label"],
23
+ base: {
24
+ root: {
25
+ display: "inline-flex",
26
+ alignItems: "center",
27
+ verticalAlign: "top",
28
+ cursor: "pointer",
29
+ position: "relative",
30
+ // The control's `borderColor: inherit` reads this. Stated here rather
31
+ // than on the control so a call site can still tint the whole control
32
+ // by setting borderColor on the root. Without it, inherit fell through
33
+ // to the reset's gray.200 (~1.3:1) — the accessible outline stop, as
34
+ // the input recipe (docs/gray-ramp.md).
35
+ borderColor: "gray.400",
36
+ "&[data-disabled]": { cursor: "not-allowed" },
37
+ },
38
+ control: {
39
+ display: "inline-flex",
40
+ alignItems: "center",
41
+ justifyContent: "center",
42
+ flexShrink: 0,
43
+ transitionProperty: "box-shadow",
44
+ transitionDuration: "normal",
45
+ borderWidth: "2px",
46
+ borderStyle: "solid",
47
+ borderRadius: "sm",
48
+ borderColor: "inherit",
49
+ color: "white",
50
+ bg: "white",
51
+ "&[data-selected]": {
52
+ bg: "controlCheckedBg",
53
+ borderColor: "controlCheckedBg",
54
+ color: "white",
55
+ _hover: {
56
+ bg: "controlCheckedHoverBg",
57
+ borderColor: "controlCheckedHoverBg",
58
+ },
59
+ },
60
+ // Chakra's disabled greys; the selected block restates _hover so the
61
+ // widened native-:hover condition can't re-tint a disabled control.
62
+ "&[data-disabled]": {
63
+ bg: "gray.100",
64
+ borderColor: "gray.100",
65
+ },
66
+ "&[data-selected][data-disabled]": {
67
+ bg: "gray.200",
68
+ borderColor: "gray.200",
69
+ color: "gray.500",
70
+ _hover: { bg: "gray.200", borderColor: "gray.200" },
71
+ },
72
+ "&[data-focus-visible]": {
73
+ focusShadow: "outline",
74
+ },
75
+ },
76
+ icon: {
77
+ transitionProperty: "transform",
78
+ transitionDuration: "normal",
79
+ },
80
+ label: {
81
+ userSelect: "none",
82
+ marginStart: "2",
83
+ "&[data-disabled]": { opacity: 0.4 },
84
+ },
85
+ },
86
+ variants: {
87
+ // Chakra's Checkbox size scale. The icon dimensions are Chakra's
88
+ // 1.2em-wide check glyph at each size's icon fontSize (3xs/2xs/2xs),
89
+ // resolved to rem.
90
+ size: {
91
+ sm: {
92
+ control: { width: "3", height: "3" },
93
+ icon: { width: "0.54rem", height: "0.54rem" },
94
+ label: { fontSize: "sm" },
95
+ },
96
+ md: {
97
+ control: { width: "4", height: "4" },
98
+ icon: { width: "0.75rem", height: "0.75rem" },
99
+ label: { fontSize: "md" },
100
+ },
101
+ lg: {
102
+ control: { width: "5", height: "5" },
103
+ icon: { width: "0.75rem", height: "0.75rem" },
104
+ label: { fontSize: "lg" },
105
+ },
106
+ },
107
+ },
108
+ defaultVariants: {
109
+ size: "md",
110
+ },
111
+ });
@@ -0,0 +1,106 @@
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
+ Checkbox as RACCheckbox,
9
+ CheckboxProps as RACCheckboxProps,
10
+ } from "react-aria-components";
11
+ import { css, cx } from "styled-system/css";
12
+ import { checkbox, CheckboxVariantProps } from "styled-system/recipes";
13
+ import { SystemStyleObject } from "styled-system/types";
14
+
15
+ /** What a render-prop child is told about the checkbox. */
16
+ export interface CheckboxState {
17
+ isSelected: boolean;
18
+ isFocusVisible: boolean;
19
+ isDisabled: boolean;
20
+ }
21
+
22
+ export interface CheckboxProps
23
+ extends Omit<RACCheckboxProps, "className" | "children" | "style">,
24
+ CheckboxVariantProps {
25
+ /** Per-instance style overrides for the root, merged after the recipe. */
26
+ css?: SystemStyleObject;
27
+ className?: string;
28
+ /**
29
+ * The label. A function receives the checkbox's state, for a label that
30
+ * changes with it.
31
+ */
32
+ children?: ReactNode | ((state: CheckboxState) => ReactNode);
33
+ /**
34
+ * Whether to draw the box. `false` is for a checkbox whose children draw
35
+ * the selected state themselves — a selectable tile, or an avatar that
36
+ * grows a tick. The label wrapper goes with it, so the children own the
37
+ * whole row, including the focus ring the box would otherwise carry.
38
+ *
39
+ * @default true
40
+ */
41
+ control?: boolean;
42
+ }
43
+
44
+ /**
45
+ * Checkbox — react-aria-components <Checkbox> styled like Chakra's checkbox.
46
+ * Children render as the label; wrap them in a visually-hidden span for
47
+ * icon-less checkboxes.
48
+ */
49
+ export const Checkbox = ({
50
+ size,
51
+ css: cssProp,
52
+ className,
53
+ children,
54
+ control,
55
+ ...rest
56
+ }: CheckboxProps) => {
57
+ const slots = checkbox({ size });
58
+ return (
59
+ <RACCheckbox
60
+ className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
61
+ {...rest}
62
+ >
63
+ {({ isSelected, isFocusVisible, isDisabled }) => {
64
+ const content =
65
+ typeof children === "function"
66
+ ? children({ isSelected, isFocusVisible, isDisabled })
67
+ : children;
68
+ if (control === false) {
69
+ return content;
70
+ }
71
+ return (
72
+ <>
73
+ <span
74
+ className={slots.control}
75
+ data-selected={isSelected || undefined}
76
+ data-focus-visible={isFocusVisible || undefined}
77
+ data-disabled={isDisabled || undefined}
78
+ aria-hidden
79
+ >
80
+ {isSelected && (
81
+ <svg viewBox="0 0 12 10" className={slots.icon} aria-hidden>
82
+ <polyline
83
+ points="1.5 6 4.5 9 10.5 1"
84
+ fill="none"
85
+ stroke="currentColor"
86
+ strokeWidth="2"
87
+ strokeLinecap="round"
88
+ strokeLinejoin="round"
89
+ />
90
+ </svg>
91
+ )}
92
+ </span>
93
+ {content != null && (
94
+ <span
95
+ className={slots.label}
96
+ data-disabled={isDisabled || undefined}
97
+ >
98
+ {content}
99
+ </span>
100
+ )}
101
+ </>
102
+ );
103
+ }}
104
+ </RACCheckbox>
105
+ );
106
+ };