@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.cjs CHANGED
@@ -1187,8 +1187,13 @@ const globalCss = react.defineGlobalStyles({
1187
1187
  },
1188
1188
  /* ─── Drag & drop stages (drag-drop-visuals) ─────────────────────────
1189
1189
  * The DragLayerProvider drives these via data attributes; surfaces get
1190
- * the treatment for free by registering cells/sinks. */
1191
- "[data-dragging]": {
1190
+ * the treatment for free by registering cells/sinks.
1191
+ *
1192
+ * Scoped `:not([data-scope])` so it dims only our own drag origins —
1193
+ * Ark/Zag widgets (Slider, etc.) set `data-dragging` on themselves for
1194
+ * their own drag state and carry a `data-scope`; without this exclusion
1195
+ * a slider goes to 40% opacity while its thumb is dragged. */
1196
+ "[data-dragging]:not([data-scope])": {
1192
1197
  opacity: "0.4"
1193
1198
  },
1194
1199
  "[data-drag-ghost]": {
@@ -3008,6 +3013,7 @@ const schematicSlotRecipe = react.defineSlotRecipe({
3008
3013
  "root",
3009
3014
  "canvas",
3010
3015
  "grid",
3016
+ "cardLayer",
3011
3017
  "underlay",
3012
3018
  "zone",
3013
3019
  "zoneLabel",
@@ -3085,6 +3091,15 @@ const schematicSlotRecipe = react.defineSlotRecipe({
3085
3091
  linear-gradient(to right, color-mix(in oklch, {colors.border.subtle} 22%, transparent) 1px, transparent 1px),
3086
3092
  linear-gradient(to bottom, color-mix(in oklch, {colors.border.subtle} 22%, transparent) 1px, transparent 1px)`
3087
3093
  },
3094
+ /* Holds the live `--cam-ppu` / `--cam-tx` / `--cam-ty` custom
3095
+ * properties (written by applyCamera each frame). Transparent and
3096
+ * click-through; each card re-enables its own pointer events so the
3097
+ * full-cover layer never swallows canvas clicks (issue #57). */
3098
+ cardLayer: {
3099
+ position: "absolute",
3100
+ inset: "0",
3101
+ pointerEvents: "none"
3102
+ },
3088
3103
  /* Links draw in pixel space; tone → token mapping lives here so
3089
3104
  * data only ever names a tone. */
3090
3105
  underlay: {
@@ -3281,6 +3296,7 @@ const schematicSlotRecipe = react.defineSlotRecipe({
3281
3296
  item: {
3282
3297
  position: "absolute",
3283
3298
  transform: "translate(-50%, -50%)",
3299
+ pointerEvents: "auto",
3284
3300
  display: "flex",
3285
3301
  flexDirection: "column",
3286
3302
  gap: "2px",
@@ -3476,14 +3492,24 @@ const schematicSlotRecipe = react.defineSlotRecipe({
3476
3492
  minHeight: "0",
3477
3493
  overflowY: "auto",
3478
3494
  paddingBottom: "{spacing.2}",
3479
- lineHeight: "1.7"
3480
- },
3495
+ lineHeight: "1.7",
3496
+ // Row height shared by the sticky zone headers (their stacked `top`
3497
+ // offset is a multiple of this) so an ancestor chain pins flush.
3498
+ "--nav-row-h": "22px"
3499
+ },
3500
+ /* Zone headers pin to the top of the scroll area like a tree view: each
3501
+ * depth level sticks at `top = depth × --nav-row-h` (set inline), so the
3502
+ * area (and its ancestors) stays visible while you scroll its items.
3503
+ * The opaque surface + z-index occlude the items scrolling underneath. */
3481
3504
  navZone: {
3482
3505
  display: "flex",
3483
3506
  alignItems: "center",
3484
3507
  gap: "{spacing.1}",
3485
- paddingY: "2px",
3486
- paddingRight: "{spacing.3}"
3508
+ height: "var(--nav-row-h)",
3509
+ paddingRight: "{spacing.3}",
3510
+ position: "sticky",
3511
+ background: "bg.surface",
3512
+ zIndex: "1"
3487
3513
  },
3488
3514
  navCaret: {
3489
3515
  background: "transparent",
@@ -3530,6 +3556,9 @@ const schematicSlotRecipe = react.defineSlotRecipe({
3530
3556
  border: "none",
3531
3557
  paddingY: "2px",
3532
3558
  paddingRight: "{spacing.3}",
3559
+ // Keep the auto-scroll-to-selected item clear of the pinned zone
3560
+ // headers (up to two ancestor levels).
3561
+ scrollMarginTop: "calc(var(--nav-row-h) * 2)",
3533
3562
  fontFamily: "mono",
3534
3563
  fontSize: "11px",
3535
3564
  letterSpacing: "0.06em",
@@ -61620,6 +61649,83 @@ function multiSelect(arr, left, right, n2, compare) {
61620
61649
  stack.push(left, mid, mid, right);
61621
61650
  }
61622
61651
  }
61652
+ const IDENTITY = { zoom: 1, tx: 0, ty: 0 };
61653
+ function project(x2, y2, cam) {
61654
+ return { sx: x2 * cam.ppu + cam.tx, sy: y2 * cam.ppu + cam.ty };
61655
+ }
61656
+ function cardTranslateCss(x2, y2) {
61657
+ return `translate(calc(var(--cam-ppu) * ${x2} * 1px + var(--cam-tx)), calc(var(--cam-ppu) * ${y2} * 1px + var(--cam-ty)))`;
61658
+ }
61659
+ function cardWidthCss(width) {
61660
+ return `calc(var(--cam-ppu) * ${width} * 1px)`;
61661
+ }
61662
+ function viewportWorldBbox(cam, width, height) {
61663
+ return {
61664
+ minX: -cam.tx / cam.ppu,
61665
+ minY: -cam.ty / cam.ppu,
61666
+ maxX: (width - cam.tx) / cam.ppu,
61667
+ maxY: (height - cam.ty) / cam.ppu
61668
+ };
61669
+ }
61670
+ function bboxOverlaps(a2, b2) {
61671
+ return a2.maxX >= b2.minX && a2.minX <= b2.maxX && a2.maxY >= b2.minY && a2.minY <= b2.maxY;
61672
+ }
61673
+ function pointsBbox(pts) {
61674
+ if (pts.length === 0) return null;
61675
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
61676
+ for (const p2 of pts) {
61677
+ if (p2.x < minX) minX = p2.x;
61678
+ if (p2.y < minY) minY = p2.y;
61679
+ if (p2.x > maxX) maxX = p2.x;
61680
+ if (p2.y > maxY) maxY = p2.y;
61681
+ }
61682
+ return { minX, minY, maxX, maxY };
61683
+ }
61684
+ function zoomAbout(prev, factor, px, py, min2, max2) {
61685
+ const zoom = Math.max(min2, Math.min(max2, prev.zoom * factor));
61686
+ if (zoom === 1) return IDENTITY;
61687
+ const k2 = zoom / prev.zoom;
61688
+ return { zoom, tx: px - k2 * (px - prev.tx), ty: py - k2 * (py - prev.ty) };
61689
+ }
61690
+ function nextMode(mode, event) {
61691
+ switch (event) {
61692
+ case "pointerDown":
61693
+ return "panning";
61694
+ case "wheel":
61695
+ return "wheeling";
61696
+ case "zoom":
61697
+ return "wheeling";
61698
+ case "flyStart":
61699
+ return "flying";
61700
+ case "pointerUp":
61701
+ return mode === "panning" ? "idle" : mode;
61702
+ case "flyEnd":
61703
+ return mode === "flying" ? "idle" : mode;
61704
+ case "cancel":
61705
+ return "idle";
61706
+ }
61707
+ }
61708
+ function cancelsFly(mode, event) {
61709
+ return mode === "flying" && (event === "pointerDown" || event === "wheel" || event === "zoom" || event === "cancel");
61710
+ }
61711
+ function makeRafCoalescer(schedule, cancel) {
61712
+ let handle2 = null;
61713
+ return {
61714
+ request(run) {
61715
+ if (handle2 !== null) return;
61716
+ handle2 = schedule(() => {
61717
+ handle2 = null;
61718
+ run();
61719
+ });
61720
+ },
61721
+ cancel() {
61722
+ if (handle2 !== null) {
61723
+ cancel(handle2);
61724
+ handle2 = null;
61725
+ }
61726
+ }
61727
+ };
61728
+ }
61623
61729
  const css = (c2, a2 = 1) => a2 >= 1 ? `rgb(${c2[0]},${c2[1]},${c2[2]})` : `rgba(${c2[0]},${c2[1]},${c2[2]},${a2})`;
61624
61730
  function toneRGB(p2, tone, kind) {
61625
61731
  switch (tone) {
@@ -61700,6 +61806,7 @@ function arcFromBulge(p1, p2, bulge) {
61700
61806
  const mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2;
61701
61807
  const d2 = chord / 2 / Math.tan(theta / 2);
61702
61808
  const cx2 = mx - dy / chord * d2, cy = my + dx / chord * d2;
61809
+ if (!Number.isFinite(radius) || !Number.isFinite(d2) || !Number.isFinite(cx2) || !Number.isFinite(cy)) return null;
61703
61810
  return {
61704
61811
  cx: cx2,
61705
61812
  cy,
@@ -61727,22 +61834,76 @@ function traceVertices(ctx, pts, closed) {
61727
61834
  ctx.closePath();
61728
61835
  }
61729
61836
  }
61837
+ const MARKER_LABEL_FONT = '600 10px ui-monospace, "SF Mono", Menlo, monospace';
61838
+ const MARKER_DOT_RADIUS = 5;
61839
+ const MARKER_DOT_HIT_RADIUS = 10;
61840
+ const MARKER_PIN_PAD_X = 6, MARKER_PIN_DOT_W = 7, MARKER_PIN_GAP = 4, MARKER_PIN_H = 16;
61841
+ function markerHitbox(item, tier, cam, measure) {
61842
+ const { sx, sy } = project(item.x, item.y, cam);
61843
+ if (tier === "dot") return { kind: "circle", cx: sx, cy: sy, r: MARKER_DOT_HIT_RADIUS };
61844
+ const tw = measure(item.label);
61845
+ const w2 = MARKER_PIN_PAD_X + MARKER_PIN_DOT_W + MARKER_PIN_GAP + tw + MARKER_PIN_PAD_X;
61846
+ return { kind: "rect", left: sx - w2 / 2, top: sy - MARKER_PIN_H / 2, w: w2, h: MARKER_PIN_H };
61847
+ }
61848
+ function markerHit(box, sx, sy) {
61849
+ 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;
61850
+ }
61851
+ function hatchSpacing(rawSpacing) {
61852
+ return Number.isFinite(rawSpacing) ? Math.max(1, rawSpacing) : 1;
61853
+ }
61854
+ function hatchSweepBounds(x2, y2, w2, h2, width, height, dx, dy, diag, spacing) {
61855
+ const nx = -dy, ny = dx;
61856
+ if (Math.abs(nx) <= 1e-6) return { oStart: -diag, oEnd: diag };
61857
+ const cx0 = Math.max(x2, 0), cy0 = Math.max(y2, 0);
61858
+ const cx1 = Math.min(x2 + w2, width), cy1 = Math.min(y2 + h2, height);
61859
+ const cs = [cx0 * nx + cy0 * ny, cx1 * nx + cy0 * ny, cx0 * nx + cy1 * ny, cx1 * nx + cy1 * ny];
61860
+ const oOfC = (c2) => (c2 - x2 * nx - (y2 - diag * dy) * ny) / nx;
61861
+ const oA = oOfC(Math.min(...cs)), oB = oOfC(Math.max(...cs));
61862
+ return {
61863
+ oStart: Math.max(-diag, -diag + Math.floor((Math.min(oA, oB) + diag) / spacing) * spacing - spacing),
61864
+ oEnd: Math.min(diag, Math.max(oA, oB) + spacing)
61865
+ };
61866
+ }
61867
+ function zoneWorldBbox(zone, geom) {
61868
+ const bb = { minX: zone.x, minY: zone.y, maxX: zone.x + zone.width, maxY: zone.y + zone.height };
61869
+ if (geom !== void 0 && geom.type === "circle") {
61870
+ const cx2 = zone.x + zone.width / 2, cy = zone.y + zone.height / 2, r2 = geom.value.radius;
61871
+ bb.minX = Math.min(bb.minX, cx2 - r2);
61872
+ bb.minY = Math.min(bb.minY, cy - r2);
61873
+ bb.maxX = Math.max(bb.maxX, cx2 + r2);
61874
+ bb.maxY = Math.max(bb.maxY, cy + r2);
61875
+ } else if (geom !== void 0 && geom.type !== "rect") {
61876
+ const g2 = pointsBbox(geom.value.vertices);
61877
+ if (g2 !== null) {
61878
+ bb.minX = Math.min(bb.minX, g2.minX);
61879
+ bb.minY = Math.min(bb.minY, g2.minY);
61880
+ bb.maxX = Math.max(bb.maxX, g2.maxX);
61881
+ bb.maxY = Math.max(bb.maxY, g2.maxY);
61882
+ }
61883
+ }
61884
+ return bb;
61885
+ }
61730
61886
  function paintSchematic(input) {
61731
61887
  var _a2, _b, _c, _d;
61732
61888
  const { ctx, value, cam, width, height, visibleItems, tiers, selected, centers, palette: p2 } = input;
61733
61889
  const wx = (x2) => x2 * cam.ppu + cam.tx;
61734
61890
  const wy = (y2) => y2 * cam.ppu + cam.ty;
61735
61891
  const ppu = cam.ppu;
61736
- ctx.clearRect(0, 0, width, height);
61892
+ const viewBbox = viewportWorldBbox(cam, width, height);
61893
+ ctx.save();
61894
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
61895
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
61896
+ ctx.restore();
61737
61897
  ctx.lineJoin = "round";
61738
61898
  for (const zone of value.zones) {
61899
+ const geom = getSomeorUndefined(zone.geometry);
61900
+ if (!bboxOverlaps(zoneWorldBbox(zone, geom), viewBbox)) continue;
61739
61901
  const pattern = zone.pattern;
61740
61902
  const patternTone = (pattern.value.tone.type === "some" ? pattern.value.tone.value.type : void 0) ?? "muted";
61741
61903
  const tint = resolveTint(p2, getSomeorUndefined(zone.color), (_a2 = getSomeorUndefined(zone.tone)) == null ? void 0 : _a2.type, toneRGB(p2, patternTone, "zone"));
61742
61904
  const zbg = getSomeorUndefined(zone.bg);
61743
61905
  const zFillAlpha = getSomeorUndefined(zone.fillOpacity) ?? 0.15;
61744
61906
  const zWeight = getSomeorUndefined(zone.weight);
61745
- const geom = getSomeorUndefined(zone.geometry);
61746
61907
  const x2 = wx(zone.x), y2 = wy(zone.y), w2 = zone.width * ppu, h2 = zone.height * ppu;
61747
61908
  const fillShape = () => {
61748
61909
  if (zbg === void 0) return;
@@ -61790,8 +61951,8 @@ function paintSchematic(input) {
61790
61951
  }
61791
61952
  }
61792
61953
  } else if (pattern.type === "hatch") {
61793
- const spacing = pattern.value.spacing.type === "some" ? pattern.value.spacing.value : 8;
61794
- const angle = (pattern.value.angle.type === "some" ? pattern.value.angle.value : 45) * Math.PI / 180;
61954
+ const spacing = hatchSpacing(getSomeorUndefined(pattern.value.spacing) ?? 8);
61955
+ const angle = (getSomeorUndefined(pattern.value.angle) ?? 45) * Math.PI / 180;
61795
61956
  ctx.save();
61796
61957
  ctx.beginPath();
61797
61958
  ctx.rect(x2, y2, w2, h2);
@@ -61808,8 +61969,9 @@ function paintSchematic(input) {
61808
61969
  ctx.lineWidth = zWeight ?? 1;
61809
61970
  const dx = Math.cos(angle), dy = Math.sin(angle);
61810
61971
  const diag = Math.hypot(w2, h2);
61972
+ const { oStart, oEnd } = hatchSweepBounds(x2, y2, w2, h2, width, height, dx, dy, diag, spacing);
61811
61973
  ctx.beginPath();
61812
- for (let o2 = -diag; o2 < diag; o2 += spacing) {
61974
+ for (let o2 = oStart; o2 < oEnd; o2 += spacing) {
61813
61975
  ctx.moveTo(x2 + o2, y2 - diag * dy);
61814
61976
  ctx.lineTo(x2 + o2 + dx * 2 * diag, y2 - diag * dy + dy * 2 * diag);
61815
61977
  }
@@ -61843,6 +62005,8 @@ function paintSchematic(input) {
61843
62005
  for (const link2 of value.links) {
61844
62006
  const from = centers.get(link2.from), to = centers.get(link2.to);
61845
62007
  if (!from || !to) continue;
62008
+ const linkBbox = pointsBbox([from, ...link2.via, to]);
62009
+ if (linkBbox !== null && !bboxOverlaps(linkBbox, viewBbox)) continue;
61846
62010
  const anchors = [from, ...link2.via, to].map((q) => ({ x: wx(q.x), y: wy(q.y) }));
61847
62011
  const corner = link2.route.type === "orthogonal" ? link2.route.value.corner.type === "some" ? link2.route.value.corner.value : 8 : 0;
61848
62012
  const pts = link2.route.type === "orthogonal" ? orthogonalize(anchors) : anchors;
@@ -61916,7 +62080,7 @@ function paintSchematic(input) {
61916
62080
  const x2 = wx(item.x), y2 = wy(item.y);
61917
62081
  if (tier === "dot") {
61918
62082
  ctx.beginPath();
61919
- ctx.arc(x2, y2, 5, 0, Math.PI * 2);
62083
+ ctx.arc(x2, y2, MARKER_DOT_RADIUS, 0, Math.PI * 2);
61920
62084
  ctx.fillStyle = tint;
61921
62085
  ctx.fill();
61922
62086
  ctx.lineWidth = 1.5;
@@ -61931,11 +62095,10 @@ function paintSchematic(input) {
61931
62095
  }
61932
62096
  continue;
61933
62097
  }
61934
- ctx.font = '600 10px ui-monospace, "SF Mono", Menlo, monospace';
61935
- const tw = ctx.measureText(item.label).width;
61936
- const padX = 6, dotW = 7, gap = 4, h2 = 16;
61937
- const w2 = padX + dotW + gap + tw + padX;
61938
- const left = x2 - w2 / 2, top = y2 - h2 / 2;
62098
+ ctx.font = MARKER_LABEL_FONT;
62099
+ const box = markerHitbox(item, "label", cam, (t4) => ctx.measureText(t4).width);
62100
+ if (box.kind !== "rect") continue;
62101
+ const { left, top, w: w2, h: h2 } = box;
61939
62102
  ctx.beginPath();
61940
62103
  (_d = ctx.roundRect) == null ? void 0 : _d.call(ctx, left, top, w2, h2, h2 / 2);
61941
62104
  if (!ctx.roundRect) ctx.rect(left, top, w2, h2);
@@ -61945,12 +62108,12 @@ function paintSchematic(input) {
61945
62108
  ctx.strokeStyle = isSel ? css(p2.fg) : css(p2.borderStrong);
61946
62109
  ctx.stroke();
61947
62110
  ctx.beginPath();
61948
- ctx.arc(left + padX + dotW / 2, y2, dotW / 2, 0, Math.PI * 2);
62111
+ ctx.arc(left + MARKER_PIN_PAD_X + MARKER_PIN_DOT_W / 2, y2, MARKER_PIN_DOT_W / 2, 0, Math.PI * 2);
61949
62112
  ctx.fillStyle = tint;
61950
62113
  ctx.fill();
61951
62114
  ctx.fillStyle = css(p2.fg);
61952
62115
  ctx.textBaseline = "middle";
61953
- ctx.fillText(item.label, left + padX + dotW + gap, y2);
62116
+ ctx.fillText(item.label, left + MARKER_PIN_PAD_X + MARKER_PIN_DOT_W + MARKER_PIN_GAP, y2);
61954
62117
  }
61955
62118
  }
61956
62119
  const TOKENS = {
@@ -61997,7 +62160,6 @@ const SchematicPaletteProbe = React.memo(function SchematicPaletteProbe2({ onRes
61997
62160
  });
61998
62161
  library$1.add(icons$2);
61999
62162
  const schematicEqual = east.equalFor(internal.Schematic.Types.Schematic);
62000
- const IDENTITY = { zoom: 1, tx: 0, ty: 0 };
62001
62163
  const MAX_ZOOM = 40;
62002
62164
  const LOD_CARD_PPU = 30;
62003
62165
  const LOD_LABEL_PPU = 16;
@@ -62102,7 +62264,7 @@ function declutterTiers(items, tree, baseLod, ppu, selected) {
62102
62264
  if (selected !== null && tiers.has(selected)) tiers.set(selected, baseLod);
62103
62265
  return tiers;
62104
62266
  }
62105
- const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value }) {
62267
+ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value, storageKey }) {
62106
62268
  const styles2 = react.useSlotRecipe({ key: "schematic" })();
62107
62269
  const { width: W2, height: H } = value.extent;
62108
62270
  const onSelectFn = React.useMemo(() => getSomeorUndefined(value.onSelect), [value.onSelect]);
@@ -62112,14 +62274,17 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62112
62274
  const showMinimap = getSomeorUndefined(value.minimap) ?? value.items.length >= MINIMAP_AUTO;
62113
62275
  const fixedHeight = getSomeorUndefined(value.height);
62114
62276
  const [selected, setSelected] = React.useState(null);
62115
- const [view, setView] = React.useState(IDENTITY);
62116
62277
  const [query, setQuery] = React.useState("");
62117
62278
  const [openZone, setOpenZone] = React.useState(null);
62118
- const [navCollapsed, setNavCollapsed] = React.useState(false);
62279
+ const { state: persisted, setState: setPersisted } = usePersistedState(storageKey, { navCollapsed: false });
62280
+ const navCollapsed = persisted.navCollapsed;
62281
+ const setNavCollapsed = React.useCallback((next) => setPersisted((prev) => ({ ...prev, navCollapsed: next })), [setPersisted]);
62119
62282
  const [palette, setPalette] = React.useState(null);
62120
62283
  const navTreeRef = React.useRef(null);
62121
62284
  const canvasRef = React.useRef(null);
62122
62285
  const drawRef = React.useRef(null);
62286
+ const cardLayerRef = React.useRef(null);
62287
+ const gridRef = React.useRef(null);
62123
62288
  const [size, setSize] = React.useState(null);
62124
62289
  React.useLayoutEffect(() => {
62125
62290
  const el = canvasRef.current;
@@ -62130,10 +62295,30 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62130
62295
  ro.observe(el);
62131
62296
  return () => ro.disconnect();
62132
62297
  }, []);
62298
+ const [dpr, setDpr] = React.useState(() => typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1);
62299
+ React.useEffect(() => {
62300
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
62301
+ let cleanup = () => {
62302
+ };
62303
+ const subscribe2 = () => {
62304
+ const mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
62305
+ const onChange2 = () => {
62306
+ cleanup();
62307
+ setDpr(window.devicePixelRatio || 1);
62308
+ subscribe2();
62309
+ };
62310
+ mql.addEventListener("change", onChange2);
62311
+ cleanup = () => mql.removeEventListener("change", onChange2);
62312
+ };
62313
+ subscribe2();
62314
+ return () => cleanup();
62315
+ }, []);
62316
+ const cameraRef = React.useRef(IDENTITY);
62317
+ const modeRef = React.useRef("idle");
62318
+ const animRef = React.useRef(null);
62319
+ const [cameraSnapshot, setCameraSnapshot] = React.useState(IDENTITY);
62133
62320
  const fit = size !== null ? Math.min(size.w / W2, size.h / H) : 0;
62134
- const ppu = fit * view.zoom;
62135
- const wx = (x2) => x2 * ppu + view.tx;
62136
- const wy = (y2) => y2 * ppu + view.ty;
62321
+ const ppu = fit * cameraSnapshot.zoom;
62137
62322
  const centers = React.useMemo(() => {
62138
62323
  const out = /* @__PURE__ */ new Map();
62139
62324
  for (const item of value.items) out.set(item.key, { x: item.x, y: item.y });
@@ -62166,12 +62351,12 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62166
62351
  const visibleItems = React.useMemo(() => {
62167
62352
  if (size === null || ppu === 0) return [];
62168
62353
  return index2.search({
62169
- minX: -view.tx / ppu,
62170
- minY: -view.ty / ppu,
62171
- maxX: (size.w - view.tx) / ppu,
62172
- maxY: (size.h - view.ty) / ppu
62354
+ minX: -cameraSnapshot.tx / ppu,
62355
+ minY: -cameraSnapshot.ty / ppu,
62356
+ maxX: (size.w - cameraSnapshot.tx) / ppu,
62357
+ maxY: (size.h - cameraSnapshot.ty) / ppu
62173
62358
  }).map((b2) => b2.item);
62174
- }, [index2, size, ppu, view]);
62359
+ }, [index2, size, ppu, cameraSnapshot]);
62175
62360
  const nav = React.useMemo(() => buildNavTree(value.zones, value.items), [value.zones, value.items]);
62176
62361
  const zoneDisplay = React.useMemo(() => {
62177
62362
  const counts = /* @__PURE__ */ new Map();
@@ -62197,106 +62382,15 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62197
62382
  const indexOf = new Map(ordered.map((item, i2) => [item.key, i2]));
62198
62383
  return { ordered, indexOf };
62199
62384
  }, [nav]);
62200
- const animRef = React.useRef(null);
62201
- const viewRef = React.useRef(view);
62202
- viewRef.current = view;
62203
- const flyTo = React.useCallback((rect) => {
62204
- if (size === null || fit === 0) return;
62205
- const pad2 = 1.18;
62206
- const zoom = Math.min(MAX_ZOOM, Math.min(size.w / (rect.w * pad2 * fit), size.h / (rect.h * pad2 * fit)));
62207
- const target = {
62208
- zoom,
62209
- tx: size.w / 2 - (rect.x + rect.w / 2) * fit * zoom,
62210
- ty: size.h / 2 - (rect.y + rect.h / 2) * fit * zoom
62211
- };
62212
- const from = viewRef.current;
62213
- const start = performance.now();
62214
- if (animRef.current !== null) cancelAnimationFrame(animRef.current);
62215
- const step = (now) => {
62216
- const t4 = Math.min(1, (now - start) / 350);
62217
- const e3 = 1 - Math.pow(1 - t4, 3);
62218
- setView({
62219
- zoom: from.zoom + (target.zoom - from.zoom) * e3,
62220
- tx: from.tx + (target.tx - from.tx) * e3,
62221
- ty: from.ty + (target.ty - from.ty) * e3
62222
- });
62223
- if (t4 < 1) animRef.current = requestAnimationFrame(step);
62224
- };
62225
- animRef.current = requestAnimationFrame(step);
62226
- }, [size, fit]);
62227
- React.useEffect(() => () => {
62228
- if (animRef.current !== null) cancelAnimationFrame(animRef.current);
62229
- }, []);
62230
- const flyToItem = React.useCallback((item) => {
62231
- flyTo({ x: item.x - 4, y: item.y - 3, w: 8, h: 6 });
62232
- setSelected(item.key);
62233
- setOpenZone(nav.zoneOf.get(item.key) ?? null);
62234
- if (onSelectFn) queueMicrotask(() => onSelectFn(item.key));
62235
- }, [flyTo, nav.zoneOf, onSelectFn]);
62236
- const stepSelection = React.useCallback((delta) => {
62237
- const { ordered, indexOf } = itemOrder;
62238
- if (ordered.length === 0) return;
62239
- const idx = selected !== null ? indexOf.get(selected) ?? -1 : -1;
62240
- flyToItem(ordered[(idx + delta + ordered.length) % ordered.length]);
62241
- }, [itemOrder, selected, flyToItem]);
62242
- const flyToZone = React.useCallback((zone) => {
62243
- flyTo({ x: zone.x, y: zone.y, w: zone.width, h: zone.height });
62244
- setOpenZone(zone.key);
62245
- }, [flyTo]);
62246
- React.useEffect(() => {
62247
- const el = canvasRef.current;
62248
- if (!el) return;
62249
- const onWheel = (e3) => {
62250
- e3.preventDefault();
62251
- const rect = el.getBoundingClientRect();
62252
- const px = e3.clientX - rect.left, py = e3.clientY - rect.top;
62253
- setView((prev) => {
62254
- const zoom = Math.max(1, Math.min(MAX_ZOOM, prev.zoom * Math.exp(-e3.deltaY * 15e-4)));
62255
- const k2 = zoom / prev.zoom;
62256
- const next = { zoom, tx: px - k2 * (px - prev.tx), ty: py - k2 * (py - prev.ty) };
62257
- return zoom === 1 ? IDENTITY : next;
62258
- });
62259
- };
62260
- el.addEventListener("wheel", onWheel, { passive: false });
62261
- return () => el.removeEventListener("wheel", onWheel);
62262
- }, []);
62263
- const panRef = React.useRef(null);
62264
- const movedRef = React.useRef(false);
62265
- const onCanvasPointerDown = React.useCallback((e3) => {
62266
- if (e3.button !== 0) return;
62267
- if (e3.target.closest("button") !== null) return;
62268
- panRef.current = { x: e3.clientX, y: e3.clientY, tx: viewRef.current.tx, ty: viewRef.current.ty };
62269
- movedRef.current = false;
62270
- e3.currentTarget.setPointerCapture(e3.pointerId);
62271
- }, []);
62272
- const onCanvasPointerMove = React.useCallback((e3) => {
62273
- const pan = panRef.current;
62274
- if (!pan) return;
62275
- if (Math.abs(e3.clientX - pan.x) > 4 || Math.abs(e3.clientY - pan.y) > 4) movedRef.current = true;
62276
- setView((prev) => ({ ...prev, tx: pan.tx + e3.clientX - pan.x, ty: pan.ty + e3.clientY - pan.y }));
62277
- }, []);
62278
- const onCanvasPointerUp = React.useCallback(() => {
62279
- panRef.current = null;
62280
- }, []);
62281
- const resetView = React.useCallback(() => setView(IDENTITY), []);
62282
- const zoomBy = React.useCallback((factor) => {
62283
- if (size === null) return;
62284
- const px = size.w / 2, py = size.h / 2;
62285
- setView((prev) => {
62286
- const zoom = Math.max(1, Math.min(MAX_ZOOM, prev.zoom * factor));
62287
- const k2 = zoom / prev.zoom;
62288
- return zoom === 1 ? IDENTITY : { zoom, tx: px - k2 * (px - prev.tx), ty: py - k2 * (py - prev.ty) };
62289
- });
62290
- }, [size]);
62291
62385
  const [currentZone, setCurrentZone] = React.useState(void 0);
62292
62386
  React.useEffect(() => {
62293
62387
  const SPY_ENTER = 0.3, SPY_STAY = 0.18, SPY_MARGIN = 0.15;
62294
- if (size === null || ppu === 0 || view.zoom <= 1.05) {
62388
+ if (size === null || ppu === 0 || cameraSnapshot.zoom <= 1.05) {
62295
62389
  setCurrentZone(void 0);
62296
62390
  return;
62297
62391
  }
62298
- const vx0 = -view.tx / ppu, vy0 = -view.ty / ppu;
62299
- const vx1 = (size.w - view.tx) / ppu, vy1 = (size.h - view.ty) / ppu;
62392
+ const vx0 = -cameraSnapshot.tx / ppu, vy0 = -cameraSnapshot.ty / ppu;
62393
+ const vx1 = (size.w - cameraSnapshot.tx) / ppu, vy1 = (size.h - cameraSnapshot.ty) / ppu;
62300
62394
  const viewArea = (vx1 - vx0) * (vy1 - vy0);
62301
62395
  const scoreOf = (z) => {
62302
62396
  const ix = Math.max(0, Math.min(vx1, z.x + z.width) - Math.max(vx0, z.x));
@@ -62320,73 +62414,321 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62320
62414
  }
62321
62415
  return bestScore >= SPY_ENTER ? best : void 0;
62322
62416
  });
62323
- }, [size, ppu, view, value.zones]);
62417
+ }, [size, ppu, cameraSnapshot, value.zones]);
62324
62418
  const lod = ppu >= LOD_CARD_PPU ? "card" : ppu >= LOD_LABEL_PPU ? "label" : "dot";
62325
62419
  const centerTree = React.useMemo(() => buildCenterTree(visibleItems), [visibleItems]);
62326
62420
  const tiers = React.useMemo(() => declutterTiers(visibleItems, centerTree, lod, ppu, selected), [visibleItems, centerTree, lod, ppu, selected]);
62327
62421
  const lowerQuery = query.trim().toLowerCase();
62328
62422
  const searchHits = React.useMemo(() => lowerQuery === "" ? [] : value.items.filter((i2) => i2.key.toLowerCase().includes(lowerQuery) || i2.label.toLowerCase().includes(lowerQuery)).slice(0, 12), [lowerQuery, value.items]);
62329
62423
  const scaleLen = ppu > 0 ? niceScaleLength(ppu, 100) : 0;
62330
- React.useEffect(() => {
62331
- const canvas = drawRef.current;
62332
- if (canvas === null || size === null || palette === null) return;
62333
- const dpr = window.devicePixelRatio || 1;
62334
- const bw = Math.round(size.w * dpr), bh = Math.round(size.h * dpr);
62424
+ const valueRef = React.useRef(value);
62425
+ valueRef.current = value;
62426
+ const sizeRef = React.useRef(size);
62427
+ sizeRef.current = size;
62428
+ const fitRef = React.useRef(fit);
62429
+ fitRef.current = fit;
62430
+ const dprRef = React.useRef(dpr);
62431
+ dprRef.current = dpr;
62432
+ const paletteRef = React.useRef(palette);
62433
+ paletteRef.current = palette;
62434
+ const visibleItemsRef = React.useRef(visibleItems);
62435
+ visibleItemsRef.current = visibleItems;
62436
+ const tiersRef = React.useRef(tiers);
62437
+ tiersRef.current = tiers;
62438
+ const selectedRef = React.useRef(selected);
62439
+ selectedRef.current = selected;
62440
+ const centersRef = React.useRef(centers);
62441
+ centersRef.current = centers;
62442
+ const openZoneRef = React.useRef(openZone);
62443
+ openZoneRef.current = openZone;
62444
+ const lastPaintRef = React.useRef(null);
62445
+ const applyCamera = React.useCallback(() => {
62446
+ const sz = sizeRef.current;
62447
+ if (sz === null) return;
62448
+ const cam = cameraRef.current;
62449
+ const ppuLive = fitRef.current * cam.zoom;
62450
+ const dprLive = dprRef.current;
62451
+ const layer = cardLayerRef.current;
62452
+ if (layer !== null) {
62453
+ layer.style.setProperty("--cam-ppu", String(ppuLive));
62454
+ layer.style.setProperty("--cam-tx", `${cam.tx}px`);
62455
+ layer.style.setProperty("--cam-ty", `${cam.ty}px`);
62456
+ }
62457
+ const grid = gridRef.current;
62458
+ if (grid !== null) {
62459
+ const len = ppuLive > 0 ? niceScaleLength(ppuLive, 100) : 0;
62460
+ if (len > 0) {
62461
+ const major = len * ppuLive, minor = major / 5;
62462
+ grid.style.backgroundSize = `${major}px ${major}px, ${major}px ${major}px, ${minor}px ${minor}px, ${minor}px ${minor}px`;
62463
+ grid.style.backgroundPosition = `${cam.tx}px 0, 0 ${cam.ty}px, ${cam.tx}px 0, 0 ${cam.ty}px`;
62464
+ }
62465
+ }
62466
+ const canvas = drawRef.current, pal = paletteRef.current;
62467
+ if (canvas === null || pal === null) return;
62468
+ const last = lastPaintRef.current;
62469
+ 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;
62470
+ lastPaintRef.current = {
62471
+ zoom: cam.zoom,
62472
+ tx: cam.tx,
62473
+ ty: cam.ty,
62474
+ vis: visibleItemsRef.current,
62475
+ tiers: tiersRef.current,
62476
+ sel: selectedRef.current,
62477
+ pal,
62478
+ w: sz.w,
62479
+ h: sz.h,
62480
+ dpr: dprLive,
62481
+ val: valueRef.current
62482
+ };
62483
+ const bw = Math.round(sz.w * dprLive), bh = Math.round(sz.h * dprLive);
62335
62484
  if (canvas.width !== bw || canvas.height !== bh) {
62336
62485
  canvas.width = bw;
62337
62486
  canvas.height = bh;
62338
62487
  }
62339
62488
  const ctx = canvas.getContext("2d");
62340
62489
  if (ctx === null) return;
62341
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
62490
+ ctx.setTransform(dprLive, 0, 0, dprLive, 0, 0);
62342
62491
  paintSchematic({
62343
62492
  ctx,
62344
- value,
62345
- cam: { ppu, tx: view.tx, ty: view.ty },
62346
- width: size.w,
62347
- height: size.h,
62348
- visibleItems,
62349
- tiers,
62350
- selected,
62351
- centers,
62352
- palette
62493
+ value: valueRef.current,
62494
+ cam: { ppu: ppuLive, tx: cam.tx, ty: cam.ty },
62495
+ width: sz.w,
62496
+ height: sz.h,
62497
+ visibleItems: visibleItemsRef.current,
62498
+ tiers: tiersRef.current,
62499
+ selected: selectedRef.current,
62500
+ centers: centersRef.current,
62501
+ palette: pal
62353
62502
  });
62354
- }, [value, view, ppu, size, visibleItems, tiers, selected, centers, palette]);
62355
- const onCanvasClick = React.useCallback((e3) => {
62356
- if (size === null || movedRef.current) return;
62357
- const rect = e3.currentTarget.getBoundingClientRect();
62358
- const sx = e3.clientX - rect.left, sy = e3.clientY - rect.top;
62359
- const wxp = (sx - view.tx) / ppu, wyp = (sy - view.ty) / ppu;
62360
- for (const box of index2.search({ minX: wxp, minY: wyp, maxX: wxp, maxY: wyp })) {
62361
- const it = box.item;
62503
+ }, []);
62504
+ const coalescerRef = React.useRef(null);
62505
+ if (coalescerRef.current === null && typeof requestAnimationFrame !== "undefined") {
62506
+ coalescerRef.current = makeRafCoalescer(requestAnimationFrame, cancelAnimationFrame);
62507
+ }
62508
+ const requestRender = React.useCallback(() => {
62509
+ const run = () => {
62510
+ applyCamera();
62511
+ const cam = cameraRef.current;
62512
+ setCameraSnapshot((prev) => prev.zoom === cam.zoom && prev.tx === cam.tx && prev.ty === cam.ty ? prev : { zoom: cam.zoom, tx: cam.tx, ty: cam.ty });
62513
+ };
62514
+ const cz = coalescerRef.current;
62515
+ if (cz !== null) cz.request(run);
62516
+ else run();
62517
+ }, [applyCamera]);
62518
+ const cancelFlyRaf = React.useCallback(() => {
62519
+ if (animRef.current !== null) {
62520
+ cancelAnimationFrame(animRef.current);
62521
+ animRef.current = null;
62522
+ }
62523
+ }, []);
62524
+ const transition = React.useCallback((event) => {
62525
+ if (cancelsFly(modeRef.current, event)) cancelFlyRaf();
62526
+ modeRef.current = nextMode(modeRef.current, event);
62527
+ }, [cancelFlyRaf]);
62528
+ const flyTo = React.useCallback((rect) => {
62529
+ if (sizeRef.current === null || fitRef.current === 0) return;
62530
+ const pad2 = 1.18;
62531
+ const frame = () => {
62532
+ const sz = sizeRef.current, ft = fitRef.current;
62533
+ if (sz === null || ft === 0) return null;
62534
+ const zoom = Math.min(MAX_ZOOM, Math.min(sz.w / (rect.w * pad2 * ft), sz.h / (rect.h * pad2 * ft)));
62535
+ return {
62536
+ zoom,
62537
+ tx: sz.w / 2 - (rect.x + rect.w / 2) * ft * zoom,
62538
+ ty: sz.h / 2 - (rect.y + rect.h / 2) * ft * zoom
62539
+ };
62540
+ };
62541
+ const from = { ...cameraRef.current };
62542
+ const start = performance.now();
62543
+ if (animRef.current !== null) cancelAnimationFrame(animRef.current);
62544
+ transition("flyStart");
62545
+ const step = (now) => {
62546
+ const t4 = Math.min(1, (now - start) / 350);
62547
+ const e3 = 1 - Math.pow(1 - t4, 3);
62548
+ const target = frame();
62549
+ if (target !== null) {
62550
+ cameraRef.current = {
62551
+ zoom: from.zoom + (target.zoom - from.zoom) * e3,
62552
+ tx: from.tx + (target.tx - from.tx) * e3,
62553
+ ty: from.ty + (target.ty - from.ty) * e3
62554
+ };
62555
+ applyCamera();
62556
+ const cam = cameraRef.current;
62557
+ setCameraSnapshot({ zoom: cam.zoom, tx: cam.tx, ty: cam.ty });
62558
+ }
62559
+ if (t4 < 1) {
62560
+ animRef.current = requestAnimationFrame(step);
62561
+ } else {
62562
+ animRef.current = null;
62563
+ transition("flyEnd");
62564
+ }
62565
+ };
62566
+ animRef.current = requestAnimationFrame(step);
62567
+ }, [applyCamera, transition]);
62568
+ const zoneOverrideRef = React.useRef(null);
62569
+ const flyToItem = React.useCallback((item) => {
62570
+ flyTo({ x: item.x - 4, y: item.y - 3, w: 8, h: 6 });
62571
+ setSelected(item.key);
62572
+ zoneOverrideRef.current = null;
62573
+ setOpenZone(nav.zoneOf.get(item.key) ?? null);
62574
+ if (onSelectFn) queueMicrotask(() => onSelectFn(item.key));
62575
+ }, [flyTo, nav.zoneOf, onSelectFn]);
62576
+ const stepSelection = React.useCallback((delta) => {
62577
+ const { ordered, indexOf } = itemOrder;
62578
+ if (ordered.length === 0) return;
62579
+ const idx = selected !== null ? indexOf.get(selected) ?? -1 : -1;
62580
+ flyToItem(ordered[(idx + delta + ordered.length) % ordered.length]);
62581
+ }, [itemOrder, selected, flyToItem]);
62582
+ const flyToZone = React.useCallback((zone) => {
62583
+ flyTo({ x: zone.x, y: zone.y, w: zone.width, h: zone.height });
62584
+ zoneOverrideRef.current = null;
62585
+ setOpenZone(zone.key);
62586
+ }, [flyTo]);
62587
+ React.useEffect(() => {
62588
+ const el = canvasRef.current;
62589
+ if (!el) return;
62590
+ const onWheel = (e3) => {
62591
+ e3.preventDefault();
62592
+ const rect = el.getBoundingClientRect();
62593
+ const px = e3.clientX - rect.left, py = e3.clientY - rect.top;
62594
+ transition("wheel");
62595
+ cameraRef.current = zoomAbout(cameraRef.current, Math.exp(-e3.deltaY * 15e-4), px, py, 1, MAX_ZOOM);
62596
+ requestRender();
62597
+ };
62598
+ el.addEventListener("wheel", onWheel, { passive: false });
62599
+ return () => el.removeEventListener("wheel", onWheel);
62600
+ }, [transition, requestRender]);
62601
+ const panRef = React.useRef(null);
62602
+ const movedRef = React.useRef(false);
62603
+ const pickAt = React.useCallback((host, clientX, clientY) => {
62604
+ var _a2, _b, _c;
62605
+ const sz = sizeRef.current;
62606
+ if (sz === null) return false;
62607
+ const cam = cameraRef.current;
62608
+ const ppuLive = fitRef.current * cam.zoom;
62609
+ if (ppuLive === 0) return false;
62610
+ const rect = host.getBoundingClientRect();
62611
+ const sx = clientX - rect.left, sy = clientY - rect.top;
62612
+ const wxp = (sx - cam.tx) / ppuLive, wyp = (sy - cam.ty) / ppuLive;
62613
+ const select = (key) => {
62614
+ setSelected(key);
62615
+ if (onSelectFn) queueMicrotask(() => onSelectFn(key));
62616
+ };
62617
+ for (const it of visibleItems) {
62362
62618
  if ((tiers.get(it.key) ?? lod) !== "card") continue;
62363
62619
  const fp = getSomeorUndefined(it.footprint);
62364
62620
  if (fp === void 0 || fp.type === "rect") continue;
62365
62621
  const hit = fp.type === "circle" ? Math.hypot(wxp - it.x, wyp - it.y) <= fp.value.radius : pointInPolygon(wxp, wyp, fp.value.vertices);
62366
62622
  if (hit) {
62367
- setSelected(it.key);
62368
- if (onSelectFn) queueMicrotask(() => onSelectFn(it.key));
62369
- return;
62623
+ select(it.key);
62624
+ return true;
62625
+ }
62626
+ }
62627
+ if (typeof document !== "undefined") {
62628
+ const cardKey = (_b = (_a2 = document.elementFromPoint(clientX, clientY)) == null ? void 0 : _a2.closest("[data-card-key]")) == null ? void 0 : _b.getAttribute("data-card-key");
62629
+ if (cardKey !== null && cardKey !== void 0) {
62630
+ select(cardKey);
62631
+ return true;
62370
62632
  }
62371
62633
  }
62634
+ const ctx = ((_c = drawRef.current) == null ? void 0 : _c.getContext("2d")) ?? null;
62635
+ if (ctx !== null) {
62636
+ ctx.save();
62637
+ ctx.font = MARKER_LABEL_FONT;
62638
+ }
62639
+ const camScreen = { ppu: ppuLive, tx: cam.tx, ty: cam.ty };
62372
62640
  let best = null, bestD = Infinity;
62373
62641
  for (const it of visibleItems) {
62374
62642
  const tier = tiers.get(it.key) ?? lod;
62375
62643
  if (tier === "card") continue;
62376
- const d2 = Math.hypot(it.x * ppu + view.tx - sx, it.y * ppu + view.ty - sy);
62377
- const reach = tier === "dot" ? 9 : 18;
62378
- if (d2 < reach && d2 < bestD) {
62644
+ const box = markerHitbox(it, tier, camScreen, (t4) => ctx !== null ? ctx.measureText(t4).width : t4.length * 6);
62645
+ if (!markerHit(box, sx, sy)) continue;
62646
+ const d2 = Math.hypot(it.x * ppuLive + cam.tx - sx, it.y * ppuLive + cam.ty - sy);
62647
+ if (d2 < bestD) {
62379
62648
  best = it;
62380
62649
  bestD = d2;
62381
62650
  }
62382
62651
  }
62383
- if (best !== null) flyToItem(best);
62384
- }, [size, view, ppu, index2, tiers, lod, visibleItems, onSelectFn, flyToItem]);
62652
+ if (ctx !== null) ctx.restore();
62653
+ if (best !== null) {
62654
+ flyToItem(best);
62655
+ return true;
62656
+ }
62657
+ return false;
62658
+ }, [tiers, lod, visibleItems, onSelectFn, flyToItem]);
62659
+ const onCanvasPointerDown = React.useCallback((e3) => {
62660
+ if (e3.button !== 0) return;
62661
+ if (e3.target.closest("button") !== null) return;
62662
+ transition("pointerDown");
62663
+ panRef.current = { x: e3.clientX, y: e3.clientY, tx: cameraRef.current.tx, ty: cameraRef.current.ty };
62664
+ movedRef.current = false;
62665
+ e3.currentTarget.setPointerCapture(e3.pointerId);
62666
+ }, [transition]);
62667
+ const onCanvasPointerMove = React.useCallback((e3) => {
62668
+ const pan = panRef.current;
62669
+ if (!pan) return;
62670
+ if (Math.abs(e3.clientX - pan.x) > 4 || Math.abs(e3.clientY - pan.y) > 4) movedRef.current = true;
62671
+ cameraRef.current = { ...cameraRef.current, tx: pan.tx + e3.clientX - pan.x, ty: pan.ty + e3.clientY - pan.y };
62672
+ requestRender();
62673
+ }, [requestRender]);
62674
+ const endPan = React.useCallback(() => {
62675
+ if (panRef.current === null) return false;
62676
+ panRef.current = null;
62677
+ transition("pointerUp");
62678
+ return true;
62679
+ }, [transition]);
62680
+ const onCanvasPointerUp = React.useCallback((e3) => {
62681
+ const wasPress = endPan();
62682
+ if (!wasPress || movedRef.current) return;
62683
+ if (!pickAt(e3.currentTarget, e3.clientX, e3.clientY)) setSelected(null);
62684
+ }, [endPan, pickAt]);
62685
+ const onCanvasPointerCancel = React.useCallback(() => {
62686
+ movedRef.current = false;
62687
+ endPan();
62688
+ }, [endPan]);
62689
+ const resetView = React.useCallback(() => {
62690
+ transition("cancel");
62691
+ cameraRef.current = IDENTITY;
62692
+ requestRender();
62693
+ }, [transition, requestRender]);
62694
+ const zoomBy = React.useCallback((factor) => {
62695
+ const sz = sizeRef.current;
62696
+ if (sz === null) return;
62697
+ transition("zoom");
62698
+ cameraRef.current = zoomAbout(cameraRef.current, factor, sz.w / 2, sz.h / 2, 1, MAX_ZOOM);
62699
+ requestRender();
62700
+ }, [transition, requestRender]);
62701
+ const minimapDragRef = React.useRef(false);
62702
+ const minimapJump = React.useCallback((el, clientX, clientY) => {
62703
+ const sz = sizeRef.current;
62704
+ if (sz === null) return;
62705
+ const rect = el.getBoundingClientRect();
62706
+ const cx2 = (clientX - rect.left) / rect.width * W2;
62707
+ const cy = (clientY - rect.top) / rect.height * H;
62708
+ const cam = cameraRef.current;
62709
+ cameraRef.current = { ...cam, tx: sz.w / 2 - cx2 * fitRef.current * cam.zoom, ty: sz.h / 2 - cy * fitRef.current * cam.zoom };
62710
+ requestRender();
62711
+ }, [W2, H, requestRender]);
62712
+ React.useEffect(() => () => {
62713
+ var _a2;
62714
+ (_a2 = coalescerRef.current) == null ? void 0 : _a2.cancel();
62715
+ if (animRef.current !== null) cancelAnimationFrame(animRef.current);
62716
+ }, [animRef]);
62717
+ React.useLayoutEffect(() => {
62718
+ applyCamera();
62719
+ }, [applyCamera, value, visibleItems, tiers, selected, palette, size, dpr, fit, cameraSnapshot]);
62385
62720
  const toggleZone = React.useCallback((key) => {
62386
- setOpenZone((prev) => prev === key ? nav.parentOf.get(key) ?? null : key);
62721
+ const collapsing = openZoneRef.current === key;
62722
+ zoneOverrideRef.current = collapsing ? key : null;
62723
+ setOpenZone(collapsing ? nav.parentOf.get(key) ?? null : key);
62387
62724
  }, [nav.parentOf]);
62388
62725
  React.useEffect(() => {
62389
- if (currentZone !== void 0) setOpenZone(currentZone);
62726
+ if (currentZone === void 0) return;
62727
+ if (zoneOverrideRef.current !== null) {
62728
+ if (zoneOverrideRef.current === currentZone) return;
62729
+ zoneOverrideRef.current = null;
62730
+ }
62731
+ setOpenZone(currentZone);
62390
62732
  }, [currentZone]);
62391
62733
  const openPath = React.useMemo(() => {
62392
62734
  const path2 = /* @__PURE__ */ new Set();
@@ -62411,7 +62753,15 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62411
62753
  react.Box,
62412
62754
  {
62413
62755
  css: styles2.navZone,
62414
- style: { paddingLeft: `${8 + depth * 14}px` },
62756
+ style: {
62757
+ paddingLeft: `${8 + depth * 14}px`,
62758
+ // Pin this header below its ancestors' pinned headers
62759
+ // (each level offset by one row) so the whole path stays
62760
+ // visible while scrolling the zone's items; shallower
62761
+ // levels stack above deeper ones.
62762
+ top: `calc(var(--nav-row-h) * ${depth})`,
62763
+ zIndex: 30 - depth
62764
+ },
62415
62765
  ...node2.zone.key === currentZone ? { "data-current": "" } : {},
62416
62766
  children: [
62417
62767
  expandable ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -62500,32 +62850,20 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62500
62850
  onPointerDown: onCanvasPointerDown,
62501
62851
  onPointerMove: onCanvasPointerMove,
62502
62852
  onPointerUp: onCanvasPointerUp,
62853
+ onPointerCancel: onCanvasPointerCancel,
62854
+ onLostPointerCapture: onCanvasPointerCancel,
62503
62855
  onDoubleClick: resetView,
62504
62856
  children: [
62505
- showGrid && size !== null && scaleLen > 0 && (() => {
62506
- const major = scaleLen * ppu;
62507
- const minor = major / 5;
62508
- return /* @__PURE__ */ jsxRuntime.jsx(
62509
- react.Box,
62510
- {
62511
- css: styles2.grid,
62512
- style: {
62513
- backgroundSize: `${major}px ${major}px, ${major}px ${major}px, ${minor}px ${minor}px, ${minor}px ${minor}px`,
62514
- backgroundPosition: `${view.tx}px 0, 0 ${view.ty}px, ${view.tx}px 0, 0 ${view.ty}px`
62515
- }
62516
- }
62517
- );
62518
- })(),
62857
+ showGrid && size !== null && /* @__PURE__ */ jsxRuntime.jsx(react.Box, { ref: gridRef, css: styles2.grid }),
62519
62858
  size !== null && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
62520
62859
  /* @__PURE__ */ jsxRuntime.jsx(
62521
62860
  "canvas",
62522
62861
  {
62523
62862
  ref: drawRef,
62524
- onClick: onCanvasClick,
62525
62863
  style: { position: "absolute", inset: 0, width: "100%", height: "100%" }
62526
62864
  }
62527
62865
  ),
62528
- visibleItems.map((item) => {
62866
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { ref: cardLayerRef, css: styles2.cardLayer, children: visibleItems.map((item) => {
62529
62867
  if ((tiers.get(item.key) ?? lod) !== "card") return null;
62530
62868
  const tone = statusTone(item.status);
62531
62869
  const isSelected = selected === item.key;
@@ -62539,17 +62877,14 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62539
62877
  react.Box,
62540
62878
  {
62541
62879
  css: styles2.item,
62880
+ "data-card-key": item.key,
62542
62881
  ...isSelected ? { "data-selected": "" } : {},
62543
62882
  style: {
62544
- left: wx(item.x),
62545
- top: wy(item.y),
62546
- ...typeof width === "number" ? { width: width * ppu } : {}
62547
- },
62548
- onClick: () => {
62549
- setSelected(item.key);
62550
- if (onSelectFn) queueMicrotask(() => onSelectFn(item.key));
62883
+ left: 0,
62884
+ top: 0,
62885
+ transform: `${cardTranslateCss(item.x, item.y)} translate(-50%, -50%)`,
62886
+ ...typeof width === "number" ? { width: cardWidthCss(width) } : {}
62551
62887
  },
62552
- onPointerDown: (e3) => e3.stopPropagation(),
62553
62888
  children: [
62554
62889
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { css: styles2.itemHead, children: [
62555
62890
  iconDef !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "span", css: styles2.itemIcon, children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: iconDef }) }),
@@ -62563,7 +62898,7 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62563
62898
  },
62564
62899
  item.key
62565
62900
  );
62566
- }),
62901
+ }) }),
62567
62902
  /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { css: styles2.controls, children: [
62568
62903
  /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "button", css: styles2.controlButton, "aria-label": "Zoom in", title: "Zoom in (scroll)", onClick: () => zoomBy(1.5), children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faPlus }) }),
62569
62904
  /* @__PURE__ */ jsxRuntime.jsx(react.Box, { as: "button", css: styles2.controlButton, "aria-label": "Zoom out", title: "Zoom out (scroll)", onClick: () => zoomBy(1 / 1.5), children: /* @__PURE__ */ jsxRuntime.jsx(FontAwesomeIcon, { icon: faMinus }) }),
@@ -62607,16 +62942,20 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62607
62942
  style: { width: 150, height: 150 * H / W2 },
62608
62943
  onPointerDown: (e3) => {
62609
62944
  e3.stopPropagation();
62610
- const rect = e3.currentTarget.getBoundingClientRect();
62611
- const cx2 = (e3.clientX - rect.left) / rect.width * W2;
62612
- const cy = (e3.clientY - rect.top) / rect.height * H;
62613
- if (size !== null) {
62614
- setView((prev) => ({
62615
- ...prev,
62616
- tx: size.w / 2 - cx2 * fit * prev.zoom,
62617
- ty: size.h / 2 - cy * fit * prev.zoom
62618
- }));
62619
- }
62945
+ const el = e3.currentTarget;
62946
+ el.setPointerCapture(e3.pointerId);
62947
+ transition("cancel");
62948
+ minimapDragRef.current = true;
62949
+ minimapJump(el, e3.clientX, e3.clientY);
62950
+ },
62951
+ onPointerMove: (e3) => {
62952
+ if (!minimapDragRef.current) return;
62953
+ e3.stopPropagation();
62954
+ minimapJump(e3.currentTarget, e3.clientX, e3.clientY);
62955
+ },
62956
+ onPointerUp: (e3) => {
62957
+ minimapDragRef.current = false;
62958
+ e3.currentTarget.releasePointerCapture(e3.pointerId);
62620
62959
  },
62621
62960
  children: [
62622
62961
  value.zones.map((zone) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -62637,8 +62976,8 @@ const EastChakraSchematic = React.memo(function EastChakraSchematic2({ value })
62637
62976
  {
62638
62977
  css: styles2.minimapViewport,
62639
62978
  style: {
62640
- left: `${Math.max(0, -view.tx / ppu / W2 * 100)}%`,
62641
- top: `${Math.max(0, -view.ty / ppu / H * 100)}%`,
62979
+ left: `${Math.max(0, -cameraSnapshot.tx / ppu / W2 * 100)}%`,
62980
+ top: `${Math.max(0, -cameraSnapshot.ty / ppu / H * 100)}%`,
62642
62981
  width: `${Math.min(100, size.w / ppu / W2 * 100)}%`,
62643
62982
  height: `${Math.min(100, size.h / ppu / H * 100)}%`
62644
62983
  }