@iris-ui-kit/vue 0.2.24 → 0.2.26

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 (57) hide show
  1. package/dist/admin.cjs +390 -170
  2. package/dist/admin.cjs.map +1 -1
  3. package/dist/admin.d.cts +3 -2
  4. package/dist/admin.d.ts +3 -2
  5. package/dist/admin.js +2 -2
  6. package/dist/behaviors.cjs +74 -8
  7. package/dist/behaviors.cjs.map +1 -1
  8. package/dist/behaviors.d.cts +57 -1
  9. package/dist/behaviors.d.ts +57 -1
  10. package/dist/behaviors.js +1 -1
  11. package/dist/{chunk-FSDQHBI2.js → chunk-3547J6IN.js} +2 -2
  12. package/dist/chunk-3547J6IN.js.map +1 -0
  13. package/dist/{chunk-WL67XCH5.js → chunk-IN3DQ55U.js} +2 -2
  14. package/dist/chunk-IN3DQ55U.js.map +1 -0
  15. package/dist/{chunk-3IH7IJ5S.js → chunk-IOMKC5OI.js} +78 -13
  16. package/dist/chunk-IOMKC5OI.js.map +1 -0
  17. package/dist/{chunk-3FJUCHCC.js → chunk-IPR5JAVF.js} +11 -3
  18. package/dist/chunk-IPR5JAVF.js.map +1 -0
  19. package/dist/{chunk-BEPH6PPL.js → chunk-LWABTQCB.js} +57 -37
  20. package/dist/chunk-LWABTQCB.js.map +1 -0
  21. package/dist/{chunk-DNRQVM3Q.js → chunk-QBFYIDY7.js} +3 -2
  22. package/dist/chunk-QBFYIDY7.js.map +1 -0
  23. package/dist/{chunk-QQ25RKOO.js → chunk-ZWFSGMOB.js} +385 -173
  24. package/dist/chunk-ZWFSGMOB.js.map +1 -0
  25. package/dist/data.cjs +1 -0
  26. package/dist/data.cjs.map +1 -1
  27. package/dist/data.d.cts +4 -2
  28. package/dist/data.d.ts +4 -2
  29. package/dist/data.js +1 -1
  30. package/dist/error-boundary.cjs.map +1 -1
  31. package/dist/error-boundary.js +1 -1
  32. package/dist/floating.cjs +9 -1
  33. package/dist/floating.cjs.map +1 -1
  34. package/dist/floating.d.cts +6 -0
  35. package/dist/floating.d.ts +6 -0
  36. package/dist/floating.js +1 -1
  37. package/dist/index.cjs +1933 -689
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +171 -7
  40. package/dist/index.d.ts +171 -7
  41. package/dist/index.js +1413 -476
  42. package/dist/index.js.map +1 -1
  43. package/dist/layouts.cjs.map +1 -1
  44. package/dist/layouts.d.cts +1 -1
  45. package/dist/layouts.d.ts +1 -1
  46. package/dist/layouts.js +1 -1
  47. package/dist/skeletons.cjs +55 -35
  48. package/dist/skeletons.cjs.map +1 -1
  49. package/dist/skeletons.js +1 -1
  50. package/package.json +1 -1
  51. package/dist/chunk-3FJUCHCC.js.map +0 -1
  52. package/dist/chunk-3IH7IJ5S.js.map +0 -1
  53. package/dist/chunk-BEPH6PPL.js.map +0 -1
  54. package/dist/chunk-DNRQVM3Q.js.map +0 -1
  55. package/dist/chunk-FSDQHBI2.js.map +0 -1
  56. package/dist/chunk-QQ25RKOO.js.map +0 -1
  57. package/dist/chunk-WL67XCH5.js.map +0 -1
package/dist/admin.cjs CHANGED
@@ -351,7 +351,7 @@ var __NAV_MENU_STYLE_ID = "iris-nav-menu-styles";
351
351
  var CSS = `
352
352
  :where(.iris-nav-menu) {
353
353
  --iris-nav-indent-step: 16px;
354
- --iris-nav-item-padding-inline: 10px;
354
+ --iris-nav-item-padding-inline: 12px;
355
355
  --iris-nav-item-padding-block: 8px;
356
356
  --iris-nav-item-border-radius: var(--iris-radius-md, 6px);
357
357
  --iris-nav-item-hover: var(--iris-surface-hover, var(--iris-surface));
@@ -359,7 +359,7 @@ var CSS = `
359
359
  display: flex;
360
360
  flex-direction: column;
361
361
  align-items: stretch;
362
- gap: 2px;
362
+ gap: var(--iris-space-xxs, 4px);
363
363
  width: 100%;
364
364
  }
365
365
 
@@ -384,16 +384,16 @@ var CSS = `
384
384
  border: none;
385
385
  border-radius: var(--iris-nav-item-border-radius);
386
386
  background: transparent;
387
- color: var(--iris-foreground, var(--iris-color-text, var(--iris-foreground)));
387
+ color: var(--iris-foreground);
388
388
  font: inherit;
389
- font-size: 14px;
389
+ font-size: var(--iris-font-size-md, 14px);
390
390
  line-height: 1.2;
391
391
  font-weight: 400;
392
392
  text-align: start;
393
393
  cursor: pointer;
394
394
  opacity: 1;
395
395
  margin: 0;
396
- gap: 10px;
396
+ gap: var(--iris-space-sm, 12px);
397
397
  padding-block: var(--iris-nav-item-padding-block);
398
398
  padding-inline: var(--iris-nav-item-padding-inline);
399
399
  text-overflow: ellipsis;
@@ -406,7 +406,7 @@ var CSS = `
406
406
  }
407
407
 
408
408
  :where(.iris-nav-menu-item[data-depth='0']) {
409
- --iris-nav-item-padding-inline-start: 10px;
409
+ --iris-nav-item-padding-inline-start: 12px;
410
410
  }
411
411
 
412
412
  :where(.iris-nav-menu-item[data-depth='1']) {
@@ -454,7 +454,7 @@ var CSS = `
454
454
  background: var(--iris-nav-item-hover);
455
455
  }
456
456
 
457
- :where(.iris-nav-menu-item[data-active='true']) {
457
+ .iris-nav-menu-item[data-active='true'] {
458
458
  background: var(--iris-primary);
459
459
  color: var(--iris-primary-foreground, #fff);
460
460
  font-weight: 600;
@@ -483,13 +483,13 @@ var CSS = `
483
483
  }
484
484
 
485
485
  :where(.iris-nav-menu-badge) {
486
- margin-inline-start: 6px;
487
- font-size: 11px;
486
+ margin-inline-start: var(--iris-space-xs, 8px);
487
+ font-size: var(--iris-font-size-xs, 12px);
488
488
  line-height: 1;
489
- padding: 2px 6px;
489
+ padding: var(--iris-space-xxs, 4px) var(--iris-space-xs, 8px);
490
490
  border-radius: 999px;
491
- background: var(--iris-danger, #e5484d);
492
- color: #fff;
491
+ background: var(--iris-danger, #ef4444);
492
+ color: var(--iris-primary-foreground, #fff);
493
493
  flex: 0 0 auto;
494
494
  }
495
495
 
@@ -519,44 +519,68 @@ var CSS = `
519
519
  opacity: 0.75;
520
520
  }
521
521
 
522
- :where(.iris-nav-menu-group--depth-0 > .iris-nav-menu-children) {
523
- display: none;
522
+ /* Vertical branches stay in the DOM and animate height via the grid-rows
523
+ trick (0fr \u2192 1fr); the inner wrapper provides the overflow clipping so the
524
+ transition is a smooth accordion without a max-height cap. */
525
+ :where(.iris-nav-menu--vertical .iris-nav-menu-group > .iris-nav-menu-children) {
526
+ display: grid;
527
+ grid-template-rows: 0fr;
528
+ opacity: 0;
529
+ visibility: hidden;
530
+ transition:
531
+ grid-template-rows 200ms ease,
532
+ opacity 150ms ease,
533
+ visibility 0s linear 200ms;
524
534
  }
525
535
 
526
- :where(.iris-nav-menu-group--depth-1 > .iris-nav-menu-children),
527
- :where(.iris-nav-menu-group--depth-2 > .iris-nav-menu-children),
528
- :where(.iris-nav-menu-group--depth-3 > .iris-nav-menu-children),
529
- :where(.iris-nav-menu-group--depth-4 > .iris-nav-menu-children),
530
- :where(.iris-nav-menu-group--depth-5 > .iris-nav-menu-children),
531
- :where(.iris-nav-menu-group--depth-6 > .iris-nav-menu-children),
532
- :where(.iris-nav-menu-group--depth-7 > .iris-nav-menu-children),
533
- :where(.iris-nav-menu-group--depth-8 > .iris-nav-menu-children),
534
- :where(.iris-nav-menu-group--depth-9 > .iris-nav-menu-children),
535
- :where(.iris-nav-menu-group > .iris-nav-menu-children) {
536
- display: none;
536
+ :where(.iris-nav-menu--vertical .iris-nav-menu-group > .iris-nav-menu-children > .iris-nav-menu-children-inner) {
537
+ overflow: hidden;
538
+ min-height: 0;
537
539
  }
538
540
 
539
- :where(.iris-nav-menu-group[data-open='true'] > .iris-nav-menu-children) {
540
- display: block;
541
+ :where(.iris-nav-menu--vertical .iris-nav-menu-group[data-open='true'] > .iris-nav-menu-children) {
542
+ grid-template-rows: 1fr;
543
+ opacity: 1;
544
+ visibility: visible;
545
+ transition:
546
+ grid-template-rows 200ms ease,
547
+ opacity 150ms ease;
541
548
  }
542
549
 
543
- :where(.iris-nav-menu--horizontal .iris-nav-menu-group > .iris-nav-menu-children) {
550
+ /* Flyout popups (horizontal mode + the collapsed rail) share the popup chrome
551
+ and are hard-hidden when closed (no height animation needed). */
552
+ :where(.iris-nav-menu--horizontal .iris-nav-menu-group > .iris-nav-menu-children),
553
+ :where(.iris-nav-menu--collapsed .iris-nav-menu-group > .iris-nav-menu-children) {
554
+ display: none;
544
555
  position: absolute;
545
556
  min-width: 220px;
546
- padding: 6px;
557
+ padding: var(--iris-space-xs, 8px);
547
558
  border: 1px solid var(--iris-border);
548
559
  border-radius: var(--iris-radius-md, 6px);
549
560
  background: var(--iris-surface);
550
561
  box-shadow: var(--iris-shadow-md);
551
562
  }
552
563
 
564
+ :where(.iris-nav-menu--horizontal .iris-nav-menu-group[data-open='true'] > .iris-nav-menu-children),
565
+ :where(.iris-nav-menu--collapsed .iris-nav-menu-group[data-open='true'] > .iris-nav-menu-children) {
566
+ display: block;
567
+ }
568
+
553
569
  :where(.iris-nav-menu--horizontal .iris-nav-menu-group[data-depth='0'] > .iris-nav-menu-children) {
554
570
  inset-block-start: 100%;
555
571
  inset-inline-start: 0;
556
572
  z-index: 60;
557
573
  }
558
574
 
559
- :where(.iris-nav-menu--horizontal .iris-nav-menu-group[data-depth]:not([data-depth='0']) > .iris-nav-menu-children) {
575
+ /* A9: the collapsed rail popup opens to the right of the icon. */
576
+ :where(.iris-nav-menu--collapsed .iris-nav-menu-group[data-depth='0'] > .iris-nav-menu-children) {
577
+ inset-block-start: 0;
578
+ inset-inline-start: 100%;
579
+ z-index: 60;
580
+ }
581
+
582
+ :where(.iris-nav-menu--horizontal .iris-nav-menu-group[data-depth]:not([data-depth='0']) > .iris-nav-menu-children),
583
+ :where(.iris-nav-menu--collapsed .iris-nav-menu-group[data-depth]:not([data-depth='0']) > .iris-nav-menu-children) {
560
584
  inset-block-start: 0;
561
585
  inset-inline-start: 100%;
562
586
  z-index: 61;
@@ -572,10 +596,20 @@ var CSS = `
572
596
  padding-inline: var(--iris-nav-item-padding-inline);
573
597
  }
574
598
 
599
+ /* A9: the collapsed rail stays centered even though rows carry data-depth
600
+ (the vertical indent rules would otherwise win the specificity tie). */
601
+ :where(.iris-nav-menu--collapsed .iris-nav-menu-item[data-depth]) {
602
+ justify-content: center;
603
+ padding-inline: var(--iris-nav-item-padding-inline);
604
+ }
605
+
575
606
  @media (prefers-reduced-motion: reduce) {
576
607
  :where(.iris-nav-menu-arrow) {
577
608
  transition: none;
578
609
  }
610
+ :where(.iris-nav-menu--vertical .iris-nav-menu-group > .iris-nav-menu-children) {
611
+ transition: none;
612
+ }
579
613
  }
580
614
  `.trim();
581
615
  var installed = false;
@@ -594,6 +628,8 @@ function installNavMenuStyles() {
594
628
  }
595
629
 
596
630
  // src/admin/NavMenu.ts
631
+ var HOVER_OPEN_DELAY = 100;
632
+ var HOVER_CLOSE_DELAY = 150;
597
633
  var IrisNavMenu = vue.defineComponent({
598
634
  name: "IrisNavMenu",
599
635
  inheritAttrs: false,
@@ -625,6 +661,7 @@ var IrisNavMenu = vue.defineComponent({
625
661
  const activePath = vue.computed(
626
662
  () => props.activeKey ? core.findNavPath(props.items, props.activeKey).map((n) => n.key) : []
627
663
  );
664
+ const flyoutMode = vue.computed(() => props.orientation === "horizontal" || props.collapsed);
628
665
  const isControlled = vue.computed(() => props.expandedKeys !== void 0);
629
666
  const model = core.createExpansion({
630
667
  mode: "multiple",
@@ -639,12 +676,9 @@ var IrisNavMenu = vue.defineComponent({
639
676
  const expanded = vue.computed(
640
677
  () => isControlled.value ? props.expandedKeys : internalExpanded.value
641
678
  );
642
- vue.watch(
643
- () => props.activeKey,
644
- (key) => {
645
- if (!isControlled.value) model.merge(key ? core.branchTrail(props.items, key) : []);
646
- }
647
- );
679
+ vue.watch([() => props.activeKey, () => props.items], ([key]) => {
680
+ if (!isControlled.value) model.merge(key ? core.branchTrail(props.items, key) : []);
681
+ });
648
682
  const toggle = (key) => {
649
683
  if (isControlled.value) {
650
684
  const cur = props.expandedKeys;
@@ -660,17 +694,113 @@ var IrisNavMenu = vue.defineComponent({
660
694
  const hovered = vue.ref(null);
661
695
  const hoveredBranches = vue.ref([]);
662
696
  const focusedBranches = vue.ref([]);
697
+ const clickedBranches = vue.ref([]);
698
+ let suppressFocusOpen = false;
699
+ let openTimer = null;
700
+ const closeTimers = /* @__PURE__ */ new Map();
701
+ const clearOpenTimer = () => {
702
+ if (openTimer) {
703
+ clearTimeout(openTimer);
704
+ openTimer = null;
705
+ }
706
+ };
707
+ const cancelClose = (key) => {
708
+ const timer = closeTimers.get(key);
709
+ if (timer) {
710
+ clearTimeout(timer);
711
+ closeTimers.delete(key);
712
+ }
713
+ };
714
+ const cancelAllTimers = () => {
715
+ clearOpenTimer();
716
+ for (const timer of closeTimers.values()) clearTimeout(timer);
717
+ closeTimers.clear();
718
+ };
719
+ vue.onBeforeUnmount(cancelAllTimers);
720
+ const flyoutTriggers = /* @__PURE__ */ new Map();
721
+ const flyoutPanels = /* @__PURE__ */ new Map();
722
+ const setFlyoutRef = (map, key, el) => {
723
+ if (el instanceof HTMLElement) map.set(key, el);
724
+ else map.delete(key);
725
+ };
726
+ const positionFlyout = (key) => {
727
+ const trigger = flyoutTriggers.get(key);
728
+ const panel = flyoutPanels.get(key);
729
+ if (!trigger || !panel || typeof document === "undefined") return;
730
+ const rect = trigger.getBoundingClientRect();
731
+ const dir = getComputedStyle(trigger).direction;
732
+ panel.style.position = "fixed";
733
+ panel.style.top = `${props.collapsed ? rect.top : rect.bottom}px`;
734
+ if (dir === "rtl") {
735
+ panel.style.left = "";
736
+ panel.style.right = `${window.innerWidth - (props.collapsed ? rect.left : rect.right)}px`;
737
+ } else {
738
+ panel.style.right = "";
739
+ panel.style.left = `${props.collapsed ? rect.right : rect.left}px`;
740
+ }
741
+ };
742
+ vue.onBeforeUnmount(() => {
743
+ detachViewportListeners();
744
+ flyoutTriggers.clear();
745
+ flyoutPanels.clear();
746
+ });
747
+ const isolateAtDepth = (key) => {
748
+ const depth = core.findNavPath(props.items, key).length - 1;
749
+ const isSameDepthOther = (k) => k !== key && core.findNavPath(props.items, k).length - 1 === depth;
750
+ for (const state of [hoveredBranches, clickedBranches, focusedBranches]) {
751
+ if (state.value.some(isSameDepthOther)) {
752
+ state.value = state.value.filter((k) => !isSameDepthOther(k));
753
+ }
754
+ }
755
+ };
756
+ const scheduleOpen = (key) => {
757
+ clearOpenTimer();
758
+ openTimer = setTimeout(() => {
759
+ openTimer = null;
760
+ isolateAtDepth(key);
761
+ setBranchInteraction(hoveredBranches, key, true);
762
+ }, HOVER_OPEN_DELAY);
763
+ };
764
+ const scheduleClose = (key) => {
765
+ cancelClose(key);
766
+ closeTimers.set(
767
+ key,
768
+ setTimeout(() => {
769
+ closeTimers.delete(key);
770
+ setBranchInteraction(hoveredBranches, key, false);
771
+ }, HOVER_CLOSE_DELAY)
772
+ );
773
+ };
663
774
  const closeHorizontalMenus = () => {
664
- if (props.orientation !== "horizontal") return;
775
+ if (!flyoutMode.value) return;
776
+ cancelAllTimers();
665
777
  hovered.value = null;
666
778
  hoveredBranches.value = [];
667
779
  focusedBranches.value = [];
780
+ clickedBranches.value = [];
668
781
  };
669
782
  const select = (node) => {
670
783
  if (node.disabled) return;
671
784
  closeHorizontalMenus();
672
785
  emit("select", node.key, node);
673
786
  };
787
+ const toggleFlyout = (key) => {
788
+ cancelAllTimers();
789
+ const current = clickedBranches.value;
790
+ if (current.includes(key)) {
791
+ clickedBranches.value = current.filter((k) => k !== key);
792
+ setBranchInteraction(hoveredBranches, key, false);
793
+ setBranchInteraction(focusedBranches, key, false);
794
+ } else {
795
+ isolateAtDepth(key);
796
+ clickedBranches.value = [...clickedBranches.value, key];
797
+ }
798
+ };
799
+ const openFlyout = (key) => {
800
+ cancelAllTimers();
801
+ isolateAtDepth(key);
802
+ setBranchInteraction(focusedBranches, key, true);
803
+ };
674
804
  const setBranchInteraction = (state, key, enabled) => {
675
805
  const current = state.value;
676
806
  if (enabled) {
@@ -688,8 +818,43 @@ var IrisNavMenu = vue.defineComponent({
688
818
  const horizontalBranchVisible = (key, depth) => {
689
819
  const hoveredAtDepth = interactionKeyAtDepth(hoveredBranches.value, depth);
690
820
  if (hoveredAtDepth) return hoveredAtDepth === key;
821
+ const clickedAtDepth = interactionKeyAtDepth(clickedBranches.value, depth);
822
+ if (clickedAtDepth) return clickedAtDepth === key;
691
823
  return interactionKeyAtDepth(focusedBranches.value, depth) === key;
692
824
  };
825
+ const shownFlyoutKeys = vue.computed(() => {
826
+ if (!flyoutMode.value) return [];
827
+ const keys = [];
828
+ for (const node of tree.value) {
829
+ if (core.isBranch(node) && horizontalBranchVisible(node.key, 0)) keys.push(node.key);
830
+ }
831
+ return keys;
832
+ });
833
+ const repositionOpenFlyouts = () => {
834
+ for (const key of shownFlyoutKeys.value) positionFlyout(key);
835
+ };
836
+ let viewportListenersActive = false;
837
+ const attachViewportListeners = () => {
838
+ if (viewportListenersActive || typeof document === "undefined") return;
839
+ viewportListenersActive = true;
840
+ document.addEventListener("scroll", repositionOpenFlyouts, true);
841
+ window.addEventListener("resize", repositionOpenFlyouts);
842
+ };
843
+ const detachViewportListeners = () => {
844
+ if (!viewportListenersActive || typeof document === "undefined") return;
845
+ viewportListenersActive = false;
846
+ document.removeEventListener("scroll", repositionOpenFlyouts, true);
847
+ window.removeEventListener("resize", repositionOpenFlyouts);
848
+ };
849
+ vue.watch(
850
+ shownFlyoutKeys,
851
+ (keys, prev) => {
852
+ if (keys.length > 0 && prev.length === 0) attachViewportListeners();
853
+ else if (keys.length === 0 && prev.length > 0) detachViewportListeners();
854
+ for (const key of keys) positionFlyout(key);
855
+ },
856
+ { flush: "post" }
857
+ );
693
858
  const itemClass = (opts) => {
694
859
  const depth = Math.min(9, opts.depth);
695
860
  return [
@@ -712,81 +877,47 @@ var IrisNavMenu = vue.defineComponent({
712
877
  },
713
878
  String(node.badge)
714
879
  ) : null;
715
- const renderCollapsed = () => tree.value.map((node) => {
880
+ const renderItem = (node, depth) => {
716
881
  const branch = core.isBranch(node);
717
- const active = branch ? activePath.value.includes(node.key) : node.key === props.activeKey;
718
- return vue.h(
882
+ const horizontal = flyoutMode.value;
883
+ const open = expanded.value.includes(node.key);
884
+ const shown = horizontal ? horizontalBranchVisible(node.key, depth) : open;
885
+ const active = node.key === props.activeKey;
886
+ const trail = branch && !active && activePath.value.includes(node.key);
887
+ const rowHovered = hovered.value === node.key || horizontal && shown;
888
+ const arrowReversed = branch && !node.disabled && (active || trail || shown || rowHovered);
889
+ const isFlyoutRoot = horizontal && branch && depth === 0;
890
+ const row = vue.h(
719
891
  "button",
720
892
  {
721
- key: node.key,
722
893
  type: "button",
894
+ ref: (el) => {
895
+ if (isFlyoutRoot) setFlyoutRef(flyoutTriggers, node.key, el);
896
+ },
723
897
  class: [
724
898
  ...itemClass({
725
- depth: 0,
899
+ depth,
726
900
  active,
727
- trail: false,
728
- hovered: hovered.value === node.key,
729
- open: false,
901
+ trail,
902
+ hovered: rowHovered,
903
+ open: branch ? shown : false,
730
904
  disabled: !!node.disabled,
731
905
  branch
732
906
  }),
733
- "is-collapsed"
907
+ ...props.collapsed && depth === 0 ? ["is-collapsed"] : []
734
908
  ],
735
909
  "data-iris-nav-item": "",
736
910
  "data-key": node.key,
737
- "data-active": active ? "true" : void 0,
738
- "data-branch": branch ? "true" : void 0,
739
- "data-hovered": hovered.value === node.key ? "true" : void 0,
740
- disabled: node.disabled,
741
- title: node.title,
742
- "aria-label": branch ? `${node.title} (section)` : node.title,
743
- "aria-current": active && !branch ? "page" : void 0,
744
- onMouseenter: () => {
745
- if (!node.disabled) hovered.value = node.key;
746
- },
747
- onMouseleave: (event) => {
748
- if (hovered.value === node.key && !keepsPointerInside(event.currentTarget, event)) {
749
- hovered.value = null;
750
- }
751
- },
752
- onClick: () => select(branch ? core.firstLeaf(node) : node)
753
- },
754
- [
755
- iconNode(node, 20) ?? vue.h("span", { class: "iris-nav-menu-fallback-icon" }, node.title.charAt(0))
756
- ]
757
- );
758
- });
759
- const renderItem = (node, depth) => {
760
- const branch = core.isBranch(node);
761
- const open = expanded.value.includes(node.key);
762
- const horizontal = props.orientation === "horizontal";
763
- const shown = horizontal ? horizontalBranchVisible(node.key, depth) : open;
764
- const active = node.key === props.activeKey;
765
- const trail = branch && !active && activePath.value.includes(node.key);
766
- const arrowReversed = branch && !node.disabled && (active || trail || shown || hovered.value === node.key);
767
- const row = vue.h(
768
- "button",
769
- {
770
- type: "button",
771
- class: itemClass({
772
- depth,
773
- active,
774
- trail,
775
- hovered: hovered.value === node.key,
776
- open: branch ? shown : false,
777
- disabled: !!node.disabled,
778
- branch
779
- }),
780
- "data-iris-nav-item": "",
781
- "data-key": node.key,
782
911
  "data-branch": branch ? "true" : void 0,
783
912
  "data-active": active ? "true" : void 0,
784
913
  "data-active-trail": trail ? "true" : void 0,
785
914
  "data-open": branch && shown ? "true" : void 0,
786
- "data-hovered": hovered.value === node.key ? "true" : void 0,
915
+ "data-hovered": rowHovered ? "true" : void 0,
787
916
  "data-depth": String(depth),
788
917
  "data-orientation": props.orientation,
789
918
  disabled: node.disabled,
919
+ title: props.collapsed ? node.title : void 0,
920
+ "aria-label": props.collapsed ? branch ? `${node.title} (section)` : node.title : void 0,
790
921
  "aria-expanded": branch ? shown ? "true" : "false" : void 0,
791
922
  "aria-current": active ? "page" : void 0,
792
923
  onMouseenter: () => {
@@ -797,7 +928,17 @@ var IrisNavMenu = vue.defineComponent({
797
928
  hovered.value = null;
798
929
  }
799
930
  },
800
- onClick: () => branch ? toggle(node.key) : select(node)
931
+ onClick: () => {
932
+ if (props.collapsed && branch) {
933
+ select(core.firstLeaf(node));
934
+ } else if (horizontal && branch) {
935
+ toggleFlyout(node.key);
936
+ } else if (branch) {
937
+ toggle(node.key);
938
+ } else {
939
+ select(node);
940
+ }
941
+ }
801
942
  },
802
943
  [
803
944
  iconNode(node),
@@ -809,54 +950,49 @@ var IrisNavMenu = vue.defineComponent({
809
950
  node.title
810
951
  ),
811
952
  badgeNode(node),
812
- branch ? vue.h(IrisIcon, {
813
- name: horizontal && depth === 0 ? "chevron-down" : "chevron-right",
953
+ branch && !(props.collapsed && depth === 0) ? vue.h(IrisIcon, {
954
+ name: horizontal && depth === 0 && !props.collapsed ? "chevron-down" : "chevron-right",
814
955
  size: 16,
815
956
  class: "iris-nav-menu-arrow",
816
957
  "data-reversed": arrowReversed ? "true" : void 0
817
958
  }) : null
818
959
  ]
819
960
  );
820
- if (!branch || !horizontal && !open)
821
- return vue.h(
822
- "div",
823
- {
824
- key: node.key,
825
- "data-iris-nav-group": branch ? "" : void 0,
826
- class: ["iris-nav-menu-group", `iris-nav-menu-group--depth-${Math.min(9, depth)}`]
827
- },
828
- [row]
829
- );
830
961
  return vue.h(
831
962
  "div",
832
963
  {
833
964
  key: node.key,
834
- "data-iris-nav-group": "",
835
- "data-open": shown ? "true" : void 0,
965
+ "data-iris-nav-group": branch ? "" : void 0,
966
+ "data-open": branch && shown ? "true" : void 0,
836
967
  "data-depth": String(depth),
837
968
  class: ["iris-nav-menu-group", `iris-nav-menu-group--depth-${Math.min(9, depth)}`],
838
- ...horizontal ? {
969
+ ...horizontal && branch ? {
839
970
  onMouseenter: (event) => {
840
- if (!keepsPointerInside(event.currentTarget, event)) {
841
- setBranchInteraction(hoveredBranches, node.key, true);
971
+ if (!node.disabled && !keepsPointerInside(event.currentTarget, event)) {
972
+ cancelClose(node.key);
973
+ scheduleOpen(node.key);
842
974
  }
843
975
  },
844
976
  onMouseover: (event) => {
845
- if (!keepsPointerInside(event.currentTarget, event)) {
846
- setBranchInteraction(hoveredBranches, node.key, true);
977
+ if (!node.disabled && !keepsPointerInside(event.currentTarget, event)) {
978
+ cancelClose(node.key);
979
+ scheduleOpen(node.key);
847
980
  }
848
981
  },
849
982
  onMouseout: (event) => {
850
- if (!keepsPointerInside(event.currentTarget, event)) {
851
- setBranchInteraction(hoveredBranches, node.key, false);
983
+ if (!node.disabled && !keepsPointerInside(event.currentTarget, event)) {
984
+ scheduleClose(node.key);
852
985
  }
853
986
  },
854
987
  onMouseleave: (event) => {
855
- if (!keepsPointerInside(event.currentTarget, event)) {
856
- setBranchInteraction(hoveredBranches, node.key, false);
988
+ if (!node.disabled && !keepsPointerInside(event.currentTarget, event)) {
989
+ scheduleClose(node.key);
857
990
  }
858
991
  },
859
- onFocusin: () => setBranchInteraction(focusedBranches, node.key, true),
992
+ onFocusin: () => {
993
+ if (suppressFocusOpen) return;
994
+ setBranchInteraction(focusedBranches, node.key, true);
995
+ },
860
996
  onFocusout: (event) => {
861
997
  const group = event.currentTarget;
862
998
  const next = event.relatedTarget;
@@ -868,19 +1004,45 @@ var IrisNavMenu = vue.defineComponent({
868
1004
  },
869
1005
  [
870
1006
  row,
871
- vue.h(
1007
+ branch ? vue.h(
872
1008
  "div",
873
1009
  {
874
1010
  class: "iris-nav-menu-children",
875
1011
  "data-iris-nav-children": "",
1012
+ ref: (el) => {
1013
+ if (isFlyoutRoot) setFlyoutRef(flyoutPanels, node.key, el);
1014
+ },
876
1015
  role: "group",
877
- "aria-hidden": horizontal && !shown ? "true" : void 0
1016
+ "aria-hidden": shown ? void 0 : "true"
878
1017
  },
879
- (node.children ?? []).map((child) => renderItem(child, depth + 1))
880
- )
1018
+ [
1019
+ vue.h(
1020
+ "div",
1021
+ { class: "iris-nav-menu-children-inner" },
1022
+ (node.children ?? []).map((child) => renderItem(child, depth + 1))
1023
+ )
1024
+ ]
1025
+ ) : null
881
1026
  ]
882
1027
  );
883
1028
  };
1029
+ const rootRef = vue.shallowRef(null);
1030
+ vue.watch(
1031
+ [() => props.activeKey, () => props.items],
1032
+ () => {
1033
+ if (!props.activeKey) return;
1034
+ const root = rootRef.value;
1035
+ if (!root) return;
1036
+ const el = root.querySelector('[data-iris-nav-item][data-active="true"]');
1037
+ if (!el || typeof el.scrollIntoView !== "function") return;
1038
+ let behavior = "auto";
1039
+ if (typeof window !== "undefined" && typeof window.matchMedia === "function" && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
1040
+ behavior = "smooth";
1041
+ }
1042
+ el.scrollIntoView({ block: "nearest", inline: "nearest", behavior });
1043
+ },
1044
+ { flush: "post" }
1045
+ );
884
1046
  const onKeydown = (e) => {
885
1047
  const root = e.currentTarget;
886
1048
  const buttons = Array.from(root.querySelectorAll("[data-iris-nav-item]")).filter(
@@ -889,15 +1051,33 @@ var IrisNavMenu = vue.defineComponent({
889
1051
  if (buttons.length === 0) return;
890
1052
  const idx = buttons.indexOf(document.activeElement);
891
1053
  const focusAt = (i) => buttons[(i + buttons.length) % buttons.length]?.focus();
1054
+ const key = idx >= 0 ? buttons[idx]?.getAttribute("data-key") : void 0;
1055
+ const node = key ? core.findNavNode(props.items, key) : void 0;
1056
+ const depth = idx >= 0 ? Number(buttons[idx]?.getAttribute("data-depth") ?? 0) : 0;
892
1057
  switch (e.key) {
893
- case "ArrowDown":
1058
+ case "ArrowDown": {
894
1059
  e.preventDefault();
1060
+ if (flyoutMode.value && node && core.isBranch(node)) {
1061
+ openFlyout(key);
1062
+ const group = buttons[idx].closest("[data-iris-nav-group]");
1063
+ void vue.nextTick(() => {
1064
+ group?.querySelector("[data-iris-nav-children] [data-iris-nav-item]")?.focus();
1065
+ });
1066
+ return;
1067
+ }
895
1068
  focusAt(idx < 0 ? 0 : idx + 1);
896
1069
  return;
897
- case "ArrowUp":
1070
+ }
1071
+ case "ArrowUp": {
898
1072
  e.preventDefault();
1073
+ if (flyoutMode.value && node && core.isBranch(node) && horizontalBranchVisible(key, depth)) {
1074
+ setBranchInteraction(focusedBranches, key, false);
1075
+ setBranchInteraction(clickedBranches, key, false);
1076
+ return;
1077
+ }
899
1078
  focusAt(idx < 0 ? buttons.length - 1 : idx - 1);
900
1079
  return;
1080
+ }
901
1081
  case "Home":
902
1082
  e.preventDefault();
903
1083
  buttons[0]?.focus();
@@ -906,23 +1086,56 @@ var IrisNavMenu = vue.defineComponent({
906
1086
  e.preventDefault();
907
1087
  buttons[buttons.length - 1]?.focus();
908
1088
  return;
1089
+ case "Escape": {
1090
+ if (flyoutMode.value) {
1091
+ const openKeys = [
1092
+ ...hoveredBranches.value,
1093
+ ...clickedBranches.value,
1094
+ ...focusedBranches.value
1095
+ ];
1096
+ if (openKeys.length > 0) {
1097
+ e.preventDefault();
1098
+ const openTop = openKeys.find((k) => core.findNavPath(props.items, k).length === 1);
1099
+ closeHorizontalMenus();
1100
+ if (openTop) {
1101
+ suppressFocusOpen = true;
1102
+ buttons.find((b) => b.getAttribute("data-key") === openTop)?.focus();
1103
+ void vue.nextTick(() => {
1104
+ suppressFocusOpen = false;
1105
+ });
1106
+ }
1107
+ }
1108
+ }
1109
+ return;
1110
+ }
909
1111
  }
910
- if (props.collapsed || idx < 0) return;
911
- const key = buttons[idx]?.getAttribute("data-key");
912
- if (!key) return;
913
- const node = core.findNavNode(props.items, key);
914
- if (e.key === "ArrowRight" && node && core.isBranch(node)) {
1112
+ if (props.collapsed || idx < 0 || !key || !node) return;
1113
+ if (e.key === "ArrowRight") {
915
1114
  e.preventDefault();
916
- if (!expanded.value.includes(key)) toggle(key);
917
- else focusAt(idx + 1);
1115
+ if (core.isBranch(node)) {
1116
+ if (flyoutMode.value || !expanded.value.includes(key)) {
1117
+ if (flyoutMode.value) openFlyout(key);
1118
+ else toggle(key);
1119
+ const group = buttons[idx].closest("[data-iris-nav-group]");
1120
+ void vue.nextTick(() => {
1121
+ group?.querySelector("[data-iris-nav-children] [data-iris-nav-item]")?.focus();
1122
+ });
1123
+ } else {
1124
+ focusAt(idx + 1);
1125
+ }
1126
+ }
918
1127
  } else if (e.key === "ArrowLeft") {
919
- if (node && core.isBranch(node) && expanded.value.includes(key)) {
920
- e.preventDefault();
1128
+ e.preventDefault();
1129
+ if (flyoutMode.value && core.isBranch(node)) {
1130
+ setBranchInteraction(focusedBranches, key, false);
1131
+ setBranchInteraction(clickedBranches, key, false);
1132
+ return;
1133
+ }
1134
+ if (core.isBranch(node) && expanded.value.includes(key)) {
921
1135
  toggle(key);
922
1136
  } else {
923
1137
  const parentKey = core.findNavPath(props.items, key).at(-2)?.key;
924
1138
  if (parentKey) {
925
- e.preventDefault();
926
1139
  buttons.find((b) => b.getAttribute("data-key") === parentKey)?.focus();
927
1140
  }
928
1141
  }
@@ -937,6 +1150,9 @@ var IrisNavMenu = vue.defineComponent({
937
1150
  "nav",
938
1151
  {
939
1152
  ...attrs,
1153
+ ref: (el) => {
1154
+ rootRef.value = el ?? null;
1155
+ },
940
1156
  class: [...menuClass, attrs.class],
941
1157
  "data-iris-nav-menu": "",
942
1158
  "data-collapsed": props.collapsed ? "true" : void 0,
@@ -945,7 +1161,7 @@ var IrisNavMenu = vue.defineComponent({
945
1161
  onKeydown,
946
1162
  style: attrs.style
947
1163
  },
948
- props.collapsed ? renderCollapsed() : tree.value.map((node) => renderItem(node, 0))
1164
+ tree.value.map((node) => renderItem(node, 0))
949
1165
  );
950
1166
  }
951
1167
  });
@@ -1015,11 +1231,11 @@ var IrisBreadcrumb = vue.defineComponent({
1015
1231
  style: {
1016
1232
  display: "inline-flex",
1017
1233
  alignItems: "center",
1018
- gap: "6px",
1234
+ gap: "var(--iris-space-xs, 8px)",
1019
1235
  margin: "0",
1020
1236
  padding: "0",
1021
1237
  listStyle: "none",
1022
- fontSize: "14px"
1238
+ fontSize: "var(--iris-font-size-md, 14px)"
1023
1239
  }
1024
1240
  },
1025
1241
  items
@@ -1352,6 +1568,7 @@ var IrisDropdownTrigger = vue.defineComponent({
1352
1568
  function useFloating(options) {
1353
1569
  const x = vue.ref(0);
1354
1570
  const y = vue.ref(0);
1571
+ const positioned = vue.ref(false);
1355
1572
  const strategy = vue.ref(options.strategy ?? "absolute");
1356
1573
  const finalPlacement = vue.ref(options.placement ?? "bottom");
1357
1574
  const arrowX = vue.ref();
@@ -1397,6 +1614,7 @@ function useFloating(options) {
1397
1614
  if (token !== epoch) return;
1398
1615
  x.value = result.x;
1399
1616
  y.value = result.y;
1617
+ positioned.value = true;
1400
1618
  finalPlacement.value = result.placement;
1401
1619
  const arrowData = result.middlewareData.arrow;
1402
1620
  arrowX.value = arrowData?.x;
@@ -1409,6 +1627,7 @@ function useFloating(options) {
1409
1627
  cleanupAutoUpdate = null;
1410
1628
  epoch++;
1411
1629
  if (isOpen && anchorEl && floatingEl) {
1630
+ positioned.value = false;
1412
1631
  cleanupAutoUpdate = dom.autoUpdate(anchorEl, floatingEl, () => {
1413
1632
  void update();
1414
1633
  });
@@ -1435,7 +1654,11 @@ function useFloating(options) {
1435
1654
  top: "0",
1436
1655
  left: "0",
1437
1656
  transform: `translate3d(${Math.round(x.value)}px, ${Math.round(y.value)}px, 0)`,
1438
- width: "max-content"
1657
+ width: "max-content",
1658
+ // Hidden until the first positioning cycle lands — prevents a flash at
1659
+ // the viewport origin. visibility (not display:none) keeps the panel
1660
+ // measurable so Floating UI can compute its real coordinates.
1661
+ visibility: positioned.value ? "visible" : "hidden"
1439
1662
  }));
1440
1663
  return { x, y, strategy, finalPlacement, floatingStyles, update, arrowX, arrowY, arrowSide };
1441
1664
  }
@@ -1454,6 +1677,7 @@ function useDismiss(options) {
1454
1677
  const onPointerDown = (event) => {
1455
1678
  if (!options.enabled.value) return;
1456
1679
  if (isExcluded(event.target)) return;
1680
+ if (options.excludePredicate?.(event.target)) return;
1457
1681
  options.onDismiss();
1458
1682
  };
1459
1683
  const onKeyDown = (event) => {
@@ -1601,12 +1825,12 @@ var IrisDropdownMenu = vue.defineComponent({
1601
1825
  onKeydown: onKeyDown,
1602
1826
  style: {
1603
1827
  ...floatingStyles.value,
1604
- background: "var(--iris-surface)",
1828
+ background: "var(--iris-surface-floating)",
1605
1829
  color: "var(--iris-foreground)",
1606
1830
  border: "1px solid var(--iris-border)",
1607
1831
  borderRadius: "var(--iris-radius-md)",
1608
1832
  padding: "var(--iris-padding-sm)",
1609
- boxShadow: "0 8px 24px -8px rgba(0, 0, 0, 0.16), 0 4px 8px -2px rgba(0, 0, 0, 0.08)",
1833
+ boxShadow: "var(--iris-shadow-lg)",
1610
1834
  minWidth: "160px",
1611
1835
  outline: "none",
1612
1836
  zIndex: "1000",
@@ -1671,14 +1895,14 @@ var IrisDropdownItem = vue.defineComponent({
1671
1895
  display: "flex",
1672
1896
  alignItems: "center",
1673
1897
  gap: "var(--iris-gap-sm)",
1674
- padding: "6px var(--iris-padding-md)",
1898
+ padding: "var(--iris-padding-sm, 6px) var(--iris-padding-md)",
1675
1899
  borderRadius: "var(--iris-radius-sm)",
1676
1900
  cursor: props.disabled ? "not-allowed" : "pointer",
1677
1901
  opacity: props.disabled ? "0.5" : "1",
1678
1902
  background: hovered.value && !props.disabled ? "var(--iris-surface-hover)" : "transparent",
1679
1903
  color: "inherit",
1680
1904
  outline: "none",
1681
- fontSize: "14px",
1905
+ fontSize: "var(--iris-font-size-md, 14px)",
1682
1906
  transition: "background-color 80ms ease",
1683
1907
  ...attrs.style ?? {}
1684
1908
  }
@@ -1953,12 +2177,12 @@ var IrisAdminTabs = vue.defineComponent({
1953
2177
  style: {
1954
2178
  display: "inline-flex",
1955
2179
  alignItems: "center",
1956
- gap: "6px",
2180
+ gap: "var(--iris-space-xs, 8px)",
1957
2181
  border: "none",
1958
2182
  background: "transparent",
1959
2183
  color: "inherit",
1960
2184
  font: "inherit",
1961
- fontSize: "13px",
2185
+ fontSize: "var(--iris-font-size-sm, 13px)",
1962
2186
  cursor: "pointer",
1963
2187
  padding: "0"
1964
2188
  },
@@ -2008,7 +2232,7 @@ var IrisAdminTabs = vue.defineComponent({
2008
2232
  display: "inline-flex",
2009
2233
  alignItems: "center",
2010
2234
  gap: "4px",
2011
- padding: "5px 8px 5px 10px",
2235
+ padding: "var(--iris-padding-sm, 6px) var(--iris-space-xs, 8px) var(--iris-padding-sm, 6px) var(--iris-space-sm, 12px)",
2012
2236
  borderRadius: "var(--iris-radius-md, 6px)",
2013
2237
  border: `1px solid ${active ? "var(--iris-primary)" : "var(--iris-border)"}`,
2014
2238
  background: active ? "var(--iris-primary)" : "var(--iris-surface)",
@@ -2024,27 +2248,23 @@ var IrisAdminTabs = vue.defineComponent({
2024
2248
  default: () => [
2025
2249
  vue.h(
2026
2250
  IrisDropdownTrigger,
2027
- { "aria-label": tr("admin.tabActions") },
2028
2251
  {
2029
- default: () => vue.h(
2030
- "span",
2031
- {
2032
- "data-iris-tab-actions": "",
2033
- style: {
2034
- display: "inline-flex",
2035
- alignItems: "center",
2036
- justifyContent: "center",
2037
- width: "28px",
2038
- height: "28px",
2039
- borderRadius: "var(--iris-radius-md, 6px)",
2040
- border: "1px solid var(--iris-border)",
2041
- background: "var(--iris-surface)",
2042
- color: "var(--iris-foreground)",
2043
- cursor: "pointer"
2044
- }
2045
- },
2046
- [vue.h(IrisIcon, { name: "more-horizontal", size: 16 })]
2047
- )
2252
+ "aria-label": tr("admin.tabActions"),
2253
+ style: {
2254
+ display: "inline-flex",
2255
+ alignItems: "center",
2256
+ justifyContent: "center",
2257
+ width: "28px",
2258
+ height: "28px",
2259
+ borderRadius: "var(--iris-radius-md, 6px)",
2260
+ border: "1px solid var(--iris-border)",
2261
+ background: "var(--iris-surface)",
2262
+ color: "var(--iris-foreground)",
2263
+ cursor: "pointer"
2264
+ }
2265
+ },
2266
+ {
2267
+ default: () => vue.h(IrisIcon, { name: "more-horizontal", size: 16 })
2048
2268
  }
2049
2269
  ),
2050
2270
  vue.h(IrisDropdownMenu, null, {
@@ -2073,8 +2293,8 @@ var IrisAdminTabs = vue.defineComponent({
2073
2293
  style: {
2074
2294
  display: "flex",
2075
2295
  alignItems: "center",
2076
- gap: "6px",
2077
- padding: "6px 10px",
2296
+ gap: "var(--iris-space-xs, 8px)",
2297
+ padding: "var(--iris-space-xs, 8px) var(--iris-space-sm, 12px)",
2078
2298
  borderBottom: "1px solid var(--iris-border)",
2079
2299
  background: "var(--iris-background)",
2080
2300
  ...attrs.style ?? {}
@@ -2104,7 +2324,7 @@ var IrisAdminTabs = vue.defineComponent({
2104
2324
  orientation: "horizontal",
2105
2325
  style: {
2106
2326
  alignItems: "center",
2107
- gap: "6px",
2327
+ gap: "var(--iris-space-xs, 8px)",
2108
2328
  width: "max-content"
2109
2329
  }
2110
2330
  },
@@ -2238,12 +2458,12 @@ var IrisAdminLayout = vue.defineComponent({
2238
2458
  style: {
2239
2459
  display: "flex",
2240
2460
  alignItems: "center",
2241
- gap: "10px",
2461
+ gap: "var(--iris-space-sm, 12px)",
2242
2462
  height: "52px",
2243
- padding: "0 16px",
2463
+ padding: "0 var(--iris-space-md, 16px)",
2244
2464
  color: "var(--iris-foreground)",
2245
2465
  fontWeight: "700",
2246
- fontSize: "16px",
2466
+ fontSize: "var(--iris-font-size-lg, 16px)",
2247
2467
  whiteSpace: "nowrap",
2248
2468
  overflow: "hidden",
2249
2469
  flexShrink: 0