@microbit/ui 0.1.0-alpha.9 → 0.1.0
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 +8 -0
- package/README.md +171 -20
- package/lang/ui.ar.json +62 -0
- package/lang/ui.ca.json +43 -3
- package/lang/ui.cy.json +62 -0
- package/lang/ui.de.json +44 -4
- 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 +43 -3
- 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 +44 -4
- package/lang/ui.zh-tw.json +44 -4
- package/package.json +12 -10
- package/src/Avatar.recipe.ts +191 -0
- package/src/Avatar.tsx +278 -0
- package/src/Breadcrumb.recipe.ts +45 -0
- package/src/Breadcrumb.tsx +114 -0
- package/src/Button.recipe.ts +88 -50
- package/src/Button.tsx +67 -14
- package/src/ButtonGroup.tsx +37 -15
- package/src/Card.recipe.ts +1 -2
- package/src/Card.tsx +2 -1
- package/src/Checkbox.recipe.ts +13 -12
- package/src/Checkbox.tsx +103 -34
- package/src/CheckboxGroup.tsx +70 -0
- package/src/CloseButton.tsx +3 -3
- package/src/CloseIcon.tsx +6 -4
- package/src/Code.tsx +1 -1
- package/src/Collapse.tsx +13 -14
- package/src/ComboBox.tsx +246 -0
- package/src/Divider.tsx +40 -7
- package/src/Drawer.recipe.ts +21 -10
- package/src/Drawer.tsx +3 -4
- package/src/ExternalLink.tsx +43 -0
- package/src/Fade.tsx +18 -4
- 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 +8 -18
- package/src/Image.tsx +1 -1
- package/src/Input.recipe.ts +30 -26
- package/src/Input.tsx +16 -7
- package/src/InputGroup.tsx +9 -9
- package/src/Kbd.tsx +1 -1
- package/src/Link.tsx +3 -3
- package/src/LinkBox.tsx +2 -3
- package/src/LinkButton.tsx +14 -13
- package/src/List.tsx +5 -7
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +22 -19
- package/src/Menu.tsx +54 -28
- package/src/Modal.recipe.ts +17 -9
- package/src/Modal.tsx +123 -24
- package/src/MoreMenuButton.tsx +63 -0
- package/src/NativeSelect.tsx +37 -14
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +56 -15
- package/src/NumberField.tsx +74 -22
- package/src/PopoverArrow.tsx +1 -2
- package/src/ProgressBar.tsx +3 -5
- package/src/Radio.recipe.ts +13 -12
- package/src/Radio.tsx +2 -29
- package/src/RadioGroup.tsx +68 -0
- package/src/Select.recipe.ts +211 -0
- package/src/Select.tsx +194 -0
- package/src/SharedUIProvider.tsx +63 -7
- package/src/Skeleton.tsx +146 -0
- package/src/Slide.tsx +2 -2
- package/src/Slider.recipe.ts +27 -16
- package/src/Slider.tsx +20 -12
- package/src/Spinner.tsx +11 -9
- package/src/Svg.tsx +2 -3
- package/src/Switch.recipe.ts +40 -21
- package/src/Switch.tsx +35 -5
- package/src/Text.recipe.ts +26 -0
- package/src/Text.tsx +6 -2
- package/src/TextField.tsx +35 -38
- package/src/Toast.recipe.ts +52 -15
- package/src/Toast.tsx +147 -48
- package/src/Tooltip.recipe.ts +32 -0
- package/src/Tooltip.tsx +68 -28
- package/src/TooltipButton.tsx +229 -0
- package/src/UnmountCallback.tsx +2 -2
- package/src/VisuallyHidden.tsx +1 -1
- package/src/base-preset.ts +323 -84
- package/src/base-tokens.ts +0 -3
- package/src/button-icon.ts +4 -5
- 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 +5 -6
- package/src/hooks/useDisclosure.ts +31 -0
- package/src/hooks/useMediaQuery.ts +3 -4
- package/src/hooks/usePrevious.ts +1 -4
- package/src/index.ts +19 -3
- package/src/rac-locale.ts +33 -0
- package/src/system.ts +22 -1
- package/src/TextField.recipe.ts +0 -54
package/src/NumberField.tsx
CHANGED
|
@@ -8,57 +8,102 @@ import {
|
|
|
8
8
|
Button as RACButton,
|
|
9
9
|
Group as RACGroup,
|
|
10
10
|
Input as RACInput,
|
|
11
|
-
Label as RACLabel,
|
|
12
11
|
NumberField as RACNumberField,
|
|
13
12
|
NumberFieldProps as RACNumberFieldProps,
|
|
14
13
|
} from "react-aria-components";
|
|
14
|
+
import { useIntl } from "react-intl";
|
|
15
15
|
import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
|
|
16
16
|
import { css, cx } from "styled-system/css";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
field,
|
|
19
|
+
input,
|
|
20
|
+
InputVariantProps,
|
|
21
|
+
numberField,
|
|
22
|
+
} from "styled-system/recipes";
|
|
18
23
|
import { SystemStyleObject } from "styled-system/types";
|
|
24
|
+
import {
|
|
25
|
+
FieldLabel,
|
|
26
|
+
FieldLayoutProps,
|
|
27
|
+
FieldSupport,
|
|
28
|
+
FieldSupportProps,
|
|
29
|
+
} from "./Field";
|
|
19
30
|
import { Icon } from "./Icon";
|
|
31
|
+
import { uiMessage } from "./messages";
|
|
20
32
|
|
|
21
33
|
export interface NumberFieldProps
|
|
22
|
-
extends Omit<RACNumberFieldProps, "className" | "children" | "style"
|
|
34
|
+
extends Omit<RACNumberFieldProps, "className" | "children" | "style">,
|
|
35
|
+
InputVariantProps,
|
|
36
|
+
FieldSupportProps,
|
|
37
|
+
FieldLayoutProps {
|
|
23
38
|
/** Visible label (optional; otherwise pass `aria-label`). */
|
|
24
39
|
label?: ReactNode;
|
|
25
|
-
/** Root style overrides (
|
|
26
|
-
|
|
40
|
+
/** Root style overrides (for a label-beside-field row, prefer
|
|
41
|
+
* `labelPosition="side"`). */
|
|
42
|
+
rootCss?: SystemStyleObject;
|
|
27
43
|
/** Label style overrides. */
|
|
28
44
|
labelCss?: SystemStyleObject;
|
|
29
45
|
/** Group (input + steppers) style overrides — set `width` here. */
|
|
30
46
|
groupCss?: SystemStyleObject;
|
|
31
|
-
/** Input style overrides (
|
|
47
|
+
/** Input style overrides (for sizing, prefer the `size` prop). */
|
|
32
48
|
inputCss?: SystemStyleObject;
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
/**
|
|
36
|
-
* NumberField — react-aria-components <NumberField
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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.
|
|
40
56
|
*/
|
|
41
57
|
export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
42
58
|
function NumberField(
|
|
43
|
-
{
|
|
59
|
+
{
|
|
60
|
+
label,
|
|
61
|
+
helperText,
|
|
62
|
+
errorMessage,
|
|
63
|
+
helperTextCss,
|
|
64
|
+
rootCss,
|
|
65
|
+
labelCss,
|
|
66
|
+
groupCss,
|
|
67
|
+
inputCss,
|
|
68
|
+
labelPosition,
|
|
69
|
+
...props
|
|
70
|
+
},
|
|
44
71
|
ref,
|
|
45
72
|
) {
|
|
46
|
-
|
|
47
|
-
|
|
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 : "");
|
|
48
83
|
return (
|
|
49
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()}
|
|
50
91
|
{...rest}
|
|
51
|
-
className={cx(
|
|
92
|
+
className={cx(
|
|
93
|
+
fieldSlots.root,
|
|
94
|
+
slots.root,
|
|
95
|
+
rootCss ? css(rootCss) : undefined,
|
|
96
|
+
)}
|
|
52
97
|
>
|
|
53
98
|
{label != null && (
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
99
|
+
<FieldLabel
|
|
100
|
+
size={variantProps.size}
|
|
101
|
+
labelPosition={labelPosition}
|
|
102
|
+
isRequired={rest.isRequired}
|
|
103
|
+
css={labelCss}
|
|
59
104
|
>
|
|
60
105
|
{label}
|
|
61
|
-
</
|
|
106
|
+
</FieldLabel>
|
|
62
107
|
)}
|
|
63
108
|
<RACGroup
|
|
64
109
|
className={cx(slots.group, groupCss ? css(groupCss) : undefined)}
|
|
@@ -66,8 +111,9 @@ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
66
111
|
<RACInput
|
|
67
112
|
ref={ref}
|
|
68
113
|
className={cx(
|
|
69
|
-
input(),
|
|
70
|
-
// Room for the stepper column
|
|
114
|
+
input(variantProps),
|
|
115
|
+
// Room for the stepper column (constant across sizes, as the
|
|
116
|
+
// stepper's width is).
|
|
71
117
|
css({ paddingEnd: "6" }, inputCss),
|
|
72
118
|
)}
|
|
73
119
|
/>
|
|
@@ -80,6 +126,12 @@ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
|
|
|
80
126
|
</RACButton>
|
|
81
127
|
</div>
|
|
82
128
|
</RACGroup>
|
|
129
|
+
<FieldSupport
|
|
130
|
+
helperText={helperText}
|
|
131
|
+
errorMessage={errorMessage}
|
|
132
|
+
helperTextCss={helperTextCss}
|
|
133
|
+
labelPosition={labelPosition}
|
|
134
|
+
/>
|
|
83
135
|
</RACNumberField>
|
|
84
136
|
);
|
|
85
137
|
},
|
package/src/PopoverArrow.tsx
CHANGED
|
@@ -38,8 +38,7 @@ const arrowBase = css({
|
|
|
38
38
|
export interface PopoverArrowProps {
|
|
39
39
|
/**
|
|
40
40
|
* Base width of the visible triangle in px; it protrudes half this into
|
|
41
|
-
* the gap. Defaults to
|
|
42
|
-
* rotated 45°).
|
|
41
|
+
* the gap. Defaults to the proportions of an 8px square rotated 45°.
|
|
43
42
|
*/
|
|
44
43
|
size?: number;
|
|
45
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
|
)}
|
package/src/Radio.recipe.ts
CHANGED
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
import { defineSlotRecipe } from "@pandacss/dev";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Radio slot recipe —
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `borderColor` set on the root.
|
|
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.
|
|
14
13
|
*
|
|
15
14
|
* State styling keys off data attributes stamped by the shared-ui Radio
|
|
16
15
|
* (react-aria provides the state via render props).
|
|
@@ -28,6 +27,10 @@ export const radio = defineSlotRecipe({
|
|
|
28
27
|
verticalAlign: "top",
|
|
29
28
|
cursor: "pointer",
|
|
30
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",
|
|
31
34
|
"&[data-disabled]": { cursor: "not-allowed" },
|
|
32
35
|
},
|
|
33
36
|
control: {
|
|
@@ -35,8 +38,6 @@ export const radio = defineSlotRecipe({
|
|
|
35
38
|
alignItems: "center",
|
|
36
39
|
justifyContent: "center",
|
|
37
40
|
flexShrink: 0,
|
|
38
|
-
transitionProperty: "box-shadow",
|
|
39
|
-
transitionDuration: "normal",
|
|
40
41
|
borderWidth: "2px",
|
|
41
42
|
borderStyle: "solid",
|
|
42
43
|
borderRadius: "full",
|
|
@@ -51,7 +52,7 @@ export const radio = defineSlotRecipe({
|
|
|
51
52
|
bg: "controlCheckedHoverBg",
|
|
52
53
|
borderColor: "controlCheckedHoverBg",
|
|
53
54
|
},
|
|
54
|
-
//
|
|
55
|
+
// The radio dot.
|
|
55
56
|
_before: {
|
|
56
57
|
content: '""',
|
|
57
58
|
display: "inline-block",
|
|
@@ -62,8 +63,8 @@ export const radio = defineSlotRecipe({
|
|
|
62
63
|
bg: "currentColor",
|
|
63
64
|
},
|
|
64
65
|
},
|
|
65
|
-
//
|
|
66
|
-
//
|
|
66
|
+
// Disabled greys; the selected block restates _hover so the widened
|
|
67
|
+
// native-:hover condition can't re-tint a disabled control.
|
|
67
68
|
"&[data-disabled]": {
|
|
68
69
|
bg: "gray.100",
|
|
69
70
|
borderColor: "gray.100",
|
|
@@ -75,7 +76,7 @@ export const radio = defineSlotRecipe({
|
|
|
75
76
|
_hover: { bg: "gray.200", borderColor: "gray.200" },
|
|
76
77
|
},
|
|
77
78
|
"&[data-focus-visible]": {
|
|
78
|
-
|
|
79
|
+
focusRing: "outline",
|
|
79
80
|
},
|
|
80
81
|
},
|
|
81
82
|
label: {
|
|
@@ -85,7 +86,7 @@ export const radio = defineSlotRecipe({
|
|
|
85
86
|
},
|
|
86
87
|
},
|
|
87
88
|
variants: {
|
|
88
|
-
//
|
|
89
|
+
// The sm/md/lg size scale; control dimensions match the checkbox's.
|
|
89
90
|
size: {
|
|
90
91
|
sm: {
|
|
91
92
|
control: { width: "3", height: "3" },
|
package/src/Radio.tsx
CHANGED
|
@@ -7,38 +7,11 @@ import { ReactNode } from "react";
|
|
|
7
7
|
import {
|
|
8
8
|
Radio as RACRadio,
|
|
9
9
|
RadioProps as RACRadioProps,
|
|
10
|
-
RadioGroup as RACRadioGroup,
|
|
11
|
-
RadioGroupProps as RACRadioGroupProps,
|
|
12
10
|
} from "react-aria-components";
|
|
13
11
|
import { css, cx } from "styled-system/css";
|
|
14
12
|
import { radio, RadioVariantProps } from "styled-system/recipes";
|
|
15
13
|
import { SystemStyleObject } from "styled-system/types";
|
|
16
14
|
|
|
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
15
|
export interface RadioProps
|
|
43
16
|
extends Omit<RACRadioProps, "className" | "children" | "style">,
|
|
44
17
|
RadioVariantProps {
|
|
@@ -49,8 +22,8 @@ export interface RadioProps
|
|
|
49
22
|
}
|
|
50
23
|
|
|
51
24
|
/**
|
|
52
|
-
* Radio — react-aria-components <Radio
|
|
53
|
-
*
|
|
25
|
+
* Radio — a styled react-aria-components <Radio>. Must be rendered inside a
|
|
26
|
+
* RadioGroup. Children render as the label.
|
|
54
27
|
*/
|
|
55
28
|
export const Radio = ({
|
|
56
29
|
size,
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
// The common transition-property list, inlined (Panda has no
|
|
9
|
+
// transitionProperty token category).
|
|
10
|
+
// No box-shadow: nothing in this recipe uses one, and the focus ring's
|
|
11
|
+
// layers must never fade in.
|
|
12
|
+
const transitionCommon =
|
|
13
|
+
"background-color, border-color, color, fill, stroke, opacity, transform";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Select slot recipe — the dropdown pair, shared by `Select` (a listbox behind
|
|
17
|
+
* a button) and `ComboBox` (a listbox behind a text input). One recipe because
|
|
18
|
+
* the two differ only in what the control is: keeping them together is what
|
|
19
|
+
* stops a searchable and a non-searchable picker drifting apart visually.
|
|
20
|
+
*
|
|
21
|
+
* `trigger` matches the `input` recipe's outline field so a select sits level
|
|
22
|
+
* with a TextField beside it; `content` matches the `menu` recipe's card so
|
|
23
|
+
* every dropdown surface in the family agrees.
|
|
24
|
+
*
|
|
25
|
+
* Apps restyle it through the `variant` group — classroom's `classroom`
|
|
26
|
+
* variant is the rounded pill its join form uses.
|
|
27
|
+
*
|
|
28
|
+
* The label is not a slot here: it comes from the `field` recipe, as every
|
|
29
|
+
* other labelled field's does. The only thing that costs is a per-`variant`
|
|
30
|
+
* label style — restyle the control and let the label match the family.
|
|
31
|
+
*
|
|
32
|
+
* Registered in the base preset (base-preset.ts).
|
|
33
|
+
*/
|
|
34
|
+
export const select = defineSlotRecipe({
|
|
35
|
+
className: "select",
|
|
36
|
+
slots: [
|
|
37
|
+
"root",
|
|
38
|
+
"trigger",
|
|
39
|
+
"value",
|
|
40
|
+
"indicator",
|
|
41
|
+
"content",
|
|
42
|
+
"list",
|
|
43
|
+
"option",
|
|
44
|
+
"optionIndicator",
|
|
45
|
+
"empty",
|
|
46
|
+
],
|
|
47
|
+
base: {
|
|
48
|
+
// No layout here: the component wears the `field` recipe's root alongside
|
|
49
|
+
// this slot, and that recipe is the single owner of field-root layout
|
|
50
|
+
// (else `labelPosition` would fight this slot over `flexDirection`). The
|
|
51
|
+
// slot stays for apps and variants to target.
|
|
52
|
+
root: {},
|
|
53
|
+
trigger: {
|
|
54
|
+
display: "flex",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
justifyContent: "space-between",
|
|
57
|
+
gap: "2",
|
|
58
|
+
width: "100%",
|
|
59
|
+
minWidth: 0,
|
|
60
|
+
outline: "none",
|
|
61
|
+
appearance: "none",
|
|
62
|
+
font: "inherit",
|
|
63
|
+
textAlign: "start",
|
|
64
|
+
cursor: "pointer",
|
|
65
|
+
transitionProperty: transitionCommon,
|
|
66
|
+
transitionDuration: "normal",
|
|
67
|
+
border: "2px solid",
|
|
68
|
+
// As the input recipe (rationale there). The dropdown card below
|
|
69
|
+
// keeps gray.200 — a surface edge, not a form-control boundary.
|
|
70
|
+
borderColor: "gray.300",
|
|
71
|
+
bg: "white",
|
|
72
|
+
color: "inherit",
|
|
73
|
+
// As the input recipe, so mixed fields tint together on hover.
|
|
74
|
+
_hover: { borderColor: "gray.500" },
|
|
75
|
+
// `data-invalid` lands on the root — and, in a ComboBox, on the input —
|
|
76
|
+
// but never on the trigger: a RAC Button has no validity state, and our
|
|
77
|
+
// ComboBox control is a plain div. So it comes down from the parent.
|
|
78
|
+
// `> &` rather than a descendant selector, so an app's own invalid form
|
|
79
|
+
// wrapper cannot paint every control inside it red. Repeated `&`: the
|
|
80
|
+
// hover < focused < invalid specificity ladder, as the input recipe.
|
|
81
|
+
"&&:is([data-focused], :has(input:focus))": {
|
|
82
|
+
borderColor: "focusBorder",
|
|
83
|
+
},
|
|
84
|
+
// Border colour alone, as the input recipe.
|
|
85
|
+
"[data-invalid] > &&&": {
|
|
86
|
+
borderColor: "danger.500",
|
|
87
|
+
},
|
|
88
|
+
// Keyboard-only ring, two cases. Select's button: RAC's attribute.
|
|
89
|
+
// ComboBox: the input's RAC attributes are stripped by react-aria's
|
|
90
|
+
// synthetic blur during virtual focus, so its case combines the two
|
|
91
|
+
// signals that survive — native :focus plus the global-modality
|
|
92
|
+
// attribute the component renders (see ComboBox.tsx). No flicker
|
|
93
|
+
// during list navigation; can't match Select (no input).
|
|
94
|
+
"&&&[data-focus-visible], &&&[data-keyboard-modality]:has(input:focus)": {
|
|
95
|
+
focusRing: "outline",
|
|
96
|
+
},
|
|
97
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
98
|
+
},
|
|
99
|
+
// Whatever shows the current value: Select's SelectValue, ComboBox's
|
|
100
|
+
// input. One slot for both, so an app restyling the placeholder (say)
|
|
101
|
+
// does not have to know which kind of control it is looking at.
|
|
102
|
+
value: {
|
|
103
|
+
flex: "1",
|
|
104
|
+
minWidth: 0,
|
|
105
|
+
overflow: "hidden",
|
|
106
|
+
textOverflow: "ellipsis",
|
|
107
|
+
whiteSpace: "nowrap",
|
|
108
|
+
outline: "none",
|
|
109
|
+
bg: "transparent",
|
|
110
|
+
color: "inherit",
|
|
111
|
+
font: "inherit",
|
|
112
|
+
// RAC sets data-placeholder on SelectValue when nothing is chosen; the
|
|
113
|
+
// ComboBox input uses the real placeholder attribute.
|
|
114
|
+
"&[data-placeholder]": { color: "gray.500" },
|
|
115
|
+
_placeholder: { color: "gray.500" },
|
|
116
|
+
},
|
|
117
|
+
indicator: {
|
|
118
|
+
display: "inline-flex",
|
|
119
|
+
flexShrink: 0,
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
justifyContent: "center",
|
|
122
|
+
fontSize: "1.25em",
|
|
123
|
+
color: "inherit",
|
|
124
|
+
// No pointer-events:none here: in a ComboBox this slot is the button
|
|
125
|
+
// that opens the list. Select's is an aria-hidden span inside the
|
|
126
|
+
// trigger, so it needs no help being inert.
|
|
127
|
+
background: "transparent",
|
|
128
|
+
border: "none",
|
|
129
|
+
cursor: "pointer",
|
|
130
|
+
// No focus styling, deliberately: react-aria keeps a ComboBox's toggle
|
|
131
|
+
// button out of the tab order (the input owns the keyboard), so a ring
|
|
132
|
+
// here would only ever be reachable programmatically, and would suggest
|
|
133
|
+
// the chevron is a tab stop. The whole control shows focus instead.
|
|
134
|
+
outline: "none",
|
|
135
|
+
},
|
|
136
|
+
content: {
|
|
137
|
+
// Line the card up with the control, as a select should. `Select` gets
|
|
138
|
+
// this from RAC, whose trigger is the button it measures; `ComboBox`
|
|
139
|
+
// measures its own control and sets the width inline, because RAC's var
|
|
140
|
+
// is the *input's* width there.
|
|
141
|
+
minWidth: "var(--trigger-width)",
|
|
142
|
+
display: "flex",
|
|
143
|
+
flexDirection: "column",
|
|
144
|
+
bg: "white",
|
|
145
|
+
color: "inherit",
|
|
146
|
+
py: "2",
|
|
147
|
+
zIndex: "popover",
|
|
148
|
+
borderRadius: "md",
|
|
149
|
+
borderWidth: "1px",
|
|
150
|
+
borderColor: "gray.200",
|
|
151
|
+
boxShadow: "sm",
|
|
152
|
+
// Matches the menu recipe's fade/scale.
|
|
153
|
+
transformOrigin: "top",
|
|
154
|
+
opacity: 1,
|
|
155
|
+
transform: "scale(1)",
|
|
156
|
+
transition: "opacity 0.1s ease-out, transform 0.1s ease-out",
|
|
157
|
+
"&[data-entering]": { opacity: 0, transform: "scale(0.95)" },
|
|
158
|
+
"&[data-exiting]": { opacity: 0, transform: "scale(0.95)" },
|
|
159
|
+
_motionReduce: { transition: "none" },
|
|
160
|
+
},
|
|
161
|
+
list: {
|
|
162
|
+
outline: "none",
|
|
163
|
+
overflowY: "auto",
|
|
164
|
+
},
|
|
165
|
+
option: {
|
|
166
|
+
display: "flex",
|
|
167
|
+
alignItems: "center",
|
|
168
|
+
gap: "2",
|
|
169
|
+
py: "1.5",
|
|
170
|
+
px: "3",
|
|
171
|
+
cursor: "pointer",
|
|
172
|
+
color: "inherit",
|
|
173
|
+
outline: "none",
|
|
174
|
+
// As the menu recipe: the highlight is focus indication (no
|
|
175
|
+
// transition — it snaps with the ring, which keyboard nav adds;
|
|
176
|
+
// inset, the rows being full-bleed).
|
|
177
|
+
"&[data-focused]": { bg: "gray.100" },
|
|
178
|
+
"&[data-focus-visible]": { focusRing: "outlineInset" },
|
|
179
|
+
"&[data-pressed]": { bg: "gray.200" },
|
|
180
|
+
"&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
|
|
181
|
+
},
|
|
182
|
+
optionIndicator: {
|
|
183
|
+
display: "inline-flex",
|
|
184
|
+
flexShrink: 0,
|
|
185
|
+
alignItems: "center",
|
|
186
|
+
justifyContent: "center",
|
|
187
|
+
marginStart: "auto",
|
|
188
|
+
fontSize: "0.8em",
|
|
189
|
+
opacity: 0,
|
|
190
|
+
"[data-selected] &": { opacity: 1 },
|
|
191
|
+
},
|
|
192
|
+
empty: {
|
|
193
|
+
px: "3",
|
|
194
|
+
py: "2",
|
|
195
|
+
color: "gray.600",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
variants: {
|
|
199
|
+
// The `input` recipe's size ladder, step for step, so a Select sits level
|
|
200
|
+
// with a TextField beside it at every size. The indicator is em-sized and
|
|
201
|
+
// scales free; the dropdown card keeps one density across sizes.
|
|
202
|
+
size: {
|
|
203
|
+
lg: { trigger: { fontSize: "lg", px: "4", h: "12", borderRadius: "md" } },
|
|
204
|
+
md: { trigger: { fontSize: "md", px: "4", h: "10", borderRadius: "md" } },
|
|
205
|
+
sm: { trigger: { fontSize: "sm", px: "3", h: "8", borderRadius: "sm" } },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
defaultVariants: {
|
|
209
|
+
size: "md",
|
|
210
|
+
},
|
|
211
|
+
});
|