@lyric_dev/data-loom 0.7.0 → 0.8.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyric_dev/data-loom",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Local dashboard for spec-driven development: a phased OpenSpec roadmap (WHAT) and an MCP topology (HOW).",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/public/app.js CHANGED
@@ -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 — ask Claude to <b>weave</b> ${n > 1 ? "them" : "it"} via the data-loom MCP server.`;
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() {
@@ -272,6 +276,16 @@ function renderCard(c) {
272
276
  const text = [capsParts.join(" "), tasks].filter(Boolean).join(" · ");
273
277
  body.appendChild(el("div", "gcard-caps", text));
274
278
  }
279
+ const action = changeAction(c);
280
+ if (action) {
281
+ const btn = el("button", "card-action " + action.kind, action.label);
282
+ btn.title = "Copy " + action.command;
283
+ btn.addEventListener("click", (ev) => {
284
+ ev.stopPropagation(); // copy only — don't also select the card / open detail
285
+ copyCommand(action.command);
286
+ });
287
+ body.appendChild(btn);
288
+ }
275
289
  card.appendChild(body);
276
290
 
277
291
  if (nextUpNames.has(c.name)) card.appendChild(el("div", "gcard-next", "NEXT UP"));
@@ -524,6 +538,13 @@ function renderChangeDetail(c) {
524
538
  pills.appendChild(rp);
525
539
  inner.appendChild(pills);
526
540
 
541
+ const action = changeAction(c);
542
+ if (action) {
543
+ const btn = el("button", "detail-action " + action.kind, "Copy " + action.command);
544
+ btn.addEventListener("click", () => copyCommand(action.command));
545
+ inner.appendChild(btn);
546
+ }
547
+
527
548
  if (c.totalTasks > 0) {
528
549
  const pct = Math.round((c.completedTasks / c.totalTasks) * 100);
529
550
  const tasks = el("div", "detail-tasks");
@@ -607,6 +628,57 @@ function itemList(items, cls, prefix) {
607
628
  return list;
608
629
  }
609
630
 
631
+ // ═══════════════ command handoff (clipboard + toast) ═══════════════
632
+
633
+ // A single reused toast node, auto-dismissed. DataLoom prepares a command; the
634
+ // user runs it in their own Claude Code session — so a copy is confirmed here,
635
+ // never executed.
636
+ let toastEl = null;
637
+ let toastTimer = null;
638
+ function showToast(message, tone) {
639
+ if (!toastEl) {
640
+ toastEl = el("div", "toast");
641
+ document.body.appendChild(toastEl);
642
+ }
643
+ toastEl.textContent = message;
644
+ toastEl.className = "toast" + (tone ? " " + tone : "");
645
+ // force reflow so re-triggering the transition restarts it
646
+ void toastEl.offsetWidth;
647
+ toastEl.classList.add("show");
648
+ clearTimeout(toastTimer);
649
+ toastTimer = setTimeout(() => toastEl.classList.remove("show"), 3200);
650
+ }
651
+
652
+ // Copy a Claude Code command to the clipboard and confirm via a toast. Purely
653
+ // client-side: no network call, no daemon/agent activity. On localhost the
654
+ // Clipboard API is available (secure context); a rejection or its absence falls
655
+ // back to showing the command for manual copy.
656
+ async function copyCommand(command) {
657
+ try {
658
+ await navigator.clipboard.writeText(command);
659
+ showToast(`Copied ${command} — paste into Claude Code`);
660
+ } catch {
661
+ showToast(`Couldn't copy — run this in Claude Code: ${command}`, "warn");
662
+ }
663
+ }
664
+
665
+ // The command action a change offers, derived from state the model already
666
+ // carries. Kept in one place so a card and the detail panel always agree:
667
+ // archived → nothing
668
+ // tasks all complete → archive
669
+ // ready (incomplete) → apply
670
+ // otherwise → nothing
671
+ function changeAction(c) {
672
+ if (c.archived) return null;
673
+ if (c.totalTasks > 0 && c.completedTasks === c.totalTasks) {
674
+ return { kind: "archive", label: "Archive", command: "/opsx:archive " + c.name };
675
+ }
676
+ if (c.readiness === "ready") {
677
+ return { kind: "apply", label: "Apply", command: "/opsx:apply " + c.name };
678
+ }
679
+ return null;
680
+ }
681
+
610
682
  // ═══════════════ helpers ═══════════════
611
683
 
612
684
  function statusColor(status) {
package/public/style.css CHANGED
@@ -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 {