@majeanson/lac 3.5.0 → 3.5.2
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/dist/index.mjs +1804 -149
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -135,7 +135,7 @@ function mergeDefs$1(...defs) {
|
|
|
135
135
|
for (const def of defs) Object.assign(mergedDescriptors, Object.getOwnPropertyDescriptors(def));
|
|
136
136
|
return Object.defineProperties({}, mergedDescriptors);
|
|
137
137
|
}
|
|
138
|
-
function esc$
|
|
138
|
+
function esc$18(str) {
|
|
139
139
|
return JSON.stringify(str);
|
|
140
140
|
}
|
|
141
141
|
function slugify$1(input) {
|
|
@@ -1539,7 +1539,7 @@ const $ZodObjectJIT$1 = /* @__PURE__ */ $constructor$1("$ZodObjectJIT", (inst, d
|
|
|
1539
1539
|
]);
|
|
1540
1540
|
const normalized = _normalized.value;
|
|
1541
1541
|
const parseStr = (key) => {
|
|
1542
|
-
const k = esc$
|
|
1542
|
+
const k = esc$18(key);
|
|
1543
1543
|
return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
|
|
1544
1544
|
};
|
|
1545
1545
|
doc.write(`const input = payload.value;`);
|
|
@@ -1549,7 +1549,7 @@ const $ZodObjectJIT$1 = /* @__PURE__ */ $constructor$1("$ZodObjectJIT", (inst, d
|
|
|
1549
1549
|
doc.write(`const newResult = {};`);
|
|
1550
1550
|
for (const key of normalized.keys) {
|
|
1551
1551
|
const id = ids[key];
|
|
1552
|
-
const k = esc$
|
|
1552
|
+
const k = esc$18(key);
|
|
1553
1553
|
const isOptionalOut = shape[key]?._zod?.optout === "optional";
|
|
1554
1554
|
doc.write(`const ${id} = ${parseStr(key)};`);
|
|
1555
1555
|
if (isOptionalOut) doc.write(`
|
|
@@ -4115,7 +4115,7 @@ function mergeDefs(...defs) {
|
|
|
4115
4115
|
for (const def of defs) Object.assign(mergedDescriptors, Object.getOwnPropertyDescriptors(def));
|
|
4116
4116
|
return Object.defineProperties({}, mergedDescriptors);
|
|
4117
4117
|
}
|
|
4118
|
-
function esc$
|
|
4118
|
+
function esc$17(str) {
|
|
4119
4119
|
return JSON.stringify(str);
|
|
4120
4120
|
}
|
|
4121
4121
|
function slugify(input) {
|
|
@@ -5505,7 +5505,7 @@ const $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def)
|
|
|
5505
5505
|
]);
|
|
5506
5506
|
const normalized = _normalized.value;
|
|
5507
5507
|
const parseStr = (key) => {
|
|
5508
|
-
const k = esc$
|
|
5508
|
+
const k = esc$17(key);
|
|
5509
5509
|
return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
|
|
5510
5510
|
};
|
|
5511
5511
|
doc.write(`const input = payload.value;`);
|
|
@@ -5515,7 +5515,7 @@ const $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def)
|
|
|
5515
5515
|
doc.write(`const newResult = {};`);
|
|
5516
5516
|
for (const key of normalized.keys) {
|
|
5517
5517
|
const id = ids[key];
|
|
5518
|
-
const k = esc$
|
|
5518
|
+
const k = esc$17(key);
|
|
5519
5519
|
const isOptionalOut = shape[key]?._zod?.optout === "optional";
|
|
5520
5520
|
doc.write(`const ${id} = ${parseStr(key)};`);
|
|
5521
5521
|
if (isOptionalOut) doc.write(`
|
|
@@ -10470,10 +10470,10 @@ const doctorCommand = new Command("doctor").description("Check workspace health
|
|
|
10470
10470
|
});
|
|
10471
10471
|
//#endregion
|
|
10472
10472
|
//#region src/lib/htmlGenerator.ts
|
|
10473
|
-
function esc$
|
|
10473
|
+
function esc$16(s) {
|
|
10474
10474
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
10475
10475
|
}
|
|
10476
|
-
function generateHtmlWiki(features, projectName, viewLabel, viewName) {
|
|
10476
|
+
function generateHtmlWiki(features, projectName, viewLabel, viewName, renderMode) {
|
|
10477
10477
|
const dataJson = JSON.stringify(features).replace(/<\/script>/gi, "<\\/script>");
|
|
10478
10478
|
features.filter((f) => f.status === "active").length, features.filter((f) => f.status === "frozen").length, features.filter((f) => f.status === "draft").length, features.filter((f) => f.status === "deprecated").length;
|
|
10479
10479
|
const domains = [...new Set(features.map((f) => f.domain).filter(Boolean))].sort();
|
|
@@ -10482,7 +10482,7 @@ function generateHtmlWiki(features, projectName, viewLabel, viewName) {
|
|
|
10482
10482
|
<head>
|
|
10483
10483
|
<meta charset="UTF-8">
|
|
10484
10484
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10485
|
-
<title>${esc$
|
|
10485
|
+
<title>${esc$16(projectName)}${viewLabel ? ` · ${esc$16(viewLabel)}` : ""} — LAC Wiki</title>
|
|
10486
10486
|
<style>
|
|
10487
10487
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10488
10488
|
|
|
@@ -11003,7 +11003,7 @@ body { background: var(--bg); color: var(--text); font-family: var(--sans); font
|
|
|
11003
11003
|
<div class="topbar">
|
|
11004
11004
|
<span class="topbar-logo">◈ lac</span>
|
|
11005
11005
|
<span class="topbar-sep">|</span>
|
|
11006
|
-
<span class="topbar-project">${esc$
|
|
11006
|
+
<span class="topbar-project">${esc$16(projectName)}${viewLabel ? ` <span style="opacity:.55;font-weight:400">· ${esc$16(viewLabel)} view</span>` : ""}</span>
|
|
11007
11007
|
<span class="topbar-count">${features.length} features · ${domains.length} domains</span>
|
|
11008
11008
|
</div>
|
|
11009
11009
|
<div class="body-row">
|
|
@@ -11082,7 +11082,7 @@ function statusColor(s) {
|
|
|
11082
11082
|
|
|
11083
11083
|
// ── Tree building ────────────────────────────────────────────────────────────
|
|
11084
11084
|
|
|
11085
|
-
const VIEW = '${viewName || ""}';
|
|
11085
|
+
const VIEW = '${renderMode || viewName || ""}';
|
|
11086
11086
|
const byKey = new Map(FEATURES.map(f => [f.featureKey, f]));
|
|
11087
11087
|
|
|
11088
11088
|
function getChildren(key) {
|
|
@@ -11457,7 +11457,7 @@ window.setSortMode = setSortMode;
|
|
|
11457
11457
|
}
|
|
11458
11458
|
//#endregion
|
|
11459
11459
|
//#region src/lib/rawHtmlGenerator.ts
|
|
11460
|
-
function esc$
|
|
11460
|
+
function esc$15(s) {
|
|
11461
11461
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
11462
11462
|
}
|
|
11463
11463
|
function generateRawHtml(features, projectName, viewLabel, viewName) {
|
|
@@ -11468,7 +11468,7 @@ function generateRawHtml(features, projectName, viewLabel, viewName) {
|
|
|
11468
11468
|
<head>
|
|
11469
11469
|
<meta charset="UTF-8">
|
|
11470
11470
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
11471
|
-
<title>${esc$
|
|
11471
|
+
<title>${esc$15(projectName)}${viewLabel ? ` · ${esc$15(viewLabel)}` : ""} — LAC Raw</title>
|
|
11472
11472
|
<style>
|
|
11473
11473
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
11474
11474
|
|
|
@@ -12000,7 +12000,7 @@ body { background: var(--bg); color: var(--text); font-family: var(--sans); font
|
|
|
12000
12000
|
<div class="topbar">
|
|
12001
12001
|
<span class="topbar-logo">◈ lac</span>
|
|
12002
12002
|
<span class="topbar-sep">|</span>
|
|
12003
|
-
<span class="topbar-project">${esc$
|
|
12003
|
+
<span class="topbar-project">${esc$15(projectName)}${viewLabel ? ` <span style="opacity:.55;font-weight:400">· ${esc$15(viewLabel)} view</span>` : ""}</span>
|
|
12004
12004
|
<span class="topbar-badge">raw</span>
|
|
12005
12005
|
<span class="topbar-count">${features.length} features · ${domains.length} domains</span>
|
|
12006
12006
|
</div>
|
|
@@ -12451,7 +12451,7 @@ async function generateSite(features, outDir, viewLabel, viewName) {
|
|
|
12451
12451
|
}
|
|
12452
12452
|
//#endregion
|
|
12453
12453
|
//#region src/lib/postcardGenerator.ts
|
|
12454
|
-
function esc$
|
|
12454
|
+
function esc$14(s) {
|
|
12455
12455
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
12456
12456
|
}
|
|
12457
12457
|
function renderInline$1(s) {
|
|
@@ -12459,7 +12459,7 @@ function renderInline$1(s) {
|
|
|
12459
12459
|
}
|
|
12460
12460
|
/** Minimal markdown → HTML for postcard body text. */
|
|
12461
12461
|
function md(text) {
|
|
12462
|
-
const lines = esc$
|
|
12462
|
+
const lines = esc$14(text).split("\n");
|
|
12463
12463
|
const result = [];
|
|
12464
12464
|
let inList = false;
|
|
12465
12465
|
for (const line of lines) {
|
|
@@ -12521,14 +12521,14 @@ function generatePostcard(feature, projectName) {
|
|
|
12521
12521
|
<li class="decision-item">
|
|
12522
12522
|
<div class="decision-header">
|
|
12523
12523
|
<span class="decision-index">${i + 1}</span>
|
|
12524
|
-
<span class="decision-text">${esc$
|
|
12525
|
-
${d.date ? `<span class="decision-date">${esc$
|
|
12524
|
+
<span class="decision-text">${esc$14(d.decision)}</span>
|
|
12525
|
+
${d.date ? `<span class="decision-date">${esc$14(d.date)}</span>` : ""}
|
|
12526
12526
|
</div>
|
|
12527
|
-
<div class="decision-rationale">${esc$
|
|
12527
|
+
<div class="decision-rationale">${esc$14(d.rationale)}</div>
|
|
12528
12528
|
${d.alternativesConsidered && d.alternativesConsidered.length > 0 ? `
|
|
12529
12529
|
<div class="decision-alts">
|
|
12530
12530
|
<span class="alts-label">Alternatives considered:</span>
|
|
12531
|
-
${d.alternativesConsidered.map((a) => `<span class="alt-pill">${esc$
|
|
12531
|
+
${d.alternativesConsidered.map((a) => `<span class="alt-pill">${esc$14(a)}</span>`).join("")}
|
|
12532
12532
|
</div>
|
|
12533
12533
|
` : ""}
|
|
12534
12534
|
</li>
|
|
@@ -12544,12 +12544,12 @@ function generatePostcard(feature, projectName) {
|
|
|
12544
12544
|
<section class="section">
|
|
12545
12545
|
<h2 class="section-label">Known Limitations</h2>
|
|
12546
12546
|
<ul class="limitations-list">
|
|
12547
|
-
${feature.knownLimitations.map((l) => `<li>${esc$
|
|
12547
|
+
${feature.knownLimitations.map((l) => `<li>${esc$14(l)}</li>`).join("")}
|
|
12548
12548
|
</ul>
|
|
12549
12549
|
</section>` : "";
|
|
12550
12550
|
const tagsHtml = feature.tags && feature.tags.length > 0 ? `
|
|
12551
12551
|
<div class="tags-row">
|
|
12552
|
-
${feature.tags.map((t) => `<span class="tag">${esc$
|
|
12552
|
+
${feature.tags.map((t) => `<span class="tag">${esc$14(t)}</span>`).join("")}
|
|
12553
12553
|
</div>` : "";
|
|
12554
12554
|
const priorityHtml = feature.priority != null ? `<span class="meta-pill priority-pill">P${feature.priority}</span>` : "";
|
|
12555
12555
|
return `<!DOCTYPE html>
|
|
@@ -12557,7 +12557,7 @@ function generatePostcard(feature, projectName) {
|
|
|
12557
12557
|
<head>
|
|
12558
12558
|
<meta charset="UTF-8">
|
|
12559
12559
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
12560
|
-
<title>${esc$
|
|
12560
|
+
<title>${esc$14(feature.title)} — ${esc$14(projectName)}</title>
|
|
12561
12561
|
<style>
|
|
12562
12562
|
:root {
|
|
12563
12563
|
--bg: #12100e;
|
|
@@ -12915,7 +12915,7 @@ function generatePostcard(feature, projectName) {
|
|
|
12915
12915
|
<header class="topbar">
|
|
12916
12916
|
<span class="topbar-logo">◈ lac</span>
|
|
12917
12917
|
<span class="topbar-project">
|
|
12918
|
-
<strong>${esc$
|
|
12918
|
+
<strong>${esc$14(projectName)}</strong> / ${esc$14(feature.featureKey)}
|
|
12919
12919
|
</span>
|
|
12920
12920
|
<span class="topbar-date">${today}</span>
|
|
12921
12921
|
</header>
|
|
@@ -12923,12 +12923,12 @@ function generatePostcard(feature, projectName) {
|
|
|
12923
12923
|
<!-- Hero -->
|
|
12924
12924
|
<div class="hero">
|
|
12925
12925
|
<div class="badges">
|
|
12926
|
-
<span class="badge-status">${esc$
|
|
12927
|
-
${feature.domain ? `<span class="badge-domain">${esc$
|
|
12926
|
+
<span class="badge-status">${esc$14(statusLabel)}</span>
|
|
12927
|
+
${feature.domain ? `<span class="badge-domain">${esc$14(feature.domain)}</span>` : ""}
|
|
12928
12928
|
${priorityHtml}
|
|
12929
|
-
${feature.owner ? `<span class="meta-pill">${esc$
|
|
12929
|
+
${feature.owner ? `<span class="meta-pill">${esc$14(feature.owner)}</span>` : ""}
|
|
12930
12930
|
</div>
|
|
12931
|
-
<h1 class="feature-title">${esc$
|
|
12931
|
+
<h1 class="feature-title">${esc$14(feature.title)}</h1>
|
|
12932
12932
|
</div>
|
|
12933
12933
|
|
|
12934
12934
|
<!-- Body -->
|
|
@@ -12951,7 +12951,7 @@ function generatePostcard(feature, projectName) {
|
|
|
12951
12951
|
<footer class="postcard-footer">
|
|
12952
12952
|
<span class="footer-brand">generated via life-as-code</span>
|
|
12953
12953
|
<span class="footer-sep">·</span>
|
|
12954
|
-
<span class="footer-key">${esc$
|
|
12954
|
+
<span class="footer-key">${esc$14(feature.featureKey)}</span>
|
|
12955
12955
|
</footer>
|
|
12956
12956
|
|
|
12957
12957
|
</article>
|
|
@@ -12961,7 +12961,7 @@ function generatePostcard(feature, projectName) {
|
|
|
12961
12961
|
}
|
|
12962
12962
|
//#endregion
|
|
12963
12963
|
//#region src/lib/printGenerator.ts
|
|
12964
|
-
function esc$
|
|
12964
|
+
function esc$13(s) {
|
|
12965
12965
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
12966
12966
|
}
|
|
12967
12967
|
/**
|
|
@@ -12977,11 +12977,11 @@ function mdToHtml(text) {
|
|
|
12977
12977
|
while (i < lines.length) {
|
|
12978
12978
|
const line = lines[i] ?? "";
|
|
12979
12979
|
if (line.startsWith("```")) {
|
|
12980
|
-
const lang = esc$
|
|
12980
|
+
const lang = esc$13(line.slice(3).trim());
|
|
12981
12981
|
const codeLines = [];
|
|
12982
12982
|
i++;
|
|
12983
12983
|
while (i < lines.length && !(lines[i] ?? "").startsWith("```")) {
|
|
12984
|
-
codeLines.push(esc$
|
|
12984
|
+
codeLines.push(esc$13(lines[i] ?? ""));
|
|
12985
12985
|
i++;
|
|
12986
12986
|
}
|
|
12987
12987
|
if (i < lines.length) i++;
|
|
@@ -12990,24 +12990,24 @@ function mdToHtml(text) {
|
|
|
12990
12990
|
continue;
|
|
12991
12991
|
}
|
|
12992
12992
|
if (line.startsWith("### ")) {
|
|
12993
|
-
out.push(`<h3>${renderInline(esc$
|
|
12993
|
+
out.push(`<h3>${renderInline(esc$13(line.slice(4)))}</h3>`);
|
|
12994
12994
|
i++;
|
|
12995
12995
|
continue;
|
|
12996
12996
|
}
|
|
12997
12997
|
if (line.startsWith("## ")) {
|
|
12998
|
-
out.push(`<h2>${renderInline(esc$
|
|
12998
|
+
out.push(`<h2>${renderInline(esc$13(line.slice(3)))}</h2>`);
|
|
12999
12999
|
i++;
|
|
13000
13000
|
continue;
|
|
13001
13001
|
}
|
|
13002
13002
|
if (line.startsWith("# ")) {
|
|
13003
|
-
out.push(`<h1>${renderInline(esc$
|
|
13003
|
+
out.push(`<h1>${renderInline(esc$13(line.slice(2)))}</h1>`);
|
|
13004
13004
|
i++;
|
|
13005
13005
|
continue;
|
|
13006
13006
|
}
|
|
13007
13007
|
if (/^[-*] /.test(line)) {
|
|
13008
13008
|
const items = [];
|
|
13009
13009
|
while (i < lines.length && /^[-*] /.test(lines[i] ?? "")) {
|
|
13010
|
-
items.push(`<li>${renderInline(esc$
|
|
13010
|
+
items.push(`<li>${renderInline(esc$13((lines[i] ?? "").slice(2)))}</li>`);
|
|
13011
13011
|
i++;
|
|
13012
13012
|
}
|
|
13013
13013
|
out.push(`<ul>${items.join("")}</ul>`);
|
|
@@ -13016,7 +13016,7 @@ function mdToHtml(text) {
|
|
|
13016
13016
|
if (/^[1-9]\d*\. /.test(line)) {
|
|
13017
13017
|
const items = [];
|
|
13018
13018
|
while (i < lines.length && /^[1-9]\d*\. /.test(lines[i] ?? "")) {
|
|
13019
|
-
items.push(`<li>${renderInline(esc$
|
|
13019
|
+
items.push(`<li>${renderInline(esc$13((lines[i] ?? "").replace(/^[1-9]\d*\. /, "")))}</li>`);
|
|
13020
13020
|
i++;
|
|
13021
13021
|
}
|
|
13022
13022
|
out.push(`<ol>${items.join("")}</ol>`);
|
|
@@ -13031,7 +13031,7 @@ function mdToHtml(text) {
|
|
|
13031
13031
|
paraLines.push(lines[i] ?? "");
|
|
13032
13032
|
i++;
|
|
13033
13033
|
}
|
|
13034
|
-
if (paraLines.length > 0) out.push(`<p>${renderInline(esc$
|
|
13034
|
+
if (paraLines.length > 0) out.push(`<p>${renderInline(esc$13(paraLines.join(" ")))}</p>`);
|
|
13035
13035
|
}
|
|
13036
13036
|
return out.join("\n");
|
|
13037
13037
|
}
|
|
@@ -13491,8 +13491,8 @@ function buildCoverPage(features, projectName, viewLabel, today) {
|
|
|
13491
13491
|
<hr class="page-rule">
|
|
13492
13492
|
<div class="cover">
|
|
13493
13493
|
<div class="cover-eyebrow">◈ life-as-code · Feature Documentation</div>
|
|
13494
|
-
<div class="cover-title">${esc$
|
|
13495
|
-
<div class="cover-subtitle">${esc$
|
|
13494
|
+
<div class="cover-title">${esc$13(projectName)}</div>
|
|
13495
|
+
<div class="cover-subtitle">${esc$13(viewLabel)}</div>
|
|
13496
13496
|
<div class="cover-meta">
|
|
13497
13497
|
<div class="cover-meta-item">
|
|
13498
13498
|
<strong>Generated</strong>
|
|
@@ -13514,7 +13514,7 @@ function buildCoverPage(features, projectName, viewLabel, today) {
|
|
|
13514
13514
|
</div>
|
|
13515
13515
|
</div>
|
|
13516
13516
|
<div class="page-footer">
|
|
13517
|
-
<span class="footer-left">${esc$
|
|
13517
|
+
<span class="footer-left">${esc$13(projectName)}</span>
|
|
13518
13518
|
<span class="footer-right">${today}</span>
|
|
13519
13519
|
</div>
|
|
13520
13520
|
</div>`;
|
|
@@ -13531,14 +13531,14 @@ function buildTocPage(features, projectName, today) {
|
|
|
13531
13531
|
return `
|
|
13532
13532
|
<li class="toc-item">
|
|
13533
13533
|
<span class="toc-num">${idx + 1}.</span>
|
|
13534
|
-
<span class="toc-key">${esc$
|
|
13535
|
-
<span class="toc-feature-title">${esc$
|
|
13536
|
-
<span class="toc-badge" style="color:${color};background:${color}1a">${esc$
|
|
13534
|
+
<span class="toc-key">${esc$13(f.featureKey)}</span>
|
|
13535
|
+
<span class="toc-feature-title">${esc$13(f.title)}</span>
|
|
13536
|
+
<span class="toc-badge" style="color:${color};background:${color}1a">${esc$13(label)}</span>
|
|
13537
13537
|
</li>`;
|
|
13538
13538
|
}).join("")}
|
|
13539
13539
|
</ol>
|
|
13540
13540
|
<div class="page-footer">
|
|
13541
|
-
<span class="footer-left">${esc$
|
|
13541
|
+
<span class="footer-left">${esc$13(projectName)}</span>
|
|
13542
13542
|
<span class="footer-right">${today}</span>
|
|
13543
13543
|
</div>
|
|
13544
13544
|
</div>`;
|
|
@@ -13554,11 +13554,11 @@ function buildFeaturePage(feature, projectName, today) {
|
|
|
13554
13554
|
<li class="decision-item">
|
|
13555
13555
|
<div class="decision-header">
|
|
13556
13556
|
<span class="decision-num">${i + 1}.</span>
|
|
13557
|
-
<span class="decision-text">${esc$
|
|
13558
|
-
${d.date ? `<span class="decision-date">${esc$
|
|
13557
|
+
<span class="decision-text">${esc$13(d.decision)}</span>
|
|
13558
|
+
${d.date ? `<span class="decision-date">${esc$13(d.date)}</span>` : ""}
|
|
13559
13559
|
</div>
|
|
13560
|
-
<div class="decision-rationale">${esc$
|
|
13561
|
-
${d.alternativesConsidered && d.alternativesConsidered.length > 0 ? `<div class="decision-alts">Alternatives considered: ${d.alternativesConsidered.map((a) => esc$
|
|
13560
|
+
<div class="decision-rationale">${esc$13(d.rationale)}</div>
|
|
13561
|
+
${d.alternativesConsidered && d.alternativesConsidered.length > 0 ? `<div class="decision-alts">Alternatives considered: ${d.alternativesConsidered.map((a) => esc$13(a)).join(", ")}</div>` : ""}
|
|
13562
13562
|
</li>`).join("")}
|
|
13563
13563
|
</ol>
|
|
13564
13564
|
</div>` : "";
|
|
@@ -13581,26 +13581,26 @@ function buildFeaturePage(feature, projectName, today) {
|
|
|
13581
13581
|
<div class="section">
|
|
13582
13582
|
<div class="section-label">Known Limitations</div>
|
|
13583
13583
|
<ul class="limitations-list">
|
|
13584
|
-
${feature.knownLimitations.map((l) => `<li>${esc$
|
|
13584
|
+
${feature.knownLimitations.map((l) => `<li>${esc$13(l)}</li>`).join("")}
|
|
13585
13585
|
</ul>
|
|
13586
13586
|
</div>` : "";
|
|
13587
13587
|
const tagsHtml = feature.tags && feature.tags.length > 0 ? `
|
|
13588
13588
|
<div class="section">
|
|
13589
13589
|
<div class="section-label">Tags</div>
|
|
13590
13590
|
<div class="tags-row">
|
|
13591
|
-
${feature.tags.map((t) => `<span class="tag">${esc$
|
|
13591
|
+
${feature.tags.map((t) => `<span class="tag">${esc$13(t)}</span>`).join("")}
|
|
13592
13592
|
</div>
|
|
13593
13593
|
</div>` : "";
|
|
13594
13594
|
return `
|
|
13595
13595
|
<div class="page">
|
|
13596
13596
|
<hr class="page-rule">
|
|
13597
13597
|
<div class="feature-eyebrow">
|
|
13598
|
-
<span class="feature-key">${esc$
|
|
13599
|
-
<span class="feature-badge" style="color:${color};background:${color}1a">${esc$
|
|
13600
|
-
${feature.domain ? `<span class="feature-domain">${esc$
|
|
13598
|
+
<span class="feature-key">${esc$13(feature.featureKey)}</span>
|
|
13599
|
+
<span class="feature-badge" style="color:${color};background:${color}1a">${esc$13(label)}</span>
|
|
13600
|
+
${feature.domain ? `<span class="feature-domain">${esc$13(feature.domain)}</span>` : ""}
|
|
13601
13601
|
${feature.priority != null ? `<span class="feature-domain">P${feature.priority}</span>` : ""}
|
|
13602
13602
|
</div>
|
|
13603
|
-
<h1 class="feature-h1">${esc$
|
|
13603
|
+
<h1 class="feature-h1">${esc$13(feature.title)}</h1>
|
|
13604
13604
|
|
|
13605
13605
|
<div class="section">
|
|
13606
13606
|
<div class="section-label">Problem</div>
|
|
@@ -13615,7 +13615,7 @@ function buildFeaturePage(feature, projectName, today) {
|
|
|
13615
13615
|
${tagsHtml}
|
|
13616
13616
|
|
|
13617
13617
|
<div class="page-footer">
|
|
13618
|
-
<span class="footer-left">${esc$
|
|
13618
|
+
<span class="footer-left">${esc$13(projectName)}</span>
|
|
13619
13619
|
<span class="footer-right">${today}</span>
|
|
13620
13620
|
</div>
|
|
13621
13621
|
</div>`;
|
|
@@ -13637,7 +13637,7 @@ function generatePrint(features, projectName, viewLabel) {
|
|
|
13637
13637
|
<head>
|
|
13638
13638
|
<meta charset="UTF-8">
|
|
13639
13639
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13640
|
-
<title>${esc$
|
|
13640
|
+
<title>${esc$13(projectName)} — ${esc$13(resolvedLabel)}</title>
|
|
13641
13641
|
<style>
|
|
13642
13642
|
${buildCss(today)}
|
|
13643
13643
|
</style>
|
|
@@ -13653,7 +13653,7 @@ ${featurePages}
|
|
|
13653
13653
|
}
|
|
13654
13654
|
//#endregion
|
|
13655
13655
|
//#region src/lib/resumeGenerator.ts
|
|
13656
|
-
function esc$
|
|
13656
|
+
function esc$12(s) {
|
|
13657
13657
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13658
13658
|
}
|
|
13659
13659
|
function today$3() {
|
|
@@ -13691,17 +13691,17 @@ function renderStatusBadge() {
|
|
|
13691
13691
|
return `<span class="badge badge-frozen">frozen</span>`;
|
|
13692
13692
|
}
|
|
13693
13693
|
function renderTagPills(tags) {
|
|
13694
|
-
return tags.map((t) => `<span class="pill">${esc$
|
|
13694
|
+
return tags.map((t) => `<span class="pill">${esc$12(t)}</span>`).join("");
|
|
13695
13695
|
}
|
|
13696
13696
|
function renderNpmPills(pkgs) {
|
|
13697
|
-
return pkgs.map((p) => `<span class="npm-pill">${esc$
|
|
13697
|
+
return pkgs.map((p) => `<span class="npm-pill">${esc$12(p)}</span>`).join("");
|
|
13698
13698
|
}
|
|
13699
13699
|
function renderDecisions(decisions) {
|
|
13700
13700
|
return `
|
|
13701
13701
|
<div class="section-block">
|
|
13702
13702
|
<span class="section-label">Key decisions:</span>
|
|
13703
13703
|
<ul class="decision-list">
|
|
13704
|
-
${decisions.slice(0, 3).map((d) => `<li>${esc$
|
|
13704
|
+
${decisions.slice(0, 3).map((d) => `<li>${esc$12(d.decision)}</li>`).join("\n ")}
|
|
13705
13705
|
${decisions.length > 3 ? `<li class="more-decisions">+${decisions.length - 3} more decision${decisions.length - 3 === 1 ? "" : "s"}</li>` : ""}
|
|
13706
13706
|
</ul>
|
|
13707
13707
|
</div>`;
|
|
@@ -13712,12 +13712,12 @@ function renderFeatureCard(f) {
|
|
|
13712
13712
|
const successCriteria = f.successCriteria ? `
|
|
13713
13713
|
<div class="section-block outcome">
|
|
13714
13714
|
<span class="section-label accent">Outcome:</span>
|
|
13715
|
-
<span class="outcome-text">${esc$
|
|
13715
|
+
<span class="outcome-text">${esc$12(f.successCriteria)}</span>
|
|
13716
13716
|
</div>` : "";
|
|
13717
13717
|
const spawnReason = f.lineage?.spawnReason ? `
|
|
13718
13718
|
<div class="section-block spawn-note">
|
|
13719
13719
|
<span class="section-label">Spawned from:</span>
|
|
13720
|
-
<span class="spawn-text">${esc$
|
|
13720
|
+
<span class="spawn-text">${esc$12(f.lineage.spawnReason)}</span>
|
|
13721
13721
|
</div>` : "";
|
|
13722
13722
|
const npmPackages = f.npmPackages && f.npmPackages.length > 0 ? `
|
|
13723
13723
|
<div class="section-block">
|
|
@@ -13728,8 +13728,8 @@ function renderFeatureCard(f) {
|
|
|
13728
13728
|
<article class="feature-card">
|
|
13729
13729
|
<header class="card-header">
|
|
13730
13730
|
<div class="card-title-row">
|
|
13731
|
-
<h3 class="card-title">${esc$
|
|
13732
|
-
<code class="feature-key">${esc$
|
|
13731
|
+
<h3 class="card-title">${esc$12(f.title)}</h3>
|
|
13732
|
+
<code class="feature-key">${esc$12(f.featureKey)}</code>
|
|
13733
13733
|
</div>
|
|
13734
13734
|
<div class="card-meta">
|
|
13735
13735
|
${renderStatusBadge()}
|
|
@@ -13737,7 +13737,7 @@ function renderFeatureCard(f) {
|
|
|
13737
13737
|
</div>
|
|
13738
13738
|
</header>
|
|
13739
13739
|
|
|
13740
|
-
<blockquote class="problem-block">${esc$
|
|
13740
|
+
<blockquote class="problem-block">${esc$12(f.problem)}</blockquote>
|
|
13741
13741
|
${decisions}
|
|
13742
13742
|
${successCriteria}
|
|
13743
13743
|
${spawnReason}
|
|
@@ -13749,7 +13749,7 @@ function renderDomainGroup(domain, features) {
|
|
|
13749
13749
|
return `
|
|
13750
13750
|
<section class="domain-group">
|
|
13751
13751
|
<h2 class="domain-heading">
|
|
13752
|
-
<span class="domain-label">${esc$
|
|
13752
|
+
<span class="domain-label">${esc$12(domain.toUpperCase())}</span>
|
|
13753
13753
|
<span class="domain-count">${features.length} feature${features.length === 1 ? "" : "s"}</span>
|
|
13754
13754
|
</h2>
|
|
13755
13755
|
${cards}
|
|
@@ -13767,7 +13767,7 @@ function renderEmptyState(projectName) {
|
|
|
13767
13767
|
have been met.
|
|
13768
13768
|
</p>
|
|
13769
13769
|
<p class="empty-hint">
|
|
13770
|
-
Run <code>lac advance ${esc$
|
|
13770
|
+
Run <code>lac advance ${esc$12(projectName)} --status frozen</code> when a feature ships.
|
|
13771
13771
|
</p>
|
|
13772
13772
|
</div>
|
|
13773
13773
|
</main>`;
|
|
@@ -13813,7 +13813,7 @@ function generateResume(features, projectName) {
|
|
|
13813
13813
|
<head>
|
|
13814
13814
|
<meta charset="UTF-8">
|
|
13815
13815
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13816
|
-
<title>${esc$
|
|
13816
|
+
<title>${esc$12(projectName)} — Feature Portfolio</title>
|
|
13817
13817
|
<style>
|
|
13818
13818
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
13819
13819
|
|
|
@@ -14258,7 +14258,7 @@ function generateResume(features, projectName) {
|
|
|
14258
14258
|
<header class="page-header">
|
|
14259
14259
|
<div class="header-eyebrow">feature portfolio</div>
|
|
14260
14260
|
<h1 class="header-title">
|
|
14261
|
-
<span class="header-diamond">◈</span>${esc$
|
|
14261
|
+
<span class="header-diamond">◈</span>${esc$12(projectName)}
|
|
14262
14262
|
</h1>
|
|
14263
14263
|
<p class="header-subtitle">
|
|
14264
14264
|
${frozen.length} shipped feature${frozen.length === 1 ? "" : "s"}
|
|
@@ -17032,7 +17032,7 @@ const FIELDS = [
|
|
|
17032
17032
|
}
|
|
17033
17033
|
];
|
|
17034
17034
|
const TOTAL_FIELDS = FIELDS.length;
|
|
17035
|
-
function esc$
|
|
17035
|
+
function esc$11(s) {
|
|
17036
17036
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
17037
17037
|
}
|
|
17038
17038
|
function isFilled(f, field) {
|
|
@@ -17109,7 +17109,7 @@ function renderColumnHeaders() {
|
|
|
17109
17109
|
${FIELDS.map((fd) => `
|
|
17110
17110
|
<th class="col-header" scope="col">
|
|
17111
17111
|
<div class="col-label-wrap">
|
|
17112
|
-
<span class="col-label">${esc$
|
|
17112
|
+
<span class="col-label">${esc$11(fd.label)}</span>
|
|
17113
17113
|
</div>
|
|
17114
17114
|
</th>`).join("")}
|
|
17115
17115
|
<th class="pct-header" scope="col">done</th>
|
|
@@ -17137,16 +17137,16 @@ function renderFeatureRow(f, idx) {
|
|
|
17137
17137
|
data-feature-idx="${idx}"
|
|
17138
17138
|
data-col-idx="${colIdx}"
|
|
17139
17139
|
role="gridcell"
|
|
17140
|
-
aria-label="${esc$
|
|
17140
|
+
aria-label="${esc$11(fd.key)}: ${filled ? "filled" : "empty"}"
|
|
17141
17141
|
><span class="cell-inner"></span></td>`;
|
|
17142
17142
|
}).join("");
|
|
17143
17143
|
return `
|
|
17144
17144
|
<tr class="feature-row" data-feature-idx="${idx}">
|
|
17145
17145
|
<th class="row-header" scope="row">
|
|
17146
17146
|
<div class="row-header-inner">
|
|
17147
|
-
<span class="row-key">${esc$
|
|
17148
|
-
<span class="row-title">${esc$
|
|
17149
|
-
${domain ? `<span class="row-domain">${esc$
|
|
17147
|
+
<span class="row-key">${esc$11(f.featureKey)}</span>
|
|
17148
|
+
<span class="row-title">${esc$11(f.title)}</span>
|
|
17149
|
+
${domain ? `<span class="row-domain">${esc$11(domain)}</span>` : ""}
|
|
17150
17150
|
</div>
|
|
17151
17151
|
</th>
|
|
17152
17152
|
${cells}
|
|
@@ -17181,7 +17181,7 @@ function generateHeatmap(features, projectName) {
|
|
|
17181
17181
|
<head>
|
|
17182
17182
|
<meta charset="UTF-8">
|
|
17183
17183
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
17184
|
-
<title>${esc$
|
|
17184
|
+
<title>${esc$11(projectName)} — LAC Heatmap</title>
|
|
17185
17185
|
<style>
|
|
17186
17186
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
17187
17187
|
|
|
@@ -17636,7 +17636,7 @@ function generateHeatmap(features, projectName) {
|
|
|
17636
17636
|
<span>heatmap</span>
|
|
17637
17637
|
</div>
|
|
17638
17638
|
<div class="topbar-right">
|
|
17639
|
-
<span class="topbar-project">${esc$
|
|
17639
|
+
<span class="topbar-project">${esc$11(projectName)}</span>
|
|
17640
17640
|
<span class="topbar-count">${features.length} feature${features.length === 1 ? "" : "s"}</span>
|
|
17641
17641
|
</div>
|
|
17642
17642
|
</div>
|
|
@@ -17665,13 +17665,13 @@ function generateHeatmap(features, projectName) {
|
|
|
17665
17665
|
<div class="footer-block">
|
|
17666
17666
|
<div class="footer-block-title">Most missed fields</div>
|
|
17667
17667
|
<ul class="footer-list">
|
|
17668
|
-
${missed.length > 0 ? missed.map((m) => `<li>${esc$
|
|
17668
|
+
${missed.length > 0 ? missed.map((m) => `<li>${esc$11(m)}</li>`).join("\n ") : "<li>All fields well covered</li>"}
|
|
17669
17669
|
</ul>
|
|
17670
17670
|
</div>
|
|
17671
17671
|
<div class="footer-block">
|
|
17672
17672
|
<div class="footer-block-title">Most complete features</div>
|
|
17673
17673
|
<ul class="footer-list">
|
|
17674
|
-
${stars.length > 0 ? stars.map((s) => `<li>${esc$
|
|
17674
|
+
${stars.length > 0 ? stars.map((s) => `<li>${esc$11(s)}</li>`).join("\n ") : "<li>No features yet</li>"}
|
|
17675
17675
|
</ul>
|
|
17676
17676
|
</div>
|
|
17677
17677
|
</div>
|
|
@@ -17770,7 +17770,7 @@ function generateHeatmap(features, projectName) {
|
|
|
17770
17770
|
}
|
|
17771
17771
|
//#endregion
|
|
17772
17772
|
//#region src/lib/diffGenerator.ts
|
|
17773
|
-
function esc$
|
|
17773
|
+
function esc$10(s) {
|
|
17774
17774
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
17775
17775
|
}
|
|
17776
17776
|
const SCALAR_FIELDS = [
|
|
@@ -17828,42 +17828,42 @@ function diffFeatures(a, b) {
|
|
|
17828
17828
|
return diffs;
|
|
17829
17829
|
}
|
|
17830
17830
|
function statusBadge(status) {
|
|
17831
|
-
return `<span class="badge badge-${esc$
|
|
17831
|
+
return `<span class="badge badge-${esc$10(status)}">${esc$10(status)}</span>`;
|
|
17832
17832
|
}
|
|
17833
17833
|
function renderAdded(f) {
|
|
17834
17834
|
return `<div class="feature-card added">
|
|
17835
17835
|
<div class="card-header">
|
|
17836
17836
|
<span class="card-sign added-sign">+</span>
|
|
17837
|
-
<span class="feature-key">${esc$
|
|
17837
|
+
<span class="feature-key">${esc$10(f.featureKey)}</span>
|
|
17838
17838
|
${statusBadge(f.status)}
|
|
17839
|
-
${f.domain ? `<span class="domain-badge">${esc$
|
|
17840
|
-
<span class="feature-title">${esc$
|
|
17839
|
+
${f.domain ? `<span class="domain-badge">${esc$10(f.domain)}</span>` : ""}
|
|
17840
|
+
<span class="feature-title">${esc$10(f.title)}</span>
|
|
17841
17841
|
</div>
|
|
17842
|
-
<div class="card-problem">${esc$
|
|
17843
|
-
${f.tags && f.tags.length ? `<div class="card-tags">${f.tags.map((t) => `<span class="tag">${esc$
|
|
17842
|
+
<div class="card-problem">${esc$10(f.problem)}</div>
|
|
17843
|
+
${f.tags && f.tags.length ? `<div class="card-tags">${f.tags.map((t) => `<span class="tag">${esc$10(t)}</span>`).join("")}</div>` : ""}
|
|
17844
17844
|
</div>`;
|
|
17845
17845
|
}
|
|
17846
17846
|
function renderRemoved(f) {
|
|
17847
17847
|
return `<div class="feature-card removed">
|
|
17848
17848
|
<div class="card-header">
|
|
17849
17849
|
<span class="card-sign removed-sign">−</span>
|
|
17850
|
-
<span class="feature-key">${esc$
|
|
17850
|
+
<span class="feature-key">${esc$10(f.featureKey)}</span>
|
|
17851
17851
|
${statusBadge(f.status)}
|
|
17852
|
-
${f.domain ? `<span class="domain-badge">${esc$
|
|
17853
|
-
<span class="feature-title">${esc$
|
|
17852
|
+
${f.domain ? `<span class="domain-badge">${esc$10(f.domain)}</span>` : ""}
|
|
17853
|
+
<span class="feature-title">${esc$10(f.title)}</span>
|
|
17854
17854
|
</div>
|
|
17855
|
-
<div class="card-problem">${esc$
|
|
17856
|
-
${f.tags && f.tags.length ? `<div class="card-tags">${f.tags.map((t) => `<span class="tag">${esc$
|
|
17855
|
+
<div class="card-problem">${esc$10(f.problem)}</div>
|
|
17856
|
+
${f.tags && f.tags.length ? `<div class="card-tags">${f.tags.map((t) => `<span class="tag">${esc$10(t)}</span>`).join("")}</div>` : ""}
|
|
17857
17857
|
</div>`;
|
|
17858
17858
|
}
|
|
17859
17859
|
function renderChanged(a, b, diffs) {
|
|
17860
17860
|
const rows = diffs.map((d) => {
|
|
17861
17861
|
const oldEmpty = d.oldVal === "";
|
|
17862
17862
|
const newEmpty = d.newVal === "";
|
|
17863
|
-
const oldDisplay = oldEmpty ? "<span class=\"diff-empty\">— not set</span>" : `<pre class="diff-val">${esc$
|
|
17864
|
-
const newDisplay = newEmpty ? "<span class=\"diff-empty\">— not set</span>" : `<pre class="diff-val">${esc$
|
|
17863
|
+
const oldDisplay = oldEmpty ? "<span class=\"diff-empty\">— not set</span>" : `<pre class="diff-val">${esc$10(d.oldVal.length > 400 ? d.oldVal.slice(0, 400) + "…" : d.oldVal)}</pre>`;
|
|
17864
|
+
const newDisplay = newEmpty ? "<span class=\"diff-empty\">— not set</span>" : `<pre class="diff-val">${esc$10(d.newVal.length > 400 ? d.newVal.slice(0, 400) + "…" : d.newVal)}</pre>`;
|
|
17865
17865
|
return `<tr>
|
|
17866
|
-
<td class="diff-field"><code>${esc$
|
|
17866
|
+
<td class="diff-field"><code>${esc$10(d.field)}</code></td>
|
|
17867
17867
|
<td class="diff-old">${oldDisplay}</td>
|
|
17868
17868
|
<td class="diff-new">${newDisplay}</td>
|
|
17869
17869
|
</tr>`;
|
|
@@ -17871,10 +17871,10 @@ function renderChanged(a, b, diffs) {
|
|
|
17871
17871
|
return `<div class="feature-card changed">
|
|
17872
17872
|
<div class="card-header">
|
|
17873
17873
|
<span class="card-sign changed-sign">~</span>
|
|
17874
|
-
<span class="feature-key">${esc$
|
|
17874
|
+
<span class="feature-key">${esc$10(b.featureKey)}</span>
|
|
17875
17875
|
${statusBadge(b.status)}
|
|
17876
|
-
${b.domain ? `<span class="domain-badge">${esc$
|
|
17877
|
-
<span class="feature-title">${esc$
|
|
17876
|
+
${b.domain ? `<span class="domain-badge">${esc$10(b.domain)}</span>` : ""}
|
|
17877
|
+
<span class="feature-title">${esc$10(b.title)}</span>
|
|
17878
17878
|
<span class="diff-count">${diffs.length} field${diffs.length === 1 ? "" : "s"} changed</span>
|
|
17879
17879
|
</div>
|
|
17880
17880
|
<div class="diff-table-wrap">
|
|
@@ -17913,7 +17913,7 @@ function generateDiff(featuresA, featuresB, nameA, nameB) {
|
|
|
17913
17913
|
<head>
|
|
17914
17914
|
<meta charset="UTF-8">
|
|
17915
17915
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
17916
|
-
<title>Diff: ${esc$
|
|
17916
|
+
<title>Diff: ${esc$10(nameA)} → ${esc$10(nameB)}</title>
|
|
17917
17917
|
<style>
|
|
17918
17918
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
17919
17919
|
|
|
@@ -18203,9 +18203,9 @@ body {
|
|
|
18203
18203
|
<span class="topbar-logo">◈ lac</span>
|
|
18204
18204
|
<span class="topbar-sep">|</span>
|
|
18205
18205
|
<div class="topbar-dirs">
|
|
18206
|
-
<span>${esc$
|
|
18206
|
+
<span>${esc$10(nameA)}</span>
|
|
18207
18207
|
<span class="topbar-arrow">→</span>
|
|
18208
|
-
<span>${esc$
|
|
18208
|
+
<span>${esc$10(nameB)}</span>
|
|
18209
18209
|
</div>
|
|
18210
18210
|
<div class="topbar-right">${date} · ${totalChanges} change${totalChanges === 1 ? "" : "s"}</div>
|
|
18211
18211
|
</div>
|
|
@@ -18257,7 +18257,7 @@ body {
|
|
|
18257
18257
|
` : ""}
|
|
18258
18258
|
|
|
18259
18259
|
${totalChanges === 0 ? `
|
|
18260
|
-
<div class="empty-state">No differences found between ${esc$
|
|
18260
|
+
<div class="empty-state">No differences found between ${esc$10(nameA)} and ${esc$10(nameB)}</div>
|
|
18261
18261
|
` : ""}
|
|
18262
18262
|
|
|
18263
18263
|
</div>
|
|
@@ -18266,7 +18266,7 @@ body {
|
|
|
18266
18266
|
}
|
|
18267
18267
|
//#endregion
|
|
18268
18268
|
//#region src/lib/treemapGenerator.ts
|
|
18269
|
-
function esc$
|
|
18269
|
+
function esc$9(s) {
|
|
18270
18270
|
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
18271
18271
|
}
|
|
18272
18272
|
function generateTreemap(features, projectName) {
|
|
@@ -18274,13 +18274,13 @@ function generateTreemap(features, projectName) {
|
|
|
18274
18274
|
const count = features.length;
|
|
18275
18275
|
const domainSet = /* @__PURE__ */ new Set();
|
|
18276
18276
|
for (const f of features) if (f.domain) domainSet.add(f.domain);
|
|
18277
|
-
const domainChips = Array.from(domainSet).sort().map((d) => `<button class="domain-chip" data-domain="${esc$
|
|
18277
|
+
const domainChips = Array.from(domainSet).sort().map((d) => `<button class="domain-chip" data-domain="${esc$9(d)}">${esc$9(d)}</button>`).join("\n ");
|
|
18278
18278
|
return `<!DOCTYPE html>
|
|
18279
18279
|
<html lang="en">
|
|
18280
18280
|
<head>
|
|
18281
18281
|
<meta charset="UTF-8">
|
|
18282
18282
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18283
|
-
<title>${esc$
|
|
18283
|
+
<title>${esc$9(projectName)} · LAC Treemap</title>
|
|
18284
18284
|
<style>
|
|
18285
18285
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
|
18286
18286
|
:root{
|
|
@@ -18484,7 +18484,7 @@ html,body{
|
|
|
18484
18484
|
<div id="topbar">
|
|
18485
18485
|
<span id="topbar-brand">◈ lac · treemap</span>
|
|
18486
18486
|
<span id="topbar-sep">|</span>
|
|
18487
|
-
<span id="topbar-project">${esc$
|
|
18487
|
+
<span id="topbar-project">${esc$9(projectName)}</span>
|
|
18488
18488
|
<span id="topbar-count">${count} feature${count === 1 ? "" : "s"}</span>
|
|
18489
18489
|
<div id="domain-chips">
|
|
18490
18490
|
<button id="chip-all">All</button>
|
|
@@ -18802,7 +18802,7 @@ buildTiles();
|
|
|
18802
18802
|
}
|
|
18803
18803
|
//#endregion
|
|
18804
18804
|
//#region src/lib/kanbanGenerator.ts
|
|
18805
|
-
function esc$
|
|
18805
|
+
function esc$8(s) {
|
|
18806
18806
|
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
18807
18807
|
}
|
|
18808
18808
|
function generateKanban(features, projectName) {
|
|
@@ -18810,13 +18810,13 @@ function generateKanban(features, projectName) {
|
|
|
18810
18810
|
const count = features.length;
|
|
18811
18811
|
const domainSet = /* @__PURE__ */ new Set();
|
|
18812
18812
|
for (const f of features) if (f.domain) domainSet.add(f.domain);
|
|
18813
|
-
const domainOptions = Array.from(domainSet).sort().map((d) => `<option value="${esc$
|
|
18813
|
+
const domainOptions = Array.from(domainSet).sort().map((d) => `<option value="${esc$8(d)}">${esc$8(d)}</option>`).join("\n ");
|
|
18814
18814
|
return `<!DOCTYPE html>
|
|
18815
18815
|
<html lang="en">
|
|
18816
18816
|
<head>
|
|
18817
18817
|
<meta charset="UTF-8">
|
|
18818
18818
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18819
|
-
<title>${esc$
|
|
18819
|
+
<title>${esc$8(projectName)} · LAC Kanban</title>
|
|
18820
18820
|
<style>
|
|
18821
18821
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
|
18822
18822
|
:root{
|
|
@@ -19050,7 +19050,7 @@ select.ctrl-select:focus{border-color:var(--accent);}
|
|
|
19050
19050
|
<div id="topbar">
|
|
19051
19051
|
<span id="topbar-brand">◈ lac · kanban</span>
|
|
19052
19052
|
<span id="topbar-sep">|</span>
|
|
19053
|
-
<span id="topbar-project">${esc$
|
|
19053
|
+
<span id="topbar-project">${esc$8(projectName)}</span>
|
|
19054
19054
|
<span id="topbar-count">${count} feature${count === 1 ? "" : "s"}</span>
|
|
19055
19055
|
<div id="topbar-controls">
|
|
19056
19056
|
<span class="ctrl-label">Domain</span>
|
|
@@ -19329,7 +19329,7 @@ buildBoard();
|
|
|
19329
19329
|
}
|
|
19330
19330
|
//#endregion
|
|
19331
19331
|
//#region src/lib/healthGenerator.ts
|
|
19332
|
-
function esc$
|
|
19332
|
+
function esc$7(s) {
|
|
19333
19333
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
19334
19334
|
}
|
|
19335
19335
|
function today$2() {
|
|
@@ -19520,7 +19520,7 @@ function renderStatusBar(features) {
|
|
|
19520
19520
|
<div class="section-card">
|
|
19521
19521
|
<h3 class="section-heading">Status Breakdown</h3>
|
|
19522
19522
|
<div class="status-bar">${segments.map((s) => `<div class="status-segment" style="width:${(s.count / n * 100).toFixed(1)}%;background:${s.color}" title="${s.key}: ${s.count}"></div>`).join("")}</div>
|
|
19523
|
-
<div class="status-legend">${segments.map((s) => `<span class="status-legend-item"><span class="status-dot" style="background:${s.color}"></span>${esc$
|
|
19523
|
+
<div class="status-legend">${segments.map((s) => `<span class="status-legend-item"><span class="status-dot" style="background:${s.color}"></span>${esc$7(s.key)} <strong>${s.count}</strong></span>`).join("")}</div>
|
|
19524
19524
|
</div>`;
|
|
19525
19525
|
}
|
|
19526
19526
|
function renderDomainMiniChart(grouped) {
|
|
@@ -19534,7 +19534,7 @@ function renderDomainMiniChart(grouped) {
|
|
|
19534
19534
|
const pct = maxCount > 0 ? (fs.length / maxCount * 100).toFixed(1) : "0";
|
|
19535
19535
|
return `
|
|
19536
19536
|
<div class="domain-row">
|
|
19537
|
-
<span class="domain-name">${esc$
|
|
19537
|
+
<span class="domain-name">${esc$7(domain)}</span>
|
|
19538
19538
|
<span class="domain-count-label">${fs.length}</span>
|
|
19539
19539
|
<div class="domain-mini-bar-wrap">
|
|
19540
19540
|
<div class="domain-mini-bar" style="width:${pct}%"></div>
|
|
@@ -19554,7 +19554,7 @@ function renderFieldCoverageBars(features) {
|
|
|
19554
19554
|
const color = barColor(pct);
|
|
19555
19555
|
return `
|
|
19556
19556
|
<div class="coverage-row">
|
|
19557
|
-
<span class="coverage-label">${esc$
|
|
19557
|
+
<span class="coverage-label">${esc$7(label)}</span>
|
|
19558
19558
|
<div class="coverage-bar-wrap">
|
|
19559
19559
|
<div class="coverage-bar" style="width:${pct}%;background:${color}"></div>
|
|
19560
19560
|
</div>
|
|
@@ -19569,7 +19569,7 @@ function renderTechDebt(features) {
|
|
|
19569
19569
|
const noDomain = features.filter((f) => !f.domain);
|
|
19570
19570
|
function debtList(items, emptyText) {
|
|
19571
19571
|
if (items.length === 0) return `<div class="debt-empty">✓ ${emptyText}</div>`;
|
|
19572
|
-
return `<ul class="debt-list">${items.map((f) => `<li><code class="debt-key">${esc$
|
|
19572
|
+
return `<ul class="debt-list">${items.map((f) => `<li><code class="debt-key">${esc$7(f.featureKey)}</code> <span class="debt-title">${esc$7(f.title)}</span></li>`).join("")}</ul>`;
|
|
19573
19573
|
}
|
|
19574
19574
|
return `
|
|
19575
19575
|
<div class="section-card debt-card">
|
|
@@ -19599,7 +19599,7 @@ function generateHealth(features, projectName) {
|
|
|
19599
19599
|
<head>
|
|
19600
19600
|
<meta charset="UTF-8">
|
|
19601
19601
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19602
|
-
<title>${esc$
|
|
19602
|
+
<title>${esc$7(projectName)} — LAC Health</title>
|
|
19603
19603
|
<style>
|
|
19604
19604
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
19605
19605
|
|
|
@@ -19982,7 +19982,7 @@ body {
|
|
|
19982
19982
|
<div class="topbar">
|
|
19983
19983
|
<span class="topbar-logo">◈ lac · health</span>
|
|
19984
19984
|
<span class="topbar-sep">|</span>
|
|
19985
|
-
<span class="topbar-project">${esc$
|
|
19985
|
+
<span class="topbar-project">${esc$7(projectName)}</span>
|
|
19986
19986
|
<span class="topbar-date">${today$2()}</span>
|
|
19987
19987
|
</div>
|
|
19988
19988
|
|
|
@@ -20041,7 +20041,7 @@ body {
|
|
|
20041
20041
|
}
|
|
20042
20042
|
//#endregion
|
|
20043
20043
|
//#region src/lib/embedGenerator.ts
|
|
20044
|
-
function esc$
|
|
20044
|
+
function esc$6(s) {
|
|
20045
20045
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20046
20046
|
}
|
|
20047
20047
|
function today$1() {
|
|
@@ -20098,7 +20098,7 @@ function generateEmbed(features, projectName) {
|
|
|
20098
20098
|
const extraDomains = domains.length > MAX_DOMAINS_SHOWN ? domains.length - MAX_DOMAINS_SHOWN : 0;
|
|
20099
20099
|
const domainDots = shownDomains.map((d, i) => `<span class="domain-dot-item">
|
|
20100
20100
|
<span class="domain-dot" style="background:${domainDotColor(i)}"></span>
|
|
20101
|
-
<span class="domain-dot-name">${esc$
|
|
20101
|
+
<span class="domain-dot-name">${esc$6(d)}</span>
|
|
20102
20102
|
</span>`).join("");
|
|
20103
20103
|
const extraBadge = extraDomains > 0 ? `<span class="domain-extra">+${extraDomains} more</span>` : "";
|
|
20104
20104
|
const iframeCode = `<iframe src="lac-embed.html" width="380" height="320" frameborder="0" style="border-radius:8px;"></iframe>`;
|
|
@@ -20107,7 +20107,7 @@ function generateEmbed(features, projectName) {
|
|
|
20107
20107
|
<head>
|
|
20108
20108
|
<meta charset="UTF-8">
|
|
20109
20109
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
20110
|
-
<title>${esc$
|
|
20110
|
+
<title>${esc$6(projectName)} — LAC Embed</title>
|
|
20111
20111
|
<style>
|
|
20112
20112
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
20113
20113
|
|
|
@@ -20388,7 +20388,7 @@ html, body {
|
|
|
20388
20388
|
<div class="widget">
|
|
20389
20389
|
|
|
20390
20390
|
<div class="widget-header">
|
|
20391
|
-
<div class="widget-brand">◈ ${esc$
|
|
20391
|
+
<div class="widget-brand">◈ ${esc$6(projectName)}</div>
|
|
20392
20392
|
<div class="widget-sub">life-as-code feature map</div>
|
|
20393
20393
|
</div>
|
|
20394
20394
|
|
|
@@ -20424,7 +20424,7 @@ html, body {
|
|
|
20424
20424
|
<span class="footer-sep">·</span>
|
|
20425
20425
|
<span class="footer-item">${domainCount} domain${domainCount === 1 ? "" : "s"}</span>
|
|
20426
20426
|
<span class="footer-sep">·</span>
|
|
20427
|
-
<span class="footer-item">Updated: ${esc$
|
|
20427
|
+
<span class="footer-item">Updated: ${esc$6(updated)}</span>
|
|
20428
20428
|
</div>
|
|
20429
20429
|
|
|
20430
20430
|
<div class="embed-area">
|
|
@@ -20433,7 +20433,7 @@ html, body {
|
|
|
20433
20433
|
<span>embed code</span>
|
|
20434
20434
|
</button>
|
|
20435
20435
|
<div class="embed-code-wrap" id="embed-code-wrap">
|
|
20436
|
-
<pre class="embed-pre" id="embed-pre">${esc$
|
|
20436
|
+
<pre class="embed-pre" id="embed-pre">${esc$6(iframeCode)}</pre>
|
|
20437
20437
|
<div class="embed-copy-row">
|
|
20438
20438
|
<button class="embed-copy-btn" id="embed-copy-btn">copy</button>
|
|
20439
20439
|
</div>
|
|
@@ -20483,7 +20483,7 @@ html, body {
|
|
|
20483
20483
|
}
|
|
20484
20484
|
//#endregion
|
|
20485
20485
|
//#region src/lib/decisionLogGenerator.ts
|
|
20486
|
-
function esc$
|
|
20486
|
+
function esc$5(s) {
|
|
20487
20487
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20488
20488
|
}
|
|
20489
20489
|
function today() {
|
|
@@ -20536,7 +20536,7 @@ function mostDecidedFeature(entries) {
|
|
|
20536
20536
|
if (entries.length === 0) return "—";
|
|
20537
20537
|
let best = entries[0];
|
|
20538
20538
|
for (const e of entries) if (e.decisions.length > best.decisions.length) best = e;
|
|
20539
|
-
return `${esc$
|
|
20539
|
+
return `${esc$5(best.featureKey)}: ${best.decisions.length}`;
|
|
20540
20540
|
}
|
|
20541
20541
|
function mostCommonTagAmongDecisionHeavy(entries) {
|
|
20542
20542
|
if (entries.length === 0) return "—";
|
|
@@ -20547,7 +20547,7 @@ function mostCommonTagAmongDecisionHeavy(entries) {
|
|
|
20547
20547
|
for (const e of heavy) for (const t of e.tags) freq.set(t, (freq.get(t) ?? 0) + 1);
|
|
20548
20548
|
if (freq.size === 0) return "—";
|
|
20549
20549
|
const top = [...freq.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
20550
|
-
return top ? esc$
|
|
20550
|
+
return top ? esc$5(top[0]) : "—";
|
|
20551
20551
|
}
|
|
20552
20552
|
function renderSidebar(features) {
|
|
20553
20553
|
return `
|
|
@@ -20563,19 +20563,19 @@ function renderSidebar(features) {
|
|
|
20563
20563
|
const dimClass = hasDecisions ? "" : " nav-item-dim";
|
|
20564
20564
|
const countBadge = hasDecisions ? `<span class="nav-dec-count">${f.decisions.length}</span>` : "";
|
|
20565
20565
|
return `
|
|
20566
|
-
<a class="nav-item${dimClass}" href="#${esc$
|
|
20567
|
-
data-title="${esc$
|
|
20566
|
+
<a class="nav-item${dimClass}" href="#${esc$5(f.featureKey)}" data-key="${esc$5(f.featureKey)}"
|
|
20567
|
+
data-title="${esc$5(f.title.toLowerCase())}" data-domain="${esc$5(domain.toLowerCase())}">
|
|
20568
20568
|
<span class="nav-dot" style="background:${statusColor(f.status)};opacity:${hasDecisions ? "1" : "0.35"}"></span>
|
|
20569
|
-
<span class="nav-item-key">${esc$
|
|
20570
|
-
<span class="nav-item-title">${esc$
|
|
20569
|
+
<span class="nav-item-key">${esc$5(f.featureKey)}</span>
|
|
20570
|
+
<span class="nav-item-title">${esc$5(f.title)}</span>
|
|
20571
20571
|
${countBadge}
|
|
20572
20572
|
</a>`;
|
|
20573
20573
|
}).join("");
|
|
20574
20574
|
return `
|
|
20575
|
-
<div class="nav-group" data-domain="${esc$
|
|
20575
|
+
<div class="nav-group" data-domain="${esc$5(domain.toLowerCase())}">
|
|
20576
20576
|
<div class="nav-domain">
|
|
20577
20577
|
<span class="nav-domain-arrow">▼</span>
|
|
20578
|
-
<span class="nav-domain-name">${esc$
|
|
20578
|
+
<span class="nav-domain-name">${esc$5(domain)}</span>
|
|
20579
20579
|
<span class="nav-domain-count">${fs.length}</span>
|
|
20580
20580
|
</div>
|
|
20581
20581
|
<div class="nav-group-items">
|
|
@@ -20588,14 +20588,14 @@ function renderSidebar(features) {
|
|
|
20588
20588
|
}
|
|
20589
20589
|
function renderDecisionSection(entry) {
|
|
20590
20590
|
const decItems = entry.decisions.map((d, i) => {
|
|
20591
|
-
const rationale = d.rationale ? `<div class="dec-field"><span class="dec-field-label">Rationale</span><p class="dec-field-body">${esc$
|
|
20592
|
-
const alternativesHtml = d.alternativesConsidered && d.alternativesConsidered.length > 0 ? `<div class="dec-field"><span class="dec-field-label">Alternatives considered</span><ul class="dec-alts">${d.alternativesConsidered.map((a) => `<li>${esc$
|
|
20593
|
-
const date = d.date ? `<div class="dec-field dec-field-inline"><span class="dec-field-label">Date</span><span class="dec-field-value">${esc$
|
|
20591
|
+
const rationale = d.rationale ? `<div class="dec-field"><span class="dec-field-label">Rationale</span><p class="dec-field-body">${esc$5(d.rationale)}</p></div>` : "";
|
|
20592
|
+
const alternativesHtml = d.alternativesConsidered && d.alternativesConsidered.length > 0 ? `<div class="dec-field"><span class="dec-field-label">Alternatives considered</span><ul class="dec-alts">${d.alternativesConsidered.map((a) => `<li>${esc$5(a)}</li>`).join("")}</ul></div>` : "";
|
|
20593
|
+
const date = d.date ? `<div class="dec-field dec-field-inline"><span class="dec-field-label">Date</span><span class="dec-field-value">${esc$5(d.date)}</span></div>` : "";
|
|
20594
20594
|
return `
|
|
20595
20595
|
<div class="dec-item" data-index="${i}">
|
|
20596
20596
|
<h4 class="dec-title">
|
|
20597
20597
|
<span class="dec-num">${i + 1}</span>
|
|
20598
|
-
<span class="dec-text">${esc$
|
|
20598
|
+
<span class="dec-text">${esc$5(d.decision)}</span>
|
|
20599
20599
|
</h4>
|
|
20600
20600
|
${rationale}
|
|
20601
20601
|
${alternativesHtml}
|
|
@@ -20603,20 +20603,20 @@ function renderDecisionSection(entry) {
|
|
|
20603
20603
|
</div>`;
|
|
20604
20604
|
}).join("\n");
|
|
20605
20605
|
return `
|
|
20606
|
-
<section class="feature-section" id="${esc$
|
|
20607
|
-
data-domain="${esc$
|
|
20608
|
-
data-key="${esc$
|
|
20606
|
+
<section class="feature-section" id="${esc$5(entry.featureKey)}"
|
|
20607
|
+
data-domain="${esc$5(entry.domain.toLowerCase())}"
|
|
20608
|
+
data-key="${esc$5(entry.featureKey.toLowerCase())}"
|
|
20609
20609
|
data-date="${entry.decisions.map((d) => d.date ?? "").filter(Boolean).sort().reverse()[0] ?? ""}">
|
|
20610
20610
|
<header class="feature-section-header">
|
|
20611
20611
|
<div class="feature-section-title-row">
|
|
20612
|
-
<h2 class="feature-section-title">${esc$
|
|
20613
|
-
<code class="feature-section-key">${esc$
|
|
20612
|
+
<h2 class="feature-section-title">${esc$5(entry.title)}</h2>
|
|
20613
|
+
<code class="feature-section-key">${esc$5(entry.featureKey)}</code>
|
|
20614
20614
|
</div>
|
|
20615
20615
|
<div class="feature-section-meta">
|
|
20616
|
-
<span class="domain-chip">${esc$
|
|
20616
|
+
<span class="domain-chip">${esc$5(entry.domain)}</span>
|
|
20617
20617
|
<span class="status-badge"
|
|
20618
20618
|
style="color:${statusColor(entry.status)};background:${statusBg(entry.status)};border-color:${statusColor(entry.status)}40">
|
|
20619
|
-
${esc$
|
|
20619
|
+
${esc$5(entry.status)}
|
|
20620
20620
|
</span>
|
|
20621
20621
|
<span class="dec-count-badge">${entry.decisions.length} decision${entry.decisions.length === 1 ? "" : "s"}</span>
|
|
20622
20622
|
</div>
|
|
@@ -20657,7 +20657,7 @@ function generateDecisionLog(features, projectName) {
|
|
|
20657
20657
|
<head>
|
|
20658
20658
|
<meta charset="UTF-8">
|
|
20659
20659
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
20660
|
-
<title>${esc$
|
|
20660
|
+
<title>${esc$5(projectName)} — Decision Log</title>
|
|
20661
20661
|
<style>
|
|
20662
20662
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
20663
20663
|
|
|
@@ -21100,7 +21100,7 @@ mark {
|
|
|
21100
21100
|
<div class="topbar">
|
|
21101
21101
|
<span class="topbar-logo">◈ lac · decisions</span>
|
|
21102
21102
|
<span class="topbar-sep">|</span>
|
|
21103
|
-
<span class="topbar-project">${esc$
|
|
21103
|
+
<span class="topbar-project">${esc$5(projectName)}</span>
|
|
21104
21104
|
<span class="topbar-count">${totalDecisions} total decisions</span>
|
|
21105
21105
|
<div class="sort-controls">
|
|
21106
21106
|
<span style="font-family:var(--mono);font-size:10px;color:var(--text-soft);margin-right:4px;">sort:</span>
|
|
@@ -23026,6 +23026,34 @@ const ALL_HUB_ENTRIES = [
|
|
|
23026
23026
|
description: "Field-by-field dump of every feature.json with sidebar navigation",
|
|
23027
23027
|
icon: "🔩",
|
|
23028
23028
|
primary: false
|
|
23029
|
+
},
|
|
23030
|
+
{
|
|
23031
|
+
file: "lac-radar.html",
|
|
23032
|
+
label: "Maturity Radar",
|
|
23033
|
+
description: "SVG polar chart — 5 quality dimensions scored per domain",
|
|
23034
|
+
icon: "🎯",
|
|
23035
|
+
primary: false
|
|
23036
|
+
},
|
|
23037
|
+
{
|
|
23038
|
+
file: "lac-successboard.html",
|
|
23039
|
+
label: "Success Board",
|
|
23040
|
+
description: "successCriteria + acceptanceCriteria organized by delivery status",
|
|
23041
|
+
icon: "✅",
|
|
23042
|
+
primary: false
|
|
23043
|
+
},
|
|
23044
|
+
{
|
|
23045
|
+
file: "lac-pitch.html",
|
|
23046
|
+
label: "Pitch Deck",
|
|
23047
|
+
description: "Full-screen keyboard-navigable slide deck — present your product story",
|
|
23048
|
+
icon: "🎤",
|
|
23049
|
+
primary: true
|
|
23050
|
+
},
|
|
23051
|
+
{
|
|
23052
|
+
file: "lac-timeline.html",
|
|
23053
|
+
label: "Feature Timeline",
|
|
23054
|
+
description: "Horizontal swim-lane timeline built from statusHistory — velocity at a glance",
|
|
23055
|
+
icon: "📆",
|
|
23056
|
+
primary: false
|
|
23029
23057
|
}
|
|
23030
23058
|
];
|
|
23031
23059
|
function generateHub(projectName, stats, entries, generatedAt = (/* @__PURE__ */ new Date()).toISOString(), prefix) {
|
|
@@ -23229,6 +23257,1552 @@ function lacGo(file){location.href=location.href.replace(/[^\\/]*$/,'')+file}
|
|
|
23229
23257
|
</html>`;
|
|
23230
23258
|
}
|
|
23231
23259
|
//#endregion
|
|
23260
|
+
//#region src/lib/radarGenerator.ts
|
|
23261
|
+
function hasText$1(v, min = 10) {
|
|
23262
|
+
return typeof v === "string" && v.trim().length >= min;
|
|
23263
|
+
}
|
|
23264
|
+
function arrLen(v) {
|
|
23265
|
+
return Array.isArray(v) ? v.length : 0;
|
|
23266
|
+
}
|
|
23267
|
+
function esc$4(s) {
|
|
23268
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
23269
|
+
}
|
|
23270
|
+
const METRICS = [
|
|
23271
|
+
{
|
|
23272
|
+
key: "docs",
|
|
23273
|
+
label: "Documentation",
|
|
23274
|
+
color: "#5b82cc",
|
|
23275
|
+
desc: "Avg fill of problem, analysis, implementation",
|
|
23276
|
+
score(fs) {
|
|
23277
|
+
if (!fs.length) return 0;
|
|
23278
|
+
return fs.reduce((s, f) => {
|
|
23279
|
+
let n = 0;
|
|
23280
|
+
if (hasText$1(f["problem"])) n++;
|
|
23281
|
+
if (hasText$1(f["analysis"])) n++;
|
|
23282
|
+
if (hasText$1(f["implementation"])) n++;
|
|
23283
|
+
return s + n / 3;
|
|
23284
|
+
}, 0) / fs.length;
|
|
23285
|
+
}
|
|
23286
|
+
},
|
|
23287
|
+
{
|
|
23288
|
+
key: "decisions",
|
|
23289
|
+
label: "Decision Quality",
|
|
23290
|
+
color: "#9b7ecc",
|
|
23291
|
+
desc: "% features with 2+ documented decisions",
|
|
23292
|
+
score(fs) {
|
|
23293
|
+
if (!fs.length) return 0;
|
|
23294
|
+
return fs.filter((f) => arrLen(f["decisions"]) >= 2).length / fs.length;
|
|
23295
|
+
}
|
|
23296
|
+
},
|
|
23297
|
+
{
|
|
23298
|
+
key: "guide",
|
|
23299
|
+
label: "User Guide",
|
|
23300
|
+
color: "#4aad72",
|
|
23301
|
+
desc: "% features with userGuide written",
|
|
23302
|
+
score(fs) {
|
|
23303
|
+
if (!fs.length) return 0;
|
|
23304
|
+
return fs.filter((f) => hasText$1(f["userGuide"], 1)).length / fs.length;
|
|
23305
|
+
}
|
|
23306
|
+
},
|
|
23307
|
+
{
|
|
23308
|
+
key: "code",
|
|
23309
|
+
label: "Code Reference",
|
|
23310
|
+
color: "#d4a853",
|
|
23311
|
+
desc: "% features with componentFile linked",
|
|
23312
|
+
score(fs) {
|
|
23313
|
+
if (!fs.length) return 0;
|
|
23314
|
+
return fs.filter((f) => hasText$1(f["componentFile"], 1)).length / fs.length;
|
|
23315
|
+
}
|
|
23316
|
+
},
|
|
23317
|
+
{
|
|
23318
|
+
key: "shipped",
|
|
23319
|
+
label: "Ship Rate",
|
|
23320
|
+
color: "#e07b54",
|
|
23321
|
+
desc: "% features frozen / shipped",
|
|
23322
|
+
score(fs) {
|
|
23323
|
+
if (!fs.length) return 0;
|
|
23324
|
+
return fs.filter((f) => f["status"] === "frozen").length / fs.length;
|
|
23325
|
+
}
|
|
23326
|
+
}
|
|
23327
|
+
];
|
|
23328
|
+
function generateRadar(features, projectName) {
|
|
23329
|
+
const domains = [...new Set(features.map((f) => f["domain"] || "misc"))].sort();
|
|
23330
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
23331
|
+
for (const f of features) {
|
|
23332
|
+
const d = f["domain"] || "misc";
|
|
23333
|
+
if (!byDomain.has(d)) byDomain.set(d, []);
|
|
23334
|
+
byDomain.get(d).push(f);
|
|
23335
|
+
}
|
|
23336
|
+
const N = domains.length;
|
|
23337
|
+
const CX = 240, CY = 220, R = 170;
|
|
23338
|
+
const PAD_LABEL = 28;
|
|
23339
|
+
const scores = METRICS.map((m) => domains.map((d) => m.score(byDomain.get(d) ?? [])));
|
|
23340
|
+
function polar(i, r) {
|
|
23341
|
+
const angle = i / N * 2 * Math.PI - Math.PI / 2;
|
|
23342
|
+
return {
|
|
23343
|
+
x: CX + r * Math.cos(angle),
|
|
23344
|
+
y: CY + r * Math.sin(angle)
|
|
23345
|
+
};
|
|
23346
|
+
}
|
|
23347
|
+
function polygonPts(scoreRow) {
|
|
23348
|
+
return scoreRow.map((s, i) => {
|
|
23349
|
+
const p = polar(i, s * R);
|
|
23350
|
+
return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
|
|
23351
|
+
}).join(" ");
|
|
23352
|
+
}
|
|
23353
|
+
const rings = [
|
|
23354
|
+
.25,
|
|
23355
|
+
.5,
|
|
23356
|
+
.75,
|
|
23357
|
+
1
|
|
23358
|
+
].map((pct) => {
|
|
23359
|
+
return `<polygon points="${domains.map((_, i) => {
|
|
23360
|
+
const p = polar(i, pct * R);
|
|
23361
|
+
return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
|
|
23362
|
+
}).join(" ")}" fill="none" stroke="#2a2724" stroke-width="${pct === 1 ? 1.5 : .8}"/>`;
|
|
23363
|
+
}).join("\n");
|
|
23364
|
+
const spokes = domains.map((_, i) => {
|
|
23365
|
+
const p = polar(i, R);
|
|
23366
|
+
return `<line x1="${CX}" y1="${CY}" x2="${p.x.toFixed(1)}" y2="${p.y.toFixed(1)}" stroke="#2a2724" stroke-width="0.8"/>`;
|
|
23367
|
+
}).join("\n");
|
|
23368
|
+
const labels = domains.map((d, i) => {
|
|
23369
|
+
const p = polar(i, R + PAD_LABEL);
|
|
23370
|
+
const anchor = p.x < CX - 5 ? "end" : p.x > CX + 5 ? "start" : "middle";
|
|
23371
|
+
const label = d.replace(/-/g, "‑");
|
|
23372
|
+
return `<text x="${p.x.toFixed(1)}" y="${(p.y + 4).toFixed(1)}" text-anchor="${anchor}" class="domain-label">${esc$4(label)}</text>`;
|
|
23373
|
+
}).join("\n");
|
|
23374
|
+
const metricPolygons = METRICS.map((m, mi) => {
|
|
23375
|
+
const pts = polygonPts(scores[mi]);
|
|
23376
|
+
return `<polygon id="poly-${m.key}" points="${pts}" fill="${m.color}" fill-opacity="0.12" stroke="${m.color}" stroke-width="1.8" stroke-linejoin="round" class="metric-poly" data-metric="${m.key}"/>`;
|
|
23377
|
+
}).join("\n");
|
|
23378
|
+
const ringLabels = [
|
|
23379
|
+
25,
|
|
23380
|
+
50,
|
|
23381
|
+
75,
|
|
23382
|
+
100
|
|
23383
|
+
].map((pct) => {
|
|
23384
|
+
const p = polar(0, pct / 100 * R);
|
|
23385
|
+
return `<text x="${(CX - 6).toFixed(1)}" y="${(p.y + 3).toFixed(1)}" text-anchor="end" class="ring-label">${pct}%</text>`;
|
|
23386
|
+
}).join("\n");
|
|
23387
|
+
const tableRows = domains.map((d, di) => {
|
|
23388
|
+
const domFeats = byDomain.get(d) ?? [];
|
|
23389
|
+
const cells = METRICS.map((m, mi) => {
|
|
23390
|
+
const pct = Math.round(scores[mi][di] * 100);
|
|
23391
|
+
return `<td style="color:${pct >= 70 ? "#4aad72" : pct >= 40 ? "#c4a255" : "#cc5b5b"};font-family:var(--mono);text-align:center">${pct}%</td>`;
|
|
23392
|
+
});
|
|
23393
|
+
return `<tr>
|
|
23394
|
+
<td><strong>${esc$4(d)}</strong></td>
|
|
23395
|
+
<td style="color:var(--text-soft);text-align:center">${domFeats.length}</td>
|
|
23396
|
+
${cells.join("")}
|
|
23397
|
+
</tr>`;
|
|
23398
|
+
}).join("\n");
|
|
23399
|
+
const compositeByDomain = domains.map((_, di) => Math.round(METRICS.reduce((s, _, mi) => s + scores[mi][di], 0) / METRICS.length * 100));
|
|
23400
|
+
const dataJson = JSON.stringify(domains.map((d, di) => ({
|
|
23401
|
+
domain: d,
|
|
23402
|
+
count: (byDomain.get(d) ?? []).length,
|
|
23403
|
+
composite: compositeByDomain[di],
|
|
23404
|
+
scores: Object.fromEntries(METRICS.map((m, mi) => [m.key, Math.round(scores[mi][di] * 100)]))
|
|
23405
|
+
}))).replace(/<\/script>/gi, "<\\/script>");
|
|
23406
|
+
const svgWidth = 480, svgHeight = 440;
|
|
23407
|
+
return `<!DOCTYPE html>
|
|
23408
|
+
<html lang="en">
|
|
23409
|
+
<head>
|
|
23410
|
+
<meta charset="UTF-8">
|
|
23411
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
23412
|
+
<title>${esc$4(projectName)} — Domain Maturity Radar</title>
|
|
23413
|
+
<style>
|
|
23414
|
+
:root {
|
|
23415
|
+
--bg: #12100e;
|
|
23416
|
+
--bg-card: #1a1714;
|
|
23417
|
+
--bg-hover: #201d1a;
|
|
23418
|
+
--border: #2a2724;
|
|
23419
|
+
--border-soft: #221f1c;
|
|
23420
|
+
--text: #e8e0d4;
|
|
23421
|
+
--text-soft: #8a7f74;
|
|
23422
|
+
--accent: #d4a853;
|
|
23423
|
+
--mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
23424
|
+
}
|
|
23425
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
23426
|
+
html, body { height: 100%; background: var(--bg); color: var(--text); font-family: system-ui, -apple-system, sans-serif; }
|
|
23427
|
+
body { display: flex; flex-direction: column; min-height: 100vh; }
|
|
23428
|
+
|
|
23429
|
+
.topbar {
|
|
23430
|
+
display: flex; align-items: center; gap: 10px; padding: 0 20px; height: 48px;
|
|
23431
|
+
background: #0e0c0a; border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
23432
|
+
font-size: 13px;
|
|
23433
|
+
}
|
|
23434
|
+
.topbar-logo { color: var(--accent); font-weight: 700; font-family: var(--mono); }
|
|
23435
|
+
.topbar-sep { color: var(--border); }
|
|
23436
|
+
.topbar-project { color: var(--text); }
|
|
23437
|
+
.topbar-count { margin-left: auto; color: var(--text-soft); font-size: 12px; font-family: var(--mono); }
|
|
23438
|
+
|
|
23439
|
+
.main { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 32px 24px; gap: 32px; }
|
|
23440
|
+
|
|
23441
|
+
h1 { font-size: 20px; font-weight: 600; color: var(--text); }
|
|
23442
|
+
.subtitle { font-size: 13px; color: var(--text-soft); margin-top: 4px; text-align: center; }
|
|
23443
|
+
|
|
23444
|
+
.radar-wrap {
|
|
23445
|
+
display: flex; gap: 40px; align-items: flex-start; flex-wrap: wrap; justify-content: center;
|
|
23446
|
+
}
|
|
23447
|
+
|
|
23448
|
+
svg.radar { overflow: visible; }
|
|
23449
|
+
.domain-label { font-size: 11px; fill: var(--text); font-family: var(--mono); }
|
|
23450
|
+
.ring-label { font-size: 9px; fill: var(--text-soft); font-family: var(--mono); }
|
|
23451
|
+
.metric-poly { cursor: pointer; transition: fill-opacity 0.2s, stroke-width 0.2s; }
|
|
23452
|
+
.metric-poly:hover { fill-opacity: 0.35; stroke-width: 2.5; }
|
|
23453
|
+
.metric-poly.dimmed { fill-opacity: 0.04; stroke-opacity: 0.25; }
|
|
23454
|
+
|
|
23455
|
+
.legend {
|
|
23456
|
+
display: flex; flex-direction: column; gap: 10px; padding: 20px;
|
|
23457
|
+
background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
|
|
23458
|
+
min-width: 200px;
|
|
23459
|
+
}
|
|
23460
|
+
.legend-title { font-size: 11px; color: var(--text-soft); font-family: var(--mono); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 4px; }
|
|
23461
|
+
.legend-item {
|
|
23462
|
+
display: flex; align-items: flex-start; gap: 10px; cursor: pointer;
|
|
23463
|
+
padding: 6px 8px; border-radius: 4px; transition: background 0.15s;
|
|
23464
|
+
}
|
|
23465
|
+
.legend-item:hover { background: var(--bg-hover); }
|
|
23466
|
+
.legend-item.dimmed { opacity: 0.35; }
|
|
23467
|
+
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; margin-top: 2px; }
|
|
23468
|
+
.legend-text { display: flex; flex-direction: column; }
|
|
23469
|
+
.legend-label { font-size: 13px; font-weight: 500; color: var(--text); }
|
|
23470
|
+
.legend-desc { font-size: 11px; color: var(--text-soft); margin-top: 1px; }
|
|
23471
|
+
|
|
23472
|
+
.table-wrap {
|
|
23473
|
+
width: 100%; max-width: 760px;
|
|
23474
|
+
background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
|
|
23475
|
+
overflow: hidden;
|
|
23476
|
+
}
|
|
23477
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
23478
|
+
th {
|
|
23479
|
+
padding: 10px 14px; text-align: left; font-weight: 500; font-size: 11px;
|
|
23480
|
+
color: var(--text-soft); border-bottom: 1px solid var(--border);
|
|
23481
|
+
text-transform: uppercase; letter-spacing: .06em; font-family: var(--mono);
|
|
23482
|
+
}
|
|
23483
|
+
th.metric-header { cursor: pointer; }
|
|
23484
|
+
th.metric-header:hover { color: var(--text); }
|
|
23485
|
+
td { padding: 10px 14px; border-bottom: 1px solid var(--border-soft); }
|
|
23486
|
+
tr:last-child td { border-bottom: none; }
|
|
23487
|
+
tr:hover td { background: var(--bg-hover); }
|
|
23488
|
+
|
|
23489
|
+
.tooltip {
|
|
23490
|
+
position: fixed; pointer-events: none; z-index: 999;
|
|
23491
|
+
background: #1e1b18; border: 1px solid var(--border); border-radius: 6px;
|
|
23492
|
+
padding: 10px 14px; font-size: 12px; color: var(--text); max-width: 220px;
|
|
23493
|
+
display: none; box-shadow: 0 8px 24px rgba(0,0,0,.5);
|
|
23494
|
+
}
|
|
23495
|
+
.tooltip.visible { display: block; }
|
|
23496
|
+
.tooltip-domain { font-weight: 600; margin-bottom: 6px; color: var(--accent); font-family: var(--mono); font-size: 11px; }
|
|
23497
|
+
.tooltip-row { display: flex; justify-content: space-between; gap: 16px; margin: 2px 0; }
|
|
23498
|
+
.tooltip-label { color: var(--text-soft); }
|
|
23499
|
+
.tooltip-val { font-family: var(--mono); }
|
|
23500
|
+
</style>
|
|
23501
|
+
</head>
|
|
23502
|
+
<body>
|
|
23503
|
+
<div class="topbar">
|
|
23504
|
+
<span class="topbar-logo">◈ lac</span>
|
|
23505
|
+
<span class="topbar-sep">|</span>
|
|
23506
|
+
<span class="topbar-project">${esc$4(projectName)}</span>
|
|
23507
|
+
<span class="topbar-count">${features.length} features · ${domains.length} domains · Domain Maturity Radar</span>
|
|
23508
|
+
</div>
|
|
23509
|
+
|
|
23510
|
+
<div class="main">
|
|
23511
|
+
<div style="text-align:center">
|
|
23512
|
+
<h1>Domain Maturity Radar</h1>
|
|
23513
|
+
<p class="subtitle">5 quality dimensions scored per domain — hover a legend item to isolate a metric</p>
|
|
23514
|
+
</div>
|
|
23515
|
+
|
|
23516
|
+
<div class="radar-wrap">
|
|
23517
|
+
<svg class="radar" width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">
|
|
23518
|
+
<!-- Guide rings -->
|
|
23519
|
+
${rings}
|
|
23520
|
+
<!-- Spokes -->
|
|
23521
|
+
${spokes}
|
|
23522
|
+
<!-- Ring labels -->
|
|
23523
|
+
${ringLabels}
|
|
23524
|
+
<!-- Metric polygons -->
|
|
23525
|
+
${metricPolygons}
|
|
23526
|
+
<!-- Domain labels -->
|
|
23527
|
+
${labels}
|
|
23528
|
+
<!-- Center dot -->
|
|
23529
|
+
<circle cx="${CX}" cy="${CY}" r="3" fill="var(--border)"/>
|
|
23530
|
+
</svg>
|
|
23531
|
+
|
|
23532
|
+
<div class="legend">
|
|
23533
|
+
<div class="legend-title">Metric</div>
|
|
23534
|
+
${METRICS.map((m) => `
|
|
23535
|
+
<div class="legend-item" data-metric="${m.key}" onclick="toggleMetric('${m.key}')">
|
|
23536
|
+
<span class="legend-dot" style="background:${m.color}"></span>
|
|
23537
|
+
<span class="legend-text">
|
|
23538
|
+
<span class="legend-label">${esc$4(m.label)}</span>
|
|
23539
|
+
<span class="legend-desc">${esc$4(m.desc)}</span>
|
|
23540
|
+
</span>
|
|
23541
|
+
</div>`).join("")}
|
|
23542
|
+
</div>
|
|
23543
|
+
</div>
|
|
23544
|
+
|
|
23545
|
+
<div class="table-wrap">
|
|
23546
|
+
<table>
|
|
23547
|
+
<thead>
|
|
23548
|
+
<tr>
|
|
23549
|
+
<th>Domain</th>
|
|
23550
|
+
<th style="text-align:center">Features</th>
|
|
23551
|
+
${METRICS.map((m) => `<th class="metric-header" style="text-align:center;color:${m.color}" title="${esc$4(m.desc)}">${esc$4(m.label)}</th>`).join("")}
|
|
23552
|
+
</tr>
|
|
23553
|
+
</thead>
|
|
23554
|
+
<tbody id="table-body">
|
|
23555
|
+
${tableRows}
|
|
23556
|
+
</tbody>
|
|
23557
|
+
</table>
|
|
23558
|
+
</div>
|
|
23559
|
+
</div>
|
|
23560
|
+
|
|
23561
|
+
<div class="tooltip" id="tooltip"></div>
|
|
23562
|
+
|
|
23563
|
+
<script>
|
|
23564
|
+
const DATA = ${dataJson};
|
|
23565
|
+
const byDomain = new Map(DATA.map(d => [d.domain, d]));
|
|
23566
|
+
const METRIC_COLORS = {${METRICS.map((m) => `'${m.key}': '${m.color}'`).join(", ")}};
|
|
23567
|
+
|
|
23568
|
+
let activeMetrics = new Set(${JSON.stringify(METRICS.map((m) => m.key))});
|
|
23569
|
+
|
|
23570
|
+
function toggleMetric(key) {
|
|
23571
|
+
if (activeMetrics.size === ${METRICS.length} && activeMetrics.has(key)) {
|
|
23572
|
+
// Solo this metric
|
|
23573
|
+
activeMetrics.clear();
|
|
23574
|
+
activeMetrics.add(key);
|
|
23575
|
+
} else if (activeMetrics.size === 1 && activeMetrics.has(key)) {
|
|
23576
|
+
// Re-activate all
|
|
23577
|
+
${JSON.stringify(METRICS.map((m) => m.key))}.forEach(k => activeMetrics.add(k));
|
|
23578
|
+
} else {
|
|
23579
|
+
if (activeMetrics.has(key)) activeMetrics.delete(key);
|
|
23580
|
+
else activeMetrics.add(key);
|
|
23581
|
+
}
|
|
23582
|
+
updateVisibility();
|
|
23583
|
+
}
|
|
23584
|
+
|
|
23585
|
+
function updateVisibility() {
|
|
23586
|
+
document.querySelectorAll('.metric-poly').forEach(el => {
|
|
23587
|
+
const k = el.dataset.metric;
|
|
23588
|
+
el.classList.toggle('dimmed', !activeMetrics.has(k));
|
|
23589
|
+
});
|
|
23590
|
+
document.querySelectorAll('.legend-item').forEach(el => {
|
|
23591
|
+
const k = el.dataset.metric;
|
|
23592
|
+
el.classList.toggle('dimmed', !activeMetrics.has(k));
|
|
23593
|
+
});
|
|
23594
|
+
}
|
|
23595
|
+
|
|
23596
|
+
// Domain hover tooltip via SVG polygon hit-test approximation
|
|
23597
|
+
// Attach mousemove to SVG, find nearest domain spoke
|
|
23598
|
+
const svg = document.querySelector('svg.radar');
|
|
23599
|
+
const tooltip = document.getElementById('tooltip');
|
|
23600
|
+
const CX = ${CX}, CY = ${CY};
|
|
23601
|
+
const domainAngles = ${JSON.stringify(domains.map((_, i) => i / N * 360 - 90))};
|
|
23602
|
+
const domainNames = ${JSON.stringify(domains)};
|
|
23603
|
+
|
|
23604
|
+
svg.addEventListener('mousemove', e => {
|
|
23605
|
+
const rect = svg.getBoundingClientRect();
|
|
23606
|
+
const x = (e.clientX - rect.left) * (${svgWidth} / rect.width) - CX;
|
|
23607
|
+
const y = (e.clientY - rect.top) * (${svgHeight} / rect.height) - CY;
|
|
23608
|
+
const dist = Math.sqrt(x*x + y*y);
|
|
23609
|
+
if (dist < 15 || dist > ${R + 40}) { tooltip.classList.remove('visible'); return; }
|
|
23610
|
+
|
|
23611
|
+
let angle = Math.atan2(y, x) * 180 / Math.PI + 90;
|
|
23612
|
+
if (angle < 0) angle += 360;
|
|
23613
|
+
|
|
23614
|
+
// Find nearest domain spoke
|
|
23615
|
+
const step = 360 / domainNames.length;
|
|
23616
|
+
const idx = Math.round(angle / step) % domainNames.length;
|
|
23617
|
+
const domain = domainNames[idx];
|
|
23618
|
+
const d = byDomain.get(domain);
|
|
23619
|
+
if (!d) { tooltip.classList.remove('visible'); return; }
|
|
23620
|
+
|
|
23621
|
+
const metricsHtml = Object.entries(d.scores).map(([k, v]) => {
|
|
23622
|
+
const color = v >= 70 ? '#4aad72' : v >= 40 ? '#c4a255' : '#cc5b5b';
|
|
23623
|
+
return '<div class="tooltip-row"><span class="tooltip-label">' + k + '</span><span class="tooltip-val" style="color:' + color + '">' + v + '%</span></div>';
|
|
23624
|
+
}).join('');
|
|
23625
|
+
|
|
23626
|
+
tooltip.innerHTML = '<div class="tooltip-domain">' + domain + '</div>' +
|
|
23627
|
+
'<div class="tooltip-row"><span class="tooltip-label">features</span><span class="tooltip-val">' + d.count + '</span></div>' +
|
|
23628
|
+
metricsHtml;
|
|
23629
|
+
tooltip.style.left = (e.clientX + 14) + 'px';
|
|
23630
|
+
tooltip.style.top = (e.clientY - 10) + 'px';
|
|
23631
|
+
tooltip.classList.add('visible');
|
|
23632
|
+
});
|
|
23633
|
+
|
|
23634
|
+
svg.addEventListener('mouseleave', () => tooltip.classList.remove('visible'));
|
|
23635
|
+
<\/script>
|
|
23636
|
+
</body>
|
|
23637
|
+
</html>`;
|
|
23638
|
+
}
|
|
23639
|
+
//#endregion
|
|
23640
|
+
//#region src/lib/successboardGenerator.ts
|
|
23641
|
+
function esc$3(s) {
|
|
23642
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
23643
|
+
}
|
|
23644
|
+
function first(s, chars = 160) {
|
|
23645
|
+
const str = typeof s === "string" ? s.trim() : "";
|
|
23646
|
+
return str.length > chars ? str.slice(0, chars - 1) + "…" : str;
|
|
23647
|
+
}
|
|
23648
|
+
const STATUS_COLOR$2 = {
|
|
23649
|
+
frozen: "#5b82cc",
|
|
23650
|
+
active: "#4aad72",
|
|
23651
|
+
draft: "#c4a255",
|
|
23652
|
+
deprecated: "#664444"
|
|
23653
|
+
};
|
|
23654
|
+
const STATUS_LABEL = {
|
|
23655
|
+
frozen: "🔒 Achieved",
|
|
23656
|
+
active: "🟡 In Progress",
|
|
23657
|
+
draft: "⚪ Planned",
|
|
23658
|
+
deprecated: "❌ Deprecated"
|
|
23659
|
+
};
|
|
23660
|
+
const COLUMN_ORDER = [
|
|
23661
|
+
"frozen",
|
|
23662
|
+
"active",
|
|
23663
|
+
"draft"
|
|
23664
|
+
];
|
|
23665
|
+
function generateSuccessboard(features, projectName) {
|
|
23666
|
+
const hasCriteria = (f) => typeof f["successCriteria"] === "string" && f["successCriteria"].trim().length > 0 || Array.isArray(f["acceptanceCriteria"]) && f["acceptanceCriteria"].length > 0;
|
|
23667
|
+
const withCriteria = features.filter(hasCriteria);
|
|
23668
|
+
const withoutCriteria = features.filter((f) => !hasCriteria(f));
|
|
23669
|
+
const domains = [...new Set(features.map((f) => f["domain"] || "misc"))].sort();
|
|
23670
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
23671
|
+
for (const status of COLUMN_ORDER) byStatus.set(status, []);
|
|
23672
|
+
for (const f of withCriteria) {
|
|
23673
|
+
const s = f["status"] || "draft";
|
|
23674
|
+
if (!byStatus.has(s)) byStatus.set(s, []);
|
|
23675
|
+
byStatus.get(s).push(f);
|
|
23676
|
+
}
|
|
23677
|
+
const frozenCount = features.filter((f) => f["status"] === "frozen").length;
|
|
23678
|
+
const criteriaPct = features.length ? Math.round(withCriteria.length / features.length * 100) : 0;
|
|
23679
|
+
const frozenWithCriteria = withCriteria.filter((f) => f["status"] === "frozen").length;
|
|
23680
|
+
function renderAC(f) {
|
|
23681
|
+
const ac = f["acceptanceCriteria"];
|
|
23682
|
+
if (!Array.isArray(ac) || ac.length === 0) return "";
|
|
23683
|
+
const isFrozen = f["status"] === "frozen";
|
|
23684
|
+
return `<ul class="ac-list">` + ac.map((item) => {
|
|
23685
|
+
return `<li class="ac-item${isFrozen ? " ac-done" : ""}">
|
|
23686
|
+
<span class="ac-check">${isFrozen ? "✓" : "○"}</span>
|
|
23687
|
+
<span class="ac-text">${esc$3(typeof item === "string" ? item : String(item))}</span>
|
|
23688
|
+
</li>`;
|
|
23689
|
+
}).join("") + `</ul>`;
|
|
23690
|
+
}
|
|
23691
|
+
function renderCard(f) {
|
|
23692
|
+
const status = f["status"] || "draft";
|
|
23693
|
+
const sc = typeof f["successCriteria"] === "string" ? f["successCriteria"].trim() : "";
|
|
23694
|
+
const domain = f["domain"] || "";
|
|
23695
|
+
const priority = f["priority"] != null ? `P${f["priority"]}` : "";
|
|
23696
|
+
const acHtml = renderAC(f);
|
|
23697
|
+
const key = f["featureKey"] || "";
|
|
23698
|
+
return `<div class="card" data-domain="${esc$3(domain)}" data-status="${esc$3(status)}" onclick="window.open('lac-wiki.html#${esc$3(key)}','_self')">
|
|
23699
|
+
<div class="card-header">
|
|
23700
|
+
${domain ? `<span class="badge badge-domain">${esc$3(domain)}</span>` : ""}
|
|
23701
|
+
${priority ? `<span class="badge badge-priority">${esc$3(priority)}</span>` : ""}
|
|
23702
|
+
</div>
|
|
23703
|
+
<div class="card-title">${esc$3(first(f["title"], 80))}</div>
|
|
23704
|
+
${sc ? `<blockquote class="card-sc">${esc$3(first(sc, 200))}</blockquote>` : ""}
|
|
23705
|
+
${acHtml}
|
|
23706
|
+
<div class="card-key">${esc$3(key)}</div>
|
|
23707
|
+
</div>`;
|
|
23708
|
+
}
|
|
23709
|
+
const columnHtml = COLUMN_ORDER.map((status) => {
|
|
23710
|
+
const colFeatures = (byStatus.get(status) ?? []).sort((a, b) => (a["priority"] ?? 99) - (b["priority"] ?? 99));
|
|
23711
|
+
return `<div class="column">
|
|
23712
|
+
<div class="col-header" style="border-left:3px solid ${STATUS_COLOR$2[status] ?? "#888"}">
|
|
23713
|
+
<span class="col-title">${STATUS_LABEL[status] ?? status}</span>
|
|
23714
|
+
<span class="col-count">${colFeatures.length}</span>
|
|
23715
|
+
</div>
|
|
23716
|
+
<div class="col-cards" id="col-${status}">
|
|
23717
|
+
${colFeatures.length === 0 ? `<div class="col-empty">No features yet</div>` : colFeatures.map(renderCard).join("\n")}
|
|
23718
|
+
</div>
|
|
23719
|
+
</div>`;
|
|
23720
|
+
}).join("\n");
|
|
23721
|
+
const domainPills = domains.map((d) => {
|
|
23722
|
+
const count = withCriteria.filter((f) => f["domain"] === d).length;
|
|
23723
|
+
return `<span class="domain-pill" data-domain="${esc$3(d)}" onclick="toggleDomain('${esc$3(d)}')">${esc$3(d)} <span class="pill-count">${count}</span></span>`;
|
|
23724
|
+
}).join("");
|
|
23725
|
+
const missingCards = withoutCriteria.slice(0, 12).map((f) => {
|
|
23726
|
+
const status = f["status"] || "draft";
|
|
23727
|
+
return `<div class="missing-card" onclick="window.open('lac-wiki.html#${esc$3(f["featureKey"] || "")}','_self')">
|
|
23728
|
+
<span class="missing-status" style="color:${STATUS_COLOR$2[status] ?? "#888"}">●</span>
|
|
23729
|
+
<span class="missing-title">${esc$3(first(f["title"], 60))}</span>
|
|
23730
|
+
<span class="missing-domain">${esc$3(f["domain"] || "")}</span>
|
|
23731
|
+
<span class="missing-hint">+ add successCriteria</span>
|
|
23732
|
+
</div>`;
|
|
23733
|
+
}).join("");
|
|
23734
|
+
const dataJson = JSON.stringify({
|
|
23735
|
+
total: features.length,
|
|
23736
|
+
withCriteria: withCriteria.length,
|
|
23737
|
+
without: withoutCriteria.length,
|
|
23738
|
+
frozenWithCriteria,
|
|
23739
|
+
criteriaPct
|
|
23740
|
+
}).replace(/<\/script>/gi, "<\\/script>");
|
|
23741
|
+
return `<!DOCTYPE html>
|
|
23742
|
+
<html lang="en">
|
|
23743
|
+
<head>
|
|
23744
|
+
<meta charset="UTF-8">
|
|
23745
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
23746
|
+
<title>${esc$3(projectName)} — Success Board</title>
|
|
23747
|
+
<style>
|
|
23748
|
+
:root {
|
|
23749
|
+
--bg: #12100e; --bg-card: #1a1714; --bg-hover: #201d1a;
|
|
23750
|
+
--border: #2a2724; --border-soft: #221f1c;
|
|
23751
|
+
--text: #e8e0d4; --text-soft: #8a7f74; --accent: #d4a853;
|
|
23752
|
+
--mono: 'SF Mono','Fira Code','Cascadia Code',monospace;
|
|
23753
|
+
}
|
|
23754
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
23755
|
+
html, body { height: 100%; background: var(--bg); color: var(--text); font-family: system-ui,-apple-system,sans-serif; }
|
|
23756
|
+
|
|
23757
|
+
.topbar {
|
|
23758
|
+
display: flex; align-items: center; gap: 10px; padding: 0 20px; height: 48px;
|
|
23759
|
+
background: #0e0c0a; border-bottom: 1px solid var(--border); flex-shrink: 0; font-size: 13px;
|
|
23760
|
+
}
|
|
23761
|
+
.topbar-logo { color: var(--accent); font-weight: 700; font-family: var(--mono); }
|
|
23762
|
+
.topbar-sep { color: var(--border); }
|
|
23763
|
+
.topbar-count { margin-left: auto; color: var(--text-soft); font-size: 12px; font-family: var(--mono); }
|
|
23764
|
+
|
|
23765
|
+
.hero {
|
|
23766
|
+
padding: 28px 24px 0;
|
|
23767
|
+
display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 20px;
|
|
23768
|
+
}
|
|
23769
|
+
.hero-text h1 { font-size: 22px; font-weight: 600; }
|
|
23770
|
+
.hero-text p { font-size: 13px; color: var(--text-soft); margin-top: 4px; }
|
|
23771
|
+
|
|
23772
|
+
.stats-row { display: flex; gap: 16px; flex-wrap: wrap; }
|
|
23773
|
+
.stat-card {
|
|
23774
|
+
background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
|
|
23775
|
+
padding: 14px 18px; text-align: center; min-width: 90px;
|
|
23776
|
+
}
|
|
23777
|
+
.stat-num { font-size: 28px; font-weight: 700; font-family: var(--mono); color: var(--accent); }
|
|
23778
|
+
.stat-label { font-size: 11px; color: var(--text-soft); margin-top: 2px; }
|
|
23779
|
+
|
|
23780
|
+
.progress-bar-wrap { padding: 16px 24px 0; }
|
|
23781
|
+
.progress-bar-label { font-size: 12px; color: var(--text-soft); margin-bottom: 6px; font-family: var(--mono); }
|
|
23782
|
+
.progress-bar { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
|
|
23783
|
+
.progress-fill { height: 100%; background: linear-gradient(90deg, #5b82cc, #4aad72); border-radius: 3px; transition: width .6s ease; }
|
|
23784
|
+
|
|
23785
|
+
.filters { padding: 16px 24px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; border-bottom: 1px solid var(--border); }
|
|
23786
|
+
.filter-label { font-size: 11px; color: var(--text-soft); font-family: var(--mono); }
|
|
23787
|
+
.domain-pill {
|
|
23788
|
+
font-size: 11px; padding: 4px 10px; border-radius: 12px; cursor: pointer;
|
|
23789
|
+
background: var(--bg-card); border: 1px solid var(--border); color: var(--text-soft);
|
|
23790
|
+
transition: all .15s; user-select: none;
|
|
23791
|
+
}
|
|
23792
|
+
.domain-pill:hover { border-color: var(--accent); color: var(--text); }
|
|
23793
|
+
.domain-pill.active { background: var(--accent); color: #12100e; border-color: var(--accent); font-weight: 600; }
|
|
23794
|
+
.pill-count { opacity: .6; }
|
|
23795
|
+
|
|
23796
|
+
.board { display: flex; gap: 0; flex: 1; overflow: hidden; height: calc(100vh - 260px); min-height: 400px; }
|
|
23797
|
+
.column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 260px; }
|
|
23798
|
+
.column:last-child { border-right: none; }
|
|
23799
|
+
.col-header { padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; background: var(--bg-card); border-bottom: 1px solid var(--border); flex-shrink: 0; margin: 0; padding-left: 13px; }
|
|
23800
|
+
.col-title { font-size: 13px; font-weight: 600; }
|
|
23801
|
+
.col-count { font-size: 12px; color: var(--text-soft); background: var(--border); border-radius: 10px; padding: 1px 8px; font-family: var(--mono); }
|
|
23802
|
+
.col-cards { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }
|
|
23803
|
+
.col-empty { padding: 24px 0; text-align: center; color: var(--text-soft); font-size: 13px; font-style: italic; }
|
|
23804
|
+
|
|
23805
|
+
.card {
|
|
23806
|
+
background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
|
|
23807
|
+
padding: 14px 14px 12px; cursor: pointer; transition: border-color .15s, background .15s;
|
|
23808
|
+
}
|
|
23809
|
+
.card:hover { border-color: var(--accent); background: var(--bg-hover); }
|
|
23810
|
+
.card-header { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
23811
|
+
.badge { font-size: 10px; padding: 2px 8px; border-radius: 10px; font-family: var(--mono); }
|
|
23812
|
+
.badge-domain { background: #2a2724; color: var(--text-soft); border: 1px solid var(--border); }
|
|
23813
|
+
.badge-priority { background: #1e1b12; color: var(--accent); border: 1px solid #3a2f10; }
|
|
23814
|
+
.card-title { font-size: 14px; font-weight: 600; line-height: 1.35; margin-bottom: 8px; }
|
|
23815
|
+
.card-sc {
|
|
23816
|
+
font-size: 12px; color: var(--text-soft); font-style: italic; line-height: 1.5;
|
|
23817
|
+
border-left: 2px solid var(--accent); padding-left: 8px; margin: 6px 0 8px;
|
|
23818
|
+
}
|
|
23819
|
+
.ac-list { list-style: none; display: flex; flex-direction: column; gap: 4px; margin: 6px 0; }
|
|
23820
|
+
.ac-item { display: flex; align-items: flex-start; gap: 6px; font-size: 12px; }
|
|
23821
|
+
.ac-check { color: var(--text-soft); flex-shrink: 0; font-size: 11px; margin-top: 1px; font-family: var(--mono); }
|
|
23822
|
+
.ac-done .ac-check { color: #4aad72; }
|
|
23823
|
+
.ac-text { color: var(--text-soft); line-height: 1.4; }
|
|
23824
|
+
.ac-done .ac-text { color: var(--text); text-decoration: line-through; opacity: .6; }
|
|
23825
|
+
.card-key { font-size: 10px; font-family: var(--mono); color: #4a4540; margin-top: 8px; }
|
|
23826
|
+
|
|
23827
|
+
.missing-section { padding: 20px 24px; border-top: 1px solid var(--border); }
|
|
23828
|
+
.missing-title { font-size: 13px; font-weight: 600; color: var(--text-soft); margin-bottom: 12px; }
|
|
23829
|
+
.missing-grid { display: flex; flex-direction: column; gap: 6px; }
|
|
23830
|
+
.missing-card {
|
|
23831
|
+
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
|
|
23832
|
+
background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px;
|
|
23833
|
+
cursor: pointer; font-size: 12px; transition: border-color .15s;
|
|
23834
|
+
}
|
|
23835
|
+
.missing-card:hover { border-color: var(--accent); }
|
|
23836
|
+
.missing-status { flex-shrink: 0; }
|
|
23837
|
+
.missing-title-text, .missing-title { flex: 1; }
|
|
23838
|
+
.missing-domain { color: var(--text-soft); font-family: var(--mono); font-size: 10px; }
|
|
23839
|
+
.missing-hint { color: #4aad72; font-size: 10px; opacity: .6; }
|
|
23840
|
+
</style>
|
|
23841
|
+
</head>
|
|
23842
|
+
<body>
|
|
23843
|
+
<div class="topbar">
|
|
23844
|
+
<span class="topbar-logo">◈ lac</span>
|
|
23845
|
+
<span class="topbar-sep">|</span>
|
|
23846
|
+
<span class="topbar-project">${esc$3(projectName)} — Success Board</span>
|
|
23847
|
+
<span class="topbar-count">${withCriteria.length}/${features.length} features have criteria</span>
|
|
23848
|
+
</div>
|
|
23849
|
+
|
|
23850
|
+
<div class="hero">
|
|
23851
|
+
<div class="hero-text">
|
|
23852
|
+
<h1>What does "done" look like?</h1>
|
|
23853
|
+
<p>Success criteria and acceptance criteria — organized by delivery status</p>
|
|
23854
|
+
</div>
|
|
23855
|
+
<div class="stats-row">
|
|
23856
|
+
<div class="stat-card">
|
|
23857
|
+
<div class="stat-num">${frozenWithCriteria}</div>
|
|
23858
|
+
<div class="stat-label">criteria met</div>
|
|
23859
|
+
</div>
|
|
23860
|
+
<div class="stat-card">
|
|
23861
|
+
<div class="stat-num">${withCriteria.filter((f) => f["status"] === "active").length}</div>
|
|
23862
|
+
<div class="stat-label">in progress</div>
|
|
23863
|
+
</div>
|
|
23864
|
+
<div class="stat-card">
|
|
23865
|
+
<div class="stat-num">${criteriaPct}%</div>
|
|
23866
|
+
<div class="stat-label">coverage</div>
|
|
23867
|
+
</div>
|
|
23868
|
+
<div class="stat-card">
|
|
23869
|
+
<div class="stat-num">${frozenCount}</div>
|
|
23870
|
+
<div class="stat-label">shipped total</div>
|
|
23871
|
+
</div>
|
|
23872
|
+
</div>
|
|
23873
|
+
</div>
|
|
23874
|
+
|
|
23875
|
+
<div class="progress-bar-wrap">
|
|
23876
|
+
<div class="progress-bar-label">${criteriaPct}% of features have defined success criteria</div>
|
|
23877
|
+
<div class="progress-bar"><div class="progress-fill" style="width:${criteriaPct}%"></div></div>
|
|
23878
|
+
</div>
|
|
23879
|
+
|
|
23880
|
+
<div class="filters">
|
|
23881
|
+
<span class="filter-label">Domain:</span>
|
|
23882
|
+
<span class="domain-pill active" data-domain="__all__" onclick="toggleDomain('__all__')">All <span class="pill-count">${withCriteria.length}</span></span>
|
|
23883
|
+
${domainPills}
|
|
23884
|
+
</div>
|
|
23885
|
+
|
|
23886
|
+
<div class="board">
|
|
23887
|
+
${columnHtml}
|
|
23888
|
+
</div>
|
|
23889
|
+
|
|
23890
|
+
${withoutCriteria.length > 0 ? `
|
|
23891
|
+
<div class="missing-section">
|
|
23892
|
+
<div class="missing-title">⚠ ${withoutCriteria.length} feature${withoutCriteria.length !== 1 ? "s" : ""} missing success criteria${withoutCriteria.length > 12 ? ` (showing 12 of ${withoutCriteria.length})` : ""}</div>
|
|
23893
|
+
<div class="missing-grid">${missingCards}</div>
|
|
23894
|
+
</div>` : ""}
|
|
23895
|
+
|
|
23896
|
+
<script>
|
|
23897
|
+
const DATA = ${dataJson};
|
|
23898
|
+
let activeDomain = '__all__';
|
|
23899
|
+
|
|
23900
|
+
function toggleDomain(d) {
|
|
23901
|
+
activeDomain = d;
|
|
23902
|
+
document.querySelectorAll('.domain-pill').forEach(el => {
|
|
23903
|
+
el.classList.toggle('active', el.dataset.domain === d);
|
|
23904
|
+
});
|
|
23905
|
+
document.querySelectorAll('.card').forEach(el => {
|
|
23906
|
+
const match = d === '__all__' || el.dataset.domain === d;
|
|
23907
|
+
el.style.display = match ? '' : 'none';
|
|
23908
|
+
});
|
|
23909
|
+
// Update column counts
|
|
23910
|
+
['frozen','active','draft'].forEach(status => {
|
|
23911
|
+
const col = document.getElementById('col-' + status);
|
|
23912
|
+
if (!col) return;
|
|
23913
|
+
const visible = col.querySelectorAll('.card:not([style*="none"])').length;
|
|
23914
|
+
const hdr = col.closest('.column').querySelector('.col-count');
|
|
23915
|
+
if (hdr) hdr.textContent = visible;
|
|
23916
|
+
});
|
|
23917
|
+
}
|
|
23918
|
+
<\/script>
|
|
23919
|
+
</body>
|
|
23920
|
+
</html>`;
|
|
23921
|
+
}
|
|
23922
|
+
//#endregion
|
|
23923
|
+
//#region src/lib/pitchGenerator.ts
|
|
23924
|
+
function esc$2(s) {
|
|
23925
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
23926
|
+
}
|
|
23927
|
+
function firstSentence(s) {
|
|
23928
|
+
if (typeof s !== "string") return "";
|
|
23929
|
+
const match = s.match(/^[^.!?]*[.!?]/);
|
|
23930
|
+
return match ? match[0].trim() : s.trim().slice(0, 120);
|
|
23931
|
+
}
|
|
23932
|
+
function hasText(v, min = 1) {
|
|
23933
|
+
return typeof v === "string" && v.trim().length >= min;
|
|
23934
|
+
}
|
|
23935
|
+
const DOMAIN_HUE = {
|
|
23936
|
+
"app-shell": 215,
|
|
23937
|
+
"auth": 195,
|
|
23938
|
+
"recording": 10,
|
|
23939
|
+
"editing": 35,
|
|
23940
|
+
"sessions": 265,
|
|
23941
|
+
"versioning": 155,
|
|
23942
|
+
"collaboration": 320,
|
|
23943
|
+
"band": 55,
|
|
23944
|
+
"render": 180,
|
|
23945
|
+
"storage": 240
|
|
23946
|
+
};
|
|
23947
|
+
function domainBg(domain) {
|
|
23948
|
+
return `radial-gradient(ellipse at 30% 40%, hsl(${DOMAIN_HUE[domain] ?? 215},28%,12%) 0%, #0d0b09 70%)`;
|
|
23949
|
+
}
|
|
23950
|
+
const STATUS_COLOR$1 = {
|
|
23951
|
+
frozen: "#5b82cc",
|
|
23952
|
+
active: "#4aad72",
|
|
23953
|
+
draft: "#c4a255",
|
|
23954
|
+
deprecated: "#664444"
|
|
23955
|
+
};
|
|
23956
|
+
function generatePitch(features, projectName) {
|
|
23957
|
+
const domains = [...new Set(features.map((f) => f["domain"] || "misc"))].sort();
|
|
23958
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
23959
|
+
for (const f of features) {
|
|
23960
|
+
const d = f["domain"] || "misc";
|
|
23961
|
+
if (!byDomain.has(d)) byDomain.set(d, []);
|
|
23962
|
+
byDomain.get(d).push(f);
|
|
23963
|
+
}
|
|
23964
|
+
const frozen = features.filter((f) => f["status"] === "frozen");
|
|
23965
|
+
const active = features.filter((f) => f["status"] === "active");
|
|
23966
|
+
const draft = features.filter((f) => f["status"] === "draft");
|
|
23967
|
+
const frozenWithGuide = frozen.filter((f) => hasText(f["userGuide"], 20)).sort((a, b) => (a["priority"] ?? 99) - (b["priority"] ?? 99));
|
|
23968
|
+
const allDecisions = [];
|
|
23969
|
+
for (const f of features) {
|
|
23970
|
+
const decs = f["decisions"];
|
|
23971
|
+
if (!Array.isArray(decs)) continue;
|
|
23972
|
+
for (const d of decs) {
|
|
23973
|
+
const obj = d;
|
|
23974
|
+
const rationale = typeof obj["rationale"] === "string" ? obj["rationale"] : "";
|
|
23975
|
+
if (rationale.length > 40) allDecisions.push({
|
|
23976
|
+
decision: String(obj["decision"] ?? ""),
|
|
23977
|
+
rationale,
|
|
23978
|
+
domain: f["domain"] || "",
|
|
23979
|
+
feature: f["title"] || ""
|
|
23980
|
+
});
|
|
23981
|
+
}
|
|
23982
|
+
}
|
|
23983
|
+
allDecisions.sort((a, b) => b.rationale.length - a.rationale.length);
|
|
23984
|
+
const topDecisions = allDecisions.slice(0, 3);
|
|
23985
|
+
const taglineSource = frozen.sort((a, b) => (a["priority"] ?? 99) - (b["priority"] ?? 99))[0];
|
|
23986
|
+
const tagline = taglineSource ? firstSentence(taglineSource["problem"]) : "";
|
|
23987
|
+
const slides = [];
|
|
23988
|
+
slides.push({
|
|
23989
|
+
type: "cover",
|
|
23990
|
+
title: projectName,
|
|
23991
|
+
tagline: tagline || `${features.length} features across ${domains.length} domains`,
|
|
23992
|
+
stats: {
|
|
23993
|
+
total: features.length,
|
|
23994
|
+
frozen: frozen.length,
|
|
23995
|
+
active: active.length,
|
|
23996
|
+
draft: draft.length,
|
|
23997
|
+
domains: domains.length
|
|
23998
|
+
}
|
|
23999
|
+
});
|
|
24000
|
+
slides.push({
|
|
24001
|
+
type: "overview",
|
|
24002
|
+
title: "At a Glance",
|
|
24003
|
+
stats: {
|
|
24004
|
+
frozen: frozen.length,
|
|
24005
|
+
active: active.length,
|
|
24006
|
+
draft: draft.length
|
|
24007
|
+
},
|
|
24008
|
+
domains
|
|
24009
|
+
});
|
|
24010
|
+
for (const domain of domains) {
|
|
24011
|
+
const domFeats = (byDomain.get(domain) ?? []).sort((a, b) => (a["priority"] ?? 99) - (b["priority"] ?? 99));
|
|
24012
|
+
slides.push({
|
|
24013
|
+
type: "domain",
|
|
24014
|
+
domain,
|
|
24015
|
+
title: domain.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
24016
|
+
features: domFeats.slice(0, 8).map((f) => ({
|
|
24017
|
+
title: f["title"],
|
|
24018
|
+
status: f["status"],
|
|
24019
|
+
key: f["featureKey"]
|
|
24020
|
+
})),
|
|
24021
|
+
total: domFeats.length,
|
|
24022
|
+
frozen: domFeats.filter((f) => f["status"] === "frozen").length
|
|
24023
|
+
});
|
|
24024
|
+
}
|
|
24025
|
+
for (const f of frozenWithGuide.slice(0, 12)) {
|
|
24026
|
+
const decs = Array.isArray(f["decisions"]) ? f["decisions"] : [];
|
|
24027
|
+
const topDec = decs[0] ? String(decs[0]["rationale"] ?? "") : "";
|
|
24028
|
+
slides.push({
|
|
24029
|
+
type: "feature",
|
|
24030
|
+
domain: f["domain"] || "",
|
|
24031
|
+
title: f["title"],
|
|
24032
|
+
userGuide: f["userGuide"],
|
|
24033
|
+
keyDecision: firstSentence(topDec),
|
|
24034
|
+
key: f["featureKey"]
|
|
24035
|
+
});
|
|
24036
|
+
}
|
|
24037
|
+
if (topDecisions.length > 0) slides.push({
|
|
24038
|
+
type: "decisions",
|
|
24039
|
+
title: "What We Decided",
|
|
24040
|
+
decisions: topDecisions.map((d) => ({
|
|
24041
|
+
decision: d.decision.slice(0, 100),
|
|
24042
|
+
rationale: firstSentence(d.rationale),
|
|
24043
|
+
domain: d.domain,
|
|
24044
|
+
feature: d.feature
|
|
24045
|
+
}))
|
|
24046
|
+
});
|
|
24047
|
+
if (active.length > 0) slides.push({
|
|
24048
|
+
type: "roadmap",
|
|
24049
|
+
title: "What's Next",
|
|
24050
|
+
features: active.sort((a, b) => (a["priority"] ?? 99) - (b["priority"] ?? 99)).slice(0, 8).map((f) => ({
|
|
24051
|
+
title: f["title"],
|
|
24052
|
+
domain: f["domain"],
|
|
24053
|
+
priority: f["priority"],
|
|
24054
|
+
problem: firstSentence(f["problem"])
|
|
24055
|
+
}))
|
|
24056
|
+
});
|
|
24057
|
+
slides.push({
|
|
24058
|
+
type: "outro",
|
|
24059
|
+
stats: {
|
|
24060
|
+
frozen: frozen.length,
|
|
24061
|
+
domains: domains.length,
|
|
24062
|
+
total: features.length
|
|
24063
|
+
}
|
|
24064
|
+
});
|
|
24065
|
+
const slidesJson = JSON.stringify(slides).replace(/<\/script>/gi, "<\\/script>");
|
|
24066
|
+
const domainBgMap = {};
|
|
24067
|
+
for (const d of domains) domainBgMap[d] = domainBg(d);
|
|
24068
|
+
const domainBgJson = JSON.stringify(domainBgMap).replace(/<\/script>/gi, "<\\/script>");
|
|
24069
|
+
const statusColorJson = JSON.stringify(STATUS_COLOR$1).replace(/<\/script>/gi, "<\\/script>");
|
|
24070
|
+
return `<!DOCTYPE html>
|
|
24071
|
+
<html lang="en">
|
|
24072
|
+
<head>
|
|
24073
|
+
<meta charset="UTF-8">
|
|
24074
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
24075
|
+
<title>${esc$2(projectName)} — Pitch Deck</title>
|
|
24076
|
+
<style>
|
|
24077
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
24078
|
+
html, body { width: 100%; height: 100%; overflow: hidden; background: #0d0b09; color: #e8e0d4; font-family: system-ui, -apple-system, sans-serif; }
|
|
24079
|
+
|
|
24080
|
+
.deck { position: fixed; inset: 0; }
|
|
24081
|
+
|
|
24082
|
+
/* Slide */
|
|
24083
|
+
.slide {
|
|
24084
|
+
position: absolute; inset: 0;
|
|
24085
|
+
display: flex; flex-direction: column;
|
|
24086
|
+
justify-content: center; align-items: center;
|
|
24087
|
+
padding: 48px 64px;
|
|
24088
|
+
transform: translateX(100%);
|
|
24089
|
+
transition: transform 0.42s cubic-bezier(0.4, 0, 0.2, 1);
|
|
24090
|
+
will-change: transform;
|
|
24091
|
+
}
|
|
24092
|
+
.slide.active { transform: translateX(0); }
|
|
24093
|
+
.slide.prev { transform: translateX(-100%); }
|
|
24094
|
+
|
|
24095
|
+
/* Cover */
|
|
24096
|
+
.cover { background: radial-gradient(ellipse at 30% 30%, #1a1508 0%, #0d0b09 65%); }
|
|
24097
|
+
.cover-eyebrow { font-size: 13px; color: #d4a853; font-family: monospace; letter-spacing: .15em; text-transform: uppercase; margin-bottom: 20px; }
|
|
24098
|
+
.cover-title { font-size: clamp(36px, 6vw, 72px); font-weight: 800; text-align: center; line-height: 1.1; letter-spacing: -.02em; margin-bottom: 20px; }
|
|
24099
|
+
.cover-tagline { font-size: clamp(14px, 2vw, 20px); color: #8a7f74; text-align: center; max-width: 640px; line-height: 1.55; margin-bottom: 40px; }
|
|
24100
|
+
.cover-pills { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; }
|
|
24101
|
+
.cover-pill { font-size: 13px; padding: 6px 16px; border-radius: 20px; border: 1px solid #2a2724; color: #8a7f74; font-family: monospace; }
|
|
24102
|
+
.cover-pill span { color: #d4a853; font-weight: 700; }
|
|
24103
|
+
|
|
24104
|
+
/* Overview */
|
|
24105
|
+
.overview { background: #0d0b09; }
|
|
24106
|
+
.slide-title { font-size: clamp(24px, 4vw, 48px); font-weight: 700; text-align: center; margin-bottom: 40px; letter-spacing: -.01em; }
|
|
24107
|
+
.overview-stats { display: flex; gap: 40px; margin-bottom: 48px; flex-wrap: wrap; justify-content: center; }
|
|
24108
|
+
.stat-block { text-align: center; }
|
|
24109
|
+
.stat-num { font-size: clamp(48px, 7vw, 88px); font-weight: 800; font-family: monospace; }
|
|
24110
|
+
.stat-label { font-size: 14px; color: #8a7f74; margin-top: 4px; }
|
|
24111
|
+
.domain-cloud { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; max-width: 700px; }
|
|
24112
|
+
.domain-chip { font-size: 13px; padding: 6px 14px; border-radius: 14px; background: #1a1714; border: 1px solid #2a2724; color: #8a7f74; }
|
|
24113
|
+
|
|
24114
|
+
/* Domain slide */
|
|
24115
|
+
.domain-slide { text-align: left; align-items: flex-start; }
|
|
24116
|
+
.domain-eyebrow { font-size: 11px; color: #d4a853; font-family: monospace; letter-spacing: .15em; text-transform: uppercase; margin-bottom: 16px; }
|
|
24117
|
+
.domain-title { font-size: clamp(32px, 5vw, 64px); font-weight: 800; letter-spacing: -.02em; margin-bottom: 32px; }
|
|
24118
|
+
.domain-features { display: flex; flex-direction: column; gap: 10px; max-width: 640px; }
|
|
24119
|
+
.domain-feat { display: flex; align-items: center; gap: 12px; }
|
|
24120
|
+
.feat-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
24121
|
+
.feat-title { font-size: clamp(13px, 1.8vw, 18px); color: #c8c0b4; line-height: 1.3; }
|
|
24122
|
+
.domain-stat { margin-top: 32px; font-size: 13px; color: #8a7f74; font-family: monospace; }
|
|
24123
|
+
|
|
24124
|
+
/* Feature slide */
|
|
24125
|
+
.feature-slide { align-items: flex-start; text-align: left; }
|
|
24126
|
+
.feature-domain { font-size: 11px; color: #d4a853; font-family: monospace; letter-spacing: .15em; text-transform: uppercase; margin-bottom: 16px; }
|
|
24127
|
+
.feature-title { font-size: clamp(24px, 4vw, 48px); font-weight: 700; line-height: 1.2; letter-spacing: -.01em; margin-bottom: 24px; max-width: 800px; }
|
|
24128
|
+
.feature-guide-label { font-size: 11px; color: #4aad72; font-family: monospace; text-transform: uppercase; letter-spacing: .1em; margin-bottom: 10px; }
|
|
24129
|
+
.feature-guide { font-size: clamp(15px, 2vw, 20px); color: #c8c0b4; line-height: 1.65; max-width: 680px; margin-bottom: 28px; }
|
|
24130
|
+
.feature-decision { font-size: 13px; color: #8a7f74; border-left: 2px solid #d4a853; padding-left: 12px; max-width: 600px; line-height: 1.5; }
|
|
24131
|
+
.feature-key { font-size: 10px; color: #3a3530; font-family: monospace; position: absolute; bottom: 60px; right: 64px; }
|
|
24132
|
+
|
|
24133
|
+
/* Decisions slide */
|
|
24134
|
+
.decisions-grid { display: flex; gap: 24px; flex-wrap: wrap; justify-content: center; max-width: 960px; }
|
|
24135
|
+
.decision-card {
|
|
24136
|
+
flex: 1; min-width: 240px; max-width: 280px;
|
|
24137
|
+
background: #1a1714; border: 1px solid #2a2724; border-radius: 12px;
|
|
24138
|
+
padding: 20px 18px;
|
|
24139
|
+
}
|
|
24140
|
+
.decision-meta { font-size: 10px; color: #8a7f74; font-family: monospace; margin-bottom: 10px; }
|
|
24141
|
+
.decision-text { font-size: 14px; font-weight: 600; color: #e8e0d4; line-height: 1.4; margin-bottom: 10px; }
|
|
24142
|
+
.decision-rationale { font-size: 12px; color: #8a7f74; line-height: 1.55; }
|
|
24143
|
+
|
|
24144
|
+
/* Roadmap */
|
|
24145
|
+
.roadmap-grid { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; max-width: 960px; }
|
|
24146
|
+
.roadmap-card {
|
|
24147
|
+
background: #1a1714; border: 1px solid #2a2724; border-radius: 10px;
|
|
24148
|
+
padding: 16px 16px 14px; min-width: 200px; max-width: 240px; flex: 1;
|
|
24149
|
+
}
|
|
24150
|
+
.roadmap-domain { font-size: 10px; color: #d4a853; font-family: monospace; margin-bottom: 6px; }
|
|
24151
|
+
.roadmap-title { font-size: 13px; font-weight: 600; line-height: 1.35; margin-bottom: 6px; }
|
|
24152
|
+
.roadmap-problem { font-size: 11px; color: #8a7f74; line-height: 1.4; }
|
|
24153
|
+
|
|
24154
|
+
/* Outro */
|
|
24155
|
+
.outro { background: radial-gradient(ellipse at 70% 60%, #0f1008 0%, #0d0b09 65%); }
|
|
24156
|
+
.outro-title { font-size: clamp(28px, 5vw, 56px); font-weight: 800; margin-bottom: 12px; letter-spacing: -.01em; }
|
|
24157
|
+
.outro-sub { font-size: 16px; color: #8a7f74; margin-bottom: 40px; }
|
|
24158
|
+
.outro-stats { display: flex; gap: 32px; flex-wrap: wrap; justify-content: center; margin-bottom: 40px; }
|
|
24159
|
+
.outro-stat { text-align: center; }
|
|
24160
|
+
.outro-num { font-size: 40px; font-weight: 800; font-family: monospace; color: #d4a853; }
|
|
24161
|
+
.outro-label { font-size: 12px; color: #8a7f74; }
|
|
24162
|
+
.lac-badge { font-size: 12px; color: #4a4540; font-family: monospace; }
|
|
24163
|
+
|
|
24164
|
+
/* Nav */
|
|
24165
|
+
.nav { position: fixed; bottom: 0; left: 0; right: 0; display: flex; align-items: center; padding: 0 32px; height: 52px; background: rgba(13,11,9,.85); backdrop-filter: blur(8px); z-index: 100; gap: 16px; }
|
|
24166
|
+
.progress-track { flex: 1; height: 2px; background: #2a2724; border-radius: 1px; overflow: hidden; }
|
|
24167
|
+
.progress-bar { height: 100%; background: #d4a853; border-radius: 1px; transition: width .3s ease; }
|
|
24168
|
+
.slide-counter { font-size: 12px; color: #8a7f74; font-family: monospace; min-width: 48px; text-align: right; }
|
|
24169
|
+
.nav-btn { width: 32px; height: 32px; border-radius: 6px; border: 1px solid #2a2724; background: transparent; color: #8a7f74; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all .15s; }
|
|
24170
|
+
.nav-btn:hover { border-color: #d4a853; color: #d4a853; }
|
|
24171
|
+
.key-hint { font-size: 10px; color: #3a3530; font-family: monospace; }
|
|
24172
|
+
|
|
24173
|
+
/* Grid overview */
|
|
24174
|
+
.grid-overlay {
|
|
24175
|
+
position: fixed; inset: 0; background: rgba(13,11,9,.96); z-index: 200;
|
|
24176
|
+
display: none; overflow-y: auto; padding: 24px;
|
|
24177
|
+
}
|
|
24178
|
+
.grid-overlay.visible { display: block; }
|
|
24179
|
+
.grid-title { font-size: 14px; color: #8a7f74; font-family: monospace; margin-bottom: 16px; letter-spacing: .05em; }
|
|
24180
|
+
.grid-slides { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
|
|
24181
|
+
.grid-thumb {
|
|
24182
|
+
aspect-ratio: 16/9; border: 1px solid #2a2724; border-radius: 6px; background: #1a1714;
|
|
24183
|
+
cursor: pointer; display: flex; flex-direction: column; justify-content: center; align-items: center;
|
|
24184
|
+
padding: 10px; text-align: center; transition: border-color .15s; overflow: hidden;
|
|
24185
|
+
}
|
|
24186
|
+
.grid-thumb:hover { border-color: #d4a853; }
|
|
24187
|
+
.grid-thumb.active-thumb { border-color: #d4a853; border-width: 2px; }
|
|
24188
|
+
.grid-num { font-size: 9px; color: #4a4540; font-family: monospace; margin-bottom: 4px; }
|
|
24189
|
+
.grid-thumb-title { font-size: 10px; color: #8a7f74; line-height: 1.3; }
|
|
24190
|
+
.grid-thumb-type { font-size: 9px; color: #4a4540; font-family: monospace; margin-top: 3px; }
|
|
24191
|
+
</style>
|
|
24192
|
+
</head>
|
|
24193
|
+
<body>
|
|
24194
|
+
<div class="deck" id="deck"></div>
|
|
24195
|
+
|
|
24196
|
+
<div class="nav">
|
|
24197
|
+
<button class="nav-btn" onclick="prev()" title="Previous (←)">‹</button>
|
|
24198
|
+
<div class="progress-track"><div class="progress-bar" id="prog"></div></div>
|
|
24199
|
+
<div class="slide-counter" id="counter">1 / ${slides.length}</div>
|
|
24200
|
+
<button class="nav-btn" onclick="next()" title="Next (→ or Space)">›</button>
|
|
24201
|
+
<span class="key-hint">G=grid P=notes</span>
|
|
24202
|
+
</div>
|
|
24203
|
+
|
|
24204
|
+
<div class="grid-overlay" id="grid-overlay">
|
|
24205
|
+
<div class="grid-title">◈ SLIDE OVERVIEW — click to jump · G or Esc to close</div>
|
|
24206
|
+
<div class="grid-slides" id="grid-slides"></div>
|
|
24207
|
+
</div>
|
|
24208
|
+
|
|
24209
|
+
<script>
|
|
24210
|
+
const SLIDES = ${slidesJson};
|
|
24211
|
+
const DOMAIN_BG = ${domainBgJson};
|
|
24212
|
+
const STATUS_COLOR = ${statusColorJson};
|
|
24213
|
+
|
|
24214
|
+
let current = 0;
|
|
24215
|
+
let presenterMode = false;
|
|
24216
|
+
let gridVisible = false;
|
|
24217
|
+
|
|
24218
|
+
function esc(s) {
|
|
24219
|
+
if (s == null) return '';
|
|
24220
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
24221
|
+
}
|
|
24222
|
+
|
|
24223
|
+
function renderSlide(slide, idx) {
|
|
24224
|
+
const isActive = idx === current;
|
|
24225
|
+
const isPrev = idx < current;
|
|
24226
|
+
const cls = isActive ? 'active' : isPrev ? 'prev' : '';
|
|
24227
|
+
|
|
24228
|
+
if (slide.type === 'cover') {
|
|
24229
|
+
const s = slide.stats;
|
|
24230
|
+
return \`<div class="slide cover \${cls}" data-idx="\${idx}">
|
|
24231
|
+
<div class="cover-eyebrow">◈ life-as-code</div>
|
|
24232
|
+
<div class="cover-title">\${esc(slide.title)}</div>
|
|
24233
|
+
\${slide.tagline ? \`<div class="cover-tagline">\${esc(slide.tagline)}</div>\` : ''}
|
|
24234
|
+
<div class="cover-pills">
|
|
24235
|
+
<div class="cover-pill"><span>\${s.frozen}</span> shipped</div>
|
|
24236
|
+
<div class="cover-pill"><span>\${s.active}</span> active</div>
|
|
24237
|
+
<div class="cover-pill"><span>\${s.draft}</span> planned</div>
|
|
24238
|
+
<div class="cover-pill"><span>\${s.domains}</span> domains</div>
|
|
24239
|
+
<div class="cover-pill"><span>\${s.total}</span> features</div>
|
|
24240
|
+
</div>
|
|
24241
|
+
</div>\`;
|
|
24242
|
+
}
|
|
24243
|
+
|
|
24244
|
+
if (slide.type === 'overview') {
|
|
24245
|
+
const s = slide.stats;
|
|
24246
|
+
return \`<div class="slide \${cls}" data-idx="\${idx}" style="background:#0d0b09">
|
|
24247
|
+
<div class="slide-title">\${esc(slide.title)}</div>
|
|
24248
|
+
<div class="overview-stats">
|
|
24249
|
+
<div class="stat-block"><div class="stat-num" style="color:#5b82cc">\${s.frozen}</div><div class="stat-label">shipped features</div></div>
|
|
24250
|
+
<div class="stat-block"><div class="stat-num" style="color:#4aad72">\${s.active}</div><div class="stat-label">in progress</div></div>
|
|
24251
|
+
<div class="stat-block"><div class="stat-num" style="color:#c4a255">\${s.draft}</div><div class="stat-label">planned</div></div>
|
|
24252
|
+
</div>
|
|
24253
|
+
<div class="domain-cloud">
|
|
24254
|
+
\${slide.domains.map(d => \`<span class="domain-chip">\${esc(d)}</span>\`).join('')}
|
|
24255
|
+
</div>
|
|
24256
|
+
</div>\`;
|
|
24257
|
+
}
|
|
24258
|
+
|
|
24259
|
+
if (slide.type === 'domain') {
|
|
24260
|
+
const bg = DOMAIN_BG[slide.domain] || 'radial-gradient(ellipse at 30% 40%, #181410 0%, #0d0b09 70%)';
|
|
24261
|
+
return \`<div class="slide domain-slide \${cls}" data-idx="\${idx}" style="background:\${bg}">
|
|
24262
|
+
<div class="domain-eyebrow">domain</div>
|
|
24263
|
+
<div class="domain-title">\${esc(slide.title)}</div>
|
|
24264
|
+
<div class="domain-features">
|
|
24265
|
+
\${slide.features.map(f => \`<div class="domain-feat">
|
|
24266
|
+
<span class="feat-dot" style="background:\${STATUS_COLOR[f.status]||'#888'}"></span>
|
|
24267
|
+
<span class="feat-title">\${esc(f.title)}</span>
|
|
24268
|
+
</div>\`).join('')}
|
|
24269
|
+
</div>
|
|
24270
|
+
<div class="domain-stat">\${slide.total} feature\${slide.total!==1?'s':''} · \${slide.frozen} shipped</div>
|
|
24271
|
+
</div>\`;
|
|
24272
|
+
}
|
|
24273
|
+
|
|
24274
|
+
if (slide.type === 'feature') {
|
|
24275
|
+
const bg = DOMAIN_BG[slide.domain] || 'radial-gradient(ellipse at 30% 40%, #181410 0%, #0d0b09 70%)';
|
|
24276
|
+
return \`<div class="slide feature-slide \${cls}" data-idx="\${idx}" style="background:\${bg}">
|
|
24277
|
+
<div class="feature-domain">\${esc(slide.domain)}</div>
|
|
24278
|
+
<div class="feature-title">\${esc(slide.title)}</div>
|
|
24279
|
+
<div class="feature-guide-label">what you can do</div>
|
|
24280
|
+
<div class="feature-guide">\${esc(String(slide.userGuide||''))}</div>
|
|
24281
|
+
\${slide.keyDecision && presenterMode ? \`<div class="feature-decision">💡 \${esc(slide.keyDecision)}</div>\` : ''}
|
|
24282
|
+
<div class="feature-key">\${esc(slide.key)}</div>
|
|
24283
|
+
</div>\`;
|
|
24284
|
+
}
|
|
24285
|
+
|
|
24286
|
+
if (slide.type === 'decisions') {
|
|
24287
|
+
return \`<div class="slide \${cls}" data-idx="\${idx}" style="background:#0d0b09">
|
|
24288
|
+
<div class="slide-title">\${esc(slide.title)}</div>
|
|
24289
|
+
<div class="decisions-grid">
|
|
24290
|
+
\${slide.decisions.map(d => \`<div class="decision-card">
|
|
24291
|
+
<div class="decision-meta">\${esc(d.domain)} · \${esc(d.feature.slice(0,40))}</div>
|
|
24292
|
+
<div class="decision-text">\${esc(d.decision)}</div>
|
|
24293
|
+
<div class="decision-rationale">\${esc(d.rationale)}</div>
|
|
24294
|
+
</div>\`).join('')}
|
|
24295
|
+
</div>
|
|
24296
|
+
</div>\`;
|
|
24297
|
+
}
|
|
24298
|
+
|
|
24299
|
+
if (slide.type === 'roadmap') {
|
|
24300
|
+
return \`<div class="slide \${cls}" data-idx="\${idx}" style="background:#0d0b09">
|
|
24301
|
+
<div class="slide-title">\${esc(slide.title)}</div>
|
|
24302
|
+
<div class="roadmap-grid">
|
|
24303
|
+
\${slide.features.map(f => \`<div class="roadmap-card">
|
|
24304
|
+
\${f.domain ? \`<div class="roadmap-domain">\${esc(f.domain)}</div>\` : ''}
|
|
24305
|
+
<div class="roadmap-title">\${esc(f.title)}</div>
|
|
24306
|
+
\${f.problem ? \`<div class="roadmap-problem">\${esc(f.problem)}</div>\` : ''}
|
|
24307
|
+
</div>\`).join('')}
|
|
24308
|
+
</div>
|
|
24309
|
+
</div>\`;
|
|
24310
|
+
}
|
|
24311
|
+
|
|
24312
|
+
if (slide.type === 'outro') {
|
|
24313
|
+
const s = slide.stats;
|
|
24314
|
+
return \`<div class="slide outro \${cls}" data-idx="\${idx}">
|
|
24315
|
+
<div class="outro-title">\${esc(SLIDES[0].title)}</div>
|
|
24316
|
+
<div class="outro-sub">Built with life-as-code</div>
|
|
24317
|
+
<div class="outro-stats">
|
|
24318
|
+
<div class="outro-stat"><div class="outro-num">\${s.frozen}</div><div class="outro-label">features shipped</div></div>
|
|
24319
|
+
<div class="outro-stat"><div class="outro-num">\${s.domains}</div><div class="outro-label">domains</div></div>
|
|
24320
|
+
<div class="outro-stat"><div class="outro-num">\${s.total}</div><div class="outro-label">total features</div></div>
|
|
24321
|
+
</div>
|
|
24322
|
+
<div class="lac-badge">◈ lac · /lac/</div>
|
|
24323
|
+
</div>\`;
|
|
24324
|
+
}
|
|
24325
|
+
|
|
24326
|
+
return \`<div class="slide \${cls}" data-idx="\${idx}" style="background:#0d0b09">
|
|
24327
|
+
<div class="slide-title">\${esc(slide.title||slide.type)}</div>
|
|
24328
|
+
</div>\`;
|
|
24329
|
+
}
|
|
24330
|
+
|
|
24331
|
+
function render() {
|
|
24332
|
+
const deck = document.getElementById('deck');
|
|
24333
|
+
const start = Math.max(0, current - 1);
|
|
24334
|
+
const end = Math.min(SLIDES.length - 1, current + 1);
|
|
24335
|
+
|
|
24336
|
+
// Remove stale slides
|
|
24337
|
+
deck.querySelectorAll('.slide').forEach(el => {
|
|
24338
|
+
const idx = parseInt(el.dataset.idx, 10);
|
|
24339
|
+
if (idx < start || idx > end) el.remove();
|
|
24340
|
+
});
|
|
24341
|
+
|
|
24342
|
+
// Render/update visible slides
|
|
24343
|
+
for (let i = start; i <= end; i++) {
|
|
24344
|
+
const existing = deck.querySelector(\`.slide[data-idx="\${i}"]\`);
|
|
24345
|
+
const html = renderSlide(SLIDES[i], i);
|
|
24346
|
+
if (existing) {
|
|
24347
|
+
const cls = i === current ? 'active' : i < current ? 'prev' : '';
|
|
24348
|
+
existing.className = existing.className.replace(/\\b(active|prev)\\b/g, '').trim() + (cls ? ' ' + cls : '');
|
|
24349
|
+
} else {
|
|
24350
|
+
deck.insertAdjacentHTML('beforeend', html);
|
|
24351
|
+
}
|
|
24352
|
+
}
|
|
24353
|
+
|
|
24354
|
+
// Progress
|
|
24355
|
+
const pct = SLIDES.length > 1 ? (current / (SLIDES.length - 1)) * 100 : 100;
|
|
24356
|
+
document.getElementById('prog').style.width = pct + '%';
|
|
24357
|
+
document.getElementById('counter').textContent = (current + 1) + ' / ' + SLIDES.length;
|
|
24358
|
+
|
|
24359
|
+
// Grid thumbs
|
|
24360
|
+
document.querySelectorAll('.grid-thumb').forEach(el => {
|
|
24361
|
+
const idx = parseInt(el.dataset.idx, 10);
|
|
24362
|
+
el.classList.toggle('active-thumb', idx === current);
|
|
24363
|
+
});
|
|
24364
|
+
}
|
|
24365
|
+
|
|
24366
|
+
function next() { if (current < SLIDES.length - 1) { current++; render(); } }
|
|
24367
|
+
function prev() { if (current > 0) { current--; render(); } }
|
|
24368
|
+
|
|
24369
|
+
function toggleGrid() {
|
|
24370
|
+
gridVisible = !gridVisible;
|
|
24371
|
+
const overlay = document.getElementById('grid-overlay');
|
|
24372
|
+
overlay.classList.toggle('visible', gridVisible);
|
|
24373
|
+
if (gridVisible && !document.getElementById('grid-slides').children.length) {
|
|
24374
|
+
document.getElementById('grid-slides').innerHTML = SLIDES.map((s, i) =>
|
|
24375
|
+
\`<div class="grid-thumb\${i===current?' active-thumb':''}" data-idx="\${i}" onclick="jumpTo(\${i})">
|
|
24376
|
+
<div class="grid-num">\${i+1}</div>
|
|
24377
|
+
<div class="grid-thumb-title">\${esc(s.title||s.type)}</div>
|
|
24378
|
+
<div class="grid-thumb-type">\${esc(s.type)}</div>
|
|
24379
|
+
</div>\`
|
|
24380
|
+
).join('');
|
|
24381
|
+
}
|
|
24382
|
+
}
|
|
24383
|
+
|
|
24384
|
+
function jumpTo(idx) {
|
|
24385
|
+
current = idx;
|
|
24386
|
+
gridVisible = false;
|
|
24387
|
+
document.getElementById('grid-overlay').classList.remove('visible');
|
|
24388
|
+
render();
|
|
24389
|
+
}
|
|
24390
|
+
|
|
24391
|
+
// Keyboard
|
|
24392
|
+
document.addEventListener('keydown', e => {
|
|
24393
|
+
if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); next(); }
|
|
24394
|
+
else if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); }
|
|
24395
|
+
else if (e.key === 'g' || e.key === 'G') toggleGrid();
|
|
24396
|
+
else if (e.key === 'Escape' && gridVisible) { gridVisible = false; document.getElementById('grid-overlay').classList.remove('visible'); }
|
|
24397
|
+
else if (e.key === 'p' || e.key === 'P') { presenterMode = !presenterMode; render(); }
|
|
24398
|
+
});
|
|
24399
|
+
|
|
24400
|
+
// Touch swipe
|
|
24401
|
+
let tx = 0;
|
|
24402
|
+
document.addEventListener('touchstart', e => { tx = e.touches[0].clientX; }, { passive: true });
|
|
24403
|
+
document.addEventListener('touchend', e => {
|
|
24404
|
+
const dx = e.changedTouches[0].clientX - tx;
|
|
24405
|
+
if (Math.abs(dx) > 50) dx < 0 ? next() : prev();
|
|
24406
|
+
});
|
|
24407
|
+
|
|
24408
|
+
// Init
|
|
24409
|
+
render();
|
|
24410
|
+
<\/script>
|
|
24411
|
+
</body>
|
|
24412
|
+
</html>`;
|
|
24413
|
+
}
|
|
24414
|
+
//#endregion
|
|
24415
|
+
//#region src/lib/timelineGenerator.ts
|
|
24416
|
+
function esc$1(s) {
|
|
24417
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
24418
|
+
}
|
|
24419
|
+
const STATUS_COLOR = {
|
|
24420
|
+
frozen: "#5b82cc",
|
|
24421
|
+
active: "#4aad72",
|
|
24422
|
+
draft: "#c4a255",
|
|
24423
|
+
deprecated: "#664444"
|
|
24424
|
+
};
|
|
24425
|
+
const DOMAIN_ORDER = [
|
|
24426
|
+
"app-shell",
|
|
24427
|
+
"auth",
|
|
24428
|
+
"recording",
|
|
24429
|
+
"editing",
|
|
24430
|
+
"sessions",
|
|
24431
|
+
"versioning",
|
|
24432
|
+
"collaboration",
|
|
24433
|
+
"band",
|
|
24434
|
+
"render",
|
|
24435
|
+
"storage"
|
|
24436
|
+
];
|
|
24437
|
+
function getFeatureDates(f) {
|
|
24438
|
+
const history = f["statusHistory"];
|
|
24439
|
+
const transitions = [];
|
|
24440
|
+
if (Array.isArray(history)) {
|
|
24441
|
+
for (const entry of history) {
|
|
24442
|
+
const e = entry;
|
|
24443
|
+
const dateStr = e["date"];
|
|
24444
|
+
if (dateStr && typeof dateStr === "string") {
|
|
24445
|
+
const d = new Date(dateStr);
|
|
24446
|
+
if (!isNaN(d.getTime())) transitions.push({
|
|
24447
|
+
from: String(e["from"] ?? ""),
|
|
24448
|
+
to: String(e["to"] ?? ""),
|
|
24449
|
+
date: d
|
|
24450
|
+
});
|
|
24451
|
+
}
|
|
24452
|
+
}
|
|
24453
|
+
transitions.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
24454
|
+
}
|
|
24455
|
+
if (transitions.length === 0) return {
|
|
24456
|
+
start: null,
|
|
24457
|
+
end: null,
|
|
24458
|
+
transitions
|
|
24459
|
+
};
|
|
24460
|
+
const start = transitions[0].date;
|
|
24461
|
+
const lastStatus = f["status"] || "draft";
|
|
24462
|
+
return {
|
|
24463
|
+
start,
|
|
24464
|
+
end: lastStatus === "frozen" || lastStatus === "deprecated" ? transitions[transitions.length - 1].date : /* @__PURE__ */ new Date(),
|
|
24465
|
+
transitions
|
|
24466
|
+
};
|
|
24467
|
+
}
|
|
24468
|
+
function generateTimeline(features, projectName) {
|
|
24469
|
+
const today = /* @__PURE__ */ new Date();
|
|
24470
|
+
const allDates = [];
|
|
24471
|
+
for (const f of features) {
|
|
24472
|
+
const { start, end } = getFeatureDates(f);
|
|
24473
|
+
if (start) allDates.push(start);
|
|
24474
|
+
if (end) allDates.push(end);
|
|
24475
|
+
}
|
|
24476
|
+
const minDate = allDates.length > 0 ? new Date(Math.min(...allDates.map((d) => d.getTime()))) : new Date(today.getFullYear() - 1, 0, 1);
|
|
24477
|
+
const maxDate = today;
|
|
24478
|
+
const padded = (maxDate.getTime() - minDate.getTime()) * .04;
|
|
24479
|
+
const domainStart = new Date(minDate.getTime() - padded);
|
|
24480
|
+
const domainEnd = new Date(maxDate.getTime() + padded);
|
|
24481
|
+
const totalMs = domainEnd.getTime() - domainStart.getTime();
|
|
24482
|
+
function toPct(d) {
|
|
24483
|
+
return (d.getTime() - domainStart.getTime()) / totalMs * 100;
|
|
24484
|
+
}
|
|
24485
|
+
const domains = [...DOMAIN_ORDER.filter((d) => features.some((f) => f["domain"] === d)), ...[...new Set(features.map((f) => f["domain"] || "misc"))].filter((d) => !DOMAIN_ORDER.includes(d)).sort()];
|
|
24486
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
24487
|
+
for (const f of features) {
|
|
24488
|
+
const d = f["domain"] || "misc";
|
|
24489
|
+
if (!byDomain.has(d)) byDomain.set(d, []);
|
|
24490
|
+
byDomain.get(d).push(f);
|
|
24491
|
+
}
|
|
24492
|
+
const ticks = [];
|
|
24493
|
+
const cursor = new Date(domainStart.getFullYear(), domainStart.getMonth(), 1);
|
|
24494
|
+
while (cursor <= domainEnd) {
|
|
24495
|
+
ticks.push({
|
|
24496
|
+
date: new Date(cursor),
|
|
24497
|
+
label: cursor.toLocaleString("en", {
|
|
24498
|
+
month: "short",
|
|
24499
|
+
year: "2-digit"
|
|
24500
|
+
})
|
|
24501
|
+
});
|
|
24502
|
+
cursor.setMonth(cursor.getMonth() + 1);
|
|
24503
|
+
}
|
|
24504
|
+
const featData = features.map((f) => {
|
|
24505
|
+
const { start, end, transitions } = getFeatureDates(f);
|
|
24506
|
+
return {
|
|
24507
|
+
key: String(f["featureKey"] ?? ""),
|
|
24508
|
+
title: String(f["title"] ?? ""),
|
|
24509
|
+
status: String(f["status"] ?? "draft"),
|
|
24510
|
+
domain: String(f["domain"] ?? "misc"),
|
|
24511
|
+
startPct: start ? toPct(start) : null,
|
|
24512
|
+
endPct: end ? toPct(end) : null,
|
|
24513
|
+
hasHistory: transitions.length > 0,
|
|
24514
|
+
transitions: transitions.map((t) => ({
|
|
24515
|
+
from: t.from,
|
|
24516
|
+
to: t.to,
|
|
24517
|
+
date: t.date.toISOString().slice(0, 10)
|
|
24518
|
+
}))
|
|
24519
|
+
};
|
|
24520
|
+
});
|
|
24521
|
+
const dataJson = JSON.stringify({
|
|
24522
|
+
projectName,
|
|
24523
|
+
today: today.toISOString().slice(0, 10),
|
|
24524
|
+
todayPct: toPct(today),
|
|
24525
|
+
domains,
|
|
24526
|
+
features: featData,
|
|
24527
|
+
ticks: ticks.map((t) => ({
|
|
24528
|
+
label: t.label,
|
|
24529
|
+
pct: toPct(t.date)
|
|
24530
|
+
}))
|
|
24531
|
+
}).replace(/<\/script>/gi, "<\\/script>");
|
|
24532
|
+
const statusColorJson = JSON.stringify(STATUS_COLOR).replace(/<\/script>/gi, "<\\/script>");
|
|
24533
|
+
const featuresWithHistory = features.filter((f) => getFeatureDates(f).transitions.length > 0).length;
|
|
24534
|
+
const featuresWithoutHistory = features.length - featuresWithHistory;
|
|
24535
|
+
return `<!DOCTYPE html>
|
|
24536
|
+
<html lang="en">
|
|
24537
|
+
<head>
|
|
24538
|
+
<meta charset="UTF-8">
|
|
24539
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
24540
|
+
<title>${esc$1(projectName)} — Feature Timeline</title>
|
|
24541
|
+
<style>
|
|
24542
|
+
:root {
|
|
24543
|
+
--bg: #12100e; --bg-card: #1a1714; --bg-hover: #201d1a;
|
|
24544
|
+
--border: #2a2724; --border-soft: #221f1c;
|
|
24545
|
+
--text: #e8e0d4; --text-soft: #8a7f74; --accent: #d4a853;
|
|
24546
|
+
--lane-h: 52px; --label-w: 120px;
|
|
24547
|
+
--mono: 'SF Mono','Fira Code','Cascadia Code',monospace;
|
|
24548
|
+
}
|
|
24549
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
24550
|
+
html, body { height: 100%; background: var(--bg); color: var(--text); font-family: system-ui,-apple-system,sans-serif; overflow: hidden; display: flex; flex-direction: column; }
|
|
24551
|
+
|
|
24552
|
+
.topbar {
|
|
24553
|
+
display: flex; align-items: center; gap: 10px; padding: 0 20px; height: 48px;
|
|
24554
|
+
background: #0e0c0a; border-bottom: 1px solid var(--border); flex-shrink: 0; font-size: 13px;
|
|
24555
|
+
}
|
|
24556
|
+
.topbar-logo { color: var(--accent); font-weight: 700; font-family: var(--mono); }
|
|
24557
|
+
.topbar-sep { color: var(--border); }
|
|
24558
|
+
.topbar-count { margin-left: auto; color: var(--text-soft); font-size: 12px; font-family: var(--mono); }
|
|
24559
|
+
|
|
24560
|
+
.controls {
|
|
24561
|
+
display: flex; align-items: center; gap: 12px; padding: 10px 20px;
|
|
24562
|
+
background: var(--bg-card); border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
24563
|
+
font-size: 12px;
|
|
24564
|
+
}
|
|
24565
|
+
.ctrl-label { color: var(--text-soft); font-family: var(--mono); }
|
|
24566
|
+
.ctrl-btn {
|
|
24567
|
+
padding: 4px 12px; border-radius: 4px; border: 1px solid var(--border);
|
|
24568
|
+
background: transparent; color: var(--text-soft); cursor: pointer; font-size: 12px; font-family: var(--mono);
|
|
24569
|
+
transition: all .15s;
|
|
24570
|
+
}
|
|
24571
|
+
.ctrl-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
24572
|
+
.ctrl-sep { color: var(--border); }
|
|
24573
|
+
.legend { display: flex; gap: 14px; margin-left: auto; }
|
|
24574
|
+
.leg-item { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--text-soft); }
|
|
24575
|
+
.leg-dot { width: 10px; height: 10px; border-radius: 2px; }
|
|
24576
|
+
.zoom-info { font-size: 11px; color: var(--text-soft); font-family: var(--mono); }
|
|
24577
|
+
|
|
24578
|
+
.timeline-wrap { flex: 1; overflow: hidden; position: relative; display: flex; flex-direction: column; }
|
|
24579
|
+
|
|
24580
|
+
/* Tick labels row */
|
|
24581
|
+
.tick-row {
|
|
24582
|
+
display: flex; height: 32px; flex-shrink: 0;
|
|
24583
|
+
padding-left: var(--label-w); position: relative; overflow: hidden;
|
|
24584
|
+
border-bottom: 1px solid var(--border);
|
|
24585
|
+
}
|
|
24586
|
+
.tick-track { position: absolute; left: var(--label-w); right: 0; top: 0; bottom: 0; }
|
|
24587
|
+
|
|
24588
|
+
/* Lanes */
|
|
24589
|
+
.lanes-scroll { flex: 1; overflow-y: auto; overflow-x: hidden; position: relative; }
|
|
24590
|
+
.lanes-inner { position: relative; }
|
|
24591
|
+
.lane { display: flex; height: var(--lane-h); border-bottom: 1px solid var(--border-soft); }
|
|
24592
|
+
.lane:hover { background: rgba(255,255,255,.012); }
|
|
24593
|
+
.lane-label {
|
|
24594
|
+
width: var(--label-w); flex-shrink: 0; padding: 0 12px 0 16px;
|
|
24595
|
+
display: flex; align-items: center; border-right: 1px solid var(--border);
|
|
24596
|
+
position: sticky; left: 0; background: var(--bg); z-index: 2;
|
|
24597
|
+
}
|
|
24598
|
+
.lane-label-text { font-size: 11px; color: var(--text-soft); font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
24599
|
+
.lane-track { flex: 1; position: relative; }
|
|
24600
|
+
|
|
24601
|
+
/* Feature pill */
|
|
24602
|
+
.feat-pill {
|
|
24603
|
+
position: absolute; top: 8px; height: 34px;
|
|
24604
|
+
border-radius: 5px; cursor: pointer;
|
|
24605
|
+
display: flex; align-items: center; padding: 0 6px;
|
|
24606
|
+
transition: filter .15s, z-index .15s;
|
|
24607
|
+
border: 1px solid rgba(255,255,255,.08);
|
|
24608
|
+
min-width: 6px; overflow: hidden;
|
|
24609
|
+
}
|
|
24610
|
+
.feat-pill:hover { filter: brightness(1.3); z-index: 10; }
|
|
24611
|
+
.feat-pill-text { font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: rgba(255,255,255,.85); font-family: var(--mono); }
|
|
24612
|
+
|
|
24613
|
+
/* Dot for features without history */
|
|
24614
|
+
.feat-dot {
|
|
24615
|
+
position: absolute; top: 50%; width: 8px; height: 8px;
|
|
24616
|
+
border-radius: 50%; transform: translate(-50%, -50%);
|
|
24617
|
+
cursor: pointer; border: 1px solid rgba(255,255,255,.2);
|
|
24618
|
+
}
|
|
24619
|
+
|
|
24620
|
+
/* Today line */
|
|
24621
|
+
.today-line { position: absolute; top: 0; bottom: 0; width: 1.5px; background: rgba(212,168,83,.5); pointer-events: none; z-index: 5; }
|
|
24622
|
+
.today-label { position: absolute; top: 4px; font-size: 9px; color: var(--accent); font-family: var(--mono); transform: translateX(-50%); white-space: nowrap; }
|
|
24623
|
+
|
|
24624
|
+
/* Tooltip */
|
|
24625
|
+
.tooltip {
|
|
24626
|
+
position: fixed; pointer-events: none; z-index: 999;
|
|
24627
|
+
background: #1e1b18; border: 1px solid var(--border); border-radius: 8px;
|
|
24628
|
+
padding: 12px 14px; font-size: 12px; color: var(--text); max-width: 260px;
|
|
24629
|
+
display: none; box-shadow: 0 8px 24px rgba(0,0,0,.5);
|
|
24630
|
+
}
|
|
24631
|
+
.tooltip.visible { display: block; }
|
|
24632
|
+
.tooltip-title { font-weight: 600; margin-bottom: 6px; font-size: 13px; line-height: 1.3; }
|
|
24633
|
+
.tooltip-row { display: flex; justify-content: space-between; gap: 20px; margin: 2px 0; font-size: 11px; }
|
|
24634
|
+
.tooltip-label { color: var(--text-soft); }
|
|
24635
|
+
.tooltip-val { font-family: var(--mono); }
|
|
24636
|
+
.tooltip-hist { margin-top: 8px; border-top: 1px solid var(--border); padding-top: 8px; }
|
|
24637
|
+
.tooltip-trans { font-size: 10px; color: var(--text-soft); font-family: var(--mono); margin: 1px 0; }
|
|
24638
|
+
</style>
|
|
24639
|
+
</head>
|
|
24640
|
+
<body>
|
|
24641
|
+
<div class="topbar">
|
|
24642
|
+
<span class="topbar-logo">◈ lac</span>
|
|
24643
|
+
<span class="topbar-sep">|</span>
|
|
24644
|
+
<span class="topbar-project">${esc$1(projectName)} — Feature Timeline</span>
|
|
24645
|
+
<span class="topbar-count">${features.length} features · ${featuresWithHistory} with history · ${featuresWithoutHistory} undated</span>
|
|
24646
|
+
</div>
|
|
24647
|
+
|
|
24648
|
+
<div class="controls">
|
|
24649
|
+
<span class="ctrl-label">Zoom:</span>
|
|
24650
|
+
<button class="ctrl-btn" onclick="zoom(0.7)">−</button>
|
|
24651
|
+
<span class="zoom-info" id="zoom-info">100%</span>
|
|
24652
|
+
<button class="ctrl-btn" onclick="zoom(1.4)">+</button>
|
|
24653
|
+
<button class="ctrl-btn" onclick="resetZoom()">Reset</button>
|
|
24654
|
+
<span class="ctrl-sep">|</span>
|
|
24655
|
+
<span class="ctrl-label">Sort:</span>
|
|
24656
|
+
<button class="ctrl-btn" onclick="setSortMode('domain')">Domain</button>
|
|
24657
|
+
<button class="ctrl-btn" onclick="setSortMode('start')">Start date</button>
|
|
24658
|
+
<button class="ctrl-btn" onclick="setSortMode('status')">Status</button>
|
|
24659
|
+
<div class="legend">
|
|
24660
|
+
${Object.entries(STATUS_COLOR).map(([s, c]) => `<div class="leg-item"><div class="leg-dot" style="background:${c}"></div>${s}</div>`).join("")}
|
|
24661
|
+
<div class="leg-item"><div class="leg-dot" style="background:#3a3530;border:1px solid #5a5550"></div>no history</div>
|
|
24662
|
+
</div>
|
|
24663
|
+
</div>
|
|
24664
|
+
|
|
24665
|
+
<div class="timeline-wrap">
|
|
24666
|
+
<div class="tick-row">
|
|
24667
|
+
<div class="tick-track" id="tick-track"></div>
|
|
24668
|
+
</div>
|
|
24669
|
+
<div class="lanes-scroll">
|
|
24670
|
+
<div class="lanes-inner" id="lanes"></div>
|
|
24671
|
+
</div>
|
|
24672
|
+
</div>
|
|
24673
|
+
|
|
24674
|
+
<div class="tooltip" id="tooltip"></div>
|
|
24675
|
+
|
|
24676
|
+
<script>
|
|
24677
|
+
const DATA = ${dataJson};
|
|
24678
|
+
const STATUS_COLOR = ${statusColorJson};
|
|
24679
|
+
|
|
24680
|
+
let zoomLevel = 1;
|
|
24681
|
+
let sortMode = 'domain';
|
|
24682
|
+
const tooltip = document.getElementById('tooltip');
|
|
24683
|
+
|
|
24684
|
+
// Scale: zoomLevel * 100% width for the track
|
|
24685
|
+
function trackWidth() { return Math.round(zoomLevel * 100) + '%'; }
|
|
24686
|
+
|
|
24687
|
+
function pctToPx(pct) {
|
|
24688
|
+
const track = document.getElementById('lanes');
|
|
24689
|
+
return (pct / 100) * track.offsetWidth;
|
|
24690
|
+
}
|
|
24691
|
+
|
|
24692
|
+
function render() {
|
|
24693
|
+
document.getElementById('zoom-info').textContent = Math.round(zoomLevel * 100) + '%';
|
|
24694
|
+
renderTicks();
|
|
24695
|
+
renderLanes();
|
|
24696
|
+
}
|
|
24697
|
+
|
|
24698
|
+
function renderTicks() {
|
|
24699
|
+
const track = document.getElementById('tick-track');
|
|
24700
|
+
track.style.width = trackWidth();
|
|
24701
|
+
track.innerHTML = DATA.ticks.map(t =>
|
|
24702
|
+
t.pct >= 0 && t.pct <= 100
|
|
24703
|
+
? \`<div style="position:absolute;left:\${t.pct}%;top:0;bottom:0;border-left:1px solid #2a2724;padding-top:8px">
|
|
24704
|
+
<span style="font-size:9px;color:#6a6055;font-family:var(--mono);padding-left:4px">\${t.label}</span>
|
|
24705
|
+
</div>\`
|
|
24706
|
+
: ''
|
|
24707
|
+
).join('') + \`<div class="today-line" style="left:\${DATA.todayPct}%">
|
|
24708
|
+
<div class="today-label" style="left:50%">today</div>
|
|
24709
|
+
</div>\`;
|
|
24710
|
+
}
|
|
24711
|
+
|
|
24712
|
+
function renderLanes() {
|
|
24713
|
+
const lanesEl = document.getElementById('lanes');
|
|
24714
|
+
// Sort domains
|
|
24715
|
+
let domainOrder = [...DATA.domains];
|
|
24716
|
+
if (sortMode === 'status') {
|
|
24717
|
+
// sort domains by avg frozen ratio descending
|
|
24718
|
+
}
|
|
24719
|
+
|
|
24720
|
+
lanesEl.innerHTML = domainOrder.map(domain => {
|
|
24721
|
+
const domFeats = DATA.features.filter(f => f.domain === domain);
|
|
24722
|
+
// Sort features within domain
|
|
24723
|
+
let sorted = [...domFeats];
|
|
24724
|
+
if (sortMode === 'start') sorted.sort((a,b) => (a.startPct??101) - (b.startPct??101));
|
|
24725
|
+
else if (sortMode === 'status') sorted.sort((a,b) => a.status.localeCompare(b.status));
|
|
24726
|
+
else sorted.sort((a,b) => (a.startPct??101) - (b.startPct??101));
|
|
24727
|
+
|
|
24728
|
+
const laneH = Math.max(${DOMAIN_ORDER.length > 0 ? "Math.ceil(sorted.length / 1) * 44 + 8" : "52"}, 52);
|
|
24729
|
+
|
|
24730
|
+
const pills = sorted.map((f, fi) => {
|
|
24731
|
+
const color = STATUS_COLOR[f.status] || '#444';
|
|
24732
|
+
const bgAlpha = f.hasHistory ? '55' : '22';
|
|
24733
|
+
const top = 8 + Math.floor(fi * 0) ; // stack vertically — one row per feature lane
|
|
24734
|
+
if (f.startPct !== null && f.endPct !== null) {
|
|
24735
|
+
const left = Math.max(0, f.startPct);
|
|
24736
|
+
const width = Math.max(0.4, f.endPct - f.startPct);
|
|
24737
|
+
return \`<div class="feat-pill"
|
|
24738
|
+
style="left:\${left}%;width:\${width}%;background:\${color}\${bgAlpha};top:8px"
|
|
24739
|
+
data-key="\${f.key}"
|
|
24740
|
+
onmousemove="showTooltip(event, '\${f.key}')"
|
|
24741
|
+
onmouseleave="hideTooltip()"
|
|
24742
|
+
onclick="window.open('lac-wiki.html#\${f.key}','_self')">
|
|
24743
|
+
\${width > 2 ? \`<span class="feat-pill-text">\${f.title}</span>\` : ''}
|
|
24744
|
+
</div>\`;
|
|
24745
|
+
} else {
|
|
24746
|
+
// dot at end of timeline
|
|
24747
|
+
return \`<div class="feat-dot"
|
|
24748
|
+
style="left:\${DATA.todayPct}%;background:\${color}44;border-color:\${color}88"
|
|
24749
|
+
data-key="\${f.key}"
|
|
24750
|
+
onmousemove="showTooltip(event, '\${f.key}')"
|
|
24751
|
+
onmouseleave="hideTooltip()"
|
|
24752
|
+
onclick="window.open('lac-wiki.html#\${f.key}','_self')">
|
|
24753
|
+
</div>\`;
|
|
24754
|
+
}
|
|
24755
|
+
}).join('');
|
|
24756
|
+
|
|
24757
|
+
return \`<div class="lane" style="height:var(--lane-h)">
|
|
24758
|
+
<div class="lane-label"><span class="lane-label-text" title="\${domain}">\${domain}</span></div>
|
|
24759
|
+
<div class="lane-track" style="width:\${trackWidth()}">
|
|
24760
|
+
\${pills}
|
|
24761
|
+
<div class="today-line" style="left:\${DATA.todayPct}%"></div>
|
|
24762
|
+
</div>
|
|
24763
|
+
</div>\`;
|
|
24764
|
+
}).join('');
|
|
24765
|
+
}
|
|
24766
|
+
|
|
24767
|
+
const featByKey = new Map(DATA.features.map(f => [f.key, f]));
|
|
24768
|
+
|
|
24769
|
+
function showTooltip(e, key) {
|
|
24770
|
+
const f = featByKey.get(key);
|
|
24771
|
+
if (!f) return;
|
|
24772
|
+
const color = STATUS_COLOR[f.status] || '#888';
|
|
24773
|
+
const histHtml = f.transitions.length > 0
|
|
24774
|
+
? '<div class="tooltip-hist">' + f.transitions.map(t =>
|
|
24775
|
+
\`<div class="tooltip-trans">\${t.date} · \${t.from || '–'} → \${t.to}</div>\`
|
|
24776
|
+
).join('') + '</div>'
|
|
24777
|
+
: '<div class="tooltip-row"><span class="tooltip-label">history</span><span class="tooltip-val" style="color:#4a4540">not recorded</span></div>';
|
|
24778
|
+
|
|
24779
|
+
tooltip.innerHTML =
|
|
24780
|
+
\`<div class="tooltip-title">\${f.title}</div>\` +
|
|
24781
|
+
\`<div class="tooltip-row"><span class="tooltip-label">status</span><span class="tooltip-val" style="color:\${color}">\${f.status}</span></div>\` +
|
|
24782
|
+
\`<div class="tooltip-row"><span class="tooltip-label">domain</span><span class="tooltip-val">\${f.domain}</span></div>\` +
|
|
24783
|
+
\`<div class="tooltip-row"><span class="tooltip-label">key</span><span class="tooltip-val">\${f.key}</span></div>\` +
|
|
24784
|
+
histHtml;
|
|
24785
|
+
|
|
24786
|
+
tooltip.style.left = (e.clientX + 14) + 'px';
|
|
24787
|
+
tooltip.style.top = (e.clientY - 10) + 'px';
|
|
24788
|
+
tooltip.classList.add('visible');
|
|
24789
|
+
}
|
|
24790
|
+
function hideTooltip() { tooltip.classList.remove('visible'); }
|
|
24791
|
+
|
|
24792
|
+
function zoom(factor) {
|
|
24793
|
+
zoomLevel = Math.max(0.5, Math.min(8, zoomLevel * factor));
|
|
24794
|
+
render();
|
|
24795
|
+
}
|
|
24796
|
+
function resetZoom() { zoomLevel = 1; render(); }
|
|
24797
|
+
function setSortMode(mode) { sortMode = mode; render(); }
|
|
24798
|
+
|
|
24799
|
+
render();
|
|
24800
|
+
window.addEventListener('resize', render);
|
|
24801
|
+
<\/script>
|
|
24802
|
+
</body>
|
|
24803
|
+
</html>`;
|
|
24804
|
+
}
|
|
24805
|
+
//#endregion
|
|
23232
24806
|
//#region src/lib/views.ts
|
|
23233
24807
|
/** Fields always shown at summary density regardless of view */
|
|
23234
24808
|
const SUMMARY_FIELDS = new Set([
|
|
@@ -23679,7 +25253,7 @@ function buildReconstructionPrompt(features, projectName, promptDir) {
|
|
|
23679
25253
|
lines.push("");
|
|
23680
25254
|
return lines.join("\n");
|
|
23681
25255
|
}
|
|
23682
|
-
const exportCommand = new Command("export").description("Export feature.json as JSON, Markdown, or generate a static HTML view").option("--out <path>", "Output file or directory path").option("--html [dir]", "Scan <dir> (default: cwd) and emit a single self-contained HTML wiki").option("--raw [dir]", "Raw field-by-field HTML dump with sidebar navigation").option("--print [dir]", "Print-ready HTML document (A4, all features, @media print CSS)").option("--postcard", "Beautiful single-feature shareable card (nearest feature.json)").option("--resume [dir]", "Portfolio page from all frozen features").option("--slide [dir]", "Full-screen HTML slideshow, one slide per feature").option("--graph [dir]", "Interactive force-directed feature lineage graph").option("--heatmap [dir]", "Completeness heatmap — fields × features grid").option("--quiz [dir]", "Flashcard-style quiz to test knowledge of your feature set").option("--story [dir]", "Long-form narrative document — product case study from feature data").option("--treemap [dir]", "Rectangular treemap — features sized by decisions × completeness, grouped by domain").option("--kanban [dir]", "Kanban board — Active / Frozen / Draft columns with sortable, filterable cards").option("--health [dir]", "Project health scorecard — completeness, coverage, tech debt, and health score").option("--embed [dir]", "Compact embeddable stats widget (iframe-ready)").option("--decisions [dir]", "Consolidated ADR — all decisions from all features, searchable by domain").option("--guide [dir]", "User guide — one page per feature that has a non-empty userGuide field").option("--hub [dir]", "Hub landing page linking to all generated views → lac-hub.html").option("--all [dir]", "Generate all HTML views + hub index.html → --out dir (default: ./lac-output)").option("--prefix <prefix>", "URL prefix for hub links (no leading slash), e.g. lac → hrefs become /lac/lac-guide.html").option("--diff <dir-b>", "Compare cwd workspace against <dir-b> and show added/removed/changed").option("--site <dir>", "Generate a multi-page static site → --out dir (default: ./lac-site)").option("--prompt [dir]", "AI reconstruction prompt for all features (stdout or --out file)").option("--markdown", "Single feature as Markdown (nearest feature.json)").option("--changelog [dir]", "Structured changelog grouped by month — from revisions[] across all features").option("--since <date>", "Filter --changelog and --release-notes to entries after this date (YYYY-MM-DD)").option("--release-notes [dir]", "User-facing release notes — features that went frozen since --since date or --release version").option("--release <version>", "Filter --release-notes to features matching this releaseVersion (e.g. 3.5.0)").option("--sprint [dir]", "Sprint planning view — draft+active features sorted by priority, summary density").option("--api-surface [dir]", "Aggregated publicInterface[] reference across all features → lac-api-surface.html").option("--dependency-map [dir]", "Runtime dependency graph from externalDependencies[] → lac-depmap.html").option("--tags <tags>", "Comma-separated tags to filter by (OR logic) — applies to all multi-feature modes").option("--sort <mode>", "Sort order for multi-feature modes: key (default) | build-order (parents before children)").option("--view <name>", `Audience view — built-in (${VIEW_NAMES.join(", ")}) or custom name from lac.config.json views`).option("--density <level>", "Content density: summary | standard | verbose (default: standard)").addHelpText("after", `
|
|
25256
|
+
const exportCommand = new Command("export").description("Export feature.json as JSON, Markdown, or generate a static HTML view").option("--out <path>", "Output file or directory path").option("--html [dir]", "Scan <dir> (default: cwd) and emit a single self-contained HTML wiki").option("--raw [dir]", "Raw field-by-field HTML dump with sidebar navigation").option("--print [dir]", "Print-ready HTML document (A4, all features, @media print CSS)").option("--postcard", "Beautiful single-feature shareable card (nearest feature.json)").option("--resume [dir]", "Portfolio page from all frozen features").option("--slide [dir]", "Full-screen HTML slideshow, one slide per feature").option("--graph [dir]", "Interactive force-directed feature lineage graph").option("--heatmap [dir]", "Completeness heatmap — fields × features grid").option("--quiz [dir]", "Flashcard-style quiz to test knowledge of your feature set").option("--story [dir]", "Long-form narrative document — product case study from feature data").option("--treemap [dir]", "Rectangular treemap — features sized by decisions × completeness, grouped by domain").option("--kanban [dir]", "Kanban board — Active / Frozen / Draft columns with sortable, filterable cards").option("--health [dir]", "Project health scorecard — completeness, coverage, tech debt, and health score").option("--embed [dir]", "Compact embeddable stats widget (iframe-ready)").option("--decisions [dir]", "Consolidated ADR — all decisions from all features, searchable by domain").option("--guide [dir]", "User guide — one page per feature that has a non-empty userGuide field").option("--hub [dir]", "Hub landing page linking to all generated views → lac-hub.html").option("--all [dir]", "Generate all HTML views + hub index.html → --out dir (default: ./lac-output)").option("--prefix <prefix>", "URL prefix for hub links (no leading slash), e.g. lac → hrefs become /lac/lac-guide.html").option("--diff <dir-b>", "Compare cwd workspace against <dir-b> and show added/removed/changed").option("--site <dir>", "Generate a multi-page static site → --out dir (default: ./lac-site)").option("--prompt [dir]", "AI reconstruction prompt for all features (stdout or --out file)").option("--markdown", "Single feature as Markdown (nearest feature.json)").option("--changelog [dir]", "Structured changelog grouped by month — from revisions[] across all features").option("--since <date>", "Filter --changelog and --release-notes to entries after this date (YYYY-MM-DD)").option("--release-notes [dir]", "User-facing release notes — features that went frozen since --since date or --release version").option("--release <version>", "Filter --release-notes to features matching this releaseVersion (e.g. 3.5.0)").option("--sprint [dir]", "Sprint planning view — draft+active features sorted by priority, summary density").option("--api-surface [dir]", "Aggregated publicInterface[] reference across all features → lac-api-surface.html").option("--dependency-map [dir]", "Runtime dependency graph from externalDependencies[] → lac-depmap.html").option("--radar [dir]", "Domain maturity radar — SVG polar chart across 5 quality dimensions → lac-radar.html").option("--successboard [dir]", "Success criteria board — achieved/in-progress/planned by successCriteria → lac-successboard.html").option("--pitch [dir]", "Demo slide deck — keyboard-navigable fullscreen presentation → lac-pitch.html").option("--timeline [dir]", "Feature velocity timeline — swim-lane history from statusHistory → lac-timeline.html").option("--tags <tags>", "Comma-separated tags to filter by (OR logic) — applies to all multi-feature modes").option("--sort <mode>", "Sort order for multi-feature modes: key (default) | build-order (parents before children)").option("--view <name>", `Audience view — built-in (${VIEW_NAMES.join(", ")}) or custom name from lac.config.json views`).option("--density <level>", "Content density: summary | standard | verbose (default: standard)").addHelpText("after", `
|
|
23683
25257
|
Examples:
|
|
23684
25258
|
lac export --html HTML wiki (cwd) → lac-wiki.html
|
|
23685
25259
|
lac export --raw Raw field dump → lac-raw.html
|
|
@@ -23717,6 +25291,7 @@ Views (--view):
|
|
|
23717
25291
|
tech Complete technical record — all fields including history and revisions`).action(async (options) => {
|
|
23718
25292
|
const config = loadConfig(process$1.cwd());
|
|
23719
25293
|
let activeView = options.view ? resolveView(options.view, config.views) : void 0;
|
|
25294
|
+
const activeViewRenderMode = options.view && config.views[options.view] ? config.views[options.view].extends : void 0;
|
|
23720
25295
|
if (options.view && !activeView) {
|
|
23721
25296
|
const customNames = Object.keys(config.views);
|
|
23722
25297
|
const allNames = [...VIEW_NAMES, ...customNames];
|
|
@@ -23782,7 +25357,7 @@ Views (--view):
|
|
|
23782
25357
|
}
|
|
23783
25358
|
const projectName = basename(htmlDir);
|
|
23784
25359
|
const densityFeatures = withDensity(features);
|
|
23785
|
-
const html = generateHtmlWiki(activeView ? densityFeatures.map((f) => applyViewForHtml(f.feature, activeView)) : densityFeatures.map((f) => f.feature), projectName, activeView?.label, activeView?.name);
|
|
25360
|
+
const html = generateHtmlWiki(activeView ? densityFeatures.map((f) => applyViewForHtml(f.feature, activeView)) : densityFeatures.map((f) => f.feature), projectName, activeView?.label, activeView?.name, activeViewRenderMode);
|
|
23786
25361
|
const outFile = options.out ? resolve(options.out) : resolve(process$1.cwd(), "lac-wiki.html");
|
|
23787
25362
|
try {
|
|
23788
25363
|
await writeFile(outFile, html, "utf-8");
|
|
@@ -24223,6 +25798,80 @@ Views (--view):
|
|
|
24223
25798
|
}
|
|
24224
25799
|
return;
|
|
24225
25800
|
}
|
|
25801
|
+
if (options.radar !== void 0) {
|
|
25802
|
+
const dir = typeof options.radar === "string" ? resolve(options.radar) : resolve(process$1.cwd());
|
|
25803
|
+
const features = await scanAndFilter(dir);
|
|
25804
|
+
if (features.length === 0) {
|
|
25805
|
+
process$1.stdout.write(`No valid feature.json files found in "${dir}".\n`);
|
|
25806
|
+
process$1.exit(0);
|
|
25807
|
+
}
|
|
25808
|
+
const html = generateRadar(features.map((f) => f.feature), basename(dir));
|
|
25809
|
+
const outFile = options.out ? resolve(options.out) : resolve(process$1.cwd(), "lac-radar.html");
|
|
25810
|
+
try {
|
|
25811
|
+
await writeFile(outFile, html, "utf-8");
|
|
25812
|
+
process$1.stdout.write(`✓ Radar (${features.length} features, ${new Set(features.map((f) => f.feature.domain)).size} domains) → ${options.out ?? "lac-radar.html"}\n`);
|
|
25813
|
+
} catch (err) {
|
|
25814
|
+
process$1.stderr.write(`Error writing "${outFile}": ${err instanceof Error ? err.message : String(err)}\n`);
|
|
25815
|
+
process$1.exit(1);
|
|
25816
|
+
}
|
|
25817
|
+
return;
|
|
25818
|
+
}
|
|
25819
|
+
if (options.successboard !== void 0) {
|
|
25820
|
+
const dir = typeof options.successboard === "string" ? resolve(options.successboard) : resolve(process$1.cwd());
|
|
25821
|
+
const features = await scanAndFilter(dir);
|
|
25822
|
+
if (features.length === 0) {
|
|
25823
|
+
process$1.stdout.write(`No valid feature.json files found in "${dir}".\n`);
|
|
25824
|
+
process$1.exit(0);
|
|
25825
|
+
}
|
|
25826
|
+
const fs = features.map((f) => f.feature);
|
|
25827
|
+
const html = generateSuccessboard(fs, basename(dir));
|
|
25828
|
+
const withCriteria = fs.filter((f) => f["successCriteria"] || Array.isArray(f["acceptanceCriteria"])).length;
|
|
25829
|
+
const outFile = options.out ? resolve(options.out) : resolve(process$1.cwd(), "lac-successboard.html");
|
|
25830
|
+
try {
|
|
25831
|
+
await writeFile(outFile, html, "utf-8");
|
|
25832
|
+
process$1.stdout.write(`✓ Success board (${withCriteria}/${features.length} features with criteria) → ${options.out ?? "lac-successboard.html"}\n`);
|
|
25833
|
+
} catch (err) {
|
|
25834
|
+
process$1.stderr.write(`Error writing "${outFile}": ${err instanceof Error ? err.message : String(err)}\n`);
|
|
25835
|
+
process$1.exit(1);
|
|
25836
|
+
}
|
|
25837
|
+
return;
|
|
25838
|
+
}
|
|
25839
|
+
if (options.pitch !== void 0) {
|
|
25840
|
+
const dir = typeof options.pitch === "string" ? resolve(options.pitch) : resolve(process$1.cwd());
|
|
25841
|
+
const features = await scanAndFilter(dir);
|
|
25842
|
+
if (features.length === 0) {
|
|
25843
|
+
process$1.stdout.write(`No valid feature.json files found in "${dir}".\n`);
|
|
25844
|
+
process$1.exit(0);
|
|
25845
|
+
}
|
|
25846
|
+
const html = generatePitch(features.map((f) => f.feature), basename(dir));
|
|
25847
|
+
const outFile = options.out ? resolve(options.out) : resolve(process$1.cwd(), "lac-pitch.html");
|
|
25848
|
+
try {
|
|
25849
|
+
await writeFile(outFile, html, "utf-8");
|
|
25850
|
+
process$1.stdout.write(`✓ Pitch deck (${features.length} features) → ${options.out ?? "lac-pitch.html"}\n`);
|
|
25851
|
+
} catch (err) {
|
|
25852
|
+
process$1.stderr.write(`Error writing "${outFile}": ${err instanceof Error ? err.message : String(err)}\n`);
|
|
25853
|
+
process$1.exit(1);
|
|
25854
|
+
}
|
|
25855
|
+
return;
|
|
25856
|
+
}
|
|
25857
|
+
if (options.timeline !== void 0) {
|
|
25858
|
+
const dir = typeof options.timeline === "string" ? resolve(options.timeline) : resolve(process$1.cwd());
|
|
25859
|
+
const features = await scanAndFilter(dir);
|
|
25860
|
+
if (features.length === 0) {
|
|
25861
|
+
process$1.stdout.write(`No valid feature.json files found in "${dir}".\n`);
|
|
25862
|
+
process$1.exit(0);
|
|
25863
|
+
}
|
|
25864
|
+
const html = generateTimeline(features.map((f) => f.feature), basename(dir));
|
|
25865
|
+
const outFile = options.out ? resolve(options.out) : resolve(process$1.cwd(), "lac-timeline.html");
|
|
25866
|
+
try {
|
|
25867
|
+
await writeFile(outFile, html, "utf-8");
|
|
25868
|
+
process$1.stdout.write(`✓ Timeline (${features.length} features) → ${options.out ?? "lac-timeline.html"}\n`);
|
|
25869
|
+
} catch (err) {
|
|
25870
|
+
process$1.stderr.write(`Error writing "${outFile}": ${err instanceof Error ? err.message : String(err)}\n`);
|
|
25871
|
+
process$1.exit(1);
|
|
25872
|
+
}
|
|
25873
|
+
return;
|
|
25874
|
+
}
|
|
24226
25875
|
if (options.all !== void 0) {
|
|
24227
25876
|
const dir = typeof options.all === "string" ? resolve(options.all) : resolve(process$1.cwd());
|
|
24228
25877
|
const outDir = resolve(options.out ?? "./lac-output");
|
|
@@ -24264,6 +25913,10 @@ Views (--view):
|
|
|
24264
25913
|
await write("lac-sprint.html", generateSprint(fs.filter((f) => f.status === "draft" || f.status === "active"), projectName));
|
|
24265
25914
|
await write("lac-api-surface.html", generateApiSurface(fs, projectName));
|
|
24266
25915
|
await write("lac-depmap.html", generateDependencyMap(fs, projectName));
|
|
25916
|
+
await write("lac-radar.html", generateRadar(fs, projectName));
|
|
25917
|
+
await write("lac-successboard.html", generateSuccessboard(fs, projectName));
|
|
25918
|
+
await write("lac-pitch.html", generatePitch(fs, projectName));
|
|
25919
|
+
await write("lac-timeline.html", generateTimeline(fs, projectName));
|
|
24267
25920
|
const stats = {
|
|
24268
25921
|
total: fs.length,
|
|
24269
25922
|
frozen: fs.filter((f) => f.status === "frozen").length,
|
|
@@ -24312,7 +25965,9 @@ Views (--view):
|
|
|
24312
25965
|
filterStatus: resolved.filterStatus,
|
|
24313
25966
|
sortBy: resolved.sortBy
|
|
24314
25967
|
});
|
|
24315
|
-
|
|
25968
|
+
const viewHtmlFeatures = viewFeatures.map((f) => applyViewForHtml(f, resolved));
|
|
25969
|
+
const renderMode = viewDef.extends;
|
|
25970
|
+
await write(filename, generateHtmlWiki(viewHtmlFeatures, projectName, label, viewName, renderMode));
|
|
24316
25971
|
customEntries.push({
|
|
24317
25972
|
file: filename,
|
|
24318
25973
|
label,
|
|
@@ -24322,7 +25977,7 @@ Views (--view):
|
|
|
24322
25977
|
});
|
|
24323
25978
|
}
|
|
24324
25979
|
await write("index.html", generateHub(projectName, stats, [...ALL_HUB_ENTRIES, ...customEntries], (/* @__PURE__ */ new Date()).toISOString(), options.prefix));
|
|
24325
|
-
const totalFiles =
|
|
25980
|
+
const totalFiles = 19 + customEntries.length + 1;
|
|
24326
25981
|
process$1.stdout.write(`Done — ${features.length} features, ${totalFiles} files written to ${outDir}\n`);
|
|
24327
25982
|
return;
|
|
24328
25983
|
}
|