@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
package/src/Field.tsx ADDED
@@ -0,0 +1,187 @@
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 helper/error chrome every labelled form field shares — TextField,
19
+ * Select, ComboBox, NumberField, RadioGroup and CheckboxGroup carry the same
20
+ * props (data-microbit-org's forms attach helper and error text to all of
21
+ * these).
22
+ */
23
+ export interface FieldSupportProps {
24
+ /** Help text below the field. */
25
+ helperText?: ReactNode;
26
+ /** Shown below the field when invalid. */
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 for a control react-aria isn't wiring — a native
91
+ * 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, the counterpart to `FieldHelperText` — see its
115
+ * 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. Render inside the field's label when
138
+ * `isRequired`; aria-hidden because react-aria already announces requiredness
139
+ * 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, asterisk included. Every labelled field in the
162
+ * library renders its label through this, so the `field` recipe is the single
163
+ * answer to what a label looks like.
164
+ *
165
+ * Also exported for app-side composites: inside a RAC field container the
166
+ * association is automatic, and outside one (a masked or native input) pass
167
+ * `id` and `htmlFor` yourself.
168
+ */
169
+ export const FieldLabel = ({
170
+ children,
171
+ isRequired,
172
+ size,
173
+ labelPosition,
174
+ css: cssProp,
175
+ ...rest
176
+ }: FieldLabelProps) => (
177
+ <RACLabel
178
+ {...rest}
179
+ className={cx(
180
+ field({ size, labelPosition }).label,
181
+ cssProp ? css(cssProp) : undefined,
182
+ )}
183
+ >
184
+ {children}
185
+ {isRequired ? <FieldRequiredIndicator /> : null}
186
+ </RACLabel>
187
+ );
@@ -0,0 +1,57 @@
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
+ * GridList slot recipe — a vertical list of selectable rows, each of which may
10
+ * hold its own interactive controls (which is what makes it a grid rather than
11
+ * a listbox: the roving tab index moves through rows, and the controls inside
12
+ * a row are reachable without leaving it).
13
+ *
14
+ * The greys here are the family's neutral list styling; an app with a strong
15
+ * selection colour restates them (classroom's roster does).
16
+ *
17
+ * Registered in the base preset (base-preset.ts), which also has the
18
+ * `staticCss` entry that keeps the runtime-prop variants generated.
19
+ */
20
+ export const gridList = defineSlotRecipe({
21
+ className: "grid-list",
22
+ slots: ["root", "item"],
23
+ base: {
24
+ root: {
25
+ // The list takes the roving tab index, so it is focusable itself and
26
+ // would otherwise draw the platform ring around the whole list.
27
+ outline: "none",
28
+ },
29
+ item: {
30
+ display: "flex",
31
+ alignItems: "center",
32
+ position: "relative",
33
+ // A row is interactive by definition — it selects, or it acts.
34
+ cursor: "pointer",
35
+ outline: "none",
36
+ transitionProperty: "background",
37
+ transitionDuration: "ultra-fast",
38
+ transitionTimingFunction: "ease-in",
39
+ _hover: { bg: "gray.50" },
40
+ // A row holding an open menu (or any other popover) keeps the hover
41
+ // grey, so the row an open menu belongs to stays visible. Hover state
42
+ // cannot do this on its own: a Popover lays a fixed full-viewport
43
+ // underlay over the page while open, which takes the pointer off the
44
+ // row — `:hover` and RAC's own `data-hovered` both drop the moment the
45
+ // menu appears. A trigger carries `aria-expanded` (useOverlayTrigger),
46
+ // so the row can see its own open overlay.
47
+ "&:has([aria-expanded=true])": { bg: "gray.50" },
48
+ "&[data-selected]": {
49
+ bg: "gray.100",
50
+ _hover: { bg: "gray.100" },
51
+ "&:has([aria-expanded=true])": { bg: "gray.100" },
52
+ },
53
+ "&[data-focus-visible]": { focusRing: "outline" },
54
+ "&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
55
+ },
56
+ },
57
+ });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { ReactNode } from "react";
7
+ import {
8
+ GridList as RACGridList,
9
+ GridListItem as RACGridListItem,
10
+ GridListItemProps as RACGridListItemProps,
11
+ GridListProps as RACGridListProps,
12
+ } from "react-aria-components";
13
+ import { css, cx } from "styled-system/css";
14
+ import { gridList } from "styled-system/recipes";
15
+ import { SystemStyleObject } from "styled-system/types";
16
+
17
+ export interface GridListProps<T extends object>
18
+ extends Omit<RACGridListProps<T>, "className" | "style" | "children"> {
19
+ /** `GridListItem`s, or a render function when `items` is given. */
20
+ children: RACGridListProps<T>["children"];
21
+ /** Per-instance style overrides for the list, merged after the recipe. */
22
+ css?: SystemStyleObject;
23
+ className?: string;
24
+ }
25
+
26
+ /**
27
+ * GridList — react-aria-components' <GridList>: a list of selectable rows,
28
+ * each of which may contain its own buttons and menus.
29
+ *
30
+ * Reach for it over a `ListBox` when the rows carry controls: a listbox option
31
+ * is a leaf, so a button inside one is unreachable by keyboard, where a grid
32
+ * row's contents are part of the grid's navigation.
33
+ */
34
+ export const GridList = <T extends object>({
35
+ css: cssProp,
36
+ className,
37
+ children,
38
+ ...rest
39
+ }: GridListProps<T>) => {
40
+ const slots = gridList();
41
+ return (
42
+ <RACGridList
43
+ {...rest}
44
+ className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
45
+ >
46
+ {children}
47
+ </RACGridList>
48
+ );
49
+ };
50
+
51
+ export interface GridListItemProps<T extends object = object>
52
+ extends Omit<RACGridListItemProps<T>, "className" | "style" | "children"> {
53
+ children?: ReactNode;
54
+ /** Per-instance style overrides for the row, merged after the recipe. */
55
+ css?: SystemStyleObject;
56
+ className?: string;
57
+ }
58
+
59
+ /**
60
+ * A row in a `GridList`. Its children are laid out by the row itself — the
61
+ * gridcell react-aria puts between them is `display: contents`.
62
+ *
63
+ * Give every row a `textValue`: react-aria derives typeahead text from string
64
+ * children only, and a row is usually a composition rather than a string.
65
+ */
66
+ export const GridListItem = <T extends object = object>({
67
+ css: cssProp,
68
+ className,
69
+ children,
70
+ ...rest
71
+ }: GridListItemProps<T>) => {
72
+ const slots = gridList();
73
+ return (
74
+ <RACGridListItem
75
+ {...rest}
76
+ className={cx(slots.item, cssProp ? css(cssProp) : undefined, className)}
77
+ >
78
+ {children}
79
+ </RACGridListItem>
80
+ );
81
+ };
@@ -6,7 +6,7 @@
6
6
  import { defineRecipe } from "@pandacss/dev";
7
7
 
8
8
  /**
9
- * Heading recipe — Chakra's default Heading base + responsive sizes.
9
+ * Heading recipe — a bold heading-font base + responsive sizes.
10
10
  * A config recipe for the same reasons as `button` (see Button.recipe.ts);
11
11
  * the `marketing` variant is token-driven via the `display` font, so brands
12
12
  * only override tokens.
@@ -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 and emits variant rules in the order it
54
+ // meets them, so which of the two wins would depend on the breakpoint
55
+ // and on what other call sites exist. Two classes beat one at every
56
+ // 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: {
package/src/Heading.tsx CHANGED
@@ -7,8 +7,8 @@ import { styled } from "styled-system/jsx";
7
7
  import { heading } from "styled-system/recipes";
8
8
 
9
9
  /**
10
- * Heading — Chakra's <Heading> equivalent, backed by the `heading` config
11
- * recipe (so the private preset can add brand variants). Defaults to an <h2>;
12
- * pass `as` to change the element and `size` to pick the type scale.
10
+ * Heading — a heading backed by the `heading` config recipe (so the private
11
+ * preset can add brand variants). Defaults to an <h2>; pass `as` to change
12
+ * the element and `size` to pick the type scale.
13
13
  */
14
14
  export const Heading = styled("h2", heading);
package/src/Icon.tsx CHANGED
@@ -3,13 +3,26 @@
3
3
  *
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
- import { IconType } from "react-icons/lib";
6
+ import { ComponentType, SVGProps } from "react";
7
7
  import { css, cx } from "styled-system/css";
8
8
  import { SystemStyleObject } from "styled-system/types";
9
9
 
10
+ /**
11
+ * Any component that renders an `<svg>` from svg props. Deliberately no
12
+ * narrower than the props `Icon` actually passes, so it accepts both
13
+ * react-icons' `IconType` and svgr components (`import X from "./x.svg?react"`,
14
+ * which the apps use for their custom-path icons).
15
+ */
16
+ export type IconComponent = ComponentType<
17
+ Pick<
18
+ SVGProps<SVGSVGElement>,
19
+ "className" | "focusable" | "role" | "aria-label" | "aria-hidden"
20
+ >
21
+ >;
22
+
10
23
  export interface IconProps {
11
- /** The react-icons component to render. */
12
- as: IconType;
24
+ /** The icon component to render: a react-icons glyph or an svgr import. */
25
+ as: IconComponent;
13
26
  /** Panda style overrides (size via fontSize/boxSize, colour, etc.). */
14
27
  css?: SystemStyleObject;
15
28
  className?: string;
@@ -18,10 +31,9 @@ export interface IconProps {
18
31
  }
19
32
 
20
33
  /**
21
- * Icon — renders a react-icons glyph inline at `1em`, matching Chakra's <Icon>
22
- * base styles. `fill: currentColor` means the glyph follows the surrounding
23
- * text colour (set `css={{ color: ... }}` to override), so it inherits colour
24
- * like Chakra's icons rather than defaulting to black.
34
+ * Icon — renders a react-icons glyph inline at `1em`. `fill: currentColor`
35
+ * means the glyph follows the surrounding text colour rather than defaulting
36
+ * to black (set `css={{ color: ... }}` to override).
25
37
  *
26
38
  * Unlabelled icons are treated as decorative and hidden from assistive tech;
27
39
  * a labelled icon gets `role="img"` (a bare svg aria-label is unreliably
@@ -43,6 +55,10 @@ export const Icon = ({
43
55
  lineHeight: "1em",
44
56
  flexShrink: 0,
45
57
  fill: "currentColor",
58
+ // Set on the element itself: an inline-block icon sits ~3px off
59
+ // without it. Don't rely on Panda's preflight happening to set it on
60
+ // every svg.
61
+ verticalAlign: "middle",
46
62
  ...cssProp,
47
63
  }),
48
64
  className,
@@ -7,11 +7,9 @@ import { forwardRef } from "react";
7
7
  import { Button, ButtonProps } from "./Button";
8
8
 
9
9
  export interface IconButtonProps
10
- extends Omit<ButtonProps, "leftIcon" | "rightIcon"> {
10
+ extends Omit<ButtonProps, "startIcon" | "endIcon"> {
11
11
  /** Icon-only buttons have no visible label, so this is required. */
12
12
  "aria-label": string;
13
- /** Circular rather than the recipe's border-radius (Chakra's `isRound`). */
14
- isRound?: boolean;
15
13
  }
16
14
 
17
15
  /**
@@ -19,19 +17,16 @@ export interface IconButtonProps
19
17
  * padding (its size variants add `px`, which would squeeze a single glyph in a
20
18
  * fixed-width button) and keeps the `minW` from the size so the box stays
21
19
  * square. Pass the icon as children.
20
+ *
21
+ * Circular at every size without asking: the 2rem `button` radius exceeds half
22
+ * the tallest size (3rem), so CSS clamps it to a full round. Override
23
+ * `borderRadius` per instance for anything else — an attached group or a
24
+ * button that sits in a card corner needs per-corner control, not a shape flag.
22
25
  */
23
26
  export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
24
- function IconButton({ isRound, css: cssProp, children, ...rest }, ref) {
27
+ function IconButton({ css: cssProp, children, ...rest }, ref) {
25
28
  return (
26
- <Button
27
- ref={ref}
28
- css={{
29
- px: 0,
30
- ...(isRound ? { borderRadius: "full" } : {}),
31
- ...cssProp,
32
- }}
33
- {...rest}
34
- >
29
+ <Button ref={ref} css={{ px: 0, ...cssProp }} {...rest}>
35
30
  {children}
36
31
  </Button>
37
32
  );
package/src/Image.tsx CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { styled } from "styled-system/jsx";
7
7
 
8
- /** Image — an <img> accepting Panda style props. Replaces Chakra's <Image>. */
8
+ /** Image — an <img> accepting Panda style props. */
9
9
  export const Image = styled("img", {
10
10
  base: { maxWidth: "100%" },
11
11
  });
@@ -5,21 +5,22 @@
5
5
  */
6
6
  import { defineRecipe } from "@pandacss/dev";
7
7
 
8
- // Chakra's transition.property.common, inlined (Panda has no transitionProperty
9
- // token category).
8
+ // The common transition-property list, inlined (Panda has no
9
+ // transitionProperty token category).
10
10
  const transitionCommon =
11
- "background-color, border-color, color, fill, stroke, opacity, box-shadow, transform";
11
+ "background-color, border-color, color, fill, stroke, opacity, transform";
12
12
 
13
13
  /**
14
- * Input recipe — Chakra's outline Input field at md size (light mode). Used by
14
+ * Input recipe — the outline text field with an sm/md/lg size scale. Used by
15
15
  * the shared-ui Input and NativeSelect, and by TextField's input slot.
16
16
  *
17
- * Focus matches both native `:focus-visible` (plain inputs; browsers treat any
18
- * focus in a text field as focus-visible) and react-aria's `data-focused`
19
- * (inputs inside RAC TextField). Focus is declared after invalid so a focused
20
- * invalid field shows the focus ring, as in Chakra.
17
+ * hover < focused < invalid is a specificity ladder (`&&`, `&&&`): Panda
18
+ * sorts state rules against its own pseudo-class table, not declaration
19
+ * order, so equal-specificity borderColor ties would leave hover winning.
20
+ * Variants still override freely (later cascade layer).
21
21
  *
22
- * Registered in the base preset (base-preset.ts).
22
+ * Registered in the base preset (base-preset.ts), which also has the
23
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
23
24
  */
24
25
  export const input = defineRecipe({
25
26
  className: "input",
@@ -32,32 +33,44 @@ export const input = defineRecipe({
32
33
  font: "inherit",
33
34
  transitionProperty: transitionCommon,
34
35
  transitionDuration: "normal",
35
- fontSize: "md",
36
- px: "4",
37
- h: "10",
38
- borderRadius: "md",
39
- border: "1px solid",
40
- borderColor: "gray.200",
36
+ border: "2px solid",
37
+ // Deliberately below 3:1 — valid while something else visually
38
+ // identifies the field (label, or a ≥3:1 icon/placeholder).
39
+ // Checkbox/Radio keep gray.400: their box IS the identifier.
40
+ // Rationale: ui-private docs/a11y-positions.md.
41
+ borderColor: "gray.300",
41
42
  bg: "inherit",
42
43
  color: "inherit",
43
- _hover: { borderColor: "gray.300" },
44
- "&[data-invalid], &:user-invalid": {
44
+ _hover: { borderColor: "gray.500" },
45
+ // Any focus, pointer included; the keyboard ring composes on top.
46
+ // zIndex so the border — and the ring with it — paint over
47
+ // attached-group neighbours.
48
+ "&&:is(:focus, [data-focused])": { zIndex: 1, borderColor: "focusBorder" },
49
+ "&&&:is([data-invalid], :user-invalid)": {
45
50
  borderColor: "danger.500",
46
- boxShadow: "0 0 0 1px token(colors.danger.500)",
47
- },
48
- "&:is(:focus-visible, [data-focused])": {
49
- zIndex: 1,
50
- borderColor: "focusBorder",
51
- boxShadow: "0 0 0 1px token(colors.focusBorder)",
52
- // Focus indicator for forced-colors modes, which strip the box-shadow
53
- // and force the border colour (the focusShadow utility's technique; the
54
- // ring here is the 1px border tint, not an outline* shadow token).
55
- outline: "2px solid transparent",
56
- outlineOffset: "2px",
57
51
  },
52
+ // Modality-tracked: RAC's attribute, or Input.tsx's. Native
53
+ // :focus-visible is the fallback for a bare element wearing the recipe,
54
+ // but not text-entry ones — browsers match it there on a pointer click,
55
+ // which is what the tracking exists to exclude.
56
+ "&&&:is([data-focus-visible], :focus-visible:not([data-rac], input, textarea))":
57
+ {
58
+ focusRing: "outline",
59
+ },
58
60
  "&:is(:disabled, [data-disabled])": {
59
61
  opacity: 0.4,
60
62
  cursor: "not-allowed",
61
63
  },
62
64
  },
65
+ variants: {
66
+ // The sm/md/lg size scale.
67
+ size: {
68
+ lg: { fontSize: "lg", px: "4", h: "12", borderRadius: "md" },
69
+ md: { fontSize: "md", px: "4", h: "10", borderRadius: "md" },
70
+ sm: { fontSize: "sm", px: "3", h: "8", borderRadius: "sm" },
71
+ },
72
+ },
73
+ defaultVariants: {
74
+ size: "md",
75
+ },
63
76
  });
package/src/Input.tsx CHANGED
@@ -4,30 +4,46 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  import { forwardRef, InputHTMLAttributes } from "react";
7
+ import { mergeProps, useFocusRing } from "react-aria";
7
8
  import { css, cx } from "styled-system/css";
8
- import { input } from "styled-system/recipes";
9
+ import { input, InputVariantProps } from "styled-system/recipes";
9
10
  import { SystemStyleObject } from "styled-system/types";
10
11
 
11
12
  export interface InputProps
12
- extends Omit<InputHTMLAttributes<HTMLInputElement>, "className"> {
13
+ // `size` is the recipe's size scale; the native character-count attribute
14
+ // it shadows was unused.
15
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, "className" | "size">,
16
+ InputVariantProps {
13
17
  /** Per-instance style overrides, merged after the recipe. */
14
18
  css?: SystemStyleObject;
15
19
  className?: string;
16
20
  }
17
21
 
18
22
  /**
19
- * Input — a native input styled like Chakra's outline Input (md). For a
20
- * labelled field with help/error text use TextField instead.
23
+ * Input — a native input in the outline field style. For a labelled field
24
+ * with help/error text use TextField instead.
21
25
  */
22
26
  export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
23
- { css: cssProp, className, ...rest },
27
+ { css: cssProp, className, ...props },
24
28
  ref,
25
29
  ) {
30
+ // splitVariantProps, not a hand-picked `size`: an app preset can add variant
31
+ // groups to the recipe (classroom adds `variant`), and cherry-picking would
32
+ // silently drop them onto the DOM as unknown attributes instead.
33
+ const [variantProps, rest] = input.splitVariantProps(props);
34
+ // A native input never gets RAC's data-focus-visible, which the recipe's
35
+ // ring keys off — track modality ourselves, as TextField's input does.
36
+ const { isFocusVisible, focusProps } = useFocusRing({ isTextInput: true });
26
37
  return (
27
38
  <input
28
39
  ref={ref}
29
- className={cx(input(), cssProp ? css(cssProp) : undefined, className)}
30
- {...rest}
40
+ data-focus-visible={isFocusVisible || undefined}
41
+ className={cx(
42
+ input(variantProps),
43
+ cssProp ? css(cssProp) : undefined,
44
+ className,
45
+ )}
46
+ {...mergeProps(rest, focusProps)}
31
47
  />
32
48
  );
33
49
  });