@fluix-ui/vanilla 0.0.6 → 0.0.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.
@@ -521,6 +521,713 @@ var Fluix = (function (exports) {
521
521
  }
522
522
  };
523
523
  }
524
+ var MENU_DEFAULTS = {
525
+ orientation: "vertical",
526
+ roundness: 16
527
+ };
528
+ function createMenuMachine(initialConfig) {
529
+ const store = createStore({
530
+ activeId: initialConfig?.initialActiveId ?? null,
531
+ config: { ...initialConfig }
532
+ });
533
+ function setActive(id) {
534
+ store.update((prev) => {
535
+ if (prev.activeId === id) return prev;
536
+ return { ...prev, activeId: id };
537
+ });
538
+ }
539
+ function configure(config) {
540
+ store.update((prev) => ({ ...prev, config: { ...prev.config, ...config } }));
541
+ }
542
+ function destroy() {
543
+ }
544
+ return { store, setActive, configure, destroy };
545
+ }
546
+ function getMenuAttrs(context) {
547
+ const root = {
548
+ "data-fluix-menu": "",
549
+ "data-orientation": context.orientation
550
+ };
551
+ if (context.theme) {
552
+ root["data-theme"] = context.theme;
553
+ }
554
+ if (context.variant) {
555
+ root["data-variant"] = context.variant;
556
+ }
557
+ return {
558
+ root,
559
+ list: {
560
+ "data-fluix-menu-list": ""
561
+ },
562
+ canvas: {
563
+ "data-fluix-menu-canvas": ""
564
+ },
565
+ indicator: {
566
+ "data-fluix-menu-indicator": ""
567
+ },
568
+ item(itemContext) {
569
+ const item = {
570
+ "data-fluix-menu-item": "",
571
+ "data-menu-id": itemContext.id,
572
+ "data-state": itemContext.active ? "active" : "inactive"
573
+ };
574
+ if (itemContext.disabled) {
575
+ item["data-disabled"] = "true";
576
+ }
577
+ return item;
578
+ }
579
+ };
580
+ }
581
+ var ITEM_SELECTOR = "[data-fluix-menu-item]";
582
+ var TAB_CURVE_RADIUS = 14;
583
+ function readItemFrame(root, activeId, padding, variant, orientation) {
584
+ const activeItem = root.querySelector(
585
+ `${ITEM_SELECTOR}[data-menu-id="${CSS.escape(activeId)}"]`
586
+ );
587
+ if (!activeItem) return null;
588
+ const rootRect = root.getBoundingClientRect();
589
+ const itemRect = activeItem.getBoundingClientRect();
590
+ const width = Math.max(0, itemRect.width + padding * 2);
591
+ const height = Math.max(0, itemRect.height + padding * 2);
592
+ const x = itemRect.left - rootRect.left - padding;
593
+ const y = itemRect.top - rootRect.top - padding;
594
+ if (variant === "tab") {
595
+ if (orientation === "horizontal") {
596
+ const extendedHeight = rootRect.height - y + 1;
597
+ return {
598
+ x,
599
+ y,
600
+ width,
601
+ height: extendedHeight,
602
+ radius: height / 2,
603
+ // use original item height for top pill arc
604
+ visible: width > 0 && height > 0
605
+ };
606
+ }
607
+ const extendedWidth = rootRect.width - x;
608
+ return {
609
+ x,
610
+ y,
611
+ width: extendedWidth,
612
+ height,
613
+ radius: height / 2,
614
+ visible: width > 0 && height > 0
615
+ };
616
+ }
617
+ return {
618
+ x,
619
+ y,
620
+ width,
621
+ height,
622
+ radius: height / 2,
623
+ visible: width > 0 && height > 0
624
+ };
625
+ }
626
+ function generateTabPath(frame, cr) {
627
+ const { x, y, width, height } = frame;
628
+ const r = Math.min(frame.radius, height / 2, width / 2);
629
+ const rw = x + width;
630
+ const concaveR = Math.min(cr, height / 2, Math.max(0, width / 2 - r));
631
+ return [
632
+ `M ${x + r} ${y}`,
633
+ `L ${rw - concaveR} ${y}`,
634
+ `Q ${rw} ${y} ${rw} ${y - concaveR}`,
635
+ `L ${rw} ${y + height + concaveR}`,
636
+ `Q ${rw} ${y + height} ${rw - concaveR} ${y + height}`,
637
+ `L ${x + r} ${y + height}`,
638
+ `A ${r} ${r} 0 0 1 ${x} ${y + height - r}`,
639
+ `L ${x} ${y + r}`,
640
+ `A ${r} ${r} 0 0 1 ${x + r} ${y}`,
641
+ "Z"
642
+ ].join(" ");
643
+ }
644
+ function generateHorizontalTabPath(frame, cr) {
645
+ const { x, y, width, height } = frame;
646
+ const r = Math.min(frame.radius, width / 2, height / 2);
647
+ const bottom = y + height;
648
+ const concaveR = Math.min(cr, width / 2, Math.max(0, height - r));
649
+ return [
650
+ // Start at top-left, after the rounded corner
651
+ `M ${x} ${y + r}`,
652
+ // Arc from left edge to top edge (top-left rounded corner)
653
+ `A ${r} ${r} 0 0 1 ${x + r} ${y}`,
654
+ // Top edge to top-right corner
655
+ `L ${x + width - r} ${y}`,
656
+ // Arc from top edge to right edge (top-right rounded corner)
657
+ `A ${r} ${r} 0 0 1 ${x + width} ${y + r}`,
658
+ // Right edge down to bottom-right concave
659
+ `L ${x + width} ${bottom - concaveR}`,
660
+ // Concave curve at bottom-right (curves outward)
661
+ `Q ${x + width} ${bottom} ${x + width + concaveR} ${bottom}`,
662
+ // Flat bottom edge (off to the right, then back to the left)
663
+ `L ${x - concaveR} ${bottom}`,
664
+ // Concave curve at bottom-left (curves outward)
665
+ `Q ${x} ${bottom} ${x} ${bottom - concaveR}`,
666
+ // Left edge back up
667
+ `L ${x} ${y + r}`,
668
+ "Z"
669
+ ].join(" ");
670
+ }
671
+ function applyFrame(indicator, frame, variant, orientation) {
672
+ if (variant === "tab") {
673
+ const path = indicator;
674
+ const generator = orientation === "horizontal" ? generateHorizontalTabPath : generateTabPath;
675
+ path.setAttribute("d", generator(frame, TAB_CURVE_RADIUS));
676
+ path.setAttribute("opacity", frame.visible ? "1" : "0");
677
+ } else {
678
+ const rect = indicator;
679
+ rect.setAttribute("x", String(frame.x));
680
+ rect.setAttribute("y", String(frame.y));
681
+ rect.setAttribute("width", String(frame.width));
682
+ rect.setAttribute("height", String(frame.height));
683
+ rect.setAttribute("rx", String(frame.radius));
684
+ rect.setAttribute("ry", String(frame.radius));
685
+ rect.setAttribute("opacity", frame.visible ? "1" : "0");
686
+ }
687
+ }
688
+ function frameEquals(a, b) {
689
+ if (!a || !b) return false;
690
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height && a.radius === b.radius && a.visible === b.visible;
691
+ }
692
+ function simulateSpringValues(config) {
693
+ const { stiffness, damping, mass } = config;
694
+ const dt = 1 / 120;
695
+ const maxDuration = 3;
696
+ const samples = [0];
697
+ let position = 0;
698
+ let velocity = 0;
699
+ let t = 0;
700
+ while (t < maxDuration) {
701
+ const acceleration = (-stiffness * (position - 1) - damping * velocity) / mass;
702
+ const midVelocity = velocity + acceleration * (dt / 2);
703
+ const midPosition = position + velocity * (dt / 2);
704
+ const midAcceleration = (-stiffness * (midPosition - 1) - damping * midVelocity) / mass;
705
+ velocity = velocity + midAcceleration * dt;
706
+ position = position + midVelocity * dt;
707
+ t += dt;
708
+ samples.push(position);
709
+ if (Math.abs(position - 1) < 1e-3 && Math.abs(velocity) < 1e-3) break;
710
+ }
711
+ samples.push(1);
712
+ return samples;
713
+ }
714
+ function lerp(a, b, t) {
715
+ return a + (b - a) * t;
716
+ }
717
+ function easeOutCubic(t) {
718
+ return 1 - (1 - t) ** 3;
719
+ }
720
+ var EXIT_MS = 130;
721
+ function animateTabEnter(path, from, to, cr, spring) {
722
+ const rightEdge = from.x + from.width;
723
+ const samples = simulateSpringValues(spring);
724
+ const count = samples.length;
725
+ const durationMs = count / 120 * 1e3;
726
+ const startTime = performance.now();
727
+ let cancelled = false;
728
+ const handle = {
729
+ onfinish: null,
730
+ cancel() {
731
+ cancelled = true;
732
+ }
733
+ };
734
+ function tick() {
735
+ if (cancelled) return;
736
+ const elapsed = performance.now() - startTime;
737
+ const progress = Math.min(elapsed / durationMs, 1);
738
+ const idx = Math.min(Math.floor(progress * (count - 1)), count - 1);
739
+ const t = samples[idx];
740
+ const frame = {
741
+ x: lerp(rightEdge, to.x, t),
742
+ y: to.y,
743
+ width: lerp(0, to.width, t),
744
+ height: to.height,
745
+ radius: to.radius
746
+ };
747
+ path.setAttribute("d", generateTabPath(frame, cr));
748
+ if (progress < 1) {
749
+ requestAnimationFrame(tick);
750
+ } else {
751
+ handle.onfinish?.();
752
+ }
753
+ }
754
+ requestAnimationFrame(tick);
755
+ return handle;
756
+ }
757
+ function animateHorizontalTabEnter(path, from, to, cr, spring) {
758
+ const bottomEdge = from.y + from.height;
759
+ const samples = simulateSpringValues(spring);
760
+ const count = samples.length;
761
+ const durationMs = count / 120 * 1e3;
762
+ const startTime = performance.now();
763
+ let cancelled = false;
764
+ const handle = {
765
+ onfinish: null,
766
+ cancel() {
767
+ cancelled = true;
768
+ }
769
+ };
770
+ function tick() {
771
+ if (cancelled) return;
772
+ const elapsed = performance.now() - startTime;
773
+ const progress = Math.min(elapsed / durationMs, 1);
774
+ const idx = Math.min(Math.floor(progress * (count - 1)), count - 1);
775
+ const t = samples[idx];
776
+ const frame = {
777
+ x: to.x,
778
+ y: lerp(bottomEdge, to.y, t),
779
+ width: to.width,
780
+ height: lerp(0, to.height, t),
781
+ radius: to.radius
782
+ };
783
+ path.setAttribute("d", generateHorizontalTabPath(frame, cr));
784
+ if (progress < 1) {
785
+ requestAnimationFrame(tick);
786
+ } else {
787
+ handle.onfinish?.();
788
+ }
789
+ }
790
+ requestAnimationFrame(tick);
791
+ return handle;
792
+ }
793
+ function animateTabIndicator(path, from, to, cr, spring, onEnterStart) {
794
+ const rightEdge = from.x + from.width;
795
+ const enterSamples = simulateSpringValues({
796
+ stiffness: spring.stiffness * 3,
797
+ damping: spring.damping * 1.8,
798
+ mass: spring.mass
799
+ });
800
+ const enterCount = enterSamples.length;
801
+ const enterMs = enterCount / 120 * 1e3;
802
+ const totalMs = EXIT_MS + enterMs;
803
+ const startTime = performance.now();
804
+ let cancelled = false;
805
+ let enteredPhase2 = false;
806
+ const handle = {
807
+ onfinish: null,
808
+ cancel() {
809
+ cancelled = true;
810
+ }
811
+ };
812
+ function tick() {
813
+ if (cancelled) return;
814
+ const elapsed = performance.now() - startTime;
815
+ let frame;
816
+ if (elapsed < EXIT_MS) {
817
+ const t = easeOutCubic(elapsed / EXIT_MS);
818
+ frame = {
819
+ x: lerp(from.x, rightEdge, t),
820
+ y: from.y,
821
+ width: lerp(from.width, 0, t),
822
+ height: from.height,
823
+ radius: from.radius,
824
+ visible: true
825
+ };
826
+ } else {
827
+ if (!enteredPhase2) {
828
+ enteredPhase2 = true;
829
+ onEnterStart?.();
830
+ }
831
+ const phaseElapsed = elapsed - EXIT_MS;
832
+ const phaseProgress = Math.min(phaseElapsed / enterMs, 1);
833
+ const idx = Math.min(
834
+ Math.floor(phaseProgress * (enterCount - 1)),
835
+ enterCount - 1
836
+ );
837
+ const t = enterSamples[idx];
838
+ frame = {
839
+ x: lerp(rightEdge, to.x, t),
840
+ y: to.y,
841
+ width: lerp(0, to.width, t),
842
+ height: to.height,
843
+ radius: to.radius,
844
+ visible: true
845
+ };
846
+ }
847
+ path.setAttribute("d", generateTabPath(frame, cr));
848
+ if (elapsed < totalMs) {
849
+ requestAnimationFrame(tick);
850
+ } else {
851
+ handle.onfinish?.();
852
+ }
853
+ }
854
+ requestAnimationFrame(tick);
855
+ return handle;
856
+ }
857
+ function animateHorizontalTabIndicator(path, from, to, cr, spring, onEnterStart) {
858
+ const bottomEdge = from.y + from.height;
859
+ const enterSamples = simulateSpringValues({
860
+ stiffness: spring.stiffness * 3,
861
+ damping: spring.damping * 1.8,
862
+ mass: spring.mass
863
+ });
864
+ const enterCount = enterSamples.length;
865
+ const enterMs = enterCount / 120 * 1e3;
866
+ const totalMs = EXIT_MS + enterMs;
867
+ const startTime = performance.now();
868
+ let cancelled = false;
869
+ let enteredPhase2 = false;
870
+ const handle = {
871
+ onfinish: null,
872
+ cancel() {
873
+ cancelled = true;
874
+ }
875
+ };
876
+ function tick() {
877
+ if (cancelled) return;
878
+ const elapsed = performance.now() - startTime;
879
+ let frame;
880
+ if (elapsed < EXIT_MS) {
881
+ const t = easeOutCubic(elapsed / EXIT_MS);
882
+ frame = {
883
+ x: from.x,
884
+ y: lerp(from.y, bottomEdge, t),
885
+ width: from.width,
886
+ height: lerp(from.height, 0, t),
887
+ radius: from.radius,
888
+ visible: true
889
+ };
890
+ } else {
891
+ if (!enteredPhase2) {
892
+ enteredPhase2 = true;
893
+ onEnterStart?.();
894
+ }
895
+ const phaseElapsed = elapsed - EXIT_MS;
896
+ const phaseProgress = Math.min(phaseElapsed / enterMs, 1);
897
+ const idx = Math.min(
898
+ Math.floor(phaseProgress * (enterCount - 1)),
899
+ enterCount - 1
900
+ );
901
+ const t = enterSamples[idx];
902
+ frame = {
903
+ x: to.x,
904
+ y: lerp(bottomEdge, to.y, t),
905
+ width: to.width,
906
+ height: lerp(0, to.height, t),
907
+ radius: to.radius,
908
+ visible: true
909
+ };
910
+ }
911
+ path.setAttribute("d", generateHorizontalTabPath(frame, cr));
912
+ if (elapsed < totalMs) {
913
+ requestAnimationFrame(tick);
914
+ } else {
915
+ handle.onfinish?.();
916
+ }
917
+ }
918
+ requestAnimationFrame(tick);
919
+ return handle;
920
+ }
921
+ var STRETCH_MS = 150;
922
+ function animatePillMorph(rect, from, to, spring) {
923
+ const stretchedX = Math.min(from.x, to.x);
924
+ const stretchedRight = Math.max(from.x + from.width, to.x + to.width);
925
+ const stretchedWidth = stretchedRight - stretchedX;
926
+ const contractSamples = simulateSpringValues({
927
+ stiffness: spring.stiffness * 2.5,
928
+ damping: spring.damping * 1.6,
929
+ mass: spring.mass
930
+ });
931
+ const contractCount = contractSamples.length;
932
+ const contractMs = contractCount / 120 * 1e3;
933
+ const totalMs = STRETCH_MS + contractMs;
934
+ const startTime = performance.now();
935
+ let cancelled = false;
936
+ const handle = {
937
+ onfinish: null,
938
+ cancel() {
939
+ cancelled = true;
940
+ }
941
+ };
942
+ function applyRect(x, y, w, h, r) {
943
+ rect.setAttribute("x", String(x));
944
+ rect.setAttribute("y", String(y));
945
+ rect.setAttribute("width", String(w));
946
+ rect.setAttribute("height", String(h));
947
+ rect.setAttribute("rx", String(r));
948
+ rect.setAttribute("ry", String(r));
949
+ }
950
+ function tick() {
951
+ if (cancelled) return;
952
+ const elapsed = performance.now() - startTime;
953
+ if (elapsed < STRETCH_MS) {
954
+ const t = easeOutCubic(elapsed / STRETCH_MS);
955
+ applyRect(
956
+ lerp(from.x, stretchedX, t),
957
+ lerp(from.y, to.y, t),
958
+ lerp(from.width, stretchedWidth, t),
959
+ lerp(from.height, to.height, t),
960
+ lerp(from.radius, to.radius, t)
961
+ );
962
+ } else {
963
+ const phaseElapsed = elapsed - STRETCH_MS;
964
+ const phaseProgress = Math.min(phaseElapsed / contractMs, 1);
965
+ const idx = Math.min(Math.floor(phaseProgress * (contractCount - 1)), contractCount - 1);
966
+ const t = contractSamples[idx];
967
+ applyRect(
968
+ lerp(stretchedX, to.x, t),
969
+ to.y,
970
+ lerp(stretchedWidth, to.width, t),
971
+ to.height,
972
+ to.radius
973
+ );
974
+ }
975
+ if (elapsed < totalMs) {
976
+ requestAnimationFrame(tick);
977
+ } else {
978
+ handle.onfinish?.();
979
+ }
980
+ }
981
+ requestAnimationFrame(tick);
982
+ return handle;
983
+ }
984
+ function connectMenu(options) {
985
+ const spring = options.spring ?? FLUIX_SPRING;
986
+ const padding = options.padding ?? 6;
987
+ const variant = options.variant;
988
+ const orientation = options.orientation;
989
+ const cleanups = [];
990
+ let currentAnimation = null;
991
+ let lastFrame = null;
992
+ let rafId = 0;
993
+ let resizeObserver = null;
994
+ let mutationObserver = null;
995
+ let previousActiveId = null;
996
+ let animOldId = null;
997
+ let animNewId = null;
998
+ let animPhase = null;
999
+ function setItemState(id, state) {
1000
+ const el = options.root.querySelector(
1001
+ `${ITEM_SELECTOR}[data-menu-id="${CSS.escape(id)}"]`
1002
+ );
1003
+ if (el && el.dataset["state"] !== state) {
1004
+ el.dataset["state"] = state;
1005
+ }
1006
+ }
1007
+ function enforceAnimStates() {
1008
+ if (!animPhase) return;
1009
+ if (animPhase === "exit") {
1010
+ if (animOldId) setItemState(animOldId, "active");
1011
+ if (animNewId) setItemState(animNewId, "inactive");
1012
+ } else {
1013
+ if (animOldId) setItemState(animOldId, "inactive");
1014
+ if (animNewId) setItemState(animNewId, "active");
1015
+ }
1016
+ }
1017
+ function clearAnimState() {
1018
+ animOldId = null;
1019
+ animNewId = null;
1020
+ animPhase = null;
1021
+ }
1022
+ function apply(frame) {
1023
+ applyFrame(options.indicator, frame, variant, orientation);
1024
+ }
1025
+ const updateIndicator = (immediate = false) => {
1026
+ const activeId = options.getActiveId();
1027
+ const nextFrame = activeId ? readItemFrame(options.root, activeId, padding, variant, orientation) : null;
1028
+ const fallbackFrame = nextFrame ?? lastFrame ?? {
1029
+ x: 0,
1030
+ y: 0,
1031
+ width: 0,
1032
+ height: 0,
1033
+ radius: 0,
1034
+ visible: false
1035
+ };
1036
+ if (!lastFrame) {
1037
+ lastFrame = fallbackFrame;
1038
+ previousActiveId = activeId;
1039
+ apply(fallbackFrame);
1040
+ return;
1041
+ }
1042
+ if (frameEquals(lastFrame, fallbackFrame)) return;
1043
+ if (currentAnimation) {
1044
+ currentAnimation.cancel();
1045
+ currentAnimation = null;
1046
+ clearAnimState();
1047
+ }
1048
+ if (variant === "tab" && !immediate && fallbackFrame.visible && !lastFrame.visible) {
1049
+ const to2 = fallbackFrame;
1050
+ lastFrame = to2;
1051
+ previousActiveId = activeId;
1052
+ const isHorizontal = orientation === "horizontal";
1053
+ const collapsedFrom = isHorizontal ? {
1054
+ x: to2.x,
1055
+ y: to2.y + to2.height,
1056
+ // bottom edge
1057
+ width: to2.width,
1058
+ height: 0,
1059
+ radius: to2.radius
1060
+ } : {
1061
+ x: to2.x + to2.width,
1062
+ // right edge
1063
+ y: to2.y,
1064
+ width: 0,
1065
+ height: to2.height,
1066
+ radius: to2.radius
1067
+ };
1068
+ options.indicator.setAttribute("opacity", "1");
1069
+ const springConfig = { stiffness: spring.stiffness ?? 170, damping: spring.damping ?? 18, mass: spring.mass ?? 1 };
1070
+ const enterAnim = isHorizontal ? animateHorizontalTabEnter(
1071
+ options.indicator,
1072
+ collapsedFrom,
1073
+ to2,
1074
+ TAB_CURVE_RADIUS,
1075
+ springConfig
1076
+ ) : animateTabEnter(
1077
+ options.indicator,
1078
+ collapsedFrom,
1079
+ to2,
1080
+ TAB_CURVE_RADIUS,
1081
+ springConfig
1082
+ );
1083
+ currentAnimation = enterAnim;
1084
+ enterAnim.onfinish = () => {
1085
+ currentAnimation = null;
1086
+ const settledId = options.getActiveId();
1087
+ const settled = settledId ? readItemFrame(options.root, settledId, padding, variant, orientation) : null;
1088
+ if (settled) {
1089
+ lastFrame = settled;
1090
+ apply(settled);
1091
+ } else {
1092
+ apply(to2);
1093
+ }
1094
+ };
1095
+ return;
1096
+ }
1097
+ if (immediate || !fallbackFrame.visible || !lastFrame.visible) {
1098
+ lastFrame = fallbackFrame;
1099
+ previousActiveId = activeId;
1100
+ apply(fallbackFrame);
1101
+ return;
1102
+ }
1103
+ const from = lastFrame;
1104
+ const to = fallbackFrame;
1105
+ const oldActiveId = previousActiveId;
1106
+ const newActiveId = activeId;
1107
+ lastFrame = to;
1108
+ previousActiveId = activeId;
1109
+ let animation = null;
1110
+ if (variant === "tab") {
1111
+ animOldId = oldActiveId;
1112
+ animNewId = newActiveId;
1113
+ animPhase = "exit";
1114
+ enforceAnimStates();
1115
+ const springConfig = { stiffness: spring.stiffness ?? 170, damping: spring.damping ?? 18, mass: spring.mass ?? 1 };
1116
+ const onEnterStart = () => {
1117
+ animPhase = "enter";
1118
+ enforceAnimStates();
1119
+ };
1120
+ animation = orientation === "horizontal" ? animateHorizontalTabIndicator(
1121
+ options.indicator,
1122
+ from,
1123
+ to,
1124
+ TAB_CURVE_RADIUS,
1125
+ springConfig,
1126
+ onEnterStart
1127
+ ) : animateTabIndicator(
1128
+ options.indicator,
1129
+ from,
1130
+ to,
1131
+ TAB_CURVE_RADIUS,
1132
+ springConfig,
1133
+ onEnterStart
1134
+ );
1135
+ } else {
1136
+ animation = animatePillMorph(
1137
+ options.indicator,
1138
+ from,
1139
+ to,
1140
+ { stiffness: spring.stiffness ?? 170, damping: spring.damping ?? 18, mass: spring.mass ?? 1 }
1141
+ );
1142
+ }
1143
+ if (!animation) {
1144
+ apply(to);
1145
+ return;
1146
+ }
1147
+ currentAnimation = animation;
1148
+ animation.onfinish = () => {
1149
+ currentAnimation = null;
1150
+ if (variant === "tab") {
1151
+ if (newActiveId) setItemState(newActiveId, "active");
1152
+ if (oldActiveId && oldActiveId !== newActiveId) setItemState(oldActiveId, "inactive");
1153
+ clearAnimState();
1154
+ }
1155
+ const settledId = options.getActiveId();
1156
+ const settled = settledId ? readItemFrame(options.root, settledId, padding, variant, orientation) : null;
1157
+ if (settled) {
1158
+ lastFrame = settled;
1159
+ apply(settled);
1160
+ } else {
1161
+ apply(to);
1162
+ }
1163
+ };
1164
+ };
1165
+ const sync = (immediate = false) => {
1166
+ cancelAnimationFrame(rafId);
1167
+ rafId = requestAnimationFrame(() => updateIndicator(immediate));
1168
+ };
1169
+ const handleClick = (event) => {
1170
+ if (!options.onSelect) return;
1171
+ const target = event.target;
1172
+ if (!target) return;
1173
+ const item = target.closest(ITEM_SELECTOR);
1174
+ if (!item || item.dataset["disabled"] === "true") return;
1175
+ const id = item.dataset["menuId"];
1176
+ if (!id) return;
1177
+ options.onSelect(id);
1178
+ };
1179
+ options.root.addEventListener("click", handleClick);
1180
+ cleanups.push(() => options.root.removeEventListener("click", handleClick));
1181
+ const scheduleSync = () => {
1182
+ if (animPhase) return;
1183
+ sync(false);
1184
+ };
1185
+ resizeObserver = new ResizeObserver(scheduleSync);
1186
+ resizeObserver.observe(options.root);
1187
+ for (const item of options.root.querySelectorAll(ITEM_SELECTOR)) {
1188
+ resizeObserver.observe(item);
1189
+ }
1190
+ cleanups.push(() => {
1191
+ resizeObserver?.disconnect();
1192
+ resizeObserver = null;
1193
+ });
1194
+ mutationObserver = new MutationObserver(() => {
1195
+ if (animPhase) {
1196
+ enforceAnimStates();
1197
+ return;
1198
+ }
1199
+ if (!resizeObserver) return;
1200
+ resizeObserver.disconnect();
1201
+ resizeObserver.observe(options.root);
1202
+ for (const item of options.root.querySelectorAll(ITEM_SELECTOR)) {
1203
+ resizeObserver.observe(item);
1204
+ }
1205
+ sync(false);
1206
+ });
1207
+ mutationObserver.observe(options.root, {
1208
+ childList: true,
1209
+ subtree: true,
1210
+ attributes: true,
1211
+ attributeFilter: ["data-menu-id", "data-state"]
1212
+ });
1213
+ cleanups.push(() => {
1214
+ mutationObserver?.disconnect();
1215
+ mutationObserver = null;
1216
+ });
1217
+ sync(true);
1218
+ return {
1219
+ sync,
1220
+ destroy() {
1221
+ cancelAnimationFrame(rafId);
1222
+ if (currentAnimation) {
1223
+ currentAnimation.cancel();
1224
+ currentAnimation = null;
1225
+ }
1226
+ for (const cleanup of cleanups) cleanup();
1227
+ cleanups.length = 0;
1228
+ }
1229
+ };
1230
+ }
524
1231
 
525
1232
  // src/toast.ts
526
1233
  var WIDTH = 350;
@@ -1758,6 +2465,253 @@ var Fluix = (function (exports) {
1758
2465
  };
1759
2466
  }
1760
2467
 
2468
+ // src/menu.ts
2469
+ var SVG_NS3 = "http://www.w3.org/2000/svg";
2470
+ var GOO_MATRIX = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -10";
2471
+ function applyAttrs3(el, attrs) {
2472
+ for (const [key, value] of Object.entries(attrs)) {
2473
+ el.setAttribute(key, value);
2474
+ }
2475
+ }
2476
+ function createMenu(container, options) {
2477
+ let {
2478
+ orientation = MENU_DEFAULTS.orientation,
2479
+ variant = "pill",
2480
+ theme = "dark",
2481
+ activeId: controlledActiveId,
2482
+ onActiveChange,
2483
+ spring,
2484
+ roundness = MENU_DEFAULTS.roundness,
2485
+ blur: blurProp,
2486
+ fill,
2487
+ items
2488
+ } = options;
2489
+ const springConfig = () => spring ?? FLUIX_SPRING;
2490
+ const resolvedBlur = () => blurProp ?? Math.min(10, Math.max(6, roundness * 0.45));
2491
+ const machine2 = createMenuMachine({
2492
+ orientation,
2493
+ variant,
2494
+ spring,
2495
+ roundness,
2496
+ blur: blurProp,
2497
+ fill,
2498
+ initialActiveId: controlledActiveId ?? null
2499
+ });
2500
+ let snapshot = machine2.store.getSnapshot();
2501
+ let lastActiveNotified = snapshot.activeId;
2502
+ const attrs = getMenuAttrs({ orientation, theme, variant });
2503
+ const filterId = `fluix-menu-goo-${Math.random().toString(36).slice(2, 8)}`;
2504
+ const isTab = variant === "tab";
2505
+ const navEl = document.createElement("nav");
2506
+ applyAttrs3(navEl, attrs.root);
2507
+ navEl.setAttribute("aria-label", "Fluix menu");
2508
+ const canvasDiv = document.createElement("div");
2509
+ applyAttrs3(canvasDiv, attrs.canvas);
2510
+ const svg = document.createElementNS(SVG_NS3, "svg");
2511
+ svg.setAttribute("xmlns", SVG_NS3);
2512
+ svg.setAttribute("width", "1");
2513
+ svg.setAttribute("height", "1");
2514
+ svg.setAttribute("viewBox", "0 0 1 1");
2515
+ svg.setAttribute("aria-hidden", "true");
2516
+ let indicatorEl;
2517
+ if (isTab) {
2518
+ indicatorEl = document.createElementNS(SVG_NS3, "path");
2519
+ applyAttrs3(indicatorEl, attrs.indicator);
2520
+ indicatorEl.setAttribute("d", "");
2521
+ indicatorEl.setAttribute("opacity", "0");
2522
+ indicatorEl.setAttribute("fill", fill ?? "var(--fluix-menu-indicator)");
2523
+ svg.appendChild(indicatorEl);
2524
+ } else {
2525
+ const defs = document.createElementNS(SVG_NS3, "defs");
2526
+ const filter = document.createElementNS(SVG_NS3, "filter");
2527
+ filter.setAttribute("id", filterId);
2528
+ filter.setAttribute("x", "-20%");
2529
+ filter.setAttribute("y", "-20%");
2530
+ filter.setAttribute("width", "140%");
2531
+ filter.setAttribute("height", "140%");
2532
+ filter.setAttribute("color-interpolation-filters", "sRGB");
2533
+ const feBlur = document.createElementNS(SVG_NS3, "feGaussianBlur");
2534
+ feBlur.setAttribute("in", "SourceGraphic");
2535
+ feBlur.setAttribute("stdDeviation", String(resolvedBlur()));
2536
+ feBlur.setAttribute("result", "blur");
2537
+ const feCM = document.createElementNS(SVG_NS3, "feColorMatrix");
2538
+ feCM.setAttribute("in", "blur");
2539
+ feCM.setAttribute("type", "matrix");
2540
+ feCM.setAttribute("values", GOO_MATRIX);
2541
+ feCM.setAttribute("result", "goo");
2542
+ const feComp = document.createElementNS(SVG_NS3, "feComposite");
2543
+ feComp.setAttribute("in", "SourceGraphic");
2544
+ feComp.setAttribute("in2", "goo");
2545
+ feComp.setAttribute("operator", "atop");
2546
+ filter.appendChild(feBlur);
2547
+ filter.appendChild(feCM);
2548
+ filter.appendChild(feComp);
2549
+ defs.appendChild(filter);
2550
+ svg.appendChild(defs);
2551
+ const gGroup = document.createElementNS(SVG_NS3, "g");
2552
+ gGroup.setAttribute("filter", `url(#${filterId})`);
2553
+ indicatorEl = document.createElementNS(SVG_NS3, "rect");
2554
+ applyAttrs3(indicatorEl, attrs.indicator);
2555
+ indicatorEl.setAttribute("x", "0");
2556
+ indicatorEl.setAttribute("y", "0");
2557
+ indicatorEl.setAttribute("width", "0");
2558
+ indicatorEl.setAttribute("height", "0");
2559
+ indicatorEl.setAttribute("rx", "0");
2560
+ indicatorEl.setAttribute("ry", "0");
2561
+ indicatorEl.setAttribute("opacity", "0");
2562
+ indicatorEl.setAttribute("fill", fill ?? "var(--fluix-menu-indicator)");
2563
+ gGroup.appendChild(indicatorEl);
2564
+ svg.appendChild(gGroup);
2565
+ }
2566
+ canvasDiv.appendChild(svg);
2567
+ navEl.appendChild(canvasDiv);
2568
+ const listDiv = document.createElement("div");
2569
+ applyAttrs3(listDiv, attrs.list);
2570
+ const buttonMap = /* @__PURE__ */ new Map();
2571
+ function createItemButton(item) {
2572
+ const btn = document.createElement("button");
2573
+ btn.type = "button";
2574
+ const active = snapshot.activeId === item.id;
2575
+ const itemAttrs = attrs.item({ id: item.id, active, disabled: item.disabled });
2576
+ applyAttrs3(btn, itemAttrs);
2577
+ if (item.disabled) btn.disabled = true;
2578
+ btn.textContent = item.label;
2579
+ btn.addEventListener("click", () => {
2580
+ if (item.disabled) return;
2581
+ if (controlledActiveId === void 0) {
2582
+ machine2.setActive(item.id);
2583
+ } else {
2584
+ onActiveChange?.(item.id);
2585
+ }
2586
+ });
2587
+ buttonMap.set(item.id, btn);
2588
+ listDiv.appendChild(btn);
2589
+ }
2590
+ for (const item of items) {
2591
+ createItemButton(item);
2592
+ }
2593
+ navEl.appendChild(listDiv);
2594
+ container.appendChild(navEl);
2595
+ let size = { width: 0, height: 0 };
2596
+ let measureRaf = 0;
2597
+ const measure = () => {
2598
+ const rect = navEl.getBoundingClientRect();
2599
+ const w = Math.ceil(rect.width);
2600
+ const h = Math.ceil(rect.height);
2601
+ if (size.width !== w || size.height !== h) {
2602
+ size = { width: w, height: h };
2603
+ updateSvgSize();
2604
+ connection?.sync(false);
2605
+ }
2606
+ };
2607
+ const resizeObs = new ResizeObserver(() => {
2608
+ cancelAnimationFrame(measureRaf);
2609
+ measureRaf = requestAnimationFrame(measure);
2610
+ });
2611
+ resizeObs.observe(navEl);
2612
+ function updateSvgSize() {
2613
+ const w = Math.max(1, size.width);
2614
+ const h = Math.max(1, size.height);
2615
+ svg.setAttribute("width", String(w));
2616
+ svg.setAttribute("height", String(h));
2617
+ svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
2618
+ }
2619
+ let connection = connectMenu({
2620
+ root: navEl,
2621
+ indicator: indicatorEl,
2622
+ getActiveId: () => snapshot.activeId,
2623
+ onSelect(id) {
2624
+ if (controlledActiveId === void 0) {
2625
+ machine2.setActive(id);
2626
+ } else {
2627
+ onActiveChange?.(id);
2628
+ }
2629
+ },
2630
+ spring: springConfig(),
2631
+ variant,
2632
+ orientation
2633
+ });
2634
+ requestAnimationFrame(() => {
2635
+ measure();
2636
+ connection.sync(false);
2637
+ });
2638
+ const unsubscribe = machine2.store.subscribe(() => {
2639
+ const next = machine2.store.getSnapshot();
2640
+ snapshot = next;
2641
+ for (const item of items) {
2642
+ const btn = buttonMap.get(item.id);
2643
+ if (btn) {
2644
+ const active = next.activeId === item.id;
2645
+ const itemAttrs = attrs.item({ id: item.id, active, disabled: item.disabled });
2646
+ applyAttrs3(btn, itemAttrs);
2647
+ }
2648
+ }
2649
+ if (next.activeId && lastActiveNotified !== next.activeId && onActiveChange) {
2650
+ onActiveChange(next.activeId);
2651
+ }
2652
+ lastActiveNotified = next.activeId;
2653
+ connection.sync(false);
2654
+ });
2655
+ return {
2656
+ setActive(id) {
2657
+ machine2.setActive(id);
2658
+ },
2659
+ update(opts) {
2660
+ if (opts.orientation !== void 0) orientation = opts.orientation;
2661
+ if (opts.variant !== void 0) variant = opts.variant;
2662
+ if (opts.theme !== void 0) theme = opts.theme;
2663
+ if (opts.activeId !== void 0) controlledActiveId = opts.activeId;
2664
+ if (opts.onActiveChange !== void 0) onActiveChange = opts.onActiveChange;
2665
+ if (opts.spring !== void 0) spring = opts.spring;
2666
+ if (opts.roundness !== void 0) roundness = opts.roundness;
2667
+ if (opts.blur !== void 0) blurProp = opts.blur;
2668
+ if (opts.fill !== void 0) fill = opts.fill;
2669
+ machine2.configure({ orientation, variant, spring, roundness, blur: blurProp, fill });
2670
+ if (controlledActiveId !== void 0) {
2671
+ machine2.setActive(controlledActiveId ?? null);
2672
+ }
2673
+ const newAttrs = getMenuAttrs({ orientation, theme, variant });
2674
+ applyAttrs3(navEl, newAttrs.root);
2675
+ if (opts.items !== void 0) {
2676
+ items = opts.items;
2677
+ listDiv.innerHTML = "";
2678
+ buttonMap.clear();
2679
+ for (const item of items) {
2680
+ createItemButton(item);
2681
+ }
2682
+ }
2683
+ connection.destroy();
2684
+ connection = connectMenu({
2685
+ root: navEl,
2686
+ indicator: indicatorEl,
2687
+ getActiveId: () => snapshot.activeId,
2688
+ onSelect(id) {
2689
+ if (controlledActiveId === void 0) {
2690
+ machine2.setActive(id);
2691
+ } else {
2692
+ onActiveChange?.(id);
2693
+ }
2694
+ },
2695
+ spring: springConfig(),
2696
+ variant,
2697
+ orientation
2698
+ });
2699
+ requestAnimationFrame(() => {
2700
+ measure();
2701
+ connection.sync(false);
2702
+ });
2703
+ },
2704
+ destroy() {
2705
+ unsubscribe();
2706
+ cancelAnimationFrame(measureRaf);
2707
+ resizeObs.disconnect();
2708
+ connection.destroy();
2709
+ navEl.remove();
2710
+ }
2711
+ };
2712
+ }
2713
+
2714
+ exports.createMenu = createMenu;
1761
2715
  exports.createNotch = createNotch;
1762
2716
  exports.createToaster = createToaster;
1763
2717
  exports.fluix = fluix;