@microbit/ui 0.1.0-alpha.3 → 0.1.0-alpha.30
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 +40 -0
- package/README.md +256 -26
- package/lang/ui.ca.json +43 -3
- package/lang/ui.cy.json +62 -0
- package/lang/ui.de.json +62 -0
- package/lang/ui.en-us.json +40 -0
- package/lang/ui.en.json +40 -0
- package/lang/ui.es-es.json +43 -3
- package/lang/ui.fr.json +44 -4
- package/lang/ui.ga-ie.json +62 -0
- package/lang/ui.it.json +62 -0
- package/lang/ui.ja.json +44 -4
- package/lang/ui.ko.json +44 -4
- package/lang/ui.lo.json +62 -0
- package/lang/ui.lol.json +40 -0
- package/lang/ui.nl.json +44 -4
- package/lang/ui.pl.json +44 -4
- package/lang/ui.pt-br.json +44 -4
- package/lang/ui.vi.json +62 -0
- package/lang/ui.zh-cn.json +62 -0
- package/lang/ui.zh-tw.json +44 -4
- package/package.json +19 -10
- package/postcss-legacy-safari.cjs +96 -0
- package/reset.css +35 -0
- package/src/Avatar.recipe.ts +166 -0
- package/src/Avatar.tsx +278 -0
- package/src/Breadcrumb.recipe.ts +39 -0
- package/src/Breadcrumb.tsx +114 -0
- package/src/Button.recipe.ts +88 -50
- package/src/Button.tsx +64 -26
- package/src/ButtonGroup.tsx +37 -15
- package/src/Card.recipe.ts +1 -2
- package/src/Card.tsx +2 -1
- package/src/Checkbox.recipe.ts +49 -14
- package/src/Checkbox.tsx +111 -32
- package/src/CheckboxGroup.tsx +70 -0
- package/src/CloseButton.tsx +3 -3
- package/src/CloseIcon.tsx +6 -4
- package/src/Code.tsx +20 -0
- package/src/Collapse.tsx +179 -0
- package/src/ComboBox.tsx +246 -0
- package/src/Divider.tsx +72 -8
- package/src/Drawer.recipe.ts +4 -5
- package/src/Drawer.tsx +3 -4
- package/src/ExternalLink.tsx +43 -0
- package/src/Fade.tsx +62 -0
- package/src/Field.recipe.ts +114 -0
- package/src/Field.tsx +187 -0
- package/src/GridList.recipe.ts +57 -0
- package/src/GridList.tsx +81 -0
- package/src/Heading.recipe.ts +20 -1
- package/src/Heading.tsx +3 -3
- package/src/Icon.tsx +23 -7
- package/src/IconButton.tsx +7 -12
- package/src/Image.tsx +1 -1
- package/src/Input.recipe.ts +41 -28
- package/src/Input.tsx +23 -7
- package/src/InputGroup.tsx +21 -8
- package/src/Kbd.tsx +26 -0
- package/src/Link.tsx +3 -3
- package/src/LinkBox.tsx +2 -3
- package/src/LinkButton.tsx +81 -0
- package/src/List.tsx +8 -6
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +51 -17
- package/src/Menu.tsx +117 -2
- package/src/Modal.recipe.ts +14 -9
- package/src/Modal.tsx +137 -24
- package/src/MoreMenuButton.tsx +63 -0
- package/src/NativeSelect.tsx +39 -12
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +108 -0
- package/src/NumberField.tsx +138 -0
- package/src/PopoverArrow.tsx +19 -5
- package/src/ProgressBar.tsx +3 -5
- package/src/Radio.recipe.ts +108 -0
- package/src/Radio.tsx +62 -0
- package/src/RadioGroup.tsx +68 -0
- package/src/Select.recipe.ts +211 -0
- package/src/Select.tsx +194 -0
- package/src/SharedUIProvider.tsx +51 -7
- package/src/Skeleton.tsx +146 -0
- package/src/Slide.tsx +2 -2
- package/src/Slider.recipe.ts +24 -15
- package/src/Slider.tsx +65 -5
- package/src/Spinner.tsx +11 -9
- package/src/Svg.tsx +2 -3
- package/src/Switch.recipe.ts +58 -13
- package/src/Switch.tsx +39 -7
- package/src/Text.recipe.ts +26 -0
- package/src/Text.tsx +6 -2
- package/src/TextField.tsx +54 -40
- package/src/Toast.recipe.ts +49 -12
- package/src/Toast.tsx +151 -46
- package/src/Tooltip.recipe.ts +32 -0
- package/src/Tooltip.tsx +51 -28
- package/src/TooltipButton.tsx +229 -0
- package/src/UnmountCallback.tsx +2 -2
- package/src/VisuallyHidden.tsx +1 -1
- package/src/base-preset.ts +336 -84
- package/src/{chakra-tokens.ts → base-tokens.ts} +7 -7
- package/src/button-icon.ts +22 -0
- package/src/data-attrs.ts +16 -0
- package/src/dense-preset.ts +107 -0
- package/src/hooks/useBreakpointValue.ts +4 -4
- package/src/hooks/useClipboard.ts +63 -0
- package/src/hooks/useDisclosure.ts +31 -0
- package/src/hooks/useMediaQuery.ts +27 -0
- package/src/hooks/usePrevious.ts +15 -0
- package/src/index.ts +29 -3
- package/src/rac-locale.ts +33 -0
- package/src/system.ts +22 -1
- package/src/TextField.recipe.ts +0 -54
|
@@ -0,0 +1,108 @@
|
|
|
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 — an outline input (the `input` recipe styles the
|
|
10
|
+
* input itself) with a right-hand stepper column of two stacked buttons.
|
|
11
|
+
* Consumed by the shared-ui NumberField (react-aria-components NumberField).
|
|
12
|
+
*
|
|
13
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
14
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
15
|
+
*/
|
|
16
|
+
export const numberField = defineSlotRecipe({
|
|
17
|
+
className: "numberField",
|
|
18
|
+
slots: ["root", "group", "stepper", "stepperButton"],
|
|
19
|
+
base: {
|
|
20
|
+
// No layout here: the component wears the `field` recipe's root alongside
|
|
21
|
+
// this slot, and that recipe is the single owner of field-root layout
|
|
22
|
+
// (else `labelPosition` would fight this slot over `flexDirection`). The
|
|
23
|
+
// slot stays for apps and variants to target.
|
|
24
|
+
root: {},
|
|
25
|
+
group: {
|
|
26
|
+
position: "relative",
|
|
27
|
+
zIndex: 0,
|
|
28
|
+
// The stepper overlays the input as a sibling, so the input's own
|
|
29
|
+
// :hover misses it — the group carries the hover tint. The :not()
|
|
30
|
+
// list is required: slot recipes layer above plain recipes
|
|
31
|
+
// (recipes.slots), so this would otherwise override focused/invalid
|
|
32
|
+
// (see docs/hints.md). :user-invalid takes its own :not(): :not() is
|
|
33
|
+
// not forgiving, so one list would drop the whole selector on a
|
|
34
|
+
// browser that doesn't know the pseudo-class (Safari < 16.5).
|
|
35
|
+
"&:hover input:not(:focus, [data-focused], [data-invalid]):not(:user-invalid)":
|
|
36
|
+
{ borderColor: "gray.500" },
|
|
37
|
+
},
|
|
38
|
+
// A column overlaying the input's right edge, inset by the input border.
|
|
39
|
+
stepper: {
|
|
40
|
+
display: "flex",
|
|
41
|
+
flexDirection: "column",
|
|
42
|
+
position: "absolute",
|
|
43
|
+
insetEnd: "0",
|
|
44
|
+
top: "0",
|
|
45
|
+
height: "calc(100% - 4px)",
|
|
46
|
+
margin: "2px",
|
|
47
|
+
width: "6",
|
|
48
|
+
zIndex: 1,
|
|
49
|
+
},
|
|
50
|
+
stepperButton: {
|
|
51
|
+
display: "flex",
|
|
52
|
+
alignItems: "center",
|
|
53
|
+
justifyContent: "center",
|
|
54
|
+
flex: 1,
|
|
55
|
+
cursor: "pointer",
|
|
56
|
+
lineHeight: "normal",
|
|
57
|
+
fontSize: "xs",
|
|
58
|
+
color: "inherit",
|
|
59
|
+
bg: "transparent",
|
|
60
|
+
borderStart: "1px solid",
|
|
61
|
+
borderColor: "gray.200",
|
|
62
|
+
transitionProperty: "background",
|
|
63
|
+
transitionDuration: "ultra-fast",
|
|
64
|
+
// Follow the input's corners, less the 2px border the stepper is inset
|
|
65
|
+
// by, so the hover and pressed fills curve away with the border instead
|
|
66
|
+
// of squaring off over its arc. Radii track the input recipe's size
|
|
67
|
+
// scale, so the `sm` variant restates them.
|
|
68
|
+
"&:first-child": {
|
|
69
|
+
borderStartEndRadius: "calc(token(radii.md) - 2px)",
|
|
70
|
+
},
|
|
71
|
+
"&:last-child": {
|
|
72
|
+
borderTop: "1px solid",
|
|
73
|
+
borderTopColor: "gray.200",
|
|
74
|
+
marginTop: "-1px",
|
|
75
|
+
borderEndEndRadius: "calc(token(radii.md) - 2px)",
|
|
76
|
+
},
|
|
77
|
+
"&[data-hovered]": { bg: "gray.100" },
|
|
78
|
+
"&[data-pressed]": { bg: "gray.200" },
|
|
79
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
variants: {
|
|
83
|
+
// The stepper column stays 24px wide at every size (as does the input
|
|
84
|
+
// padding paired with it in NumberField.tsx); only the arrow glyphs
|
|
85
|
+
// scale, at 0.75 × the field's font size. The base's `xs` is exactly
|
|
86
|
+
// md × 0.75, so md adds nothing.
|
|
87
|
+
size: {
|
|
88
|
+
lg: {
|
|
89
|
+
stepperButton: { fontSize: "calc(token(fontSizes.lg) * 0.75)" },
|
|
90
|
+
},
|
|
91
|
+
md: {},
|
|
92
|
+
sm: {
|
|
93
|
+
stepperButton: {
|
|
94
|
+
fontSize: "calc(token(fontSizes.sm) * 0.75)",
|
|
95
|
+
"&:first-child": {
|
|
96
|
+
borderStartEndRadius: "calc(token(radii.sm) - 2px)",
|
|
97
|
+
},
|
|
98
|
+
"&:last-child": {
|
|
99
|
+
borderEndEndRadius: "calc(token(radii.sm) - 2px)",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
defaultVariants: {
|
|
106
|
+
size: "md",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
NumberField as RACNumberField,
|
|
12
|
+
NumberFieldProps as RACNumberFieldProps,
|
|
13
|
+
} from "react-aria-components";
|
|
14
|
+
import { useIntl } from "react-intl";
|
|
15
|
+
import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
|
|
16
|
+
import { css, cx } from "styled-system/css";
|
|
17
|
+
import {
|
|
18
|
+
field,
|
|
19
|
+
input,
|
|
20
|
+
InputVariantProps,
|
|
21
|
+
numberField,
|
|
22
|
+
} from "styled-system/recipes";
|
|
23
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
24
|
+
import {
|
|
25
|
+
FieldLabel,
|
|
26
|
+
FieldLayoutProps,
|
|
27
|
+
FieldSupport,
|
|
28
|
+
FieldSupportProps,
|
|
29
|
+
} from "./Field";
|
|
30
|
+
import { Icon } from "./Icon";
|
|
31
|
+
import { uiMessage } from "./messages";
|
|
32
|
+
|
|
33
|
+
export interface NumberFieldProps
|
|
34
|
+
extends Omit<RACNumberFieldProps, "className" | "children" | "style">,
|
|
35
|
+
InputVariantProps,
|
|
36
|
+
FieldSupportProps,
|
|
37
|
+
FieldLayoutProps {
|
|
38
|
+
/** Visible label (optional; otherwise pass `aria-label`). */
|
|
39
|
+
label?: ReactNode;
|
|
40
|
+
/** Root style overrides (for a label-beside-field row, prefer
|
|
41
|
+
* `labelPosition="side"`). */
|
|
42
|
+
rootCss?: SystemStyleObject;
|
|
43
|
+
/** Label style overrides. */
|
|
44
|
+
labelCss?: SystemStyleObject;
|
|
45
|
+
/** Group (input + steppers) style overrides — set `width` here. */
|
|
46
|
+
groupCss?: SystemStyleObject;
|
|
47
|
+
/** Input style overrides (for sizing, prefer the `size` prop). */
|
|
48
|
+
inputCss?: SystemStyleObject;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* NumberField — react-aria-components <NumberField>: an outline input with a
|
|
53
|
+
* right-hand stepper column. The ref is forwarded to the input element. Value
|
|
54
|
+
* clamping to min/maxValue is handled by react-aria; onChange receives NaN
|
|
55
|
+
* when the field is emptied.
|
|
56
|
+
*/
|
|
57
|
+
export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
58
|
+
function NumberField(
|
|
59
|
+
{
|
|
60
|
+
label,
|
|
61
|
+
helperText,
|
|
62
|
+
errorMessage,
|
|
63
|
+
helperTextCss,
|
|
64
|
+
rootCss,
|
|
65
|
+
labelCss,
|
|
66
|
+
groupCss,
|
|
67
|
+
inputCss,
|
|
68
|
+
labelPosition,
|
|
69
|
+
...props
|
|
70
|
+
},
|
|
71
|
+
ref,
|
|
72
|
+
) {
|
|
73
|
+
// As Input: forward every recipe variant group, not just `size`, so a
|
|
74
|
+
// preset that adds one keeps working.
|
|
75
|
+
const [variantProps, rest] = input.splitVariantProps(props);
|
|
76
|
+
const slots = numberField({ size: variantProps.size });
|
|
77
|
+
const fieldSlots = field({ size: variantProps.size, labelPosition });
|
|
78
|
+
const intl = useIntl();
|
|
79
|
+
// Fold the field's name into the stepper labels as react-aria does; the
|
|
80
|
+
// trim eats the leftover space when there is no usable name.
|
|
81
|
+
const fieldLabel =
|
|
82
|
+
rest["aria-label"] ?? (typeof label === "string" ? label : "");
|
|
83
|
+
return (
|
|
84
|
+
<RACNumberField
|
|
85
|
+
incrementAriaLabel={intl
|
|
86
|
+
.formatMessage(uiMessage("ui.numberfield-increase"), { fieldLabel })
|
|
87
|
+
.trim()}
|
|
88
|
+
decrementAriaLabel={intl
|
|
89
|
+
.formatMessage(uiMessage("ui.numberfield-decrease"), { fieldLabel })
|
|
90
|
+
.trim()}
|
|
91
|
+
{...rest}
|
|
92
|
+
className={cx(
|
|
93
|
+
fieldSlots.root,
|
|
94
|
+
slots.root,
|
|
95
|
+
rootCss ? css(rootCss) : undefined,
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{label != null && (
|
|
99
|
+
<FieldLabel
|
|
100
|
+
size={variantProps.size}
|
|
101
|
+
labelPosition={labelPosition}
|
|
102
|
+
isRequired={rest.isRequired}
|
|
103
|
+
css={labelCss}
|
|
104
|
+
>
|
|
105
|
+
{label}
|
|
106
|
+
</FieldLabel>
|
|
107
|
+
)}
|
|
108
|
+
<RACGroup
|
|
109
|
+
className={cx(slots.group, groupCss ? css(groupCss) : undefined)}
|
|
110
|
+
>
|
|
111
|
+
<RACInput
|
|
112
|
+
ref={ref}
|
|
113
|
+
className={cx(
|
|
114
|
+
input(variantProps),
|
|
115
|
+
// Room for the stepper column (constant across sizes, as the
|
|
116
|
+
// stepper's width is).
|
|
117
|
+
css({ paddingEnd: "6" }, inputCss),
|
|
118
|
+
)}
|
|
119
|
+
/>
|
|
120
|
+
<div className={slots.stepper}>
|
|
121
|
+
<RACButton slot="increment" className={slots.stepperButton}>
|
|
122
|
+
<Icon as={RiArrowUpSFill} />
|
|
123
|
+
</RACButton>
|
|
124
|
+
<RACButton slot="decrement" className={slots.stepperButton}>
|
|
125
|
+
<Icon as={RiArrowDownSFill} />
|
|
126
|
+
</RACButton>
|
|
127
|
+
</div>
|
|
128
|
+
</RACGroup>
|
|
129
|
+
<FieldSupport
|
|
130
|
+
helperText={helperText}
|
|
131
|
+
errorMessage={errorMessage}
|
|
132
|
+
helperTextCss={helperTextCss}
|
|
133
|
+
labelPosition={labelPosition}
|
|
134
|
+
/>
|
|
135
|
+
</RACNumberField>
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
);
|
package/src/PopoverArrow.tsx
CHANGED
|
@@ -13,18 +13,32 @@ 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 {
|
|
24
39
|
/**
|
|
25
40
|
* Base width of the visible triangle in px; it protrudes half this into
|
|
26
|
-
* the gap. Defaults to
|
|
27
|
-
* rotated 45°).
|
|
41
|
+
* the gap. Defaults to the proportions of an 8px square rotated 45°.
|
|
28
42
|
*/
|
|
29
43
|
size?: number;
|
|
30
44
|
/** Styles merged after the base, e.g. `{ "& svg": { fill: "white" } }`. */
|
package/src/ProgressBar.tsx
CHANGED
|
@@ -18,9 +18,8 @@ export interface ProgressBarProps {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* ProgressBar — react-aria-components <ProgressBar>
|
|
22
|
-
*
|
|
23
|
-
* `barCss` (Chakra's colorScheme).
|
|
21
|
+
* ProgressBar — a styled react-aria-components <ProgressBar> (12px gray.100
|
|
22
|
+
* track). Set the fill colour per call site via `barCss`.
|
|
24
23
|
*/
|
|
25
24
|
export const ProgressBar = ({
|
|
26
25
|
value,
|
|
@@ -49,8 +48,7 @@ export const ProgressBar = ({
|
|
|
49
48
|
className={css(
|
|
50
49
|
{
|
|
51
50
|
height: "100%",
|
|
52
|
-
bg: "
|
|
53
|
-
transition: "width 0.2s",
|
|
51
|
+
bg: "brand.500",
|
|
54
52
|
},
|
|
55
53
|
barCss,
|
|
56
54
|
)}
|
|
@@ -0,0 +1,108 @@
|
|
|
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 — the checkbox control rounded fully, with a 50%
|
|
10
|
+
* `currentColor` dot when selected instead of the check glyph, across the
|
|
11
|
+
* same sm/md/lg size scale. As with the checkbox, `borderColor: inherit`
|
|
12
|
+
* picks up a `borderColor` set on the root.
|
|
13
|
+
*
|
|
14
|
+
* State styling keys off data attributes stamped by the shared-ui Radio
|
|
15
|
+
* (react-aria provides the state via render props).
|
|
16
|
+
*
|
|
17
|
+
* Registered in the base preset (base-preset.ts), which also has the
|
|
18
|
+
* `staticCss` entry that keeps the runtime-prop size variants generated.
|
|
19
|
+
*/
|
|
20
|
+
export const radio = defineSlotRecipe({
|
|
21
|
+
className: "radio",
|
|
22
|
+
slots: ["root", "control", "label"],
|
|
23
|
+
base: {
|
|
24
|
+
root: {
|
|
25
|
+
display: "inline-flex",
|
|
26
|
+
alignItems: "center",
|
|
27
|
+
verticalAlign: "top",
|
|
28
|
+
cursor: "pointer",
|
|
29
|
+
position: "relative",
|
|
30
|
+
// As the checkbox: the accessible outline stop (WCAG 1.4.11), on the
|
|
31
|
+
// root so the control's `borderColor: inherit` reads it and call
|
|
32
|
+
// sites can still tint at the root.
|
|
33
|
+
borderColor: "gray.400",
|
|
34
|
+
"&[data-disabled]": { cursor: "not-allowed" },
|
|
35
|
+
},
|
|
36
|
+
control: {
|
|
37
|
+
display: "inline-flex",
|
|
38
|
+
alignItems: "center",
|
|
39
|
+
justifyContent: "center",
|
|
40
|
+
flexShrink: 0,
|
|
41
|
+
borderWidth: "2px",
|
|
42
|
+
borderStyle: "solid",
|
|
43
|
+
borderRadius: "full",
|
|
44
|
+
borderColor: "inherit",
|
|
45
|
+
color: "white",
|
|
46
|
+
bg: "white",
|
|
47
|
+
"&[data-selected]": {
|
|
48
|
+
bg: "controlCheckedBg",
|
|
49
|
+
borderColor: "controlCheckedBg",
|
|
50
|
+
color: "white",
|
|
51
|
+
_hover: {
|
|
52
|
+
bg: "controlCheckedHoverBg",
|
|
53
|
+
borderColor: "controlCheckedHoverBg",
|
|
54
|
+
},
|
|
55
|
+
// The radio dot.
|
|
56
|
+
_before: {
|
|
57
|
+
content: '""',
|
|
58
|
+
display: "inline-block",
|
|
59
|
+
position: "relative",
|
|
60
|
+
width: "50%",
|
|
61
|
+
height: "50%",
|
|
62
|
+
borderRadius: "50%",
|
|
63
|
+
bg: "currentColor",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
// Disabled greys; the selected block restates _hover so the widened
|
|
67
|
+
// native-:hover condition can't re-tint a disabled control.
|
|
68
|
+
"&[data-disabled]": {
|
|
69
|
+
bg: "gray.100",
|
|
70
|
+
borderColor: "gray.100",
|
|
71
|
+
},
|
|
72
|
+
"&[data-selected][data-disabled]": {
|
|
73
|
+
bg: "gray.200",
|
|
74
|
+
borderColor: "gray.200",
|
|
75
|
+
color: "gray.500",
|
|
76
|
+
_hover: { bg: "gray.200", borderColor: "gray.200" },
|
|
77
|
+
},
|
|
78
|
+
"&[data-focus-visible]": {
|
|
79
|
+
focusRing: "outline",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
label: {
|
|
83
|
+
userSelect: "none",
|
|
84
|
+
marginStart: "2",
|
|
85
|
+
"&[data-disabled]": { opacity: 0.4 },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
variants: {
|
|
89
|
+
// The sm/md/lg size scale; control dimensions match the checkbox's.
|
|
90
|
+
size: {
|
|
91
|
+
sm: {
|
|
92
|
+
control: { width: "3", height: "3" },
|
|
93
|
+
label: { fontSize: "sm" },
|
|
94
|
+
},
|
|
95
|
+
md: {
|
|
96
|
+
control: { width: "4", height: "4" },
|
|
97
|
+
label: { fontSize: "md" },
|
|
98
|
+
},
|
|
99
|
+
lg: {
|
|
100
|
+
control: { width: "5", height: "5" },
|
|
101
|
+
label: { fontSize: "lg" },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
defaultVariants: {
|
|
106
|
+
size: "md",
|
|
107
|
+
},
|
|
108
|
+
});
|
package/src/Radio.tsx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
} from "react-aria-components";
|
|
11
|
+
import { css, cx } from "styled-system/css";
|
|
12
|
+
import { radio, RadioVariantProps } from "styled-system/recipes";
|
|
13
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
14
|
+
|
|
15
|
+
export interface RadioProps
|
|
16
|
+
extends Omit<RACRadioProps, "className" | "children" | "style">,
|
|
17
|
+
RadioVariantProps {
|
|
18
|
+
/** Per-instance style overrides for the root, merged after the recipe. */
|
|
19
|
+
css?: SystemStyleObject;
|
|
20
|
+
className?: string;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Radio — a styled react-aria-components <Radio>. Must be rendered inside a
|
|
26
|
+
* RadioGroup. Children render as the label.
|
|
27
|
+
*/
|
|
28
|
+
export const Radio = ({
|
|
29
|
+
size,
|
|
30
|
+
css: cssProp,
|
|
31
|
+
className,
|
|
32
|
+
children,
|
|
33
|
+
...rest
|
|
34
|
+
}: RadioProps) => {
|
|
35
|
+
const slots = radio({ size });
|
|
36
|
+
return (
|
|
37
|
+
<RACRadio
|
|
38
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
39
|
+
{...rest}
|
|
40
|
+
>
|
|
41
|
+
{({ isSelected, isFocusVisible, isDisabled }) => (
|
|
42
|
+
<>
|
|
43
|
+
<span
|
|
44
|
+
className={slots.control}
|
|
45
|
+
data-selected={isSelected || undefined}
|
|
46
|
+
data-focus-visible={isFocusVisible || undefined}
|
|
47
|
+
data-disabled={isDisabled || undefined}
|
|
48
|
+
aria-hidden
|
|
49
|
+
/>
|
|
50
|
+
{children != null && (
|
|
51
|
+
<span
|
|
52
|
+
className={slots.label}
|
|
53
|
+
data-disabled={isDisabled || undefined}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</span>
|
|
57
|
+
)}
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
</RACRadio>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
RadioGroup as RACRadioGroup,
|
|
9
|
+
RadioGroupProps as RACRadioGroupProps,
|
|
10
|
+
} from "react-aria-components";
|
|
11
|
+
import { css, cx } from "styled-system/css";
|
|
12
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
13
|
+
import { FieldLabel, FieldSupport, FieldSupportProps } from "./Field";
|
|
14
|
+
|
|
15
|
+
export interface RadioGroupProps
|
|
16
|
+
extends Omit<RACRadioGroupProps, "className" | "style">,
|
|
17
|
+
FieldSupportProps {
|
|
18
|
+
/**
|
|
19
|
+
* Visible label for the group. Use `aria-label` instead where the design
|
|
20
|
+
* has none.
|
|
21
|
+
*/
|
|
22
|
+
label?: ReactNode;
|
|
23
|
+
/** Label style overrides. */
|
|
24
|
+
labelCss?: SystemStyleObject;
|
|
25
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
26
|
+
css?: SystemStyleObject;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* RadioGroup — react-aria-components <RadioGroup> for a set of Radios. Beyond
|
|
32
|
+
* the optional field chrome (label/helperText/errorMessage) it carries no
|
|
33
|
+
* styling of its own: compose with Stack for layout.
|
|
34
|
+
*/
|
|
35
|
+
export const RadioGroup = ({
|
|
36
|
+
label,
|
|
37
|
+
labelCss,
|
|
38
|
+
helperText,
|
|
39
|
+
errorMessage,
|
|
40
|
+
helperTextCss,
|
|
41
|
+
css: cssProp,
|
|
42
|
+
className,
|
|
43
|
+
children,
|
|
44
|
+
...rest
|
|
45
|
+
}: RadioGroupProps) => {
|
|
46
|
+
return (
|
|
47
|
+
<RACRadioGroup
|
|
48
|
+
className={cx(cssProp ? css(cssProp) : undefined, className)}
|
|
49
|
+
{...rest}
|
|
50
|
+
>
|
|
51
|
+
{(renderProps) => (
|
|
52
|
+
<>
|
|
53
|
+
{label != null && (
|
|
54
|
+
<FieldLabel isRequired={rest.isRequired} css={labelCss}>
|
|
55
|
+
{label}
|
|
56
|
+
</FieldLabel>
|
|
57
|
+
)}
|
|
58
|
+
{typeof children === "function" ? children(renderProps) : children}
|
|
59
|
+
<FieldSupport
|
|
60
|
+
helperText={helperText}
|
|
61
|
+
errorMessage={errorMessage}
|
|
62
|
+
helperTextCss={helperTextCss}
|
|
63
|
+
/>
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
</RACRadioGroup>
|
|
67
|
+
);
|
|
68
|
+
};
|