@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { OverlayArrow } from "react-aria-components";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
// react-aria positions the arrow and stamps the resolved side on the wrapper
|
|
11
|
+
// as data-placement, but leaves orienting the glyph to us. The svg is square
|
|
12
|
+
// with the triangle in the half nearest the overlay (tip at centre), so
|
|
13
|
+
// rotating about the centre keeps it flush against the overlay on every side
|
|
14
|
+
// — no translate fix-ups, whose signs depend on rotation direction and are
|
|
15
|
+
// easy to get wrong (they detached the tooltip arrow from its box).
|
|
16
|
+
const arrowBase = css({
|
|
17
|
+
"& svg": { display: "block" },
|
|
18
|
+
"&[data-placement='bottom'] svg": { transform: "rotate(180deg)" },
|
|
19
|
+
"&[data-placement='right'] svg": { transform: "rotate(90deg)" },
|
|
20
|
+
"&[data-placement='left'] svg": { transform: "rotate(-90deg)" },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export interface PopoverArrowProps {
|
|
24
|
+
/**
|
|
25
|
+
* Base width of the visible triangle in px; it protrudes half this into
|
|
26
|
+
* the gap. Defaults to Chakra's tooltip arrow proportions (an 8px square
|
|
27
|
+
* rotated 45°).
|
|
28
|
+
*/
|
|
29
|
+
size?: number;
|
|
30
|
+
/** Styles merged after the base, e.g. `{ "& svg": { fill: "white" } }`. */
|
|
31
|
+
css?: SystemStyleObject;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* PopoverArrow — the notch pointing from a popover or tooltip at its
|
|
37
|
+
* trigger, oriented automatically for react-aria's resolved placement.
|
|
38
|
+
* Render inside a RAC Popover/Tooltip; set the fill via the css prop.
|
|
39
|
+
*/
|
|
40
|
+
export const PopoverArrow = ({
|
|
41
|
+
size = 11.314,
|
|
42
|
+
css: cssProp,
|
|
43
|
+
className,
|
|
44
|
+
}: PopoverArrowProps) => (
|
|
45
|
+
<OverlayArrow
|
|
46
|
+
className={cx(arrowBase, cssProp ? css(cssProp) : undefined, className)}
|
|
47
|
+
>
|
|
48
|
+
<svg width={size} height={size} viewBox="0 0 12 12">
|
|
49
|
+
<path d="M0 0 L6 6 L12 0" />
|
|
50
|
+
</svg>
|
|
51
|
+
</OverlayArrow>
|
|
52
|
+
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ProgressBar as RACProgressBar } from "react-aria-components";
|
|
7
|
+
import { css } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
export interface ProgressBarProps {
|
|
11
|
+
/** 0–100. */
|
|
12
|
+
value: number;
|
|
13
|
+
"aria-label": string;
|
|
14
|
+
/** Track overrides (size, radius, width), merged after the base. */
|
|
15
|
+
css?: SystemStyleObject;
|
|
16
|
+
/** Fill overrides (colour), merged after the base. */
|
|
17
|
+
barCss?: SystemStyleObject;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* ProgressBar — react-aria-components <ProgressBar> styled like Chakra's md
|
|
22
|
+
* Progress (12px gray.100 track). Set the fill colour per call site via
|
|
23
|
+
* `barCss` (Chakra's colorScheme).
|
|
24
|
+
*/
|
|
25
|
+
export const ProgressBar = ({
|
|
26
|
+
value,
|
|
27
|
+
"aria-label": ariaLabel,
|
|
28
|
+
css: cssProp,
|
|
29
|
+
barCss,
|
|
30
|
+
}: ProgressBarProps) => (
|
|
31
|
+
<RACProgressBar
|
|
32
|
+
value={value}
|
|
33
|
+
aria-label={ariaLabel}
|
|
34
|
+
// Single css() calls so caller overrides of base properties (radius, fill
|
|
35
|
+
// colour) are deduped at merge time rather than racing on stylesheet order.
|
|
36
|
+
className={css(
|
|
37
|
+
{
|
|
38
|
+
width: "100%",
|
|
39
|
+
height: 3,
|
|
40
|
+
bg: "gray.100",
|
|
41
|
+
overflow: "hidden",
|
|
42
|
+
borderRadius: "sm",
|
|
43
|
+
},
|
|
44
|
+
cssProp,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{({ percentage }) => (
|
|
48
|
+
<div
|
|
49
|
+
className={css(
|
|
50
|
+
{
|
|
51
|
+
height: "100%",
|
|
52
|
+
bg: "progressFilledTrack",
|
|
53
|
+
transition: "width 0.2s",
|
|
54
|
+
},
|
|
55
|
+
barCss,
|
|
56
|
+
)}
|
|
57
|
+
style={{ width: `${percentage ?? 0}%` }}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</RACProgressBar>
|
|
61
|
+
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { createContext, ReactNode, useContext, useMemo } from "react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Registers the close function of the currently open dismissable overlay, or
|
|
10
|
+
* clears it (null) when the overlay closes. Installed by apps that need to
|
|
11
|
+
* dismiss overlays from outside the component tree — e.g. the Android
|
|
12
|
+
* hardware back button. Only one overlay is open at a time.
|
|
13
|
+
*/
|
|
14
|
+
export type OverlayCloseRegistrar = (close: (() => void) | null) => void;
|
|
15
|
+
|
|
16
|
+
interface SharedUIContextValue {
|
|
17
|
+
overlayCloseRegistrar?: OverlayCloseRegistrar;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SharedUIContext = createContext<SharedUIContextValue | null>(null);
|
|
21
|
+
|
|
22
|
+
export interface SharedUIProviderProps {
|
|
23
|
+
overlayCloseRegistrar?: OverlayCloseRegistrar;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SharedUIProvider — the app-side installation point for optional shared-ui
|
|
29
|
+
* integrations. Currently that is only the overlay-close registrar, so apps
|
|
30
|
+
* without one can omit the provider entirely. Localized strings come from
|
|
31
|
+
* react-intl: an IntlProvider must be mounted above shared-ui components
|
|
32
|
+
* (see the package README for merging this package's message catalogs).
|
|
33
|
+
*/
|
|
34
|
+
export const SharedUIProvider = ({
|
|
35
|
+
overlayCloseRegistrar,
|
|
36
|
+
children,
|
|
37
|
+
}: SharedUIProviderProps) => {
|
|
38
|
+
const value = useMemo(
|
|
39
|
+
() => ({ overlayCloseRegistrar }),
|
|
40
|
+
[overlayCloseRegistrar],
|
|
41
|
+
);
|
|
42
|
+
return (
|
|
43
|
+
<SharedUIContext.Provider value={value}>
|
|
44
|
+
{children}
|
|
45
|
+
</SharedUIContext.Provider>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The app-installed overlay-close registrar, if any. Overlay components run
|
|
51
|
+
* controlled and register their close function while open so the app can
|
|
52
|
+
* dismiss them; without a registrar they manage open state internally.
|
|
53
|
+
*/
|
|
54
|
+
export const useOverlayCloseRegistrar = (): OverlayCloseRegistrar | undefined =>
|
|
55
|
+
useContext(SharedUIContext)?.overlayCloseRegistrar;
|
package/src/Slide.tsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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 SlideProps {
|
|
11
|
+
/** Slides in when true; sits translated offscreen when false. */
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
/** Per-instance style overrides (e.g. zIndex), merged after the base. */
|
|
14
|
+
css?: SystemStyleObject;
|
|
15
|
+
className?: string;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Slide — Chakra's Slide transition, bottom edge only (add other directions
|
|
21
|
+
* when a use appears): a full-width panel pinned to the viewport bottom,
|
|
22
|
+
* translated offscreen when closed. Content stays mounted throughout, so
|
|
23
|
+
* closing doesn't drop in-flight state and the exit animates.
|
|
24
|
+
*/
|
|
25
|
+
export const Slide = ({
|
|
26
|
+
isOpen,
|
|
27
|
+
css: cssProp,
|
|
28
|
+
className,
|
|
29
|
+
children,
|
|
30
|
+
}: SlideProps) => (
|
|
31
|
+
<div
|
|
32
|
+
data-open={isOpen ? "" : undefined}
|
|
33
|
+
className={cx(
|
|
34
|
+
css(
|
|
35
|
+
{
|
|
36
|
+
position: "fixed",
|
|
37
|
+
left: 0,
|
|
38
|
+
right: 0,
|
|
39
|
+
bottom: 0,
|
|
40
|
+
transform: "translateY(100%)",
|
|
41
|
+
"&[data-open]": { transform: "translateY(0)" },
|
|
42
|
+
transitionProperty: "transform",
|
|
43
|
+
transitionDuration: "0.25s",
|
|
44
|
+
transitionTimingFunction: "cubic-bezier(0, 0, 0.2, 1)",
|
|
45
|
+
_motionReduce: { transition: "none" },
|
|
46
|
+
},
|
|
47
|
+
cssProp,
|
|
48
|
+
),
|
|
49
|
+
className,
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
* Slider slot recipe — Chakra's horizontal md slider (14px thumb, 4px
|
|
10
|
+
* gray.200 track, blue filled track; light mode). Consumed by the shared-ui
|
|
11
|
+
* Slider, which maps the slots onto react-aria-components'
|
|
12
|
+
* Slider/SliderTrack/SliderThumb.
|
|
13
|
+
*
|
|
14
|
+
* The `mark` slot is hidden until the slider has focus (Chakra call sites did
|
|
15
|
+
* this with `_focusWithin` + a class); the reveal lives here so call sites
|
|
16
|
+
* only style the mark's look.
|
|
17
|
+
*
|
|
18
|
+
* Registered in the base preset (base-preset.ts). No variants, so it
|
|
19
|
+
* needs no `staticCss` entry.
|
|
20
|
+
*/
|
|
21
|
+
export const slider = defineSlotRecipe({
|
|
22
|
+
className: "slider",
|
|
23
|
+
slots: ["root", "track", "filledTrack", "thumb", "mark"],
|
|
24
|
+
base: {
|
|
25
|
+
root: {
|
|
26
|
+
position: "relative",
|
|
27
|
+
display: "block",
|
|
28
|
+
width: "100%",
|
|
29
|
+
touchAction: "none",
|
|
30
|
+
// Half the thumb so it doesn't overflow the ends (Chakra container px).
|
|
31
|
+
px: "calc(14px / 2)",
|
|
32
|
+
"&:focus-within [data-part='mark']": {
|
|
33
|
+
display: "block",
|
|
34
|
+
},
|
|
35
|
+
"&[data-disabled]": {
|
|
36
|
+
opacity: 0.4,
|
|
37
|
+
cursor: "not-allowed",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
track: {
|
|
41
|
+
position: "relative",
|
|
42
|
+
height: "4px",
|
|
43
|
+
overflow: "hidden",
|
|
44
|
+
borderRadius: "sm",
|
|
45
|
+
bg: "gray.200",
|
|
46
|
+
cursor: "pointer",
|
|
47
|
+
"[data-disabled] &": {
|
|
48
|
+
bg: "gray.300",
|
|
49
|
+
cursor: "default",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
filledTrack: {
|
|
53
|
+
position: "absolute",
|
|
54
|
+
top: 0,
|
|
55
|
+
left: 0,
|
|
56
|
+
height: "100%",
|
|
57
|
+
bg: "sliderFilledTrack",
|
|
58
|
+
},
|
|
59
|
+
thumb: {
|
|
60
|
+
position: "absolute",
|
|
61
|
+
top: "50%",
|
|
62
|
+
width: "14px",
|
|
63
|
+
height: "14px",
|
|
64
|
+
display: "flex",
|
|
65
|
+
alignItems: "center",
|
|
66
|
+
justifyContent: "center",
|
|
67
|
+
outline: "none",
|
|
68
|
+
zIndex: 1,
|
|
69
|
+
borderRadius: "full",
|
|
70
|
+
bg: "white",
|
|
71
|
+
boxShadow: "base",
|
|
72
|
+
transitionProperty: "transform",
|
|
73
|
+
transitionDuration: "normal",
|
|
74
|
+
"&[data-dragging]": {
|
|
75
|
+
transform: "translate(-50%, -50%) scale(1.15)",
|
|
76
|
+
},
|
|
77
|
+
transform: "translate(-50%, -50%)",
|
|
78
|
+
"&[data-focus-visible]": {
|
|
79
|
+
focusShadow: "outline",
|
|
80
|
+
},
|
|
81
|
+
"&[data-disabled]": {
|
|
82
|
+
bg: "gray.300",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
mark: {
|
|
86
|
+
display: "none",
|
|
87
|
+
position: "absolute",
|
|
88
|
+
whiteSpace: "nowrap",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
package/src/Slider.tsx
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
Slider as RACSlider,
|
|
9
|
+
SliderThumb,
|
|
10
|
+
SliderTrack,
|
|
11
|
+
} from "react-aria-components";
|
|
12
|
+
import { css, cx } from "styled-system/css";
|
|
13
|
+
import { slider } from "styled-system/recipes";
|
|
14
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
15
|
+
|
|
16
|
+
export interface SliderProps {
|
|
17
|
+
value: number;
|
|
18
|
+
onChange: (value: number) => void;
|
|
19
|
+
minValue?: number;
|
|
20
|
+
maxValue?: number;
|
|
21
|
+
"aria-label": string;
|
|
22
|
+
isDisabled?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Number format for the value announced to assistive tech (e.g.
|
|
25
|
+
* `{ style: "unit", unit: "percent" }` so units are heard, not bare
|
|
26
|
+
* numbers).
|
|
27
|
+
*/
|
|
28
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
29
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
30
|
+
css?: SystemStyleObject;
|
|
31
|
+
trackCss?: SystemStyleObject;
|
|
32
|
+
filledTrackCss?: SystemStyleObject;
|
|
33
|
+
thumbCss?: SystemStyleObject;
|
|
34
|
+
/**
|
|
35
|
+
* Positioned at the current value along the track and shown while the
|
|
36
|
+
* slider has focus (Chakra's SliderMark usage).
|
|
37
|
+
*/
|
|
38
|
+
mark?: ReactNode;
|
|
39
|
+
markCss?: SystemStyleObject;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Slider — react-aria-components <Slider> styled like Chakra's horizontal md
|
|
44
|
+
* slider.
|
|
45
|
+
*/
|
|
46
|
+
export const Slider = ({
|
|
47
|
+
value,
|
|
48
|
+
onChange,
|
|
49
|
+
minValue = 0,
|
|
50
|
+
maxValue = 100,
|
|
51
|
+
"aria-label": ariaLabel,
|
|
52
|
+
isDisabled,
|
|
53
|
+
formatOptions,
|
|
54
|
+
css: cssProp,
|
|
55
|
+
trackCss,
|
|
56
|
+
filledTrackCss,
|
|
57
|
+
thumbCss,
|
|
58
|
+
mark,
|
|
59
|
+
markCss,
|
|
60
|
+
}: SliderProps) => {
|
|
61
|
+
const slots = slider();
|
|
62
|
+
const percent = ((value - minValue) / (maxValue - minValue)) * 100;
|
|
63
|
+
return (
|
|
64
|
+
<RACSlider
|
|
65
|
+
value={value}
|
|
66
|
+
onChange={onChange}
|
|
67
|
+
minValue={minValue}
|
|
68
|
+
maxValue={maxValue}
|
|
69
|
+
aria-label={ariaLabel}
|
|
70
|
+
isDisabled={isDisabled}
|
|
71
|
+
formatOptions={formatOptions}
|
|
72
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined)}
|
|
73
|
+
>
|
|
74
|
+
{mark && (
|
|
75
|
+
<div
|
|
76
|
+
data-part="mark"
|
|
77
|
+
className={cx(slots.mark, markCss ? css(markCss) : undefined)}
|
|
78
|
+
style={{ left: `${percent}%` }}
|
|
79
|
+
>
|
|
80
|
+
{mark}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
<SliderTrack
|
|
84
|
+
className={cx(slots.track, trackCss ? css(trackCss) : undefined)}
|
|
85
|
+
>
|
|
86
|
+
<div
|
|
87
|
+
className={cx(
|
|
88
|
+
slots.filledTrack,
|
|
89
|
+
filledTrackCss ? css(filledTrackCss) : undefined,
|
|
90
|
+
)}
|
|
91
|
+
style={{ width: `${percent}%` }}
|
|
92
|
+
/>
|
|
93
|
+
</SliderTrack>
|
|
94
|
+
<SliderThumb
|
|
95
|
+
className={cx(slots.thumb, thumbCss ? css(thumbCss) : undefined)}
|
|
96
|
+
/>
|
|
97
|
+
</RACSlider>
|
|
98
|
+
);
|
|
99
|
+
};
|
package/src/Spinner.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { CSSProperties } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
export interface SpinnerProps {
|
|
11
|
+
/** Chakra Spinner sizes: sm 1rem, md 1.5rem (default). */
|
|
12
|
+
size?: "sm" | "md";
|
|
13
|
+
/**
|
|
14
|
+
* Revolution time (Chakra's `speed`, default 0.45s). Under
|
|
15
|
+
* prefers-reduced-motion the spin is slowed to ~3x this value.
|
|
16
|
+
*/
|
|
17
|
+
speed?: string;
|
|
18
|
+
/** Per-instance style overrides, merged after the base. */
|
|
19
|
+
css?: SystemStyleObject;
|
|
20
|
+
className?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Accessible name. Required: Chakra's Spinner announced a visually hidden
|
|
23
|
+
* "Loading..." by default, so a nameless spinner would regress on it.
|
|
24
|
+
*/
|
|
25
|
+
"aria-label": string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Spinner — Chakra's border-based spinner (currentColor with transparent
|
|
30
|
+
* bottom/left quadrants).
|
|
31
|
+
*/
|
|
32
|
+
export const Spinner = ({
|
|
33
|
+
size = "md",
|
|
34
|
+
speed,
|
|
35
|
+
css: cssProp,
|
|
36
|
+
className,
|
|
37
|
+
"aria-label": ariaLabel,
|
|
38
|
+
}: SpinnerProps) => (
|
|
39
|
+
<span
|
|
40
|
+
role="status"
|
|
41
|
+
aria-label={ariaLabel}
|
|
42
|
+
style={speed ? ({ "--spinner-speed": speed } as CSSProperties) : undefined}
|
|
43
|
+
className={cx(
|
|
44
|
+
// Single css() call so caller overrides of base properties (e.g. width)
|
|
45
|
+
// are deduped at merge time rather than racing on stylesheet order.
|
|
46
|
+
css(
|
|
47
|
+
{
|
|
48
|
+
display: "inline-block",
|
|
49
|
+
borderWidth: "2px",
|
|
50
|
+
borderStyle: "solid",
|
|
51
|
+
borderColor: "currentColor",
|
|
52
|
+
borderBottomColor: "transparent",
|
|
53
|
+
borderLeftColor: "transparent",
|
|
54
|
+
borderRadius: "full",
|
|
55
|
+
// The duration rides a variable so the reduced-motion slow-down
|
|
56
|
+
// scales with a per-instance `speed` rather than fighting it.
|
|
57
|
+
animation: "spin var(--spinner-speed, 0.45s) linear infinite",
|
|
58
|
+
_motionReduce: {
|
|
59
|
+
animationDuration: "calc(var(--spinner-speed, 0.45s) * 3.33)",
|
|
60
|
+
},
|
|
61
|
+
width: size === "sm" ? "1rem" : "1.5rem",
|
|
62
|
+
height: size === "sm" ? "1rem" : "1.5rem",
|
|
63
|
+
},
|
|
64
|
+
cssProp,
|
|
65
|
+
),
|
|
66
|
+
className,
|
|
67
|
+
)}
|
|
68
|
+
/>
|
|
69
|
+
);
|
package/src/Svg.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
|
+
* Svg — a Panda-styled svg element with Chakra Icon's base sizing (1em,
|
|
10
|
+
* inline-block). For custom-path icons; react-icons glyphs use `Icon`.
|
|
11
|
+
* As a styled-factory component its call-site style props are extracted
|
|
12
|
+
* (unlike style props forwarded through a plain wrapper — gotcha #9).
|
|
13
|
+
*/
|
|
14
|
+
export const Svg = styled("svg", {
|
|
15
|
+
base: {
|
|
16
|
+
width: "1em",
|
|
17
|
+
height: "1em",
|
|
18
|
+
display: "inline-block",
|
|
19
|
+
lineHeight: "1em",
|
|
20
|
+
flexShrink: 0,
|
|
21
|
+
verticalAlign: "middle",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
* Switch slot recipe — Chakra's md switch with the default blue colorScheme
|
|
10
|
+
* (light mode). Track is 1.875rem x 1rem with a 2px inset; the thumb slides by
|
|
11
|
+
* the track/thumb width difference when selected.
|
|
12
|
+
*
|
|
13
|
+
* State styling keys off data attributes stamped by the shared-ui Switch
|
|
14
|
+
* (react-aria provides the state via render props).
|
|
15
|
+
*
|
|
16
|
+
* Registered in the base preset (base-preset.ts). No variants, so it
|
|
17
|
+
* needs no `staticCss` entry.
|
|
18
|
+
*/
|
|
19
|
+
export const switchRecipe = defineSlotRecipe({
|
|
20
|
+
className: "switch",
|
|
21
|
+
slots: ["root", "track", "thumb", "label"],
|
|
22
|
+
base: {
|
|
23
|
+
root: {
|
|
24
|
+
display: "inline-flex",
|
|
25
|
+
alignItems: "center",
|
|
26
|
+
verticalAlign: "top",
|
|
27
|
+
cursor: "pointer",
|
|
28
|
+
position: "relative",
|
|
29
|
+
"&[data-disabled]": { cursor: "not-allowed" },
|
|
30
|
+
},
|
|
31
|
+
track: {
|
|
32
|
+
display: "inline-flex",
|
|
33
|
+
flexShrink: 0,
|
|
34
|
+
justifyContent: "flex-start",
|
|
35
|
+
boxSizing: "content-box",
|
|
36
|
+
borderRadius: "full",
|
|
37
|
+
p: "0.5",
|
|
38
|
+
width: "1.875rem",
|
|
39
|
+
height: "4",
|
|
40
|
+
transitionProperty: "background-color",
|
|
41
|
+
transitionDuration: "fast",
|
|
42
|
+
bg: "gray.300",
|
|
43
|
+
"&[data-selected]": {
|
|
44
|
+
bg: "controlCheckedBg",
|
|
45
|
+
},
|
|
46
|
+
"&[data-focus-visible]": {
|
|
47
|
+
focusShadow: "outline",
|
|
48
|
+
},
|
|
49
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
50
|
+
// Forced-colors modes strip author backgrounds, flattening track and
|
|
51
|
+
// thumb to the same colour; a border keeps the track visible and the
|
|
52
|
+
// system SelectedItem pair conveys on/off.
|
|
53
|
+
_forcedColors: {
|
|
54
|
+
borderWidth: "1px",
|
|
55
|
+
borderStyle: "solid",
|
|
56
|
+
"&[data-selected]": { bg: "SelectedItem" },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
thumb: {
|
|
60
|
+
bg: "white",
|
|
61
|
+
transitionProperty: "transform",
|
|
62
|
+
transitionDuration: "normal",
|
|
63
|
+
borderRadius: "inherit",
|
|
64
|
+
width: "4",
|
|
65
|
+
height: "4",
|
|
66
|
+
"&[data-selected]": {
|
|
67
|
+
transform: "translateX(0.875rem)",
|
|
68
|
+
},
|
|
69
|
+
_forcedColors: {
|
|
70
|
+
bg: "ButtonText",
|
|
71
|
+
"&[data-selected]": { bg: "SelectedItemText" },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
label: {
|
|
75
|
+
userSelect: "none",
|
|
76
|
+
marginStart: "2",
|
|
77
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
package/src/Switch.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
Switch as RACSwitch,
|
|
9
|
+
SwitchProps as RACSwitchProps,
|
|
10
|
+
} from "react-aria-components";
|
|
11
|
+
import { css, cx } from "styled-system/css";
|
|
12
|
+
import { switchRecipe } from "styled-system/recipes";
|
|
13
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
14
|
+
|
|
15
|
+
export interface SwitchProps
|
|
16
|
+
extends Omit<RACSwitchProps, "className" | "children" | "style"> {
|
|
17
|
+
/** Per-instance style overrides for the root, merged after the recipe. */
|
|
18
|
+
css?: SystemStyleObject;
|
|
19
|
+
className?: string;
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Switch — react-aria-components <Switch> styled like Chakra's md switch.
|
|
25
|
+
* Children render as the label; pass `aria-label` for label-less switches.
|
|
26
|
+
*/
|
|
27
|
+
export const Switch = ({
|
|
28
|
+
css: cssProp,
|
|
29
|
+
className,
|
|
30
|
+
children,
|
|
31
|
+
...rest
|
|
32
|
+
}: SwitchProps) => {
|
|
33
|
+
const slots = switchRecipe();
|
|
34
|
+
return (
|
|
35
|
+
<RACSwitch
|
|
36
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
37
|
+
{...rest}
|
|
38
|
+
>
|
|
39
|
+
{({ isSelected, isFocusVisible, isDisabled }) => {
|
|
40
|
+
const state = {
|
|
41
|
+
"data-selected": isSelected || undefined,
|
|
42
|
+
"data-focus-visible": isFocusVisible || undefined,
|
|
43
|
+
"data-disabled": isDisabled || undefined,
|
|
44
|
+
};
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<span className={slots.track} {...state} aria-hidden>
|
|
48
|
+
<span className={slots.thumb} {...state} />
|
|
49
|
+
</span>
|
|
50
|
+
{children != null && (
|
|
51
|
+
<span className={slots.label} {...state}>
|
|
52
|
+
{children}
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
57
|
+
}}
|
|
58
|
+
</RACSwitch>
|
|
59
|
+
);
|
|
60
|
+
};
|
package/src/Text.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
* Text — a paragraph that accepts Panda style props. Use `as` to render a
|
|
10
|
+
* different element (`span`, `div`, `h2`). Replaces Chakra's <Text>.
|
|
11
|
+
*/
|
|
12
|
+
export const Text = styled("p");
|