@bvdm/delano 0.2.6 → 0.2.8

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.
@@ -16,6 +16,8 @@ const I = {
16
16
  block: <><circle cx="12" cy="12" r="8.5"/><path d="M6 6l12 12"/></>,
17
17
  trend: <><path d="M3 17l6-6 4 4 8-8"/><path d="M14 7h7v7"/></>,
18
18
  check: <><rect x="3.5" y="3.5" width="17" height="17" rx="2"/><path d="M8 12.5l3 3 5-6"/></>,
19
+ checkMark: <path d="M5 12.5l4 4 10-11"/>,
20
+ copy: <><rect x="8" y="4" width="11" height="14" rx="1.5"/><path d="M16 18v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2"/></>,
19
21
  warn: <><path d="M12 3.5 21 19H3z"/><path d="M12 10v4.5"/><circle cx="12" cy="17" r="0.6" fill="currentColor"/></>,
20
22
  doc: <><path d="M6 3.5h8l4 4V20.5H6z"/><path d="M14 3.5V8h4"/></>,
21
23
  plan: <><path d="M4 5.5h16"/><path d="M4 12h16"/><path d="M4 18.5h10"/></>,
@@ -81,6 +83,72 @@ const escapeHtml = (s) =>
81
83
  const titleCase = (s) =>
82
84
  String(s || "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
83
85
 
86
+ const normalizeCopyValue = (value) => {
87
+ if (value == null) return "";
88
+ if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean).join(", ");
89
+ if (typeof value === "boolean" || typeof value === "number") return String(value);
90
+ if (typeof value === "object") return JSON.stringify(value);
91
+ return String(value).trim();
92
+ };
93
+
94
+ const isCopyableMetaKey = (key) => {
95
+ const normalized = String(key || "").toLowerCase();
96
+ return (
97
+ normalized === "id" ||
98
+ normalized === "slug" ||
99
+ normalized === "workstream" ||
100
+ normalized === "depends_on" ||
101
+ normalized === "conflicts_with" ||
102
+ /(?:^|_)(?:id|ids)$/.test(normalized)
103
+ );
104
+ };
105
+
106
+ const copyLabelFromMetaKey = (key, role) => {
107
+ const normalized = String(key || "").toLowerCase();
108
+ if (normalized === "id" && role) return `${titleCase(role)} ID`;
109
+ if (normalized === "workstream") return "workstream ID";
110
+ if (normalized === "depends_on") return "dependency IDs";
111
+ if (normalized === "conflicts_with") return "conflict IDs";
112
+ return normalized.replace(/_/g, " ") || "value";
113
+ };
114
+
115
+ async function copyTextToClipboard(text) {
116
+ if (navigator.clipboard && navigator.clipboard.writeText) {
117
+ try {
118
+ await navigator.clipboard.writeText(text);
119
+ return true;
120
+ } catch (_) {
121
+ /* fall through */
122
+ }
123
+ }
124
+
125
+ const textArea = document.createElement("textarea");
126
+ textArea.value = text;
127
+ textArea.setAttribute("readonly", "");
128
+ textArea.style.position = "fixed";
129
+ textArea.style.top = "-1000px";
130
+ textArea.style.opacity = "0";
131
+ document.body.appendChild(textArea);
132
+ textArea.select();
133
+ let ok = false;
134
+ try {
135
+ ok = document.execCommand("copy");
136
+ } catch (_) {
137
+ ok = false;
138
+ }
139
+ document.body.removeChild(textArea);
140
+ return ok;
141
+ }
142
+
143
+ function announceCopy(label) {
144
+ const live = document.getElementById("copy-live");
145
+ if (!live) return;
146
+ live.textContent = "";
147
+ window.setTimeout(() => {
148
+ live.textContent = `Copied ${label || "value"}`;
149
+ }, 10);
150
+ }
151
+
84
152
  const formatShortDateTime = (value) => {
85
153
  if (!value) return "";
86
154
  const date = new Date(value);
@@ -527,10 +595,48 @@ const Pagination = ({ page, totalPages, onPageChange }) => {
527
595
  /* ================================================================
528
596
  Reusable components
529
597
  ================================================================ */
530
- const Field = ({ label, children, mono }) => (
598
+ const CopyButton = ({ value, label = "value", className = "" }) => {
599
+ const [copied, setCopied] = useState(false);
600
+ const text = normalizeCopyValue(value);
601
+
602
+ useEffect(() => {
603
+ if (!copied) return undefined;
604
+ const timeout = window.setTimeout(() => setCopied(false), 1200);
605
+ return () => window.clearTimeout(timeout);
606
+ }, [copied]);
607
+
608
+ if (!text) return null;
609
+
610
+ const stateLabel = copied ? `Copied ${label}` : `Copy ${label}`;
611
+ const handleClick = async (event) => {
612
+ event.preventDefault();
613
+ event.stopPropagation();
614
+ const ok = await copyTextToClipboard(text);
615
+ if (!ok) return;
616
+ setCopied(true);
617
+ announceCopy(label);
618
+ };
619
+
620
+ return (
621
+ <button
622
+ className={`copy-btn${copied ? " is-copied" : ""}${className ? ` ${className}` : ""}`}
623
+ type="button"
624
+ onClick={handleClick}
625
+ aria-label={stateLabel}
626
+ title={stateLabel}
627
+ >
628
+ <Icon d={copied ? I.checkMark : I.copy} size={13} />
629
+ </button>
630
+ );
631
+ };
632
+
633
+ const Field = ({ label, children, mono, copyValue, copyLabel }) => (
531
634
  <div className="field">
532
635
  <div className="field-label">{label}</div>
533
- <div className={"field-value" + (mono ? " mono" : "")}>{children}</div>
636
+ <div className={"field-value" + (mono ? " mono" : "")}>
637
+ {children}
638
+ <CopyButton value={copyValue} label={copyLabel || label} />
639
+ </div>
534
640
  </div>
535
641
  );
536
642
 
@@ -986,7 +1092,10 @@ function Overview({ index, project, docs, scrollTarget, onOpenWorkstream, onOpen
986
1092
  </div>
987
1093
 
988
1094
  <section className="summary overview-summary">
989
- <Field label="Project">{project.title}</Field>
1095
+ <Field label="Project" copyValue={project.slug} copyLabel="project ID">
1096
+ <span>{project.title}</span>
1097
+ <span className="field-id mono">{project.slug}</span>
1098
+ </Field>
990
1099
  <Field label="Status"><StatusChip>{project.status || "Planned"}</StatusChip></Field>
991
1100
  <Field label="Health">
992
1101
  <span className="health">
@@ -1116,11 +1225,29 @@ function Overview({ index, project, docs, scrollTarget, onOpenWorkstream, onOpen
1116
1225
  Workspace Pages
1117
1226
  ================================================================ */
1118
1227
  function WorkspacePage({ index, view, page, onPageChange, onOpenProject, onOpenProjectDoc, onOpenProjectWorkstream }) {
1228
+ const [projectFilter, setProjectFilter] = useState("all");
1119
1229
  const workspace = useMemo(() => getWorkspaceModel(index), [index]);
1120
1230
  const projectStats = useMemo(
1121
1231
  () => (index.projects || []).filter((p) => p.outline).map((project) => getProjectStats(index, project)),
1122
1232
  [index]
1123
1233
  );
1234
+ const projectFilterCounts = useMemo(() => {
1235
+ const active = projectStats.filter((stat) => statusLabel(stat.project.status) !== "Complete").length;
1236
+ return {
1237
+ all: projectStats.length,
1238
+ active,
1239
+ complete: projectStats.length - active,
1240
+ };
1241
+ }, [projectStats]);
1242
+ const filteredProjectStats = useMemo(() => {
1243
+ if (projectFilter === "active") {
1244
+ return projectStats.filter((stat) => statusLabel(stat.project.status) !== "Complete");
1245
+ }
1246
+ if (projectFilter === "complete") {
1247
+ return projectStats.filter((stat) => statusLabel(stat.project.status) === "Complete");
1248
+ }
1249
+ return projectStats;
1250
+ }, [projectFilter, projectStats]);
1124
1251
  const currentPage = page || 1;
1125
1252
 
1126
1253
  const titleMap = {
@@ -1133,7 +1260,7 @@ function WorkspacePage({ index, view, page, onPageChange, onOpenProject, onOpenP
1133
1260
  };
1134
1261
  const title = titleMap[view] || "Workspace";
1135
1262
  const itemsForView =
1136
- view === "workspace-projects" ? projectStats :
1263
+ view === "workspace-projects" ? filteredProjectStats :
1137
1264
  view === "workspace-current" ? workspace.current :
1138
1265
  view === "workspace-blockers" ? workspace.blockers :
1139
1266
  view === "workspace-validation" ? workspace.validation :
@@ -1166,10 +1293,31 @@ function WorkspacePage({ index, view, page, onPageChange, onOpenProject, onOpenP
1166
1293
  <Pagination page={pagination.safePage} totalPages={pagination.totalPages} onPageChange={onPageChange} />
1167
1294
  );
1168
1295
 
1296
+ const projectFilterControl = view === "workspace-projects" ? (
1297
+ <div className="project-filter" aria-label="Project status filter">
1298
+ {[
1299
+ ["all", "All"],
1300
+ ["active", "Active"],
1301
+ ["complete", "Complete"],
1302
+ ].map(([value, label]) => (
1303
+ <button
1304
+ className={"project-filter-option" + (projectFilter === value ? " is-active" : "")}
1305
+ type="button"
1306
+ onClick={() => setProjectFilter(value)}
1307
+ aria-pressed={projectFilter === value}
1308
+ key={value}
1309
+ >
1310
+ <span>{label}</span>
1311
+ <span className="mono">{projectFilterCounts[value]}</span>
1312
+ </button>
1313
+ ))}
1314
+ </div>
1315
+ ) : null;
1316
+
1169
1317
  const renderProjects = () =>
1170
- projectStats.length > 0 ? (
1318
+ filteredProjectStats.length > 0 ? (
1171
1319
  <div className="project-grid">
1172
- {projectStats.map((stat) => (
1320
+ {filteredProjectStats.map((stat) => (
1173
1321
  <button className="project-card" key={stat.project.slug} type="button" onClick={() => onOpenProject(stat.project.slug)}>
1174
1322
  <span className="project-card-head">
1175
1323
  <span className="project-card-title">{stat.project.title}</span>
@@ -1189,7 +1337,7 @@ function WorkspacePage({ index, view, page, onPageChange, onOpenProject, onOpenP
1189
1337
  ))}
1190
1338
  </div>
1191
1339
  ) : (
1192
- <div className="empty-state">No projects found.</div>
1340
+ <div className="empty-state">No projects match this filter.</div>
1193
1341
  );
1194
1342
 
1195
1343
  const renderTaskRows = (items, emptyText, kind) => {
@@ -1311,7 +1459,7 @@ function WorkspacePage({ index, view, page, onPageChange, onOpenProject, onOpenP
1311
1459
  };
1312
1460
 
1313
1461
  const count =
1314
- view === "workspace-projects" ? workspace.counts.projects :
1462
+ view === "workspace-projects" ? filteredProjectStats.length :
1315
1463
  view === "workspace-current" ? workspace.counts.current :
1316
1464
  view === "workspace-blockers" ? workspace.counts.blockers :
1317
1465
  view === "workspace-validation" ? workspace.counts.validation :
@@ -1321,9 +1469,8 @@ function WorkspacePage({ index, view, page, onPageChange, onOpenProject, onOpenP
1321
1469
 
1322
1470
  return (
1323
1471
  <div className="page">
1324
- <h1 className="page-title">{title}</h1>
1325
1472
  <section className="block">
1326
- <SectionHeader title={title} count={count} />
1473
+ <SectionHeader title={title} count={count} right={projectFilterControl} />
1327
1474
  {renderBody()}
1328
1475
  </section>
1329
1476
  </div>
@@ -1735,6 +1882,7 @@ function WorkstreamDetail({ index, project, wsPath, onBack, onOpenDoc }) {
1735
1882
  <ul className="checklist">
1736
1883
  {tasks.map((t, i) => {
1737
1884
  const done = statusLabel(t.status) === "Complete";
1885
+ const taskId = t.taskId || t.path.split("/").pop()?.replace(/\.md$/, "");
1738
1886
  return (
1739
1887
  <li key={i} className={done ? "done" : ""}>
1740
1888
  <span className="cb">
@@ -1745,7 +1893,10 @@ function WorkstreamDetail({ index, project, wsPath, onBack, onOpenDoc }) {
1745
1893
  </LinkButton>
1746
1894
  <span className="checklist-meta">
1747
1895
  <StatusChip>{t.status || "Planned"}</StatusChip>
1748
- <span className="mono">{t.taskId || t.path.split("/").pop()?.replace(/\.md$/, "")}</span>
1896
+ <span className="task-id-copy mono">
1897
+ <span>{taskId}</span>
1898
+ <CopyButton value={taskId} label="task ID" />
1899
+ </span>
1749
1900
  </span>
1750
1901
  </li>
1751
1902
  );
@@ -1784,12 +1935,22 @@ function WorkstreamDetail({ index, project, wsPath, onBack, onOpenDoc }) {
1784
1935
  <dd>{owner}</dd>
1785
1936
  </>
1786
1937
  )}
1787
- <dt>Source path</dt>
1788
- <dd className="mono small">{wsPath}</dd>
1938
+ <dt>
1939
+ <span>Source path</span>
1940
+ <CopyButton value={wsPath} label="source path" />
1941
+ </dt>
1942
+ <dd className="mono small">
1943
+ <span className="copy-value">{wsPath}</span>
1944
+ </dd>
1789
1945
  {wsOutline?.id && (
1790
1946
  <>
1791
- <dt>ID</dt>
1792
- <dd className="mono">{wsOutline.id}</dd>
1947
+ <dt>
1948
+ <span>ID</span>
1949
+ <CopyButton value={wsOutline.id} label="workstream ID" />
1950
+ </dt>
1951
+ <dd className="mono">
1952
+ <span>{wsOutline.id}</span>
1953
+ </dd>
1793
1954
  </>
1794
1955
  )}
1795
1956
  </dl>
@@ -1938,8 +2099,13 @@ function DocumentReader({ doc, project, index, onBack, onOpenAction, onOpenDoc,
1938
2099
  <div className="doc-meta-panel" aria-label="Document metadata">
1939
2100
  <div className="doc-meta-title">Metadata</div>
1940
2101
  <dl className="dl doc-meta-list">
1941
- <dt>Path</dt>
1942
- <dd className="mono">{doc.path}</dd>
2102
+ <dt>
2103
+ <span>Path</span>
2104
+ <CopyButton value={doc.path} label="source path" />
2105
+ </dt>
2106
+ <dd className="mono">
2107
+ <span className="copy-value">{doc.path}</span>
2108
+ </dd>
1943
2109
  {doc.status && (
1944
2110
  <>
1945
2111
  <dt>Status</dt>
@@ -1948,12 +2114,22 @@ function DocumentReader({ doc, project, index, onBack, onOpenAction, onOpenDoc,
1948
2114
  )}
1949
2115
  <dt>Updated</dt>
1950
2116
  <dd>{fmtDate(doc.updated)}</dd>
1951
- {props.map(([k, v]) => (
1952
- <React.Fragment key={k}>
1953
- <dt>{k}</dt>
1954
- <dd>{renderMetaValue(k, v)}</dd>
1955
- </React.Fragment>
1956
- ))}
2117
+ {props.map(([k, v]) => {
2118
+ const copyable = isCopyableMetaKey(k);
2119
+ return (
2120
+ <React.Fragment key={k}>
2121
+ <dt>
2122
+ <span>{k}</span>
2123
+ {copyable && (
2124
+ <CopyButton value={v} label={copyLabelFromMetaKey(k, doc.role)} />
2125
+ )}
2126
+ </dt>
2127
+ <dd>
2128
+ {renderMetaValue(k, v)}
2129
+ </dd>
2130
+ </React.Fragment>
2131
+ );
2132
+ })}
1957
2133
  </dl>
1958
2134
  </div>
1959
2135
  </aside>
@@ -2305,6 +2481,7 @@ function App() {
2305
2481
  <div className="content content-reader-head-c">{mainContent}</div>
2306
2482
 
2307
2483
  </div>
2484
+ <div id="copy-live" className="sr-only" aria-live="polite" aria-atomic="true"></div>
2308
2485
  </div>
2309
2486
  );
2310
2487
  }
@@ -47,6 +47,17 @@ body {
47
47
  button { font-family: inherit; font-size: inherit; color: inherit; background: none; border: none; cursor: pointer; padding: 0; }
48
48
  .mono { font-family: var(--font-mono); font-size: 12.5px; letter-spacing: -0.01em; }
49
49
  .small { font-size: 12px; }
50
+ .sr-only {
51
+ position: absolute;
52
+ width: 1px;
53
+ height: 1px;
54
+ padding: 0;
55
+ margin: -1px;
56
+ overflow: hidden;
57
+ clip: rect(0, 0, 0, 0);
58
+ white-space: nowrap;
59
+ border: 0;
60
+ }
50
61
 
51
62
  /* ---------- App shell ---------- */
52
63
  .app {
@@ -234,6 +245,31 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
234
245
  background: var(--ink); color: var(--bg); border-color: var(--ink);
235
246
  }
236
247
  .btn-primary:hover { background: oklch(0.13 0.008 80); color: var(--bg); }
248
+ .copy-btn {
249
+ flex: none;
250
+ width: 22px;
251
+ height: 22px;
252
+ display: inline-grid;
253
+ place-items: center;
254
+ border: 1px solid transparent;
255
+ border-radius: var(--r-sm);
256
+ color: var(--ink-40);
257
+ text-decoration: none;
258
+ transition: background 90ms, border-color 90ms, color 90ms;
259
+ }
260
+ .copy-btn:hover,
261
+ .copy-btn:focus-visible {
262
+ background: var(--line-soft);
263
+ border-color: var(--line);
264
+ color: var(--ink);
265
+ outline: none;
266
+ }
267
+ .copy-btn.is-copied {
268
+ background: var(--ok-soft);
269
+ border-color: color-mix(in oklch, var(--ok) 30%, var(--line));
270
+ color: var(--ok);
271
+ }
272
+ .copy-btn svg { pointer-events: none; }
237
273
 
238
274
  /* ---------- Page ---------- */
239
275
  .content { padding: 36px 48px 64px; max-width: 1320px; width: 100%; align-self: center; }
@@ -453,7 +489,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
453
489
  .summary-tight > .field:first-child { padding-left: 0; }
454
490
  .doc-reader-page {
455
491
  display: grid;
456
- grid-template-columns: minmax(0, 1fr) minmax(260px, 320px);
492
+ grid-template-columns: minmax(0, 1fr) minmax(260px, 400px);
457
493
  gap: 44px;
458
494
  align-items: start;
459
495
  }
@@ -470,7 +506,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
470
506
  margin-bottom: 6px;
471
507
  }
472
508
  .content-reader-head-c .doc-reader-main .page-title {
473
- max-width: 17ch;
509
+ max-width: 100%;
474
510
  }
475
511
  .doc-meta-panel {
476
512
  position: sticky;
@@ -481,7 +517,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
481
517
  overflow: hidden;
482
518
  }
483
519
  .doc-side {
484
- min-width: 0;
520
+ min-width: 400px;
485
521
  position: sticky;
486
522
  top: 86px;
487
523
  display: flex;
@@ -505,6 +541,10 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
505
541
  .doc-meta-list dd {
506
542
  overflow-wrap: anywhere;
507
543
  }
544
+ .copy-value {
545
+ min-width: 0;
546
+ overflow-wrap: anywhere;
547
+ }
508
548
 
509
549
  .field-label {
510
550
  font-size: 11px;
@@ -515,6 +555,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
515
555
  margin-bottom: 6px;
516
556
  }
517
557
  .field-value { font-size: 14px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
558
+ .field-id { color: var(--ink-50); }
518
559
 
519
560
  /* health */
520
561
  .health { display: flex; align-items: center; gap: 10px; }
@@ -600,6 +641,40 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
600
641
  }
601
642
 
602
643
  /* ---------- Workspace dashboard ---------- */
644
+ .project-filter {
645
+ display: inline-flex;
646
+ align-items: center;
647
+ gap: 2px;
648
+ padding: 2px;
649
+ border: 1px solid var(--line);
650
+ border-radius: var(--r-sm);
651
+ background: var(--surface);
652
+ }
653
+ .project-filter-option {
654
+ display: inline-flex;
655
+ align-items: center;
656
+ gap: 6px;
657
+ min-height: 26px;
658
+ padding: 4px 8px;
659
+ border-radius: 3px;
660
+ color: var(--ink-50);
661
+ font-size: 12.5px;
662
+ line-height: 1;
663
+ transition: background 90ms, color 90ms;
664
+ }
665
+ .project-filter-option:hover {
666
+ background: var(--line-soft);
667
+ color: var(--ink);
668
+ }
669
+ .project-filter-option.is-active {
670
+ background: var(--ink);
671
+ color: var(--bg);
672
+ }
673
+ .project-filter-option .mono {
674
+ font-size: 11px;
675
+ color: currentColor;
676
+ opacity: 0.72;
677
+ }
603
678
  .project-grid {
604
679
  display: grid;
605
680
  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
@@ -867,6 +942,12 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
867
942
  flex: none;
868
943
  color: var(--ink-50);
869
944
  }
945
+ .task-id-copy {
946
+ display: inline-flex;
947
+ align-items: center;
948
+ gap: 4px;
949
+ color: var(--ink-50);
950
+ }
870
951
 
871
952
  .decisions { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
872
953
  .decisions li {
@@ -938,26 +1019,42 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
938
1019
 
939
1020
  .dl {
940
1021
  margin: 0; padding: 8px 0;
941
- display: grid;
942
- grid-template-columns: max-content 1fr;
943
- gap: 0 16px;
1022
+ display: flex;
1023
+ flex-direction: column;
1024
+ gap: 0;
944
1025
  }
945
1026
  .dl dt {
946
- padding: 8px 14px;
1027
+ width: 100%;
1028
+ padding: 8px 14px 0;
947
1029
  font-size: 12px;
948
1030
  color: var(--ink-50);
949
1031
  text-transform: uppercase;
950
1032
  letter-spacing: 0.06em;
951
1033
  font-weight: 500;
1034
+ overflow: hidden;
1035
+ text-overflow: ellipsis;
1036
+ white-space: nowrap;
1037
+ flex: 1;
1038
+ display: flex;
1039
+ align-items: center;
1040
+ justify-content: space-between;
1041
+ gap: 8px;
1042
+ }
1043
+ .dl dt > span {
1044
+ min-width: 0;
1045
+ overflow: hidden;
1046
+ text-overflow: ellipsis;
952
1047
  }
953
1048
  .dl dd {
954
1049
  margin: 0;
955
- padding: 8px 14px;
1050
+ padding: 4px 14px 8px;
956
1051
  font-size: 13px;
957
1052
  color: var(--ink);
958
1053
  display: flex; align-items: center;
959
1054
  gap: 6px;
960
1055
  flex-wrap: wrap;
1056
+ flex: 1;
1057
+ min-width: 0;
961
1058
  }
962
1059
 
963
1060
  .side-list { margin: 0; padding: 0; list-style: none; }
@@ -1209,6 +1306,10 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
1209
1306
  .doc-reader-page,
1210
1307
  .two-col { grid-template-columns: 1fr; }
1211
1308
  .doc-side,
1212
- .col-side { position: static; }
1309
+ .col-side {
1310
+ min-width: 0;
1311
+ width: 100%;
1312
+ position: static;
1313
+ }
1213
1314
  .outline-subitem { grid-template-columns: 1fr; gap: 5px; }
1214
1315
  }
package/README.md CHANGED
@@ -15,7 +15,7 @@ The npm package is intentionally thin. It distributes the approved runtime paylo
15
15
  ## Delano CLI
16
16
 
17
17
  - Package: `@bvdm/delano`
18
- - Current package version: `0.2.4`
18
+ - Current package version: `0.2.8`
19
19
  - Binary: `delano`
20
20
  - Commands: `onboarding`, `install`, `viewer`, `init`, `validate`, `status`, `next`
21
21
  - Primary goal: bootstrap a repo safely, expose local delivery state clearly, and keep runtime gates verifiable
@@ -47,6 +47,17 @@ body {
47
47
  button { font-family: inherit; font-size: inherit; color: inherit; background: none; border: none; cursor: pointer; padding: 0; }
48
48
  .mono { font-family: var(--font-mono); font-size: 12.5px; letter-spacing: -0.01em; }
49
49
  .small { font-size: 12px; }
50
+ .sr-only {
51
+ position: absolute;
52
+ width: 1px;
53
+ height: 1px;
54
+ padding: 0;
55
+ margin: -1px;
56
+ overflow: hidden;
57
+ clip: rect(0, 0, 0, 0);
58
+ white-space: nowrap;
59
+ border: 0;
60
+ }
50
61
 
51
62
  /* ---------- App shell ---------- */
52
63
  .app {
@@ -234,6 +245,31 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
234
245
  background: var(--ink); color: var(--bg); border-color: var(--ink);
235
246
  }
236
247
  .btn-primary:hover { background: oklch(0.13 0.008 80); color: var(--bg); }
248
+ .copy-btn {
249
+ flex: none;
250
+ width: 22px;
251
+ height: 22px;
252
+ display: inline-grid;
253
+ place-items: center;
254
+ border: 1px solid transparent;
255
+ border-radius: var(--r-sm);
256
+ color: var(--ink-40);
257
+ text-decoration: none;
258
+ transition: background 90ms, border-color 90ms, color 90ms;
259
+ }
260
+ .copy-btn:hover,
261
+ .copy-btn:focus-visible {
262
+ background: var(--line-soft);
263
+ border-color: var(--line);
264
+ color: var(--ink);
265
+ outline: none;
266
+ }
267
+ .copy-btn.is-copied {
268
+ background: var(--ok-soft);
269
+ border-color: color-mix(in oklch, var(--ok) 30%, var(--line));
270
+ color: var(--ok);
271
+ }
272
+ .copy-btn svg { pointer-events: none; }
237
273
 
238
274
  /* ---------- Page ---------- */
239
275
  .content { padding: 36px 48px 64px; max-width: 1320px; width: 100%; align-self: center; }
@@ -453,7 +489,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
453
489
  .summary-tight > .field:first-child { padding-left: 0; }
454
490
  .doc-reader-page {
455
491
  display: grid;
456
- grid-template-columns: minmax(0, 1fr) minmax(260px, 320px);
492
+ grid-template-columns: minmax(0, 1fr) minmax(260px, 400px);
457
493
  gap: 44px;
458
494
  align-items: start;
459
495
  }
@@ -470,7 +506,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
470
506
  margin-bottom: 6px;
471
507
  }
472
508
  .content-reader-head-c .doc-reader-main .page-title {
473
- max-width: 17ch;
509
+ max-width: 100%;
474
510
  }
475
511
  .doc-meta-panel {
476
512
  position: sticky;
@@ -481,7 +517,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
481
517
  overflow: hidden;
482
518
  }
483
519
  .doc-side {
484
- min-width: 0;
520
+ min-width: 400px;
485
521
  position: sticky;
486
522
  top: 86px;
487
523
  display: flex;
@@ -505,6 +541,10 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
505
541
  .doc-meta-list dd {
506
542
  overflow-wrap: anywhere;
507
543
  }
544
+ .copy-value {
545
+ min-width: 0;
546
+ overflow-wrap: anywhere;
547
+ }
508
548
 
509
549
  .field-label {
510
550
  font-size: 11px;
@@ -515,6 +555,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
515
555
  margin-bottom: 6px;
516
556
  }
517
557
  .field-value { font-size: 14px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
558
+ .field-id { color: var(--ink-50); }
518
559
 
519
560
  /* health */
520
561
  .health { display: flex; align-items: center; gap: 10px; }
@@ -600,6 +641,40 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
600
641
  }
601
642
 
602
643
  /* ---------- Workspace dashboard ---------- */
644
+ .project-filter {
645
+ display: inline-flex;
646
+ align-items: center;
647
+ gap: 2px;
648
+ padding: 2px;
649
+ border: 1px solid var(--line);
650
+ border-radius: var(--r-sm);
651
+ background: var(--surface);
652
+ }
653
+ .project-filter-option {
654
+ display: inline-flex;
655
+ align-items: center;
656
+ gap: 6px;
657
+ min-height: 26px;
658
+ padding: 4px 8px;
659
+ border-radius: 3px;
660
+ color: var(--ink-50);
661
+ font-size: 12.5px;
662
+ line-height: 1;
663
+ transition: background 90ms, color 90ms;
664
+ }
665
+ .project-filter-option:hover {
666
+ background: var(--line-soft);
667
+ color: var(--ink);
668
+ }
669
+ .project-filter-option.is-active {
670
+ background: var(--ink);
671
+ color: var(--bg);
672
+ }
673
+ .project-filter-option .mono {
674
+ font-size: 11px;
675
+ color: currentColor;
676
+ opacity: 0.72;
677
+ }
603
678
  .project-grid {
604
679
  display: grid;
605
680
  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
@@ -867,6 +942,12 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
867
942
  flex: none;
868
943
  color: var(--ink-50);
869
944
  }
945
+ .task-id-copy {
946
+ display: inline-flex;
947
+ align-items: center;
948
+ gap: 4px;
949
+ color: var(--ink-50);
950
+ }
870
951
 
871
952
  .decisions { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
872
953
  .decisions li {
@@ -938,26 +1019,42 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
938
1019
 
939
1020
  .dl {
940
1021
  margin: 0; padding: 8px 0;
941
- display: grid;
942
- grid-template-columns: max-content 1fr;
943
- gap: 0 16px;
1022
+ display: flex;
1023
+ flex-direction: column;
1024
+ gap: 0;
944
1025
  }
945
1026
  .dl dt {
946
- padding: 8px 14px;
1027
+ width: 100%;
1028
+ padding: 8px 14px 0;
947
1029
  font-size: 12px;
948
1030
  color: var(--ink-50);
949
1031
  text-transform: uppercase;
950
1032
  letter-spacing: 0.06em;
951
1033
  font-weight: 500;
1034
+ overflow: hidden;
1035
+ text-overflow: ellipsis;
1036
+ white-space: nowrap;
1037
+ flex: 1;
1038
+ display: flex;
1039
+ align-items: center;
1040
+ justify-content: space-between;
1041
+ gap: 8px;
1042
+ }
1043
+ .dl dt > span {
1044
+ min-width: 0;
1045
+ overflow: hidden;
1046
+ text-overflow: ellipsis;
952
1047
  }
953
1048
  .dl dd {
954
1049
  margin: 0;
955
- padding: 8px 14px;
1050
+ padding: 4px 14px 8px;
956
1051
  font-size: 13px;
957
1052
  color: var(--ink);
958
1053
  display: flex; align-items: center;
959
1054
  gap: 6px;
960
1055
  flex-wrap: wrap;
1056
+ flex: 1;
1057
+ min-width: 0;
961
1058
  }
962
1059
 
963
1060
  .side-list { margin: 0; padding: 0; list-style: none; }
@@ -1209,6 +1306,10 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
1209
1306
  .doc-reader-page,
1210
1307
  .two-col { grid-template-columns: 1fr; }
1211
1308
  .doc-side,
1212
- .col-side { position: static; }
1309
+ .col-side {
1310
+ min-width: 0;
1311
+ width: 100%;
1312
+ position: static;
1313
+ }
1213
1314
  .outline-subitem { grid-template-columns: 1fr; gap: 5px; }
1214
1315
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bvdm/delano",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "CLI for the Delano delivery runtime.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -472,12 +472,56 @@ function applyTaskRollups({ project, task, action, previousStatus, timestamp, ch
472
472
  promoteWorkstreamToActive(project, workstream, timestamp, changes);
473
473
  }
474
474
 
475
+ if (action === "close") {
476
+ openDependencyReadyTasks(project, task, timestamp, changes);
477
+ }
478
+
475
479
  if (["close", "defer"].includes(action)) {
476
480
  closeWorkstreamIfDone(project, workstream, timestamp, changes);
477
481
  closeProjectIfDone(project, timestamp, changes);
478
482
  }
479
483
  }
480
484
 
485
+ function openDependencyReadyTasks(project, completedTask, timestamp, changes) {
486
+ const completedTaskId = String(completedTask.frontmatter.id || "").trim();
487
+ if (!completedTaskId) {
488
+ return;
489
+ }
490
+
491
+ for (const candidate of project.tasks) {
492
+ if (candidate === completedTask || !isDependencyOnlyBlockedTask(project, candidate, completedTaskId)) {
493
+ continue;
494
+ }
495
+
496
+ setFrontmatter(candidate, "status", "ready");
497
+ setFrontmatter(candidate, "updated", timestamp);
498
+ removeFrontmatter(candidate, "blocked_owner");
499
+ removeFrontmatter(candidate, "blocked_check_back");
500
+ appendEvidence(candidate, timestamp, `Opened automatically because dependencies are done after ${completedTaskId} closed.`);
501
+ changes.push(`${relativeProjectPath(project, candidate.path)} status -> ready`);
502
+ }
503
+ }
504
+
505
+ function isDependencyOnlyBlockedTask(project, task, completedTaskId) {
506
+ if ((task.frontmatter.status || "") !== "blocked") {
507
+ return false;
508
+ }
509
+
510
+ const dependencies = parseInlineList(task.frontmatter.depends_on || "[]");
511
+ if (!dependencies.includes(completedTaskId)) {
512
+ return false;
513
+ }
514
+ if (!dependencies.every((dependencyId) => findTask(project, dependencyId)?.frontmatter.status === "done")) {
515
+ return false;
516
+ }
517
+
518
+ const owner = String(task.frontmatter.blocked_owner || "").trim().toLowerCase();
519
+ if (!owner) {
520
+ return true;
521
+ }
522
+ return ["dependency", "dependencies", "delano", "delano-cli", "system", "auto", "automation"].includes(owner);
523
+ }
524
+
481
525
  function assertTaskWorkstreamExists(project, task, action) {
482
526
  const workstreamId = String(task.frontmatter.workstream || "").trim();
483
527
  if (!workstreamId) {