@microbit/ui 0.1.0-alpha.7 → 0.1.0-alpha.9
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/package.json +1 -1
- package/reset.css +12 -0
- package/src/Checkbox.recipe.ts +45 -11
- package/src/Checkbox.tsx +18 -8
- package/src/Collapse.tsx +13 -1
- package/src/Input.recipe.ts +16 -7
- package/src/Input.tsx +12 -5
- package/src/InputGroup.tsx +18 -4
- package/src/NativeSelect.tsx +13 -9
- package/src/Radio.recipe.ts +107 -0
- package/src/Radio.tsx +89 -0
- package/src/Switch.recipe.ts +39 -12
- package/src/Switch.tsx +6 -4
- package/src/TextField.tsx +7 -5
- package/src/base-preset.ts +6 -0
- package/src/hooks/useClipboard.ts +43 -6
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microbit/ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.9",
|
|
4
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",
|
package/reset.css
CHANGED
|
@@ -21,3 +21,15 @@
|
|
|
21
21
|
border-color: var(--colors-gray-200);
|
|
22
22
|
word-wrap: break-word;
|
|
23
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
|
+
}
|
package/src/Checkbox.recipe.ts
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
import { defineSlotRecipe } from "@pandacss/dev";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Checkbox slot recipe — Chakra's
|
|
10
|
-
*
|
|
11
|
-
* `borderColor` set on the root, matching
|
|
12
|
-
* box from the call site.
|
|
9
|
+
* Checkbox slot recipe — Chakra's checkbox with the default blue colorScheme
|
|
10
|
+
* (light mode) and its sm/md/lg size scale. The control's
|
|
11
|
+
* `borderColor: inherit` picks up a `borderColor` set on the root, matching
|
|
12
|
+
* Chakra's convention for tinting the box from the call site.
|
|
13
13
|
*
|
|
14
14
|
* State styling keys off data attributes stamped on the control by the
|
|
15
15
|
* shared-ui Checkbox (react-aria provides the state via render props).
|
|
16
16
|
*
|
|
17
|
-
* Registered in the base preset (base-preset.ts)
|
|
18
|
-
*
|
|
17
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
18
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
19
19
|
*/
|
|
20
20
|
export const checkbox = defineSlotRecipe({
|
|
21
21
|
className: "checkbox",
|
|
@@ -34,11 +34,10 @@ export const checkbox = defineSlotRecipe({
|
|
|
34
34
|
alignItems: "center",
|
|
35
35
|
justifyContent: "center",
|
|
36
36
|
flexShrink: 0,
|
|
37
|
-
width: "4",
|
|
38
|
-
height: "4",
|
|
39
37
|
transitionProperty: "box-shadow",
|
|
40
38
|
transitionDuration: "normal",
|
|
41
|
-
|
|
39
|
+
borderWidth: "2px",
|
|
40
|
+
borderStyle: "solid",
|
|
42
41
|
borderRadius: "sm",
|
|
43
42
|
borderColor: "inherit",
|
|
44
43
|
color: "white",
|
|
@@ -52,13 +51,23 @@ export const checkbox = defineSlotRecipe({
|
|
|
52
51
|
borderColor: "controlCheckedHoverBg",
|
|
53
52
|
},
|
|
54
53
|
},
|
|
54
|
+
// Chakra's disabled greys; the selected block restates _hover so the
|
|
55
|
+
// widened native-:hover condition can't re-tint a disabled control.
|
|
56
|
+
"&[data-disabled]": {
|
|
57
|
+
bg: "gray.100",
|
|
58
|
+
borderColor: "gray.100",
|
|
59
|
+
},
|
|
60
|
+
"&[data-selected][data-disabled]": {
|
|
61
|
+
bg: "gray.200",
|
|
62
|
+
borderColor: "gray.200",
|
|
63
|
+
color: "gray.500",
|
|
64
|
+
_hover: { bg: "gray.200", borderColor: "gray.200" },
|
|
65
|
+
},
|
|
55
66
|
"&[data-focus-visible]": {
|
|
56
67
|
focusShadow: "outline",
|
|
57
68
|
},
|
|
58
69
|
},
|
|
59
70
|
icon: {
|
|
60
|
-
width: "0.75rem",
|
|
61
|
-
height: "0.75rem",
|
|
62
71
|
transitionProperty: "transform",
|
|
63
72
|
transitionDuration: "normal",
|
|
64
73
|
},
|
|
@@ -68,4 +77,29 @@ export const checkbox = defineSlotRecipe({
|
|
|
68
77
|
"&[data-disabled]": { opacity: 0.4 },
|
|
69
78
|
},
|
|
70
79
|
},
|
|
80
|
+
variants: {
|
|
81
|
+
// Chakra's Checkbox size scale. The icon dimensions are Chakra's
|
|
82
|
+
// 1.2em-wide check glyph at each size's icon fontSize (3xs/2xs/2xs),
|
|
83
|
+
// resolved to rem.
|
|
84
|
+
size: {
|
|
85
|
+
sm: {
|
|
86
|
+
control: { width: "3", height: "3" },
|
|
87
|
+
icon: { width: "0.54rem", height: "0.54rem" },
|
|
88
|
+
label: { fontSize: "sm" },
|
|
89
|
+
},
|
|
90
|
+
md: {
|
|
91
|
+
control: { width: "4", height: "4" },
|
|
92
|
+
icon: { width: "0.75rem", height: "0.75rem" },
|
|
93
|
+
label: { fontSize: "md" },
|
|
94
|
+
},
|
|
95
|
+
lg: {
|
|
96
|
+
control: { width: "5", height: "5" },
|
|
97
|
+
icon: { width: "0.75rem", height: "0.75rem" },
|
|
98
|
+
label: { fontSize: "lg" },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
defaultVariants: {
|
|
103
|
+
size: "md",
|
|
104
|
+
},
|
|
71
105
|
});
|
package/src/Checkbox.tsx
CHANGED
|
@@ -9,11 +9,12 @@ import {
|
|
|
9
9
|
CheckboxProps as RACCheckboxProps,
|
|
10
10
|
} from "react-aria-components";
|
|
11
11
|
import { css, cx } from "styled-system/css";
|
|
12
|
-
import { checkbox } from "styled-system/recipes";
|
|
12
|
+
import { checkbox, CheckboxVariantProps } from "styled-system/recipes";
|
|
13
13
|
import { SystemStyleObject } from "styled-system/types";
|
|
14
14
|
|
|
15
15
|
export interface CheckboxProps
|
|
16
|
-
extends Omit<RACCheckboxProps, "className" | "children" | "style"
|
|
16
|
+
extends Omit<RACCheckboxProps, "className" | "children" | "style">,
|
|
17
|
+
CheckboxVariantProps {
|
|
17
18
|
/** Per-instance style overrides for the root, merged after the recipe. */
|
|
18
19
|
css?: SystemStyleObject;
|
|
19
20
|
className?: string;
|
|
@@ -21,28 +22,30 @@ export interface CheckboxProps
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
* Checkbox — react-aria-components <Checkbox> styled like Chakra's
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Checkbox — react-aria-components <Checkbox> styled like Chakra's checkbox.
|
|
26
|
+
* Children render as the label; wrap them in a visually-hidden span for
|
|
27
|
+
* icon-less checkboxes.
|
|
27
28
|
*/
|
|
28
29
|
export const Checkbox = ({
|
|
30
|
+
size,
|
|
29
31
|
css: cssProp,
|
|
30
32
|
className,
|
|
31
33
|
children,
|
|
32
34
|
...rest
|
|
33
35
|
}: CheckboxProps) => {
|
|
34
|
-
const slots = checkbox();
|
|
36
|
+
const slots = checkbox({ size });
|
|
35
37
|
return (
|
|
36
38
|
<RACCheckbox
|
|
37
39
|
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
38
40
|
{...rest}
|
|
39
41
|
>
|
|
40
|
-
{({ isSelected, isFocusVisible }) => (
|
|
42
|
+
{({ isSelected, isFocusVisible, isDisabled }) => (
|
|
41
43
|
<>
|
|
42
44
|
<span
|
|
43
45
|
className={slots.control}
|
|
44
46
|
data-selected={isSelected || undefined}
|
|
45
47
|
data-focus-visible={isFocusVisible || undefined}
|
|
48
|
+
data-disabled={isDisabled || undefined}
|
|
46
49
|
aria-hidden
|
|
47
50
|
>
|
|
48
51
|
{isSelected && (
|
|
@@ -58,7 +61,14 @@ export const Checkbox = ({
|
|
|
58
61
|
</svg>
|
|
59
62
|
)}
|
|
60
63
|
</span>
|
|
61
|
-
{children != null &&
|
|
64
|
+
{children != null && (
|
|
65
|
+
<span
|
|
66
|
+
className={slots.label}
|
|
67
|
+
data-disabled={isDisabled || undefined}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
62
72
|
</>
|
|
63
73
|
)}
|
|
64
74
|
</RACCheckbox>
|
package/src/Collapse.tsx
CHANGED
|
@@ -162,7 +162,19 @@ export const Collapse = ({
|
|
|
162
162
|
...style,
|
|
163
163
|
}}
|
|
164
164
|
>
|
|
165
|
-
<div
|
|
165
|
+
<div
|
|
166
|
+
ref={contentRef}
|
|
167
|
+
// flow-root: the measured element must be a block formatting
|
|
168
|
+
// context, or children's margins collapse through it and
|
|
169
|
+
// scrollHeight under-measures — the outer (overflow: hidden)
|
|
170
|
+
// wrapper IS a BFC and needs the full height, so the mismatch
|
|
171
|
+
// clips the last line of margined content (python-editor's API
|
|
172
|
+
// docs). Rendering is unchanged: the margins always displayed
|
|
173
|
+
// inside the outer BFC.
|
|
174
|
+
className={css({ display: "flow-root" })}
|
|
175
|
+
>
|
|
176
|
+
{children}
|
|
177
|
+
</div>
|
|
166
178
|
</div>
|
|
167
179
|
);
|
|
168
180
|
};
|
package/src/Input.recipe.ts
CHANGED
|
@@ -11,15 +11,17 @@ const transitionCommon =
|
|
|
11
11
|
"background-color, border-color, color, fill, stroke, opacity, box-shadow, transform";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Input recipe — Chakra's outline Input field
|
|
15
|
-
* the shared-ui Input and NativeSelect, and by TextField's
|
|
14
|
+
* Input recipe — Chakra's outline Input field (light mode) with its size
|
|
15
|
+
* scale. Used by the shared-ui Input and NativeSelect, and by TextField's
|
|
16
|
+
* input slot.
|
|
16
17
|
*
|
|
17
18
|
* Focus matches both native `:focus-visible` (plain inputs; browsers treat any
|
|
18
19
|
* focus in a text field as focus-visible) and react-aria's `data-focused`
|
|
19
20
|
* (inputs inside RAC TextField). Focus is declared after invalid so a focused
|
|
20
21
|
* invalid field shows the focus ring, as in Chakra.
|
|
21
22
|
*
|
|
22
|
-
* Registered in the base preset (base-preset.ts)
|
|
23
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
24
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
23
25
|
*/
|
|
24
26
|
export const input = defineRecipe({
|
|
25
27
|
className: "input",
|
|
@@ -32,10 +34,6 @@ export const input = defineRecipe({
|
|
|
32
34
|
font: "inherit",
|
|
33
35
|
transitionProperty: transitionCommon,
|
|
34
36
|
transitionDuration: "normal",
|
|
35
|
-
fontSize: "md",
|
|
36
|
-
px: "4",
|
|
37
|
-
h: "10",
|
|
38
|
-
borderRadius: "md",
|
|
39
37
|
border: "1px solid",
|
|
40
38
|
borderColor: "gray.200",
|
|
41
39
|
bg: "inherit",
|
|
@@ -60,4 +58,15 @@ export const input = defineRecipe({
|
|
|
60
58
|
cursor: "not-allowed",
|
|
61
59
|
},
|
|
62
60
|
},
|
|
61
|
+
variants: {
|
|
62
|
+
// Chakra's Input size scale, minus the unused xs.
|
|
63
|
+
size: {
|
|
64
|
+
lg: { fontSize: "lg", px: "4", h: "12", borderRadius: "md" },
|
|
65
|
+
md: { fontSize: "md", px: "4", h: "10", borderRadius: "md" },
|
|
66
|
+
sm: { fontSize: "sm", px: "3", h: "8", borderRadius: "sm" },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
defaultVariants: {
|
|
70
|
+
size: "md",
|
|
71
|
+
},
|
|
63
72
|
});
|
package/src/Input.tsx
CHANGED
|
@@ -5,28 +5,35 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { forwardRef, InputHTMLAttributes } from "react";
|
|
7
7
|
import { css, cx } from "styled-system/css";
|
|
8
|
-
import { input } from "styled-system/recipes";
|
|
8
|
+
import { input, InputVariantProps } from "styled-system/recipes";
|
|
9
9
|
import { SystemStyleObject } from "styled-system/types";
|
|
10
10
|
|
|
11
11
|
export interface InputProps
|
|
12
|
-
|
|
12
|
+
// `size` is the recipe's size scale, as in Chakra; the native character-count
|
|
13
|
+
// attribute it shadows was unused.
|
|
14
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, "className" | "size">,
|
|
15
|
+
InputVariantProps {
|
|
13
16
|
/** Per-instance style overrides, merged after the recipe. */
|
|
14
17
|
css?: SystemStyleObject;
|
|
15
18
|
className?: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
|
-
* Input — a native input styled like Chakra's outline Input
|
|
22
|
+
* Input — a native input styled like Chakra's outline Input. For a
|
|
20
23
|
* labelled field with help/error text use TextField instead.
|
|
21
24
|
*/
|
|
22
25
|
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
23
|
-
{ css: cssProp, className, ...rest },
|
|
26
|
+
{ size, css: cssProp, className, ...rest },
|
|
24
27
|
ref,
|
|
25
28
|
) {
|
|
26
29
|
return (
|
|
27
30
|
<input
|
|
28
31
|
ref={ref}
|
|
29
|
-
className={cx(
|
|
32
|
+
className={cx(
|
|
33
|
+
input({ size }),
|
|
34
|
+
cssProp ? css(cssProp) : undefined,
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
30
37
|
{...rest}
|
|
31
38
|
/>
|
|
32
39
|
);
|
package/src/InputGroup.tsx
CHANGED
|
@@ -16,6 +16,8 @@ export const InputGroup = styled("div", {
|
|
|
16
16
|
|
|
17
17
|
// The element styles are written out twice rather than shared via a spread:
|
|
18
18
|
// Panda's extractor only reliably evaluates inline literals.
|
|
19
|
+
// The size variants are square boxes matching the input recipe's height per
|
|
20
|
+
// size, as in Chakra; pass the same `size` as the grouped Input.
|
|
19
21
|
|
|
20
22
|
/** Element overlaying the start of an InputGroup (Chakra InputLeftElement). */
|
|
21
23
|
export const InputLeftElement = styled("div", {
|
|
@@ -23,13 +25,19 @@ export const InputLeftElement = styled("div", {
|
|
|
23
25
|
position: "absolute",
|
|
24
26
|
top: 0,
|
|
25
27
|
left: 0,
|
|
26
|
-
width: "10",
|
|
27
|
-
height: "10",
|
|
28
28
|
display: "flex",
|
|
29
29
|
alignItems: "center",
|
|
30
30
|
justifyContent: "center",
|
|
31
31
|
zIndex: 2,
|
|
32
32
|
},
|
|
33
|
+
variants: {
|
|
34
|
+
size: {
|
|
35
|
+
lg: { width: "12", height: "12" },
|
|
36
|
+
md: { width: "10", height: "10" },
|
|
37
|
+
sm: { width: "8", height: "8" },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
defaultVariants: { size: "md" },
|
|
33
41
|
});
|
|
34
42
|
|
|
35
43
|
/** Element overlaying the end of an InputGroup (Chakra InputRightElement). */
|
|
@@ -38,11 +46,17 @@ export const InputRightElement = styled("div", {
|
|
|
38
46
|
position: "absolute",
|
|
39
47
|
top: 0,
|
|
40
48
|
right: 0,
|
|
41
|
-
width: "10",
|
|
42
|
-
height: "10",
|
|
43
49
|
display: "flex",
|
|
44
50
|
alignItems: "center",
|
|
45
51
|
justifyContent: "center",
|
|
46
52
|
zIndex: 2,
|
|
47
53
|
},
|
|
54
|
+
variants: {
|
|
55
|
+
size: {
|
|
56
|
+
lg: { width: "12", height: "12" },
|
|
57
|
+
md: { width: "10", height: "10" },
|
|
58
|
+
sm: { width: "8", height: "8" },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
defaultVariants: { size: "md" },
|
|
48
62
|
});
|
package/src/NativeSelect.tsx
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { forwardRef, SelectHTMLAttributes } from "react";
|
|
7
7
|
import { css, cx } from "styled-system/css";
|
|
8
|
-
import { input } from "styled-system/recipes";
|
|
8
|
+
import { input, InputVariantProps } from "styled-system/recipes";
|
|
9
9
|
import { SystemStyleObject } from "styled-system/types";
|
|
10
10
|
|
|
11
11
|
export interface NativeSelectProps
|
|
12
|
-
|
|
12
|
+
// `size` is the recipe's size scale, as in Chakra; the native visible-rows
|
|
13
|
+
// attribute it shadows was unused.
|
|
14
|
+
extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "className" | "size">,
|
|
15
|
+
InputVariantProps {
|
|
13
16
|
/**
|
|
14
17
|
* Suppress the dropdown chevron, e.g. when an adjacent control provides the
|
|
15
18
|
* affordance. Without the chevron a bare <select> element is rendered (no
|
|
@@ -22,23 +25,24 @@ export interface NativeSelectProps
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
|
-
* NativeSelect — a native select styled like Chakra's Select field (
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
+
* NativeSelect — a native select styled like Chakra's Select field (outline).
|
|
29
|
+
* The recipe's `appearance: none` removes the platform chevron, so one is
|
|
30
|
+
* drawn back in by default (Chakra Select's glyph, `currentColor`).
|
|
28
31
|
*/
|
|
29
32
|
export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
30
33
|
function NativeSelect(
|
|
31
|
-
{ hideChevron = false, css: cssProp, className, ...rest },
|
|
34
|
+
{ hideChevron = false, size, css: cssProp, className, ...rest },
|
|
32
35
|
ref,
|
|
33
36
|
) {
|
|
34
37
|
const select = (
|
|
35
38
|
<select
|
|
36
39
|
ref={ref}
|
|
37
40
|
className={cx(
|
|
38
|
-
input(),
|
|
41
|
+
input({ size }),
|
|
39
42
|
css(
|
|
40
43
|
{ cursor: "pointer" },
|
|
41
|
-
// Room for the chevron overlay
|
|
44
|
+
// Room for the chevron overlay (Chakra Select's icon spacing,
|
|
45
|
+
// constant across sizes).
|
|
42
46
|
hideChevron ? undefined : { paddingRight: "8" },
|
|
43
47
|
cssProp,
|
|
44
48
|
),
|
|
@@ -53,7 +57,7 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
|
53
57
|
return (
|
|
54
58
|
<span className={css({ position: "relative", display: "inline-flex" })}>
|
|
55
59
|
{select}
|
|
56
|
-
{/* Chakra Select's chevron. */}
|
|
60
|
+
{/* Chakra Select's chevron, fixed-size across field sizes. */}
|
|
57
61
|
<svg
|
|
58
62
|
viewBox="0 0 24 24"
|
|
59
63
|
aria-hidden
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { defineSlotRecipe } from "@pandacss/dev";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Radio slot recipe — Chakra's radio with the default blue colorScheme
|
|
10
|
+
* (light mode) and its sm/md/lg size scale: the checkbox control rounded
|
|
11
|
+
* fully, with a 50% `currentColor` dot when selected instead of the check
|
|
12
|
+
* glyph. As with the checkbox, `borderColor: inherit` picks up a
|
|
13
|
+
* `borderColor` set on the root.
|
|
14
|
+
*
|
|
15
|
+
* State styling keys off data attributes stamped by the shared-ui Radio
|
|
16
|
+
* (react-aria provides the state via render props).
|
|
17
|
+
*
|
|
18
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
19
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
20
|
+
*/
|
|
21
|
+
export const radio = defineSlotRecipe({
|
|
22
|
+
className: "radio",
|
|
23
|
+
slots: ["root", "control", "label"],
|
|
24
|
+
base: {
|
|
25
|
+
root: {
|
|
26
|
+
display: "inline-flex",
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
verticalAlign: "top",
|
|
29
|
+
cursor: "pointer",
|
|
30
|
+
position: "relative",
|
|
31
|
+
"&[data-disabled]": { cursor: "not-allowed" },
|
|
32
|
+
},
|
|
33
|
+
control: {
|
|
34
|
+
display: "inline-flex",
|
|
35
|
+
alignItems: "center",
|
|
36
|
+
justifyContent: "center",
|
|
37
|
+
flexShrink: 0,
|
|
38
|
+
transitionProperty: "box-shadow",
|
|
39
|
+
transitionDuration: "normal",
|
|
40
|
+
borderWidth: "2px",
|
|
41
|
+
borderStyle: "solid",
|
|
42
|
+
borderRadius: "full",
|
|
43
|
+
borderColor: "inherit",
|
|
44
|
+
color: "white",
|
|
45
|
+
bg: "white",
|
|
46
|
+
"&[data-selected]": {
|
|
47
|
+
bg: "controlCheckedBg",
|
|
48
|
+
borderColor: "controlCheckedBg",
|
|
49
|
+
color: "white",
|
|
50
|
+
_hover: {
|
|
51
|
+
bg: "controlCheckedHoverBg",
|
|
52
|
+
borderColor: "controlCheckedHoverBg",
|
|
53
|
+
},
|
|
54
|
+
// Chakra's radio dot.
|
|
55
|
+
_before: {
|
|
56
|
+
content: '""',
|
|
57
|
+
display: "inline-block",
|
|
58
|
+
position: "relative",
|
|
59
|
+
width: "50%",
|
|
60
|
+
height: "50%",
|
|
61
|
+
borderRadius: "50%",
|
|
62
|
+
bg: "currentColor",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
// Chakra's disabled greys; the selected block restates _hover so the
|
|
66
|
+
// widened native-:hover condition can't re-tint a disabled control.
|
|
67
|
+
"&[data-disabled]": {
|
|
68
|
+
bg: "gray.100",
|
|
69
|
+
borderColor: "gray.100",
|
|
70
|
+
},
|
|
71
|
+
"&[data-selected][data-disabled]": {
|
|
72
|
+
bg: "gray.200",
|
|
73
|
+
borderColor: "gray.200",
|
|
74
|
+
color: "gray.500",
|
|
75
|
+
_hover: { bg: "gray.200", borderColor: "gray.200" },
|
|
76
|
+
},
|
|
77
|
+
"&[data-focus-visible]": {
|
|
78
|
+
focusShadow: "outline",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
label: {
|
|
82
|
+
userSelect: "none",
|
|
83
|
+
marginStart: "2",
|
|
84
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
variants: {
|
|
88
|
+
// Chakra's Radio size scale (the Checkbox control scale).
|
|
89
|
+
size: {
|
|
90
|
+
sm: {
|
|
91
|
+
control: { width: "3", height: "3" },
|
|
92
|
+
label: { fontSize: "sm" },
|
|
93
|
+
},
|
|
94
|
+
md: {
|
|
95
|
+
control: { width: "4", height: "4" },
|
|
96
|
+
label: { fontSize: "md" },
|
|
97
|
+
},
|
|
98
|
+
lg: {
|
|
99
|
+
control: { width: "5", height: "5" },
|
|
100
|
+
label: { fontSize: "lg" },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
defaultVariants: {
|
|
105
|
+
size: "md",
|
|
106
|
+
},
|
|
107
|
+
});
|
package/src/Radio.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { ReactNode } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Radio as RACRadio,
|
|
9
|
+
RadioProps as RACRadioProps,
|
|
10
|
+
RadioGroup as RACRadioGroup,
|
|
11
|
+
RadioGroupProps as RACRadioGroupProps,
|
|
12
|
+
} from "react-aria-components";
|
|
13
|
+
import { css, cx } from "styled-system/css";
|
|
14
|
+
import { radio, RadioVariantProps } from "styled-system/recipes";
|
|
15
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
16
|
+
|
|
17
|
+
export interface RadioGroupProps
|
|
18
|
+
extends Omit<RACRadioGroupProps, "className" | "style"> {
|
|
19
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
20
|
+
css?: SystemStyleObject;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* RadioGroup — react-aria-components <RadioGroup> for a set of Radios. Carries
|
|
26
|
+
* no styling of its own: compose with Stack for layout, as Chakra call sites
|
|
27
|
+
* did.
|
|
28
|
+
*/
|
|
29
|
+
export const RadioGroup = ({
|
|
30
|
+
css: cssProp,
|
|
31
|
+
className,
|
|
32
|
+
...rest
|
|
33
|
+
}: RadioGroupProps) => {
|
|
34
|
+
return (
|
|
35
|
+
<RACRadioGroup
|
|
36
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
37
|
+
{...rest}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface RadioProps
|
|
43
|
+
extends Omit<RACRadioProps, "className" | "children" | "style">,
|
|
44
|
+
RadioVariantProps {
|
|
45
|
+
/** Per-instance style overrides for the root, merged after the recipe. */
|
|
46
|
+
css?: SystemStyleObject;
|
|
47
|
+
className?: string;
|
|
48
|
+
children?: ReactNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Radio — react-aria-components <Radio> styled like Chakra's radio. Must be
|
|
53
|
+
* rendered inside a RadioGroup. Children render as the label.
|
|
54
|
+
*/
|
|
55
|
+
export const Radio = ({
|
|
56
|
+
size,
|
|
57
|
+
css: cssProp,
|
|
58
|
+
className,
|
|
59
|
+
children,
|
|
60
|
+
...rest
|
|
61
|
+
}: RadioProps) => {
|
|
62
|
+
const slots = radio({ size });
|
|
63
|
+
return (
|
|
64
|
+
<RACRadio
|
|
65
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
66
|
+
{...rest}
|
|
67
|
+
>
|
|
68
|
+
{({ isSelected, isFocusVisible, isDisabled }) => (
|
|
69
|
+
<>
|
|
70
|
+
<span
|
|
71
|
+
className={slots.control}
|
|
72
|
+
data-selected={isSelected || undefined}
|
|
73
|
+
data-focus-visible={isFocusVisible || undefined}
|
|
74
|
+
data-disabled={isDisabled || undefined}
|
|
75
|
+
aria-hidden
|
|
76
|
+
/>
|
|
77
|
+
{children != null && (
|
|
78
|
+
<span
|
|
79
|
+
className={slots.label}
|
|
80
|
+
data-disabled={isDisabled || undefined}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</>
|
|
86
|
+
)}
|
|
87
|
+
</RACRadio>
|
|
88
|
+
);
|
|
89
|
+
};
|
package/src/Switch.recipe.ts
CHANGED
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
import { defineSlotRecipe } from "@pandacss/dev";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Switch slot recipe — Chakra's
|
|
10
|
-
* (light mode)
|
|
11
|
-
* the track
|
|
9
|
+
* Switch slot recipe — Chakra's switch with the default blue colorScheme
|
|
10
|
+
* (light mode) and its sm/md/lg size scale. The track has a 2px inset; the
|
|
11
|
+
* thumb matches the track height and slides by the track width/height
|
|
12
|
+
* difference when selected.
|
|
12
13
|
*
|
|
13
14
|
* State styling keys off data attributes stamped by the shared-ui Switch
|
|
14
15
|
* (react-aria provides the state via render props).
|
|
15
16
|
*
|
|
16
|
-
* Registered in the base preset (base-preset.ts)
|
|
17
|
-
*
|
|
17
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
18
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
18
19
|
*/
|
|
19
20
|
export const switchRecipe = defineSlotRecipe({
|
|
20
21
|
className: "switch",
|
|
@@ -35,8 +36,6 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
35
36
|
boxSizing: "content-box",
|
|
36
37
|
borderRadius: "full",
|
|
37
38
|
p: "0.5",
|
|
38
|
-
width: "1.875rem",
|
|
39
|
-
height: "4",
|
|
40
39
|
transitionProperty: "background-color",
|
|
41
40
|
transitionDuration: "fast",
|
|
42
41
|
bg: "gray.300",
|
|
@@ -61,11 +60,6 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
61
60
|
transitionProperty: "transform",
|
|
62
61
|
transitionDuration: "normal",
|
|
63
62
|
borderRadius: "inherit",
|
|
64
|
-
width: "4",
|
|
65
|
-
height: "4",
|
|
66
|
-
"&[data-selected]": {
|
|
67
|
-
transform: "translateX(0.875rem)",
|
|
68
|
-
},
|
|
69
63
|
_forcedColors: {
|
|
70
64
|
bg: "ButtonText",
|
|
71
65
|
"&[data-selected]": { bg: "SelectedItemText" },
|
|
@@ -77,4 +71,37 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
77
71
|
"&[data-disabled]": { opacity: 0.4 },
|
|
78
72
|
},
|
|
79
73
|
},
|
|
74
|
+
variants: {
|
|
75
|
+
// Chakra's Switch size scale (track width x height; the selected-thumb
|
|
76
|
+
// translate is the difference between them).
|
|
77
|
+
size: {
|
|
78
|
+
sm: {
|
|
79
|
+
track: { width: "1.375rem", height: "3" },
|
|
80
|
+
thumb: {
|
|
81
|
+
width: "3",
|
|
82
|
+
height: "3",
|
|
83
|
+
"&[data-selected]": { transform: "translateX(0.625rem)" },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
md: {
|
|
87
|
+
track: { width: "1.875rem", height: "4" },
|
|
88
|
+
thumb: {
|
|
89
|
+
width: "4",
|
|
90
|
+
height: "4",
|
|
91
|
+
"&[data-selected]": { transform: "translateX(0.875rem)" },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
lg: {
|
|
95
|
+
track: { width: "2.875rem", height: "6" },
|
|
96
|
+
thumb: {
|
|
97
|
+
width: "6",
|
|
98
|
+
height: "6",
|
|
99
|
+
"&[data-selected]": { transform: "translateX(1.375rem)" },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
defaultVariants: {
|
|
105
|
+
size: "md",
|
|
106
|
+
},
|
|
80
107
|
});
|
package/src/Switch.tsx
CHANGED
|
@@ -9,11 +9,12 @@ import {
|
|
|
9
9
|
SwitchProps as RACSwitchProps,
|
|
10
10
|
} from "react-aria-components";
|
|
11
11
|
import { css, cx } from "styled-system/css";
|
|
12
|
-
import { switchRecipe } from "styled-system/recipes";
|
|
12
|
+
import { switchRecipe, SwitchRecipeVariantProps } from "styled-system/recipes";
|
|
13
13
|
import { SystemStyleObject } from "styled-system/types";
|
|
14
14
|
|
|
15
15
|
export interface SwitchProps
|
|
16
|
-
extends Omit<RACSwitchProps, "className" | "children" | "style"
|
|
16
|
+
extends Omit<RACSwitchProps, "className" | "children" | "style">,
|
|
17
|
+
SwitchRecipeVariantProps {
|
|
17
18
|
/** Per-instance style overrides for the root, merged after the recipe. */
|
|
18
19
|
css?: SystemStyleObject;
|
|
19
20
|
className?: string;
|
|
@@ -21,16 +22,17 @@ export interface SwitchProps
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
* Switch — react-aria-components <Switch> styled like Chakra's
|
|
25
|
+
* Switch — react-aria-components <Switch> styled like Chakra's switch.
|
|
25
26
|
* Children render as the label; pass `aria-label` for label-less switches.
|
|
26
27
|
*/
|
|
27
28
|
export const Switch = ({
|
|
29
|
+
size,
|
|
28
30
|
css: cssProp,
|
|
29
31
|
className,
|
|
30
32
|
children,
|
|
31
33
|
...rest
|
|
32
34
|
}: SwitchProps) => {
|
|
33
|
-
const slots = switchRecipe();
|
|
35
|
+
const slots = switchRecipe({ size });
|
|
34
36
|
return (
|
|
35
37
|
<RACSwitch
|
|
36
38
|
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
package/src/TextField.tsx
CHANGED
|
@@ -13,14 +13,15 @@ import {
|
|
|
13
13
|
TextFieldProps as RACTextFieldProps,
|
|
14
14
|
} from "react-aria-components";
|
|
15
15
|
import { css, cx } from "styled-system/css";
|
|
16
|
-
import { field, input } from "styled-system/recipes";
|
|
16
|
+
import { field, input, InputVariantProps } from "styled-system/recipes";
|
|
17
17
|
import { SystemStyleObject } from "styled-system/types";
|
|
18
18
|
|
|
19
19
|
export interface TextFieldProps
|
|
20
20
|
extends Omit<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
RACTextFieldProps,
|
|
22
|
+
"className" | "children" | "style" | "onFocus" | "onBlur"
|
|
23
|
+
>,
|
|
24
|
+
InputVariantProps {
|
|
24
25
|
/** Visible label (Chakra's FormLabel; asterisk added when `isRequired`). */
|
|
25
26
|
label: ReactNode;
|
|
26
27
|
/** Help text below the input (Chakra's FormHelperText). */
|
|
@@ -48,6 +49,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
48
49
|
helperTextCss,
|
|
49
50
|
onFocus,
|
|
50
51
|
autoCapitalize,
|
|
52
|
+
size,
|
|
51
53
|
...rest
|
|
52
54
|
},
|
|
53
55
|
ref,
|
|
@@ -65,7 +67,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
65
67
|
</RACLabel>
|
|
66
68
|
<RACInput
|
|
67
69
|
ref={ref}
|
|
68
|
-
className={input()}
|
|
70
|
+
className={input({ size })}
|
|
69
71
|
onFocus={onFocus}
|
|
70
72
|
autoCapitalize={autoCapitalize}
|
|
71
73
|
/>
|
package/src/base-preset.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { button } from "./Button.recipe";
|
|
27
27
|
import { card } from "./Card.recipe";
|
|
28
28
|
import { checkbox } from "./Checkbox.recipe";
|
|
29
|
+
import { radio } from "./Radio.recipe";
|
|
29
30
|
import { drawer } from "./Drawer.recipe";
|
|
30
31
|
import { heading } from "./Heading.recipe";
|
|
31
32
|
import { input } from "./Input.recipe";
|
|
@@ -180,6 +181,7 @@ export const basePreset = definePreset({
|
|
|
180
181
|
field,
|
|
181
182
|
menu,
|
|
182
183
|
numberField,
|
|
184
|
+
radio,
|
|
183
185
|
slider,
|
|
184
186
|
switchRecipe,
|
|
185
187
|
toast,
|
|
@@ -237,12 +239,16 @@ export const basePreset = definePreset({
|
|
|
237
239
|
staticCss: {
|
|
238
240
|
recipes: {
|
|
239
241
|
button: ["*"],
|
|
242
|
+
checkbox: ["*"],
|
|
240
243
|
heading: ["*"],
|
|
241
244
|
card: ["*"],
|
|
242
245
|
// Dialog size is chosen with responsive objects ({ base, md }) passed
|
|
243
246
|
// as a runtime prop, so generate the breakpoint-prefixed variants too.
|
|
244
247
|
dialog: [{ size: ["*"], responsive: true }, { centered: ["*"] }],
|
|
245
248
|
drawer: ["*"],
|
|
249
|
+
input: ["*"],
|
|
250
|
+
radio: ["*"],
|
|
251
|
+
switchRecipe: ["*"],
|
|
246
252
|
// Toast status is chosen at runtime from the toast content.
|
|
247
253
|
toast: ["*"],
|
|
248
254
|
},
|
|
@@ -15,13 +15,50 @@ export function useClipboard(
|
|
|
15
15
|
): { onCopy: () => void; hasCopied: boolean } {
|
|
16
16
|
const [hasCopied, setHasCopied] = useState(false);
|
|
17
17
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
18
|
+
const markCopied = useCallback(() => {
|
|
19
|
+
setHasCopied(true);
|
|
20
|
+
clearTimeout(timeoutRef.current);
|
|
21
|
+
timeoutRef.current = setTimeout(() => setHasCopied(false), timeoutMs);
|
|
22
|
+
}, [timeoutMs]);
|
|
18
23
|
const onCopy = useCallback(() => {
|
|
19
|
-
navigator.clipboard
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
})
|
|
24
|
-
|
|
24
|
+
if (navigator.clipboard?.writeText) {
|
|
25
|
+
navigator.clipboard
|
|
26
|
+
.writeText(value)
|
|
27
|
+
.then(markCopied, () => execCommandCopy(value) && markCopied());
|
|
28
|
+
} else if (execCommandCopy(value)) {
|
|
29
|
+
markCopied();
|
|
30
|
+
}
|
|
31
|
+
}, [value, markCopied]);
|
|
25
32
|
useEffect(() => () => clearTimeout(timeoutRef.current), []);
|
|
26
33
|
return { onCopy, hasCopied };
|
|
27
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Chakra's useClipboard wrapped the execCommand-based copy-to-clipboard
|
|
38
|
+
* package, which works on a user gesture in contexts the async API refuses:
|
|
39
|
+
* insecure (non-localhost http) origins, where `navigator.clipboard` is
|
|
40
|
+
* undefined, and frames without a clipboard-write permissions policy, where
|
|
41
|
+
* `writeText` rejects.
|
|
42
|
+
*/
|
|
43
|
+
function execCommandCopy(value: string): boolean {
|
|
44
|
+
const active = document.activeElement;
|
|
45
|
+
const textarea = document.createElement("textarea");
|
|
46
|
+
textarea.value = value;
|
|
47
|
+
textarea.setAttribute("readonly", "");
|
|
48
|
+
textarea.style.position = "fixed";
|
|
49
|
+
textarea.style.opacity = "0";
|
|
50
|
+
document.body.appendChild(textarea);
|
|
51
|
+
textarea.select();
|
|
52
|
+
let copied = false;
|
|
53
|
+
try {
|
|
54
|
+
copied = document.execCommand("copy");
|
|
55
|
+
} catch {
|
|
56
|
+
// Unsupported (execCommand is deprecated but universally kept for copy);
|
|
57
|
+
// report failure so the caller shows no false "Copied!".
|
|
58
|
+
}
|
|
59
|
+
textarea.remove();
|
|
60
|
+
if (active instanceof HTMLElement) {
|
|
61
|
+
active.focus();
|
|
62
|
+
}
|
|
63
|
+
return copied;
|
|
64
|
+
}
|
package/src/index.ts
CHANGED