@microbit/ui 0.1.0-alpha.20 → 0.1.0-alpha.21
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 +1 -1
- package/src/Modal.recipe.ts +7 -1
- package/src/Modal.tsx +6 -0
- package/src/Toast.tsx +39 -1
- package/src/base-preset.ts +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microbit/ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.21",
|
|
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/src/Modal.recipe.ts
CHANGED
|
@@ -54,7 +54,13 @@ export const dialog = defineSlotRecipe({
|
|
|
54
54
|
overlay: {
|
|
55
55
|
position: "fixed",
|
|
56
56
|
inset: 0,
|
|
57
|
-
|
|
57
|
+
// 100vw, not 100%: react-aria's scroll lock reserves the root
|
|
58
|
+
// scrollbar gutter (scrollbar-gutter: stable), which narrows the
|
|
59
|
+
// containing block for fixed elements — 100% leaves an uncovered
|
|
60
|
+
// strip where the page scrollbar was. Viewport units span the
|
|
61
|
+
// reserved gutter, so the backdrop (and a full-size dialog) reach
|
|
62
|
+
// the real viewport edge.
|
|
63
|
+
w: "100vw",
|
|
58
64
|
h: "100%",
|
|
59
65
|
bg: "blackAlpha.600",
|
|
60
66
|
zIndex: "modal",
|
package/src/Modal.tsx
CHANGED
|
@@ -195,6 +195,12 @@ export const Modal = ({
|
|
|
195
195
|
}}
|
|
196
196
|
isDismissable={isDismissable}
|
|
197
197
|
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
|
|
198
|
+
// Marker for the html:has() rule in base-preset.ts that releases the
|
|
199
|
+
// scroll lock's reserved scrollbar gutter while a full-size dialog is
|
|
200
|
+
// open: the reserved strip is a hit-testing dead zone (clicks fall
|
|
201
|
+
// through to the root and read as outside-dismissal), and the page
|
|
202
|
+
// reflowing behind an opaque full-screen dialog is invisible.
|
|
203
|
+
data-fullsize={size === "full" || undefined}
|
|
198
204
|
className={cx(
|
|
199
205
|
slots.overlay,
|
|
200
206
|
motionlessClass,
|
package/src/Toast.tsx
CHANGED
|
@@ -36,11 +36,43 @@ export interface ToastContent {
|
|
|
36
36
|
isClosable?: boolean;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Enter/exit/reflow animation: queue updates run inside a view transition
|
|
40
|
+
// (react-aria's supported mechanism — toasts unmount synchronously, so CSS
|
|
41
|
+
// transitions on the element can't animate the exit). The keyframes and the
|
|
42
|
+
// ::view-transition rules live in base-preset.ts, scoped by this class,
|
|
43
|
+
// which marks the transition as toast-initiated while it runs: the rules
|
|
44
|
+
// select entering/exiting groups with `(*)`, avoiding view-transition-class
|
|
45
|
+
// (needs Safari 18.2/Chrome 125 vs 18.0/111 for the API itself), and the
|
|
46
|
+
// scoping keeps them — and the pointer-events override — away from any view
|
|
47
|
+
// transitions the app runs. Browsers without the API and reduced-motion
|
|
48
|
+
// users get the bare update.
|
|
49
|
+
const TRANSITION_CLASS = "microbit-ui-toast-transition";
|
|
50
|
+
let activeTransitions = 0;
|
|
51
|
+
const wrapUpdate = (fn: () => void) => {
|
|
52
|
+
if (
|
|
53
|
+
typeof document !== "undefined" &&
|
|
54
|
+
document.startViewTransition &&
|
|
55
|
+
!window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
56
|
+
) {
|
|
57
|
+
activeTransitions++;
|
|
58
|
+
document.documentElement.classList.add(TRANSITION_CLASS);
|
|
59
|
+
const transition = document.startViewTransition(fn);
|
|
60
|
+
transition.finished.finally(() => {
|
|
61
|
+
if (--activeTransitions === 0) {
|
|
62
|
+
document.documentElement.classList.remove(TRANSITION_CLASS);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
fn();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
39
70
|
// Module-level queue shared by useToast() and the <ToastProvider/> region.
|
|
40
71
|
// (RAC's Toast API is still flagged UNSTABLE_*; the surface is small and behind
|
|
41
72
|
// this module, so a swap to a custom queue later is contained.)
|
|
42
73
|
export const toastQueue = new RACToastQueue<ToastContent>({
|
|
43
74
|
maxVisibleToasts: 5,
|
|
75
|
+
wrapUpdate,
|
|
44
76
|
});
|
|
45
77
|
|
|
46
78
|
// Status icon matching Chakra's AlertIcon (filled glyphs, coloured by the
|
|
@@ -83,7 +115,13 @@ export const ToastProvider = () => {
|
|
|
83
115
|
{({ toast }) => {
|
|
84
116
|
const status = toast.content.status ?? "info";
|
|
85
117
|
return (
|
|
86
|
-
<RACToast
|
|
118
|
+
<RACToast
|
|
119
|
+
toast={toast}
|
|
120
|
+
className={toastRecipe({ status }).root}
|
|
121
|
+
// A unique view-transition-name per toast creates its snapshot
|
|
122
|
+
// group and lets old/new pair up across the transition.
|
|
123
|
+
style={{ viewTransitionName: toast.key }}
|
|
124
|
+
>
|
|
87
125
|
<Icon as={statusIcon[status]} className={slots.icon} aria-hidden />
|
|
88
126
|
<RACToastContent>
|
|
89
127
|
{/* Colour and icon are the only visible status signals; say it
|
package/src/base-preset.ts
CHANGED
|
@@ -125,6 +125,15 @@ export const basePreset = definePreset({
|
|
|
125
125
|
background: "var(--skeleton-end-color)",
|
|
126
126
|
},
|
|
127
127
|
},
|
|
128
|
+
// Toast enter/exit, played on the view-transition snapshots (see the
|
|
129
|
+
// ::view-transition rules in globalCss). Chakra-ballpark timings: fade
|
|
130
|
+
// + short slide in, quicker fade + shrink out.
|
|
131
|
+
toastSlideIn: {
|
|
132
|
+
from: { opacity: 0, transform: "translateY(-24px)" },
|
|
133
|
+
},
|
|
134
|
+
toastSlideOut: {
|
|
135
|
+
to: { opacity: 0, transform: "scale(0.85)" },
|
|
136
|
+
},
|
|
128
137
|
},
|
|
129
138
|
tokens: {
|
|
130
139
|
colors: {
|
|
@@ -314,6 +323,40 @@ export const basePreset = definePreset({
|
|
|
314
323
|
"h1, h2, h3, h4, h5, h6": {
|
|
315
324
|
textWrap: "wrap",
|
|
316
325
|
},
|
|
326
|
+
// While a full-size dialog is open (the Modal stamps data-fullsize on
|
|
327
|
+
// its overlay), release the scrollbar gutter that react-aria's scroll
|
|
328
|
+
// lock reserves on the root. The reserved strip is scrollbar chrome to
|
|
329
|
+
// hit-testing — elementFromPoint returns null there, so clicks fall
|
|
330
|
+
// through to the root and dismiss the dialog, and controls near the
|
|
331
|
+
// right edge lose part of their target. With the page fully covered,
|
|
332
|
+
// the reflow this causes is invisible. !important: react-aria sets the
|
|
333
|
+
// reservation as a non-important inline style.
|
|
334
|
+
"html:has([data-fullsize])": {
|
|
335
|
+
scrollbarGutter: "auto !important",
|
|
336
|
+
},
|
|
337
|
+
// Toast enter/exit (the ToastQueue wraps updates in
|
|
338
|
+
// document.startViewTransition — see Toast.tsx, which also stamps the
|
|
339
|
+
// scoping class on <html> while its transitions run). Timings sit in
|
|
340
|
+
// Chakra's ballpark: 0.4s fade+slide in, 0.2s fade+shrink out; the
|
|
341
|
+
// stack reflow comes from the default group animation. `(*)` +
|
|
342
|
+
// `:only-child` matches exactly the entering/exiting toast groups: the
|
|
343
|
+
// root snapshot always has both old and new children, and toasts are
|
|
344
|
+
// the only named groups during a toast transition. The snapshot
|
|
345
|
+
// overlay must not eat clicks while a toast animates, hence
|
|
346
|
+
// pointer-events, scoped likewise.
|
|
347
|
+
"html.microbit-ui-toast-transition::view-transition": {
|
|
348
|
+
pointerEvents: "none",
|
|
349
|
+
},
|
|
350
|
+
// `both` fill: the snapshots must hold the keyframes' end states for
|
|
351
|
+
// however long the rest of the transition (e.g. the 0.25s default group
|
|
352
|
+
// animation) outlives them, or they snap back to full size/opacity for
|
|
353
|
+
// the remainder.
|
|
354
|
+
"html.microbit-ui-toast-transition::view-transition-new(*):only-child": {
|
|
355
|
+
animation: "toastSlideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) both",
|
|
356
|
+
},
|
|
357
|
+
"html.microbit-ui-toast-transition::view-transition-old(*):only-child": {
|
|
358
|
+
animation: "toastSlideOut 0.2s cubic-bezier(0.4, 0, 1, 1) both",
|
|
359
|
+
},
|
|
317
360
|
},
|
|
318
361
|
// shared-ui components forward `variant`/`size`/etc. as runtime props to
|
|
319
362
|
// the recipe functions, so Panda's static analysis can't see which variants
|