@mafianacho/nacholens 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.de.md +46 -0
- package/README.fr.md +46 -0
- package/README.ja.md +46 -0
- package/README.ko.md +46 -0
- package/README.md +45 -2
- package/README.zh.md +46 -0
- package/demo/shop/v1/README.md +7 -0
- package/demo/shop/v1/api.js +78 -0
- package/demo/shop/v1/db.js +38 -0
- package/demo/shop/v1/gateway.js +50 -0
- package/demo/shop/v1/mock-external.js +17 -0
- package/demo/shop/v1/public/app.js +32 -0
- package/demo/shop/v1/public/format.js +2 -0
- package/demo/shop/v1/public/index.html +13 -0
- package/demo/shop/v1/public/styles.css +4 -0
- package/demo/shop/v2/README.md +7 -0
- package/demo/shop/v2/api.js +88 -0
- package/demo/shop/v2/db.js +38 -0
- package/demo/shop/v2/gateway.js +50 -0
- package/demo/shop/v2/mock-external.js +17 -0
- package/demo/shop/v2/public/app.js +33 -0
- package/demo/shop/v2/public/format.js +3 -0
- package/demo/shop/v2/public/index.html +13 -0
- package/demo/shop/v2/public/styles.css +5 -0
- package/docs/guide.ko.md +97 -0
- package/docs/guide.md +142 -0
- package/docs/model-schema.md +137 -0
- package/docs/review.md +78 -0
- package/examples/demo-shop.artifact.html +1535 -0
- package/examples/demo-shop.artifact.json +7 -0
- package/examples/demo-shop.html +1535 -0
- package/examples/external/README.md +12 -0
- package/examples/external/full-stack-fastapi-template/LICENSE +21 -0
- package/examples/external/full-stack-fastapi-template/changes.json +56 -0
- package/examples/external/full-stack-fastapi-template/full-stack-fastapi-template-backend.html +1535 -0
- package/models/demo-shop/changes.auto.json +121 -0
- package/models/demo-shop/changes.example.json +16 -0
- package/models/demo-shop/intent.example.json +75 -0
- package/models/demo-shop/model.json +3151 -0
- package/package.json +60 -4
- package/skills/nacholens/SKILL.md +235 -0
- package/src/cli.js +377 -0
- package/src/demo.js +42 -0
- package/src/diff.js +254 -0
- package/src/gitio.js +67 -0
- package/src/install.js +94 -0
- package/src/model.js +434 -0
- package/src/page.js +87 -0
- package/src/player/fonts/FONTLOG.md +30 -0
- package/src/player/fonts/OFL-Anton.txt +93 -0
- package/src/player/fonts/OFL-IBMPlex.txt +94 -0
- package/src/player/fonts/anton-400.woff2 +0 -0
- package/src/player/fonts/nacho-mono-500.woff2 +0 -0
- package/src/player/fonts/nacho-mono-600.woff2 +0 -0
- package/src/player/fonts/nacho-sans-400.woff2 +0 -0
- package/src/player/fonts/nacho-sans-500.woff2 +0 -0
- package/src/player/fonts/nacho-sans-600.woff2 +0 -0
- package/src/player/fonts/nacho-sans-700.woff2 +0 -0
- package/src/player/player.css +318 -0
- package/src/player/player.js +872 -0
- package/src/player/review.css +49 -0
- package/src/player/review.js +230 -0
- package/src/player/template.html +58 -0
- package/src/review.js +321 -0
- package/src/scan.js +255 -0
- package/src/template.js +36 -0
- package/src/verify.js +90 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* Review dock (below the stage). Tokens and shared components (badge, check, seg, link, steplink, empty) come from player.css. */
|
|
2
|
+
.dock { display: flex; flex-direction: column; min-height: 0; min-width: 0; border-top: 1px solid var(--border); background: var(--surface); position: relative; }
|
|
3
|
+
.dock.collapsed { height: auto; }
|
|
4
|
+
.dock.collapsed .grip { display: none; }
|
|
5
|
+
.grip { height: 7px; margin-top: -4px; cursor: row-resize; position: relative; z-index: 3; flex: none; }
|
|
6
|
+
.grip::after { content: ""; position: absolute; left: 50%; top: 2px; width: 36px; height: 3px; margin-left: -18px; border-radius: 2px; background: var(--border-strong); opacity: 0; transition: opacity var(--t-fast); }
|
|
7
|
+
.grip:hover::after { opacity: 1; }
|
|
8
|
+
.dtabs { display: flex; align-items: stretch; gap: 2px; padding: 0 var(--s2) 0 var(--s3); border-bottom: 1px solid var(--border); flex: none; overflow-x: auto; }
|
|
9
|
+
.dock.collapsed .dtabs { border-bottom: 0; }
|
|
10
|
+
.dtabs .grow { flex: 1; }
|
|
11
|
+
.dtabs .fold { align-self: center; }
|
|
12
|
+
.dtabs .fold svg.i { transition: transform var(--t-base) var(--ease-out); }
|
|
13
|
+
.dock.collapsed .dtabs .fold svg.i { transform: rotate(180deg); }
|
|
14
|
+
.dtab { display: inline-flex; align-items: center; gap: var(--s2); height: 44px; padding: 0 var(--s3); border: 0; background: none; color: var(--text-2); font-weight: 500; position: relative; white-space: nowrap; transition: color var(--t-fast); }
|
|
15
|
+
.dtab:hover { color: var(--text); }
|
|
16
|
+
.dtab[aria-selected="true"] { color: var(--text); font-weight: 600; }
|
|
17
|
+
.dtab[aria-selected="true"]::after { content: ""; position: absolute; left: var(--s3); right: var(--s3); bottom: -1px; height: 2px; border-radius: 2px; background: var(--accent-line); }
|
|
18
|
+
.dtab .role { font-size: 11px; font-weight: 600; color: var(--text-3); padding: 0 5px; border: 1px solid var(--border); height: 18px; display: inline-flex; align-items: center; border-radius: var(--r-sm); background: var(--sunken); }
|
|
19
|
+
.dtab .left { font-size: var(--fs-xs); font-weight: 700; color: var(--t); font-variant-numeric: tabular-nums; }
|
|
20
|
+
.dtab .left.none { color: var(--text-4); font-weight: 600; }
|
|
21
|
+
.dtab .ok { color: var(--success); display: inline-flex; }
|
|
22
|
+
.dbody { flex: 1; min-height: 0; overflow: auto; }
|
|
23
|
+
.dhead { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s1) var(--s3); padding: var(--s3) var(--s4) var(--s2); }
|
|
24
|
+
.dhead .q { font-weight: 600; }
|
|
25
|
+
.dhead .sub { font-size: var(--fs-sm); color: var(--text-3); }
|
|
26
|
+
.dhead .grow { flex: 1; }
|
|
27
|
+
.rowlist { list-style: none; margin: 0; padding: 0 var(--s2) var(--s3); }
|
|
28
|
+
.ri { display: flex; align-items: flex-start; gap: var(--s3); padding: var(--s2); border-radius: var(--r-md); transition: background var(--t-fast); }
|
|
29
|
+
.ri:hover { background: var(--hover); }
|
|
30
|
+
.ri .check { margin-top: 1px; }
|
|
31
|
+
.ri .gap { width: 18px; flex: none; }
|
|
32
|
+
.ri .main { flex: 1; min-width: 0; }
|
|
33
|
+
.ri .main.small { font-size: var(--fs-sm); }
|
|
34
|
+
.ri .t { font-weight: 500; }
|
|
35
|
+
.ri .t.mono { font-size: 13px; }
|
|
36
|
+
.ri .m { font-size: var(--fs-xs); color: var(--text-3); margin-top: 1px; display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; }
|
|
37
|
+
.ri .end { display: flex; gap: 6px; align-items: center; flex: none; }
|
|
38
|
+
.ri.done .t { color: var(--text-3); text-decoration: line-through; text-decoration-color: var(--text-4); }
|
|
39
|
+
.ri .link.plain { color: inherit; font-weight: 500; text-align: left; }
|
|
40
|
+
.chev { border: 0; background: none; padding: 2px; color: var(--text-3); border-radius: var(--r-sm); display: inline-flex; }
|
|
41
|
+
.chev svg.i { transition: transform var(--t-base) var(--ease-out); }
|
|
42
|
+
.chev[aria-expanded="true"] svg.i { transform: rotate(180deg); }
|
|
43
|
+
.files { list-style: none; margin: 6px 0 0; padding: 0; font-family: var(--mono); font-size: 12px; color: var(--text-2); }
|
|
44
|
+
.files li { display: flex; gap: var(--s2); padding: 2px 0; }
|
|
45
|
+
.files li span:first-child { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
46
|
+
.files .ln, .files .st { color: var(--text-4); flex: none; }
|
|
47
|
+
.subhead { display: flex; align-items: center; gap: var(--s2); padding: var(--s3) var(--s4) var(--s1); font-size: var(--fs-xs); font-weight: 600; color: var(--text-3); }
|
|
48
|
+
.subhead .link { font-size: var(--fs-xs); }
|
|
49
|
+
@media (max-width: 1100px) { .dock { height: auto; max-height: 70vh; } }
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* Review dock: the three review tracks under the stage — planner intent (planner), regression tests (QA) and changes the
|
|
2
|
+
model does not cover (dev). Each tab carries its own status (a dot and the count still to check), so the tab bar is
|
|
3
|
+
the to-do summary of the page. Rows link to steps through the player API and never repeat a step's details.
|
|
4
|
+
Ticks are a per-viewer convenience (browser storage, may be unavailable). Text nodes only. */
|
|
5
|
+
(function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
var doc = document, root = doc.getElementById("sv-review");
|
|
8
|
+
if (!root) return;
|
|
9
|
+
var api = window.__nacholens || null;
|
|
10
|
+
function json(id) { var e = doc.getElementById(id); try { return e ? JSON.parse(e.textContent) : null; } catch (x) { return null; } }
|
|
11
|
+
var model = api ? api.model : json("sv-model"), changes = api ? api.changes : json("sv-changes"), data = json("sv-review-data") || {};
|
|
12
|
+
if (!changes && !data.intent && !data.impact) return; // nothing to review: the dock stays hidden
|
|
13
|
+
|
|
14
|
+
/* ── helpers ── */
|
|
15
|
+
function h(tag, a) {
|
|
16
|
+
var e = doc.createElement(tag);
|
|
17
|
+
if (a) Object.keys(a).forEach(function (k) {
|
|
18
|
+
var v = a[k]; if (v == null || v === false) return;
|
|
19
|
+
if (k === "on") Object.keys(v).forEach(function (x) { e.addEventListener(x, v[x]); });
|
|
20
|
+
else if (k === "text") e.textContent = v;
|
|
21
|
+
else e.setAttribute(k, v === true ? "" : v);
|
|
22
|
+
});
|
|
23
|
+
for (var i = 2; i < arguments.length; i++) put(e, arguments[i]);
|
|
24
|
+
return e;
|
|
25
|
+
}
|
|
26
|
+
function put(e, c) { if (c == null || c === false) return; if (Array.isArray(c)) { c.forEach(function (x) { put(e, x); }); return; } e.appendChild(typeof c === "object" ? c : doc.createTextNode(String(c))); }
|
|
27
|
+
function clear(e) { while (e.firstChild) e.removeChild(e.firstChild); }
|
|
28
|
+
var NS = "http://www.w3.org/2000/svg";
|
|
29
|
+
function icon(d) { var e = doc.createElementNS(NS, "svg"); e.setAttribute("class", "i"); e.setAttribute("viewBox", "0 0 24 24"); e.setAttribute("aria-hidden", "true"); var p = doc.createElementNS(NS, "path"); p.setAttribute("d", d); e.appendChild(p); return e; }
|
|
30
|
+
var CHECK = "M5 12.5l4.5 4.5L19 7.5", CHEV = "m6 9 6 6 6-6";
|
|
31
|
+
/* words: English by default, Korean when the page is built with --lang ko (html lang=ko) */
|
|
32
|
+
var KO = String((doc.documentElement && doc.documentElement.getAttribute && doc.documentElement.getAttribute("lang")) || "en").toLowerCase().indexOf("ko") === 0;
|
|
33
|
+
var L = KO ? {
|
|
34
|
+
tone: { met: "충족", unmet: "미충족", unknown: "확인 불가", direct: "직접 영향", indirect: "간접 영향", "covered-changed": "테스트 있음 · 함께 바뀜", covered: "테스트 있음", "none-found": "테스트 못 찾음", tests: "테스트 미확인", added: "신규", modified: "변경", deleted: "삭제", removed: "삭제" },
|
|
35
|
+
ignore: { test: "테스트", docs: "문서", lockfile: "잠금 파일", asset: "정적 자산" },
|
|
36
|
+
step: function (n) { return n + "단계"; },
|
|
37
|
+
tIntent: "기획 의도", rIntent: "기획", tImpact: "회귀 테스트", rImpact: "QA", tUnmapped: "모델 밖 변경", rUnmapped: "개발",
|
|
38
|
+
noIntent: "intent.json 을 주면 기획 기대사항을 이 모델과 대조합니다 (build --intent).", qIntent: "기획대로 동작하나요?",
|
|
39
|
+
sumIntent: function (m) { return "충족 " + m.met + " · 미충족 " + m.unmet + " · 확인 불가 " + m.unknown; },
|
|
40
|
+
showMet: function (n, open) { return "충족 " + n + "건 " + (open ? "접기" : "보기"); },
|
|
41
|
+
uncovered: function (n) { return "기획에 연결되지 않은 변경 " + n + "건 — 범위 이탈인지 판단"; }, scopeOk: "범위 확인함", checked: "확인함",
|
|
42
|
+
unprovable: "모델로 증명할 수 없음 — 사람이 직접 확인", touched: "이번 변경이 건드림",
|
|
43
|
+
noImpact: "저장소와 기준 커밋을 주면 다시 테스트할 흐름을 계산합니다 (build --repo --base).", qImpact: "무엇을 다시 테스트하나요?",
|
|
44
|
+
sumImpact: function (m) { return "직접 " + m.direct + " · 간접 " + m.indirect + " · 테스트 못 찾음 " + m.withoutTests + (m.testsKnown ? " — 테스트 파일이 경로를 언급하는지로만 찾습니다" : " — 테스트는 저장소를 읽지 않아 확인하지 않았습니다"); },
|
|
45
|
+
tested: "테스트함", why: { table: "테이블 ", endpoint: "외부 호출 ", part: "부품 ", wire: "배선 " },
|
|
46
|
+
changedSteps: function (n) { return "바뀐 단계 " + n; }, testFiles: function (n) { return "테스트 " + n + "개"; }, testFilesLabel: "테스트 파일", kindLabel: "종류",
|
|
47
|
+
cats: function (code, other, ign) { return [["code", "코드 " + code], ["other", "설정·스타일 " + other], ["ignored", "무시 " + ign]]; },
|
|
48
|
+
qUnmapped: "흐름에 안 잡힌 변경이 있나요?", subUnmapped: "어떤 단계의 근거에도 없는 변경 — 단계를 추가하거나 무관한 변경인지 확인",
|
|
49
|
+
noIgnored: "무시한 파일이 없습니다", allMapped: "모든 코드 변경이 흐름의 단계에 잡혔습니다", noOther: "바뀐 설정·스타일 파일이 없습니다",
|
|
50
|
+
nFiles: function (n) { return n + "개"; }, fileList: "파일 목록", grip: "검토 패널 높이", dock: "검토", none: "없음", left: function (n) { return n + "건 남음"; }, allDone: "모두 확인함",
|
|
51
|
+
expand: "검토 패널 펼치기", fold: "검토 패널 접기"
|
|
52
|
+
} : {
|
|
53
|
+
tone: { met: "Met", unmet: "Not met", unknown: "Can't tell", direct: "Direct", indirect: "Indirect", "covered-changed": "Tested · changed too", covered: "Tested", "none-found": "No test found", tests: "Tests not checked", added: "New", modified: "Changed", deleted: "Deleted", removed: "Removed" },
|
|
54
|
+
ignore: { test: "Test", docs: "Docs", lockfile: "Lockfile", asset: "Asset" },
|
|
55
|
+
step: function (n) { return "step " + n; },
|
|
56
|
+
tIntent: "Plan", rIntent: "Planner", tImpact: "Retest", rImpact: "QA", tUnmapped: "Outside the model", rUnmapped: "Dev",
|
|
57
|
+
noIntent: "Pass intent.json to check the planner's expectations against this model (build --intent).", qIntent: "Does it work as planned?",
|
|
58
|
+
sumIntent: function (m) { return "Met " + m.met + " · Not met " + m.unmet + " · Can't tell " + m.unknown; },
|
|
59
|
+
showMet: function (n, open) { return (open ? "Hide " : "Show ") + n + " met"; },
|
|
60
|
+
uncovered: function (n) { return n + (n === 1 ? " change is" : " changes are") + " not linked to the plan: is it out of scope?"; }, scopeOk: "Scope checked", checked: "Checked",
|
|
61
|
+
unprovable: "The model cannot prove this: a person has to check", touched: "Touched by this change",
|
|
62
|
+
noImpact: "Pass the repository and a base commit to find the flows to retest (build --repo --base).", qImpact: "What should be retested?",
|
|
63
|
+
sumImpact: function (m) { return "Direct " + m.direct + " · Indirect " + m.indirect + " · No test found " + m.withoutTests + (m.testsKnown ? ": tests are found only by the paths they mention" : ": tests were not checked because the repository was not read"); },
|
|
64
|
+
tested: "Tested", why: { table: "table ", endpoint: "call ", part: "part ", wire: "wire " },
|
|
65
|
+
changedSteps: function (n) { return n + (n === 1 ? " changed step" : " changed steps"); }, testFiles: function (n) { return n + (n === 1 ? " test" : " tests"); }, testFilesLabel: "Test files", kindLabel: "Kind",
|
|
66
|
+
cats: function (code, other, ign) { return [["code", "Code " + code], ["other", "Config & style " + other], ["ignored", "Ignored " + ign]]; },
|
|
67
|
+
qUnmapped: "Did any change miss the flows?", subUnmapped: "Changes that are not in any step's evidence: add a step, or confirm they are unrelated",
|
|
68
|
+
noIgnored: "No ignored files", allMapped: "Every code change is in a step of a flow", noOther: "No config or style files changed",
|
|
69
|
+
nFiles: function (n) { return n + (n === 1 ? " file" : " files"); }, fileList: "File list", grip: "Review panel height", dock: "Review", none: "None", left: function (n) { return n + " to check"; }, allDone: "All checked",
|
|
70
|
+
expand: "Expand the review panel", fold: "Collapse the review panel"
|
|
71
|
+
};
|
|
72
|
+
var TONE = { met: "success", unmet: "danger", unknown: "neutral", direct: "warning", indirect: "info", "covered-changed": "success", covered: "success", "none-found": "danger", tests: "neutral", added: "success", modified: "warning", deleted: "danger", removed: "danger" };
|
|
73
|
+
function badge(k) { return h("span", { "class": "badge", "data-tone": TONE[k] || "neutral" }, L.tone[k] || k); }
|
|
74
|
+
var ucs = {}; model.useCases.forEach(function (u) { ucs[u.id] = u; });
|
|
75
|
+
function ucTitle(id) { return ucs[id] ? ucs[id].title : id; }
|
|
76
|
+
function jump(ucId, stepId) {
|
|
77
|
+
if (!api) return;
|
|
78
|
+
var u = ucs[ucId], i = -1; if (u && stepId) u.steps.forEach(function (st, k) { if (st.id === stepId) i = k; });
|
|
79
|
+
api.selectUseCase(ucId, i >= 0 ? i : null);
|
|
80
|
+
}
|
|
81
|
+
function stepLinks(list) {
|
|
82
|
+
return list.map(function (p) {
|
|
83
|
+
var u = ucs[p.useCase], i = -1; if (u) u.steps.forEach(function (st, k) { if (st.id === p.step) i = k; });
|
|
84
|
+
var t = ucTitle(p.useCase);
|
|
85
|
+
return h("button", { type: "button", "class": "steplink", title: p.label || "", on: { click: function () { jump(p.useCase, p.step); } } }, (t.length > 12 ? t.slice(0, 12) + "…" : t) + " " + L.step(i + 1));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── ticks (per viewer) ── */
|
|
90
|
+
var KEY = "sv-review:" + ((changes && changes.ref) || model.product.id), done = {};
|
|
91
|
+
try { done = JSON.parse(localStorage.getItem(KEY) || "{}") || {}; } catch (e) { done = {}; }
|
|
92
|
+
function tick(id, on) { if (on) done[id] = 1; else delete done[id]; try { localStorage.setItem(KEY, JSON.stringify(done)); } catch (e) { /* storage blocked: ticks last for this visit */ } paint(); }
|
|
93
|
+
function checkbox(id, label) {
|
|
94
|
+
var on = !!done[id];
|
|
95
|
+
return h("button", { type: "button", "class": "check", role: "checkbox", "aria-checked": on ? "true" : "false", "aria-label": label, on: { click: function (e) { e.stopPropagation(); tick(id, !on); } } }, icon(CHECK));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ── tracks: what each tab still asks a person to check ── */
|
|
99
|
+
var unmapped = (changes && changes.unmapped) || [];
|
|
100
|
+
function folders(code) {
|
|
101
|
+
var m = {}, order = [];
|
|
102
|
+
unmapped.filter(function (f) { return code ? f.category === "code" : f.category !== "code"; }).forEach(function (f) {
|
|
103
|
+
var p = f.file.split("/"), dir = p.slice(0, Math.min(p.length - 1, 3)).join("/") || ".";
|
|
104
|
+
if (!m[dir]) { m[dir] = []; order.push(dir); } m[dir].push(f);
|
|
105
|
+
});
|
|
106
|
+
return order.map(function (d) { return { dir: d, files: m[d] }; }).sort(function (a, b) { return b.files.length - a.files.length || (a.dir < b.dir ? -1 : 1); });
|
|
107
|
+
}
|
|
108
|
+
function uncoveredByUc() { var m = {}, order = []; ((data.intent && data.intent.uncovered) || []).forEach(function (x) { if (!m[x.useCase]) { m[x.useCase] = []; order.push(x.useCase); } m[x.useCase].push(x); }); return order.map(function (id) { return { id: id, items: m[id] }; }); }
|
|
109
|
+
function tracks() {
|
|
110
|
+
return [
|
|
111
|
+
{ id: "intent", label: L.tIntent, role: L.rIntent, avail: !!data.intent,
|
|
112
|
+
items: data.intent ? data.intent.results.filter(function (r) { return r.status !== "met"; }).map(function (r) { return "it:" + r.id; }).concat(uncoveredByUc().map(function (g) { return "sc:" + g.id; })) : [],
|
|
113
|
+
danger: !!(data.intent && data.intent.summary.unmet) },
|
|
114
|
+
{ id: "impact", label: L.tImpact, role: L.rImpact, avail: !!data.impact,
|
|
115
|
+
items: data.impact ? data.impact.useCases.map(function (u) { return "rt:" + u.id; }) : [],
|
|
116
|
+
danger: !!(data.impact && data.impact.summary.withoutTests) },
|
|
117
|
+
{ id: "unmapped", label: L.tUnmapped, role: L.rUnmapped, avail: !!changes,
|
|
118
|
+
items: folders(true).map(function (g) { return "dir:" + g.dir; }), danger: false }
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
function left(t) { return t.items.filter(function (id) { return !done[id]; }).length; }
|
|
122
|
+
|
|
123
|
+
/* ── state ── */
|
|
124
|
+
var hashTab = api && api.hash ? api.hash.dock : null, T0 = tracks();
|
|
125
|
+
var avail = T0.filter(function (t) { return t.avail; });
|
|
126
|
+
var S = { tab: (T0.filter(function (t) { return t.id === hashTab && t.avail; })[0] || avail.filter(function (t) { return left(t); })[0] || avail[0]).id, collapsed: false, showMet: false, open: {}, ucat: "code" };
|
|
127
|
+
|
|
128
|
+
/* ── tab bodies ── */
|
|
129
|
+
function head(q, sub, extra) { return h("div", { "class": "dhead" }, h("span", { "class": "q kr", text: q }), sub ? h("span", { "class": "sub kr", text: sub }) : null, h("span", { "class": "grow" }), extra || null); }
|
|
130
|
+
function bodyIntent(b) {
|
|
131
|
+
var I = data.intent;
|
|
132
|
+
if (!I) { put(b, h("div", { "class": "empty kr", text: L.noIntent })); return; }
|
|
133
|
+
var sm = I.summary;
|
|
134
|
+
put(b, head(L.qIntent, L.sumIntent(sm) + (I.title ? " — " + I.title : "")));
|
|
135
|
+
var todo = I.results.filter(function (r) { return r.status !== "met"; }), met = I.results.filter(function (r) { return r.status === "met"; });
|
|
136
|
+
if (todo.length) put(b, h("ul", { "class": "rowlist" }, todo.map(function (r) { return intentRow(r, true); })));
|
|
137
|
+
if (met.length) {
|
|
138
|
+
put(b, h("div", { "class": "subhead" }, h("button", { type: "button", "class": "link", on: { click: function () { S.showMet = !S.showMet; paint(); } } }, L.showMet(met.length, S.showMet))));
|
|
139
|
+
if (S.showMet) put(b, h("ul", { "class": "rowlist" }, met.map(function (r) { return intentRow(r, false); })));
|
|
140
|
+
}
|
|
141
|
+
var groups = uncoveredByUc();
|
|
142
|
+
if (groups.length) {
|
|
143
|
+
put(b, h("div", { "class": "subhead kr", text: L.uncovered(I.uncovered.length) }));
|
|
144
|
+
put(b, h("ul", { "class": "rowlist" }, groups.map(function (g) {
|
|
145
|
+
var key = "sc:" + g.id;
|
|
146
|
+
return h("li", { "class": "ri" + (done[key] ? " done" : "") }, checkbox(key, L.scopeOk), h("div", { "class": "main" }, h("div", { "class": "t kr", text: ucTitle(g.id) }), h("div", { "class": "m" }, stepLinks(g.items))));
|
|
147
|
+
})));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function intentRow(r, todo) {
|
|
151
|
+
var key = "it:" + r.id;
|
|
152
|
+
return h("li", { "class": "ri" + (todo && done[key] ? " done" : "") },
|
|
153
|
+
todo ? checkbox(key, L.checked) : h("span", { "class": "gap" }),
|
|
154
|
+
h("div", { "class": "main" }, h("div", { "class": "t kr", text: r.text }),
|
|
155
|
+
h("div", { "class": "m" }, r.status === "unknown" ? h("span", { "class": "kr", text: L.unprovable }) : r.status === "unmet" ? h("span", { "class": "kr", text: r.reason }) : null,
|
|
156
|
+
r.useCase && !(r.proof || []).length ? h("span", { "class": "kr", text: ucTitle(r.useCase) }) : null, stepLinks(r.proof || []))),
|
|
157
|
+
h("span", { "class": "end" }, r.touched ? h("span", { "class": "badge", "data-tone": "info" }, L.touched) : null, badge(r.status)));
|
|
158
|
+
}
|
|
159
|
+
function bodyImpact(b) {
|
|
160
|
+
var M = data.impact;
|
|
161
|
+
if (!M) { put(b, h("div", { "class": "empty kr", text: L.noImpact })); return; }
|
|
162
|
+
var sm = M.summary;
|
|
163
|
+
put(b, head(L.qImpact, L.sumImpact(sm)));
|
|
164
|
+
put(b, h("ul", { "class": "rowlist" }, M.useCases.map(function (u) {
|
|
165
|
+
var key = "rt:" + u.id, open = !!S.open[key], files = (u.tests && u.tests.files) || [];
|
|
166
|
+
return h("li", { "class": "ri" + (done[key] ? " done" : "") }, checkbox(key, L.tested),
|
|
167
|
+
h("div", { "class": "main" },
|
|
168
|
+
h("div", { "class": "t kr" }, h("button", { type: "button", "class": "link plain", on: { click: function () { jump(u.id); } } }, u.title)),
|
|
169
|
+
h("div", { "class": "m" }, (u.reasons || []).map(function (r) { return h("span", { "class": "mono", text: (L.why[r.kind] || "") + r.ref }); }),
|
|
170
|
+
(u.direct || []).length ? h("span", { text: L.changedSteps(u.direct.length) }) : null, files.length ? h("span", { text: L.testFiles(files.length) }) : null),
|
|
171
|
+
open ? h("ul", { "class": "files" }, files.map(function (f) { return h("li", null, h("span", { text: f.file })); })) : null),
|
|
172
|
+
h("span", { "class": "end" }, badge(u.relation), badge(u.tests && u.tests.state !== "unknown" ? u.tests.state : "tests"),
|
|
173
|
+
files.length ? h("button", { type: "button", "class": "chev", "aria-expanded": open ? "true" : "false", "aria-label": L.testFilesLabel, on: { click: function () { S.open[key] = !open; paint(); } } }, icon(CHEV)) : null));
|
|
174
|
+
})));
|
|
175
|
+
}
|
|
176
|
+
function bodyUnmapped(b) {
|
|
177
|
+
var code = unmapped.filter(function (f) { return f.category === "code"; }).length, ign = (changes && changes.ignored) || [];
|
|
178
|
+
var seg = h("div", { "class": "seg", role: "group", "aria-label": L.kindLabel });
|
|
179
|
+
L.cats(code, unmapped.length - code, ign.length).forEach(function (x) {
|
|
180
|
+
put(seg, h("button", { type: "button", "aria-pressed": S.ucat === x[0] ? "true" : "false", on: { click: function () { S.ucat = x[0]; paint(); } } }, x[1]));
|
|
181
|
+
});
|
|
182
|
+
put(b, head(L.qUnmapped, L.subUnmapped, seg));
|
|
183
|
+
if (S.ucat === "ignored") {
|
|
184
|
+
put(b, ign.length ? h("ul", { "class": "rowlist" }, ign.map(function (f) { return h("li", { "class": "ri" }, h("span", { "class": "gap" }), h("div", { "class": "main mono small", text: f.file }), h("span", { "class": "end" }, h("span", { "class": "badge", text: L.ignore[f.reason] || f.reason }))); }))
|
|
185
|
+
: h("div", { "class": "empty", text: L.noIgnored }));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
var gs = folders(S.ucat === "code");
|
|
189
|
+
if (!gs.length) { put(b, h("div", { "class": "empty kr", text: S.ucat === "code" ? L.allMapped : L.noOther })); return; }
|
|
190
|
+
put(b, h("ul", { "class": "rowlist" }, gs.map(function (g) {
|
|
191
|
+
var key = "dir:" + g.dir, open = !!S.open[key];
|
|
192
|
+
return h("li", { "class": "ri" + (done[key] ? " done" : "") }, checkbox(key, L.checked),
|
|
193
|
+
h("div", { "class": "main" }, h("div", { "class": "t mono", text: g.dir + "/" }),
|
|
194
|
+
open ? h("ul", { "class": "files" }, g.files.map(function (f) { return h("li", null, h("span", { text: f.file.slice(g.dir.length + 1) }), h("span", { "class": "ln", text: f.lines }), h("span", { "class": "st", text: (WORD[f.status] || ["", f.status])[1] })); }))
|
|
195
|
+
: h("div", { "class": "m mono ell", text: g.files.map(function (f) { return f.file.slice(g.dir.length + 1); }).join(" · ") })),
|
|
196
|
+
h("span", { "class": "end" }, h("span", { "class": "badge", text: L.nFiles(g.files.length) }), h("button", { type: "button", "class": "chev", "aria-expanded": open ? "true" : "false", "aria-label": L.fileList, on: { click: function () { S.open[key] = !open; paint(); } } }, icon(CHEV))));
|
|
197
|
+
})));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* ── frame ── */
|
|
201
|
+
var grip = h("div", { "class": "grip", role: "separator", "aria-orientation": "horizontal", "aria-label": L.grip });
|
|
202
|
+
var bar = h("div", { "class": "dtabs", role: "tablist", "aria-label": L.dock }), body = h("div", { "class": "dbody", role: "tabpanel", id: "sv-review-body" });
|
|
203
|
+
function paint() {
|
|
204
|
+
clear(bar); clear(body);
|
|
205
|
+
tracks().forEach(function (t) {
|
|
206
|
+
var n = left(t), tone = !t.avail ? "neutral" : n === 0 ? "success" : t.danger ? "danger" : "warning";
|
|
207
|
+
put(bar, h("button", { type: "button", "class": "dtab", role: "tab", "data-tab": t.id, "aria-selected": S.tab === t.id && !S.collapsed ? "true" : "false", "aria-controls": "sv-review-body",
|
|
208
|
+
on: { click: function () { if (S.tab === t.id) S.collapsed = !S.collapsed; else { S.tab = t.id; S.collapsed = false; } paint(); } } },
|
|
209
|
+
h("span", { "class": "dot", "data-tone": tone }), t.label, h("span", { "class": "role", text: t.role }),
|
|
210
|
+
!t.avail ? h("span", { "class": "left none", text: L.none }) : n ? h("span", { "class": "left", "data-tone": tone, text: L.left(n) }) : h("span", { "class": "ok", title: L.allDone }, icon(CHECK))));
|
|
211
|
+
});
|
|
212
|
+
put(bar, [h("span", { "class": "grow" }), h("button", { type: "button", "class": "btn icon ghost fold", "aria-label": S.collapsed ? L.expand : L.fold, "aria-expanded": S.collapsed ? "false" : "true", on: { click: function () { S.collapsed = !S.collapsed; paint(); } } }, icon(CHEV))]);
|
|
213
|
+
if (root.classList) root.classList[S.collapsed ? "add" : "remove"]("collapsed");
|
|
214
|
+
if (!S.collapsed) ({ intent: bodyIntent, impact: bodyImpact, unmapped: bodyUnmapped })[S.tab](body);
|
|
215
|
+
}
|
|
216
|
+
(function resizable() {
|
|
217
|
+
var y0 = 0, h0 = 0, dragging = false;
|
|
218
|
+
grip.addEventListener("pointerdown", function (e) { dragging = true; y0 = e.clientY; h0 = root.getBoundingClientRect().height; if (grip.setPointerCapture) grip.setPointerCapture(e.pointerId); });
|
|
219
|
+
grip.addEventListener("pointermove", function (e) { if (!dragging) return; var v = Math.max(120, Math.min(window.innerHeight * 0.6, h0 - (e.clientY - y0))); doc.documentElement.style.setProperty("--dock-h", Math.round(v) + "px"); });
|
|
220
|
+
grip.addEventListener("pointerup", function () { dragging = false; });
|
|
221
|
+
})();
|
|
222
|
+
put(root, [grip, bar, body]);
|
|
223
|
+
root.removeAttribute("hidden");
|
|
224
|
+
paint();
|
|
225
|
+
window.__nacholensReview = {
|
|
226
|
+
get state() { return { tab: S.tab, collapsed: S.collapsed, ucat: S.ucat }; },
|
|
227
|
+
tracks: function () { return tracks().map(function (t) { return { id: t.id, avail: t.avail, left: left(t), total: t.items.length }; }); },
|
|
228
|
+
open: function (tab) { S.tab = tab; S.collapsed = false; paint(); }
|
|
229
|
+
};
|
|
230
|
+
})();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="{{LANG}}" data-target="{{TARGET}}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="generator" content="nacholens">
|
|
7
|
+
<meta name="nacholens-target" content="{{TARGET}}">
|
|
8
|
+
<title>{{TITLE}}</title>
|
|
9
|
+
<style>
|
|
10
|
+
{{FONTS}}
|
|
11
|
+
</style>
|
|
12
|
+
<style>
|
|
13
|
+
{{CSS}}
|
|
14
|
+
</style>
|
|
15
|
+
<style>
|
|
16
|
+
{{REVIEWCSS}}
|
|
17
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div class="app" id="app">
|
|
21
|
+
<header class="topbar">
|
|
22
|
+
<div class="brand"><span class="logo" aria-hidden="true">N</span>nacholens</div>
|
|
23
|
+
<span class="sep" aria-hidden="true"></span>
|
|
24
|
+
<h1 class="product" id="sv-product"></h1>
|
|
25
|
+
<span class="ref" id="sv-ref" hidden></span>
|
|
26
|
+
<span class="commit ell" id="sv-commit"></span>
|
|
27
|
+
<span class="grow"></span>
|
|
28
|
+
<button class="switch" id="sv-overlay" type="button" role="switch" aria-checked="true" hidden><span class="track" aria-hidden="true"></span>{{UOVERLAY}}</button>
|
|
29
|
+
</header>
|
|
30
|
+
<div class="body" id="sv-body">
|
|
31
|
+
<nav class="side" aria-label="{{UNAV}}">
|
|
32
|
+
<div class="side-tools">
|
|
33
|
+
<label class="search"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><input id="sv-q" type="search" placeholder="{{USEARCH}}" aria-label="{{USEARCH}}"></label>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="side-list" id="sv-list"></div>
|
|
36
|
+
</nav>
|
|
37
|
+
<section class="stage" id="sv-stage" aria-label="{{USTAGE}}">
|
|
38
|
+
<div class="crumbs" id="sv-crumbs" hidden></div>
|
|
39
|
+
<div class="caption" id="sv-caption" aria-live="polite"></div>
|
|
40
|
+
<svg class="canvas" id="sv-canvas" role="img" aria-label="{{UCANVAS}}"></svg>
|
|
41
|
+
</section>
|
|
42
|
+
<div class="transport" id="sv-transport" role="group" aria-label="{{UTRANSPORT}}"></div>
|
|
43
|
+
<section id="sv-review" class="dock" hidden aria-label="{{UREVIEW}}"></section>
|
|
44
|
+
<aside class="insp" id="sv-insp" aria-label="{{UINSP}}"></aside>
|
|
45
|
+
</div>
|
|
46
|
+
<footer class="foot" id="sv-foot"><a href="https://mafianacho.com" target="_blank" rel="noopener noreferrer">Made by Mafianacho Inc.</a></footer>
|
|
47
|
+
</div>
|
|
48
|
+
<script type="application/json" id="sv-model">{{MODEL}}</script>
|
|
49
|
+
<script type="application/json" id="sv-changes">{{CHANGES}}</script>
|
|
50
|
+
<script type="application/json" id="sv-review-data">{{REVIEW}}</script>
|
|
51
|
+
<script>
|
|
52
|
+
{{JS}}
|
|
53
|
+
</script>
|
|
54
|
+
<script>
|
|
55
|
+
{{REVIEWJS}}
|
|
56
|
+
</script>
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|