@luxfi/ui 7.4.6 → 7.4.7

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/src/vite.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  // Node-only (it is a build-time module) and dependency-free: it returns a plain
15
15
  // Vite plugin object, so it does not force `vite` into anyone's runtime graph.
16
16
 
17
- import { GUI_DEFINES, GUI_PACKAGES } from './engine';
17
+ import { ENGINE_DEDUPE, GUI_DEFINES } from './engine';
18
18
 
19
19
  interface LuxUiViteOptions {
20
20
  /**
@@ -42,14 +42,9 @@ interface LuxUiVitePlugin {
42
42
  // The WHOLE engine, not a hand-picked three. `@hanzogui/popper` and
43
43
  // `@hanzogui/popover` publish the contexts a Tooltip trigger reads, and they
44
44
  // were the two missing from the old list — so a consumer using this plugin
45
- // could still get two poppers and the `setReference` crash it causes. One list,
46
- // shared with the Next wrapper. See ./engine.
47
- const DEDUPE = [
48
- 'react',
49
- 'react-dom',
50
- '@tanstack/react-query',
51
- ...GUI_PACKAGES,
52
- ];
45
+ // could still get two poppers and the `setReference` crash it causes. The list
46
+ // itself lives in ./engine, shared with the Next wrapper.
47
+ const DEDUPE = ENGINE_DEDUPE;
53
48
 
54
49
  const INCLUDE = [
55
50
  'react-native-web',
package/tokens.css CHANGED
@@ -730,3 +730,220 @@
730
730
  --sidebar-border: var(--lux-paper-10);
731
731
  --sidebar-ring: var(--lux-n-500);
732
732
  }
733
+
734
+ /* ================================================================
735
+ 6 — MOTION. One system: five durations, four curves, six moves.
736
+
737
+ Two rules make "smooth" measurable. Both were established by
738
+ benchmark, not by taste, and the second one is the one that bites.
739
+
740
+ 1. WHAT you animate. A decorative animation may touch `transform`
741
+ and `opacity` and nothing else. Those two are the only properties
742
+ the compositor can run off the main thread, so they survive a
743
+ busy CPU. Animating width / height / top / left / margin /
744
+ background-position forces layout or paint every frame. Worse,
745
+ the layout ones also register as layout shift: the explorer's
746
+ mobile drawer animated `left` and scored CLS 0.726 every time it
747
+ opened — Google calls anything over 0.25 "poor". The same drawer
748
+ on `transform` scores 0.
749
+
750
+ 2. HOW MANY things animate. Keep it O(1) in the size of the page.
751
+ Every independently animated element is a compositor layer to
752
+ allocate, rasterise and composite. Past a few hundred, that count
753
+ — not the property — is the bottleneck, and rule 1 alone will
754
+ happily walk you off a cliff. The measurements are in 6c.
755
+
756
+ The one deliberate exception is a disclosure widget (accordion,
757
+ collapsible). There the box change IS the content the user asked
758
+ for, not decoration, so animating its height is correct. Decoration
759
+ animates transform; disclosure animates the box it discloses.
760
+
761
+ Durations are named for their ROLE so a caller cannot pick "300ms
762
+ because it felt right". Exits are shorter than entrances — by the
763
+ time a thing leaves the user has already decided, and waiting on it
764
+ reads as lag.
765
+ ================================================================ */
766
+ :root {
767
+ /* --- Durations -------------------------------------------------- */
768
+ --lux-dur-press: 80ms; /* tap acknowledgement */
769
+ --lux-dur-hover: 120ms; /* pointer feedback */
770
+ --lux-dur-exit: 140ms; /* something leaves */
771
+ --lux-dur-enter: 200ms; /* something appears */
772
+ --lux-dur-move: 280ms; /* a panel travels across the viewport */
773
+
774
+ /* --- Curves ----------------------------------------------------- */
775
+ --lux-ease: cubic-bezier(0.4, 0, 0.2, 1); /* A -> B */
776
+ --lux-ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* entering */
777
+ --lux-ease-in: cubic-bezier(0.7, 0, 0.84, 0); /* leaving */
778
+ --lux-ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1); /* press */
779
+
780
+ /* --- Distances. Enter offsets are one value, not a per-component
781
+ guess. Both collapse to 0 under reduced motion. -------------- */
782
+ --lux-lift: 2px; /* hover raise */
783
+ --lux-rise: 8px; /* enter offset */
784
+ }
785
+
786
+ /* --- The six moves. transform + opacity only. --------------------- */
787
+ @keyframes lux-fade-in { from { opacity: 0 } to { opacity: 1 } }
788
+ @keyframes lux-fade-out { from { opacity: 1 } to { opacity: 0 } }
789
+
790
+ @keyframes lux-rise-in {
791
+ from { opacity: 0; transform: translate3d(0, var(--lux-rise), 0) }
792
+ to { opacity: 1; transform: translate3d(0, 0, 0) }
793
+ }
794
+
795
+ @keyframes lux-scale-in {
796
+ from { opacity: 0; transform: scale(0.96) }
797
+ to { opacity: 1; transform: scale(1) }
798
+ }
799
+
800
+ @keyframes lux-scale-out {
801
+ from { opacity: 1; transform: scale(1) }
802
+ to { opacity: 0; transform: scale(0.96) }
803
+ }
804
+
805
+ /* The loading sweep — ONE per page, never one per skeleton. See 6c. */
806
+ @keyframes lux-sweep {
807
+ from { transform: translate3d(-100%, 0, 0) }
808
+ to { transform: translate3d(350%, 0, 0) }
809
+ }
810
+
811
+ @keyframes lux-spin { to { transform: rotate(360deg) } }
812
+
813
+ /* ================================================================
814
+ 6b — The primitives. Every surface reaches for these, never for a
815
+ hand-rolled keyframe.
816
+ ================================================================ */
817
+
818
+ .lux-enter { animation: lux-rise-in var(--lux-dur-enter) var(--lux-ease-out) both }
819
+ .lux-exit { animation: lux-fade-out var(--lux-dur-exit) var(--lux-ease-in) both }
820
+
821
+ /* Floating surfaces — menu, popover, tooltip, dialog. They grow from
822
+ where they were summoned rather than sliding in from nowhere. */
823
+ .lux-pop { animation: lux-scale-in var(--lux-dur-enter) var(--lux-ease-out) both }
824
+ .lux-pop-exit { animation: lux-scale-out var(--lux-dur-exit) var(--lux-ease-in) both }
825
+
826
+ /* A list cascades by setting --lux-i on each row; one property, no JS. */
827
+ .lux-stagger { animation-delay: calc(var(--lux-i, 0) * 40ms) }
828
+
829
+ /* Hover / press feedback. `transform` and `opacity` are the only
830
+ transitioned properties, so this is safe on any element. */
831
+ .lux-interactive {
832
+ transition:
833
+ transform var(--lux-dur-hover) var(--lux-ease-out),
834
+ opacity var(--lux-dur-hover) var(--lux-ease-out);
835
+ }
836
+ .lux-interactive:hover { transform: translate3d(0, calc(-1 * var(--lux-lift)), 0) }
837
+ .lux-interactive:active { transform: scale(0.98); transition-duration: var(--lux-dur-press) }
838
+ .lux-interactive:disabled,
839
+ .lux-interactive[aria-disabled='true'] { transform: none }
840
+
841
+ /* ================================================================
842
+ 6c — The loading state. Two primitives, one job each.
843
+
844
+ `.lux-skeleton` RESERVES SPACE. `.lux-loading` SAYS WORK IS IN
845
+ FLIGHT. Keeping those apart is what makes the whole thing cheap,
846
+ because only the second one moves — once for the page, not once per
847
+ skeleton.
848
+
849
+ This is the part that is counter-intuitive, so here are the numbers
850
+ that settled it. Chromium, 390x844 @3x, 4x CPU throttle, 5s samples,
851
+ n = concurrently-mounted skeletons:
852
+
853
+ n=600 bgpos 79.6fps sheen 57.2fps pulse 115.4fps bar 120fps
854
+ n=1460 bgpos 33.6fps sheen 23.0fps pulse 39.0fps bar 120fps
855
+ n=3000 bgpos 13.8fps sheen 5.4fps pulse 18.2fps bar 120fps
856
+
857
+ bgpos = per-element background-position gradient (what we shipped)
858
+ sheen = per-element transform on a ::after (the "obvious" fix)
859
+ pulse = per-element opacity keyframe
860
+ bar = flat skeletons + ONE page-level sweep (what we ship now)
861
+
862
+ Read the middle column twice. Rewriting the shimmer as a per-element
863
+ `transform` — obeying "animate transform and opacity only" to the
864
+ letter — made it 32% SLOWER than the background-position version it
865
+ replaced, and at n=3000 it produced a single 2.5-SECOND frame.
866
+
867
+ The property rule is necessary and not sufficient. Past a few
868
+ hundred elements the dominant cost is the NUMBER of independently
869
+ animated elements: each one is a compositor layer to allocate,
870
+ rasterise and composite, and `overflow:hidden` on each adds a
871
+ render surface on top. Only the O(1) design is flat in n — 120fps
872
+ at 600, at 1460 and at 3000, identical to animating nothing.
873
+
874
+ So: a skeleton is a promise about layout and nothing else. It
875
+ occupies exactly the space the real content will (zero CLS) and it
876
+ holds still.
877
+ ================================================================ */
878
+ .lux-skeleton {
879
+ background: var(--color-skeleton-start);
880
+ /* Nothing inside a skeleton is readable yet; say so once, here. */
881
+ color: transparent;
882
+ user-select: none;
883
+ }
884
+
885
+ /* The one moving part. Fixed, 2px, above everything, out of flow — so it
886
+ cannot shift layout and cannot tint content. Mount it while any request
887
+ is in flight; unmount it when they land. One per page. */
888
+ .lux-loading {
889
+ position: fixed;
890
+ inset: 0 0 auto;
891
+ height: 2px;
892
+ z-index: 60;
893
+ overflow: hidden;
894
+ background: var(--color-skeleton-start);
895
+ pointer-events: none;
896
+ }
897
+
898
+ .lux-loading::after {
899
+ content: '';
900
+ position: absolute;
901
+ inset: 0;
902
+ width: 40%;
903
+ background: linear-gradient(
904
+ 90deg,
905
+ transparent 0%,
906
+ var(--color-skeleton-end) 50%,
907
+ transparent 100%
908
+ );
909
+ animation: lux-sweep 1.1s var(--lux-ease) infinite;
910
+ }
911
+
912
+ /* ================================================================
913
+ 6d — Reduced motion.
914
+
915
+ Belt AND braces, because either alone leaks:
916
+ - zeroing the TOKENS catches anything that reads them, including
917
+ inline styles and the Web Animations API, which !important
918
+ cannot reach;
919
+ - the global override catches every hand-rolled transition that
920
+ never adopted a token.
921
+ Unscoped on purpose. A reduced-motion rule that only applies inside
922
+ one component is a rule the rest of the app ignores.
923
+
924
+ Motion goes; FEEDBACK stays. State changes still land, instantly.
925
+ ================================================================ */
926
+ @media (prefers-reduced-motion: reduce) {
927
+ :root {
928
+ --lux-dur-press: 1ms;
929
+ --lux-dur-hover: 1ms;
930
+ --lux-dur-exit: 1ms;
931
+ --lux-dur-enter: 1ms;
932
+ --lux-dur-move: 1ms;
933
+ --lux-lift: 0px;
934
+ --lux-rise: 0px;
935
+ }
936
+
937
+ *,
938
+ *::before,
939
+ *::after {
940
+ animation-duration: 1ms !important;
941
+ animation-iteration-count: 1 !important;
942
+ transition-duration: 1ms !important;
943
+ scroll-behavior: auto !important;
944
+ }
945
+
946
+ /* A sweep that runs once and freezes mid-track is a smear. Drop the
947
+ motion; the bar itself stays, so "work in flight" is still visible. */
948
+ .lux-loading::after { animation: none; width: 100%; opacity: 0.5 }
949
+ }