@unbrained/pm-web 2026.9.13 → 2026.9.23

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.
Files changed (129) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +6 -4
  3. package/dist/app.d.ts +10 -0
  4. package/dist/app.js +19 -9
  5. package/dist/app.js.map +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/routes/admin.js +6 -6
  8. package/dist/routes/admin.js.map +1 -1
  9. package/dist/routes/auth.js +2 -2
  10. package/dist/routes/auth.js.map +1 -1
  11. package/dist/routes/extensions.js +24 -37
  12. package/dist/routes/extensions.js.map +1 -1
  13. package/dist/routes/github.js +169 -107
  14. package/dist/routes/github.js.map +1 -1
  15. package/dist/routes/groups.js +9 -14
  16. package/dist/routes/groups.js.map +1 -1
  17. package/dist/routes/pm.js +532 -1015
  18. package/dist/routes/pm.js.map +1 -1
  19. package/dist/routes/projects.js +10 -6
  20. package/dist/routes/projects.js.map +1 -1
  21. package/dist/routes/route-helpers.d.ts +74 -0
  22. package/dist/routes/route-helpers.js +85 -0
  23. package/dist/routes/route-helpers.js.map +1 -0
  24. package/dist/routes/sharing.js +30 -18
  25. package/dist/routes/sharing.js.map +1 -1
  26. package/dist/server.js +18 -23
  27. package/dist/server.js.map +1 -1
  28. package/dist/services/mutation-event-watcher.js +3 -28
  29. package/dist/services/mutation-event-watcher.js.map +1 -1
  30. package/dist/services/pm-runner.js +3 -1
  31. package/dist/services/pm-runner.js.map +1 -1
  32. package/dist/services/project-watcher.js +14 -38
  33. package/dist/services/project-watcher.js.map +1 -1
  34. package/dist/services/sse.d.ts +7 -0
  35. package/dist/services/sse.js +22 -0
  36. package/dist/services/sse.js.map +1 -1
  37. package/dist/services/watcher-utils.d.ts +42 -0
  38. package/dist/services/watcher-utils.js +60 -0
  39. package/dist/services/watcher-utils.js.map +1 -0
  40. package/manifest.json +2 -2
  41. package/package.json +20 -9
  42. package/public/src/app.js +22 -52
  43. package/public/src/app.js.map +1 -1
  44. package/public/src/app.ts +18 -75
  45. package/public/src/browser-window.js +29 -0
  46. package/public/src/browser-window.js.map +1 -0
  47. package/public/src/browser-window.ts +56 -0
  48. package/public/src/components/modals.js +1 -1
  49. package/public/src/components/modals.js.map +1 -1
  50. package/public/src/components/modals.ts +1 -1
  51. package/public/src/i18n.js +14 -6
  52. package/public/src/i18n.js.map +1 -1
  53. package/public/src/i18n.ts +16 -6
  54. package/public/src/sw.ts +1 -0
  55. package/public/src/utils.js +58 -0
  56. package/public/src/utils.js.map +1 -1
  57. package/public/src/utils.ts +53 -0
  58. package/public/src/views/admin.js +2 -5
  59. package/public/src/views/admin.js.map +1 -1
  60. package/public/src/views/admin.ts +2 -2
  61. package/public/src/views/auth.js +5 -1
  62. package/public/src/views/auth.js.map +1 -1
  63. package/public/src/views/auth.ts +6 -1
  64. package/public/src/views/calendar.js +1 -2
  65. package/public/src/views/calendar.js.map +1 -1
  66. package/public/src/views/calendar.ts +1 -2
  67. package/public/src/views/config.js +14 -21
  68. package/public/src/views/config.js.map +1 -1
  69. package/public/src/views/config.ts +14 -19
  70. package/public/src/views/context.js +11 -12
  71. package/public/src/views/context.js.map +1 -1
  72. package/public/src/views/context.ts +13 -12
  73. package/public/src/views/create.js +3 -11
  74. package/public/src/views/create.js.map +1 -1
  75. package/public/src/views/create.ts +3 -3
  76. package/public/src/views/export.js +2 -3
  77. package/public/src/views/export.js.map +1 -1
  78. package/public/src/views/export.ts +2 -3
  79. package/public/src/views/github.js +33 -47
  80. package/public/src/views/github.js.map +1 -1
  81. package/public/src/views/github.ts +35 -28
  82. package/public/src/views/graph-canvas.js +92 -99
  83. package/public/src/views/graph-canvas.js.map +1 -1
  84. package/public/src/views/graph-canvas.ts +94 -93
  85. package/public/src/views/graph.js +88 -73
  86. package/public/src/views/graph.js.map +1 -1
  87. package/public/src/views/graph.ts +97 -63
  88. package/public/src/views/groups.js +2 -5
  89. package/public/src/views/groups.js.map +1 -1
  90. package/public/src/views/groups.ts +2 -2
  91. package/public/src/views/guide.js +11 -10
  92. package/public/src/views/guide.js.map +1 -1
  93. package/public/src/views/guide.ts +12 -10
  94. package/public/src/views/items.js +100 -130
  95. package/public/src/views/items.js.map +1 -1
  96. package/public/src/views/items.ts +99 -117
  97. package/public/src/views/normalize.js +2 -9
  98. package/public/src/views/normalize.js.map +1 -1
  99. package/public/src/views/normalize.ts +2 -9
  100. package/public/src/views/packages.js +7 -4
  101. package/public/src/views/packages.js.map +1 -1
  102. package/public/src/views/packages.ts +8 -4
  103. package/public/src/views/plan-execution.js.map +1 -1
  104. package/public/src/views/plan-execution.ts +5 -15
  105. package/public/src/views/plan.js +2 -1
  106. package/public/src/views/plan.js.map +1 -1
  107. package/public/src/views/plan.ts +6 -17
  108. package/public/src/views/projects.js +5 -6
  109. package/public/src/views/projects.js.map +1 -1
  110. package/public/src/views/projects.ts +5 -6
  111. package/public/src/views/search.js +5 -1
  112. package/public/src/views/search.js.map +1 -1
  113. package/public/src/views/search.ts +7 -1
  114. package/public/src/views/settings.js +30 -55
  115. package/public/src/views/settings.js.map +1 -1
  116. package/public/src/views/settings.ts +23 -17
  117. package/public/src/views/shared.js +2 -5
  118. package/public/src/views/shared.js.map +1 -1
  119. package/public/src/views/shared.ts +2 -5
  120. package/public/src/views/sharing.js +2 -5
  121. package/public/src/views/sharing.js.map +1 -1
  122. package/public/src/views/sharing.ts +2 -2
  123. package/public/src/views/templates.js +9 -16
  124. package/public/src/views/templates.js.map +1 -1
  125. package/public/src/views/templates.ts +9 -14
  126. package/public/src/views/validate.js +2 -9
  127. package/public/src/views/validate.js.map +1 -1
  128. package/public/src/views/validate.ts +2 -9
  129. package/public/sw.js +1 -0
@@ -3,11 +3,32 @@
3
3
  // ═══════════════════════════════════════════════════════════════
4
4
  import { state } from '../state.js';
5
5
  import { api } from '../api.js';
6
- import { escHtml } from '../utils.js';
6
+ import { escHtml, resetButton, showError } from '../utils.js';
7
7
  import { confirmDialog } from '../components/modals.js';
8
8
  import { toast } from '../components/toast.js';
9
+ import { loadItemsBadge } from './projects.js';
9
10
  import type { GitHubImportResponse, GitHubIssuesResponse, GitHubIssueRow, GitHubPushResponse, GitHubRepoResponse, ListResponse } from '../api-types.js';
10
11
 
12
+ /** Renders a styled result box with a title header and inner HTML body, used
13
+ * by the push and import result displays. */
14
+ function githubResultBox(title: string, bodyHtml: string): string {
15
+ return `<div style="background:var(--bg-card2);border:1px solid var(--border);border-radius:var(--radius);padding:12px">
16
+ <div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">${title}</div>
17
+ ${bodyHtml}
18
+ </div>`;
19
+ }
20
+
21
+ /** Toast the result of a GitHub push or import operation with a count and any
22
+ * errors, matching the pluralisation and error-count format both share. */
23
+ function githubOpToast(verb: string, noun: string, count: number, errors: string[]): void {
24
+ toast(`${verb} ${count} ${noun}${count!==1?'s':''}${errors.length?' ('+errors.length+' error'+(errors.length!==1?'s':'')+')':''}`, errors.length ? 'info' : 'success');
25
+ }
26
+
27
+ /** Show result HTML in a result element, making it visible. No-op when null. */
28
+ function showResultEl(el: HTMLElement | null, html: string): void {
29
+ if (el) { el.style.display = ''; el.innerHTML = html; }
30
+ }
31
+
11
32
  /**
12
33
  * Renders the GitHub integration page for the current project: a header, a
13
34
  * token-required prompt when the user has no GitHub token, and otherwise the
@@ -150,8 +171,8 @@ export async function linkGitHubRepo(): Promise<void> {
150
171
  toast('Repository linked','success');
151
172
  renderGitHubView();
152
173
  } catch(err: unknown) {
153
- if (errEl) { errEl.textContent = err instanceof Error ? err.message : String(err); errEl.style.display = 'block'; }
154
- if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Link Repository'; }
174
+ showError(errEl, err);
175
+ resetButton(btn, 'Link Repository');
155
176
  }
156
177
  }
157
178
 
@@ -296,21 +317,15 @@ export async function pushItemsToGitHub(): Promise<void> {
296
317
  const data = await api<GitHubPushResponse>('POST', `/projects/${state.currentProject!.id}/github/push`, { itemIds });
297
318
  const pushed = data.pushed || [];
298
319
  const errors = data.errors || [];
299
- toast(`Pushed ${pushed.length} item${pushed.length!==1?'s':''}${errors.length?' ('+errors.length+' error'+( errors.length!==1?'s':'')+')':''}`, errors.length ? 'info' : 'success');
300
- if (resultEl) {
301
- resultEl.style.display = '';
302
- resultEl.innerHTML = `
303
- <div style="background:var(--bg-card2);border:1px solid var(--border);border-radius:var(--radius);padding:12px">
304
- <div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">Push Results</div>
305
- ${pushed.length > 0 ? `<div style="color:var(--status-closed);font-size:13px;margin-bottom:6px">✓ Pushed ${pushed.length}: ${pushed.map((p)=>`<a href="${escHtml(p.issueUrl)}" target="_blank" style="color:var(--accent)">#${p.issueNumber}</a>`).join(', ')}</div>` : ''}
306
- ${errors.length > 0 ? `<div style="color:var(--status-blocked);font-size:13px">✗ ${errors.length} failed: ${errors.map((e)=>escHtml(String(e))).join('; ')}</div>` : ''}
307
- </div>`;
308
- }
320
+ githubOpToast('Pushed', 'item', pushed.length, errors);
321
+ showResultEl(resultEl, githubResultBox('Push Results',
322
+ `${pushed.length > 0 ? `<div style="color:var(--status-closed);font-size:13px;margin-bottom:6px">✓ Pushed ${pushed.length}: ${pushed.map((p)=>`<a href="${escHtml(p.issueUrl)}" target="_blank" style="color:var(--accent)">#${p.issueNumber}</a>`).join(', ')}</div>` : ''}
323
+ ${errors.length > 0 ? `<div style="color:var(--status-blocked);font-size:13px">✗ ${errors.length} failed: ${errors.map((e)=>escHtml(String(e))).join('; ')}</div>` : ''}`));
309
324
  loadItemsForPush();
310
325
  } catch(err: unknown) {
311
326
  toast(`Push failed: ${err instanceof Error ? err.message : String(err)}`, 'error');
312
327
  } finally {
313
- if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Push to GitHub'; }
328
+ resetButton(btn, 'Push to GitHub');
314
329
  }
315
330
  }
316
331
 
@@ -347,22 +362,14 @@ export async function importGitHubIssues(): Promise<void> {
347
362
  const data = await api<GitHubImportResponse>('POST',`/projects/${state.currentProject!.id}/github/import`,{issueNumbers});
348
363
  const created = data.created || [];
349
364
  const errors = data.errors || [];
350
- toast(`Imported ${created.length} issue${created.length!==1?'s':''}${errors.length?' ('+errors.length+' error'+( errors.length!==1?'s':'')+')':''}`, errors.length ? 'info' : 'success');
351
- if (resultEl) {
352
- resultEl.style.display = '';
353
- resultEl.innerHTML = `
354
- <div style="background:var(--bg-card2);border:1px solid var(--border);border-radius:var(--radius);padding:12px">
355
- <div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">Import Results</div>
356
- ${created.length > 0 ? `<div style="color:var(--status-closed);font-size:13px;margin-bottom:6px">✓ Created ${created.length} item${created.length!==1?'s':''}: ${created.map((id)=>`<span class="mono" style="font-size:11px">${escHtml(String(id))}</span>`).join(', ')}</div>` : ''}
357
- ${errors.length > 0 ? `<div style="color:var(--status-blocked);font-size:13px">✗ ${errors.length} error${errors.length!==1?'s':''}: ${errors.map((e)=>escHtml(String(e))).join('; ')}</div>` : ''}
358
- </div>`;
359
- }
360
- // loadItemsBadge is in projects.ts, import via app bridge
361
- const loadBadge = window.__app?.loadItemsBadge;
362
- if (loadBadge) loadBadge();
365
+ githubOpToast('Imported', 'issue', created.length, errors);
366
+ showResultEl(resultEl, githubResultBox('Import Results',
367
+ `${created.length > 0 ? `<div style="color:var(--status-closed);font-size:13px;margin-bottom:6px">✓ Created ${created.length} item${created.length!==1?'s':''}: ${created.map((id)=>`<span class="mono" style="font-size:11px">${escHtml(String(id))}</span>`).join(', ')}</div>` : ''}
368
+ ${errors.length > 0 ? `<div style="color:var(--status-blocked);font-size:13px">✗ ${errors.length} error${errors.length!==1?'s':''}: ${errors.map((e)=>escHtml(String(e))).join('; ')}</div>` : ''}`));
369
+ void loadItemsBadge();
363
370
  } catch(err: unknown) {
364
371
  toast(`Import failed: ${err instanceof Error ? err.message : String(err)}`,'error');
365
372
  } finally {
366
- if (btn) { btn.disabled = false; const sp = btn.querySelector('span'); if (sp) sp.textContent = 'Import Selected'; }
373
+ resetButton(btn, 'Import Selected');
367
374
  }
368
375
  }
@@ -137,6 +137,11 @@ function escHtml(s) {
137
137
  .replace(/>/g, '&gt;')
138
138
  .replace(/"/g, '&quot;');
139
139
  }
140
+ /** Builds a tag-to-color map from a tag frequency map, assigning palette colors to the most frequent tags in descending order. */
141
+ export function tagColorMapFromFreq(freq) {
142
+ const top = [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, TAG_PALETTE.length).map(([t]) => t);
143
+ return new Map(top.map((t, i) => [t, TAG_PALETTE[i]]));
144
+ }
140
145
  // ═══════════════════════════════════════════════════════════════
141
146
  // GraphCanvas class
142
147
  // ═══════════════════════════════════════════════════════════════
@@ -548,6 +553,21 @@ export class GraphCanvas {
548
553
  return false;
549
554
  return a.source.id === b.source.id && a.target.id === b.target.id && a.type === b.type;
550
555
  }
556
+ /** Returns the bidirectional edge key, flag, and curve direction for the given source/target node pair. */
557
+ edgeBidirInfo(s, t) {
558
+ const key = [s.id, t.id].sort().join('|');
559
+ const isBiDir = this.biDirPairs.has(key);
560
+ const curveDir = isBiDir ? (s.id < t.id ? 1 : -1) : 0;
561
+ return { key, isBiDir, curveDir };
562
+ }
563
+ /** Computes the start and end points of an edge segment, offset from the node radii along the unit direction vector. */
564
+ edgeEndpoints(s, t, nx, ny) {
565
+ const x1 = s.x + nx * s.r;
566
+ const y1 = s.y + ny * s.r;
567
+ const x2 = t.x - nx * (t.r + 7);
568
+ const y2 = t.y - ny * (t.r + 7);
569
+ return { x1, y1, x2, y2 };
570
+ }
551
571
  buildEdgeGeometry(edge) {
552
572
  const { source: s, target: t } = edge;
553
573
  const dx = t.x - s.x || 0.01;
@@ -557,16 +577,11 @@ export class GraphCanvas {
557
577
  const ny = dy / len;
558
578
  const px = -dy / len;
559
579
  const py = dx / len;
560
- const key = [s.id, t.id].sort().join('|');
561
- const isBiDir = this.biDirPairs.has(key);
562
- const curveDir = isBiDir ? (s.id < t.id ? 1 : -1) : 0;
580
+ const { key, isBiDir, curveDir } = this.edgeBidirInfo(s, t);
563
581
  const cpOffset = isBiDir ? len * 0.22 * curveDir : 0;
564
582
  const cpX = (s.x + t.x) / 2 + px * cpOffset;
565
583
  const cpY = (s.y + t.y) / 2 + py * cpOffset;
566
- const x1 = s.x + nx * s.r;
567
- const y1 = s.y + ny * s.r;
568
- const x2 = t.x - nx * (t.r + 7);
569
- const y2 = t.y - ny * (t.r + 7);
584
+ const { x1, y1, x2, y2 } = this.edgeEndpoints(s, t, nx, ny);
570
585
  const qX = (u) => (1 - u) * (1 - u) * x1 + 2 * (1 - u) * u * cpX + u * u * x2;
571
586
  const qY = (u) => (1 - u) * (1 - u) * y1 + 2 * (1 - u) * u * cpY + u * u * y2;
572
587
  const midU = 0.5;
@@ -683,8 +698,7 @@ export class GraphCanvas {
683
698
  for (const t of n.tags ?? [])
684
699
  freq.set(t, (freq.get(t) ?? 0) + 1);
685
700
  }
686
- const top = [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, TAG_PALETTE.length).map(([t]) => t);
687
- this.tagColorMap = new Map(top.map((t, i) => [t, TAG_PALETTE[i]]));
701
+ this.tagColorMap = tagColorMapFromFreq(freq);
688
702
  }
689
703
  else {
690
704
  this.tagColorMap = new Map();
@@ -1272,9 +1286,7 @@ export class GraphCanvas {
1272
1286
  const px = -dy / len;
1273
1287
  const py = dx / len;
1274
1288
  // Determine curvature direction for bidirectional edges
1275
- const key = [s.id, t.id].sort().join('|');
1276
- const isBiDir = this.biDirPairs.has(key);
1277
- const curveDir = isBiDir ? (s.id < t.id ? 1 : -1) : 0;
1289
+ const { isBiDir, curveDir } = this.edgeBidirInfo(s, t);
1278
1290
  const curvature = isBiDir ? 0.22 : 0.0;
1279
1291
  const cpFactor = len * curvature * curveDir;
1280
1292
  // Control point (on the perpendicular bisector)
@@ -1283,10 +1295,7 @@ export class GraphCanvas {
1283
1295
  // Start/end points offset from node radii
1284
1296
  const nx = dx / len;
1285
1297
  const ny = dy / len;
1286
- const x1 = s.x + nx * s.r;
1287
- const y1 = s.y + ny * s.r;
1288
- const x2 = t.x - nx * (t.r + 7);
1289
- const y2 = t.y - ny * (t.r + 7);
1298
+ const { x1, y1, x2, y2 } = this.edgeEndpoints(s, t, nx, ny);
1290
1299
  const color = onCritPath ? '#fbbf24' : getEdgeColor(edge.type);
1291
1300
  const isActive = highlighted || onCritPath;
1292
1301
  ctx.save();
@@ -1777,11 +1786,8 @@ export class GraphCanvas {
1777
1786
  break;
1778
1787
  }
1779
1788
  }
1780
- onMouseDown(e) {
1781
- if (e.button !== 0)
1782
- return;
1783
- this.canvas.focus();
1784
- const { x, y } = this.getPos(e);
1789
+ /** Initiates a pointer drag at the given canvas coordinates: records the start position, performs a hit test, and begins either a node drag or canvas pan. */
1790
+ beginPointerDrag(x, y, setCursor) {
1785
1791
  this.lastX = x;
1786
1792
  this.lastY = y;
1787
1793
  this.downX = x;
@@ -1795,33 +1801,75 @@ export class GraphCanvas {
1795
1801
  hit.fx = hit.x;
1796
1802
  hit.fy = hit.y;
1797
1803
  this.alpha = Math.max(this.alpha, 0.25);
1798
- this.canvas.style.cursor = 'grabbing';
1804
+ if (setCursor)
1805
+ this.canvas.style.cursor = 'grabbing';
1799
1806
  }
1800
1807
  else {
1801
1808
  this.isDraggingCanvas = true;
1802
- this.canvas.style.cursor = 'grabbing';
1809
+ if (setCursor)
1810
+ this.canvas.style.cursor = 'grabbing';
1803
1811
  }
1804
1812
  }
1805
- onMouseMove(e) {
1806
- const r = this.canvas.getBoundingClientRect();
1807
- const x = e.clientX - r.left;
1808
- const y = e.clientY - r.top;
1809
- const dx = x - this.lastX;
1810
- const dy = y - this.lastY;
1811
- if (Math.abs(x - this.downX) > 3 || Math.abs(y - this.downY) > 3)
1813
+ /** Returns the canvas-relative coordinates of the first touch, or null when there is not exactly one active touch. */
1814
+ firstTouchPos(e) {
1815
+ if (e.touches.length !== 1)
1816
+ return null;
1817
+ const rect = this.canvas.getBoundingClientRect();
1818
+ return { x: e.touches[0].clientX - rect.left, y: e.touches[0].clientY - rect.top };
1819
+ }
1820
+ /** Handles a pointer move during an active drag: updates hasMoved, drags the grabbed node or pans the canvas, and returns true when a drag consumed the event. */
1821
+ handleDragMove(x, y, dx, dy, moveThreshold, clearFlyOnNode) {
1822
+ if (Math.abs(x - this.downX) > moveThreshold || Math.abs(y - this.downY) > moveThreshold)
1812
1823
  this.hasMoved = true;
1813
1824
  if (this.isDraggingNode && this.dragNode) {
1814
1825
  const [wx, wy] = this.toWorld(x, y);
1815
1826
  this.dragNode.fx = wx;
1816
1827
  this.dragNode.fy = wy;
1817
- this.flyTarget = null;
1828
+ if (clearFlyOnNode)
1829
+ this.flyTarget = null;
1830
+ return true;
1818
1831
  }
1819
- else if (this.isDraggingCanvas) {
1832
+ if (this.isDraggingCanvas) {
1820
1833
  this.tx += dx;
1821
1834
  this.ty += dy;
1822
1835
  this.flyTarget = null;
1836
+ return true;
1823
1837
  }
1824
- else {
1838
+ return false;
1839
+ }
1840
+ /** Ends a node drag gesture: if the pointer did not move, toggles selection and flies to the node, then clears the drag state. Returns true when a node drag was in progress. */
1841
+ endNodeDrag() {
1842
+ if (!this.isDraggingNode || !this.dragNode)
1843
+ return false;
1844
+ if (!this.hasMoved) {
1845
+ const id = this.dragNode.id;
1846
+ const newSel = this.filter.selectedId === id ? null : id;
1847
+ this.filter = { ...this.filter, selectedId: newSel };
1848
+ this.particles = [];
1849
+ this.lastParticleSpawn = 0;
1850
+ this.onSelectNode(newSel);
1851
+ if (newSel)
1852
+ this.flyTo(this.dragNode);
1853
+ }
1854
+ this.dragNode.fx = null;
1855
+ this.dragNode.fy = null;
1856
+ this.dragNode = null;
1857
+ return true;
1858
+ }
1859
+ onMouseDown(e) {
1860
+ if (e.button !== 0)
1861
+ return;
1862
+ this.canvas.focus();
1863
+ const { x, y } = this.getPos(e);
1864
+ this.beginPointerDrag(x, y, true);
1865
+ }
1866
+ onMouseMove(e) {
1867
+ const r = this.canvas.getBoundingClientRect();
1868
+ const x = e.clientX - r.left;
1869
+ const y = e.clientY - r.top;
1870
+ const dx = x - this.lastX;
1871
+ const dy = y - this.lastY;
1872
+ if (!this.handleDragMove(x, y, dx, dy, 3, true)) {
1825
1873
  const [wx, wy] = this.toWorld(x, y);
1826
1874
  const hit = this.hitTest(wx, wy);
1827
1875
  const newHov = hit?.id ?? null;
@@ -1838,20 +1886,7 @@ export class GraphCanvas {
1838
1886
  onMouseUp(e) {
1839
1887
  if (e.button !== 0)
1840
1888
  return;
1841
- if (this.isDraggingNode && this.dragNode) {
1842
- if (!this.hasMoved) {
1843
- const id = this.dragNode.id;
1844
- const newSel = this.filter.selectedId === id ? null : id;
1845
- this.filter = { ...this.filter, selectedId: newSel };
1846
- this.particles = [];
1847
- this.lastParticleSpawn = 0;
1848
- this.onSelectNode(newSel);
1849
- if (newSel)
1850
- this.flyTo(this.dragNode);
1851
- }
1852
- this.dragNode.fx = null;
1853
- this.dragNode.fy = null;
1854
- this.dragNode = null;
1889
+ if (this.endNodeDrag()) {
1855
1890
  this.isDraggingNode = false;
1856
1891
  }
1857
1892
  else if (this.isDraggingCanvas) {
@@ -1879,29 +1914,12 @@ export class GraphCanvas {
1879
1914
  }
1880
1915
  onTouchStart(e) {
1881
1916
  e.preventDefault();
1882
- const rect = this.canvas.getBoundingClientRect();
1883
- if (e.touches.length === 1) {
1884
- const x = e.touches[0].clientX - rect.left;
1885
- const y = e.touches[0].clientY - rect.top;
1886
- this.lastX = x;
1887
- this.lastY = y;
1888
- this.downX = x;
1889
- this.downY = y;
1890
- this.hasMoved = false;
1891
- const [wx, wy] = this.toWorld(x, y);
1892
- const hit = this.hitTest(wx, wy);
1893
- if (hit) {
1894
- this.isDraggingNode = true;
1895
- this.dragNode = hit;
1896
- hit.fx = hit.x;
1897
- hit.fy = hit.y;
1898
- this.alpha = Math.max(this.alpha, 0.25);
1899
- }
1900
- else {
1901
- this.isDraggingCanvas = true;
1902
- }
1917
+ const pos = this.firstTouchPos(e);
1918
+ if (pos) {
1919
+ this.beginPointerDrag(pos.x, pos.y, false);
1903
1920
  }
1904
1921
  else if (e.touches.length === 2) {
1922
+ const rect = this.canvas.getBoundingClientRect();
1905
1923
  this.isDraggingNode = false;
1906
1924
  this.isDraggingCanvas = false;
1907
1925
  if (this.dragNode) {
@@ -1918,28 +1936,17 @@ export class GraphCanvas {
1918
1936
  }
1919
1937
  onTouchMove(e) {
1920
1938
  e.preventDefault();
1921
- const rect = this.canvas.getBoundingClientRect();
1922
- if (e.touches.length === 1) {
1923
- const x = e.touches[0].clientX - rect.left;
1924
- const y = e.touches[0].clientY - rect.top;
1939
+ const pos = this.firstTouchPos(e);
1940
+ if (pos) {
1941
+ const { x, y } = pos;
1925
1942
  const dx = x - this.lastX;
1926
1943
  const dy = y - this.lastY;
1927
- if (Math.abs(x - this.downX) > 5 || Math.abs(y - this.downY) > 5)
1928
- this.hasMoved = true;
1929
- if (this.isDraggingNode && this.dragNode) {
1930
- const [wx, wy] = this.toWorld(x, y);
1931
- this.dragNode.fx = wx;
1932
- this.dragNode.fy = wy;
1933
- }
1934
- else if (this.isDraggingCanvas) {
1935
- this.tx += dx;
1936
- this.ty += dy;
1937
- this.flyTarget = null;
1938
- }
1944
+ this.handleDragMove(x, y, dx, dy, 5, false);
1939
1945
  this.lastX = x;
1940
1946
  this.lastY = y;
1941
1947
  }
1942
1948
  else if (e.touches.length === 2) {
1949
+ const rect = this.canvas.getBoundingClientRect();
1943
1950
  const dx = e.touches[0].clientX - e.touches[1].clientX;
1944
1951
  const dy = e.touches[0].clientY - e.touches[1].clientY;
1945
1952
  const dist = Math.sqrt(dx * dx + dy * dy);
@@ -1957,21 +1964,7 @@ export class GraphCanvas {
1957
1964
  onTouchEnd(e) {
1958
1965
  e.preventDefault();
1959
1966
  if (e.touches.length === 0) {
1960
- if (this.isDraggingNode && this.dragNode) {
1961
- if (!this.hasMoved) {
1962
- const id = this.dragNode.id;
1963
- const newSel = this.filter.selectedId === id ? null : id;
1964
- this.filter = { ...this.filter, selectedId: newSel };
1965
- this.particles = [];
1966
- this.lastParticleSpawn = 0;
1967
- this.onSelectNode(newSel);
1968
- if (newSel)
1969
- this.flyTo(this.dragNode);
1970
- }
1971
- this.dragNode.fx = null;
1972
- this.dragNode.fy = null;
1973
- this.dragNode = null;
1974
- }
1967
+ this.endNodeDrag();
1975
1968
  this.isDraggingNode = false;
1976
1969
  this.isDraggingCanvas = false;
1977
1970
  }