@microbit/ui 0.1.0-alpha.3 → 0.1.0-alpha.31

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 (115) hide show
  1. package/LICENSE.md +40 -0
  2. package/README.md +279 -26
  3. package/lang/ui.ar.json +62 -0
  4. package/lang/ui.ca.json +43 -3
  5. package/lang/ui.cy.json +62 -0
  6. package/lang/ui.de.json +62 -0
  7. package/lang/ui.en-us.json +40 -0
  8. package/lang/ui.en.json +40 -0
  9. package/lang/ui.es-es.json +43 -3
  10. package/lang/ui.fr.json +44 -4
  11. package/lang/ui.ga-ie.json +62 -0
  12. package/lang/ui.it.json +62 -0
  13. package/lang/ui.ja.json +44 -4
  14. package/lang/ui.ko.json +44 -4
  15. package/lang/ui.lo.json +62 -0
  16. package/lang/ui.lol.json +40 -0
  17. package/lang/ui.nl.json +44 -4
  18. package/lang/ui.pl.json +44 -4
  19. package/lang/ui.pt-br.json +44 -4
  20. package/lang/ui.vi.json +62 -0
  21. package/lang/ui.zh-cn.json +62 -0
  22. package/lang/ui.zh-tw.json +44 -4
  23. package/package.json +19 -10
  24. package/postcss-legacy-safari.cjs +96 -0
  25. package/reset.css +35 -0
  26. package/src/Avatar.recipe.ts +191 -0
  27. package/src/Avatar.tsx +278 -0
  28. package/src/Breadcrumb.recipe.ts +45 -0
  29. package/src/Breadcrumb.tsx +114 -0
  30. package/src/Button.recipe.ts +88 -50
  31. package/src/Button.tsx +68 -30
  32. package/src/ButtonGroup.tsx +37 -15
  33. package/src/Card.recipe.ts +1 -2
  34. package/src/Card.tsx +2 -1
  35. package/src/Checkbox.recipe.ts +49 -14
  36. package/src/Checkbox.tsx +111 -32
  37. package/src/CheckboxGroup.tsx +70 -0
  38. package/src/CloseButton.tsx +3 -3
  39. package/src/CloseIcon.tsx +6 -4
  40. package/src/Code.tsx +20 -0
  41. package/src/Collapse.tsx +179 -0
  42. package/src/ComboBox.tsx +246 -0
  43. package/src/Divider.tsx +72 -8
  44. package/src/Drawer.recipe.ts +21 -10
  45. package/src/Drawer.tsx +3 -4
  46. package/src/ExternalLink.tsx +43 -0
  47. package/src/Fade.tsx +62 -0
  48. package/src/Field.recipe.ts +114 -0
  49. package/src/Field.tsx +187 -0
  50. package/src/GridList.recipe.ts +57 -0
  51. package/src/GridList.tsx +81 -0
  52. package/src/Heading.recipe.ts +20 -1
  53. package/src/Heading.tsx +3 -3
  54. package/src/Icon.tsx +23 -7
  55. package/src/IconButton.tsx +8 -13
  56. package/src/Image.tsx +1 -1
  57. package/src/Input.recipe.ts +41 -28
  58. package/src/Input.tsx +23 -7
  59. package/src/InputGroup.tsx +26 -12
  60. package/src/Kbd.tsx +26 -0
  61. package/src/Link.tsx +3 -3
  62. package/src/LinkBox.tsx +2 -3
  63. package/src/LinkButton.tsx +81 -0
  64. package/src/List.tsx +8 -6
  65. package/src/ListBox.recipe.ts +43 -0
  66. package/src/ListBox.tsx +88 -0
  67. package/src/Menu.recipe.ts +51 -17
  68. package/src/Menu.tsx +117 -2
  69. package/src/Modal.recipe.ts +17 -9
  70. package/src/Modal.tsx +137 -24
  71. package/src/MoreMenuButton.tsx +63 -0
  72. package/src/NativeSelect.tsx +41 -14
  73. package/src/NativeSelectField.tsx +84 -0
  74. package/src/NumberField.recipe.ts +108 -0
  75. package/src/NumberField.tsx +138 -0
  76. package/src/PopoverArrow.tsx +19 -5
  77. package/src/ProgressBar.tsx +3 -5
  78. package/src/Radio.recipe.ts +108 -0
  79. package/src/Radio.tsx +62 -0
  80. package/src/RadioGroup.tsx +68 -0
  81. package/src/Select.recipe.ts +211 -0
  82. package/src/Select.tsx +194 -0
  83. package/src/SharedUIProvider.tsx +63 -7
  84. package/src/Skeleton.tsx +146 -0
  85. package/src/Slide.tsx +2 -2
  86. package/src/Slider.recipe.ts +27 -16
  87. package/src/Slider.tsx +78 -6
  88. package/src/Spinner.tsx +11 -9
  89. package/src/Svg.tsx +2 -3
  90. package/src/Switch.recipe.ts +57 -11
  91. package/src/Switch.tsx +39 -7
  92. package/src/Text.recipe.ts +26 -0
  93. package/src/Text.tsx +6 -2
  94. package/src/TextField.tsx +54 -40
  95. package/src/Toast.recipe.ts +52 -15
  96. package/src/Toast.tsx +151 -46
  97. package/src/Tooltip.recipe.ts +32 -0
  98. package/src/Tooltip.tsx +68 -28
  99. package/src/TooltipButton.tsx +229 -0
  100. package/src/UnmountCallback.tsx +2 -2
  101. package/src/VisuallyHidden.tsx +1 -1
  102. package/src/base-preset.ts +336 -84
  103. package/src/{chakra-tokens.ts → base-tokens.ts} +7 -7
  104. package/src/button-icon.ts +22 -0
  105. package/src/data-attrs.ts +16 -0
  106. package/src/dense-preset.ts +107 -0
  107. package/src/hooks/useBreakpointValue.ts +4 -4
  108. package/src/hooks/useClipboard.ts +63 -0
  109. package/src/hooks/useDisclosure.ts +31 -0
  110. package/src/hooks/useMediaQuery.ts +27 -0
  111. package/src/hooks/usePrevious.ts +15 -0
  112. package/src/index.ts +29 -3
  113. package/src/rac-locale.ts +33 -0
  114. package/src/system.ts +22 -1
  115. package/src/TextField.recipe.ts +0 -54
@@ -0,0 +1,108 @@
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
+ * NumberField slot recipe — an outline input (the `input` recipe styles the
10
+ * input itself) with a right-hand stepper column of two stacked buttons.
11
+ * Consumed by the shared-ui NumberField (react-aria-components NumberField).
12
+ *
13
+ * Registered in the base preset (base-preset.ts), which also has the
14
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
15
+ */
16
+ export const numberField = defineSlotRecipe({
17
+ className: "numberField",
18
+ slots: ["root", "group", "stepper", "stepperButton"],
19
+ base: {
20
+ // No layout here: the component wears the `field` recipe's root alongside
21
+ // this slot, and that recipe is the single owner of field-root layout
22
+ // (else `labelPosition` would fight this slot over `flexDirection`). The
23
+ // slot stays for apps and variants to target.
24
+ root: {},
25
+ group: {
26
+ position: "relative",
27
+ zIndex: 0,
28
+ // The stepper overlays the input as a sibling, so the input's own
29
+ // :hover misses it — the group carries the hover tint. The :not()
30
+ // list is required: slot recipes layer above plain recipes
31
+ // (recipes.slots), so this would otherwise override focused/invalid
32
+ // (see docs/hints.md). :user-invalid takes its own :not(): :not() is
33
+ // not forgiving, so one list would drop the whole selector on a
34
+ // browser that doesn't know the pseudo-class (Safari < 16.5).
35
+ "&:hover input:not(:focus, [data-focused], [data-invalid]):not(:user-invalid)":
36
+ { borderColor: "gray.500" },
37
+ },
38
+ // A column overlaying the input's right edge, inset by the input border.
39
+ stepper: {
40
+ display: "flex",
41
+ flexDirection: "column",
42
+ position: "absolute",
43
+ insetEnd: "0",
44
+ top: "0",
45
+ height: "calc(100% - 4px)",
46
+ margin: "2px",
47
+ width: "6",
48
+ zIndex: 1,
49
+ },
50
+ stepperButton: {
51
+ display: "flex",
52
+ alignItems: "center",
53
+ justifyContent: "center",
54
+ flex: 1,
55
+ cursor: "pointer",
56
+ lineHeight: "normal",
57
+ fontSize: "xs",
58
+ color: "inherit",
59
+ bg: "transparent",
60
+ borderStart: "1px solid",
61
+ borderColor: "gray.200",
62
+ transitionProperty: "background",
63
+ transitionDuration: "ultra-fast",
64
+ // Follow the input's corners, less the 2px border the stepper is inset
65
+ // by, so the hover and pressed fills curve away with the border instead
66
+ // of squaring off over its arc. Radii track the input recipe's size
67
+ // scale, so the `sm` variant restates them.
68
+ "&:first-child": {
69
+ borderStartEndRadius: "calc(token(radii.md) - 2px)",
70
+ },
71
+ "&:last-child": {
72
+ borderTop: "1px solid",
73
+ borderTopColor: "gray.200",
74
+ marginTop: "-1px",
75
+ borderEndEndRadius: "calc(token(radii.md) - 2px)",
76
+ },
77
+ "&[data-hovered]": { bg: "gray.100" },
78
+ "&[data-pressed]": { bg: "gray.200" },
79
+ "&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
80
+ },
81
+ },
82
+ variants: {
83
+ // The stepper column stays 24px wide at every size (as does the input
84
+ // padding paired with it in NumberField.tsx); only the arrow glyphs
85
+ // scale, at 0.75 × the field's font size. The base's `xs` is exactly
86
+ // md × 0.75, so md adds nothing.
87
+ size: {
88
+ lg: {
89
+ stepperButton: { fontSize: "calc(token(fontSizes.lg) * 0.75)" },
90
+ },
91
+ md: {},
92
+ sm: {
93
+ stepperButton: {
94
+ fontSize: "calc(token(fontSizes.sm) * 0.75)",
95
+ "&:first-child": {
96
+ borderStartEndRadius: "calc(token(radii.sm) - 2px)",
97
+ },
98
+ "&:last-child": {
99
+ borderEndEndRadius: "calc(token(radii.sm) - 2px)",
100
+ },
101
+ },
102
+ },
103
+ },
104
+ },
105
+ defaultVariants: {
106
+ size: "md",
107
+ },
108
+ });
@@ -0,0 +1,138 @@
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
+ Group as RACGroup,
10
+ Input as RACInput,
11
+ NumberField as RACNumberField,
12
+ NumberFieldProps as RACNumberFieldProps,
13
+ } from "react-aria-components";
14
+ import { useIntl } from "react-intl";
15
+ import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
16
+ import { css, cx } from "styled-system/css";
17
+ import {
18
+ field,
19
+ input,
20
+ InputVariantProps,
21
+ numberField,
22
+ } from "styled-system/recipes";
23
+ import { SystemStyleObject } from "styled-system/types";
24
+ import {
25
+ FieldLabel,
26
+ FieldLayoutProps,
27
+ FieldSupport,
28
+ FieldSupportProps,
29
+ } from "./Field";
30
+ import { Icon } from "./Icon";
31
+ import { uiMessage } from "./messages";
32
+
33
+ export interface NumberFieldProps
34
+ extends Omit<RACNumberFieldProps, "className" | "children" | "style">,
35
+ InputVariantProps,
36
+ FieldSupportProps,
37
+ FieldLayoutProps {
38
+ /** Visible label (optional; otherwise pass `aria-label`). */
39
+ label?: ReactNode;
40
+ /** Root style overrides (for a label-beside-field row, prefer
41
+ * `labelPosition="side"`). */
42
+ rootCss?: SystemStyleObject;
43
+ /** Label style overrides. */
44
+ labelCss?: SystemStyleObject;
45
+ /** Group (input + steppers) style overrides — set `width` here. */
46
+ groupCss?: SystemStyleObject;
47
+ /** Input style overrides (for sizing, prefer the `size` prop). */
48
+ inputCss?: SystemStyleObject;
49
+ }
50
+
51
+ /**
52
+ * NumberField — react-aria-components <NumberField>: an outline input with a
53
+ * right-hand stepper column. The ref is forwarded to the input element. Value
54
+ * clamping to min/maxValue is handled by react-aria; onChange receives NaN
55
+ * when the field is emptied.
56
+ */
57
+ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
58
+ function NumberField(
59
+ {
60
+ label,
61
+ helperText,
62
+ errorMessage,
63
+ helperTextCss,
64
+ rootCss,
65
+ labelCss,
66
+ groupCss,
67
+ inputCss,
68
+ labelPosition,
69
+ ...props
70
+ },
71
+ ref,
72
+ ) {
73
+ // As Input: forward every recipe variant group, not just `size`, so a
74
+ // preset that adds one keeps working.
75
+ const [variantProps, rest] = input.splitVariantProps(props);
76
+ const slots = numberField({ size: variantProps.size });
77
+ const fieldSlots = field({ size: variantProps.size, labelPosition });
78
+ const intl = useIntl();
79
+ // Fold the field's name into the stepper labels as react-aria does; the
80
+ // trim eats the leftover space when there is no usable name.
81
+ const fieldLabel =
82
+ rest["aria-label"] ?? (typeof label === "string" ? label : "");
83
+ return (
84
+ <RACNumberField
85
+ incrementAriaLabel={intl
86
+ .formatMessage(uiMessage("ui.numberfield-increase"), { fieldLabel })
87
+ .trim()}
88
+ decrementAriaLabel={intl
89
+ .formatMessage(uiMessage("ui.numberfield-decrease"), { fieldLabel })
90
+ .trim()}
91
+ {...rest}
92
+ className={cx(
93
+ fieldSlots.root,
94
+ slots.root,
95
+ rootCss ? css(rootCss) : undefined,
96
+ )}
97
+ >
98
+ {label != null && (
99
+ <FieldLabel
100
+ size={variantProps.size}
101
+ labelPosition={labelPosition}
102
+ isRequired={rest.isRequired}
103
+ css={labelCss}
104
+ >
105
+ {label}
106
+ </FieldLabel>
107
+ )}
108
+ <RACGroup
109
+ className={cx(slots.group, groupCss ? css(groupCss) : undefined)}
110
+ >
111
+ <RACInput
112
+ ref={ref}
113
+ className={cx(
114
+ input(variantProps),
115
+ // Room for the stepper column (constant across sizes, as the
116
+ // stepper's width is).
117
+ css({ paddingEnd: "6" }, inputCss),
118
+ )}
119
+ />
120
+ <div className={slots.stepper}>
121
+ <RACButton slot="increment" className={slots.stepperButton}>
122
+ <Icon as={RiArrowUpSFill} />
123
+ </RACButton>
124
+ <RACButton slot="decrement" className={slots.stepperButton}>
125
+ <Icon as={RiArrowDownSFill} />
126
+ </RACButton>
127
+ </div>
128
+ </RACGroup>
129
+ <FieldSupport
130
+ helperText={helperText}
131
+ errorMessage={errorMessage}
132
+ helperTextCss={helperTextCss}
133
+ labelPosition={labelPosition}
134
+ />
135
+ </RACNumberField>
136
+ );
137
+ },
138
+ );
@@ -13,18 +13,32 @@ import { SystemStyleObject } from "styled-system/types";
13
13
  // rotating about the centre keeps it flush against the overlay on every side
14
14
  // — no translate fix-ups, whose signs depend on rotation direction and are
15
15
  // easy to get wrong (they detached the tooltip arrow from its box).
16
+ //
17
+ // "Flush" still antialiases to a hairline seam when the overlay's edge lands
18
+ // on a subpixel boundary, so each placement also pulls the svg 1px into the
19
+ // overlay with a negative margin on the overlay-facing side (margins move the
20
+ // box before the rotate, so the bleed direction is unaffected by it).
16
21
  const arrowBase = css({
17
22
  "& 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)" },
23
+ "&[data-placement='top'] svg": { marginTop: "-1px" },
24
+ "&[data-placement='bottom'] svg": {
25
+ transform: "rotate(180deg)",
26
+ marginBottom: "-1px",
27
+ },
28
+ "&[data-placement='right'] svg": {
29
+ transform: "rotate(90deg)",
30
+ marginRight: "-1px",
31
+ },
32
+ "&[data-placement='left'] svg": {
33
+ transform: "rotate(-90deg)",
34
+ marginLeft: "-1px",
35
+ },
21
36
  });
22
37
 
23
38
  export interface PopoverArrowProps {
24
39
  /**
25
40
  * 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°).
41
+ * the gap. Defaults to the proportions of an 8px square rotated 45°.
28
42
  */
29
43
  size?: number;
30
44
  /** Styles merged after the base, e.g. `{ "& svg": { fill: "white" } }`. */
@@ -18,9 +18,8 @@ export interface ProgressBarProps {
18
18
  }
19
19
 
20
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).
21
+ * ProgressBar — a styled react-aria-components <ProgressBar> (12px gray.100
22
+ * track). Set the fill colour per call site via `barCss`.
24
23
  */
25
24
  export const ProgressBar = ({
26
25
  value,
@@ -49,8 +48,7 @@ export const ProgressBar = ({
49
48
  className={css(
50
49
  {
51
50
  height: "100%",
52
- bg: "progressFilledTrack",
53
- transition: "width 0.2s",
51
+ bg: "brand.500",
54
52
  },
55
53
  barCss,
56
54
  )}
@@ -0,0 +1,108 @@
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
+ * Radio slot recipe — the checkbox control rounded fully, with a 50%
10
+ * `currentColor` dot when selected instead of the check glyph, across the
11
+ * same sm/md/lg size scale. As with the checkbox, `borderColor: inherit`
12
+ * picks up a `borderColor` set on the root.
13
+ *
14
+ * State styling keys off data attributes stamped by the shared-ui Radio
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 radio = defineSlotRecipe({
21
+ className: "radio",
22
+ slots: ["root", "control", "label"],
23
+ base: {
24
+ root: {
25
+ display: "inline-flex",
26
+ alignItems: "center",
27
+ verticalAlign: "top",
28
+ cursor: "pointer",
29
+ position: "relative",
30
+ // As the checkbox: the accessible outline stop (WCAG 1.4.11), on the
31
+ // root so the control's `borderColor: inherit` reads it and call
32
+ // sites can still tint at the root.
33
+ borderColor: "gray.400",
34
+ "&[data-disabled]": { cursor: "not-allowed" },
35
+ },
36
+ control: {
37
+ display: "inline-flex",
38
+ alignItems: "center",
39
+ justifyContent: "center",
40
+ flexShrink: 0,
41
+ borderWidth: "2px",
42
+ borderStyle: "solid",
43
+ borderRadius: "full",
44
+ borderColor: "inherit",
45
+ color: "white",
46
+ bg: "white",
47
+ "&[data-selected]": {
48
+ bg: "controlCheckedBg",
49
+ borderColor: "controlCheckedBg",
50
+ color: "white",
51
+ _hover: {
52
+ bg: "controlCheckedHoverBg",
53
+ borderColor: "controlCheckedHoverBg",
54
+ },
55
+ // The radio dot.
56
+ _before: {
57
+ content: '""',
58
+ display: "inline-block",
59
+ position: "relative",
60
+ width: "50%",
61
+ height: "50%",
62
+ borderRadius: "50%",
63
+ bg: "currentColor",
64
+ },
65
+ },
66
+ // Disabled greys; the selected block restates _hover so the widened
67
+ // native-:hover condition can't re-tint a disabled control.
68
+ "&[data-disabled]": {
69
+ bg: "gray.100",
70
+ borderColor: "gray.100",
71
+ },
72
+ "&[data-selected][data-disabled]": {
73
+ bg: "gray.200",
74
+ borderColor: "gray.200",
75
+ color: "gray.500",
76
+ _hover: { bg: "gray.200", borderColor: "gray.200" },
77
+ },
78
+ "&[data-focus-visible]": {
79
+ focusRing: "outline",
80
+ },
81
+ },
82
+ label: {
83
+ userSelect: "none",
84
+ marginStart: "2",
85
+ "&[data-disabled]": { opacity: 0.4 },
86
+ },
87
+ },
88
+ variants: {
89
+ // The sm/md/lg size scale; control dimensions match the checkbox's.
90
+ size: {
91
+ sm: {
92
+ control: { width: "3", height: "3" },
93
+ label: { fontSize: "sm" },
94
+ },
95
+ md: {
96
+ control: { width: "4", height: "4" },
97
+ label: { fontSize: "md" },
98
+ },
99
+ lg: {
100
+ control: { width: "5", height: "5" },
101
+ label: { fontSize: "lg" },
102
+ },
103
+ },
104
+ },
105
+ defaultVariants: {
106
+ size: "md",
107
+ },
108
+ });
package/src/Radio.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
+ Radio as RACRadio,
9
+ RadioProps as RACRadioProps,
10
+ } from "react-aria-components";
11
+ import { css, cx } from "styled-system/css";
12
+ import { radio, RadioVariantProps } from "styled-system/recipes";
13
+ import { SystemStyleObject } from "styled-system/types";
14
+
15
+ export interface RadioProps
16
+ extends Omit<RACRadioProps, "className" | "children" | "style">,
17
+ RadioVariantProps {
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
+ * Radio — a styled react-aria-components <Radio>. Must be rendered inside a
26
+ * RadioGroup. Children render as the label.
27
+ */
28
+ export const Radio = ({
29
+ size,
30
+ css: cssProp,
31
+ className,
32
+ children,
33
+ ...rest
34
+ }: RadioProps) => {
35
+ const slots = radio({ size });
36
+ return (
37
+ <RACRadio
38
+ className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
39
+ {...rest}
40
+ >
41
+ {({ isSelected, isFocusVisible, isDisabled }) => (
42
+ <>
43
+ <span
44
+ className={slots.control}
45
+ data-selected={isSelected || undefined}
46
+ data-focus-visible={isFocusVisible || undefined}
47
+ data-disabled={isDisabled || undefined}
48
+ aria-hidden
49
+ />
50
+ {children != null && (
51
+ <span
52
+ className={slots.label}
53
+ data-disabled={isDisabled || undefined}
54
+ >
55
+ {children}
56
+ </span>
57
+ )}
58
+ </>
59
+ )}
60
+ </RACRadio>
61
+ );
62
+ };
@@ -0,0 +1,68 @@
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
+ RadioGroup as RACRadioGroup,
9
+ RadioGroupProps as RACRadioGroupProps,
10
+ } from "react-aria-components";
11
+ import { css, cx } from "styled-system/css";
12
+ import { SystemStyleObject } from "styled-system/types";
13
+ import { FieldLabel, FieldSupport, FieldSupportProps } from "./Field";
14
+
15
+ export interface RadioGroupProps
16
+ extends Omit<RACRadioGroupProps, "className" | "style">,
17
+ FieldSupportProps {
18
+ /**
19
+ * Visible label for the group. Use `aria-label` instead where the design
20
+ * has none.
21
+ */
22
+ label?: ReactNode;
23
+ /** Label style overrides. */
24
+ labelCss?: SystemStyleObject;
25
+ /** Per-instance style overrides, merged after the recipe. */
26
+ css?: SystemStyleObject;
27
+ className?: string;
28
+ }
29
+
30
+ /**
31
+ * RadioGroup — react-aria-components <RadioGroup> for a set of Radios. Beyond
32
+ * the optional field chrome (label/helperText/errorMessage) it carries no
33
+ * styling of its own: compose with Stack for layout.
34
+ */
35
+ export const RadioGroup = ({
36
+ label,
37
+ labelCss,
38
+ helperText,
39
+ errorMessage,
40
+ helperTextCss,
41
+ css: cssProp,
42
+ className,
43
+ children,
44
+ ...rest
45
+ }: RadioGroupProps) => {
46
+ return (
47
+ <RACRadioGroup
48
+ className={cx(cssProp ? css(cssProp) : undefined, className)}
49
+ {...rest}
50
+ >
51
+ {(renderProps) => (
52
+ <>
53
+ {label != null && (
54
+ <FieldLabel isRequired={rest.isRequired} css={labelCss}>
55
+ {label}
56
+ </FieldLabel>
57
+ )}
58
+ {typeof children === "function" ? children(renderProps) : children}
59
+ <FieldSupport
60
+ helperText={helperText}
61
+ errorMessage={errorMessage}
62
+ helperTextCss={helperTextCss}
63
+ />
64
+ </>
65
+ )}
66
+ </RACRadioGroup>
67
+ );
68
+ };