@microbit/ui 0.1.0-alpha.3
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 +21 -0
- package/README.md +118 -0
- package/lang/ui.ca.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.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-tw.json +22 -0
- package/package.json +54 -0
- package/src/Button.recipe.ts +162 -0
- package/src/Button.tsx +84 -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 +71 -0
- package/src/Checkbox.tsx +66 -0
- package/src/CloseButton.tsx +87 -0
- package/src/CloseIcon.tsx +40 -0
- package/src/Divider.tsx +21 -0
- package/src/Drawer.recipe.ts +98 -0
- package/src/Drawer.tsx +138 -0
- package/src/Heading.recipe.ts +52 -0
- package/src/Heading.tsx +14 -0
- package/src/Icon.tsx +55 -0
- package/src/IconButton.tsx +39 -0
- package/src/Image.tsx +11 -0
- package/src/Input.recipe.ts +63 -0
- package/src/Input.tsx +33 -0
- package/src/InputGroup.tsx +48 -0
- package/src/Link.tsx +23 -0
- package/src/LinkBox.tsx +88 -0
- package/src/List.tsx +27 -0
- package/src/Menu.recipe.ts +88 -0
- package/src/Menu.tsx +172 -0
- package/src/Modal.recipe.ts +163 -0
- package/src/Modal.tsx +265 -0
- package/src/NativeSelect.tsx +76 -0
- package/src/PopoverArrow.tsx +52 -0
- package/src/ProgressBar.tsx +61 -0
- package/src/SharedUIProvider.tsx +55 -0
- package/src/Slide.tsx +54 -0
- package/src/Slider.recipe.ts +91 -0
- package/src/Slider.tsx +99 -0
- package/src/Spinner.tsx +69 -0
- package/src/Svg.tsx +23 -0
- package/src/Switch.recipe.ts +80 -0
- package/src/Switch.tsx +60 -0
- package/src/Text.tsx +12 -0
- package/src/TextField.recipe.ts +54 -0
- package/src/TextField.tsx +72 -0
- package/src/Toast.recipe.ts +98 -0
- package/src/Toast.tsx +165 -0
- package/src/Tooltip.tsx +91 -0
- package/src/UnmountCallback.tsx +18 -0
- package/src/VisuallyHidden.tsx +14 -0
- package/src/base-preset.ts +278 -0
- package/src/chakra-tokens.ts +1014 -0
- package/src/hooks/useBreakpointValue.ts +63 -0
- package/src/index.ts +45 -0
- package/src/messages.ts +17 -0
- package/src/system.ts +36 -0
package/src/Drawer.tsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { createContext, ReactNode, useContext } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
Heading as RACHeading,
|
|
10
|
+
Modal as RACModal,
|
|
11
|
+
ModalOverlay,
|
|
12
|
+
} from "react-aria-components";
|
|
13
|
+
import { css, cx } from "styled-system/css";
|
|
14
|
+
import { drawer, DrawerVariantProps } from "styled-system/recipes";
|
|
15
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
16
|
+
import { UnmountCallback } from "./UnmountCallback";
|
|
17
|
+
|
|
18
|
+
type DrawerSlots = ReturnType<typeof drawer>;
|
|
19
|
+
|
|
20
|
+
const SlotContext = createContext<DrawerSlots>(drawer({}));
|
|
21
|
+
|
|
22
|
+
export interface DrawerProps extends DrawerVariantProps {
|
|
23
|
+
isOpen: boolean;
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Called after the drawer has fully closed (exit transition done and the
|
|
27
|
+
* drawer removed). Matches Chakra's `onCloseComplete`.
|
|
28
|
+
*/
|
|
29
|
+
onCloseComplete?: () => void;
|
|
30
|
+
/** Allow closing by clicking the backdrop / pressing Escape (default true). */
|
|
31
|
+
isDismissable?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Accessible name for the dialog. Required unless the drawer contains a
|
|
34
|
+
* `DrawerTitle`, which then labels it (react-aria warns in dev builds when
|
|
35
|
+
* neither is present). An explicit `aria-label` wins over a `DrawerTitle`.
|
|
36
|
+
*/
|
|
37
|
+
"aria-label"?: string;
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Drawer — a focus-trapping panel that slides in from the side. Collapses
|
|
43
|
+
* Chakra's Drawer/DrawerOverlay/DrawerContent into a single shell; place
|
|
44
|
+
* DrawerHeader and DrawerBody inside.
|
|
45
|
+
*/
|
|
46
|
+
export const Drawer = ({
|
|
47
|
+
isOpen,
|
|
48
|
+
onClose,
|
|
49
|
+
onCloseComplete,
|
|
50
|
+
placement,
|
|
51
|
+
isDismissable = true,
|
|
52
|
+
"aria-label": ariaLabel,
|
|
53
|
+
children,
|
|
54
|
+
}: DrawerProps) => {
|
|
55
|
+
const slots = drawer({ placement });
|
|
56
|
+
return (
|
|
57
|
+
<ModalOverlay
|
|
58
|
+
isOpen={isOpen}
|
|
59
|
+
onOpenChange={(open) => {
|
|
60
|
+
if (!open) {
|
|
61
|
+
onClose();
|
|
62
|
+
}
|
|
63
|
+
}}
|
|
64
|
+
isDismissable={isDismissable}
|
|
65
|
+
className={slots.overlay}
|
|
66
|
+
>
|
|
67
|
+
<UnmountCallback callback={onCloseComplete} />
|
|
68
|
+
<RACModal className={slots.content}>
|
|
69
|
+
<Dialog aria-label={ariaLabel} className={slots.inner}>
|
|
70
|
+
<SlotContext.Provider value={slots}>{children}</SlotContext.Provider>
|
|
71
|
+
</Dialog>
|
|
72
|
+
</RACModal>
|
|
73
|
+
</ModalOverlay>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
interface SlotProps {
|
|
78
|
+
children: ReactNode;
|
|
79
|
+
css?: SystemStyleObject;
|
|
80
|
+
className?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const DrawerHeader = ({
|
|
84
|
+
children,
|
|
85
|
+
css: cssProp,
|
|
86
|
+
className,
|
|
87
|
+
}: SlotProps) => {
|
|
88
|
+
const slots = useContext(SlotContext);
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
className={cx(
|
|
92
|
+
slots.header,
|
|
93
|
+
cssProp ? css(cssProp) : undefined,
|
|
94
|
+
className,
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
{children}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* DrawerTitle — a heading that labels the drawer dialog (react-aria's title
|
|
104
|
+
* slot). Use inside DrawerHeader when the drawer has a visible text title;
|
|
105
|
+
* DrawerHeader itself stays a layout element so it can hold non-heading
|
|
106
|
+
* content (logos, close buttons).
|
|
107
|
+
*/
|
|
108
|
+
export const DrawerTitle = ({
|
|
109
|
+
children,
|
|
110
|
+
css: cssProp,
|
|
111
|
+
className,
|
|
112
|
+
level,
|
|
113
|
+
}: SlotProps & {
|
|
114
|
+
/** Heading element level (default 2, like react-aria). */ level?: number;
|
|
115
|
+
}) => (
|
|
116
|
+
<RACHeading
|
|
117
|
+
slot="title"
|
|
118
|
+
level={level}
|
|
119
|
+
className={cssProp || className ? cx(css(cssProp), className) : undefined}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</RACHeading>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
export const DrawerBody = ({
|
|
126
|
+
children,
|
|
127
|
+
css: cssProp,
|
|
128
|
+
className,
|
|
129
|
+
}: SlotProps) => {
|
|
130
|
+
const slots = useContext(SlotContext);
|
|
131
|
+
return (
|
|
132
|
+
<div
|
|
133
|
+
className={cx(slots.body, cssProp ? css(cssProp) : undefined, className)}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { defineRecipe } from "@pandacss/dev";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Heading recipe — Chakra's default Heading base + responsive sizes.
|
|
10
|
+
* A config recipe for the same reasons as `button` (see Button.recipe.ts);
|
|
11
|
+
* the `marketing` variant is token-driven via the `display` font, so brands
|
|
12
|
+
* only override tokens.
|
|
13
|
+
*
|
|
14
|
+
* Registered in the base preset (base-preset.ts).
|
|
15
|
+
*/
|
|
16
|
+
export const heading = defineRecipe({
|
|
17
|
+
className: "heading",
|
|
18
|
+
jsx: ["Heading"],
|
|
19
|
+
base: {
|
|
20
|
+
fontFamily: "heading",
|
|
21
|
+
fontWeight: "bold",
|
|
22
|
+
},
|
|
23
|
+
variants: {
|
|
24
|
+
size: {
|
|
25
|
+
"4xl": { fontSize: { base: "6xl", md: "7xl" }, lineHeight: 1 },
|
|
26
|
+
"3xl": { fontSize: { base: "5xl", md: "6xl" }, lineHeight: 1 },
|
|
27
|
+
"2xl": {
|
|
28
|
+
fontSize: { base: "4xl", md: "5xl" },
|
|
29
|
+
lineHeight: { base: 1.2, md: 1 },
|
|
30
|
+
},
|
|
31
|
+
xl: {
|
|
32
|
+
fontSize: { base: "3xl", md: "4xl" },
|
|
33
|
+
lineHeight: { base: 1.33, md: 1.2 },
|
|
34
|
+
},
|
|
35
|
+
lg: {
|
|
36
|
+
fontSize: { base: "2xl", md: "3xl" },
|
|
37
|
+
lineHeight: { base: 1.33, md: 1.2 },
|
|
38
|
+
},
|
|
39
|
+
md: { fontSize: "xl", lineHeight: 1.2 },
|
|
40
|
+
sm: { fontSize: "md", lineHeight: 1.2 },
|
|
41
|
+
xs: { fontSize: "sm", lineHeight: 1.2 },
|
|
42
|
+
},
|
|
43
|
+
// Brand marketing headings. The `display` font token is Helvetica in OSS and
|
|
44
|
+
// GT Walsheim in the private preset.
|
|
45
|
+
variant: {
|
|
46
|
+
marketing: { fontFamily: "display" },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
defaultVariants: {
|
|
50
|
+
size: "xl",
|
|
51
|
+
},
|
|
52
|
+
});
|
package/src/Heading.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
import { heading } from "styled-system/recipes";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Heading — Chakra's <Heading> equivalent, backed by the `heading` config
|
|
11
|
+
* recipe (so the private preset can add brand variants). Defaults to an <h2>;
|
|
12
|
+
* pass `as` to change the element and `size` to pick the type scale.
|
|
13
|
+
*/
|
|
14
|
+
export const Heading = styled("h2", heading);
|
package/src/Icon.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { IconType } from "react-icons/lib";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
export interface IconProps {
|
|
11
|
+
/** The react-icons component to render. */
|
|
12
|
+
as: IconType;
|
|
13
|
+
/** Panda style overrides (size via fontSize/boxSize, colour, etc.). */
|
|
14
|
+
css?: SystemStyleObject;
|
|
15
|
+
className?: string;
|
|
16
|
+
"aria-label"?: string;
|
|
17
|
+
"aria-hidden"?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Icon — renders a react-icons glyph inline at `1em`, matching Chakra's <Icon>
|
|
22
|
+
* base styles. `fill: currentColor` means the glyph follows the surrounding
|
|
23
|
+
* text colour (set `css={{ color: ... }}` to override), so it inherits colour
|
|
24
|
+
* like Chakra's icons rather than defaulting to black.
|
|
25
|
+
*
|
|
26
|
+
* Unlabelled icons are treated as decorative and hidden from assistive tech;
|
|
27
|
+
* a labelled icon gets `role="img"` (a bare svg aria-label is unreliably
|
|
28
|
+
* announced without it).
|
|
29
|
+
*/
|
|
30
|
+
export const Icon = ({
|
|
31
|
+
as: As,
|
|
32
|
+
css: cssProp,
|
|
33
|
+
className,
|
|
34
|
+
"aria-label": ariaLabel,
|
|
35
|
+
"aria-hidden": ariaHidden,
|
|
36
|
+
}: IconProps) => (
|
|
37
|
+
<As
|
|
38
|
+
className={cx(
|
|
39
|
+
css({
|
|
40
|
+
width: "1em",
|
|
41
|
+
height: "1em",
|
|
42
|
+
display: "inline-block",
|
|
43
|
+
lineHeight: "1em",
|
|
44
|
+
flexShrink: 0,
|
|
45
|
+
fill: "currentColor",
|
|
46
|
+
...cssProp,
|
|
47
|
+
}),
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
focusable="false"
|
|
51
|
+
aria-label={ariaLabel}
|
|
52
|
+
aria-hidden={ariaHidden ?? (ariaLabel ? undefined : true)}
|
|
53
|
+
role={ariaLabel ? "img" : undefined}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { forwardRef } from "react";
|
|
7
|
+
import { Button, ButtonProps } from "./Button";
|
|
8
|
+
|
|
9
|
+
export interface IconButtonProps
|
|
10
|
+
extends Omit<ButtonProps, "leftIcon" | "rightIcon"> {
|
|
11
|
+
/** Icon-only buttons have no visible label, so this is required. */
|
|
12
|
+
"aria-label": string;
|
|
13
|
+
/** Circular rather than the recipe's border-radius (Chakra's `isRound`). */
|
|
14
|
+
isRound?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* IconButton — a square, icon-only Button. Zeroes the recipe's horizontal
|
|
19
|
+
* padding (its size variants add `px`, which would squeeze a single glyph in a
|
|
20
|
+
* fixed-width button) and keeps the `minW` from the size so the box stays
|
|
21
|
+
* square. Pass the icon as children.
|
|
22
|
+
*/
|
|
23
|
+
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
24
|
+
function IconButton({ isRound, css: cssProp, children, ...rest }, ref) {
|
|
25
|
+
return (
|
|
26
|
+
<Button
|
|
27
|
+
ref={ref}
|
|
28
|
+
css={{
|
|
29
|
+
px: 0,
|
|
30
|
+
...(isRound ? { borderRadius: "full" } : {}),
|
|
31
|
+
...cssProp,
|
|
32
|
+
}}
|
|
33
|
+
{...rest}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</Button>
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
);
|
package/src/Image.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
/** Image — an <img> accepting Panda style props. Replaces Chakra's <Image>. */
|
|
9
|
+
export const Image = styled("img", {
|
|
10
|
+
base: { maxWidth: "100%" },
|
|
11
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { defineRecipe } from "@pandacss/dev";
|
|
7
|
+
|
|
8
|
+
// Chakra's transition.property.common, inlined (Panda has no transitionProperty
|
|
9
|
+
// token category).
|
|
10
|
+
const transitionCommon =
|
|
11
|
+
"background-color, border-color, color, fill, stroke, opacity, box-shadow, transform";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Input recipe — Chakra's outline Input field at md size (light mode). Used by
|
|
15
|
+
* the shared-ui Input and NativeSelect, and by TextField's input slot.
|
|
16
|
+
*
|
|
17
|
+
* Focus matches both native `:focus-visible` (plain inputs; browsers treat any
|
|
18
|
+
* focus in a text field as focus-visible) and react-aria's `data-focused`
|
|
19
|
+
* (inputs inside RAC TextField). Focus is declared after invalid so a focused
|
|
20
|
+
* invalid field shows the focus ring, as in Chakra.
|
|
21
|
+
*
|
|
22
|
+
* Registered in the base preset (base-preset.ts).
|
|
23
|
+
*/
|
|
24
|
+
export const input = defineRecipe({
|
|
25
|
+
className: "input",
|
|
26
|
+
base: {
|
|
27
|
+
width: "100%",
|
|
28
|
+
minWidth: 0,
|
|
29
|
+
outline: "none",
|
|
30
|
+
position: "relative",
|
|
31
|
+
appearance: "none",
|
|
32
|
+
font: "inherit",
|
|
33
|
+
transitionProperty: transitionCommon,
|
|
34
|
+
transitionDuration: "normal",
|
|
35
|
+
fontSize: "md",
|
|
36
|
+
px: "4",
|
|
37
|
+
h: "10",
|
|
38
|
+
borderRadius: "md",
|
|
39
|
+
border: "1px solid",
|
|
40
|
+
borderColor: "gray.200",
|
|
41
|
+
bg: "inherit",
|
|
42
|
+
color: "inherit",
|
|
43
|
+
_hover: { borderColor: "gray.300" },
|
|
44
|
+
"&[data-invalid], &:user-invalid": {
|
|
45
|
+
borderColor: "danger.500",
|
|
46
|
+
boxShadow: "0 0 0 1px token(colors.danger.500)",
|
|
47
|
+
},
|
|
48
|
+
"&:is(:focus-visible, [data-focused])": {
|
|
49
|
+
zIndex: 1,
|
|
50
|
+
borderColor: "focusBorder",
|
|
51
|
+
boxShadow: "0 0 0 1px token(colors.focusBorder)",
|
|
52
|
+
// Focus indicator for forced-colors modes, which strip the box-shadow
|
|
53
|
+
// and force the border colour (the focusShadow utility's technique; the
|
|
54
|
+
// ring here is the 1px border tint, not an outline* shadow token).
|
|
55
|
+
outline: "2px solid transparent",
|
|
56
|
+
outlineOffset: "2px",
|
|
57
|
+
},
|
|
58
|
+
"&:is(:disabled, [data-disabled])": {
|
|
59
|
+
opacity: 0.4,
|
|
60
|
+
cursor: "not-allowed",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
package/src/Input.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { forwardRef, InputHTMLAttributes } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { input } from "styled-system/recipes";
|
|
9
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
10
|
+
|
|
11
|
+
export interface InputProps
|
|
12
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, "className"> {
|
|
13
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
14
|
+
css?: SystemStyleObject;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Input — a native input styled like Chakra's outline Input (md). For a
|
|
20
|
+
* labelled field with help/error text use TextField instead.
|
|
21
|
+
*/
|
|
22
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
23
|
+
{ css: cssProp, className, ...rest },
|
|
24
|
+
ref,
|
|
25
|
+
) {
|
|
26
|
+
return (
|
|
27
|
+
<input
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cx(input(), cssProp ? css(cssProp) : undefined, className)}
|
|
30
|
+
{...rest}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
* InputGroup — relative wrapper for an Input with addon elements. Pad the
|
|
10
|
+
* input (`pl`/`pr`) to make room for the elements. Replaces Chakra's
|
|
11
|
+
* InputGroup.
|
|
12
|
+
*/
|
|
13
|
+
export const InputGroup = styled("div", {
|
|
14
|
+
base: { position: "relative", width: "100%", display: "flex" },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// The element styles are written out twice rather than shared via a spread:
|
|
18
|
+
// Panda's extractor only reliably evaluates inline literals.
|
|
19
|
+
|
|
20
|
+
/** Element overlaying the start of an InputGroup (Chakra InputLeftElement). */
|
|
21
|
+
export const InputLeftElement = styled("div", {
|
|
22
|
+
base: {
|
|
23
|
+
position: "absolute",
|
|
24
|
+
top: 0,
|
|
25
|
+
left: 0,
|
|
26
|
+
width: "10",
|
|
27
|
+
height: "10",
|
|
28
|
+
display: "flex",
|
|
29
|
+
alignItems: "center",
|
|
30
|
+
justifyContent: "center",
|
|
31
|
+
zIndex: 2,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** Element overlaying the end of an InputGroup (Chakra InputRightElement). */
|
|
36
|
+
export const InputRightElement = styled("div", {
|
|
37
|
+
base: {
|
|
38
|
+
position: "absolute",
|
|
39
|
+
top: 0,
|
|
40
|
+
right: 0,
|
|
41
|
+
width: "10",
|
|
42
|
+
height: "10",
|
|
43
|
+
display: "flex",
|
|
44
|
+
alignItems: "center",
|
|
45
|
+
justifyContent: "center",
|
|
46
|
+
zIndex: 2,
|
|
47
|
+
},
|
|
48
|
+
});
|
package/src/Link.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
* Link — anchor styled to match Chakra's <Link> base (no underline until hover,
|
|
10
|
+
* focus ring on keyboard focus). Accepts Panda style props for colour etc.
|
|
11
|
+
*/
|
|
12
|
+
export const Link = styled("a", {
|
|
13
|
+
base: {
|
|
14
|
+
cursor: "pointer",
|
|
15
|
+
textDecoration: "none",
|
|
16
|
+
outline: "none",
|
|
17
|
+
transitionProperty:
|
|
18
|
+
"background-color, border-color, color, fill, stroke, opacity, box-shadow, transform",
|
|
19
|
+
transitionDuration: "normal",
|
|
20
|
+
_hover: { textDecoration: "underline" },
|
|
21
|
+
_focusVisible: { focusShadow: "outline" },
|
|
22
|
+
},
|
|
23
|
+
});
|
package/src/LinkBox.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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 { styled } from "styled-system/jsx";
|
|
9
|
+
import { button } from "styled-system/recipes";
|
|
10
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* LinkBox — makes a whole box clickable via a nested LinkOverlay (or any
|
|
14
|
+
* element with an inset `_before` overlay). Other interactive children must
|
|
15
|
+
* be raised with `zIndex: 1`. Replaces Chakra's <LinkBox>.
|
|
16
|
+
*/
|
|
17
|
+
export const LinkBox = styled("div", {
|
|
18
|
+
base: { position: "relative" },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* LinkOverlay — an anchor whose `::before` stretches over the nearest
|
|
23
|
+
* LinkBox, making the whole box its hit area. For button-driven overlays,
|
|
24
|
+
* apply the same `_before` inset style to a Button instead.
|
|
25
|
+
*/
|
|
26
|
+
export const LinkOverlay = styled("a", {
|
|
27
|
+
base: {
|
|
28
|
+
position: "static",
|
|
29
|
+
_before: {
|
|
30
|
+
content: '""',
|
|
31
|
+
cursor: "inherit",
|
|
32
|
+
display: "block",
|
|
33
|
+
position: "absolute",
|
|
34
|
+
top: 0,
|
|
35
|
+
left: 0,
|
|
36
|
+
zIndex: 0,
|
|
37
|
+
width: "100%",
|
|
38
|
+
height: "100%",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export interface LinkOverlayButtonProps
|
|
44
|
+
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className"> {
|
|
45
|
+
/** Per-instance style overrides, merged after the base. */
|
|
46
|
+
css?: SystemStyleObject;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* LinkOverlayButton — the button flavour of LinkOverlay: its `::before`
|
|
52
|
+
* stretches over the nearest LinkBox so the whole box is clickable.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately a plain button, not a react-aria one: react-aria's usePress
|
|
55
|
+
* cancels presses that land outside the button's bounding rect, which defeats
|
|
56
|
+
* the overlay. `position: static` overrides the button recipe's base so the
|
|
57
|
+
* overlay anchors to the LinkBox (Chakra's LinkOverlay did the same over its
|
|
58
|
+
* Button).
|
|
59
|
+
*/
|
|
60
|
+
export const LinkOverlayButton = forwardRef<
|
|
61
|
+
HTMLButtonElement,
|
|
62
|
+
LinkOverlayButtonProps
|
|
63
|
+
>(function LinkOverlayButton({ css: cssProp, className, ...rest }, ref) {
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
ref={ref}
|
|
67
|
+
type="button"
|
|
68
|
+
className={cx(
|
|
69
|
+
button({ variant: "unstyled" }),
|
|
70
|
+
css({
|
|
71
|
+
cursor: "pointer",
|
|
72
|
+
position: "static",
|
|
73
|
+
_before: {
|
|
74
|
+
content: '""',
|
|
75
|
+
cursor: "pointer",
|
|
76
|
+
display: "block",
|
|
77
|
+
position: "absolute",
|
|
78
|
+
inset: 0,
|
|
79
|
+
zIndex: 0,
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
cssProp ? css(cssProp) : undefined,
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
{...rest}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
});
|
package/src/List.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
* Unstyled list, matching Chakra's <List> (Panda's preflight already zeroes
|
|
10
|
+
* margins/padding and removes list markers).
|
|
11
|
+
*/
|
|
12
|
+
export const List = styled("ul");
|
|
13
|
+
|
|
14
|
+
/** List item, matching Chakra's <ListItem>. */
|
|
15
|
+
export const ListItem = styled("li", {
|
|
16
|
+
base: {},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** Bulleted list matching Chakra's <UnorderedList>. */
|
|
20
|
+
export const UnorderedList = styled("ul", {
|
|
21
|
+
base: { listStyleType: "disc", marginStart: "1em" },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** Numbered list matching Chakra's <OrderedList>. */
|
|
25
|
+
export const OrderedList = styled("ol", {
|
|
26
|
+
base: { listStyleType: "decimal", marginStart: "1em" },
|
|
27
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
* Menu slot recipe — Chakra's default Menu parts (light mode). `content` is the
|
|
10
|
+
* dropdown card (react-aria-components' Popover), `list` the RAC Menu, `item` a
|
|
11
|
+
* MenuItem, `icon` the leading-icon wrapper.
|
|
12
|
+
*
|
|
13
|
+
* A config slot recipe (rather than an atomic `sva`) for consistency with
|
|
14
|
+
* `dialog` and so presets can override it later if brands diverge.
|
|
15
|
+
* No variants, so it needs no `staticCss` entry.
|
|
16
|
+
*
|
|
17
|
+
* Registered in the base preset (base-preset.ts).
|
|
18
|
+
*/
|
|
19
|
+
export const menu = defineSlotRecipe({
|
|
20
|
+
className: "menu",
|
|
21
|
+
slots: ["content", "list", "item", "icon", "label", "divider"],
|
|
22
|
+
base: {
|
|
23
|
+
content: {
|
|
24
|
+
bg: "white",
|
|
25
|
+
color: "inherit",
|
|
26
|
+
minWidth: "3xs",
|
|
27
|
+
py: "2",
|
|
28
|
+
zIndex: "dropdown",
|
|
29
|
+
borderRadius: "md",
|
|
30
|
+
borderWidth: "1px",
|
|
31
|
+
borderColor: "gray.200",
|
|
32
|
+
boxShadow: "sm",
|
|
33
|
+
// Approximate Chakra's menu fade/scale. RAC toggles data-entering/
|
|
34
|
+
// data-exiting on the Popover and waits for the transition before unmount.
|
|
35
|
+
transformOrigin: "top",
|
|
36
|
+
opacity: 1,
|
|
37
|
+
transform: "scale(1)",
|
|
38
|
+
transition: "opacity 0.1s ease-out, transform 0.1s ease-out",
|
|
39
|
+
"&[data-entering]": { opacity: 0, transform: "scale(0.95)" },
|
|
40
|
+
"&[data-exiting]": { opacity: 0, transform: "scale(0.95)" },
|
|
41
|
+
_motionReduce: { transition: "none" },
|
|
42
|
+
},
|
|
43
|
+
list: {
|
|
44
|
+
outline: "none",
|
|
45
|
+
},
|
|
46
|
+
item: {
|
|
47
|
+
display: "flex",
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
py: "1.5",
|
|
50
|
+
px: "3",
|
|
51
|
+
cursor: "pointer",
|
|
52
|
+
color: "inherit",
|
|
53
|
+
textDecoration: "none",
|
|
54
|
+
outline: "none",
|
|
55
|
+
transitionProperty: "background",
|
|
56
|
+
transitionDuration: "ultra-fast",
|
|
57
|
+
transitionTimingFunction: "ease-in",
|
|
58
|
+
// RAC highlights the active item (keyboard or pointer) with data-focused;
|
|
59
|
+
// data-pressed is the pressed state — mirrors Chakra's _focus/_active.
|
|
60
|
+
"&[data-focused]": { bg: "gray.100" },
|
|
61
|
+
"&[data-pressed]": { bg: "gray.200" },
|
|
62
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
63
|
+
},
|
|
64
|
+
label: {
|
|
65
|
+
// Chakra wraps an icon-item's children in a flex:1 span, so block
|
|
66
|
+
// children (e.g. two stacked <Text>s) lay out vertically rather than as
|
|
67
|
+
// flex-row siblings of the icon.
|
|
68
|
+
flex: "1",
|
|
69
|
+
},
|
|
70
|
+
icon: {
|
|
71
|
+
display: "inline-flex",
|
|
72
|
+
alignItems: "center",
|
|
73
|
+
justifyContent: "center",
|
|
74
|
+
flexShrink: 0,
|
|
75
|
+
marginEnd: "0.75rem",
|
|
76
|
+
// Chakra's MenuIcon shrinks glyphs to 0.8em; items passing an explicitly
|
|
77
|
+
// sized icon (e.g. h/w) override this.
|
|
78
|
+
fontSize: "0.8em",
|
|
79
|
+
},
|
|
80
|
+
divider: {
|
|
81
|
+
border: 0,
|
|
82
|
+
borderBottom: "1px solid",
|
|
83
|
+
borderColor: "gray.200",
|
|
84
|
+
my: "2",
|
|
85
|
+
opacity: 0.6,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|