@microbit/ui 0.1.0-alpha.13 → 0.1.0-alpha.15
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/README.md +7 -5
- package/lang/ui.cy.json +22 -0
- package/lang/ui.it.json +22 -0
- package/package.json +2 -1
- package/src/Avatar.recipe.ts +168 -0
- package/src/Avatar.tsx +276 -0
- package/src/Button.recipe.ts +39 -12
- package/src/Checkbox.tsx +59 -29
- package/src/ComboBox.tsx +192 -0
- package/src/GridList.recipe.ts +46 -0
- package/src/GridList.tsx +81 -0
- package/src/Icon.tsx +23 -3
- package/src/Input.tsx +6 -2
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +6 -1
- package/src/Menu.tsx +54 -24
- package/src/Modal.tsx +105 -7
- package/src/Select.recipe.ts +179 -0
- package/src/Select.tsx +153 -0
- package/src/Skeleton.tsx +146 -0
- package/src/Spinner.tsx +5 -0
- package/src/TextField.tsx +5 -3
- package/src/Tooltip.recipe.ts +37 -0
- package/src/Tooltip.tsx +3 -18
- package/src/base-preset.ts +54 -2
- package/src/data-attrs.ts +16 -0
- package/src/dense-preset.ts +108 -0
- package/src/hooks/useDisclosure.ts +32 -0
- package/src/index.ts +8 -0
- package/src/system.ts +9 -0
package/src/Skeleton.tsx
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { HTMLAttributes, ReactNode } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The placeholder block, as an object rather than a precomputed class so a
|
|
12
|
+
* caller's `css` is merged into one `css()` call and its overrides win
|
|
13
|
+
* (playbook gotcha #8).
|
|
14
|
+
*
|
|
15
|
+
* The colours are Chakra's, through the same pair of custom properties, so a
|
|
16
|
+
* call site can retint one skeleton without knowing how the animation works.
|
|
17
|
+
*/
|
|
18
|
+
const skeletonBase: SystemStyleObject = {
|
|
19
|
+
"--skeleton-start-color": "token(colors.gray.100)",
|
|
20
|
+
"--skeleton-end-color": "token(colors.gray.400)",
|
|
21
|
+
background: "var(--skeleton-start-color)",
|
|
22
|
+
borderColor: "var(--skeleton-end-color)",
|
|
23
|
+
opacity: 0.7,
|
|
24
|
+
borderRadius: "sm",
|
|
25
|
+
boxShadow: "none",
|
|
26
|
+
backgroundClip: "padding-box",
|
|
27
|
+
cursor: "default",
|
|
28
|
+
color: "transparent",
|
|
29
|
+
pointerEvents: "none",
|
|
30
|
+
userSelect: "none",
|
|
31
|
+
// Chakra hid the content rather than unmounting it, so a skeleton sized
|
|
32
|
+
// from real children keeps their dimensions.
|
|
33
|
+
"&::before, &::after, *": { visibility: "hidden" },
|
|
34
|
+
animation:
|
|
35
|
+
"skeletonFade var(--skeleton-speed, 0.8s) linear infinite alternate",
|
|
36
|
+
_motionReduce: { animation: "none" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface SkeletonProps
|
|
40
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, "color"> {
|
|
41
|
+
/** Show the children instead of the placeholder. */
|
|
42
|
+
isLoaded?: boolean;
|
|
43
|
+
/** Seconds per pulse (Chakra's `speed`, default 0.8). */
|
|
44
|
+
speed?: number;
|
|
45
|
+
children?: ReactNode;
|
|
46
|
+
/** Per-instance style overrides, merged after the base. */
|
|
47
|
+
css?: SystemStyleObject;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Skeleton — Chakra's loading placeholder: a block pulsing between two greys
|
|
53
|
+
* until its content is ready.
|
|
54
|
+
*
|
|
55
|
+
* Chakra faded the real content in over 0.4s when `isLoaded` turned true;
|
|
56
|
+
* here it simply appears. Wrap in `Fade` where that transition matters.
|
|
57
|
+
*/
|
|
58
|
+
export const Skeleton = ({
|
|
59
|
+
isLoaded,
|
|
60
|
+
speed,
|
|
61
|
+
children,
|
|
62
|
+
css: cssProp,
|
|
63
|
+
className,
|
|
64
|
+
style,
|
|
65
|
+
...rest
|
|
66
|
+
}: SkeletonProps) => {
|
|
67
|
+
if (isLoaded) {
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
{...rest}
|
|
71
|
+
style={style}
|
|
72
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
{...rest}
|
|
81
|
+
style={
|
|
82
|
+
speed === undefined
|
|
83
|
+
? style
|
|
84
|
+
: ({ ...style, "--skeleton-speed": `${speed}s` } as typeof style)
|
|
85
|
+
}
|
|
86
|
+
className={cx(css({ ...skeletonBase, ...cssProp }), className)}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export interface SkeletonTextProps extends SkeletonProps {
|
|
94
|
+
/** How many lines to draw (Chakra's default is 3). */
|
|
95
|
+
noOfLines?: number;
|
|
96
|
+
/** Gap between the lines. Any CSS length. */
|
|
97
|
+
spacing?: string;
|
|
98
|
+
/** Height of each line. Any CSS length. */
|
|
99
|
+
skeletonHeight?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* SkeletonText — a paragraph-shaped `Skeleton`: evenly spaced lines, the last
|
|
104
|
+
* one short, as Chakra drew them.
|
|
105
|
+
*/
|
|
106
|
+
export const SkeletonText = ({
|
|
107
|
+
noOfLines = 3,
|
|
108
|
+
spacing = "0.5rem",
|
|
109
|
+
skeletonHeight = "0.5rem",
|
|
110
|
+
isLoaded,
|
|
111
|
+
speed,
|
|
112
|
+
children,
|
|
113
|
+
css: cssProp,
|
|
114
|
+
className,
|
|
115
|
+
...rest
|
|
116
|
+
}: SkeletonTextProps) => {
|
|
117
|
+
if (isLoaded) {
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
{...rest}
|
|
121
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
122
|
+
>
|
|
123
|
+
{children}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
{...rest}
|
|
130
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
131
|
+
>
|
|
132
|
+
{Array.from({ length: noOfLines }, (_, index) => (
|
|
133
|
+
<Skeleton
|
|
134
|
+
key={index}
|
|
135
|
+
speed={speed}
|
|
136
|
+
style={{
|
|
137
|
+
height: skeletonHeight,
|
|
138
|
+
// Chakra's shape: a last line at 80%, and no gap after it.
|
|
139
|
+
width: noOfLines > 1 && index === noOfLines - 1 ? "80%" : "100%",
|
|
140
|
+
marginBottom: index === noOfLines - 1 ? "0" : spacing,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
package/src/Spinner.tsx
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { CSSProperties } from "react";
|
|
7
7
|
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { dataAttrs } from "./data-attrs";
|
|
8
9
|
import { SystemStyleObject } from "styled-system/types";
|
|
9
10
|
|
|
10
11
|
export interface SpinnerProps {
|
|
@@ -23,6 +24,8 @@ export interface SpinnerProps {
|
|
|
23
24
|
* "Loading..." by default, so a nameless spinner would regress on it.
|
|
24
25
|
*/
|
|
25
26
|
"aria-label": string;
|
|
27
|
+
/** `data-*` attributes land on the spinner, for tests that wait on it. */
|
|
28
|
+
[key: `data-${string}`]: unknown;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -35,8 +38,10 @@ export const Spinner = ({
|
|
|
35
38
|
css: cssProp,
|
|
36
39
|
className,
|
|
37
40
|
"aria-label": ariaLabel,
|
|
41
|
+
...rest
|
|
38
42
|
}: SpinnerProps) => (
|
|
39
43
|
<span
|
|
44
|
+
{...dataAttrs(rest)}
|
|
40
45
|
role="status"
|
|
41
46
|
aria-label={ariaLabel}
|
|
42
47
|
style={speed ? ({ "--spinner-speed": speed } as CSSProperties) : undefined}
|
package/src/TextField.tsx
CHANGED
|
@@ -49,11 +49,13 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
49
49
|
helperTextCss,
|
|
50
50
|
onFocus,
|
|
51
51
|
autoCapitalize,
|
|
52
|
-
|
|
53
|
-
...rest
|
|
52
|
+
...props
|
|
54
53
|
},
|
|
55
54
|
ref,
|
|
56
55
|
) {
|
|
56
|
+
// As Input: forward every recipe variant group, not just `size`, so a
|
|
57
|
+
// preset that adds one keeps working.
|
|
58
|
+
const [variantProps, rest] = input.splitVariantProps(props);
|
|
57
59
|
const slots = field();
|
|
58
60
|
return (
|
|
59
61
|
<RACTextField {...rest} className={slots.root}>
|
|
@@ -67,7 +69,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
67
69
|
</RACLabel>
|
|
68
70
|
<RACInput
|
|
69
71
|
ref={ref}
|
|
70
|
-
className={input(
|
|
72
|
+
className={input(variantProps)}
|
|
71
73
|
onFocus={onFocus}
|
|
72
74
|
autoCapitalize={autoCapitalize}
|
|
73
75
|
/>
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
* Tooltip recipe — Chakra's dark tooltip.
|
|
10
|
+
*
|
|
11
|
+
* A recipe rather than styles inside the component because tooltip typography
|
|
12
|
+
* is the kind of thing an app sets once for all of them: classroom's Chakra
|
|
13
|
+
* theme did exactly that (`fontSize: md`), and a `css` override at today's
|
|
14
|
+
* call sites would quietly not apply to tomorrow's.
|
|
15
|
+
*
|
|
16
|
+
* The colour, vertical padding and radius are Chakra's exactly. They had
|
|
17
|
+
* drifted (white, `py: 1`, `borderRadius: md`) while this lived inside the
|
|
18
|
+
* component, which classroom's port measured: a 6px radius where Chakra drew
|
|
19
|
+
* 2px. ml-trainer and python-editor pick the correction up too.
|
|
20
|
+
*
|
|
21
|
+
* Registered in the base preset (base-preset.ts).
|
|
22
|
+
*/
|
|
23
|
+
export const tooltip = defineRecipe({
|
|
24
|
+
className: "tooltip",
|
|
25
|
+
base: {
|
|
26
|
+
bg: "gray.700",
|
|
27
|
+
color: "whiteAlpha.900",
|
|
28
|
+
px: "2",
|
|
29
|
+
py: "0.5",
|
|
30
|
+
borderRadius: "sm",
|
|
31
|
+
fontSize: "sm",
|
|
32
|
+
fontWeight: "medium",
|
|
33
|
+
boxShadow: "md",
|
|
34
|
+
maxW: "xs",
|
|
35
|
+
zIndex: "tooltip",
|
|
36
|
+
},
|
|
37
|
+
});
|
package/src/Tooltip.tsx
CHANGED
|
@@ -5,26 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { ReactElement, ReactNode, RefObject } from "react";
|
|
7
7
|
import { Tooltip as RACTooltip, TooltipTrigger } from "react-aria-components";
|
|
8
|
-
import { css } from "styled-system/css";
|
|
8
|
+
import { css, cx } from "styled-system/css";
|
|
9
|
+
import { tooltip } from "styled-system/recipes";
|
|
9
10
|
import { SystemStyleObject } from "styled-system/types";
|
|
10
11
|
import { PopoverArrow } from "./PopoverArrow";
|
|
11
12
|
|
|
12
|
-
// Base as an object (not a precomputed class) so a caller's `css` override is
|
|
13
|
-
// merged into a single css() call — Panda then dedupes conflicting utilities
|
|
14
|
-
// (e.g. px/py) so overrides actually win.
|
|
15
|
-
const tooltipBase: SystemStyleObject = {
|
|
16
|
-
bg: "gray.700",
|
|
17
|
-
color: "white",
|
|
18
|
-
px: "2",
|
|
19
|
-
py: "1",
|
|
20
|
-
borderRadius: "md",
|
|
21
|
-
fontSize: "sm",
|
|
22
|
-
fontWeight: "medium",
|
|
23
|
-
boxShadow: "md",
|
|
24
|
-
maxW: "xs",
|
|
25
|
-
zIndex: "tooltip",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
13
|
export interface TooltipProps {
|
|
29
14
|
/**
|
|
30
15
|
* Tooltip body (Chakra's `label`). Not named `content`: Panda extracts
|
|
@@ -82,7 +67,7 @@ export const Tooltip = ({
|
|
|
82
67
|
triggerRef={triggerRef}
|
|
83
68
|
placement={placement}
|
|
84
69
|
offset={hasArrow ? 8 : 4}
|
|
85
|
-
className={
|
|
70
|
+
className={cx(tooltip(), cssProp ? css(cssProp) : undefined)}
|
|
86
71
|
>
|
|
87
72
|
{hasArrow && <PopoverArrow css={{ "& svg": { fill: "gray.700" } }} />}
|
|
88
73
|
{label}
|
package/src/base-preset.ts
CHANGED
|
@@ -23,19 +23,24 @@ import {
|
|
|
23
23
|
} from "./base-tokens";
|
|
24
24
|
// Config recipes are colocated with the shared-ui components they style; this
|
|
25
25
|
// preset registers them so Panda merges them at codegen time.
|
|
26
|
+
import { avatar } from "./Avatar.recipe";
|
|
26
27
|
import { button } from "./Button.recipe";
|
|
27
28
|
import { card } from "./Card.recipe";
|
|
28
29
|
import { checkbox } from "./Checkbox.recipe";
|
|
29
30
|
import { radio } from "./Radio.recipe";
|
|
30
31
|
import { drawer } from "./Drawer.recipe";
|
|
32
|
+
import { gridList } from "./GridList.recipe";
|
|
31
33
|
import { heading } from "./Heading.recipe";
|
|
32
34
|
import { input } from "./Input.recipe";
|
|
35
|
+
import { listBox } from "./ListBox.recipe";
|
|
33
36
|
import { numberField } from "./NumberField.recipe";
|
|
34
37
|
import { menu } from "./Menu.recipe";
|
|
38
|
+
import { select } from "./Select.recipe";
|
|
35
39
|
import { slider } from "./Slider.recipe";
|
|
36
40
|
import { switchRecipe } from "./Switch.recipe";
|
|
37
41
|
import { dialog } from "./Modal.recipe";
|
|
38
42
|
import { text } from "./Text.recipe";
|
|
43
|
+
import { tooltip } from "./Tooltip.recipe";
|
|
39
44
|
import { field } from "./TextField.recipe";
|
|
40
45
|
import { toast } from "./Toast.recipe";
|
|
41
46
|
|
|
@@ -65,11 +70,23 @@ export const basePreset = definePreset({
|
|
|
65
70
|
theme: {
|
|
66
71
|
breakpoints,
|
|
67
72
|
keyframes: {
|
|
68
|
-
// Spinner's revolution
|
|
73
|
+
// Spinner's revolution.
|
|
69
74
|
spin: {
|
|
70
75
|
"0%": { transform: "rotate(0deg)" },
|
|
71
76
|
"100%": { transform: "rotate(360deg)" },
|
|
72
77
|
},
|
|
78
|
+
// Skeleton's pulse, over the pair of custom properties the component
|
|
79
|
+
// sets, so a retinted skeleton animates between its own colours.
|
|
80
|
+
skeletonFade: {
|
|
81
|
+
from: {
|
|
82
|
+
borderColor: "var(--skeleton-start-color)",
|
|
83
|
+
background: "var(--skeleton-start-color)",
|
|
84
|
+
},
|
|
85
|
+
to: {
|
|
86
|
+
borderColor: "var(--skeleton-end-color)",
|
|
87
|
+
background: "var(--skeleton-end-color)",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
73
90
|
},
|
|
74
91
|
tokens: {
|
|
75
92
|
colors: {
|
|
@@ -158,6 +175,27 @@ export const basePreset = definePreset({
|
|
|
158
175
|
// follows; OSS language buttons are brand blue.)
|
|
159
176
|
languageText: { value: "{colors.brand.500}" },
|
|
160
177
|
languageTextHover: { value: "{colors.brand.600}" },
|
|
178
|
+
// The `primary`/`secondary` button variants' colours. Two brand
|
|
179
|
+
// idioms exist in the family: brand-coloured buttons (ml-trainer,
|
|
180
|
+
// python-editor — the defaults below) and a black-on-white system
|
|
181
|
+
// (classroom, data-microbit-org: black solid, black outline, no
|
|
182
|
+
// border colour change on hover but a blackAlpha wash instead).
|
|
183
|
+
// Tokens rather than per-app recipe overrides so both idioms share
|
|
184
|
+
// one recipe — a `variant` fork would be duplicated by every app on
|
|
185
|
+
// the far side of it. `primary`'s text colour stays a literal
|
|
186
|
+
// `white`: every app in the family puts white on a dark solid.
|
|
187
|
+
// `ghost` needs no tokens (black + blackAlpha in all four apps).
|
|
188
|
+
button: {
|
|
189
|
+
primaryBg: { value: "{colors.brand.500}" },
|
|
190
|
+
primaryHoverBg: { value: "{colors.brand.600}" },
|
|
191
|
+
primaryActiveBg: { value: "{colors.brand.700}" },
|
|
192
|
+
secondaryText: { value: "{colors.brand.700}" },
|
|
193
|
+
secondaryBorder: { value: "{colors.brand.500}" },
|
|
194
|
+
secondaryHoverBorder: { value: "{colors.brand.600}" },
|
|
195
|
+
secondaryHoverBg: { value: "transparent" },
|
|
196
|
+
secondaryActiveBorder: { value: "{colors.brand.700}" },
|
|
197
|
+
secondaryActiveBg: { value: "{colors.brand.50}" },
|
|
198
|
+
},
|
|
161
199
|
// Toast status colours: the Chakra-era toast Alert restyle (teal for
|
|
162
200
|
// every status except error) shared across the app family.
|
|
163
201
|
toastInfoBg: { value: "{colors.teal.800}" },
|
|
@@ -174,16 +212,21 @@ export const basePreset = definePreset({
|
|
|
174
212
|
heading,
|
|
175
213
|
input,
|
|
176
214
|
text,
|
|
215
|
+
tooltip,
|
|
177
216
|
},
|
|
178
217
|
slotRecipes: {
|
|
218
|
+
avatar,
|
|
179
219
|
card,
|
|
180
220
|
checkbox,
|
|
181
221
|
dialog,
|
|
182
222
|
drawer,
|
|
183
223
|
field,
|
|
224
|
+
gridList,
|
|
225
|
+
listBox,
|
|
184
226
|
menu,
|
|
185
227
|
numberField,
|
|
186
228
|
radio,
|
|
229
|
+
select,
|
|
187
230
|
slider,
|
|
188
231
|
switchRecipe,
|
|
189
232
|
toast,
|
|
@@ -240,7 +283,12 @@ export const basePreset = definePreset({
|
|
|
240
283
|
// can silently lose runtime-prop variants.
|
|
241
284
|
staticCss: {
|
|
242
285
|
recipes: {
|
|
243
|
-
|
|
286
|
+
// Size is passed responsively at call sites ported from Chakra's
|
|
287
|
+
// `size={["md", "lg"]}`, so generate the breakpoint-prefixed variants
|
|
288
|
+
// too — otherwise the class lands on the element with no rule behind it
|
|
289
|
+
// and the button silently falls back to the base size.
|
|
290
|
+
avatar: ["*"],
|
|
291
|
+
button: [{ size: ["*"], responsive: true }, { variant: ["*"] }],
|
|
244
292
|
checkbox: ["*"],
|
|
245
293
|
heading: ["*"],
|
|
246
294
|
card: ["*"],
|
|
@@ -248,10 +296,14 @@ export const basePreset = definePreset({
|
|
|
248
296
|
// as a runtime prop, so generate the breakpoint-prefixed variants too.
|
|
249
297
|
dialog: [{ size: ["*"], responsive: true }, { centered: ["*"] }],
|
|
250
298
|
drawer: ["*"],
|
|
299
|
+
gridList: ["*"],
|
|
300
|
+
listBox: ["*"],
|
|
251
301
|
input: ["*"],
|
|
252
302
|
radio: ["*"],
|
|
303
|
+
select: ["*"],
|
|
253
304
|
switchRecipe: ["*"],
|
|
254
305
|
text: ["*"],
|
|
306
|
+
tooltip: ["*"],
|
|
255
307
|
// Toast status is chosen at runtime from the toast content.
|
|
256
308
|
toast: ["*"],
|
|
257
309
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The `data-*` entries of a props object, for components that let a caller
|
|
9
|
+
* put test hooks on an inner element rather than the one their props land on.
|
|
10
|
+
*
|
|
11
|
+
* Internal: not exported from the package.
|
|
12
|
+
*/
|
|
13
|
+
export const dataAttrs = (props: object): Record<string, unknown> =>
|
|
14
|
+
Object.fromEntries(
|
|
15
|
+
Object.entries(props).filter(([key]) => key.startsWith("data-")),
|
|
16
|
+
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { definePreset } from "@pandacss/dev";
|
|
7
|
+
|
|
8
|
+
const toTokens = (values: Record<string, string>) =>
|
|
9
|
+
Object.fromEntries(
|
|
10
|
+
Object.entries(values).map(([k, value]) => [k, { value }]),
|
|
11
|
+
) as Record<string, { value: string }>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The numeric spacing/size grid, Chakra's 0.25rem step × 0.88.
|
|
15
|
+
*
|
|
16
|
+
* Only the numeric scale is touched: the named `sizes` (`xs`…`8xl`, `max`,
|
|
17
|
+
* `full`, `container.*`) stay at their base-preset values, as they did in
|
|
18
|
+
* both apps' Chakra themes.
|
|
19
|
+
*/
|
|
20
|
+
const scale = toTokens({
|
|
21
|
+
px: "1px",
|
|
22
|
+
0.5: "0.11rem",
|
|
23
|
+
1: "0.22rem",
|
|
24
|
+
1.5: "0.33rem",
|
|
25
|
+
2: "0.44rem",
|
|
26
|
+
2.5: "0.55rem",
|
|
27
|
+
3: "0.66rem",
|
|
28
|
+
3.5: "0.77rem",
|
|
29
|
+
4: "0.88rem",
|
|
30
|
+
5: "1.1rem",
|
|
31
|
+
6: "1.32rem",
|
|
32
|
+
7: "1.54rem",
|
|
33
|
+
8: "1.76rem",
|
|
34
|
+
9: "1.98rem",
|
|
35
|
+
10: "2.2rem",
|
|
36
|
+
12: "2.64rem",
|
|
37
|
+
14: "3.08rem",
|
|
38
|
+
16: "3.52rem",
|
|
39
|
+
20: "4.4rem",
|
|
40
|
+
24: "5.28rem",
|
|
41
|
+
28: "6.16rem",
|
|
42
|
+
32: "7.04rem",
|
|
43
|
+
36: "7.92rem",
|
|
44
|
+
40: "8.8rem",
|
|
45
|
+
44: "9.68rem",
|
|
46
|
+
48: "10.56rem",
|
|
47
|
+
52: "11.44rem",
|
|
48
|
+
56: "12.32rem",
|
|
49
|
+
60: "13.2rem",
|
|
50
|
+
64: "14.08rem",
|
|
51
|
+
72: "15.84rem",
|
|
52
|
+
80: "17.6rem",
|
|
53
|
+
96: "21.12rem",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Font sizes from `md` up, × 0.9. `xs`/`sm` keep their full size so small
|
|
58
|
+
* text never gets too small, and `3xs`/`2xs` (which neither app's theme
|
|
59
|
+
* listed) stay at their base-preset values.
|
|
60
|
+
*/
|
|
61
|
+
const denseFontSizes = toTokens({
|
|
62
|
+
md: "0.9rem",
|
|
63
|
+
lg: "1.012rem",
|
|
64
|
+
xl: "1.125rem",
|
|
65
|
+
"2xl": "1.35rem",
|
|
66
|
+
"3xl": "1.687rem",
|
|
67
|
+
"4xl": "2.025rem",
|
|
68
|
+
"5xl": "2.7rem",
|
|
69
|
+
"6xl": "3.375rem",
|
|
70
|
+
"7xl": "4.05rem",
|
|
71
|
+
"8xl": "5.4rem",
|
|
72
|
+
"9xl": "7.2rem",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The dense preset — an optional density override for the information-dense
|
|
77
|
+
* apps in the family. Stacks between the base preset and the app preset:
|
|
78
|
+
*
|
|
79
|
+
* ```ts
|
|
80
|
+
* presets: ["@pandacss/preset-base", basePreset, densePreset, appPreset]
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* Both python-editor and classroom shipped the same "make everything
|
|
84
|
+
* smaller" Chakra theme change (2022): the numeric spacing/sizes grid at
|
|
85
|
+
* × 0.88 and `fontSizes` from `md` up at × 0.9. The two themes' values were
|
|
86
|
+
* byte-identical, so the scale lives here rather than being replicated in
|
|
87
|
+
* each app preset (migration-playbook gotcha #25 — a global scale override
|
|
88
|
+
* hides from every safeguard, so it needs to be explicit and shared).
|
|
89
|
+
*
|
|
90
|
+
* Whether this density stays or the family aligns on one scale is an open
|
|
91
|
+
* design question; when it is answered, this preset is the single place the
|
|
92
|
+
* answer lands (deleting it from an app's stack un-shrinks that app).
|
|
93
|
+
* ml-trainer and data-microbit-org do not use it.
|
|
94
|
+
*/
|
|
95
|
+
export const densePreset = definePreset({
|
|
96
|
+
name: "microbit-ui-dense",
|
|
97
|
+
theme: {
|
|
98
|
+
extend: {
|
|
99
|
+
tokens: {
|
|
100
|
+
spacing: scale,
|
|
101
|
+
sizes: scale,
|
|
102
|
+
fontSizes: denseFontSizes,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export default densePreset;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useMemo, useState } from "react";
|
|
7
|
+
|
|
8
|
+
export interface Disclosure {
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
onOpen: () => void;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
onToggle: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* useDisclosure — Chakra's hook of the same name: the open/closed state of a
|
|
17
|
+
* dialog, menu or drawer, and the three functions that change it.
|
|
18
|
+
*
|
|
19
|
+
* A thin `useState` wrapper, kept because it is the shape a Chakra app's
|
|
20
|
+
* dialog call sites are written in, and because a stable object means a
|
|
21
|
+
* disclosure can be passed to a memoised child without re-rendering it.
|
|
22
|
+
*/
|
|
23
|
+
export const useDisclosure = (defaultIsOpen = false): Disclosure => {
|
|
24
|
+
const [isOpen, setIsOpen] = useState(defaultIsOpen);
|
|
25
|
+
const onOpen = useCallback(() => setIsOpen(true), []);
|
|
26
|
+
const onClose = useCallback(() => setIsOpen(false), []);
|
|
27
|
+
const onToggle = useCallback(() => setIsOpen((open) => !open), []);
|
|
28
|
+
return useMemo(
|
|
29
|
+
() => ({ isOpen, onOpen, onClose, onToggle }),
|
|
30
|
+
[isOpen, onOpen, onClose, onToggle],
|
|
31
|
+
);
|
|
32
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* match the Chakra theme; behaviour follows react-aria patterns.
|
|
10
10
|
*/
|
|
11
11
|
export * from "./system";
|
|
12
|
+
export * from "./Avatar";
|
|
12
13
|
export * from "./Button";
|
|
13
14
|
export * from "./LinkButton";
|
|
14
15
|
export * from "./ButtonGroup";
|
|
@@ -23,8 +24,10 @@ export * from "./NativeSelect";
|
|
|
23
24
|
export * from "./NumberField";
|
|
24
25
|
export * from "./ProgressBar";
|
|
25
26
|
export * from "./Radio";
|
|
27
|
+
export * from "./Skeleton";
|
|
26
28
|
export * from "./Slide";
|
|
27
29
|
export * from "./Slider";
|
|
30
|
+
export * from "./Select";
|
|
28
31
|
export * from "./Spinner";
|
|
29
32
|
export * from "./Svg";
|
|
30
33
|
export * from "./Switch";
|
|
@@ -40,8 +43,11 @@ export * from "./Collapse";
|
|
|
40
43
|
export * from "./Fade";
|
|
41
44
|
export * from "./Kbd";
|
|
42
45
|
export * from "./Divider";
|
|
46
|
+
export * from "./GridList";
|
|
43
47
|
export * from "./Drawer";
|
|
44
48
|
export * from "./List";
|
|
49
|
+
export * from "./ListBox";
|
|
50
|
+
export * from "./ComboBox";
|
|
45
51
|
export * from "./Menu";
|
|
46
52
|
export * from "./Modal";
|
|
47
53
|
export * from "./PopoverArrow";
|
|
@@ -51,5 +57,7 @@ export * from "./Toast";
|
|
|
51
57
|
export * from "./VisuallyHidden";
|
|
52
58
|
export { useBreakpointValue } from "./hooks/useBreakpointValue";
|
|
53
59
|
export { useClipboard } from "./hooks/useClipboard";
|
|
60
|
+
export { useDisclosure } from "./hooks/useDisclosure";
|
|
61
|
+
export type { Disclosure } from "./hooks/useDisclosure";
|
|
54
62
|
export { useMediaQuery } from "./hooks/useMediaQuery";
|
|
55
63
|
export { usePrevious } from "./hooks/usePrevious";
|
package/src/system.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
export { css, cva, sva, cx } from "styled-system/css";
|
|
12
12
|
export { token } from "styled-system/tokens";
|
|
13
13
|
export type { SystemStyleObject } from "styled-system/types";
|
|
14
|
+
// react-aria collection types call sites need for selection handlers.
|
|
15
|
+
export type { Key, Selection } from "react-aria-components";
|
|
14
16
|
export type {
|
|
15
17
|
BoxProps,
|
|
16
18
|
FlexProps,
|
|
@@ -21,9 +23,16 @@ export type {
|
|
|
21
23
|
} from "styled-system/jsx";
|
|
22
24
|
|
|
23
25
|
// Layout patterns — the Panda-native equivalents of Chakra's Box/Flex/Stack/etc.
|
|
26
|
+
//
|
|
27
|
+
// `styled` is re-exported for the `styled(Component)` form, which works from
|
|
28
|
+
// anywhere. The `styled.tag` JSX form does NOT: Panda recognises the factory by
|
|
29
|
+
// the module it was imported from, so `<styled.table css={…}>` on a `styled`
|
|
30
|
+
// imported from here silently produces no CSS. Import it from
|
|
31
|
+
// "styled-system/jsx" for that (playbook gotcha #41).
|
|
24
32
|
export {
|
|
25
33
|
AspectRatio,
|
|
26
34
|
Box,
|
|
35
|
+
Container,
|
|
27
36
|
Flex,
|
|
28
37
|
Stack,
|
|
29
38
|
HStack,
|