@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,87 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { ButtonHTMLAttributes, forwardRef } from "react";
7
+ import { css, cx } from "styled-system/css";
8
+ import { SystemStyleObject } from "styled-system/types";
9
+ import { CloseIcon } from "./CloseIcon";
10
+
11
+ export interface CloseButtonProps
12
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className"> {
13
+ /** Chakra CloseButton sizes: sm 24px box, md 32px (default). */
14
+ size?: "sm" | "md";
15
+ /**
16
+ * Grow the touch target 8px beyond the visible button on every side via an
17
+ * ::after overlay.
18
+ */
19
+ expandHitArea?: boolean;
20
+ "aria-label": string;
21
+ /** Per-instance style overrides, merged after the base. */
22
+ css?: SystemStyleObject;
23
+ className?: string;
24
+ }
25
+
26
+ /**
27
+ * CloseButton — Chakra's standalone X button. A plain button (not react-aria)
28
+ * so call sites can extend the hit area with pseudo-elements, which
29
+ * react-aria's press bounding-rect check would defeat.
30
+ */
31
+ export const CloseButton = forwardRef<HTMLButtonElement, CloseButtonProps>(
32
+ function CloseButton(
33
+ { size = "md", expandHitArea = false, css: cssProp, className, ...rest },
34
+ ref,
35
+ ) {
36
+ return (
37
+ <button
38
+ ref={ref}
39
+ type="button"
40
+ className={cx(
41
+ // A single css() call so per-instance overrides of base properties
42
+ // (e.g. borderRadius) merge rather than racing on stylesheet order.
43
+ css(
44
+ {
45
+ display: "inline-flex",
46
+ alignItems: "center",
47
+ justifyContent: "center",
48
+ flexShrink: 0,
49
+ position: "relative",
50
+ cursor: "pointer",
51
+ bg: "transparent",
52
+ border: "none",
53
+ color: "inherit",
54
+ outline: "none",
55
+ borderRadius: "md",
56
+ transitionProperty: "background-color, box-shadow",
57
+ transitionDuration: "normal",
58
+ _hover: { bg: "blackAlpha.100" },
59
+ _active: { bg: "blackAlpha.200" },
60
+ _focusVisible: { focusShadow: "outline" },
61
+ },
62
+ size === "sm"
63
+ ? { width: "6", height: "6", fontSize: "2xs" }
64
+ : { width: "8", height: "8", fontSize: "xs" },
65
+ expandHitArea
66
+ ? {
67
+ _after: {
68
+ position: "absolute",
69
+ top: -2,
70
+ right: -2,
71
+ bottom: -2,
72
+ left: -2,
73
+ content: '""',
74
+ },
75
+ }
76
+ : undefined,
77
+ cssProp,
78
+ ),
79
+ className,
80
+ )}
81
+ {...rest}
82
+ >
83
+ <CloseIcon />
84
+ </button>
85
+ );
86
+ },
87
+ );
@@ -0,0 +1,40 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { css, cx } from "styled-system/css";
7
+ import { SystemStyleObject } from "styled-system/types";
8
+
9
+ export interface CloseIconProps {
10
+ css?: SystemStyleObject;
11
+ className?: string;
12
+ }
13
+
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.
19
+ */
20
+ export const CloseIcon = ({ css: cssProp, className }: CloseIconProps) => (
21
+ <svg
22
+ viewBox="0 0 24 24"
23
+ focusable="false"
24
+ aria-hidden="true"
25
+ className={cx(
26
+ css({
27
+ width: "1em",
28
+ height: "1em",
29
+ display: "inline-block",
30
+ lineHeight: "1em",
31
+ flexShrink: 0,
32
+ fill: "currentColor",
33
+ ...cssProp,
34
+ }),
35
+ className,
36
+ )}
37
+ >
38
+ <path d="M.439,21.44a1.5,1.5,0,0,0,2.122,2.121L11.823,14.3a.25.25,0,0,1,.354,0l9.262,9.263a1.5,1.5,0,1,0,2.122-2.121L14.3,12.177a.25.25,0,0,1,0-.354l9.263-9.262A1.5,1.5,0,0,0,21.439.44L12.177,9.7a.25.25,0,0,1-.354,0L2.561.44A1.5,1.5,0,0,0,.439,2.561L9.7,11.823a.25.25,0,0,1,0,.354Z" />
39
+ </svg>
40
+ );
package/src/Code.tsx ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { styled } from "styled-system/jsx";
7
+
8
+ /**
9
+ * Code — inline code chip matching Chakra's <Code> (gray subtle, light mode).
10
+ */
11
+ export const Code = styled("code", {
12
+ base: {
13
+ fontFamily: "mono",
14
+ fontSize: "sm",
15
+ px: "0.2em",
16
+ borderRadius: "sm",
17
+ bg: "gray.100",
18
+ color: "gray.800",
19
+ },
20
+ });
@@ -0,0 +1,180 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import {
7
+ CSSProperties,
8
+ ReactNode,
9
+ useEffect,
10
+ useLayoutEffect,
11
+ useRef,
12
+ useState,
13
+ } from "react";
14
+ import { css, cx } from "styled-system/css";
15
+ import { SystemStyleObject } from "styled-system/types";
16
+
17
+ export interface CollapseProps {
18
+ /** Expanded when true (Chakra's `in`). */
19
+ isOpen: boolean;
20
+ /** Height when collapsed (Chakra's `startingHeight`). */
21
+ startingHeight?: number | string;
22
+ /**
23
+ * Height when expanded (Chakra's `endingHeight`; defaults to the measured
24
+ * content height, tracked with a ResizeObserver so nested expansion works).
25
+ */
26
+ endingHeight?: number | string;
27
+ /** Remove the content from the DOM once the exit transition finishes. */
28
+ unmountOnExit?: boolean;
29
+ css?: SystemStyleObject;
30
+ className?: string;
31
+ style?: CSSProperties;
32
+ children: ReactNode;
33
+ }
34
+
35
+ const toCssSize = (v: number | string) =>
36
+ typeof v === "number" ? `${v}px` : v;
37
+
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.
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.
48
+ */
49
+ export const Collapse = ({
50
+ isOpen,
51
+ startingHeight = 0,
52
+ endingHeight,
53
+ unmountOnExit = false,
54
+ css: cssProp,
55
+ className,
56
+ style,
57
+ children,
58
+ }: CollapseProps) => {
59
+ const contentRef = useRef<HTMLDivElement>(null);
60
+ const [measuredHeight, setMeasuredHeight] = useState<number>();
61
+ // For unmountOnExit: stay in the DOM until the exit transition ends.
62
+ const [present, setPresent] = useState(isOpen);
63
+ // The state the DOM shows. It follows isOpen from a layout effect so the
64
+ // transition-enable and the height flip land in the same commit (a
65
+ // render-time flip detector is unreliable: React can discard renders,
66
+ // losing the state update while keeping ref mutations).
67
+ const [displayOpen, setDisplayOpen] = useState(isOpen);
68
+ const [animating, setAnimating] = useState(false);
69
+
70
+ useLayoutEffect(() => {
71
+ if (isOpen === displayOpen) {
72
+ return;
73
+ }
74
+ if (isOpen) {
75
+ // Opening: mount (collapsed) first, then flip on the next frame so
76
+ // the browser has a computed collapsed state to transition from.
77
+ setPresent(true);
78
+ const raf = requestAnimationFrame(() => {
79
+ setAnimating(true);
80
+ setDisplayOpen(true);
81
+ });
82
+ return () => cancelAnimationFrame(raf);
83
+ }
84
+ // Closing: enable the transition and flip together, pre-paint.
85
+ setAnimating(true);
86
+ setDisplayOpen(false);
87
+ }, [isOpen, displayOpen]);
88
+
89
+ useLayoutEffect(() => {
90
+ const el = contentRef.current;
91
+ if (!el || endingHeight !== undefined) {
92
+ return;
93
+ }
94
+ setMeasuredHeight(el.scrollHeight);
95
+ const observer = new ResizeObserver(() =>
96
+ setMeasuredHeight(el.scrollHeight),
97
+ );
98
+ observer.observe(el);
99
+ return () => observer.disconnect();
100
+ }, [endingHeight, present]);
101
+
102
+ // Fallback for browsers/edge cases where transitionend doesn't fire.
103
+ useEffect(() => {
104
+ if (animating) {
105
+ const timeout = setTimeout(() => {
106
+ setAnimating(false);
107
+ if (!isOpen && unmountOnExit) {
108
+ setPresent(false);
109
+ }
110
+ }, 400);
111
+ return () => clearTimeout(timeout);
112
+ }
113
+ }, [animating, isOpen, unmountOnExit]);
114
+
115
+ if (unmountOnExit && !present) {
116
+ return null;
117
+ }
118
+
119
+ const expanded =
120
+ endingHeight !== undefined
121
+ ? toCssSize(endingHeight)
122
+ : measuredHeight !== undefined
123
+ ? `${measuredHeight}px`
124
+ : "auto";
125
+ const collapsed = toCssSize(startingHeight);
126
+ const hideWhenCollapsed = collapsed === "0px" || collapsed === "0";
127
+
128
+ return (
129
+ <div
130
+ onTransitionEnd={(e) => {
131
+ if (e.propertyName === "height" && e.target === e.currentTarget) {
132
+ setAnimating(false);
133
+ if (!isOpen && unmountOnExit) {
134
+ setPresent(false);
135
+ }
136
+ }
137
+ }}
138
+ className={cx(
139
+ css(
140
+ {
141
+ overflow: "hidden",
142
+ display: "block",
143
+ transitionDuration: "0.25s",
144
+ transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
145
+ _motionReduce: { transition: "none" },
146
+ },
147
+ cssProp,
148
+ ),
149
+ className,
150
+ )}
151
+ // Runtime-measured/caller-supplied heights; transitions enabled only
152
+ // while an isOpen change is in flight.
153
+ style={{
154
+ transitionProperty: animating ? "height, opacity" : "none",
155
+ height: displayOpen ? expanded : collapsed,
156
+ opacity: displayOpen || !hideWhenCollapsed ? 1 : 0,
157
+ // 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).
160
+ visibility:
161
+ displayOpen || animating || !hideWhenCollapsed ? undefined : "hidden",
162
+ ...style,
163
+ }}
164
+ >
165
+ <div
166
+ ref={contentRef}
167
+ // flow-root: the measured element must be a block formatting
168
+ // context, or children's margins collapse through it and
169
+ // scrollHeight under-measures — the outer (overflow: hidden)
170
+ // wrapper IS a BFC and needs the full height, so the mismatch
171
+ // clips the last line of margined content (python-editor's API
172
+ // docs). Rendering is unchanged: the margins always displayed
173
+ // inside the outer BFC.
174
+ className={css({ display: "flow-root" })}
175
+ >
176
+ {children}
177
+ </div>
178
+ </div>
179
+ );
180
+ };
@@ -0,0 +1,192 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import {
7
+ ForwardedRef,
8
+ forwardRef,
9
+ ReactNode,
10
+ useLayoutEffect,
11
+ useRef,
12
+ useState,
13
+ } from "react";
14
+ import {
15
+ Button as RACButton,
16
+ ComboBox as RACComboBox,
17
+ ComboBoxProps as RACComboBoxProps,
18
+ Input as RACInput,
19
+ Label as RACLabel,
20
+ ListBox as RACListBox,
21
+ Popover,
22
+ PopoverProps,
23
+ } from "react-aria-components";
24
+ import { RiArrowDownSLine } from "react-icons/ri";
25
+ import { css, cx } from "styled-system/css";
26
+ import { select, SelectVariantProps } from "styled-system/recipes";
27
+ import { SystemStyleObject } from "styled-system/types";
28
+ import { Icon } from "./Icon";
29
+ import { SelectSlotProvider } from "./Select";
30
+
31
+ export interface ComboBoxProps<T extends object>
32
+ extends Omit<RACComboBoxProps<T>, "className" | "children" | "style">,
33
+ SelectVariantProps {
34
+ /** Visible label. Use `aria-label` instead where the design has none. */
35
+ label?: ReactNode;
36
+ placeholder?: string;
37
+ /**
38
+ * Rendered inside the control, before the input — an icon for the current
39
+ * value, say. A ComboBox's control is a text input, so unlike a Select it
40
+ * cannot show anything but text for what is chosen; this is the way round
41
+ * that (react-select did it with a custom `SingleValue`).
42
+ */
43
+ startContent?: ReactNode;
44
+ /** `SelectOption`s. */
45
+ children: ReactNode;
46
+ /**
47
+ * Replaces the chevron; pass `null` for none, which is what a plain
48
+ * autocomplete wants (react-select's `dropdownIndicator: display none`).
49
+ */
50
+ indicator?: ReactNode | null;
51
+ /**
52
+ * Shown in place of the list when nothing matches (react-select's
53
+ * `noOptionsMessage`). Implies `allowsEmptyCollection`, since RAC otherwise
54
+ * closes the popover the moment the collection empties.
55
+ */
56
+ emptyState?: ReactNode;
57
+ /**
58
+ * Keep the dropdown shut until this prop is true. For gating on a minimum
59
+ * query length — react-aria has no `minLength`, and rendering an empty list
60
+ * still opens an empty card.
61
+ */
62
+ isPopoverHidden?: boolean;
63
+ placement?: PopoverProps["placement"];
64
+ /**
65
+ * Cap the dropdown's height (react-select's `maxMenuHeight`). A prop rather
66
+ * than a `contentCss` rule because RAC writes its own max-height inline
67
+ * while positioning, which beats any class.
68
+ */
69
+ maxHeight?: number;
70
+ /**
71
+ * Per-instance overrides for the control — the box around the input, its
72
+ * `startContent` and its indicator, which is what `Select`'s `css` styles
73
+ * too. Reach the input itself through the `select` recipe's `value` slot.
74
+ */
75
+ css?: SystemStyleObject;
76
+ /** Per-instance overrides for the dropdown card. */
77
+ contentCss?: SystemStyleObject;
78
+ className?: string;
79
+ }
80
+
81
+ /**
82
+ * ComboBox — a text input that filters a listbox, for choosing one of a known
83
+ * set where typing to narrow it down is the point. Use Select where the list
84
+ * is short enough to just pick from.
85
+ *
86
+ * Note the react-select difference this replaces: react-select filtered on
87
+ * `label` and kept the menu open on selection unless told otherwise, whereas
88
+ * react-aria filters on each item's `textValue` and closes on selection.
89
+ */
90
+ const ComboBoxInner = <T extends object>(
91
+ {
92
+ label,
93
+ placeholder,
94
+ startContent,
95
+ children,
96
+ indicator,
97
+ emptyState,
98
+ isPopoverHidden,
99
+ placement = "bottom start",
100
+ maxHeight,
101
+ css: cssProp,
102
+ contentCss,
103
+ className,
104
+ ...props
105
+ }: ComboBoxProps<T>,
106
+ ref: ForwardedRef<HTMLInputElement>,
107
+ ) => {
108
+ // As Select: forward whatever variant groups the merged recipe has.
109
+ const [variantProps, rest] = select.splitVariantProps(props);
110
+ const slots = select(variantProps);
111
+ // Anchor the card to the whole control, not to the bare input inside it —
112
+ // otherwise it hangs off the text baseline and is as narrow as the input.
113
+ const triggerRef = useRef<HTMLDivElement>(null);
114
+ // RAC's --trigger-width measures the input it anchors a ComboBox to, which
115
+ // is the control's content box — so a card sized from it is narrower than
116
+ // the field by the padding and border. Measure the control instead. State
117
+ // rather than reading the ref at render time: the popover is mounted from
118
+ // the first render, before the ref is set, and nothing would re-render it.
119
+ const [triggerWidth, setTriggerWidth] = useState<number>();
120
+ useLayoutEffect(() => {
121
+ const el = triggerRef.current;
122
+ if (!el) {
123
+ return;
124
+ }
125
+ const update = () => setTriggerWidth(el.offsetWidth);
126
+ update();
127
+ if (typeof ResizeObserver === "undefined") {
128
+ return;
129
+ }
130
+ const observer = new ResizeObserver(update);
131
+ observer.observe(el);
132
+ return () => observer.disconnect();
133
+ }, []);
134
+ return (
135
+ <SelectSlotProvider value={slots}>
136
+ <RACComboBox
137
+ allowsEmptyCollection={emptyState != null}
138
+ {...(rest as RACComboBoxProps<T>)}
139
+ className={cx(slots.root, className)}
140
+ >
141
+ {label != null && <RACLabel className={slots.label}>{label}</RACLabel>}
142
+ <div
143
+ ref={triggerRef}
144
+ className={cx(slots.trigger, cssProp ? css(cssProp) : undefined)}
145
+ >
146
+ {startContent}
147
+ <RACInput
148
+ ref={ref}
149
+ placeholder={placeholder}
150
+ className={slots.value}
151
+ />
152
+ {indicator !== null && (
153
+ <RACButton className={slots.indicator}>
154
+ {indicator ?? <Icon as={RiArrowDownSLine} />}
155
+ </RACButton>
156
+ )}
157
+ </div>
158
+ {!isPopoverHidden && (
159
+ <Popover
160
+ triggerRef={triggerRef}
161
+ placement={placement}
162
+ maxHeight={maxHeight}
163
+ style={triggerWidth ? { width: triggerWidth } : undefined}
164
+ className={cx(
165
+ slots.content,
166
+ contentCss ? css(contentCss) : undefined,
167
+ )}
168
+ >
169
+ <RACListBox
170
+ className={slots.list}
171
+ renderEmptyState={
172
+ emptyState
173
+ ? () => <div className={slots.empty}>{emptyState}</div>
174
+ : undefined
175
+ }
176
+ >
177
+ {children}
178
+ </RACListBox>
179
+ </Popover>
180
+ )}
181
+ </RACComboBox>
182
+ </SelectSlotProvider>
183
+ );
184
+ };
185
+
186
+ /**
187
+ * forwardRef with generics needs the cast (React's types cannot express it),
188
+ * so the ref lands on the input — call sites focus it for validation.
189
+ */
190
+ export const ComboBox = forwardRef(ComboBoxInner) as <T extends object>(
191
+ props: ComboBoxProps<T> & { ref?: ForwardedRef<HTMLInputElement> },
192
+ ) => ReturnType<typeof ComboBoxInner>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { ComponentProps, forwardRef } from "react";
7
+ import { styled } from "styled-system/jsx";
8
+
9
+ const StyledDivider = styled("hr", {
10
+ base: {
11
+ border: 0,
12
+ borderColor: "gray.200",
13
+ opacity: 0.6,
14
+ },
15
+ variants: {
16
+ orientation: {
17
+ horizontal: {
18
+ borderBottomStyle: "solid",
19
+ width: "100%",
20
+ },
21
+ vertical: {
22
+ borderLeftStyle: "solid",
23
+ height: "100%",
24
+ },
25
+ },
26
+ // Rule weight, on the orientation's drawn edge (see compoundVariants).
27
+ // Semantic rather than a raw border width so call sites don't need to
28
+ // know which edge an orientation draws with.
29
+ thickness: {
30
+ thin: {},
31
+ thick: {},
32
+ },
33
+ },
34
+ compoundVariants: [
35
+ {
36
+ orientation: "horizontal",
37
+ thickness: "thin",
38
+ css: { borderBottomWidth: "1px" },
39
+ },
40
+ {
41
+ orientation: "horizontal",
42
+ thickness: "thick",
43
+ css: { borderBottomWidth: "2px" },
44
+ },
45
+ {
46
+ orientation: "vertical",
47
+ thickness: "thin",
48
+ css: { borderLeftWidth: "1px" },
49
+ },
50
+ {
51
+ orientation: "vertical",
52
+ thickness: "thick",
53
+ css: { borderLeftWidth: "2px" },
54
+ },
55
+ ],
56
+ defaultVariants: { orientation: "horizontal", thickness: "thin" },
57
+ });
58
+
59
+ export interface DividerProps extends ComponentProps<typeof StyledDivider> {}
60
+
61
+ /**
62
+ * Divider — a hairline rule matching Chakra's <Divider> (60% opacity; set
63
+ * `borderColor` to tint, `thickness="thick"` for a 2px rule).
64
+ * `orientation="vertical"` needs a height from the layout, e.g. a stretched
65
+ * flex row (Chakra's vertical divider likewise relied on `height: 100%`).
66
+ *
67
+ * Decorative by default: hidden from assistive tech, since a visual rule
68
+ * between sections is noise as an announced separator (and every call site
69
+ * was opting out by hand). Pass `aria-hidden={false}` for a divider that
70
+ * should be exposed as a semantic separator.
71
+ */
72
+ export const Divider = forwardRef<HTMLHRElement, DividerProps>(
73
+ function Divider(props, ref) {
74
+ return (
75
+ <StyledDivider
76
+ ref={ref}
77
+ aria-hidden
78
+ aria-orientation={
79
+ props.orientation === "vertical" ? "vertical" : "horizontal"
80
+ }
81
+ {...props}
82
+ />
83
+ );
84
+ },
85
+ );
@@ -0,0 +1,98 @@
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
+ * Drawer slot recipe — Chakra's default Drawer parts (light mode) at its
10
+ * default `xs` size (20rem panel). Consumed by the shared-ui Drawer, which maps
11
+ * the slots onto react-aria-components' ModalOverlay / Modal / Dialog.
12
+ *
13
+ * The enter transition approximates Chakra's slide spring (damping 25,
14
+ * stiffness 180) with a decelerating tween; exit matches its 0.15s ease-in-out
15
+ * tween.
16
+ *
17
+ * Registered in the base preset (base-preset.ts); `placement` is
18
+ * forwarded as a runtime prop so the variants are generated via `staticCss`.
19
+ */
20
+ export const drawer = defineSlotRecipe({
21
+ className: "drawer",
22
+ slots: ["overlay", "content", "inner", "header", "body"],
23
+ base: {
24
+ overlay: {
25
+ position: "fixed",
26
+ inset: 0,
27
+ w: "100%",
28
+ h: "100%",
29
+ bg: "blackAlpha.600",
30
+ zIndex: "modal",
31
+ opacity: 1,
32
+ transition: "opacity 0.2s ease-out",
33
+ "&[data-entering]": { opacity: 0 },
34
+ "&[data-exiting]": { opacity: 0 },
35
+ _motionReduce: { transition: "none" },
36
+ },
37
+ content: {
38
+ position: "fixed",
39
+ top: 0,
40
+ bottom: 0,
41
+ width: "100%",
42
+ maxWidth: "xs",
43
+ maxH: "100dvh",
44
+ color: "inherit",
45
+ bg: "white",
46
+ boxShadow: "lg",
47
+ display: "flex",
48
+ flexDirection: "column",
49
+ outline: "none",
50
+ zIndex: "modal",
51
+ transform: "translateX(0)",
52
+ transition: "transform 0.3s cubic-bezier(0, 0, 0.2, 1)",
53
+ "&[data-exiting]": {
54
+ transition: "transform 0.15s cubic-bezier(0.4, 0, 0.2, 1)",
55
+ },
56
+ _motionReduce: { transition: "none" },
57
+ },
58
+ inner: {
59
+ outline: "none",
60
+ display: "flex",
61
+ flexDirection: "column",
62
+ width: "100%",
63
+ height: "100%",
64
+ },
65
+ header: {
66
+ px: "6",
67
+ py: "4",
68
+ fontSize: "xl",
69
+ fontWeight: "semibold",
70
+ flexShrink: 0,
71
+ },
72
+ body: {
73
+ px: "6",
74
+ py: "2",
75
+ flex: "1",
76
+ overflow: "auto",
77
+ },
78
+ },
79
+ variants: {
80
+ placement: {
81
+ left: {
82
+ content: {
83
+ left: 0,
84
+ "&[data-entering]": { transform: "translateX(-100%)" },
85
+ "&[data-exiting]": { transform: "translateX(-100%)" },
86
+ },
87
+ },
88
+ right: {
89
+ content: {
90
+ right: 0,
91
+ "&[data-entering]": { transform: "translateX(100%)" },
92
+ "&[data-exiting]": { transform: "translateX(100%)" },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ defaultVariants: { placement: "left" },
98
+ });