@microbit/ui 0.1.0-alpha.8 → 0.1.0

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 (114) 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 +12 -10
  24. package/reset.css +12 -0
  25. package/src/Avatar.recipe.ts +191 -0
  26. package/src/Avatar.tsx +278 -0
  27. package/src/Breadcrumb.recipe.ts +45 -0
  28. package/src/Breadcrumb.tsx +114 -0
  29. package/src/Button.recipe.ts +88 -50
  30. package/src/Button.tsx +67 -14
  31. package/src/ButtonGroup.tsx +37 -15
  32. package/src/Card.recipe.ts +1 -2
  33. package/src/Card.tsx +2 -1
  34. package/src/Checkbox.recipe.ts +49 -14
  35. package/src/Checkbox.tsx +111 -32
  36. package/src/CheckboxGroup.tsx +70 -0
  37. package/src/CloseButton.tsx +3 -3
  38. package/src/CloseIcon.tsx +6 -4
  39. package/src/Code.tsx +1 -1
  40. package/src/Collapse.tsx +13 -14
  41. package/src/ComboBox.tsx +246 -0
  42. package/src/Divider.tsx +40 -7
  43. package/src/Drawer.recipe.ts +21 -10
  44. package/src/Drawer.tsx +3 -4
  45. package/src/ExternalLink.tsx +43 -0
  46. package/src/Fade.tsx +18 -4
  47. package/src/Field.recipe.ts +114 -0
  48. package/src/Field.tsx +187 -0
  49. package/src/GridList.recipe.ts +57 -0
  50. package/src/GridList.tsx +81 -0
  51. package/src/Heading.recipe.ts +20 -1
  52. package/src/Heading.tsx +3 -3
  53. package/src/Icon.tsx +23 -7
  54. package/src/IconButton.tsx +8 -18
  55. package/src/Image.tsx +1 -1
  56. package/src/Input.recipe.ts +41 -28
  57. package/src/Input.tsx +23 -7
  58. package/src/InputGroup.tsx +26 -12
  59. package/src/Kbd.tsx +1 -1
  60. package/src/Link.tsx +3 -3
  61. package/src/LinkBox.tsx +2 -3
  62. package/src/LinkButton.tsx +14 -13
  63. package/src/List.tsx +5 -7
  64. package/src/ListBox.recipe.ts +43 -0
  65. package/src/ListBox.tsx +88 -0
  66. package/src/Menu.recipe.ts +22 -19
  67. package/src/Menu.tsx +54 -28
  68. package/src/Modal.recipe.ts +17 -9
  69. package/src/Modal.tsx +123 -24
  70. package/src/MoreMenuButton.tsx +63 -0
  71. package/src/NativeSelect.tsx +41 -14
  72. package/src/NativeSelectField.tsx +84 -0
  73. package/src/NumberField.recipe.ts +56 -15
  74. package/src/NumberField.tsx +74 -22
  75. package/src/PopoverArrow.tsx +1 -2
  76. package/src/ProgressBar.tsx +3 -5
  77. package/src/Radio.recipe.ts +108 -0
  78. package/src/Radio.tsx +62 -0
  79. package/src/RadioGroup.tsx +68 -0
  80. package/src/Select.recipe.ts +211 -0
  81. package/src/Select.tsx +194 -0
  82. package/src/SharedUIProvider.tsx +63 -7
  83. package/src/Skeleton.tsx +146 -0
  84. package/src/Slide.tsx +2 -2
  85. package/src/Slider.recipe.ts +27 -16
  86. package/src/Slider.tsx +20 -12
  87. package/src/Spinner.tsx +11 -9
  88. package/src/Svg.tsx +2 -3
  89. package/src/Switch.recipe.ts +57 -11
  90. package/src/Switch.tsx +39 -7
  91. package/src/Text.recipe.ts +26 -0
  92. package/src/Text.tsx +6 -2
  93. package/src/TextField.tsx +39 -40
  94. package/src/Toast.recipe.ts +52 -15
  95. package/src/Toast.tsx +147 -48
  96. package/src/Tooltip.recipe.ts +32 -0
  97. package/src/Tooltip.tsx +68 -28
  98. package/src/TooltipButton.tsx +229 -0
  99. package/src/UnmountCallback.tsx +2 -2
  100. package/src/VisuallyHidden.tsx +1 -1
  101. package/src/base-preset.ts +329 -84
  102. package/src/base-tokens.ts +0 -3
  103. package/src/button-icon.ts +4 -5
  104. package/src/data-attrs.ts +16 -0
  105. package/src/dense-preset.ts +107 -0
  106. package/src/hooks/useBreakpointValue.ts +4 -4
  107. package/src/hooks/useClipboard.ts +43 -7
  108. package/src/hooks/useDisclosure.ts +31 -0
  109. package/src/hooks/useMediaQuery.ts +3 -4
  110. package/src/hooks/usePrevious.ts +1 -4
  111. package/src/index.ts +20 -3
  112. package/src/rac-locale.ts +33 -0
  113. package/src/system.ts +22 -1
  114. 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,16 +6,15 @@
6
6
  import { defineSlotRecipe } from "@pandacss/dev";
7
7
 
8
8
  /**
9
- * Checkbox slot recipe Chakra's md checkbox with the default blue
10
- * colorScheme (light mode). The control's `borderColor: inherit` picks up a
11
- * `borderColor` set on the root, matching Chakra's convention for tinting the
12
- * 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).
16
15
  *
17
- * Registered in the base preset (base-preset.ts). No variants, so it
18
- * needs no `staticCss` entry.
16
+ * Registered in the base preset (base-preset.ts), which also has the
17
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
19
18
  */
20
19
  export const checkbox = defineSlotRecipe({
21
20
  className: "checkbox",
@@ -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,11 +38,8 @@ export const checkbox = defineSlotRecipe({
34
38
  alignItems: "center",
35
39
  justifyContent: "center",
36
40
  flexShrink: 0,
37
- width: "4",
38
- height: "4",
39
- transitionProperty: "box-shadow",
40
- transitionDuration: "normal",
41
- border: "2px solid",
41
+ borderWidth: "2px",
42
+ borderStyle: "solid",
42
43
  borderRadius: "sm",
43
44
  borderColor: "inherit",
44
45
  color: "white",
@@ -52,13 +53,23 @@ export const checkbox = defineSlotRecipe({
52
53
  borderColor: "controlCheckedHoverBg",
53
54
  },
54
55
  },
56
+ // Disabled greys; the selected block restates _hover so the widened
57
+ // native-:hover condition can't re-tint a disabled control.
58
+ "&[data-disabled]": {
59
+ bg: "gray.100",
60
+ borderColor: "gray.100",
61
+ },
62
+ "&[data-selected][data-disabled]": {
63
+ bg: "gray.200",
64
+ borderColor: "gray.200",
65
+ color: "gray.500",
66
+ _hover: { bg: "gray.200", borderColor: "gray.200" },
67
+ },
55
68
  "&[data-focus-visible]": {
56
- focusShadow: "outline",
69
+ focusRing: "outline",
57
70
  },
58
71
  },
59
72
  icon: {
60
- width: "0.75rem",
61
- height: "0.75rem",
62
73
  transitionProperty: "transform",
63
74
  transitionDuration: "normal",
64
75
  },
@@ -68,4 +79,28 @@ export const checkbox = defineSlotRecipe({
68
79
  "&[data-disabled]": { opacity: 0.4 },
69
80
  },
70
81
  },
82
+ variants: {
83
+ // The icon dimensions are the 1.2em-wide check glyph at each size's icon
84
+ // fontSize (3xs/2xs/2xs), resolved to rem.
85
+ size: {
86
+ sm: {
87
+ control: { width: "3", height: "3" },
88
+ icon: { width: "0.54rem", height: "0.54rem" },
89
+ label: { fontSize: "sm" },
90
+ },
91
+ md: {
92
+ control: { width: "4", height: "4" },
93
+ icon: { width: "0.75rem", height: "0.75rem" },
94
+ label: { fontSize: "md" },
95
+ },
96
+ lg: {
97
+ control: { width: "5", height: "5" },
98
+ icon: { width: "0.75rem", height: "0.75rem" },
99
+ label: { fontSize: "lg" },
100
+ },
101
+ },
102
+ },
103
+ defaultVariants: {
104
+ size: "md",
105
+ },
71
106
  });
package/src/Checkbox.tsx CHANGED
@@ -3,64 +3,143 @@
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
- import { checkbox } from "styled-system/recipes";
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
- extends Omit<RACCheckboxProps, "className" | "children" | "style"> {
26
+ extends Omit<RACCheckboxProps, "className" | "children" | "style">,
27
+ CheckboxVariantProps {
17
28
  /** Per-instance style overrides for the root, merged after the recipe. */
18
29
  css?: SystemStyleObject;
19
30
  className?: string;
20
- 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;
21
53
  }
22
54
 
23
55
  /**
24
- * Checkbox — react-aria-components <Checkbox> styled like Chakra's md
25
- * checkbox. Children render as the label; wrap them in a visually-hidden
26
- * span for 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.
27
58
  */
28
59
  export const Checkbox = ({
60
+ size,
29
61
  css: cssProp,
30
62
  className,
31
63
  children,
64
+ control,
65
+ helperText,
66
+ helperTextCss,
32
67
  ...rest
33
68
  }: CheckboxProps) => {
34
- const slots = checkbox();
35
- return (
69
+ const slots = checkbox({ size });
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 = (
36
85
  <RACCheckbox
37
86
  className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
38
87
  {...rest}
88
+ aria-label={ariaLabel}
89
+ aria-describedby={describedBy}
39
90
  >
40
- {({ isSelected, isFocusVisible }) => (
41
- <>
42
- <span
43
- className={slots.control}
44
- data-selected={isSelected || undefined}
45
- data-focus-visible={isFocusVisible || undefined}
46
- aria-hidden
47
- >
48
- {isSelected && (
49
- <svg viewBox="0 0 12 10" className={slots.icon} aria-hidden>
50
- <polyline
51
- points="1.5 6 4.5 9 10.5 1"
52
- fill="none"
53
- stroke="currentColor"
54
- strokeWidth="2"
55
- strokeLinecap="round"
56
- strokeLinejoin="round"
57
- />
58
- </svg>
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
+ <>
101
+ <span
102
+ className={slots.control}
103
+ data-selected={isSelected || undefined}
104
+ data-focus-visible={isFocusVisible || undefined}
105
+ data-disabled={isDisabled || undefined}
106
+ aria-hidden
107
+ >
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
+ )}
120
+ </span>
121
+ {content != null && (
122
+ <span
123
+ className={slots.label}
124
+ data-disabled={isDisabled || undefined}
125
+ >
126
+ {content}
127
+ </span>
59
128
  )}
60
- </span>
61
- {children != null && <span className={slots.label}>{children}</span>}
62
- </>
63
- )}
129
+ </>
130
+ );
131
+ }}
64
132
  </RACCheckbox>
65
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
+ );
66
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: {