@evinvest/uikit 0.24.0 → 0.26.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/README.md +147 -4
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/alert-dialog.js.map +1 -1
- package/dist/components/app-shell.js +85 -0
- package/dist/components/app-shell.js.map +1 -0
- package/dist/components/bottom-tab-bar.js +91 -0
- package/dist/components/bottom-tab-bar.js.map +1 -0
- package/dist/components/dialog.js +2 -1
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/drawer.js +6 -4
- package/dist/components/drawer.js.map +1 -1
- package/dist/components/list-rows.js +73 -0
- package/dist/components/list-rows.js.map +1 -0
- package/dist/components/mobile-app-bar.js +51 -0
- package/dist/components/mobile-app-bar.js.map +1 -0
- package/dist/components/nav-badge.js +61 -0
- package/dist/components/nav-badge.js.map +1 -0
- package/dist/components/nav-link.js +50 -0
- package/dist/components/nav-link.js.map +1 -0
- package/dist/components/nav-model.js +55 -0
- package/dist/components/nav-model.js.map +1 -0
- package/dist/components/page-frame.js +92 -0
- package/dist/components/page-frame.js.map +1 -0
- package/dist/components/resource-error.js +47 -0
- package/dist/components/resource-error.js.map +1 -0
- package/dist/components/section-nav.js +86 -0
- package/dist/components/section-nav.js.map +1 -0
- package/dist/components/settled.js +19 -0
- package/dist/components/settled.js.map +1 -0
- package/dist/components/sheet.js +8 -2
- package/dist/components/sheet.js.map +1 -1
- package/dist/components/shell-icons.js +57 -0
- package/dist/components/shell-icons.js.map +1 -0
- package/dist/components/shell-nav.js +141 -0
- package/dist/components/shell-nav.js.map +1 -0
- package/dist/components/system-banner.js +53 -0
- package/dist/components/system-banner.js.map +1 -0
- package/dist/components/use-shell-nav.js +46 -0
- package/dist/components/use-shell-nav.js.map +1 -0
- package/dist/generated/alert-dialog.js +2 -2
- package/dist/generated/alert-dialog.js.map +1 -1
- package/dist/generated/dialog.js +3 -3
- package/dist/generated/dialog.js.map +1 -1
- package/dist/generated/drawer.js +1 -1
- package/dist/generated/drawer.js.map +1 -1
- package/dist/generated/sheet.js +3 -3
- package/dist/generated/sheet.js.map +1 -1
- package/dist/index.d.ts +451 -6
- package/dist/index.js +12 -0
- package/dist/primitives/focus-scope.js +22 -6
- package/dist/primitives/focus-scope.js.map +1 -1
- package/package.json +1 -1
- package/styles/theme.css +180 -1
- package/styles/tokens.css +182 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/primitives/focus-scope.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n// `tabindex=\"-1\"` opts an element out of Tab order whatever its tag, so the\n// exclusion goes on every branch — otherwise a roving-tabindex grid's parked\n// cells would count as tab stops.\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]\",\n]\n .map(sel => `${sel}:not([tabindex=\"-1\"])`)\n .join(\",\");\n\n/**\n * Every enabled, Tab-reachable candidate under `root` in DOM (= Tab) order,\n * before the layout-based visibility filter; `tabindex=\"-1\"` is skipped on any\n * tag. For content that is fully visible while it exists (a popover), this is\n * the tab order itself. Internal to the kit.\n */\nexport function focusCandidates(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE));\n}\n\nfunction focusable(root: HTMLElement): HTMLElement[] {\n return focusCandidates(root).filter(\n (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement,\n );\n}\n\n/**\n * Traps Tab focus inside the returned ref's subtree and restores focus to the\n * previously-focused element on unmount — the dep-light core of dialogs/menus,\n * replacing `@radix-ui/react-focus-scope`. On mount, focuses
|
|
1
|
+
{"version":3,"sources":["../../src/primitives/focus-scope.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n// `tabindex=\"-1\"` opts an element out of Tab order whatever its tag, so the\n// exclusion goes on every branch — otherwise a roving-tabindex grid's parked\n// cells would count as tab stops.\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"textarea:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"[tabindex]\",\n]\n .map(sel => `${sel}:not([tabindex=\"-1\"])`)\n .join(\",\");\n\n/**\n * Every enabled, Tab-reachable candidate under `root` in DOM (= Tab) order,\n * before the layout-based visibility filter; `tabindex=\"-1\"` is skipped on any\n * tag. For content that is fully visible while it exists (a popover), this is\n * the tab order itself. Internal to the kit.\n */\nexport function focusCandidates(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE));\n}\n\nfunction focusable(root: HTMLElement): HTMLElement[] {\n return focusCandidates(root).filter(\n (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement,\n );\n}\n\n/**\n * Where focus lands when the scope opens. `first` is the first Tab stop inside\n * (an input, an alert dialog's least destructive action); `container` is the\n * scope root itself, so a reader starts at the top and the first Tab reaches the\n * first control — right for a panel of content, where the first stop may be an\n * action one keypress away from firing.\n */\nexport type InitialFocus = \"first\" | \"container\";\n\nexport interface FocusScopeOptions {\n /** Default `first`; falls back to the container when nothing inside is focusable. */\n initialFocus?: InitialFocus;\n /**\n * Where focus goes back on close when the element focused before opening is\n * no use: `<body>` (Safari does not focus a clicked button), or detached (the\n * opener was re-rendered away). Pass the overlay's trigger.\n */\n returnFocusTo?: React.RefObject<HTMLElement | null>;\n}\n\nfunction usable(el: HTMLElement | null | undefined): el is HTMLElement {\n return !!el && el !== document.body && el.isConnected;\n}\n\n// A `<div>` without a tabindex ignores `.focus()`, so a scope with no Tab stops\n// used to leave focus on the trigger behind the scrim. `-1` makes the root\n// focusable by script (and by a click on its empty area, which keeps focus in\n// the scope) without adding it to the Tab order.\nfunction focusRoot(root: HTMLElement) {\n if (!root.hasAttribute(\"tabindex\")) root.tabIndex = -1;\n root.focus();\n}\n\n/**\n * Traps Tab focus inside the returned ref's subtree and restores focus to the\n * previously-focused element on unmount — the dep-light core of dialogs/menus,\n * replacing `@radix-ui/react-focus-scope`. On mount, focuses per\n * `initialFocus`; the root gets `tabindex=\"-1\"` whenever it is the target.\n *\n * Rust dialogs rely on the browser's native focus order within a fixed overlay;\n * see the README \"Limitations\".\n */\nexport function useFocusScope(\n enabled: boolean,\n { initialFocus = \"first\", returnFocusTo }: FocusScopeOptions = {},\n): React.RefObject<HTMLDivElement | null> {\n const ref = React.useRef<HTMLDivElement | null>(null);\n // Read at close, not captured at open: a trigger re-rendered while the\n // overlay was up is a new node by then.\n const fallbackRef = React.useRef(returnFocusTo);\n fallbackRef.current = returnFocusTo;\n\n React.useEffect(() => {\n if (!enabled) return;\n const root = ref.current;\n if (!root) return;\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n const first = initialFocus === \"first\" ? focusable(root)[0] : undefined;\n if (first) first.focus();\n else focusRoot(root);\n\n function onKeyDown(event: KeyboardEvent) {\n if (event.key !== \"Tab\" || !root) return;\n const items = focusable(root);\n if (items.length === 0) {\n event.preventDefault();\n return;\n }\n const firstEl = items[0]!;\n const lastEl = items[items.length - 1]!;\n const active = document.activeElement;\n // From the root itself Shift+Tab would leave the scope for whatever\n // precedes the portal, so both directions are steered explicitly.\n if (active === root) {\n event.preventDefault();\n (event.shiftKey ? lastEl : firstEl).focus();\n } else if (event.shiftKey && active === firstEl) {\n event.preventDefault();\n lastEl.focus();\n } else if (!event.shiftKey && active === lastEl) {\n event.preventDefault();\n firstEl.focus();\n }\n }\n\n root.addEventListener(\"keydown\", onKeyDown);\n return () => {\n root.removeEventListener(\"keydown\", onKeyDown);\n // Focus that already left the scope (a click outside landed on a field)\n // is the user's; only focus still inside, or dropped to <body>, returns.\n const active = document.activeElement;\n if (active && active !== document.body && !root.contains(active)) return;\n const target = usable(previouslyFocused) ? previouslyFocused : fallbackRef.current?.current;\n if (usable(target)) target.focus({ preventScroll: true });\n };\n }, [enabled, initialFocus]);\n\n return ref;\n}\n"],"mappings":";AAEA,YAAY,WAAW;AAKvB,MAAM,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EACG,IAAI,SAAO,GAAG,GAAG,uBAAuB,EACxC,KAAK,GAAG;AAQJ,SAAS,gBAAgB,MAAkC;AAChE,SAAO,MAAM,KAAK,KAAK,iBAA8B,SAAS,CAAC;AACjE;AAEA,SAAS,UAAU,MAAkC;AACnD,SAAO,gBAAgB,IAAI,EAAE;AAAA,IAC3B,CAAC,OAAO,GAAG,cAAc,KAAK,GAAG,eAAe,KAAK,OAAO,SAAS;AAAA,EACvE;AACF;AAsBA,SAAS,OAAO,IAAuD;AACrE,SAAO,CAAC,CAAC,MAAM,OAAO,SAAS,QAAQ,GAAG;AAC5C;AAMA,SAAS,UAAU,MAAmB;AACpC,MAAI,CAAC,KAAK,aAAa,UAAU,EAAG,MAAK,WAAW;AACpD,OAAK,MAAM;AACb;AAWO,SAAS,cACd,SACA,EAAE,eAAe,SAAS,cAAc,IAAuB,CAAC,GACxB;AACxC,QAAM,MAAM,MAAM,OAA8B,IAAI;AAGpD,QAAM,cAAc,MAAM,OAAO,aAAa;AAC9C,cAAY,UAAU;AAEtB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,QAAS;AACd,UAAM,OAAO,IAAI;AACjB,QAAI,CAAC,KAAM;AAEX,UAAM,oBAAoB,SAAS;AACnC,UAAM,QAAQ,iBAAiB,UAAU,UAAU,IAAI,EAAE,CAAC,IAAI;AAC9D,QAAI,MAAO,OAAM,MAAM;AAAA,QAClB,WAAU,IAAI;AAEnB,aAAS,UAAU,OAAsB;AACvC,UAAI,MAAM,QAAQ,SAAS,CAAC,KAAM;AAClC,YAAM,QAAQ,UAAU,IAAI;AAC5B,UAAI,MAAM,WAAW,GAAG;AACtB,cAAM,eAAe;AACrB;AAAA,MACF;AACA,YAAM,UAAU,MAAM,CAAC;AACvB,YAAM,SAAS,MAAM,MAAM,SAAS,CAAC;AACrC,YAAM,SAAS,SAAS;AAGxB,UAAI,WAAW,MAAM;AACnB,cAAM,eAAe;AACrB,SAAC,MAAM,WAAW,SAAS,SAAS,MAAM;AAAA,MAC5C,WAAW,MAAM,YAAY,WAAW,SAAS;AAC/C,cAAM,eAAe;AACrB,eAAO,MAAM;AAAA,MACf,WAAW,CAAC,MAAM,YAAY,WAAW,QAAQ;AAC/C,cAAM,eAAe;AACrB,gBAAQ,MAAM;AAAA,MAChB;AAAA,IACF;AAEA,SAAK,iBAAiB,WAAW,SAAS;AAC1C,WAAO,MAAM;AACX,WAAK,oBAAoB,WAAW,SAAS;AAG7C,YAAM,SAAS,SAAS;AACxB,UAAI,UAAU,WAAW,SAAS,QAAQ,CAAC,KAAK,SAAS,MAAM,EAAG;AAClE,YAAM,SAAS,OAAO,iBAAiB,IAAI,oBAAoB,YAAY,SAAS;AACpF,UAAI,OAAO,MAAM,EAAG,QAAO,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,IAC1D;AAAA,EACF,GAAG,CAAC,SAAS,YAAY,CAAC;AAE1B,SAAO;AACT;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evinvest/uikit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Dep-light React UI kit: shadcn-semantics components with zero @radix-ui / cva, mirroring the ev_lib::uikit Rust feature. Ships the shared design tokens.",
|
|
5
5
|
"license": "BlueOak-1.0.0",
|
|
6
6
|
"publishConfig": {
|
package/styles/theme.css
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* AUTO-GENERATED by `nix run .#gen` from the repo-root theme.css — do not edit. */
|
|
2
2
|
/*
|
|
3
|
-
* Toast (sonner) and Drawer enter/exit motion,
|
|
3
|
+
* Toast (sonner) and Drawer enter/exit motion, the reduced-motion fade for Sheet and
|
|
4
|
+
* the dialogs, the entrances (rise / stagger / settled / skeleton) and the app-shell
|
|
5
|
+
* markers, shared by both ports. Imported by the token
|
|
4
6
|
* sheets so the lifecycle ships with the theme contract.
|
|
5
7
|
*/
|
|
6
8
|
|
|
@@ -306,6 +308,163 @@
|
|
|
306
308
|
}
|
|
307
309
|
}
|
|
308
310
|
|
|
311
|
+
/* ── Sheet and dialogs under reduced motion ──────────────────────────────────
|
|
312
|
+
Both ports animate these with tw-animate-css (`animate-in` / `animate-out` +
|
|
313
|
+
`slide-*` / `zoom-*`), whose `enter` / `exit` keyframes read the
|
|
314
|
+
--tw-enter-* / --tw-exit-* variables the slide and zoom utilities set. Zeroing
|
|
315
|
+
the travel and the scale here leaves a fade: the dialogs already fade, the
|
|
316
|
+
sheet does not, so its opacity is set too. The keyframes still run on their
|
|
317
|
+
usual clock, so the TS port's exit `animationend` unmount is untouched.
|
|
318
|
+
Unlayered, like the rest of this sheet, so it outranks those utilities
|
|
319
|
+
without !important. */
|
|
320
|
+
@media (prefers-reduced-motion: reduce) {
|
|
321
|
+
[data-slot="sheet-content"],
|
|
322
|
+
[data-slot="dialog-content"],
|
|
323
|
+
[data-slot="alert-dialog-content"] {
|
|
324
|
+
--tw-enter-translate-x: 0;
|
|
325
|
+
--tw-enter-translate-y: 0;
|
|
326
|
+
--tw-exit-translate-x: 0;
|
|
327
|
+
--tw-exit-translate-y: 0;
|
|
328
|
+
--tw-enter-scale: 1;
|
|
329
|
+
--tw-exit-scale: 1;
|
|
330
|
+
--tw-enter-opacity: 0;
|
|
331
|
+
--tw-exit-opacity: 0;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ── Entrances: rise, stagger, settled, skeleton ─────────────────────────────
|
|
336
|
+
One keyframe for every "arriving" thing, driven by attributes so a consumer
|
|
337
|
+
without a motion library (and a Server Component) gets it for free:
|
|
338
|
+
|
|
339
|
+
data-enter="rise" the element fades in and rises --ev-rise, once, on
|
|
340
|
+
insertion; --enter-delay postpones it.
|
|
341
|
+
data-enter="stagger" each direct child does the same, one --enter-step
|
|
342
|
+
(default --ev-stagger) after the previous. The index
|
|
343
|
+
comes from nth-child; set --enter-index inline to
|
|
344
|
+
override it. Capped at 12 — past that the tail is
|
|
345
|
+
still arriving after the head has been read.
|
|
346
|
+
[data-slot="settled"][data-state="revealed"]
|
|
347
|
+
a skeleton → content handover (uikit <Settled>).
|
|
348
|
+
|
|
349
|
+
It animates the independent `translate` property, not `transform`, so it
|
|
350
|
+
composes with any transform the element already has. `backwards` holds the
|
|
351
|
+
start frame through the delay and then lets go, so nothing about the element
|
|
352
|
+
(stacking, containing block for `fixed` children) lingers after it lands.
|
|
353
|
+
Reduced motion keeps the fade and drops the travel. */
|
|
354
|
+
@keyframes ev-enter-rise {
|
|
355
|
+
from {
|
|
356
|
+
opacity: 0;
|
|
357
|
+
translate: 0 var(--ev-rise, 8px);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
[data-enter="rise"],
|
|
361
|
+
[data-enter="stagger"] > *,
|
|
362
|
+
[data-slot="settled"][data-state="revealed"] {
|
|
363
|
+
animation: ev-enter-rise var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out)
|
|
364
|
+
var(--enter-delay, 0ms) backwards;
|
|
365
|
+
}
|
|
366
|
+
[data-enter="stagger"] > * {
|
|
367
|
+
animation-delay: calc(
|
|
368
|
+
var(--enter-delay, 0ms) + var(--enter-index, 0) * var(--enter-step, var(--ev-stagger, 35ms))
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
[data-enter="stagger"] > :nth-child(2) { --enter-index: 1; }
|
|
372
|
+
[data-enter="stagger"] > :nth-child(3) { --enter-index: 2; }
|
|
373
|
+
[data-enter="stagger"] > :nth-child(4) { --enter-index: 3; }
|
|
374
|
+
[data-enter="stagger"] > :nth-child(5) { --enter-index: 4; }
|
|
375
|
+
[data-enter="stagger"] > :nth-child(6) { --enter-index: 5; }
|
|
376
|
+
[data-enter="stagger"] > :nth-child(7) { --enter-index: 6; }
|
|
377
|
+
[data-enter="stagger"] > :nth-child(8) { --enter-index: 7; }
|
|
378
|
+
[data-enter="stagger"] > :nth-child(9) { --enter-index: 8; }
|
|
379
|
+
[data-enter="stagger"] > :nth-child(10) { --enter-index: 9; }
|
|
380
|
+
[data-enter="stagger"] > :nth-child(11) { --enter-index: 10; }
|
|
381
|
+
[data-enter="stagger"] > :nth-child(n + 12) { --enter-index: 11; }
|
|
382
|
+
|
|
383
|
+
/* Every skeleton fades in before it starts to pulse. Keyed on the pulse class as
|
|
384
|
+
well as the slot: a running animation beats a transition on the same property,
|
|
385
|
+
so the entrance has to be the first of two animations rather than anything
|
|
386
|
+
layered beside `animate-pulse` — and a caller's `animate-none` (which `cn`
|
|
387
|
+
merges over the pulse) removes the class and with it this rule. Reduced motion
|
|
388
|
+
keeps the pulse, the signal that work is in flight, and drops the entrance. */
|
|
389
|
+
@keyframes ev-skeleton-in {
|
|
390
|
+
from {
|
|
391
|
+
opacity: 0;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
@keyframes ev-skeleton-pulse {
|
|
395
|
+
50% {
|
|
396
|
+
opacity: 0.5;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
[data-slot="skeleton"].animate-pulse {
|
|
400
|
+
animation:
|
|
401
|
+
ev-skeleton-in 200ms var(--ev-ease-out, ease-out),
|
|
402
|
+
ev-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) 200ms infinite;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* ── App shell: the rail's active-row marker and the tab bar's rule ──────────
|
|
406
|
+
ShellNav: one marker node per nav section, placed on the [aria-current] row by
|
|
407
|
+
measurement and moved by a `transform` transition — on the compositor, so it
|
|
408
|
+
keeps its frames while the main thread renders the page just navigated to (a
|
|
409
|
+
JS layout projection here dropped exactly those). Until the first placement
|
|
410
|
+
(SSR, every frame to hydration) the marker has no position: it stays hidden
|
|
411
|
+
and the marked row carries the fill itself, so the first paint is right.
|
|
412
|
+
`data-placed` flips both at once; `entered` is a marker that mounted into a
|
|
413
|
+
section already on screen (a move across sections — a fade), `first` one that
|
|
414
|
+
arrived with its section (a cut: fading under a row whose own fill fades out
|
|
415
|
+
would dip). */
|
|
416
|
+
@keyframes ev-shell-fade-in {
|
|
417
|
+
from {
|
|
418
|
+
opacity: 0;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
[data-slot="shell-nav-marker"] {
|
|
422
|
+
transition: transform var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
|
|
423
|
+
}
|
|
424
|
+
[data-slot="shell-nav-marker"]:not([data-placed]) {
|
|
425
|
+
visibility: hidden;
|
|
426
|
+
}
|
|
427
|
+
[data-slot="shell-nav-marker"][data-placed="entered"] {
|
|
428
|
+
animation: ev-shell-fade-in var(--ev-dur-fast, 180ms) var(--ev-ease-out, ease-out);
|
|
429
|
+
}
|
|
430
|
+
[data-slot="shell-nav-marker"]:not([data-placed]) ~ * [aria-current="page"] {
|
|
431
|
+
background-color: var(--primary);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* BottomTabBar: one rule for the whole bar, mounted once and translated by
|
|
435
|
+
whole tab widths (it is exactly one tab wide, so the two cannot drift). A
|
|
436
|
+
route no tab claims fades it out where it stands, and coming back fades it in
|
|
437
|
+
already in place. */
|
|
438
|
+
[data-slot="tab-bar-marker"] {
|
|
439
|
+
transition:
|
|
440
|
+
translate var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out),
|
|
441
|
+
opacity var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
|
|
442
|
+
}
|
|
443
|
+
[data-slot="tab-bar-marker"][data-state="idle"] {
|
|
444
|
+
opacity: 0;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
@keyframes ev-enter-fade {
|
|
448
|
+
from {
|
|
449
|
+
opacity: 0;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
@media (prefers-reduced-motion: reduce) {
|
|
453
|
+
[data-enter="rise"],
|
|
454
|
+
[data-enter="stagger"] > *,
|
|
455
|
+
[data-slot="settled"][data-state="revealed"] {
|
|
456
|
+
animation-name: ev-enter-fade;
|
|
457
|
+
}
|
|
458
|
+
[data-slot="skeleton"].animate-pulse {
|
|
459
|
+
animation: ev-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
460
|
+
}
|
|
461
|
+
[data-slot="shell-nav-marker"],
|
|
462
|
+
[data-slot="tab-bar-marker"] {
|
|
463
|
+
transition: none;
|
|
464
|
+
animation: none;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
309
468
|
/* ═══ CONTRACT ═══════════════════════════════════════════════════ */
|
|
310
469
|
|
|
311
470
|
@theme inline {
|
|
@@ -428,6 +587,26 @@
|
|
|
428
587
|
|
|
429
588
|
--sidebar-width: 14rem;
|
|
430
589
|
|
|
590
|
+
/* app shell (uikit <AppShell> / <ShellNav> / <BottomTabBar>) */
|
|
591
|
+
--shell-rail-w: 15.5rem; /* 248px */
|
|
592
|
+
--shell-tab-bar-h: 4rem; /* 64px, before the bottom safe area */
|
|
593
|
+
|
|
594
|
+
/* ── motion ──────────────────────────────────────────────────── *
|
|
595
|
+
* One place to retune the feel of every animation the kit ships (motion.css
|
|
596
|
+
* reads these) and every one a consumer writes against them:
|
|
597
|
+
* `ease-(--ev-ease-out) duration-(--ev-dur-base)`. Quiet on purpose — short,
|
|
598
|
+
* small travel: motion here says what changed, it never decorates. */
|
|
599
|
+
--ev-ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* every entrance */
|
|
600
|
+
--ev-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* state that reverses */
|
|
601
|
+
--ev-dur-fast: 180ms;
|
|
602
|
+
--ev-dur-base: 280ms;
|
|
603
|
+
--ev-dur-slow: 400ms;
|
|
604
|
+
--ev-rise: 8px; /* how far an entrance travels */
|
|
605
|
+
--ev-stagger: 35ms; /* between rows of a list */
|
|
606
|
+
/* between the sections of a page: cards, not rows — at 35ms six of them read
|
|
607
|
+
as one block twitching; at 50ms the last lands ~0.55s in, still "opening" */
|
|
608
|
+
--ev-stagger-section: 50ms;
|
|
609
|
+
|
|
431
610
|
/* page container — responsive gutter + max width (uikit <Container>) */
|
|
432
611
|
--page-max: 90rem; /* 1440px */
|
|
433
612
|
--page-px: 1rem; /* 16px — mobile gutter, scales up below */
|
package/styles/tokens.css
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* accent-trace · accent-debug · accent-info · accent-warn · accent-error
|
|
22
22
|
* primary-ink — the primary role as ink on a surface, where the
|
|
23
23
|
* fill and the readable value are different colours
|
|
24
|
-
* SCALARS radius · control-* · display-scale · page-* · shadows · fonts
|
|
24
|
+
* SCALARS radius · control-* · display-scale · page-* · shell-* · shadows · fonts
|
|
25
|
+
* MOTION ev-ease-* · ev-dur-* · ev-rise · ev-stagger(-section)
|
|
25
26
|
* CHARTS chart-1 … chart-5
|
|
26
27
|
* MARK brand-mark · brand-aspect — supplied, never defined here
|
|
27
28
|
*
|
|
@@ -40,7 +41,9 @@
|
|
|
40
41
|
*/
|
|
41
42
|
|
|
42
43
|
/*
|
|
43
|
-
* Toast (sonner) and Drawer enter/exit motion,
|
|
44
|
+
* Toast (sonner) and Drawer enter/exit motion, the reduced-motion fade for Sheet and
|
|
45
|
+
* the dialogs, the entrances (rise / stagger / settled / skeleton) and the app-shell
|
|
46
|
+
* markers, shared by both ports. Imported by the token
|
|
44
47
|
* sheets so the lifecycle ships with the theme contract.
|
|
45
48
|
*/
|
|
46
49
|
|
|
@@ -346,6 +349,163 @@
|
|
|
346
349
|
}
|
|
347
350
|
}
|
|
348
351
|
|
|
352
|
+
/* ── Sheet and dialogs under reduced motion ──────────────────────────────────
|
|
353
|
+
Both ports animate these with tw-animate-css (`animate-in` / `animate-out` +
|
|
354
|
+
`slide-*` / `zoom-*`), whose `enter` / `exit` keyframes read the
|
|
355
|
+
--tw-enter-* / --tw-exit-* variables the slide and zoom utilities set. Zeroing
|
|
356
|
+
the travel and the scale here leaves a fade: the dialogs already fade, the
|
|
357
|
+
sheet does not, so its opacity is set too. The keyframes still run on their
|
|
358
|
+
usual clock, so the TS port's exit `animationend` unmount is untouched.
|
|
359
|
+
Unlayered, like the rest of this sheet, so it outranks those utilities
|
|
360
|
+
without !important. */
|
|
361
|
+
@media (prefers-reduced-motion: reduce) {
|
|
362
|
+
[data-slot="sheet-content"],
|
|
363
|
+
[data-slot="dialog-content"],
|
|
364
|
+
[data-slot="alert-dialog-content"] {
|
|
365
|
+
--tw-enter-translate-x: 0;
|
|
366
|
+
--tw-enter-translate-y: 0;
|
|
367
|
+
--tw-exit-translate-x: 0;
|
|
368
|
+
--tw-exit-translate-y: 0;
|
|
369
|
+
--tw-enter-scale: 1;
|
|
370
|
+
--tw-exit-scale: 1;
|
|
371
|
+
--tw-enter-opacity: 0;
|
|
372
|
+
--tw-exit-opacity: 0;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* ── Entrances: rise, stagger, settled, skeleton ─────────────────────────────
|
|
377
|
+
One keyframe for every "arriving" thing, driven by attributes so a consumer
|
|
378
|
+
without a motion library (and a Server Component) gets it for free:
|
|
379
|
+
|
|
380
|
+
data-enter="rise" the element fades in and rises --ev-rise, once, on
|
|
381
|
+
insertion; --enter-delay postpones it.
|
|
382
|
+
data-enter="stagger" each direct child does the same, one --enter-step
|
|
383
|
+
(default --ev-stagger) after the previous. The index
|
|
384
|
+
comes from nth-child; set --enter-index inline to
|
|
385
|
+
override it. Capped at 12 — past that the tail is
|
|
386
|
+
still arriving after the head has been read.
|
|
387
|
+
[data-slot="settled"][data-state="revealed"]
|
|
388
|
+
a skeleton → content handover (uikit <Settled>).
|
|
389
|
+
|
|
390
|
+
It animates the independent `translate` property, not `transform`, so it
|
|
391
|
+
composes with any transform the element already has. `backwards` holds the
|
|
392
|
+
start frame through the delay and then lets go, so nothing about the element
|
|
393
|
+
(stacking, containing block for `fixed` children) lingers after it lands.
|
|
394
|
+
Reduced motion keeps the fade and drops the travel. */
|
|
395
|
+
@keyframes ev-enter-rise {
|
|
396
|
+
from {
|
|
397
|
+
opacity: 0;
|
|
398
|
+
translate: 0 var(--ev-rise, 8px);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
[data-enter="rise"],
|
|
402
|
+
[data-enter="stagger"] > *,
|
|
403
|
+
[data-slot="settled"][data-state="revealed"] {
|
|
404
|
+
animation: ev-enter-rise var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out)
|
|
405
|
+
var(--enter-delay, 0ms) backwards;
|
|
406
|
+
}
|
|
407
|
+
[data-enter="stagger"] > * {
|
|
408
|
+
animation-delay: calc(
|
|
409
|
+
var(--enter-delay, 0ms) + var(--enter-index, 0) * var(--enter-step, var(--ev-stagger, 35ms))
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
[data-enter="stagger"] > :nth-child(2) { --enter-index: 1; }
|
|
413
|
+
[data-enter="stagger"] > :nth-child(3) { --enter-index: 2; }
|
|
414
|
+
[data-enter="stagger"] > :nth-child(4) { --enter-index: 3; }
|
|
415
|
+
[data-enter="stagger"] > :nth-child(5) { --enter-index: 4; }
|
|
416
|
+
[data-enter="stagger"] > :nth-child(6) { --enter-index: 5; }
|
|
417
|
+
[data-enter="stagger"] > :nth-child(7) { --enter-index: 6; }
|
|
418
|
+
[data-enter="stagger"] > :nth-child(8) { --enter-index: 7; }
|
|
419
|
+
[data-enter="stagger"] > :nth-child(9) { --enter-index: 8; }
|
|
420
|
+
[data-enter="stagger"] > :nth-child(10) { --enter-index: 9; }
|
|
421
|
+
[data-enter="stagger"] > :nth-child(11) { --enter-index: 10; }
|
|
422
|
+
[data-enter="stagger"] > :nth-child(n + 12) { --enter-index: 11; }
|
|
423
|
+
|
|
424
|
+
/* Every skeleton fades in before it starts to pulse. Keyed on the pulse class as
|
|
425
|
+
well as the slot: a running animation beats a transition on the same property,
|
|
426
|
+
so the entrance has to be the first of two animations rather than anything
|
|
427
|
+
layered beside `animate-pulse` — and a caller's `animate-none` (which `cn`
|
|
428
|
+
merges over the pulse) removes the class and with it this rule. Reduced motion
|
|
429
|
+
keeps the pulse, the signal that work is in flight, and drops the entrance. */
|
|
430
|
+
@keyframes ev-skeleton-in {
|
|
431
|
+
from {
|
|
432
|
+
opacity: 0;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
@keyframes ev-skeleton-pulse {
|
|
436
|
+
50% {
|
|
437
|
+
opacity: 0.5;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
[data-slot="skeleton"].animate-pulse {
|
|
441
|
+
animation:
|
|
442
|
+
ev-skeleton-in 200ms var(--ev-ease-out, ease-out),
|
|
443
|
+
ev-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) 200ms infinite;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/* ── App shell: the rail's active-row marker and the tab bar's rule ──────────
|
|
447
|
+
ShellNav: one marker node per nav section, placed on the [aria-current] row by
|
|
448
|
+
measurement and moved by a `transform` transition — on the compositor, so it
|
|
449
|
+
keeps its frames while the main thread renders the page just navigated to (a
|
|
450
|
+
JS layout projection here dropped exactly those). Until the first placement
|
|
451
|
+
(SSR, every frame to hydration) the marker has no position: it stays hidden
|
|
452
|
+
and the marked row carries the fill itself, so the first paint is right.
|
|
453
|
+
`data-placed` flips both at once; `entered` is a marker that mounted into a
|
|
454
|
+
section already on screen (a move across sections — a fade), `first` one that
|
|
455
|
+
arrived with its section (a cut: fading under a row whose own fill fades out
|
|
456
|
+
would dip). */
|
|
457
|
+
@keyframes ev-shell-fade-in {
|
|
458
|
+
from {
|
|
459
|
+
opacity: 0;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
[data-slot="shell-nav-marker"] {
|
|
463
|
+
transition: transform var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
|
|
464
|
+
}
|
|
465
|
+
[data-slot="shell-nav-marker"]:not([data-placed]) {
|
|
466
|
+
visibility: hidden;
|
|
467
|
+
}
|
|
468
|
+
[data-slot="shell-nav-marker"][data-placed="entered"] {
|
|
469
|
+
animation: ev-shell-fade-in var(--ev-dur-fast, 180ms) var(--ev-ease-out, ease-out);
|
|
470
|
+
}
|
|
471
|
+
[data-slot="shell-nav-marker"]:not([data-placed]) ~ * [aria-current="page"] {
|
|
472
|
+
background-color: var(--primary);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* BottomTabBar: one rule for the whole bar, mounted once and translated by
|
|
476
|
+
whole tab widths (it is exactly one tab wide, so the two cannot drift). A
|
|
477
|
+
route no tab claims fades it out where it stands, and coming back fades it in
|
|
478
|
+
already in place. */
|
|
479
|
+
[data-slot="tab-bar-marker"] {
|
|
480
|
+
transition:
|
|
481
|
+
translate var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out),
|
|
482
|
+
opacity var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
|
|
483
|
+
}
|
|
484
|
+
[data-slot="tab-bar-marker"][data-state="idle"] {
|
|
485
|
+
opacity: 0;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
@keyframes ev-enter-fade {
|
|
489
|
+
from {
|
|
490
|
+
opacity: 0;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
@media (prefers-reduced-motion: reduce) {
|
|
494
|
+
[data-enter="rise"],
|
|
495
|
+
[data-enter="stagger"] > *,
|
|
496
|
+
[data-slot="settled"][data-state="revealed"] {
|
|
497
|
+
animation-name: ev-enter-fade;
|
|
498
|
+
}
|
|
499
|
+
[data-slot="skeleton"].animate-pulse {
|
|
500
|
+
animation: ev-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
501
|
+
}
|
|
502
|
+
[data-slot="shell-nav-marker"],
|
|
503
|
+
[data-slot="tab-bar-marker"] {
|
|
504
|
+
transition: none;
|
|
505
|
+
animation: none;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
349
509
|
/* ═══ CONTRACT ═══════════════════════════════════════════════════ */
|
|
350
510
|
|
|
351
511
|
@theme inline {
|
|
@@ -468,6 +628,26 @@
|
|
|
468
628
|
|
|
469
629
|
--sidebar-width: 14rem;
|
|
470
630
|
|
|
631
|
+
/* app shell (uikit <AppShell> / <ShellNav> / <BottomTabBar>) */
|
|
632
|
+
--shell-rail-w: 15.5rem; /* 248px */
|
|
633
|
+
--shell-tab-bar-h: 4rem; /* 64px, before the bottom safe area */
|
|
634
|
+
|
|
635
|
+
/* ── motion ──────────────────────────────────────────────────── *
|
|
636
|
+
* One place to retune the feel of every animation the kit ships (motion.css
|
|
637
|
+
* reads these) and every one a consumer writes against them:
|
|
638
|
+
* `ease-(--ev-ease-out) duration-(--ev-dur-base)`. Quiet on purpose — short,
|
|
639
|
+
* small travel: motion here says what changed, it never decorates. */
|
|
640
|
+
--ev-ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* every entrance */
|
|
641
|
+
--ev-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* state that reverses */
|
|
642
|
+
--ev-dur-fast: 180ms;
|
|
643
|
+
--ev-dur-base: 280ms;
|
|
644
|
+
--ev-dur-slow: 400ms;
|
|
645
|
+
--ev-rise: 8px; /* how far an entrance travels */
|
|
646
|
+
--ev-stagger: 35ms; /* between rows of a list */
|
|
647
|
+
/* between the sections of a page: cards, not rows — at 35ms six of them read
|
|
648
|
+
as one block twitching; at 50ms the last lands ~0.55s in, still "opening" */
|
|
649
|
+
--ev-stagger-section: 50ms;
|
|
650
|
+
|
|
471
651
|
/* page container — responsive gutter + max width (uikit <Container>) */
|
|
472
652
|
--page-max: 90rem; /* 1440px */
|
|
473
653
|
--page-px: 1rem; /* 16px — mobile gutter, scales up below */
|