@microbit/ui 0.1.0-alpha.9 → 0.1.1

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 (113) hide show
  1. package/LICENSE.md +8 -0
  2. package/README.md +171 -20
  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 +44 -4
  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 +43 -3
  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 +44 -4
  22. package/lang/ui.zh-tw.json +44 -4
  23. package/package.json +13 -11
  24. package/src/Avatar.recipe.ts +191 -0
  25. package/src/Avatar.tsx +278 -0
  26. package/src/Breadcrumb.recipe.ts +45 -0
  27. package/src/Breadcrumb.tsx +114 -0
  28. package/src/Button.recipe.ts +88 -50
  29. package/src/Button.tsx +67 -14
  30. package/src/ButtonGroup.tsx +37 -15
  31. package/src/Card.recipe.ts +1 -2
  32. package/src/Card.tsx +2 -1
  33. package/src/Checkbox.recipe.ts +13 -12
  34. package/src/Checkbox.tsx +103 -34
  35. package/src/CheckboxGroup.tsx +70 -0
  36. package/src/CloseButton.tsx +3 -3
  37. package/src/CloseIcon.tsx +6 -4
  38. package/src/Code.tsx +1 -1
  39. package/src/Collapse.tsx +13 -14
  40. package/src/ComboBox.tsx +246 -0
  41. package/src/Divider.tsx +40 -7
  42. package/src/Drawer.recipe.ts +21 -10
  43. package/src/Drawer.tsx +3 -4
  44. package/src/ExternalLink.tsx +43 -0
  45. package/src/Fade.tsx +18 -4
  46. package/src/Field.recipe.ts +114 -0
  47. package/src/Field.tsx +187 -0
  48. package/src/GridList.recipe.ts +57 -0
  49. package/src/GridList.tsx +81 -0
  50. package/src/Heading.recipe.ts +20 -1
  51. package/src/Heading.tsx +3 -3
  52. package/src/Icon.tsx +23 -7
  53. package/src/IconButton.tsx +8 -18
  54. package/src/Image.tsx +1 -1
  55. package/src/Input.recipe.ts +30 -26
  56. package/src/Input.tsx +16 -7
  57. package/src/InputGroup.tsx +9 -9
  58. package/src/Kbd.tsx +1 -1
  59. package/src/Link.tsx +3 -3
  60. package/src/LinkBox.tsx +2 -3
  61. package/src/LinkButton.tsx +14 -13
  62. package/src/List.tsx +5 -7
  63. package/src/ListBox.recipe.ts +43 -0
  64. package/src/ListBox.tsx +88 -0
  65. package/src/Menu.recipe.ts +22 -19
  66. package/src/Menu.tsx +54 -28
  67. package/src/Modal.recipe.ts +17 -9
  68. package/src/Modal.tsx +123 -24
  69. package/src/MoreMenuButton.tsx +63 -0
  70. package/src/NativeSelect.tsx +37 -14
  71. package/src/NativeSelectField.tsx +84 -0
  72. package/src/NumberField.recipe.ts +56 -15
  73. package/src/NumberField.tsx +74 -22
  74. package/src/PopoverArrow.tsx +1 -2
  75. package/src/ProgressBar.tsx +3 -5
  76. package/src/Radio.recipe.ts +13 -12
  77. package/src/Radio.tsx +2 -29
  78. package/src/RadioGroup.tsx +68 -0
  79. package/src/Select.recipe.ts +211 -0
  80. package/src/Select.tsx +194 -0
  81. package/src/SharedUIProvider.tsx +63 -7
  82. package/src/Skeleton.tsx +146 -0
  83. package/src/Slide.tsx +2 -2
  84. package/src/Slider.recipe.ts +27 -16
  85. package/src/Slider.tsx +20 -12
  86. package/src/Spinner.tsx +11 -9
  87. package/src/Svg.tsx +2 -3
  88. package/src/Switch.recipe.ts +40 -21
  89. package/src/Switch.tsx +35 -5
  90. package/src/Text.recipe.ts +26 -0
  91. package/src/Text.tsx +6 -2
  92. package/src/TextField.tsx +35 -38
  93. package/src/Toast.recipe.ts +52 -15
  94. package/src/Toast.tsx +147 -48
  95. package/src/Tooltip.recipe.ts +32 -0
  96. package/src/Tooltip.tsx +68 -28
  97. package/src/TooltipButton.tsx +229 -0
  98. package/src/UnmountCallback.tsx +2 -2
  99. package/src/VisuallyHidden.tsx +1 -1
  100. package/src/base-preset.ts +323 -84
  101. package/src/base-tokens.ts +0 -3
  102. package/src/button-icon.ts +4 -5
  103. package/src/data-attrs.ts +16 -0
  104. package/src/dense-preset.ts +107 -0
  105. package/src/hooks/useBreakpointValue.ts +4 -4
  106. package/src/hooks/useClipboard.ts +5 -6
  107. package/src/hooks/useDisclosure.ts +31 -0
  108. package/src/hooks/useMediaQuery.ts +3 -4
  109. package/src/hooks/usePrevious.ts +1 -4
  110. package/src/index.ts +19 -3
  111. package/src/rac-locale.ts +33 -0
  112. package/src/system.ts +22 -1
  113. package/src/TextField.recipe.ts +0 -54
package/src/Button.tsx CHANGED
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  import { forwardRef, ReactNode } from "react";
7
+ import { useIntl } from "react-intl";
7
8
  import {
8
9
  Button as RACButton,
9
10
  ButtonProps as RACButtonProps,
@@ -12,6 +13,33 @@ import { css, cx } from "styled-system/css";
12
13
  import { button, ButtonVariantProps } from "styled-system/recipes";
13
14
  import { SystemStyleObject } from "styled-system/types";
14
15
  import { buttonIcon } from "./button-icon";
16
+ import { uiMessage } from "./messages";
17
+ import { Spinner } from "./Spinner";
18
+
19
+ // A 1em spinner centred over the hidden label. Out of flow, so the label alone
20
+ // sets the button's size; the recipe's base `position: relative` is what it
21
+ // anchors to. Its own component so useIntl runs only while a button is
22
+ // actually loading — a bare Button must keep working without an IntlProvider
23
+ // (test renders commonly lack one).
24
+ const ButtonSpinner = () => {
25
+ const intl = useIntl();
26
+ return (
27
+ <span
28
+ className={css({
29
+ position: "absolute",
30
+ inset: 0,
31
+ display: "flex",
32
+ alignItems: "center",
33
+ justifyContent: "center",
34
+ })}
35
+ >
36
+ <Spinner
37
+ aria-label={intl.formatMessage(uiMessage("ui.loading"))}
38
+ css={{ width: "1em", height: "1em" }}
39
+ />
40
+ </span>
41
+ );
42
+ };
15
43
 
16
44
  export interface ButtonProps
17
45
  extends Omit<RACButtonProps, "className" | "children">,
@@ -19,10 +47,16 @@ export interface ButtonProps
19
47
  /** Per-instance style overrides, merged after the recipe. */
20
48
  css?: SystemStyleObject;
21
49
  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;
50
+ /** Icon rendered before the label. */
51
+ startIcon?: ReactNode;
52
+ /** Icon rendered after the label. */
53
+ endIcon?: ReactNode;
54
+ /**
55
+ * Show a spinner in place of the label and disable interaction: the label
56
+ * stays in the layout but invisible, so the button keeps its size, and the
57
+ * dimmed disabled look applies.
58
+ */
59
+ isLoading?: boolean;
26
60
  children?: ReactNode;
27
61
  }
28
62
 
@@ -37,32 +71,51 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
37
71
  {
38
72
  variant,
39
73
  size,
74
+ tone,
40
75
  css: cssProp,
41
76
  className,
42
- leftIcon,
43
- rightIcon,
77
+ startIcon,
78
+ endIcon,
79
+ isLoading,
44
80
  children,
45
81
  ...rest
46
82
  },
47
83
  ref,
48
84
  ) {
85
+ const label = (
86
+ <>
87
+ {startIcon ? (
88
+ <span className={buttonIcon({ side: "start" })}>{startIcon}</span>
89
+ ) : null}
90
+ {children}
91
+ {endIcon ? (
92
+ <span className={buttonIcon({ side: "end" })}>{endIcon}</span>
93
+ ) : null}
94
+ </>
95
+ );
49
96
  return (
50
97
  <RACButton
51
98
  ref={ref}
52
99
  className={cx(
53
- button({ variant, size }),
100
+ button({ variant, size, tone }),
54
101
  cssProp ? css(cssProp) : undefined,
55
102
  className,
56
103
  )}
104
+ data-loading={isLoading ? "" : undefined}
57
105
  {...rest}
106
+ isDisabled={isLoading || rest.isDisabled}
58
107
  >
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}
108
+ {isLoading ? (
109
+ <>
110
+ <ButtonSpinner />
111
+ {/* Hidden with opacity, not removed: it keeps the button the size
112
+ it is when idle, so a row of buttons doesn't reflow. Still in
113
+ the accessibility tree, so the button keeps its name. */}
114
+ <span className={css({ opacity: 0 })}>{label}</span>
115
+ </>
116
+ ) : (
117
+ label
118
+ )}
66
119
  </RACButton>
67
120
  );
68
121
  },
@@ -9,8 +9,8 @@ import { SystemStyleObject } from "styled-system/types";
9
9
 
10
10
  export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
11
11
  /**
12
- * Square the inner radii so adjacent buttons form one control (Chakra's
13
- * `isAttached`). Otherwise buttons get a small gap.
12
+ * Square the inner radii so adjacent buttons form one control. Otherwise
13
+ * buttons get a small gap.
14
14
  */
15
15
  isAttached?: boolean;
16
16
  /** Per-instance style overrides. */
@@ -20,36 +20,58 @@ export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
20
20
 
21
21
  /**
22
22
  * ButtonGroup — lays out related buttons in a row. Works with any button
23
- * elements (shared-ui or native). Replaces Chakra's ButtonGroup.
23
+ * elements (shared-ui or native).
24
+ *
25
+ * Attached, the buttons divide by a hairline and the group keeps whatever
26
+ * outline the variant draws round the outside, so a 2px `secondary` reads as
27
+ * one bordered control with cells inside it rather than three boxes. A
28
+ * variant with no border of its own (`primary`, `solid`, `neutral`) divides
29
+ * by a gap in the same place, the surface behind the group showing through —
30
+ * see the `[data-attached]` rule in base-preset.ts.
24
31
  */
25
32
  export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
26
33
  function ButtonGroup({ isAttached, css: cssProp, className, ...rest }, ref) {
27
34
  return (
28
35
  <div
29
36
  ref={ref}
37
+ data-attached={isAttached ? "" : undefined}
30
38
  className={cx(
31
39
  css({
32
40
  display: "inline-flex",
33
41
  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's Button applied zIndex 1 on focus when grouped).
37
- "& > *": { _focus: { zIndex: 1 } },
42
+ // Buttons are position: relative, so a sibling would paint over
43
+ // the focused button's ring. Only while it shows: a permanently
44
+ // raised child would own the seam it shares, so a hover on its
45
+ // neighbour would stop short of it.
46
+ "& > *": { _focusVisible: { zIndex: 1 } },
38
47
  }),
39
48
  isAttached
40
49
  ? css({
41
50
  gap: 0,
42
- // :first-child/:last-child, not :first-of-type: attached
43
- // groups can mix element types (e.g. select + button), and
44
- // -of-type matches per element type.
45
- "& > *:first-child:not(:last-child)": {
51
+ // :first-child/:last-child, not -of-type: attached groups can
52
+ // mix element types (e.g. select + button).
53
+ //
54
+ // A hairline whatever the variant's border, which stays as the
55
+ // group's outline — matching it made a 2px seam read as a gap
56
+ // (microbit-foundation/ui#22). Both sides of a seam keep a
57
+ // border and overlap by its width so the two paint as one
58
+ // line; dropping one instead would move that button's border
59
+ // box off the cell you see, and `focusRing` is an outline, so
60
+ // the ring would land on the seam rather than outside it.
61
+ //
62
+ // padding-box so a fill doesn't paint under the transparent
63
+ // seam a borderless variant gets from the preset. Here rather
64
+ // than with that rule because `bg` is the `background`
65
+ // shorthand, which resets it from `recipes`.
66
+ "& > *": { backgroundClip: "padding-box" },
67
+ "& > *:not(:last-child)": {
46
68
  borderEndRadius: 0,
69
+ borderInlineEndWidth: "1px",
47
70
  },
48
- "& > *:not(:first-child):not(:last-child)": {
49
- borderRadius: 0,
50
- },
51
- "& > *:not(:first-child):last-child": {
71
+ "& > *:not(:first-child)": {
52
72
  borderStartRadius: 0,
73
+ borderInlineStartWidth: "1px",
74
+ marginInlineStart: "-1px",
53
75
  },
54
76
  })
55
77
  : css({ gap: 2 }),
@@ -6,8 +6,7 @@
6
6
  import { defineSlotRecipe } from "@pandacss/dev";
7
7
 
8
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.
9
+ * Card slot recipe, with `elevated` (default) and `outline` variants.
11
10
  *
12
11
  * Registered in the base preset (base-preset.ts); `variant` is
13
12
  * forwarded as a runtime prop so the variants are generated via `staticCss`.
package/src/Card.tsx CHANGED
@@ -16,7 +16,8 @@ export interface CardProps
16
16
  }
17
17
 
18
18
  /**
19
- * Card — Chakra's <Card> equivalent (md size). Place a CardBody inside.
19
+ * Card — a container styled with the `card` config recipe (md size). Place a
20
+ * CardBody inside.
20
21
  */
21
22
  export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
22
23
  { variant, css: cssProp, className, ...rest },
@@ -6,10 +6,9 @@
6
6
  import { defineSlotRecipe } from "@pandacss/dev";
7
7
 
8
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.
9
+ * Checkbox slot recipe with an sm/md/lg size scale. The control's
10
+ * `borderColor: inherit` picks up a `borderColor` set on the root, so call
11
+ * sites can tint the box from there.
13
12
  *
14
13
  * State styling keys off data attributes stamped on the control by the
15
14
  * shared-ui Checkbox (react-aria provides the state via render props).
@@ -27,6 +26,11 @@ export const checkbox = defineSlotRecipe({
27
26
  verticalAlign: "top",
28
27
  cursor: "pointer",
29
28
  position: "relative",
29
+ // The accessible outline stop (WCAG 1.4.11), as the input recipe.
30
+ // Stated here rather than on the control — whose `borderColor:
31
+ // inherit` reads it — so a call site can still tint the whole control
32
+ // by setting borderColor on the root.
33
+ borderColor: "gray.400",
30
34
  "&[data-disabled]": { cursor: "not-allowed" },
31
35
  },
32
36
  control: {
@@ -34,8 +38,6 @@ export const checkbox = defineSlotRecipe({
34
38
  alignItems: "center",
35
39
  justifyContent: "center",
36
40
  flexShrink: 0,
37
- transitionProperty: "box-shadow",
38
- transitionDuration: "normal",
39
41
  borderWidth: "2px",
40
42
  borderStyle: "solid",
41
43
  borderRadius: "sm",
@@ -51,8 +53,8 @@ export const checkbox = defineSlotRecipe({
51
53
  borderColor: "controlCheckedHoverBg",
52
54
  },
53
55
  },
54
- // Chakra's disabled greys; the selected block restates _hover so the
55
- // widened native-:hover condition can't re-tint a disabled control.
56
+ // Disabled greys; the selected block restates _hover so the widened
57
+ // native-:hover condition can't re-tint a disabled control.
56
58
  "&[data-disabled]": {
57
59
  bg: "gray.100",
58
60
  borderColor: "gray.100",
@@ -64,7 +66,7 @@ export const checkbox = defineSlotRecipe({
64
66
  _hover: { bg: "gray.200", borderColor: "gray.200" },
65
67
  },
66
68
  "&[data-focus-visible]": {
67
- focusShadow: "outline",
69
+ focusRing: "outline",
68
70
  },
69
71
  },
70
72
  icon: {
@@ -78,9 +80,8 @@ export const checkbox = defineSlotRecipe({
78
80
  },
79
81
  },
80
82
  variants: {
81
- // Chakra's Checkbox size scale. The icon dimensions are Chakra's
82
- // 1.2em-wide check glyph at each size's icon fontSize (3xs/2xs/2xs),
83
- // resolved to rem.
83
+ // The icon dimensions are the 1.2em-wide check glyph at each size's icon
84
+ // fontSize (3xs/2xs/2xs), resolved to rem.
84
85
  size: {
85
86
  sm: {
86
87
  control: { width: "3", height: "3" },
package/src/Checkbox.tsx CHANGED
@@ -3,14 +3,24 @@
3
3
  *
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
- import { ReactNode } from "react";
6
+ import { ReactNode, useId } from "react";
7
7
  import {
8
8
  Checkbox as RACCheckbox,
9
9
  CheckboxProps as RACCheckboxProps,
10
10
  } from "react-aria-components";
11
+ import { useIntl } from "react-intl";
11
12
  import { css, cx } from "styled-system/css";
12
13
  import { checkbox, CheckboxVariantProps } from "styled-system/recipes";
13
14
  import { SystemStyleObject } from "styled-system/types";
15
+ import { FieldHelperText } from "./Field";
16
+ import { uiMessage } from "./messages";
17
+
18
+ /** What a render-prop child is told about the checkbox. */
19
+ export interface CheckboxState {
20
+ isSelected: boolean;
21
+ isFocusVisible: boolean;
22
+ isDisabled: boolean;
23
+ }
14
24
 
15
25
  export interface CheckboxProps
16
26
  extends Omit<RACCheckboxProps, "className" | "children" | "style">,
@@ -18,59 +28,118 @@ export interface CheckboxProps
18
28
  /** Per-instance style overrides for the root, merged after the recipe. */
19
29
  css?: SystemStyleObject;
20
30
  className?: string;
21
- children?: ReactNode;
31
+ /**
32
+ * The label. A function receives the checkbox's state, for a label that
33
+ * changes with it.
34
+ */
35
+ children?: ReactNode | ((state: CheckboxState) => ReactNode);
36
+ /**
37
+ * Whether to draw the box. `false` is for a checkbox whose children draw
38
+ * the selected state themselves — a selectable tile, or an avatar that
39
+ * grows a tick. The label wrapper goes with it, so the children own the
40
+ * whole row, including the focus ring the box would otherwise carry.
41
+ *
42
+ * @default true
43
+ */
44
+ control?: boolean;
45
+ /**
46
+ * Help text below the checkbox, wired to its `aria-describedby` — the same
47
+ * chrome the labelled fields' `helperText` renders. With it the component
48
+ * gains a wrapping `<div>`, so the checkbox-plus-text moves as one block.
49
+ */
50
+ helperText?: ReactNode;
51
+ /** Per-instance style overrides for the helper text. */
52
+ helperTextCss?: SystemStyleObject;
22
53
  }
23
54
 
24
55
  /**
25
- * Checkbox — react-aria-components <Checkbox> styled like Chakra's checkbox.
26
- * Children render as the label; wrap them in a visually-hidden span for
27
- * icon-less checkboxes.
56
+ * Checkbox — a styled react-aria-components <Checkbox>. Children render as
57
+ * the label; wrap them in a visually-hidden span for icon-less checkboxes.
28
58
  */
29
59
  export const Checkbox = ({
30
60
  size,
31
61
  css: cssProp,
32
62
  className,
33
63
  children,
64
+ control,
65
+ helperText,
66
+ helperTextCss,
34
67
  ...rest
35
68
  }: CheckboxProps) => {
36
69
  const slots = checkbox({ size });
37
- return (
70
+ const helperId = useId();
71
+ const intl = useIntl();
72
+ const describedBy =
73
+ [rest["aria-describedby"], helperText != null ? helperId : undefined]
74
+ .filter(Boolean)
75
+ .join(" ") || undefined;
76
+ // Default name for a row-selection checkbox (a GridList row's). A future
77
+ // Table's select-all header checkbox shares the slot name and will need a
78
+ // different label.
79
+ const ariaLabel =
80
+ rest["aria-label"] ??
81
+ (rest.slot === "selection"
82
+ ? intl.formatMessage(uiMessage("ui.select-row-action"))
83
+ : undefined);
84
+ const checkboxElement = (
38
85
  <RACCheckbox
39
86
  className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
40
87
  {...rest}
88
+ aria-label={ariaLabel}
89
+ aria-describedby={describedBy}
41
90
  >
42
- {({ isSelected, isFocusVisible, isDisabled }) => (
43
- <>
44
- <span
45
- className={slots.control}
46
- data-selected={isSelected || undefined}
47
- data-focus-visible={isFocusVisible || undefined}
48
- data-disabled={isDisabled || undefined}
49
- aria-hidden
50
- >
51
- {isSelected && (
52
- <svg viewBox="0 0 12 10" className={slots.icon} aria-hidden>
53
- <polyline
54
- points="1.5 6 4.5 9 10.5 1"
55
- fill="none"
56
- stroke="currentColor"
57
- strokeWidth="2"
58
- strokeLinecap="round"
59
- strokeLinejoin="round"
60
- />
61
- </svg>
62
- )}
63
- </span>
64
- {children != null && (
91
+ {({ isSelected, isFocusVisible, isDisabled }) => {
92
+ const content =
93
+ typeof children === "function"
94
+ ? children({ isSelected, isFocusVisible, isDisabled })
95
+ : children;
96
+ if (control === false) {
97
+ return content;
98
+ }
99
+ return (
100
+ <>
65
101
  <span
66
- className={slots.label}
102
+ className={slots.control}
103
+ data-selected={isSelected || undefined}
104
+ data-focus-visible={isFocusVisible || undefined}
67
105
  data-disabled={isDisabled || undefined}
106
+ aria-hidden
68
107
  >
69
- {children}
108
+ {isSelected && (
109
+ <svg viewBox="0 0 12 10" className={slots.icon} aria-hidden>
110
+ <polyline
111
+ points="1.5 6 4.5 9 10.5 1"
112
+ fill="none"
113
+ stroke="currentColor"
114
+ strokeWidth="2"
115
+ strokeLinecap="round"
116
+ strokeLinejoin="round"
117
+ />
118
+ </svg>
119
+ )}
70
120
  </span>
71
- )}
72
- </>
73
- )}
121
+ {content != null && (
122
+ <span
123
+ className={slots.label}
124
+ data-disabled={isDisabled || undefined}
125
+ >
126
+ {content}
127
+ </span>
128
+ )}
129
+ </>
130
+ );
131
+ }}
74
132
  </RACCheckbox>
75
133
  );
134
+ if (helperText == null) {
135
+ return checkboxElement;
136
+ }
137
+ return (
138
+ <div>
139
+ {checkboxElement}
140
+ <FieldHelperText id={helperId} css={helperTextCss}>
141
+ {helperText}
142
+ </FieldHelperText>
143
+ </div>
144
+ );
76
145
  };
@@ -0,0 +1,70 @@
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
+ CheckboxGroup as RACCheckboxGroup,
9
+ CheckboxGroupProps as RACCheckboxGroupProps,
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 CheckboxGroupProps
16
+ extends Omit<RACCheckboxGroupProps, "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. */
26
+ css?: SystemStyleObject;
27
+ className?: string;
28
+ }
29
+
30
+ /**
31
+ * CheckboxGroup — react-aria-components <CheckboxGroup> for a set of
32
+ * Checkboxes sharing one value array (each Checkbox's `value` marks its
33
+ * entry). Beyond the optional field chrome (label/helperText/errorMessage)
34
+ * it carries no styling of its own: compose with Stack for layout, as
35
+ * RadioGroup does.
36
+ */
37
+ export const CheckboxGroup = ({
38
+ label,
39
+ labelCss,
40
+ helperText,
41
+ errorMessage,
42
+ helperTextCss,
43
+ css: cssProp,
44
+ className,
45
+ children,
46
+ ...rest
47
+ }: CheckboxGroupProps) => {
48
+ return (
49
+ <RACCheckboxGroup
50
+ className={cx(cssProp ? css(cssProp) : undefined, className)}
51
+ {...rest}
52
+ >
53
+ {(renderProps) => (
54
+ <>
55
+ {label != null && (
56
+ <FieldLabel isRequired={rest.isRequired} css={labelCss}>
57
+ {label}
58
+ </FieldLabel>
59
+ )}
60
+ {typeof children === "function" ? children(renderProps) : children}
61
+ <FieldSupport
62
+ helperText={helperText}
63
+ errorMessage={errorMessage}
64
+ helperTextCss={helperTextCss}
65
+ />
66
+ </>
67
+ )}
68
+ </RACCheckboxGroup>
69
+ );
70
+ };
@@ -10,7 +10,7 @@ import { CloseIcon } from "./CloseIcon";
10
10
 
11
11
  export interface CloseButtonProps
12
12
  extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className"> {
13
- /** Chakra CloseButton sizes: sm 24px box, md 32px (default). */
13
+ /** sm is a 24px box, md 32px (default). */
14
14
  size?: "sm" | "md";
15
15
  /**
16
16
  * Grow the touch target 8px beyond the visible button on every side via an
@@ -24,7 +24,7 @@ export interface CloseButtonProps
24
24
  }
25
25
 
26
26
  /**
27
- * CloseButton — Chakra's standalone X button. A plain button (not react-aria)
27
+ * CloseButton — a standalone X button. A plain button (not react-aria)
28
28
  * so call sites can extend the hit area with pseudo-elements, which
29
29
  * react-aria's press bounding-rect check would defeat.
30
30
  */
@@ -57,7 +57,7 @@ export const CloseButton = forwardRef<HTMLButtonElement, CloseButtonProps>(
57
57
  transitionDuration: "normal",
58
58
  _hover: { bg: "blackAlpha.100" },
59
59
  _active: { bg: "blackAlpha.200" },
60
- _focusVisible: { focusShadow: "outline" },
60
+ _focusVisible: { focusRing: "outline" },
61
61
  },
62
62
  size === "sm"
63
63
  ? { width: "6", height: "6", fontSize: "2xs" }
package/src/CloseIcon.tsx CHANGED
@@ -12,10 +12,12 @@ export interface CloseIconProps {
12
12
  }
13
13
 
14
14
  /**
15
- * CloseIcon — the "✕" glyph used on close buttons (dialogs, toasts). This is
16
- * Chakra's exact CloseButton path so the visual matches; sized to `1em` and
17
- * `fill: currentColor` like the other icons. Reused wherever a close control
18
- * is needed.
15
+ * CloseIcon — the "✕" glyph used on close buttons (dialogs, toasts). Sized to
16
+ * `1em` and `fill: currentColor` like the other icons. Reused wherever a
17
+ * close control is needed.
18
+ *
19
+ * The path is Chakra UI's CloseButton icon, inlined for visual parity with
20
+ * the apps' original look (see the notice in LICENSE.md).
19
21
  */
20
22
  export const CloseIcon = ({ css: cssProp, className }: CloseIconProps) => (
21
23
  <svg
package/src/Code.tsx CHANGED
@@ -6,7 +6,7 @@
6
6
  import { styled } from "styled-system/jsx";
7
7
 
8
8
  /**
9
- * Code — inline code chip matching Chakra's <Code> (gray subtle, light mode).
9
+ * Code — an inline code chip (subtle grey, light mode).
10
10
  */
11
11
  export const Code = styled("code", {
12
12
  base: {
package/src/Collapse.tsx CHANGED
@@ -15,13 +15,13 @@ import { css, cx } from "styled-system/css";
15
15
  import { SystemStyleObject } from "styled-system/types";
16
16
 
17
17
  export interface CollapseProps {
18
- /** Expanded when true (Chakra's `in`). */
18
+ /** Expanded when true. */
19
19
  isOpen: boolean;
20
- /** Height when collapsed (Chakra's `startingHeight`). */
20
+ /** Height when collapsed. */
21
21
  startingHeight?: number | string;
22
22
  /**
23
- * Height when expanded (Chakra's `endingHeight`; defaults to the measured
24
- * content height, tracked with a ResizeObserver so nested expansion works).
23
+ * Height when expanded (defaults to the measured content height, tracked
24
+ * with a ResizeObserver so nested expansion works).
25
25
  */
26
26
  endingHeight?: number | string;
27
27
  /** Remove the content from the DOM once the exit transition finishes. */
@@ -36,15 +36,14 @@ const toCssSize = (v: number | string) =>
36
36
  typeof v === "number" ? `${v}px` : v;
37
37
 
38
38
  /**
39
- * Collapse — Chakra's Collapse transition without framer-motion: the content
40
- * is measured and the wrapper's height (and opacity, when collapsing to zero)
41
- * transitions between the collapsed and expanded sizes. Content stays mounted
42
- * unless `unmountOnExit` is set.
39
+ * Collapse — an expand/collapse transition: the content is measured and the
40
+ * wrapper's height (and opacity, when collapsing to zero) transitions between
41
+ * the collapsed and expanded sizes. Content stays mounted unless
42
+ * `unmountOnExit` is set.
43
43
  *
44
- * As with framer-motion, only `isOpen` changes animate: initial mounts render
45
- * at rest, and height corrections (late measurement, content growth) snap —
46
- * otherwise a collapse mounted inside an entering view morphs during the
47
- * outer animation.
44
+ * Only `isOpen` changes animate: initial mounts render at rest, and height
45
+ * corrections (late measurement, content growth) snap — otherwise a collapse
46
+ * mounted inside an entering view morphs during the outer animation.
48
47
  */
49
48
  export const Collapse = ({
50
49
  isOpen,
@@ -155,8 +154,8 @@ export const Collapse = ({
155
154
  height: displayOpen ? expanded : collapsed,
156
155
  opacity: displayOpen || !hideWhenCollapsed ? 1 : 0,
157
156
  // At rest fully-collapsed, remove the content from the a11y tree
158
- // and tab order, as Chakra did (kept visible while animating so
159
- // the exit transition shows).
157
+ // and tab order (kept visible while animating so the exit
158
+ // transition shows).
160
159
  visibility:
161
160
  displayOpen || animating || !hideWhenCollapsed ? undefined : "hidden",
162
161
  ...style,