@microbit/ui 0.1.0-alpha.16 → 0.1.0-alpha.18

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.
@@ -0,0 +1,113 @@
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
+ * Field slot recipe — Chakra's FormControl parts (FormLabel/FormHelperText/
10
+ * FormErrorMessage, light mode), mapped onto react-aria-components'
11
+ * Label/Text/FieldError by `FieldLabel` and `FieldSupport`. Every labelled
12
+ * field in the library draws its chrome from here, including Select and
13
+ * ComboBox, whose own recipe styles only the dropdown pair; the input itself is
14
+ * styled by the `input` recipe (Input.recipe.ts).
15
+ *
16
+ * `root` is also the single owner of field-root *layout*: Select, ComboBox and
17
+ * NumberField wear it alongside their own recipe's root slot, which carries no
18
+ * layout of its own — two recipes fighting over `flexDirection` would leave
19
+ * `labelPosition` at the mercy of emission order. RadioGroup and CheckboxGroup
20
+ * deliberately don't wear it (their roots carry no layout at all).
21
+ *
22
+ * Registered in the base preset (base-preset.ts), which also has the
23
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
24
+ */
25
+ export const field = defineSlotRecipe({
26
+ className: "field",
27
+ slots: ["root", "label", "requiredIndicator", "helperText", "errorMessage"],
28
+ base: {
29
+ root: {
30
+ display: "flex",
31
+ flexDirection: "column",
32
+ alignItems: "stretch",
33
+ width: "100%",
34
+ },
35
+ label: {
36
+ display: "block",
37
+ fontSize: "md",
38
+ // Deliberately not Chakra FormLabel's `medium`: no font in the family's
39
+ // stack has a 500 face, so the two were pixel-identical on macOS and
40
+ // Windows, and the only call sites that cared overrode to `normal`
41
+ // (settings rows). See the playbook's expected behavioural deltas.
42
+ fontWeight: "normal",
43
+ marginEnd: "3",
44
+ mb: "2",
45
+ transitionProperty: "opacity",
46
+ transitionDuration: "normal",
47
+ // RAC stamps `data-disabled` on the field root and on the control, never
48
+ // on the label, so an `&[data-disabled]` rule here matches nothing — it
49
+ // has to come down from the root (gotcha #45). Direct child rather than a
50
+ // descendant selector, as the select recipe's invalid rule: an app's own
51
+ // disabled form wrapper must not be able to dim every label inside it.
52
+ "[data-disabled] > &": { opacity: 0.4 },
53
+ },
54
+ requiredIndicator: {
55
+ marginStart: "1",
56
+ color: "danger.500",
57
+ },
58
+ helperText: {
59
+ // RAC's Text renders a span, and RadioGroup/CheckboxGroup roots are not
60
+ // flex containers to blockify it, where an inline box would drop the
61
+ // margin below (gotcha #44).
62
+ display: "block",
63
+ mt: "2",
64
+ fontSize: "sm",
65
+ lineHeight: "normal",
66
+ color: "gray.600",
67
+ },
68
+ errorMessage: {
69
+ display: "flex",
70
+ alignItems: "center",
71
+ mt: "2",
72
+ fontSize: "sm",
73
+ lineHeight: "normal",
74
+ color: "danger.500",
75
+ },
76
+ },
77
+ variants: {
78
+ // The label follows its control's size (the control itself is sized by
79
+ // the `input`/`select` recipes), so one `size` prop scales the whole row.
80
+ // Chakra's FormLabel never scaled — a deliberate delta; see the playbook's
81
+ // field-chrome notes. Helper and error text stay `sm` at every size, as
82
+ // Chakra's did.
83
+ size: {
84
+ lg: { label: { fontSize: "lg" } },
85
+ md: { label: { fontSize: "md" } },
86
+ sm: { label: { fontSize: "sm" } },
87
+ },
88
+ // `side` puts the label beside its control — the settings-row pattern,
89
+ // where the label is a preference name and absorbs the free space. The
90
+ // control keeps its own width; give it one at the call site. Named after
91
+ // React Spectrum's `labelPosition`, not `orientation`, which RAC's
92
+ // RadioGroup already uses for the radios' own layout.
93
+ labelPosition: {
94
+ top: {},
95
+ side: {
96
+ root: {
97
+ flexDirection: "row",
98
+ alignItems: "center",
99
+ // Helper and error text are full-width items, so they wrap to
100
+ // their own line below the label/control pair.
101
+ flexWrap: "wrap",
102
+ },
103
+ label: { mb: "0", flex: "1 1 auto" },
104
+ helperText: { width: "100%" },
105
+ errorMessage: { width: "100%" },
106
+ },
107
+ },
108
+ },
109
+ defaultVariants: {
110
+ size: "md",
111
+ labelPosition: "top",
112
+ },
113
+ });
package/src/Field.tsx ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { HTMLAttributes, ReactNode } from "react";
7
+ import {
8
+ FieldError,
9
+ Label as RACLabel,
10
+ LabelProps as RACLabelProps,
11
+ Text as RACText,
12
+ } from "react-aria-components";
13
+ import { css, cx } from "styled-system/css";
14
+ import { field, FieldVariantProps } from "styled-system/recipes";
15
+ import { SystemStyleObject } from "styled-system/types";
16
+
17
+ /**
18
+ * The label/helper/error chrome every labelled form field shares — Chakra's
19
+ * FormControl parts, generalised out of TextField so Select, ComboBox,
20
+ * NumberField, RadioGroup and CheckboxGroup carry the same props
21
+ * (data-microbit-org's forms attach helper and error text to all of these).
22
+ */
23
+ export interface FieldSupportProps {
24
+ /** Help text below the field (Chakra's FormHelperText). */
25
+ helperText?: ReactNode;
26
+ /** Shown below the field when invalid (Chakra's FormErrorMessage). */
27
+ errorMessage?: ReactNode;
28
+ /** Per-instance style overrides for the helper text. */
29
+ helperTextCss?: SystemStyleObject;
30
+ }
31
+
32
+ /**
33
+ * `labelPosition` for the four single-control fields (TextField, NumberField,
34
+ * Select, ComboBox). Deliberately not on RadioGroup/CheckboxGroup: their roots
35
+ * carry no layout, and RAC's own `orientation` prop is a different axis there
36
+ * (it lays out the radios, not the label).
37
+ */
38
+ export interface FieldLayoutProps {
39
+ /**
40
+ * `side` puts the label beside the control — the settings-row pattern. The
41
+ * label absorbs the free space; the control keeps its own width, so give it
42
+ * one (`groupCss`, `wrapperCss` or `triggerCss` depending on the field).
43
+ * Helper and error text drop to a full-width line below the pair.
44
+ */
45
+ labelPosition?: FieldVariantProps["labelPosition"];
46
+ }
47
+
48
+ /**
49
+ * Helper text and error message for a react-aria field container. Render
50
+ * inside any RAC component with field validation context (TextField, Select,
51
+ * ComboBox, NumberField, RadioGroup, CheckboxGroup) — react-aria wires the
52
+ * description to the input's aria-describedby, and the error renders only
53
+ * while the field is invalid. Also exported for app-side composites built on
54
+ * RAC containers.
55
+ */
56
+ export const FieldSupport = ({
57
+ helperText,
58
+ errorMessage,
59
+ helperTextCss,
60
+ labelPosition,
61
+ }: FieldSupportProps & FieldLayoutProps) => {
62
+ const slots = field({ labelPosition });
63
+ return (
64
+ <>
65
+ {helperText != null && (
66
+ <RACText
67
+ slot="description"
68
+ className={cx(
69
+ slots.helperText,
70
+ helperTextCss ? css(helperTextCss) : undefined,
71
+ )}
72
+ >
73
+ {helperText}
74
+ </RACText>
75
+ )}
76
+ <FieldError className={slots.errorMessage}>{errorMessage}</FieldError>
77
+ </>
78
+ );
79
+ };
80
+
81
+ interface FieldTextProps extends HTMLAttributes<HTMLDivElement> {
82
+ children: ReactNode;
83
+ /** The field's `labelPosition`, so the text lays out to match. */
84
+ labelPosition?: FieldVariantProps["labelPosition"];
85
+ /** Per-instance style overrides. */
86
+ css?: SystemStyleObject;
87
+ }
88
+
89
+ /**
90
+ * Context-free helper text (Chakra's FormHelperText) for a control react-aria
91
+ * isn't wiring — a native select or a masked input. Inside a RAC field
92
+ * container use `FieldSupport`, which wires `aria-describedby` and validation
93
+ * for free; here the caller owns that wiring: give this an `id` and reference
94
+ * it from the control's `aria-describedby` (as `NativeSelectField` does).
95
+ */
96
+ export const FieldHelperText = ({
97
+ children,
98
+ labelPosition,
99
+ css: cssProp,
100
+ ...rest
101
+ }: FieldTextProps) => (
102
+ <div
103
+ {...rest}
104
+ className={cx(
105
+ field({ labelPosition }).helperText,
106
+ cssProp ? css(cssProp) : undefined,
107
+ )}
108
+ >
109
+ {children}
110
+ </div>
111
+ );
112
+
113
+ /**
114
+ * Context-free error message (Chakra's FormErrorMessage), the counterpart to
115
+ * `FieldHelperText` — see its note on the wiring the caller owns. RAC's
116
+ * `FieldError` renders only while its field is invalid; here that decision is
117
+ * the caller's too: render it conditionally.
118
+ */
119
+ export const FieldErrorMessage = ({
120
+ children,
121
+ labelPosition,
122
+ css: cssProp,
123
+ ...rest
124
+ }: FieldTextProps) => (
125
+ <div
126
+ {...rest}
127
+ className={cx(
128
+ field({ labelPosition }).errorMessage,
129
+ cssProp ? css(cssProp) : undefined,
130
+ )}
131
+ >
132
+ {children}
133
+ </div>
134
+ );
135
+
136
+ /**
137
+ * The required-field asterisk (Chakra's FormLabel indicator). Render inside
138
+ * the field's label when `isRequired`; aria-hidden because react-aria already
139
+ * announces requiredness from the input itself.
140
+ */
141
+ export const FieldRequiredIndicator = () => (
142
+ <span aria-hidden className={field().requiredIndicator}>
143
+ *
144
+ </span>
145
+ );
146
+
147
+ export interface FieldLabelProps
148
+ extends Omit<RACLabelProps, "className" | "children" | "style"> {
149
+ children: ReactNode;
150
+ /** Adds the required asterisk; pass the field's `isRequired`. */
151
+ isRequired?: boolean;
152
+ /** The field's `size`, so the label scales with its control. */
153
+ size?: FieldVariantProps["size"];
154
+ /** The field's `labelPosition`, so the label lays out to match. */
155
+ labelPosition?: FieldVariantProps["labelPosition"];
156
+ /** Per-instance style overrides. */
157
+ css?: SystemStyleObject;
158
+ }
159
+
160
+ /**
161
+ * A field's visible label (Chakra's FormLabel), asterisk included. Every
162
+ * labelled field in the library renders its label through this, so the `field`
163
+ * recipe is the single answer to what a label looks like — Select and ComboBox
164
+ * previously carried a near-identical `label` slot on the `select` recipe,
165
+ * which is how the two drifted.
166
+ *
167
+ * Also exported for app-side composites: inside a RAC field container the
168
+ * association is automatic, and outside one (a masked or native input) pass
169
+ * `id` and `htmlFor` yourself.
170
+ */
171
+ export const FieldLabel = ({
172
+ children,
173
+ isRequired,
174
+ size,
175
+ labelPosition,
176
+ css: cssProp,
177
+ ...rest
178
+ }: FieldLabelProps) => (
179
+ <RACLabel
180
+ {...rest}
181
+ className={cx(
182
+ field({ size, labelPosition }).label,
183
+ cssProp ? css(cssProp) : undefined,
184
+ )}
185
+ >
186
+ {children}
187
+ {isRequired ? <FieldRequiredIndicator /> : null}
188
+ </RACLabel>
189
+ );
@@ -44,6 +44,25 @@ export const heading = defineRecipe({
44
44
  // GT Walsheim in the private preset.
45
45
  variant: {
46
46
  marketing: { fontFamily: "display" },
47
+ // Page-title chrome in the accent colour (`headingAccent` — see
48
+ // base-preset.ts). Converged from classroom and data-microbit-org,
49
+ // which carried these two byte-identically app-side.
50
+ //
51
+ // `fontSize` goes through a doubled selector because `size` sets it too,
52
+ // responsively. Source order can't settle that: Panda hoists media
53
+ // queries below the base rules (gotcha #31) and emits variant rules in
54
+ // the order it meets them, so which of the two wins would depend on the
55
+ // breakpoint and on what other call sites exist. Two classes beat one at
56
+ // every width instead.
57
+ label: {
58
+ "&&": { fontSize: "4xl" },
59
+ color: "headingAccent",
60
+ },
61
+ subtitle: {
62
+ "&&": { fontSize: "xl" },
63
+ fontWeight: "normal",
64
+ color: "headingAccent",
65
+ },
47
66
  },
48
67
  },
49
68
  defaultVariants: {
@@ -17,8 +17,17 @@ const transitionCommon =
17
17
  *
18
18
  * Focus matches both native `:focus-visible` (plain inputs; browsers treat any
19
19
  * focus in a text field as focus-visible) and react-aria's `data-focused`
20
- * (inputs inside RAC TextField). Focus is declared after invalid so a focused
21
- * invalid field shows the focus ring, as in Chakra.
20
+ * (inputs inside RAC TextField).
21
+ *
22
+ * Hover, invalid and focus all set `borderColor`, so their precedence has to be
23
+ * hover < invalid < focus. Declaration order will not buy that: Panda sorts a
24
+ * recipe's state rules itself, ranking selectors against a fixed
25
+ * link/visited/focus/hover/active table, which puts `_hover` *after* focus and
26
+ * after anything the table doesn't mention (`[data-invalid]`). Equal-specificity
27
+ * rules then leave hover winning. So the ladder is spelled with repeated `&`
28
+ * instead — `&&` and `&&&` emit `.input.input` and `.input.input.input`, making
29
+ * precedence specificity rather than order, which nothing downstream can
30
+ * resort. Variants still override freely; they land in a later cascade layer.
22
31
  *
23
32
  * Registered in the base preset (base-preset.ts), which also has the
24
33
  * `staticCss` entry that keeps the runtime-prop size variants generated.
@@ -39,11 +48,11 @@ export const input = defineRecipe({
39
48
  bg: "inherit",
40
49
  color: "inherit",
41
50
  _hover: { borderColor: "gray.300" },
42
- "&[data-invalid], &:user-invalid": {
51
+ "&&:is([data-invalid], :user-invalid)": {
43
52
  borderColor: "danger.500",
44
53
  boxShadow: "0 0 0 1px token(colors.danger.500)",
45
54
  },
46
- "&:is(:focus-visible, [data-focused])": {
55
+ "&&&:is(:focus-visible, [data-focused])": {
47
56
  zIndex: 1,
48
57
  borderColor: "focusBorder",
49
58
  boxShadow: "0 0 0 1px token(colors.focusBorder)",
@@ -47,7 +47,13 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
47
47
  className={cx(
48
48
  input({ size }),
49
49
  css(
50
- { cursor: "pointer" },
50
+ // Chakra's Select field carried a 1px bottom padding its Input
51
+ // didn't (its option text sits a hair higher than input text).
52
+ {
53
+ cursor: "pointer",
54
+ paddingBottom: "1px",
55
+ _disabled: { cursor: "not-allowed" },
56
+ },
51
57
  // Room for the chevron overlay (Chakra Select's icon spacing,
52
58
  // constant across sizes).
53
59
  hideChevron ? undefined : { paddingRight: "8" },
@@ -84,6 +90,9 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
84
90
  height: "5",
85
91
  pointerEvents: "none",
86
92
  fill: "currentColor",
93
+ // The chevron sits outside the select so it doesn't inherit its
94
+ // disabled dimming; Chakra's Select icon dimmed to 0.5.
95
+ "select:disabled + &": { opacity: 0.5 },
87
96
  })}
88
97
  >
89
98
  <path d="M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z" />
@@ -0,0 +1,81 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { forwardRef, ReactNode, useId } from "react";
7
+ import { css, cx } from "styled-system/css";
8
+ import { field } from "styled-system/recipes";
9
+ import { SystemStyleObject } from "styled-system/types";
10
+ import { FieldHelperText, FieldLabel, FieldLayoutProps } from "./Field";
11
+ import { NativeSelect, NativeSelectProps } from "./NativeSelect";
12
+
13
+ export interface NativeSelectFieldProps
14
+ extends NativeSelectProps,
15
+ FieldLayoutProps {
16
+ /** Visible label, associated with the select via `htmlFor`. */
17
+ label: ReactNode;
18
+ /** Help text below the field, wired to the select's `aria-describedby`. */
19
+ helperText?: ReactNode;
20
+ /** Per-instance style overrides for the field root. */
21
+ rootCss?: SystemStyleObject;
22
+ }
23
+
24
+ /**
25
+ * NativeSelectField — a labelled `NativeSelect`, pairing the bare control with
26
+ * the field chrome the RAC fields get from react-aria, as `TextField` pairs
27
+ * with `Input`. The label association and `aria-describedby` are wired here,
28
+ * since there is no RAC context to do it; the label dims with the control
29
+ * exactly as the RAC fields' do. No `errorMessage` yet — no consumer needs
30
+ * one; render `FieldErrorMessage` beside it if yours does.
31
+ *
32
+ * With `labelPosition="side"` this is the settings row both
33
+ * `SelectFormControl`s hand-rolled: label beside a fixed-width select
34
+ * (`wrapperCss={{ width: "28ch" }}`), label absorbing the free space.
35
+ */
36
+ export const NativeSelectField = forwardRef<
37
+ HTMLSelectElement,
38
+ NativeSelectFieldProps
39
+ >(function NativeSelectField(
40
+ { label, helperText, labelPosition, rootCss, id, ...rest },
41
+ ref,
42
+ ) {
43
+ const generatedId = useId();
44
+ const selectId = id ?? generatedId;
45
+ const helperId = useId();
46
+ const describedBy =
47
+ [rest["aria-describedby"], helperText != null ? helperId : undefined]
48
+ .filter(Boolean)
49
+ .join(" ") || undefined;
50
+ return (
51
+ <div
52
+ className={cx(
53
+ field({ size: rest.size, labelPosition }).root,
54
+ rootCss ? css(rootCss) : undefined,
55
+ )}
56
+ // The field recipe's label dims off the root's data-disabled, which RAC
57
+ // stamps for the other fields; here it is restated from the attribute.
58
+ data-disabled={rest.disabled || undefined}
59
+ >
60
+ <FieldLabel
61
+ htmlFor={selectId}
62
+ size={rest.size}
63
+ labelPosition={labelPosition}
64
+ isRequired={rest.required}
65
+ >
66
+ {label}
67
+ </FieldLabel>
68
+ <NativeSelect
69
+ ref={ref}
70
+ id={selectId}
71
+ {...rest}
72
+ aria-describedby={describedBy}
73
+ />
74
+ {helperText != null && (
75
+ <FieldHelperText id={helperId} labelPosition={labelPosition}>
76
+ {helperText}
77
+ </FieldHelperText>
78
+ )}
79
+ </div>
80
+ );
81
+ });
@@ -11,18 +11,18 @@ import { defineSlotRecipe } from "@pandacss/dev";
11
11
  * two stacked buttons. Consumed by the shared-ui NumberField
12
12
  * (react-aria-components NumberField).
13
13
  *
14
- * Registered in the base preset (base-preset.ts). No variants, so it needs
15
- * no `staticCss` entry.
14
+ * Registered in the base preset (base-preset.ts), which also has the
15
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
16
16
  */
17
17
  export const numberField = defineSlotRecipe({
18
18
  className: "numberField",
19
19
  slots: ["root", "group", "stepper", "stepperButton"],
20
20
  base: {
21
- root: {
22
- display: "flex",
23
- flexDirection: "column",
24
- alignItems: "stretch",
25
- },
21
+ // No layout here: the component wears the `field` recipe's root alongside
22
+ // this slot, and that recipe is the single owner of field-root layout
23
+ // (else `labelPosition` would fight this slot over `flexDirection`). The
24
+ // slot stays for apps and variants to target.
25
+ root: {},
26
26
  group: {
27
27
  position: "relative",
28
28
  zIndex: 0,
@@ -64,4 +64,22 @@ export const numberField = defineSlotRecipe({
64
64
  "&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
65
65
  },
66
66
  },
67
+ variants: {
68
+ // Chakra's NumberInput: the stepper column stays 24px wide at every size
69
+ // (as does the input padding paired with it in NumberField.tsx); only the
70
+ // arrow glyphs scale, at 0.75 × the field's font size. The base's `xs` is
71
+ // exactly md × 0.75, so md adds nothing.
72
+ size: {
73
+ lg: {
74
+ stepperButton: { fontSize: "calc(token(fontSizes.lg) * 0.75)" },
75
+ },
76
+ md: {},
77
+ sm: {
78
+ stepperButton: { fontSize: "calc(token(fontSizes.sm) * 0.75)" },
79
+ },
80
+ },
81
+ },
82
+ defaultVariants: {
83
+ size: "md",
84
+ },
67
85
  });
@@ -8,27 +8,41 @@ 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";
15
14
  import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
16
15
  import { css, cx } from "styled-system/css";
17
- import { field, input, numberField } from "styled-system/recipes";
16
+ import {
17
+ field,
18
+ input,
19
+ InputVariantProps,
20
+ numberField,
21
+ } from "styled-system/recipes";
18
22
  import { SystemStyleObject } from "styled-system/types";
23
+ import {
24
+ FieldLabel,
25
+ FieldLayoutProps,
26
+ FieldSupport,
27
+ FieldSupportProps,
28
+ } from "./Field";
19
29
  import { Icon } from "./Icon";
20
30
 
21
31
  export interface NumberFieldProps
22
- extends Omit<RACNumberFieldProps, "className" | "children" | "style"> {
32
+ extends Omit<RACNumberFieldProps, "className" | "children" | "style">,
33
+ InputVariantProps,
34
+ FieldSupportProps,
35
+ FieldLayoutProps {
23
36
  /** Visible label (optional; otherwise pass `aria-label`). */
24
37
  label?: ReactNode;
25
- /** Root style overrides (e.g. row layout for label-beside-field forms). */
26
- css?: SystemStyleObject;
38
+ /** Root style overrides (for a label-beside-field row, prefer
39
+ * `labelPosition="side"`). */
40
+ rootCss?: SystemStyleObject;
27
41
  /** Label style overrides. */
28
42
  labelCss?: SystemStyleObject;
29
43
  /** Group (input + steppers) style overrides — set `width` here. */
30
44
  groupCss?: SystemStyleObject;
31
- /** Input style overrides (e.g. a smaller size than the recipe's md). */
45
+ /** Input style overrides (for sizing, prefer the `size` prop). */
32
46
  inputCss?: SystemStyleObject;
33
47
  }
34
48
 
@@ -40,25 +54,43 @@ export interface NumberFieldProps
40
54
  */
41
55
  export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
42
56
  function NumberField(
43
- { label, css: cssProp, labelCss, groupCss, inputCss, ...rest },
57
+ {
58
+ label,
59
+ helperText,
60
+ errorMessage,
61
+ helperTextCss,
62
+ rootCss,
63
+ labelCss,
64
+ groupCss,
65
+ inputCss,
66
+ labelPosition,
67
+ ...props
68
+ },
44
69
  ref,
45
70
  ) {
46
- const slots = numberField();
47
- const fieldSlots = field();
71
+ // As Input: forward every recipe variant group, not just `size`, so a
72
+ // preset that adds one keeps working.
73
+ const [variantProps, rest] = input.splitVariantProps(props);
74
+ const slots = numberField({ size: variantProps.size });
75
+ const fieldSlots = field({ size: variantProps.size, labelPosition });
48
76
  return (
49
77
  <RACNumberField
50
78
  {...rest}
51
- className={cx(slots.root, cssProp ? css(cssProp) : undefined)}
79
+ className={cx(
80
+ fieldSlots.root,
81
+ slots.root,
82
+ rootCss ? css(rootCss) : undefined,
83
+ )}
52
84
  >
53
85
  {label != null && (
54
- <RACLabel
55
- className={cx(
56
- fieldSlots.label,
57
- labelCss ? css(labelCss) : undefined,
58
- )}
86
+ <FieldLabel
87
+ size={variantProps.size}
88
+ labelPosition={labelPosition}
89
+ isRequired={rest.isRequired}
90
+ css={labelCss}
59
91
  >
60
92
  {label}
61
- </RACLabel>
93
+ </FieldLabel>
62
94
  )}
63
95
  <RACGroup
64
96
  className={cx(slots.group, groupCss ? css(groupCss) : undefined)}
@@ -66,8 +98,9 @@ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
66
98
  <RACInput
67
99
  ref={ref}
68
100
  className={cx(
69
- input(),
70
- // Room for the stepper column.
101
+ input(variantProps),
102
+ // Room for the stepper column (constant across sizes, as the
103
+ // stepper's width is).
71
104
  css({ paddingEnd: "6" }, inputCss),
72
105
  )}
73
106
  />
@@ -80,6 +113,12 @@ export const NumberField = forwardRef<HTMLInputElement, NumberFieldProps>(
80
113
  </RACButton>
81
114
  </div>
82
115
  </RACGroup>
116
+ <FieldSupport
117
+ helperText={helperText}
118
+ errorMessage={errorMessage}
119
+ helperTextCss={helperTextCss}
120
+ labelPosition={labelPosition}
121
+ />
83
122
  </RACNumberField>
84
123
  );
85
124
  },
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 {