fix(rtime): pool circle resize + remove hex port dots + auto-weave + tooltip

- Fix pool circle not resizing: clear inline canvas dimensions before
  measuring, observe pool panel via ResizeObserver, use rAF for layout
- Remove visible port dot on hexagon commitment nodes — lines connect
  directly to hex edge, invisible hit area preserved
- Auto-weave: dropping commitment on canvas auto-connects to nearest
  unfulfilled task (was showing suggestion preview requiring confirmation)
- Add SVG tooltip on proposed connections: "{name} has been notified of
  this proposed commitment, and can approve/deny for 48 hours"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-14 11:49:06 -04:00
parent d998409b7d
commit 2e43b6aadc
1 changed files with 31 additions and 13 deletions

View File

@ -839,9 +839,11 @@ class FolkTimebankApp extends HTMLElement {
this.renderTaskEditLinks(current); this.renderTaskEditLinks(current);
}); });
// Resize — now targets pool panel // Resize — observe host + pool panel so canvas redraws on any size change
this._resizeObserver = new ResizeObserver(() => { if (this.currentView === 'canvas') this.resizePoolCanvas(); }); this._resizeObserver = new ResizeObserver(() => { if (this.currentView === 'canvas') requestAnimationFrame(() => this.resizePoolCanvas()); });
this._resizeObserver.observe(this); this._resizeObserver.observe(this);
const poolPanel = this.shadow.getElementById('poolPanel');
if (poolPanel) this._resizeObserver.observe(poolPanel);
this.resizePoolCanvas(); this.resizePoolCanvas();
this.poolFrame(); this.poolFrame();
@ -851,15 +853,15 @@ class FolkTimebankApp extends HTMLElement {
if (this.poolPanelCollapsed) return; if (this.poolPanelCollapsed) return;
const canvasEl = this.canvas; const canvasEl = this.canvas;
if (!canvasEl || !canvasEl.parentElement) return; if (!canvasEl || !canvasEl.parentElement) return;
// Pool canvas lives inside pool-panel; measure the canvas element's allocated space // Clear any previously set inline dimensions so the canvas can flex
canvasEl.style.width = '';
canvasEl.style.height = '';
const rect = canvasEl.getBoundingClientRect(); const rect = canvasEl.getBoundingClientRect();
this.poolW = rect.width; this.poolW = rect.width;
this.poolH = rect.height; this.poolH = rect.height;
if (this.poolW < 10 || this.poolH < 10) return; if (this.poolW < 10 || this.poolH < 10) return;
canvasEl.width = this.poolW * this.dpr; canvasEl.width = this.poolW * this.dpr;
canvasEl.height = this.poolH * this.dpr; canvasEl.height = this.poolH * this.dpr;
canvasEl.style.width = this.poolW + 'px';
canvasEl.style.height = this.poolH + 'px';
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
this.basketCX = this.poolW / 2; this.basketCX = this.poolW / 2;
this.basketCY = this.poolH / 2 + 10; this.basketCY = this.poolH / 2 + 10;
@ -1481,6 +1483,13 @@ class FolkTimebankApp extends HTMLElement {
} else { } else {
// Commitment: multi-strand woven path // Commitment: multi-strand woven path
const wovenG = this.wovenPath(x1, y1, x2, y2, conn.skill, conn.hours, isCommitted); const wovenG = this.wovenPath(x1, y1, x2, y2, conn.skill, conn.hours, isCommitted);
if (!isCommitted) {
const fromNode = this.weaveNodes.find(n => n.id === conn.from);
const memberName = fromNode?.type === 'commitment' ? (fromNode.data as any).memberName || 'This member' : 'This member';
const tip = ns('title');
tip.textContent = memberName + ' has been notified of this proposed commitment, and can approve/deny for 48 hours';
wovenG.insertBefore(tip, wovenG.firstChild);
}
this.connectionsLayer.appendChild(wovenG); this.connectionsLayer.appendChild(wovenG);
} }
@ -1495,6 +1504,14 @@ class FolkTimebankApp extends HTMLElement {
label.setAttribute('font-weight', '600'); label.setAttribute('font-weight', '600');
label.setAttribute('fill', isCommitted ? '#10b981' : '#f59e0b'); label.setAttribute('fill', isCommitted ? '#10b981' : '#f59e0b');
label.textContent = conn.hours + 'h ' + (isCommitted ? '\u2713' : '\u23f3'); label.textContent = conn.hours + 'h ' + (isCommitted ? '\u2713' : '\u23f3');
// Tooltip for proposed connections
if (!isCommitted) {
const fromNode = this.weaveNodes.find(n => n.id === conn.from);
const memberName = fromNode?.type === 'commitment' ? (fromNode.data as any).memberName || 'This member' : 'This member';
const tip = ns('title');
tip.textContent = memberName + ' has been notified of this proposed commitment, and can approve/deny for 48 hours';
label.appendChild(tip);
}
this.connectionsLayer.appendChild(label); this.connectionsLayer.appendChild(label);
} }
}); });
@ -1614,6 +1631,7 @@ class FolkTimebankApp extends HTMLElement {
g.appendChild(svgText(descText, cx, cy + 32, 9, '#64748b', '400', 'middle')); g.appendChild(svgText(descText, cx, cy + 32, 9, '#64748b', '400', 'middle'));
} }
// Invisible hit area on hex right vertex for dragging connections
const pts = hexPoints(cx, cy, hr); const pts = hexPoints(cx, cy, hr);
const portHit = ns('circle'); const portHit = ns('circle');
portHit.setAttribute('cx', String(pts[1][0])); portHit.setAttribute('cy', String(pts[1][1])); portHit.setAttribute('cx', String(pts[1][0])); portHit.setAttribute('cy', String(pts[1][1]));
@ -1621,12 +1639,6 @@ class FolkTimebankApp extends HTMLElement {
portHit.setAttribute('class', 'port'); portHit.setAttribute('class', 'port');
portHit.setAttribute('data-node', node.id); portHit.setAttribute('data-port', 'output'); portHit.setAttribute('data-node', node.id); portHit.setAttribute('data-port', 'output');
g.appendChild(portHit); g.appendChild(portHit);
const port = ns('circle');
port.setAttribute('cx', String(pts[1][0])); port.setAttribute('cy', String(pts[1][1]));
port.setAttribute('r', String(PORT_R));
port.setAttribute('fill', col); port.setAttribute('stroke', '#fff'); port.setAttribute('stroke-width', '2');
port.style.pointerEvents = 'none';
g.appendChild(port);
// Approval buttons for proposed connections (visible to commitment owner) // Approval buttons for proposed connections (visible to commitment owner)
const proposedWires = this.connections.filter(w => w.from === node.id && w.status === 'proposed' && w.connectionId); const proposedWires = this.connections.filter(w => w.from === node.id && w.status === 'proposed' && w.connectionId);
@ -2253,7 +2265,7 @@ class FolkTimebankApp extends HTMLElement {
} }
} }
} else { } else {
// Dropped on open canvas — find nearest matching task and suggest connection // Dropped on open canvas — auto-connect to nearest matching task
const matchTask = this.findNearestUnfulfilledTask(pt.x, pt.y, c.skill); const matchTask = this.findNearestUnfulfilledTask(pt.x, pt.y, c.skill);
if (matchTask) { if (matchTask) {
const available = this.availableHours(c.id); const available = this.availableHours(c.id);
@ -2270,7 +2282,13 @@ class FolkTimebankApp extends HTMLElement {
if (!this.weaveNodes.find(n => n.id === fromId)) { if (!this.weaveNodes.find(n => n.id === fromId)) {
this.weaveNodes.push(this.mkCommitNode(c, commitX, commitY)); this.weaveNodes.push(this.mkCommitNode(c, commitX, commitY));
} }
this.pendingSuggestion = { fromId, toNode: matchTask, skill: c.skill, hours: allocate, commitX, commitY }; // Auto-weave: create connection immediately
if (!this.connections.find(w => w.from === fromId && w.to === matchTask.id && w.skill === c.skill)) {
this.connections.push({ from: fromId, to: matchTask.id, skill: c.skill, hours: allocate, status: 'proposed' });
if (!matchTask.data.fulfilled) matchTask.data.fulfilled = {};
matchTask.data.fulfilled[c.skill] = (matchTask.data.fulfilled[c.skill] || 0) + allocate;
this.persistConnection(fromId, matchTask.id, c.skill, allocate);
}
} else { } else {
// No hours to allocate — just place the node // No hours to allocate — just place the node
if (!this.weaveNodes.find(n => n.id === 'cn-' + c.id)) { if (!this.weaveNodes.find(n => n.id === 'cn-' + c.id)) {