@microbit/ui 0.1.0-alpha.13 → 0.1.0-alpha.15

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/README.md CHANGED
@@ -19,8 +19,10 @@ an app must do:
19
19
 
20
20
  1. **Panda preset stack** (`panda.config.ts`): `@pandacss/preset-base`, then
21
21
  the **base preset** (`@microbit/ui/base-preset` — the complete micro:bit
22
- design system), then optionally the app's own preset, then optionally a
23
- **private brand preset** (Foundation colours, licensed fonts).
22
+ design system), then optionally `@microbit/ui/dense-preset` (the × 0.88
23
+ spacing / × 0.9 font-size density the information-dense apps use), then
24
+ optionally the app's own preset, then optionally a **private brand
25
+ preset** (Foundation colours, licensed fonts).
24
26
 
25
27
  Later presets override earlier ones token-by-token — the base recipes and
26
28
  semantic tokens reference the brand tokens, which is how a brand swap
@@ -162,9 +164,9 @@ keep them stable:
162
164
  - Brand/app presets may change token _values_, never token _names_.
163
165
 
164
166
  Semantic tokens (`languageText`, `statusBarBg`, `danger.*`, `toast*Bg`,
165
- `controlCheckedBg`, `focusBorder`, …) are the extension points brand presets
166
- override; they resolve through var indirection, so overrides apply wherever
167
- the token is consumed.
167
+ `button.*`, `controlCheckedBg`, `focusBorder`, …) are the extension points
168
+ brand presets override; they resolve through var indirection, so overrides
169
+ apply wherever the token is consumed.
168
170
 
169
171
  ## Runtime token lookups
170
172
 
@@ -0,0 +1,22 @@
1
+ {
2
+ "ui.close-action": {
3
+ "defaultMessage": "Cau",
4
+ "description": "Close button text or label"
5
+ },
6
+ "ui.toast-status-error": {
7
+ "defaultMessage": "Gwall",
8
+ "description": "Announced by screen readers before an error notification"
9
+ },
10
+ "ui.toast-status-info": {
11
+ "defaultMessage": "Information",
12
+ "description": "Announced by screen readers before an informational notification"
13
+ },
14
+ "ui.toast-status-success": {
15
+ "defaultMessage": "Success",
16
+ "description": "Announced by screen readers before a success notification"
17
+ },
18
+ "ui.toast-status-warning": {
19
+ "defaultMessage": "Rhybudd",
20
+ "description": "Announced by screen readers before a warning notification"
21
+ }
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "ui.close-action": {
3
+ "defaultMessage": "Chiudi",
4
+ "description": "Close button text or label"
5
+ },
6
+ "ui.toast-status-error": {
7
+ "defaultMessage": "Errore",
8
+ "description": "Announced by screen readers before an error notification"
9
+ },
10
+ "ui.toast-status-info": {
11
+ "defaultMessage": "Information",
12
+ "description": "Announced by screen readers before an informational notification"
13
+ },
14
+ "ui.toast-status-success": {
15
+ "defaultMessage": "Success",
16
+ "description": "Announced by screen readers before a success notification"
17
+ },
18
+ "ui.toast-status-warning": {
19
+ "defaultMessage": "Attenzione",
20
+ "description": "Announced by screen readers before a warning notification"
21
+ }
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microbit/ui",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.15",
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",
@@ -8,6 +8,7 @@
8
8
  ".": "./src/index.ts",
9
9
  "./base-preset": "./src/base-preset.ts",
10
10
  "./base-tokens": "./src/base-tokens.ts",
11
+ "./dense-preset": "./src/dense-preset.ts",
11
12
  "./messages": "./src/messages.ts",
12
13
  "./postcss-legacy-safari": "./postcss-legacy-safari.cjs",
13
14
  "./reset.css": "./reset.css",
@@ -0,0 +1,168 @@
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
+ * Avatar slot recipe — Chakra's avatar: a circle showing an image, the
10
+ * initials of a name, or a generic person glyph, optionally with a badge
11
+ * pinned to one corner.
12
+ *
13
+ * The background and text colour come from `var(--avatar-bg)` and
14
+ * `var(--avatar-color)` rather than being flat values, because the component
15
+ * derives them from the name (see Avatar.tsx) and writes them as inline custom
16
+ * properties — exactly as Chakra did. Two reasons, both about letting a call
17
+ * site win with a plain `css={{ bg: …, color: … }}`: an inline *property*
18
+ * would beat any class, where an inline *variable* only feeds this
19
+ * declaration; and both must stay single-class selectors, since a state
20
+ * selector like `&[data-light-bg]` outranks the call site's utility class on
21
+ * specificity wherever cascade layers aren't in play — which is every app
22
+ * still coexisting with Chakra (playbook gotcha #40).
23
+ *
24
+ * Sizes are Chakra's, with its `calc(size / 2.5)` font size resolved per size
25
+ * so an app preset can restate either independently (classroom's avatars are
26
+ * a grade larger than Chakra's).
27
+ *
28
+ * Registered in the base preset (base-preset.ts), which also has the
29
+ * `staticCss` entry that keeps the runtime-prop variants generated.
30
+ */
31
+ export const avatar = defineSlotRecipe({
32
+ className: "avatar",
33
+ slots: ["root", "label", "image", "badge"],
34
+ base: {
35
+ root: {
36
+ display: "inline-flex",
37
+ alignItems: "center",
38
+ justifyContent: "center",
39
+ flexShrink: 0,
40
+ position: "relative",
41
+ verticalAlign: "top",
42
+ textAlign: "center",
43
+ textTransform: "uppercase",
44
+ fontWeight: "medium",
45
+ borderRadius: "full",
46
+ // Chakra's no-name defaults; the name-derived pair arrives inline.
47
+ background: "var(--avatar-bg, token(colors.gray.400))",
48
+ color: "var(--avatar-color, token(colors.white))",
49
+ borderColor: "white",
50
+ },
51
+ label: {
52
+ lineHeight: "1",
53
+ },
54
+ image: {
55
+ width: "100%",
56
+ height: "100%",
57
+ objectFit: "cover",
58
+ borderRadius: "inherit",
59
+ },
60
+ badge: {
61
+ position: "absolute",
62
+ display: "flex",
63
+ alignItems: "center",
64
+ justifyContent: "center",
65
+ borderRadius: "full",
66
+ // em-relative, so a badge keeps its proportions at every avatar size.
67
+ borderWidth: "0.2em",
68
+ borderStyle: "solid",
69
+ borderColor: "white",
70
+ },
71
+ },
72
+ variants: {
73
+ // Chakra's scale: the container size, and Chakra's `calc(size / 2.5)`
74
+ // font size kept as a calc over the same token so both track a preset
75
+ // that rescales `sizes` (the dense preset does, by 0.88).
76
+ //
77
+ // The font size lands on the root *and* the label, as Chakra's did
78
+ // (through one variable). They are separate declarations so an app can
79
+ // move one without the other: the root's is the em basis for a badge,
80
+ // the label's is how big the initials are, and the two are not always
81
+ // the same wish.
82
+ size: {
83
+ "2xs": {
84
+ root: {
85
+ width: "4",
86
+ height: "4",
87
+ fontSize: "calc(token(sizes.4) / 2.5)",
88
+ },
89
+ label: { fontSize: "calc(token(sizes.4) / 2.5)" },
90
+ },
91
+ xs: {
92
+ root: {
93
+ width: "6",
94
+ height: "6",
95
+ fontSize: "calc(token(sizes.6) / 2.5)",
96
+ },
97
+ label: { fontSize: "calc(token(sizes.6) / 2.5)" },
98
+ },
99
+ sm: {
100
+ root: {
101
+ width: "8",
102
+ height: "8",
103
+ fontSize: "calc(token(sizes.8) / 2.5)",
104
+ },
105
+ label: { fontSize: "calc(token(sizes.8) / 2.5)" },
106
+ },
107
+ md: {
108
+ root: {
109
+ width: "12",
110
+ height: "12",
111
+ fontSize: "calc(token(sizes.12) / 2.5)",
112
+ },
113
+ label: { fontSize: "calc(token(sizes.12) / 2.5)" },
114
+ },
115
+ lg: {
116
+ root: {
117
+ width: "16",
118
+ height: "16",
119
+ fontSize: "calc(token(sizes.16) / 2.5)",
120
+ },
121
+ label: { fontSize: "calc(token(sizes.16) / 2.5)" },
122
+ },
123
+ xl: {
124
+ root: {
125
+ width: "24",
126
+ height: "24",
127
+ fontSize: "calc(token(sizes.24) / 2.5)",
128
+ },
129
+ label: { fontSize: "calc(token(sizes.24) / 2.5)" },
130
+ },
131
+ "2xl": {
132
+ root: {
133
+ width: "32",
134
+ height: "32",
135
+ fontSize: "calc(token(sizes.32) / 2.5)",
136
+ },
137
+ label: { fontSize: "calc(token(sizes.32) / 2.5)" },
138
+ },
139
+ },
140
+ /** Which corner the badge sits in. Chakra's placements, same offsets. */
141
+ placement: {
142
+ "top-start": {
143
+ badge: {
144
+ top: "0",
145
+ insetStart: "0",
146
+ transform: "translate(-25%, -25%)",
147
+ },
148
+ },
149
+ "top-end": {
150
+ badge: { top: "0", insetEnd: "0", transform: "translate(25%, -25%)" },
151
+ },
152
+ "bottom-start": {
153
+ badge: {
154
+ bottom: "0",
155
+ insetStart: "0",
156
+ transform: "translate(-25%, 25%)",
157
+ },
158
+ },
159
+ "bottom-end": {
160
+ badge: { bottom: "0", insetEnd: "0", transform: "translate(25%, 25%)" },
161
+ },
162
+ },
163
+ },
164
+ defaultVariants: {
165
+ size: "md",
166
+ placement: "bottom-end",
167
+ },
168
+ });
package/src/Avatar.tsx ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import {
7
+ cloneElement,
8
+ CSSProperties,
9
+ HTMLAttributes,
10
+ isValidElement,
11
+ ReactElement,
12
+ ReactNode,
13
+ SVGProps,
14
+ useEffect,
15
+ useState,
16
+ } from "react";
17
+ import { css, cx } from "styled-system/css";
18
+ import { avatar, AvatarVariantProps } from "styled-system/recipes";
19
+ import { token } from "styled-system/tokens";
20
+ import { SystemStyleObject } from "styled-system/types";
21
+
22
+ /**
23
+ * Chakra's `randomColor({ string })`, reproduced exactly: a djb2-style hash of
24
+ * the name, its low three bytes read as a colour. Not random despite the name
25
+ * — the same name is always the same colour, which is the point, and
26
+ * reproducing the hash means avatars keep the colours they had under Chakra.
27
+ */
28
+ const colorFromName = (name: string): string => {
29
+ let hash = 0;
30
+ for (let i = 0; i < name.length; i += 1) {
31
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
32
+ hash = hash & hash;
33
+ }
34
+ let color = "#";
35
+ for (let j = 0; j < 3; j += 1) {
36
+ const value = (hash >> (j * 8)) & 255;
37
+ color += `00${value.toString(16)}`.slice(-2);
38
+ }
39
+ return color;
40
+ };
41
+
42
+ /**
43
+ * Chakra's contrast rule for the generated background: perceived brightness
44
+ * (the classic 299/587/114 weighting) below 128 counts as dark, and dark
45
+ * backgrounds take white text.
46
+ */
47
+ const isLight = (hex: string): boolean => {
48
+ const r = parseInt(hex.slice(1, 3), 16);
49
+ const g = parseInt(hex.slice(3, 5), 16);
50
+ const b = parseInt(hex.slice(5, 7), 16);
51
+ return (r * 299 + g * 587 + b * 114) / 1000 >= 128;
52
+ };
53
+
54
+ /**
55
+ * Chakra's `initials`: first letter of the first and last words. Prefixed
56
+ * because it is exported from the package root, where a bare `initials`
57
+ * would be a broad name to claim.
58
+ */
59
+ export const avatarInitials = (name: string): string => {
60
+ const names = name.trim().split(" ");
61
+ const firstName = names[0] ?? "";
62
+ const lastName = names.length > 1 ? names[names.length - 1] : "";
63
+ return firstName && lastName
64
+ ? `${firstName.charAt(0)}${lastName.charAt(0)}`
65
+ : firstName.charAt(0);
66
+ };
67
+
68
+ /**
69
+ * Chakra's generic person glyph, the fallback when there is no name. Chakra
70
+ * hardcoded it white; here it inherits `currentColor`, which is the same white
71
+ * on the no-name grey background and stays visible if a call site recolours.
72
+ */
73
+ export const GenericAvatarIcon = (props: SVGProps<SVGSVGElement>) => (
74
+ <svg viewBox="0 0 128 128" width="100%" height="100%" {...props}>
75
+ <path
76
+ fill="currentColor"
77
+ d="M103,102.1388 C93.094,111.92 79.3504,118 64.1638,118 C48.8056,118 34.9294,111.768 25,101.7892 L25,95.2 C25,86.8096 31.981,80 40.6,80 L87.4,80 C96.019,80 103,86.8096 103,95.2 L103,102.1388 Z"
78
+ />
79
+ <path
80
+ fill="currentColor"
81
+ d="M63.9961647,24 C51.2938136,24 41,34.2938136 41,46.9961647 C41,59.7061864 51.2938136,70 63.9961647,70 C76.6985159,70 87,59.7061864 87,46.9961647 C87,34.2938136 76.6985159,24 63.9961647,24"
82
+ />
83
+ </svg>
84
+ );
85
+
86
+ type ImageStatus = "pending" | "loading" | "loaded" | "failed";
87
+
88
+ /**
89
+ * Chakra's `useImage`: load the photo out of band and report how it went, so
90
+ * the avatar can show the initials or the icon meanwhile and keep showing
91
+ * them if it never arrives.
92
+ *
93
+ * The <img> element is only mounted once this says "loaded", which is what
94
+ * keeps a broken URL from leaving the browser's broken-image glyph inside the
95
+ * circle — the failure mode a fallback exists to prevent.
96
+ */
97
+ const useImageStatus = (src?: string, srcSet?: string): ImageStatus => {
98
+ const [status, setStatus] = useState<ImageStatus>(
99
+ src ? "loading" : "pending",
100
+ );
101
+ useEffect(() => {
102
+ if (!src) {
103
+ setStatus("pending");
104
+ return;
105
+ }
106
+ // A new src starts again: without this the avatar would keep showing the
107
+ // previous person's photo, or stay stuck on a fallback it has outgrown.
108
+ setStatus("loading");
109
+ const img = new Image();
110
+ let current = true;
111
+ img.onload = () => {
112
+ if (current) {
113
+ setStatus("loaded");
114
+ }
115
+ };
116
+ img.onerror = () => {
117
+ if (current) {
118
+ setStatus("failed");
119
+ }
120
+ };
121
+ // srcSet before src, so the browser has the candidates to choose from
122
+ // when the load starts.
123
+ if (srcSet) {
124
+ img.srcset = srcSet;
125
+ }
126
+ img.src = src;
127
+ return () => {
128
+ current = false;
129
+ img.onload = null;
130
+ img.onerror = null;
131
+ };
132
+ }, [src, srcSet]);
133
+ return status;
134
+ };
135
+
136
+ export interface AvatarProps
137
+ extends Omit<HTMLAttributes<HTMLSpanElement>, "color" | "children">,
138
+ Pick<AvatarVariantProps, "size"> {
139
+ /**
140
+ * The person. Shown as initials, and hashed into the background colour, so
141
+ * two people are unlikely to share one.
142
+ */
143
+ name?: string;
144
+ /**
145
+ * Photo. The initials (or the icon) show until it has loaded, and go on
146
+ * showing if it fails — the avatar never renders a broken image.
147
+ */
148
+ src?: string;
149
+ srcSet?: string;
150
+ /** Shown when there is no name. Defaults to Chakra's person glyph. */
151
+ icon?: ReactNode;
152
+ /** Accessible name for the icon fallback. Chakra's default was " avatar". */
153
+ iconLabel?: string;
154
+ /** Override how a name becomes initials. */
155
+ getInitials?: (name: string) => string;
156
+ /** Chakra's `showBorder`: a 2px ring in the avatar's border colour. */
157
+ showBorder?: boolean;
158
+ /** An `AvatarBadge`. */
159
+ children?: ReactNode;
160
+ /** Per-instance style overrides, merged after the recipe. */
161
+ css?: SystemStyleObject;
162
+ className?: string;
163
+ }
164
+
165
+ /**
166
+ * Avatar — Chakra's <Avatar>: a circular identity marker showing a photo, the
167
+ * initials of a name, or a generic glyph, in a colour derived from the name.
168
+ *
169
+ * Decorative in most designs — pass `aria-hidden` where the name is already
170
+ * beside it, as Chakra's call sites did.
171
+ */
172
+ export const Avatar = ({
173
+ name,
174
+ src,
175
+ srcSet,
176
+ icon,
177
+ iconLabel = " avatar",
178
+ getInitials = avatarInitials,
179
+ showBorder,
180
+ size,
181
+ children,
182
+ css: cssProp,
183
+ className,
184
+ style,
185
+ ...rest
186
+ }: AvatarProps) => {
187
+ const status = useImageStatus(src, srcSet);
188
+ const isLoaded = status === "loaded";
189
+ const slots = avatar({ size });
190
+ // Only while the image isn't showing, matching Chakra's `:not([data-loaded])`.
191
+ const bg = name && !isLoaded ? colorFromName(name) : undefined;
192
+ return (
193
+ <span
194
+ {...rest}
195
+ data-loaded={isLoaded || undefined}
196
+ className={cx(
197
+ slots.root,
198
+ showBorder ? css({ borderWidth: "2px" }) : undefined,
199
+ cssProp ? css(cssProp) : undefined,
200
+ className,
201
+ )}
202
+ style={
203
+ bg
204
+ ? ({
205
+ ...style,
206
+ "--avatar-bg": bg,
207
+ // Chakra's contrast rule, as a variable rather than a state
208
+ // selector so a call site's `css` colour still wins (see the
209
+ // recipe).
210
+ "--avatar-color": isLight(bg)
211
+ ? token("colors.gray.800")
212
+ : token("colors.white"),
213
+ } as CSSProperties)
214
+ : style
215
+ }
216
+ >
217
+ {isLoaded ? (
218
+ <img
219
+ src={src}
220
+ srcSet={srcSet}
221
+ alt={name ?? iconLabel}
222
+ className={slots.image}
223
+ />
224
+ ) : name ? (
225
+ <span role="img" aria-label={name} className={slots.label}>
226
+ {getInitials(name)}
227
+ </span>
228
+ ) : // The icon is labelled in place rather than wrapped, as Chakra did:
229
+ // a wrapper would make it an inline child with a line box of its own,
230
+ // where directly in the flex container it is a flex item and centres
231
+ // exactly.
232
+ isValidElement(icon) ? (
233
+ cloneElement(icon as ReactElement<Record<string, unknown>>, {
234
+ role: "img",
235
+ "aria-label": iconLabel,
236
+ })
237
+ ) : (
238
+ icon ?? <GenericAvatarIcon role="img" aria-label={iconLabel} />
239
+ )}
240
+ {children}
241
+ </span>
242
+ );
243
+ };
244
+
245
+ export interface AvatarBadgeProps
246
+ extends Omit<HTMLAttributes<HTMLDivElement>, "color">,
247
+ Pick<AvatarVariantProps, "placement"> {
248
+ children?: ReactNode;
249
+ /**
250
+ * Per-instance style overrides. The badge has no size of its own — Chakra's
251
+ * didn't either, so call sites set one (`boxSize: "1.5em"` scales with the
252
+ * avatar).
253
+ */
254
+ css?: SystemStyleObject;
255
+ className?: string;
256
+ }
257
+
258
+ /** AvatarBadge — a status dot pinned to a corner of its `Avatar`. */
259
+ export const AvatarBadge = ({
260
+ placement,
261
+ children,
262
+ css: cssProp,
263
+ className,
264
+ ...rest
265
+ }: AvatarBadgeProps) => (
266
+ <div
267
+ {...rest}
268
+ className={cx(
269
+ avatar({ placement }).badge,
270
+ cssProp ? css(cssProp) : undefined,
271
+ className,
272
+ )}
273
+ >
274
+ {children}
275
+ </div>
276
+ );
@@ -24,7 +24,7 @@ const transitionCommon =
24
24
  * Chakra variants plus the family-wide `language`/`toolbar` variants; a
25
25
  * consuming app's preset extends it with app vocabulary (e.g. ml-trainer's
26
26
  * `led`/`record*`/`secondary-disabled`). Brand divergence within a variant is
27
- * token-driven (see the `languageText` semantic tokens).
27
+ * token-driven (see the `button.*` and `languageText` semantic tokens).
28
28
  *
29
29
  * Registered in the base preset (base-preset.ts).
30
30
  */
@@ -92,13 +92,22 @@ export const button = defineRecipe({
92
92
  textDecoration: "underline",
93
93
  },
94
94
  },
95
+ // Colours come from the `button.*` semantic tokens so the family's two
96
+ // button idioms (brand-coloured vs black-on-white) share this recipe —
97
+ // see the token block in base-preset.ts.
95
98
  secondary: {
96
99
  borderWidth: "2px",
97
- borderColor: "brand.500",
98
- color: "brand.700",
100
+ borderColor: "button.secondaryBorder",
101
+ color: "button.secondaryText",
99
102
  bg: "transparent",
100
- _hover: { borderColor: "brand.600" },
101
- _active: { bg: "brand.50", borderColor: "brand.700" },
103
+ _hover: {
104
+ borderColor: "button.secondaryHoverBorder",
105
+ bg: "button.secondaryHoverBg",
106
+ },
107
+ _active: {
108
+ bg: "button.secondaryActiveBg",
109
+ borderColor: "button.secondaryActiveBorder",
110
+ },
102
111
  },
103
112
  ghost: {
104
113
  color: "black",
@@ -116,17 +125,35 @@ export const button = defineRecipe({
116
125
  },
117
126
  primary: {
118
127
  color: "white",
119
- bg: "brand.500",
120
- _hover: { bg: "brand.600", _disabled: { bg: "brand.500" } },
121
- _active: { bg: "brand.700" },
128
+ bg: "button.primaryBg",
129
+ _hover: {
130
+ bg: "button.primaryHoverBg",
131
+ _disabled: { bg: "button.primaryBg" },
132
+ },
133
+ _active: { bg: "button.primaryActiveBg" },
122
134
  },
135
+ // 600/700, matching what python-editor's Chakra outline + red
136
+ // colorScheme resolved to. (Extracted from ml-trainer at 500/600, but
137
+ // its one warning button tolerates the darkening; python-editor's
138
+ // "Reset project" was visibly lighter than its Chakra self.)
139
+ // NOTE (2026-08-02, from classroom): two Chakra `outline` shapes have
140
+ // no home here and are currently restated per call site in that app —
141
+ // worth considering as variants once a second consumer wants them.
142
+ // - a neutral outline (transparent, 1px gray.200, inherited text,
143
+ // gray.50/gray.100 hover/press): Chakra's default-colorScheme
144
+ // `outline`, and python-editor's *default* variant per the
145
+ // playbook's cross-app vocabulary, so likely already a 2-app shape.
146
+ // - an on-colour outline (white 2px + white text over a coloured bar,
147
+ // whiteAlpha hover/press): Chakra's `outline` + `whiteAlpha`.
148
+ // `warning` below is the *destructive* outline and is not a substitute
149
+ // for either; `warningSolid` did map exactly onto Chakra solid+red.
123
150
  warning: {
124
151
  borderWidth: "2px",
125
- borderColor: "danger.500",
126
- color: "danger.500",
152
+ borderColor: "danger.600",
153
+ color: "danger.600",
127
154
  bg: "transparent",
128
- _hover: { borderColor: "danger.600" },
129
- _active: { bg: "danger.50", borderColor: "danger.500" },
155
+ _hover: { borderColor: "danger.700", color: "danger.700" },
156
+ _active: { bg: "danger.50" },
130
157
  },
131
158
  // Chakra's built-in solid + red colorScheme (destructive confirm
132
159
  // buttons). Same values as `record` today, but a separate variant so