@elaraai/east-ui-components 1.0.13 → 1.0.14

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.js CHANGED
@@ -1170,8 +1170,13 @@ const globalCss = defineGlobalStyles({
1170
1170
  },
1171
1171
  /* ─── Drag & drop stages (drag-drop-visuals) ─────────────────────────
1172
1172
  * The DragLayerProvider drives these via data attributes; surfaces get
1173
- * the treatment for free by registering cells/sinks. */
1174
- "[data-dragging]": {
1173
+ * the treatment for free by registering cells/sinks.
1174
+ *
1175
+ * Scoped `:not([data-scope])` so it dims only our own drag origins —
1176
+ * Ark/Zag widgets (Slider, etc.) set `data-dragging` on themselves for
1177
+ * their own drag state and carry a `data-scope`; without this exclusion
1178
+ * a slider goes to 40% opacity while its thumb is dragged. */
1179
+ "[data-dragging]:not([data-scope])": {
1175
1180
  opacity: "0.4"
1176
1181
  },
1177
1182
  "[data-drag-ghost]": {
@@ -2991,6 +2996,7 @@ const schematicSlotRecipe = defineSlotRecipe({
2991
2996
  "root",
2992
2997
  "canvas",
2993
2998
  "grid",
2999
+ "cardLayer",
2994
3000
  "underlay",
2995
3001
  "zone",
2996
3002
  "zoneLabel",
@@ -3068,6 +3074,15 @@ const schematicSlotRecipe = defineSlotRecipe({
3068
3074
  linear-gradient(to right, color-mix(in oklch, {colors.border.subtle} 22%, transparent) 1px, transparent 1px),
3069
3075
  linear-gradient(to bottom, color-mix(in oklch, {colors.border.subtle} 22%, transparent) 1px, transparent 1px)`
3070
3076
  },
3077
+ /* Holds the live `--cam-ppu` / `--cam-tx` / `--cam-ty` custom
3078
+ * properties (written by applyCamera each frame). Transparent and
3079
+ * click-through; each card re-enables its own pointer events so the
3080
+ * full-cover layer never swallows canvas clicks (issue #57). */
3081
+ cardLayer: {
3082
+ position: "absolute",
3083
+ inset: "0",
3084
+ pointerEvents: "none"
3085
+ },
3071
3086
  /* Links draw in pixel space; tone → token mapping lives here so
3072
3087
  * data only ever names a tone. */
3073
3088
  underlay: {
@@ -3264,6 +3279,7 @@ const schematicSlotRecipe = defineSlotRecipe({
3264
3279
  item: {
3265
3280
  position: "absolute",
3266
3281
  transform: "translate(-50%, -50%)",
3282
+ pointerEvents: "auto",
3267
3283
  display: "flex",
3268
3284
  flexDirection: "column",
3269
3285
  gap: "2px",
@@ -3459,14 +3475,24 @@ const schematicSlotRecipe = defineSlotRecipe({
3459
3475
  minHeight: "0",
3460
3476
  overflowY: "auto",
3461
3477
  paddingBottom: "{spacing.2}",
3462
- lineHeight: "1.7"
3463
- },
3478
+ lineHeight: "1.7",
3479
+ // Row height shared by the sticky zone headers (their stacked `top`
3480
+ // offset is a multiple of this) so an ancestor chain pins flush.
3481
+ "--nav-row-h": "22px"
3482
+ },
3483
+ /* Zone headers pin to the top of the scroll area like a tree view: each
3484
+ * depth level sticks at `top = depth × --nav-row-h` (set inline), so the
3485
+ * area (and its ancestors) stays visible while you scroll its items.
3486
+ * The opaque surface + z-index occlude the items scrolling underneath. */
3464
3487
  navZone: {
3465
3488
  display: "flex",
3466
3489
  alignItems: "center",
3467
3490
  gap: "{spacing.1}",
3468
- paddingY: "2px",
3469
- paddingRight: "{spacing.3}"
3491
+ height: "var(--nav-row-h)",
3492
+ paddingRight: "{spacing.3}",
3493
+ position: "sticky",
3494
+ background: "bg.surface",
3495
+ zIndex: "1"
3470
3496
  },
3471
3497
  navCaret: {
3472
3498
  background: "transparent",
@@ -3513,6 +3539,9 @@ const schematicSlotRecipe = defineSlotRecipe({
3513
3539
  border: "none",
3514
3540
  paddingY: "2px",
3515
3541
  paddingRight: "{spacing.3}",
3542
+ // Keep the auto-scroll-to-selected item clear of the pinned zone
3543
+ // headers (up to two ancestor levels).
3544
+ scrollMarginTop: "calc(var(--nav-row-h) * 2)",
3516
3545
  fontFamily: "mono",
3517
3546
  fontSize: "11px",
3518
3547
  letterSpacing: "0.06em",
@@ -61603,6 +61632,83 @@ function multiSelect(arr, left, right, n2, compare) {
61603
61632
  stack.push(left, mid, mid, right);
61604
61633
  }
61605
61634
  }
61635
+ const IDENTITY = { zoom: 1, tx: 0, ty: 0 };
61636
+ function project(x2, y2, cam) {
61637
+ return { sx: x2 * cam.ppu + cam.tx, sy: y2 * cam.ppu + cam.ty };
61638
+ }
61639
+ function cardTranslateCss(x2, y2) {
61640
+ return `translate(calc(var(--cam-ppu) * ${x2} * 1px + var(--cam-tx)), calc(var(--cam-ppu) * ${y2} * 1px + var(--cam-ty)))`;
61641
+ }
61642
+ function cardWidthCss(width) {
61643
+ return `calc(var(--cam-ppu) * ${width} * 1px)`;
61644
+ }
61645
+ function viewportWorldBbox(cam, width, height) {
61646
+ return {
61647
+ minX: -cam.tx / cam.ppu,
61648
+ minY: -cam.ty / cam.ppu,
61649
+ maxX: (width - cam.tx) / cam.ppu,
61650
+ maxY: (height - cam.ty) / cam.ppu
61651
+ };
61652
+ }
61653
+ function bboxOverlaps(a2, b2) {
61654
+ return a2.maxX >= b2.minX && a2.minX <= b2.maxX && a2.maxY >= b2.minY && a2.minY <= b2.maxY;
61655
+ }
61656
+ function pointsBbox(pts) {
61657
+ if (pts.length === 0) return null;
61658
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
61659
+ for (const p2 of pts) {
61660
+ if (p2.x < minX) minX = p2.x;
61661
+ if (p2.y < minY) minY = p2.y;
61662
+ if (p2.x > maxX) maxX = p2.x;
61663
+ if (p2.y > maxY) maxY = p2.y;
61664
+ }
61665
+ return { minX, minY, maxX, maxY };
61666
+ }
61667
+ function zoomAbout(prev, factor, px, py, min2, max2) {
61668
+ const zoom = Math.max(min2, Math.min(max2, prev.zoom * factor));
61669
+ if (zoom === 1) return IDENTITY;
61670
+ const k2 = zoom / prev.zoom;
61671
+ return { zoom, tx: px - k2 * (px - prev.tx), ty: py - k2 * (py - prev.ty) };
61672
+ }
61673
+ function nextMode(mode, event) {
61674
+ switch (event) {
61675
+ case "pointerDown":
61676
+ return "panning";
61677
+ case "wheel":
61678
+ return "wheeling";
61679
+ case "zoom":
61680
+ return "wheeling";
61681
+ case "flyStart":
61682
+ return "flying";
61683
+ case "pointerUp":
61684
+ return mode === "panning" ? "idle" : mode;
61685
+ case "flyEnd":
61686
+ return mode === "flying" ? "idle" : mode;
61687
+ case "cancel":
61688
+ return "idle";
61689
+ }
61690
+ }
61691
+ function cancelsFly(mode, event) {
61692
+ return mode === "flying" && (event === "pointerDown" || event === "wheel" || event === "zoom" || event === "cancel");
61693
+ }
61694
+ function makeRafCoalescer(schedule, cancel) {
61695
+ let handle2 = null;
61696
+ return {
61697
+ request(run) {
61698
+ if (handle2 !== null) return;
61699
+ handle2 = schedule(() => {
61700
+ handle2 = null;
61701
+ run();
61702
+ });
61703
+ },
61704
+ cancel() {
61705
+ if (handle2 !== null) {
61706
+ cancel(handle2);
61707
+ handle2 = null;
61708
+ }
61709
+ }
61710
+ };
61711
+ }
61606
61712
  const css = (c2, a2 = 1) => a2 >= 1 ? `rgb(${c2[0]},${c2[1]},${c2[2]})` : `rgba(${c2[0]},${c2[1]},${c2[2]},${a2})`;
61607
61713
  function toneRGB(p2, tone, kind) {
61608
61714
  switch (tone) {
@@ -61683,6 +61789,7 @@ function arcFromBulge(p1, p2, bulge) {
61683
61789
  const mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2;
61684
61790
  const d2 = chord / 2 / Math.tan(theta / 2);
61685
61791
  const cx2 = mx - dy / chord * d2, cy = my + dx / chord * d2;
61792
+ if (!Number.isFinite(radius) || !Number.isFinite(d2) || !Number.isFinite(cx2) || !Number.isFinite(cy)) return null;
61686
61793
  return {
61687
61794
  cx: cx2,
61688
61795
  cy,
@@ -61710,22 +61817,76 @@ function traceVertices(ctx, pts, closed) {
61710
61817
  ctx.closePath();
61711
61818
  }
61712
61819
  }
61820
+ const MARKER_LABEL_FONT = '600 10px ui-monospace, "SF Mono", Menlo, monospace';
61821
+ const MARKER_DOT_RADIUS = 5;
61822
+ const MARKER_DOT_HIT_RADIUS = 10;
61823
+ const MARKER_PIN_PAD_X = 6, MARKER_PIN_DOT_W = 7, MARKER_PIN_GAP = 4, MARKER_PIN_H = 16;
61824
+ function markerHitbox(item, tier, cam, measure) {
61825
+ const { sx, sy } = project(item.x, item.y, cam);
61826
+ if (tier === "dot") return { kind: "circle", cx: sx, cy: sy, r: MARKER_DOT_HIT_RADIUS };
61827
+ const tw = measure(item.label);
61828
+ const w2 = MARKER_PIN_PAD_X + MARKER_PIN_DOT_W + MARKER_PIN_GAP + tw + MARKER_PIN_PAD_X;
61829
+ return { kind: "rect", left: sx - w2 / 2, top: sy - MARKER_PIN_H / 2, w: w2, h: MARKER_PIN_H };
61830
+ }
61831
+ function markerHit(box, sx, sy) {
61832
+ return box.kind === "circle" ? Math.hypot(sx - box.cx, sy - box.cy) <= box.r : sx >= box.left && sx <= box.left + box.w && sy >= box.top && sy <= box.top + box.h;
61833
+ }
61834
+ function hatchSpacing(rawSpacing) {
61835
+ return Number.isFinite(rawSpacing) ? Math.max(1, rawSpacing) : 1;
61836
+ }
61837
+ function hatchSweepBounds(x2, y2, w2, h2, width, height, dx, dy, diag, spacing) {
61838
+ const nx = -dy, ny = dx;
61839
+ if (Math.abs(nx) <= 1e-6) return { oStart: -diag, oEnd: diag };
61840
+ const cx0 = Math.max(x2, 0), cy0 = Math.max(y2, 0);
61841
+ const cx1 = Math.min(x2 + w2, width), cy1 = Math.min(y2 + h2, height);
61842
+ const cs = [cx0 * nx + cy0 * ny, cx1 * nx + cy0 * ny, cx0 * nx + cy1 * ny, cx1 * nx + cy1 * ny];
61843
+ const oOfC = (c2) => (c2 - x2 * nx - (y2 - diag * dy) * ny) / nx;
61844
+ const oA = oOfC(Math.min(...cs)), oB = oOfC(Math.max(...cs));
61845
+ return {
61846
+ oStart: Math.max(-diag, -diag + Math.floor((Math.min(oA, oB) + diag) / spacing) * spacing - spacing),
61847
+ oEnd: Math.min(diag, Math.max(oA, oB) + spacing)
61848
+ };
61849
+ }
61850
+ function zoneWorldBbox(zone, geom) {
61851
+ const bb = { minX: zone.x, minY: zone.y, maxX: zone.x + zone.width, maxY: zone.y + zone.height };
61852
+ if (geom !== void 0 && geom.type === "circle") {
61853
+ const cx2 = zone.x + zone.width / 2, cy = zone.y + zone.height / 2, r2 = geom.value.radius;
61854
+ bb.minX = Math.min(bb.minX, cx2 - r2);
61855
+ bb.minY = Math.min(bb.minY, cy - r2);
61856
+ bb.maxX = Math.max(bb.maxX, cx2 + r2);
61857
+ bb.maxY = Math.max(bb.maxY, cy + r2);
61858
+ } else if (geom !== void 0 && geom.type !== "rect") {
61859
+ const g2 = pointsBbox(geom.value.vertices);
61860
+ if (g2 !== null) {
61861
+ bb.minX = Math.min(bb.minX, g2.minX);
61862
+ bb.minY = Math.min(bb.minY, g2.minY);
61863
+ bb.maxX = Math.max(bb.maxX, g2.maxX);
61864
+ bb.maxY = Math.max(bb.maxY, g2.maxY);
61865
+ }
61866
+ }
61867
+ return bb;
61868
+ }
61713
61869
  function paintSchematic(input) {
61714
61870
  var _a2, _b, _c, _d;
61715
61871
  const { ctx, value, cam, width, height, visibleItems, tiers, selected, centers, palette: p2 } = input;
61716
61872
  const wx = (x2) => x2 * cam.ppu + cam.tx;
61717
61873
  const wy = (y2) => y2 * cam.ppu + cam.ty;
61718
61874
  const ppu = cam.ppu;
61719
- ctx.clearRect(0, 0, width, height);
61875
+ const viewBbox = viewportWorldBbox(cam, width, height);
61876
+ ctx.save();
61877
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
61878
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
61879
+ ctx.restore();
61720
61880
  ctx.lineJoin = "round";
61721
61881
  for (const zone of value.zones) {
61882
+ const geom = getSomeorUndefined(zone.geometry);
61883
+ if (!bboxOverlaps(zoneWorldBbox(zone, geom), viewBbox)) continue;
61722
61884
  const pattern = zone.pattern;
61723
61885
  const patternTone = (pattern.value.tone.type === "some" ? pattern.value.tone.value.type : void 0) ?? "muted";
61724
61886
  const tint = resolveTint(p2, getSomeorUndefined(zone.color), (_a2 = getSomeorUndefined(zone.tone)) == null ? void 0 : _a2.type, toneRGB(p2, patternTone, "zone"));
61725
61887
  const zbg = getSomeorUndefined(zone.bg);
61726
61888
  const zFillAlpha = getSomeorUndefined(zone.fillOpacity) ?? 0.15;
61727
61889
  const zWeight = getSomeorUndefined(zone.weight);
61728
- const geom = getSomeorUndefined(zone.geometry);
61729
61890
  const x2 = wx(zone.x), y2 = wy(zone.y), w2 = zone.width * ppu, h2 = zone.height * ppu;
61730
61891
  const fillShape = () => {
61731
61892
  if (zbg === void 0) return;
@@ -61773,8 +61934,8 @@ function paintSchematic(input) {
61773
61934
  }
61774
61935
  }
61775
61936
  } else if (pattern.type === "hatch") {
61776
- const spacing = pattern.value.spacing.type === "some" ? pattern.value.spacing.value : 8;
61777
- const angle = (pattern.value.angle.type === "some" ? pattern.value.angle.value : 45) * Math.PI / 180;
61937
+ const spacing = hatchSpacing(getSomeorUndefined(pattern.value.spacing) ?? 8);
61938
+ const angle = (getSomeorUndefined(pattern.value.angle) ?? 45) * Math.PI / 180;
61778
61939
  ctx.save();
61779
61940
  ctx.beginPath();
61780
61941
  ctx.rect(x2, y2, w2, h2);
@@ -61791,8 +61952,9 @@ function paintSchematic(input) {
61791
61952
  ctx.lineWidth = zWeight ?? 1;
61792
61953
  const dx = Math.cos(angle), dy = Math.sin(angle);
61793
61954
  const diag = Math.hypot(w2, h2);
61955
+ const { oStart, oEnd } = hatchSweepBounds(x2, y2, w2, h2, width, height, dx, dy, diag, spacing);
61794
61956
  ctx.beginPath();
61795
- for (let o2 = -diag; o2 < diag; o2 += spacing) {
61957
+ for (let o2 = oStart; o2 < oEnd; o2 += spacing) {
61796
61958
  ctx.moveTo(x2 + o2, y2 - diag * dy);
61797
61959
  ctx.lineTo(x2 + o2 + dx * 2 * diag, y2 - diag * dy + dy * 2 * diag);
61798
61960
  }
@@ -61826,6 +61988,8 @@ function paintSchematic(input) {
61826
61988
  for (const link2 of value.links) {
61827
61989
  const from = centers.get(link2.from), to = centers.get(link2.to);
61828
61990
  if (!from || !to) continue;
61991
+ const linkBbox = pointsBbox([from, ...link2.via, to]);
61992
+ if (linkBbox !== null && !bboxOverlaps(linkBbox, viewBbox)) continue;
61829
61993
  const anchors = [from, ...link2.via, to].map((q) => ({ x: wx(q.x), y: wy(q.y) }));
61830
61994
  const corner = link2.route.type === "orthogonal" ? link2.route.value.corner.type === "some" ? link2.route.value.corner.value : 8 : 0;
61831
61995
  const pts = link2.route.type === "orthogonal" ? orthogonalize(anchors) : anchors;
@@ -61899,7 +62063,7 @@ function paintSchematic(input) {
61899
62063
  const x2 = wx(item.x), y2 = wy(item.y);
61900
62064
  if (tier === "dot") {
61901
62065
  ctx.beginPath();
61902
- ctx.arc(x2, y2, 5, 0, Math.PI * 2);
62066
+ ctx.arc(x2, y2, MARKER_DOT_RADIUS, 0, Math.PI * 2);
61903
62067
  ctx.fillStyle = tint;
61904
62068
  ctx.fill();
61905
62069
  ctx.lineWidth = 1.5;
@@ -61914,11 +62078,10 @@ function paintSchematic(input) {
61914
62078
  }
61915
62079
  continue;
61916
62080
  }
61917
- ctx.font = '600 10px ui-monospace, "SF Mono", Menlo, monospace';
61918
- const tw = ctx.measureText(item.label).width;
61919
- const padX = 6, dotW = 7, gap = 4, h2 = 16;
61920
- const w2 = padX + dotW + gap + tw + padX;
61921
- const left = x2 - w2 / 2, top = y2 - h2 / 2;
62081
+ ctx.font = MARKER_LABEL_FONT;
62082
+ const box = markerHitbox(item, "label", cam, (t4) => ctx.measureText(t4).width);
62083
+ if (box.kind !== "rect") continue;
62084
+ const { left, top, w: w2, h: h2 } = box;
61922
62085
  ctx.beginPath();
61923
62086
  (_d = ctx.roundRect) == null ? void 0 : _d.call(ctx, left, top, w2, h2, h2 / 2);
61924
62087
  if (!ctx.roundRect) ctx.rect(left, top, w2, h2);
@@ -61928,12 +62091,12 @@ function paintSchematic(input) {
61928
62091
  ctx.strokeStyle = isSel ? css(p2.fg) : css(p2.borderStrong);
61929
62092
  ctx.stroke();
61930
62093
  ctx.beginPath();
61931
- ctx.arc(left + padX + dotW / 2, y2, dotW / 2, 0, Math.PI * 2);
62094
+ ctx.arc(left + MARKER_PIN_PAD_X + MARKER_PIN_DOT_W / 2, y2, MARKER_PIN_DOT_W / 2, 0, Math.PI * 2);
61932
62095
  ctx.fillStyle = tint;
61933
62096
  ctx.fill();
61934
62097
  ctx.fillStyle = css(p2.fg);
61935
62098
  ctx.textBaseline = "middle";
61936
- ctx.fillText(item.label, left + padX + dotW + gap, y2);
62099
+ ctx.fillText(item.label, left + MARKER_PIN_PAD_X + MARKER_PIN_DOT_W + MARKER_PIN_GAP, y2);
61937
62100
  }
61938
62101
  }
61939
62102
  const TOKENS = {
@@ -61980,7 +62143,6 @@ const SchematicPaletteProbe = memo$2(function SchematicPaletteProbe2({ onResolve
61980
62143
  });
61981
62144
  library$1.add(icons$2);
61982
62145
  const schematicEqual = equalFor(Schematic.Types.Schematic);
61983
- const IDENTITY = { zoom: 1, tx: 0, ty: 0 };
61984
62146
  const MAX_ZOOM = 40;
61985
62147
  const LOD_CARD_PPU = 30;
61986
62148
  const LOD_LABEL_PPU = 16;
@@ -62085,7 +62247,7 @@ function declutterTiers(items, tree, baseLod, ppu, selected) {
62085
62247
  if (selected !== null && tiers.has(selected)) tiers.set(selected, baseLod);
62086
62248
  return tiers;
62087
62249
  }
62088
- const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62250
+ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value, storageKey }) {
62089
62251
  const styles2 = useSlotRecipe({ key: "schematic" })();
62090
62252
  const { width: W2, height: H } = value.extent;
62091
62253
  const onSelectFn = useMemo(() => getSomeorUndefined(value.onSelect), [value.onSelect]);
@@ -62095,14 +62257,17 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62095
62257
  const showMinimap = getSomeorUndefined(value.minimap) ?? value.items.length >= MINIMAP_AUTO;
62096
62258
  const fixedHeight = getSomeorUndefined(value.height);
62097
62259
  const [selected, setSelected] = useState(null);
62098
- const [view, setView] = useState(IDENTITY);
62099
62260
  const [query, setQuery] = useState("");
62100
62261
  const [openZone, setOpenZone] = useState(null);
62101
- const [navCollapsed, setNavCollapsed] = useState(false);
62262
+ const { state: persisted, setState: setPersisted } = usePersistedState(storageKey, { navCollapsed: false });
62263
+ const navCollapsed = persisted.navCollapsed;
62264
+ const setNavCollapsed = useCallback((next) => setPersisted((prev) => ({ ...prev, navCollapsed: next })), [setPersisted]);
62102
62265
  const [palette, setPalette] = useState(null);
62103
62266
  const navTreeRef = useRef(null);
62104
62267
  const canvasRef = useRef(null);
62105
62268
  const drawRef = useRef(null);
62269
+ const cardLayerRef = useRef(null);
62270
+ const gridRef = useRef(null);
62106
62271
  const [size, setSize] = useState(null);
62107
62272
  useLayoutEffect(() => {
62108
62273
  const el = canvasRef.current;
@@ -62113,10 +62278,30 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62113
62278
  ro.observe(el);
62114
62279
  return () => ro.disconnect();
62115
62280
  }, []);
62281
+ const [dpr, setDpr] = useState(() => typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1);
62282
+ useEffect(() => {
62283
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
62284
+ let cleanup = () => {
62285
+ };
62286
+ const subscribe2 = () => {
62287
+ const mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
62288
+ const onChange2 = () => {
62289
+ cleanup();
62290
+ setDpr(window.devicePixelRatio || 1);
62291
+ subscribe2();
62292
+ };
62293
+ mql.addEventListener("change", onChange2);
62294
+ cleanup = () => mql.removeEventListener("change", onChange2);
62295
+ };
62296
+ subscribe2();
62297
+ return () => cleanup();
62298
+ }, []);
62299
+ const cameraRef = useRef(IDENTITY);
62300
+ const modeRef = useRef("idle");
62301
+ const animRef = useRef(null);
62302
+ const [cameraSnapshot, setCameraSnapshot] = useState(IDENTITY);
62116
62303
  const fit = size !== null ? Math.min(size.w / W2, size.h / H) : 0;
62117
- const ppu = fit * view.zoom;
62118
- const wx = (x2) => x2 * ppu + view.tx;
62119
- const wy = (y2) => y2 * ppu + view.ty;
62304
+ const ppu = fit * cameraSnapshot.zoom;
62120
62305
  const centers = useMemo(() => {
62121
62306
  const out = /* @__PURE__ */ new Map();
62122
62307
  for (const item of value.items) out.set(item.key, { x: item.x, y: item.y });
@@ -62149,12 +62334,12 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62149
62334
  const visibleItems = useMemo(() => {
62150
62335
  if (size === null || ppu === 0) return [];
62151
62336
  return index2.search({
62152
- minX: -view.tx / ppu,
62153
- minY: -view.ty / ppu,
62154
- maxX: (size.w - view.tx) / ppu,
62155
- maxY: (size.h - view.ty) / ppu
62337
+ minX: -cameraSnapshot.tx / ppu,
62338
+ minY: -cameraSnapshot.ty / ppu,
62339
+ maxX: (size.w - cameraSnapshot.tx) / ppu,
62340
+ maxY: (size.h - cameraSnapshot.ty) / ppu
62156
62341
  }).map((b2) => b2.item);
62157
- }, [index2, size, ppu, view]);
62342
+ }, [index2, size, ppu, cameraSnapshot]);
62158
62343
  const nav = useMemo(() => buildNavTree(value.zones, value.items), [value.zones, value.items]);
62159
62344
  const zoneDisplay = useMemo(() => {
62160
62345
  const counts = /* @__PURE__ */ new Map();
@@ -62180,106 +62365,15 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62180
62365
  const indexOf = new Map(ordered.map((item, i2) => [item.key, i2]));
62181
62366
  return { ordered, indexOf };
62182
62367
  }, [nav]);
62183
- const animRef = useRef(null);
62184
- const viewRef = useRef(view);
62185
- viewRef.current = view;
62186
- const flyTo = useCallback((rect) => {
62187
- if (size === null || fit === 0) return;
62188
- const pad2 = 1.18;
62189
- const zoom = Math.min(MAX_ZOOM, Math.min(size.w / (rect.w * pad2 * fit), size.h / (rect.h * pad2 * fit)));
62190
- const target = {
62191
- zoom,
62192
- tx: size.w / 2 - (rect.x + rect.w / 2) * fit * zoom,
62193
- ty: size.h / 2 - (rect.y + rect.h / 2) * fit * zoom
62194
- };
62195
- const from = viewRef.current;
62196
- const start = performance.now();
62197
- if (animRef.current !== null) cancelAnimationFrame(animRef.current);
62198
- const step = (now) => {
62199
- const t4 = Math.min(1, (now - start) / 350);
62200
- const e3 = 1 - Math.pow(1 - t4, 3);
62201
- setView({
62202
- zoom: from.zoom + (target.zoom - from.zoom) * e3,
62203
- tx: from.tx + (target.tx - from.tx) * e3,
62204
- ty: from.ty + (target.ty - from.ty) * e3
62205
- });
62206
- if (t4 < 1) animRef.current = requestAnimationFrame(step);
62207
- };
62208
- animRef.current = requestAnimationFrame(step);
62209
- }, [size, fit]);
62210
- useEffect(() => () => {
62211
- if (animRef.current !== null) cancelAnimationFrame(animRef.current);
62212
- }, []);
62213
- const flyToItem = useCallback((item) => {
62214
- flyTo({ x: item.x - 4, y: item.y - 3, w: 8, h: 6 });
62215
- setSelected(item.key);
62216
- setOpenZone(nav.zoneOf.get(item.key) ?? null);
62217
- if (onSelectFn) queueMicrotask(() => onSelectFn(item.key));
62218
- }, [flyTo, nav.zoneOf, onSelectFn]);
62219
- const stepSelection = useCallback((delta) => {
62220
- const { ordered, indexOf } = itemOrder;
62221
- if (ordered.length === 0) return;
62222
- const idx = selected !== null ? indexOf.get(selected) ?? -1 : -1;
62223
- flyToItem(ordered[(idx + delta + ordered.length) % ordered.length]);
62224
- }, [itemOrder, selected, flyToItem]);
62225
- const flyToZone = useCallback((zone) => {
62226
- flyTo({ x: zone.x, y: zone.y, w: zone.width, h: zone.height });
62227
- setOpenZone(zone.key);
62228
- }, [flyTo]);
62229
- useEffect(() => {
62230
- const el = canvasRef.current;
62231
- if (!el) return;
62232
- const onWheel = (e3) => {
62233
- e3.preventDefault();
62234
- const rect = el.getBoundingClientRect();
62235
- const px = e3.clientX - rect.left, py = e3.clientY - rect.top;
62236
- setView((prev) => {
62237
- const zoom = Math.max(1, Math.min(MAX_ZOOM, prev.zoom * Math.exp(-e3.deltaY * 15e-4)));
62238
- const k2 = zoom / prev.zoom;
62239
- const next = { zoom, tx: px - k2 * (px - prev.tx), ty: py - k2 * (py - prev.ty) };
62240
- return zoom === 1 ? IDENTITY : next;
62241
- });
62242
- };
62243
- el.addEventListener("wheel", onWheel, { passive: false });
62244
- return () => el.removeEventListener("wheel", onWheel);
62245
- }, []);
62246
- const panRef = useRef(null);
62247
- const movedRef = useRef(false);
62248
- const onCanvasPointerDown = useCallback((e3) => {
62249
- if (e3.button !== 0) return;
62250
- if (e3.target.closest("button") !== null) return;
62251
- panRef.current = { x: e3.clientX, y: e3.clientY, tx: viewRef.current.tx, ty: viewRef.current.ty };
62252
- movedRef.current = false;
62253
- e3.currentTarget.setPointerCapture(e3.pointerId);
62254
- }, []);
62255
- const onCanvasPointerMove = useCallback((e3) => {
62256
- const pan = panRef.current;
62257
- if (!pan) return;
62258
- if (Math.abs(e3.clientX - pan.x) > 4 || Math.abs(e3.clientY - pan.y) > 4) movedRef.current = true;
62259
- setView((prev) => ({ ...prev, tx: pan.tx + e3.clientX - pan.x, ty: pan.ty + e3.clientY - pan.y }));
62260
- }, []);
62261
- const onCanvasPointerUp = useCallback(() => {
62262
- panRef.current = null;
62263
- }, []);
62264
- const resetView = useCallback(() => setView(IDENTITY), []);
62265
- const zoomBy = useCallback((factor) => {
62266
- if (size === null) return;
62267
- const px = size.w / 2, py = size.h / 2;
62268
- setView((prev) => {
62269
- const zoom = Math.max(1, Math.min(MAX_ZOOM, prev.zoom * factor));
62270
- const k2 = zoom / prev.zoom;
62271
- return zoom === 1 ? IDENTITY : { zoom, tx: px - k2 * (px - prev.tx), ty: py - k2 * (py - prev.ty) };
62272
- });
62273
- }, [size]);
62274
62368
  const [currentZone, setCurrentZone] = useState(void 0);
62275
62369
  useEffect(() => {
62276
62370
  const SPY_ENTER = 0.3, SPY_STAY = 0.18, SPY_MARGIN = 0.15;
62277
- if (size === null || ppu === 0 || view.zoom <= 1.05) {
62371
+ if (size === null || ppu === 0 || cameraSnapshot.zoom <= 1.05) {
62278
62372
  setCurrentZone(void 0);
62279
62373
  return;
62280
62374
  }
62281
- const vx0 = -view.tx / ppu, vy0 = -view.ty / ppu;
62282
- const vx1 = (size.w - view.tx) / ppu, vy1 = (size.h - view.ty) / ppu;
62375
+ const vx0 = -cameraSnapshot.tx / ppu, vy0 = -cameraSnapshot.ty / ppu;
62376
+ const vx1 = (size.w - cameraSnapshot.tx) / ppu, vy1 = (size.h - cameraSnapshot.ty) / ppu;
62283
62377
  const viewArea = (vx1 - vx0) * (vy1 - vy0);
62284
62378
  const scoreOf = (z) => {
62285
62379
  const ix = Math.max(0, Math.min(vx1, z.x + z.width) - Math.max(vx0, z.x));
@@ -62303,73 +62397,321 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62303
62397
  }
62304
62398
  return bestScore >= SPY_ENTER ? best : void 0;
62305
62399
  });
62306
- }, [size, ppu, view, value.zones]);
62400
+ }, [size, ppu, cameraSnapshot, value.zones]);
62307
62401
  const lod = ppu >= LOD_CARD_PPU ? "card" : ppu >= LOD_LABEL_PPU ? "label" : "dot";
62308
62402
  const centerTree = useMemo(() => buildCenterTree(visibleItems), [visibleItems]);
62309
62403
  const tiers = useMemo(() => declutterTiers(visibleItems, centerTree, lod, ppu, selected), [visibleItems, centerTree, lod, ppu, selected]);
62310
62404
  const lowerQuery = query.trim().toLowerCase();
62311
62405
  const searchHits = useMemo(() => lowerQuery === "" ? [] : value.items.filter((i2) => i2.key.toLowerCase().includes(lowerQuery) || i2.label.toLowerCase().includes(lowerQuery)).slice(0, 12), [lowerQuery, value.items]);
62312
62406
  const scaleLen = ppu > 0 ? niceScaleLength(ppu, 100) : 0;
62313
- useEffect(() => {
62314
- const canvas = drawRef.current;
62315
- if (canvas === null || size === null || palette === null) return;
62316
- const dpr = window.devicePixelRatio || 1;
62317
- const bw = Math.round(size.w * dpr), bh = Math.round(size.h * dpr);
62407
+ const valueRef = useRef(value);
62408
+ valueRef.current = value;
62409
+ const sizeRef = useRef(size);
62410
+ sizeRef.current = size;
62411
+ const fitRef = useRef(fit);
62412
+ fitRef.current = fit;
62413
+ const dprRef = useRef(dpr);
62414
+ dprRef.current = dpr;
62415
+ const paletteRef = useRef(palette);
62416
+ paletteRef.current = palette;
62417
+ const visibleItemsRef = useRef(visibleItems);
62418
+ visibleItemsRef.current = visibleItems;
62419
+ const tiersRef = useRef(tiers);
62420
+ tiersRef.current = tiers;
62421
+ const selectedRef = useRef(selected);
62422
+ selectedRef.current = selected;
62423
+ const centersRef = useRef(centers);
62424
+ centersRef.current = centers;
62425
+ const openZoneRef = useRef(openZone);
62426
+ openZoneRef.current = openZone;
62427
+ const lastPaintRef = useRef(null);
62428
+ const applyCamera = useCallback(() => {
62429
+ const sz = sizeRef.current;
62430
+ if (sz === null) return;
62431
+ const cam = cameraRef.current;
62432
+ const ppuLive = fitRef.current * cam.zoom;
62433
+ const dprLive = dprRef.current;
62434
+ const layer = cardLayerRef.current;
62435
+ if (layer !== null) {
62436
+ layer.style.setProperty("--cam-ppu", String(ppuLive));
62437
+ layer.style.setProperty("--cam-tx", `${cam.tx}px`);
62438
+ layer.style.setProperty("--cam-ty", `${cam.ty}px`);
62439
+ }
62440
+ const grid = gridRef.current;
62441
+ if (grid !== null) {
62442
+ const len = ppuLive > 0 ? niceScaleLength(ppuLive, 100) : 0;
62443
+ if (len > 0) {
62444
+ const major = len * ppuLive, minor = major / 5;
62445
+ grid.style.backgroundSize = `${major}px ${major}px, ${major}px ${major}px, ${minor}px ${minor}px, ${minor}px ${minor}px`;
62446
+ grid.style.backgroundPosition = `${cam.tx}px 0, 0 ${cam.ty}px, ${cam.tx}px 0, 0 ${cam.ty}px`;
62447
+ }
62448
+ }
62449
+ const canvas = drawRef.current, pal = paletteRef.current;
62450
+ if (canvas === null || pal === null) return;
62451
+ const last = lastPaintRef.current;
62452
+ if (last !== null && last.zoom === cam.zoom && last.tx === cam.tx && last.ty === cam.ty && last.vis === visibleItemsRef.current && last.tiers === tiersRef.current && last.sel === selectedRef.current && last.pal === pal && last.w === sz.w && last.h === sz.h && last.dpr === dprLive && last.val === valueRef.current) return;
62453
+ lastPaintRef.current = {
62454
+ zoom: cam.zoom,
62455
+ tx: cam.tx,
62456
+ ty: cam.ty,
62457
+ vis: visibleItemsRef.current,
62458
+ tiers: tiersRef.current,
62459
+ sel: selectedRef.current,
62460
+ pal,
62461
+ w: sz.w,
62462
+ h: sz.h,
62463
+ dpr: dprLive,
62464
+ val: valueRef.current
62465
+ };
62466
+ const bw = Math.round(sz.w * dprLive), bh = Math.round(sz.h * dprLive);
62318
62467
  if (canvas.width !== bw || canvas.height !== bh) {
62319
62468
  canvas.width = bw;
62320
62469
  canvas.height = bh;
62321
62470
  }
62322
62471
  const ctx = canvas.getContext("2d");
62323
62472
  if (ctx === null) return;
62324
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
62473
+ ctx.setTransform(dprLive, 0, 0, dprLive, 0, 0);
62325
62474
  paintSchematic({
62326
62475
  ctx,
62327
- value,
62328
- cam: { ppu, tx: view.tx, ty: view.ty },
62329
- width: size.w,
62330
- height: size.h,
62331
- visibleItems,
62332
- tiers,
62333
- selected,
62334
- centers,
62335
- palette
62476
+ value: valueRef.current,
62477
+ cam: { ppu: ppuLive, tx: cam.tx, ty: cam.ty },
62478
+ width: sz.w,
62479
+ height: sz.h,
62480
+ visibleItems: visibleItemsRef.current,
62481
+ tiers: tiersRef.current,
62482
+ selected: selectedRef.current,
62483
+ centers: centersRef.current,
62484
+ palette: pal
62336
62485
  });
62337
- }, [value, view, ppu, size, visibleItems, tiers, selected, centers, palette]);
62338
- const onCanvasClick = useCallback((e3) => {
62339
- if (size === null || movedRef.current) return;
62340
- const rect = e3.currentTarget.getBoundingClientRect();
62341
- const sx = e3.clientX - rect.left, sy = e3.clientY - rect.top;
62342
- const wxp = (sx - view.tx) / ppu, wyp = (sy - view.ty) / ppu;
62343
- for (const box of index2.search({ minX: wxp, minY: wyp, maxX: wxp, maxY: wyp })) {
62344
- const it = box.item;
62486
+ }, []);
62487
+ const coalescerRef = useRef(null);
62488
+ if (coalescerRef.current === null && typeof requestAnimationFrame !== "undefined") {
62489
+ coalescerRef.current = makeRafCoalescer(requestAnimationFrame, cancelAnimationFrame);
62490
+ }
62491
+ const requestRender = useCallback(() => {
62492
+ const run = () => {
62493
+ applyCamera();
62494
+ const cam = cameraRef.current;
62495
+ setCameraSnapshot((prev) => prev.zoom === cam.zoom && prev.tx === cam.tx && prev.ty === cam.ty ? prev : { zoom: cam.zoom, tx: cam.tx, ty: cam.ty });
62496
+ };
62497
+ const cz = coalescerRef.current;
62498
+ if (cz !== null) cz.request(run);
62499
+ else run();
62500
+ }, [applyCamera]);
62501
+ const cancelFlyRaf = useCallback(() => {
62502
+ if (animRef.current !== null) {
62503
+ cancelAnimationFrame(animRef.current);
62504
+ animRef.current = null;
62505
+ }
62506
+ }, []);
62507
+ const transition = useCallback((event) => {
62508
+ if (cancelsFly(modeRef.current, event)) cancelFlyRaf();
62509
+ modeRef.current = nextMode(modeRef.current, event);
62510
+ }, [cancelFlyRaf]);
62511
+ const flyTo = useCallback((rect) => {
62512
+ if (sizeRef.current === null || fitRef.current === 0) return;
62513
+ const pad2 = 1.18;
62514
+ const frame = () => {
62515
+ const sz = sizeRef.current, ft = fitRef.current;
62516
+ if (sz === null || ft === 0) return null;
62517
+ const zoom = Math.min(MAX_ZOOM, Math.min(sz.w / (rect.w * pad2 * ft), sz.h / (rect.h * pad2 * ft)));
62518
+ return {
62519
+ zoom,
62520
+ tx: sz.w / 2 - (rect.x + rect.w / 2) * ft * zoom,
62521
+ ty: sz.h / 2 - (rect.y + rect.h / 2) * ft * zoom
62522
+ };
62523
+ };
62524
+ const from = { ...cameraRef.current };
62525
+ const start = performance.now();
62526
+ if (animRef.current !== null) cancelAnimationFrame(animRef.current);
62527
+ transition("flyStart");
62528
+ const step = (now) => {
62529
+ const t4 = Math.min(1, (now - start) / 350);
62530
+ const e3 = 1 - Math.pow(1 - t4, 3);
62531
+ const target = frame();
62532
+ if (target !== null) {
62533
+ cameraRef.current = {
62534
+ zoom: from.zoom + (target.zoom - from.zoom) * e3,
62535
+ tx: from.tx + (target.tx - from.tx) * e3,
62536
+ ty: from.ty + (target.ty - from.ty) * e3
62537
+ };
62538
+ applyCamera();
62539
+ const cam = cameraRef.current;
62540
+ setCameraSnapshot({ zoom: cam.zoom, tx: cam.tx, ty: cam.ty });
62541
+ }
62542
+ if (t4 < 1) {
62543
+ animRef.current = requestAnimationFrame(step);
62544
+ } else {
62545
+ animRef.current = null;
62546
+ transition("flyEnd");
62547
+ }
62548
+ };
62549
+ animRef.current = requestAnimationFrame(step);
62550
+ }, [applyCamera, transition]);
62551
+ const zoneOverrideRef = useRef(null);
62552
+ const flyToItem = useCallback((item) => {
62553
+ flyTo({ x: item.x - 4, y: item.y - 3, w: 8, h: 6 });
62554
+ setSelected(item.key);
62555
+ zoneOverrideRef.current = null;
62556
+ setOpenZone(nav.zoneOf.get(item.key) ?? null);
62557
+ if (onSelectFn) queueMicrotask(() => onSelectFn(item.key));
62558
+ }, [flyTo, nav.zoneOf, onSelectFn]);
62559
+ const stepSelection = useCallback((delta) => {
62560
+ const { ordered, indexOf } = itemOrder;
62561
+ if (ordered.length === 0) return;
62562
+ const idx = selected !== null ? indexOf.get(selected) ?? -1 : -1;
62563
+ flyToItem(ordered[(idx + delta + ordered.length) % ordered.length]);
62564
+ }, [itemOrder, selected, flyToItem]);
62565
+ const flyToZone = useCallback((zone) => {
62566
+ flyTo({ x: zone.x, y: zone.y, w: zone.width, h: zone.height });
62567
+ zoneOverrideRef.current = null;
62568
+ setOpenZone(zone.key);
62569
+ }, [flyTo]);
62570
+ useEffect(() => {
62571
+ const el = canvasRef.current;
62572
+ if (!el) return;
62573
+ const onWheel = (e3) => {
62574
+ e3.preventDefault();
62575
+ const rect = el.getBoundingClientRect();
62576
+ const px = e3.clientX - rect.left, py = e3.clientY - rect.top;
62577
+ transition("wheel");
62578
+ cameraRef.current = zoomAbout(cameraRef.current, Math.exp(-e3.deltaY * 15e-4), px, py, 1, MAX_ZOOM);
62579
+ requestRender();
62580
+ };
62581
+ el.addEventListener("wheel", onWheel, { passive: false });
62582
+ return () => el.removeEventListener("wheel", onWheel);
62583
+ }, [transition, requestRender]);
62584
+ const panRef = useRef(null);
62585
+ const movedRef = useRef(false);
62586
+ const pickAt = useCallback((host, clientX, clientY) => {
62587
+ var _a2, _b, _c;
62588
+ const sz = sizeRef.current;
62589
+ if (sz === null) return false;
62590
+ const cam = cameraRef.current;
62591
+ const ppuLive = fitRef.current * cam.zoom;
62592
+ if (ppuLive === 0) return false;
62593
+ const rect = host.getBoundingClientRect();
62594
+ const sx = clientX - rect.left, sy = clientY - rect.top;
62595
+ const wxp = (sx - cam.tx) / ppuLive, wyp = (sy - cam.ty) / ppuLive;
62596
+ const select = (key) => {
62597
+ setSelected(key);
62598
+ if (onSelectFn) queueMicrotask(() => onSelectFn(key));
62599
+ };
62600
+ for (const it of visibleItems) {
62345
62601
  if ((tiers.get(it.key) ?? lod) !== "card") continue;
62346
62602
  const fp = getSomeorUndefined(it.footprint);
62347
62603
  if (fp === void 0 || fp.type === "rect") continue;
62348
62604
  const hit = fp.type === "circle" ? Math.hypot(wxp - it.x, wyp - it.y) <= fp.value.radius : pointInPolygon(wxp, wyp, fp.value.vertices);
62349
62605
  if (hit) {
62350
- setSelected(it.key);
62351
- if (onSelectFn) queueMicrotask(() => onSelectFn(it.key));
62352
- return;
62606
+ select(it.key);
62607
+ return true;
62608
+ }
62609
+ }
62610
+ if (typeof document !== "undefined") {
62611
+ const cardKey = (_b = (_a2 = document.elementFromPoint(clientX, clientY)) == null ? void 0 : _a2.closest("[data-card-key]")) == null ? void 0 : _b.getAttribute("data-card-key");
62612
+ if (cardKey !== null && cardKey !== void 0) {
62613
+ select(cardKey);
62614
+ return true;
62353
62615
  }
62354
62616
  }
62617
+ const ctx = ((_c = drawRef.current) == null ? void 0 : _c.getContext("2d")) ?? null;
62618
+ if (ctx !== null) {
62619
+ ctx.save();
62620
+ ctx.font = MARKER_LABEL_FONT;
62621
+ }
62622
+ const camScreen = { ppu: ppuLive, tx: cam.tx, ty: cam.ty };
62355
62623
  let best = null, bestD = Infinity;
62356
62624
  for (const it of visibleItems) {
62357
62625
  const tier = tiers.get(it.key) ?? lod;
62358
62626
  if (tier === "card") continue;
62359
- const d2 = Math.hypot(it.x * ppu + view.tx - sx, it.y * ppu + view.ty - sy);
62360
- const reach = tier === "dot" ? 9 : 18;
62361
- if (d2 < reach && d2 < bestD) {
62627
+ const box = markerHitbox(it, tier, camScreen, (t4) => ctx !== null ? ctx.measureText(t4).width : t4.length * 6);
62628
+ if (!markerHit(box, sx, sy)) continue;
62629
+ const d2 = Math.hypot(it.x * ppuLive + cam.tx - sx, it.y * ppuLive + cam.ty - sy);
62630
+ if (d2 < bestD) {
62362
62631
  best = it;
62363
62632
  bestD = d2;
62364
62633
  }
62365
62634
  }
62366
- if (best !== null) flyToItem(best);
62367
- }, [size, view, ppu, index2, tiers, lod, visibleItems, onSelectFn, flyToItem]);
62635
+ if (ctx !== null) ctx.restore();
62636
+ if (best !== null) {
62637
+ flyToItem(best);
62638
+ return true;
62639
+ }
62640
+ return false;
62641
+ }, [tiers, lod, visibleItems, onSelectFn, flyToItem]);
62642
+ const onCanvasPointerDown = useCallback((e3) => {
62643
+ if (e3.button !== 0) return;
62644
+ if (e3.target.closest("button") !== null) return;
62645
+ transition("pointerDown");
62646
+ panRef.current = { x: e3.clientX, y: e3.clientY, tx: cameraRef.current.tx, ty: cameraRef.current.ty };
62647
+ movedRef.current = false;
62648
+ e3.currentTarget.setPointerCapture(e3.pointerId);
62649
+ }, [transition]);
62650
+ const onCanvasPointerMove = useCallback((e3) => {
62651
+ const pan = panRef.current;
62652
+ if (!pan) return;
62653
+ if (Math.abs(e3.clientX - pan.x) > 4 || Math.abs(e3.clientY - pan.y) > 4) movedRef.current = true;
62654
+ cameraRef.current = { ...cameraRef.current, tx: pan.tx + e3.clientX - pan.x, ty: pan.ty + e3.clientY - pan.y };
62655
+ requestRender();
62656
+ }, [requestRender]);
62657
+ const endPan = useCallback(() => {
62658
+ if (panRef.current === null) return false;
62659
+ panRef.current = null;
62660
+ transition("pointerUp");
62661
+ return true;
62662
+ }, [transition]);
62663
+ const onCanvasPointerUp = useCallback((e3) => {
62664
+ const wasPress = endPan();
62665
+ if (!wasPress || movedRef.current) return;
62666
+ if (!pickAt(e3.currentTarget, e3.clientX, e3.clientY)) setSelected(null);
62667
+ }, [endPan, pickAt]);
62668
+ const onCanvasPointerCancel = useCallback(() => {
62669
+ movedRef.current = false;
62670
+ endPan();
62671
+ }, [endPan]);
62672
+ const resetView = useCallback(() => {
62673
+ transition("cancel");
62674
+ cameraRef.current = IDENTITY;
62675
+ requestRender();
62676
+ }, [transition, requestRender]);
62677
+ const zoomBy = useCallback((factor) => {
62678
+ const sz = sizeRef.current;
62679
+ if (sz === null) return;
62680
+ transition("zoom");
62681
+ cameraRef.current = zoomAbout(cameraRef.current, factor, sz.w / 2, sz.h / 2, 1, MAX_ZOOM);
62682
+ requestRender();
62683
+ }, [transition, requestRender]);
62684
+ const minimapDragRef = useRef(false);
62685
+ const minimapJump = useCallback((el, clientX, clientY) => {
62686
+ const sz = sizeRef.current;
62687
+ if (sz === null) return;
62688
+ const rect = el.getBoundingClientRect();
62689
+ const cx2 = (clientX - rect.left) / rect.width * W2;
62690
+ const cy = (clientY - rect.top) / rect.height * H;
62691
+ const cam = cameraRef.current;
62692
+ cameraRef.current = { ...cam, tx: sz.w / 2 - cx2 * fitRef.current * cam.zoom, ty: sz.h / 2 - cy * fitRef.current * cam.zoom };
62693
+ requestRender();
62694
+ }, [W2, H, requestRender]);
62695
+ useEffect(() => () => {
62696
+ var _a2;
62697
+ (_a2 = coalescerRef.current) == null ? void 0 : _a2.cancel();
62698
+ if (animRef.current !== null) cancelAnimationFrame(animRef.current);
62699
+ }, [animRef]);
62700
+ useLayoutEffect(() => {
62701
+ applyCamera();
62702
+ }, [applyCamera, value, visibleItems, tiers, selected, palette, size, dpr, fit, cameraSnapshot]);
62368
62703
  const toggleZone = useCallback((key) => {
62369
- setOpenZone((prev) => prev === key ? nav.parentOf.get(key) ?? null : key);
62704
+ const collapsing = openZoneRef.current === key;
62705
+ zoneOverrideRef.current = collapsing ? key : null;
62706
+ setOpenZone(collapsing ? nav.parentOf.get(key) ?? null : key);
62370
62707
  }, [nav.parentOf]);
62371
62708
  useEffect(() => {
62372
- if (currentZone !== void 0) setOpenZone(currentZone);
62709
+ if (currentZone === void 0) return;
62710
+ if (zoneOverrideRef.current !== null) {
62711
+ if (zoneOverrideRef.current === currentZone) return;
62712
+ zoneOverrideRef.current = null;
62713
+ }
62714
+ setOpenZone(currentZone);
62373
62715
  }, [currentZone]);
62374
62716
  const openPath = useMemo(() => {
62375
62717
  const path2 = /* @__PURE__ */ new Set();
@@ -62394,7 +62736,15 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62394
62736
  Box,
62395
62737
  {
62396
62738
  css: styles2.navZone,
62397
- style: { paddingLeft: `${8 + depth * 14}px` },
62739
+ style: {
62740
+ paddingLeft: `${8 + depth * 14}px`,
62741
+ // Pin this header below its ancestors' pinned headers
62742
+ // (each level offset by one row) so the whole path stays
62743
+ // visible while scrolling the zone's items; shallower
62744
+ // levels stack above deeper ones.
62745
+ top: `calc(var(--nav-row-h) * ${depth})`,
62746
+ zIndex: 30 - depth
62747
+ },
62398
62748
  ...node2.zone.key === currentZone ? { "data-current": "" } : {},
62399
62749
  children: [
62400
62750
  expandable ? /* @__PURE__ */ jsx(
@@ -62483,32 +62833,20 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62483
62833
  onPointerDown: onCanvasPointerDown,
62484
62834
  onPointerMove: onCanvasPointerMove,
62485
62835
  onPointerUp: onCanvasPointerUp,
62836
+ onPointerCancel: onCanvasPointerCancel,
62837
+ onLostPointerCapture: onCanvasPointerCancel,
62486
62838
  onDoubleClick: resetView,
62487
62839
  children: [
62488
- showGrid && size !== null && scaleLen > 0 && (() => {
62489
- const major = scaleLen * ppu;
62490
- const minor = major / 5;
62491
- return /* @__PURE__ */ jsx(
62492
- Box,
62493
- {
62494
- css: styles2.grid,
62495
- style: {
62496
- backgroundSize: `${major}px ${major}px, ${major}px ${major}px, ${minor}px ${minor}px, ${minor}px ${minor}px`,
62497
- backgroundPosition: `${view.tx}px 0, 0 ${view.ty}px, ${view.tx}px 0, 0 ${view.ty}px`
62498
- }
62499
- }
62500
- );
62501
- })(),
62840
+ showGrid && size !== null && /* @__PURE__ */ jsx(Box, { ref: gridRef, css: styles2.grid }),
62502
62841
  size !== null && /* @__PURE__ */ jsxs(Fragment, { children: [
62503
62842
  /* @__PURE__ */ jsx(
62504
62843
  "canvas",
62505
62844
  {
62506
62845
  ref: drawRef,
62507
- onClick: onCanvasClick,
62508
62846
  style: { position: "absolute", inset: 0, width: "100%", height: "100%" }
62509
62847
  }
62510
62848
  ),
62511
- visibleItems.map((item) => {
62849
+ /* @__PURE__ */ jsx(Box, { ref: cardLayerRef, css: styles2.cardLayer, children: visibleItems.map((item) => {
62512
62850
  if ((tiers.get(item.key) ?? lod) !== "card") return null;
62513
62851
  const tone = statusTone(item.status);
62514
62852
  const isSelected = selected === item.key;
@@ -62522,17 +62860,14 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62522
62860
  Box,
62523
62861
  {
62524
62862
  css: styles2.item,
62863
+ "data-card-key": item.key,
62525
62864
  ...isSelected ? { "data-selected": "" } : {},
62526
62865
  style: {
62527
- left: wx(item.x),
62528
- top: wy(item.y),
62529
- ...typeof width === "number" ? { width: width * ppu } : {}
62530
- },
62531
- onClick: () => {
62532
- setSelected(item.key);
62533
- if (onSelectFn) queueMicrotask(() => onSelectFn(item.key));
62866
+ left: 0,
62867
+ top: 0,
62868
+ transform: `${cardTranslateCss(item.x, item.y)} translate(-50%, -50%)`,
62869
+ ...typeof width === "number" ? { width: cardWidthCss(width) } : {}
62534
62870
  },
62535
- onPointerDown: (e3) => e3.stopPropagation(),
62536
62871
  children: [
62537
62872
  /* @__PURE__ */ jsxs(Box, { css: styles2.itemHead, children: [
62538
62873
  iconDef !== void 0 && /* @__PURE__ */ jsx(Box, { as: "span", css: styles2.itemIcon, children: /* @__PURE__ */ jsx(FontAwesomeIcon, { icon: iconDef }) }),
@@ -62546,7 +62881,7 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62546
62881
  },
62547
62882
  item.key
62548
62883
  );
62549
- }),
62884
+ }) }),
62550
62885
  /* @__PURE__ */ jsxs(Box, { css: styles2.controls, children: [
62551
62886
  /* @__PURE__ */ jsx(Box, { as: "button", css: styles2.controlButton, "aria-label": "Zoom in", title: "Zoom in (scroll)", onClick: () => zoomBy(1.5), children: /* @__PURE__ */ jsx(FontAwesomeIcon, { icon: faPlus }) }),
62552
62887
  /* @__PURE__ */ jsx(Box, { as: "button", css: styles2.controlButton, "aria-label": "Zoom out", title: "Zoom out (scroll)", onClick: () => zoomBy(1 / 1.5), children: /* @__PURE__ */ jsx(FontAwesomeIcon, { icon: faMinus }) }),
@@ -62590,16 +62925,20 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62590
62925
  style: { width: 150, height: 150 * H / W2 },
62591
62926
  onPointerDown: (e3) => {
62592
62927
  e3.stopPropagation();
62593
- const rect = e3.currentTarget.getBoundingClientRect();
62594
- const cx2 = (e3.clientX - rect.left) / rect.width * W2;
62595
- const cy = (e3.clientY - rect.top) / rect.height * H;
62596
- if (size !== null) {
62597
- setView((prev) => ({
62598
- ...prev,
62599
- tx: size.w / 2 - cx2 * fit * prev.zoom,
62600
- ty: size.h / 2 - cy * fit * prev.zoom
62601
- }));
62602
- }
62928
+ const el = e3.currentTarget;
62929
+ el.setPointerCapture(e3.pointerId);
62930
+ transition("cancel");
62931
+ minimapDragRef.current = true;
62932
+ minimapJump(el, e3.clientX, e3.clientY);
62933
+ },
62934
+ onPointerMove: (e3) => {
62935
+ if (!minimapDragRef.current) return;
62936
+ e3.stopPropagation();
62937
+ minimapJump(e3.currentTarget, e3.clientX, e3.clientY);
62938
+ },
62939
+ onPointerUp: (e3) => {
62940
+ minimapDragRef.current = false;
62941
+ e3.currentTarget.releasePointerCapture(e3.pointerId);
62603
62942
  },
62604
62943
  children: [
62605
62944
  value.zones.map((zone) => /* @__PURE__ */ jsx(
@@ -62620,8 +62959,8 @@ const EastChakraSchematic = memo$2(function EastChakraSchematic2({ value }) {
62620
62959
  {
62621
62960
  css: styles2.minimapViewport,
62622
62961
  style: {
62623
- left: `${Math.max(0, -view.tx / ppu / W2 * 100)}%`,
62624
- top: `${Math.max(0, -view.ty / ppu / H * 100)}%`,
62962
+ left: `${Math.max(0, -cameraSnapshot.tx / ppu / W2 * 100)}%`,
62963
+ top: `${Math.max(0, -cameraSnapshot.ty / ppu / H * 100)}%`,
62625
62964
  width: `${Math.min(100, size.w / ppu / W2 * 100)}%`,
62626
62965
  height: `${Math.min(100, size.h / ppu / H * 100)}%`
62627
62966
  }