@microbit/ui 0.1.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +53 -0
- package/README.md +220 -0
- package/lang/ui.ca.json +22 -0
- package/lang/ui.de.json +22 -0
- package/lang/ui.en-us.json +22 -0
- package/lang/ui.en.json +22 -0
- package/lang/ui.es-es.json +22 -0
- package/lang/ui.fr.json +22 -0
- package/lang/ui.ga-ie.json +22 -0
- package/lang/ui.ja.json +22 -0
- package/lang/ui.ko.json +22 -0
- package/lang/ui.lol.json +22 -0
- package/lang/ui.nl.json +22 -0
- package/lang/ui.pl.json +22 -0
- package/lang/ui.pt-br.json +22 -0
- package/lang/ui.zh-cn.json +22 -0
- package/lang/ui.zh-tw.json +22 -0
- package/package.json +58 -0
- package/postcss-legacy-safari.cjs +96 -0
- package/reset.css +35 -0
- package/src/Button.recipe.ts +162 -0
- package/src/Button.tsx +69 -0
- package/src/ButtonGroup.tsx +63 -0
- package/src/Card.recipe.ts +49 -0
- package/src/Card.tsx +56 -0
- package/src/Checkbox.recipe.ts +105 -0
- package/src/Checkbox.tsx +76 -0
- package/src/CloseButton.tsx +87 -0
- package/src/CloseIcon.tsx +40 -0
- package/src/Code.tsx +20 -0
- package/src/Collapse.tsx +180 -0
- package/src/Divider.tsx +52 -0
- package/src/Drawer.recipe.ts +98 -0
- package/src/Drawer.tsx +138 -0
- package/src/Fade.tsx +48 -0
- package/src/Heading.recipe.ts +52 -0
- package/src/Heading.tsx +14 -0
- package/src/Icon.tsx +55 -0
- package/src/IconButton.tsx +44 -0
- package/src/Image.tsx +11 -0
- package/src/Input.recipe.ts +72 -0
- package/src/Input.tsx +40 -0
- package/src/InputGroup.tsx +62 -0
- package/src/Kbd.tsx +26 -0
- package/src/Link.tsx +23 -0
- package/src/LinkBox.tsx +88 -0
- package/src/LinkButton.tsx +80 -0
- package/src/List.tsx +31 -0
- package/src/Menu.recipe.ts +119 -0
- package/src/Menu.tsx +261 -0
- package/src/Modal.recipe.ts +163 -0
- package/src/Modal.tsx +279 -0
- package/src/NativeSelect.tsx +94 -0
- package/src/NumberField.recipe.ts +67 -0
- package/src/NumberField.tsx +86 -0
- package/src/PopoverArrow.tsx +67 -0
- package/src/ProgressBar.tsx +61 -0
- package/src/Radio.recipe.ts +107 -0
- package/src/Radio.tsx +89 -0
- package/src/SharedUIProvider.tsx +55 -0
- package/src/Slide.tsx +54 -0
- package/src/Slider.recipe.ts +102 -0
- package/src/Slider.tsx +163 -0
- package/src/Spinner.tsx +69 -0
- package/src/Svg.tsx +23 -0
- package/src/Switch.recipe.ts +107 -0
- package/src/Switch.tsx +62 -0
- package/src/Text.recipe.ts +29 -0
- package/src/Text.tsx +16 -0
- package/src/TextField.recipe.ts +54 -0
- package/src/TextField.tsx +89 -0
- package/src/Toast.recipe.ts +98 -0
- package/src/Toast.tsx +171 -0
- package/src/Tooltip.tsx +91 -0
- package/src/UnmountCallback.tsx +18 -0
- package/src/VisuallyHidden.tsx +14 -0
- package/src/base-preset.ts +294 -0
- package/src/base-tokens.ts +1017 -0
- package/src/button-icon.ts +23 -0
- package/src/hooks/useBreakpointValue.ts +63 -0
- package/src/hooks/useClipboard.ts +64 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/usePrevious.ts +18 -0
- package/src/index.ts +55 -0
- package/src/messages.ts +17 -0
- package/src/system.ts +36 -0
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
* Radio slot recipe — Chakra's radio with the default blue colorScheme
|
|
10
|
+
* (light mode) and its sm/md/lg size scale: the checkbox control rounded
|
|
11
|
+
* fully, with a 50% `currentColor` dot when selected instead of the check
|
|
12
|
+
* glyph. As with the checkbox, `borderColor: inherit` picks up a
|
|
13
|
+
* `borderColor` set on the root.
|
|
14
|
+
*
|
|
15
|
+
* State styling keys off data attributes stamped by the shared-ui Radio
|
|
16
|
+
* (react-aria provides the state via render props).
|
|
17
|
+
*
|
|
18
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
19
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
20
|
+
*/
|
|
21
|
+
export const radio = defineSlotRecipe({
|
|
22
|
+
className: "radio",
|
|
23
|
+
slots: ["root", "control", "label"],
|
|
24
|
+
base: {
|
|
25
|
+
root: {
|
|
26
|
+
display: "inline-flex",
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
verticalAlign: "top",
|
|
29
|
+
cursor: "pointer",
|
|
30
|
+
position: "relative",
|
|
31
|
+
"&[data-disabled]": { cursor: "not-allowed" },
|
|
32
|
+
},
|
|
33
|
+
control: {
|
|
34
|
+
display: "inline-flex",
|
|
35
|
+
alignItems: "center",
|
|
36
|
+
justifyContent: "center",
|
|
37
|
+
flexShrink: 0,
|
|
38
|
+
transitionProperty: "box-shadow",
|
|
39
|
+
transitionDuration: "normal",
|
|
40
|
+
borderWidth: "2px",
|
|
41
|
+
borderStyle: "solid",
|
|
42
|
+
borderRadius: "full",
|
|
43
|
+
borderColor: "inherit",
|
|
44
|
+
color: "white",
|
|
45
|
+
bg: "white",
|
|
46
|
+
"&[data-selected]": {
|
|
47
|
+
bg: "controlCheckedBg",
|
|
48
|
+
borderColor: "controlCheckedBg",
|
|
49
|
+
color: "white",
|
|
50
|
+
_hover: {
|
|
51
|
+
bg: "controlCheckedHoverBg",
|
|
52
|
+
borderColor: "controlCheckedHoverBg",
|
|
53
|
+
},
|
|
54
|
+
// Chakra's radio dot.
|
|
55
|
+
_before: {
|
|
56
|
+
content: '""',
|
|
57
|
+
display: "inline-block",
|
|
58
|
+
position: "relative",
|
|
59
|
+
width: "50%",
|
|
60
|
+
height: "50%",
|
|
61
|
+
borderRadius: "50%",
|
|
62
|
+
bg: "currentColor",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
// Chakra's disabled greys; the selected block restates _hover so the
|
|
66
|
+
// widened native-:hover condition can't re-tint a disabled control.
|
|
67
|
+
"&[data-disabled]": {
|
|
68
|
+
bg: "gray.100",
|
|
69
|
+
borderColor: "gray.100",
|
|
70
|
+
},
|
|
71
|
+
"&[data-selected][data-disabled]": {
|
|
72
|
+
bg: "gray.200",
|
|
73
|
+
borderColor: "gray.200",
|
|
74
|
+
color: "gray.500",
|
|
75
|
+
_hover: { bg: "gray.200", borderColor: "gray.200" },
|
|
76
|
+
},
|
|
77
|
+
"&[data-focus-visible]": {
|
|
78
|
+
focusShadow: "outline",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
label: {
|
|
82
|
+
userSelect: "none",
|
|
83
|
+
marginStart: "2",
|
|
84
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
variants: {
|
|
88
|
+
// Chakra's Radio size scale (the Checkbox control scale).
|
|
89
|
+
size: {
|
|
90
|
+
sm: {
|
|
91
|
+
control: { width: "3", height: "3" },
|
|
92
|
+
label: { fontSize: "sm" },
|
|
93
|
+
},
|
|
94
|
+
md: {
|
|
95
|
+
control: { width: "4", height: "4" },
|
|
96
|
+
label: { fontSize: "md" },
|
|
97
|
+
},
|
|
98
|
+
lg: {
|
|
99
|
+
control: { width: "5", height: "5" },
|
|
100
|
+
label: { fontSize: "lg" },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
defaultVariants: {
|
|
105
|
+
size: "md",
|
|
106
|
+
},
|
|
107
|
+
});
|
package/src/Radio.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
Radio as RACRadio,
|
|
9
|
+
RadioProps as RACRadioProps,
|
|
10
|
+
RadioGroup as RACRadioGroup,
|
|
11
|
+
RadioGroupProps as RACRadioGroupProps,
|
|
12
|
+
} from "react-aria-components";
|
|
13
|
+
import { css, cx } from "styled-system/css";
|
|
14
|
+
import { radio, RadioVariantProps } from "styled-system/recipes";
|
|
15
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
16
|
+
|
|
17
|
+
export interface RadioGroupProps
|
|
18
|
+
extends Omit<RACRadioGroupProps, "className" | "style"> {
|
|
19
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
20
|
+
css?: SystemStyleObject;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* RadioGroup — react-aria-components <RadioGroup> for a set of Radios. Carries
|
|
26
|
+
* no styling of its own: compose with Stack for layout, as Chakra call sites
|
|
27
|
+
* did.
|
|
28
|
+
*/
|
|
29
|
+
export const RadioGroup = ({
|
|
30
|
+
css: cssProp,
|
|
31
|
+
className,
|
|
32
|
+
...rest
|
|
33
|
+
}: RadioGroupProps) => {
|
|
34
|
+
return (
|
|
35
|
+
<RACRadioGroup
|
|
36
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
37
|
+
{...rest}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface RadioProps
|
|
43
|
+
extends Omit<RACRadioProps, "className" | "children" | "style">,
|
|
44
|
+
RadioVariantProps {
|
|
45
|
+
/** Per-instance style overrides for the root, merged after the recipe. */
|
|
46
|
+
css?: SystemStyleObject;
|
|
47
|
+
className?: string;
|
|
48
|
+
children?: ReactNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Radio — react-aria-components <Radio> styled like Chakra's radio. Must be
|
|
53
|
+
* rendered inside a RadioGroup. Children render as the label.
|
|
54
|
+
*/
|
|
55
|
+
export const Radio = ({
|
|
56
|
+
size,
|
|
57
|
+
css: cssProp,
|
|
58
|
+
className,
|
|
59
|
+
children,
|
|
60
|
+
...rest
|
|
61
|
+
}: RadioProps) => {
|
|
62
|
+
const slots = radio({ size });
|
|
63
|
+
return (
|
|
64
|
+
<RACRadio
|
|
65
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
66
|
+
{...rest}
|
|
67
|
+
>
|
|
68
|
+
{({ isSelected, isFocusVisible, isDisabled }) => (
|
|
69
|
+
<>
|
|
70
|
+
<span
|
|
71
|
+
className={slots.control}
|
|
72
|
+
data-selected={isSelected || undefined}
|
|
73
|
+
data-focus-visible={isFocusVisible || undefined}
|
|
74
|
+
data-disabled={isDisabled || undefined}
|
|
75
|
+
aria-hidden
|
|
76
|
+
/>
|
|
77
|
+
{children != null && (
|
|
78
|
+
<span
|
|
79
|
+
className={slots.label}
|
|
80
|
+
data-disabled={isDisabled || undefined}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</>
|
|
86
|
+
)}
|
|
87
|
+
</RACRadio>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
@@ -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,102 @@
|
|
|
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 (3.5-token thumb, 1-token
|
|
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
|
+
* Geometry matches Chakra's DOM: the root is thumb-height with NO horizontal
|
|
15
|
+
* padding, the track is absolutely centred inside it at full width, and the
|
|
16
|
+
* thumb overhangs the track ends at 0%/100%. Call sites position overlays
|
|
17
|
+
* (marks, value labels) against that root box, so Chakra-era offsets work
|
|
18
|
+
* unchanged. Sizes are tokens, not px, so apps with resized scales (e.g.
|
|
19
|
+
* python-editor's ×0.88) get their Chakra-equivalent geometry.
|
|
20
|
+
*
|
|
21
|
+
* The `mark` slot is hidden until the slider has focus (Chakra call sites did
|
|
22
|
+
* this with `_focusWithin` + a class); the reveal lives here so call sites
|
|
23
|
+
* only style the mark's look.
|
|
24
|
+
*
|
|
25
|
+
* Registered in the base preset (base-preset.ts). No variants, so it
|
|
26
|
+
* needs no `staticCss` entry.
|
|
27
|
+
*/
|
|
28
|
+
export const slider = defineSlotRecipe({
|
|
29
|
+
className: "slider",
|
|
30
|
+
slots: ["root", "track", "filledTrack", "thumb", "mark"],
|
|
31
|
+
base: {
|
|
32
|
+
root: {
|
|
33
|
+
position: "relative",
|
|
34
|
+
display: "block",
|
|
35
|
+
width: "100%",
|
|
36
|
+
// Thumb-height, like Chakra's container; the track centres inside.
|
|
37
|
+
height: "3.5",
|
|
38
|
+
touchAction: "none",
|
|
39
|
+
"&:focus-within [data-part='mark']": {
|
|
40
|
+
display: "block",
|
|
41
|
+
},
|
|
42
|
+
"&[data-disabled]": {
|
|
43
|
+
opacity: 0.4,
|
|
44
|
+
cursor: "not-allowed",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
track: {
|
|
48
|
+
position: "absolute",
|
|
49
|
+
top: "50%",
|
|
50
|
+
transform: "translateY(-50%)",
|
|
51
|
+
left: 0,
|
|
52
|
+
width: "100%",
|
|
53
|
+
height: "1",
|
|
54
|
+
overflow: "hidden",
|
|
55
|
+
borderRadius: "sm",
|
|
56
|
+
bg: "gray.200",
|
|
57
|
+
cursor: "pointer",
|
|
58
|
+
"[data-disabled] &": {
|
|
59
|
+
bg: "gray.300",
|
|
60
|
+
cursor: "default",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
filledTrack: {
|
|
64
|
+
position: "absolute",
|
|
65
|
+
top: 0,
|
|
66
|
+
left: 0,
|
|
67
|
+
height: "100%",
|
|
68
|
+
bg: "sliderFilledTrack",
|
|
69
|
+
},
|
|
70
|
+
thumb: {
|
|
71
|
+
position: "absolute",
|
|
72
|
+
top: "50%",
|
|
73
|
+
width: "3.5",
|
|
74
|
+
height: "3.5",
|
|
75
|
+
display: "flex",
|
|
76
|
+
alignItems: "center",
|
|
77
|
+
justifyContent: "center",
|
|
78
|
+
outline: "none",
|
|
79
|
+
zIndex: 1,
|
|
80
|
+
borderRadius: "full",
|
|
81
|
+
bg: "white",
|
|
82
|
+
boxShadow: "base",
|
|
83
|
+
transitionProperty: "transform",
|
|
84
|
+
transitionDuration: "normal",
|
|
85
|
+
"&[data-dragging]": {
|
|
86
|
+
transform: "translate(-50%, -50%) scale(1.15)",
|
|
87
|
+
},
|
|
88
|
+
transform: "translate(-50%, -50%)",
|
|
89
|
+
"&[data-focus-visible]": {
|
|
90
|
+
focusShadow: "outline",
|
|
91
|
+
},
|
|
92
|
+
"&[data-disabled]": {
|
|
93
|
+
bg: "gray.300",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
mark: {
|
|
97
|
+
display: "none",
|
|
98
|
+
position: "absolute",
|
|
99
|
+
whiteSpace: "nowrap",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
package/src/Slider.tsx
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
* Additional positioned overlays rendered inside the slider root
|
|
42
|
+
* (always-visible value labels, threshold markers, ...). The root is
|
|
43
|
+
* position: relative; position children absolutely, typically with a
|
|
44
|
+
* percentage `left` for the track position.
|
|
45
|
+
*/
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
/**
|
|
48
|
+
* Tooltip-styled bubble anchored above the thumb (Chakra's
|
|
49
|
+
* Tooltip-around-SliderThumb pattern). Rendered only while
|
|
50
|
+
* `isThumbTooltipOpen`; drive it from hover/focus, e.g. via
|
|
51
|
+
* `onThumbFocusChange` and mouse handlers on an enclosing element.
|
|
52
|
+
*/
|
|
53
|
+
thumbTooltip?: ReactNode;
|
|
54
|
+
isThumbTooltipOpen?: boolean;
|
|
55
|
+
/** Thumb focus tracking (react-aria focus events). */
|
|
56
|
+
onThumbFocusChange?: (isFocused: boolean) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Slider — react-aria-components <Slider> styled like Chakra's horizontal md
|
|
61
|
+
* slider.
|
|
62
|
+
*/
|
|
63
|
+
export const Slider = ({
|
|
64
|
+
value,
|
|
65
|
+
onChange,
|
|
66
|
+
minValue = 0,
|
|
67
|
+
maxValue = 100,
|
|
68
|
+
"aria-label": ariaLabel,
|
|
69
|
+
isDisabled,
|
|
70
|
+
formatOptions,
|
|
71
|
+
css: cssProp,
|
|
72
|
+
trackCss,
|
|
73
|
+
filledTrackCss,
|
|
74
|
+
thumbCss,
|
|
75
|
+
mark,
|
|
76
|
+
markCss,
|
|
77
|
+
children,
|
|
78
|
+
thumbTooltip,
|
|
79
|
+
isThumbTooltipOpen,
|
|
80
|
+
onThumbFocusChange,
|
|
81
|
+
}: SliderProps) => {
|
|
82
|
+
const slots = slider();
|
|
83
|
+
const percent = ((value - minValue) / (maxValue - minValue)) * 100;
|
|
84
|
+
return (
|
|
85
|
+
<RACSlider
|
|
86
|
+
value={value}
|
|
87
|
+
onChange={onChange}
|
|
88
|
+
minValue={minValue}
|
|
89
|
+
maxValue={maxValue}
|
|
90
|
+
aria-label={ariaLabel}
|
|
91
|
+
isDisabled={isDisabled}
|
|
92
|
+
formatOptions={formatOptions}
|
|
93
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined)}
|
|
94
|
+
>
|
|
95
|
+
{mark && (
|
|
96
|
+
<div
|
|
97
|
+
data-part="mark"
|
|
98
|
+
className={cx(slots.mark, markCss ? css(markCss) : undefined)}
|
|
99
|
+
style={{ left: `${percent}%` }}
|
|
100
|
+
>
|
|
101
|
+
{mark}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
<SliderTrack
|
|
105
|
+
className={cx(slots.track, trackCss ? css(trackCss) : undefined)}
|
|
106
|
+
>
|
|
107
|
+
<div
|
|
108
|
+
className={cx(
|
|
109
|
+
slots.filledTrack,
|
|
110
|
+
filledTrackCss ? css(filledTrackCss) : undefined,
|
|
111
|
+
)}
|
|
112
|
+
style={{ width: `${percent}%` }}
|
|
113
|
+
/>
|
|
114
|
+
</SliderTrack>
|
|
115
|
+
{thumbTooltip && isThumbTooltipOpen && (
|
|
116
|
+
// Matches the shared Tooltip's look (tooltipBase) with a bottom
|
|
117
|
+
// arrow, anchored above the thumb.
|
|
118
|
+
<div
|
|
119
|
+
role="presentation"
|
|
120
|
+
className={css({
|
|
121
|
+
position: "absolute",
|
|
122
|
+
bottom: "calc(50% + token(sizes.3.5))",
|
|
123
|
+
transform: "translateX(-50%)",
|
|
124
|
+
bg: "gray.700",
|
|
125
|
+
color: "white",
|
|
126
|
+
px: "2",
|
|
127
|
+
py: "1",
|
|
128
|
+
borderRadius: "md",
|
|
129
|
+
fontSize: "sm",
|
|
130
|
+
fontWeight: "medium",
|
|
131
|
+
boxShadow: "md",
|
|
132
|
+
zIndex: "tooltip",
|
|
133
|
+
whiteSpace: "nowrap",
|
|
134
|
+
_after: {
|
|
135
|
+
content: '""',
|
|
136
|
+
position: "absolute",
|
|
137
|
+
// 1px into the box so subpixel edges can't antialias into a
|
|
138
|
+
// hairline seam (see PopoverArrow).
|
|
139
|
+
top: "calc(100% - 1px)",
|
|
140
|
+
left: "50%",
|
|
141
|
+
transform: "translateX(-50%)",
|
|
142
|
+
borderWidth: "4px",
|
|
143
|
+
borderStyle: "solid",
|
|
144
|
+
// Per-side: tokens don't resolve inside multi-value
|
|
145
|
+
// shorthands (they emit verbatim and the browser drops the
|
|
146
|
+
// invalid declaration).
|
|
147
|
+
borderColor: "transparent",
|
|
148
|
+
borderTopColor: "gray.700",
|
|
149
|
+
},
|
|
150
|
+
})}
|
|
151
|
+
style={{ left: `${percent}%` }}
|
|
152
|
+
>
|
|
153
|
+
{thumbTooltip}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
<SliderThumb
|
|
157
|
+
onFocusChange={onThumbFocusChange}
|
|
158
|
+
className={cx(slots.thumb, thumbCss ? css(thumbCss) : undefined)}
|
|
159
|
+
/>
|
|
160
|
+
{children}
|
|
161
|
+
</RACSlider>
|
|
162
|
+
);
|
|
163
|
+
};
|
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
|
+
});
|