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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE.md +40 -0
  2. package/README.md +256 -26
  3. package/lang/ui.ca.json +43 -3
  4. package/lang/ui.cy.json +62 -0
  5. package/lang/ui.de.json +62 -0
  6. package/lang/ui.en-us.json +40 -0
  7. package/lang/ui.en.json +40 -0
  8. package/lang/ui.es-es.json +43 -3
  9. package/lang/ui.fr.json +44 -4
  10. package/lang/ui.ga-ie.json +62 -0
  11. package/lang/ui.it.json +62 -0
  12. package/lang/ui.ja.json +44 -4
  13. package/lang/ui.ko.json +44 -4
  14. package/lang/ui.lo.json +62 -0
  15. package/lang/ui.lol.json +40 -0
  16. package/lang/ui.nl.json +44 -4
  17. package/lang/ui.pl.json +44 -4
  18. package/lang/ui.pt-br.json +44 -4
  19. package/lang/ui.vi.json +62 -0
  20. package/lang/ui.zh-cn.json +62 -0
  21. package/lang/ui.zh-tw.json +44 -4
  22. package/package.json +19 -10
  23. package/postcss-legacy-safari.cjs +96 -0
  24. package/reset.css +35 -0
  25. package/src/Avatar.recipe.ts +166 -0
  26. package/src/Avatar.tsx +278 -0
  27. package/src/Breadcrumb.recipe.ts +39 -0
  28. package/src/Breadcrumb.tsx +114 -0
  29. package/src/Button.recipe.ts +88 -50
  30. package/src/Button.tsx +64 -26
  31. package/src/ButtonGroup.tsx +37 -15
  32. package/src/Card.recipe.ts +1 -2
  33. package/src/Card.tsx +2 -1
  34. package/src/Checkbox.recipe.ts +49 -14
  35. package/src/Checkbox.tsx +111 -32
  36. package/src/CheckboxGroup.tsx +70 -0
  37. package/src/CloseButton.tsx +3 -3
  38. package/src/CloseIcon.tsx +6 -4
  39. package/src/Code.tsx +20 -0
  40. package/src/Collapse.tsx +179 -0
  41. package/src/ComboBox.tsx +246 -0
  42. package/src/Divider.tsx +72 -8
  43. package/src/Drawer.recipe.ts +4 -5
  44. package/src/Drawer.tsx +3 -4
  45. package/src/ExternalLink.tsx +43 -0
  46. package/src/Fade.tsx +62 -0
  47. package/src/Field.recipe.ts +114 -0
  48. package/src/Field.tsx +187 -0
  49. package/src/GridList.recipe.ts +57 -0
  50. package/src/GridList.tsx +81 -0
  51. package/src/Heading.recipe.ts +20 -1
  52. package/src/Heading.tsx +3 -3
  53. package/src/Icon.tsx +23 -7
  54. package/src/IconButton.tsx +7 -12
  55. package/src/Image.tsx +1 -1
  56. package/src/Input.recipe.ts +41 -28
  57. package/src/Input.tsx +23 -7
  58. package/src/InputGroup.tsx +21 -8
  59. package/src/Kbd.tsx +26 -0
  60. package/src/Link.tsx +3 -3
  61. package/src/LinkBox.tsx +2 -3
  62. package/src/LinkButton.tsx +81 -0
  63. package/src/List.tsx +8 -6
  64. package/src/ListBox.recipe.ts +43 -0
  65. package/src/ListBox.tsx +88 -0
  66. package/src/Menu.recipe.ts +51 -17
  67. package/src/Menu.tsx +117 -2
  68. package/src/Modal.recipe.ts +14 -9
  69. package/src/Modal.tsx +137 -24
  70. package/src/MoreMenuButton.tsx +63 -0
  71. package/src/NativeSelect.tsx +39 -12
  72. package/src/NativeSelectField.tsx +84 -0
  73. package/src/NumberField.recipe.ts +108 -0
  74. package/src/NumberField.tsx +138 -0
  75. package/src/PopoverArrow.tsx +19 -5
  76. package/src/ProgressBar.tsx +3 -5
  77. package/src/Radio.recipe.ts +108 -0
  78. package/src/Radio.tsx +62 -0
  79. package/src/RadioGroup.tsx +68 -0
  80. package/src/Select.recipe.ts +211 -0
  81. package/src/Select.tsx +194 -0
  82. package/src/SharedUIProvider.tsx +51 -7
  83. package/src/Skeleton.tsx +146 -0
  84. package/src/Slide.tsx +2 -2
  85. package/src/Slider.recipe.ts +24 -15
  86. package/src/Slider.tsx +65 -5
  87. package/src/Spinner.tsx +11 -9
  88. package/src/Svg.tsx +2 -3
  89. package/src/Switch.recipe.ts +58 -13
  90. package/src/Switch.tsx +39 -7
  91. package/src/Text.recipe.ts +26 -0
  92. package/src/Text.tsx +6 -2
  93. package/src/TextField.tsx +54 -40
  94. package/src/Toast.recipe.ts +49 -12
  95. package/src/Toast.tsx +151 -46
  96. package/src/Tooltip.recipe.ts +32 -0
  97. package/src/Tooltip.tsx +51 -28
  98. package/src/TooltipButton.tsx +229 -0
  99. package/src/UnmountCallback.tsx +2 -2
  100. package/src/VisuallyHidden.tsx +1 -1
  101. package/src/base-preset.ts +336 -84
  102. package/src/{chakra-tokens.ts → base-tokens.ts} +7 -7
  103. package/src/button-icon.ts +22 -0
  104. package/src/data-attrs.ts +16 -0
  105. package/src/dense-preset.ts +107 -0
  106. package/src/hooks/useBreakpointValue.ts +4 -4
  107. package/src/hooks/useClipboard.ts +63 -0
  108. package/src/hooks/useDisclosure.ts +31 -0
  109. package/src/hooks/useMediaQuery.ts +27 -0
  110. package/src/hooks/usePrevious.ts +15 -0
  111. package/src/index.ts +29 -3
  112. package/src/rac-locale.ts +33 -0
  113. package/src/system.ts +22 -1
  114. 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",
@@ -54,13 +62,14 @@ export const slider = defineSlotRecipe({
54
62
  top: 0,
55
63
  left: 0,
56
64
  height: "100%",
57
- bg: "sliderFilledTrack",
65
+ // Fill colour is set per call site via `filledTrackCss`.
66
+ bg: "brand.500",
58
67
  },
59
68
  thumb: {
60
69
  position: "absolute",
61
70
  top: "50%",
62
- width: "14px",
63
- height: "14px",
71
+ width: "3.5",
72
+ height: "3.5",
64
73
  display: "flex",
65
74
  alignItems: "center",
66
75
  justifyContent: "center",
@@ -76,7 +85,7 @@ export const slider = defineSlotRecipe({
76
85
  },
77
86
  transform: "translate(-50%, -50%)",
78
87
  "&[data-focus-visible]": {
79
- focusShadow: "outline",
88
+ focusRing: "outline",
80
89
  },
81
90
  "&[data-disabled]": {
82
91
  bg: "gray.300",
package/src/Slider.tsx CHANGED
@@ -33,16 +33,29 @@ export interface SliderProps {
33
33
  thumbCss?: SystemStyleObject;
34
34
  /**
35
35
  * Positioned at the current value along the track and shown while the
36
- * slider has focus (Chakra's SliderMark usage).
36
+ * slider has focus.
37
37
  */
38
38
  mark?: ReactNode;
39
39
  markCss?: SystemStyleObject;
40
+ /**
41
+ * Additional positioned overlays rendered inside the slider root
42
+ * (always-visible value labels, threshold markers, ...). The root is
43
+ * position: relative; position children absolutely, typically with a
44
+ * percentage `left` for the track position.
45
+ */
46
+ children?: ReactNode;
47
+ /**
48
+ * Tooltip-styled bubble anchored above the thumb. Rendered only while
49
+ * `isThumbTooltipOpen`; drive it from hover/focus, e.g. via
50
+ * `onThumbFocusChange` and mouse handlers on an enclosing element.
51
+ */
52
+ thumbTooltip?: ReactNode;
53
+ isThumbTooltipOpen?: boolean;
54
+ /** Thumb focus tracking (react-aria focus events). */
55
+ onThumbFocusChange?: (isFocused: boolean) => void;
40
56
  }
41
57
 
42
- /**
43
- * Slider — react-aria-components <Slider> styled like Chakra's horizontal md
44
- * slider.
45
- */
58
+ /** Slider — a styled horizontal react-aria-components <Slider>. */
46
59
  export const Slider = ({
47
60
  value,
48
61
  onChange,
@@ -57,6 +70,10 @@ export const Slider = ({
57
70
  thumbCss,
58
71
  mark,
59
72
  markCss,
73
+ children,
74
+ thumbTooltip,
75
+ isThumbTooltipOpen,
76
+ onThumbFocusChange,
60
77
  }: SliderProps) => {
61
78
  const slots = slider();
62
79
  const percent = ((value - minValue) / (maxValue - minValue)) * 100;
@@ -91,9 +108,52 @@ export const Slider = ({
91
108
  style={{ width: `${percent}%` }}
92
109
  />
93
110
  </SliderTrack>
111
+ {thumbTooltip && isThumbTooltipOpen && (
112
+ // Matches the shared Tooltip's look (tooltipBase) with a bottom
113
+ // arrow, anchored above the thumb.
114
+ <div
115
+ role="presentation"
116
+ className={css({
117
+ position: "absolute",
118
+ bottom: "calc(50% + token(sizes.3.5))",
119
+ transform: "translateX(-50%)",
120
+ bg: "gray.700",
121
+ color: "white",
122
+ px: "2",
123
+ py: "1",
124
+ borderRadius: "md",
125
+ fontSize: "sm",
126
+ fontWeight: "medium",
127
+ boxShadow: "md",
128
+ zIndex: "tooltip",
129
+ whiteSpace: "nowrap",
130
+ _after: {
131
+ content: '""',
132
+ position: "absolute",
133
+ // 1px into the box so subpixel edges can't antialias into a
134
+ // hairline seam (see PopoverArrow).
135
+ top: "calc(100% - 1px)",
136
+ left: "50%",
137
+ transform: "translateX(-50%)",
138
+ borderWidth: "4px",
139
+ borderStyle: "solid",
140
+ // Per-side: tokens don't resolve inside multi-value
141
+ // shorthands (they emit verbatim and the browser drops the
142
+ // invalid declaration).
143
+ borderColor: "transparent",
144
+ borderTopColor: "gray.700",
145
+ },
146
+ })}
147
+ style={{ left: `${percent}%` }}
148
+ >
149
+ {thumbTooltip}
150
+ </div>
151
+ )}
94
152
  <SliderThumb
153
+ onFocusChange={onThumbFocusChange}
95
154
  className={cx(slots.thumb, thumbCss ? css(thumbCss) : undefined)}
96
155
  />
156
+ {children}
97
157
  </RACSlider>
98
158
  );
99
159
  };
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,11 +59,6 @@ export const switchRecipe = defineSlotRecipe({
61
59
  transitionProperty: "transform",
62
60
  transitionDuration: "normal",
63
61
  borderRadius: "inherit",
64
- width: "4",
65
- height: "4",
66
- "&[data-selected]": {
67
- transform: "translateX(0.875rem)",
68
- },
69
62
  _forcedColors: {
70
63
  bg: "ButtonText",
71
64
  "&[data-selected]": { bg: "SelectedItemText" },
@@ -77,4 +70,56 @@ export const switchRecipe = defineSlotRecipe({
77
70
  "&[data-disabled]": { opacity: 0.4 },
78
71
  },
79
72
  },
73
+ variants: {
74
+ // `start` puts the label first and the control at the row's end — the
75
+ // settings-row pattern (a preference name beside its switch), the toggle
76
+ // counterpart of the field recipe's `labelPosition="side"`. Values are
77
+ // start/end rather than top/side because the default label is already
78
+ // beside the control, after it. The label keeps an end margin so a long
79
+ // translation can't butt against the track (the SelectFormControl
80
+ // lesson — see the field-chrome roadmap bullet).
81
+ labelPosition: {
82
+ end: {},
83
+ start: {
84
+ root: {
85
+ flexDirection: "row-reverse",
86
+ justifyContent: "space-between",
87
+ width: "100%",
88
+ },
89
+ label: { marginStart: "0", marginEnd: "3" },
90
+ },
91
+ },
92
+ // Track width x height per size; the selected-thumb translate is the
93
+ // difference between them.
94
+ size: {
95
+ sm: {
96
+ track: { width: "1.375rem", height: "3" },
97
+ thumb: {
98
+ width: "3",
99
+ height: "3",
100
+ "&[data-selected]": { transform: "translateX(0.625rem)" },
101
+ },
102
+ },
103
+ md: {
104
+ track: { width: "1.875rem", height: "4" },
105
+ thumb: {
106
+ width: "4",
107
+ height: "4",
108
+ "&[data-selected]": { transform: "translateX(0.875rem)" },
109
+ },
110
+ },
111
+ lg: {
112
+ track: { width: "2.875rem", height: "6" },
113
+ thumb: {
114
+ width: "6",
115
+ height: "6",
116
+ "&[data-selected]": { transform: "translateX(1.375rem)" },
117
+ },
118
+ },
119
+ },
120
+ },
121
+ defaultVariants: {
122
+ size: "md",
123
+ labelPosition: "end",
124
+ },
80
125
  });
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
  },