@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
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@microbit/ui",
|
|
3
|
+
"version": "0.1.0-alpha.10",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./base-preset": "./src/base-preset.ts",
|
|
10
|
+
"./base-tokens": "./src/base-tokens.ts",
|
|
11
|
+
"./messages": "./src/messages.ts",
|
|
12
|
+
"./postcss-legacy-safari": "./postcss-legacy-safari.cjs",
|
|
13
|
+
"./reset.css": "./reset.css",
|
|
14
|
+
"./lang/*": "./lang/*"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"lang",
|
|
19
|
+
"postcss-legacy-safari.cjs",
|
|
20
|
+
"reset.css",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE.md"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"panda": "panda codegen",
|
|
27
|
+
"typecheck": "npm run panda && tsc --noEmit",
|
|
28
|
+
"prestorybook": "npm run panda",
|
|
29
|
+
"storybook": "storybook dev -p 6006 --no-open",
|
|
30
|
+
"prebuild-storybook": "npm run panda",
|
|
31
|
+
"build-storybook": "storybook build"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@pandacss/dev": "^1.11.4",
|
|
35
|
+
"react": "^18.3.1",
|
|
36
|
+
"react-aria-components": "^1.19.0",
|
|
37
|
+
"react-icons": "^4.12.0 || ^5.0.0",
|
|
38
|
+
"react-intl": "^6.6.8 || ^7.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@pandacss/dev": "^1.11.4",
|
|
42
|
+
"@storybook/react-vite": "^10.5.2",
|
|
43
|
+
"@types/react": "^18.3.3",
|
|
44
|
+
"@types/react-dom": "^18.3.0",
|
|
45
|
+
"@vitejs/plugin-react": "^4.5.2",
|
|
46
|
+
"react": "^18.3.1",
|
|
47
|
+
"react-aria-components": "^1.19.0",
|
|
48
|
+
"react-dom": "^18.3.1",
|
|
49
|
+
"react-icons": "^4.12.0",
|
|
50
|
+
"react-intl": "^6.6.8",
|
|
51
|
+
"storybook": "^10.5.2",
|
|
52
|
+
"typescript": "^5.4.2",
|
|
53
|
+
"vite": "^6.3.5"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"tag": "alpha"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* TEMPORARY compatibility shim for consuming apps that still support Safari
|
|
7
|
+
* below 15. Delete this file (and the app-side wiring — see the README) once
|
|
8
|
+
* every consuming app has raised its support floor past the affected browsers.
|
|
9
|
+
*
|
|
10
|
+
* The bug: Safari 14.x silently drops logical *shorthands* whose value
|
|
11
|
+
* contains var() — `padding-inline: var(--spacing-2)` applies nothing, even
|
|
12
|
+
* though `padding-inline: 10px` (literal) and `padding-inline-start:
|
|
13
|
+
* var(--spacing-2)` (longhand) both work. Panda emits these shorthands for its
|
|
14
|
+
* px/py/mx/my utilities, so the bug removes most token-based spacing.
|
|
15
|
+
*
|
|
16
|
+
* The fix: rewrite the inline/block logical shorthands into their -start/-end
|
|
17
|
+
* longhands. Kept logical (not physical left/right) so RTL still flips.
|
|
18
|
+
*
|
|
19
|
+
* This is one of TWO legacy concerns for the same era of browsers; the other
|
|
20
|
+
* is @layer, which those browsers drop wholesale. Apps also run
|
|
21
|
+
* @csstools/postcss-cascade-layers (de-layering) and pin build.cssTarget — see
|
|
22
|
+
* the "Legacy browser support" section of the README. All of it is expected to
|
|
23
|
+
* be removed together when the floor rises.
|
|
24
|
+
*
|
|
25
|
+
* Exposed as a PostCSS plugin factory:
|
|
26
|
+
* const { expandLogicalShorthands } = require("@microbit/ui/postcss-legacy-safari");
|
|
27
|
+
* module.exports = { plugins: [expandLogicalShorthands(), ...] };
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Logical shorthands whose value is `<start> <end>` (one value applies to
|
|
31
|
+
// both). NOT included: border-inline / border-block — those are compound
|
|
32
|
+
// (`width style color`) and duplicate the whole value to each side rather than
|
|
33
|
+
// splitting, so they would need different handling. Add them explicitly if a
|
|
34
|
+
// consuming app ever emits them.
|
|
35
|
+
const LOGICAL_SHORTHANDS = {
|
|
36
|
+
"padding-inline": ["padding-inline-start", "padding-inline-end"],
|
|
37
|
+
"padding-block": ["padding-block-start", "padding-block-end"],
|
|
38
|
+
"margin-inline": ["margin-inline-start", "margin-inline-end"],
|
|
39
|
+
"margin-block": ["margin-block-start", "margin-block-end"],
|
|
40
|
+
"inset-inline": ["inset-inline-start", "inset-inline-end"],
|
|
41
|
+
"inset-block": ["inset-block-start", "inset-block-end"],
|
|
42
|
+
"scroll-margin-inline": [
|
|
43
|
+
"scroll-margin-inline-start",
|
|
44
|
+
"scroll-margin-inline-end",
|
|
45
|
+
],
|
|
46
|
+
"scroll-margin-block": [
|
|
47
|
+
"scroll-margin-block-start",
|
|
48
|
+
"scroll-margin-block-end",
|
|
49
|
+
],
|
|
50
|
+
"scroll-padding-inline": [
|
|
51
|
+
"scroll-padding-inline-start",
|
|
52
|
+
"scroll-padding-inline-end",
|
|
53
|
+
],
|
|
54
|
+
"scroll-padding-block": [
|
|
55
|
+
"scroll-padding-block-start",
|
|
56
|
+
"scroll-padding-block-end",
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Split a value on top-level whitespace, ignoring spaces inside parens so
|
|
61
|
+
// var() fallbacks stay intact. One value applies to both sides; two map to
|
|
62
|
+
// start then end (per the CSS shorthand rules).
|
|
63
|
+
const splitTopLevel = (value) => {
|
|
64
|
+
const parts = [];
|
|
65
|
+
let depth = 0;
|
|
66
|
+
let current = "";
|
|
67
|
+
for (const ch of value) {
|
|
68
|
+
if (ch === "(") depth++;
|
|
69
|
+
else if (ch === ")") depth--;
|
|
70
|
+
if (depth === 0 && /\s/.test(ch)) {
|
|
71
|
+
if (current.trim()) parts.push(current.trim());
|
|
72
|
+
current = "";
|
|
73
|
+
} else {
|
|
74
|
+
current += ch;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (current.trim()) parts.push(current.trim());
|
|
78
|
+
return parts;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const expandLogicalShorthands = () => ({
|
|
82
|
+
postcssPlugin: "microbit-ui-expand-logical-shorthands",
|
|
83
|
+
Declaration(decl) {
|
|
84
|
+
const longhands = LOGICAL_SHORTHANDS[decl.prop.toLowerCase()];
|
|
85
|
+
if (!longhands) return;
|
|
86
|
+
const parts = splitTopLevel(decl.value);
|
|
87
|
+
if (parts.length === 0) return;
|
|
88
|
+
const [start, end = start] = parts;
|
|
89
|
+
decl.cloneBefore({ prop: longhands[0], value: start });
|
|
90
|
+
decl.cloneBefore({ prop: longhands[1], value: end });
|
|
91
|
+
decl.remove();
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
expandLogicalShorthands.postcss = true;
|
|
95
|
+
|
|
96
|
+
module.exports = { expandLogicalShorthands };
|
package/reset.css
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*
|
|
6
|
+
* Chakra-reset parity that must live in the `reset` cascade layer: import
|
|
7
|
+
* from the app's layers.css with
|
|
8
|
+
*
|
|
9
|
+
* @import "@microbit/ui/reset.css" layer(reset);
|
|
10
|
+
*
|
|
11
|
+
* Deliberately not in the preset's globalCss: globalCss emits into the
|
|
12
|
+
* `base` layer, and the legacy-Safari cascade-layer flattening
|
|
13
|
+
* specificity-boosts base+ rules above CSS it can't see (runtime-injected
|
|
14
|
+
* styles such as CodeMirror themes, and other per-file-processed app CSS).
|
|
15
|
+
* A reset must stay at the bottom of the cascade in production exactly as
|
|
16
|
+
* it is in dev — see playbook gotcha #28.
|
|
17
|
+
*/
|
|
18
|
+
*,
|
|
19
|
+
*::before,
|
|
20
|
+
*::after {
|
|
21
|
+
border-color: var(--colors-gray-200);
|
|
22
|
+
word-wrap: break-word;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* Panda's preflight (base layer) beats the rule above with
|
|
27
|
+
* `border-color: var(--global-color-border, currentcolor)`. The currentcolor
|
|
28
|
+
* fallback poisons `border-color: inherit` chains (inherit copies the
|
|
29
|
+
* currentcolor KEYWORD, which then resolves against the child's own `color`
|
|
30
|
+
* — e.g. white on the checkbox/radio control). Defining the hook keeps the
|
|
31
|
+
* default border colour a real colour, as Chakra's global reset did.
|
|
32
|
+
*/
|
|
33
|
+
:root {
|
|
34
|
+
--global-color-border: var(--colors-gray-200);
|
|
35
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { defineRecipe } from "@pandacss/dev";
|
|
7
|
+
|
|
8
|
+
// Chakra's transition.property.common, inlined (Panda has no transitionProperty
|
|
9
|
+
// token category). Matches Chakra's Button base transition.
|
|
10
|
+
const transitionCommon =
|
|
11
|
+
"background-color, border-color, color, fill, stroke, opacity, box-shadow, transform";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Button recipe — Chakra's default Button base + sizes, with this app's
|
|
15
|
+
* borderRadius (`button` = 2rem) and variant set ported from
|
|
16
|
+
* `components/button.ts`. Interaction conditions (`_hover`/`_active`/
|
|
17
|
+
* `_disabled`/`_focusVisible`) are widened in the preset to also match
|
|
18
|
+
* react-aria-components' data attributes, so these Chakra-shaped variant
|
|
19
|
+
* objects work unchanged on RAC's <Button>.
|
|
20
|
+
*
|
|
21
|
+
* A config recipe (not a component cva): styles land in the `recipes` layer so
|
|
22
|
+
* call sites can override with plain style props, and presets extend the
|
|
23
|
+
* variants. This file holds the brand-independent variant set — the core
|
|
24
|
+
* Chakra variants plus the family-wide `language`/`toolbar` variants; a
|
|
25
|
+
* consuming app's preset extends it with app vocabulary (e.g. ml-trainer's
|
|
26
|
+
* `led`/`record*`/`secondary-disabled`). Brand divergence within a variant is
|
|
27
|
+
* token-driven (see the `languageText` semantic tokens).
|
|
28
|
+
*
|
|
29
|
+
* Registered in the base preset (base-preset.ts).
|
|
30
|
+
*/
|
|
31
|
+
export const button = defineRecipe({
|
|
32
|
+
className: "btn",
|
|
33
|
+
jsx: ["Button", "IconButton"],
|
|
34
|
+
base: {
|
|
35
|
+
lineHeight: "1.2",
|
|
36
|
+
borderRadius: "button",
|
|
37
|
+
fontWeight: "semibold",
|
|
38
|
+
transitionProperty: transitionCommon,
|
|
39
|
+
transitionDuration: "normal",
|
|
40
|
+
display: "inline-flex",
|
|
41
|
+
appearance: "none",
|
|
42
|
+
alignItems: "center",
|
|
43
|
+
justifyContent: "center",
|
|
44
|
+
userSelect: "none",
|
|
45
|
+
position: "relative",
|
|
46
|
+
whiteSpace: "nowrap",
|
|
47
|
+
verticalAlign: "middle",
|
|
48
|
+
outline: "none",
|
|
49
|
+
_focusVisible: {
|
|
50
|
+
focusShadow: "outline",
|
|
51
|
+
},
|
|
52
|
+
_disabled: {
|
|
53
|
+
opacity: 0.4,
|
|
54
|
+
cursor: "not-allowed",
|
|
55
|
+
boxShadow: "none",
|
|
56
|
+
},
|
|
57
|
+
_hover: {
|
|
58
|
+
_disabled: {
|
|
59
|
+
bg: "initial",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
variants: {
|
|
64
|
+
size: {
|
|
65
|
+
lg: { h: "12", minW: "12", fontSize: "lg", px: "6" },
|
|
66
|
+
md: { h: "10", minW: "10", fontSize: "md", px: "4" },
|
|
67
|
+
sm: { h: "8", minW: "8", fontSize: "sm", px: "3" },
|
|
68
|
+
xs: { h: "6", minW: "6", fontSize: "xs", px: "2" },
|
|
69
|
+
},
|
|
70
|
+
variant: {
|
|
71
|
+
// Chakra's unstyled reset + this app's border-radius removal.
|
|
72
|
+
unstyled: {
|
|
73
|
+
bg: "none",
|
|
74
|
+
color: "inherit",
|
|
75
|
+
display: "inline",
|
|
76
|
+
lineHeight: "inherit",
|
|
77
|
+
m: "0",
|
|
78
|
+
p: "0",
|
|
79
|
+
borderRadius: "unset",
|
|
80
|
+
},
|
|
81
|
+
// Chakra's link layout (no padding/height) + this app's colours.
|
|
82
|
+
link: {
|
|
83
|
+
padding: 0,
|
|
84
|
+
height: "auto",
|
|
85
|
+
lineHeight: "normal",
|
|
86
|
+
verticalAlign: "baseline",
|
|
87
|
+
borderWidth: "0",
|
|
88
|
+
color: "brand.600",
|
|
89
|
+
fontWeight: "normal",
|
|
90
|
+
bg: "transparent",
|
|
91
|
+
_hover: {
|
|
92
|
+
textDecoration: "underline",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
secondary: {
|
|
96
|
+
borderWidth: "2px",
|
|
97
|
+
borderColor: "brand.500",
|
|
98
|
+
color: "brand.700",
|
|
99
|
+
bg: "transparent",
|
|
100
|
+
_hover: { borderColor: "brand.600" },
|
|
101
|
+
_active: { bg: "brand.50", borderColor: "brand.700" },
|
|
102
|
+
},
|
|
103
|
+
ghost: {
|
|
104
|
+
color: "black",
|
|
105
|
+
bg: "transparent",
|
|
106
|
+
_hover: { bg: "blackAlpha.50" },
|
|
107
|
+
_active: { bg: "blackAlpha.100" },
|
|
108
|
+
},
|
|
109
|
+
// Chakra had no `plain` variant, so `variant="plain"` fell through to
|
|
110
|
+
// base-only styling: a transparent, colour-inheriting button. Used for the
|
|
111
|
+
// action-bar icon-button menu triggers (settings/help), which supply their
|
|
112
|
+
// own colour and shape via instance styles.
|
|
113
|
+
plain: {
|
|
114
|
+
bg: "transparent",
|
|
115
|
+
color: "inherit",
|
|
116
|
+
},
|
|
117
|
+
primary: {
|
|
118
|
+
color: "white",
|
|
119
|
+
bg: "brand.500",
|
|
120
|
+
_hover: { bg: "brand.600", _disabled: { bg: "brand.500" } },
|
|
121
|
+
_active: { bg: "brand.700" },
|
|
122
|
+
},
|
|
123
|
+
warning: {
|
|
124
|
+
borderWidth: "2px",
|
|
125
|
+
borderColor: "danger.500",
|
|
126
|
+
color: "danger.500",
|
|
127
|
+
bg: "transparent",
|
|
128
|
+
_hover: { borderColor: "danger.600" },
|
|
129
|
+
_active: { bg: "danger.50", borderColor: "danger.500" },
|
|
130
|
+
},
|
|
131
|
+
// Chakra's built-in solid + red colorScheme (destructive confirm
|
|
132
|
+
// buttons). Same values as `record` today, but a separate variant so
|
|
133
|
+
// recording UI and destructive actions can diverge - hence danger
|
|
134
|
+
// tokens here, red.* literals there.
|
|
135
|
+
warningSolid: {
|
|
136
|
+
color: "white",
|
|
137
|
+
bg: "danger.500",
|
|
138
|
+
_hover: { bg: "danger.600", _disabled: { bg: "danger.500" } },
|
|
139
|
+
_active: { bg: "danger.700" },
|
|
140
|
+
},
|
|
141
|
+
// Family-wide variants (every censused app has language- and
|
|
142
|
+
// toolbar-class buttons).
|
|
143
|
+
toolbar: {
|
|
144
|
+
color: "black",
|
|
145
|
+
bg: "white",
|
|
146
|
+
_hover: { bg: "whiteAlpha.900", _disabled: { bg: "white" } },
|
|
147
|
+
_active: { bg: "whiteAlpha.800" },
|
|
148
|
+
_focusVisible: { focusShadow: "outlineDark" },
|
|
149
|
+
},
|
|
150
|
+
language: {
|
|
151
|
+
borderWidth: "2px",
|
|
152
|
+
borderColor: "gray.200",
|
|
153
|
+
color: "languageText",
|
|
154
|
+
_hover: { color: "languageTextHover", bg: "gray.100" },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
defaultVariants: {
|
|
159
|
+
variant: "secondary",
|
|
160
|
+
size: "md",
|
|
161
|
+
},
|
|
162
|
+
});
|
package/src/Button.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 { forwardRef, ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Button as RACButton,
|
|
9
|
+
ButtonProps as RACButtonProps,
|
|
10
|
+
} from "react-aria-components";
|
|
11
|
+
import { css, cx } from "styled-system/css";
|
|
12
|
+
import { button, ButtonVariantProps } from "styled-system/recipes";
|
|
13
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
14
|
+
import { buttonIcon } from "./button-icon";
|
|
15
|
+
|
|
16
|
+
export interface ButtonProps
|
|
17
|
+
extends Omit<RACButtonProps, "className" | "children">,
|
|
18
|
+
ButtonVariantProps {
|
|
19
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
20
|
+
css?: SystemStyleObject;
|
|
21
|
+
className?: string;
|
|
22
|
+
/** Icon rendered before the label, matching Chakra's `leftIcon`. */
|
|
23
|
+
leftIcon?: ReactNode;
|
|
24
|
+
/** Icon rendered after the label, matching Chakra's `rightIcon`. */
|
|
25
|
+
rightIcon?: ReactNode;
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Button — react-aria-components <Button> styled with the `button` config
|
|
31
|
+
* recipe. The recipe's interaction states are driven by RAC's data attributes
|
|
32
|
+
* (see the preset's widened conditions), so disabled/hover/press/focus all work
|
|
33
|
+
* without extra wiring.
|
|
34
|
+
*/
|
|
35
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
36
|
+
function Button(
|
|
37
|
+
{
|
|
38
|
+
variant,
|
|
39
|
+
size,
|
|
40
|
+
css: cssProp,
|
|
41
|
+
className,
|
|
42
|
+
leftIcon,
|
|
43
|
+
rightIcon,
|
|
44
|
+
children,
|
|
45
|
+
...rest
|
|
46
|
+
},
|
|
47
|
+
ref,
|
|
48
|
+
) {
|
|
49
|
+
return (
|
|
50
|
+
<RACButton
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cx(
|
|
53
|
+
button({ variant, size }),
|
|
54
|
+
cssProp ? css(cssProp) : undefined,
|
|
55
|
+
className,
|
|
56
|
+
)}
|
|
57
|
+
{...rest}
|
|
58
|
+
>
|
|
59
|
+
{leftIcon ? (
|
|
60
|
+
<span className={buttonIcon({ side: "left" })}>{leftIcon}</span>
|
|
61
|
+
) : null}
|
|
62
|
+
{children}
|
|
63
|
+
{rightIcon ? (
|
|
64
|
+
<span className={buttonIcon({ side: "right" })}>{rightIcon}</span>
|
|
65
|
+
) : null}
|
|
66
|
+
</RACButton>
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { forwardRef, HTMLAttributes } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
9
|
+
|
|
10
|
+
export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
/**
|
|
12
|
+
* Square the inner radii so adjacent buttons form one control (Chakra's
|
|
13
|
+
* `isAttached`). Otherwise buttons get a small gap.
|
|
14
|
+
*/
|
|
15
|
+
isAttached?: boolean;
|
|
16
|
+
/** Per-instance style overrides. */
|
|
17
|
+
css?: SystemStyleObject;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* ButtonGroup — lays out related buttons in a row. Works with any button
|
|
23
|
+
* elements (shared-ui or native). Replaces Chakra's ButtonGroup.
|
|
24
|
+
*/
|
|
25
|
+
export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
26
|
+
function ButtonGroup({ isAttached, css: cssProp, className, ...rest }, ref) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cx(
|
|
31
|
+
css({
|
|
32
|
+
display: "inline-flex",
|
|
33
|
+
alignItems: "center",
|
|
34
|
+
// Buttons are position: relative, so without this an attached
|
|
35
|
+
// sibling paints over the focused button's focus-ring shadow
|
|
36
|
+
// (Chakra's Button applied zIndex 1 on focus when grouped).
|
|
37
|
+
"& > *": { _focus: { zIndex: 1 } },
|
|
38
|
+
}),
|
|
39
|
+
isAttached
|
|
40
|
+
? css({
|
|
41
|
+
gap: 0,
|
|
42
|
+
// :first-child/:last-child, not :first-of-type: attached
|
|
43
|
+
// groups can mix element types (e.g. select + button), and
|
|
44
|
+
// -of-type matches per element type.
|
|
45
|
+
"& > *:first-child:not(:last-child)": {
|
|
46
|
+
borderEndRadius: 0,
|
|
47
|
+
},
|
|
48
|
+
"& > *:not(:first-child):not(:last-child)": {
|
|
49
|
+
borderRadius: 0,
|
|
50
|
+
},
|
|
51
|
+
"& > *:not(:first-child):last-child": {
|
|
52
|
+
borderStartRadius: 0,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
: css({ gap: 2 }),
|
|
56
|
+
cssProp ? css(cssProp) : undefined,
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
{...rest}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
);
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
* Card slot recipe — Chakra's Card at its default md size (light mode), with
|
|
10
|
+
* the `elevated` (default) and `outline` variants this app uses.
|
|
11
|
+
*
|
|
12
|
+
* Registered in the base preset (base-preset.ts); `variant` is
|
|
13
|
+
* forwarded as a runtime prop so the variants are generated via `staticCss`.
|
|
14
|
+
*/
|
|
15
|
+
export const card = defineSlotRecipe({
|
|
16
|
+
className: "card",
|
|
17
|
+
slots: ["container", "body"],
|
|
18
|
+
base: {
|
|
19
|
+
container: {
|
|
20
|
+
display: "flex",
|
|
21
|
+
flexDirection: "column",
|
|
22
|
+
position: "relative",
|
|
23
|
+
minWidth: 0,
|
|
24
|
+
wordWrap: "break-word",
|
|
25
|
+
bg: "white",
|
|
26
|
+
borderRadius: "md",
|
|
27
|
+
color: "inherit",
|
|
28
|
+
},
|
|
29
|
+
body: {
|
|
30
|
+
padding: "5",
|
|
31
|
+
flex: "1 1 0%",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
variants: {
|
|
35
|
+
variant: {
|
|
36
|
+
elevated: {
|
|
37
|
+
container: { boxShadow: "base" },
|
|
38
|
+
},
|
|
39
|
+
outline: {
|
|
40
|
+
container: {
|
|
41
|
+
borderWidth: "1px",
|
|
42
|
+
borderStyle: "solid",
|
|
43
|
+
borderColor: "gray.200",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
defaultVariants: { variant: "elevated" },
|
|
49
|
+
});
|
package/src/Card.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { forwardRef, HTMLAttributes } from "react";
|
|
7
|
+
import { css, cx } from "styled-system/css";
|
|
8
|
+
import { card, CardVariantProps } from "styled-system/recipes";
|
|
9
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
10
|
+
|
|
11
|
+
export interface CardProps
|
|
12
|
+
extends HTMLAttributes<HTMLDivElement>,
|
|
13
|
+
CardVariantProps {
|
|
14
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
15
|
+
css?: SystemStyleObject;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Card — Chakra's <Card> equivalent (md size). Place a CardBody inside.
|
|
20
|
+
*/
|
|
21
|
+
export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
|
|
22
|
+
{ variant, css: cssProp, className, ...rest },
|
|
23
|
+
ref,
|
|
24
|
+
) {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
ref={ref}
|
|
28
|
+
className={cx(
|
|
29
|
+
card({ variant }).container,
|
|
30
|
+
cssProp ? css(cssProp) : undefined,
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
{...rest}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export interface CardBodyProps extends HTMLAttributes<HTMLDivElement> {
|
|
39
|
+
css?: SystemStyleObject;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
|
|
43
|
+
function CardBody({ css: cssProp, className, ...rest }, ref) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cx(
|
|
48
|
+
card({}).body,
|
|
49
|
+
cssProp ? css(cssProp) : undefined,
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
{...rest}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
);
|