@youtyan/code-viewer 0.13.0 → 0.14.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 +10 -2
- package/package.json +1 -1
- package/web/app.js +443 -35
- package/web/style.css +79 -3
package/README.md
CHANGED
|
@@ -454,9 +454,17 @@ Open Datastores in the global navigation to access:
|
|
|
454
454
|
row-count changes. Double-click a cell to edit inline when Edit mode is on;
|
|
455
455
|
the whole pending edit batch commits atomically.
|
|
456
456
|
- **Detail footer and related panel** — click any cell to open a resizable
|
|
457
|
-
detail footer
|
|
457
|
+
detail footer, where JSON values are pretty-printed and syntax-highlighted;
|
|
458
|
+
foreign-key cells open a related panel showing the
|
|
458
459
|
referenced or referencing rows, with multi-step drill-down breadcrumbs.
|
|
459
|
-
Cells that match the focused row are highlighted in both panels.
|
|
460
|
+
Cells that match the focused row are highlighted in both panels. Once the
|
|
461
|
+
grid has focus, arrow keys move the active cell from data cell to data cell
|
|
462
|
+
and the detail footer follows it, `Enter` follows a foreign key (arrow keys
|
|
463
|
+
alone never fire a related-table query), `Escape` closes whichever panel is
|
|
464
|
+
open, and `Tab` / `Shift+Tab` move between the main grid and the related
|
|
465
|
+
grid. The related panel's reference list keeps each entry on one line with
|
|
466
|
+
the full table name and condition in a tooltip, and its width can be dragged
|
|
467
|
+
and is remembered.
|
|
460
468
|
- **Footer dock with Query History & Session log** — JetBrains-style bottom
|
|
461
469
|
dock that hosts two tabs: the per-database **Query History** (master/detail
|
|
462
470
|
list of saved queries, SSE-synced across tabs) and a **Session log** that
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -875,6 +875,17 @@ Details: ${JSON.stringify(output)}` : "";
|
|
|
875
875
|
};
|
|
876
876
|
}
|
|
877
877
|
|
|
878
|
+
// web-src/core/file-refresh.ts
|
|
879
|
+
function rawFileInfoSignature(info) {
|
|
880
|
+
if (info.missing) return "missing";
|
|
881
|
+
if (info.size == null && !info.updated_at && !info.commit_updated_at)
|
|
882
|
+
return null;
|
|
883
|
+
return `${info.size ?? ""}|${info.updated_at ?? ""}|${info.commit_updated_at ?? ""}`;
|
|
884
|
+
}
|
|
885
|
+
function fileSignatureUnchanged(stored, key, sig) {
|
|
886
|
+
return stored !== null && stored.key === key && stored.sig === sig;
|
|
887
|
+
}
|
|
888
|
+
|
|
878
889
|
// web-src/core/focus-scope.ts
|
|
879
890
|
function isEditableKeyTarget(target) {
|
|
880
891
|
if (!target) return false;
|
|
@@ -12467,6 +12478,7 @@ ${frontmatter.yaml}
|
|
|
12467
12478
|
exportAction: "Export",
|
|
12468
12479
|
foreignKeyHint: "Foreign key — click to view related rows",
|
|
12469
12480
|
relatedEmpty: "No matching row in the referenced table",
|
|
12481
|
+
relatedListResize: "Resize the related-reference list",
|
|
12470
12482
|
filteredEmptyTitle: (count) => `No rows match ${count} active filter${count === 1 ? "" : "s"}`,
|
|
12471
12483
|
filteredEmptyHint: "The table was loaded, but the current search or column filters hide every row.",
|
|
12472
12484
|
filteredEmptyAction: "Clear filters",
|
|
@@ -12824,6 +12836,7 @@ ${frontmatter.yaml}
|
|
|
12824
12836
|
exportAction: "エクスポート",
|
|
12825
12837
|
foreignKeyHint: "外部キー: クリックして関連データを表示",
|
|
12826
12838
|
relatedEmpty: "参照先に該当する行がありません",
|
|
12839
|
+
relatedListResize: "関連参照リストの幅を変える",
|
|
12827
12840
|
filteredEmptyTitle: (count) => `フィルタ ${count} 件に一致する行がありません`,
|
|
12828
12841
|
filteredEmptyHint: "表は読み込めていますが、現在の検索/列フィルタですべての行が隠れています。",
|
|
12829
12842
|
filteredEmptyAction: "フィルタ解除",
|
|
@@ -20076,17 +20089,29 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20076
20089
|
|
|
20077
20090
|
// web-src/views/database/table-grid.ts
|
|
20078
20091
|
var ROW_HEIGHT = 28;
|
|
20092
|
+
var ROWNUM_WIDTH = 50;
|
|
20079
20093
|
var OVERSCAN = 20;
|
|
20080
20094
|
var PAGE_SIZE = 200;
|
|
20081
20095
|
var MAX_PAGE_CACHE_PAGES = 32;
|
|
20082
20096
|
var FILTER_DEBOUNCE_MS = 300;
|
|
20083
20097
|
var DEFAULT_COL_WIDTH = 180;
|
|
20084
20098
|
var CELL_PREVIEW_MAX_CHARS = 4e3;
|
|
20099
|
+
var DETAIL_JSON_HIGHLIGHT_MAX_CHARS = 1e5;
|
|
20085
20100
|
var RELATED_PANEL_DEFAULT_HEIGHT = 320;
|
|
20086
20101
|
var RELATED_PANEL_MIN_HEIGHT = 60;
|
|
20087
20102
|
var DETAIL_PANEL_DEFAULT_HEIGHT = 200;
|
|
20088
20103
|
var DETAIL_PANEL_MIN_HEIGHT = 40;
|
|
20089
20104
|
var PANEL_MAX_RESERVE = 20;
|
|
20105
|
+
var RELATED_LIST_DEFAULT_WIDTH = 200;
|
|
20106
|
+
var RELATED_LIST_MIN_WIDTH = 120;
|
|
20107
|
+
var RELATED_LIST_MAX_WIDTH = 480;
|
|
20108
|
+
var RELATED_LIST_WIDTH_KEY = "code-viewer:db-related-list-width";
|
|
20109
|
+
var ARROW_STEP = {
|
|
20110
|
+
ArrowUp: { row: -1, col: 0 },
|
|
20111
|
+
ArrowDown: { row: 1, col: 0 },
|
|
20112
|
+
ArrowLeft: { row: 0, col: -1 },
|
|
20113
|
+
ArrowRight: { row: 0, col: 1 }
|
|
20114
|
+
};
|
|
20090
20115
|
function createTableGrid(callbacks, options = {}) {
|
|
20091
20116
|
const embedded = options.embedded === true;
|
|
20092
20117
|
const text3 = () => callbacks.getText?.() ?? dbText("en");
|
|
@@ -20211,6 +20236,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20211
20236
|
filterRowWrap.appendChild(filterRow);
|
|
20212
20237
|
const viewport = document.createElement("div");
|
|
20213
20238
|
viewport.className = "db-grid-viewport";
|
|
20239
|
+
viewport.tabIndex = 0;
|
|
20214
20240
|
const spacer = document.createElement("div");
|
|
20215
20241
|
spacer.className = "db-grid-spacer";
|
|
20216
20242
|
const body = document.createElement("div");
|
|
@@ -20314,6 +20340,156 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20314
20340
|
function clearActiveCell() {
|
|
20315
20341
|
setActiveCell(-1, -1);
|
|
20316
20342
|
}
|
|
20343
|
+
function syncHorizontalScroll() {
|
|
20344
|
+
headerWrap.scrollLeft = viewport.scrollLeft;
|
|
20345
|
+
filterRowWrap.scrollLeft = viewport.scrollLeft;
|
|
20346
|
+
}
|
|
20347
|
+
function focusCell(rowIndex, row, colIndex) {
|
|
20348
|
+
setSelectedRow(rowIndex, row);
|
|
20349
|
+
setActiveCell(rowIndex, colIndex);
|
|
20350
|
+
viewport.focus({ preventScroll: true });
|
|
20351
|
+
}
|
|
20352
|
+
function moveActiveCell(rowIndex, colIndex) {
|
|
20353
|
+
const rendered = body.children[rowIndex - renderStartRow];
|
|
20354
|
+
focusCell(rowIndex, rendered ?? null, colIndex);
|
|
20355
|
+
scrollCellIntoView(rowIndex, colIndex);
|
|
20356
|
+
showDetailForActiveCell();
|
|
20357
|
+
}
|
|
20358
|
+
function scrollCellIntoView(rowIndex, colIndex) {
|
|
20359
|
+
const viewHeight = viewport.clientHeight;
|
|
20360
|
+
if (viewHeight > 0) {
|
|
20361
|
+
const top = rowIndex * ROW_HEIGHT;
|
|
20362
|
+
const bottom = top + ROW_HEIGHT;
|
|
20363
|
+
if (top < viewport.scrollTop) viewport.scrollTop = top;
|
|
20364
|
+
else if (bottom > viewport.scrollTop + viewHeight) {
|
|
20365
|
+
viewport.scrollTop = bottom - viewHeight;
|
|
20366
|
+
}
|
|
20367
|
+
}
|
|
20368
|
+
const viewWidth = viewport.clientWidth;
|
|
20369
|
+
if (viewWidth <= 0) return;
|
|
20370
|
+
let left = ROWNUM_WIDTH;
|
|
20371
|
+
for (let c2 = 0; c2 < colIndex; c2++) left += getColWidth(columnNames[c2]);
|
|
20372
|
+
const right = left + getColWidth(columnNames[colIndex]);
|
|
20373
|
+
if (left < viewport.scrollLeft) {
|
|
20374
|
+
viewport.scrollLeft = colIndex === 0 ? 0 : left;
|
|
20375
|
+
} else if (right > viewport.scrollLeft + viewWidth) {
|
|
20376
|
+
viewport.scrollLeft = right - viewWidth;
|
|
20377
|
+
}
|
|
20378
|
+
}
|
|
20379
|
+
function cachedRow(rowIndex) {
|
|
20380
|
+
const pageStart = Math.floor(rowIndex / PAGE_SIZE) * PAGE_SIZE;
|
|
20381
|
+
return getCachedPage(pageStart)?.[rowIndex - pageStart];
|
|
20382
|
+
}
|
|
20383
|
+
function cachedCellValue(rowIndex, colIndex) {
|
|
20384
|
+
const row = cachedRow(rowIndex);
|
|
20385
|
+
return row ? row[colIndex] : void 0;
|
|
20386
|
+
}
|
|
20387
|
+
function showDetailForActiveCell() {
|
|
20388
|
+
const rowIndex = activeCellRowIndex;
|
|
20389
|
+
const colIndex = activeCellColIndex;
|
|
20390
|
+
if (rowIndex < 0 || colIndex < 0) return;
|
|
20391
|
+
const value = cachedCellValue(rowIndex, colIndex);
|
|
20392
|
+
if (value !== void 0) {
|
|
20393
|
+
showCellDetail(colIndex, value);
|
|
20394
|
+
return;
|
|
20395
|
+
}
|
|
20396
|
+
const pageStart = Math.floor(rowIndex / PAGE_SIZE) * PAGE_SIZE;
|
|
20397
|
+
void ensurePage(pageStart).then(() => {
|
|
20398
|
+
if (activeCellRowIndex !== rowIndex || activeCellColIndex !== colIndex) {
|
|
20399
|
+
return;
|
|
20400
|
+
}
|
|
20401
|
+
const loaded = cachedCellValue(rowIndex, colIndex);
|
|
20402
|
+
if (loaded !== void 0) showCellDetail(colIndex, loaded);
|
|
20403
|
+
});
|
|
20404
|
+
}
|
|
20405
|
+
function moveGridFocus(back) {
|
|
20406
|
+
if (back) {
|
|
20407
|
+
if (!embedded || !callbacks.onFocusParentGrid) return false;
|
|
20408
|
+
callbacks.onFocusParentGrid();
|
|
20409
|
+
return true;
|
|
20410
|
+
}
|
|
20411
|
+
if (embedded || !relatedPanel || relatedPanel.hidden || !embeddedGrid) {
|
|
20412
|
+
return false;
|
|
20413
|
+
}
|
|
20414
|
+
embeddedGrid.focusGrid();
|
|
20415
|
+
return true;
|
|
20416
|
+
}
|
|
20417
|
+
function closeOpenPanel() {
|
|
20418
|
+
if (relatedPanel && !relatedPanel.hidden) {
|
|
20419
|
+
hideRelatedPanel();
|
|
20420
|
+
clearActiveCell();
|
|
20421
|
+
return true;
|
|
20422
|
+
}
|
|
20423
|
+
if (!detailPanel.hidden) {
|
|
20424
|
+
detailPanel.hidden = true;
|
|
20425
|
+
clearDetailContent();
|
|
20426
|
+
clearActiveCell();
|
|
20427
|
+
return true;
|
|
20428
|
+
}
|
|
20429
|
+
return false;
|
|
20430
|
+
}
|
|
20431
|
+
function activateActiveCell() {
|
|
20432
|
+
const rowIndex = activeCellRowIndex;
|
|
20433
|
+
const colIndex = activeCellColIndex;
|
|
20434
|
+
if (rowIndex < 0 || colIndex < 0) return false;
|
|
20435
|
+
const rowValues = cachedRow(rowIndex);
|
|
20436
|
+
if (!rowValues) return false;
|
|
20437
|
+
const colName = columnNames[colIndex];
|
|
20438
|
+
const fkClickable = fkColumns.has(colName) && (!embedded || !!callbacks.onForeignKeyCellClick);
|
|
20439
|
+
if (!fkClickable) {
|
|
20440
|
+
showDetailForActiveCell();
|
|
20441
|
+
return true;
|
|
20442
|
+
}
|
|
20443
|
+
if (embedded) {
|
|
20444
|
+
callbacks.onForeignKeyCellClick?.(
|
|
20445
|
+
currentTable,
|
|
20446
|
+
columnNames,
|
|
20447
|
+
rowValues,
|
|
20448
|
+
colName
|
|
20449
|
+
);
|
|
20450
|
+
} else {
|
|
20451
|
+
openRelatedForRow(currentTable, columnNames, rowValues, colName);
|
|
20452
|
+
}
|
|
20453
|
+
return true;
|
|
20454
|
+
}
|
|
20455
|
+
function onViewportKeydown(e2) {
|
|
20456
|
+
if (isImeComposing(e2)) return;
|
|
20457
|
+
if (isEditableKeyTarget(e2.target)) return;
|
|
20458
|
+
if (e2.key === "Tab" && !e2.ctrlKey && !e2.metaKey && !e2.altKey) {
|
|
20459
|
+
if (moveGridFocus(e2.shiftKey)) e2.preventDefault();
|
|
20460
|
+
return;
|
|
20461
|
+
}
|
|
20462
|
+
if (e2.ctrlKey || e2.metaKey || e2.altKey || e2.shiftKey) return;
|
|
20463
|
+
if (e2.key === "Escape") {
|
|
20464
|
+
if (closeOpenPanel()) e2.preventDefault();
|
|
20465
|
+
return;
|
|
20466
|
+
}
|
|
20467
|
+
if (e2.key === "Enter") {
|
|
20468
|
+
if (activateActiveCell()) e2.preventDefault();
|
|
20469
|
+
return;
|
|
20470
|
+
}
|
|
20471
|
+
const step = ARROW_STEP[e2.key];
|
|
20472
|
+
if (!step) return;
|
|
20473
|
+
const lastRow = totalRows - 1;
|
|
20474
|
+
const lastCol = columnNames.length - 1;
|
|
20475
|
+
if (lastRow < 0 || lastCol < 0) return;
|
|
20476
|
+
e2.preventDefault();
|
|
20477
|
+
if (activeCellRowIndex < 0 || activeCellColIndex < 0) {
|
|
20478
|
+
moveActiveCell(Math.min(Math.max(selectedRowIndex, 0), lastRow), 0);
|
|
20479
|
+
return;
|
|
20480
|
+
}
|
|
20481
|
+
const nextRow = Math.min(
|
|
20482
|
+
Math.max(activeCellRowIndex + step.row, 0),
|
|
20483
|
+
lastRow
|
|
20484
|
+
);
|
|
20485
|
+
const nextCol = Math.min(
|
|
20486
|
+
Math.max(activeCellColIndex + step.col, 0),
|
|
20487
|
+
lastCol
|
|
20488
|
+
);
|
|
20489
|
+
if (nextRow === activeCellRowIndex && nextCol === activeCellColIndex)
|
|
20490
|
+
return;
|
|
20491
|
+
moveActiveCell(nextRow, nextCol);
|
|
20492
|
+
}
|
|
20317
20493
|
function clearDetailContent() {
|
|
20318
20494
|
for (const child of Array.from(detailPanel.children)) {
|
|
20319
20495
|
if (child !== detailResize) child.remove();
|
|
@@ -20388,6 +20564,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20388
20564
|
let editingCellCol = -1;
|
|
20389
20565
|
let relatedPanel = null;
|
|
20390
20566
|
let relatedListEl = null;
|
|
20567
|
+
let relatedListResizeEl = null;
|
|
20391
20568
|
let relatedGridHost = null;
|
|
20392
20569
|
let relatedEmptyEl = null;
|
|
20393
20570
|
let relatedCrumbEl = null;
|
|
@@ -20401,6 +20578,20 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20401
20578
|
RELATED_PANEL_MIN_HEIGHT,
|
|
20402
20579
|
Math.min(panelMaxHeight(), savedRelatedHeight)
|
|
20403
20580
|
) : RELATED_PANEL_DEFAULT_HEIGHT;
|
|
20581
|
+
let relatedListWidth = clampRelatedListWidth(
|
|
20582
|
+
readStoredSize(RELATED_LIST_WIDTH_KEY, RELATED_LIST_DEFAULT_WIDTH)
|
|
20583
|
+
);
|
|
20584
|
+
let relatedListResizeDetach = null;
|
|
20585
|
+
function clampRelatedListWidth(width) {
|
|
20586
|
+
return Math.max(
|
|
20587
|
+
RELATED_LIST_MIN_WIDTH,
|
|
20588
|
+
Math.min(RELATED_LIST_MAX_WIDTH, Math.round(width))
|
|
20589
|
+
);
|
|
20590
|
+
}
|
|
20591
|
+
function applyRelatedListWidth(width) {
|
|
20592
|
+
relatedListWidth = clampRelatedListWidth(width);
|
|
20593
|
+
el2.style.setProperty("--db-related-list-w", `${relatedListWidth}px`);
|
|
20594
|
+
}
|
|
20404
20595
|
if (!embedded) {
|
|
20405
20596
|
relatedPanel = document.createElement("div");
|
|
20406
20597
|
relatedPanel.className = "db-related-panel";
|
|
@@ -20580,6 +20771,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20580
20771
|
function resetSelectionAndDetail() {
|
|
20581
20772
|
selectedRowIndex = -1;
|
|
20582
20773
|
selectedRowElement = null;
|
|
20774
|
+
clearActiveCell();
|
|
20583
20775
|
detailPanel.hidden = true;
|
|
20584
20776
|
clearDetailContent();
|
|
20585
20777
|
}
|
|
@@ -20786,6 +20978,24 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20786
20978
|
bodyRow.className = "db-related-body";
|
|
20787
20979
|
relatedListEl = document.createElement("div");
|
|
20788
20980
|
relatedListEl.className = "db-related-list";
|
|
20981
|
+
const listResize = document.createElement("div");
|
|
20982
|
+
listResize.className = "db-related-list-resize";
|
|
20983
|
+
listResize.tabIndex = 0;
|
|
20984
|
+
listResize.setAttribute("role", "separator");
|
|
20985
|
+
listResize.setAttribute("aria-orientation", "vertical");
|
|
20986
|
+
listResize.setAttribute("aria-label", text3().grid.relatedListResize);
|
|
20987
|
+
relatedListResizeEl = listResize;
|
|
20988
|
+
applyRelatedListWidth(relatedListWidth);
|
|
20989
|
+
relatedListResizeDetach = attachDragResizer({
|
|
20990
|
+
handle: listResize,
|
|
20991
|
+
getSize: () => relatedListWidth,
|
|
20992
|
+
applySize: applyRelatedListWidth,
|
|
20993
|
+
direction: 1,
|
|
20994
|
+
axis: "x",
|
|
20995
|
+
onEnd: () => writeStoredSize(RELATED_LIST_WIDTH_KEY, relatedListWidth),
|
|
20996
|
+
activeClassTarget: relatedPanel,
|
|
20997
|
+
activeClassName: "db-related-list-resizing"
|
|
20998
|
+
});
|
|
20789
20999
|
relatedGridHost = document.createElement("div");
|
|
20790
21000
|
relatedGridHost.className = "db-related-grid-host";
|
|
20791
21001
|
embeddedGrid = createTableGrid(
|
|
@@ -20806,7 +21016,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20806
21016
|
getBaseEq: () => relatedEq,
|
|
20807
21017
|
getText: callbacks.getText,
|
|
20808
21018
|
// 埋め込みグリッドの FK クリックで 1 段潜る。
|
|
20809
|
-
onForeignKeyCellClick: (sourceTable, colNames, rowData, clicked) => drillIntoRelated(sourceTable, colNames, rowData, clicked)
|
|
21019
|
+
onForeignKeyCellClick: (sourceTable, colNames, rowData, clicked) => drillIntoRelated(sourceTable, colNames, rowData, clicked),
|
|
21020
|
+
// 埋め込み側で Shift+Tab を押したらメイングリッドへ戻す。
|
|
21021
|
+
onFocusParentGrid: () => viewport.focus({ preventScroll: true })
|
|
20810
21022
|
},
|
|
20811
21023
|
{ embedded: true }
|
|
20812
21024
|
);
|
|
@@ -20816,7 +21028,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20816
21028
|
relatedEmptyEl.textContent = text3().grid.relatedEmpty;
|
|
20817
21029
|
relatedEmptyEl.hidden = true;
|
|
20818
21030
|
relatedGridHost.appendChild(relatedEmptyEl);
|
|
20819
|
-
bodyRow.append(relatedListEl, relatedGridHost);
|
|
21031
|
+
bodyRow.append(relatedListEl, listResize, relatedGridHost);
|
|
20820
21032
|
relatedPanel.append(resizer, header, bodyRow);
|
|
20821
21033
|
}
|
|
20822
21034
|
function startRelatedResize(e2) {
|
|
@@ -20934,7 +21146,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20934
21146
|
if (i2 === level.selectedIndex) item.classList.add("active");
|
|
20935
21147
|
const name = document.createElement("span");
|
|
20936
21148
|
name.className = "db-related-list-name";
|
|
20937
|
-
|
|
21149
|
+
const tableName = document.createElement("span");
|
|
21150
|
+
tableName.className = "db-related-list-table";
|
|
21151
|
+
tableName.textContent = relatedDrillTable(target);
|
|
21152
|
+
tableName.title = relatedDrillTable(target);
|
|
21153
|
+
name.appendChild(tableName);
|
|
20938
21154
|
if (target.fk.inferred) {
|
|
20939
21155
|
const badge = document.createElement("span");
|
|
20940
21156
|
badge.className = "db-related-list-inferred-badge";
|
|
@@ -20944,7 +21160,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20944
21160
|
}
|
|
20945
21161
|
const via = document.createElement("span");
|
|
20946
21162
|
via.className = "db-related-list-via";
|
|
20947
|
-
|
|
21163
|
+
const condition = target.direction === "outgoing" ? `${target.fk.fromColumn} = ${target.value}` : `${target.fk.fromTable}.${target.fk.fromColumn} = ${target.value}`;
|
|
21164
|
+
via.textContent = condition;
|
|
21165
|
+
via.title = condition;
|
|
20948
21166
|
item.append(name, via);
|
|
20949
21167
|
item.addEventListener("click", () => selectRelatedTarget(i2));
|
|
20950
21168
|
relatedListEl?.appendChild(item);
|
|
@@ -20981,9 +21199,42 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
20981
21199
|
grid.showError(err instanceof Error ? err.message : String(err));
|
|
20982
21200
|
}
|
|
20983
21201
|
}
|
|
21202
|
+
let jsonHighlighter = null;
|
|
21203
|
+
let jsonHighlighterRequested = false;
|
|
21204
|
+
let detailJsonPre = null;
|
|
21205
|
+
let detailJsonText = "";
|
|
21206
|
+
function paintJsonHighlight(pre, json) {
|
|
21207
|
+
if (json.length > DETAIL_JSON_HIGHLIGHT_MAX_CHARS) return false;
|
|
21208
|
+
const inner = highlightToInnerHtml(json, "json", jsonHighlighter);
|
|
21209
|
+
if (!inner) return false;
|
|
21210
|
+
pre.innerHTML = inner;
|
|
21211
|
+
return true;
|
|
21212
|
+
}
|
|
21213
|
+
function showJsonDetail(pre, json) {
|
|
21214
|
+
detailJsonPre = pre;
|
|
21215
|
+
detailJsonText = json;
|
|
21216
|
+
if (paintJsonHighlight(pre, json)) return;
|
|
21217
|
+
pre.textContent = json;
|
|
21218
|
+
if (json.length > DETAIL_JSON_HIGHLIGHT_MAX_CHARS) return;
|
|
21219
|
+
ensureJsonHighlighter();
|
|
21220
|
+
}
|
|
21221
|
+
function ensureJsonHighlighter() {
|
|
21222
|
+
if (jsonHighlighterRequested) return;
|
|
21223
|
+
jsonHighlighterRequested = true;
|
|
21224
|
+
void loadShikiHighlighter({
|
|
21225
|
+
themes: ["github-light", "github-dark"],
|
|
21226
|
+
langs: ["json"]
|
|
21227
|
+
}).then((highlighter) => {
|
|
21228
|
+
jsonHighlighter = highlighter;
|
|
21229
|
+
if (!highlighter || !detailJsonPre?.isConnected) return;
|
|
21230
|
+
paintJsonHighlight(detailJsonPre, detailJsonText);
|
|
21231
|
+
});
|
|
21232
|
+
}
|
|
20984
21233
|
function showCellDetail(colIndex, value) {
|
|
20985
21234
|
const colName = columnNames[colIndex];
|
|
20986
21235
|
const colType = columns[colIndex]?.type || "";
|
|
21236
|
+
detailJsonPre = null;
|
|
21237
|
+
detailJsonText = "";
|
|
20987
21238
|
hideRelatedPanel();
|
|
20988
21239
|
detailPanel.hidden = false;
|
|
20989
21240
|
clearDetailContent();
|
|
@@ -21035,7 +21286,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21035
21286
|
const parsed = JSON.parse(str);
|
|
21036
21287
|
const pre = document.createElement("pre");
|
|
21037
21288
|
pre.className = "db-grid-detail-json";
|
|
21038
|
-
pre
|
|
21289
|
+
showJsonDetail(pre, JSON.stringify(parsed, null, 2));
|
|
21039
21290
|
content.appendChild(pre);
|
|
21040
21291
|
} catch {
|
|
21041
21292
|
content.textContent = str;
|
|
@@ -21100,6 +21351,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21100
21351
|
}
|
|
21101
21352
|
renderFilterRow();
|
|
21102
21353
|
syncContentWidth();
|
|
21354
|
+
syncHorizontalScroll();
|
|
21103
21355
|
}
|
|
21104
21356
|
function startResize(colIndex, startEvent) {
|
|
21105
21357
|
cleanupResize();
|
|
@@ -21180,9 +21432,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21180
21432
|
invalidateData();
|
|
21181
21433
|
}, FILTER_DEBOUNCE_MS);
|
|
21182
21434
|
}
|
|
21435
|
+
let scrollbarGutterPx = -1;
|
|
21436
|
+
function syncScrollbarGutter() {
|
|
21437
|
+
const outer = viewport.offsetWidth;
|
|
21438
|
+
const inner = viewport.clientWidth;
|
|
21439
|
+
if (!(outer > 0) || !(inner > 0)) return;
|
|
21440
|
+
const gutter = outer - inner;
|
|
21441
|
+
if (gutter === scrollbarGutterPx) return;
|
|
21442
|
+
scrollbarGutterPx = gutter;
|
|
21443
|
+
el2.style.setProperty("--db-grid-scrollbar-w", `${gutter}px`);
|
|
21444
|
+
}
|
|
21445
|
+
function measureContentWidth() {
|
|
21446
|
+
let total = 0;
|
|
21447
|
+
for (const cell of headerRow.children) {
|
|
21448
|
+
total += cell.getBoundingClientRect().width;
|
|
21449
|
+
}
|
|
21450
|
+
return total;
|
|
21451
|
+
}
|
|
21183
21452
|
function syncContentWidth() {
|
|
21184
21453
|
requestAnimationFrame(() => {
|
|
21185
|
-
|
|
21454
|
+
syncScrollbarGutter();
|
|
21455
|
+
const w = measureContentWidth();
|
|
21186
21456
|
if (w > 0) {
|
|
21187
21457
|
spacer.style.minWidth = `${w}px`;
|
|
21188
21458
|
body.style.minWidth = `${w}px`;
|
|
@@ -21382,8 +21652,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21382
21652
|
const fkClickable = fkColumns.has(cellColName) && (!embedded || !!callbacks.onForeignKeyCellClick);
|
|
21383
21653
|
const isEditingThisCell = !ro && i2 === editingCellRow && c2 === editingCellCol;
|
|
21384
21654
|
const activate = () => {
|
|
21385
|
-
|
|
21386
|
-
setActiveCell(rowIndex, cellColIndex);
|
|
21655
|
+
focusCell(rowIndex, row, cellColIndex);
|
|
21387
21656
|
if (fkClickable) {
|
|
21388
21657
|
if (embedded) {
|
|
21389
21658
|
callbacks.onForeignKeyCellClick?.(
|
|
@@ -21475,8 +21744,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21475
21744
|
}
|
|
21476
21745
|
cell.addEventListener("click", (e2) => {
|
|
21477
21746
|
e2.stopPropagation();
|
|
21478
|
-
|
|
21479
|
-
setActiveCell(rowIndex, cellColIndex);
|
|
21747
|
+
focusCell(rowIndex, row, cellColIndex);
|
|
21480
21748
|
if (fkClickable) {
|
|
21481
21749
|
if (embedded) {
|
|
21482
21750
|
callbacks.onForeignKeyCellClick?.(
|
|
@@ -21586,6 +21854,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21586
21854
|
);
|
|
21587
21855
|
}
|
|
21588
21856
|
syncFilteredEmptyState();
|
|
21857
|
+
syncHorizontalScroll();
|
|
21589
21858
|
if (focusRestore) {
|
|
21590
21859
|
const next = body.querySelector(
|
|
21591
21860
|
`.db-grid-cell-input[data-edit-row="${focusRestore.row}"][data-edit-col="${focusRestore.col}"]`
|
|
@@ -21751,11 +22020,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21751
22020
|
void refreshCurrentTable();
|
|
21752
22021
|
});
|
|
21753
22022
|
const onViewportScroll = () => {
|
|
21754
|
-
|
|
21755
|
-
filterRowWrap.scrollLeft = viewport.scrollLeft;
|
|
22023
|
+
syncHorizontalScroll();
|
|
21756
22024
|
renderViewport();
|
|
21757
22025
|
};
|
|
22026
|
+
const onWrapScroll = (wrap) => () => {
|
|
22027
|
+
if (wrap.scrollLeft === viewport.scrollLeft) return;
|
|
22028
|
+
viewport.scrollLeft = wrap.scrollLeft;
|
|
22029
|
+
syncHorizontalScroll();
|
|
22030
|
+
};
|
|
22031
|
+
const onHeaderWrapScroll = onWrapScroll(headerWrap);
|
|
22032
|
+
const onFilterWrapScroll = onWrapScroll(filterRowWrap);
|
|
21758
22033
|
viewport.addEventListener("scroll", onViewportScroll, { passive: true });
|
|
22034
|
+
viewport.addEventListener("keydown", onViewportKeydown);
|
|
22035
|
+
headerWrap.addEventListener("scroll", onHeaderWrapScroll, { passive: true });
|
|
22036
|
+
filterRowWrap.addEventListener("scroll", onFilterWrapScroll, {
|
|
22037
|
+
passive: true
|
|
22038
|
+
});
|
|
21759
22039
|
const onCompositionStart = () => {
|
|
21760
22040
|
isComposing = true;
|
|
21761
22041
|
};
|
|
@@ -21769,7 +22049,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21769
22049
|
clear();
|
|
21770
22050
|
embeddedGrid?.destroy();
|
|
21771
22051
|
embeddedGrid = null;
|
|
22052
|
+
relatedListResizeDetach?.();
|
|
22053
|
+
relatedListResizeDetach = null;
|
|
21772
22054
|
viewport.removeEventListener("scroll", onViewportScroll);
|
|
22055
|
+
viewport.removeEventListener("keydown", onViewportKeydown);
|
|
22056
|
+
headerWrap.removeEventListener("scroll", onHeaderWrapScroll);
|
|
22057
|
+
filterRowWrap.removeEventListener("scroll", onFilterWrapScroll);
|
|
21773
22058
|
body.removeEventListener("compositionstart", onCompositionStart);
|
|
21774
22059
|
body.removeEventListener("compositionend", onCompositionEnd);
|
|
21775
22060
|
detailResizeCleanup?.();
|
|
@@ -21793,6 +22078,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
21793
22078
|
updateStatus();
|
|
21794
22079
|
}
|
|
21795
22080
|
renderRelatedCrumbs();
|
|
22081
|
+
renderRelatedList();
|
|
22082
|
+
if (relatedListResizeEl) {
|
|
22083
|
+
relatedListResizeEl.setAttribute("aria-label", t2.grid.relatedListResize);
|
|
22084
|
+
}
|
|
21796
22085
|
embeddedGrid?.localize();
|
|
21797
22086
|
}
|
|
21798
22087
|
function rebuildFkColumnsForCurrentTable() {
|
|
@@ -22042,6 +22331,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
22042
22331
|
}
|
|
22043
22332
|
return {
|
|
22044
22333
|
el: el2,
|
|
22334
|
+
focusGrid: () => viewport.focus({ preventScroll: true }),
|
|
22045
22335
|
load,
|
|
22046
22336
|
refresh: refreshCurrentTable,
|
|
22047
22337
|
showError,
|
|
@@ -25714,6 +26004,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
25714
26004
|
(f2) => `${fileKey2(f2)}\0${f2.path}\0${f2.old_path || ""}\0${f2.status || "M"}`
|
|
25715
26005
|
).join("\n");
|
|
25716
26006
|
}
|
|
26007
|
+
function stripLoadUrlGeneration(url) {
|
|
26008
|
+
const queryStart = url.indexOf("?");
|
|
26009
|
+
if (queryStart === -1) return url;
|
|
26010
|
+
const params = new URLSearchParams(url.slice(queryStart + 1));
|
|
26011
|
+
if (!params.has("generation")) return url;
|
|
26012
|
+
params.delete("generation");
|
|
26013
|
+
const s2 = params.toString();
|
|
26014
|
+
return s2 ? `${url.slice(0, queryStart)}?${s2}` : url.slice(0, queryStart);
|
|
26015
|
+
}
|
|
26016
|
+
function diffResponseSignature(data) {
|
|
26017
|
+
return JSON.stringify({ ...data, generation: void 0 });
|
|
26018
|
+
}
|
|
25717
26019
|
function computeCardSignature(f2) {
|
|
25718
26020
|
return [
|
|
25719
26021
|
fileKey2(f2),
|
|
@@ -25725,12 +26027,18 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
25725
26027
|
f2.size_class || "small",
|
|
25726
26028
|
f2.force_layout || "",
|
|
25727
26029
|
f2.highlight ? 1 : 0,
|
|
25728
|
-
f2.load_url,
|
|
25729
|
-
f2.preview_url
|
|
26030
|
+
stripLoadUrlGeneration(f2.load_url),
|
|
26031
|
+
f2.preview_url ? stripLoadUrlGeneration(f2.preview_url) : "",
|
|
25730
26032
|
f2.estimated_height_px || 0,
|
|
25731
26033
|
f2.untracked ? 1 : 0
|
|
25732
26034
|
].join("\0");
|
|
25733
26035
|
}
|
|
26036
|
+
function cardNeedsLoad(card) {
|
|
26037
|
+
if (card.classList.contains("loading")) return false;
|
|
26038
|
+
if (card.dataset.staleLoading === "1") return false;
|
|
26039
|
+
if (card.classList.contains("loaded")) return card.dataset.stale === "1";
|
|
26040
|
+
return true;
|
|
26041
|
+
}
|
|
25734
26042
|
function ensureLazyObserver() {
|
|
25735
26043
|
if (!lazyObserver) {
|
|
25736
26044
|
lazyObserver = new IntersectionObserver(
|
|
@@ -25738,8 +26046,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
25738
26046
|
for (const entry of entries) {
|
|
25739
26047
|
if (!entry.isIntersecting) continue;
|
|
25740
26048
|
const card = entry.target;
|
|
25741
|
-
if (
|
|
25742
|
-
continue;
|
|
26049
|
+
if (!cardNeedsLoad(card)) continue;
|
|
25743
26050
|
const f2 = card._file || STATE.files.find((x) => x.path === card.dataset.path);
|
|
25744
26051
|
if (f2) enqueueLoad(f2, card, 0);
|
|
25745
26052
|
}
|
|
@@ -25761,15 +26068,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
25761
26068
|
return false;
|
|
25762
26069
|
if (!changedPathsCoverPath(changedPaths, file.path)) return false;
|
|
25763
26070
|
card.dataset.reqId = String(++CLIENT_REQ_SEQ);
|
|
26071
|
+
delete card.dataset.stale;
|
|
26072
|
+
delete card.dataset.staleLoading;
|
|
25764
26073
|
card.style.minHeight = `${measuredHeight ?? card.offsetHeight}px`;
|
|
25765
26074
|
card.classList.remove("loaded", "loading", "error");
|
|
25766
26075
|
card.classList.add("pending");
|
|
25767
26076
|
card._diffData = null;
|
|
26077
|
+
card._loadedSig = null;
|
|
26078
|
+
card._loadedSigUrl = null;
|
|
25768
26079
|
const indicator = card.querySelector(".loading-indicator");
|
|
25769
26080
|
if (indicator) indicator.hidden = false;
|
|
25770
26081
|
activatePendingCard(card, file);
|
|
25771
26082
|
return true;
|
|
25772
26083
|
}
|
|
26084
|
+
function revalidateCardSilently(card, file) {
|
|
26085
|
+
if (!card.classList.contains("loaded")) return false;
|
|
26086
|
+
card.dataset.reqId = String(++CLIENT_REQ_SEQ);
|
|
26087
|
+
card.dataset.stale = "1";
|
|
26088
|
+
card._file = file;
|
|
26089
|
+
activatePendingCard(card, file);
|
|
26090
|
+
return true;
|
|
26091
|
+
}
|
|
25773
26092
|
function updateSidebarStats(files) {
|
|
25774
26093
|
for (const f2 of files) {
|
|
25775
26094
|
const li = document.querySelector(
|
|
@@ -25800,8 +26119,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
25800
26119
|
delete card.dataset.manualRendered;
|
|
25801
26120
|
delete card.dataset.manualLoad;
|
|
25802
26121
|
delete card.dataset.manualMode;
|
|
26122
|
+
delete card.dataset.stale;
|
|
26123
|
+
delete card.dataset.staleLoading;
|
|
25803
26124
|
card.style.minHeight = `${file.estimated_height_px || 80}px`;
|
|
25804
26125
|
card._diffData = null;
|
|
26126
|
+
card._loadedSig = null;
|
|
26127
|
+
card._loadedSigUrl = null;
|
|
25805
26128
|
card._file = file;
|
|
25806
26129
|
}
|
|
25807
26130
|
function renderShell(meta, changedPaths) {
|
|
@@ -25877,23 +26200,37 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
25877
26200
|
activatePendingCard(card, f2);
|
|
25878
26201
|
invalidatedCards++;
|
|
25879
26202
|
sidebarNeedsStatsUpdate = true;
|
|
25880
|
-
} else {
|
|
26203
|
+
} else if (sigChanged) {
|
|
25881
26204
|
const stats = card.querySelector(".gdp-shell-header .stats");
|
|
25882
26205
|
if (stats) {
|
|
25883
26206
|
stats.innerHTML = '<span class="a">+' + (f2.additions || 0) + '</span><span class="d">−' + (f2.deletions || 0) + "</span>";
|
|
25884
26207
|
}
|
|
25885
26208
|
card._file = f2;
|
|
25886
|
-
const didInvalidate =
|
|
26209
|
+
const didInvalidate = invalidateLoadedCard(
|
|
25887
26210
|
card,
|
|
25888
26211
|
f2,
|
|
25889
|
-
|
|
26212
|
+
null,
|
|
25890
26213
|
measuredHeights.get(key)
|
|
25891
26214
|
);
|
|
25892
26215
|
if (didInvalidate) invalidatedCards++;
|
|
25893
|
-
else if (
|
|
26216
|
+
else if (card.classList.contains("pending")) {
|
|
25894
26217
|
activatePendingCard(card, f2);
|
|
25895
26218
|
}
|
|
25896
|
-
|
|
26219
|
+
sidebarNeedsStatsUpdate = true;
|
|
26220
|
+
} else {
|
|
26221
|
+
card._file = f2;
|
|
26222
|
+
if (!revalidateCardSilently(card, f2)) {
|
|
26223
|
+
const didInvalidate = invalidateLoadedCard(
|
|
26224
|
+
card,
|
|
26225
|
+
f2,
|
|
26226
|
+
changedPaths,
|
|
26227
|
+
measuredHeights.get(key)
|
|
26228
|
+
);
|
|
26229
|
+
if (didInvalidate) invalidatedCards++;
|
|
26230
|
+
else if (card.classList.contains("pending")) {
|
|
26231
|
+
activatePendingCard(card, f2);
|
|
26232
|
+
}
|
|
26233
|
+
}
|
|
25897
26234
|
}
|
|
25898
26235
|
}
|
|
25899
26236
|
if (sidebarNeedsStatsUpdate && canUpdateSidebar)
|
|
@@ -25988,8 +26325,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
25988
26325
|
entries.forEach((entry) => {
|
|
25989
26326
|
if (!entry.isIntersecting) return;
|
|
25990
26327
|
const card = entry.target;
|
|
25991
|
-
if (
|
|
25992
|
-
return;
|
|
26328
|
+
if (!cardNeedsLoad(card)) return;
|
|
25993
26329
|
const f2 = card._file || STATE.files.find((x) => x.path === card.dataset.path);
|
|
25994
26330
|
if (!f2) return;
|
|
25995
26331
|
enqueueLoad(f2, card, 0);
|
|
@@ -26030,8 +26366,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
26030
26366
|
while (ACTIVE_LOADS < MAX_PARALLEL && LOAD_QUEUE.length) {
|
|
26031
26367
|
const item = LOAD_QUEUE.shift();
|
|
26032
26368
|
if (item.epoch !== LOAD_EPOCH) continue;
|
|
26033
|
-
if (
|
|
26034
|
-
continue;
|
|
26369
|
+
if (!cardNeedsLoad(item.card)) continue;
|
|
26035
26370
|
ACTIVE_LOADS++;
|
|
26036
26371
|
loadFile(item.file, item.card).finally(() => {
|
|
26037
26372
|
if (item.epoch === LOAD_EPOCH) {
|
|
@@ -26126,20 +26461,29 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
26126
26461
|
});
|
|
26127
26462
|
}
|
|
26128
26463
|
function loadFile(file, card, urlOverride, options) {
|
|
26129
|
-
card.classList.
|
|
26130
|
-
card.classList.add("loading");
|
|
26131
|
-
if (lazyObserver) lazyObserver.unobserve(card);
|
|
26464
|
+
const silent = card.classList.contains("loaded") && card.dataset.stale === "1";
|
|
26132
26465
|
const indicator = card.querySelector(".loading-indicator");
|
|
26133
|
-
if (
|
|
26466
|
+
if (silent) {
|
|
26467
|
+
card.dataset.staleLoading = "1";
|
|
26468
|
+
} else {
|
|
26469
|
+
card.classList.remove("pending");
|
|
26470
|
+
card.classList.add("loading");
|
|
26471
|
+
if (indicator) indicator.hidden = false;
|
|
26472
|
+
}
|
|
26473
|
+
if (lazyObserver) lazyObserver.unobserve(card);
|
|
26134
26474
|
const url = urlOverride || (card.dataset.manualMode === "full" ? file.load_url : file.preview_url || file.load_url);
|
|
26135
26475
|
const myGen = getServerGeneration();
|
|
26136
26476
|
const myReq = ++CLIENT_REQ_SEQ;
|
|
26137
26477
|
card.dataset.reqId = String(myReq);
|
|
26138
26478
|
const retryStale = () => {
|
|
26139
26479
|
if (String(myReq) !== card.dataset.reqId) return;
|
|
26140
|
-
|
|
26141
|
-
|
|
26142
|
-
|
|
26480
|
+
if (silent) {
|
|
26481
|
+
delete card.dataset.staleLoading;
|
|
26482
|
+
} else {
|
|
26483
|
+
card.classList.remove("loading");
|
|
26484
|
+
card.classList.add("pending");
|
|
26485
|
+
if (indicator) indicator.hidden = true;
|
|
26486
|
+
}
|
|
26143
26487
|
const fresh = card._file || STATE.files.find((x) => x.path === card.dataset.path);
|
|
26144
26488
|
if (fresh && card.isConnected) enqueueLoad(fresh, card, 0);
|
|
26145
26489
|
};
|
|
@@ -26158,11 +26502,32 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
|
|
|
26158
26502
|
retryStale();
|
|
26159
26503
|
return;
|
|
26160
26504
|
}
|
|
26505
|
+
const strippedUrl = stripLoadUrlGeneration(url);
|
|
26506
|
+
if (silent) {
|
|
26507
|
+
const sig = diffResponseSignature(data);
|
|
26508
|
+
if (card._loadedSigUrl === strippedUrl && card._loadedSig === sig) {
|
|
26509
|
+
delete card.dataset.staleLoading;
|
|
26510
|
+
delete card.dataset.stale;
|
|
26511
|
+
return;
|
|
26512
|
+
}
|
|
26513
|
+
}
|
|
26161
26514
|
if (!options?.immediate) await nextIdle();
|
|
26162
26515
|
if (String(myReq) !== card.dataset.reqId) return;
|
|
26516
|
+
if (silent) {
|
|
26517
|
+
delete card.dataset.staleLoading;
|
|
26518
|
+
delete card.dataset.stale;
|
|
26519
|
+
}
|
|
26520
|
+
card._loadedSig = diffResponseSignature(data);
|
|
26521
|
+
card._loadedSigUrl = strippedUrl;
|
|
26163
26522
|
renderFile(file, data, card);
|
|
26164
26523
|
}).catch((error2) => {
|
|
26165
26524
|
if (String(myReq) !== card.dataset.reqId) return;
|
|
26525
|
+
if (silent) {
|
|
26526
|
+
delete card.dataset.staleLoading;
|
|
26527
|
+
delete card.dataset.stale;
|
|
26528
|
+
console.error("[code-viewer] silent diff revalidation failed", error2);
|
|
26529
|
+
return;
|
|
26530
|
+
}
|
|
26166
26531
|
console.error("[code-viewer] failed to load diff", error2);
|
|
26167
26532
|
card.classList.remove("loading");
|
|
26168
26533
|
card.classList.add("error");
|
|
@@ -29256,7 +29621,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
29256
29621
|
],
|
|
29257
29622
|
[
|
|
29258
29623
|
"Detail footer & related panel",
|
|
29259
|
-
"Click any cell to open a resizable detail footer. Foreign-key cells open a related-rows panel with multi-step drill-down breadcrumbs, supporting both outgoing (FK → PK) and incoming (PK ← FK) navigation."
|
|
29624
|
+
"Click any cell to open a resizable detail footer; JSON values are pretty-printed and syntax-highlighted there. Once the grid has focus, arrow keys move the active cell from data cell to data cell and the footer follows the value under it, scrolling only as far as needed; Enter follows a foreign key (arrow keys alone never fire a related-table query), Escape closes whichever panel is open, and Tab / Shift+Tab move between the main grid and the related grid. Foreign-key cells open a related-rows panel with multi-step drill-down breadcrumbs, supporting both outgoing (FK → PK) and incoming (PK ← FK) navigation. Its reference list keeps each entry on one line with the full table name and condition in a tooltip, and the list width can be dragged and is remembered."
|
|
29260
29625
|
],
|
|
29261
29626
|
[
|
|
29262
29627
|
"Schema tab",
|
|
@@ -30023,7 +30388,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
30023
30388
|
],
|
|
30024
30389
|
[
|
|
30025
30390
|
"詳細フッタ・関連パネル",
|
|
30026
|
-
"
|
|
30391
|
+
"セルをクリックするとリサイズ可能な詳細フッタが開きます。JSON 値は整形してシンタックスハイライト付きで表示されます。グリッドにフォーカスがある間は矢印キーでデータセル間を移動でき、詳細フッタもその値に追従します(スクロールは見える位置まで必要なぶんだけ)。Enter で外部キーを辿り(矢印キーだけでは関連テーブルへのクエリは飛びません)、Escape で開いているパネルを閉じ、Tab / Shift+Tab でメイングリッドと関連グリッドを行き来できます。外部キー値からは関連行パネルが開き、ブレッドクラム付きで多段ドリルダウン可能。outgoing (FK→PK) と incoming (PK←FK) の両方向に対応。左の参照リストは各項目を1行に保ち、テーブル名と条件の全文は tooltip で確認できます。リスト幅はドラッグで変更でき、次回も保持されます。"
|
|
30027
30392
|
],
|
|
30028
30393
|
[
|
|
30029
30394
|
"Schema タブ",
|
|
@@ -36086,6 +36451,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
36086
36451
|
async function loadRawFileInfo(target) {
|
|
36087
36452
|
try {
|
|
36088
36453
|
const res = await fetch(buildRawFileUrl(target), { method: "HEAD" });
|
|
36454
|
+
if (res.status === 404) return { missing: true };
|
|
36089
36455
|
if (!res.ok) return {};
|
|
36090
36456
|
const rawSize = res.headers.get("content-length");
|
|
36091
36457
|
const size = rawSize == null ? NaN : Number(rawSize);
|
|
@@ -47326,12 +47692,53 @@ ${error2.stack}` : ""}`
|
|
|
47326
47692
|
);
|
|
47327
47693
|
}
|
|
47328
47694
|
}
|
|
47695
|
+
let fileRouteSignature = null;
|
|
47696
|
+
let fileRouteSignatureSeed = null;
|
|
47697
|
+
let fileRouteSignatureCheck = null;
|
|
47698
|
+
function fileRouteSignatureKey(route) {
|
|
47699
|
+
return `${route.view || "blob"}\0${route.path}\0${route.ref || "worktree"}`;
|
|
47700
|
+
}
|
|
47701
|
+
async function readFileRouteSignature(route) {
|
|
47702
|
+
const info = await REPO_VIEW.loadRawFileInfo({
|
|
47703
|
+
path: route.path,
|
|
47704
|
+
ref: route.ref || "worktree"
|
|
47705
|
+
});
|
|
47706
|
+
return rawFileInfoSignature(info);
|
|
47707
|
+
}
|
|
47708
|
+
function seedFileRouteSignature(route) {
|
|
47709
|
+
const key = fileRouteSignatureKey(route);
|
|
47710
|
+
const seed = trackLoad(readFileRouteSignature(route)).then((sig) => {
|
|
47711
|
+
if (fileRouteSignatureSeed === seed) fileRouteSignatureSeed = null;
|
|
47712
|
+
if (sig === null) return;
|
|
47713
|
+
fileRouteSignature = { key, sig };
|
|
47714
|
+
});
|
|
47715
|
+
fileRouteSignatureSeed = seed;
|
|
47716
|
+
}
|
|
47717
|
+
function refreshFileRouteIfChanged(route) {
|
|
47718
|
+
if (fileRouteSignatureCheck) return;
|
|
47719
|
+
const key = fileRouteSignatureKey(route);
|
|
47720
|
+
const pendingSeed = fileRouteSignatureSeed ?? Promise.resolve();
|
|
47721
|
+
fileRouteSignatureCheck = pendingSeed.then(() => trackLoad(readFileRouteSignature(route))).then((sig) => {
|
|
47722
|
+
fileRouteSignatureCheck = null;
|
|
47723
|
+
if (sig === null) return;
|
|
47724
|
+
const routeNow = STATE.route;
|
|
47725
|
+
if (routeNow.screen !== "file" || !isBlobOrBlameFileRoute(routeNow) || fileRouteSignatureKey(routeNow) !== key)
|
|
47726
|
+
return;
|
|
47727
|
+
if (fileSignatureUnchanged(fileRouteSignature, key, sig)) return;
|
|
47728
|
+
fileRouteSignature = { key, sig };
|
|
47729
|
+
dispatchFileRoute(routeNow, { refresh: true });
|
|
47730
|
+
});
|
|
47731
|
+
fileRouteSignatureCheck.catch(() => {
|
|
47732
|
+
fileRouteSignatureCheck = null;
|
|
47733
|
+
});
|
|
47734
|
+
}
|
|
47329
47735
|
function dispatchFileRoute(route, options = {}) {
|
|
47330
47736
|
if (route.view === "blob") {
|
|
47331
47737
|
setStatus("live");
|
|
47332
47738
|
removeFileHistoryShell2();
|
|
47333
47739
|
BLAME_VIEW.removeBlamePage();
|
|
47334
47740
|
applySourceRouteToShell(options);
|
|
47741
|
+
seedFileRouteSignature(route);
|
|
47335
47742
|
return true;
|
|
47336
47743
|
}
|
|
47337
47744
|
if (route.view === "blame") {
|
|
@@ -47339,6 +47746,7 @@ ${error2.stack}` : ""}`
|
|
|
47339
47746
|
cancelActiveSourceLoad("navigation");
|
|
47340
47747
|
removeFileHistoryShell2();
|
|
47341
47748
|
void BLAME_VIEW.renderBlamePage({ path: route.path, ref: route.ref });
|
|
47749
|
+
seedFileRouteSignature(route);
|
|
47342
47750
|
return true;
|
|
47343
47751
|
}
|
|
47344
47752
|
if (route.view === "history") {
|
|
@@ -49510,7 +49918,7 @@ ${error2.stack}` : ""}`
|
|
|
49510
49918
|
if (isBlobOrBlameFileRoute(route)) {
|
|
49511
49919
|
const viewingPath = route.path;
|
|
49512
49920
|
if (viewingPath && !changedPathsCoverPath(paths, viewingPath)) return;
|
|
49513
|
-
|
|
49921
|
+
refreshFileRouteIfChanged(route);
|
|
49514
49922
|
return;
|
|
49515
49923
|
}
|
|
49516
49924
|
if (route.screen === "repo") {
|
package/web/style.css
CHANGED
|
@@ -10464,6 +10464,14 @@ body.journal-task-editor-resizing * {
|
|
|
10464
10464
|
.db-grid-header-wrap {
|
|
10465
10465
|
flex-shrink: 0;
|
|
10466
10466
|
overflow: hidden;
|
|
10467
|
+
/* 本文 (.db-grid-viewport) の縦スクロールバーが食う幅を、右端に透明な
|
|
10468
|
+
border として空ける。空けないと「見える幅」が本文だけ狭くなり、本文が
|
|
10469
|
+
右へ余分にスクロールできてしまう。ヘッダは scrollLeft を同期しても
|
|
10470
|
+
そこまで追随できず、右端で列がスクロールバー幅ぶんずれる。
|
|
10471
|
+
scrollbar-gutter は overflow:hidden だと見た目の幅しか縮まず、
|
|
10472
|
+
スクロール範囲は縮まないので使えない (実測済み)。
|
|
10473
|
+
幅は table-grid.ts が実測して --db-grid-scrollbar-w に入れる。 */
|
|
10474
|
+
border-right: var(--db-grid-scrollbar-w, 0px) solid transparent;
|
|
10467
10475
|
border-bottom: 2px solid var(--border);
|
|
10468
10476
|
background: var(--bg-soft);
|
|
10469
10477
|
}
|
|
@@ -10508,6 +10516,20 @@ body.journal-task-editor-resizing * {
|
|
|
10508
10516
|
flex: 1;
|
|
10509
10517
|
overflow: auto;
|
|
10510
10518
|
position: relative;
|
|
10519
|
+
/* 行が少なくて縦スクロールバーが出ないときも幅を変えない。ここを可変に
|
|
10520
|
+
すると、ヘッダ / フィルタ行に渡す --db-grid-scrollbar-w が行数やフィルタで
|
|
10521
|
+
変わり、列の位置が動く。 */
|
|
10522
|
+
scrollbar-gutter: stable;
|
|
10523
|
+
}
|
|
10524
|
+
/* 矢印キーでセルを移動するため viewport 自体がフォーカスを受け取る
|
|
10525
|
+
(tabIndex=0)。セルをクリックするたびに枠が出ると常時ノイズになるので、
|
|
10526
|
+
キーボードで到達したときだけリングを出す。親の .db-grid が
|
|
10527
|
+
overflow:hidden なので外側に描くと切れる → inset で内側に描く。 */
|
|
10528
|
+
.db-grid-viewport:focus {
|
|
10529
|
+
outline: none;
|
|
10530
|
+
}
|
|
10531
|
+
.db-grid-viewport:focus-visible {
|
|
10532
|
+
box-shadow: inset var(--db-focus-ring);
|
|
10511
10533
|
}
|
|
10512
10534
|
|
|
10513
10535
|
.db-grid-spacer {
|
|
@@ -11097,6 +11119,8 @@ html[data-theme="dark"] .db-schema-table tr.pk-row {
|
|
|
11097
11119
|
.db-grid-filter-row-wrap {
|
|
11098
11120
|
flex-shrink: 0;
|
|
11099
11121
|
overflow: hidden;
|
|
11122
|
+
/* ヘッダと同じ理由でスクロールバー幅を空ける (.db-grid-header-wrap 参照)。 */
|
|
11123
|
+
border-right: var(--db-grid-scrollbar-w, 0px) solid transparent;
|
|
11100
11124
|
border-bottom: 1px solid var(--border);
|
|
11101
11125
|
background: var(--bg-soft);
|
|
11102
11126
|
}
|
|
@@ -11276,6 +11300,23 @@ html[data-theme="dark"] .db-schema-table tr.pk-row {
|
|
|
11276
11300
|
white-space: pre-wrap;
|
|
11277
11301
|
word-break: break-word;
|
|
11278
11302
|
}
|
|
11303
|
+
/* shiki は `<span style="--shiki-light:#xxx;--shiki-dark:#yyy">` の inline
|
|
11304
|
+
CSS 変数で token 色を出すので、light / dark でそれぞれ参照する。
|
|
11305
|
+
明示テーマ (html[data-theme]) を OS 設定より後に置いて優先させる。 */
|
|
11306
|
+
.db-grid-detail-json span[style] {
|
|
11307
|
+
color: var(--shiki-light);
|
|
11308
|
+
}
|
|
11309
|
+
@media (prefers-color-scheme: dark) {
|
|
11310
|
+
.db-grid-detail-json span[style] {
|
|
11311
|
+
color: var(--shiki-dark);
|
|
11312
|
+
}
|
|
11313
|
+
}
|
|
11314
|
+
html[data-theme="light"] .db-grid-detail-json span[style] {
|
|
11315
|
+
color: var(--shiki-light);
|
|
11316
|
+
}
|
|
11317
|
+
html[data-theme="dark"] .db-grid-detail-json span[style] {
|
|
11318
|
+
color: var(--shiki-dark);
|
|
11319
|
+
}
|
|
11279
11320
|
|
|
11280
11321
|
/* ---------- Foreign-key related data ---------- */
|
|
11281
11322
|
.db-grid-header-fk-icon {
|
|
@@ -11373,13 +11414,32 @@ html[data-theme="dark"] .db-schema-table tr.pk-row {
|
|
|
11373
11414
|
}
|
|
11374
11415
|
|
|
11375
11416
|
.db-related-list {
|
|
11376
|
-
|
|
11417
|
+
/* 既定 / 下限 / 上限は table-grid.ts が持つ。ここは JS 実行前の初回描画用の
|
|
11418
|
+
fallback だけ (ui-layout: CSS と TS に同じ数値を書かない)。 */
|
|
11419
|
+
width: var(--db-related-list-w, 200px);
|
|
11377
11420
|
flex-shrink: 0;
|
|
11378
11421
|
overflow: auto;
|
|
11379
|
-
border-right: 1px solid var(--border);
|
|
11380
11422
|
padding: 4px;
|
|
11381
11423
|
}
|
|
11382
11424
|
|
|
11425
|
+
/* リストとグリッドの間の掴みしろ。境界線の役目も兼ねるので、リスト側の
|
|
11426
|
+
border-right はこちらへ移してある (二重線にしない)。 */
|
|
11427
|
+
.db-related-list-resize {
|
|
11428
|
+
flex: 0 0 auto;
|
|
11429
|
+
width: 5px;
|
|
11430
|
+
cursor: col-resize;
|
|
11431
|
+
background: var(--border);
|
|
11432
|
+
background-clip: content-box;
|
|
11433
|
+
border-left: 2px solid transparent;
|
|
11434
|
+
border-right: 2px solid transparent;
|
|
11435
|
+
}
|
|
11436
|
+
.db-related-list-resize:hover,
|
|
11437
|
+
.db-related-list-resize:focus-visible,
|
|
11438
|
+
.db-related-panel.db-related-list-resizing .db-related-list-resize {
|
|
11439
|
+
background: var(--accent);
|
|
11440
|
+
outline: none;
|
|
11441
|
+
}
|
|
11442
|
+
|
|
11383
11443
|
.db-related-list-item {
|
|
11384
11444
|
display: flex;
|
|
11385
11445
|
flex-direction: column;
|
|
@@ -11410,6 +11470,17 @@ html[data-theme="dark"] .db-schema-table tr.pk-row {
|
|
|
11410
11470
|
display: inline-flex;
|
|
11411
11471
|
align-items: baseline;
|
|
11412
11472
|
gap: 6px;
|
|
11473
|
+
/* 項目の幅を超えたら省略記号にする。min-width:0 が無いと flex 子は
|
|
11474
|
+
内容幅より縮まず、はみ出したまま折り返す。 */
|
|
11475
|
+
max-width: 100%;
|
|
11476
|
+
min-width: 0;
|
|
11477
|
+
}
|
|
11478
|
+
|
|
11479
|
+
.db-related-list-table {
|
|
11480
|
+
min-width: 0;
|
|
11481
|
+
overflow: hidden;
|
|
11482
|
+
text-overflow: ellipsis;
|
|
11483
|
+
white-space: nowrap;
|
|
11413
11484
|
}
|
|
11414
11485
|
/* 方向アイコンを name の前に出す: outgoing は → (自分→他テーブル)、
|
|
11415
11486
|
incoming は ← (他テーブル→自分の PK)。テキスト 1 文字なのでフォントに
|
|
@@ -11430,7 +11501,12 @@ html[data-theme="dark"] .db-schema-table tr.pk-row {
|
|
|
11430
11501
|
.db-related-list-via {
|
|
11431
11502
|
font-size: 11px;
|
|
11432
11503
|
color: var(--fg-muted);
|
|
11433
|
-
|
|
11504
|
+
/* break-all だと "sample_association_table.sa / mple_column_id = 12" の
|
|
11505
|
+
ように語の途中で折り返って読めなくなる。1 行で省略し、全文は title で出す。 */
|
|
11506
|
+
max-width: 100%;
|
|
11507
|
+
overflow: hidden;
|
|
11508
|
+
text-overflow: ellipsis;
|
|
11509
|
+
white-space: nowrap;
|
|
11434
11510
|
}
|
|
11435
11511
|
|
|
11436
11512
|
.db-related-grid-host {
|