@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.
Files changed (86) hide show
  1. package/LICENSE.md +53 -0
  2. package/README.md +220 -0
  3. package/lang/ui.ca.json +22 -0
  4. package/lang/ui.de.json +22 -0
  5. package/lang/ui.en-us.json +22 -0
  6. package/lang/ui.en.json +22 -0
  7. package/lang/ui.es-es.json +22 -0
  8. package/lang/ui.fr.json +22 -0
  9. package/lang/ui.ga-ie.json +22 -0
  10. package/lang/ui.ja.json +22 -0
  11. package/lang/ui.ko.json +22 -0
  12. package/lang/ui.lol.json +22 -0
  13. package/lang/ui.nl.json +22 -0
  14. package/lang/ui.pl.json +22 -0
  15. package/lang/ui.pt-br.json +22 -0
  16. package/lang/ui.zh-cn.json +22 -0
  17. package/lang/ui.zh-tw.json +22 -0
  18. package/package.json +58 -0
  19. package/postcss-legacy-safari.cjs +96 -0
  20. package/reset.css +35 -0
  21. package/src/Button.recipe.ts +162 -0
  22. package/src/Button.tsx +69 -0
  23. package/src/ButtonGroup.tsx +63 -0
  24. package/src/Card.recipe.ts +49 -0
  25. package/src/Card.tsx +56 -0
  26. package/src/Checkbox.recipe.ts +105 -0
  27. package/src/Checkbox.tsx +76 -0
  28. package/src/CloseButton.tsx +87 -0
  29. package/src/CloseIcon.tsx +40 -0
  30. package/src/Code.tsx +20 -0
  31. package/src/Collapse.tsx +180 -0
  32. package/src/Divider.tsx +52 -0
  33. package/src/Drawer.recipe.ts +98 -0
  34. package/src/Drawer.tsx +138 -0
  35. package/src/Fade.tsx +48 -0
  36. package/src/Heading.recipe.ts +52 -0
  37. package/src/Heading.tsx +14 -0
  38. package/src/Icon.tsx +55 -0
  39. package/src/IconButton.tsx +44 -0
  40. package/src/Image.tsx +11 -0
  41. package/src/Input.recipe.ts +72 -0
  42. package/src/Input.tsx +40 -0
  43. package/src/InputGroup.tsx +62 -0
  44. package/src/Kbd.tsx +26 -0
  45. package/src/Link.tsx +23 -0
  46. package/src/LinkBox.tsx +88 -0
  47. package/src/LinkButton.tsx +80 -0
  48. package/src/List.tsx +31 -0
  49. package/src/Menu.recipe.ts +119 -0
  50. package/src/Menu.tsx +261 -0
  51. package/src/Modal.recipe.ts +163 -0
  52. package/src/Modal.tsx +279 -0
  53. package/src/NativeSelect.tsx +94 -0
  54. package/src/NumberField.recipe.ts +67 -0
  55. package/src/NumberField.tsx +86 -0
  56. package/src/PopoverArrow.tsx +67 -0
  57. package/src/ProgressBar.tsx +61 -0
  58. package/src/Radio.recipe.ts +107 -0
  59. package/src/Radio.tsx +89 -0
  60. package/src/SharedUIProvider.tsx +55 -0
  61. package/src/Slide.tsx +54 -0
  62. package/src/Slider.recipe.ts +102 -0
  63. package/src/Slider.tsx +163 -0
  64. package/src/Spinner.tsx +69 -0
  65. package/src/Svg.tsx +23 -0
  66. package/src/Switch.recipe.ts +107 -0
  67. package/src/Switch.tsx +62 -0
  68. package/src/Text.recipe.ts +29 -0
  69. package/src/Text.tsx +16 -0
  70. package/src/TextField.recipe.ts +54 -0
  71. package/src/TextField.tsx +89 -0
  72. package/src/Toast.recipe.ts +98 -0
  73. package/src/Toast.tsx +171 -0
  74. package/src/Tooltip.tsx +91 -0
  75. package/src/UnmountCallback.tsx +18 -0
  76. package/src/VisuallyHidden.tsx +14 -0
  77. package/src/base-preset.ts +294 -0
  78. package/src/base-tokens.ts +1017 -0
  79. package/src/button-icon.ts +23 -0
  80. package/src/hooks/useBreakpointValue.ts +63 -0
  81. package/src/hooks/useClipboard.ts +64 -0
  82. package/src/hooks/useMediaQuery.ts +28 -0
  83. package/src/hooks/usePrevious.ts +18 -0
  84. package/src/index.ts +55 -0
  85. package/src/messages.ts +17 -0
  86. 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
+ );