diff --git a/modules/rtime/components/folk-timebank-app.ts b/modules/rtime/components/folk-timebank-app.ts index 2ece107a..386fcf39 100644 --- a/modules/rtime/components/folk-timebank-app.ts +++ b/modules/rtime/components/folk-timebank-app.ts @@ -839,9 +839,11 @@ class FolkTimebankApp extends HTMLElement { this.renderTaskEditLinks(current); }); - // Resize — now targets pool panel - this._resizeObserver = new ResizeObserver(() => { if (this.currentView === 'canvas') this.resizePoolCanvas(); }); + // Resize — observe host + pool panel so canvas redraws on any size change + this._resizeObserver = new ResizeObserver(() => { if (this.currentView === 'canvas') requestAnimationFrame(() => this.resizePoolCanvas()); }); this._resizeObserver.observe(this); + const poolPanel = this.shadow.getElementById('poolPanel'); + if (poolPanel) this._resizeObserver.observe(poolPanel); this.resizePoolCanvas(); this.poolFrame(); @@ -851,15 +853,15 @@ class FolkTimebankApp extends HTMLElement { if (this.poolPanelCollapsed) return; const canvasEl = this.canvas; 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(); this.poolW = rect.width; this.poolH = rect.height; if (this.poolW < 10 || this.poolH < 10) return; canvasEl.width = this.poolW * 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.basketCX = this.poolW / 2; this.basketCY = this.poolH / 2 + 10; @@ -1481,6 +1483,13 @@ class FolkTimebankApp extends HTMLElement { } else { // Commitment: multi-strand woven path 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); } @@ -1495,6 +1504,14 @@ class FolkTimebankApp extends HTMLElement { label.setAttribute('font-weight', '600'); label.setAttribute('fill', isCommitted ? '#10b981' : '#f59e0b'); 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); } }); @@ -1614,6 +1631,7 @@ class FolkTimebankApp extends HTMLElement { 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 portHit = ns('circle'); 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('data-node', node.id); portHit.setAttribute('data-port', 'output'); 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) const proposedWires = this.connections.filter(w => w.from === node.id && w.status === 'proposed' && w.connectionId); @@ -2253,7 +2265,7 @@ class FolkTimebankApp extends HTMLElement { } } } 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); if (matchTask) { const available = this.availableHours(c.id); @@ -2270,7 +2282,13 @@ class FolkTimebankApp extends HTMLElement { if (!this.weaveNodes.find(n => n.id === fromId)) { 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 { // No hours to allocate — just place the node if (!this.weaveNodes.find(n => n.id === 'cn-' + c.id)) {