@ai-matrx/design-system 0.9.0 → 0.10.1

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/dist/styles.css CHANGED
@@ -230,6 +230,489 @@
230
230
  }
231
231
  }
232
232
 
233
+ /* ═══════════════════════════════════════════════════════════════════════
234
+ THE MOTION LAYER (0.10.0) — every overlay animation this package emits.
235
+
236
+ THE DOCTRINE (FEATURE.md): a primitive that leaves motion to the host has
237
+ not shipped motion, it has shipped a coin flip. 0.8.0 and 0.9.0 closed that
238
+ for Accordion and Collapsible one primitive at a time. The sweep that
239
+ followed found the SAME shape on ten more, and far worse than a missing
240
+ keyframe: every floating surface in this package — Dialog, AlertDialog,
241
+ Sheet, CommandDialog, Popover, DropdownMenu, ContextMenu, HoverCard,
242
+ Select, Tooltip — animated with `animate-in` / `animate-out` /
243
+ `fade-in-0` / `zoom-in-95` / `slide-in-from-*-2`. Those are not Tailwind
244
+ utilities. They come from the `tailwindcss-animate` plugin (or its v4
245
+ successor `tw-animate-css`), which is a HOST dependency this package never
246
+ declared and cannot declare — a Tailwind plugin is loaded by the host's CSS
247
+ entry, not by a node_modules package.
248
+
249
+ Measured on built output, 2026-09-07, before this file owned any of it:
250
+
251
+ matrx-frontend `@import "tw-animate-css"` in globals.css → animated
252
+ aidream/dashboard no plugin → 0 rules
253
+ aidream/workflow-studio no plugin → 0 rules
254
+ matrx-extend `tailwindcss-animate` in package.json but
255
+ NEVER LOADED (it is a Tailwind v3 plugin and
256
+ extend is on v4 with no `@plugin` directive)
257
+ → 0 rules
258
+
259
+ Three consumers out of four. `grep -c 'animate-in\|slide-in-from'` on each
260
+ app's built stylesheet returned 0. Every dialog, dropdown, popover, tooltip
261
+ and sheet in the two aidream apps and the Chrome extension appeared
262
+ instantly, with no animation and no error, for as long as they existed.
263
+ The one host that worked, worked by accident of its own CSS entry.
264
+
265
+ So the package owns it. Below: package-defined keyframes, applied through
266
+ `matrx-`-prefixed classes the components put on the DOM themselves, timed
267
+ from `--matrx-motion-*` tokens, silenced by `prefers-reduced-motion`, and
268
+ asserted by `styles.test.ts` + the packed-tarball canary.
269
+
270
+ WHERE THE LINE IS. This layer owns KEYFRAME ANIMATION — motion that either
271
+ happens or silently does not. It deliberately does NOT own Tailwind's
272
+ `transition-*` / `duration-*` / `ease-*` utilities that the primitives also
273
+ carry (a hover colour easing, the progress bar's width, the close button's
274
+ opacity). Those are CORE Tailwind, present in every Tailwind host, and
275
+ their absence degrades to an instant-but-correct change rather than to a
276
+ surface that lies about its state. Re-implementing them here would mean
277
+ shipping a copy of Tailwind.
278
+
279
+ UNLAYERED on purpose, exactly like the accordion and collapsible rules
280
+ above: a host that still loads `tw-animate-css` would otherwise emit a
281
+ competing `animation` from `@layer utilities` and win, giving that one host
282
+ different motion from everyone else — the coin flip again, wearing a
283
+ working coat. Unlayered means every consumer gets identical motion. The
284
+ sanctioned escape is the `animated={false}` prop on the component, not a
285
+ className.
286
+ ═══════════════════════════════════════════════════════════════════════ */
287
+
288
+ /* --- scrim ------------------------------------------------------------- */
289
+ @keyframes matrx-fade-in {
290
+ from {
291
+ opacity: 0;
292
+ }
293
+ to {
294
+ opacity: 1;
295
+ }
296
+ }
297
+ @keyframes matrx-fade-out {
298
+ from {
299
+ opacity: 1;
300
+ }
301
+ to {
302
+ opacity: 0;
303
+ }
304
+ }
305
+ .matrx-motion-overlay[data-state="open"] {
306
+ animation: matrx-fade-in var(--matrx-motion-duration-base)
307
+ var(--matrx-motion-ease-out);
308
+ }
309
+ .matrx-motion-overlay[data-state="closed"] {
310
+ animation: matrx-fade-out var(--matrx-motion-duration-base)
311
+ var(--matrx-motion-ease-in);
312
+ }
313
+
314
+ /* --- centred dialog card ----------------------------------------------
315
+ CENTRING AND MOTION ARE TWO DIFFERENT CSS PROPERTIES HERE, ON PURPOSE.
316
+ 0.10.0 centred the card with `translate-x-[-50%] translate-y-[-50%]`
317
+ utilities and RE-STATED that centring inside the keyframes, on the
318
+ assumption an animation of `transform` replaces whatever positioned the
319
+ element. That assumption is a Tailwind v3 assumption, and it shipped a
320
+ live regression:
321
+
322
+ Tailwind v3 `translate-x-[-50%]` -> `transform: translate(…)` (replaced)
323
+ Tailwind v4 `translate-x-[-50%]` -> `translate: -50% -50%` (COMPOSES)
324
+
325
+ `translate` is an independent transform property: per spec it is applied
326
+ IN ADDITION to `transform`, not replaced by it. So on every v4 host the
327
+ card was centred twice for the animation's whole duration and snapped back
328
+ at the end. Measured live, 2560x1289, resting rect x=128 y=32:
329
+ currentTime=0 -> x=-966 y=-525 (off the top-left of the viewport);
330
+ `el.style.translate='none'` mid-animation -> x=137 y=41, i.e. resting.
331
+ Three of four consumers regressed (aidream/dashboard, workflow-studio,
332
+ matrx-extend went from instant-and-centred to flying in from off-screen).
333
+
334
+ The fix does not pick a Tailwind version. The package owns the centring
335
+ itself, in the `translate` property, on `.matrx-dialog-centered` below —
336
+ so it reads identically under v3 and v4 hosts — and the keyframes express
337
+ ONLY the delta: opacity, and a 2% rise scaled up from 95% carried by
338
+ `transform`, which now has no other job. Resting state is
339
+ `translate: -50% -50%; transform: none`, byte-identical to 0.10.0's
340
+ rendered rect. The two never restate each other again, and
341
+ `motion.test.tsx` fails the build if a centred surface goes back to
342
+ wearing a non-zero translate utility. */
343
+ .matrx-dialog-centered {
344
+ translate: -50% -50%;
345
+ }
346
+ @keyframes matrx-dialog-in {
347
+ from {
348
+ opacity: 0;
349
+ transform: translateY(2%) scale(0.95);
350
+ }
351
+ to {
352
+ opacity: 1;
353
+ transform: none;
354
+ }
355
+ }
356
+ @keyframes matrx-dialog-out {
357
+ from {
358
+ opacity: 1;
359
+ transform: none;
360
+ }
361
+ to {
362
+ opacity: 0;
363
+ transform: translateY(2%) scale(0.95);
364
+ }
365
+ }
366
+ .matrx-motion-dialog[data-state="open"] {
367
+ animation: matrx-dialog-in var(--matrx-motion-duration-base)
368
+ var(--matrx-motion-ease-out);
369
+ }
370
+ .matrx-motion-dialog[data-state="closed"] {
371
+ animation: matrx-dialog-out var(--matrx-motion-duration-base)
372
+ var(--matrx-motion-ease-in);
373
+ }
374
+
375
+ /* --- edge sheets -------------------------------------------------------
376
+ Five named classes rather than one `data-side` rule, because a Sheet's side
377
+ is a `sheetVariants` variant on the element's own class list — Radix
378
+ publishes no `data-side` on a Dialog-based sheet. The mobile Dialog /
379
+ CommandDialog bottom sheet reuses `-bottom`. */
380
+ @keyframes matrx-slide-in-top {
381
+ from {
382
+ transform: translateY(-100%);
383
+ }
384
+ to {
385
+ transform: translateY(0);
386
+ }
387
+ }
388
+ @keyframes matrx-slide-out-top {
389
+ from {
390
+ transform: translateY(0);
391
+ }
392
+ to {
393
+ transform: translateY(-100%);
394
+ }
395
+ }
396
+ @keyframes matrx-slide-in-bottom {
397
+ from {
398
+ transform: translateY(100%);
399
+ }
400
+ to {
401
+ transform: translateY(0);
402
+ }
403
+ }
404
+ @keyframes matrx-slide-out-bottom {
405
+ from {
406
+ transform: translateY(0);
407
+ }
408
+ to {
409
+ transform: translateY(100%);
410
+ }
411
+ }
412
+ @keyframes matrx-slide-in-left {
413
+ from {
414
+ transform: translateX(-100%);
415
+ }
416
+ to {
417
+ transform: translateX(0);
418
+ }
419
+ }
420
+ @keyframes matrx-slide-out-left {
421
+ from {
422
+ transform: translateX(0);
423
+ }
424
+ to {
425
+ transform: translateX(-100%);
426
+ }
427
+ }
428
+ @keyframes matrx-slide-in-right {
429
+ from {
430
+ transform: translateX(100%);
431
+ }
432
+ to {
433
+ transform: translateX(0);
434
+ }
435
+ }
436
+ @keyframes matrx-slide-out-right {
437
+ from {
438
+ transform: translateX(0);
439
+ }
440
+ to {
441
+ transform: translateX(100%);
442
+ }
443
+ }
444
+ .matrx-motion-sheet-top[data-state="open"] {
445
+ animation: matrx-slide-in-top var(--matrx-motion-duration-sheet)
446
+ var(--matrx-motion-ease-out);
447
+ }
448
+ .matrx-motion-sheet-top[data-state="closed"] {
449
+ animation: matrx-slide-out-top var(--matrx-motion-duration-slow)
450
+ var(--matrx-motion-ease-in);
451
+ }
452
+ .matrx-motion-sheet-bottom[data-state="open"] {
453
+ animation: matrx-slide-in-bottom var(--matrx-motion-duration-sheet)
454
+ var(--matrx-motion-ease-out);
455
+ }
456
+ .matrx-motion-sheet-bottom[data-state="closed"] {
457
+ animation: matrx-slide-out-bottom var(--matrx-motion-duration-slow)
458
+ var(--matrx-motion-ease-in);
459
+ }
460
+ .matrx-motion-sheet-left[data-state="open"] {
461
+ animation: matrx-slide-in-left var(--matrx-motion-duration-sheet)
462
+ var(--matrx-motion-ease-out);
463
+ }
464
+ .matrx-motion-sheet-left[data-state="closed"] {
465
+ animation: matrx-slide-out-left var(--matrx-motion-duration-slow)
466
+ var(--matrx-motion-ease-in);
467
+ }
468
+ .matrx-motion-sheet-right[data-state="open"] {
469
+ animation: matrx-slide-in-right var(--matrx-motion-duration-sheet)
470
+ var(--matrx-motion-ease-out);
471
+ }
472
+ .matrx-motion-sheet-right[data-state="closed"] {
473
+ animation: matrx-slide-out-right var(--matrx-motion-duration-slow)
474
+ var(--matrx-motion-ease-in);
475
+ }
476
+ /* A `side="center"` Sheet has no edge to come from, so it fades like a
477
+ dialog scrim rather than sliding from nowhere. */
478
+ .matrx-motion-sheet-center[data-state="open"] {
479
+ animation: matrx-fade-in var(--matrx-motion-duration-sheet)
480
+ var(--matrx-motion-ease-out);
481
+ }
482
+ .matrx-motion-sheet-center[data-state="closed"] {
483
+ animation: matrx-fade-out var(--matrx-motion-duration-slow)
484
+ var(--matrx-motion-ease-in);
485
+ }
486
+ /* The mobile Dialog / CommandDialog bottom sheet: slides AND fades, at the
487
+ dialog's 200ms rather than the Sheet's 500ms, because it is a dialog
488
+ wearing sheet geometry and it opens from a tap, not from a nav gesture. */
489
+ @keyframes matrx-bottom-sheet-in {
490
+ from {
491
+ opacity: 0;
492
+ transform: translateY(100%);
493
+ }
494
+ to {
495
+ opacity: 1;
496
+ transform: translateY(0);
497
+ }
498
+ }
499
+ @keyframes matrx-bottom-sheet-out {
500
+ from {
501
+ opacity: 1;
502
+ transform: translateY(0);
503
+ }
504
+ to {
505
+ opacity: 0;
506
+ transform: translateY(100%);
507
+ }
508
+ }
509
+ .matrx-motion-bottom-sheet[data-state="open"] {
510
+ animation: matrx-bottom-sheet-in var(--matrx-motion-duration-base)
511
+ var(--matrx-motion-ease-out);
512
+ }
513
+ .matrx-motion-bottom-sheet[data-state="closed"] {
514
+ animation: matrx-bottom-sheet-out var(--matrx-motion-duration-base)
515
+ var(--matrx-motion-ease-in);
516
+ }
517
+
518
+ /* --- poppers -----------------------------------------------------------
519
+ Popover, DropdownMenu, ContextMenu, HoverCard, Select and Tooltip all
520
+ position through Radix's popper, which publishes `data-side` on the content
521
+ and `--radix-popper-transform-origin` — the point on the surface that faces
522
+ the trigger. Reading that origin is a correctness fix the `zoom-in-95`
523
+ original never had: the surface now grows OUT OF its anchor instead of out
524
+ of its own centre, so a menu below a button no longer appears to expand
525
+ upward into the button.
526
+
527
+ `data-side` says where the surface sits, so it travels FROM the trigger:
528
+ a surface on the `bottom` starts slightly above where it lands. Exit fades
529
+ and shrinks without sliding — matching the originals, and the right
530
+ behaviour, since a surface leaving should not appear to move somewhere. */
531
+ @keyframes matrx-popper-in-from-top {
532
+ from {
533
+ opacity: 0;
534
+ transform: scale(0.95) translateY(calc(-1 * var(--matrx-motion-popper-travel)));
535
+ }
536
+ to {
537
+ opacity: 1;
538
+ transform: scale(1) translateY(0);
539
+ }
540
+ }
541
+ @keyframes matrx-popper-in-from-bottom {
542
+ from {
543
+ opacity: 0;
544
+ transform: scale(0.95) translateY(var(--matrx-motion-popper-travel));
545
+ }
546
+ to {
547
+ opacity: 1;
548
+ transform: scale(1) translateY(0);
549
+ }
550
+ }
551
+ @keyframes matrx-popper-in-from-left {
552
+ from {
553
+ opacity: 0;
554
+ transform: scale(0.95) translateX(calc(-1 * var(--matrx-motion-popper-travel)));
555
+ }
556
+ to {
557
+ opacity: 1;
558
+ transform: scale(1) translateX(0);
559
+ }
560
+ }
561
+ @keyframes matrx-popper-in-from-right {
562
+ from {
563
+ opacity: 0;
564
+ transform: scale(0.95) translateX(var(--matrx-motion-popper-travel));
565
+ }
566
+ to {
567
+ opacity: 1;
568
+ transform: scale(1) translateX(0);
569
+ }
570
+ }
571
+ @keyframes matrx-popper-out {
572
+ from {
573
+ opacity: 1;
574
+ transform: scale(1);
575
+ }
576
+ to {
577
+ opacity: 0;
578
+ transform: scale(0.95);
579
+ }
580
+ }
581
+ /* THE RIGHT VARIABLE, PER PRIMITIVE. Radix publishes the anchor-facing origin
582
+ under the shared popper name AND under a per-primitive alias, and which one
583
+ is present depends on the primitive — `Select` sets
584
+ `--radix-select-content-transform-origin`, `Tooltip` sets
585
+ `--radix-tooltip-content-transform-origin`, and so on. Reusing one
586
+ primitive's variable on another is the exact silent death census row 19f
587
+ recorded (accordion keyframes on a collapsible panel), so this states the
588
+ whole chain rather than betting on one name. If none resolves, the origin
589
+ falls back to the element's centre — degraded, never broken. */
590
+ .matrx-motion-popper {
591
+ transform-origin: var(
592
+ --radix-popper-transform-origin,
593
+ var(
594
+ --radix-select-content-transform-origin,
595
+ var(
596
+ --radix-tooltip-content-transform-origin,
597
+ var(
598
+ --radix-dropdown-menu-content-transform-origin,
599
+ var(
600
+ --radix-context-menu-content-transform-origin,
601
+ var(
602
+ --radix-hover-card-content-transform-origin,
603
+ var(--radix-popover-content-transform-origin, center)
604
+ )
605
+ )
606
+ )
607
+ )
608
+ )
609
+ );
610
+ }
611
+ /* A surface rendered BELOW the trigger enters from above it, and so on. The
612
+ bare `[data-state=open]` rule is the fallback for the frame before the
613
+ popper has measured a side (and for `Select` in `item-aligned` position,
614
+ which publishes no side at all). */
615
+ .matrx-motion-popper[data-state="open"] {
616
+ animation: matrx-popper-in-from-top var(--matrx-motion-duration-fast)
617
+ var(--matrx-motion-ease-out);
618
+ }
619
+ .matrx-motion-popper[data-state="open"][data-side="bottom"] {
620
+ animation-name: matrx-popper-in-from-top;
621
+ }
622
+ .matrx-motion-popper[data-state="open"][data-side="top"] {
623
+ animation-name: matrx-popper-in-from-bottom;
624
+ }
625
+ .matrx-motion-popper[data-state="open"][data-side="right"] {
626
+ animation-name: matrx-popper-in-from-left;
627
+ }
628
+ .matrx-motion-popper[data-state="open"][data-side="left"] {
629
+ animation-name: matrx-popper-in-from-right;
630
+ }
631
+ .matrx-motion-popper[data-state="closed"] {
632
+ animation: matrx-popper-out var(--matrx-motion-duration-fast)
633
+ var(--matrx-motion-ease-in);
634
+ }
635
+
636
+ /* THE POPPER'S RESTING GAP — same law as `.matrx-dialog-centered` above, same
637
+ reason. `Select` in `popper` position used to nudge itself off its trigger
638
+ with `data-[side=bottom]:translate-y-1` and three siblings. Those are
639
+ Tailwind translate utilities on a surface whose keyframes animate
640
+ `transform` — the identical composite hazard the centred card shipped in
641
+ 0.10.0, at 4px instead of half a viewport: on a v4 host the gap composed
642
+ correctly by luck, on a v3 host the animation replaced it and the surface
643
+ snapped 4px at the end. Stated as the `translate` property, it is the
644
+ resting offset in every host and the keyframes keep `transform` to
645
+ themselves. Sides are Radix's `data-side` (where the surface SITS), so a
646
+ surface below its trigger moves further down. */
647
+ .matrx-popper-offset[data-side="bottom"] {
648
+ translate: 0 var(--matrx-popper-offset);
649
+ }
650
+ .matrx-popper-offset[data-side="top"] {
651
+ translate: 0 calc(-1 * var(--matrx-popper-offset));
652
+ }
653
+ .matrx-popper-offset[data-side="right"] {
654
+ translate: var(--matrx-popper-offset) 0;
655
+ }
656
+ .matrx-popper-offset[data-side="left"] {
657
+ translate: calc(-1 * var(--matrx-popper-offset)) 0;
658
+ }
659
+
660
+ /* --- the two indefinite loops -----------------------------------------
661
+ `animate-pulse` and `animate-spin` ARE core Tailwind, so they survive in a
662
+ Tailwind host — but a Skeleton that does not pulse is indistinguishable
663
+ from an empty grey box, and a spinner that does not spin claims the work
664
+ has stopped. Both are load-bearing lies in a non-Tailwind consumer, and
665
+ both are four lines. The package owns them. */
666
+ @keyframes matrx-pulse {
667
+ 0%,
668
+ 100% {
669
+ opacity: 1;
670
+ }
671
+ 50% {
672
+ opacity: 0.5;
673
+ }
674
+ }
675
+ @keyframes matrx-spin {
676
+ to {
677
+ transform: rotate(360deg);
678
+ }
679
+ }
680
+ .matrx-pulse {
681
+ animation: matrx-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
682
+ }
683
+ .matrx-spin {
684
+ animation: matrx-spin 1s linear infinite;
685
+ }
686
+
687
+ /* --- the accessibility switch -----------------------------------------
688
+ ONE block for the whole motion layer. A surface still appears and
689
+ disappears — it just does so without travelling. The two indefinite loops
690
+ are the exception that proves the rule: a Skeleton and a spinner say
691
+ "still working" with motion and nothing else, so silencing them entirely
692
+ would remove the information. They slow down instead of stopping.
693
+
694
+ Deliberately NOT a `--matrx-motion-*` token: a user's OS-level reduced
695
+ motion preference is not an org setting a host may override. */
696
+ @media (prefers-reduced-motion: reduce) {
697
+ .matrx-motion-overlay[data-state],
698
+ .matrx-motion-dialog[data-state],
699
+ .matrx-motion-sheet-top[data-state],
700
+ .matrx-motion-sheet-bottom[data-state],
701
+ .matrx-motion-sheet-left[data-state],
702
+ .matrx-motion-sheet-right[data-state],
703
+ .matrx-motion-sheet-center[data-state],
704
+ .matrx-motion-bottom-sheet[data-state],
705
+ .matrx-motion-popper[data-state] {
706
+ animation: none;
707
+ }
708
+ .matrx-pulse {
709
+ animation-duration: 4s;
710
+ }
711
+ .matrx-spin {
712
+ animation-duration: 3s;
713
+ }
714
+ }
715
+
233
716
  /* ────────────────────────────────────────────────────────────────────────
234
717
  Scroll fade — the "there is more below" cue. UNLAYERED on purpose.
235
718
  Driven by `useScrollFade`, which sets the data attributes ONLY on edges
package/dist/tokens.css CHANGED
@@ -117,6 +117,36 @@
117
117
  0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
118
118
  --matrx-glass-shadow-lg:
119
119
  0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.04);
120
+
121
+ /* MOTION (0.10.0). Every package-owned animation in styles.css reads its
122
+ duration and easing from here, so a host retimes the whole system by
123
+ redeclaring four values — and an org that wants calmer motion sets them
124
+ once instead of overriding twenty rules. They are NOT the accessibility
125
+ switch: `prefers-reduced-motion: reduce` silences the animations
126
+ outright in styles.css, regardless of these values.
127
+
128
+ The numbers are the ones the primitives already carried as Tailwind
129
+ `duration-*` utilities before the package owned the keyframes:
130
+ poppers/tooltips 150ms, dialogs and overlays 200ms, a sheet closing
131
+ 300ms and opening 500ms. */
132
+ --matrx-motion-duration-fast: 150ms;
133
+ --matrx-motion-duration-base: 200ms;
134
+ --matrx-motion-duration-slow: 300ms;
135
+ --matrx-motion-duration-sheet: 500ms;
136
+ /* Enters decelerate (the surface arrives and settles); exits accelerate
137
+ (it leaves and is gone). Linear on both — which is what
138
+ `tailwindcss-animate` defaulted to — reads mechanical at these
139
+ durations. */
140
+ --matrx-motion-ease-out: cubic-bezier(0, 0, 0.2, 1);
141
+ --matrx-motion-ease-in: cubic-bezier(0.4, 0, 1, 1);
142
+ /* The distance a popper travels toward its anchor as it opens. 0.5rem is
143
+ the `slide-in-from-*-2` the primitives carried. */
144
+ --matrx-motion-popper-travel: 0.5rem;
145
+ /* The RESTING gap between a `popper`-positioned surface and its trigger —
146
+ not motion, geometry. 0.25rem is the `translate-y-1` the primitives
147
+ carried before 0.10.1 moved it off Tailwind's translate utilities and
148
+ into the `translate` property (see `.matrx-popper-offset`). */
149
+ --matrx-popper-offset: 0.25rem;
120
150
  }
121
151
 
122
152
  /* Dark theme. Matches the `.dark`-class convention (Tailwind v4 hosts wire
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/design-system",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "AI Matrx semantic React primitives shared across Vite, Next.js, desktop, and customer-built applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",