@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
|
|
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" : "")}>
|
|
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"
|
|
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" ?
|
|
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
|
-
|
|
1318
|
+
filteredProjectStats.length > 0 ? (
|
|
1171
1319
|
<div className="project-grid">
|
|
1172
|
-
{
|
|
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
|
|
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" ?
|
|
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">
|
|
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>
|
|
1788
|
-
|
|
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>
|
|
1792
|
-
|
|
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>
|
|
1942
|
-
|
|
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
|
-
|
|
1953
|
-
|
|
1954
|
-
<
|
|
1955
|
-
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
942
|
-
|
|
943
|
-
gap: 0
|
|
1022
|
+
display: flex;
|
|
1023
|
+
flex-direction: column;
|
|
1024
|
+
gap: 0;
|
|
944
1025
|
}
|
|
945
1026
|
.dl dt {
|
|
946
|
-
|
|
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:
|
|
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 {
|
|
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.
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
942
|
-
|
|
943
|
-
gap: 0
|
|
1022
|
+
display: flex;
|
|
1023
|
+
flex-direction: column;
|
|
1024
|
+
gap: 0;
|
|
944
1025
|
}
|
|
945
1026
|
.dl dt {
|
|
946
|
-
|
|
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:
|
|
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 {
|
|
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
|
@@ -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) {
|