@grifhinz/logics-manager 2.1.2 → 2.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/README.md +106 -4
- package/VERSION +1 -1
- package/clients/README.md +9 -0
- package/clients/shared-web/media/css/board.css +658 -0
- package/clients/shared-web/media/css/details.css +457 -0
- package/clients/shared-web/media/css/layout.css +123 -0
- package/clients/shared-web/media/css/toolbar.css +576 -0
- package/clients/shared-web/media/harnessApi.js +324 -0
- package/clients/shared-web/media/hostApi.js +213 -0
- package/clients/shared-web/media/hostApiContract.js +55 -0
- package/clients/shared-web/media/icon.png +0 -0
- package/clients/shared-web/media/layoutController.js +246 -0
- package/clients/shared-web/media/logics.svg +7 -0
- package/clients/shared-web/media/logicsModel.js +910 -0
- package/clients/shared-web/media/main.css +112 -0
- package/clients/shared-web/media/main.js +3 -0
- package/clients/shared-web/media/mainApp.js +1005 -0
- package/clients/shared-web/media/mainCore.js +604 -0
- package/clients/shared-web/media/mainInteractionHandlers.js +324 -0
- package/clients/shared-web/media/mainInteractions.js +378 -0
- package/clients/shared-web/media/renderBoard.js +3 -0
- package/clients/shared-web/media/renderBoardApp.js +1339 -0
- package/clients/shared-web/media/renderDetails.js +685 -0
- package/clients/shared-web/media/renderMarkdown.js +449 -0
- package/clients/shared-web/media/toolsPanelLayout.js +172 -0
- package/clients/shared-web/media/uiStatus.js +54 -0
- package/clients/shared-web/media/webviewChrome.js +405 -0
- package/clients/shared-web/media/webviewPersistence.js +116 -0
- package/clients/shared-web/media/webviewSelectors.js +491 -0
- package/clients/viewer/README.md +5 -0
- package/clients/viewer/browser-host.js +847 -0
- package/clients/viewer/index.html +237 -0
- package/clients/viewer/viewer.css +433 -0
- package/logics_manager/assist.py +94 -63
- package/logics_manager/assist_handoff.py +132 -0
- package/logics_manager/assist_surface.py +38 -0
- package/logics_manager/cli.py +152 -12
- package/logics_manager/cli_output.py +18 -0
- package/logics_manager/flow.py +1360 -84
- package/logics_manager/flow_evidence.py +63 -0
- package/logics_manager/index.py +3 -7
- package/logics_manager/insights.py +418 -0
- package/logics_manager/mcp.py +50 -0
- package/logics_manager/path_utils.py +31 -0
- package/logics_manager/sync.py +24 -12
- package/logics_manager/update_check.py +138 -0
- package/logics_manager/viewer.py +533 -0
- package/package.json +12 -6
- package/pyproject.toml +1 -1
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const stateKey = "logics.localViewer.state";
|
|
3
|
+
const autoRefreshIntervalMs = 60 * 1000;
|
|
4
|
+
const meta = () => document.getElementById("viewer-meta");
|
|
5
|
+
const documentPanel = () => document.getElementById("viewer-document");
|
|
6
|
+
const documentTitle = () => document.getElementById("viewer-document-title");
|
|
7
|
+
const documentContent = () => document.getElementById("viewer-document-content");
|
|
8
|
+
const editDocumentButton = () => document.querySelector('[data-viewer-action="edit-document"]');
|
|
9
|
+
const updateBanner = () => document.getElementById("viewer-update");
|
|
10
|
+
const updateCopy = () => document.getElementById("viewer-update-copy");
|
|
11
|
+
const updateCommand = () => document.getElementById("viewer-update-command");
|
|
12
|
+
const filterCount = () => document.getElementById("viewer-filter-count");
|
|
13
|
+
const defaultFilterState = {
|
|
14
|
+
focus: "active",
|
|
15
|
+
type: "all",
|
|
16
|
+
status: "any",
|
|
17
|
+
relation: "any",
|
|
18
|
+
activity: "any"
|
|
19
|
+
};
|
|
20
|
+
let viewerFilterState = { ...defaultFilterState };
|
|
21
|
+
let latestItems = [];
|
|
22
|
+
let applyingLocalChrome = false;
|
|
23
|
+
let autoRefreshStarted = false;
|
|
24
|
+
let itemsLoadInFlight = false;
|
|
25
|
+
let refreshAfterVisible = false;
|
|
26
|
+
let mermaidInitialized = false;
|
|
27
|
+
let focusApplied = false;
|
|
28
|
+
|
|
29
|
+
function readStoredState() {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(window.localStorage.getItem(stateKey) || "null");
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sanitizeViewerFilterState(value) {
|
|
38
|
+
const nextState = { ...defaultFilterState };
|
|
39
|
+
if (!value || typeof value !== "object") {
|
|
40
|
+
return nextState;
|
|
41
|
+
}
|
|
42
|
+
Object.keys(defaultFilterState).forEach((key) => {
|
|
43
|
+
if (typeof value[key] === "string" && value[key]) {
|
|
44
|
+
nextState[key] = value[key];
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return nextState;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hydrateViewerFilterState() {
|
|
51
|
+
const storedState = readStoredState();
|
|
52
|
+
viewerFilterState = sanitizeViewerFilterState(storedState?.viewerFilterState);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeStoredState(value) {
|
|
56
|
+
window.localStorage.setItem(stateKey, JSON.stringify(value || null));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function persistViewerFilterState() {
|
|
60
|
+
const storedState = readStoredState();
|
|
61
|
+
const nextState = storedState && typeof storedState === "object" ? storedState : {};
|
|
62
|
+
writeStoredState({ ...nextState, viewerFilterState: { ...viewerFilterState } });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function markdownApi() {
|
|
66
|
+
if (typeof window.createCdxLogicsMarkdownApi === "function") {
|
|
67
|
+
return window.createCdxLogicsMarkdownApi();
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function escapeHtml(value) {
|
|
73
|
+
const api = markdownApi();
|
|
74
|
+
if (api && typeof api.escapeHtml === "function") {
|
|
75
|
+
return api.escapeHtml(value);
|
|
76
|
+
}
|
|
77
|
+
return String(value ?? "")
|
|
78
|
+
.replace(/&/g, "&")
|
|
79
|
+
.replace(/</g, "<")
|
|
80
|
+
.replace(/>/g, ">")
|
|
81
|
+
.replace(/"/g, """)
|
|
82
|
+
.replace(/'/g, "'");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cssEscape(value) {
|
|
86
|
+
if (window.CSS && typeof window.CSS.escape === "function") {
|
|
87
|
+
return window.CSS.escape(value);
|
|
88
|
+
}
|
|
89
|
+
return String(value ?? "").replace(/["\\]/g, "\\$&");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function setMeta(text) {
|
|
93
|
+
const node = meta();
|
|
94
|
+
if (node) {
|
|
95
|
+
node.textContent = text;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function findItemByPath(relPath) {
|
|
100
|
+
const normalized = String(relPath || "").replace(/\\/g, "/").replace(/^\//, "");
|
|
101
|
+
return latestItems.find((entry) => entry.relPath === normalized || entry.path === normalized) || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeFocusTarget(value) {
|
|
105
|
+
const normalized = String(value || "").replace(/\\/g, "/").replace(/^\.?\//, "").replace(/^\//, "").trim();
|
|
106
|
+
if (!normalized || normalized.startsWith("~") || /^[A-Za-z]:/.test(normalized)) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
if (normalized.split("/").includes("..")) {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function focusRequest() {
|
|
116
|
+
try {
|
|
117
|
+
const params = new URLSearchParams(window.location.search || "");
|
|
118
|
+
const focus = normalizeFocusTarget(params.get("focus") || "");
|
|
119
|
+
return {
|
|
120
|
+
focus,
|
|
121
|
+
read: params.get("read") === "1" || params.get("read") === "true"
|
|
122
|
+
};
|
|
123
|
+
} catch {
|
|
124
|
+
return { focus: "", read: false };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findFocusItem(target) {
|
|
129
|
+
const normalized = normalizeFocusTarget(target);
|
|
130
|
+
if (!normalized) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const bare = normalized.endsWith(".md") ? normalized.slice(0, -3).split("/").pop() : normalized;
|
|
134
|
+
return latestItems.find((entry) => {
|
|
135
|
+
const relPath = String(entry.relPath || "").replace(/\\/g, "/");
|
|
136
|
+
const fullPath = String(entry.path || "").replace(/\\/g, "/");
|
|
137
|
+
return entry.id === normalized ||
|
|
138
|
+
entry.id === bare ||
|
|
139
|
+
entry.filename === normalized ||
|
|
140
|
+
relPath === normalized ||
|
|
141
|
+
fullPath.endsWith(`/${normalized}`);
|
|
142
|
+
}) || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function persistSelectedItem(id) {
|
|
146
|
+
const storedState = readStoredState();
|
|
147
|
+
const nextState = storedState && typeof storedState === "object" ? { ...storedState } : {};
|
|
148
|
+
writeStoredState({ ...nextState, selectedId: id, viewerFilterState: { ...viewerFilterState } });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function revealFocusedCard(item) {
|
|
152
|
+
window.setTimeout(() => {
|
|
153
|
+
const escapedId = cssEscape(item.id);
|
|
154
|
+
const selector = `.card[data-id="${escapedId}"], [data-id="${escapedId}"]`;
|
|
155
|
+
const card = document.querySelector(selector);
|
|
156
|
+
if (card instanceof HTMLElement && typeof card.scrollIntoView === "function") {
|
|
157
|
+
card.scrollIntoView({ block: "center", inline: "nearest" });
|
|
158
|
+
card.focus?.({ preventScroll: true });
|
|
159
|
+
}
|
|
160
|
+
applyLocalViewerChrome();
|
|
161
|
+
}, 0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function applyFocusRequest(payload) {
|
|
165
|
+
if (focusApplied) {
|
|
166
|
+
return payload;
|
|
167
|
+
}
|
|
168
|
+
const request = focusRequest();
|
|
169
|
+
if (!request.focus) {
|
|
170
|
+
if (window.location.search.includes("focus=")) {
|
|
171
|
+
window.setTimeout(() => setMeta("Invalid focus target. Loaded corpus without changing selection."), 0);
|
|
172
|
+
}
|
|
173
|
+
focusApplied = true;
|
|
174
|
+
return payload;
|
|
175
|
+
}
|
|
176
|
+
const item = findFocusItem(request.focus);
|
|
177
|
+
if (!item) {
|
|
178
|
+
window.setTimeout(() => setMeta(`Focus target not found: ${request.focus}`), 0);
|
|
179
|
+
focusApplied = true;
|
|
180
|
+
return payload;
|
|
181
|
+
}
|
|
182
|
+
viewerFilterState = { ...viewerFilterState, focus: "all", type: "all", status: "any", relation: "any", activity: "any" };
|
|
183
|
+
persistSelectedItem(item.id);
|
|
184
|
+
focusApplied = true;
|
|
185
|
+
const nextPayload = { ...payload, selectedId: item.id };
|
|
186
|
+
window.setTimeout(() => {
|
|
187
|
+
revealFocusedCard(item);
|
|
188
|
+
if (request.read) {
|
|
189
|
+
showDocument(item).catch((error) => setMeta(error.message));
|
|
190
|
+
} else {
|
|
191
|
+
setMeta(`Focused ${item.relPath || item.id}.`);
|
|
192
|
+
}
|
|
193
|
+
}, 0);
|
|
194
|
+
return nextPayload;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function selectedItem() {
|
|
198
|
+
const selectedCard = document.querySelector(".card--selected[data-id]");
|
|
199
|
+
const selectedCardId = selectedCard instanceof HTMLElement ? selectedCard.dataset.id : "";
|
|
200
|
+
if (selectedCardId) {
|
|
201
|
+
return latestItems.find((entry) => entry.id === selectedCardId) || null;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const state = readStoredState();
|
|
205
|
+
const selectedId = typeof state?.selectedId === "string" ? state.selectedId : "";
|
|
206
|
+
return latestItems.find((entry) => entry.id === selectedId) || null;
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function setDocument(titleText, html) {
|
|
213
|
+
const panel = documentPanel();
|
|
214
|
+
const title = documentTitle();
|
|
215
|
+
const content = documentContent();
|
|
216
|
+
if (title) {
|
|
217
|
+
title.textContent = titleText || "Document";
|
|
218
|
+
}
|
|
219
|
+
if (content) {
|
|
220
|
+
content.innerHTML = html || "";
|
|
221
|
+
}
|
|
222
|
+
if (panel) {
|
|
223
|
+
panel.hidden = false;
|
|
224
|
+
if (typeof panel.scrollIntoView === "function") {
|
|
225
|
+
panel.scrollIntoView({ block: "nearest" });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
renderMermaidDiagrams();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function showMermaidFallback(message) {
|
|
232
|
+
document.querySelectorAll(".markdown-preview__mermaid-fallback").forEach((node) => {
|
|
233
|
+
if (!(node instanceof HTMLElement)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
node.hidden = false;
|
|
237
|
+
if (message) {
|
|
238
|
+
node.textContent = message;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderMermaidDiagrams() {
|
|
244
|
+
const nodes = Array.from(document.querySelectorAll(".mermaid"));
|
|
245
|
+
if (nodes.length === 0) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!window.mermaid) {
|
|
249
|
+
showMermaidFallback("Mermaid preview unavailable. Raw diagram source shown below.");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
if (!mermaidInitialized && typeof window.mermaid.initialize === "function") {
|
|
254
|
+
window.mermaid.initialize({ startOnLoad: false, securityLevel: "strict", theme: "dark" });
|
|
255
|
+
mermaidInitialized = true;
|
|
256
|
+
}
|
|
257
|
+
if (typeof window.mermaid.run !== "function") {
|
|
258
|
+
showMermaidFallback("Mermaid preview unavailable. Raw diagram source shown below.");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
Promise.resolve(window.mermaid.run({ nodes })).catch((error) => {
|
|
262
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
263
|
+
showMermaidFallback(`Mermaid preview unavailable. Raw diagram source shown below. (${detail})`);
|
|
264
|
+
});
|
|
265
|
+
} catch (error) {
|
|
266
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
267
|
+
showMermaidFallback(`Mermaid preview unavailable. Raw diagram source shown below. (${detail})`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function applyLocalViewerChrome() {
|
|
272
|
+
if (applyingLocalChrome) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
applyingLocalChrome = true;
|
|
276
|
+
try {
|
|
277
|
+
const hiddenActions = ["promote", "mark-done", "mark-obsolete", "change-status"];
|
|
278
|
+
hiddenActions.forEach((action) => {
|
|
279
|
+
document.querySelectorAll(`[data-action="${action}"]`).forEach((element) => {
|
|
280
|
+
if (!(element instanceof HTMLElement)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
element.hidden = true;
|
|
284
|
+
element.setAttribute("aria-hidden", "true");
|
|
285
|
+
if ("disabled" in element) {
|
|
286
|
+
element.disabled = true;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
document.querySelectorAll('[data-action="open"]').forEach((element) => {
|
|
292
|
+
if (!(element instanceof HTMLElement)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
element.hidden = true;
|
|
296
|
+
element.setAttribute("aria-hidden", "true");
|
|
297
|
+
if ("disabled" in element) {
|
|
298
|
+
element.disabled = true;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
document.querySelectorAll('[data-action="read"]').forEach((element) => {
|
|
303
|
+
if (!(element instanceof HTMLElement)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
element.textContent = "Read document";
|
|
307
|
+
element.title = "Read selected document";
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const editButton = editDocumentButton();
|
|
311
|
+
if (editButton instanceof HTMLButtonElement) {
|
|
312
|
+
const item = selectedItem();
|
|
313
|
+
editButton.disabled = !item;
|
|
314
|
+
editButton.title = item ? "Open selected document in the system editor" : "Select a document to edit";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
document.querySelectorAll(".column__menu-item").forEach((element) => {
|
|
318
|
+
if (!(element instanceof HTMLElement)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const label = (element.textContent || "").trim().toLowerCase();
|
|
322
|
+
if (label === "promote" || label === "open") {
|
|
323
|
+
element.hidden = true;
|
|
324
|
+
element.setAttribute("aria-hidden", "true");
|
|
325
|
+
}
|
|
326
|
+
if (label === "read") {
|
|
327
|
+
element.textContent = "Read document";
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
} finally {
|
|
331
|
+
applyingLocalChrome = false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function postToApp(payload, options = {}) {
|
|
336
|
+
latestItems = Array.isArray(payload.items) ? payload.items : [];
|
|
337
|
+
const nextPayload = options.silent ? payload : applyFocusRequest(payload);
|
|
338
|
+
window.dispatchEvent(new MessageEvent("message", { data: { type: "data", payload: nextPayload } }));
|
|
339
|
+
const rootName = payload.root ? payload.root.split(/[\\/]/).filter(Boolean).pop() : "repository";
|
|
340
|
+
if (!options.silent) {
|
|
341
|
+
setMeta(`${rootName} · ${payload.items.length} docs · refreshed ${new Date().toLocaleTimeString()}`);
|
|
342
|
+
}
|
|
343
|
+
renderUpdateNotice(payload.updateInfo);
|
|
344
|
+
updateFilterSummary();
|
|
345
|
+
applyLocalViewerChrome();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function renderUpdateNotice(updateInfo) {
|
|
349
|
+
const banner = updateBanner();
|
|
350
|
+
if (!(banner instanceof HTMLElement)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (!updateInfo || updateInfo.updateAvailable !== true || !updateInfo.latestVersion) {
|
|
354
|
+
banner.hidden = true;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const copy = updateCopy();
|
|
358
|
+
const command = updateCommand();
|
|
359
|
+
if (copy) {
|
|
360
|
+
copy.textContent = `logics-manager ${updateInfo.latestVersion} is available. Current version: ${updateInfo.currentVersion || "unknown"}.`;
|
|
361
|
+
}
|
|
362
|
+
if (command) {
|
|
363
|
+
command.textContent = updateInfo.updateCommand || "logics-manager self-update";
|
|
364
|
+
}
|
|
365
|
+
banner.hidden = false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function loadItems(method = "GET", options = {}) {
|
|
369
|
+
if (itemsLoadInFlight) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
itemsLoadInFlight = true;
|
|
373
|
+
try {
|
|
374
|
+
if (!options.silent) {
|
|
375
|
+
setMeta("Refreshing...");
|
|
376
|
+
}
|
|
377
|
+
const response = await fetch(method === "POST" ? "/api/refresh" : "/api/items", { method });
|
|
378
|
+
const data = await response.json();
|
|
379
|
+
if (!response.ok || !data.ok) {
|
|
380
|
+
throw new Error(data.error || "Unable to load viewer data.");
|
|
381
|
+
}
|
|
382
|
+
postToApp(data.payload, { silent: Boolean(options.silent) });
|
|
383
|
+
return true;
|
|
384
|
+
} finally {
|
|
385
|
+
itemsLoadInFlight = false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function autoRefreshItems() {
|
|
390
|
+
if (document.hidden) {
|
|
391
|
+
refreshAfterVisible = true;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
loadItems("POST", { silent: true }).catch((error) => setMeta(error.message));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function startAutoRefresh() {
|
|
398
|
+
if (autoRefreshStarted) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
autoRefreshStarted = true;
|
|
402
|
+
window.setInterval(autoRefreshItems, autoRefreshIntervalMs);
|
|
403
|
+
document.addEventListener("visibilitychange", () => {
|
|
404
|
+
if (!document.hidden && refreshAfterVisible) {
|
|
405
|
+
refreshAfterVisible = false;
|
|
406
|
+
autoRefreshItems();
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function statusValue(item) {
|
|
412
|
+
return String(item?.indicators?.Status || "").toLowerCase();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function isClosed(item) {
|
|
416
|
+
const status = statusValue(item);
|
|
417
|
+
return status.includes("done") || status.includes("archived") || status.includes("obsolete");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function hasLinks(item) {
|
|
421
|
+
return (item.references || []).length > 0 || (item.usedBy || []).length > 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function needsPromotion(item) {
|
|
425
|
+
return ["request", "backlog"].includes(item.stage) && !item.isPromoted && !isClosed(item);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function updatedWithin(item, days) {
|
|
429
|
+
const timestamp = Date.parse(item.updatedAt || "") || 0;
|
|
430
|
+
return timestamp > 0 && timestamp >= Date.now() - days * 24 * 60 * 60 * 1000;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function isStale(item) {
|
|
434
|
+
const timestamp = Date.parse(item.updatedAt || "") || 0;
|
|
435
|
+
return timestamp > 0 && timestamp < Date.now() - 30 * 24 * 60 * 60 * 1000 && !isClosed(item);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function matchesViewerFilter(item) {
|
|
439
|
+
if (!item) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
const status = statusValue(item);
|
|
443
|
+
if (viewerFilterState.focus === "active" && isClosed(item)) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
if (viewerFilterState.focus === "blocked" && !status.includes("blocked")) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
if (viewerFilterState.focus === "needs-promotion" && !needsPromotion(item)) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
if (viewerFilterState.focus === "recent" && !updatedWithin(item, 14)) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (viewerFilterState.type === "workflow" && !["request", "backlog", "task"].includes(item.stage)) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
if (viewerFilterState.type === "companion" && !["product", "architecture", "spec"].includes(item.stage)) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
if (!["all", "workflow", "companion"].includes(viewerFilterState.type) && item.stage !== viewerFilterState.type) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (viewerFilterState.status === "ready" && !status.includes("ready")) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
if (viewerFilterState.status === "in-progress" && !status.includes("in progress")) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
if (viewerFilterState.status === "blocked" && !status.includes("blocked")) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
if (viewerFilterState.status === "done" && !isClosed(item)) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (viewerFilterState.relation === "unlinked" && hasLinks(item)) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
if (viewerFilterState.relation === "linked" && !hasLinks(item)) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
if (viewerFilterState.relation === "needs-promotion" && !needsPromotion(item)) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (viewerFilterState.activity === "recent" && !updatedWithin(item, 14)) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
if (viewerFilterState.activity === "stale" && !isStale(item)) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function setControlValue(id, value, eventName) {
|
|
500
|
+
const element = document.getElementById(id);
|
|
501
|
+
if (!element) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (element instanceof HTMLInputElement && element.type === "checkbox") {
|
|
505
|
+
element.checked = Boolean(value);
|
|
506
|
+
} else if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement) {
|
|
507
|
+
element.value = String(value ?? "");
|
|
508
|
+
}
|
|
509
|
+
element.dispatchEvent(new Event(eventName, { bubbles: true }));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function applyViewerFilter(group, value) {
|
|
513
|
+
if (!Object.prototype.hasOwnProperty.call(defaultFilterState, group)) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
viewerFilterState = { ...viewerFilterState, [group]: value || defaultFilterState[group] };
|
|
517
|
+
window.__CDX_LOGICS_VIEWER_FILTER__ = matchesViewerFilter;
|
|
518
|
+
persistViewerFilterState();
|
|
519
|
+
setControlValue("hide-complete", false, "change");
|
|
520
|
+
setControlValue("hide-processed-requests", false, "change");
|
|
521
|
+
setControlValue("hide-spec", false, "change");
|
|
522
|
+
setControlValue("show-companion-docs", true, "change");
|
|
523
|
+
setControlValue("hide-empty-columns", true, "change");
|
|
524
|
+
updateFilterSummary();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function clearLocalPreset() {
|
|
528
|
+
viewerFilterState = { ...defaultFilterState };
|
|
529
|
+
window.__CDX_LOGICS_VIEWER_FILTER__ = matchesViewerFilter;
|
|
530
|
+
persistViewerFilterState();
|
|
531
|
+
setControlValue("search-input", "", "input");
|
|
532
|
+
setControlValue("hide-complete", false, "change");
|
|
533
|
+
setControlValue("hide-processed-requests", false, "change");
|
|
534
|
+
setControlValue("hide-spec", false, "change");
|
|
535
|
+
setControlValue("show-companion-docs", true, "change");
|
|
536
|
+
setControlValue("hide-empty-columns", true, "change");
|
|
537
|
+
updateFilterSummary();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function updateFilterSummary() {
|
|
541
|
+
document.querySelectorAll("[data-viewer-filter-group]").forEach((control) => {
|
|
542
|
+
if (control instanceof HTMLSelectElement) {
|
|
543
|
+
const group = control.getAttribute("data-viewer-filter-group") || "";
|
|
544
|
+
control.value = viewerFilterState[group] || defaultFilterState[group] || "";
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (control instanceof HTMLElement) {
|
|
548
|
+
const group = control.getAttribute("data-viewer-filter-group") || "";
|
|
549
|
+
const value = control.getAttribute("data-viewer-filter-value") || "";
|
|
550
|
+
control.setAttribute("aria-pressed", viewerFilterState[group] === value ? "true" : "false");
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
const count = filterCount();
|
|
554
|
+
if (!count) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const visibleCount = latestItems.filter(matchesViewerFilter).length;
|
|
558
|
+
const activeLabels = Object.entries(viewerFilterState)
|
|
559
|
+
.filter(([key, value]) => value !== defaultFilterState[key])
|
|
560
|
+
.map(([key, value]) => `${key}: ${String(value).replace("-", " ")}`);
|
|
561
|
+
const suffix = activeLabels.length > 0 ? ` · ${activeLabels.join(" · ")}` : " · Active work";
|
|
562
|
+
count.textContent = `${visibleCount} of ${latestItems.length} docs shown${suffix}`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function buildCorpusInsights() {
|
|
566
|
+
const countsByStage = latestItems.reduce((acc, item) => {
|
|
567
|
+
acc[item.stage] = (acc[item.stage] || 0) + 1;
|
|
568
|
+
return acc;
|
|
569
|
+
}, {});
|
|
570
|
+
const active = latestItems.filter((item) => !isClosed(item)).length;
|
|
571
|
+
const blocked = latestItems.filter((item) => statusValue(item).includes("blocked")).length;
|
|
572
|
+
const unlinked = latestItems.filter((item) => (item.references || []).length === 0 && (item.usedBy || []).length === 0).length;
|
|
573
|
+
const incompleteChains = latestItems.filter((item) => ["request", "backlog"].includes(item.stage) && !item.isPromoted && !isClosed(item)).length;
|
|
574
|
+
const cards = [
|
|
575
|
+
["Docs", latestItems.length],
|
|
576
|
+
["Active", active],
|
|
577
|
+
["Blocked", blocked],
|
|
578
|
+
["Unlinked", unlinked]
|
|
579
|
+
].map(([label, value]) => `
|
|
580
|
+
<div class="viewer-insights__card">
|
|
581
|
+
<div class="viewer-insights__label">${escapeHtml(label)}</div>
|
|
582
|
+
<div class="viewer-insights__value">${escapeHtml(value)}</div>
|
|
583
|
+
</div>
|
|
584
|
+
`).join("");
|
|
585
|
+
const stageRows = Object.entries(countsByStage)
|
|
586
|
+
.sort((left, right) => String(left[0]).localeCompare(String(right[0])))
|
|
587
|
+
.map(([stage, count]) => `<li class="viewer-insights__item"><span>${escapeHtml(stage)}</span><strong>${escapeHtml(count)}</strong></li>`)
|
|
588
|
+
.join("");
|
|
589
|
+
const recentRows = [...latestItems]
|
|
590
|
+
.sort((left, right) => (Date.parse(right.updatedAt || "") || 0) - (Date.parse(left.updatedAt || "") || 0))
|
|
591
|
+
.slice(0, 8)
|
|
592
|
+
.map((item) => `<li class="viewer-insights__item"><span>${escapeHtml(item.id)}</span><strong>${escapeHtml(item.indicators?.Status || "No status")}</strong></li>`)
|
|
593
|
+
.join("");
|
|
594
|
+
return `
|
|
595
|
+
<div class="viewer-insights">
|
|
596
|
+
<div class="viewer-insights__summary">${cards}</div>
|
|
597
|
+
<div class="viewer-insights__grid">
|
|
598
|
+
<div class="viewer-insights__card">
|
|
599
|
+
<div class="viewer-insights__label">Incomplete chains</div>
|
|
600
|
+
<div class="viewer-insights__value">${escapeHtml(incompleteChains)}</div>
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
<section class="viewer-insights__section">
|
|
604
|
+
<h2>Corpus families</h2>
|
|
605
|
+
<ul class="viewer-insights__list">${stageRows || '<li class="viewer-insights__item">No docs loaded</li>'}</ul>
|
|
606
|
+
</section>
|
|
607
|
+
<section class="viewer-insights__section">
|
|
608
|
+
<h2>Recent activity</h2>
|
|
609
|
+
<ul class="viewer-insights__list">${recentRows || '<li class="viewer-insights__item">No recent docs</li>'}</ul>
|
|
610
|
+
</section>
|
|
611
|
+
</div>
|
|
612
|
+
`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function showCorpusInsights() {
|
|
616
|
+
setDocument("Corpus insights", buildCorpusInsights());
|
|
617
|
+
setMeta("Corpus insights loaded.");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function showDocument(item) {
|
|
621
|
+
if (!item || !item.relPath) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const response = await fetch(`/api/doc?path=${encodeURIComponent(item.relPath)}`);
|
|
625
|
+
const data = await response.json();
|
|
626
|
+
if (!response.ok || !data.ok) {
|
|
627
|
+
setMeta(data.error || "Unable to read document.");
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const api = markdownApi();
|
|
631
|
+
let markdown = data.document.content || "";
|
|
632
|
+
if (api && typeof api.stripLeadingDocumentFrontMatter === "function") {
|
|
633
|
+
markdown = api.stripLeadingDocumentFrontMatter(markdown, item);
|
|
634
|
+
}
|
|
635
|
+
const html = api && typeof api.renderMarkdownToHtml === "function"
|
|
636
|
+
? api.renderMarkdownToHtml(markdown)
|
|
637
|
+
: `<pre>${escapeHtml(markdown)}</pre>`;
|
|
638
|
+
setDocument(data.document.path, html);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function showDocumentByPath(relPath) {
|
|
642
|
+
const item = findItemByPath(relPath) || { relPath, title: relPath, id: relPath };
|
|
643
|
+
await showDocument(item);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function editDocument(item) {
|
|
647
|
+
if (!item || !item.relPath) {
|
|
648
|
+
setMeta("Select a document to edit.");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
setMeta("Opening document in system editor...");
|
|
652
|
+
const response = await fetch(`/api/edit?path=${encodeURIComponent(item.relPath)}`, { method: "POST" });
|
|
653
|
+
const data = await response.json();
|
|
654
|
+
if (!response.ok || !data.ok) {
|
|
655
|
+
if (response.status === 404 && data.error === "Not found") {
|
|
656
|
+
throw new Error("Edit endpoint unavailable. Restart the local viewer so it loads the current logics-manager code.");
|
|
657
|
+
}
|
|
658
|
+
throw new Error(data.error || "Unable to open document editor.");
|
|
659
|
+
}
|
|
660
|
+
setMeta(`Opened ${data.document.path} in system editor.`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function countPayloadEntries(payload, keys) {
|
|
664
|
+
for (const key of keys) {
|
|
665
|
+
if (Array.isArray(payload?.[key])) {
|
|
666
|
+
return payload[key].length;
|
|
667
|
+
}
|
|
668
|
+
if (typeof payload?.[key] === "number") {
|
|
669
|
+
return payload[key];
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function collectHealthFindings(lintData, auditData) {
|
|
676
|
+
const findings = [];
|
|
677
|
+
const append = (source, payload) => {
|
|
678
|
+
["issues", "warnings", "findings", "strict"].forEach((key) => {
|
|
679
|
+
const entries = Array.isArray(payload?.[key]) ? payload[key] : [];
|
|
680
|
+
entries.forEach((entry) => findings.push({ source, ...entry }));
|
|
681
|
+
});
|
|
682
|
+
};
|
|
683
|
+
append("lint", lintData.payload || {});
|
|
684
|
+
append("audit", auditData.payload || {});
|
|
685
|
+
return findings;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function renderHealthSummary(lintData, auditData) {
|
|
689
|
+
const lintPayload = lintData.payload || {};
|
|
690
|
+
const auditPayload = auditData.payload || {};
|
|
691
|
+
const blocking = countPayloadEntries(lintPayload, ["issue_count", "issues"]) +
|
|
692
|
+
countPayloadEntries(auditPayload, ["issue_count", "issues"]);
|
|
693
|
+
const warnings = countPayloadEntries(lintPayload, ["warning_count", "warnings"]) +
|
|
694
|
+
countPayloadEntries(auditPayload, ["warning_count", "warnings"]);
|
|
695
|
+
const findings = collectHealthFindings(lintData, auditData);
|
|
696
|
+
const releaseReady = Boolean(lintPayload.ok) && Boolean(auditPayload.release_ready ?? auditPayload.ok);
|
|
697
|
+
|
|
698
|
+
const cards = [
|
|
699
|
+
["Blocking", blocking],
|
|
700
|
+
["Warnings", warnings],
|
|
701
|
+
["Release ready", releaseReady ? "Yes" : "No"]
|
|
702
|
+
]
|
|
703
|
+
.map(([label, value]) => `
|
|
704
|
+
<div class="viewer-health__card">
|
|
705
|
+
<div class="viewer-health__label">${escapeHtml(label)}</div>
|
|
706
|
+
<div class="viewer-health__value">${escapeHtml(value)}</div>
|
|
707
|
+
</div>
|
|
708
|
+
`)
|
|
709
|
+
.join("");
|
|
710
|
+
|
|
711
|
+
const list = findings.length
|
|
712
|
+
? findings.slice(0, 50).map((finding) => {
|
|
713
|
+
const path = finding.path || "";
|
|
714
|
+
const pathControl = path
|
|
715
|
+
? `<button class="viewer-health__path" type="button" data-viewer-doc-path="${escapeHtml(path)}">${escapeHtml(path)}</button>`
|
|
716
|
+
: '<span class="viewer-health__meta">Repository-level finding</span>';
|
|
717
|
+
const severity = finding.severity || finding.code || finding.source || "finding";
|
|
718
|
+
return `
|
|
719
|
+
<li class="viewer-health__issue">
|
|
720
|
+
${pathControl}
|
|
721
|
+
<div>${escapeHtml(finding.message || finding.code || "Validation finding")}</div>
|
|
722
|
+
<div class="viewer-health__meta">${escapeHtml(finding.source)} · ${escapeHtml(severity)}</div>
|
|
723
|
+
</li>
|
|
724
|
+
`;
|
|
725
|
+
}).join("")
|
|
726
|
+
: '<li class="viewer-health__empty">No lint or audit findings were reported.</li>';
|
|
727
|
+
|
|
728
|
+
return `
|
|
729
|
+
<div class="viewer-health">
|
|
730
|
+
<div class="viewer-health__summary">${cards}</div>
|
|
731
|
+
<section class="viewer-health__section">
|
|
732
|
+
<h2 class="viewer-health__heading">Validation findings</h2>
|
|
733
|
+
<ul class="viewer-health__list">${list}</ul>
|
|
734
|
+
</section>
|
|
735
|
+
</div>
|
|
736
|
+
`;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function showHealth() {
|
|
740
|
+
setMeta("Checking health...");
|
|
741
|
+
const [lintResponse, auditResponse] = await Promise.all([fetch("/api/lint"), fetch("/api/audit")]);
|
|
742
|
+
const [lintData, auditData] = await Promise.all([lintResponse.json(), auditResponse.json()]);
|
|
743
|
+
setDocument("Validation health", renderHealthSummary(lintData, auditData));
|
|
744
|
+
setMeta("Health loaded.");
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
window.acquireVsCodeApi = function acquireVsCodeApi() {
|
|
748
|
+
return {
|
|
749
|
+
postMessage(message) {
|
|
750
|
+
if (!message || typeof message.type !== "string") {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (message.type === "ready") {
|
|
754
|
+
loadItems().catch((error) => setMeta(error.message));
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (message.type === "refresh") {
|
|
758
|
+
loadItems("POST").catch((error) => setMeta(error.message));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (message.type === "open" || message.type === "read") {
|
|
762
|
+
const item = latestItems.find((entry) => entry.id === message.id);
|
|
763
|
+
showDocument(item).catch((error) => setMeta(error.message));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
setMeta("This action is read-only in the local viewer. Use the CLI for workflow changes.");
|
|
767
|
+
},
|
|
768
|
+
getState() {
|
|
769
|
+
return readStoredState();
|
|
770
|
+
},
|
|
771
|
+
setState(value) {
|
|
772
|
+
const nextState = value && typeof value === "object" ? { ...value } : null;
|
|
773
|
+
if (nextState) {
|
|
774
|
+
nextState.viewerFilterState = sanitizeViewerFilterState(nextState.viewerFilterState || viewerFilterState);
|
|
775
|
+
}
|
|
776
|
+
writeStoredState(nextState);
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
};
|
|
780
|
+
window.addEventListener("load", () => {
|
|
781
|
+
hydrateViewerFilterState();
|
|
782
|
+
window.__CDX_LOGICS_VIEWER_FILTER__ = matchesViewerFilter;
|
|
783
|
+
setControlValue("hide-complete", false, "change");
|
|
784
|
+
setControlValue("hide-processed-requests", false, "change");
|
|
785
|
+
setControlValue("hide-spec", false, "change");
|
|
786
|
+
setControlValue("show-companion-docs", true, "change");
|
|
787
|
+
setControlValue("hide-empty-columns", true, "change");
|
|
788
|
+
applyLocalViewerChrome();
|
|
789
|
+
[document.getElementById("viewer-insights"), document.getElementById("header-logics-insights")].forEach((button) => {
|
|
790
|
+
button?.addEventListener("click", () => {
|
|
791
|
+
showCorpusInsights();
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
document.querySelectorAll('[data-action="refresh"]').forEach((element) => {
|
|
795
|
+
if (!(element instanceof HTMLElement)) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
element.addEventListener("click", () => {
|
|
799
|
+
loadItems("POST").catch((error) => setMeta(error.message));
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
document.getElementById("viewer-health")?.addEventListener("click", () => {
|
|
803
|
+
showHealth().catch((error) => setMeta(error.message));
|
|
804
|
+
});
|
|
805
|
+
document.querySelectorAll("[data-viewer-filter-group]").forEach((element) => {
|
|
806
|
+
if (element instanceof HTMLSelectElement) {
|
|
807
|
+
element.addEventListener("change", () => {
|
|
808
|
+
applyViewerFilter(element.getAttribute("data-viewer-filter-group") || "", element.value || "");
|
|
809
|
+
});
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (!(element instanceof HTMLElement)) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
element.addEventListener("click", () => {
|
|
816
|
+
applyViewerFilter(element.getAttribute("data-viewer-filter-group") || "", element.getAttribute("data-viewer-filter-value") || "");
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
document.getElementById("filter-reset")?.addEventListener("click", () => {
|
|
820
|
+
clearLocalPreset();
|
|
821
|
+
});
|
|
822
|
+
const editButton = editDocumentButton();
|
|
823
|
+
if (editButton instanceof HTMLElement) {
|
|
824
|
+
editButton.addEventListener("click", () => {
|
|
825
|
+
editDocument(selectedItem()).catch((error) => setMeta(error.message));
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
document.addEventListener("click", (event) => {
|
|
829
|
+
window.setTimeout(() => applyLocalViewerChrome(), 0);
|
|
830
|
+
const target = event.target instanceof Element ? event.target.closest("[data-viewer-doc-path]") : null;
|
|
831
|
+
if (!(target instanceof HTMLElement)) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const path = target.getAttribute("data-viewer-doc-path");
|
|
835
|
+
if (path) {
|
|
836
|
+
showDocumentByPath(path).catch((error) => setMeta(error.message));
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
document.getElementById("viewer-document-close")?.addEventListener("click", () => {
|
|
840
|
+
const panel = documentPanel();
|
|
841
|
+
if (panel) {
|
|
842
|
+
panel.hidden = true;
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
startAutoRefresh();
|
|
846
|
+
});
|
|
847
|
+
})();
|