@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.
- package/LICENSE.md +53 -0
- package/README.md +220 -0
- package/lang/ui.ca.json +22 -0
- package/lang/ui.de.json +22 -0
- package/lang/ui.en-us.json +22 -0
- package/lang/ui.en.json +22 -0
- package/lang/ui.es-es.json +22 -0
- package/lang/ui.fr.json +22 -0
- package/lang/ui.ga-ie.json +22 -0
- package/lang/ui.ja.json +22 -0
- package/lang/ui.ko.json +22 -0
- package/lang/ui.lol.json +22 -0
- package/lang/ui.nl.json +22 -0
- package/lang/ui.pl.json +22 -0
- package/lang/ui.pt-br.json +22 -0
- package/lang/ui.zh-cn.json +22 -0
- package/lang/ui.zh-tw.json +22 -0
- package/package.json +58 -0
- package/postcss-legacy-safari.cjs +96 -0
- package/reset.css +35 -0
- package/src/Button.recipe.ts +162 -0
- package/src/Button.tsx +69 -0
- package/src/ButtonGroup.tsx +63 -0
- package/src/Card.recipe.ts +49 -0
- package/src/Card.tsx +56 -0
- package/src/Checkbox.recipe.ts +105 -0
- package/src/Checkbox.tsx +76 -0
- package/src/CloseButton.tsx +87 -0
- package/src/CloseIcon.tsx +40 -0
- package/src/Code.tsx +20 -0
- package/src/Collapse.tsx +180 -0
- package/src/Divider.tsx +52 -0
- package/src/Drawer.recipe.ts +98 -0
- package/src/Drawer.tsx +138 -0
- package/src/Fade.tsx +48 -0
- package/src/Heading.recipe.ts +52 -0
- package/src/Heading.tsx +14 -0
- package/src/Icon.tsx +55 -0
- package/src/IconButton.tsx +44 -0
- package/src/Image.tsx +11 -0
- package/src/Input.recipe.ts +72 -0
- package/src/Input.tsx +40 -0
- package/src/InputGroup.tsx +62 -0
- package/src/Kbd.tsx +26 -0
- package/src/Link.tsx +23 -0
- package/src/LinkBox.tsx +88 -0
- package/src/LinkButton.tsx +80 -0
- package/src/List.tsx +31 -0
- package/src/Menu.recipe.ts +119 -0
- package/src/Menu.tsx +261 -0
- package/src/Modal.recipe.ts +163 -0
- package/src/Modal.tsx +279 -0
- package/src/NativeSelect.tsx +94 -0
- package/src/NumberField.recipe.ts +67 -0
- package/src/NumberField.tsx +86 -0
- package/src/PopoverArrow.tsx +67 -0
- package/src/ProgressBar.tsx +61 -0
- package/src/Radio.recipe.ts +107 -0
- package/src/Radio.tsx +89 -0
- package/src/SharedUIProvider.tsx +55 -0
- package/src/Slide.tsx +54 -0
- package/src/Slider.recipe.ts +102 -0
- package/src/Slider.tsx +163 -0
- package/src/Spinner.tsx +69 -0
- package/src/Svg.tsx +23 -0
- package/src/Switch.recipe.ts +107 -0
- package/src/Switch.tsx +62 -0
- package/src/Text.recipe.ts +29 -0
- package/src/Text.tsx +16 -0
- package/src/TextField.recipe.ts +54 -0
- package/src/TextField.tsx +89 -0
- package/src/Toast.recipe.ts +98 -0
- package/src/Toast.tsx +171 -0
- package/src/Tooltip.tsx +91 -0
- package/src/UnmountCallback.tsx +18 -0
- package/src/VisuallyHidden.tsx +14 -0
- package/src/base-preset.ts +294 -0
- package/src/base-tokens.ts +1017 -0
- package/src/button-icon.ts +23 -0
- package/src/hooks/useBreakpointValue.ts +63 -0
- package/src/hooks/useClipboard.ts +64 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/usePrevious.ts +18 -0
- package/src/index.ts +55 -0
- package/src/messages.ts +17 -0
- 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
|
+
});
|
package/src/Checkbox.tsx
ADDED
|
@@ -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
|
+
});
|
package/src/Collapse.tsx
ADDED
|
@@ -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
|
+
};
|
package/src/Divider.tsx
ADDED
|
@@ -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
|
+
});
|