@microbit/ui 0.1.0-alpha.8 → 0.1.0-alpha.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microbit/ui",
3
- "version": "0.1.0-alpha.8",
3
+ "version": "0.1.0-alpha.9",
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",
package/reset.css CHANGED
@@ -21,3 +21,15 @@
21
21
  border-color: var(--colors-gray-200);
22
22
  word-wrap: break-word;
23
23
  }
24
+
25
+ /*
26
+ * Panda's preflight (base layer) beats the rule above with
27
+ * `border-color: var(--global-color-border, currentcolor)`. The currentcolor
28
+ * fallback poisons `border-color: inherit` chains (inherit copies the
29
+ * currentcolor KEYWORD, which then resolves against the child's own `color`
30
+ * — e.g. white on the checkbox/radio control). Defining the hook keeps the
31
+ * default border colour a real colour, as Chakra's global reset did.
32
+ */
33
+ :root {
34
+ --global-color-border: var(--colors-gray-200);
35
+ }
@@ -6,16 +6,16 @@
6
6
  import { defineSlotRecipe } from "@pandacss/dev";
7
7
 
8
8
  /**
9
- * Checkbox slot recipe — Chakra's md checkbox with the default blue
10
- * colorScheme (light mode). The control's `borderColor: inherit` picks up a
11
- * `borderColor` set on the root, matching Chakra's convention for tinting the
12
- * box from the call site.
9
+ * Checkbox slot recipe — Chakra's checkbox with the default blue colorScheme
10
+ * (light mode) and its sm/md/lg size scale. The control's
11
+ * `borderColor: inherit` picks up a `borderColor` set on the root, matching
12
+ * Chakra's convention for tinting the box from the call site.
13
13
  *
14
14
  * State styling keys off data attributes stamped on the control by the
15
15
  * shared-ui Checkbox (react-aria provides the state via render props).
16
16
  *
17
- * Registered in the base preset (base-preset.ts). No variants, so it
18
- * needs no `staticCss` entry.
17
+ * Registered in the base preset (base-preset.ts), which also has the
18
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
19
19
  */
20
20
  export const checkbox = defineSlotRecipe({
21
21
  className: "checkbox",
@@ -34,11 +34,10 @@ export const checkbox = defineSlotRecipe({
34
34
  alignItems: "center",
35
35
  justifyContent: "center",
36
36
  flexShrink: 0,
37
- width: "4",
38
- height: "4",
39
37
  transitionProperty: "box-shadow",
40
38
  transitionDuration: "normal",
41
- border: "2px solid",
39
+ borderWidth: "2px",
40
+ borderStyle: "solid",
42
41
  borderRadius: "sm",
43
42
  borderColor: "inherit",
44
43
  color: "white",
@@ -52,13 +51,23 @@ export const checkbox = defineSlotRecipe({
52
51
  borderColor: "controlCheckedHoverBg",
53
52
  },
54
53
  },
54
+ // Chakra's disabled greys; the selected block restates _hover so the
55
+ // widened native-:hover condition can't re-tint a disabled control.
56
+ "&[data-disabled]": {
57
+ bg: "gray.100",
58
+ borderColor: "gray.100",
59
+ },
60
+ "&[data-selected][data-disabled]": {
61
+ bg: "gray.200",
62
+ borderColor: "gray.200",
63
+ color: "gray.500",
64
+ _hover: { bg: "gray.200", borderColor: "gray.200" },
65
+ },
55
66
  "&[data-focus-visible]": {
56
67
  focusShadow: "outline",
57
68
  },
58
69
  },
59
70
  icon: {
60
- width: "0.75rem",
61
- height: "0.75rem",
62
71
  transitionProperty: "transform",
63
72
  transitionDuration: "normal",
64
73
  },
@@ -68,4 +77,29 @@ export const checkbox = defineSlotRecipe({
68
77
  "&[data-disabled]": { opacity: 0.4 },
69
78
  },
70
79
  },
80
+ variants: {
81
+ // Chakra's Checkbox size scale. The icon dimensions are Chakra's
82
+ // 1.2em-wide check glyph at each size's icon fontSize (3xs/2xs/2xs),
83
+ // resolved to rem.
84
+ size: {
85
+ sm: {
86
+ control: { width: "3", height: "3" },
87
+ icon: { width: "0.54rem", height: "0.54rem" },
88
+ label: { fontSize: "sm" },
89
+ },
90
+ md: {
91
+ control: { width: "4", height: "4" },
92
+ icon: { width: "0.75rem", height: "0.75rem" },
93
+ label: { fontSize: "md" },
94
+ },
95
+ lg: {
96
+ control: { width: "5", height: "5" },
97
+ icon: { width: "0.75rem", height: "0.75rem" },
98
+ label: { fontSize: "lg" },
99
+ },
100
+ },
101
+ },
102
+ defaultVariants: {
103
+ size: "md",
104
+ },
71
105
  });
package/src/Checkbox.tsx CHANGED
@@ -9,11 +9,12 @@ import {
9
9
  CheckboxProps as RACCheckboxProps,
10
10
  } from "react-aria-components";
11
11
  import { css, cx } from "styled-system/css";
12
- import { checkbox } from "styled-system/recipes";
12
+ import { checkbox, CheckboxVariantProps } from "styled-system/recipes";
13
13
  import { SystemStyleObject } from "styled-system/types";
14
14
 
15
15
  export interface CheckboxProps
16
- extends Omit<RACCheckboxProps, "className" | "children" | "style"> {
16
+ extends Omit<RACCheckboxProps, "className" | "children" | "style">,
17
+ CheckboxVariantProps {
17
18
  /** Per-instance style overrides for the root, merged after the recipe. */
18
19
  css?: SystemStyleObject;
19
20
  className?: string;
@@ -21,28 +22,30 @@ export interface CheckboxProps
21
22
  }
22
23
 
23
24
  /**
24
- * Checkbox — react-aria-components <Checkbox> styled like Chakra's md
25
- * checkbox. Children render as the label; wrap them in a visually-hidden
26
- * span for icon-less checkboxes.
25
+ * Checkbox — react-aria-components <Checkbox> styled like Chakra's checkbox.
26
+ * Children render as the label; wrap them in a visually-hidden span for
27
+ * icon-less checkboxes.
27
28
  */
28
29
  export const Checkbox = ({
30
+ size,
29
31
  css: cssProp,
30
32
  className,
31
33
  children,
32
34
  ...rest
33
35
  }: CheckboxProps) => {
34
- const slots = checkbox();
36
+ const slots = checkbox({ size });
35
37
  return (
36
38
  <RACCheckbox
37
39
  className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
38
40
  {...rest}
39
41
  >
40
- {({ isSelected, isFocusVisible }) => (
42
+ {({ isSelected, isFocusVisible, isDisabled }) => (
41
43
  <>
42
44
  <span
43
45
  className={slots.control}
44
46
  data-selected={isSelected || undefined}
45
47
  data-focus-visible={isFocusVisible || undefined}
48
+ data-disabled={isDisabled || undefined}
46
49
  aria-hidden
47
50
  >
48
51
  {isSelected && (
@@ -58,7 +61,14 @@ export const Checkbox = ({
58
61
  </svg>
59
62
  )}
60
63
  </span>
61
- {children != null && <span className={slots.label}>{children}</span>}
64
+ {children != null && (
65
+ <span
66
+ className={slots.label}
67
+ data-disabled={isDisabled || undefined}
68
+ >
69
+ {children}
70
+ </span>
71
+ )}
62
72
  </>
63
73
  )}
64
74
  </RACCheckbox>
@@ -11,15 +11,17 @@ const transitionCommon =
11
11
  "background-color, border-color, color, fill, stroke, opacity, box-shadow, transform";
12
12
 
13
13
  /**
14
- * Input recipe — Chakra's outline Input field at md size (light mode). Used by
15
- * the shared-ui Input and NativeSelect, and by TextField's input slot.
14
+ * Input recipe — Chakra's outline Input field (light mode) with its size
15
+ * scale. Used by the shared-ui Input and NativeSelect, and by TextField's
16
+ * input slot.
16
17
  *
17
18
  * Focus matches both native `:focus-visible` (plain inputs; browsers treat any
18
19
  * focus in a text field as focus-visible) and react-aria's `data-focused`
19
20
  * (inputs inside RAC TextField). Focus is declared after invalid so a focused
20
21
  * invalid field shows the focus ring, as in Chakra.
21
22
  *
22
- * Registered in the base preset (base-preset.ts).
23
+ * Registered in the base preset (base-preset.ts), which also has the
24
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
23
25
  */
24
26
  export const input = defineRecipe({
25
27
  className: "input",
@@ -32,10 +34,6 @@ export const input = defineRecipe({
32
34
  font: "inherit",
33
35
  transitionProperty: transitionCommon,
34
36
  transitionDuration: "normal",
35
- fontSize: "md",
36
- px: "4",
37
- h: "10",
38
- borderRadius: "md",
39
37
  border: "1px solid",
40
38
  borderColor: "gray.200",
41
39
  bg: "inherit",
@@ -60,4 +58,15 @@ export const input = defineRecipe({
60
58
  cursor: "not-allowed",
61
59
  },
62
60
  },
61
+ variants: {
62
+ // Chakra's Input size scale, minus the unused xs.
63
+ size: {
64
+ lg: { fontSize: "lg", px: "4", h: "12", borderRadius: "md" },
65
+ md: { fontSize: "md", px: "4", h: "10", borderRadius: "md" },
66
+ sm: { fontSize: "sm", px: "3", h: "8", borderRadius: "sm" },
67
+ },
68
+ },
69
+ defaultVariants: {
70
+ size: "md",
71
+ },
63
72
  });
package/src/Input.tsx CHANGED
@@ -5,28 +5,35 @@
5
5
  */
6
6
  import { forwardRef, InputHTMLAttributes } from "react";
7
7
  import { css, cx } from "styled-system/css";
8
- import { input } from "styled-system/recipes";
8
+ import { input, InputVariantProps } from "styled-system/recipes";
9
9
  import { SystemStyleObject } from "styled-system/types";
10
10
 
11
11
  export interface InputProps
12
- extends Omit<InputHTMLAttributes<HTMLInputElement>, "className"> {
12
+ // `size` is the recipe's size scale, as in Chakra; the native character-count
13
+ // attribute it shadows was unused.
14
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, "className" | "size">,
15
+ InputVariantProps {
13
16
  /** Per-instance style overrides, merged after the recipe. */
14
17
  css?: SystemStyleObject;
15
18
  className?: string;
16
19
  }
17
20
 
18
21
  /**
19
- * Input — a native input styled like Chakra's outline Input (md). For a
22
+ * Input — a native input styled like Chakra's outline Input. For a
20
23
  * labelled field with help/error text use TextField instead.
21
24
  */
22
25
  export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
23
- { css: cssProp, className, ...rest },
26
+ { size, css: cssProp, className, ...rest },
24
27
  ref,
25
28
  ) {
26
29
  return (
27
30
  <input
28
31
  ref={ref}
29
- className={cx(input(), cssProp ? css(cssProp) : undefined, className)}
32
+ className={cx(
33
+ input({ size }),
34
+ cssProp ? css(cssProp) : undefined,
35
+ className,
36
+ )}
30
37
  {...rest}
31
38
  />
32
39
  );
@@ -16,6 +16,8 @@ export const InputGroup = styled("div", {
16
16
 
17
17
  // The element styles are written out twice rather than shared via a spread:
18
18
  // Panda's extractor only reliably evaluates inline literals.
19
+ // The size variants are square boxes matching the input recipe's height per
20
+ // size, as in Chakra; pass the same `size` as the grouped Input.
19
21
 
20
22
  /** Element overlaying the start of an InputGroup (Chakra InputLeftElement). */
21
23
  export const InputLeftElement = styled("div", {
@@ -23,13 +25,19 @@ export const InputLeftElement = styled("div", {
23
25
  position: "absolute",
24
26
  top: 0,
25
27
  left: 0,
26
- width: "10",
27
- height: "10",
28
28
  display: "flex",
29
29
  alignItems: "center",
30
30
  justifyContent: "center",
31
31
  zIndex: 2,
32
32
  },
33
+ variants: {
34
+ size: {
35
+ lg: { width: "12", height: "12" },
36
+ md: { width: "10", height: "10" },
37
+ sm: { width: "8", height: "8" },
38
+ },
39
+ },
40
+ defaultVariants: { size: "md" },
33
41
  });
34
42
 
35
43
  /** Element overlaying the end of an InputGroup (Chakra InputRightElement). */
@@ -38,11 +46,17 @@ export const InputRightElement = styled("div", {
38
46
  position: "absolute",
39
47
  top: 0,
40
48
  right: 0,
41
- width: "10",
42
- height: "10",
43
49
  display: "flex",
44
50
  alignItems: "center",
45
51
  justifyContent: "center",
46
52
  zIndex: 2,
47
53
  },
54
+ variants: {
55
+ size: {
56
+ lg: { width: "12", height: "12" },
57
+ md: { width: "10", height: "10" },
58
+ sm: { width: "8", height: "8" },
59
+ },
60
+ },
61
+ defaultVariants: { size: "md" },
48
62
  });
@@ -5,11 +5,14 @@
5
5
  */
6
6
  import { forwardRef, SelectHTMLAttributes } from "react";
7
7
  import { css, cx } from "styled-system/css";
8
- import { input } from "styled-system/recipes";
8
+ import { input, InputVariantProps } from "styled-system/recipes";
9
9
  import { SystemStyleObject } from "styled-system/types";
10
10
 
11
11
  export interface NativeSelectProps
12
- extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "className"> {
12
+ // `size` is the recipe's size scale, as in Chakra; the native visible-rows
13
+ // attribute it shadows was unused.
14
+ extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "className" | "size">,
15
+ InputVariantProps {
13
16
  /**
14
17
  * Suppress the dropdown chevron, e.g. when an adjacent control provides the
15
18
  * affordance. Without the chevron a bare <select> element is rendered (no
@@ -22,23 +25,24 @@ export interface NativeSelectProps
22
25
  }
23
26
 
24
27
  /**
25
- * NativeSelect — a native select styled like Chakra's Select field (md,
26
- * outline). The recipe's `appearance: none` removes the platform chevron, so
27
- * one is drawn back in by default (Chakra Select's glyph, `currentColor`).
28
+ * NativeSelect — a native select styled like Chakra's Select field (outline).
29
+ * The recipe's `appearance: none` removes the platform chevron, so one is
30
+ * drawn back in by default (Chakra Select's glyph, `currentColor`).
28
31
  */
29
32
  export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
30
33
  function NativeSelect(
31
- { hideChevron = false, css: cssProp, className, ...rest },
34
+ { hideChevron = false, size, css: cssProp, className, ...rest },
32
35
  ref,
33
36
  ) {
34
37
  const select = (
35
38
  <select
36
39
  ref={ref}
37
40
  className={cx(
38
- input(),
41
+ input({ size }),
39
42
  css(
40
43
  { cursor: "pointer" },
41
- // Room for the chevron overlay.
44
+ // Room for the chevron overlay (Chakra Select's icon spacing,
45
+ // constant across sizes).
42
46
  hideChevron ? undefined : { paddingRight: "8" },
43
47
  cssProp,
44
48
  ),
@@ -53,7 +57,7 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
53
57
  return (
54
58
  <span className={css({ position: "relative", display: "inline-flex" })}>
55
59
  {select}
56
- {/* Chakra Select's chevron. */}
60
+ {/* Chakra Select's chevron, fixed-size across field sizes. */}
57
61
  <svg
58
62
  viewBox="0 0 24 24"
59
63
  aria-hidden
@@ -0,0 +1,107 @@
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
+ * Radio slot recipe — Chakra's radio with the default blue colorScheme
10
+ * (light mode) and its sm/md/lg size scale: the checkbox control rounded
11
+ * fully, with a 50% `currentColor` dot when selected instead of the check
12
+ * glyph. As with the checkbox, `borderColor: inherit` picks up a
13
+ * `borderColor` set on the root.
14
+ *
15
+ * State styling keys off data attributes stamped by the shared-ui Radio
16
+ * (react-aria provides the state via render props).
17
+ *
18
+ * Registered in the base preset (base-preset.ts), which also has the
19
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
20
+ */
21
+ export const radio = defineSlotRecipe({
22
+ className: "radio",
23
+ slots: ["root", "control", "label"],
24
+ base: {
25
+ root: {
26
+ display: "inline-flex",
27
+ alignItems: "center",
28
+ verticalAlign: "top",
29
+ cursor: "pointer",
30
+ position: "relative",
31
+ "&[data-disabled]": { cursor: "not-allowed" },
32
+ },
33
+ control: {
34
+ display: "inline-flex",
35
+ alignItems: "center",
36
+ justifyContent: "center",
37
+ flexShrink: 0,
38
+ transitionProperty: "box-shadow",
39
+ transitionDuration: "normal",
40
+ borderWidth: "2px",
41
+ borderStyle: "solid",
42
+ borderRadius: "full",
43
+ borderColor: "inherit",
44
+ color: "white",
45
+ bg: "white",
46
+ "&[data-selected]": {
47
+ bg: "controlCheckedBg",
48
+ borderColor: "controlCheckedBg",
49
+ color: "white",
50
+ _hover: {
51
+ bg: "controlCheckedHoverBg",
52
+ borderColor: "controlCheckedHoverBg",
53
+ },
54
+ // Chakra's radio dot.
55
+ _before: {
56
+ content: '""',
57
+ display: "inline-block",
58
+ position: "relative",
59
+ width: "50%",
60
+ height: "50%",
61
+ borderRadius: "50%",
62
+ bg: "currentColor",
63
+ },
64
+ },
65
+ // Chakra's disabled greys; the selected block restates _hover so the
66
+ // widened native-:hover condition can't re-tint a disabled control.
67
+ "&[data-disabled]": {
68
+ bg: "gray.100",
69
+ borderColor: "gray.100",
70
+ },
71
+ "&[data-selected][data-disabled]": {
72
+ bg: "gray.200",
73
+ borderColor: "gray.200",
74
+ color: "gray.500",
75
+ _hover: { bg: "gray.200", borderColor: "gray.200" },
76
+ },
77
+ "&[data-focus-visible]": {
78
+ focusShadow: "outline",
79
+ },
80
+ },
81
+ label: {
82
+ userSelect: "none",
83
+ marginStart: "2",
84
+ "&[data-disabled]": { opacity: 0.4 },
85
+ },
86
+ },
87
+ variants: {
88
+ // Chakra's Radio size scale (the Checkbox control scale).
89
+ size: {
90
+ sm: {
91
+ control: { width: "3", height: "3" },
92
+ label: { fontSize: "sm" },
93
+ },
94
+ md: {
95
+ control: { width: "4", height: "4" },
96
+ label: { fontSize: "md" },
97
+ },
98
+ lg: {
99
+ control: { width: "5", height: "5" },
100
+ label: { fontSize: "lg" },
101
+ },
102
+ },
103
+ },
104
+ defaultVariants: {
105
+ size: "md",
106
+ },
107
+ });
package/src/Radio.tsx ADDED
@@ -0,0 +1,89 @@
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
+ Radio as RACRadio,
9
+ RadioProps as RACRadioProps,
10
+ RadioGroup as RACRadioGroup,
11
+ RadioGroupProps as RACRadioGroupProps,
12
+ } from "react-aria-components";
13
+ import { css, cx } from "styled-system/css";
14
+ import { radio, RadioVariantProps } from "styled-system/recipes";
15
+ import { SystemStyleObject } from "styled-system/types";
16
+
17
+ export interface RadioGroupProps
18
+ extends Omit<RACRadioGroupProps, "className" | "style"> {
19
+ /** Per-instance style overrides, merged after the recipe. */
20
+ css?: SystemStyleObject;
21
+ className?: string;
22
+ }
23
+
24
+ /**
25
+ * RadioGroup — react-aria-components <RadioGroup> for a set of Radios. Carries
26
+ * no styling of its own: compose with Stack for layout, as Chakra call sites
27
+ * did.
28
+ */
29
+ export const RadioGroup = ({
30
+ css: cssProp,
31
+ className,
32
+ ...rest
33
+ }: RadioGroupProps) => {
34
+ return (
35
+ <RACRadioGroup
36
+ className={cx(cssProp ? css(cssProp) : undefined, className)}
37
+ {...rest}
38
+ />
39
+ );
40
+ };
41
+
42
+ export interface RadioProps
43
+ extends Omit<RACRadioProps, "className" | "children" | "style">,
44
+ RadioVariantProps {
45
+ /** Per-instance style overrides for the root, merged after the recipe. */
46
+ css?: SystemStyleObject;
47
+ className?: string;
48
+ children?: ReactNode;
49
+ }
50
+
51
+ /**
52
+ * Radio — react-aria-components <Radio> styled like Chakra's radio. Must be
53
+ * rendered inside a RadioGroup. Children render as the label.
54
+ */
55
+ export const Radio = ({
56
+ size,
57
+ css: cssProp,
58
+ className,
59
+ children,
60
+ ...rest
61
+ }: RadioProps) => {
62
+ const slots = radio({ size });
63
+ return (
64
+ <RACRadio
65
+ className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
66
+ {...rest}
67
+ >
68
+ {({ isSelected, isFocusVisible, isDisabled }) => (
69
+ <>
70
+ <span
71
+ className={slots.control}
72
+ data-selected={isSelected || undefined}
73
+ data-focus-visible={isFocusVisible || undefined}
74
+ data-disabled={isDisabled || undefined}
75
+ aria-hidden
76
+ />
77
+ {children != null && (
78
+ <span
79
+ className={slots.label}
80
+ data-disabled={isDisabled || undefined}
81
+ >
82
+ {children}
83
+ </span>
84
+ )}
85
+ </>
86
+ )}
87
+ </RACRadio>
88
+ );
89
+ };
@@ -6,15 +6,16 @@
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 — Chakra's switch with the default blue colorScheme
10
+ * (light mode) and its sm/md/lg size scale. The track has a 2px inset; the
11
+ * thumb matches the track height and slides by the track width/height
12
+ * difference when selected.
12
13
  *
13
14
  * State styling keys off data attributes stamped by the shared-ui Switch
14
15
  * (react-aria provides the state via render props).
15
16
  *
16
- * Registered in the base preset (base-preset.ts). No variants, so it
17
- * needs no `staticCss` entry.
17
+ * Registered in the base preset (base-preset.ts), which also has the
18
+ * `staticCss` entry that keeps the runtime-prop size variants generated.
18
19
  */
19
20
  export const switchRecipe = defineSlotRecipe({
20
21
  className: "switch",
@@ -35,8 +36,6 @@ export const switchRecipe = defineSlotRecipe({
35
36
  boxSizing: "content-box",
36
37
  borderRadius: "full",
37
38
  p: "0.5",
38
- width: "1.875rem",
39
- height: "4",
40
39
  transitionProperty: "background-color",
41
40
  transitionDuration: "fast",
42
41
  bg: "gray.300",
@@ -61,11 +60,6 @@ export const switchRecipe = defineSlotRecipe({
61
60
  transitionProperty: "transform",
62
61
  transitionDuration: "normal",
63
62
  borderRadius: "inherit",
64
- width: "4",
65
- height: "4",
66
- "&[data-selected]": {
67
- transform: "translateX(0.875rem)",
68
- },
69
63
  _forcedColors: {
70
64
  bg: "ButtonText",
71
65
  "&[data-selected]": { bg: "SelectedItemText" },
@@ -77,4 +71,37 @@ export const switchRecipe = defineSlotRecipe({
77
71
  "&[data-disabled]": { opacity: 0.4 },
78
72
  },
79
73
  },
74
+ variants: {
75
+ // Chakra's Switch size scale (track width x height; the selected-thumb
76
+ // translate is the difference between them).
77
+ size: {
78
+ sm: {
79
+ track: { width: "1.375rem", height: "3" },
80
+ thumb: {
81
+ width: "3",
82
+ height: "3",
83
+ "&[data-selected]": { transform: "translateX(0.625rem)" },
84
+ },
85
+ },
86
+ md: {
87
+ track: { width: "1.875rem", height: "4" },
88
+ thumb: {
89
+ width: "4",
90
+ height: "4",
91
+ "&[data-selected]": { transform: "translateX(0.875rem)" },
92
+ },
93
+ },
94
+ lg: {
95
+ track: { width: "2.875rem", height: "6" },
96
+ thumb: {
97
+ width: "6",
98
+ height: "6",
99
+ "&[data-selected]": { transform: "translateX(1.375rem)" },
100
+ },
101
+ },
102
+ },
103
+ },
104
+ defaultVariants: {
105
+ size: "md",
106
+ },
80
107
  });
package/src/Switch.tsx CHANGED
@@ -9,11 +9,12 @@ import {
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
14
 
15
15
  export interface SwitchProps
16
- extends Omit<RACSwitchProps, "className" | "children" | "style"> {
16
+ extends Omit<RACSwitchProps, "className" | "children" | "style">,
17
+ SwitchRecipeVariantProps {
17
18
  /** Per-instance style overrides for the root, merged after the recipe. */
18
19
  css?: SystemStyleObject;
19
20
  className?: string;
@@ -21,16 +22,17 @@ export interface SwitchProps
21
22
  }
22
23
 
23
24
  /**
24
- * Switch — react-aria-components <Switch> styled like Chakra's md switch.
25
+ * Switch — react-aria-components <Switch> styled like Chakra's switch.
25
26
  * Children render as the label; pass `aria-label` for label-less switches.
26
27
  */
27
28
  export const Switch = ({
29
+ size,
28
30
  css: cssProp,
29
31
  className,
30
32
  children,
31
33
  ...rest
32
34
  }: SwitchProps) => {
33
- const slots = switchRecipe();
35
+ const slots = switchRecipe({ size });
34
36
  return (
35
37
  <RACSwitch
36
38
  className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
package/src/TextField.tsx CHANGED
@@ -13,14 +13,15 @@ import {
13
13
  TextFieldProps as RACTextFieldProps,
14
14
  } from "react-aria-components";
15
15
  import { css, cx } from "styled-system/css";
16
- import { field, input } from "styled-system/recipes";
16
+ import { field, input, InputVariantProps } from "styled-system/recipes";
17
17
  import { SystemStyleObject } from "styled-system/types";
18
18
 
19
19
  export interface TextFieldProps
20
20
  extends Omit<
21
- RACTextFieldProps,
22
- "className" | "children" | "style" | "onFocus" | "onBlur"
23
- > {
21
+ RACTextFieldProps,
22
+ "className" | "children" | "style" | "onFocus" | "onBlur"
23
+ >,
24
+ InputVariantProps {
24
25
  /** Visible label (Chakra's FormLabel; asterisk added when `isRequired`). */
25
26
  label: ReactNode;
26
27
  /** Help text below the input (Chakra's FormHelperText). */
@@ -48,6 +49,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
48
49
  helperTextCss,
49
50
  onFocus,
50
51
  autoCapitalize,
52
+ size,
51
53
  ...rest
52
54
  },
53
55
  ref,
@@ -65,7 +67,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
65
67
  </RACLabel>
66
68
  <RACInput
67
69
  ref={ref}
68
- className={input()}
70
+ className={input({ size })}
69
71
  onFocus={onFocus}
70
72
  autoCapitalize={autoCapitalize}
71
73
  />
@@ -26,6 +26,7 @@ import {
26
26
  import { button } from "./Button.recipe";
27
27
  import { card } from "./Card.recipe";
28
28
  import { checkbox } from "./Checkbox.recipe";
29
+ import { radio } from "./Radio.recipe";
29
30
  import { drawer } from "./Drawer.recipe";
30
31
  import { heading } from "./Heading.recipe";
31
32
  import { input } from "./Input.recipe";
@@ -180,6 +181,7 @@ export const basePreset = definePreset({
180
181
  field,
181
182
  menu,
182
183
  numberField,
184
+ radio,
183
185
  slider,
184
186
  switchRecipe,
185
187
  toast,
@@ -237,12 +239,16 @@ export const basePreset = definePreset({
237
239
  staticCss: {
238
240
  recipes: {
239
241
  button: ["*"],
242
+ checkbox: ["*"],
240
243
  heading: ["*"],
241
244
  card: ["*"],
242
245
  // Dialog size is chosen with responsive objects ({ base, md }) passed
243
246
  // as a runtime prop, so generate the breakpoint-prefixed variants too.
244
247
  dialog: [{ size: ["*"], responsive: true }, { centered: ["*"] }],
245
248
  drawer: ["*"],
249
+ input: ["*"],
250
+ radio: ["*"],
251
+ switchRecipe: ["*"],
246
252
  // Toast status is chosen at runtime from the toast content.
247
253
  toast: ["*"],
248
254
  },
@@ -15,13 +15,50 @@ export function useClipboard(
15
15
  ): { onCopy: () => void; hasCopied: boolean } {
16
16
  const [hasCopied, setHasCopied] = useState(false);
17
17
  const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
18
+ const markCopied = useCallback(() => {
19
+ setHasCopied(true);
20
+ clearTimeout(timeoutRef.current);
21
+ timeoutRef.current = setTimeout(() => setHasCopied(false), timeoutMs);
22
+ }, [timeoutMs]);
18
23
  const onCopy = useCallback(() => {
19
- navigator.clipboard.writeText(value).then(() => {
20
- setHasCopied(true);
21
- clearTimeout(timeoutRef.current);
22
- timeoutRef.current = setTimeout(() => setHasCopied(false), timeoutMs);
23
- });
24
- }, [value, timeoutMs]);
24
+ if (navigator.clipboard?.writeText) {
25
+ navigator.clipboard
26
+ .writeText(value)
27
+ .then(markCopied, () => execCommandCopy(value) && markCopied());
28
+ } else if (execCommandCopy(value)) {
29
+ markCopied();
30
+ }
31
+ }, [value, markCopied]);
25
32
  useEffect(() => () => clearTimeout(timeoutRef.current), []);
26
33
  return { onCopy, hasCopied };
27
34
  }
35
+
36
+ /**
37
+ * Chakra's useClipboard wrapped the execCommand-based copy-to-clipboard
38
+ * package, which works on a user gesture in contexts the async API refuses:
39
+ * insecure (non-localhost http) origins, where `navigator.clipboard` is
40
+ * undefined, and frames without a clipboard-write permissions policy, where
41
+ * `writeText` rejects.
42
+ */
43
+ function execCommandCopy(value: string): boolean {
44
+ const active = document.activeElement;
45
+ const textarea = document.createElement("textarea");
46
+ textarea.value = value;
47
+ textarea.setAttribute("readonly", "");
48
+ textarea.style.position = "fixed";
49
+ textarea.style.opacity = "0";
50
+ document.body.appendChild(textarea);
51
+ textarea.select();
52
+ let copied = false;
53
+ try {
54
+ copied = document.execCommand("copy");
55
+ } catch {
56
+ // Unsupported (execCommand is deprecated but universally kept for copy);
57
+ // report failure so the caller shows no false "Copied!".
58
+ }
59
+ textarea.remove();
60
+ if (active instanceof HTMLElement) {
61
+ active.focus();
62
+ }
63
+ return copied;
64
+ }
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export * from "./LinkBox";
22
22
  export * from "./NativeSelect";
23
23
  export * from "./NumberField";
24
24
  export * from "./ProgressBar";
25
+ export * from "./Radio";
25
26
  export * from "./Slide";
26
27
  export * from "./Slider";
27
28
  export * from "./Spinner";