@aviala-design/spiral 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -110,6 +110,10 @@ __export(index_exports, {
110
110
  NavigationGroup: () => NavigationGroup,
111
111
  NavigationItem: () => NavigationItem,
112
112
  NavigationItemGroup: () => NavigationItemGroup,
113
+ NavigationItemMenu: () => NavigationItemMenu,
114
+ NavigationItemMenuContent: () => NavigationItemMenuContent,
115
+ NavigationItemMenuItem: () => NavigationItemMenuItem,
116
+ NavigationItemMenuTrigger: () => NavigationItemMenuTrigger,
113
117
  NavigationSection: () => NavigationSection,
114
118
  PALETTE_HUE_FAMILIES: () => import_tokens2.PALETTE_HUE_FAMILIES,
115
119
  Pagehead: () => Pagehead,
@@ -7005,6 +7009,7 @@ var ListItem = (0, import_react45.forwardRef)(
7005
7009
  ListItem.displayName = "ListItem";
7006
7010
 
7007
7011
  // src/components/navigation.tsx
7012
+ var PopoverPrimitive7 = __toESM(require("@radix-ui/react-popover"), 1);
7008
7013
  var import_react_slot5 = require("@radix-ui/react-slot");
7009
7014
  var import_react46 = require("react");
7010
7015
  var import_jsx_runtime45 = require("react/jsx-runtime");
@@ -7117,6 +7122,9 @@ function measureIndicatorFromElement(indicatorEl, groupEl) {
7117
7122
  visible: indicatorEl.dataset.visible === "true"
7118
7123
  };
7119
7124
  }
7125
+ function easeOutCubic(t) {
7126
+ return 1 - Math.pow(1 - t, 3);
7127
+ }
7120
7128
  function metricsApproxEqual2(a, b, epsilon = 0.5) {
7121
7129
  return Math.abs(a.x - b.x) < epsilon && Math.abs(a.y - b.y) < epsilon && Math.abs(a.width - b.width) < epsilon && Math.abs(a.height - b.height) < epsilon && a.visible === b.visible;
7122
7130
  }
@@ -7202,73 +7210,63 @@ function useNavigationIndicator(groupRef, direction, layoutKey) {
7202
7210
  isAnimatingRef.current = false;
7203
7211
  syncIndicator(metrics, true);
7204
7212
  }, [measureIndicator, syncIndicator]);
7205
- const startExpandCollapseTracking = (0, import_react46.useCallback)(
7206
- (expanded) => {
7207
- const group = groupRef.current;
7208
- const el = indicatorRef.current;
7209
- if (!group || !el) return;
7210
- cancelLayoutTracking();
7211
- animationRef.current?.cancel();
7212
- animationRef.current = null;
7213
- isAnimatingRef.current = false;
7214
- const prefersReducedMotion2 = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
7215
- if (prefersReducedMotion2) {
7216
- const metrics = measureIndicator();
7217
- if (metrics) syncIndicator(metrics, true);
7218
- return;
7219
- }
7220
- const { durationMs, easing } = getNavigationAnimationTiming(group);
7221
- const startMetrics = measureIndicatorFromElement(el, group);
7222
- if (!expanded) {
7223
- const next = measureIndicator();
7224
- if (!next?.visible) {
7225
- syncIndicator(next ?? startMetrics, true);
7226
- return;
7227
- }
7228
- const generation = ++animationGenerationRef.current;
7229
- isLayoutTrackingRef.current = true;
7230
- runIndicatorAnimation(
7231
- el,
7232
- group,
7233
- startMetrics,
7234
- next,
7235
- durationMs,
7236
- easing,
7237
- generation,
7238
- animationGenerationRef,
7239
- animationRef,
7240
- isAnimatingRef,
7241
- syncIndicator
7242
- );
7243
- window.setTimeout(() => {
7244
- if (!isLayoutTrackingRef.current) return;
7245
- isLayoutTrackingRef.current = false;
7246
- const final = measureIndicator();
7247
- if (final) syncIndicator(final, true);
7248
- }, durationMs);
7213
+ const startExpandCollapseTracking = (0, import_react46.useCallback)(() => {
7214
+ const group = groupRef.current;
7215
+ const el = indicatorRef.current;
7216
+ if (!group || !el) return;
7217
+ cancelLayoutTracking();
7218
+ animationRef.current?.cancel();
7219
+ animationRef.current = null;
7220
+ isAnimatingRef.current = false;
7221
+ const prefersReducedMotion2 = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
7222
+ if (prefersReducedMotion2) {
7223
+ const metrics = measureIndicator();
7224
+ if (metrics) syncIndicator(metrics, true);
7225
+ return;
7226
+ }
7227
+ const { durationMs } = getNavigationAnimationTiming(group);
7228
+ const startRect = el.getBoundingClientRect();
7229
+ const startVisible = el.dataset.visible === "true";
7230
+ const startTime = performance.now();
7231
+ isLayoutTrackingRef.current = true;
7232
+ const finish = () => {
7233
+ layoutTrackingFrameRef.current = null;
7234
+ isLayoutTrackingRef.current = false;
7235
+ const final = measureIndicator();
7236
+ if (final) syncIndicator(final, true);
7237
+ };
7238
+ const tick = (now) => {
7239
+ if (!isLayoutTrackingRef.current) return;
7240
+ const progress = Math.min(1, (now - startTime) / durationMs);
7241
+ if (progress >= 1) {
7242
+ finish();
7249
7243
  return;
7250
7244
  }
7251
- isLayoutTrackingRef.current = true;
7252
- applyIndicatorMetrics(el, startMetrics, true);
7253
- metricsRef.current = startMetrics;
7254
- const deadline = performance.now() + durationMs;
7255
- const tick = () => {
7256
- if (!isLayoutTrackingRef.current) return;
7257
- const metrics = measureIndicator();
7258
- if (metrics) syncIndicator(metrics, true);
7259
- if (performance.now() < deadline) {
7260
- layoutTrackingFrameRef.current = requestAnimationFrame(tick);
7245
+ const target = measureIndicator();
7246
+ if (target) {
7247
+ if (!startVisible || !target.visible) {
7248
+ syncIndicator(target, true);
7261
7249
  } else {
7262
- layoutTrackingFrameRef.current = null;
7263
- isLayoutTrackingRef.current = false;
7264
- const final = measureIndicator();
7265
- if (final) syncIndicator(final, true);
7250
+ const eased = easeOutCubic(progress);
7251
+ const groupRect = group.getBoundingClientRect();
7252
+ const targetAbsX = groupRect.left + target.x;
7253
+ const targetAbsY = groupRect.top + target.y;
7254
+ syncIndicator(
7255
+ {
7256
+ x: startRect.left + (targetAbsX - startRect.left) * eased - groupRect.left,
7257
+ y: startRect.top + (targetAbsY - startRect.top) * eased - groupRect.top,
7258
+ width: startRect.width + (target.width - startRect.width) * eased,
7259
+ height: startRect.height + (target.height - startRect.height) * eased,
7260
+ visible: true
7261
+ },
7262
+ true
7263
+ );
7266
7264
  }
7267
- };
7265
+ }
7268
7266
  layoutTrackingFrameRef.current = requestAnimationFrame(tick);
7269
- },
7270
- [cancelLayoutTracking, groupRef, measureIndicator, syncIndicator]
7271
- );
7267
+ };
7268
+ layoutTrackingFrameRef.current = requestAnimationFrame(tick);
7269
+ }, [cancelLayoutTracking, groupRef, measureIndicator, syncIndicator]);
7272
7270
  const onIndicatorRef = (0, import_react46.useCallback)((node) => {
7273
7271
  indicatorRef.current = node;
7274
7272
  if (node && metricsRef.current && isInitialMount.current) {
@@ -7328,14 +7326,11 @@ function useNavigationIndicator(groupRef, direction, layoutKey) {
7328
7326
  const group = groupRef.current;
7329
7327
  if (!group) return;
7330
7328
  const observer = new MutationObserver((mutations) => {
7331
- let expandCollapseChange = null;
7332
- for (const mutation of mutations) {
7333
- if (mutation.type === "attributes" && mutation.attributeName === "data-expanded" && mutation.target instanceof HTMLElement) {
7334
- expandCollapseChange = mutation.target.getAttribute("data-expanded") === "true";
7335
- }
7336
- }
7337
- if (expandCollapseChange !== null) {
7338
- startExpandCollapseTracking(expandCollapseChange);
7329
+ const expandCollapseChanged = mutations.some(
7330
+ (mutation) => mutation.type === "attributes" && mutation.attributeName === "data-expanded" && mutation.target instanceof HTMLElement
7331
+ );
7332
+ if (expandCollapseChanged) {
7333
+ startExpandCollapseTracking();
7339
7334
  }
7340
7335
  setActiveRevision((revision) => revision + 1);
7341
7336
  });
@@ -7355,7 +7350,9 @@ function useNavigationIndicator(groupRef, direction, layoutKey) {
7355
7350
  resizeObserver.observe(inner);
7356
7351
  });
7357
7352
  const onTransitionEnd = (event) => {
7358
- if (event.propertyName !== "grid-template-rows") return;
7353
+ if (event.propertyName !== "grid-template-rows" && event.propertyName !== "width") {
7354
+ return;
7355
+ }
7359
7356
  if (!(event.target instanceof HTMLElement)) return;
7360
7357
  if (!event.target.classList.contains("aviala-navigation-item-group")) {
7361
7358
  return;
@@ -7586,6 +7583,225 @@ var NavigationActionsSlot = (0, import_react46.forwardRef)(({ className, ...prop
7586
7583
  }
7587
7584
  ));
7588
7585
  NavigationActionsSlot.displayName = "NavigationActionsSlot";
7586
+ var NAVIGATION_MENU_CLOSE_DELAY_MS = 150;
7587
+ var NavigationItemMenuContext = (0, import_react46.createContext)(null);
7588
+ function useNavigationItemMenuContext(component) {
7589
+ const context = (0, import_react46.useContext)(NavigationItemMenuContext);
7590
+ if (!context) {
7591
+ throw new Error(`${component} must be used within <NavigationItemMenu>`);
7592
+ }
7593
+ return context;
7594
+ }
7595
+ function NavigationItemMenu({
7596
+ value: valueProp,
7597
+ defaultValue,
7598
+ onValueChange,
7599
+ open: openProp,
7600
+ onOpenChange,
7601
+ children
7602
+ }) {
7603
+ const [internalValue, setInternalValue] = (0, import_react46.useState)(defaultValue);
7604
+ const isValueControlled = valueProp !== void 0;
7605
+ const value = isValueControlled ? valueProp : internalValue;
7606
+ const [internalOpen, setInternalOpen] = (0, import_react46.useState)(false);
7607
+ const isOpenControlled = openProp !== void 0;
7608
+ const open = isOpenControlled ? openProp : internalOpen;
7609
+ const closeTimerRef = (0, import_react46.useRef)(null);
7610
+ const openedByHoverRef = (0, import_react46.useRef)(false);
7611
+ const setOpen = (0, import_react46.useCallback)(
7612
+ (nextOpen) => {
7613
+ if (!isOpenControlled) setInternalOpen(nextOpen);
7614
+ onOpenChange?.(nextOpen);
7615
+ },
7616
+ [isOpenControlled, onOpenChange]
7617
+ );
7618
+ const cancelClose = (0, import_react46.useCallback)(() => {
7619
+ if (closeTimerRef.current != null) {
7620
+ window.clearTimeout(closeTimerRef.current);
7621
+ closeTimerRef.current = null;
7622
+ }
7623
+ }, []);
7624
+ const openMenu = (0, import_react46.useCallback)(
7625
+ (viaHover) => {
7626
+ cancelClose();
7627
+ openedByHoverRef.current = viaHover;
7628
+ setOpen(true);
7629
+ },
7630
+ [cancelClose, setOpen]
7631
+ );
7632
+ const scheduleClose = (0, import_react46.useCallback)(() => {
7633
+ cancelClose();
7634
+ closeTimerRef.current = window.setTimeout(() => {
7635
+ closeTimerRef.current = null;
7636
+ setOpen(false);
7637
+ }, NAVIGATION_MENU_CLOSE_DELAY_MS);
7638
+ }, [cancelClose, setOpen]);
7639
+ const select = (0, import_react46.useCallback)(
7640
+ (nextValue) => {
7641
+ if (!isValueControlled) setInternalValue(nextValue);
7642
+ onValueChange?.(nextValue);
7643
+ cancelClose();
7644
+ setOpen(false);
7645
+ },
7646
+ [cancelClose, isValueControlled, onValueChange, setOpen]
7647
+ );
7648
+ (0, import_react46.useEffect)(() => cancelClose, [cancelClose]);
7649
+ const contextValue = (0, import_react46.useMemo)(
7650
+ () => ({
7651
+ value,
7652
+ hasSelection: value != null && value !== "",
7653
+ select,
7654
+ openMenu,
7655
+ scheduleClose,
7656
+ cancelClose,
7657
+ openedByHoverRef
7658
+ }),
7659
+ [cancelClose, openMenu, scheduleClose, select, value]
7660
+ );
7661
+ const handleOpenChange = (0, import_react46.useCallback)(
7662
+ (nextOpen) => {
7663
+ if (nextOpen) openedByHoverRef.current = false;
7664
+ cancelClose();
7665
+ setOpen(nextOpen);
7666
+ },
7667
+ [cancelClose, setOpen]
7668
+ );
7669
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(NavigationItemMenuContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Popover, { open, onOpenChange: handleOpenChange, children }) });
7670
+ }
7671
+ var NavigationItemMenuTrigger = (0, import_react46.forwardRef)(
7672
+ ({
7673
+ className,
7674
+ active,
7675
+ leftIcon,
7676
+ rightIcon,
7677
+ children,
7678
+ ...props
7679
+ }, ref) => {
7680
+ const menu = useNavigationItemMenuContext("NavigationItemMenuTrigger");
7681
+ const isActive = active ?? menu.hasSelection;
7682
+ const mode = navigationItemButtonMode(isActive);
7683
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
7684
+ "span",
7685
+ {
7686
+ className: cn("aviala-navigation-item", className),
7687
+ "data-item-type": "default",
7688
+ "data-active": isActive ? "true" : "false",
7689
+ onMouseEnter: () => menu.openMenu(true),
7690
+ onMouseLeave: menu.scheduleClose,
7691
+ children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(PopoverPrimitive7.Trigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
7692
+ "button",
7693
+ {
7694
+ ref,
7695
+ type: "button",
7696
+ className: cn(
7697
+ buttonVariants({ mode }),
7698
+ "aviala-navigation-item__control whitespace-normal"
7699
+ ),
7700
+ "data-size": "regular",
7701
+ ...props,
7702
+ children: [
7703
+ renderItemIcon3(leftIcon),
7704
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
7705
+ Typography,
7706
+ {
7707
+ level: "text",
7708
+ as: "span",
7709
+ className: "aviala-navigation-item__label",
7710
+ children
7711
+ }
7712
+ ),
7713
+ rightIcon ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "aviala-navigation-item__chevron", children: renderItemIcon3(rightIcon) }) : null
7714
+ ]
7715
+ }
7716
+ ) })
7717
+ }
7718
+ );
7719
+ }
7720
+ );
7721
+ NavigationItemMenuTrigger.displayName = "NavigationItemMenuTrigger";
7722
+ var NavigationItemMenuContent = (0, import_react46.forwardRef)(
7723
+ ({
7724
+ className,
7725
+ side,
7726
+ align = "start",
7727
+ sideOffset = 6,
7728
+ collisionPadding = 8,
7729
+ onOpenAutoFocus,
7730
+ ...props
7731
+ }, ref) => {
7732
+ const direction = useNavigationDirection();
7733
+ const menu = useNavigationItemMenuContext("NavigationItemMenuContent");
7734
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(PopoverPrimitive7.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
7735
+ PopoverPrimitive7.Content,
7736
+ {
7737
+ ref,
7738
+ side: side ?? (direction === "vertical" ? "right" : "bottom"),
7739
+ align,
7740
+ sideOffset,
7741
+ collisionPadding,
7742
+ className: cn("aviala-navigation-menu", className),
7743
+ onMouseEnter: menu.cancelClose,
7744
+ onMouseLeave: menu.scheduleClose,
7745
+ onOpenAutoFocus: (event) => {
7746
+ if (menu.openedByHoverRef.current) event.preventDefault();
7747
+ onOpenAutoFocus?.(event);
7748
+ },
7749
+ ...props
7750
+ }
7751
+ ) });
7752
+ }
7753
+ );
7754
+ NavigationItemMenuContent.displayName = "NavigationItemMenuContent";
7755
+ var NavigationItemMenuItem = (0, import_react46.forwardRef)(
7756
+ ({
7757
+ className,
7758
+ value,
7759
+ selected,
7760
+ leftIcon,
7761
+ rightIcon,
7762
+ disabled,
7763
+ onClick,
7764
+ children,
7765
+ ...props
7766
+ }, ref) => {
7767
+ const menu = useNavigationItemMenuContext("NavigationItemMenuItem");
7768
+ const isSelected = selected ?? menu.value === value;
7769
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
7770
+ "button",
7771
+ {
7772
+ ref,
7773
+ type: "button",
7774
+ disabled,
7775
+ className: cn(
7776
+ "aviala-navigation-menu-item aviala-focus-ring",
7777
+ className
7778
+ ),
7779
+ "data-selected": isSelected ? "true" : "false",
7780
+ "data-disabled": disabled ? "" : void 0,
7781
+ "aria-current": isSelected ? "true" : void 0,
7782
+ onClick: (event) => {
7783
+ onClick?.(event);
7784
+ if (!event.defaultPrevented) menu.select(value);
7785
+ },
7786
+ ...props,
7787
+ children: [
7788
+ renderItemIcon3(leftIcon),
7789
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
7790
+ Typography,
7791
+ {
7792
+ level: "text",
7793
+ as: "span",
7794
+ className: "aviala-navigation-menu-item__label",
7795
+ children
7796
+ }
7797
+ ),
7798
+ rightIcon ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "aviala-navigation-menu-item__trailing", children: renderItemIcon3(rightIcon) }) : null
7799
+ ]
7800
+ }
7801
+ );
7802
+ }
7803
+ );
7804
+ NavigationItemMenuItem.displayName = "NavigationItemMenuItem";
7589
7805
 
7590
7806
  // src/components/slider.tsx
7591
7807
  var SliderPrimitive = __toESM(require("@radix-ui/react-slider"), 1);
@@ -8852,6 +9068,10 @@ function initKeyboardFocus() {
8852
9068
  NavigationGroup,
8853
9069
  NavigationItem,
8854
9070
  NavigationItemGroup,
9071
+ NavigationItemMenu,
9072
+ NavigationItemMenuContent,
9073
+ NavigationItemMenuItem,
9074
+ NavigationItemMenuTrigger,
8855
9075
  NavigationSection,
8856
9076
  PALETTE_HUE_FAMILIES,
8857
9077
  Pagehead,