@evinvest/uikit 0.23.0 → 0.25.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.
Files changed (36) hide show
  1. package/README.md +151 -4
  2. package/dist/components/app-shell.js +85 -0
  3. package/dist/components/app-shell.js.map +1 -0
  4. package/dist/components/bottom-tab-bar.js +91 -0
  5. package/dist/components/bottom-tab-bar.js.map +1 -0
  6. package/dist/components/mobile-app-bar.js +51 -0
  7. package/dist/components/mobile-app-bar.js.map +1 -0
  8. package/dist/components/nav-badge.js +61 -0
  9. package/dist/components/nav-badge.js.map +1 -0
  10. package/dist/components/nav-link.js +50 -0
  11. package/dist/components/nav-link.js.map +1 -0
  12. package/dist/components/nav-model.js +55 -0
  13. package/dist/components/nav-model.js.map +1 -0
  14. package/dist/components/page-frame.js +92 -0
  15. package/dist/components/page-frame.js.map +1 -0
  16. package/dist/components/resource-error.js +47 -0
  17. package/dist/components/resource-error.js.map +1 -0
  18. package/dist/components/section-nav.js +86 -0
  19. package/dist/components/section-nav.js.map +1 -0
  20. package/dist/components/settled.js +19 -0
  21. package/dist/components/settled.js.map +1 -0
  22. package/dist/components/shell-icons.js +57 -0
  23. package/dist/components/shell-icons.js.map +1 -0
  24. package/dist/components/shell-nav.js +141 -0
  25. package/dist/components/shell-nav.js.map +1 -0
  26. package/dist/components/system-banner.js +53 -0
  27. package/dist/components/system-banner.js.map +1 -0
  28. package/dist/components/use-shell-nav.js +46 -0
  29. package/dist/components/use-shell-nav.js.map +1 -0
  30. package/dist/generated/button.js +4 -2
  31. package/dist/generated/button.js.map +1 -1
  32. package/dist/index.d.ts +421 -1
  33. package/dist/index.js +12 -0
  34. package/package.json +1 -1
  35. package/styles/theme.css +155 -1
  36. package/styles/tokens.css +157 -2
package/styles/theme.css CHANGED
@@ -1,6 +1,7 @@
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, shared by both ports. Imported by the token
3
+ * Toast (sonner) and Drawer enter/exit motion, the entrances (rise / stagger / settled /
4
+ * skeleton) and the app-shell markers, shared by both ports. Imported by the token
4
5
  * sheets so the lifecycle ships with the theme contract.
5
6
  */
6
7
 
@@ -306,6 +307,139 @@
306
307
  }
307
308
  }
308
309
 
310
+ /* ── Entrances: rise, stagger, settled, skeleton ─────────────────────────────
311
+ One keyframe for every "arriving" thing, driven by attributes so a consumer
312
+ without a motion library (and a Server Component) gets it for free:
313
+
314
+ data-enter="rise" the element fades in and rises --ev-rise, once, on
315
+ insertion; --enter-delay postpones it.
316
+ data-enter="stagger" each direct child does the same, one --enter-step
317
+ (default --ev-stagger) after the previous. The index
318
+ comes from nth-child; set --enter-index inline to
319
+ override it. Capped at 12 — past that the tail is
320
+ still arriving after the head has been read.
321
+ [data-slot="settled"][data-state="revealed"]
322
+ a skeleton → content handover (uikit <Settled>).
323
+
324
+ It animates the independent `translate` property, not `transform`, so it
325
+ composes with any transform the element already has. `backwards` holds the
326
+ start frame through the delay and then lets go, so nothing about the element
327
+ (stacking, containing block for `fixed` children) lingers after it lands.
328
+ Reduced motion keeps the fade and drops the travel. */
329
+ @keyframes ev-enter-rise {
330
+ from {
331
+ opacity: 0;
332
+ translate: 0 var(--ev-rise, 8px);
333
+ }
334
+ }
335
+ [data-enter="rise"],
336
+ [data-enter="stagger"] > *,
337
+ [data-slot="settled"][data-state="revealed"] {
338
+ animation: ev-enter-rise var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out)
339
+ var(--enter-delay, 0ms) backwards;
340
+ }
341
+ [data-enter="stagger"] > * {
342
+ animation-delay: calc(
343
+ var(--enter-delay, 0ms) + var(--enter-index, 0) * var(--enter-step, var(--ev-stagger, 35ms))
344
+ );
345
+ }
346
+ [data-enter="stagger"] > :nth-child(2) { --enter-index: 1; }
347
+ [data-enter="stagger"] > :nth-child(3) { --enter-index: 2; }
348
+ [data-enter="stagger"] > :nth-child(4) { --enter-index: 3; }
349
+ [data-enter="stagger"] > :nth-child(5) { --enter-index: 4; }
350
+ [data-enter="stagger"] > :nth-child(6) { --enter-index: 5; }
351
+ [data-enter="stagger"] > :nth-child(7) { --enter-index: 6; }
352
+ [data-enter="stagger"] > :nth-child(8) { --enter-index: 7; }
353
+ [data-enter="stagger"] > :nth-child(9) { --enter-index: 8; }
354
+ [data-enter="stagger"] > :nth-child(10) { --enter-index: 9; }
355
+ [data-enter="stagger"] > :nth-child(11) { --enter-index: 10; }
356
+ [data-enter="stagger"] > :nth-child(n + 12) { --enter-index: 11; }
357
+
358
+ /* Every skeleton fades in before it starts to pulse. Keyed on the pulse class as
359
+ well as the slot: a running animation beats a transition on the same property,
360
+ so the entrance has to be the first of two animations rather than anything
361
+ layered beside `animate-pulse` — and a caller's `animate-none` (which `cn`
362
+ merges over the pulse) removes the class and with it this rule. Reduced motion
363
+ keeps the pulse, the signal that work is in flight, and drops the entrance. */
364
+ @keyframes ev-skeleton-in {
365
+ from {
366
+ opacity: 0;
367
+ }
368
+ }
369
+ @keyframes ev-skeleton-pulse {
370
+ 50% {
371
+ opacity: 0.5;
372
+ }
373
+ }
374
+ [data-slot="skeleton"].animate-pulse {
375
+ animation:
376
+ ev-skeleton-in 200ms var(--ev-ease-out, ease-out),
377
+ ev-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) 200ms infinite;
378
+ }
379
+
380
+ /* ── App shell: the rail's active-row marker and the tab bar's rule ──────────
381
+ ShellNav: one marker node per nav section, placed on the [aria-current] row by
382
+ measurement and moved by a `transform` transition — on the compositor, so it
383
+ keeps its frames while the main thread renders the page just navigated to (a
384
+ JS layout projection here dropped exactly those). Until the first placement
385
+ (SSR, every frame to hydration) the marker has no position: it stays hidden
386
+ and the marked row carries the fill itself, so the first paint is right.
387
+ `data-placed` flips both at once; `entered` is a marker that mounted into a
388
+ section already on screen (a move across sections — a fade), `first` one that
389
+ arrived with its section (a cut: fading under a row whose own fill fades out
390
+ would dip). */
391
+ @keyframes ev-shell-fade-in {
392
+ from {
393
+ opacity: 0;
394
+ }
395
+ }
396
+ [data-slot="shell-nav-marker"] {
397
+ transition: transform var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
398
+ }
399
+ [data-slot="shell-nav-marker"]:not([data-placed]) {
400
+ visibility: hidden;
401
+ }
402
+ [data-slot="shell-nav-marker"][data-placed="entered"] {
403
+ animation: ev-shell-fade-in var(--ev-dur-fast, 180ms) var(--ev-ease-out, ease-out);
404
+ }
405
+ [data-slot="shell-nav-marker"]:not([data-placed]) ~ * [aria-current="page"] {
406
+ background-color: var(--primary);
407
+ }
408
+
409
+ /* BottomTabBar: one rule for the whole bar, mounted once and translated by
410
+ whole tab widths (it is exactly one tab wide, so the two cannot drift). A
411
+ route no tab claims fades it out where it stands, and coming back fades it in
412
+ already in place. */
413
+ [data-slot="tab-bar-marker"] {
414
+ transition:
415
+ translate var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out),
416
+ opacity var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
417
+ }
418
+ [data-slot="tab-bar-marker"][data-state="idle"] {
419
+ opacity: 0;
420
+ }
421
+
422
+ @keyframes ev-enter-fade {
423
+ from {
424
+ opacity: 0;
425
+ }
426
+ }
427
+ @media (prefers-reduced-motion: reduce) {
428
+ [data-enter="rise"],
429
+ [data-enter="stagger"] > *,
430
+ [data-slot="settled"][data-state="revealed"] {
431
+ animation-name: ev-enter-fade;
432
+ }
433
+ [data-slot="skeleton"].animate-pulse {
434
+ animation: ev-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
435
+ }
436
+ [data-slot="shell-nav-marker"],
437
+ [data-slot="tab-bar-marker"] {
438
+ transition: none;
439
+ animation: none;
440
+ }
441
+ }
442
+
309
443
  /* ═══ CONTRACT ═══════════════════════════════════════════════════ */
310
444
 
311
445
  @theme inline {
@@ -428,6 +562,26 @@
428
562
 
429
563
  --sidebar-width: 14rem;
430
564
 
565
+ /* app shell (uikit <AppShell> / <ShellNav> / <BottomTabBar>) */
566
+ --shell-rail-w: 15.5rem; /* 248px */
567
+ --shell-tab-bar-h: 4rem; /* 64px, before the bottom safe area */
568
+
569
+ /* ── motion ──────────────────────────────────────────────────── *
570
+ * One place to retune the feel of every animation the kit ships (motion.css
571
+ * reads these) and every one a consumer writes against them:
572
+ * `ease-(--ev-ease-out) duration-(--ev-dur-base)`. Quiet on purpose — short,
573
+ * small travel: motion here says what changed, it never decorates. */
574
+ --ev-ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* every entrance */
575
+ --ev-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* state that reverses */
576
+ --ev-dur-fast: 180ms;
577
+ --ev-dur-base: 280ms;
578
+ --ev-dur-slow: 400ms;
579
+ --ev-rise: 8px; /* how far an entrance travels */
580
+ --ev-stagger: 35ms; /* between rows of a list */
581
+ /* between the sections of a page: cards, not rows — at 35ms six of them read
582
+ as one block twitching; at 50ms the last lands ~0.55s in, still "opening" */
583
+ --ev-stagger-section: 50ms;
584
+
431
585
  /* page container — responsive gutter + max width (uikit <Container>) */
432
586
  --page-max: 90rem; /* 1440px */
433
587
  --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,8 @@
40
41
  */
41
42
 
42
43
  /*
43
- * Toast (sonner) and Drawer enter/exit motion, shared by both ports. Imported by the token
44
+ * Toast (sonner) and Drawer enter/exit motion, the entrances (rise / stagger / settled /
45
+ * skeleton) and the app-shell markers, shared by both ports. Imported by the token
44
46
  * sheets so the lifecycle ships with the theme contract.
45
47
  */
46
48
 
@@ -346,6 +348,139 @@
346
348
  }
347
349
  }
348
350
 
351
+ /* ── Entrances: rise, stagger, settled, skeleton ─────────────────────────────
352
+ One keyframe for every "arriving" thing, driven by attributes so a consumer
353
+ without a motion library (and a Server Component) gets it for free:
354
+
355
+ data-enter="rise" the element fades in and rises --ev-rise, once, on
356
+ insertion; --enter-delay postpones it.
357
+ data-enter="stagger" each direct child does the same, one --enter-step
358
+ (default --ev-stagger) after the previous. The index
359
+ comes from nth-child; set --enter-index inline to
360
+ override it. Capped at 12 — past that the tail is
361
+ still arriving after the head has been read.
362
+ [data-slot="settled"][data-state="revealed"]
363
+ a skeleton → content handover (uikit <Settled>).
364
+
365
+ It animates the independent `translate` property, not `transform`, so it
366
+ composes with any transform the element already has. `backwards` holds the
367
+ start frame through the delay and then lets go, so nothing about the element
368
+ (stacking, containing block for `fixed` children) lingers after it lands.
369
+ Reduced motion keeps the fade and drops the travel. */
370
+ @keyframes ev-enter-rise {
371
+ from {
372
+ opacity: 0;
373
+ translate: 0 var(--ev-rise, 8px);
374
+ }
375
+ }
376
+ [data-enter="rise"],
377
+ [data-enter="stagger"] > *,
378
+ [data-slot="settled"][data-state="revealed"] {
379
+ animation: ev-enter-rise var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out)
380
+ var(--enter-delay, 0ms) backwards;
381
+ }
382
+ [data-enter="stagger"] > * {
383
+ animation-delay: calc(
384
+ var(--enter-delay, 0ms) + var(--enter-index, 0) * var(--enter-step, var(--ev-stagger, 35ms))
385
+ );
386
+ }
387
+ [data-enter="stagger"] > :nth-child(2) { --enter-index: 1; }
388
+ [data-enter="stagger"] > :nth-child(3) { --enter-index: 2; }
389
+ [data-enter="stagger"] > :nth-child(4) { --enter-index: 3; }
390
+ [data-enter="stagger"] > :nth-child(5) { --enter-index: 4; }
391
+ [data-enter="stagger"] > :nth-child(6) { --enter-index: 5; }
392
+ [data-enter="stagger"] > :nth-child(7) { --enter-index: 6; }
393
+ [data-enter="stagger"] > :nth-child(8) { --enter-index: 7; }
394
+ [data-enter="stagger"] > :nth-child(9) { --enter-index: 8; }
395
+ [data-enter="stagger"] > :nth-child(10) { --enter-index: 9; }
396
+ [data-enter="stagger"] > :nth-child(11) { --enter-index: 10; }
397
+ [data-enter="stagger"] > :nth-child(n + 12) { --enter-index: 11; }
398
+
399
+ /* Every skeleton fades in before it starts to pulse. Keyed on the pulse class as
400
+ well as the slot: a running animation beats a transition on the same property,
401
+ so the entrance has to be the first of two animations rather than anything
402
+ layered beside `animate-pulse` — and a caller's `animate-none` (which `cn`
403
+ merges over the pulse) removes the class and with it this rule. Reduced motion
404
+ keeps the pulse, the signal that work is in flight, and drops the entrance. */
405
+ @keyframes ev-skeleton-in {
406
+ from {
407
+ opacity: 0;
408
+ }
409
+ }
410
+ @keyframes ev-skeleton-pulse {
411
+ 50% {
412
+ opacity: 0.5;
413
+ }
414
+ }
415
+ [data-slot="skeleton"].animate-pulse {
416
+ animation:
417
+ ev-skeleton-in 200ms var(--ev-ease-out, ease-out),
418
+ ev-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) 200ms infinite;
419
+ }
420
+
421
+ /* ── App shell: the rail's active-row marker and the tab bar's rule ──────────
422
+ ShellNav: one marker node per nav section, placed on the [aria-current] row by
423
+ measurement and moved by a `transform` transition — on the compositor, so it
424
+ keeps its frames while the main thread renders the page just navigated to (a
425
+ JS layout projection here dropped exactly those). Until the first placement
426
+ (SSR, every frame to hydration) the marker has no position: it stays hidden
427
+ and the marked row carries the fill itself, so the first paint is right.
428
+ `data-placed` flips both at once; `entered` is a marker that mounted into a
429
+ section already on screen (a move across sections — a fade), `first` one that
430
+ arrived with its section (a cut: fading under a row whose own fill fades out
431
+ would dip). */
432
+ @keyframes ev-shell-fade-in {
433
+ from {
434
+ opacity: 0;
435
+ }
436
+ }
437
+ [data-slot="shell-nav-marker"] {
438
+ transition: transform var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
439
+ }
440
+ [data-slot="shell-nav-marker"]:not([data-placed]) {
441
+ visibility: hidden;
442
+ }
443
+ [data-slot="shell-nav-marker"][data-placed="entered"] {
444
+ animation: ev-shell-fade-in var(--ev-dur-fast, 180ms) var(--ev-ease-out, ease-out);
445
+ }
446
+ [data-slot="shell-nav-marker"]:not([data-placed]) ~ * [aria-current="page"] {
447
+ background-color: var(--primary);
448
+ }
449
+
450
+ /* BottomTabBar: one rule for the whole bar, mounted once and translated by
451
+ whole tab widths (it is exactly one tab wide, so the two cannot drift). A
452
+ route no tab claims fades it out where it stands, and coming back fades it in
453
+ already in place. */
454
+ [data-slot="tab-bar-marker"] {
455
+ transition:
456
+ translate var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out),
457
+ opacity var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
458
+ }
459
+ [data-slot="tab-bar-marker"][data-state="idle"] {
460
+ opacity: 0;
461
+ }
462
+
463
+ @keyframes ev-enter-fade {
464
+ from {
465
+ opacity: 0;
466
+ }
467
+ }
468
+ @media (prefers-reduced-motion: reduce) {
469
+ [data-enter="rise"],
470
+ [data-enter="stagger"] > *,
471
+ [data-slot="settled"][data-state="revealed"] {
472
+ animation-name: ev-enter-fade;
473
+ }
474
+ [data-slot="skeleton"].animate-pulse {
475
+ animation: ev-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
476
+ }
477
+ [data-slot="shell-nav-marker"],
478
+ [data-slot="tab-bar-marker"] {
479
+ transition: none;
480
+ animation: none;
481
+ }
482
+ }
483
+
349
484
  /* ═══ CONTRACT ═══════════════════════════════════════════════════ */
350
485
 
351
486
  @theme inline {
@@ -468,6 +603,26 @@
468
603
 
469
604
  --sidebar-width: 14rem;
470
605
 
606
+ /* app shell (uikit <AppShell> / <ShellNav> / <BottomTabBar>) */
607
+ --shell-rail-w: 15.5rem; /* 248px */
608
+ --shell-tab-bar-h: 4rem; /* 64px, before the bottom safe area */
609
+
610
+ /* ── motion ──────────────────────────────────────────────────── *
611
+ * One place to retune the feel of every animation the kit ships (motion.css
612
+ * reads these) and every one a consumer writes against them:
613
+ * `ease-(--ev-ease-out) duration-(--ev-dur-base)`. Quiet on purpose — short,
614
+ * small travel: motion here says what changed, it never decorates. */
615
+ --ev-ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* every entrance */
616
+ --ev-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* state that reverses */
617
+ --ev-dur-fast: 180ms;
618
+ --ev-dur-base: 280ms;
619
+ --ev-dur-slow: 400ms;
620
+ --ev-rise: 8px; /* how far an entrance travels */
621
+ --ev-stagger: 35ms; /* between rows of a list */
622
+ /* between the sections of a page: cards, not rows — at 35ms six of them read
623
+ as one block twitching; at 50ms the last lands ~0.55s in, still "opening" */
624
+ --ev-stagger-section: 50ms;
625
+
471
626
  /* page container — responsive gutter + max width (uikit <Container>) */
472
627
  --page-max: 90rem; /* 1440px */
473
628
  --page-px: 1rem; /* 16px — mobile gutter, scales up below */