@microbit/ui 0.1.0-alpha.9 → 0.1.0
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/LICENSE.md +8 -0
- package/README.md +171 -20
- package/lang/ui.ar.json +62 -0
- package/lang/ui.ca.json +43 -3
- package/lang/ui.cy.json +62 -0
- package/lang/ui.de.json +44 -4
- package/lang/ui.en-us.json +40 -0
- package/lang/ui.en.json +40 -0
- package/lang/ui.es-es.json +43 -3
- package/lang/ui.fr.json +44 -4
- package/lang/ui.ga-ie.json +43 -3
- package/lang/ui.it.json +62 -0
- package/lang/ui.ja.json +44 -4
- package/lang/ui.ko.json +44 -4
- package/lang/ui.lo.json +62 -0
- package/lang/ui.lol.json +40 -0
- package/lang/ui.nl.json +44 -4
- package/lang/ui.pl.json +44 -4
- package/lang/ui.pt-br.json +44 -4
- package/lang/ui.vi.json +62 -0
- package/lang/ui.zh-cn.json +44 -4
- package/lang/ui.zh-tw.json +44 -4
- package/package.json +12 -10
- package/src/Avatar.recipe.ts +191 -0
- package/src/Avatar.tsx +278 -0
- package/src/Breadcrumb.recipe.ts +45 -0
- package/src/Breadcrumb.tsx +114 -0
- package/src/Button.recipe.ts +88 -50
- package/src/Button.tsx +67 -14
- package/src/ButtonGroup.tsx +37 -15
- package/src/Card.recipe.ts +1 -2
- package/src/Card.tsx +2 -1
- package/src/Checkbox.recipe.ts +13 -12
- package/src/Checkbox.tsx +103 -34
- package/src/CheckboxGroup.tsx +70 -0
- package/src/CloseButton.tsx +3 -3
- package/src/CloseIcon.tsx +6 -4
- package/src/Code.tsx +1 -1
- package/src/Collapse.tsx +13 -14
- package/src/ComboBox.tsx +246 -0
- package/src/Divider.tsx +40 -7
- package/src/Drawer.recipe.ts +21 -10
- package/src/Drawer.tsx +3 -4
- package/src/ExternalLink.tsx +43 -0
- package/src/Fade.tsx +18 -4
- package/src/Field.recipe.ts +114 -0
- package/src/Field.tsx +187 -0
- package/src/GridList.recipe.ts +57 -0
- package/src/GridList.tsx +81 -0
- package/src/Heading.recipe.ts +20 -1
- package/src/Heading.tsx +3 -3
- package/src/Icon.tsx +23 -7
- package/src/IconButton.tsx +8 -18
- package/src/Image.tsx +1 -1
- package/src/Input.recipe.ts +30 -26
- package/src/Input.tsx +16 -7
- package/src/InputGroup.tsx +9 -9
- package/src/Kbd.tsx +1 -1
- package/src/Link.tsx +3 -3
- package/src/LinkBox.tsx +2 -3
- package/src/LinkButton.tsx +14 -13
- package/src/List.tsx +5 -7
- package/src/ListBox.recipe.ts +43 -0
- package/src/ListBox.tsx +88 -0
- package/src/Menu.recipe.ts +22 -19
- package/src/Menu.tsx +54 -28
- package/src/Modal.recipe.ts +17 -9
- package/src/Modal.tsx +123 -24
- package/src/MoreMenuButton.tsx +63 -0
- package/src/NativeSelect.tsx +37 -14
- package/src/NativeSelectField.tsx +84 -0
- package/src/NumberField.recipe.ts +56 -15
- package/src/NumberField.tsx +74 -22
- package/src/PopoverArrow.tsx +1 -2
- package/src/ProgressBar.tsx +3 -5
- package/src/Radio.recipe.ts +13 -12
- package/src/Radio.tsx +2 -29
- package/src/RadioGroup.tsx +68 -0
- package/src/Select.recipe.ts +211 -0
- package/src/Select.tsx +194 -0
- package/src/SharedUIProvider.tsx +63 -7
- package/src/Skeleton.tsx +146 -0
- package/src/Slide.tsx +2 -2
- package/src/Slider.recipe.ts +27 -16
- package/src/Slider.tsx +20 -12
- package/src/Spinner.tsx +11 -9
- package/src/Svg.tsx +2 -3
- package/src/Switch.recipe.ts +40 -21
- package/src/Switch.tsx +35 -5
- package/src/Text.recipe.ts +26 -0
- package/src/Text.tsx +6 -2
- package/src/TextField.tsx +35 -38
- package/src/Toast.recipe.ts +52 -15
- package/src/Toast.tsx +147 -48
- package/src/Tooltip.recipe.ts +32 -0
- package/src/Tooltip.tsx +68 -28
- package/src/TooltipButton.tsx +229 -0
- package/src/UnmountCallback.tsx +2 -2
- package/src/VisuallyHidden.tsx +1 -1
- package/src/base-preset.ts +323 -84
- package/src/base-tokens.ts +0 -3
- package/src/button-icon.ts +4 -5
- package/src/data-attrs.ts +16 -0
- package/src/dense-preset.ts +107 -0
- package/src/hooks/useBreakpointValue.ts +4 -4
- package/src/hooks/useClipboard.ts +5 -6
- package/src/hooks/useDisclosure.ts +31 -0
- package/src/hooks/useMediaQuery.ts +3 -4
- package/src/hooks/usePrevious.ts +1 -4
- package/src/index.ts +19 -3
- package/src/rac-locale.ts +33 -0
- package/src/system.ts +22 -1
- package/src/TextField.recipe.ts +0 -54
package/src/Avatar.tsx
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
* A djb2-style hash of the name, its low three bytes read as a colour. The
|
|
24
|
+
* same name is always the same colour, and the exact hash is a compatibility
|
|
25
|
+
* contract: apps' existing avatar rosters keep the colours they have always
|
|
26
|
+
* had, so don't change it.
|
|
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
|
+
* Contrast rule for the generated background: perceived brightness (the
|
|
44
|
+
* 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
|
+
* First letter of the first and last words. Prefixed because it is exported
|
|
56
|
+
* from the package root, where a bare `initials` would be a broad name to
|
|
57
|
+
* 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
|
+
* The generic person glyph, the fallback when there is no name. It inherits
|
|
70
|
+
* `currentColor` — white on the no-name grey background — so it stays visible
|
|
71
|
+
* if a call site recolours.
|
|
72
|
+
*
|
|
73
|
+
* The paths are Chakra UI's avatar glyph, inlined for visual parity with the
|
|
74
|
+
* apps' original look (see the notice in LICENSE.md).
|
|
75
|
+
*/
|
|
76
|
+
export const GenericAvatarIcon = (props: SVGProps<SVGSVGElement>) => (
|
|
77
|
+
<svg viewBox="0 0 128 128" width="100%" height="100%" {...props}>
|
|
78
|
+
<path
|
|
79
|
+
fill="currentColor"
|
|
80
|
+
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"
|
|
81
|
+
/>
|
|
82
|
+
<path
|
|
83
|
+
fill="currentColor"
|
|
84
|
+
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"
|
|
85
|
+
/>
|
|
86
|
+
</svg>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
type ImageStatus = "pending" | "loading" | "loaded" | "failed";
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load the photo out of band and report how it went, so the avatar can show
|
|
93
|
+
* the initials or the icon meanwhile and keep showing them if it never
|
|
94
|
+
* arrives.
|
|
95
|
+
*
|
|
96
|
+
* The <img> element is only mounted once this says "loaded", which is what
|
|
97
|
+
* keeps a broken URL from leaving the browser's broken-image glyph inside the
|
|
98
|
+
* circle — the failure mode a fallback exists to prevent.
|
|
99
|
+
*/
|
|
100
|
+
const useImageStatus = (src?: string, srcSet?: string): ImageStatus => {
|
|
101
|
+
const [status, setStatus] = useState<ImageStatus>(
|
|
102
|
+
src ? "loading" : "pending",
|
|
103
|
+
);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!src) {
|
|
106
|
+
setStatus("pending");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// A new src starts again: without this the avatar would keep showing the
|
|
110
|
+
// previous person's photo, or stay stuck on a fallback it has outgrown.
|
|
111
|
+
setStatus("loading");
|
|
112
|
+
const img = new Image();
|
|
113
|
+
let current = true;
|
|
114
|
+
img.onload = () => {
|
|
115
|
+
if (current) {
|
|
116
|
+
setStatus("loaded");
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
img.onerror = () => {
|
|
120
|
+
if (current) {
|
|
121
|
+
setStatus("failed");
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
// srcSet before src, so the browser has the candidates to choose from
|
|
125
|
+
// when the load starts.
|
|
126
|
+
if (srcSet) {
|
|
127
|
+
img.srcset = srcSet;
|
|
128
|
+
}
|
|
129
|
+
img.src = src;
|
|
130
|
+
return () => {
|
|
131
|
+
current = false;
|
|
132
|
+
img.onload = null;
|
|
133
|
+
img.onerror = null;
|
|
134
|
+
};
|
|
135
|
+
}, [src, srcSet]);
|
|
136
|
+
return status;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export interface AvatarProps
|
|
140
|
+
extends Omit<HTMLAttributes<HTMLSpanElement>, "color" | "children">,
|
|
141
|
+
Pick<AvatarVariantProps, "size"> {
|
|
142
|
+
/**
|
|
143
|
+
* The person. Shown as initials, and hashed into the background colour, so
|
|
144
|
+
* two people are unlikely to share one.
|
|
145
|
+
*/
|
|
146
|
+
name?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Photo. The initials (or the icon) show until it has loaded, and go on
|
|
149
|
+
* showing if it fails — the avatar never renders a broken image.
|
|
150
|
+
*/
|
|
151
|
+
src?: string;
|
|
152
|
+
srcSet?: string;
|
|
153
|
+
/** Shown when there is no name. Defaults to the generic person glyph. */
|
|
154
|
+
icon?: ReactNode;
|
|
155
|
+
/** Accessible name for the icon fallback. Defaults to " avatar". */
|
|
156
|
+
iconLabel?: string;
|
|
157
|
+
/** Override how a name becomes initials. */
|
|
158
|
+
getInitials?: (name: string) => string;
|
|
159
|
+
/** A 2px ring in the avatar's border colour. */
|
|
160
|
+
showBorder?: boolean;
|
|
161
|
+
/** An `AvatarBadge`. */
|
|
162
|
+
children?: ReactNode;
|
|
163
|
+
/** Per-instance style overrides, merged after the recipe. */
|
|
164
|
+
css?: SystemStyleObject;
|
|
165
|
+
className?: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Avatar — a circular identity marker showing a photo, the initials of a
|
|
170
|
+
* name, or a generic glyph, in a colour derived from the name.
|
|
171
|
+
*
|
|
172
|
+
* Decorative in most designs — pass `aria-hidden` where the name is already
|
|
173
|
+
* beside it.
|
|
174
|
+
*/
|
|
175
|
+
export const Avatar = ({
|
|
176
|
+
name,
|
|
177
|
+
src,
|
|
178
|
+
srcSet,
|
|
179
|
+
icon,
|
|
180
|
+
iconLabel = " avatar",
|
|
181
|
+
getInitials = avatarInitials,
|
|
182
|
+
showBorder,
|
|
183
|
+
size,
|
|
184
|
+
children,
|
|
185
|
+
css: cssProp,
|
|
186
|
+
className,
|
|
187
|
+
style,
|
|
188
|
+
...rest
|
|
189
|
+
}: AvatarProps) => {
|
|
190
|
+
const status = useImageStatus(src, srcSet);
|
|
191
|
+
const isLoaded = status === "loaded";
|
|
192
|
+
const slots = avatar({ size });
|
|
193
|
+
// Only while the image isn't showing.
|
|
194
|
+
const bg = name && !isLoaded ? colorFromName(name) : undefined;
|
|
195
|
+
return (
|
|
196
|
+
<span
|
|
197
|
+
{...rest}
|
|
198
|
+
data-loaded={isLoaded || undefined}
|
|
199
|
+
className={cx(
|
|
200
|
+
slots.root,
|
|
201
|
+
showBorder ? css({ borderWidth: "2px" }) : undefined,
|
|
202
|
+
cssProp ? css(cssProp) : undefined,
|
|
203
|
+
className,
|
|
204
|
+
)}
|
|
205
|
+
style={
|
|
206
|
+
bg
|
|
207
|
+
? ({
|
|
208
|
+
...style,
|
|
209
|
+
"--avatar-bg": bg,
|
|
210
|
+
// The contrast rule, as a variable rather than a state
|
|
211
|
+
// selector so a call site's `css` colour still wins (see the
|
|
212
|
+
// recipe).
|
|
213
|
+
"--avatar-color": isLight(bg)
|
|
214
|
+
? token("colors.gray.800")
|
|
215
|
+
: token("colors.white"),
|
|
216
|
+
} as CSSProperties)
|
|
217
|
+
: style
|
|
218
|
+
}
|
|
219
|
+
>
|
|
220
|
+
{isLoaded ? (
|
|
221
|
+
<img
|
|
222
|
+
src={src}
|
|
223
|
+
srcSet={srcSet}
|
|
224
|
+
alt={name ?? iconLabel}
|
|
225
|
+
className={slots.image}
|
|
226
|
+
/>
|
|
227
|
+
) : name ? (
|
|
228
|
+
<span role="img" aria-label={name} className={slots.label}>
|
|
229
|
+
{getInitials(name)}
|
|
230
|
+
</span>
|
|
231
|
+
) : // The icon is labelled in place rather than wrapped: a wrapper
|
|
232
|
+
// would make it an inline child with a line box of its own, where
|
|
233
|
+
// directly in the flex container it is a flex item and centres
|
|
234
|
+
// exactly.
|
|
235
|
+
isValidElement(icon) ? (
|
|
236
|
+
cloneElement(icon as ReactElement<Record<string, unknown>>, {
|
|
237
|
+
role: "img",
|
|
238
|
+
"aria-label": iconLabel,
|
|
239
|
+
})
|
|
240
|
+
) : (
|
|
241
|
+
icon ?? <GenericAvatarIcon role="img" aria-label={iconLabel} />
|
|
242
|
+
)}
|
|
243
|
+
{children}
|
|
244
|
+
</span>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export interface AvatarBadgeProps
|
|
249
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, "color">,
|
|
250
|
+
Pick<AvatarVariantProps, "placement"> {
|
|
251
|
+
children?: ReactNode;
|
|
252
|
+
/**
|
|
253
|
+
* Per-instance style overrides. The badge has no size of its own — call
|
|
254
|
+
* sites set one (`boxSize: "1.5em"` scales with the avatar).
|
|
255
|
+
*/
|
|
256
|
+
css?: SystemStyleObject;
|
|
257
|
+
className?: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** AvatarBadge — a status dot pinned to a corner of its `Avatar`. */
|
|
261
|
+
export const AvatarBadge = ({
|
|
262
|
+
placement,
|
|
263
|
+
children,
|
|
264
|
+
css: cssProp,
|
|
265
|
+
className,
|
|
266
|
+
...rest
|
|
267
|
+
}: AvatarBadgeProps) => (
|
|
268
|
+
<div
|
|
269
|
+
{...rest}
|
|
270
|
+
className={cx(
|
|
271
|
+
avatar({ placement }).badge,
|
|
272
|
+
cssProp ? css(cssProp) : undefined,
|
|
273
|
+
className,
|
|
274
|
+
)}
|
|
275
|
+
>
|
|
276
|
+
{children}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
* Breadcrumb slot recipe — a flex list with a separator between items. The
|
|
10
|
+
* links themselves are the shared `Link`, so there is no link slot; the
|
|
11
|
+
* current page renders as a plain span.
|
|
12
|
+
*
|
|
13
|
+
* Registered in the base preset (base-preset.ts). No variants, so it needs
|
|
14
|
+
* no `staticCss` entry.
|
|
15
|
+
*/
|
|
16
|
+
export const breadcrumb = defineSlotRecipe({
|
|
17
|
+
className: "breadcrumb",
|
|
18
|
+
slots: ["root", "list", "item", "separator"],
|
|
19
|
+
base: {
|
|
20
|
+
root: {},
|
|
21
|
+
list: {
|
|
22
|
+
display: "flex",
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
listStyle: "none",
|
|
25
|
+
margin: 0,
|
|
26
|
+
padding: 0,
|
|
27
|
+
},
|
|
28
|
+
item: {
|
|
29
|
+
display: "inline-flex",
|
|
30
|
+
alignItems: "center",
|
|
31
|
+
// The separator renders inside every item (no children introspection);
|
|
32
|
+
// the last item's simply doesn't show.
|
|
33
|
+
"&:last-of-type [data-separator]": { display: "none" },
|
|
34
|
+
},
|
|
35
|
+
separator: {
|
|
36
|
+
mx: "2",
|
|
37
|
+
// A chevron separator points along the trail, so it has to turn round
|
|
38
|
+
// in RTL. Scoped to an svg because the default separator is the text
|
|
39
|
+
// "/", which would become "\": a caller passing a glyph owns its
|
|
40
|
+
// direction, one passing an icon gets it handled. Override via the
|
|
41
|
+
// separator slot's css for an icon that shouldn't turn.
|
|
42
|
+
_rtl: { "& svg": { transform: "scaleX(-1)" } },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (c) 2026, Micro:bit Educational Foundation and contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { createContext, ReactNode, useContext } from "react";
|
|
7
|
+
import { useIntl } from "react-intl";
|
|
8
|
+
import { css, cx } from "styled-system/css";
|
|
9
|
+
import { styled, type HTMLStyledProps } from "styled-system/jsx";
|
|
10
|
+
import { breadcrumb } from "styled-system/recipes";
|
|
11
|
+
import { SystemStyleObject } from "styled-system/types";
|
|
12
|
+
import { Link } from "./Link";
|
|
13
|
+
import { uiMessage } from "./messages";
|
|
14
|
+
|
|
15
|
+
// The separator is declared once on the Breadcrumb and rendered by every
|
|
16
|
+
// item; the current-page flag hops from BreadcrumbItem to the BreadcrumbLink
|
|
17
|
+
// inside it.
|
|
18
|
+
const SeparatorContext = createContext<ReactNode>("/");
|
|
19
|
+
const CurrentPageContext = createContext(false);
|
|
20
|
+
|
|
21
|
+
export interface BreadcrumbProps {
|
|
22
|
+
/** Between items; an element or string. Default "/". */
|
|
23
|
+
separator?: ReactNode;
|
|
24
|
+
/** Per-instance style overrides for the nav (e.g. fontSize). */
|
|
25
|
+
css?: SystemStyleObject;
|
|
26
|
+
className?: string;
|
|
27
|
+
/** BreadcrumbItems. */
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Breadcrumb — a navigation trail: nav > ol > li with a separator between
|
|
33
|
+
* items and the current page as plain text with `aria-current="page"`.
|
|
34
|
+
*/
|
|
35
|
+
export const Breadcrumb = ({
|
|
36
|
+
separator = "/",
|
|
37
|
+
css: cssProp,
|
|
38
|
+
className,
|
|
39
|
+
children,
|
|
40
|
+
}: BreadcrumbProps) => {
|
|
41
|
+
const intl = useIntl();
|
|
42
|
+
const slots = breadcrumb();
|
|
43
|
+
return (
|
|
44
|
+
<nav
|
|
45
|
+
aria-label={intl.formatMessage(uiMessage("ui.breadcrumb"))}
|
|
46
|
+
className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
|
|
47
|
+
>
|
|
48
|
+
<SeparatorContext.Provider value={separator}>
|
|
49
|
+
<ol className={slots.list}>{children}</ol>
|
|
50
|
+
</SeparatorContext.Provider>
|
|
51
|
+
</nav>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface BreadcrumbItemProps {
|
|
56
|
+
/**
|
|
57
|
+
* Marks this item as the current page: its BreadcrumbLink renders as a
|
|
58
|
+
* plain span with `aria-current="page"` rather than a link.
|
|
59
|
+
*/
|
|
60
|
+
isCurrentPage?: boolean;
|
|
61
|
+
css?: SystemStyleObject;
|
|
62
|
+
className?: string;
|
|
63
|
+
children: ReactNode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const BreadcrumbItem = ({
|
|
67
|
+
isCurrentPage = false,
|
|
68
|
+
css: cssProp,
|
|
69
|
+
className,
|
|
70
|
+
children,
|
|
71
|
+
}: BreadcrumbItemProps) => {
|
|
72
|
+
const separator = useContext(SeparatorContext);
|
|
73
|
+
const slots = breadcrumb();
|
|
74
|
+
return (
|
|
75
|
+
<li
|
|
76
|
+
className={cx(slots.item, cssProp ? css(cssProp) : undefined, className)}
|
|
77
|
+
>
|
|
78
|
+
<CurrentPageContext.Provider value={isCurrentPage}>
|
|
79
|
+
{children}
|
|
80
|
+
</CurrentPageContext.Provider>
|
|
81
|
+
<span
|
|
82
|
+
data-separator
|
|
83
|
+
role="presentation"
|
|
84
|
+
aria-hidden
|
|
85
|
+
className={slots.separator}
|
|
86
|
+
>
|
|
87
|
+
{separator}
|
|
88
|
+
</span>
|
|
89
|
+
</li>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type BreadcrumbLinkProps = HTMLStyledProps<"a">;
|
|
94
|
+
|
|
95
|
+
// The current page's text: same element shape as the link (so call-site
|
|
96
|
+
// style props keep working) minus the interactivity.
|
|
97
|
+
const CurrentPageText = styled("span");
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The trail's link: the shared `Link`, or a plain span with
|
|
101
|
+
* `aria-current="page"` inside an item marked `isCurrentPage`.
|
|
102
|
+
*/
|
|
103
|
+
export const BreadcrumbLink = (props: BreadcrumbLinkProps) => {
|
|
104
|
+
const isCurrentPage = useContext(CurrentPageContext);
|
|
105
|
+
if (isCurrentPage) {
|
|
106
|
+
const { href: _href, children, ...rest } = props;
|
|
107
|
+
return (
|
|
108
|
+
<CurrentPageText aria-current="page" {...rest}>
|
|
109
|
+
{children}
|
|
110
|
+
</CurrentPageText>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return <Link {...props} />;
|
|
114
|
+
};
|
package/src/Button.recipe.ts
CHANGED
|
@@ -5,26 +5,24 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { defineRecipe } from "@pandacss/dev";
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
// token category).
|
|
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,
|
|
12
|
+
"background-color, border-color, color, fill, stroke, opacity, transform";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Button recipe —
|
|
15
|
-
* borderRadius (`button` = 2rem)
|
|
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
|
|
19
|
-
*
|
|
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.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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: "
|
|
98
|
-
color: "
|
|
98
|
+
borderColor: "button.secondaryBorder",
|
|
99
|
+
color: "button.secondaryText",
|
|
99
100
|
bg: "transparent",
|
|
100
|
-
_hover: {
|
|
101
|
-
|
|
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
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
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: "
|
|
120
|
-
_hover: {
|
|
121
|
-
|
|
125
|
+
bg: "button.primaryBg",
|
|
126
|
+
_hover: {
|
|
127
|
+
bg: "button.primaryHoverBg",
|
|
128
|
+
_disabled: { bg: "button.primaryBg" },
|
|
129
|
+
},
|
|
130
|
+
_active: { bg: "button.primaryActiveBg" },
|
|
122
131
|
},
|
|
123
|
-
|
|
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: "
|
|
126
|
-
color: "
|
|
153
|
+
borderColor: "colorPalette.500",
|
|
154
|
+
color: "colorPalette.600",
|
|
127
155
|
bg: "transparent",
|
|
128
|
-
_hover: { borderColor: "
|
|
129
|
-
_active: { bg: "
|
|
156
|
+
_hover: { borderColor: "colorPalette.600", color: "colorPalette.700" },
|
|
157
|
+
_active: { bg: "colorPalette.50" },
|
|
130
158
|
},
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
bg: "
|
|
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
|
|
142
|
-
//
|
|
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
|
});
|