@microbit/ui 0.1.0-alpha.10

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 (86) hide show
  1. package/LICENSE.md +53 -0
  2. package/README.md +220 -0
  3. package/lang/ui.ca.json +22 -0
  4. package/lang/ui.de.json +22 -0
  5. package/lang/ui.en-us.json +22 -0
  6. package/lang/ui.en.json +22 -0
  7. package/lang/ui.es-es.json +22 -0
  8. package/lang/ui.fr.json +22 -0
  9. package/lang/ui.ga-ie.json +22 -0
  10. package/lang/ui.ja.json +22 -0
  11. package/lang/ui.ko.json +22 -0
  12. package/lang/ui.lol.json +22 -0
  13. package/lang/ui.nl.json +22 -0
  14. package/lang/ui.pl.json +22 -0
  15. package/lang/ui.pt-br.json +22 -0
  16. package/lang/ui.zh-cn.json +22 -0
  17. package/lang/ui.zh-tw.json +22 -0
  18. package/package.json +58 -0
  19. package/postcss-legacy-safari.cjs +96 -0
  20. package/reset.css +35 -0
  21. package/src/Button.recipe.ts +162 -0
  22. package/src/Button.tsx +69 -0
  23. package/src/ButtonGroup.tsx +63 -0
  24. package/src/Card.recipe.ts +49 -0
  25. package/src/Card.tsx +56 -0
  26. package/src/Checkbox.recipe.ts +105 -0
  27. package/src/Checkbox.tsx +76 -0
  28. package/src/CloseButton.tsx +87 -0
  29. package/src/CloseIcon.tsx +40 -0
  30. package/src/Code.tsx +20 -0
  31. package/src/Collapse.tsx +180 -0
  32. package/src/Divider.tsx +52 -0
  33. package/src/Drawer.recipe.ts +98 -0
  34. package/src/Drawer.tsx +138 -0
  35. package/src/Fade.tsx +48 -0
  36. package/src/Heading.recipe.ts +52 -0
  37. package/src/Heading.tsx +14 -0
  38. package/src/Icon.tsx +55 -0
  39. package/src/IconButton.tsx +44 -0
  40. package/src/Image.tsx +11 -0
  41. package/src/Input.recipe.ts +72 -0
  42. package/src/Input.tsx +40 -0
  43. package/src/InputGroup.tsx +62 -0
  44. package/src/Kbd.tsx +26 -0
  45. package/src/Link.tsx +23 -0
  46. package/src/LinkBox.tsx +88 -0
  47. package/src/LinkButton.tsx +80 -0
  48. package/src/List.tsx +31 -0
  49. package/src/Menu.recipe.ts +119 -0
  50. package/src/Menu.tsx +261 -0
  51. package/src/Modal.recipe.ts +163 -0
  52. package/src/Modal.tsx +279 -0
  53. package/src/NativeSelect.tsx +94 -0
  54. package/src/NumberField.recipe.ts +67 -0
  55. package/src/NumberField.tsx +86 -0
  56. package/src/PopoverArrow.tsx +67 -0
  57. package/src/ProgressBar.tsx +61 -0
  58. package/src/Radio.recipe.ts +107 -0
  59. package/src/Radio.tsx +89 -0
  60. package/src/SharedUIProvider.tsx +55 -0
  61. package/src/Slide.tsx +54 -0
  62. package/src/Slider.recipe.ts +102 -0
  63. package/src/Slider.tsx +163 -0
  64. package/src/Spinner.tsx +69 -0
  65. package/src/Svg.tsx +23 -0
  66. package/src/Switch.recipe.ts +107 -0
  67. package/src/Switch.tsx +62 -0
  68. package/src/Text.recipe.ts +29 -0
  69. package/src/Text.tsx +16 -0
  70. package/src/TextField.recipe.ts +54 -0
  71. package/src/TextField.tsx +89 -0
  72. package/src/Toast.recipe.ts +98 -0
  73. package/src/Toast.tsx +171 -0
  74. package/src/Tooltip.tsx +91 -0
  75. package/src/UnmountCallback.tsx +18 -0
  76. package/src/VisuallyHidden.tsx +14 -0
  77. package/src/base-preset.ts +294 -0
  78. package/src/base-tokens.ts +1017 -0
  79. package/src/button-icon.ts +23 -0
  80. package/src/hooks/useBreakpointValue.ts +63 -0
  81. package/src/hooks/useClipboard.ts +64 -0
  82. package/src/hooks/useMediaQuery.ts +28 -0
  83. package/src/hooks/usePrevious.ts +18 -0
  84. package/src/index.ts +55 -0
  85. package/src/messages.ts +17 -0
  86. package/src/system.ts +36 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { defineSlotRecipe } from "@pandacss/dev";
7
+
8
+ /**
9
+ * Checkbox slot recipe — Chakra's checkbox with the default blue colorScheme
10
+ * (light mode) and its sm/md/lg size scale. The control's
11
+ * `borderColor: inherit` picks up a `borderColor` set on the root, matching
12
+ * Chakra's convention for tinting the box from the call site.
13
+ *
14
+ * State styling keys off data attributes stamped on the control by the
15
+ * shared-ui Checkbox (react-aria provides the state via render props).
16
+ *
17
+ * Registered in the base preset (base-preset.ts), which also has the
18
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
19
+ */
20
+ export const checkbox = defineSlotRecipe({
21
+ className: "checkbox",
22
+ slots: ["root", "control", "icon", "label"],
23
+ base: {
24
+ root: {
25
+ display: "inline-flex",
26
+ alignItems: "center",
27
+ verticalAlign: "top",
28
+ cursor: "pointer",
29
+ position: "relative",
30
+ "&[data-disabled]": { cursor: "not-allowed" },
31
+ },
32
+ control: {
33
+ display: "inline-flex",
34
+ alignItems: "center",
35
+ justifyContent: "center",
36
+ flexShrink: 0,
37
+ transitionProperty: "box-shadow",
38
+ transitionDuration: "normal",
39
+ borderWidth: "2px",
40
+ borderStyle: "solid",
41
+ borderRadius: "sm",
42
+ borderColor: "inherit",
43
+ color: "white",
44
+ bg: "white",
45
+ "&[data-selected]": {
46
+ bg: "controlCheckedBg",
47
+ borderColor: "controlCheckedBg",
48
+ color: "white",
49
+ _hover: {
50
+ bg: "controlCheckedHoverBg",
51
+ borderColor: "controlCheckedHoverBg",
52
+ },
53
+ },
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
+ "&[data-disabled]": {
57
+ bg: "gray.100",
58
+ borderColor: "gray.100",
59
+ },
60
+ "&[data-selected][data-disabled]": {
61
+ bg: "gray.200",
62
+ borderColor: "gray.200",
63
+ color: "gray.500",
64
+ _hover: { bg: "gray.200", borderColor: "gray.200" },
65
+ },
66
+ "&[data-focus-visible]": {
67
+ focusShadow: "outline",
68
+ },
69
+ },
70
+ icon: {
71
+ transitionProperty: "transform",
72
+ transitionDuration: "normal",
73
+ },
74
+ label: {
75
+ userSelect: "none",
76
+ marginStart: "2",
77
+ "&[data-disabled]": { opacity: 0.4 },
78
+ },
79
+ },
80
+ 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.
84
+ size: {
85
+ sm: {
86
+ control: { width: "3", height: "3" },
87
+ icon: { width: "0.54rem", height: "0.54rem" },
88
+ label: { fontSize: "sm" },
89
+ },
90
+ md: {
91
+ control: { width: "4", height: "4" },
92
+ icon: { width: "0.75rem", height: "0.75rem" },
93
+ label: { fontSize: "md" },
94
+ },
95
+ lg: {
96
+ control: { width: "5", height: "5" },
97
+ icon: { width: "0.75rem", height: "0.75rem" },
98
+ label: { fontSize: "lg" },
99
+ },
100
+ },
101
+ },
102
+ defaultVariants: {
103
+ size: "md",
104
+ },
105
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { ReactNode } from "react";
7
+ import {
8
+ Checkbox as RACCheckbox,
9
+ CheckboxProps as RACCheckboxProps,
10
+ } from "react-aria-components";
11
+ import { css, cx } from "styled-system/css";
12
+ import { checkbox, CheckboxVariantProps } from "styled-system/recipes";
13
+ import { SystemStyleObject } from "styled-system/types";
14
+
15
+ export interface CheckboxProps
16
+ extends Omit<RACCheckboxProps, "className" | "children" | "style">,
17
+ CheckboxVariantProps {
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
+ * 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.
28
+ */
29
+ export const Checkbox = ({
30
+ size,
31
+ css: cssProp,
32
+ className,
33
+ children,
34
+ ...rest
35
+ }: CheckboxProps) => {
36
+ const slots = checkbox({ size });
37
+ return (
38
+ <RACCheckbox
39
+ className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
40
+ {...rest}
41
+ >
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 && (
65
+ <span
66
+ className={slots.label}
67
+ data-disabled={isDisabled || undefined}
68
+ >
69
+ {children}
70
+ </span>
71
+ )}
72
+ </>
73
+ )}
74
+ </RACCheckbox>
75
+ );
76
+ };
@@ -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,52 @@
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
+ borderBottomWidth: "1px",
19
+ borderBottomStyle: "solid",
20
+ width: "100%",
21
+ },
22
+ vertical: {
23
+ borderLeftWidth: "1px",
24
+ borderLeftStyle: "solid",
25
+ height: "100%",
26
+ },
27
+ },
28
+ },
29
+ defaultVariants: { orientation: "horizontal" },
30
+ });
31
+
32
+ export interface DividerProps extends ComponentProps<typeof StyledDivider> {}
33
+
34
+ /**
35
+ * Divider — a hairline rule matching Chakra's <Divider> (60% opacity; set
36
+ * `borderColor` to tint). `orientation="vertical"` needs a height from the
37
+ * layout, e.g. a stretched flex row (Chakra's vertical divider likewise
38
+ * relied on `height: 100%`).
39
+ */
40
+ export const Divider = forwardRef<HTMLHRElement, DividerProps>(
41
+ function Divider(props, ref) {
42
+ return (
43
+ <StyledDivider
44
+ ref={ref}
45
+ aria-orientation={
46
+ props.orientation === "vertical" ? "vertical" : "horizontal"
47
+ }
48
+ {...props}
49
+ />
50
+ );
51
+ },
52
+ );
@@ -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
+ });