@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,1339 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const GROUP_RENDER_PAGE_SIZE = 10;
|
|
3
|
+
const TASK_COLORS = ["#14b8a6", "#2563eb", "#8b5cf6", "#22c55e", "#06b6d4", "#84cc16", "#0ea5e9", "#7c3aed", "#3b82f6", "#0f766e"];
|
|
4
|
+
const REQUEST_COLORS = ["#f97316", "#f59e0b", "#f43f5e", "#fb7185", "#ef4444", "#d97706", "#ec4899", "#be123c", "#fca5a5", "#fdba74"];
|
|
5
|
+
const CLOSED_TASK_STATUSES = new Set(["done", "archived", "obsolete"]);
|
|
6
|
+
|
|
7
|
+
function plusIcon() {
|
|
8
|
+
return `
|
|
9
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
10
|
+
<path d="M12 5v14M5 12h14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
11
|
+
</svg>
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function chevronIcon(isCollapsed) {
|
|
16
|
+
return isCollapsed ? "▸" : "▾";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createCompanionBadge(label, count, tone) {
|
|
20
|
+
const badge = document.createElement("span");
|
|
21
|
+
badge.className = `card__badge card__badge--${tone}`;
|
|
22
|
+
badge.textContent = count > 1 ? `${label} ${count}` : label;
|
|
23
|
+
badge.title = count > 1 ? `${count} linked ${tone} companion docs` : `Linked ${tone} companion doc`;
|
|
24
|
+
return badge;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeTaskStatus(value) {
|
|
28
|
+
return String(value || "")
|
|
29
|
+
.trim()
|
|
30
|
+
.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isClosedTaskStatus(value) {
|
|
34
|
+
return CLOSED_TASK_STATUSES.has(normalizeTaskStatus(value));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getTaskColor(id) {
|
|
38
|
+
const match = String(id || "").match(/(\d+)$/);
|
|
39
|
+
const n = parseInt(match?.[1] ?? "0", 10);
|
|
40
|
+
return TASK_COLORS[n % TASK_COLORS.length];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getRequestColor(id) {
|
|
44
|
+
const match = String(id || "").match(/(\d+)$/);
|
|
45
|
+
const n = parseInt(match?.[1] ?? "0", 10);
|
|
46
|
+
return REQUEST_COLORS[n % REQUEST_COLORS.length];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isActiveTaskCandidate(item) {
|
|
50
|
+
return Boolean(item && String(item.stage || "").trim() === "task" && !isClosedTaskStatus(item?.indicators?.Status));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildTaskColorMap(items) {
|
|
54
|
+
const activeTasks = (items || []).filter(isActiveTaskCandidate).sort((left, right) => String(left.id).localeCompare(String(right.id)));
|
|
55
|
+
const assignedColors = new Set();
|
|
56
|
+
const colorMap = new Map();
|
|
57
|
+
|
|
58
|
+
for (const task of activeTasks) {
|
|
59
|
+
const preferredIndex = parseInt(String(task.id || "").match(/(\d+)$/)?.[1] ?? "0", 10) % TASK_COLORS.length;
|
|
60
|
+
let assignedColor = TASK_COLORS[preferredIndex];
|
|
61
|
+
for (let offset = 0; offset < TASK_COLORS.length; offset += 1) {
|
|
62
|
+
const candidateColor = TASK_COLORS[(preferredIndex + offset) % TASK_COLORS.length];
|
|
63
|
+
if (!assignedColors.has(candidateColor)) {
|
|
64
|
+
assignedColor = candidateColor;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
assignedColors.add(assignedColor);
|
|
69
|
+
colorMap.set(task.id, assignedColor);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return colorMap;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildRequestColorMap(items) {
|
|
76
|
+
const requests = (items || [])
|
|
77
|
+
.filter((item) => item && String(item.stage || "").trim() === "request")
|
|
78
|
+
.sort((left, right) => String(left.id).localeCompare(String(right.id)));
|
|
79
|
+
const assignedColors = new Set();
|
|
80
|
+
const colorMap = new Map();
|
|
81
|
+
|
|
82
|
+
for (const request of requests) {
|
|
83
|
+
const preferredIndex = parseInt(String(request.id || "").match(/(\d+)$/)?.[1] ?? "0", 10) % REQUEST_COLORS.length;
|
|
84
|
+
let assignedColor = REQUEST_COLORS[preferredIndex];
|
|
85
|
+
for (let offset = 0; offset < REQUEST_COLORS.length; offset += 1) {
|
|
86
|
+
const candidateColor = REQUEST_COLORS[(preferredIndex + offset) % REQUEST_COLORS.length];
|
|
87
|
+
if (!assignedColors.has(candidateColor)) {
|
|
88
|
+
assignedColor = candidateColor;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
assignedColors.add(assignedColor);
|
|
93
|
+
colorMap.set(request.id, assignedColor);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return colorMap;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
window.createCdxLogicsBoardRenderer = function createCdxLogicsBoardRenderer(options) {
|
|
100
|
+
const {
|
|
101
|
+
board,
|
|
102
|
+
hostApi,
|
|
103
|
+
getItems,
|
|
104
|
+
getTotalItemCount,
|
|
105
|
+
getSelectedId,
|
|
106
|
+
setSelectedId,
|
|
107
|
+
isListMode,
|
|
108
|
+
getVisibleStages,
|
|
109
|
+
groupByStage,
|
|
110
|
+
getListGroups,
|
|
111
|
+
isVisible,
|
|
112
|
+
isPrimaryFlowStage,
|
|
113
|
+
isRequestProcessed,
|
|
114
|
+
getStageHeading,
|
|
115
|
+
getStageLabel,
|
|
116
|
+
collectCompanionDocs,
|
|
117
|
+
collectSpecs,
|
|
118
|
+
collectPrimaryFlowItems,
|
|
119
|
+
getAttentionReasons,
|
|
120
|
+
getHealthSignals,
|
|
121
|
+
getSuggestedActions,
|
|
122
|
+
progressState,
|
|
123
|
+
getProgressValue,
|
|
124
|
+
isComplete,
|
|
125
|
+
render,
|
|
126
|
+
openSelectedItem,
|
|
127
|
+
closeColumnMenu,
|
|
128
|
+
toggleColumnMenu,
|
|
129
|
+
persistState,
|
|
130
|
+
getCollapsedListStages,
|
|
131
|
+
getHideCompleted,
|
|
132
|
+
getHideProcessedRequests,
|
|
133
|
+
getHideSpec,
|
|
134
|
+
getShowCompanionDocs,
|
|
135
|
+
getHideEmptyColumns,
|
|
136
|
+
getSearchQuery,
|
|
137
|
+
getGroupMode,
|
|
138
|
+
getSortMode,
|
|
139
|
+
getAttentionOnly
|
|
140
|
+
} = options;
|
|
141
|
+
let activeTaskColorMap = new Map();
|
|
142
|
+
let activeRequestColorMap = new Map();
|
|
143
|
+
let groupRenderLimits = new Map();
|
|
144
|
+
let previousRenderContextKey = "";
|
|
145
|
+
|
|
146
|
+
function findCardById(id) {
|
|
147
|
+
if (!board || !id) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return Array.from(board.querySelectorAll(".card")).find((card) => card.dataset.id === id) || null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function findListHeaderByKey(groupKey) {
|
|
154
|
+
if (!board || !groupKey) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return (
|
|
158
|
+
Array.from(board.querySelectorAll(".list-view__section .list-view__header")).find(
|
|
159
|
+
(header) => header.closest(".list-view__section")?.dataset.group === groupKey
|
|
160
|
+
) || null
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function findListSectionByKey(groupKey) {
|
|
165
|
+
if (!board || !groupKey) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return (
|
|
169
|
+
Array.from(board.querySelectorAll(".list-view__section")).find((section) => section.dataset.group === groupKey) || null
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let sentinelObserver = null;
|
|
174
|
+
let sentinelWrapper = null;
|
|
175
|
+
let sentinelTop = null;
|
|
176
|
+
let sentinelBottom = null;
|
|
177
|
+
|
|
178
|
+
function focusCardById(id) {
|
|
179
|
+
const card = findCardById(id);
|
|
180
|
+
if (card && typeof card.focus === "function") {
|
|
181
|
+
card.focus();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function focusListHeader(groupKey) {
|
|
186
|
+
const header = findListHeaderByKey(groupKey);
|
|
187
|
+
if (header && typeof header.focus === "function") {
|
|
188
|
+
header.focus();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getVisibleGroupedItems() {
|
|
193
|
+
return groupByStage(getItems().filter((item) => isVisible(item)));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function normalizeGroupKey(groupKey) {
|
|
197
|
+
return String(groupKey || "group");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function hasActiveSearch() {
|
|
201
|
+
return String(getSearchQuery && getSearchQuery() ? getSearchQuery() : "").trim() !== "";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getRenderContextKey() {
|
|
205
|
+
return [
|
|
206
|
+
isListMode() ? "list" : "board",
|
|
207
|
+
typeof getGroupMode === "function" ? getGroupMode() : "stage",
|
|
208
|
+
typeof getSortMode === "function" ? getSortMode() : "updated-desc",
|
|
209
|
+
getSearchQuery(),
|
|
210
|
+
getHideCompleted(),
|
|
211
|
+
getHideProcessedRequests(),
|
|
212
|
+
getHideSpec(),
|
|
213
|
+
getShowCompanionDocs(),
|
|
214
|
+
getAttentionOnly()
|
|
215
|
+
].join("|");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function reconcileGroupRenderLimits() {
|
|
219
|
+
const nextContextKey = getRenderContextKey();
|
|
220
|
+
if (nextContextKey !== previousRenderContextKey) {
|
|
221
|
+
groupRenderLimits = new Map();
|
|
222
|
+
previousRenderContextKey = nextContextKey;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function visibleSliceForGroup(groupKey, items) {
|
|
227
|
+
const allItems = Array.isArray(items) ? items : [];
|
|
228
|
+
if (hasActiveSearch() || allItems.length <= GROUP_RENDER_PAGE_SIZE) {
|
|
229
|
+
return {
|
|
230
|
+
items: allItems,
|
|
231
|
+
limit: allItems.length,
|
|
232
|
+
remaining: 0,
|
|
233
|
+
total: allItems.length,
|
|
234
|
+
truncated: false
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const key = normalizeGroupKey(groupKey);
|
|
238
|
+
const limit = Math.max(GROUP_RENDER_PAGE_SIZE, groupRenderLimits.get(key) || GROUP_RENDER_PAGE_SIZE);
|
|
239
|
+
const visibleLimit = Math.min(limit, allItems.length);
|
|
240
|
+
return {
|
|
241
|
+
items: allItems.slice(0, visibleLimit),
|
|
242
|
+
limit: visibleLimit,
|
|
243
|
+
remaining: allItems.length - visibleLimit,
|
|
244
|
+
total: allItems.length,
|
|
245
|
+
truncated: visibleLimit < allItems.length
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function ensureGroupRenderLimit(groupKey, minVisibleCount) {
|
|
250
|
+
if (hasActiveSearch()) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const key = normalizeGroupKey(groupKey);
|
|
254
|
+
const currentLimit = Math.max(GROUP_RENDER_PAGE_SIZE, groupRenderLimits.get(key) || GROUP_RENDER_PAGE_SIZE);
|
|
255
|
+
if (minVisibleCount > currentLimit) {
|
|
256
|
+
groupRenderLimits.set(key, minVisibleCount);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createShowMoreControl(groupKey, remaining, total) {
|
|
261
|
+
const revealCount = Math.min(GROUP_RENDER_PAGE_SIZE, Math.max(0, remaining));
|
|
262
|
+
const button = document.createElement("button");
|
|
263
|
+
button.type = "button";
|
|
264
|
+
button.className = "group-show-more";
|
|
265
|
+
button.dataset.group = normalizeGroupKey(groupKey);
|
|
266
|
+
button.textContent = `Show ${revealCount} more`;
|
|
267
|
+
button.title = `${remaining} of ${total} items hidden in this group`;
|
|
268
|
+
button.setAttribute("aria-label", `Show ${revealCount} more items in this group, ${remaining} remaining`);
|
|
269
|
+
button.addEventListener("click", () => {
|
|
270
|
+
const key = normalizeGroupKey(groupKey);
|
|
271
|
+
const currentLimit = Math.max(GROUP_RENDER_PAGE_SIZE, groupRenderLimits.get(key) || GROUP_RENDER_PAGE_SIZE);
|
|
272
|
+
groupRenderLimits.set(key, currentLimit + GROUP_RENDER_PAGE_SIZE);
|
|
273
|
+
render();
|
|
274
|
+
});
|
|
275
|
+
return button;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function formatRenderedCount(visibleCount, totalCount) {
|
|
279
|
+
const normalizedTotal = Math.max(0, totalCount || 0);
|
|
280
|
+
return `${visibleCount}/${normalizedTotal}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function disconnectSentinels() {
|
|
284
|
+
if (sentinelObserver) {
|
|
285
|
+
sentinelObserver.disconnect();
|
|
286
|
+
sentinelObserver = null;
|
|
287
|
+
}
|
|
288
|
+
if (sentinelTop) {
|
|
289
|
+
sentinelTop.remove();
|
|
290
|
+
sentinelTop = null;
|
|
291
|
+
}
|
|
292
|
+
if (sentinelBottom) {
|
|
293
|
+
sentinelBottom.remove();
|
|
294
|
+
sentinelBottom = null;
|
|
295
|
+
}
|
|
296
|
+
if (sentinelWrapper) {
|
|
297
|
+
sentinelWrapper.remove();
|
|
298
|
+
sentinelWrapper = null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getVisibleBoardStages(grouped) {
|
|
303
|
+
return getVisibleStages().filter((stage) => {
|
|
304
|
+
if (!getHideEmptyColumns()) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
return (grouped[stage] || []).length > 0;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function selectItemAndFocus(id) {
|
|
312
|
+
if (!id) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
setSelectedId(id);
|
|
316
|
+
render();
|
|
317
|
+
focusCardById(id);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function toggleListStageCollapsed(groupKey, collapsed) {
|
|
321
|
+
const collapsedStages = getCollapsedListStages();
|
|
322
|
+
if (collapsed) {
|
|
323
|
+
collapsedStages.add(groupKey);
|
|
324
|
+
} else {
|
|
325
|
+
collapsedStages.delete(groupKey);
|
|
326
|
+
}
|
|
327
|
+
persistState();
|
|
328
|
+
const section = findListSectionByKey(groupKey);
|
|
329
|
+
const header = findListHeaderByKey(groupKey);
|
|
330
|
+
const chevron = header ? header.querySelector(".list-view__header-icon") : null;
|
|
331
|
+
const body = section ? section.querySelector(".list-view__body") : null;
|
|
332
|
+
if (header) {
|
|
333
|
+
header.setAttribute("aria-expanded", String(!collapsed));
|
|
334
|
+
}
|
|
335
|
+
if (chevron) {
|
|
336
|
+
chevron.textContent = chevronIcon(collapsed);
|
|
337
|
+
}
|
|
338
|
+
if (body) {
|
|
339
|
+
body.hidden = collapsed;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function createSentinelElement(variant) {
|
|
344
|
+
const sentinel = document.createElement("div");
|
|
345
|
+
sentinel.className = `list-view__sentinel list-view__sentinel--${variant}`;
|
|
346
|
+
sentinel.hidden = true;
|
|
347
|
+
|
|
348
|
+
const icon = document.createElement("span");
|
|
349
|
+
icon.className = "list-view__sentinel-icon";
|
|
350
|
+
icon.setAttribute("aria-hidden", "true");
|
|
351
|
+
icon.textContent = chevronIcon(variant === "bottom");
|
|
352
|
+
sentinel.appendChild(icon);
|
|
353
|
+
|
|
354
|
+
const label = document.createElement("span");
|
|
355
|
+
label.className = "list-view__sentinel-label";
|
|
356
|
+
sentinel.appendChild(label);
|
|
357
|
+
|
|
358
|
+
const count = document.createElement("span");
|
|
359
|
+
count.className = "list-view__sentinel-count";
|
|
360
|
+
sentinel.appendChild(count);
|
|
361
|
+
|
|
362
|
+
return sentinel;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function updateSentinelFromHeader(sentinel, header) {
|
|
366
|
+
if (!sentinel) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const label = sentinel.querySelector(".list-view__sentinel-label");
|
|
370
|
+
const count = sentinel.querySelector(".list-view__sentinel-count");
|
|
371
|
+
if (!header || !label || !count) {
|
|
372
|
+
sentinel.hidden = true;
|
|
373
|
+
if (label) {
|
|
374
|
+
label.textContent = "";
|
|
375
|
+
}
|
|
376
|
+
if (count) {
|
|
377
|
+
count.textContent = "";
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
label.textContent = header.querySelector(".list-view__header-label")?.textContent?.trim() || "";
|
|
382
|
+
count.textContent = header.querySelector(".list-view__header-count")?.textContent?.trim() || "";
|
|
383
|
+
sentinel.hidden = false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function attachSentinelObserver(wrapperEl, boardListEl, topSentinel, bottomSentinel) {
|
|
387
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const headers = Array.from(wrapperEl.querySelectorAll(".list-view__header"));
|
|
391
|
+
if (headers.length === 0) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const headerStates = new Map();
|
|
395
|
+
const refreshSentinels = () => {
|
|
396
|
+
const aboveHeaders = headers.filter((header) => headerStates.get(header) === "above");
|
|
397
|
+
const belowHeaders = headers.filter((header) => headerStates.get(header) === "below");
|
|
398
|
+
updateSentinelFromHeader(topSentinel, aboveHeaders.length > 0 ? aboveHeaders[aboveHeaders.length - 1] : null);
|
|
399
|
+
updateSentinelFromHeader(bottomSentinel, belowHeaders.length > 0 ? belowHeaders[0] : null);
|
|
400
|
+
};
|
|
401
|
+
sentinelObserver = new IntersectionObserver(
|
|
402
|
+
(entries) => {
|
|
403
|
+
entries.forEach((entry) => {
|
|
404
|
+
if (entry.isIntersecting) {
|
|
405
|
+
headerStates.set(entry.target, "visible");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (entry.rootBounds && entry.boundingClientRect.bottom < entry.rootBounds.top) {
|
|
409
|
+
headerStates.set(entry.target, "above");
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (entry.rootBounds && entry.boundingClientRect.top > entry.rootBounds.bottom) {
|
|
413
|
+
headerStates.set(entry.target, "below");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
headerStates.set(entry.target, "below");
|
|
417
|
+
});
|
|
418
|
+
refreshSentinels();
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
root: boardListEl,
|
|
422
|
+
threshold: [0, 1]
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
headers.forEach((header) => sentinelObserver.observe(header));
|
|
426
|
+
refreshSentinels();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function moveBoardSelection(item, direction) {
|
|
430
|
+
const grouped = getVisibleGroupedItems();
|
|
431
|
+
const visibleStages = getVisibleBoardStages(grouped);
|
|
432
|
+
const stageIndex = visibleStages.indexOf(item.stage);
|
|
433
|
+
if (stageIndex === -1) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const stageItems = grouped[item.stage] || [];
|
|
438
|
+
const itemIndex = stageItems.findIndex((entry) => entry.id === item.id);
|
|
439
|
+
if (itemIndex === -1) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (direction === "up" && itemIndex > 0) {
|
|
444
|
+
ensureGroupRenderLimit(item.stage, itemIndex);
|
|
445
|
+
selectItemAndFocus(stageItems[itemIndex - 1].id);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (direction === "down" && itemIndex < stageItems.length - 1) {
|
|
450
|
+
ensureGroupRenderLimit(item.stage, itemIndex + 2);
|
|
451
|
+
selectItemAndFocus(stageItems[itemIndex + 1].id);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (direction !== "left" && direction !== "right") {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const step = direction === "left" ? -1 : 1;
|
|
460
|
+
for (let nextStageIndex = stageIndex + step; nextStageIndex >= 0 && nextStageIndex < visibleStages.length; nextStageIndex += step) {
|
|
461
|
+
const nextStage = visibleStages[nextStageIndex];
|
|
462
|
+
const nextItems = grouped[nextStage] || [];
|
|
463
|
+
if (!nextItems.length) {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const targetIndex = Math.min(itemIndex, nextItems.length - 1);
|
|
467
|
+
ensureGroupRenderLimit(nextStage, targetIndex + 1);
|
|
468
|
+
selectItemAndFocus(nextItems[targetIndex].id);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function moveListSelection(item, direction) {
|
|
474
|
+
const groups = typeof getListGroups === "function" ? getListGroups() : [];
|
|
475
|
+
const currentGroup = groups.find((group) => (group.items || []).some((entry) => entry.id === item.id));
|
|
476
|
+
const stageItems = currentGroup ? currentGroup.items || [] : [];
|
|
477
|
+
const itemIndex = stageItems.findIndex((entry) => entry.id === item.id);
|
|
478
|
+
if (itemIndex === -1) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (direction === "up" && itemIndex > 0) {
|
|
483
|
+
ensureGroupRenderLimit(currentGroup.key, itemIndex);
|
|
484
|
+
selectItemAndFocus(stageItems[itemIndex - 1].id);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (direction === "down" && itemIndex < stageItems.length - 1) {
|
|
489
|
+
ensureGroupRenderLimit(currentGroup.key, itemIndex + 2);
|
|
490
|
+
selectItemAndFocus(stageItems[itemIndex + 1].id);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (direction === "left" && currentGroup && !getCollapsedListStages().has(currentGroup.key)) {
|
|
495
|
+
toggleListStageCollapsed(currentGroup.key, true);
|
|
496
|
+
focusListHeader(currentGroup.key);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function handleCardKeydown(event, item) {
|
|
501
|
+
if (event.key === "ArrowUp") {
|
|
502
|
+
event.preventDefault();
|
|
503
|
+
if (isListMode()) {
|
|
504
|
+
moveListSelection(item, "up");
|
|
505
|
+
} else {
|
|
506
|
+
moveBoardSelection(item, "up");
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (event.key === "ArrowDown") {
|
|
512
|
+
event.preventDefault();
|
|
513
|
+
if (isListMode()) {
|
|
514
|
+
moveListSelection(item, "down");
|
|
515
|
+
} else {
|
|
516
|
+
moveBoardSelection(item, "down");
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (event.key === "ArrowLeft") {
|
|
522
|
+
event.preventDefault();
|
|
523
|
+
if (isListMode()) {
|
|
524
|
+
moveListSelection(item, "left");
|
|
525
|
+
} else {
|
|
526
|
+
moveBoardSelection(item, "left");
|
|
527
|
+
}
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (event.key === "ArrowRight") {
|
|
532
|
+
event.preventDefault();
|
|
533
|
+
if (!isListMode()) {
|
|
534
|
+
moveBoardSelection(item, "right");
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (event.key === "Enter") {
|
|
540
|
+
event.preventDefault();
|
|
541
|
+
setSelectedId(item.id);
|
|
542
|
+
render();
|
|
543
|
+
focusCardById(item.id);
|
|
544
|
+
if (event.shiftKey) {
|
|
545
|
+
openSelectedItem("read");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (event.metaKey || event.ctrlKey) {
|
|
549
|
+
openSelectedItem("open");
|
|
550
|
+
}
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (event.key === " ") {
|
|
555
|
+
event.preventDefault();
|
|
556
|
+
setSelectedId(item.id);
|
|
557
|
+
render();
|
|
558
|
+
focusCardById(item.id);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function captureBoardScroll() {
|
|
563
|
+
if (!board) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
const scrollLeft = board.scrollLeft;
|
|
567
|
+
const columnScroll = new Map();
|
|
568
|
+
board.querySelectorAll(".column").forEach((column) => {
|
|
569
|
+
const stage = column.dataset.stage;
|
|
570
|
+
const body = column.querySelector(".column__body");
|
|
571
|
+
if (stage && body) {
|
|
572
|
+
columnScroll.set(stage, body.scrollTop);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
return { scrollLeft, columnScroll };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function restoreBoardScroll(state) {
|
|
579
|
+
if (!board || !state) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
board.scrollLeft = state.scrollLeft;
|
|
583
|
+
board.querySelectorAll(".column").forEach((column) => {
|
|
584
|
+
const stage = column.dataset.stage;
|
|
585
|
+
const body = column.querySelector(".column__body");
|
|
586
|
+
if (!stage || !body) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const scrollTop = state.columnScroll.get(stage);
|
|
590
|
+
if (typeof scrollTop === "number") {
|
|
591
|
+
body.scrollTop = scrollTop;
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function getEmptyBoardMessage() {
|
|
597
|
+
if (typeof getTotalItemCount === "function" && getTotalItemCount() === 0) {
|
|
598
|
+
return "No Logics items found. Use Tools > New Request or Bootstrap Logics to populate the board.";
|
|
599
|
+
}
|
|
600
|
+
if (typeof getAttentionOnly === "function" && getAttentionOnly()) {
|
|
601
|
+
return "No items currently match the attention view. This view only shows blocked, orphaned, unprocessed, or inconsistent items.";
|
|
602
|
+
}
|
|
603
|
+
const query = String(typeof getSearchQuery === "function" ? getSearchQuery() : "").trim();
|
|
604
|
+
if (query) {
|
|
605
|
+
return `No items match search "${query}". Clear or refine the search to widen the view.`;
|
|
606
|
+
}
|
|
607
|
+
if (getHideCompleted() || getHideProcessedRequests() || getHideSpec() || getShowCompanionDocs() || getHideEmptyColumns()) {
|
|
608
|
+
const filters = [];
|
|
609
|
+
if (getHideCompleted()) {
|
|
610
|
+
filters.push('"Hide completed"');
|
|
611
|
+
}
|
|
612
|
+
if (getHideProcessedRequests()) {
|
|
613
|
+
filters.push('"Hide processed requests"');
|
|
614
|
+
}
|
|
615
|
+
if (getHideSpec()) {
|
|
616
|
+
filters.push('"Hide SPEC"');
|
|
617
|
+
}
|
|
618
|
+
if (getShowCompanionDocs()) {
|
|
619
|
+
filters.push('"Show companion docs"');
|
|
620
|
+
}
|
|
621
|
+
if (getHideEmptyColumns()) {
|
|
622
|
+
filters.push('"Hide empty columns"');
|
|
623
|
+
}
|
|
624
|
+
return `No items match the current filters. Adjust ${filters.join(" and ")} to change the view.`;
|
|
625
|
+
}
|
|
626
|
+
return "No Logics items found. Use Tools > New Request or Bootstrap Logics to populate the board.";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function createCompanionBadges(item) {
|
|
630
|
+
const companionDocs = collectCompanionDocs(item);
|
|
631
|
+
const specs = collectSpecs(item);
|
|
632
|
+
if (!isPrimaryFlowStage(item.stage) || (companionDocs.length === 0 && specs.length === 0)) {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const counts = companionDocs.reduce(
|
|
637
|
+
(acc, companion) => {
|
|
638
|
+
if (companion.stage === "product") acc.product += 1;
|
|
639
|
+
if (companion.stage === "architecture") acc.architecture += 1;
|
|
640
|
+
return acc;
|
|
641
|
+
},
|
|
642
|
+
{ product: 0, architecture: 0 }
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
const badges = document.createElement("div");
|
|
646
|
+
badges.className = "card__badges";
|
|
647
|
+
if (counts.product > 0) {
|
|
648
|
+
badges.appendChild(createCompanionBadge("PROD", counts.product, "product"));
|
|
649
|
+
}
|
|
650
|
+
if (counts.architecture > 0) {
|
|
651
|
+
badges.appendChild(createCompanionBadge("ADR", counts.architecture, "architecture"));
|
|
652
|
+
}
|
|
653
|
+
if (specs.length > 0) {
|
|
654
|
+
badges.appendChild(createCompanionBadge("SPEC", specs.length, "spec"));
|
|
655
|
+
}
|
|
656
|
+
return badges.childElementCount > 0 ? badges : null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function createSuggestedBadges(item) {
|
|
660
|
+
if (typeof getSuggestedActions !== "function") {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
const actions = getSuggestedActions(item).filter((action) => action.key !== "promote" && action.key !== "add-docs");
|
|
664
|
+
if (!actions || actions.length === 0) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const badges = document.createElement("div");
|
|
669
|
+
badges.className = "card__badges card__badges--suggested";
|
|
670
|
+
actions.forEach((action) => {
|
|
671
|
+
const badge = document.createElement("span");
|
|
672
|
+
badge.className = "card__badge card__badge--suggested";
|
|
673
|
+
badge.textContent = action.label;
|
|
674
|
+
badge.title = action.title;
|
|
675
|
+
badges.appendChild(badge);
|
|
676
|
+
});
|
|
677
|
+
return badges;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function createHealthBadges(item) {
|
|
681
|
+
if (typeof getAttentionReasons !== "function") {
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
const reasons = getAttentionReasons(item);
|
|
685
|
+
if (!reasons || reasons.length === 0) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const badges = document.createElement("div");
|
|
690
|
+
badges.className = "card__badges card__badges--health";
|
|
691
|
+
reasons.slice(0, 2).forEach((reason) => {
|
|
692
|
+
const badge = document.createElement("span");
|
|
693
|
+
badge.className = `card__badge card__badge--health card__badge--health-${reason.key}`;
|
|
694
|
+
badge.textContent = reason.shortLabel || reason.label;
|
|
695
|
+
badge.title = reason.description || reason.label;
|
|
696
|
+
badges.appendChild(badge);
|
|
697
|
+
});
|
|
698
|
+
return badges;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function createCardBadgeStrip(item, activeTasks) {
|
|
702
|
+
const badgeStrip = document.createElement("div");
|
|
703
|
+
badgeStrip.className = "card__badges card__badges--strip";
|
|
704
|
+
|
|
705
|
+
const badgeGroups = [
|
|
706
|
+
createProgressComplexityBadge(item),
|
|
707
|
+
createCompanionBadges(item),
|
|
708
|
+
createHealthBadges(item),
|
|
709
|
+
createSuggestedBadges(item),
|
|
710
|
+
];
|
|
711
|
+
|
|
712
|
+
badgeGroups.forEach((group) => {
|
|
713
|
+
if (group) {
|
|
714
|
+
badgeStrip.appendChild(group);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
return badgeStrip.childElementCount > 0 ? badgeStrip : null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function createMetricSegment(prefix, value) {
|
|
722
|
+
const segment = document.createElement("span");
|
|
723
|
+
segment.className = "card__badge-metric-segment";
|
|
724
|
+
|
|
725
|
+
const prefixEl = document.createElement("span");
|
|
726
|
+
prefixEl.className = "card__badge-metric-prefix";
|
|
727
|
+
prefixEl.textContent = prefix;
|
|
728
|
+
segment.appendChild(prefixEl);
|
|
729
|
+
|
|
730
|
+
const valueEl = document.createElement("span");
|
|
731
|
+
valueEl.className = "card__badge-metric-value";
|
|
732
|
+
valueEl.textContent = value;
|
|
733
|
+
segment.appendChild(valueEl);
|
|
734
|
+
|
|
735
|
+
return segment;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function createPrimaryFlowSummary(item) {
|
|
739
|
+
if (isPrimaryFlowStage(item.stage)) {
|
|
740
|
+
return "";
|
|
741
|
+
}
|
|
742
|
+
const linkedItems = collectPrimaryFlowItems(item);
|
|
743
|
+
if (linkedItems.length === 0) {
|
|
744
|
+
return "Unlinked to primary flow";
|
|
745
|
+
}
|
|
746
|
+
const preview = linkedItems
|
|
747
|
+
.slice(0, 2)
|
|
748
|
+
.map((linkedItem) => `${getStageLabel(linkedItem.stage)} • ${linkedItem.id}`)
|
|
749
|
+
.join(", ");
|
|
750
|
+
if (linkedItems.length > 2) {
|
|
751
|
+
return `For ${preview}, +${linkedItems.length - 2} more`;
|
|
752
|
+
}
|
|
753
|
+
return `For ${preview}`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function formatPreviewDate(value) {
|
|
757
|
+
const timestamp = Date.parse(value || "");
|
|
758
|
+
if (!timestamp) {
|
|
759
|
+
return "Unknown";
|
|
760
|
+
}
|
|
761
|
+
const date = new Date(timestamp);
|
|
762
|
+
const diffMs = Date.now() - date.getTime();
|
|
763
|
+
if (diffMs >= 0 && diffMs < 24 * 60 * 60 * 1000) {
|
|
764
|
+
const totalMinutes = Math.max(1, Math.round(diffMs / (60 * 1000)));
|
|
765
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
766
|
+
const minutes = totalMinutes % 60;
|
|
767
|
+
const relativeLabel =
|
|
768
|
+
hours > 0
|
|
769
|
+
? minutes > 0
|
|
770
|
+
? `${hours}h ${minutes}m ago`
|
|
771
|
+
: `${hours}h ago`
|
|
772
|
+
: `${totalMinutes}m ago`;
|
|
773
|
+
const preciseTime = new Intl.DateTimeFormat(undefined, {
|
|
774
|
+
hour: "2-digit",
|
|
775
|
+
minute: "2-digit"
|
|
776
|
+
}).format(date);
|
|
777
|
+
return `${relativeLabel} • ${preciseTime}`;
|
|
778
|
+
}
|
|
779
|
+
return date.toLocaleDateString("en-CA");
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function createPreviewRow(label, value) {
|
|
783
|
+
const row = document.createElement("div");
|
|
784
|
+
row.className = "card__preview-row";
|
|
785
|
+
|
|
786
|
+
const term = document.createElement("span");
|
|
787
|
+
term.className = "card__preview-label";
|
|
788
|
+
term.textContent = label;
|
|
789
|
+
row.appendChild(term);
|
|
790
|
+
|
|
791
|
+
const description = document.createElement("span");
|
|
792
|
+
description.className = "card__preview-value";
|
|
793
|
+
description.textContent = value;
|
|
794
|
+
row.appendChild(description);
|
|
795
|
+
|
|
796
|
+
return row;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function normalizeComplexityLabel(value) {
|
|
800
|
+
const raw = String(value || "").trim();
|
|
801
|
+
if (!raw) {
|
|
802
|
+
return "";
|
|
803
|
+
}
|
|
804
|
+
const normalized = raw.toLowerCase();
|
|
805
|
+
if (normalized === "very low") {
|
|
806
|
+
return "VL";
|
|
807
|
+
}
|
|
808
|
+
if (normalized === "low") {
|
|
809
|
+
return "L";
|
|
810
|
+
}
|
|
811
|
+
if (normalized === "medium") {
|
|
812
|
+
return "M";
|
|
813
|
+
}
|
|
814
|
+
if (normalized === "high") {
|
|
815
|
+
return "H";
|
|
816
|
+
}
|
|
817
|
+
if (normalized === "very high") {
|
|
818
|
+
return "VH";
|
|
819
|
+
}
|
|
820
|
+
if (raw.length <= 3) {
|
|
821
|
+
return raw.toUpperCase();
|
|
822
|
+
}
|
|
823
|
+
return raw.slice(0, 1).toUpperCase();
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function getDocumentPrefix(item) {
|
|
827
|
+
const stage = String(item?.stage || "").trim();
|
|
828
|
+
const prefixByStage = {
|
|
829
|
+
request: "R",
|
|
830
|
+
backlog: "I",
|
|
831
|
+
task: "T",
|
|
832
|
+
product: "P",
|
|
833
|
+
architecture: "A",
|
|
834
|
+
spec: "S"
|
|
835
|
+
};
|
|
836
|
+
const prefix = prefixByStage[stage] || (stage ? stage.slice(0, 1).toUpperCase() : "");
|
|
837
|
+
const match = String(item?.id || "").match(/^[a-z]+_(\d+)/i) || String(item?.id || "").match(/(\d+)/);
|
|
838
|
+
if (!prefix || !match) {
|
|
839
|
+
return "";
|
|
840
|
+
}
|
|
841
|
+
return `${prefix}${String(match[1] || "").padStart(3, "0")}`;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function createProgressComplexityBadge(item) {
|
|
845
|
+
const badge = document.createElement("div");
|
|
846
|
+
badge.className = "card__badges card__badges--metrics";
|
|
847
|
+
|
|
848
|
+
const stage = String(item?.stage || "").trim();
|
|
849
|
+
const indicators = item?.indicators || {};
|
|
850
|
+
const understandingValue = String(indicators.Understanding || "").trim();
|
|
851
|
+
const confidenceValue = String(indicators.Confidence || "").trim();
|
|
852
|
+
const complexityValue = String(indicators.Complexity || "").trim();
|
|
853
|
+
const progressValue = getProgressValue(item);
|
|
854
|
+
const showUnderstandingConfidence = Boolean(understandingValue || confidenceValue);
|
|
855
|
+
const useUnderstandingConfidence = showUnderstandingConfidence || stage === "request";
|
|
856
|
+
|
|
857
|
+
const normalizedPrimary =
|
|
858
|
+
useUnderstandingConfidence && understandingValue
|
|
859
|
+
? understandingValue.match(/(\d+(?:\.\d+)?)/)
|
|
860
|
+
: typeof progressValue === "number"
|
|
861
|
+
? [String(Math.max(0, Math.min(100, Math.round(progressValue))))]
|
|
862
|
+
: null;
|
|
863
|
+
const normalizedSecondary =
|
|
864
|
+
useUnderstandingConfidence && confidenceValue
|
|
865
|
+
? confidenceValue.match(/(\d+(?:\.\d+)?)/)
|
|
866
|
+
: complexityValue
|
|
867
|
+
? complexityValue.match(/(\d+(?:\.\d+)?)/)
|
|
868
|
+
: null;
|
|
869
|
+
const complexityLabel = useUnderstandingConfidence ? complexityValue : complexityValue;
|
|
870
|
+
|
|
871
|
+
if (!normalizedPrimary && !normalizedSecondary && !complexityLabel) {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const pill = document.createElement("span");
|
|
876
|
+
pill.className = "card__badge card__badge--metric";
|
|
877
|
+
const primaryText = normalizedPrimary ? `${Math.max(0, Math.min(100, Math.round(Number(normalizedPrimary[1] || normalizedPrimary[0]))))}%` : "—";
|
|
878
|
+
const secondaryText = normalizedSecondary ? `${Math.max(0, Math.min(100, Math.round(Number(normalizedSecondary[1] || normalizedSecondary[0]))))}%` : "—";
|
|
879
|
+
const complexityText = complexityLabel ? normalizeComplexityLabel(complexityLabel) : "—";
|
|
880
|
+
|
|
881
|
+
if (useUnderstandingConfidence) {
|
|
882
|
+
pill.appendChild(createMetricSegment("U", primaryText));
|
|
883
|
+
const separatorOne = document.createElement("span");
|
|
884
|
+
separatorOne.className = "card__badge-metric-separator";
|
|
885
|
+
separatorOne.textContent = "/";
|
|
886
|
+
pill.appendChild(separatorOne);
|
|
887
|
+
pill.appendChild(createMetricSegment("C", secondaryText));
|
|
888
|
+
if (complexityValue) {
|
|
889
|
+
const separatorTwo = document.createElement("span");
|
|
890
|
+
separatorTwo.className = "card__badge-metric-separator";
|
|
891
|
+
separatorTwo.textContent = "/";
|
|
892
|
+
pill.appendChild(separatorTwo);
|
|
893
|
+
const complexitySegment = document.createElement("span");
|
|
894
|
+
complexitySegment.className = "card__badge-metric-value card__badge-metric-value--complexity";
|
|
895
|
+
complexitySegment.textContent = complexityText;
|
|
896
|
+
pill.appendChild(complexitySegment);
|
|
897
|
+
}
|
|
898
|
+
pill.title = [
|
|
899
|
+
understandingValue ? `Understanding: ${understandingValue}` : null,
|
|
900
|
+
confidenceValue ? `Confidence: ${confidenceValue}` : null,
|
|
901
|
+
complexityValue ? `Complexity: ${complexityValue}` : null
|
|
902
|
+
]
|
|
903
|
+
.filter(Boolean)
|
|
904
|
+
.join(" • ");
|
|
905
|
+
} else {
|
|
906
|
+
pill.appendChild(createMetricSegment("P", primaryText));
|
|
907
|
+
const separator = document.createElement("span");
|
|
908
|
+
separator.className = "card__badge-metric-separator";
|
|
909
|
+
separator.textContent = "/";
|
|
910
|
+
pill.appendChild(separator);
|
|
911
|
+
const complexitySegment = document.createElement("span");
|
|
912
|
+
complexitySegment.className = "card__badge-metric-value card__badge-metric-value--complexity";
|
|
913
|
+
complexitySegment.textContent = complexityText;
|
|
914
|
+
pill.appendChild(complexitySegment);
|
|
915
|
+
const titleParts = [];
|
|
916
|
+
if (typeof progressValue === "number") {
|
|
917
|
+
titleParts.push(`Progress: ${Math.max(0, Math.min(100, Math.round(progressValue)))}%`);
|
|
918
|
+
}
|
|
919
|
+
if (complexityValue) {
|
|
920
|
+
titleParts.push(`Complexity: ${complexityValue}`);
|
|
921
|
+
}
|
|
922
|
+
pill.title = titleParts.join(" • ");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
badge.appendChild(pill);
|
|
926
|
+
return badge;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function createCardTitle(item) {
|
|
930
|
+
const titleEl = document.createElement("div");
|
|
931
|
+
titleEl.className = "card__title";
|
|
932
|
+
|
|
933
|
+
const prefix = getDocumentPrefix(item);
|
|
934
|
+
if (prefix) {
|
|
935
|
+
const prefixEl = document.createElement("span");
|
|
936
|
+
prefixEl.className = "card__title-prefix";
|
|
937
|
+
prefixEl.textContent = prefix;
|
|
938
|
+
titleEl.appendChild(prefixEl);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const textEl = document.createElement("span");
|
|
942
|
+
textEl.className = "card__title-text";
|
|
943
|
+
textEl.textContent = item.title;
|
|
944
|
+
titleEl.appendChild(textEl);
|
|
945
|
+
return titleEl;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function createCardPreview(item) {
|
|
949
|
+
const preview = document.createElement("div");
|
|
950
|
+
preview.className = "card__preview";
|
|
951
|
+
preview.hidden = true;
|
|
952
|
+
const theme = String(item?.indicators?.Theme || "").trim();
|
|
953
|
+
if (theme) {
|
|
954
|
+
preview.appendChild(createPreviewRow("Theme", theme));
|
|
955
|
+
}
|
|
956
|
+
preview.appendChild(createPreviewRow("Status", item?.indicators?.Status || "No status"));
|
|
957
|
+
preview.appendChild(createPreviewRow("Updated", formatPreviewDate(item.updatedAt)));
|
|
958
|
+
|
|
959
|
+
const linkage = ["spec", "product", "architecture"].includes(String(item?.stage || "").trim()) ? "" : createPrimaryFlowSummary(item);
|
|
960
|
+
if (linkage) {
|
|
961
|
+
preview.appendChild(createPreviewRow("Flow", linkage));
|
|
962
|
+
}
|
|
963
|
+
return preview;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function normalizeLinkLookupValue(value) {
|
|
967
|
+
return String(value || "")
|
|
968
|
+
.trim()
|
|
969
|
+
.replace(/\\/g, "/")
|
|
970
|
+
.replace(/^\.?\//, "");
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function resolveTaskItem(usage) {
|
|
974
|
+
const items = getItems();
|
|
975
|
+
const normalizedId = String(usage?.id || "").trim();
|
|
976
|
+
const normalizedRelPath = normalizeLinkLookupValue(usage?.relPath || usage?.path);
|
|
977
|
+
const normalizedBase = normalizedRelPath ? normalizedRelPath.split("/").pop()?.replace(/\.md$/i, "") || "" : "";
|
|
978
|
+
return (
|
|
979
|
+
items.find((candidate) => {
|
|
980
|
+
const candidateRelPath = normalizeLinkLookupValue(candidate?.relPath || candidate?.path);
|
|
981
|
+
const candidateBase = candidateRelPath ? candidateRelPath.split("/").pop()?.replace(/\.md$/i, "") || "" : "";
|
|
982
|
+
return (
|
|
983
|
+
candidate?.id === normalizedId ||
|
|
984
|
+
candidateRelPath === normalizedRelPath ||
|
|
985
|
+
candidate?.id === normalizedBase ||
|
|
986
|
+
candidateBase === normalizedBase
|
|
987
|
+
);
|
|
988
|
+
}) || null
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function getResolvedRequestItem(item) {
|
|
993
|
+
if (!item) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
if (String(item.stage || "").trim() === "request") {
|
|
997
|
+
return item;
|
|
998
|
+
}
|
|
999
|
+
const linkedRequests = typeof collectPrimaryFlowItems === "function" ? collectPrimaryFlowItems(item).filter((candidate) => candidate && String(candidate.stage || "").trim() === "request") : [];
|
|
1000
|
+
return linkedRequests.length > 0 ? linkedRequests[0] : null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function getActiveTaskUsages(item) {
|
|
1004
|
+
const activeTasks = [];
|
|
1005
|
+
const seen = new Set();
|
|
1006
|
+
for (const usage of item?.usedBy || []) {
|
|
1007
|
+
const taskItem = resolveTaskItem(usage);
|
|
1008
|
+
if (!isActiveTaskCandidate(taskItem) || !taskItem?.id || seen.has(taskItem.id)) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
seen.add(taskItem.id);
|
|
1012
|
+
activeTasks.push(taskItem);
|
|
1013
|
+
}
|
|
1014
|
+
return activeTasks;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function createLinkageBadges(item, activeTasks) {
|
|
1018
|
+
const requestItem = getResolvedRequestItem(item);
|
|
1019
|
+
if (!requestItem && activeTasks.length === 0) {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const badges = document.createElement("div");
|
|
1024
|
+
badges.className = "card__linkage-indicators";
|
|
1025
|
+
badges.setAttribute("aria-hidden", "true");
|
|
1026
|
+
|
|
1027
|
+
if (requestItem) {
|
|
1028
|
+
const requestBadge = document.createElement("span");
|
|
1029
|
+
requestBadge.className = "card__request-badge";
|
|
1030
|
+
requestBadge.style.background = activeRequestColorMap.get(requestItem.id) || getRequestColor(requestItem.id);
|
|
1031
|
+
requestBadge.title = `Request ${requestItem.id}`;
|
|
1032
|
+
badges.appendChild(requestBadge);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (activeTasks.length > 0) {
|
|
1036
|
+
const taskDotContainer = document.createElement("div");
|
|
1037
|
+
taskDotContainer.className = "card__task-dot-container";
|
|
1038
|
+
|
|
1039
|
+
const visibleTaskCount = activeTasks.length > 2 ? 1 : activeTasks.length;
|
|
1040
|
+
for (let index = 0; index < visibleTaskCount; index += 1) {
|
|
1041
|
+
const task = activeTasks[index];
|
|
1042
|
+
const taskDot = document.createElement("span");
|
|
1043
|
+
taskDot.className = "card__task-dot";
|
|
1044
|
+
taskDot.style.background = activeTaskColorMap.get(task.id) || getTaskColor(task.id);
|
|
1045
|
+
taskDot.title = `Task ${task.id}`;
|
|
1046
|
+
taskDotContainer.appendChild(taskDot);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (activeTasks.length > 2) {
|
|
1050
|
+
const overflow = document.createElement("span");
|
|
1051
|
+
overflow.className = "card__task-dot-overflow";
|
|
1052
|
+
overflow.textContent = `+${activeTasks.length - 1}`;
|
|
1053
|
+
taskDotContainer.appendChild(overflow);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
badges.appendChild(taskDotContainer);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return badges;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function createItemCard(item, compact = false) {
|
|
1063
|
+
const card = document.createElement("div");
|
|
1064
|
+
const doneClass = isComplete(item) ? " card--done" : "";
|
|
1065
|
+
const progressClass = progressState(item);
|
|
1066
|
+
const usedClass = isRequestProcessed(item) ? " card--used" : "";
|
|
1067
|
+
const progressValue = getProgressValue(item);
|
|
1068
|
+
const hasProgressBar = typeof progressValue === "number" && progressValue > 0 && progressValue < 100;
|
|
1069
|
+
card.className =
|
|
1070
|
+
"card" +
|
|
1071
|
+
(compact ? " card--compact" : "") +
|
|
1072
|
+
(item.id === getSelectedId() ? " card--selected" : "") +
|
|
1073
|
+
doneClass +
|
|
1074
|
+
(progressClass ? ` ${progressClass}` : "") +
|
|
1075
|
+
usedClass +
|
|
1076
|
+
(hasProgressBar ? " card--progress-bar" : "");
|
|
1077
|
+
if (hasProgressBar) {
|
|
1078
|
+
const clamped = Math.max(0, Math.min(100, progressValue));
|
|
1079
|
+
card.style.setProperty("--progress", `${clamped}%`);
|
|
1080
|
+
}
|
|
1081
|
+
card.dataset.id = item.id;
|
|
1082
|
+
card.setAttribute("role", "button");
|
|
1083
|
+
card.tabIndex = 0;
|
|
1084
|
+
card.setAttribute("aria-label", `${getStageLabel(item.stage)} item ${item.id}: ${item.title}`);
|
|
1085
|
+
card.title = item.title;
|
|
1086
|
+
const healthSignals = typeof getHealthSignals === "function" ? getHealthSignals(item) : [];
|
|
1087
|
+
if (healthSignals.length > 0) {
|
|
1088
|
+
card.classList.add("card--health-alert");
|
|
1089
|
+
}
|
|
1090
|
+
const preview = createCardPreview(item);
|
|
1091
|
+
const activeTasks = item.stage === "task" ? (isActiveTaskCandidate(item) ? [item] : []) : getActiveTaskUsages(item);
|
|
1092
|
+
|
|
1093
|
+
function setPreviewOpen(isOpen) {
|
|
1094
|
+
preview.hidden = !isOpen;
|
|
1095
|
+
card.classList.toggle("card--preview-open", isOpen);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
card.appendChild(createCardTitle(item));
|
|
1099
|
+
|
|
1100
|
+
const badgeStrip = createCardBadgeStrip(item, activeTasks);
|
|
1101
|
+
if (badgeStrip) {
|
|
1102
|
+
card.appendChild(badgeStrip);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const linkageBadges = createLinkageBadges(item, activeTasks);
|
|
1106
|
+
if (linkageBadges) {
|
|
1107
|
+
card.appendChild(linkageBadges);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const primaryFlowSummary = String(item?.stage || "").trim() === "spec" ? "" : createPrimaryFlowSummary(item);
|
|
1111
|
+
if (primaryFlowSummary) {
|
|
1112
|
+
const linkage = document.createElement("div");
|
|
1113
|
+
linkage.className =
|
|
1114
|
+
"card__meta card__meta--linkage" +
|
|
1115
|
+
(primaryFlowSummary === "Unlinked to primary flow" ? " card__meta--orphan" : "");
|
|
1116
|
+
linkage.textContent = primaryFlowSummary;
|
|
1117
|
+
card.appendChild(linkage);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
card.appendChild(preview);
|
|
1121
|
+
|
|
1122
|
+
card.addEventListener("click", () => {
|
|
1123
|
+
setSelectedId(item.id);
|
|
1124
|
+
render();
|
|
1125
|
+
});
|
|
1126
|
+
card.addEventListener("dblclick", () => {
|
|
1127
|
+
setSelectedId(item.id);
|
|
1128
|
+
render();
|
|
1129
|
+
openSelectedItem("read");
|
|
1130
|
+
});
|
|
1131
|
+
card.addEventListener("keydown", (event) => {
|
|
1132
|
+
if (event.key === "Escape" && !preview.hidden) {
|
|
1133
|
+
event.preventDefault();
|
|
1134
|
+
setPreviewOpen(false);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
handleCardKeydown(event, item);
|
|
1138
|
+
});
|
|
1139
|
+
card.addEventListener("mouseenter", () => setPreviewOpen(true));
|
|
1140
|
+
card.addEventListener("mouseleave", () => setPreviewOpen(false));
|
|
1141
|
+
card.addEventListener("focus", () => setPreviewOpen(true));
|
|
1142
|
+
card.addEventListener("blur", () => setPreviewOpen(false));
|
|
1143
|
+
return card;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function renderBoardColumns(grouped, totalVisibleItems) {
|
|
1147
|
+
getVisibleStages().forEach((stage) => {
|
|
1148
|
+
const stageItems = grouped[stage] || [];
|
|
1149
|
+
if (getHideEmptyColumns() && stageItems.length === 0) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const totalCount = Math.max(0, stageItems.length || 0);
|
|
1153
|
+
const visibleSlice = visibleSliceForGroup(stage, stageItems);
|
|
1154
|
+
const column = document.createElement("div");
|
|
1155
|
+
const canCreateFromColumn = isPrimaryFlowStage(stage);
|
|
1156
|
+
column.className = "column";
|
|
1157
|
+
column.dataset.stage = stage;
|
|
1158
|
+
|
|
1159
|
+
const header = document.createElement("div");
|
|
1160
|
+
header.className = "column__header";
|
|
1161
|
+
|
|
1162
|
+
const title = document.createElement("div");
|
|
1163
|
+
title.className = "column__title";
|
|
1164
|
+
const titleLabel = document.createElement("span");
|
|
1165
|
+
titleLabel.className = "column__title-label";
|
|
1166
|
+
titleLabel.textContent = getStageHeading(stage);
|
|
1167
|
+
title.appendChild(titleLabel);
|
|
1168
|
+
|
|
1169
|
+
const titleCount = document.createElement("span");
|
|
1170
|
+
titleCount.className = "column__title-count";
|
|
1171
|
+
titleCount.textContent = formatRenderedCount(visibleSlice.items.length, totalCount);
|
|
1172
|
+
title.appendChild(titleCount);
|
|
1173
|
+
header.appendChild(title);
|
|
1174
|
+
|
|
1175
|
+
const actions = document.createElement("div");
|
|
1176
|
+
actions.className = "column__actions";
|
|
1177
|
+
|
|
1178
|
+
if (canCreateFromColumn) {
|
|
1179
|
+
const addButton = document.createElement("button");
|
|
1180
|
+
addButton.type = "button";
|
|
1181
|
+
addButton.className = "column__add";
|
|
1182
|
+
addButton.innerHTML = plusIcon();
|
|
1183
|
+
addButton.setAttribute("aria-label", "Add Logics item");
|
|
1184
|
+
addButton.title = "Add Logics item";
|
|
1185
|
+
addButton.setAttribute("aria-haspopup", "menu");
|
|
1186
|
+
addButton.setAttribute("aria-expanded", "false");
|
|
1187
|
+
addButton.addEventListener("click", (event) => {
|
|
1188
|
+
event.stopPropagation();
|
|
1189
|
+
toggleColumnMenu(addButton);
|
|
1190
|
+
});
|
|
1191
|
+
actions.appendChild(addButton);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
header.appendChild(actions);
|
|
1195
|
+
column.appendChild(header);
|
|
1196
|
+
|
|
1197
|
+
const body = document.createElement("div");
|
|
1198
|
+
body.className = "column__body";
|
|
1199
|
+
if (!stageItems.length) {
|
|
1200
|
+
const empty = document.createElement("div");
|
|
1201
|
+
empty.className = "column__empty";
|
|
1202
|
+
empty.textContent = canCreateFromColumn ? "No items" : "No linked docs";
|
|
1203
|
+
body.appendChild(empty);
|
|
1204
|
+
} else {
|
|
1205
|
+
visibleSlice.items.forEach((item) => body.appendChild(createItemCard(item)));
|
|
1206
|
+
if (visibleSlice.truncated) {
|
|
1207
|
+
body.appendChild(createShowMoreControl(stage, visibleSlice.remaining, visibleSlice.total));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
column.appendChild(body);
|
|
1212
|
+
board.appendChild(column);
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function renderListView(groups) {
|
|
1217
|
+
disconnectSentinels();
|
|
1218
|
+
const listView = document.createElement("div");
|
|
1219
|
+
listView.className = "list-view";
|
|
1220
|
+
const wrapper = document.createElement("div");
|
|
1221
|
+
wrapper.className = "list-view__wrapper";
|
|
1222
|
+
sentinelTop = createSentinelElement("top");
|
|
1223
|
+
sentinelBottom = createSentinelElement("bottom");
|
|
1224
|
+
wrapper.appendChild(sentinelTop);
|
|
1225
|
+
wrapper.appendChild(listView);
|
|
1226
|
+
wrapper.appendChild(sentinelBottom);
|
|
1227
|
+
groups.forEach((group) => {
|
|
1228
|
+
const section = document.createElement("section");
|
|
1229
|
+
section.className = "list-view__section";
|
|
1230
|
+
section.dataset.group = group.key;
|
|
1231
|
+
if (group.stage) {
|
|
1232
|
+
section.dataset.stage = group.stage;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const stageItems = group.items || [];
|
|
1236
|
+
const visibleSlice = visibleSliceForGroup(group.key, stageItems);
|
|
1237
|
+
const isCollapsed = getCollapsedListStages().has(group.key);
|
|
1238
|
+
|
|
1239
|
+
const header = document.createElement("button");
|
|
1240
|
+
header.type = "button";
|
|
1241
|
+
header.className = "list-view__header";
|
|
1242
|
+
header.setAttribute("aria-expanded", String(!isCollapsed));
|
|
1243
|
+
header.setAttribute("aria-controls", `list-section-${group.key}`);
|
|
1244
|
+
|
|
1245
|
+
const chevron = document.createElement("span");
|
|
1246
|
+
chevron.className = "list-view__header-icon";
|
|
1247
|
+
chevron.setAttribute("aria-hidden", "true");
|
|
1248
|
+
chevron.textContent = chevronIcon(isCollapsed);
|
|
1249
|
+
header.appendChild(chevron);
|
|
1250
|
+
|
|
1251
|
+
const label = document.createElement("span");
|
|
1252
|
+
label.className = "list-view__header-label";
|
|
1253
|
+
label.textContent = group.heading;
|
|
1254
|
+
header.appendChild(label);
|
|
1255
|
+
|
|
1256
|
+
const count = document.createElement("span");
|
|
1257
|
+
count.className = "list-view__header-count";
|
|
1258
|
+
count.textContent = formatRenderedCount(visibleSlice.items.length, Math.max(0, group.totalCount || 0));
|
|
1259
|
+
header.appendChild(count);
|
|
1260
|
+
header.addEventListener("click", () => {
|
|
1261
|
+
toggleListStageCollapsed(group.key, !getCollapsedListStages().has(group.key));
|
|
1262
|
+
focusListHeader(group.key);
|
|
1263
|
+
});
|
|
1264
|
+
header.addEventListener("keydown", (event) => {
|
|
1265
|
+
if (event.key === "ArrowLeft" && !getCollapsedListStages().has(group.key)) {
|
|
1266
|
+
event.preventDefault();
|
|
1267
|
+
toggleListStageCollapsed(group.key, true);
|
|
1268
|
+
focusListHeader(group.key);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (event.key === "ArrowRight" && getCollapsedListStages().has(group.key)) {
|
|
1272
|
+
event.preventDefault();
|
|
1273
|
+
toggleListStageCollapsed(group.key, false);
|
|
1274
|
+
focusListHeader(group.key);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (event.key === "ArrowDown" && !getCollapsedListStages().has(group.key) && visibleSlice.items.length > 0) {
|
|
1278
|
+
event.preventDefault();
|
|
1279
|
+
selectItemAndFocus(visibleSlice.items[0].id);
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
section.appendChild(header);
|
|
1283
|
+
|
|
1284
|
+
const body = document.createElement("div");
|
|
1285
|
+
body.className = "list-view__body";
|
|
1286
|
+
body.id = `list-section-${group.key}`;
|
|
1287
|
+
body.hidden = isCollapsed;
|
|
1288
|
+
if (!stageItems.length) {
|
|
1289
|
+
const empty = document.createElement("div");
|
|
1290
|
+
empty.className = "list-view__empty";
|
|
1291
|
+
empty.textContent = group.emptyLabel || "No items";
|
|
1292
|
+
body.appendChild(empty);
|
|
1293
|
+
} else {
|
|
1294
|
+
visibleSlice.items.forEach((item) => body.appendChild(createItemCard(item, true)));
|
|
1295
|
+
if (visibleSlice.truncated) {
|
|
1296
|
+
body.appendChild(createShowMoreControl(group.key, visibleSlice.remaining, visibleSlice.total));
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
section.appendChild(body);
|
|
1300
|
+
listView.appendChild(section);
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
board.appendChild(wrapper);
|
|
1304
|
+
sentinelWrapper = wrapper;
|
|
1305
|
+
attachSentinelObserver(wrapper, board, sentinelTop, sentinelBottom);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function renderBoard() {
|
|
1309
|
+
const scrollState = captureBoardScroll();
|
|
1310
|
+
if (typeof closeColumnMenu === "function") {
|
|
1311
|
+
closeColumnMenu();
|
|
1312
|
+
}
|
|
1313
|
+
disconnectSentinels();
|
|
1314
|
+
reconcileGroupRenderLimits();
|
|
1315
|
+
board.innerHTML = "";
|
|
1316
|
+
const visibleItems = getItems().filter((item) => isVisible(item));
|
|
1317
|
+
activeTaskColorMap = buildTaskColorMap(visibleItems);
|
|
1318
|
+
activeRequestColorMap = buildRequestColorMap(visibleItems);
|
|
1319
|
+
if (!visibleItems.length) {
|
|
1320
|
+
const empty = document.createElement("div");
|
|
1321
|
+
empty.className = "state-message";
|
|
1322
|
+
empty.textContent = getEmptyBoardMessage();
|
|
1323
|
+
board.appendChild(empty);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const grouped = groupByStage(visibleItems);
|
|
1327
|
+
if (isListMode()) {
|
|
1328
|
+
renderListView(typeof getListGroups === "function" ? getListGroups() : []);
|
|
1329
|
+
} else {
|
|
1330
|
+
renderBoardColumns(grouped, visibleItems.length);
|
|
1331
|
+
}
|
|
1332
|
+
restoreBoardScroll(scrollState);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return {
|
|
1336
|
+
renderBoard
|
|
1337
|
+
};
|
|
1338
|
+
};
|
|
1339
|
+
})();
|