@microbit/ui 0.1.0-alpha.3 → 0.1.0-alpha.31

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 (115) hide show
  1. package/LICENSE.md +40 -0
  2. package/README.md +279 -26
  3. package/lang/ui.ar.json +62 -0
  4. package/lang/ui.ca.json +43 -3
  5. package/lang/ui.cy.json +62 -0
  6. package/lang/ui.de.json +62 -0
  7. package/lang/ui.en-us.json +40 -0
  8. package/lang/ui.en.json +40 -0
  9. package/lang/ui.es-es.json +43 -3
  10. package/lang/ui.fr.json +44 -4
  11. package/lang/ui.ga-ie.json +62 -0
  12. package/lang/ui.it.json +62 -0
  13. package/lang/ui.ja.json +44 -4
  14. package/lang/ui.ko.json +44 -4
  15. package/lang/ui.lo.json +62 -0
  16. package/lang/ui.lol.json +40 -0
  17. package/lang/ui.nl.json +44 -4
  18. package/lang/ui.pl.json +44 -4
  19. package/lang/ui.pt-br.json +44 -4
  20. package/lang/ui.vi.json +62 -0
  21. package/lang/ui.zh-cn.json +62 -0
  22. package/lang/ui.zh-tw.json +44 -4
  23. package/package.json +19 -10
  24. package/postcss-legacy-safari.cjs +96 -0
  25. package/reset.css +35 -0
  26. package/src/Avatar.recipe.ts +191 -0
  27. package/src/Avatar.tsx +278 -0
  28. package/src/Breadcrumb.recipe.ts +45 -0
  29. package/src/Breadcrumb.tsx +114 -0
  30. package/src/Button.recipe.ts +88 -50
  31. package/src/Button.tsx +68 -30
  32. package/src/ButtonGroup.tsx +37 -15
  33. package/src/Card.recipe.ts +1 -2
  34. package/src/Card.tsx +2 -1
  35. package/src/Checkbox.recipe.ts +49 -14
  36. package/src/Checkbox.tsx +111 -32
  37. package/src/CheckboxGroup.tsx +70 -0
  38. package/src/CloseButton.tsx +3 -3
  39. package/src/CloseIcon.tsx +6 -4
  40. package/src/Code.tsx +20 -0
  41. package/src/Collapse.tsx +179 -0
  42. package/src/ComboBox.tsx +246 -0
  43. package/src/Divider.tsx +72 -8
  44. package/src/Drawer.recipe.ts +21 -10
  45. package/src/Drawer.tsx +3 -4
  46. package/src/ExternalLink.tsx +43 -0
  47. package/src/Fade.tsx +62 -0
  48. package/src/Field.recipe.ts +114 -0
  49. package/src/Field.tsx +187 -0
  50. package/src/GridList.recipe.ts +57 -0
  51. package/src/GridList.tsx +81 -0
  52. package/src/Heading.recipe.ts +20 -1
  53. package/src/Heading.tsx +3 -3
  54. package/src/Icon.tsx +23 -7
  55. package/src/IconButton.tsx +8 -13
  56. package/src/Image.tsx +1 -1
  57. package/src/Input.recipe.ts +41 -28
  58. package/src/Input.tsx +23 -7
  59. package/src/InputGroup.tsx +26 -12
  60. package/src/Kbd.tsx +26 -0
  61. package/src/Link.tsx +3 -3
  62. package/src/LinkBox.tsx +2 -3
  63. package/src/LinkButton.tsx +81 -0
  64. package/src/List.tsx +8 -6
  65. package/src/ListBox.recipe.ts +43 -0
  66. package/src/ListBox.tsx +88 -0
  67. package/src/Menu.recipe.ts +51 -17
  68. package/src/Menu.tsx +117 -2
  69. package/src/Modal.recipe.ts +17 -9
  70. package/src/Modal.tsx +137 -24
  71. package/src/MoreMenuButton.tsx +63 -0
  72. package/src/NativeSelect.tsx +41 -14
  73. package/src/NativeSelectField.tsx +84 -0
  74. package/src/NumberField.recipe.ts +108 -0
  75. package/src/NumberField.tsx +138 -0
  76. package/src/PopoverArrow.tsx +19 -5
  77. package/src/ProgressBar.tsx +3 -5
  78. package/src/Radio.recipe.ts +108 -0
  79. package/src/Radio.tsx +62 -0
  80. package/src/RadioGroup.tsx +68 -0
  81. package/src/Select.recipe.ts +211 -0
  82. package/src/Select.tsx +194 -0
  83. package/src/SharedUIProvider.tsx +63 -7
  84. package/src/Skeleton.tsx +146 -0
  85. package/src/Slide.tsx +2 -2
  86. package/src/Slider.recipe.ts +27 -16
  87. package/src/Slider.tsx +78 -6
  88. package/src/Spinner.tsx +11 -9
  89. package/src/Svg.tsx +2 -3
  90. package/src/Switch.recipe.ts +57 -11
  91. package/src/Switch.tsx +39 -7
  92. package/src/Text.recipe.ts +26 -0
  93. package/src/Text.tsx +6 -2
  94. package/src/TextField.tsx +54 -40
  95. package/src/Toast.recipe.ts +52 -15
  96. package/src/Toast.tsx +151 -46
  97. package/src/Tooltip.recipe.ts +32 -0
  98. package/src/Tooltip.tsx +68 -28
  99. package/src/TooltipButton.tsx +229 -0
  100. package/src/UnmountCallback.tsx +2 -2
  101. package/src/VisuallyHidden.tsx +1 -1
  102. package/src/base-preset.ts +336 -84
  103. package/src/{chakra-tokens.ts → base-tokens.ts} +7 -7
  104. package/src/button-icon.ts +22 -0
  105. package/src/data-attrs.ts +16 -0
  106. package/src/dense-preset.ts +107 -0
  107. package/src/hooks/useBreakpointValue.ts +4 -4
  108. package/src/hooks/useClipboard.ts +63 -0
  109. package/src/hooks/useDisclosure.ts +31 -0
  110. package/src/hooks/useMediaQuery.ts +27 -0
  111. package/src/hooks/usePrevious.ts +15 -0
  112. package/src/index.ts +29 -3
  113. package/src/rac-locale.ts +33 -0
  114. package/src/system.ts +22 -1
  115. package/src/TextField.recipe.ts +0 -54
@@ -6,14 +6,18 @@
6
6
  import { defineSlotRecipe } from "@pandacss/dev";
7
7
 
8
8
  /**
9
- * Slider slot recipe — Chakra's horizontal md slider (14px thumb, 4px
10
- * gray.200 track, blue filled track; light mode). Consumed by the shared-ui
11
- * Slider, which maps the slots onto react-aria-components'
12
- * Slider/SliderTrack/SliderThumb.
9
+ * Slider slot recipe — a horizontal slider (3.5-token thumb, 1-token gray.200
10
+ * track, blue filled track). Consumed by the shared-ui Slider, which maps the
11
+ * slots onto react-aria-components' Slider/SliderTrack/SliderThumb.
13
12
  *
14
- * The `mark` slot is hidden until the slider has focus (Chakra call sites did
15
- * this with `_focusWithin` + a class); the reveal lives here so call sites
16
- * only style the mark's look.
13
+ * Geometry: the root is thumb-height with NO horizontal padding, the track is
14
+ * absolutely centred inside it at full width, and the thumb overhangs the
15
+ * track ends at 0%/100%. Call sites position overlays (marks, value labels)
16
+ * against that root box. Sizes are tokens, not px, so apps with resized
17
+ * scales (e.g. python-editor's ×0.88) keep proportional geometry.
18
+ *
19
+ * The `mark` slot is hidden until the slider has focus; the reveal lives here
20
+ * so call sites only style the mark's look.
17
21
  *
18
22
  * Registered in the base preset (base-preset.ts). No variants, so it
19
23
  * needs no `staticCss` entry.
@@ -26,9 +30,9 @@ export const slider = defineSlotRecipe({
26
30
  position: "relative",
27
31
  display: "block",
28
32
  width: "100%",
33
+ // Thumb-height; the track centres inside.
34
+ height: "3.5",
29
35
  touchAction: "none",
30
- // Half the thumb so it doesn't overflow the ends (Chakra container px).
31
- px: "calc(14px / 2)",
32
36
  "&:focus-within [data-part='mark']": {
33
37
  display: "block",
34
38
  },
@@ -38,8 +42,12 @@ export const slider = defineSlotRecipe({
38
42
  },
39
43
  },
40
44
  track: {
41
- position: "relative",
42
- height: "4px",
45
+ position: "absolute",
46
+ top: "50%",
47
+ transform: "translateY(-50%)",
48
+ left: 0,
49
+ width: "100%",
50
+ height: "1",
43
51
  overflow: "hidden",
44
52
  borderRadius: "sm",
45
53
  bg: "gray.200",
@@ -52,15 +60,18 @@ export const slider = defineSlotRecipe({
52
60
  filledTrack: {
53
61
  position: "absolute",
54
62
  top: 0,
55
- left: 0,
63
+ // Logical, unlike the thumb: the fill grows from the track's start, so
64
+ // in RTL it fills from the right and the two still meet.
65
+ insetStart: 0,
56
66
  height: "100%",
57
- bg: "sliderFilledTrack",
67
+ // Fill colour is set per call site via `filledTrackCss`.
68
+ bg: "brand.500",
58
69
  },
59
70
  thumb: {
60
71
  position: "absolute",
61
72
  top: "50%",
62
- width: "14px",
63
- height: "14px",
73
+ width: "3.5",
74
+ height: "3.5",
64
75
  display: "flex",
65
76
  alignItems: "center",
66
77
  justifyContent: "center",
@@ -76,7 +87,7 @@ export const slider = defineSlotRecipe({
76
87
  },
77
88
  transform: "translate(-50%, -50%)",
78
89
  "&[data-focus-visible]": {
79
- focusShadow: "outline",
90
+ focusRing: "outline",
80
91
  },
81
92
  "&[data-disabled]": {
82
93
  bg: "gray.300",
package/src/Slider.tsx CHANGED
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  import { ReactNode } from "react";
7
+ import { useLocale } from "react-aria";
7
8
  import {
8
9
  Slider as RACSlider,
9
10
  SliderThumb,
@@ -33,16 +34,34 @@ export interface SliderProps {
33
34
  thumbCss?: SystemStyleObject;
34
35
  /**
35
36
  * Positioned at the current value along the track and shown while the
36
- * slider has focus (Chakra's SliderMark usage).
37
+ * slider has focus.
37
38
  */
38
39
  mark?: ReactNode;
39
40
  markCss?: SystemStyleObject;
41
+ /**
42
+ * Additional positioned overlays rendered inside the slider root
43
+ * (always-visible value labels, threshold markers, ...). The root is
44
+ * position: relative; position children absolutely.
45
+ *
46
+ * Anything sitting at a *value* along the track takes a percentage `left`,
47
+ * physical and mirrored by hand in RTL as the thumb is (see `offset`
48
+ * below), never `insetInlineStart`. Anything pinned to the track's *ends*
49
+ * — min/max labels and the like — is ordinary layout and should be
50
+ * logical, so `insetStart`/`insetEnd`.
51
+ */
52
+ children?: ReactNode;
53
+ /**
54
+ * Tooltip-styled bubble anchored above the thumb. Rendered only while
55
+ * `isThumbTooltipOpen`; drive it from hover/focus, e.g. via
56
+ * `onThumbFocusChange` and mouse handlers on an enclosing element.
57
+ */
58
+ thumbTooltip?: ReactNode;
59
+ isThumbTooltipOpen?: boolean;
60
+ /** Thumb focus tracking (react-aria focus events). */
61
+ onThumbFocusChange?: (isFocused: boolean) => void;
40
62
  }
41
63
 
42
- /**
43
- * Slider — react-aria-components <Slider> styled like Chakra's horizontal md
44
- * slider.
45
- */
64
+ /** Slider — a styled horizontal react-aria-components <Slider>. */
46
65
  export const Slider = ({
47
66
  value,
48
67
  onChange,
@@ -57,9 +76,19 @@ export const Slider = ({
57
76
  thumbCss,
58
77
  mark,
59
78
  markCss,
79
+ children,
80
+ thumbTooltip,
81
+ isThumbTooltipOpen,
82
+ onThumbFocusChange,
60
83
  }: SliderProps) => {
61
84
  const slots = slider();
85
+ const { direction } = useLocale();
62
86
  const percent = ((value - minValue) / (maxValue - minValue)) * 100;
87
+ // react-aria positions the thumb with a physical `left` that it mirrors
88
+ // itself in RTL (useSliderThumb), so anything tracking the thumb has to
89
+ // mirror the same way rather than going logical — `insetInlineStart` would
90
+ // put these on the opposite side of the track from the thumb.
91
+ const offset = direction === "rtl" ? 100 - percent : percent;
63
92
  return (
64
93
  <RACSlider
65
94
  value={value}
@@ -75,7 +104,7 @@ export const Slider = ({
75
104
  <div
76
105
  data-part="mark"
77
106
  className={cx(slots.mark, markCss ? css(markCss) : undefined)}
78
- style={{ left: `${percent}%` }}
107
+ style={{ left: `${offset}%` }}
79
108
  >
80
109
  {mark}
81
110
  </div>
@@ -91,9 +120,52 @@ export const Slider = ({
91
120
  style={{ width: `${percent}%` }}
92
121
  />
93
122
  </SliderTrack>
123
+ {thumbTooltip && isThumbTooltipOpen && (
124
+ // Matches the shared Tooltip's look (tooltipBase) with a bottom
125
+ // arrow, anchored above the thumb.
126
+ <div
127
+ role="presentation"
128
+ className={css({
129
+ position: "absolute",
130
+ bottom: "calc(50% + token(sizes.3.5))",
131
+ transform: "translateX(-50%)",
132
+ bg: "gray.700",
133
+ color: "white",
134
+ px: "2",
135
+ py: "1",
136
+ borderRadius: "md",
137
+ fontSize: "sm",
138
+ fontWeight: "medium",
139
+ boxShadow: "md",
140
+ zIndex: "tooltip",
141
+ whiteSpace: "nowrap",
142
+ _after: {
143
+ content: '""',
144
+ position: "absolute",
145
+ // 1px into the box so subpixel edges can't antialias into a
146
+ // hairline seam (see PopoverArrow).
147
+ top: "calc(100% - 1px)",
148
+ left: "50%",
149
+ transform: "translateX(-50%)",
150
+ borderWidth: "4px",
151
+ borderStyle: "solid",
152
+ // Per-side: tokens don't resolve inside multi-value
153
+ // shorthands (they emit verbatim and the browser drops the
154
+ // invalid declaration).
155
+ borderColor: "transparent",
156
+ borderTopColor: "gray.700",
157
+ },
158
+ })}
159
+ style={{ left: `${offset}%` }}
160
+ >
161
+ {thumbTooltip}
162
+ </div>
163
+ )}
94
164
  <SliderThumb
165
+ onFocusChange={onThumbFocusChange}
95
166
  className={cx(slots.thumb, thumbCss ? css(thumbCss) : undefined)}
96
167
  />
168
+ {children}
97
169
  </RACSlider>
98
170
  );
99
171
  };
package/src/Spinner.tsx CHANGED
@@ -5,29 +5,29 @@
5
5
  */
6
6
  import { CSSProperties } from "react";
7
7
  import { css, cx } from "styled-system/css";
8
+ import { dataAttrs } from "./data-attrs";
8
9
  import { SystemStyleObject } from "styled-system/types";
9
10
 
10
11
  export interface SpinnerProps {
11
- /** Chakra Spinner sizes: sm 1rem, md 1.5rem (default). */
12
+ /** sm is 1rem, md 1.5rem (default). */
12
13
  size?: "sm" | "md";
13
14
  /**
14
- * Revolution time (Chakra's `speed`, default 0.45s). Under
15
- * prefers-reduced-motion the spin is slowed to ~3x this value.
15
+ * Revolution time (default 0.45s). Under prefers-reduced-motion the spin is
16
+ * slowed to ~3x this value.
16
17
  */
17
18
  speed?: string;
18
19
  /** Per-instance style overrides, merged after the base. */
19
20
  css?: SystemStyleObject;
20
21
  className?: string;
21
- /**
22
- * Accessible name. Required: Chakra's Spinner announced a visually hidden
23
- * "Loading..." by default, so a nameless spinner would regress on it.
24
- */
22
+ /** Accessible name. Required: a spinner must announce what is loading. */
25
23
  "aria-label": string;
24
+ /** `data-*` attributes land on the spinner, for tests that wait on it. */
25
+ [key: `data-${string}`]: unknown;
26
26
  }
27
27
 
28
28
  /**
29
- * Spinner — Chakra's border-based spinner (currentColor with transparent
30
- * bottom/left quadrants).
29
+ * Spinner — a border-based spinner (currentColor with transparent bottom/left
30
+ * quadrants).
31
31
  */
32
32
  export const Spinner = ({
33
33
  size = "md",
@@ -35,8 +35,10 @@ export const Spinner = ({
35
35
  css: cssProp,
36
36
  className,
37
37
  "aria-label": ariaLabel,
38
+ ...rest
38
39
  }: SpinnerProps) => (
39
40
  <span
41
+ {...dataAttrs(rest)}
40
42
  role="status"
41
43
  aria-label={ariaLabel}
42
44
  style={speed ? ({ "--spinner-speed": speed } as CSSProperties) : undefined}
package/src/Svg.tsx CHANGED
@@ -6,10 +6,9 @@
6
6
  import { styled } from "styled-system/jsx";
7
7
 
8
8
  /**
9
- * Svg — a Panda-styled svg element with Chakra Icon's base sizing (1em,
9
+ * Svg — a Panda-styled svg element with Icon's base sizing (1em,
10
10
  * inline-block). For custom-path icons; react-icons glyphs use `Icon`.
11
- * As a styled-factory component its call-site style props are extracted
12
- * (unlike style props forwarded through a plain wrapper — gotcha #9).
11
+ * As a styled-factory component its call-site style props are extracted.
13
12
  */
14
13
  export const Svg = styled("svg", {
15
14
  base: {
@@ -6,15 +6,15 @@
6
6
  import { defineSlotRecipe } from "@pandacss/dev";
7
7
 
8
8
  /**
9
- * Switch slot recipe Chakra's md switch with the default blue colorScheme
10
- * (light mode). Track is 1.875rem x 1rem with a 2px inset; the thumb slides by
11
- * the track/thumb width difference when selected.
9
+ * Switch slot recipe with an sm/md/lg size scale. The track has a 2px inset;
10
+ * the thumb matches the track height and slides by the track width/height
11
+ * difference when selected.
12
12
  *
13
13
  * State styling keys off data attributes stamped by the shared-ui Switch
14
14
  * (react-aria provides the state via render props).
15
15
  *
16
- * Registered in the base preset (base-preset.ts). No variants, so it
17
- * needs no `staticCss` entry.
16
+ * Registered in the base preset (base-preset.ts), which also has the
17
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
18
18
  */
19
19
  export const switchRecipe = defineSlotRecipe({
20
20
  className: "switch",
@@ -35,8 +35,6 @@ export const switchRecipe = defineSlotRecipe({
35
35
  boxSizing: "content-box",
36
36
  borderRadius: "full",
37
37
  p: "0.5",
38
- width: "1.875rem",
39
- height: "4",
40
38
  transitionProperty: "background-color",
41
39
  transitionDuration: "fast",
42
40
  bg: "gray.300",
@@ -44,7 +42,7 @@ export const switchRecipe = defineSlotRecipe({
44
42
  bg: "controlCheckedBg",
45
43
  },
46
44
  "&[data-focus-visible]": {
47
- focusShadow: "outline",
45
+ focusRing: "outline",
48
46
  },
49
47
  "&[data-disabled]": { opacity: 0.4 },
50
48
  // Forced-colors modes strip author backgrounds, flattening track and
@@ -61,10 +59,18 @@ export const switchRecipe = defineSlotRecipe({
61
59
  transitionProperty: "transform",
62
60
  transitionDuration: "normal",
63
61
  borderRadius: "inherit",
64
- width: "4",
65
- height: "4",
62
+ // The thumb rests at the track's start and slides to its end, so the
63
+ // travel has to reverse in RTL — a transform is physical however the
64
+ // rest of the switch is laid out. Distance is per size, below.
65
+ // Fallbacks carry a unit: `calc(-1 * 0)` is a number, which is invalid
66
+ // where a length is wanted, and would drop the RTL transform whole.
66
67
  "&[data-selected]": {
67
- transform: "translateX(0.875rem)",
68
+ transform: "translateX(var(--switch-travel, 0px))",
69
+ },
70
+ _rtl: {
71
+ "&[data-selected]": {
72
+ transform: "translateX(calc(-1 * var(--switch-travel, 0px)))",
73
+ },
68
74
  },
69
75
  _forcedColors: {
70
76
  bg: "ButtonText",
@@ -77,4 +83,44 @@ export const switchRecipe = defineSlotRecipe({
77
83
  "&[data-disabled]": { opacity: 0.4 },
78
84
  },
79
85
  },
86
+ variants: {
87
+ // `start` puts the label first and the control at the row's end — the
88
+ // settings-row pattern (a preference name beside its switch), the toggle
89
+ // counterpart of the field recipe's `labelPosition="side"`. Values are
90
+ // start/end rather than top/side because the default label is already
91
+ // beside the control, after it. The label keeps an end margin so a long
92
+ // translation can't butt against the track (the SelectFormControl
93
+ // lesson — see the field-chrome roadmap bullet).
94
+ labelPosition: {
95
+ end: {},
96
+ start: {
97
+ root: {
98
+ flexDirection: "row-reverse",
99
+ justifyContent: "space-between",
100
+ width: "100%",
101
+ },
102
+ label: { marginStart: "0", marginEnd: "3" },
103
+ },
104
+ },
105
+ // Track width x height per size; the thumb's travel is the difference
106
+ // between them.
107
+ size: {
108
+ sm: {
109
+ track: { width: "1.375rem", height: "3" },
110
+ thumb: { width: "3", height: "3", "--switch-travel": "0.625rem" },
111
+ },
112
+ md: {
113
+ track: { width: "1.875rem", height: "4" },
114
+ thumb: { width: "4", height: "4", "--switch-travel": "0.875rem" },
115
+ },
116
+ lg: {
117
+ track: { width: "2.875rem", height: "6" },
118
+ thumb: { width: "6", height: "6", "--switch-travel": "1.375rem" },
119
+ },
120
+ },
121
+ },
122
+ defaultVariants: {
123
+ size: "md",
124
+ labelPosition: "end",
125
+ },
80
126
  });
package/src/Switch.tsx CHANGED
@@ -3,38 +3,59 @@
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
  Switch as RACSwitch,
9
9
  SwitchProps as RACSwitchProps,
10
10
  } from "react-aria-components";
11
11
  import { css, cx } from "styled-system/css";
12
- import { switchRecipe } from "styled-system/recipes";
12
+ import { switchRecipe, SwitchRecipeVariantProps } from "styled-system/recipes";
13
13
  import { SystemStyleObject } from "styled-system/types";
14
+ import { FieldHelperText } from "./Field";
14
15
 
15
16
  export interface SwitchProps
16
- extends Omit<RACSwitchProps, "className" | "children" | "style"> {
17
+ extends Omit<RACSwitchProps, "className" | "children" | "style">,
18
+ SwitchRecipeVariantProps {
17
19
  /** Per-instance style overrides for the root, merged after the recipe. */
18
20
  css?: SystemStyleObject;
19
21
  className?: string;
20
22
  children?: ReactNode;
23
+ /**
24
+ * Help text below the switch, wired to its `aria-describedby` — the same
25
+ * chrome the labelled fields' `helperText` renders. With it the component
26
+ * gains a wrapping `<div>`, so the switch-plus-text moves as one block.
27
+ */
28
+ helperText?: ReactNode;
29
+ /** Per-instance style overrides for the helper text. */
30
+ helperTextCss?: SystemStyleObject;
21
31
  }
22
32
 
23
33
  /**
24
- * Switch — react-aria-components <Switch> styled like Chakra's md switch.
25
- * Children render as the label; pass `aria-label` for label-less switches.
34
+ * Switch — a styled react-aria-components <Switch>. Children render as the
35
+ * label; pass `aria-label` for label-less switches. `labelPosition="start"`
36
+ * is the settings-row layout: label first, switch at the row's end.
26
37
  */
27
38
  export const Switch = ({
39
+ size,
40
+ labelPosition,
28
41
  css: cssProp,
29
42
  className,
30
43
  children,
44
+ helperText,
45
+ helperTextCss,
31
46
  ...rest
32
47
  }: SwitchProps) => {
33
- const slots = switchRecipe();
34
- return (
48
+ const slots = switchRecipe({ size, labelPosition });
49
+ const helperId = useId();
50
+ const describedBy =
51
+ [rest["aria-describedby"], helperText != null ? helperId : undefined]
52
+ .filter(Boolean)
53
+ .join(" ") || undefined;
54
+ const switchElement = (
35
55
  <RACSwitch
36
56
  className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
37
57
  {...rest}
58
+ aria-describedby={describedBy}
38
59
  >
39
60
  {({ isSelected, isFocusVisible, isDisabled }) => {
40
61
  const state = {
@@ -57,4 +78,15 @@ export const Switch = ({
57
78
  }}
58
79
  </RACSwitch>
59
80
  );
81
+ if (helperText == null) {
82
+ return switchElement;
83
+ }
84
+ return (
85
+ <div>
86
+ {switchElement}
87
+ <FieldHelperText id={helperId} css={helperTextCss}>
88
+ {helperText}
89
+ </FieldHelperText>
90
+ </div>
91
+ );
60
92
  };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { defineRecipe } from "@pandacss/dev";
7
+
8
+ /**
9
+ * Text recipe — sizes only, no default: with no `size`, Text inherits, which
10
+ * is what most family apps expect. A config recipe so an app preset can set
11
+ * an app-wide default size by extending `defaultVariants` (python-editor
12
+ * defaults to md, which its shrunken fontSize scale renders at 0.9rem).
13
+ *
14
+ * Registered in the base preset (base-preset.ts).
15
+ */
16
+ export const text = defineRecipe({
17
+ className: "text",
18
+ jsx: ["Text"],
19
+ variants: {
20
+ size: {
21
+ sm: { fontSize: "sm" },
22
+ md: { fontSize: "md" },
23
+ lg: { fontSize: "lg" },
24
+ },
25
+ },
26
+ });
package/src/Text.tsx CHANGED
@@ -4,9 +4,13 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  import { styled } from "styled-system/jsx";
7
+ import { text } from "styled-system/recipes";
7
8
 
8
9
  /**
9
10
  * Text — a paragraph that accepts Panda style props. Use `as` to render a
10
- * different element (`span`, `div`, `h2`). Replaces Chakra's <Text>.
11
+ * different element (`span`, `div`, `h2`).
12
+ * Backed by the `text` config recipe: `size` picks a type-scale size, and an
13
+ * app preset can extend the recipe's `defaultVariants` to give all its Text
14
+ * a default size (see Text.recipe.ts).
11
15
  */
12
- export const Text = styled("p");
16
+ export const Text = styled("p", text);
package/src/TextField.tsx CHANGED
@@ -5,67 +5,81 @@
5
5
  */
6
6
  import { FocusEvent, forwardRef, ReactNode } from "react";
7
7
  import {
8
- FieldError,
9
8
  Input as RACInput,
10
- Label as RACLabel,
11
- Text as RACText,
12
9
  TextField as RACTextField,
13
10
  TextFieldProps as RACTextFieldProps,
14
11
  } from "react-aria-components";
15
- import { css, cx } from "styled-system/css";
16
- import { field, input } from "styled-system/recipes";
12
+ import { field, input, InputVariantProps } from "styled-system/recipes";
17
13
  import { SystemStyleObject } from "styled-system/types";
14
+ import {
15
+ FieldLabel,
16
+ FieldLayoutProps,
17
+ FieldSupport,
18
+ FieldSupportProps,
19
+ } from "./Field";
18
20
 
19
21
  export interface TextFieldProps
20
22
  extends Omit<
21
- RACTextFieldProps,
22
- "className" | "children" | "style" | "onFocus" | "onBlur"
23
- > {
24
- /** Visible label (Chakra's FormLabel; asterisk added when `isRequired`). */
23
+ RACTextFieldProps,
24
+ "className" | "children" | "style" | "onFocus" | "onBlur"
25
+ >,
26
+ InputVariantProps,
27
+ FieldSupportProps,
28
+ FieldLayoutProps {
29
+ /** Visible label (asterisk added when `isRequired`). */
25
30
  label: ReactNode;
26
- /** Help text below the input (Chakra's FormHelperText). */
27
- helperText?: ReactNode;
28
- /** Shown below the input when `isInvalid` (Chakra's FormErrorMessage). */
29
- errorMessage?: ReactNode;
30
- /** Per-instance style overrides for the helper text. */
31
- helperTextCss?: SystemStyleObject;
31
+ /** Label style overrides. */
32
+ labelCss?: SystemStyleObject;
32
33
  onFocus?: (e: FocusEvent<HTMLInputElement>) => void;
34
+ /** Input autocapitalize attribute (react-aria's TextField omits it). */
35
+ autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters";
33
36
  }
34
37
 
35
38
  /**
36
- * TextField — a labelled single-line text input, collapsing Chakra's
37
- * FormControl/FormLabel/Input/FormHelperText/FormErrorMessage. The ref is
38
- * forwarded to the input element.
39
+ * TextField — a labelled single-line text input with optional help and error
40
+ * text. The ref is forwarded to the input element.
39
41
  */
40
42
  export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
41
43
  function TextField(
42
- { label, helperText, errorMessage, helperTextCss, onFocus, ...rest },
44
+ {
45
+ label,
46
+ labelCss,
47
+ helperText,
48
+ errorMessage,
49
+ helperTextCss,
50
+ labelPosition,
51
+ onFocus,
52
+ autoCapitalize,
53
+ ...props
54
+ },
43
55
  ref,
44
56
  ) {
45
- const slots = field();
57
+ // As Input: forward every recipe variant group, not just `size`, so a
58
+ // preset that adds one keeps working.
59
+ const [variantProps, rest] = input.splitVariantProps(props);
60
+ const slots = field({ size: variantProps.size, labelPosition });
46
61
  return (
47
62
  <RACTextField {...rest} className={slots.root}>
48
- <RACLabel className={slots.label}>
63
+ <FieldLabel
64
+ size={variantProps.size}
65
+ labelPosition={labelPosition}
66
+ isRequired={rest.isRequired}
67
+ css={labelCss}
68
+ >
49
69
  {label}
50
- {rest.isRequired ? (
51
- <span aria-hidden className={slots.requiredIndicator}>
52
- *
53
- </span>
54
- ) : null}
55
- </RACLabel>
56
- <RACInput ref={ref} className={input()} onFocus={onFocus} />
57
- {helperText && (
58
- <RACText
59
- slot="description"
60
- className={cx(
61
- slots.helperText,
62
- helperTextCss ? css(helperTextCss) : undefined,
63
- )}
64
- >
65
- {helperText}
66
- </RACText>
67
- )}
68
- <FieldError className={slots.errorMessage}>{errorMessage}</FieldError>
70
+ </FieldLabel>
71
+ <RACInput
72
+ ref={ref}
73
+ className={input(variantProps)}
74
+ onFocus={onFocus}
75
+ autoCapitalize={autoCapitalize}
76
+ />
77
+ <FieldSupport
78
+ helperText={helperText}
79
+ errorMessage={errorMessage}
80
+ helperTextCss={helperTextCss}
81
+ labelPosition={labelPosition}
82
+ />
69
83
  </RACTextField>
70
84
  );
71
85
  },