@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
@@ -5,26 +5,24 @@
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). Matches Chakra's Button base transition.
8
+ // The common transition-property list, inlined (Panda has no
9
+ // transitionProperty token category). No box-shadow: focus indication
10
+ // must never fade in.
10
11
  const transitionCommon =
11
- "background-color, border-color, color, fill, stroke, opacity, box-shadow, transform";
12
+ "background-color, border-color, color, fill, stroke, opacity, transform";
12
13
 
13
14
  /**
14
- * Button recipe — Chakra's default Button base + sizes, with this app's
15
- * borderRadius (`button` = 2rem) and variant set ported from
16
- * `components/button.ts`. Interaction conditions (`_hover`/`_active`/
15
+ * Button recipe — the family button base + sizes, with the house
16
+ * borderRadius (`button` = 2rem). Interaction conditions (`_hover`/`_active`/
17
17
  * `_disabled`/`_focusVisible`) are widened in the preset to also match
18
- * react-aria-components' data attributes, so these Chakra-shaped variant
19
- * objects work unchanged on RAC's <Button>.
18
+ * react-aria-components' data attributes, so these variant objects work
19
+ * unchanged on RAC's <Button>.
20
20
  *
21
21
  * A config recipe (not a component cva): styles land in the `recipes` layer so
22
22
  * call sites can override with plain style props, and presets extend the
23
- * variants. This file holds the brand-independent variant set the core
24
- * Chakra variants plus the family-wide `language`/`toolbar` variants; a
25
- * consuming app's preset extends it with app vocabulary (e.g. ml-trainer's
26
- * `led`/`record*`/`secondary-disabled`). Brand divergence within a variant is
27
- * token-driven (see the `languageText` semantic tokens).
23
+ * variants. Colour reaches a variant either through the `button.*` semantic
24
+ * tokens (the app's button idiom) or through `tone`, never through a per-app
25
+ * fork of a shape.
28
26
  *
29
27
  * Registered in the base preset (base-preset.ts).
30
28
  */
@@ -47,7 +45,7 @@ export const button = defineRecipe({
47
45
  verticalAlign: "middle",
48
46
  outline: "none",
49
47
  _focusVisible: {
50
- focusShadow: "outline",
48
+ focusRing: "outline",
51
49
  },
52
50
  _disabled: {
53
51
  opacity: 0.4,
@@ -68,7 +66,7 @@ export const button = defineRecipe({
68
66
  xs: { h: "6", minW: "6", fontSize: "xs", px: "2" },
69
67
  },
70
68
  variant: {
71
- // Chakra's unstyled reset + this app's border-radius removal.
69
+ // A full reset: the button renders as plain inline content.
72
70
  unstyled: {
73
71
  bg: "none",
74
72
  color: "inherit",
@@ -78,7 +76,7 @@ export const button = defineRecipe({
78
76
  p: "0",
79
77
  borderRadius: "unset",
80
78
  },
81
- // Chakra's link layout (no padding/height) + this app's colours.
79
+ // Link-shaped button: no padding/height, underline on hover.
82
80
  link: {
83
81
  padding: 0,
84
82
  height: "auto",
@@ -92,13 +90,22 @@ export const button = defineRecipe({
92
90
  textDecoration: "underline",
93
91
  },
94
92
  },
93
+ // Colours come from the `button.*` semantic tokens so the family's two
94
+ // button idioms (brand-coloured vs black-on-white) share this recipe —
95
+ // see the token block in base-preset.ts.
95
96
  secondary: {
96
97
  borderWidth: "2px",
97
- borderColor: "brand.500",
98
- color: "brand.700",
98
+ borderColor: "button.secondaryBorder",
99
+ color: "button.secondaryText",
99
100
  bg: "transparent",
100
- _hover: { borderColor: "brand.600" },
101
- _active: { bg: "brand.50", borderColor: "brand.700" },
101
+ _hover: {
102
+ borderColor: "button.secondaryHoverBorder",
103
+ bg: "button.secondaryHoverBg",
104
+ },
105
+ _active: {
106
+ bg: "button.secondaryActiveBg",
107
+ borderColor: "button.secondaryActiveBorder",
108
+ },
102
109
  },
103
110
  ghost: {
104
111
  color: "black",
@@ -106,57 +113,88 @@ export const button = defineRecipe({
106
113
  _hover: { bg: "blackAlpha.50" },
107
114
  _active: { bg: "blackAlpha.100" },
108
115
  },
109
- // Chakra had no `plain` variant, so `variant="plain"` fell through to
110
- // base-only styling: a transparent, colour-inheriting button. Used for the
111
- // action-bar icon-button menu triggers (settings/help), which supply their
112
- // own colour and shape via instance styles.
116
+ // Base-only styling: a transparent, colour-inheriting button. Used for
117
+ // the action-bar icon-button menu triggers (settings/help), which supply
118
+ // their own colour and shape via instance styles.
113
119
  plain: {
114
120
  bg: "transparent",
115
121
  color: "inherit",
116
122
  },
117
123
  primary: {
118
124
  color: "white",
119
- bg: "brand.500",
120
- _hover: { bg: "brand.600", _disabled: { bg: "brand.500" } },
121
- _active: { bg: "brand.700" },
125
+ bg: "button.primaryBg",
126
+ _hover: {
127
+ bg: "button.primaryHoverBg",
128
+ _disabled: { bg: "button.primaryBg" },
129
+ },
130
+ _active: { bg: "button.primaryActiveBg" },
122
131
  },
123
- warning: {
132
+ // `solid`/`outline` are coloured by `tone`, and reference only the
133
+ // stops it guarantees (50/500/600/700). They are not `primary`/
134
+ // `secondary` in another colour: those follow the app's button idiom,
135
+ // which is black-on-white in half the family and so can't be a palette.
136
+ solid: {
137
+ color: "white",
138
+ bg: "colorPalette.500",
139
+ _hover: {
140
+ bg: "colorPalette.600",
141
+ _disabled: { bg: "colorPalette.500" },
142
+ },
143
+ _active: { bg: "colorPalette.700" },
144
+ },
145
+ // Border at 500 (a boundary needs only 3:1) so a `solid`/`outline`
146
+ // toggle pair shows the same edge either way; the label needs 600.
147
+ //
148
+ // Classroom restates two other outlines per call site — a neutral 1px
149
+ // grey one and a white-on-colour one. Promote either if a second
150
+ // consumer appears.
151
+ outline: {
124
152
  borderWidth: "2px",
125
- borderColor: "danger.500",
126
- color: "danger.500",
153
+ borderColor: "colorPalette.500",
154
+ color: "colorPalette.600",
127
155
  bg: "transparent",
128
- _hover: { borderColor: "danger.600" },
129
- _active: { bg: "danger.50", borderColor: "danger.500" },
156
+ _hover: { borderColor: "colorPalette.600", color: "colorPalette.700" },
157
+ _active: { bg: "colorPalette.50" },
130
158
  },
131
- // Chakra's built-in solid + red colorScheme (destructive confirm
132
- // buttons). Same values as `record` today, but a separate variant so
133
- // recording UI and destructive actions can diverge - hence danger
134
- // tokens here, red.* literals there.
135
- warningSolid: {
136
- color: "white",
137
- bg: "danger.500",
138
- _hover: { bg: "danger.600", _disabled: { bg: "danger.500" } },
139
- _active: { bg: "danger.700" },
159
+ // Not a palette shape: a light fill under dark text is a different
160
+ // button from `solid`, and 350 exists in no other ramp.
161
+ neutral: {
162
+ color: "gray.800",
163
+ bg: "gray.100",
164
+ _hover: { bg: "gray.300", _disabled: { bg: "gray.100" } },
165
+ _active: { bg: "gray.350" },
140
166
  },
141
- // Family-wide variants (every censused app has language- and
142
- // toolbar-class buttons).
167
+ // Family-wide variant (every censused app has toolbar-class buttons).
168
+ // No ring override: the bar decides, and a dark one must spread
169
+ // `darkSurface` — an app adopting this variant needs the tag with it.
143
170
  toolbar: {
144
171
  color: "black",
145
172
  bg: "white",
146
173
  _hover: { bg: "whiteAlpha.900", _disabled: { bg: "white" } },
147
174
  _active: { bg: "whiteAlpha.800" },
148
- _focusVisible: { focusShadow: "outlineDark" },
149
- },
150
- language: {
151
- borderWidth: "2px",
152
- borderColor: "gray.200",
153
- color: "languageText",
154
- _hover: { color: "languageTextHover", bg: "gray.100" },
155
175
  },
156
176
  },
177
+ /**
178
+ * The palette behind `solid`/`outline`. An allowlist rather than Panda's
179
+ * open `colorPalette` prop: if a shape reads a stop the palette doesn't
180
+ * define, the button renders as nothing at all, with no error anywhere
181
+ * (docs/hints.md). This is where a palette is vetted, and a tone should
182
+ * alias a whole ramp so it has nowhere to fall through. Apps may add
183
+ * their own.
184
+ */
185
+ tone: {
186
+ brand: { colorPalette: "brand" },
187
+ // The destructive role, which a brand preset can re-point.
188
+ danger: { colorPalette: "danger" },
189
+ // Conventional red that isn't destructive (record buttons), so
190
+ // deliberately not following a brand's error colour.
191
+ red: { colorPalette: "red" },
192
+ },
157
193
  },
158
194
  defaultVariants: {
159
195
  variant: "secondary",
160
196
  size: "md",
197
+ // Never absent: a shape with no palette renders nothing.
198
+ tone: "brand",
161
199
  },
162
200
  });
package/src/Button.tsx CHANGED
@@ -4,29 +4,42 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  import { forwardRef, ReactNode } from "react";
7
+ import { useIntl } from "react-intl";
7
8
  import {
8
9
  Button as RACButton,
9
10
  ButtonProps as RACButtonProps,
10
11
  } from "react-aria-components";
11
- import { css, cva, cx } from "styled-system/css";
12
+ import { css, cx } from "styled-system/css";
12
13
  import { button, ButtonVariantProps } from "styled-system/recipes";
13
14
  import { SystemStyleObject } from "styled-system/types";
15
+ import { buttonIcon } from "./button-icon";
16
+ import { uiMessage } from "./messages";
17
+ import { Spinner } from "./Spinner";
14
18
 
15
- // Chakra's ButtonIcon: keeps the glyph centred and spaced from the label
16
- // (iconSpacing 0.5rem).
17
- const buttonIcon = cva({
18
- base: {
19
- display: "inline-flex",
20
- alignSelf: "center",
21
- flexShrink: 0,
22
- },
23
- variants: {
24
- side: {
25
- left: { marginEnd: "2" },
26
- right: { marginStart: "2" },
27
- },
28
- },
29
- });
19
+ // A 1em spinner centred over the hidden label. Out of flow, so the label alone
20
+ // sets the button's size; the recipe's base `position: relative` is what it
21
+ // anchors to. Its own component so useIntl runs only while a button is
22
+ // actually loading — a bare Button must keep working without an IntlProvider
23
+ // (test renders commonly lack one).
24
+ const ButtonSpinner = () => {
25
+ const intl = useIntl();
26
+ return (
27
+ <span
28
+ className={css({
29
+ position: "absolute",
30
+ inset: 0,
31
+ display: "flex",
32
+ alignItems: "center",
33
+ justifyContent: "center",
34
+ })}
35
+ >
36
+ <Spinner
37
+ aria-label={intl.formatMessage(uiMessage("ui.loading"))}
38
+ css={{ width: "1em", height: "1em" }}
39
+ />
40
+ </span>
41
+ );
42
+ };
30
43
 
31
44
  export interface ButtonProps
32
45
  extends Omit<RACButtonProps, "className" | "children">,
@@ -34,10 +47,16 @@ export interface ButtonProps
34
47
  /** Per-instance style overrides, merged after the recipe. */
35
48
  css?: SystemStyleObject;
36
49
  className?: string;
37
- /** Icon rendered before the label, matching Chakra's `leftIcon`. */
38
- leftIcon?: ReactNode;
39
- /** Icon rendered after the label, matching Chakra's `rightIcon`. */
40
- rightIcon?: ReactNode;
50
+ /** Icon rendered before the label. */
51
+ startIcon?: ReactNode;
52
+ /** Icon rendered after the label. */
53
+ endIcon?: ReactNode;
54
+ /**
55
+ * Show a spinner in place of the label and disable interaction: the label
56
+ * stays in the layout but invisible, so the button keeps its size, and the
57
+ * dimmed disabled look applies.
58
+ */
59
+ isLoading?: boolean;
41
60
  children?: ReactNode;
42
61
  }
43
62
 
@@ -52,32 +71,51 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
52
71
  {
53
72
  variant,
54
73
  size,
74
+ tone,
55
75
  css: cssProp,
56
76
  className,
57
- leftIcon,
58
- rightIcon,
77
+ startIcon,
78
+ endIcon,
79
+ isLoading,
59
80
  children,
60
81
  ...rest
61
82
  },
62
83
  ref,
63
84
  ) {
85
+ const label = (
86
+ <>
87
+ {startIcon ? (
88
+ <span className={buttonIcon({ side: "start" })}>{startIcon}</span>
89
+ ) : null}
90
+ {children}
91
+ {endIcon ? (
92
+ <span className={buttonIcon({ side: "end" })}>{endIcon}</span>
93
+ ) : null}
94
+ </>
95
+ );
64
96
  return (
65
97
  <RACButton
66
98
  ref={ref}
67
99
  className={cx(
68
- button({ variant, size }),
100
+ button({ variant, size, tone }),
69
101
  cssProp ? css(cssProp) : undefined,
70
102
  className,
71
103
  )}
104
+ data-loading={isLoading ? "" : undefined}
72
105
  {...rest}
106
+ isDisabled={isLoading || rest.isDisabled}
73
107
  >
74
- {leftIcon ? (
75
- <span className={buttonIcon({ side: "left" })}>{leftIcon}</span>
76
- ) : null}
77
- {children}
78
- {rightIcon ? (
79
- <span className={buttonIcon({ side: "right" })}>{rightIcon}</span>
80
- ) : null}
108
+ {isLoading ? (
109
+ <>
110
+ <ButtonSpinner />
111
+ {/* Hidden with opacity, not removed: it keeps the button the size
112
+ it is when idle, so a row of buttons doesn't reflow. Still in
113
+ the accessibility tree, so the button keeps its name. */}
114
+ <span className={css({ opacity: 0 })}>{label}</span>
115
+ </>
116
+ ) : (
117
+ label
118
+ )}
81
119
  </RACButton>
82
120
  );
83
121
  },
@@ -9,8 +9,8 @@ import { SystemStyleObject } from "styled-system/types";
9
9
 
10
10
  export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
11
11
  /**
12
- * Square the inner radii so adjacent buttons form one control (Chakra's
13
- * `isAttached`). Otherwise buttons get a small gap.
12
+ * Square the inner radii so adjacent buttons form one control. Otherwise
13
+ * buttons get a small gap.
14
14
  */
15
15
  isAttached?: boolean;
16
16
  /** Per-instance style overrides. */
@@ -20,36 +20,58 @@ export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
20
20
 
21
21
  /**
22
22
  * ButtonGroup — lays out related buttons in a row. Works with any button
23
- * elements (shared-ui or native). Replaces Chakra's ButtonGroup.
23
+ * elements (shared-ui or native).
24
+ *
25
+ * Attached, the buttons divide by a hairline and the group keeps whatever
26
+ * outline the variant draws round the outside, so a 2px `secondary` reads as
27
+ * one bordered control with cells inside it rather than three boxes. A
28
+ * variant with no border of its own (`primary`, `solid`, `neutral`) divides
29
+ * by a gap in the same place, the surface behind the group showing through —
30
+ * see the `[data-attached]` rule in base-preset.ts.
24
31
  */
25
32
  export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
26
33
  function ButtonGroup({ isAttached, css: cssProp, className, ...rest }, ref) {
27
34
  return (
28
35
  <div
29
36
  ref={ref}
37
+ data-attached={isAttached ? "" : undefined}
30
38
  className={cx(
31
39
  css({
32
40
  display: "inline-flex",
33
41
  alignItems: "center",
34
- // Buttons are position: relative, so without this an attached
35
- // sibling paints over the focused button's focus-ring shadow
36
- // (Chakra's Button applied zIndex 1 on focus when grouped).
37
- "& > *": { _focus: { zIndex: 1 } },
42
+ // Buttons are position: relative, so a sibling would paint over
43
+ // the focused button's ring. Only while it shows: a permanently
44
+ // raised child would own the seam it shares, so a hover on its
45
+ // neighbour would stop short of it.
46
+ "& > *": { _focusVisible: { zIndex: 1 } },
38
47
  }),
39
48
  isAttached
40
49
  ? css({
41
50
  gap: 0,
42
- // :first-child/:last-child, not :first-of-type: attached
43
- // groups can mix element types (e.g. select + button), and
44
- // -of-type matches per element type.
45
- "& > *:first-child:not(:last-child)": {
51
+ // :first-child/:last-child, not -of-type: attached groups can
52
+ // mix element types (e.g. select + button).
53
+ //
54
+ // A hairline whatever the variant's border, which stays as the
55
+ // group's outline — matching it made a 2px seam read as a gap
56
+ // (microbit-foundation/ui#22). Both sides of a seam keep a
57
+ // border and overlap by its width so the two paint as one
58
+ // line; dropping one instead would move that button's border
59
+ // box off the cell you see, and `focusRing` is an outline, so
60
+ // the ring would land on the seam rather than outside it.
61
+ //
62
+ // padding-box so a fill doesn't paint under the transparent
63
+ // seam a borderless variant gets from the preset. Here rather
64
+ // than with that rule because `bg` is the `background`
65
+ // shorthand, which resets it from `recipes`.
66
+ "& > *": { backgroundClip: "padding-box" },
67
+ "& > *:not(:last-child)": {
46
68
  borderEndRadius: 0,
69
+ borderInlineEndWidth: "1px",
47
70
  },
48
- "& > *:not(:first-child):not(:last-child)": {
49
- borderRadius: 0,
50
- },
51
- "& > *:not(:first-child):last-child": {
71
+ "& > *:not(:first-child)": {
52
72
  borderStartRadius: 0,
73
+ borderInlineStartWidth: "1px",
74
+ marginInlineStart: "-1px",
53
75
  },
54
76
  })
55
77
  : css({ gap: 2 }),
@@ -6,8 +6,7 @@
6
6
  import { defineSlotRecipe } from "@pandacss/dev";
7
7
 
8
8
  /**
9
- * Card slot recipe Chakra's Card at its default md size (light mode), with
10
- * the `elevated` (default) and `outline` variants this app uses.
9
+ * Card slot recipe, with `elevated` (default) and `outline` variants.
11
10
  *
12
11
  * Registered in the base preset (base-preset.ts); `variant` is
13
12
  * forwarded as a runtime prop so the variants are generated via `staticCss`.
package/src/Card.tsx CHANGED
@@ -16,7 +16,8 @@ export interface CardProps
16
16
  }
17
17
 
18
18
  /**
19
- * Card — Chakra's <Card> equivalent (md size). Place a CardBody inside.
19
+ * Card — a container styled with the `card` config recipe (md size). Place a
20
+ * CardBody inside.
20
21
  */
21
22
  export const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
22
23
  { variant, css: cssProp, className, ...rest },
@@ -6,16 +6,15 @@
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 with an sm/md/lg size scale. The control's
10
+ * `borderColor: inherit` picks up a `borderColor` set on the root, so call
11
+ * sites can tint the box from there.
13
12
  *
14
13
  * State styling keys off data attributes stamped on the control by the
15
14
  * shared-ui Checkbox (react-aria provides the state via render props).
16
15
  *
17
- * Registered in the base preset (base-preset.ts). No variants, so it
18
- * 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.
19
18
  */
20
19
  export const checkbox = defineSlotRecipe({
21
20
  className: "checkbox",
@@ -27,6 +26,11 @@ export const checkbox = defineSlotRecipe({
27
26
  verticalAlign: "top",
28
27
  cursor: "pointer",
29
28
  position: "relative",
29
+ // The accessible outline stop (WCAG 1.4.11), as the input recipe.
30
+ // Stated here rather than on the control — whose `borderColor:
31
+ // inherit` reads it — so a call site can still tint the whole control
32
+ // by setting borderColor on the root.
33
+ borderColor: "gray.400",
30
34
  "&[data-disabled]": { cursor: "not-allowed" },
31
35
  },
32
36
  control: {
@@ -34,11 +38,8 @@ export const checkbox = defineSlotRecipe({
34
38
  alignItems: "center",
35
39
  justifyContent: "center",
36
40
  flexShrink: 0,
37
- width: "4",
38
- height: "4",
39
- transitionProperty: "box-shadow",
40
- transitionDuration: "normal",
41
- border: "2px solid",
41
+ borderWidth: "2px",
42
+ borderStyle: "solid",
42
43
  borderRadius: "sm",
43
44
  borderColor: "inherit",
44
45
  color: "white",
@@ -52,13 +53,23 @@ export const checkbox = defineSlotRecipe({
52
53
  borderColor: "controlCheckedHoverBg",
53
54
  },
54
55
  },
56
+ // Disabled greys; the selected block restates _hover so the widened
57
+ // native-:hover condition can't re-tint a disabled control.
58
+ "&[data-disabled]": {
59
+ bg: "gray.100",
60
+ borderColor: "gray.100",
61
+ },
62
+ "&[data-selected][data-disabled]": {
63
+ bg: "gray.200",
64
+ borderColor: "gray.200",
65
+ color: "gray.500",
66
+ _hover: { bg: "gray.200", borderColor: "gray.200" },
67
+ },
55
68
  "&[data-focus-visible]": {
56
- focusShadow: "outline",
69
+ focusRing: "outline",
57
70
  },
58
71
  },
59
72
  icon: {
60
- width: "0.75rem",
61
- height: "0.75rem",
62
73
  transitionProperty: "transform",
63
74
  transitionDuration: "normal",
64
75
  },
@@ -68,4 +79,28 @@ export const checkbox = defineSlotRecipe({
68
79
  "&[data-disabled]": { opacity: 0.4 },
69
80
  },
70
81
  },
82
+ variants: {
83
+ // The icon dimensions are the 1.2em-wide check glyph at each size's icon
84
+ // fontSize (3xs/2xs/2xs), resolved to rem.
85
+ size: {
86
+ sm: {
87
+ control: { width: "3", height: "3" },
88
+ icon: { width: "0.54rem", height: "0.54rem" },
89
+ label: { fontSize: "sm" },
90
+ },
91
+ md: {
92
+ control: { width: "4", height: "4" },
93
+ icon: { width: "0.75rem", height: "0.75rem" },
94
+ label: { fontSize: "md" },
95
+ },
96
+ lg: {
97
+ control: { width: "5", height: "5" },
98
+ icon: { width: "0.75rem", height: "0.75rem" },
99
+ label: { fontSize: "lg" },
100
+ },
101
+ },
102
+ },
103
+ defaultVariants: {
104
+ size: "md",
105
+ },
71
106
  });