@almadar/ui 2.51.0 → 2.52.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.
@@ -19388,13 +19388,13 @@ var MapViewImpl = React125.lazy(async () => {
19388
19388
  shadowSize: [41, 41]
19389
19389
  });
19390
19390
  L.Marker.prototype.options.icon = defaultIcon;
19391
- const { useEffect: useEffect87, useRef: useRef87, useCallback: useCallback124, useState: useState124 } = React125__namespace.default;
19391
+ const { useEffect: useEffect88, useRef: useRef88, useCallback: useCallback124, useState: useState124 } = React125__namespace.default;
19392
19392
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
19393
19393
  const { useEventBus: useEventBus3 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
19394
19394
  function MapUpdater({ centerLat, centerLng, zoom }) {
19395
19395
  const map = useMap();
19396
- const prevRef = useRef87({ centerLat, centerLng, zoom });
19397
- useEffect87(() => {
19396
+ const prevRef = useRef88({ centerLat, centerLng, zoom });
19397
+ useEffect88(() => {
19398
19398
  const prev = prevRef.current;
19399
19399
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
19400
19400
  map.setView([centerLat, centerLng], zoom);
@@ -19405,7 +19405,7 @@ var MapViewImpl = React125.lazy(async () => {
19405
19405
  }
19406
19406
  function MapClickHandler({ onMapClick }) {
19407
19407
  const map = useMap();
19408
- useEffect87(() => {
19408
+ useEffect88(() => {
19409
19409
  if (!onMapClick) return;
19410
19410
  const handler = (e) => {
19411
19411
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -47775,12 +47775,18 @@ var OrbPreviewNodeInner = (props) => {
47775
47775
  el = el.parentElement;
47776
47776
  if (!el || el === contentRef.current) break;
47777
47777
  }
47778
+ const containerNode = {
47779
+ orbitalName: data.orbitalName,
47780
+ traitName: data.traitName,
47781
+ transitionEvent: data.transitionEvent
47782
+ };
47778
47783
  const containerPath = el?.dataset?.patternPath;
47779
47784
  if (!containerPath) {
47780
- eventBus.emit("UI:PATTERN_INSERT", {
47785
+ eventBus.emit("UI:PATTERN_DROP", {
47781
47786
  parentPath: "root",
47782
47787
  patternType: payload.data.type,
47783
- index: 0
47788
+ index: 0,
47789
+ containerNode
47784
47790
  });
47785
47791
  return;
47786
47792
  }
@@ -47797,12 +47803,13 @@ var OrbPreviewNodeInner = (props) => {
47797
47803
  break;
47798
47804
  }
47799
47805
  }
47800
- eventBus.emit("UI:PATTERN_INSERT", {
47806
+ eventBus.emit("UI:PATTERN_DROP", {
47801
47807
  parentPath: containerPath,
47802
47808
  patternType: payload.data.type,
47803
- index: insertIndex
47809
+ index: insertIndex,
47810
+ containerNode
47804
47811
  });
47805
- }, [eventBus]);
47812
+ }, [eventBus, data.orbitalName, data.traitName, data.transitionEvent]);
47806
47813
  const handlePreviewDragOver = React125.useCallback((e) => {
47807
47814
  if (!e.dataTransfer.types.includes(ALMADAR_DND_MIME)) return;
47808
47815
  e.preventDefault();
@@ -48713,6 +48720,7 @@ function FlowCanvasInner({
48713
48720
  onNodeClick,
48714
48721
  onLevelChange,
48715
48722
  onOrbitalDoubleClick,
48723
+ cosmicEntryLevel = "expanded",
48716
48724
  initialOrbital,
48717
48725
  initialLevel,
48718
48726
  initialSelectedNode,
@@ -48786,19 +48794,24 @@ function FlowCanvasInner({
48786
48794
  }
48787
48795
  if (level === "overview") {
48788
48796
  const d = node.data;
48789
- setExpandedOrbital(d.orbitalName ?? node.id);
48797
+ const orbitalName = d.orbitalName ?? node.id;
48798
+ if (onOrbitalDoubleClick && (cosmicEntryLevel === "overview" || cosmicEntryLevel === "both")) {
48799
+ onOrbitalDoubleClick(orbitalName);
48800
+ return;
48801
+ }
48802
+ setExpandedOrbital(orbitalName);
48790
48803
  setLevel("expanded");
48791
- onLevelChange?.("expanded", d.orbitalName ?? node.id);
48804
+ onLevelChange?.("expanded", orbitalName);
48792
48805
  return;
48793
48806
  }
48794
48807
  if (level === "expanded") {
48795
48808
  const d = node.data;
48796
48809
  const orbitalName = d.orbitalName ?? node.id;
48797
- if (orbitalName && onOrbitalDoubleClick) {
48810
+ if (orbitalName && onOrbitalDoubleClick && (cosmicEntryLevel === "expanded" || cosmicEntryLevel === "both")) {
48798
48811
  onOrbitalDoubleClick(orbitalName);
48799
48812
  }
48800
48813
  }
48801
- }, [level, onLevelChange, onOrbitalDoubleClick, atBehaviorLevel, composeLevel]);
48814
+ }, [level, onLevelChange, onOrbitalDoubleClick, cosmicEntryLevel, atBehaviorLevel, composeLevel]);
48802
48815
  const handleNodeClick = React125.useCallback((_, node) => {
48803
48816
  const nodeData = node.data;
48804
48817
  if (level === "expanded") {
@@ -49334,7 +49347,10 @@ var AvlOrbitalsCosmicZoom = ({
49334
49347
  animated = true,
49335
49348
  width = "100%",
49336
49349
  height = 450,
49337
- highlightedOrbital
49350
+ highlightedOrbital,
49351
+ onOrbitalSelect,
49352
+ minZoom = 0.4,
49353
+ maxZoom = 3
49338
49354
  }) => {
49339
49355
  const parsedSchema = React125.useMemo(() => {
49340
49356
  if (typeof schemaProp === "string") return JSON.parse(schemaProp);
@@ -49365,10 +49381,83 @@ var AvlOrbitalsCosmicZoom = ({
49365
49381
  );
49366
49382
  const [selected, setSelected] = React125.useState(null);
49367
49383
  const handleSelect = React125.useCallback(
49368
- (name) => setSelected((prev) => prev === name ? null : name),
49369
- []
49384
+ (name) => {
49385
+ setSelected((prev) => prev === name ? null : name);
49386
+ onOrbitalSelect?.(name);
49387
+ },
49388
+ [onOrbitalSelect]
49370
49389
  );
49371
49390
  const selectedView = orbitalViews.find((o) => o.name === selected);
49391
+ const [zoom, setZoom] = React125.useState(1);
49392
+ const [pan, setPan] = React125.useState({ x: 0, y: 0 });
49393
+ const dragStateRef = React125.useRef(null);
49394
+ const transformWrapperRef = React125.useRef(null);
49395
+ const clampZoom = React125.useCallback(
49396
+ (z) => Math.max(minZoom, Math.min(maxZoom, z)),
49397
+ [minZoom, maxZoom]
49398
+ );
49399
+ const handlePointerDown = React125.useCallback((e) => {
49400
+ if (e.target.closest("[data-orbital-tile]")) return;
49401
+ dragStateRef.current = {
49402
+ startX: e.clientX,
49403
+ startY: e.clientY,
49404
+ panX: pan.x,
49405
+ panY: pan.y
49406
+ };
49407
+ e.target.setPointerCapture(e.pointerId);
49408
+ }, [pan]);
49409
+ const handlePointerMove = React125.useCallback((e) => {
49410
+ const drag = dragStateRef.current;
49411
+ if (!drag) return;
49412
+ setPan({
49413
+ x: drag.panX + (e.clientX - drag.startX),
49414
+ y: drag.panY + (e.clientY - drag.startY)
49415
+ });
49416
+ }, []);
49417
+ const handlePointerUp = React125.useCallback((e) => {
49418
+ if (!dragStateRef.current) return;
49419
+ dragStateRef.current = null;
49420
+ try {
49421
+ e.target.releasePointerCapture(e.pointerId);
49422
+ } catch {
49423
+ }
49424
+ }, []);
49425
+ const panRef = React125.useRef(pan);
49426
+ const zoomRef = React125.useRef(zoom);
49427
+ React125.useEffect(() => {
49428
+ panRef.current = pan;
49429
+ }, [pan]);
49430
+ React125.useEffect(() => {
49431
+ zoomRef.current = zoom;
49432
+ }, [zoom]);
49433
+ React125.useEffect(() => {
49434
+ const wrapper = transformWrapperRef.current;
49435
+ if (!wrapper) return;
49436
+ const wheelListener = (e) => {
49437
+ e.preventDefault();
49438
+ const rect = wrapper.getBoundingClientRect();
49439
+ const cursorX = e.clientX - rect.left;
49440
+ const cursorY = e.clientY - rect.top;
49441
+ const currentZoom = zoomRef.current;
49442
+ const currentPan = panRef.current;
49443
+ const worldX = (cursorX - currentPan.x) / currentZoom;
49444
+ const worldY = (cursorY - currentPan.y) / currentZoom;
49445
+ const delta = e.deltaY > 0 ? -0.1 : 0.1;
49446
+ const nextZoom = clampZoom(currentZoom * (1 + delta));
49447
+ const nextPanX = cursorX - worldX * nextZoom;
49448
+ const nextPanY = cursorY - worldY * nextZoom;
49449
+ setZoom(nextZoom);
49450
+ setPan({ x: nextPanX, y: nextPanY });
49451
+ };
49452
+ wrapper.addEventListener("wheel", wheelListener, { passive: false });
49453
+ return () => wrapper.removeEventListener("wheel", wheelListener);
49454
+ }, [clampZoom]);
49455
+ const zoomIn = React125.useCallback(() => setZoom((z) => clampZoom(z * 1.2)), [clampZoom]);
49456
+ const zoomOut = React125.useCallback(() => setZoom((z) => clampZoom(z / 1.2)), [clampZoom]);
49457
+ const resetZoom = React125.useCallback(() => {
49458
+ setZoom(1);
49459
+ setPan({ x: 0, y: 0 });
49460
+ }, []);
49372
49461
  return /* @__PURE__ */ jsxRuntime.jsxs(
49373
49462
  Box,
49374
49463
  {
@@ -49378,65 +49467,115 @@ var AvlOrbitalsCosmicZoom = ({
49378
49467
  style: { width, height: containerH },
49379
49468
  children: [
49380
49469
  /* @__PURE__ */ jsxRuntime.jsx(
49381
- EventWireOverlay,
49470
+ "div",
49382
49471
  {
49383
- orbitalViews,
49384
- crossLinks,
49385
- color,
49386
- animated,
49387
- containerW,
49388
- containerH
49389
- }
49390
- ),
49391
- orbitalViews.map((view) => {
49392
- const isHighlighted = view.name === highlightedOrbital;
49393
- return /* @__PURE__ */ jsxRuntime.jsx(
49394
- Box,
49395
- {
49396
- role: "button",
49397
- tabIndex: 0,
49398
- onClick: () => handleSelect(view.name),
49399
- onKeyDown: (e) => {
49400
- if (e.key === "Enter" || e.key === " ") handleSelect(view.name);
49401
- },
49402
- "aria-label": `Orbital: ${view.name}${isHighlighted ? " (highlighted)" : ""}`,
49472
+ ref: transformWrapperRef,
49473
+ onPointerDown: handlePointerDown,
49474
+ onPointerMove: handlePointerMove,
49475
+ onPointerUp: handlePointerUp,
49476
+ onPointerCancel: handlePointerUp,
49477
+ style: {
49403
49478
  position: "absolute",
49404
- style: {
49405
- left: view.cx - UNIT_DISPLAY_W / 2,
49406
- top: view.cy - UNIT_DISPLAY_H / 2,
49407
- width: UNIT_DISPLAY_W,
49408
- height: UNIT_DISPLAY_H,
49409
- cursor: "pointer",
49410
- transition: "transform 0.2s ease, filter 0.2s ease, box-shadow 0.3s ease",
49411
- transform: selected === view.name ? "scale(1.05)" : "scale(1)",
49412
- filter: selected && selected !== view.name ? "opacity(0.5)" : "none",
49413
- // GAP-52: persistent highlight ring (independent from user selection)
49414
- boxShadow: isHighlighted ? `0 0 0 3px ${color}, 0 0 24px 4px ${color}` : "none",
49415
- borderRadius: isHighlighted ? "12px" : void 0,
49416
- zIndex: isHighlighted ? 11 : selected === view.name ? 10 : 1
49417
- },
49418
- children: /* @__PURE__ */ jsxRuntime.jsx(
49419
- AvlOrbitalUnit,
49420
- {
49421
- entityName: view.entityName,
49422
- fields: view.fieldCount,
49423
- persistence: view.persistence,
49424
- traits: view.traits,
49425
- pages: view.pages,
49426
- color,
49427
- animated: animated && (selected === view.name || isHighlighted)
49428
- }
49429
- )
49479
+ inset: 0,
49480
+ overflow: "hidden",
49481
+ cursor: dragStateRef.current ? "grabbing" : "grab",
49482
+ touchAction: "none"
49430
49483
  },
49431
- view.name
49432
- );
49433
- }),
49434
- selectedView && /* @__PURE__ */ jsxRuntime.jsx(
49435
- InfoPanel,
49484
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
49485
+ "div",
49486
+ {
49487
+ style: {
49488
+ position: "absolute",
49489
+ inset: 0,
49490
+ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
49491
+ transformOrigin: "0 0"
49492
+ },
49493
+ children: [
49494
+ /* @__PURE__ */ jsxRuntime.jsx(
49495
+ EventWireOverlay,
49496
+ {
49497
+ orbitalViews,
49498
+ crossLinks,
49499
+ color,
49500
+ animated,
49501
+ containerW,
49502
+ containerH
49503
+ }
49504
+ ),
49505
+ orbitalViews.map((view) => {
49506
+ const isHighlighted = view.name === highlightedOrbital;
49507
+ return /* @__PURE__ */ jsxRuntime.jsx(
49508
+ Box,
49509
+ {
49510
+ role: "button",
49511
+ tabIndex: 0,
49512
+ "data-orbital-tile": "true",
49513
+ onClick: () => handleSelect(view.name),
49514
+ onKeyDown: (e) => {
49515
+ if (e.key === "Enter" || e.key === " ") handleSelect(view.name);
49516
+ },
49517
+ "aria-label": `Orbital: ${view.name}${isHighlighted ? " (highlighted)" : ""}`,
49518
+ position: "absolute",
49519
+ style: {
49520
+ left: view.cx - UNIT_DISPLAY_W / 2,
49521
+ top: view.cy - UNIT_DISPLAY_H / 2,
49522
+ width: UNIT_DISPLAY_W,
49523
+ height: UNIT_DISPLAY_H,
49524
+ cursor: "pointer",
49525
+ transition: "transform 0.2s ease, filter 0.2s ease, box-shadow 0.3s ease",
49526
+ transform: selected === view.name ? "scale(1.05)" : "scale(1)",
49527
+ filter: selected && selected !== view.name ? "opacity(0.5)" : "none",
49528
+ // GAP-52: persistent highlight ring (independent from user selection)
49529
+ boxShadow: isHighlighted ? `0 0 0 3px ${color}, 0 0 24px 4px ${color}` : "none",
49530
+ borderRadius: isHighlighted ? "12px" : void 0,
49531
+ zIndex: isHighlighted ? 11 : selected === view.name ? 10 : 1
49532
+ },
49533
+ children: /* @__PURE__ */ jsxRuntime.jsx(
49534
+ AvlOrbitalUnit,
49535
+ {
49536
+ entityName: view.entityName,
49537
+ fields: view.fieldCount,
49538
+ persistence: view.persistence,
49539
+ traits: view.traits,
49540
+ pages: view.pages,
49541
+ color,
49542
+ animated: animated && (selected === view.name || isHighlighted)
49543
+ }
49544
+ )
49545
+ },
49546
+ view.name
49547
+ );
49548
+ }),
49549
+ selectedView && /* @__PURE__ */ jsxRuntime.jsx(
49550
+ InfoPanel,
49551
+ {
49552
+ view: selectedView,
49553
+ crossLinks,
49554
+ color
49555
+ }
49556
+ )
49557
+ ]
49558
+ }
49559
+ )
49560
+ }
49561
+ ),
49562
+ /* @__PURE__ */ jsxRuntime.jsxs(
49563
+ Box,
49436
49564
  {
49437
- view: selectedView,
49438
- crossLinks,
49439
- color
49565
+ position: "absolute",
49566
+ style: {
49567
+ top: 12,
49568
+ right: 12,
49569
+ display: "flex",
49570
+ flexDirection: "column",
49571
+ gap: 4,
49572
+ zIndex: 30
49573
+ },
49574
+ children: [
49575
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", size: "sm", onClick: zoomIn, title: "Zoom in", action: "COSMIC_ZOOM_IN", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "plus", size: "sm" }) }),
49576
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", size: "sm", onClick: zoomOut, title: "Zoom out", action: "COSMIC_ZOOM_OUT", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "minus", size: "sm" }) }),
49577
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", size: "sm", onClick: resetZoom, title: "Reset", action: "COSMIC_ZOOM_RESET", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "maximize", size: "sm" }) })
49578
+ ]
49440
49579
  }
49441
49580
  )
49442
49581
  ]
@@ -1211,15 +1211,29 @@ interface FlowCanvasProps {
1211
1211
  }) => void;
1212
1212
  onLevelChange?: (level: ViewLevel, orbital?: string) => void;
1213
1213
  /**
1214
- * GAP-52: fired when the user double-clicks an orbital while ALREADY at
1215
- * `level === 'expanded'`. Consumers (e.g. the builder workspace) use this as
1216
- * the trigger to enter cosmic mode (`AvlOrbitalsCosmicZoom`) for the focused
1217
- * orbital. This does NOT replace the existing overview→expanded drill —
1218
- * that path still fires `onLevelChange('expanded', ...)` as before.
1219
- * The callback runs unconditionally; persona / permission gating is the
1214
+ * GAP-52: fired when the user double-clicks an orbital. Consumers (e.g. the
1215
+ * builder workspace) use this as the trigger to enter cosmic mode
1216
+ * (`AvlOrbitalsCosmicZoom`) for the focused orbital.
1217
+ *
1218
+ * The level at which this fires is controlled by `cosmicEntryLevel` (default
1219
+ * `'expanded'`). At `'expanded'` the existing overview→expanded drill is
1220
+ * preserved — the callback fires only on the second double-click. At
1221
+ * `'overview'` the callback fires on the FIRST double-click and the existing
1222
+ * drill is suppressed for that interaction. `'both'` fires at either level.
1223
+ *
1224
+ * The callback runs unconditionally — persona / permission gating is the
1220
1225
  * consumer's responsibility.
1221
1226
  */
1222
1227
  onOrbitalDoubleClick?: (orbital: string) => void;
1228
+ /**
1229
+ * GAP-53: which level the `onOrbitalDoubleClick` callback fires at.
1230
+ * - `'expanded'` (default, non-breaking) — fires only at L2 expanded; the
1231
+ * first overview double-click still drills overview→expanded.
1232
+ * - `'overview'` — fires at L1 overview on the FIRST double-click. The
1233
+ * overview→expanded drill is suppressed when the callback is provided.
1234
+ * - `'both'` — fires at either level.
1235
+ */
1236
+ cosmicEntryLevel?: 'expanded' | 'overview' | 'both';
1223
1237
  initialOrbital?: string;
1224
1238
  /** Start at Level 2 (expanded) when initialOrbital is set. Default: 'overview'. */
1225
1239
  initialLevel?: ViewLevel;
@@ -1385,6 +1399,22 @@ interface AvlOrbitalsCosmicZoomProps {
1385
1399
  * highlighted while the user can still click any other orbital to select it.
1386
1400
  */
1387
1401
  highlightedOrbital?: string;
1402
+ /**
1403
+ * GAP-55: fired when the user clicks an orbital tile. Consumers (e.g. the
1404
+ * builder workspace) use this as the trigger to drill INTO the clicked
1405
+ * orbital — typically by switching back to the canvas tab and opening the
1406
+ * clicked orbital at L2 expanded. Local `selected` toggle (visual highlight +
1407
+ * info panel) still fires regardless of whether the callback is provided.
1408
+ */
1409
+ onOrbitalSelect?: (orbital: string) => void;
1410
+ /**
1411
+ * GAP-54: minimum zoom factor when scroll-wheel zooming. Default 0.4.
1412
+ */
1413
+ minZoom?: number;
1414
+ /**
1415
+ * GAP-54: maximum zoom factor when scroll-wheel zooming. Default 3.
1416
+ */
1417
+ maxZoom?: number;
1388
1418
  }
1389
1419
  declare const AvlOrbitalsCosmicZoom: React__default.FC<AvlOrbitalsCosmicZoomProps>;
1390
1420
 
package/dist/avl/index.js CHANGED
@@ -19342,13 +19342,13 @@ var MapViewImpl = lazy(async () => {
19342
19342
  shadowSize: [41, 41]
19343
19343
  });
19344
19344
  L.Marker.prototype.options.icon = defaultIcon;
19345
- const { useEffect: useEffect87, useRef: useRef87, useCallback: useCallback124, useState: useState124 } = React125__default;
19345
+ const { useEffect: useEffect88, useRef: useRef88, useCallback: useCallback124, useState: useState124 } = React125__default;
19346
19346
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
19347
19347
  const { useEventBus: useEventBus3 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
19348
19348
  function MapUpdater({ centerLat, centerLng, zoom }) {
19349
19349
  const map = useMap();
19350
- const prevRef = useRef87({ centerLat, centerLng, zoom });
19351
- useEffect87(() => {
19350
+ const prevRef = useRef88({ centerLat, centerLng, zoom });
19351
+ useEffect88(() => {
19352
19352
  const prev = prevRef.current;
19353
19353
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
19354
19354
  map.setView([centerLat, centerLng], zoom);
@@ -19359,7 +19359,7 @@ var MapViewImpl = lazy(async () => {
19359
19359
  }
19360
19360
  function MapClickHandler({ onMapClick }) {
19361
19361
  const map = useMap();
19362
- useEffect87(() => {
19362
+ useEffect88(() => {
19363
19363
  if (!onMapClick) return;
19364
19364
  const handler = (e) => {
19365
19365
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -47729,12 +47729,18 @@ var OrbPreviewNodeInner = (props) => {
47729
47729
  el = el.parentElement;
47730
47730
  if (!el || el === contentRef.current) break;
47731
47731
  }
47732
+ const containerNode = {
47733
+ orbitalName: data.orbitalName,
47734
+ traitName: data.traitName,
47735
+ transitionEvent: data.transitionEvent
47736
+ };
47732
47737
  const containerPath = el?.dataset?.patternPath;
47733
47738
  if (!containerPath) {
47734
- eventBus.emit("UI:PATTERN_INSERT", {
47739
+ eventBus.emit("UI:PATTERN_DROP", {
47735
47740
  parentPath: "root",
47736
47741
  patternType: payload.data.type,
47737
- index: 0
47742
+ index: 0,
47743
+ containerNode
47738
47744
  });
47739
47745
  return;
47740
47746
  }
@@ -47751,12 +47757,13 @@ var OrbPreviewNodeInner = (props) => {
47751
47757
  break;
47752
47758
  }
47753
47759
  }
47754
- eventBus.emit("UI:PATTERN_INSERT", {
47760
+ eventBus.emit("UI:PATTERN_DROP", {
47755
47761
  parentPath: containerPath,
47756
47762
  patternType: payload.data.type,
47757
- index: insertIndex
47763
+ index: insertIndex,
47764
+ containerNode
47758
47765
  });
47759
- }, [eventBus]);
47766
+ }, [eventBus, data.orbitalName, data.traitName, data.transitionEvent]);
47760
47767
  const handlePreviewDragOver = useCallback((e) => {
47761
47768
  if (!e.dataTransfer.types.includes(ALMADAR_DND_MIME)) return;
47762
47769
  e.preventDefault();
@@ -48667,6 +48674,7 @@ function FlowCanvasInner({
48667
48674
  onNodeClick,
48668
48675
  onLevelChange,
48669
48676
  onOrbitalDoubleClick,
48677
+ cosmicEntryLevel = "expanded",
48670
48678
  initialOrbital,
48671
48679
  initialLevel,
48672
48680
  initialSelectedNode,
@@ -48740,19 +48748,24 @@ function FlowCanvasInner({
48740
48748
  }
48741
48749
  if (level === "overview") {
48742
48750
  const d = node.data;
48743
- setExpandedOrbital(d.orbitalName ?? node.id);
48751
+ const orbitalName = d.orbitalName ?? node.id;
48752
+ if (onOrbitalDoubleClick && (cosmicEntryLevel === "overview" || cosmicEntryLevel === "both")) {
48753
+ onOrbitalDoubleClick(orbitalName);
48754
+ return;
48755
+ }
48756
+ setExpandedOrbital(orbitalName);
48744
48757
  setLevel("expanded");
48745
- onLevelChange?.("expanded", d.orbitalName ?? node.id);
48758
+ onLevelChange?.("expanded", orbitalName);
48746
48759
  return;
48747
48760
  }
48748
48761
  if (level === "expanded") {
48749
48762
  const d = node.data;
48750
48763
  const orbitalName = d.orbitalName ?? node.id;
48751
- if (orbitalName && onOrbitalDoubleClick) {
48764
+ if (orbitalName && onOrbitalDoubleClick && (cosmicEntryLevel === "expanded" || cosmicEntryLevel === "both")) {
48752
48765
  onOrbitalDoubleClick(orbitalName);
48753
48766
  }
48754
48767
  }
48755
- }, [level, onLevelChange, onOrbitalDoubleClick, atBehaviorLevel, composeLevel]);
48768
+ }, [level, onLevelChange, onOrbitalDoubleClick, cosmicEntryLevel, atBehaviorLevel, composeLevel]);
48756
48769
  const handleNodeClick = useCallback((_, node) => {
48757
48770
  const nodeData = node.data;
48758
48771
  if (level === "expanded") {
@@ -49288,7 +49301,10 @@ var AvlOrbitalsCosmicZoom = ({
49288
49301
  animated = true,
49289
49302
  width = "100%",
49290
49303
  height = 450,
49291
- highlightedOrbital
49304
+ highlightedOrbital,
49305
+ onOrbitalSelect,
49306
+ minZoom = 0.4,
49307
+ maxZoom = 3
49292
49308
  }) => {
49293
49309
  const parsedSchema = useMemo(() => {
49294
49310
  if (typeof schemaProp === "string") return JSON.parse(schemaProp);
@@ -49319,10 +49335,83 @@ var AvlOrbitalsCosmicZoom = ({
49319
49335
  );
49320
49336
  const [selected, setSelected] = useState(null);
49321
49337
  const handleSelect = useCallback(
49322
- (name) => setSelected((prev) => prev === name ? null : name),
49323
- []
49338
+ (name) => {
49339
+ setSelected((prev) => prev === name ? null : name);
49340
+ onOrbitalSelect?.(name);
49341
+ },
49342
+ [onOrbitalSelect]
49324
49343
  );
49325
49344
  const selectedView = orbitalViews.find((o) => o.name === selected);
49345
+ const [zoom, setZoom] = useState(1);
49346
+ const [pan, setPan] = useState({ x: 0, y: 0 });
49347
+ const dragStateRef = useRef(null);
49348
+ const transformWrapperRef = useRef(null);
49349
+ const clampZoom = useCallback(
49350
+ (z) => Math.max(minZoom, Math.min(maxZoom, z)),
49351
+ [minZoom, maxZoom]
49352
+ );
49353
+ const handlePointerDown = useCallback((e) => {
49354
+ if (e.target.closest("[data-orbital-tile]")) return;
49355
+ dragStateRef.current = {
49356
+ startX: e.clientX,
49357
+ startY: e.clientY,
49358
+ panX: pan.x,
49359
+ panY: pan.y
49360
+ };
49361
+ e.target.setPointerCapture(e.pointerId);
49362
+ }, [pan]);
49363
+ const handlePointerMove = useCallback((e) => {
49364
+ const drag = dragStateRef.current;
49365
+ if (!drag) return;
49366
+ setPan({
49367
+ x: drag.panX + (e.clientX - drag.startX),
49368
+ y: drag.panY + (e.clientY - drag.startY)
49369
+ });
49370
+ }, []);
49371
+ const handlePointerUp = useCallback((e) => {
49372
+ if (!dragStateRef.current) return;
49373
+ dragStateRef.current = null;
49374
+ try {
49375
+ e.target.releasePointerCapture(e.pointerId);
49376
+ } catch {
49377
+ }
49378
+ }, []);
49379
+ const panRef = useRef(pan);
49380
+ const zoomRef = useRef(zoom);
49381
+ useEffect(() => {
49382
+ panRef.current = pan;
49383
+ }, [pan]);
49384
+ useEffect(() => {
49385
+ zoomRef.current = zoom;
49386
+ }, [zoom]);
49387
+ useEffect(() => {
49388
+ const wrapper = transformWrapperRef.current;
49389
+ if (!wrapper) return;
49390
+ const wheelListener = (e) => {
49391
+ e.preventDefault();
49392
+ const rect = wrapper.getBoundingClientRect();
49393
+ const cursorX = e.clientX - rect.left;
49394
+ const cursorY = e.clientY - rect.top;
49395
+ const currentZoom = zoomRef.current;
49396
+ const currentPan = panRef.current;
49397
+ const worldX = (cursorX - currentPan.x) / currentZoom;
49398
+ const worldY = (cursorY - currentPan.y) / currentZoom;
49399
+ const delta = e.deltaY > 0 ? -0.1 : 0.1;
49400
+ const nextZoom = clampZoom(currentZoom * (1 + delta));
49401
+ const nextPanX = cursorX - worldX * nextZoom;
49402
+ const nextPanY = cursorY - worldY * nextZoom;
49403
+ setZoom(nextZoom);
49404
+ setPan({ x: nextPanX, y: nextPanY });
49405
+ };
49406
+ wrapper.addEventListener("wheel", wheelListener, { passive: false });
49407
+ return () => wrapper.removeEventListener("wheel", wheelListener);
49408
+ }, [clampZoom]);
49409
+ const zoomIn = useCallback(() => setZoom((z) => clampZoom(z * 1.2)), [clampZoom]);
49410
+ const zoomOut = useCallback(() => setZoom((z) => clampZoom(z / 1.2)), [clampZoom]);
49411
+ const resetZoom = useCallback(() => {
49412
+ setZoom(1);
49413
+ setPan({ x: 0, y: 0 });
49414
+ }, []);
49326
49415
  return /* @__PURE__ */ jsxs(
49327
49416
  Box,
49328
49417
  {
@@ -49332,65 +49421,115 @@ var AvlOrbitalsCosmicZoom = ({
49332
49421
  style: { width, height: containerH },
49333
49422
  children: [
49334
49423
  /* @__PURE__ */ jsx(
49335
- EventWireOverlay,
49424
+ "div",
49336
49425
  {
49337
- orbitalViews,
49338
- crossLinks,
49339
- color,
49340
- animated,
49341
- containerW,
49342
- containerH
49343
- }
49344
- ),
49345
- orbitalViews.map((view) => {
49346
- const isHighlighted = view.name === highlightedOrbital;
49347
- return /* @__PURE__ */ jsx(
49348
- Box,
49349
- {
49350
- role: "button",
49351
- tabIndex: 0,
49352
- onClick: () => handleSelect(view.name),
49353
- onKeyDown: (e) => {
49354
- if (e.key === "Enter" || e.key === " ") handleSelect(view.name);
49355
- },
49356
- "aria-label": `Orbital: ${view.name}${isHighlighted ? " (highlighted)" : ""}`,
49426
+ ref: transformWrapperRef,
49427
+ onPointerDown: handlePointerDown,
49428
+ onPointerMove: handlePointerMove,
49429
+ onPointerUp: handlePointerUp,
49430
+ onPointerCancel: handlePointerUp,
49431
+ style: {
49357
49432
  position: "absolute",
49358
- style: {
49359
- left: view.cx - UNIT_DISPLAY_W / 2,
49360
- top: view.cy - UNIT_DISPLAY_H / 2,
49361
- width: UNIT_DISPLAY_W,
49362
- height: UNIT_DISPLAY_H,
49363
- cursor: "pointer",
49364
- transition: "transform 0.2s ease, filter 0.2s ease, box-shadow 0.3s ease",
49365
- transform: selected === view.name ? "scale(1.05)" : "scale(1)",
49366
- filter: selected && selected !== view.name ? "opacity(0.5)" : "none",
49367
- // GAP-52: persistent highlight ring (independent from user selection)
49368
- boxShadow: isHighlighted ? `0 0 0 3px ${color}, 0 0 24px 4px ${color}` : "none",
49369
- borderRadius: isHighlighted ? "12px" : void 0,
49370
- zIndex: isHighlighted ? 11 : selected === view.name ? 10 : 1
49371
- },
49372
- children: /* @__PURE__ */ jsx(
49373
- AvlOrbitalUnit,
49374
- {
49375
- entityName: view.entityName,
49376
- fields: view.fieldCount,
49377
- persistence: view.persistence,
49378
- traits: view.traits,
49379
- pages: view.pages,
49380
- color,
49381
- animated: animated && (selected === view.name || isHighlighted)
49382
- }
49383
- )
49433
+ inset: 0,
49434
+ overflow: "hidden",
49435
+ cursor: dragStateRef.current ? "grabbing" : "grab",
49436
+ touchAction: "none"
49384
49437
  },
49385
- view.name
49386
- );
49387
- }),
49388
- selectedView && /* @__PURE__ */ jsx(
49389
- InfoPanel,
49438
+ children: /* @__PURE__ */ jsxs(
49439
+ "div",
49440
+ {
49441
+ style: {
49442
+ position: "absolute",
49443
+ inset: 0,
49444
+ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
49445
+ transformOrigin: "0 0"
49446
+ },
49447
+ children: [
49448
+ /* @__PURE__ */ jsx(
49449
+ EventWireOverlay,
49450
+ {
49451
+ orbitalViews,
49452
+ crossLinks,
49453
+ color,
49454
+ animated,
49455
+ containerW,
49456
+ containerH
49457
+ }
49458
+ ),
49459
+ orbitalViews.map((view) => {
49460
+ const isHighlighted = view.name === highlightedOrbital;
49461
+ return /* @__PURE__ */ jsx(
49462
+ Box,
49463
+ {
49464
+ role: "button",
49465
+ tabIndex: 0,
49466
+ "data-orbital-tile": "true",
49467
+ onClick: () => handleSelect(view.name),
49468
+ onKeyDown: (e) => {
49469
+ if (e.key === "Enter" || e.key === " ") handleSelect(view.name);
49470
+ },
49471
+ "aria-label": `Orbital: ${view.name}${isHighlighted ? " (highlighted)" : ""}`,
49472
+ position: "absolute",
49473
+ style: {
49474
+ left: view.cx - UNIT_DISPLAY_W / 2,
49475
+ top: view.cy - UNIT_DISPLAY_H / 2,
49476
+ width: UNIT_DISPLAY_W,
49477
+ height: UNIT_DISPLAY_H,
49478
+ cursor: "pointer",
49479
+ transition: "transform 0.2s ease, filter 0.2s ease, box-shadow 0.3s ease",
49480
+ transform: selected === view.name ? "scale(1.05)" : "scale(1)",
49481
+ filter: selected && selected !== view.name ? "opacity(0.5)" : "none",
49482
+ // GAP-52: persistent highlight ring (independent from user selection)
49483
+ boxShadow: isHighlighted ? `0 0 0 3px ${color}, 0 0 24px 4px ${color}` : "none",
49484
+ borderRadius: isHighlighted ? "12px" : void 0,
49485
+ zIndex: isHighlighted ? 11 : selected === view.name ? 10 : 1
49486
+ },
49487
+ children: /* @__PURE__ */ jsx(
49488
+ AvlOrbitalUnit,
49489
+ {
49490
+ entityName: view.entityName,
49491
+ fields: view.fieldCount,
49492
+ persistence: view.persistence,
49493
+ traits: view.traits,
49494
+ pages: view.pages,
49495
+ color,
49496
+ animated: animated && (selected === view.name || isHighlighted)
49497
+ }
49498
+ )
49499
+ },
49500
+ view.name
49501
+ );
49502
+ }),
49503
+ selectedView && /* @__PURE__ */ jsx(
49504
+ InfoPanel,
49505
+ {
49506
+ view: selectedView,
49507
+ crossLinks,
49508
+ color
49509
+ }
49510
+ )
49511
+ ]
49512
+ }
49513
+ )
49514
+ }
49515
+ ),
49516
+ /* @__PURE__ */ jsxs(
49517
+ Box,
49390
49518
  {
49391
- view: selectedView,
49392
- crossLinks,
49393
- color
49519
+ position: "absolute",
49520
+ style: {
49521
+ top: 12,
49522
+ right: 12,
49523
+ display: "flex",
49524
+ flexDirection: "column",
49525
+ gap: 4,
49526
+ zIndex: 30
49527
+ },
49528
+ children: [
49529
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: zoomIn, title: "Zoom in", action: "COSMIC_ZOOM_IN", children: /* @__PURE__ */ jsx(Icon, { name: "plus", size: "sm" }) }),
49530
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: zoomOut, title: "Zoom out", action: "COSMIC_ZOOM_OUT", children: /* @__PURE__ */ jsx(Icon, { name: "minus", size: "sm" }) }),
49531
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: resetZoom, title: "Reset", action: "COSMIC_ZOOM_RESET", children: /* @__PURE__ */ jsx(Icon, { name: "maximize", size: "sm" }) })
49532
+ ]
49394
49533
  }
49395
49534
  )
49396
49535
  ]
@@ -34,5 +34,21 @@ export interface AvlOrbitalsCosmicZoomProps {
34
34
  * highlighted while the user can still click any other orbital to select it.
35
35
  */
36
36
  highlightedOrbital?: string;
37
+ /**
38
+ * GAP-55: fired when the user clicks an orbital tile. Consumers (e.g. the
39
+ * builder workspace) use this as the trigger to drill INTO the clicked
40
+ * orbital — typically by switching back to the canvas tab and opening the
41
+ * clicked orbital at L2 expanded. Local `selected` toggle (visual highlight +
42
+ * info panel) still fires regardless of whether the callback is provided.
43
+ */
44
+ onOrbitalSelect?: (orbital: string) => void;
45
+ /**
46
+ * GAP-54: minimum zoom factor when scroll-wheel zooming. Default 0.4.
47
+ */
48
+ minZoom?: number;
49
+ /**
50
+ * GAP-54: maximum zoom factor when scroll-wheel zooming. Default 3.
51
+ */
52
+ maxZoom?: number;
37
53
  }
38
54
  export declare const AvlOrbitalsCosmicZoom: React.FC<AvlOrbitalsCosmicZoomProps>;
@@ -29,15 +29,29 @@ export interface FlowCanvasProps {
29
29
  }) => void;
30
30
  onLevelChange?: (level: ViewLevel, orbital?: string) => void;
31
31
  /**
32
- * GAP-52: fired when the user double-clicks an orbital while ALREADY at
33
- * `level === 'expanded'`. Consumers (e.g. the builder workspace) use this as
34
- * the trigger to enter cosmic mode (`AvlOrbitalsCosmicZoom`) for the focused
35
- * orbital. This does NOT replace the existing overview→expanded drill —
36
- * that path still fires `onLevelChange('expanded', ...)` as before.
37
- * The callback runs unconditionally; persona / permission gating is the
32
+ * GAP-52: fired when the user double-clicks an orbital. Consumers (e.g. the
33
+ * builder workspace) use this as the trigger to enter cosmic mode
34
+ * (`AvlOrbitalsCosmicZoom`) for the focused orbital.
35
+ *
36
+ * The level at which this fires is controlled by `cosmicEntryLevel` (default
37
+ * `'expanded'`). At `'expanded'` the existing overview→expanded drill is
38
+ * preserved — the callback fires only on the second double-click. At
39
+ * `'overview'` the callback fires on the FIRST double-click and the existing
40
+ * drill is suppressed for that interaction. `'both'` fires at either level.
41
+ *
42
+ * The callback runs unconditionally — persona / permission gating is the
38
43
  * consumer's responsibility.
39
44
  */
40
45
  onOrbitalDoubleClick?: (orbital: string) => void;
46
+ /**
47
+ * GAP-53: which level the `onOrbitalDoubleClick` callback fires at.
48
+ * - `'expanded'` (default, non-breaking) — fires only at L2 expanded; the
49
+ * first overview double-click still drills overview→expanded.
50
+ * - `'overview'` — fires at L1 overview on the FIRST double-click. The
51
+ * overview→expanded drill is suppressed when the callback is provided.
52
+ * - `'both'` — fires at either level.
53
+ */
54
+ cosmicEntryLevel?: 'expanded' | 'overview' | 'both';
41
55
  initialOrbital?: string;
42
56
  /** Start at Level 2 (expanded) when initialOrbital is set. Default: 'overview'. */
43
57
  initialLevel?: ViewLevel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "2.51.0",
3
+ "version": "2.52.0",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "main": "./dist/components/index.js",