@lyric_dev/data-loom 0.7.0 → 0.8.1
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/package.json +1 -1
- package/public/app.js +82 -6
- package/public/style.css +105 -3
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -29,9 +29,9 @@ let conflictedNames = new Set();
|
|
|
29
29
|
let doneCollapsed = true;
|
|
30
30
|
const liveOverride = new Map(); // server name -> transient liveness (e.g. "checking")
|
|
31
31
|
|
|
32
|
-
// ── roadmap graph geometry (fixed
|
|
32
|
+
// ── roadmap graph geometry (fixed left/right margin + fixed per-phase pitch) ──
|
|
33
33
|
const G_MX = 150,
|
|
34
|
-
|
|
34
|
+
PHASE_SPAN = 260, // horizontal pitch between phase columns (234px frame + gutter)
|
|
35
35
|
N_HALF = 105,
|
|
36
36
|
N_TOP = 58,
|
|
37
37
|
V_SPACE = 170;
|
|
@@ -180,8 +180,12 @@ function renderReview() {
|
|
|
180
180
|
reviewEl.appendChild(el("span", "review-badge", String(n)));
|
|
181
181
|
const txt = el("span", "review-text");
|
|
182
182
|
const subject = n > 1 ? "proposals need" : "proposal needs";
|
|
183
|
-
txt.innerHTML = `${n} ${subject} dependency review —
|
|
183
|
+
txt.innerHTML = `${n} ${subject} dependency review — copy the <b>weave</b> command to run it in Claude Code.`;
|
|
184
184
|
reviewEl.appendChild(txt);
|
|
185
|
+
const btn = el("button", "banner-action", "Weave");
|
|
186
|
+
btn.title = "Copy /loom:weave";
|
|
187
|
+
btn.addEventListener("click", () => copyCommand("/loom:weave"));
|
|
188
|
+
reviewEl.appendChild(btn);
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
function clearBoard() {
|
|
@@ -200,8 +204,11 @@ function renderBoard() {
|
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
const phaseNums = [...new Set(open.map((c) => c.phase))].sort((a, b) => a - b);
|
|
203
|
-
|
|
204
|
-
|
|
207
|
+
// Fixed per-phase pitch → the canvas grows wider as phases are added instead of
|
|
208
|
+
// squeezing columns into a constant width. Wide plans then scroll horizontally
|
|
209
|
+
// (see .board-wrap) rather than overlapping.
|
|
210
|
+
const canvasW = 2 * G_MX + Math.max(phaseNums.length - 1, 0) * PHASE_SPAN;
|
|
211
|
+
const gx = (p) => G_MX + phaseNums.indexOf(p) * PHASE_SPAN;
|
|
205
212
|
|
|
206
213
|
// Grow the canvas vertically so the tallest phase always fits (real projects
|
|
207
214
|
// can stack many cards in one phase). CARD_BAND leaves headroom for the
|
|
@@ -210,8 +217,9 @@ function renderBoard() {
|
|
|
210
217
|
const maxN = Math.max(1, ...phaseNums.map((p) => open.filter((c) => c.phase === p).length));
|
|
211
218
|
const canvasH = Math.max(600, (maxN - 1) * V_SPACE + CARD_BAND);
|
|
212
219
|
const cy = canvasH / 2;
|
|
220
|
+
board.style.width = canvasW + "px";
|
|
213
221
|
board.style.height = canvasH + "px";
|
|
214
|
-
edgesSvg.setAttribute("viewBox", `0 0 ${
|
|
222
|
+
edgesSvg.setAttribute("viewBox", `0 0 ${canvasW} ${canvasH}`);
|
|
215
223
|
|
|
216
224
|
// Phase-band frames (behind everything), sized to the canvas.
|
|
217
225
|
phaseNums.forEach((p, i) => {
|
|
@@ -272,6 +280,16 @@ function renderCard(c) {
|
|
|
272
280
|
const text = [capsParts.join(" "), tasks].filter(Boolean).join(" · ");
|
|
273
281
|
body.appendChild(el("div", "gcard-caps", text));
|
|
274
282
|
}
|
|
283
|
+
const action = changeAction(c);
|
|
284
|
+
if (action) {
|
|
285
|
+
const btn = el("button", "card-action " + action.kind, action.label);
|
|
286
|
+
btn.title = "Copy " + action.command;
|
|
287
|
+
btn.addEventListener("click", (ev) => {
|
|
288
|
+
ev.stopPropagation(); // copy only — don't also select the card / open detail
|
|
289
|
+
copyCommand(action.command);
|
|
290
|
+
});
|
|
291
|
+
body.appendChild(btn);
|
|
292
|
+
}
|
|
275
293
|
card.appendChild(body);
|
|
276
294
|
|
|
277
295
|
if (nextUpNames.has(c.name)) card.appendChild(el("div", "gcard-next", "NEXT UP"));
|
|
@@ -524,6 +542,13 @@ function renderChangeDetail(c) {
|
|
|
524
542
|
pills.appendChild(rp);
|
|
525
543
|
inner.appendChild(pills);
|
|
526
544
|
|
|
545
|
+
const action = changeAction(c);
|
|
546
|
+
if (action) {
|
|
547
|
+
const btn = el("button", "detail-action " + action.kind, "Copy " + action.command);
|
|
548
|
+
btn.addEventListener("click", () => copyCommand(action.command));
|
|
549
|
+
inner.appendChild(btn);
|
|
550
|
+
}
|
|
551
|
+
|
|
527
552
|
if (c.totalTasks > 0) {
|
|
528
553
|
const pct = Math.round((c.completedTasks / c.totalTasks) * 100);
|
|
529
554
|
const tasks = el("div", "detail-tasks");
|
|
@@ -607,6 +632,57 @@ function itemList(items, cls, prefix) {
|
|
|
607
632
|
return list;
|
|
608
633
|
}
|
|
609
634
|
|
|
635
|
+
// ═══════════════ command handoff (clipboard + toast) ═══════════════
|
|
636
|
+
|
|
637
|
+
// A single reused toast node, auto-dismissed. DataLoom prepares a command; the
|
|
638
|
+
// user runs it in their own Claude Code session — so a copy is confirmed here,
|
|
639
|
+
// never executed.
|
|
640
|
+
let toastEl = null;
|
|
641
|
+
let toastTimer = null;
|
|
642
|
+
function showToast(message, tone) {
|
|
643
|
+
if (!toastEl) {
|
|
644
|
+
toastEl = el("div", "toast");
|
|
645
|
+
document.body.appendChild(toastEl);
|
|
646
|
+
}
|
|
647
|
+
toastEl.textContent = message;
|
|
648
|
+
toastEl.className = "toast" + (tone ? " " + tone : "");
|
|
649
|
+
// force reflow so re-triggering the transition restarts it
|
|
650
|
+
void toastEl.offsetWidth;
|
|
651
|
+
toastEl.classList.add("show");
|
|
652
|
+
clearTimeout(toastTimer);
|
|
653
|
+
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 3200);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Copy a Claude Code command to the clipboard and confirm via a toast. Purely
|
|
657
|
+
// client-side: no network call, no daemon/agent activity. On localhost the
|
|
658
|
+
// Clipboard API is available (secure context); a rejection or its absence falls
|
|
659
|
+
// back to showing the command for manual copy.
|
|
660
|
+
async function copyCommand(command) {
|
|
661
|
+
try {
|
|
662
|
+
await navigator.clipboard.writeText(command);
|
|
663
|
+
showToast(`Copied ${command} — paste into Claude Code`);
|
|
664
|
+
} catch {
|
|
665
|
+
showToast(`Couldn't copy — run this in Claude Code: ${command}`, "warn");
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// The command action a change offers, derived from state the model already
|
|
670
|
+
// carries. Kept in one place so a card and the detail panel always agree:
|
|
671
|
+
// archived → nothing
|
|
672
|
+
// tasks all complete → archive
|
|
673
|
+
// ready (incomplete) → apply
|
|
674
|
+
// otherwise → nothing
|
|
675
|
+
function changeAction(c) {
|
|
676
|
+
if (c.archived) return null;
|
|
677
|
+
if (c.totalTasks > 0 && c.completedTasks === c.totalTasks) {
|
|
678
|
+
return { kind: "archive", label: "Archive", command: "/opsx:archive " + c.name };
|
|
679
|
+
}
|
|
680
|
+
if (c.readiness === "ready") {
|
|
681
|
+
return { kind: "apply", label: "Apply", command: "/opsx:apply " + c.name };
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
|
|
610
686
|
// ═══════════════ helpers ═══════════════
|
|
611
687
|
|
|
612
688
|
function statusColor(status) {
|
package/public/style.css
CHANGED
|
@@ -293,12 +293,12 @@ main {
|
|
|
293
293
|
|
|
294
294
|
.board-wrap {
|
|
295
295
|
position: relative;
|
|
296
|
+
overflow-x: auto; /* wide roadmaps (many phases) pan horizontally; banners above stay fixed */
|
|
296
297
|
}
|
|
297
298
|
.board {
|
|
298
299
|
position: relative;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
margin: 0 auto;
|
|
300
|
+
height: 600px; /* width + height are set from JS per phase/card count */
|
|
301
|
+
margin: 0 auto; /* centers when it fits; collapses to 0 when it overflows and scrolls */
|
|
302
302
|
}
|
|
303
303
|
.edges {
|
|
304
304
|
position: absolute;
|
|
@@ -540,6 +540,108 @@ main {
|
|
|
540
540
|
color: var(--text);
|
|
541
541
|
font-family: var(--prose);
|
|
542
542
|
}
|
|
543
|
+
.banner-action {
|
|
544
|
+
flex: none;
|
|
545
|
+
margin-left: auto;
|
|
546
|
+
font-family: var(--prose);
|
|
547
|
+
font-weight: 600;
|
|
548
|
+
font-size: 12px;
|
|
549
|
+
padding: 6px 15px;
|
|
550
|
+
border-radius: 8px;
|
|
551
|
+
border: 1px solid var(--accent-bd);
|
|
552
|
+
background: var(--panel);
|
|
553
|
+
color: var(--accent);
|
|
554
|
+
cursor: pointer;
|
|
555
|
+
transition: background 0.15s, color 0.15s;
|
|
556
|
+
}
|
|
557
|
+
.banner-action:hover {
|
|
558
|
+
background: var(--accent);
|
|
559
|
+
color: var(--on-accent);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/* ===== Command handoff: card/detail actions + toast ===== */
|
|
563
|
+
.card-action {
|
|
564
|
+
align-self: flex-start;
|
|
565
|
+
margin-top: 10px;
|
|
566
|
+
font-family: var(--prose);
|
|
567
|
+
font-weight: 600;
|
|
568
|
+
font-size: 11px;
|
|
569
|
+
padding: 4px 11px;
|
|
570
|
+
border-radius: 7px;
|
|
571
|
+
border: 1px solid var(--accent-bd);
|
|
572
|
+
background: var(--accent-soft);
|
|
573
|
+
color: var(--accent);
|
|
574
|
+
cursor: pointer;
|
|
575
|
+
transition: background 0.15s, color 0.15s;
|
|
576
|
+
}
|
|
577
|
+
.card-action:hover {
|
|
578
|
+
background: var(--accent);
|
|
579
|
+
color: var(--on-accent);
|
|
580
|
+
}
|
|
581
|
+
.card-action.archive {
|
|
582
|
+
border-color: var(--c-done);
|
|
583
|
+
color: var(--c-done);
|
|
584
|
+
background: transparent;
|
|
585
|
+
}
|
|
586
|
+
.card-action.archive:hover {
|
|
587
|
+
background: var(--c-done);
|
|
588
|
+
color: var(--on-accent);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.detail-action {
|
|
592
|
+
margin-top: 16px;
|
|
593
|
+
width: 100%;
|
|
594
|
+
font-family: var(--prose);
|
|
595
|
+
font-weight: 600;
|
|
596
|
+
font-size: 12.5px;
|
|
597
|
+
padding: 9px;
|
|
598
|
+
border-radius: 9px;
|
|
599
|
+
border: 1px solid var(--accent-bd);
|
|
600
|
+
background: var(--accent-soft);
|
|
601
|
+
color: var(--accent);
|
|
602
|
+
cursor: pointer;
|
|
603
|
+
transition: background 0.15s, color 0.15s;
|
|
604
|
+
}
|
|
605
|
+
.detail-action:hover {
|
|
606
|
+
background: var(--accent);
|
|
607
|
+
color: var(--on-accent);
|
|
608
|
+
}
|
|
609
|
+
.detail-action.archive {
|
|
610
|
+
border-color: var(--c-done);
|
|
611
|
+
color: var(--c-done);
|
|
612
|
+
}
|
|
613
|
+
.detail-action.archive:hover {
|
|
614
|
+
background: var(--c-done);
|
|
615
|
+
color: var(--on-accent);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.toast {
|
|
619
|
+
position: fixed;
|
|
620
|
+
left: 50%;
|
|
621
|
+
bottom: 26px;
|
|
622
|
+
transform: translate(-50%, 12px);
|
|
623
|
+
max-width: min(70vw, 480px);
|
|
624
|
+
padding: 11px 16px;
|
|
625
|
+
border-radius: 10px;
|
|
626
|
+
background: var(--panel);
|
|
627
|
+
border: 1px solid var(--accent-bd);
|
|
628
|
+
color: var(--text);
|
|
629
|
+
font-family: var(--mono);
|
|
630
|
+
font-size: 12px;
|
|
631
|
+
box-shadow: var(--shadow);
|
|
632
|
+
opacity: 0;
|
|
633
|
+
pointer-events: none;
|
|
634
|
+
z-index: 40;
|
|
635
|
+
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
636
|
+
}
|
|
637
|
+
.toast.show {
|
|
638
|
+
opacity: 1;
|
|
639
|
+
transform: translate(-50%, 0);
|
|
640
|
+
}
|
|
641
|
+
.toast.warn {
|
|
642
|
+
border-color: var(--c-warn);
|
|
643
|
+
color: var(--c-warn);
|
|
644
|
+
}
|
|
543
645
|
|
|
544
646
|
/* ===== MCP Topology: circuit board ===== */
|
|
545
647
|
.topo-controls {
|