@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.
- package/LICENSE.md +53 -0
- package/README.md +222 -0
- package/lang/ui.ca.json +22 -0
- package/lang/ui.cy.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.it.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 +66 -0
- package/postcss-legacy-safari.cjs +96 -0
- package/reset.css +35 -0
- package/src/Avatar.recipe.ts +168 -0
- package/src/Avatar.tsx +276 -0
- package/src/Button.recipe.ts +192 -0
- package/src/Button.tsx +69 -0
- package/src/ButtonGroup.tsx +70 -0
- package/src/Card.recipe.ts +49 -0
- package/src/Card.tsx +56 -0
- package/src/Checkbox.recipe.ts +111 -0
- package/src/Checkbox.tsx +106 -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/ComboBox.tsx +192 -0
- package/src/Divider.tsx +85 -0
- package/src/Drawer.recipe.ts +98 -0
- package/src/Drawer.tsx +138 -0
- package/src/Fade.tsx +48 -0
- package/src/GridList.recipe.ts +58 -0
- package/src/GridList.tsx +81 -0
- package/src/Heading.recipe.ts +52 -0
- package/src/Heading.tsx +14 -0
- package/src/Icon.tsx +75 -0
- package/src/IconButton.tsx +44 -0
- package/src/Image.tsx +11 -0
- package/src/Input.recipe.ts +75 -0
- package/src/Input.tsx +44 -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/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +124 -0
- package/src/Menu.tsx +291 -0
- package/src/Modal.recipe.ts +163 -0
- package/src/Modal.tsx +377 -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 +112 -0
- package/src/Radio.tsx +89 -0
- package/src/Select.recipe.ts +210 -0
- package/src/Select.tsx +153 -0
- package/src/SharedUIProvider.tsx +55 -0
- package/src/Skeleton.tsx +146 -0
- package/src/Slide.tsx +54 -0
- package/src/Slider.recipe.ts +102 -0
- package/src/Slider.tsx +163 -0
- package/src/Spinner.tsx +74 -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 +91 -0
- package/src/Toast.recipe.ts +98 -0
- package/src/Toast.tsx +173 -0
- package/src/Tooltip.recipe.ts +37 -0
- package/src/Tooltip.tsx +76 -0
- package/src/UnmountCallback.tsx +18 -0
- package/src/VisuallyHidden.tsx +14 -0
- package/src/base-preset.ts +371 -0
- package/src/base-tokens.ts +1017 -0
- package/src/button-icon.ts +23 -0
- package/src/data-attrs.ts +16 -0
- package/src/dense-preset.ts +108 -0
- package/src/hooks/useBreakpointValue.ts +63 -0
- package/src/hooks/useClipboard.ts +64 -0
- package/src/hooks/useDisclosure.ts +32 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/usePrevious.ts +18 -0
- package/src/index.ts +63 -0
- package/src/messages.ts +17 -0
- package/src/system.ts +45 -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
|
+
};
|
package/src/Fade.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ReactNode } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
export interface FadeProps {
|
|
11
|
+
/** Visible when true; faded out (but mounted) when false. */
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
css?: SystemStyleObject;
|
|
14
|
+
className?: string;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fade — Chakra's Fade transition as a CSS opacity transition (see Slide for
|
|
20
|
+
* the pattern). Content stays mounted throughout.
|
|
21
|
+
*/
|
|
22
|
+
export const Fade = ({
|
|
23
|
+
isOpen,
|
|
24
|
+
css: cssProp,
|
|
25
|
+
className,
|
|
26
|
+
children,
|
|
27
|
+
}: FadeProps) => (
|
|
28
|
+
<div
|
|
29
|
+
data-open={isOpen ? "" : undefined}
|
|
30
|
+
className={cx(
|
|
31
|
+
css(
|
|
32
|
+
{
|
|
33
|
+
opacity: 0,
|
|
34
|
+
pointerEvents: "none",
|
|
35
|
+
"&[data-open]": { opacity: 1, pointerEvents: "auto" },
|
|
36
|
+
transitionProperty: "opacity",
|
|
37
|
+
transitionDuration: "0.2s",
|
|
38
|
+
transitionTimingFunction: "ease-out",
|
|
39
|
+
_motionReduce: { transition: "none" },
|
|
40
|
+
},
|
|
41
|
+
cssProp,
|
|
42
|
+
),
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
* GridList slot recipe — a vertical list of selectable rows, each of which may
|
|
10
|
+
* hold its own interactive controls (which is what makes it a grid rather than
|
|
11
|
+
* a listbox: the roving tab index moves through rows, and the controls inside
|
|
12
|
+
* a row are reachable without leaving it).
|
|
13
|
+
*
|
|
14
|
+
* Chakra had no equivalent, so there is no Chakra look to match: the greys
|
|
15
|
+
* here are the family's neutral list styling, and an app with a strong
|
|
16
|
+
* selection colour restates them (classroom's roster does).
|
|
17
|
+
*
|
|
18
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
19
|
+
* `staticCss` entry that keeps the runtime-prop variants generated.
|
|
20
|
+
*/
|
|
21
|
+
export const gridList = defineSlotRecipe({
|
|
22
|
+
className: "grid-list",
|
|
23
|
+
slots: ["root", "item"],
|
|
24
|
+
base: {
|
|
25
|
+
root: {
|
|
26
|
+
// The list takes the roving tab index, so it is focusable itself and
|
|
27
|
+
// would otherwise draw the platform ring around the whole list.
|
|
28
|
+
outline: "none",
|
|
29
|
+
},
|
|
30
|
+
item: {
|
|
31
|
+
display: "flex",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
position: "relative",
|
|
34
|
+
// A row is interactive by definition — it selects, or it acts.
|
|
35
|
+
cursor: "pointer",
|
|
36
|
+
outline: "none",
|
|
37
|
+
transitionProperty: "background",
|
|
38
|
+
transitionDuration: "ultra-fast",
|
|
39
|
+
transitionTimingFunction: "ease-in",
|
|
40
|
+
_hover: { bg: "gray.50" },
|
|
41
|
+
// A row holding an open menu (or any other popover) keeps the hover
|
|
42
|
+
// grey, so the row an open menu belongs to stays visible. Hover state
|
|
43
|
+
// cannot do this on its own: a Popover lays a fixed full-viewport
|
|
44
|
+
// underlay over the page while open, which takes the pointer off the
|
|
45
|
+
// row — `:hover` and RAC's own `data-hovered` both drop the moment the
|
|
46
|
+
// menu appears. A trigger carries `aria-expanded` (useOverlayTrigger),
|
|
47
|
+
// so the row can see its own open overlay.
|
|
48
|
+
"&:has([aria-expanded=true])": { bg: "gray.50" },
|
|
49
|
+
"&[data-selected]": {
|
|
50
|
+
bg: "gray.100",
|
|
51
|
+
_hover: { bg: "gray.100" },
|
|
52
|
+
"&:has([aria-expanded=true])": { bg: "gray.100" },
|
|
53
|
+
},
|
|
54
|
+
"&[data-focus-visible]": { focusShadow: "outline" },
|
|
55
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
package/src/GridList.tsx
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
GridList as RACGridList,
|
|
9
|
+
GridListItem as RACGridListItem,
|
|
10
|
+
GridListItemProps as RACGridListItemProps,
|
|
11
|
+
GridListProps as RACGridListProps,
|
|
12
|
+
} from "react-aria-components";
|
|
13
|
+
import { css, cx } from "styled-system/css";
|
|
14
|
+
import { gridList } from "styled-system/recipes";
|
|
15
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
16
|
+
|
|
17
|
+
export interface GridListProps<T extends object>
|
|
18
|
+
extends Omit<RACGridListProps<T>, "className" | "style" | "children"> {
|
|
19
|
+
/** `GridListItem`s, or a render function when `items` is given. */
|
|
20
|
+
children: RACGridListProps<T>["children"];
|
|
21
|
+
/** Per-instance style overrides for the list, merged after the recipe. */
|
|
22
|
+
css?: SystemStyleObject;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* GridList — react-aria-components' <GridList>: a list of selectable rows,
|
|
28
|
+
* each of which may contain its own buttons and menus.
|
|
29
|
+
*
|
|
30
|
+
* Reach for it over a `ListBox` when the rows carry controls: a listbox option
|
|
31
|
+
* is a leaf, so a button inside one is unreachable by keyboard, where a grid
|
|
32
|
+
* row's contents are part of the grid's navigation.
|
|
33
|
+
*/
|
|
34
|
+
export const GridList = <T extends object>({
|
|
35
|
+
css: cssProp,
|
|
36
|
+
className,
|
|
37
|
+
children,
|
|
38
|
+
...rest
|
|
39
|
+
}: GridListProps<T>) => {
|
|
40
|
+
const slots = gridList();
|
|
41
|
+
return (
|
|
42
|
+
<RACGridList
|
|
43
|
+
{...rest}
|
|
44
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</RACGridList>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface GridListItemProps<T extends object = object>
|
|
52
|
+
extends Omit<RACGridListItemProps<T>, "className" | "style" | "children"> {
|
|
53
|
+
children?: ReactNode;
|
|
54
|
+
/** Per-instance style overrides for the row, merged after the recipe. */
|
|
55
|
+
css?: SystemStyleObject;
|
|
56
|
+
className?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A row in a `GridList`. Its children are laid out by the row itself — the
|
|
61
|
+
* gridcell react-aria puts between them is `display: contents`.
|
|
62
|
+
*
|
|
63
|
+
* Give every row a `textValue`: react-aria derives typeahead text from string
|
|
64
|
+
* children only, and a row is usually a composition rather than a string.
|
|
65
|
+
*/
|
|
66
|
+
export const GridListItem = <T extends object = object>({
|
|
67
|
+
css: cssProp,
|
|
68
|
+
className,
|
|
69
|
+
children,
|
|
70
|
+
...rest
|
|
71
|
+
}: GridListItemProps<T>) => {
|
|
72
|
+
const slots = gridList();
|
|
73
|
+
return (
|
|
74
|
+
<RACGridListItem
|
|
75
|
+
{...rest}
|
|
76
|
+
className={cx(slots.item, cssProp ? css(cssProp) : undefined, className)}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</RACGridListItem>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ComponentType, SVGProps } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Any component that renders an `<svg>` from svg props. Deliberately no
|
|
12
|
+
* narrower than the props `Icon` actually passes, so it accepts both
|
|
13
|
+
* react-icons' `IconType` and svgr components (`import X from "./x.svg?react"`,
|
|
14
|
+
* which the apps use for their custom-path icons — Chakra's `<Icon as={…}>`
|
|
15
|
+
* took either).
|
|
16
|
+
*/
|
|
17
|
+
export type IconComponent = ComponentType<
|
|
18
|
+
Pick<
|
|
19
|
+
SVGProps<SVGSVGElement>,
|
|
20
|
+
"className" | "focusable" | "role" | "aria-label" | "aria-hidden"
|
|
21
|
+
>
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export interface IconProps {
|
|
25
|
+
/** The icon component to render: a react-icons glyph or an svgr import. */
|
|
26
|
+
as: IconComponent;
|
|
27
|
+
/** Panda style overrides (size via fontSize/boxSize, colour, etc.). */
|
|
28
|
+
css?: SystemStyleObject;
|
|
29
|
+
className?: string;
|
|
30
|
+
"aria-label"?: string;
|
|
31
|
+
"aria-hidden"?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Icon — renders a react-icons glyph inline at `1em`, matching Chakra's <Icon>
|
|
36
|
+
* base styles. `fill: currentColor` means the glyph follows the surrounding
|
|
37
|
+
* text colour (set `css={{ color: ... }}` to override), so it inherits colour
|
|
38
|
+
* like Chakra's icons rather than defaulting to black.
|
|
39
|
+
*
|
|
40
|
+
* Unlabelled icons are treated as decorative and hidden from assistive tech;
|
|
41
|
+
* a labelled icon gets `role="img"` (a bare svg aria-label is unreliably
|
|
42
|
+
* announced without it).
|
|
43
|
+
*/
|
|
44
|
+
export const Icon = ({
|
|
45
|
+
as: As,
|
|
46
|
+
css: cssProp,
|
|
47
|
+
className,
|
|
48
|
+
"aria-label": ariaLabel,
|
|
49
|
+
"aria-hidden": ariaHidden,
|
|
50
|
+
}: IconProps) => (
|
|
51
|
+
<As
|
|
52
|
+
className={cx(
|
|
53
|
+
css({
|
|
54
|
+
width: "1em",
|
|
55
|
+
height: "1em",
|
|
56
|
+
display: "inline-block",
|
|
57
|
+
lineHeight: "1em",
|
|
58
|
+
flexShrink: 0,
|
|
59
|
+
fill: "currentColor",
|
|
60
|
+
// Chakra's Icon set this on the element itself, and an inline-block
|
|
61
|
+
// icon sits ~3px off without it. Panda's preflight happens to set it
|
|
62
|
+
// on every svg, which hid the omission in apps that had already
|
|
63
|
+
// flipped — classroom measured the difference at its kill-switch,
|
|
64
|
+
// where the preflight arrived and moved every icon back.
|
|
65
|
+
verticalAlign: "middle",
|
|
66
|
+
...cssProp,
|
|
67
|
+
}),
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
focusable="false"
|
|
71
|
+
aria-label={ariaLabel}
|
|
72
|
+
aria-hidden={ariaHidden ?? (ariaLabel ? undefined : true)}
|
|
73
|
+
role={ariaLabel ? "img" : undefined}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
/**
|
|
14
|
+
* Circular rather than the recipe's border-radius (Chakra's `isRound`).
|
|
15
|
+
* Usually a visual no-op here: the 2rem `button` radius already renders
|
|
16
|
+
* square icon buttons as circles. Kept for ported call sites and for
|
|
17
|
+
* variants/overrides with a smaller radius (e.g. `unstyled`).
|
|
18
|
+
*/
|
|
19
|
+
isRound?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* IconButton — a square, icon-only Button. Zeroes the recipe's horizontal
|
|
24
|
+
* padding (its size variants add `px`, which would squeeze a single glyph in a
|
|
25
|
+
* fixed-width button) and keeps the `minW` from the size so the box stays
|
|
26
|
+
* square. Pass the icon as children.
|
|
27
|
+
*/
|
|
28
|
+
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
29
|
+
function IconButton({ isRound, css: cssProp, children, ...rest }, ref) {
|
|
30
|
+
return (
|
|
31
|
+
<Button
|
|
32
|
+
ref={ref}
|
|
33
|
+
css={{
|
|
34
|
+
px: 0,
|
|
35
|
+
...(isRound ? { borderRadius: "full" } : {}),
|
|
36
|
+
...cssProp,
|
|
37
|
+
}}
|
|
38
|
+
{...rest}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</Button>
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
);
|
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,75 @@
|
|
|
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 (light mode) with its size
|
|
15
|
+
* scale. Used by the shared-ui Input and NativeSelect, and by TextField's
|
|
16
|
+
* input slot.
|
|
17
|
+
*
|
|
18
|
+
* Focus matches both native `:focus-visible` (plain inputs; browsers treat any
|
|
19
|
+
* focus in a text field as focus-visible) and react-aria's `data-focused`
|
|
20
|
+
* (inputs inside RAC TextField). Focus is declared after invalid so a focused
|
|
21
|
+
* invalid field shows the focus ring, as in Chakra.
|
|
22
|
+
*
|
|
23
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
24
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
25
|
+
*/
|
|
26
|
+
export const input = defineRecipe({
|
|
27
|
+
className: "input",
|
|
28
|
+
base: {
|
|
29
|
+
width: "100%",
|
|
30
|
+
minWidth: 0,
|
|
31
|
+
outline: "none",
|
|
32
|
+
position: "relative",
|
|
33
|
+
appearance: "none",
|
|
34
|
+
font: "inherit",
|
|
35
|
+
transitionProperty: transitionCommon,
|
|
36
|
+
transitionDuration: "normal",
|
|
37
|
+
border: "1px solid",
|
|
38
|
+
// The accessible outline stops: gray.400 is the ramp's 3:1-on-white
|
|
39
|
+
// boundary grey (WCAG 1.4.11), hover steps darker, never lighter.
|
|
40
|
+
// Chakra used 200/300 (~1.3:1), which read as barely-there.
|
|
41
|
+
borderColor: "gray.400",
|
|
42
|
+
bg: "inherit",
|
|
43
|
+
color: "inherit",
|
|
44
|
+
_hover: { borderColor: "gray.500" },
|
|
45
|
+
"&[data-invalid], &:user-invalid": {
|
|
46
|
+
borderColor: "danger.500",
|
|
47
|
+
boxShadow: "0 0 0 1px token(colors.danger.500)",
|
|
48
|
+
},
|
|
49
|
+
"&:is(:focus-visible, [data-focused])": {
|
|
50
|
+
zIndex: 1,
|
|
51
|
+
borderColor: "focusBorder",
|
|
52
|
+
boxShadow: "0 0 0 1px token(colors.focusBorder)",
|
|
53
|
+
// Focus indicator for forced-colors modes, which strip the box-shadow
|
|
54
|
+
// and force the border colour (the focusShadow utility's technique; the
|
|
55
|
+
// ring here is the 1px border tint, not an outline* shadow token).
|
|
56
|
+
outline: "2px solid transparent",
|
|
57
|
+
outlineOffset: "2px",
|
|
58
|
+
},
|
|
59
|
+
"&:is(:disabled, [data-disabled])": {
|
|
60
|
+
opacity: 0.4,
|
|
61
|
+
cursor: "not-allowed",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
variants: {
|
|
65
|
+
// Chakra's Input size scale, minus the unused xs.
|
|
66
|
+
size: {
|
|
67
|
+
lg: { fontSize: "lg", px: "4", h: "12", borderRadius: "md" },
|
|
68
|
+
md: { fontSize: "md", px: "4", h: "10", borderRadius: "md" },
|
|
69
|
+
sm: { fontSize: "sm", px: "3", h: "8", borderRadius: "sm" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
defaultVariants: {
|
|
73
|
+
size: "md",
|
|
74
|
+
},
|
|
75
|
+
});
|
package/src/Input.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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, InputVariantProps } from "styled-system/recipes";
|
|
9
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
10
|
+
|
|
11
|
+
export interface InputProps
|
|
12
|
+
// `size` is the recipe's size scale, as in Chakra; the native character-count
|
|
13
|
+
// attribute it shadows was unused.
|
|
14
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, "className" | "size">,
|
|
15
|
+
InputVariantProps {
|
|
16
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
17
|
+
css?: SystemStyleObject;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Input — a native input styled like Chakra's outline Input. For a
|
|
23
|
+
* labelled field with help/error text use TextField instead.
|
|
24
|
+
*/
|
|
25
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
26
|
+
{ css: cssProp, className, ...props },
|
|
27
|
+
ref,
|
|
28
|
+
) {
|
|
29
|
+
// splitVariantProps, not a hand-picked `size`: an app preset can add variant
|
|
30
|
+
// groups to the recipe (classroom adds `variant`), and cherry-picking would
|
|
31
|
+
// silently drop them onto the DOM as unknown attributes instead.
|
|
32
|
+
const [variantProps, rest] = input.splitVariantProps(props);
|
|
33
|
+
return (
|
|
34
|
+
<input
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cx(
|
|
37
|
+
input(variantProps),
|
|
38
|
+
cssProp ? css(cssProp) : undefined,
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
{...rest}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
});
|