@cr8rcho/alkahest 0.1.11 → 0.1.13

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.
@@ -64,6 +64,8 @@
64
64
  #theme-toggle svg, #fit-btn svg { width: 16px; height: 16px; }
65
65
  [data-theme="dark"] #theme-toggle .i-sun { display: block; } [data-theme="dark"] #theme-toggle .i-moon { display: none; }
66
66
  [data-theme="light"] #theme-toggle .i-sun { display: none; } [data-theme="light"] #theme-toggle .i-moon { display: block; }
67
+ /* Fit + toggle are a tight pair (8px), set apart from the rest of the toolbar by its 16px gap. */
68
+ .toolbar-actions { display: inline-flex; align-items: center; gap: 8px; }
67
69
  .brand { font-weight: 700; letter-spacing: 0.3px; white-space: nowrap; }
68
70
  .brand .sub { color: var(--muted); font-weight: 400; margin-left: 6px; }
69
71
  .counts { color: var(--muted); white-space: nowrap; }
@@ -165,8 +167,10 @@
165
167
  <span><span class="edge" style="border-top-style: dotted; border-color: var(--edge)"></span>Contains</span>
166
168
  <span><span class="edge" style="border-top-style: dashed; border-color: var(--edge)"></span>Call</span>
167
169
  </div>
168
- <button class="theme-toggle" id="fit-btn" aria-label="Fit to view" title="Fit to view"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg></button>
169
- <button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme" title="Toggle theme"><svg class="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><svg class="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button>
170
+ <div class="toolbar-actions">
171
+ <button class="theme-toggle" id="fit-btn" aria-label="Fit to view" title="Fit to view"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg></button>
172
+ <button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme" title="Toggle theme"><svg class="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><svg class="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button>
173
+ </div>
170
174
  </header>
171
175
  <main>
172
176
  <svg id="graph">
@@ -602,18 +606,54 @@
602
606
  const DRAG_THRESHOLD = 4;
603
607
  let down = null; // {node|null, x, y, moved, pointerId}
604
608
 
609
+ // Active pointers for multi-touch. Two pointers down → pinch-to-zoom.
610
+ const pointers = new Map(); // pointerId -> {x, y}
611
+ let pinch = null; // { dist, midX, midY } for the running gesture
612
+
613
+ function startPinch() {
614
+ const pts = [...pointers.values()];
615
+ const dx = pts[0].x - pts[1].x, dy = pts[0].y - pts[1].y;
616
+ const rect = svg.getBoundingClientRect();
617
+ pinch = {
618
+ dist: Math.hypot(dx, dy) || 1,
619
+ midX: (pts[0].x + pts[1].x) / 2 - rect.left,
620
+ midY: (pts[0].y + pts[1].y) / 2 - rect.top,
621
+ };
622
+ // a pinch is purely a zoom — abandon any in-progress tap / pan / node drag
623
+ dragging = null; panStart = null; down = null;
624
+ }
625
+
605
626
  function onNodeDown(ev, n) {
606
627
  ev.stopPropagation();
607
- down = { node: n, x: ev.clientX, y: ev.clientY, moved: false, pointerId: ev.pointerId };
628
+ pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
608
629
  svg.setPointerCapture(ev.pointerId);
630
+ if (pointers.size === 2) { startPinch(); return; }
631
+ down = { node: n, x: ev.clientX, y: ev.clientY, moved: false, pointerId: ev.pointerId };
609
632
  }
610
633
  svg.addEventListener("pointerdown", (ev) => {
634
+ pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
635
+ svg.setPointerCapture(ev.pointerId);
636
+ if (pointers.size === 2) { startPinch(); return; }
611
637
  if (down) return; // already started on a node
612
638
  down = { node: null, x: ev.clientX, y: ev.clientY, moved: false, pointerId: ev.pointerId };
613
639
  panStart = { x: ev.clientX - tx, y: ev.clientY - ty };
614
- svg.setPointerCapture(ev.pointerId);
615
640
  });
616
641
  svg.addEventListener("pointermove", (ev) => {
642
+ if (pointers.has(ev.pointerId)) pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
643
+ if (pinch && pointers.size >= 2) {
644
+ const pts = [...pointers.values()];
645
+ const dx = pts[0].x - pts[1].x, dy = pts[0].y - pts[1].y;
646
+ const dist = Math.hypot(dx, dy) || 1;
647
+ const rect = svg.getBoundingClientRect();
648
+ const mx = (pts[0].x + pts[1].x) / 2 - rect.left, my = (pts[0].y + pts[1].y) / 2 - rect.top;
649
+ // pan by how far the two-finger midpoint moved, then zoom around that midpoint
650
+ tx += mx - pinch.midX; ty += my - pinch.midY;
651
+ const nk = Math.min(4, Math.max(0.2, k * (dist / pinch.dist)));
652
+ tx = mx - (mx - tx) * (nk / k); ty = my - (my - ty) * (nk / k); k = nk;
653
+ pinch.dist = dist; pinch.midX = mx; pinch.midY = my;
654
+ applyTransform();
655
+ return;
656
+ }
617
657
  if (!down) return;
618
658
  if (!down.moved && Math.hypot(ev.clientX - down.x, ev.clientY - down.y) > DRAG_THRESHOLD) {
619
659
  down.moved = true;
@@ -623,8 +663,21 @@
623
663
  if (dragging) { const w = toWorld(ev.clientX, ev.clientY); dragging.x = w.x; dragging.y = w.y; dragging.vx = 0; dragging.vy = 0; } // free 2D since it's force-based
624
664
  else if (panStart) { tx = ev.clientX - panStart.x; ty = ev.clientY - panStart.y; applyTransform(); }
625
665
  });
626
- svg.addEventListener("pointerup", () => {
627
- if (down && !down.moved) { // didn't move = tap
666
+ function onPointerUp(ev) {
667
+ pointers.delete(ev.pointerId);
668
+ if (pinch) {
669
+ if (pointers.size < 2) {
670
+ pinch = null;
671
+ if (pointers.size === 1) {
672
+ // one finger left after a pinch → resume panning from there (no jump)
673
+ const [id, p] = [...pointers.entries()][0];
674
+ down = { node: null, x: p.x, y: p.y, moved: true, pointerId: id };
675
+ panStart = { x: p.x - tx, y: p.y - ty };
676
+ }
677
+ }
678
+ return; // a pinch gesture never counts as a tap
679
+ }
680
+ if (ev.type !== "pointercancel" && down && !down.moved) { // didn't move = tap
628
681
  if (down.node) select(down.node);
629
682
  else clearSelection();
630
683
  } else if (dragging) {
@@ -632,7 +685,9 @@
632
685
  alpha = 0.06;
633
686
  }
634
687
  dragging = null; panStart = null; down = null;
635
- });
688
+ }
689
+ svg.addEventListener("pointerup", onPointerUp);
690
+ svg.addEventListener("pointercancel", onPointerUp);
636
691
  svg.addEventListener("wheel", (ev) => {
637
692
  ev.preventDefault();
638
693
  const rect = svg.getBoundingClientRect();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cr8rcho/alkahest",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },