@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/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/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,74 @@
|
|
|
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 { dataAttrs } from "./data-attrs";
|
|
9
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
10
|
+
|
|
11
|
+
export interface SpinnerProps {
|
|
12
|
+
/** Chakra Spinner sizes: sm 1rem, md 1.5rem (default). */
|
|
13
|
+
size?: "sm" | "md";
|
|
14
|
+
/**
|
|
15
|
+
* Revolution time (Chakra's `speed`, default 0.45s). Under
|
|
16
|
+
* prefers-reduced-motion the spin is slowed to ~3x this value.
|
|
17
|
+
*/
|
|
18
|
+
speed?: string;
|
|
19
|
+
/** Per-instance style overrides, merged after the base. */
|
|
20
|
+
css?: SystemStyleObject;
|
|
21
|
+
className?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Accessible name. Required: Chakra's Spinner announced a visually hidden
|
|
24
|
+
* "Loading..." by default, so a nameless spinner would regress on it.
|
|
25
|
+
*/
|
|
26
|
+
"aria-label": string;
|
|
27
|
+
/** `data-*` attributes land on the spinner, for tests that wait on it. */
|
|
28
|
+
[key: `data-${string}`]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Spinner — Chakra's border-based spinner (currentColor with transparent
|
|
33
|
+
* bottom/left quadrants).
|
|
34
|
+
*/
|
|
35
|
+
export const Spinner = ({
|
|
36
|
+
size = "md",
|
|
37
|
+
speed,
|
|
38
|
+
css: cssProp,
|
|
39
|
+
className,
|
|
40
|
+
"aria-label": ariaLabel,
|
|
41
|
+
...rest
|
|
42
|
+
}: SpinnerProps) => (
|
|
43
|
+
<span
|
|
44
|
+
{...dataAttrs(rest)}
|
|
45
|
+
role="status"
|
|
46
|
+
aria-label={ariaLabel}
|
|
47
|
+
style={speed ? ({ "--spinner-speed": speed } as CSSProperties) : undefined}
|
|
48
|
+
className={cx(
|
|
49
|
+
// Single css() call so caller overrides of base properties (e.g. width)
|
|
50
|
+
// are deduped at merge time rather than racing on stylesheet order.
|
|
51
|
+
css(
|
|
52
|
+
{
|
|
53
|
+
display: "inline-block",
|
|
54
|
+
borderWidth: "2px",
|
|
55
|
+
borderStyle: "solid",
|
|
56
|
+
borderColor: "currentColor",
|
|
57
|
+
borderBottomColor: "transparent",
|
|
58
|
+
borderLeftColor: "transparent",
|
|
59
|
+
borderRadius: "full",
|
|
60
|
+
// The duration rides a variable so the reduced-motion slow-down
|
|
61
|
+
// scales with a per-instance `speed` rather than fighting it.
|
|
62
|
+
animation: "spin var(--spinner-speed, 0.45s) linear infinite",
|
|
63
|
+
_motionReduce: {
|
|
64
|
+
animationDuration: "calc(var(--spinner-speed, 0.45s) * 3.33)",
|
|
65
|
+
},
|
|
66
|
+
width: size === "sm" ? "1rem" : "1.5rem",
|
|
67
|
+
height: size === "sm" ? "1rem" : "1.5rem",
|
|
68
|
+
},
|
|
69
|
+
cssProp,
|
|
70
|
+
),
|
|
71
|
+
className,
|
|
72
|
+
)}
|
|
73
|
+
/>
|
|
74
|
+
);
|
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,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
|
+
* Switch slot recipe — Chakra's switch with the default blue colorScheme
|
|
10
|
+
* (light mode) and its sm/md/lg size scale. The track has a 2px inset; the
|
|
11
|
+
* thumb matches the track height and slides by the track width/height
|
|
12
|
+
* difference when selected.
|
|
13
|
+
*
|
|
14
|
+
* State styling keys off data attributes stamped by the shared-ui Switch
|
|
15
|
+
* (react-aria provides the state via render props).
|
|
16
|
+
*
|
|
17
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
18
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
19
|
+
*/
|
|
20
|
+
export const switchRecipe = defineSlotRecipe({
|
|
21
|
+
className: "switch",
|
|
22
|
+
slots: ["root", "track", "thumb", "label"],
|
|
23
|
+
base: {
|
|
24
|
+
root: {
|
|
25
|
+
display: "inline-flex",
|
|
26
|
+
alignItems: "center",
|
|
27
|
+
verticalAlign: "top",
|
|
28
|
+
cursor: "pointer",
|
|
29
|
+
position: "relative",
|
|
30
|
+
"&[data-disabled]": { cursor: "not-allowed" },
|
|
31
|
+
},
|
|
32
|
+
track: {
|
|
33
|
+
display: "inline-flex",
|
|
34
|
+
flexShrink: 0,
|
|
35
|
+
justifyContent: "flex-start",
|
|
36
|
+
boxSizing: "content-box",
|
|
37
|
+
borderRadius: "full",
|
|
38
|
+
p: "0.5",
|
|
39
|
+
transitionProperty: "background-color",
|
|
40
|
+
transitionDuration: "fast",
|
|
41
|
+
bg: "gray.300",
|
|
42
|
+
"&[data-selected]": {
|
|
43
|
+
bg: "controlCheckedBg",
|
|
44
|
+
},
|
|
45
|
+
"&[data-focus-visible]": {
|
|
46
|
+
focusShadow: "outline",
|
|
47
|
+
},
|
|
48
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
49
|
+
// Forced-colors modes strip author backgrounds, flattening track and
|
|
50
|
+
// thumb to the same colour; a border keeps the track visible and the
|
|
51
|
+
// system SelectedItem pair conveys on/off.
|
|
52
|
+
_forcedColors: {
|
|
53
|
+
borderWidth: "1px",
|
|
54
|
+
borderStyle: "solid",
|
|
55
|
+
"&[data-selected]": { bg: "SelectedItem" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
thumb: {
|
|
59
|
+
bg: "white",
|
|
60
|
+
transitionProperty: "transform",
|
|
61
|
+
transitionDuration: "normal",
|
|
62
|
+
borderRadius: "inherit",
|
|
63
|
+
_forcedColors: {
|
|
64
|
+
bg: "ButtonText",
|
|
65
|
+
"&[data-selected]": { bg: "SelectedItemText" },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
label: {
|
|
69
|
+
userSelect: "none",
|
|
70
|
+
marginStart: "2",
|
|
71
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
variants: {
|
|
75
|
+
// Chakra's Switch size scale (track width x height; the selected-thumb
|
|
76
|
+
// translate is the difference between them).
|
|
77
|
+
size: {
|
|
78
|
+
sm: {
|
|
79
|
+
track: { width: "1.375rem", height: "3" },
|
|
80
|
+
thumb: {
|
|
81
|
+
width: "3",
|
|
82
|
+
height: "3",
|
|
83
|
+
"&[data-selected]": { transform: "translateX(0.625rem)" },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
md: {
|
|
87
|
+
track: { width: "1.875rem", height: "4" },
|
|
88
|
+
thumb: {
|
|
89
|
+
width: "4",
|
|
90
|
+
height: "4",
|
|
91
|
+
"&[data-selected]": { transform: "translateX(0.875rem)" },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
lg: {
|
|
95
|
+
track: { width: "2.875rem", height: "6" },
|
|
96
|
+
thumb: {
|
|
97
|
+
width: "6",
|
|
98
|
+
height: "6",
|
|
99
|
+
"&[data-selected]": { transform: "translateX(1.375rem)" },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
defaultVariants: {
|
|
105
|
+
size: "md",
|
|
106
|
+
},
|
|
107
|
+
});
|