@microbit/ui 0.1.0-alpha.4 → 0.1.0-alpha.5
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 +32 -0
- package/README.md +20 -3
- package/lang/ui.de.json +22 -0
- package/lang/ui.ga-ie.json +22 -0
- package/lang/ui.zh-cn.json +22 -0
- package/package.json +3 -3
- package/src/Code.tsx +20 -0
- package/src/Collapse.tsx +168 -0
- package/src/Divider.tsx +39 -8
- package/src/Fade.tsx +48 -0
- package/src/IconButton.tsx +6 -1
- package/src/Kbd.tsx +26 -0
- package/src/List.tsx +7 -3
- package/src/Menu.recipe.ts +32 -1
- package/src/Menu.tsx +89 -0
- package/src/Modal.tsx +15 -1
- package/src/NumberField.recipe.ts +67 -0
- package/src/NumberField.tsx +86 -0
- package/src/PopoverArrow.tsx +18 -3
- package/src/Slider.tsx +64 -0
- package/src/TextField.tsx +17 -2
- package/src/Toast.tsx +7 -1
- package/src/base-preset.ts +5 -3
- package/src/{chakra-tokens.ts → base-tokens.ts} +7 -4
- package/src/hooks/useClipboard.ts +27 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/usePrevious.ts +18 -0
- package/src/index.ts +8 -0
package/LICENSE.md
CHANGED
|
@@ -19,3 +19,35 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Third-party notices
|
|
26
|
+
|
|
27
|
+
This package includes design tokens and component styling derived from
|
|
28
|
+
[Chakra UI](https://github.com/chakra-ui/chakra-ui) v2: the token scales in
|
|
29
|
+
`src/base-tokens.ts` were snapshotted from `@chakra-ui/theme`'s default
|
|
30
|
+
values, and the component recipes were ported from Chakra's component styles.
|
|
31
|
+
Chakra UI is used under the MIT License:
|
|
32
|
+
|
|
33
|
+
> MIT License
|
|
34
|
+
>
|
|
35
|
+
> Copyright (c) 2019 Segun Adebayo
|
|
36
|
+
>
|
|
37
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
38
|
+
> of this software and associated documentation files (the "Software"), to deal
|
|
39
|
+
> in the Software without restriction, including without limitation the rights
|
|
40
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
41
|
+
> copies of the Software, and to permit persons to whom the Software is
|
|
42
|
+
> furnished to do so, subject to the following conditions:
|
|
43
|
+
>
|
|
44
|
+
> The above copyright notice and this permission notice shall be included in all
|
|
45
|
+
> copies or substantial portions of the Software.
|
|
46
|
+
>
|
|
47
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
48
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
49
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
50
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
51
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
52
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
53
|
+
> SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# @microbit/ui
|
|
2
2
|
|
|
3
3
|
react-aria-components + Panda CSS primitives with a design language ported from
|
|
4
|
-
the Micro:bit Educational Foundation apps' original
|
|
4
|
+
the Micro:bit Educational Foundation apps' original
|
|
5
|
+
[Chakra UI](https://chakra-ui.com/) v2 themes (see
|
|
6
|
+
[Chakra UI heritage](#chakra-ui-heritage-and-license)).
|
|
5
7
|
|
|
6
8
|
The package **ships as source**: components import `styled-system/*`, which
|
|
7
9
|
each consumer generates with its own Panda preset stack. There is no build
|
|
@@ -154,8 +156,9 @@ the token is consumed.
|
|
|
154
156
|
## Runtime token lookups
|
|
155
157
|
|
|
156
158
|
For values that feed _computation_ rather than stylesheets (canvas painting,
|
|
157
|
-
colour math, inline `style` for data-driven values — see gotcha #9 in
|
|
158
|
-
|
|
159
|
+
colour math, inline `style` for data-driven values — see gotcha #9 in the
|
|
160
|
+
repo's [migration playbook](../../docs/migration-playbook.md)), import the
|
|
161
|
+
runtime lookup:
|
|
159
162
|
|
|
160
163
|
```ts
|
|
161
164
|
import { token } from "@microbit/ui"; // re-exports styled-system/tokens
|
|
@@ -188,3 +191,17 @@ Crowdin via the repo-root `npm run update-translations -- <path to extracted
|
|
|
188
191
|
Crowdin ZIP>` (config-driven over packages in
|
|
189
192
|
`bin/update-translations.cjs`), after which you run `npm run i18n:tidy`
|
|
190
193
|
from the root.
|
|
194
|
+
|
|
195
|
+
## Chakra UI heritage and license
|
|
196
|
+
|
|
197
|
+
This package's design language began as a faithful port of
|
|
198
|
+
[Chakra UI](https://chakra-ui.com/) v2's, done so the apps it serves could
|
|
199
|
+
leave Chakra without a redesign: `src/base-tokens.ts` was snapshotted from
|
|
200
|
+
`@chakra-ui/theme`'s default token scales, and the `*.recipe.ts` files were
|
|
201
|
+
ported from Chakra's component styles onto Panda config recipes.
|
|
202
|
+
However far it evolves from that starting point, it owes its foundations to
|
|
203
|
+
Chakra. Thanks to Segun Adebayo and the Chakra UI contributors.
|
|
204
|
+
|
|
205
|
+
The package is [MIT](LICENSE.md) © Micro:bit Educational Foundation and
|
|
206
|
+
contributors; Chakra UI is MIT © Segun Adebayo, and its notice is carried
|
|
207
|
+
in [LICENSE.md](LICENSE.md).
|
package/lang/ui.de.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ui.close-action": {
|
|
3
|
+
"defaultMessage": "Schließen",
|
|
4
|
+
"description": "Close button text or label"
|
|
5
|
+
},
|
|
6
|
+
"ui.toast-status-error": {
|
|
7
|
+
"defaultMessage": "Error",
|
|
8
|
+
"description": "Announced by screen readers before an error notification"
|
|
9
|
+
},
|
|
10
|
+
"ui.toast-status-info": {
|
|
11
|
+
"defaultMessage": "Information",
|
|
12
|
+
"description": "Announced by screen readers before an informational notification"
|
|
13
|
+
},
|
|
14
|
+
"ui.toast-status-success": {
|
|
15
|
+
"defaultMessage": "Success",
|
|
16
|
+
"description": "Announced by screen readers before a success notification"
|
|
17
|
+
},
|
|
18
|
+
"ui.toast-status-warning": {
|
|
19
|
+
"defaultMessage": "Warning",
|
|
20
|
+
"description": "Announced by screen readers before a warning notification"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ui.close-action": {
|
|
3
|
+
"defaultMessage": "Dún",
|
|
4
|
+
"description": "Close button text or label"
|
|
5
|
+
},
|
|
6
|
+
"ui.toast-status-error": {
|
|
7
|
+
"defaultMessage": "Error",
|
|
8
|
+
"description": "Announced by screen readers before an error notification"
|
|
9
|
+
},
|
|
10
|
+
"ui.toast-status-info": {
|
|
11
|
+
"defaultMessage": "Information",
|
|
12
|
+
"description": "Announced by screen readers before an informational notification"
|
|
13
|
+
},
|
|
14
|
+
"ui.toast-status-success": {
|
|
15
|
+
"defaultMessage": "Success",
|
|
16
|
+
"description": "Announced by screen readers before a success notification"
|
|
17
|
+
},
|
|
18
|
+
"ui.toast-status-warning": {
|
|
19
|
+
"defaultMessage": "Warning",
|
|
20
|
+
"description": "Announced by screen readers before a warning notification"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ui.close-action": {
|
|
3
|
+
"defaultMessage": "关闭",
|
|
4
|
+
"description": "Close button text or label"
|
|
5
|
+
},
|
|
6
|
+
"ui.toast-status-error": {
|
|
7
|
+
"defaultMessage": "Error",
|
|
8
|
+
"description": "Announced by screen readers before an error notification"
|
|
9
|
+
},
|
|
10
|
+
"ui.toast-status-info": {
|
|
11
|
+
"defaultMessage": "Information",
|
|
12
|
+
"description": "Announced by screen readers before an informational notification"
|
|
13
|
+
},
|
|
14
|
+
"ui.toast-status-success": {
|
|
15
|
+
"defaultMessage": "Success",
|
|
16
|
+
"description": "Announced by screen readers before a success notification"
|
|
17
|
+
},
|
|
18
|
+
"ui.toast-status-warning": {
|
|
19
|
+
"defaultMessage": "Warning",
|
|
20
|
+
"description": "Announced by screen readers before a warning notification"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microbit/ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "micro:bit design-system primitives: react-aria-components + Panda CSS with a Chakra v2
|
|
3
|
+
"version": "0.1.0-alpha.5",
|
|
4
|
+
"description": "micro:bit design-system primitives: react-aria-components + Panda CSS with a design language ported from Chakra UI v2. Ships as source; see README for the consumption setup.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.ts",
|
|
9
9
|
"./base-preset": "./src/base-preset.ts",
|
|
10
|
-
"./
|
|
10
|
+
"./base-tokens": "./src/base-tokens.ts",
|
|
11
11
|
"./messages": "./src/messages.ts",
|
|
12
12
|
"./postcss-legacy-safari": "./postcss-legacy-safari.cjs",
|
|
13
13
|
"./lang/*": "./lang/*"
|
package/src/Code.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
* Code — inline code chip matching Chakra's <Code> (gray subtle, light mode).
|
|
10
|
+
*/
|
|
11
|
+
export const Code = styled("code", {
|
|
12
|
+
base: {
|
|
13
|
+
fontFamily: "mono",
|
|
14
|
+
fontSize: "sm",
|
|
15
|
+
px: "0.2em",
|
|
16
|
+
borderRadius: "sm",
|
|
17
|
+
bg: "gray.100",
|
|
18
|
+
color: "gray.800",
|
|
19
|
+
},
|
|
20
|
+
});
|
package/src/Collapse.tsx
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
CSSProperties,
|
|
8
|
+
ReactNode,
|
|
9
|
+
useEffect,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from "react";
|
|
14
|
+
import { css, cx } from "styled-system/css";
|
|
15
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
16
|
+
|
|
17
|
+
export interface CollapseProps {
|
|
18
|
+
/** Expanded when true (Chakra's `in`). */
|
|
19
|
+
isOpen: boolean;
|
|
20
|
+
/** Height when collapsed (Chakra's `startingHeight`). */
|
|
21
|
+
startingHeight?: number | string;
|
|
22
|
+
/**
|
|
23
|
+
* Height when expanded (Chakra's `endingHeight`; defaults to the measured
|
|
24
|
+
* content height, tracked with a ResizeObserver so nested expansion works).
|
|
25
|
+
*/
|
|
26
|
+
endingHeight?: number | string;
|
|
27
|
+
/** Remove the content from the DOM once the exit transition finishes. */
|
|
28
|
+
unmountOnExit?: boolean;
|
|
29
|
+
css?: SystemStyleObject;
|
|
30
|
+
className?: string;
|
|
31
|
+
style?: CSSProperties;
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const toCssSize = (v: number | string) =>
|
|
36
|
+
typeof v === "number" ? `${v}px` : v;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Collapse — Chakra's Collapse transition without framer-motion: the content
|
|
40
|
+
* is measured and the wrapper's height (and opacity, when collapsing to zero)
|
|
41
|
+
* transitions between the collapsed and expanded sizes. Content stays mounted
|
|
42
|
+
* unless `unmountOnExit` is set.
|
|
43
|
+
*
|
|
44
|
+
* As with framer-motion, only `isOpen` changes animate: initial mounts render
|
|
45
|
+
* at rest, and height corrections (late measurement, content growth) snap —
|
|
46
|
+
* otherwise a collapse mounted inside an entering view morphs during the
|
|
47
|
+
* outer animation.
|
|
48
|
+
*/
|
|
49
|
+
export const Collapse = ({
|
|
50
|
+
isOpen,
|
|
51
|
+
startingHeight = 0,
|
|
52
|
+
endingHeight,
|
|
53
|
+
unmountOnExit = false,
|
|
54
|
+
css: cssProp,
|
|
55
|
+
className,
|
|
56
|
+
style,
|
|
57
|
+
children,
|
|
58
|
+
}: CollapseProps) => {
|
|
59
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
60
|
+
const [measuredHeight, setMeasuredHeight] = useState<number>();
|
|
61
|
+
// For unmountOnExit: stay in the DOM until the exit transition ends.
|
|
62
|
+
const [present, setPresent] = useState(isOpen);
|
|
63
|
+
// The state the DOM shows. It follows isOpen from a layout effect so the
|
|
64
|
+
// transition-enable and the height flip land in the same commit (a
|
|
65
|
+
// render-time flip detector is unreliable: React can discard renders,
|
|
66
|
+
// losing the state update while keeping ref mutations).
|
|
67
|
+
const [displayOpen, setDisplayOpen] = useState(isOpen);
|
|
68
|
+
const [animating, setAnimating] = useState(false);
|
|
69
|
+
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
if (isOpen === displayOpen) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (isOpen) {
|
|
75
|
+
// Opening: mount (collapsed) first, then flip on the next frame so
|
|
76
|
+
// the browser has a computed collapsed state to transition from.
|
|
77
|
+
setPresent(true);
|
|
78
|
+
const raf = requestAnimationFrame(() => {
|
|
79
|
+
setAnimating(true);
|
|
80
|
+
setDisplayOpen(true);
|
|
81
|
+
});
|
|
82
|
+
return () => cancelAnimationFrame(raf);
|
|
83
|
+
}
|
|
84
|
+
// Closing: enable the transition and flip together, pre-paint.
|
|
85
|
+
setAnimating(true);
|
|
86
|
+
setDisplayOpen(false);
|
|
87
|
+
}, [isOpen, displayOpen]);
|
|
88
|
+
|
|
89
|
+
useLayoutEffect(() => {
|
|
90
|
+
const el = contentRef.current;
|
|
91
|
+
if (!el || endingHeight !== undefined) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
setMeasuredHeight(el.scrollHeight);
|
|
95
|
+
const observer = new ResizeObserver(() =>
|
|
96
|
+
setMeasuredHeight(el.scrollHeight),
|
|
97
|
+
);
|
|
98
|
+
observer.observe(el);
|
|
99
|
+
return () => observer.disconnect();
|
|
100
|
+
}, [endingHeight, present]);
|
|
101
|
+
|
|
102
|
+
// Fallback for browsers/edge cases where transitionend doesn't fire.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (animating) {
|
|
105
|
+
const timeout = setTimeout(() => {
|
|
106
|
+
setAnimating(false);
|
|
107
|
+
if (!isOpen && unmountOnExit) {
|
|
108
|
+
setPresent(false);
|
|
109
|
+
}
|
|
110
|
+
}, 400);
|
|
111
|
+
return () => clearTimeout(timeout);
|
|
112
|
+
}
|
|
113
|
+
}, [animating, isOpen, unmountOnExit]);
|
|
114
|
+
|
|
115
|
+
if (unmountOnExit && !present) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const expanded =
|
|
120
|
+
endingHeight !== undefined
|
|
121
|
+
? toCssSize(endingHeight)
|
|
122
|
+
: measuredHeight !== undefined
|
|
123
|
+
? `${measuredHeight}px`
|
|
124
|
+
: "auto";
|
|
125
|
+
const collapsed = toCssSize(startingHeight);
|
|
126
|
+
const hideWhenCollapsed = collapsed === "0px" || collapsed === "0";
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
onTransitionEnd={(e) => {
|
|
131
|
+
if (e.propertyName === "height" && e.target === e.currentTarget) {
|
|
132
|
+
setAnimating(false);
|
|
133
|
+
if (!isOpen && unmountOnExit) {
|
|
134
|
+
setPresent(false);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
className={cx(
|
|
139
|
+
css(
|
|
140
|
+
{
|
|
141
|
+
overflow: "hidden",
|
|
142
|
+
display: "block",
|
|
143
|
+
transitionDuration: "0.25s",
|
|
144
|
+
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
145
|
+
_motionReduce: { transition: "none" },
|
|
146
|
+
},
|
|
147
|
+
cssProp,
|
|
148
|
+
),
|
|
149
|
+
className,
|
|
150
|
+
)}
|
|
151
|
+
// Runtime-measured/caller-supplied heights; transitions enabled only
|
|
152
|
+
// while an isOpen change is in flight.
|
|
153
|
+
style={{
|
|
154
|
+
transitionProperty: animating ? "height, opacity" : "none",
|
|
155
|
+
height: displayOpen ? expanded : collapsed,
|
|
156
|
+
opacity: displayOpen || !hideWhenCollapsed ? 1 : 0,
|
|
157
|
+
// At rest fully-collapsed, remove the content from the a11y tree
|
|
158
|
+
// and tab order, as Chakra did (kept visible while animating so
|
|
159
|
+
// the exit transition shows).
|
|
160
|
+
visibility:
|
|
161
|
+
displayOpen || animating || !hideWhenCollapsed ? undefined : "hidden",
|
|
162
|
+
...style,
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<div ref={contentRef}>{children}</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
};
|
package/src/Divider.tsx
CHANGED
|
@@ -3,19 +3,50 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
+
import { ComponentProps, forwardRef } from "react";
|
|
6
7
|
import { styled } from "styled-system/jsx";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
* Divider — a horizontal rule matching Chakra's <Divider> (hairline at 60%
|
|
10
|
-
* opacity; set `borderColor` to tint). Horizontal only.
|
|
11
|
-
*/
|
|
12
|
-
export const Divider = styled("hr", {
|
|
9
|
+
const StyledDivider = styled("hr", {
|
|
13
10
|
base: {
|
|
14
11
|
border: 0,
|
|
15
|
-
borderBottomWidth: "1px",
|
|
16
|
-
borderBottomStyle: "solid",
|
|
17
12
|
borderColor: "gray.200",
|
|
18
13
|
opacity: 0.6,
|
|
19
|
-
width: "100%",
|
|
20
14
|
},
|
|
15
|
+
variants: {
|
|
16
|
+
orientation: {
|
|
17
|
+
horizontal: {
|
|
18
|
+
borderBottomWidth: "1px",
|
|
19
|
+
borderBottomStyle: "solid",
|
|
20
|
+
width: "100%",
|
|
21
|
+
},
|
|
22
|
+
vertical: {
|
|
23
|
+
borderLeftWidth: "1px",
|
|
24
|
+
borderLeftStyle: "solid",
|
|
25
|
+
height: "100%",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: { orientation: "horizontal" },
|
|
21
30
|
});
|
|
31
|
+
|
|
32
|
+
export interface DividerProps extends ComponentProps<typeof StyledDivider> {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Divider — a hairline rule matching Chakra's <Divider> (60% opacity; set
|
|
36
|
+
* `borderColor` to tint). `orientation="vertical"` needs a height from the
|
|
37
|
+
* layout, e.g. a stretched flex row (Chakra's vertical divider likewise
|
|
38
|
+
* relied on `height: 100%`).
|
|
39
|
+
*/
|
|
40
|
+
export const Divider = forwardRef<HTMLHRElement, DividerProps>(
|
|
41
|
+
function Divider(props, ref) {
|
|
42
|
+
return (
|
|
43
|
+
<StyledDivider
|
|
44
|
+
ref={ref}
|
|
45
|
+
aria-orientation={
|
|
46
|
+
props.orientation === "vertical" ? "vertical" : "horizontal"
|
|
47
|
+
}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
package/src/Fade.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ReactNode } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
export interface FadeProps {
|
|
11
|
+
/** Visible when true; faded out (but mounted) when false. */
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
css?: SystemStyleObject;
|
|
14
|
+
className?: string;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fade — Chakra's Fade transition as a CSS opacity transition (see Slide for
|
|
20
|
+
* the pattern). Content stays mounted throughout.
|
|
21
|
+
*/
|
|
22
|
+
export const Fade = ({
|
|
23
|
+
isOpen,
|
|
24
|
+
css: cssProp,
|
|
25
|
+
className,
|
|
26
|
+
children,
|
|
27
|
+
}: FadeProps) => (
|
|
28
|
+
<div
|
|
29
|
+
data-open={isOpen ? "" : undefined}
|
|
30
|
+
className={cx(
|
|
31
|
+
css(
|
|
32
|
+
{
|
|
33
|
+
opacity: 0,
|
|
34
|
+
pointerEvents: "none",
|
|
35
|
+
"&[data-open]": { opacity: 1, pointerEvents: "auto" },
|
|
36
|
+
transitionProperty: "opacity",
|
|
37
|
+
transitionDuration: "0.2s",
|
|
38
|
+
transitionTimingFunction: "ease-out",
|
|
39
|
+
_motionReduce: { transition: "none" },
|
|
40
|
+
},
|
|
41
|
+
cssProp,
|
|
42
|
+
),
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
package/src/IconButton.tsx
CHANGED
|
@@ -10,7 +10,12 @@ export interface IconButtonProps
|
|
|
10
10
|
extends Omit<ButtonProps, "leftIcon" | "rightIcon"> {
|
|
11
11
|
/** Icon-only buttons have no visible label, so this is required. */
|
|
12
12
|
"aria-label": string;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Circular rather than the recipe's border-radius (Chakra's `isRound`).
|
|
15
|
+
* Usually a visual no-op here: the 2rem `button` radius already renders
|
|
16
|
+
* square icon buttons as circles. Kept for ported call sites and for
|
|
17
|
+
* variants/overrides with a smaller radius (e.g. `unstyled`).
|
|
18
|
+
*/
|
|
14
19
|
isRound?: boolean;
|
|
15
20
|
}
|
|
16
21
|
|
package/src/Kbd.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
* Kbd — keyboard-key chip matching Chakra's <Kbd> (light mode).
|
|
10
|
+
*/
|
|
11
|
+
export const Kbd = styled("kbd", {
|
|
12
|
+
base: {
|
|
13
|
+
bg: "gray.100",
|
|
14
|
+
borderRadius: "md",
|
|
15
|
+
borderWidth: "1px",
|
|
16
|
+
borderStyle: "solid",
|
|
17
|
+
borderColor: "gray.200",
|
|
18
|
+
borderBottomWidth: "3px",
|
|
19
|
+
fontFamily: "mono",
|
|
20
|
+
fontSize: "0.8em",
|
|
21
|
+
fontWeight: "bold",
|
|
22
|
+
lineHeight: "normal",
|
|
23
|
+
px: "0.4em",
|
|
24
|
+
whiteSpace: "nowrap",
|
|
25
|
+
},
|
|
26
|
+
});
|
package/src/List.tsx
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
import { styled } from "styled-system/jsx";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Unstyled list, matching Chakra's <List
|
|
10
|
-
*
|
|
9
|
+
* Unstyled list, matching Chakra's <List>. The marker removal is explicit —
|
|
10
|
+
* Chakra's List set listStyleType itself, and coexisting apps run without
|
|
11
|
+
* Panda's preflight (margins/padding are zeroed by whichever reset is
|
|
12
|
+
* active).
|
|
11
13
|
*/
|
|
12
|
-
export const List = styled("ul"
|
|
14
|
+
export const List = styled("ul", {
|
|
15
|
+
base: { listStyleType: "none" },
|
|
16
|
+
});
|
|
13
17
|
|
|
14
18
|
/** List item, matching Chakra's <ListItem>. */
|
|
15
19
|
export const ListItem = styled("li", {
|
package/src/Menu.recipe.ts
CHANGED
|
@@ -18,7 +18,17 @@ import { defineSlotRecipe } from "@pandacss/dev";
|
|
|
18
18
|
*/
|
|
19
19
|
export const menu = defineSlotRecipe({
|
|
20
20
|
className: "menu",
|
|
21
|
-
slots: [
|
|
21
|
+
slots: [
|
|
22
|
+
"content",
|
|
23
|
+
"list",
|
|
24
|
+
"item",
|
|
25
|
+
"icon",
|
|
26
|
+
"label",
|
|
27
|
+
"divider",
|
|
28
|
+
"group",
|
|
29
|
+
"groupTitle",
|
|
30
|
+
"itemIndicator",
|
|
31
|
+
],
|
|
22
32
|
base: {
|
|
23
33
|
content: {
|
|
24
34
|
bg: "white",
|
|
@@ -84,5 +94,26 @@ export const menu = defineSlotRecipe({
|
|
|
84
94
|
my: "2",
|
|
85
95
|
opacity: 0.6,
|
|
86
96
|
},
|
|
97
|
+
group: {},
|
|
98
|
+
// Chakra's MenuGroup/MenuOptionGroup title.
|
|
99
|
+
groupTitle: {
|
|
100
|
+
display: "block",
|
|
101
|
+
mx: "4",
|
|
102
|
+
my: "2",
|
|
103
|
+
fontWeight: "semibold",
|
|
104
|
+
fontSize: "sm",
|
|
105
|
+
},
|
|
106
|
+
// Check glyph slot for MenuItemOption: space always reserved (as Chakra
|
|
107
|
+
// does), visible only on the selected item (RAC sets data-selected on it).
|
|
108
|
+
itemIndicator: {
|
|
109
|
+
display: "inline-flex",
|
|
110
|
+
alignItems: "center",
|
|
111
|
+
justifyContent: "center",
|
|
112
|
+
flexShrink: 0,
|
|
113
|
+
marginEnd: "0.75rem",
|
|
114
|
+
fontSize: "0.8em",
|
|
115
|
+
opacity: 0,
|
|
116
|
+
"[data-selected] &": { opacity: 1 },
|
|
117
|
+
},
|
|
87
118
|
},
|
|
88
119
|
});
|
package/src/Menu.tsx
CHANGED
|
@@ -5,17 +5,21 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { ReactNode, useCallback, useState } from "react";
|
|
7
7
|
import {
|
|
8
|
+
Header as RACHeader,
|
|
8
9
|
Menu as RACMenu,
|
|
9
10
|
MenuItem as RACMenuItem,
|
|
10
11
|
MenuItemProps as RACMenuItemProps,
|
|
12
|
+
MenuSection as RACMenuSection,
|
|
11
13
|
MenuTrigger as RACMenuTrigger,
|
|
12
14
|
Popover,
|
|
13
15
|
PopoverProps,
|
|
14
16
|
Separator,
|
|
15
17
|
} from "react-aria-components";
|
|
18
|
+
import { RiCheckLine } from "react-icons/ri";
|
|
16
19
|
import { css, cx } from "styled-system/css";
|
|
17
20
|
import { menu } from "styled-system/recipes";
|
|
18
21
|
import { SystemStyleObject } from "styled-system/types";
|
|
22
|
+
import { Icon } from "./Icon";
|
|
19
23
|
import { useOverlayCloseRegistrar } from "./SharedUIProvider";
|
|
20
24
|
|
|
21
25
|
export interface MenuTriggerProps {
|
|
@@ -152,6 +156,91 @@ export const MenuItem = ({
|
|
|
152
156
|
);
|
|
153
157
|
};
|
|
154
158
|
|
|
159
|
+
export interface MenuOptionGroupProps {
|
|
160
|
+
/** Group heading shown above the options (Chakra's `title`). */
|
|
161
|
+
title?: ReactNode;
|
|
162
|
+
/** The selected `MenuItemOption`'s value (radio semantics). */
|
|
163
|
+
value?: string;
|
|
164
|
+
/** Called with the newly selected option's value. */
|
|
165
|
+
onChange?: (value: string) => void;
|
|
166
|
+
/** `MenuItemOption` children. */
|
|
167
|
+
children: ReactNode;
|
|
168
|
+
css?: SystemStyleObject;
|
|
169
|
+
className?: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* MenuOptionGroup — a single-select (radio) group of `MenuItemOption`s within
|
|
174
|
+
* a menu, replacing Chakra's `MenuOptionGroup type="radio"`. Selection is
|
|
175
|
+
* section-scoped (RAC MenuSection), so a menu can mix action items and option
|
|
176
|
+
* groups.
|
|
177
|
+
*/
|
|
178
|
+
export const MenuOptionGroup = ({
|
|
179
|
+
title,
|
|
180
|
+
value,
|
|
181
|
+
onChange,
|
|
182
|
+
children,
|
|
183
|
+
css: cssProp,
|
|
184
|
+
className,
|
|
185
|
+
}: MenuOptionGroupProps) => {
|
|
186
|
+
const slots = menu();
|
|
187
|
+
return (
|
|
188
|
+
<RACMenuSection
|
|
189
|
+
className={cx(slots.group, cssProp ? css(cssProp) : undefined, className)}
|
|
190
|
+
selectionMode="single"
|
|
191
|
+
selectedKeys={value != null ? [value] : []}
|
|
192
|
+
onSelectionChange={(keys) => {
|
|
193
|
+
if (keys !== "all") {
|
|
194
|
+
const key = keys.values().next().value;
|
|
195
|
+
if (key != null) {
|
|
196
|
+
onChange?.(String(key));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{title != null && (
|
|
202
|
+
<RACHeader className={slots.groupTitle}>{title}</RACHeader>
|
|
203
|
+
)}
|
|
204
|
+
{children}
|
|
205
|
+
</RACMenuSection>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export interface MenuItemOptionProps
|
|
210
|
+
extends Omit<RACMenuItemProps, "className" | "children" | "id" | "value"> {
|
|
211
|
+
/** This option's value within its `MenuOptionGroup`. */
|
|
212
|
+
value: string;
|
|
213
|
+
css?: SystemStyleObject;
|
|
214
|
+
className?: string;
|
|
215
|
+
children?: ReactNode;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* MenuItemOption — a selectable option inside a `MenuOptionGroup`, with a
|
|
220
|
+
* check indicator on the selected item (Chakra's MenuItemOption).
|
|
221
|
+
*/
|
|
222
|
+
export const MenuItemOption = ({
|
|
223
|
+
value,
|
|
224
|
+
css: cssProp,
|
|
225
|
+
className,
|
|
226
|
+
children,
|
|
227
|
+
...rest
|
|
228
|
+
}: MenuItemOptionProps) => {
|
|
229
|
+
const slots = menu();
|
|
230
|
+
return (
|
|
231
|
+
<RACMenuItem
|
|
232
|
+
id={value}
|
|
233
|
+
className={cx(slots.item, cssProp ? css(cssProp) : undefined, className)}
|
|
234
|
+
{...rest}
|
|
235
|
+
>
|
|
236
|
+
<span className={slots.itemIndicator} aria-hidden>
|
|
237
|
+
<Icon as={RiCheckLine} />
|
|
238
|
+
</span>
|
|
239
|
+
<span className={slots.label}>{children}</span>
|
|
240
|
+
</RACMenuItem>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
155
244
|
export interface MenuDividerProps {
|
|
156
245
|
css?: SystemStyleObject;
|
|
157
246
|
className?: string;
|
package/src/Modal.tsx
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
CSSProperties,
|
|
9
|
+
ReactNode,
|
|
10
|
+
RefObject,
|
|
11
|
+
useContext,
|
|
12
|
+
} from "react";
|
|
7
13
|
import {
|
|
8
14
|
Button as RACButton,
|
|
9
15
|
Dialog,
|
|
@@ -54,6 +60,12 @@ export interface ModalProps {
|
|
|
54
60
|
isKeyboardDismissDisabled?: boolean;
|
|
55
61
|
/** Style overrides for the dialog box (Chakra's ModalContent props). */
|
|
56
62
|
contentCss?: SystemStyleObject;
|
|
63
|
+
/**
|
|
64
|
+
* Inline styles for the dialog box, for runtime-computed positioning that
|
|
65
|
+
* Panda can't statically extract (e.g. a dialog aligned to a measured
|
|
66
|
+
* element). Prefer `contentCss` for static styles.
|
|
67
|
+
*/
|
|
68
|
+
contentStyle?: CSSProperties;
|
|
57
69
|
/**
|
|
58
70
|
* Style overrides for the backdrop (Chakra's ModalOverlay props), e.g. a
|
|
59
71
|
* transparent backdrop when something else provides the dimming.
|
|
@@ -99,6 +111,7 @@ export const Modal = ({
|
|
|
99
111
|
motionless,
|
|
100
112
|
isKeyboardDismissDisabled,
|
|
101
113
|
contentCss,
|
|
114
|
+
contentStyle,
|
|
102
115
|
overlayCss,
|
|
103
116
|
role,
|
|
104
117
|
isCentered,
|
|
@@ -141,6 +154,7 @@ export const Modal = ({
|
|
|
141
154
|
>
|
|
142
155
|
<UnmountCallback callback={handleUnmount} />
|
|
143
156
|
<RACModal
|
|
157
|
+
style={contentStyle}
|
|
144
158
|
className={cx(
|
|
145
159
|
slots.content,
|
|
146
160
|
motionlessClass,
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
* NumberField slot recipe — Chakra's NumberInput look: an outline input (the
|
|
10
|
+
* `input` recipe styles the input itself) with a right-hand stepper column of
|
|
11
|
+
* two stacked buttons. Consumed by the shared-ui NumberField
|
|
12
|
+
* (react-aria-components NumberField).
|
|
13
|
+
*
|
|
14
|
+
* Registered in the base preset (base-preset.ts). No variants, so it needs
|
|
15
|
+
* no `staticCss` entry.
|
|
16
|
+
*/
|
|
17
|
+
export const numberField = defineSlotRecipe({
|
|
18
|
+
className: "numberField",
|
|
19
|
+
slots: ["root", "group", "stepper", "stepperButton"],
|
|
20
|
+
base: {
|
|
21
|
+
root: {
|
|
22
|
+
display: "flex",
|
|
23
|
+
flexDirection: "column",
|
|
24
|
+
alignItems: "stretch",
|
|
25
|
+
},
|
|
26
|
+
group: {
|
|
27
|
+
position: "relative",
|
|
28
|
+
zIndex: 0,
|
|
29
|
+
},
|
|
30
|
+
// Chakra's NumberInputStepper: a column overlaying the input's right
|
|
31
|
+
// edge, inset by the input border.
|
|
32
|
+
stepper: {
|
|
33
|
+
display: "flex",
|
|
34
|
+
flexDirection: "column",
|
|
35
|
+
position: "absolute",
|
|
36
|
+
insetEnd: "0",
|
|
37
|
+
top: "0",
|
|
38
|
+
height: "calc(100% - 2px)",
|
|
39
|
+
margin: "1px",
|
|
40
|
+
width: "6",
|
|
41
|
+
zIndex: 1,
|
|
42
|
+
},
|
|
43
|
+
stepperButton: {
|
|
44
|
+
display: "flex",
|
|
45
|
+
alignItems: "center",
|
|
46
|
+
justifyContent: "center",
|
|
47
|
+
flex: 1,
|
|
48
|
+
cursor: "pointer",
|
|
49
|
+
lineHeight: "normal",
|
|
50
|
+
fontSize: "xs",
|
|
51
|
+
color: "inherit",
|
|
52
|
+
bg: "transparent",
|
|
53
|
+
borderStart: "1px solid",
|
|
54
|
+
borderColor: "gray.200",
|
|
55
|
+
transitionProperty: "background",
|
|
56
|
+
transitionDuration: "ultra-fast",
|
|
57
|
+
"&:last-child": {
|
|
58
|
+
borderTop: "1px solid",
|
|
59
|
+
borderTopColor: "gray.200",
|
|
60
|
+
marginTop: "-1px",
|
|
61
|
+
},
|
|
62
|
+
"&[data-hovered]": { bg: "gray.100" },
|
|
63
|
+
"&[data-pressed]": { bg: "gray.200" },
|
|
64
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { forwardRef, ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Button as RACButton,
|
|
9
|
+
Group as RACGroup,
|
|
10
|
+
Input as RACInput,
|
|
11
|
+
Label as RACLabel,
|
|
12
|
+
NumberField as RACNumberField,
|
|
13
|
+
NumberFieldProps as RACNumberFieldProps,
|
|
14
|
+
} from "react-aria-components";
|
|
15
|
+
import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
|
|
16
|
+
import { css, cx } from "styled-system/css";
|
|
17
|
+
import { field, input, numberField } from "styled-system/recipes";
|
|
18
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
19
|
+
import { Icon } from "./Icon";
|
|
20
|
+
|
|
21
|
+
export interface NumberFieldProps
|
|
22
|
+
extends Omit<RACNumberFieldProps, "className" | "children" | "style"> {
|
|
23
|
+
/** Visible label (optional; otherwise pass `aria-label`). */
|
|
24
|
+
label?: ReactNode;
|
|
25
|
+
/** Root style overrides (e.g. row layout for label-beside-field forms). */
|
|
26
|
+
css?: SystemStyleObject;
|
|
27
|
+
/** Label style overrides. */
|
|
28
|
+
labelCss?: SystemStyleObject;
|
|
29
|
+
/** Group (input + steppers) style overrides — set `width` here. */
|
|
30
|
+
groupCss?: SystemStyleObject;
|
|
31
|
+
/** Input style overrides (e.g. a smaller size than the recipe's md). */
|
|
32
|
+
inputCss?: SystemStyleObject;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* NumberField — react-aria-components <NumberField> styled like Chakra's
|
|
37
|
+
* NumberInput (outline input + right-hand stepper column). The ref is
|
|
38
|
+
* forwarded to the input element. Value clamping to min/maxValue is
|
|
39
|
+
* handled by react-aria; onChange receives NaN when the field is emptied.
|
|
40
|
+
*/
|
|
41
|
+
export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
42
|
+
function NumberField(
|
|
43
|
+
{ label, css: cssProp, labelCss, groupCss, inputCss, ...rest },
|
|
44
|
+
ref,
|
|
45
|
+
) {
|
|
46
|
+
const slots = numberField();
|
|
47
|
+
const fieldSlots = field();
|
|
48
|
+
return (
|
|
49
|
+
<RACNumberField
|
|
50
|
+
{...rest}
|
|
51
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined)}
|
|
52
|
+
>
|
|
53
|
+
{label != null && (
|
|
54
|
+
<RACLabel
|
|
55
|
+
className={cx(
|
|
56
|
+
fieldSlots.label,
|
|
57
|
+
labelCss ? css(labelCss) : undefined,
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
{label}
|
|
61
|
+
</RACLabel>
|
|
62
|
+
)}
|
|
63
|
+
<RACGroup
|
|
64
|
+
className={cx(slots.group, groupCss ? css(groupCss) : undefined)}
|
|
65
|
+
>
|
|
66
|
+
<RACInput
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={cx(
|
|
69
|
+
input(),
|
|
70
|
+
// Room for the stepper column.
|
|
71
|
+
css({ paddingEnd: "6" }, inputCss),
|
|
72
|
+
)}
|
|
73
|
+
/>
|
|
74
|
+
<div className={slots.stepper}>
|
|
75
|
+
<RACButton slot="increment" className={slots.stepperButton}>
|
|
76
|
+
<Icon as={RiArrowUpSFill} />
|
|
77
|
+
</RACButton>
|
|
78
|
+
<RACButton slot="decrement" className={slots.stepperButton}>
|
|
79
|
+
<Icon as={RiArrowDownSFill} />
|
|
80
|
+
</RACButton>
|
|
81
|
+
</div>
|
|
82
|
+
</RACGroup>
|
|
83
|
+
</RACNumberField>
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
);
|
package/src/PopoverArrow.tsx
CHANGED
|
@@ -13,11 +13,26 @@ import { SystemStyleObject } from "styled-system/types";
|
|
|
13
13
|
// rotating about the centre keeps it flush against the overlay on every side
|
|
14
14
|
// — no translate fix-ups, whose signs depend on rotation direction and are
|
|
15
15
|
// easy to get wrong (they detached the tooltip arrow from its box).
|
|
16
|
+
//
|
|
17
|
+
// "Flush" still antialiases to a hairline seam when the overlay's edge lands
|
|
18
|
+
// on a subpixel boundary, so each placement also pulls the svg 1px into the
|
|
19
|
+
// overlay with a negative margin on the overlay-facing side (margins move the
|
|
20
|
+
// box before the rotate, so the bleed direction is unaffected by it).
|
|
16
21
|
const arrowBase = css({
|
|
17
22
|
"& svg": { display: "block" },
|
|
18
|
-
"&[data-placement='
|
|
19
|
-
"&[data-placement='
|
|
20
|
-
|
|
23
|
+
"&[data-placement='top'] svg": { marginTop: "-1px" },
|
|
24
|
+
"&[data-placement='bottom'] svg": {
|
|
25
|
+
transform: "rotate(180deg)",
|
|
26
|
+
marginBottom: "-1px",
|
|
27
|
+
},
|
|
28
|
+
"&[data-placement='right'] svg": {
|
|
29
|
+
transform: "rotate(90deg)",
|
|
30
|
+
marginRight: "-1px",
|
|
31
|
+
},
|
|
32
|
+
"&[data-placement='left'] svg": {
|
|
33
|
+
transform: "rotate(-90deg)",
|
|
34
|
+
marginLeft: "-1px",
|
|
35
|
+
},
|
|
21
36
|
});
|
|
22
37
|
|
|
23
38
|
export interface PopoverArrowProps {
|
package/src/Slider.tsx
CHANGED
|
@@ -37,6 +37,23 @@ export interface SliderProps {
|
|
|
37
37
|
*/
|
|
38
38
|
mark?: ReactNode;
|
|
39
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;
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
/**
|
|
@@ -57,6 +74,10 @@ export const Slider = ({
|
|
|
57
74
|
thumbCss,
|
|
58
75
|
mark,
|
|
59
76
|
markCss,
|
|
77
|
+
children,
|
|
78
|
+
thumbTooltip,
|
|
79
|
+
isThumbTooltipOpen,
|
|
80
|
+
onThumbFocusChange,
|
|
60
81
|
}: SliderProps) => {
|
|
61
82
|
const slots = slider();
|
|
62
83
|
const percent = ((value - minValue) / (maxValue - minValue)) * 100;
|
|
@@ -91,9 +112,52 @@ export const Slider = ({
|
|
|
91
112
|
style={{ width: `${percent}%` }}
|
|
92
113
|
/>
|
|
93
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% + 14px)",
|
|
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
|
+
)}
|
|
94
156
|
<SliderThumb
|
|
157
|
+
onFocusChange={onThumbFocusChange}
|
|
95
158
|
className={cx(slots.thumb, thumbCss ? css(thumbCss) : undefined)}
|
|
96
159
|
/>
|
|
160
|
+
{children}
|
|
97
161
|
</RACSlider>
|
|
98
162
|
);
|
|
99
163
|
};
|
package/src/TextField.tsx
CHANGED
|
@@ -30,6 +30,8 @@ export interface TextFieldProps
|
|
|
30
30
|
/** Per-instance style overrides for the helper text. */
|
|
31
31
|
helperTextCss?: SystemStyleObject;
|
|
32
32
|
onFocus?: (e: FocusEvent<HTMLInputElement>) => void;
|
|
33
|
+
/** Input autocapitalize attribute (react-aria's TextField omits it). */
|
|
34
|
+
autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters";
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -39,7 +41,15 @@ export interface TextFieldProps
|
|
|
39
41
|
*/
|
|
40
42
|
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
41
43
|
function TextField(
|
|
42
|
-
{
|
|
44
|
+
{
|
|
45
|
+
label,
|
|
46
|
+
helperText,
|
|
47
|
+
errorMessage,
|
|
48
|
+
helperTextCss,
|
|
49
|
+
onFocus,
|
|
50
|
+
autoCapitalize,
|
|
51
|
+
...rest
|
|
52
|
+
},
|
|
43
53
|
ref,
|
|
44
54
|
) {
|
|
45
55
|
const slots = field();
|
|
@@ -53,7 +63,12 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
53
63
|
</span>
|
|
54
64
|
) : null}
|
|
55
65
|
</RACLabel>
|
|
56
|
-
<RACInput
|
|
66
|
+
<RACInput
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={input()}
|
|
69
|
+
onFocus={onFocus}
|
|
70
|
+
autoCapitalize={autoCapitalize}
|
|
71
|
+
/>
|
|
57
72
|
{helperText && (
|
|
58
73
|
<RACText
|
|
59
74
|
slot="description"
|
package/src/Toast.tsx
CHANGED
|
@@ -118,6 +118,8 @@ export interface ToastFn {
|
|
|
118
118
|
* re-added, so unlike Chakra it re-animates and restarts any timeout.
|
|
119
119
|
*/
|
|
120
120
|
update(id: string, options: ToastOptions): void;
|
|
121
|
+
/** Dismiss all visible toasts (Chakra's toast.closeAll). */
|
|
122
|
+
closeAll(): void;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
/**
|
|
@@ -161,5 +163,9 @@ export const useToast = (): ToastFn =>
|
|
|
161
163
|
}
|
|
162
164
|
add({ ...options, id });
|
|
163
165
|
};
|
|
164
|
-
|
|
166
|
+
const closeAll = () => {
|
|
167
|
+
// Copy first: closing mutates visibleToasts as we iterate.
|
|
168
|
+
[...toastQueue.visibleToasts].forEach((t) => toastQueue.close(t.key));
|
|
169
|
+
};
|
|
170
|
+
return Object.assign(add, { isActive, update, closeAll });
|
|
165
171
|
}, []);
|
package/src/base-preset.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
sizes,
|
|
21
21
|
spacing,
|
|
22
22
|
zIndex,
|
|
23
|
-
} from "./
|
|
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
26
|
import { button } from "./Button.recipe";
|
|
@@ -29,6 +29,7 @@ import { checkbox } from "./Checkbox.recipe";
|
|
|
29
29
|
import { drawer } from "./Drawer.recipe";
|
|
30
30
|
import { heading } from "./Heading.recipe";
|
|
31
31
|
import { input } from "./Input.recipe";
|
|
32
|
+
import { numberField } from "./NumberField.recipe";
|
|
32
33
|
import { menu } from "./Menu.recipe";
|
|
33
34
|
import { slider } from "./Slider.recipe";
|
|
34
35
|
import { switchRecipe } from "./Switch.recipe";
|
|
@@ -37,8 +38,8 @@ import { field } from "./TextField.recipe";
|
|
|
37
38
|
import { toast } from "./Toast.recipe";
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
* The base preset: the complete, working micro:bit design system.
|
|
41
|
-
*
|
|
41
|
+
* The base preset: the complete, working micro:bit design system. The base
|
|
42
|
+
* token scales (base-tokens.ts), the micro:bit house style
|
|
42
43
|
* (pill `radii.button`, `outline*` focus shadows, Helvetica fonts, the
|
|
43
44
|
* `language`/`toolbar` button variants in Button.recipe.ts, the
|
|
44
45
|
* `languageText`/`toast*Bg`/`statusBarBg` semantic tokens), the shared-ui
|
|
@@ -175,6 +176,7 @@ export const basePreset = definePreset({
|
|
|
175
176
|
drawer,
|
|
176
177
|
field,
|
|
177
178
|
menu,
|
|
179
|
+
numberField,
|
|
178
180
|
slider,
|
|
179
181
|
switchRecipe,
|
|
180
182
|
toast,
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
3
|
*
|
|
4
|
+
* Token values (c) 2019, Segun Adebayo — Chakra UI's @chakra-ui/theme
|
|
5
|
+
* defaults, used under the MIT License (see LICENSE.md third-party notices).
|
|
6
|
+
*
|
|
4
7
|
* SPDX-License-Identifier: MIT
|
|
5
8
|
*/
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
9
|
+
// The base token scales, in Panda token format. Began as a mechanical
|
|
10
|
+
// snapshot of Chakra UI v2's @chakra-ui/theme defaults; hand-maintained.
|
|
11
|
+
// Deliberately absent: `fonts` (foundation-owned, defined in base-preset.ts)
|
|
12
|
+
// and `transition.property` (no Panda token category; inlined at use sites).
|
|
10
13
|
|
|
11
14
|
export const colors = {
|
|
12
15
|
transparent: {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Copy a value to the clipboard with a transient `hasCopied` flag for
|
|
10
|
+
* "Copied!" feedback. Replaces Chakra's `useClipboard`.
|
|
11
|
+
*/
|
|
12
|
+
export function useClipboard(
|
|
13
|
+
value: string,
|
|
14
|
+
timeoutMs = 1500,
|
|
15
|
+
): { onCopy: () => void; hasCopied: boolean } {
|
|
16
|
+
const [hasCopied, setHasCopied] = useState(false);
|
|
17
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
18
|
+
const onCopy = useCallback(() => {
|
|
19
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
20
|
+
setHasCopied(true);
|
|
21
|
+
clearTimeout(timeoutRef.current);
|
|
22
|
+
timeoutRef.current = setTimeout(() => setHasCopied(false), timeoutMs);
|
|
23
|
+
});
|
|
24
|
+
}, [value, timeoutMs]);
|
|
25
|
+
useEffect(() => () => clearTimeout(timeoutRef.current), []);
|
|
26
|
+
return { onCopy, hasCopied };
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tracks a raw CSS media query, replacing Chakra's `useMediaQuery` for
|
|
10
|
+
* queries that aren't breakpoint-based (custom widths, height-based
|
|
11
|
+
* queries). For the preset's breakpoint scale prefer `useBreakpointValue`.
|
|
12
|
+
* Returns false during SSR.
|
|
13
|
+
*/
|
|
14
|
+
export function useMediaQuery(query: string): boolean {
|
|
15
|
+
const subscribe = useCallback(
|
|
16
|
+
(onChange: () => void) => {
|
|
17
|
+
const list = window.matchMedia(query);
|
|
18
|
+
list.addEventListener("change", onChange);
|
|
19
|
+
return () => list.removeEventListener("change", onChange);
|
|
20
|
+
},
|
|
21
|
+
[query],
|
|
22
|
+
);
|
|
23
|
+
return useSyncExternalStore(
|
|
24
|
+
subscribe,
|
|
25
|
+
() => window.matchMedia(query).matches,
|
|
26
|
+
() => false,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { useEffect, useRef } from "react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The value from the previous render (undefined on the first render).
|
|
10
|
+
* Replaces Chakra's `usePrevious`.
|
|
11
|
+
*/
|
|
12
|
+
export function usePrevious<T>(value: T): T | undefined {
|
|
13
|
+
const ref = useRef<T>();
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
ref.current = value;
|
|
16
|
+
});
|
|
17
|
+
return ref.current;
|
|
18
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from "./Input";
|
|
|
19
19
|
export * from "./InputGroup";
|
|
20
20
|
export * from "./LinkBox";
|
|
21
21
|
export * from "./NativeSelect";
|
|
22
|
+
export * from "./NumberField";
|
|
22
23
|
export * from "./ProgressBar";
|
|
23
24
|
export * from "./Slide";
|
|
24
25
|
export * from "./Slider";
|
|
@@ -32,6 +33,10 @@ export * from "./Link";
|
|
|
32
33
|
export * from "./Icon";
|
|
33
34
|
export * from "./CloseButton";
|
|
34
35
|
export * from "./CloseIcon";
|
|
36
|
+
export * from "./Code";
|
|
37
|
+
export * from "./Collapse";
|
|
38
|
+
export * from "./Fade";
|
|
39
|
+
export * from "./Kbd";
|
|
35
40
|
export * from "./Divider";
|
|
36
41
|
export * from "./Drawer";
|
|
37
42
|
export * from "./List";
|
|
@@ -43,3 +48,6 @@ export * from "./Tooltip";
|
|
|
43
48
|
export * from "./Toast";
|
|
44
49
|
export * from "./VisuallyHidden";
|
|
45
50
|
export { useBreakpointValue } from "./hooks/useBreakpointValue";
|
|
51
|
+
export { useClipboard } from "./hooks/useClipboard";
|
|
52
|
+
export { useMediaQuery } from "./hooks/useMediaQuery";
|
|
53
|
+
export { usePrevious } from "./hooks/usePrevious";
|