profoundjs 7.20.4 → 7.22.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.
Files changed (35) hide show
  1. package/auto-testing/static/test-runs/index.html +46 -0
  2. package/auto-testing/static/test-runs/script.js +477 -0
  3. package/auto-testing/static/test-runs/style.css +237 -0
  4. package/htdocs/profoundui/proddata/css/markdown.css +23 -0
  5. package/htdocs/profoundui/proddata/css/plogic.css +4976 -0
  6. package/htdocs/profoundui/proddata/css/profoundui.css +3 -2
  7. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-700.eot +0 -0
  8. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-700.svg +276 -0
  9. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-700.ttf +0 -0
  10. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-700.woff +0 -0
  11. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-700.woff2 +0 -0
  12. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-italic.eot +0 -0
  13. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-italic.svg +301 -0
  14. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-italic.ttf +0 -0
  15. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-italic.woff +0 -0
  16. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-italic.woff2 +0 -0
  17. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-regular.eot +0 -0
  18. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-regular.svg +281 -0
  19. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-regular.ttf +0 -0
  20. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-regular.woff +0 -0
  21. package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-regular.woff2 +0 -0
  22. package/htdocs/profoundui/proddata/fonts/material_icons.woff2 +0 -0
  23. package/htdocs/profoundui/proddata/images/FlatIcons/24x24/capture-changes.png +0 -0
  24. package/htdocs/profoundui/proddata/js/atrium.js +190 -188
  25. package/htdocs/profoundui/proddata/js/designer.js +4391 -4366
  26. package/htdocs/profoundui/proddata/js/genie.js +2558 -2549
  27. package/htdocs/profoundui/proddata/js/plogic.grids.js +326 -0
  28. package/htdocs/profoundui/proddata/js/runtime.js +1418 -1414
  29. package/htdocs/profoundui/proddata/js/signon.js +101 -99
  30. package/htdocs/profoundui/userdata/html/atrium_login.html +4 -1
  31. package/package.json +3 -1
  32. package/profound.jse +1 -1
  33. package/setup/completeInstall.js +7 -0
  34. package/setup/modules/puiscreens.json +283 -0
  35. package/setup/pjsdist.savf +0 -0
@@ -0,0 +1,46 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Work with Auto-Submitted Test Runs</title>
7
+ <link rel="stylesheet" href="style.css" />
8
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet"> <!-- Material Icons added -->
9
+ </head>
10
+ <body>
11
+ <!-- New overall page header -->
12
+ <header id="page-header">
13
+ <h1>Work with Auto-Submitted Test Runs</h1>
14
+ <button id="refresh-button">
15
+ <i class="material-symbols-outlined">refresh</i>
16
+ Refresh
17
+ </button>
18
+ </header>
19
+ <div class="container">
20
+ <div class="resizable" id="resizable-container">
21
+ <!-- Left column without the refresh button -->
22
+ <div class="column left" id="left-column">
23
+ <!-- New heading for the left column -->
24
+ <h2>Test Run Batches</h2>
25
+ <!-- Filter panel for statuses -->
26
+ <div id="filter-panel">
27
+ <label><input type="checkbox" value="finished" checked> Finished</label>
28
+ <label><input type="checkbox" value="errored" checked> Errored</label>
29
+ <label><input type="checkbox" value="running" checked> Running</label>
30
+ </div>
31
+ <!-- Items will be dynamically added here -->
32
+ <div id="item-list"></div>
33
+ </div>
34
+
35
+ <!-- Resizer handle -->
36
+ <div class="resizer" id="resizer"></div>
37
+
38
+ <!-- Right column for item details -->
39
+ <div class="column right" id="item-details">
40
+ <p>Select an item to view details.</p>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ <script src="script.js"></script>
45
+ </body>
46
+ </html>
@@ -0,0 +1,477 @@
1
+ "use strict";
2
+ // Description: Fetch and display a list of batch groups.
3
+ // When a batch group is clicked, its details are displayed in the right column.
4
+ // Querystring parameters:
5
+ // - batchid: The ID of the batch group to select and display details for. If not provided,
6
+ // the first batch group is selected.
7
+
8
+ document.addEventListener("DOMContentLoaded", () => {
9
+ if (!sessionStorage["atrium-token"]) {
10
+ // Redirect to login page if not authenticated, preserving current URL as a 'redirect' parameter
11
+ const currentUrl = window.location.pathname + window.location.search;
12
+ window.location.href = "/auto-testing/login?redirect=" + encodeURIComponent(currentUrl);
13
+ return;
14
+ }
15
+
16
+ // Grab all the elements we need
17
+ const leftColumn = document.getElementById("left-column");
18
+ // Set default width to 400px
19
+ leftColumn.style.flex = "0 0 400px";
20
+
21
+ const itemDetails = document.getElementById("item-details");
22
+ const refreshButton = document.getElementById("refresh-button");
23
+ const resizer = document.getElementById("resizer");
24
+ let isResizing = false;
25
+
26
+ // Disable text selection globally during drag
27
+ document.addEventListener("selectstart", (e) => {
28
+ if (isResizing) e.preventDefault();
29
+ });
30
+
31
+ // Resizer functionality
32
+ resizer.addEventListener("mousedown", (e) => {
33
+ isResizing = true;
34
+ document.body.style.cursor = "ew-resize";
35
+ document.addEventListener("mousemove", resizeColumns);
36
+ document.addEventListener("mouseup", stopResizing);
37
+ });
38
+
39
+ function resizeColumns(e) {
40
+ if (!isResizing) return;
41
+
42
+ const containerRect = document.getElementById("resizable-container").getBoundingClientRect();
43
+ const leftWidth = e.clientX - containerRect.left;
44
+ const rightWidth = containerRect.right - e.clientX;
45
+
46
+ // Prevent columns from shrinking too much
47
+ if (leftWidth > 200 && rightWidth > 200) {
48
+ leftColumn.style.flex = `0 0 ${leftWidth}px`;
49
+ // Make the right column automatically fill the remaining space
50
+ itemDetails.style.flex = "1 1 auto";
51
+ }
52
+ }
53
+
54
+ function stopResizing() {
55
+ isResizing = false;
56
+ document.body.style.cursor = "";
57
+ document.removeEventListener("mousemove", resizeColumns);
58
+ document.removeEventListener("mouseup", stopResizing);
59
+ }
60
+
61
+ // Show error message to the user in the details pane
62
+ function displayErrorMessage(message) {
63
+ itemDetails.innerHTML = `<div class="details error">
64
+ <p>${message}</p>
65
+ </div>`;
66
+ }
67
+
68
+ // Get batch group info from server
69
+ function fetchBatchIds() {
70
+ // Insert a loading spinner before fetching data
71
+ const itemList = document.getElementById("item-list");
72
+ itemList.innerHTML = `<div class="spinner"></div>`;
73
+
74
+ const headers = {
75
+ "Content-Type": "application/json"
76
+ };
77
+ if (sessionStorage["atrium-token"]) {
78
+ headers["Authorization"] = "Basic " + sessionStorage["atrium-token"];
79
+ };
80
+ fetch("/auto-testing/batch-list", {
81
+ method: "POST",
82
+ headers
83
+ })
84
+ .then(res => res.json())
85
+ .then(data => {
86
+ renderBatchIds(data.data);
87
+ })
88
+ .catch(err => {
89
+ console.error("Error fetching batch groups:", err);
90
+ displayErrorMessage("Unable to reach the server. Please check your connection.");
91
+ });
92
+ }
93
+
94
+ function renderBatchIds(batchIds) {
95
+ // Read filter values from checkboxes
96
+ const filterPanel = document.getElementById("filter-panel");
97
+ const checkedStatuses = Array.from(filterPanel.querySelectorAll("input[type=checkbox]:checked"))
98
+ .map(checkbox => checkbox.value);
99
+
100
+ // Group batches by their "batch id" (abid)
101
+ const batchesMap = {};
102
+ batchIds.forEach(obj => {
103
+ const batchKey = obj.abid;
104
+ // Only include the batch if its status is in the checkedStatuses
105
+ const status = (obj.abstatus || "").toLowerCase();
106
+ if (!checkedStatuses.includes(status)) {
107
+ return; // skip object
108
+ }
109
+ if (!batchesMap[batchKey]) {
110
+ batchesMap[batchKey] = [];
111
+ }
112
+ batchesMap[batchKey].push(obj);
113
+ });
114
+
115
+ const itemList = document.getElementById("item-list");
116
+ itemList.innerHTML = ""; // Clear the list
117
+
118
+ // Iterate over each unique batch group key
119
+ Object.keys(batchesMap).forEach(groupKey => {
120
+ const batchData = batchesMap[groupKey];
121
+ const batchStatus = (batchData[0].batchstatussummary || "").toLowerCase();
122
+ let batchIconName = "";
123
+ let batchIconColor = "";
124
+ if (batchStatus === "finished") {
125
+ batchIconName = "check_circle";
126
+ batchIconColor = "#388E3C";
127
+ }
128
+ else if (batchStatus === "errored") {
129
+ batchIconName = "error";
130
+ batchIconColor = "#D32F2F";
131
+ }
132
+ else if (batchStatus === "running") {
133
+ batchIconName = "clock_loader_40";
134
+ batchIconColor = "#1976D2";
135
+ }
136
+
137
+ const runStatus = (batchData[0].runsummary || "").toLowerCase();
138
+ let runIconName = "";
139
+ let runIconColor = "";
140
+ if (runStatus === "passed") {
141
+ runIconName = "thumb_up";
142
+ runIconColor = "#388E3C";
143
+ }
144
+ else if (runStatus === "passed with warnings") {
145
+ runIconName = "thumb_up";
146
+ runIconColor = "#FF9800";
147
+ }
148
+ else if (runStatus === "mixed") {
149
+ runIconName = "thumbs_up_down";
150
+ runIconColor = "#FF9800";
151
+ }
152
+ else if (runStatus === "failed") {
153
+ runIconName = "thumb_down";
154
+ runIconColor = "#D32F2F";
155
+ }
156
+
157
+ const div = document.createElement("div");
158
+ div.className = "item";
159
+ div.innerHTML = `<span class="work-icon">\
160
+ <i class="material-symbols-outlined" title="Batch status: ${batchStatus}" style="color: ${batchIconColor};">${batchIconName}</i>\
161
+ </span>\
162
+ <span class="status-icon">\
163
+ <i class="material-symbols-outlined" title="Run status: ${runStatus}" style="color: ${runIconColor};">${runIconName}</i>\
164
+ </span>\
165
+ <span class="batch-text">${groupKey}</span>`;
166
+ div.addEventListener("click", () => {
167
+ document.querySelectorAll(".item").forEach(i => i.classList.remove("active"));
168
+ div.classList.add("active");
169
+ // Build details: list each batch with its segment
170
+ const batchData = batchesMap[groupKey];
171
+ const detailsHtml = `<div class="details">
172
+ <p><strong>Batch ID:</strong> ${groupKey}</p>
173
+ <p><strong>Segments:</strong></p>
174
+ <ul>
175
+ ${batchData.map(batch => `<li>${String(batch.abseg).padStart(2, "0")} - Status: ${batch.abstatus}</li>`).join("")}
176
+ </ul>
177
+ <div id="run-grid-container">
178
+ <p>Loading test runs...</p>
179
+ </div>
180
+ </div>`;
181
+ itemDetails.innerHTML = detailsHtml;
182
+ // Fetch and display run list for this batch group
183
+ fetchRunList(groupKey);
184
+ });
185
+ itemList.appendChild(div);
186
+ });
187
+
188
+ // Check for batchId in URL and simulate click on it
189
+ const params = new URLSearchParams(window.location.search);
190
+ const targetBatchId = params.get("batchId"); // new parameter
191
+ if (targetBatchId) {
192
+ const targetItem = Array.from(document.querySelectorAll(".item"))
193
+ .find(item => item.querySelector(".batch-text")?.textContent.trim() === targetBatchId);
194
+ if (targetItem) {
195
+ targetItem.click();
196
+ targetItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
197
+ // If a batchSegment is also provided, set the segment filter
198
+ const targetBatchSegment = params.get("batchSegment");
199
+ if (targetBatchSegment) {
200
+ const segmentInput = document.querySelector("#run-grid thead input[data-field='arseg']");
201
+ if (segmentInput) {
202
+ segmentInput.value = targetBatchSegment;
203
+ segmentInput.dispatchEvent(new Event("input"));
204
+ }
205
+ }
206
+ }
207
+ else {
208
+ document.querySelector(".item")?.click();
209
+ }
210
+ }
211
+ else {
212
+ document.querySelector(".item")?.click();
213
+ }
214
+ }
215
+
216
+ // Listen for changes to filter checkboxes to refresh the batch list
217
+ document.getElementById("filter-panel").addEventListener("change", () => {
218
+ fetchBatchIds();
219
+ });
220
+
221
+ // Fetch test runs for the given batch id
222
+ function fetchRunList(batchId) {
223
+ const payload = { batchId };
224
+ const headers = {
225
+ "Content-Type": "application/json"
226
+ };
227
+ if (sessionStorage["atrium-token"]) {
228
+ headers["Authorization"] = "Basic " + sessionStorage["atrium-token"];
229
+ }
230
+ fetch("/auto-testing/run-list-by-batch-id", {
231
+ method: "POST",
232
+ headers,
233
+ body: JSON.stringify(payload)
234
+ })
235
+ .then(res => res.json())
236
+ .then(data => {
237
+ renderRunGrid(data, data.length);
238
+ })
239
+ .catch(err => {
240
+ console.error("Error fetching run list:", err);
241
+ const container = document.getElementById("run-grid-container");
242
+ if (container) {
243
+ container.innerHTML = `<p>Error loading test runs.</p>`;
244
+ }
245
+ });
246
+ }
247
+
248
+ const runGridSortState = {}; // global variable to store sort state per field
249
+
250
+ function renderRunGrid(runData, total) {
251
+ const container = document.getElementById("run-grid-container");
252
+ if (!container) return;
253
+
254
+ // Build header rows dynamically using runGridSortState for sort indicators.
255
+ const fields = [
256
+ { field: "testdesc", label: "Test description" },
257
+ { field: "arseg", label: "Segment" },
258
+ { field: "navtext", label: "Navigation<br>Item Description" },
259
+ { field: "arstart", label: "Start time" },
260
+ { field: "artime", label: "Run time" },
261
+ { field: "arappjob", label: "Job" },
262
+ { field: "arstatus", label: "Status" }
263
+ ];
264
+ let headerRow = `<tr>
265
+ <th class="action-column"></th>`; // reserved for action icons
266
+ fields.forEach(f => {
267
+ let indicator = "";
268
+ if (runGridSortState[f.field]) {
269
+ indicator = runGridSortState[f.field] === "asc" ? " ▲" : " ▼";
270
+ }
271
+ headerRow += `<th data-field="${f.field}">${f.label}${indicator}</th>`;
272
+ });
273
+ headerRow += `</tr>`;
274
+
275
+ // Build filtering row
276
+ const filterRow = `<tr>
277
+ <th class="action-column"></th>
278
+ <th><input type="text" placeholder="Filter" data-field="desc"></th>
279
+ <th><input type="text" placeholder="Filter" data-field="arseg"></th>
280
+ <th><input type="text" placeholder="Filter" data-field="navtext"></th>
281
+ <th><input type="text" placeholder="Filter" data-field="arstart"></th>
282
+ <th><input type="text" placeholder="Filter" data-field="artime"></th>
283
+ <th><input type="text" placeholder="Filter" data-field="arappjob"></th>
284
+ <th><input type="text" placeholder="Filter" data-field="arstatus"></th>
285
+ </tr>`;
286
+
287
+ // Insert a colgroup to specify column widths
288
+ let html = `<table id="run-grid">
289
+ <colgroup>
290
+ <col> <!-- Action column -->
291
+ <col> <!-- Test description -->
292
+ <col style="max-width: 100px; width: 100px;"> <!-- Segment column -->
293
+ <col> <!-- Navigation -->
294
+ <col> <!-- Start time -->
295
+ <col> <!-- Run time -->
296
+ <col> <!-- Job -->
297
+ <col> <!-- Status -->
298
+ </colgroup>
299
+ <thead>
300
+ ${headerRow}
301
+ ${filterRow}
302
+ </thead>
303
+ <tbody>`;
304
+
305
+ runData.forEach(run => {
306
+ const description = run.testdesc || "N/A";
307
+ const segment = run.arseg || "";
308
+ const navDescription = run.navtext || "N/A";
309
+ // Parse the start time into date and time parts
310
+ const rawStart = run.arstart || "";
311
+ let datePart = rawStart;
312
+ let timePart = "";
313
+ if (rawStart) {
314
+ const parts = rawStart.split("-");
315
+ if (parts.length >= 4) {
316
+ datePart = parts.slice(0, 3).join("-"); // "YYYY-MM-DD"
317
+ timePart = parts[3]; // "HH.MM.SS.ssssss"
318
+ const timeParts = timePart.split(".");
319
+ if (timeParts.length >= 3) {
320
+ timePart = `${timeParts[0]}:${timeParts[1]}:${timeParts[2]}` +
321
+ (timeParts.length > 3 ? `.${timeParts.slice(3).join(".")}` : "");
322
+ }
323
+ }
324
+ }
325
+ html += `<tr>
326
+ <td class="action-cell">
327
+ <i class="material-symbols-outlined work-icon" title="Work with Test" onclick="window.open('/auto-testing/work-with-test?testid=${run.artid}','_blank');">list_alt</i>
328
+ <i class="material-symbols-outlined work-icon" title="Work with Test Run" onclick="window.open('/auto-testing/work-with-test?runid=${run.arid}','_blank');">fact_check</i>
329
+ <i class="material-symbols-outlined work-icon" title="Replay Test" onclick="window.open('/auto-testing/work-with-test?testid=${run.artid}&action=run&env=${run.arenv}','_blank');">play_circle</i>
330
+ </td>
331
+ <td>${description}</td>
332
+ <td>${segment}</td>
333
+ <td>${navDescription}</td>
334
+ <td data-sort="${rawStart}"><span class="date">${datePart}</span><br><span class="time">${timePart}</span></td>
335
+ <td>${run.artime === "0" ? "--" : run.artime}</td>
336
+ <td>${run.arappjob || ""}</td>
337
+ <td>${run.arstatus || ""}</td>
338
+ </tr>`;
339
+ });
340
+ html += `</tbody></table>`;
341
+ // Footer with total test runs; will be updated on filtering
342
+ html += `<p id="total-runs-footer">Total test runs: ${total}</p>`;
343
+ container.innerHTML = html;
344
+
345
+ // If both 'batchId' and 'batchSegment' query string parameters exist, and the segment
346
+ // filter has not been applied yet, set the segment filter
347
+ if (!window.batchSegmentApplied) {
348
+ const urlParams = new URLSearchParams(window.location.search);
349
+ if (urlParams.get("batchId") && urlParams.get("batchSegment")) {
350
+ const segmentFilterInput = document.querySelector("#run-grid thead input[data-field='arseg']");
351
+ if (segmentFilterInput) {
352
+ segmentFilterInput.value = urlParams.get("batchSegment");
353
+ setTimeout(() => {
354
+ segmentFilterInput.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
355
+ }, 0);
356
+ window.batchSegmentApplied = true;
357
+ }
358
+ }
359
+ }
360
+
361
+ // Update visible count if filtering is active.
362
+ function updateVisibleCount() {
363
+ const tbody = document.querySelector("#run-grid tbody");
364
+ let visibleCount = 0;
365
+ tbody.querySelectorAll("tr").forEach(tr => {
366
+ if (tr.style.display !== "none") visibleCount++;
367
+ });
368
+ const footer = document.getElementById("total-runs-footer");
369
+ if (visibleCount < runData.length) {
370
+ footer.textContent = `Total test runs: ${total} (${visibleCount} currently displayed)`;
371
+ }
372
+ else {
373
+ footer.textContent = `Total test runs: ${total}`;
374
+ }
375
+ }
376
+
377
+ // Add event listeners for sorting headers.
378
+ document.querySelectorAll("#run-grid thead th[data-field]").forEach(th => {
379
+ th.style.cursor = "pointer";
380
+ th.addEventListener("click", function() {
381
+ const field = th.getAttribute("data-field");
382
+ let sortDirection = "asc";
383
+ if (runGridSortState[field] === "asc") {
384
+ sortDirection = "desc";
385
+ }
386
+ // Clear sort state for all other columns.
387
+ for (const key in runGridSortState) {
388
+ if (key !== field) {
389
+ delete runGridSortState[key];
390
+ }
391
+ }
392
+ runGridSortState[field] = sortDirection; // update global state
393
+
394
+ // Clear visual sort attributes from all header cells
395
+ document.querySelectorAll("#run-grid thead th[data-field]").forEach(header => {
396
+ header.removeAttribute("data-sort");
397
+ });
398
+ th.setAttribute("data-sort", sortDirection);
399
+
400
+ runData.sort((a, b) => {
401
+ const valA = a[field] || "";
402
+ const valB = b[field] || "";
403
+ if (sortDirection === "asc") {
404
+ return valA > valB ? 1 : (valA < valB ? -1 : 0);
405
+ }
406
+ else {
407
+ return valA < valB ? 1 : (valA > valB ? -1 : 0);
408
+ }
409
+ });
410
+ renderRunGrid(runData, total);
411
+ });
412
+ });
413
+
414
+ // Add event listeners for column filtering and update visible count.
415
+ document.querySelectorAll("#run-grid thead input").forEach(input => {
416
+ input.addEventListener("input", function() {
417
+ const filters = {};
418
+ // Build an object of filter values keyed by data-field name
419
+ document.querySelectorAll("#run-grid thead input").forEach(inp => {
420
+ const field = inp.getAttribute("data-field");
421
+ filters[field] = inp.value.toLowerCase();
422
+ });
423
+ const tbody = document.querySelector("#run-grid tbody");
424
+ // Iterate over each row and check if it should be displayed
425
+ tbody.querySelectorAll("tr").forEach(tr => {
426
+ let show = true;
427
+ tr.querySelectorAll("td").forEach((td, index) => {
428
+ // If this cell contains an icon (the first three columns), skip filtering.
429
+ if (td.querySelector(".work-icon")) return;
430
+
431
+ // Otherwise, determine its corresponding data-field from the header
432
+ const headerFields = document.querySelectorAll("#run-grid thead th[data-field]");
433
+ const cellIndex = Array.from(td.parentNode.children).indexOf(td);
434
+ // Adjust for icon columns -- they don't have headers or filter fields so column indexes get thrown off
435
+ const field = headerFields[cellIndex - 1] ? headerFields[cellIndex - 1].getAttribute("data-field") : null;
436
+ if (field && filters[field] && !td.textContent.toLowerCase().includes(filters[field])) {
437
+ show = false;
438
+ }
439
+ });
440
+ tr.style.display = show ? "" : "none";
441
+ });
442
+ updateVisibleCount();
443
+ });
444
+ });
445
+ }
446
+
447
+ // Update refresh button to re-fetch data
448
+ refreshButton.addEventListener("click", () => {
449
+ fetchBatchIds();
450
+ });
451
+
452
+ // Initial render: fetch batch groups on load
453
+ fetchBatchIds();
454
+
455
+ // Handle up/down arrow key navigation for list items
456
+ document.addEventListener("keydown", function(e) {
457
+ const items = document.querySelectorAll(".item");
458
+ if (!items.length) return;
459
+ const currentIndex = Array.from(items).findIndex(item => item.classList.contains("active"));
460
+ if (e.key === "ArrowDown") {
461
+ e.preventDefault();
462
+ if (currentIndex < items.length - 1) {
463
+ const nextItem = items[currentIndex + 1];
464
+ nextItem.click();
465
+ nextItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
466
+ }
467
+ }
468
+ else if (e.key === "ArrowUp") {
469
+ e.preventDefault();
470
+ if (currentIndex > 0) {
471
+ const prevItem = items[currentIndex - 1];
472
+ prevItem.click();
473
+ prevItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
474
+ }
475
+ }
476
+ });
477
+ });