@microbit/ui 0.1.0-alpha.17 → 0.1.0-alpha.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE.md +8 -0
  2. package/README.md +34 -11
  3. package/lang/ui.ca.json +36 -0
  4. package/lang/ui.cy.json +36 -0
  5. package/lang/ui.de.json +36 -0
  6. package/lang/ui.en-us.json +36 -0
  7. package/lang/ui.en.json +28 -0
  8. package/lang/ui.es-es.json +36 -0
  9. package/lang/ui.fr.json +36 -0
  10. package/lang/ui.ga-ie.json +36 -0
  11. package/lang/ui.it.json +36 -0
  12. package/lang/ui.ja.json +36 -0
  13. package/lang/ui.ko.json +36 -0
  14. package/lang/ui.lol.json +36 -0
  15. package/lang/ui.nl.json +36 -0
  16. package/lang/ui.pl.json +36 -0
  17. package/lang/ui.pt-br.json +36 -0
  18. package/lang/ui.zh-cn.json +36 -0
  19. package/lang/ui.zh-tw.json +36 -0
  20. package/package.json +1 -1
  21. package/src/Avatar.recipe.ts +3 -1
  22. package/src/Checkbox.recipe.ts +5 -0
  23. package/src/Checkbox.tsx +42 -2
  24. package/src/CheckboxGroup.tsx +6 -10
  25. package/src/ComboBox.tsx +38 -16
  26. package/src/Field.recipe.ts +113 -0
  27. package/src/Field.tsx +189 -0
  28. package/src/Input.recipe.ts +4 -2
  29. package/src/NativeSelectField.tsx +84 -0
  30. package/src/NumberField.recipe.ts +25 -7
  31. package/src/NumberField.tsx +52 -22
  32. package/src/Radio.recipe.ts +4 -0
  33. package/src/Radio.tsx +1 -63
  34. package/src/RadioGroup.tsx +69 -0
  35. package/src/Select.recipe.ts +27 -17
  36. package/src/Select.tsx +38 -15
  37. package/src/Skeleton.tsx +5 -3
  38. package/src/Switch.recipe.ts +19 -0
  39. package/src/Switch.tsx +34 -3
  40. package/src/TextField.tsx +19 -8
  41. package/src/Toast.tsx +17 -2
  42. package/src/base-preset.ts +45 -10
  43. package/src/index.ts +3 -1
  44. package/src/FieldSupport.tsx +0 -68
  45. package/src/TextField.recipe.ts +0 -58
package/lang/ui.pl.json CHANGED
@@ -1,8 +1,44 @@
1
1
  {
2
+ "ui.breadcrumb": {
3
+ "defaultMessage": "Breadcrumb",
4
+ "description": "Accessible label for the breadcrumb navigation trail (the WAI-ARIA APG's conventional name)"
5
+ },
2
6
  "ui.close-action": {
3
7
  "defaultMessage": "Zamknij",
4
8
  "description": "Close button text or label"
5
9
  },
10
+ "ui.combobox-listbox": {
11
+ "defaultMessage": "Sugestie",
12
+ "description": "Accessible label for a ComboBox's dropdown list of suggestions"
13
+ },
14
+ "ui.combobox-trigger": {
15
+ "defaultMessage": "Wyświetlaj sugestie",
16
+ "description": "Accessible label for the button that opens a ComboBox's suggestions dropdown"
17
+ },
18
+ "ui.loading": {
19
+ "defaultMessage": "Loading",
20
+ "description": "Announced by screen readers for a button showing a loading spinner"
21
+ },
22
+ "ui.numberfield-decrease": {
23
+ "defaultMessage": "Zmniejsz {fieldLabel}",
24
+ "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
25
+ },
26
+ "ui.numberfield-increase": {
27
+ "defaultMessage": "Zwiększ {fieldLabel}",
28
+ "description": "Accessible label for a number field's increment stepper; fieldLabel is the field's label and may be empty"
29
+ },
30
+ "ui.select-placeholder": {
31
+ "defaultMessage": "Wybierz element",
32
+ "description": "Shown in a Select's trigger while nothing is chosen and the app supplies no placeholder"
33
+ },
34
+ "ui.select-row-action": {
35
+ "defaultMessage": "Zaznacz",
36
+ "description": "Accessible label for a row-selection checkbox in a list or table; screen readers read it together with the row's content"
37
+ },
38
+ "ui.toast-region": {
39
+ "defaultMessage": "{count, plural, one {# powiadomienie} few {# powiadomienia} many {# powiadomień} other {# powiadomienia}}.",
40
+ "description": "Accessible label for the notifications area; count is the number of notifications showing"
41
+ },
6
42
  "ui.toast-status-error": {
7
43
  "defaultMessage": "Error",
8
44
  "description": "Announced by screen readers before an error notification"
@@ -1,8 +1,44 @@
1
1
  {
2
+ "ui.breadcrumb": {
3
+ "defaultMessage": "Breadcrumb",
4
+ "description": "Accessible label for the breadcrumb navigation trail (the WAI-ARIA APG's conventional name)"
5
+ },
2
6
  "ui.close-action": {
3
7
  "defaultMessage": "Fechar",
4
8
  "description": "Close button text or label"
5
9
  },
10
+ "ui.combobox-listbox": {
11
+ "defaultMessage": "Sugestões",
12
+ "description": "Accessible label for a ComboBox's dropdown list of suggestions"
13
+ },
14
+ "ui.combobox-trigger": {
15
+ "defaultMessage": "Mostrar sugestões",
16
+ "description": "Accessible label for the button that opens a ComboBox's suggestions dropdown"
17
+ },
18
+ "ui.loading": {
19
+ "defaultMessage": "Loading",
20
+ "description": "Announced by screen readers for a button showing a loading spinner"
21
+ },
22
+ "ui.numberfield-decrease": {
23
+ "defaultMessage": "Diminuir {fieldLabel}",
24
+ "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
25
+ },
26
+ "ui.numberfield-increase": {
27
+ "defaultMessage": "Aumentar {fieldLabel}",
28
+ "description": "Accessible label for a number field's increment stepper; fieldLabel is the field's label and may be empty"
29
+ },
30
+ "ui.select-placeholder": {
31
+ "defaultMessage": "Selecione um item",
32
+ "description": "Shown in a Select's trigger while nothing is chosen and the app supplies no placeholder"
33
+ },
34
+ "ui.select-row-action": {
35
+ "defaultMessage": "Selecionar",
36
+ "description": "Accessible label for a row-selection checkbox in a list or table; screen readers read it together with the row's content"
37
+ },
38
+ "ui.toast-region": {
39
+ "defaultMessage": "{count, plural, one {# notificação} other {# notificações}}.",
40
+ "description": "Accessible label for the notifications area; count is the number of notifications showing"
41
+ },
6
42
  "ui.toast-status-error": {
7
43
  "defaultMessage": "Error",
8
44
  "description": "Announced by screen readers before an error notification"
@@ -1,8 +1,44 @@
1
1
  {
2
+ "ui.breadcrumb": {
3
+ "defaultMessage": "Breadcrumb",
4
+ "description": "Accessible label for the breadcrumb navigation trail (the WAI-ARIA APG's conventional name)"
5
+ },
2
6
  "ui.close-action": {
3
7
  "defaultMessage": "关闭",
4
8
  "description": "Close button text or label"
5
9
  },
10
+ "ui.combobox-listbox": {
11
+ "defaultMessage": "建议",
12
+ "description": "Accessible label for a ComboBox's dropdown list of suggestions"
13
+ },
14
+ "ui.combobox-trigger": {
15
+ "defaultMessage": "显示建议",
16
+ "description": "Accessible label for the button that opens a ComboBox's suggestions dropdown"
17
+ },
18
+ "ui.loading": {
19
+ "defaultMessage": "Loading",
20
+ "description": "Announced by screen readers for a button showing a loading spinner"
21
+ },
22
+ "ui.numberfield-decrease": {
23
+ "defaultMessage": "降低 {fieldLabel}",
24
+ "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
25
+ },
26
+ "ui.numberfield-increase": {
27
+ "defaultMessage": "提高 {fieldLabel}",
28
+ "description": "Accessible label for a number field's increment stepper; fieldLabel is the field's label and may be empty"
29
+ },
30
+ "ui.select-placeholder": {
31
+ "defaultMessage": "选择一个项目",
32
+ "description": "Shown in a Select's trigger while nothing is chosen and the app supplies no placeholder"
33
+ },
34
+ "ui.select-row-action": {
35
+ "defaultMessage": "选择",
36
+ "description": "Accessible label for a row-selection checkbox in a list or table; screen readers read it together with the row's content"
37
+ },
38
+ "ui.toast-region": {
39
+ "defaultMessage": "{count, plural, one {# 个通知} other {# 个通知}}。",
40
+ "description": "Accessible label for the notifications area; count is the number of notifications showing"
41
+ },
6
42
  "ui.toast-status-error": {
7
43
  "defaultMessage": "Error",
8
44
  "description": "Announced by screen readers before an error notification"
@@ -1,8 +1,44 @@
1
1
  {
2
+ "ui.breadcrumb": {
3
+ "defaultMessage": "Breadcrumb",
4
+ "description": "Accessible label for the breadcrumb navigation trail (the WAI-ARIA APG's conventional name)"
5
+ },
2
6
  "ui.close-action": {
3
7
  "defaultMessage": "關閉",
4
8
  "description": "Close button text or label"
5
9
  },
10
+ "ui.combobox-listbox": {
11
+ "defaultMessage": "建議",
12
+ "description": "Accessible label for a ComboBox's dropdown list of suggestions"
13
+ },
14
+ "ui.combobox-trigger": {
15
+ "defaultMessage": "顯示建議",
16
+ "description": "Accessible label for the button that opens a ComboBox's suggestions dropdown"
17
+ },
18
+ "ui.loading": {
19
+ "defaultMessage": "Loading",
20
+ "description": "Announced by screen readers for a button showing a loading spinner"
21
+ },
22
+ "ui.numberfield-decrease": {
23
+ "defaultMessage": "縮小 {fieldLabel}",
24
+ "description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
25
+ },
26
+ "ui.numberfield-increase": {
27
+ "defaultMessage": "放大 {fieldLabel}",
28
+ "description": "Accessible label for a number field's increment stepper; fieldLabel is the field's label and may be empty"
29
+ },
30
+ "ui.select-placeholder": {
31
+ "defaultMessage": "選取項目",
32
+ "description": "Shown in a Select's trigger while nothing is chosen and the app supplies no placeholder"
33
+ },
34
+ "ui.select-row-action": {
35
+ "defaultMessage": "選取",
36
+ "description": "Accessible label for a row-selection checkbox in a list or table; screen readers read it together with the row's content"
37
+ },
38
+ "ui.toast-region": {
39
+ "defaultMessage": "{count, plural, one {# 個通知} other {# 個通知}}。",
40
+ "description": "Accessible label for the notifications area; count is the number of notifications showing"
41
+ },
6
42
  "ui.toast-status-error": {
7
43
  "defaultMessage": "Error",
8
44
  "description": "Announced by screen readers before an error notification"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microbit/ui",
3
- "version": "0.1.0-alpha.17",
3
+ "version": "0.1.0-alpha.19",
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",
@@ -44,7 +44,9 @@ export const avatar = defineSlotRecipe({
44
44
  fontWeight: "medium",
45
45
  borderRadius: "full",
46
46
  // Chakra's no-name defaults; the name-derived pair arrives inline.
47
- background: "var(--avatar-bg, token(colors.gray.400))",
47
+ // gray.350 is the decorative-fill stop — 400+ are reserved for
48
+ // accessible outlines and text (see the ramp in base-preset.ts).
49
+ background: "var(--avatar-bg, token(colors.gray.350))",
48
50
  color: "var(--avatar-color, token(colors.white))",
49
51
  borderColor: "white",
50
52
  },
@@ -27,6 +27,11 @@ export const checkbox = defineSlotRecipe({
27
27
  verticalAlign: "top",
28
28
  cursor: "pointer",
29
29
  position: "relative",
30
+ // The accessible outline stop (WCAG 1.4.11), as the input recipe.
31
+ // Stated here rather than on the control — whose `borderColor:
32
+ // inherit` reads it — so a call site can still tint the whole control
33
+ // by setting borderColor on the root.
34
+ borderColor: "gray.400",
30
35
  "&[data-disabled]": { cursor: "not-allowed" },
31
36
  },
32
37
  control: {
package/src/Checkbox.tsx CHANGED
@@ -3,14 +3,17 @@
3
3
  *
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
- import { ReactNode } from "react";
6
+ import { ReactNode, useId } from "react";
7
7
  import {
8
8
  Checkbox as RACCheckbox,
9
9
  CheckboxProps as RACCheckboxProps,
10
10
  } from "react-aria-components";
11
+ import { useIntl } from "react-intl";
11
12
  import { css, cx } from "styled-system/css";
12
13
  import { checkbox, CheckboxVariantProps } from "styled-system/recipes";
13
14
  import { SystemStyleObject } from "styled-system/types";
15
+ import { FieldHelperText } from "./Field";
16
+ import { uiMessage } from "./messages";
14
17
 
15
18
  /** What a render-prop child is told about the checkbox. */
16
19
  export interface CheckboxState {
@@ -39,6 +42,14 @@ export interface CheckboxProps
39
42
  * @default true
40
43
  */
41
44
  control?: boolean;
45
+ /**
46
+ * Help text below the checkbox, wired to its `aria-describedby` — the same
47
+ * chrome the labelled fields' `helperText` renders. With it the component
48
+ * gains a wrapping `<div>`, so the checkbox-plus-text moves as one block.
49
+ */
50
+ helperText?: ReactNode;
51
+ /** Per-instance style overrides for the helper text. */
52
+ helperTextCss?: SystemStyleObject;
42
53
  }
43
54
 
44
55
  /**
@@ -52,13 +63,31 @@ export const Checkbox = ({
52
63
  className,
53
64
  children,
54
65
  control,
66
+ helperText,
67
+ helperTextCss,
55
68
  ...rest
56
69
  }: CheckboxProps) => {
57
70
  const slots = checkbox({ size });
58
- return (
71
+ const helperId = useId();
72
+ const intl = useIntl();
73
+ const describedBy =
74
+ [rest["aria-describedby"], helperText != null ? helperId : undefined]
75
+ .filter(Boolean)
76
+ .join(" ") || undefined;
77
+ // Default name for a row-selection checkbox (a GridList row's). A future
78
+ // Table's select-all header checkbox shares the slot name and will need a
79
+ // different label.
80
+ const ariaLabel =
81
+ rest["aria-label"] ??
82
+ (rest.slot === "selection"
83
+ ? intl.formatMessage(uiMessage("ui.select-row-action"))
84
+ : undefined);
85
+ const checkboxElement = (
59
86
  <RACCheckbox
60
87
  className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
61
88
  {...rest}
89
+ aria-label={ariaLabel}
90
+ aria-describedby={describedBy}
62
91
  >
63
92
  {({ isSelected, isFocusVisible, isDisabled }) => {
64
93
  const content =
@@ -103,4 +132,15 @@ export const Checkbox = ({
103
132
  }}
104
133
  </RACCheckbox>
105
134
  );
135
+ if (helperText == null) {
136
+ return checkboxElement;
137
+ }
138
+ return (
139
+ <div>
140
+ {checkboxElement}
141
+ <FieldHelperText id={helperId} css={helperTextCss}>
142
+ {helperText}
143
+ </FieldHelperText>
144
+ </div>
145
+ );
106
146
  };
@@ -7,16 +7,10 @@ import { ReactNode } from "react";
7
7
  import {
8
8
  CheckboxGroup as RACCheckboxGroup,
9
9
  CheckboxGroupProps as RACCheckboxGroupProps,
10
- Label as RACLabel,
11
10
  } from "react-aria-components";
12
11
  import { css, cx } from "styled-system/css";
13
- import { field } from "styled-system/recipes";
14
12
  import { SystemStyleObject } from "styled-system/types";
15
- import {
16
- FieldRequiredIndicator,
17
- FieldSupport,
18
- FieldSupportProps,
19
- } from "./FieldSupport";
13
+ import { FieldLabel, FieldSupport, FieldSupportProps } from "./Field";
20
14
 
21
15
  export interface CheckboxGroupProps
22
16
  extends Omit<RACCheckboxGroupProps, "className" | "style">,
@@ -26,6 +20,8 @@ export interface CheckboxGroupProps
26
20
  * `aria-label` instead where the design has none.
27
21
  */
28
22
  label?: ReactNode;
23
+ /** Label style overrides. */
24
+ labelCss?: SystemStyleObject;
29
25
  /** Per-instance style overrides. */
30
26
  css?: SystemStyleObject;
31
27
  className?: string;
@@ -42,6 +38,7 @@ export interface CheckboxGroupProps
42
38
  */
43
39
  export const CheckboxGroup = ({
44
40
  label,
41
+ labelCss,
45
42
  helperText,
46
43
  errorMessage,
47
44
  helperTextCss,
@@ -58,10 +55,9 @@ export const CheckboxGroup = ({
58
55
  {(renderProps) => (
59
56
  <>
60
57
  {label != null && (
61
- <RACLabel className={field().label}>
58
+ <FieldLabel isRequired={rest.isRequired} css={labelCss}>
62
59
  {label}
63
- {rest.isRequired ? <FieldRequiredIndicator /> : null}
64
- </RACLabel>
60
+ </FieldLabel>
65
61
  )}
66
62
  {typeof children === "function" ? children(renderProps) : children}
67
63
  <FieldSupport
package/src/ComboBox.tsx CHANGED
@@ -16,29 +16,34 @@ import {
16
16
  ComboBox as RACComboBox,
17
17
  ComboBoxProps as RACComboBoxProps,
18
18
  Input as RACInput,
19
- Label as RACLabel,
20
19
  ListBox as RACListBox,
21
20
  Popover,
22
21
  PopoverProps,
23
22
  } from "react-aria-components";
23
+ import { useIntl } from "react-intl";
24
24
  import { RiArrowDownSLine } from "react-icons/ri";
25
25
  import { css, cx } from "styled-system/css";
26
- import { select, SelectVariantProps } from "styled-system/recipes";
26
+ import { field, select, SelectVariantProps } from "styled-system/recipes";
27
27
  import { SystemStyleObject } from "styled-system/types";
28
28
  import {
29
- FieldRequiredIndicator,
29
+ FieldLabel,
30
+ FieldLayoutProps,
30
31
  FieldSupport,
31
32
  FieldSupportProps,
32
- } from "./FieldSupport";
33
+ } from "./Field";
33
34
  import { Icon } from "./Icon";
35
+ import { uiMessage } from "./messages";
34
36
  import { SelectSlotProvider } from "./Select";
35
37
 
36
38
  export interface ComboBoxProps<T extends object>
37
39
  extends Omit<RACComboBoxProps<T>, "className" | "children" | "style">,
38
40
  SelectVariantProps,
39
- FieldSupportProps {
41
+ FieldSupportProps,
42
+ FieldLayoutProps {
40
43
  /** Visible label. Use `aria-label` instead where the design has none. */
41
44
  label?: ReactNode;
45
+ /** Label style overrides. */
46
+ labelCss?: SystemStyleObject;
42
47
  placeholder?: string;
43
48
  /**
44
49
  * Rendered inside the control, before the input — an icon for the current
@@ -81,11 +86,12 @@ export interface ComboBoxProps<T extends object>
81
86
  */
82
87
  maxHeight?: number;
83
88
  /**
84
- * Per-instance overrides for the control — the box around the input, its
85
- * `startContent` and its indicator, which is what `Select`'s `css` styles
86
- * too. Reach the input itself through the `select` recipe's `value` slot.
89
+ * Per-instance overrides for the trigger — the box around the input, its
90
+ * `startContent` and its indicator, the same slot `Select`'s `triggerCss`
91
+ * styles. Reach the input itself through the `select` recipe's `value`
92
+ * slot.
87
93
  */
88
- css?: SystemStyleObject;
94
+ triggerCss?: SystemStyleObject;
89
95
  /** Per-instance overrides for the dropdown card. */
90
96
  contentCss?: SystemStyleObject;
91
97
  className?: string;
@@ -103,6 +109,7 @@ export interface ComboBoxProps<T extends object>
103
109
  const ComboBoxInner = <T extends object>(
104
110
  {
105
111
  label,
112
+ labelCss,
106
113
  placeholder,
107
114
  startContent,
108
115
  children,
@@ -114,7 +121,8 @@ const ComboBoxInner = <T extends object>(
114
121
  helperText,
115
122
  errorMessage,
116
123
  helperTextCss,
117
- css: cssProp,
124
+ labelPosition,
125
+ triggerCss,
118
126
  contentCss,
119
127
  className,
120
128
  ...props
@@ -123,7 +131,9 @@ const ComboBoxInner = <T extends object>(
123
131
  ) => {
124
132
  // As Select: forward whatever variant groups the merged recipe has.
125
133
  const [variantProps, rest] = select.splitVariantProps(props);
134
+ const intl = useIntl();
126
135
  const slots = select(variantProps);
136
+ const fieldSlots = field({ size: variantProps.size, labelPosition });
127
137
  // Anchor the card to the whole control, not to the bare input inside it —
128
138
  // otherwise it hangs off the text baseline and is as narrow as the input.
129
139
  const triggerRef = useRef<HTMLDivElement>(null);
@@ -152,17 +162,24 @@ const ComboBoxInner = <T extends object>(
152
162
  <RACComboBox
153
163
  allowsEmptyCollection={emptyState != null}
154
164
  {...(rest as RACComboBoxProps<T>)}
155
- className={cx(slots.root, className)}
165
+ className={cx(fieldSlots.root, slots.root, className)}
156
166
  >
157
167
  {label != null && (
158
- <RACLabel className={slots.label}>
168
+ <FieldLabel
169
+ size={variantProps.size}
170
+ labelPosition={labelPosition}
171
+ isRequired={props.isRequired}
172
+ css={labelCss}
173
+ >
159
174
  {label}
160
- {props.isRequired ? <FieldRequiredIndicator /> : null}
161
- </RACLabel>
175
+ </FieldLabel>
162
176
  )}
163
177
  <div
164
178
  ref={triggerRef}
165
- className={cx(slots.trigger, cssProp ? css(cssProp) : undefined)}
179
+ className={cx(
180
+ slots.trigger,
181
+ triggerCss ? css(triggerCss) : undefined,
182
+ )}
166
183
  >
167
184
  {startContent}
168
185
  <RACInput
@@ -171,7 +188,10 @@ const ComboBoxInner = <T extends object>(
171
188
  className={slots.value}
172
189
  />
173
190
  {indicator !== null && (
174
- <RACButton className={slots.indicator}>
191
+ <RACButton
192
+ aria-label={intl.formatMessage(uiMessage("ui.combobox-trigger"))}
193
+ className={slots.indicator}
194
+ >
175
195
  {indicator ?? <Icon as={RiArrowDownSLine} />}
176
196
  </RACButton>
177
197
  )}
@@ -188,6 +208,7 @@ const ComboBoxInner = <T extends object>(
188
208
  )}
189
209
  >
190
210
  <RACListBox
211
+ aria-label={intl.formatMessage(uiMessage("ui.combobox-listbox"))}
191
212
  className={slots.list}
192
213
  renderEmptyState={
193
214
  emptyState
@@ -203,6 +224,7 @@ const ComboBoxInner = <T extends object>(
203
224
  helperText={helperText}
204
225
  errorMessage={errorMessage}
205
226
  helperTextCss={helperTextCss}
227
+ labelPosition={labelPosition}
206
228
  />
207
229
  </RACComboBox>
208
230
  </SelectSlotProvider>
@@ -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
+ });