@youtyan/code-viewer 0.1.21 → 0.1.23

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 (3) hide show
  1. package/package.json +1 -1
  2. package/web/app.js +622 -45
  3. package/web/style.css +83 -36
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -243,10 +243,43 @@
243
243
  }
244
244
  return ranges;
245
245
  }
246
- function fuzzyMatchPath(query, path) {
246
+ function basenameMatchTier(loweredQuery, loweredBasename) {
247
+ if (loweredBasename === loweredQuery)
248
+ return 4;
249
+ if (loweredBasename.startsWith(`${loweredQuery}.`))
250
+ return 3;
251
+ if (loweredBasename.startsWith(loweredQuery))
252
+ return 2;
253
+ if (loweredBasename.includes(loweredQuery))
254
+ return 1;
255
+ return 0;
256
+ }
257
+ function pathMatchTier(loweredQuery, loweredPath, loweredBasename) {
258
+ if (loweredQuery.includes("/") && (loweredPath === loweredQuery || loweredPath.endsWith(`/${loweredQuery}`)))
259
+ return 4;
260
+ return basenameMatchTier(loweredQuery, loweredBasename);
261
+ }
262
+ function contiguousPathRange(loweredQuery, loweredPath, baseStart) {
263
+ const loweredBasename = loweredPath.slice(baseStart);
264
+ const basenameMatchStart = loweredBasename.indexOf(loweredQuery);
265
+ if (basenameMatchStart >= 0) {
266
+ const start = baseStart + basenameMatchStart;
267
+ return { start, end: start + loweredQuery.length };
268
+ }
269
+ if (loweredQuery.includes("/")) {
270
+ const pathMatchStart = loweredPath.endsWith(`/${loweredQuery}`) ? loweredPath.length - loweredQuery.length : loweredPath === loweredQuery ? 0 : -1;
271
+ if (pathMatchStart >= 0)
272
+ return {
273
+ start: pathMatchStart,
274
+ end: pathMatchStart + loweredQuery.length
275
+ };
276
+ }
277
+ return null;
278
+ }
279
+ function computeFuzzyMatch(query, path) {
247
280
  const q = query.trim().toLowerCase();
248
281
  if (!q)
249
- return { score: 0, ranges: [] };
282
+ return { score: 0, ranges: [], tier: 0 };
250
283
  const lowerPath = path.toLowerCase();
251
284
  const baseStart = basenameStart(path);
252
285
  const indices = [];
@@ -267,24 +300,28 @@
267
300
  score += 12;
268
301
  from = index + 1;
269
302
  }
270
- const first = indices[0] || 0;
303
+ const first = indices[0];
271
304
  score -= Math.min(first, 40);
272
305
  if (indices[0] >= baseStart)
273
306
  score += 20;
274
307
  const basename = lowerPath.slice(baseStart);
275
- if (basename.startsWith(q))
276
- score += 30;
277
- if (basename === q || basename.startsWith(`${q}.`))
278
- score += 25;
279
- if (lowerPath.endsWith(q))
280
- score += 15;
281
- return { score, ranges: toRanges(indices) };
308
+ const tier = pathMatchTier(q, lowerPath, basename);
309
+ const contiguousRange = contiguousPathRange(q, lowerPath, baseStart);
310
+ return {
311
+ score,
312
+ ranges: contiguousRange ? [contiguousRange] : toRanges(indices),
313
+ tier
314
+ };
315
+ }
316
+ function fuzzyMatchPath(query, path) {
317
+ const match = computeFuzzyMatch(query, path);
318
+ return match ? { score: match.score, ranges: match.ranges } : null;
282
319
  }
283
320
  function rankFuzzyPaths(query, items) {
284
321
  return items.map((item) => {
285
- const match = fuzzyMatchPath(query, item.path);
286
- return match ? { item, score: match.score, ranges: match.ranges } : null;
287
- }).filter((item) => item !== null).sort((a, b) => b.score - a.score || a.item.path.localeCompare(b.item.path));
322
+ const match = computeFuzzyMatch(query, item.path);
323
+ return match ? { item, score: match.score, ranges: match.ranges, tier: match.tier } : null;
324
+ }).filter((item) => item !== null).sort((a, b) => b.tier - a.tier || b.score - a.score || a.item.path.localeCompare(b.item.path)).map(({ item, score, ranges }) => ({ item, score, ranges }));
288
325
  }
289
326
  function isGlobPathQuery(query) {
290
327
  return /[*?]/.test(query.trim());
@@ -6481,7 +6518,7 @@ ${frontmatter.yaml}
6481
6518
  });
6482
6519
  }
6483
6520
  function buildMarkdownToc(root) {
6484
- const entries = Array.from(root.querySelectorAll("h1[id], h2[id], h3[id]")).map((heading2) => ({
6521
+ const entries = Array.from(root.querySelectorAll("h1[id], h2[id], h3[id], h4[id]")).map((heading2) => ({
6485
6522
  id: heading2.id,
6486
6523
  level: Number(heading2.tagName.slice(1)),
6487
6524
  text: (heading2.textContent || "").replace(/#$/, "").trim()
@@ -6500,6 +6537,7 @@ ${frontmatter.yaml}
6500
6537
  link2.href = `#${encodeURIComponent(entry.id)}`;
6501
6538
  link2.dataset.target = entry.id;
6502
6539
  link2.textContent = entry.text;
6540
+ link2.title = entry.text;
6503
6541
  item.appendChild(link2);
6504
6542
  list2.appendChild(item);
6505
6543
  });
@@ -6884,7 +6922,11 @@ ${frontmatter.yaml}
6884
6922
  const VIRTUAL_SOURCE_SIZE_THRESHOLD = 1024 * 1024;
6885
6923
  const VIRTUAL_SOURCE_PAGE_SIZE = 2000;
6886
6924
  const VIRTUAL_SOURCE_ROW_HEIGHT = 20;
6925
+ const VIRTUAL_SIDEBAR_THRESHOLD = 3000;
6926
+ const VIRTUAL_SIDEBAR_ROW_HEIGHT = 29;
6927
+ const VIRTUAL_SIDEBAR_OVERSCAN = 16;
6887
6928
  const VIRTUAL_SOURCE_HIGHLIGHT_MAX_LINE_LENGTH = 2000;
6929
+ const TEST_RE = /(^|[/_.])(test|spec|__tests__)([/_.]|$)/i;
6888
6930
  let highlightLoadPromise = null;
6889
6931
  let sourceShikiLoadPromise = null;
6890
6932
  let highlightConfigured = false;
@@ -6894,6 +6936,12 @@ ${frontmatter.yaml}
6894
6936
  let REPO_SIDEBAR_LOAD = null;
6895
6937
  let SIDEBAR_FILES = [];
6896
6938
  let SIDEBAR_ON_FILE_CLICK;
6939
+ let SIDEBAR_TREE_ROOT = null;
6940
+ let SIDEBAR_TREE_ROWS = [];
6941
+ let SIDEBAR_VISIBLE_ROWS = [];
6942
+ let SIDEBAR_ROW_BY_PATH = new Map;
6943
+ let SIDEBAR_VIRTUAL_ACTIVE_PATH = "";
6944
+ const SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
6897
6945
  let SERVER_SCOPE_OMIT_DIRS_DEFAULT = [];
6898
6946
  let PENDING_G_SCOPE = null;
6899
6947
  let PENDING_G_UNTIL = 0;
@@ -7584,6 +7632,22 @@ ${frontmatter.yaml}
7584
7632
  attachSidebarToggle(restoreHost);
7585
7633
  else if (sidebarHead)
7586
7634
  attachSidebarToggle(sidebarHead);
7635
+ placeSidebarFilter();
7636
+ }
7637
+ function placeSidebarFilter() {
7638
+ const sidebarHead = document.querySelector(".sb-head");
7639
+ const filter = document.querySelector(".sb-filter-wrap");
7640
+ const list2 = document.querySelector("#filelist");
7641
+ if (!sidebarHead || !filter || !list2)
7642
+ return;
7643
+ const repoSidebar = isRepositorySidebarMode();
7644
+ if (repoSidebar && filter.parentElement !== sidebarHead) {
7645
+ sidebarHead.appendChild(filter);
7646
+ return;
7647
+ }
7648
+ if (!repoSidebar && filter.parentElement === sidebarHead) {
7649
+ sidebarHead.after(filter);
7650
+ }
7587
7651
  }
7588
7652
  function applySidebarHidden(hidden = STATE.sidebarHidden) {
7589
7653
  STATE.sidebarHidden = hidden;
@@ -7859,6 +7923,301 @@ ${frontmatter.yaml}
7859
7923
  }
7860
7924
  }
7861
7925
  }
7926
+ function treeNodeItems(node) {
7927
+ const cached = SIDEBAR_TREE_ITEMS_CACHE.get(node);
7928
+ if (cached)
7929
+ return cached;
7930
+ const items = [];
7931
+ for (const k of Object.keys(node.dirs)) {
7932
+ const d2 = node.dirs[k];
7933
+ items.push({ kind: "dir", sortKey: d2.minOrder, dir: d2 });
7934
+ }
7935
+ for (const f2 of node.files) {
7936
+ items.push({
7937
+ kind: "file",
7938
+ sortKey: f2.order != null ? f2.order : Infinity,
7939
+ file: f2
7940
+ });
7941
+ }
7942
+ items.sort((a2, b2) => a2.sortKey - b2.sortKey);
7943
+ SIDEBAR_TREE_ITEMS_CACHE.set(node, items);
7944
+ return items;
7945
+ }
7946
+ function createTreeDirRow(dir, depth, onFileClick) {
7947
+ const li = document.createElement("li");
7948
+ li.className = "tree-dir";
7949
+ li.tabIndex = -1;
7950
+ li.dataset.dirpath = dir.path;
7951
+ if (dir.explicit)
7952
+ li.dataset.explicit = "true";
7953
+ if (dir.children_omitted) {
7954
+ li.classList.add("children-omitted");
7955
+ li.classList.add(dir.children_omitted_reason === "heavy" ? "children-omitted-heavy" : "children-omitted-internal");
7956
+ li.title = dir.children_omitted_reason === "heavy" ? "Large generated/vendor directory: open the detail pane to browse its contents" : "Internal Git metadata is not browsed";
7957
+ }
7958
+ li.style.setProperty("--lvl-pad", `${12 + depth * 14}px`);
7959
+ const chev = document.createElement("span");
7960
+ if (dir.children_omitted) {
7961
+ chev.className = "chev-spacer";
7962
+ chev.setAttribute("aria-hidden", "true");
7963
+ } else {
7964
+ chev.className = "chev";
7965
+ setChevronIcon(chev);
7966
+ }
7967
+ li.appendChild(chev);
7968
+ const dirIcon = document.createElement("span");
7969
+ dirIcon.className = "dir-icon";
7970
+ li.appendChild(dirIcon);
7971
+ const label = document.createElement("span");
7972
+ label.className = "dir-label";
7973
+ const dn = document.createElement("span");
7974
+ dn.className = "dir-name";
7975
+ dn.textContent = dir.name;
7976
+ dn.title = dir.path;
7977
+ label.appendChild(dn);
7978
+ if (dir.children_omitted) {
7979
+ const omitted = document.createElement("span");
7980
+ omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
7981
+ omitted.textContent = dir.children_omitted_reason === "heavy" ? "skipped" : "private";
7982
+ omitted.title = dir.children_omitted_reason === "heavy" ? "Tree expansion is skipped, but the directory detail can be opened" : "This directory cannot be opened from the browser";
7983
+ label.appendChild(omitted);
7984
+ }
7985
+ li.appendChild(label);
7986
+ li.appendChild(createOpenPathButton(dir.path, "directory", "open this folder in OS"));
7987
+ const updateIcon = () => {
7988
+ setFolderIcon(dirIcon, li.classList.contains("collapsed"));
7989
+ };
7990
+ const toggleDir = (e2) => {
7991
+ e2.stopPropagation();
7992
+ li.classList.toggle("collapsed");
7993
+ updateIcon();
7994
+ if (li.classList.contains("collapsed"))
7995
+ STATE.collapsedDirs.add(dir.path);
7996
+ else
7997
+ STATE.collapsedDirs.delete(dir.path);
7998
+ localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
7999
+ rerenderVirtualSidebar();
8000
+ };
8001
+ li.classList.toggle("collapsed", STATE.collapsedDirs.has(dir.path));
8002
+ updateIcon();
8003
+ if (!dir.children_omitted) {
8004
+ chev.addEventListener("click", toggleDir);
8005
+ dirIcon.addEventListener("click", toggleDir);
8006
+ }
8007
+ if (onFileClick) {
8008
+ li.addEventListener("click", (e2) => {
8009
+ e2.stopPropagation();
8010
+ if (dir.children_omitted_reason === "internal" || dir.children_omitted_reason === "truncated")
8011
+ return;
8012
+ onFileClick({
8013
+ path: dir.path,
8014
+ display_path: dir.path,
8015
+ type: "tree",
8016
+ children_omitted: dir.children_omitted,
8017
+ children_omitted_reason: dir.children_omitted_reason
8018
+ });
8019
+ scheduleMainSurfaceFocus();
8020
+ });
8021
+ } else {
8022
+ li.addEventListener("click", toggleDir);
8023
+ }
8024
+ return li;
8025
+ }
8026
+ function createTreeFileRow(f2, depth, onFileClick) {
8027
+ const li = document.createElement("li");
8028
+ li.className = "tree-file";
8029
+ li.tabIndex = -1;
8030
+ li.dataset.path = f2.path;
8031
+ li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
8032
+ li.classList.toggle("hidden-by-tests", STATE.hideTests && TEST_RE.test(f2.path || ""));
8033
+ li.style.setProperty("--lvl-pad", `${12 + depth * 14}px`);
8034
+ const spacer = document.createElement("span");
8035
+ spacer.className = "chev-spacer";
8036
+ li.appendChild(spacer);
8037
+ if (f2.status) {
8038
+ li.appendChild(fileBadge(f2.status));
8039
+ } else {
8040
+ const icon = document.createElement("span");
8041
+ icon.className = "d2h-icon-wrapper";
8042
+ icon.innerHTML = fileEntryIcon();
8043
+ li.appendChild(icon);
8044
+ }
8045
+ const name = document.createElement("span");
8046
+ name.className = "name";
8047
+ name.textContent = f2.path.split("/").pop();
8048
+ name.title = f2.path;
8049
+ li.appendChild(name);
8050
+ li.addEventListener("click", () => {
8051
+ if (onFileClick)
8052
+ onFileClick(f2);
8053
+ else
8054
+ scrollToFile(f2.path);
8055
+ scheduleMainSurfaceFocus();
8056
+ });
8057
+ if (!onFileClick)
8058
+ li.addEventListener("mouseenter", () => prefetchByPath(f2.path), {
8059
+ passive: true
8060
+ });
8061
+ return li;
8062
+ }
8063
+ function buildSidebarTreeRows(root) {
8064
+ const rows = [];
8065
+ const byPath = new Map;
8066
+ const walk = (node, depth) => {
8067
+ for (const item of treeNodeItems(node)) {
8068
+ if (item.kind === "dir") {
8069
+ const row = {
8070
+ kind: "dir",
8071
+ path: item.dir.path,
8072
+ name: item.dir.name,
8073
+ depth,
8074
+ dir: item.dir
8075
+ };
8076
+ rows.push(row);
8077
+ byPath.set(row.path, row);
8078
+ walk(item.dir, depth + 1);
8079
+ } else {
8080
+ const row = {
8081
+ kind: "file",
8082
+ path: item.file.path,
8083
+ name: item.file.path.split("/").pop() || item.file.path,
8084
+ depth,
8085
+ file: item.file
8086
+ };
8087
+ rows.push(row);
8088
+ byPath.set(row.path, row);
8089
+ }
8090
+ }
8091
+ };
8092
+ walk(root, 0);
8093
+ SIDEBAR_TREE_ROWS = rows;
8094
+ SIDEBAR_ROW_BY_PATH = byPath;
8095
+ }
8096
+ function computeVirtualSidebarVisibleRows() {
8097
+ if (!SIDEBAR_TREE_ROOT) {
8098
+ SIDEBAR_VISIBLE_ROWS = [];
8099
+ return;
8100
+ }
8101
+ const input = $("#sb-filter");
8102
+ const filter = compileFileFilter(input.value);
8103
+ const invalid = filter.kind === "invalid";
8104
+ input.toggleAttribute("aria-invalid", invalid);
8105
+ input.title = invalid ? filter.error || "invalid regular expression" : "";
8106
+ const filterActive = filter.kind !== "empty" && !invalid;
8107
+ const matches = invalid ? () => true : filter.match;
8108
+ const walk = (node, depth) => {
8109
+ let subtreeVisible = false;
8110
+ const rows = [];
8111
+ for (const item of treeNodeItems(node)) {
8112
+ if (item.kind === "dir") {
8113
+ const dirMatches = filterActive && matches(item.dir.path);
8114
+ const expanded = !item.dir.children_omitted && (filterActive || !STATE.collapsedDirs.has(item.dir.path));
8115
+ const child = walk(item.dir, depth + 1);
8116
+ const visible = item.dir.explicit && !filterActive ? true : dirMatches || child.visible;
8117
+ if (visible) {
8118
+ rows.push({
8119
+ kind: "dir",
8120
+ path: item.dir.path,
8121
+ name: item.dir.name,
8122
+ depth,
8123
+ dir: item.dir
8124
+ });
8125
+ if (expanded)
8126
+ rows.push(...child.rows);
8127
+ }
8128
+ subtreeVisible = subtreeVisible || visible;
8129
+ } else {
8130
+ const testHidden = STATE.hideTests && TEST_RE.test(item.file.path || "");
8131
+ const visible = !testHidden && matches(item.file.path || "");
8132
+ if (visible) {
8133
+ rows.push({
8134
+ kind: "file",
8135
+ path: item.file.path,
8136
+ name: item.file.path.split("/").pop() || item.file.path,
8137
+ depth,
8138
+ file: item.file
8139
+ });
8140
+ }
8141
+ subtreeVisible = subtreeVisible || visible;
8142
+ }
8143
+ }
8144
+ return { visible: subtreeVisible, rows };
8145
+ };
8146
+ SIDEBAR_VISIBLE_ROWS = walk(SIDEBAR_TREE_ROOT, 0).rows;
8147
+ }
8148
+ function sidebarVirtualRange() {
8149
+ const sidebar = document.querySelector("#sidebar");
8150
+ const scrollTop = sidebar?.scrollTop || 0;
8151
+ const height = sidebar?.clientHeight || window.innerHeight;
8152
+ const start = Math.max(0, Math.floor(scrollTop / VIRTUAL_SIDEBAR_ROW_HEIGHT) - VIRTUAL_SIDEBAR_OVERSCAN);
8153
+ const end = Math.min(SIDEBAR_VISIBLE_ROWS.length, Math.ceil((scrollTop + height) / VIRTUAL_SIDEBAR_ROW_HEIGHT) + VIRTUAL_SIDEBAR_OVERSCAN);
8154
+ return { start, end };
8155
+ }
8156
+ function renderVirtualSidebarWindow() {
8157
+ const ul = $("#filelist");
8158
+ if (!ul.classList.contains("tree-virtual"))
8159
+ return;
8160
+ const { start, end } = sidebarVirtualRange();
8161
+ const fragment = document.createDocumentFragment();
8162
+ for (let i2 = start;i2 < end; i2++) {
8163
+ const row = SIDEBAR_VISIBLE_ROWS[i2];
8164
+ const li = row.kind === "dir" && row.dir ? createTreeDirRow(row.dir, row.depth, SIDEBAR_ON_FILE_CLICK) : row.file ? createTreeFileRow(row.file, row.depth, SIDEBAR_ON_FILE_CLICK) : null;
8165
+ if (!li)
8166
+ continue;
8167
+ li.classList.toggle("active", row.path === SIDEBAR_VIRTUAL_ACTIVE_PATH);
8168
+ li.style.position = "absolute";
8169
+ li.style.top = `${i2 * VIRTUAL_SIDEBAR_ROW_HEIGHT}px`;
8170
+ li.style.left = "0";
8171
+ li.style.right = "0";
8172
+ fragment.appendChild(li);
8173
+ }
8174
+ ul.replaceChildren(fragment);
8175
+ ul.style.height = `${SIDEBAR_VISIBLE_ROWS.length * VIRTUAL_SIDEBAR_ROW_HEIGHT}px`;
8176
+ }
8177
+ function scrollVirtualSidebarPathIntoView(path) {
8178
+ const index = SIDEBAR_VISIBLE_ROWS.findIndex((row) => row.path === path);
8179
+ if (index < 0)
8180
+ return;
8181
+ const sidebar = document.querySelector("#sidebar");
8182
+ if (!sidebar)
8183
+ return;
8184
+ const ul = $("#filelist");
8185
+ const top = index * VIRTUAL_SIDEBAR_ROW_HEIGHT;
8186
+ const bottom = top + VIRTUAL_SIDEBAR_ROW_HEIGHT;
8187
+ const sidebarRect = sidebar.getBoundingClientRect();
8188
+ const stickyBottom = Math.max(sidebarRect.top, document.querySelector(".sb-head")?.getBoundingClientRect().bottom || sidebarRect.top, document.querySelector(".sb-filter-wrap")?.getBoundingClientRect().bottom || sidebarRect.top);
8189
+ const topPadding = Math.max(8, stickyBottom - sidebarRect.top + 8);
8190
+ const bottomPadding = 14;
8191
+ const listTop = ul.offsetTop;
8192
+ const maxHeight = Number.parseFloat(getComputedStyle(sidebar).maxHeight);
8193
+ const visibleHeight = Number.isFinite(maxHeight) && maxHeight > 0 ? Math.min(sidebar.clientHeight, maxHeight) : sidebar.clientHeight;
8194
+ const visibleTop = sidebar.scrollTop + topPadding - listTop;
8195
+ const visibleBottom = sidebar.scrollTop + visibleHeight - bottomPadding - listTop;
8196
+ if (top < visibleTop)
8197
+ sidebar.scrollTop = Math.max(0, top + listTop - topPadding);
8198
+ else if (bottom > visibleBottom)
8199
+ sidebar.scrollTop = bottom + listTop - visibleHeight + bottomPadding;
8200
+ renderVirtualSidebarWindow();
8201
+ }
8202
+ function rerenderVirtualSidebar() {
8203
+ const ul = document.querySelector("#filelist");
8204
+ if (!ul?.classList.contains("tree-virtual"))
8205
+ return;
8206
+ computeVirtualSidebarVisibleRows();
8207
+ renderVirtualSidebarWindow();
8208
+ }
8209
+ function renderVirtualTreeSidebar(root) {
8210
+ const ul = $("#filelist");
8211
+ SIDEBAR_TREE_ROOT = root;
8212
+ buildSidebarTreeRows(root);
8213
+ ul.classList.add("tree-virtual");
8214
+ ul.style.position = "relative";
8215
+ computeVirtualSidebarVisibleRows();
8216
+ renderVirtualSidebarWindow();
8217
+ document.querySelector("#sidebar")?.addEventListener("scroll", renderVirtualSidebarWindow, {
8218
+ passive: true
8219
+ });
8220
+ }
7862
8221
  function renderFlat(files, ul, onFileClick) {
7863
8222
  files.forEach((f2, i2) => {
7864
8223
  const li = document.createElement("li");
@@ -7897,6 +8256,13 @@ ${frontmatter.yaml}
7897
8256
  const ul = $("#filelist");
7898
8257
  ul.innerHTML = "";
7899
8258
  ul.classList.toggle("tree", STATE.sbView === "tree");
8259
+ ul.classList.remove("tree-virtual");
8260
+ ul.style.removeProperty("height");
8261
+ ul.style.removeProperty("position");
8262
+ SIDEBAR_TREE_ROOT = null;
8263
+ SIDEBAR_TREE_ROWS = [];
8264
+ SIDEBAR_VISIBLE_ROWS = [];
8265
+ SIDEBAR_ROW_BY_PATH = new Map;
7900
8266
  STATE.files = files;
7901
8267
  SIDEBAR_FILES = files;
7902
8268
  SIDEBAR_ON_FILE_CLICK = onFileClick;
@@ -7904,7 +8270,10 @@ ${frontmatter.yaml}
7904
8270
  REPO_SIDEBAR_REF = null;
7905
8271
  if (STATE.sbView === "tree") {
7906
8272
  const root = buildTree(files);
7907
- renderTreeNode(root, 0, ul, onFileClick);
8273
+ if (onFileClick && files.length >= VIRTUAL_SIDEBAR_THRESHOLD)
8274
+ renderVirtualTreeSidebar(root);
8275
+ else
8276
+ renderTreeNode(root, 0, ul, onFileClick);
7908
8277
  } else {
7909
8278
  renderFlat(files, ul, onFileClick);
7910
8279
  }
@@ -7922,6 +8291,17 @@ ${frontmatter.yaml}
7922
8291
  function setAllSidebarDirsCollapsed(collapsed) {
7923
8292
  if (!collapsed)
7924
8293
  STATE.collapsedDirs.clear();
8294
+ if ($("#filelist").classList.contains("tree-virtual")) {
8295
+ if (collapsed) {
8296
+ for (const row of SIDEBAR_TREE_ROWS) {
8297
+ if (row.kind === "dir")
8298
+ STATE.collapsedDirs.add(row.path);
8299
+ }
8300
+ }
8301
+ localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
8302
+ rerenderVirtualSidebar();
8303
+ return;
8304
+ }
7925
8305
  $$("#filelist .tree-dir[data-dirpath]").forEach((li) => {
7926
8306
  const path = li.dataset.dirpath || "";
7927
8307
  if (!path)
@@ -8098,29 +8478,32 @@ ${frontmatter.yaml}
8098
8478
  }
8099
8479
  if (changed)
8100
8480
  localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
8481
+ rerenderVirtualSidebar();
8101
8482
  }
8102
8483
  function markActive(path, options = {}) {
8103
8484
  STATE.activeFile = path;
8485
+ SIDEBAR_VIRTUAL_ACTIVE_PATH = path;
8104
8486
  if (options.reveal && STATE.sbView === "tree")
8105
8487
  expandSidebarAncestors(path);
8106
- $$("#filelist li").forEach((li) => {
8107
- const itemPath = li.dataset.path || li.dataset.dirpath;
8108
- if (itemPath)
8109
- li.classList.toggle("active", itemPath === path);
8110
- });
8488
+ setActiveSidebarItem(sidebarItemByPath(path));
8489
+ if ($("#filelist").classList.contains("tree-virtual")) {
8490
+ renderVirtualSidebarWindow();
8491
+ scrollVirtualSidebarPathIntoView(path);
8492
+ return;
8493
+ }
8111
8494
  if (options.reveal) {
8112
- const active = document.querySelector("#filelist li.active[data-path], #filelist .tree-dir.active[data-dirpath]");
8495
+ const active = activeSidebarItem();
8113
8496
  if (active)
8114
8497
  requestAnimationFrame(() => scrollSidebarItemIntoView(active));
8115
8498
  }
8116
8499
  }
8117
8500
  function applyViewedState() {
8501
+ if (isRepositorySidebarMode())
8502
+ return;
8118
8503
  $$("#filelist li[data-path]").forEach((li) => {
8119
8504
  const path = li.dataset.path || "";
8120
- li.classList.toggle("viewed", !isRepositorySidebarMode() && STATE.viewedFiles.has(path));
8505
+ li.classList.toggle("viewed", STATE.viewedFiles.has(path));
8121
8506
  });
8122
- if (isRepositorySidebarMode())
8123
- return;
8124
8507
  $$(".gdp-file-shell[data-path]").forEach((card) => {
8125
8508
  const path = card.dataset.path || "";
8126
8509
  const viewed = STATE.viewedFiles.has(path);
@@ -8129,11 +8512,16 @@ ${frontmatter.yaml}
8129
8512
  }
8130
8513
  function applyFilter() {
8131
8514
  const input = $("#sb-filter");
8515
+ if ($("#filelist").classList.contains("tree-virtual")) {
8516
+ rerenderVirtualSidebar();
8517
+ return;
8518
+ }
8132
8519
  const filter = compileFileFilter(input.value);
8133
8520
  const invalid = filter.kind === "invalid";
8134
8521
  input.toggleAttribute("aria-invalid", invalid);
8135
8522
  input.title = invalid ? filter.error || "invalid regular expression" : "";
8136
8523
  const matches = invalid ? () => true : filter.match;
8524
+ const filterActive = filter.kind !== "empty" && !invalid;
8137
8525
  $$("#filelist li[data-path]").forEach((li) => {
8138
8526
  const match2 = matches(li.dataset.path || "");
8139
8527
  li.classList.toggle("hidden", !match2);
@@ -8144,21 +8532,51 @@ ${frontmatter.yaml}
8144
8532
  card.classList.toggle("hidden-by-filter", !match2);
8145
8533
  });
8146
8534
  }
8147
- updateTreeDirVisibility(matches, filter.kind !== "empty" && !invalid);
8148
- if (typeof applyViewedState === "function")
8535
+ updateTreeDirVisibility(matches, filterActive);
8536
+ if (!isRepositorySidebarMode() && typeof applyViewedState === "function")
8149
8537
  applyViewedState();
8150
8538
  }
8151
8539
  function updateTreeDirVisibility(dirMatches, filterActive = false) {
8152
- $$("#filelist .tree-dir").forEach((dir) => {
8540
+ const dirs = $$("#filelist .tree-dir");
8541
+ for (let i2 = dirs.length - 1;i2 >= 0; i2--) {
8542
+ const dir = dirs[i2];
8153
8543
  const childUl = dir.nextElementSibling;
8154
8544
  if (!childUl?.classList.contains("tree-children"))
8155
- return;
8156
- const anyVisible = !!childUl.querySelector(".tree-file:not(.hidden):not(.hidden-by-tests)");
8545
+ continue;
8546
+ let anyVisible = false;
8547
+ for (const child of childUl.children) {
8548
+ if (!(child instanceof HTMLElement))
8549
+ continue;
8550
+ if (child.classList.contains("tree-file") && !child.classList.contains("hidden") && !child.classList.contains("hidden-by-tests")) {
8551
+ anyVisible = true;
8552
+ break;
8553
+ }
8554
+ if (child.classList.contains("tree-dir") && !child.classList.contains("hidden") && !child.classList.contains("hidden-by-tests")) {
8555
+ anyVisible = true;
8556
+ break;
8557
+ }
8558
+ }
8157
8559
  const explicitVisible = dir.dataset.explicit === "true" && !filterActive;
8158
8560
  const selfMatches = filterActive && !!dirMatches && dirMatches(dir.dataset.dirpath || "");
8159
8561
  dir.classList.toggle("hidden", !anyVisible && !explicitVisible && !selfMatches);
8562
+ }
8563
+ }
8564
+ let SIDEBAR_FILTER_RAF = 0;
8565
+ function scheduleApplyFilter() {
8566
+ if (SIDEBAR_FILTER_RAF)
8567
+ cancelAnimationFrame(SIDEBAR_FILTER_RAF);
8568
+ SIDEBAR_FILTER_RAF = requestAnimationFrame(() => {
8569
+ SIDEBAR_FILTER_RAF = 0;
8570
+ applyFilter();
8160
8571
  });
8161
8572
  }
8573
+ function flushSidebarFilter() {
8574
+ if (!SIDEBAR_FILTER_RAF)
8575
+ return;
8576
+ cancelAnimationFrame(SIDEBAR_FILTER_RAF);
8577
+ SIDEBAR_FILTER_RAF = 0;
8578
+ applyFilter();
8579
+ }
8162
8580
  let SERVER_GENERATION = 0;
8163
8581
  let CLIENT_REQ_SEQ = 0;
8164
8582
  const LOAD_QUEUE = [];
@@ -11705,8 +12123,97 @@ ${frontmatter.yaml}
11705
12123
  }
11706
12124
  return true;
11707
12125
  }
12126
+ const SIDEBAR_ITEM_SELECTOR = "#filelist li[data-path], #filelist .tree-dir[data-dirpath]";
12127
+ const ACTIVE_SIDEBAR_ITEM_SELECTOR = "#filelist li.active[data-path], #filelist .tree-dir.active[data-dirpath]";
12128
+ function sidebarItemPath(item) {
12129
+ return item.dataset.path || item.dataset.dirpath || "";
12130
+ }
12131
+ function activeSidebarItem() {
12132
+ return document.querySelector(ACTIVE_SIDEBAR_ITEM_SELECTOR);
12133
+ }
12134
+ function sidebarItemByPath(path) {
12135
+ if (isVirtualSidebarActive() && SIDEBAR_ROW_BY_PATH.has(path)) {
12136
+ return document.querySelector(`#filelist li[data-path="${CSS.escape(path)}"], #filelist .tree-dir[data-dirpath="${CSS.escape(path)}"]`) || null;
12137
+ }
12138
+ const escaped = CSS.escape(path);
12139
+ return document.querySelector(`#filelist li[data-path="${escaped}"], #filelist .tree-dir[data-dirpath="${escaped}"]`);
12140
+ }
12141
+ function setActiveSidebarItem(target) {
12142
+ document.querySelectorAll(ACTIVE_SIDEBAR_ITEM_SELECTOR).forEach((item) => {
12143
+ if (item !== target)
12144
+ item.classList.remove("active");
12145
+ });
12146
+ target?.classList.add("active");
12147
+ }
11708
12148
  function visibleSidebarItems() {
11709
- return $$("#filelist li[data-path], #filelist .tree-dir[data-dirpath]").filter(isSidebarRowVisible);
12149
+ return $$(SIDEBAR_ITEM_SELECTOR).filter(isSidebarRowVisible);
12150
+ }
12151
+ function isVirtualSidebarActive() {
12152
+ return $("#filelist").classList.contains("tree-virtual");
12153
+ }
12154
+ function virtualSidebarActiveIndex() {
12155
+ const activePath = SIDEBAR_VIRTUAL_ACTIVE_PATH || STATE.activeFile || "";
12156
+ return SIDEBAR_VISIBLE_ROWS.findIndex((row) => row.path === activePath);
12157
+ }
12158
+ function selectVirtualSidebarIndex(index, options) {
12159
+ if (!SIDEBAR_VISIBLE_ROWS.length)
12160
+ return null;
12161
+ const safeIndex = Math.max(0, Math.min(SIDEBAR_VISIBLE_ROWS.length - 1, index));
12162
+ const row = SIDEBAR_VISIBLE_ROWS[safeIndex];
12163
+ if (!row)
12164
+ return null;
12165
+ markActive(row.path);
12166
+ scrollVirtualSidebarPathIntoView(row.path);
12167
+ if (options?.open) {
12168
+ if (row.kind === "dir" && row.dir && SIDEBAR_ON_FILE_CLICK) {
12169
+ SIDEBAR_ON_FILE_CLICK({
12170
+ path: row.dir.path,
12171
+ display_path: row.dir.path,
12172
+ type: "tree",
12173
+ children_omitted: row.dir.children_omitted,
12174
+ children_omitted_reason: row.dir.children_omitted_reason
12175
+ });
12176
+ } else if (row.file && SIDEBAR_ON_FILE_CLICK) {
12177
+ SIDEBAR_ON_FILE_CLICK(row.file);
12178
+ }
12179
+ }
12180
+ return row;
12181
+ }
12182
+ function visibleSidebarItemFrom(current, direction) {
12183
+ const root = document.querySelector("#filelist");
12184
+ if (!current.isConnected)
12185
+ return null;
12186
+ if (!root)
12187
+ return null;
12188
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
12189
+ acceptNode(node) {
12190
+ if (!(node instanceof HTMLElement))
12191
+ return NodeFilter.FILTER_SKIP;
12192
+ if (node.classList.contains("tree-children")) {
12193
+ const dir = node.previousElementSibling;
12194
+ if (dir?.classList.contains("collapsed") || dir?.classList.contains("hidden") || dir?.classList.contains("hidden-by-tests"))
12195
+ return NodeFilter.FILTER_REJECT;
12196
+ }
12197
+ if (!node.matches(SIDEBAR_ITEM_SELECTOR))
12198
+ return NodeFilter.FILTER_SKIP;
12199
+ return isSidebarRowVisible(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
12200
+ }
12201
+ });
12202
+ walker.currentNode = current;
12203
+ const next = direction === 1 ? walker.nextNode() : walker.previousNode();
12204
+ return next instanceof HTMLElement ? next : null;
12205
+ }
12206
+ function adjacentVisibleSidebarItem(direction) {
12207
+ const active = activeSidebarItem();
12208
+ if (!active) {
12209
+ const items = visibleSidebarItems();
12210
+ return direction === 1 ? items[0] || null : items[items.length - 1] || null;
12211
+ }
12212
+ if (!isSidebarRowVisible(active)) {
12213
+ const items = visibleSidebarItems();
12214
+ return direction === 1 ? items[0] || null : items[items.length - 1] || null;
12215
+ }
12216
+ return visibleSidebarItemFrom(active, direction) || active;
11710
12217
  }
11711
12218
  function scrollSidebarItemIntoView(item, block2 = "nearest") {
11712
12219
  const sidebar = document.querySelector("#sidebar");
@@ -11738,6 +12245,14 @@ ${frontmatter.yaml}
11738
12245
  return document.body.classList.contains("gdp-repo-page") || document.body.classList.contains("gdp-repo-blob-page");
11739
12246
  }
11740
12247
  function moveActiveSidebarItem(direction) {
12248
+ if (isVirtualSidebarActive()) {
12249
+ const current2 = virtualSidebarActiveIndex();
12250
+ const start = current2 < 0 ? direction === 1 ? 0 : SIDEBAR_VISIBLE_ROWS.length - 1 : current2 + direction;
12251
+ const row = selectVirtualSidebarIndex(start);
12252
+ if (row?.file)
12253
+ prefetchByPath(row.file.path);
12254
+ return;
12255
+ }
11741
12256
  const items = visibleSidebarItems();
11742
12257
  if (!items.length)
11743
12258
  return;
@@ -11754,6 +12269,16 @@ ${frontmatter.yaml}
11754
12269
  prefetchByPath(target.dataset.path);
11755
12270
  }
11756
12271
  function moveActiveSidebarPage(direction) {
12272
+ if (isVirtualSidebarActive()) {
12273
+ const sidebar2 = document.querySelector("#sidebar");
12274
+ const halfPageRows2 = Math.max(1, Math.floor((sidebar2?.clientHeight || window.innerHeight) / 2 / VIRTUAL_SIDEBAR_ROW_HEIGHT));
12275
+ const current2 = virtualSidebarActiveIndex();
12276
+ const start2 = current2 < 0 ? 0 : current2;
12277
+ const row = selectVirtualSidebarIndex(start2 + direction * halfPageRows2);
12278
+ if (row?.file)
12279
+ prefetchByPath(row.file.path);
12280
+ return;
12281
+ }
11757
12282
  const items = visibleSidebarItems();
11758
12283
  if (!items.length)
11759
12284
  return;
@@ -11776,6 +12301,12 @@ ${frontmatter.yaml}
11776
12301
  prefetchByPath(target.dataset.path);
11777
12302
  }
11778
12303
  function moveActiveSidebarToEdge(edge) {
12304
+ if (isVirtualSidebarActive()) {
12305
+ const row = selectVirtualSidebarIndex(edge === "top" ? 0 : SIDEBAR_VISIBLE_ROWS.length - 1);
12306
+ if (row?.file)
12307
+ prefetchByPath(row.file.path);
12308
+ return;
12309
+ }
11779
12310
  const items = visibleSidebarItems();
11780
12311
  const repoSidebar = isRepositorySidebarMode();
11781
12312
  const target = edge === "top" ? items[0] : items[items.length - 1];
@@ -11791,6 +12322,21 @@ ${frontmatter.yaml}
11791
12322
  prefetchByPath(target.dataset.path);
11792
12323
  }
11793
12324
  function setActiveSidebarDirectoryCollapsed(collapsed) {
12325
+ if (isVirtualSidebarActive()) {
12326
+ const row = SIDEBAR_VISIBLE_ROWS[virtualSidebarActiveIndex()];
12327
+ if (row?.kind !== "dir" || !row.dir || row.dir.children_omitted)
12328
+ return;
12329
+ if (STATE.collapsedDirs.has(row.path) === collapsed)
12330
+ return;
12331
+ if (collapsed)
12332
+ STATE.collapsedDirs.add(row.path);
12333
+ else
12334
+ STATE.collapsedDirs.delete(row.path);
12335
+ localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
12336
+ rerenderVirtualSidebar();
12337
+ scrollVirtualSidebarPathIntoView(row.path);
12338
+ return;
12339
+ }
11794
12340
  const active = document.querySelector("#filelist .tree-dir.active[data-dirpath]");
11795
12341
  if (!active)
11796
12342
  return;
@@ -11801,6 +12347,13 @@ ${frontmatter.yaml}
11801
12347
  control.click();
11802
12348
  }
11803
12349
  function toggleActiveSidebarDirectoryCollapsed() {
12350
+ if (isVirtualSidebarActive()) {
12351
+ const row = SIDEBAR_VISIBLE_ROWS[virtualSidebarActiveIndex()];
12352
+ if (row?.kind !== "dir" || !row.dir || row.dir.children_omitted)
12353
+ return;
12354
+ setActiveSidebarDirectoryCollapsed(!STATE.collapsedDirs.has(row.path));
12355
+ return;
12356
+ }
11804
12357
  const active = document.querySelector("#filelist .tree-dir.active[data-dirpath]");
11805
12358
  if (!active)
11806
12359
  return;
@@ -11809,11 +12362,23 @@ ${frontmatter.yaml}
11809
12362
  control.click();
11810
12363
  }
11811
12364
  function openActiveSidebarItem() {
12365
+ if (isVirtualSidebarActive()) {
12366
+ const index = virtualSidebarActiveIndex();
12367
+ if (index >= 0)
12368
+ selectVirtualSidebarIndex(index, { open: true });
12369
+ return;
12370
+ }
11812
12371
  const active = document.querySelector("#filelist li.active[data-path], #filelist .tree-dir.active[data-dirpath]");
11813
12372
  if (active && isSidebarRowVisible(active))
11814
12373
  active.click();
11815
12374
  }
11816
12375
  function jumpToActiveOrFirstFilteredItem() {
12376
+ if (isVirtualSidebarActive()) {
12377
+ const current = virtualSidebarActiveIndex();
12378
+ selectVirtualSidebarIndex(current >= 0 ? current : 0, { open: true });
12379
+ $("#sb-filter").blur();
12380
+ return;
12381
+ }
11817
12382
  const items = visibleSidebarItems();
11818
12383
  const active = items.find((li) => li.classList.contains("active"));
11819
12384
  const target = active || items[0];
@@ -11824,17 +12389,20 @@ ${frontmatter.yaml}
11824
12389
  }
11825
12390
  const sbFilter = $("#sb-filter");
11826
12391
  if (sbFilter) {
11827
- sbFilter.addEventListener("input", () => applyFilter());
12392
+ sbFilter.addEventListener("input", () => scheduleApplyFilter());
11828
12393
  sbFilter.addEventListener("keydown", (e2) => {
11829
12394
  if (e2.key === "Enter") {
11830
12395
  e2.preventDefault();
12396
+ flushSidebarFilter();
11831
12397
  jumpToActiveOrFirstFilteredItem();
11832
12398
  } else if (e2.key === "ArrowDown" || e2.key === "ArrowUp") {
11833
12399
  e2.preventDefault();
12400
+ flushSidebarFilter();
11834
12401
  moveActiveSidebarItem(e2.key === "ArrowDown" ? 1 : -1);
11835
12402
  } else if (e2.key === "Escape") {
11836
12403
  if (sbFilter.value) {
11837
12404
  sbFilter.value = "";
12405
+ flushSidebarFilter();
11838
12406
  applyFilter();
11839
12407
  } else {
11840
12408
  sbFilter.blur();
@@ -12322,16 +12890,24 @@ ${frontmatter.yaml}
12322
12890
  }
12323
12891
  if (action === "sidebar-next" || action === "sidebar-previous") {
12324
12892
  const repoSidebar = isRepositorySidebarMode();
12325
- const items = repoSidebar ? visibleSidebarItems() : $$("#filelist li[data-path]:not(.hidden):not(.hidden-by-tests)");
12326
- if (!items.length)
12893
+ const direction = action === "sidebar-next" ? 1 : -1;
12894
+ const diffItems = repoSidebar ? [] : $$("#filelist li[data-path]:not(.hidden):not(.hidden-by-tests)");
12895
+ let diffIndex = diffItems.findIndex((li) => li.classList.contains("active"));
12896
+ if (!repoSidebar)
12897
+ diffIndex = diffIndex < 0 ? 0 : Math.max(0, Math.min(diffItems.length - 1, diffIndex + direction));
12898
+ const target = repoSidebar ? isVirtualSidebarActive() ? null : adjacentVisibleSidebarItem(direction) : diffItems[diffIndex];
12899
+ if (repoSidebar && isVirtualSidebarActive()) {
12900
+ const current = virtualSidebarActiveIndex();
12901
+ const start = current < 0 ? direction === 1 ? 0 : SIDEBAR_VISIBLE_ROWS.length - 1 : current + direction;
12902
+ const row = selectVirtualSidebarIndex(start);
12903
+ const next = row ? SIDEBAR_VISIBLE_ROWS[Math.max(0, Math.min(SIDEBAR_VISIBLE_ROWS.length - 1, SIDEBAR_VISIBLE_ROWS.indexOf(row) + direction))] : null;
12904
+ if (!repeated && next?.file)
12905
+ prefetchByPath(next.file.path);
12327
12906
  return true;
12328
- let idx = items.findIndex((li) => li.classList.contains("active"));
12329
- if (idx < 0)
12330
- idx = 0;
12331
- else
12332
- idx = action === "sidebar-next" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
12333
- const target = items[idx];
12334
- const path = target?.dataset.path || target?.dataset.dirpath;
12907
+ }
12908
+ if (!target)
12909
+ return true;
12910
+ const path = sidebarItemPath(target);
12335
12911
  if (!repoSidebar && target) {
12336
12912
  target.click();
12337
12913
  scrollSidebarItemIntoView(target);
@@ -12339,9 +12915,8 @@ ${frontmatter.yaml}
12339
12915
  markActive(path);
12340
12916
  scrollSidebarItemIntoView(target);
12341
12917
  }
12342
- const nextIdx = action === "sidebar-next" ? Math.min(items.length - 1, idx + 1) : Math.max(0, idx - 1);
12343
- const nextItem = items[nextIdx];
12344
- if (nextItem && nextItem !== target && nextItem.dataset.path)
12918
+ const nextItem = repoSidebar ? visibleSidebarItemFrom(target, direction) : diffItems[Math.max(0, Math.min(diffItems.length - 1, diffIndex + direction))];
12919
+ if (!repeated && nextItem && nextItem !== target && nextItem.dataset.path)
12345
12920
  prefetchByPath(nextItem.dataset.path);
12346
12921
  return true;
12347
12922
  }
@@ -12846,7 +13421,6 @@ ${frontmatter.yaml}
12846
13421
  if (e2.key === "gdp:syntax-highlight")
12847
13422
  setSyntaxHighlight(e2.newValue !== "0");
12848
13423
  });
12849
- const TEST_RE = /(^|[/_.])(test|spec|__tests__)([/_.]|$)/i;
12850
13424
  function applyHideTests() {
12851
13425
  const btn = $("#hide-tests");
12852
13426
  if (btn)
@@ -12859,7 +13433,10 @@ ${frontmatter.yaml}
12859
13433
  const isTest = TEST_RE.test(li.dataset.path || "");
12860
13434
  li.classList.toggle("hidden-by-tests", STATE.hideTests && isTest);
12861
13435
  });
12862
- updateTreeDirVisibility();
13436
+ if (isVirtualSidebarActive())
13437
+ rerenderVirtualSidebar();
13438
+ else
13439
+ updateTreeDirVisibility();
12863
13440
  if (typeof applyViewedState === "function")
12864
13441
  applyViewedState();
12865
13442
  }
package/web/style.css CHANGED
@@ -1352,6 +1352,16 @@ body.gdp-help-page #content {
1352
1352
  #filelist.tree {
1353
1353
  padding: 4px 0 0;
1354
1354
  }
1355
+ #filelist.tree.tree-virtual {
1356
+ padding: 0;
1357
+ }
1358
+ #filelist.tree.tree-virtual .tree-dir,
1359
+ #filelist.tree.tree-virtual .tree-file {
1360
+ height: 29px;
1361
+ min-height: 29px;
1362
+ overflow: hidden;
1363
+ box-sizing: border-box;
1364
+ }
1355
1365
  #filelist.tree .tree-dir {
1356
1366
  display: grid;
1357
1367
  grid-template-columns: 16px 16px minmax(0, 1fr) 22px;
@@ -2590,49 +2600,53 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2590
2600
  }
2591
2601
  .gdp-html-preview {
2592
2602
  width: 100%;
2593
- min-height: min(78vh, 920px);
2603
+ min-height: calc(100vh - var(--global-header-h) - 118px);
2594
2604
  background: var(--bg);
2595
2605
  }
2596
2606
  .gdp-html-preview iframe {
2597
2607
  display: block;
2598
2608
  width: 100%;
2599
- min-height: min(78vh, 920px);
2609
+ min-height: calc(100vh - var(--global-header-h) - 118px);
2600
2610
  border: 0;
2601
2611
  background: #fff;
2602
2612
  }
2603
2613
  .gdp-markdown-layout {
2604
2614
  display: grid;
2605
- grid-template-columns: 260px minmax(0, 1fr);
2606
- gap: 32px;
2615
+ grid-template-columns: 280px minmax(0, 1fr);
2616
+ gap: 28px;
2607
2617
  align-items: start;
2608
2618
  }
2609
- .gdp-standalone-source .gdp-markdown-layout {
2610
- grid-template-columns: minmax(0, 1fr);
2611
- }
2612
- .gdp-standalone-source .gdp-markdown-toc {
2613
- display: none;
2614
- }
2615
2619
  .gdp-markdown-toc {
2616
2620
  position: sticky;
2617
- top: 96px;
2618
- max-height: calc(100vh - 120px);
2621
+ top: calc(var(--global-header-h) + 16px);
2622
+ max-height: calc(100vh - var(--global-header-h) - 40px);
2619
2623
  overflow: auto;
2620
- padding: 18px 14px;
2621
- border-right: 1px solid var(--border);
2622
- background: var(--bg-soft);
2624
+ padding: 8px 12px 10px 0;
2625
+ border-right: 1px solid var(--border-muted);
2626
+ background: transparent;
2623
2627
  font-size: 13px;
2624
2628
  line-height: 1.45;
2629
+ scrollbar-gutter: stable;
2630
+ scrollbar-width: thin;
2631
+ scrollbar-color: var(--border) transparent;
2632
+ }
2633
+ .gdp-markdown-toc::-webkit-scrollbar {
2634
+ width: 8px;
2635
+ }
2636
+ .gdp-markdown-toc::-webkit-scrollbar-thumb {
2637
+ border-radius: 4px;
2638
+ background: var(--border);
2625
2639
  }
2626
2640
  .gdp-markdown-toc::before {
2627
- content: "Contents";
2641
+ content: "On this page";
2628
2642
  display: block;
2629
- margin: 0 8px 12px;
2630
- padding-bottom: 10px;
2643
+ margin: 0 0 10px 2px;
2644
+ padding: 0 8px 10px;
2631
2645
  border-bottom: 1px solid var(--border-muted);
2632
2646
  color: var(--fg-muted);
2633
2647
  font-size: 11px;
2634
- font-weight: 700;
2635
- letter-spacing: 0.12em;
2648
+ font-weight: 600;
2649
+ letter-spacing: 0.06em;
2636
2650
  text-transform: uppercase;
2637
2651
  }
2638
2652
  .gdp-markdown-toc ul {
@@ -2644,33 +2658,47 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2644
2658
  margin: 0;
2645
2659
  }
2646
2660
  .gdp-markdown-toc a {
2647
- display: block;
2648
- padding: 5px 8px;
2649
- border-left: 2px solid transparent;
2661
+ display: -webkit-box;
2662
+ min-height: 28px;
2663
+ padding: 5px 10px 5px 12px;
2664
+ border-left: 3px solid transparent;
2650
2665
  border-radius: 5px;
2651
2666
  color: var(--fg-muted);
2667
+ overflow: hidden;
2652
2668
  overflow-wrap: anywhere;
2653
2669
  text-decoration: none;
2670
+ -webkit-box-orient: vertical;
2671
+ -webkit-line-clamp: 2;
2672
+ transition:
2673
+ background-color 0.06s ease,
2674
+ border-color 0.06s ease,
2675
+ color 0.06s ease;
2654
2676
  }
2655
2677
  .gdp-markdown-toc .level-1 > a {
2656
2678
  color: var(--fg);
2657
2679
  font-weight: 600;
2658
2680
  }
2659
2681
  .gdp-markdown-toc .level-2 > a {
2660
- padding-left: 18px;
2682
+ padding-left: 24px;
2661
2683
  }
2662
2684
  .gdp-markdown-toc .level-3 > a {
2663
- padding-left: 30px;
2664
- font-size: 12px;
2685
+ padding-left: 38px;
2686
+ }
2687
+ .gdp-markdown-toc .level-4 > a {
2688
+ padding-left: 52px;
2665
2689
  }
2666
2690
  .gdp-markdown-toc a:hover {
2667
2691
  background: var(--bg-mute);
2668
2692
  color: var(--fg);
2669
2693
  }
2694
+ .gdp-markdown-toc a:focus-visible {
2695
+ outline: 2px solid var(--accent);
2696
+ outline-offset: -2px;
2697
+ }
2670
2698
  .gdp-markdown-toc a.active {
2671
2699
  background: var(--accent-subtle);
2672
2700
  border-left-color: var(--accent);
2673
- color: var(--accent-emph);
2701
+ color: var(--fg);
2674
2702
  font-weight: 600;
2675
2703
  }
2676
2704
  .gdp-markdown-preview h1,
@@ -2958,26 +2986,33 @@ body.gdp-file-detail-page.gdp-repo-blob-page #sidebar-resizer {
2958
2986
  body.gdp-repo-page #sidebar-resizer {
2959
2987
  display: block;
2960
2988
  }
2961
- body.gdp-file-detail-page.gdp-repo-blob-page .sb-title,
2962
- body.gdp-file-detail-page.gdp-repo-blob-page #totals,
2963
- body.gdp-repo-page .sb-title,
2964
- body.gdp-repo-page #totals {
2965
- display: none;
2966
- }
2967
2989
  body.gdp-repo-page .sb-head,
2968
2990
  body.gdp-file-detail-page.gdp-repo-blob-page .sb-head {
2969
2991
  display: grid;
2970
2992
  grid-template:
2971
- "toggle ref actions view" auto
2972
- / 28px minmax(80px, 240px) auto auto;
2973
- justify-content: start;
2993
+ "toggle title totals ." auto
2994
+ "ref ref ref ref" auto
2995
+ "actions actions . view" auto
2996
+ "filter filter filter filter" auto
2997
+ / 28px auto minmax(0, 1fr) auto;
2998
+ justify-content: stretch;
2974
2999
  align-items: center;
2975
3000
  gap: 8px;
3001
+ top: var(--global-header-h);
3002
+ z-index: 5;
2976
3003
  }
2977
3004
  body.gdp-repo-page .sb-head > #sidebar-toggle,
2978
3005
  body.gdp-file-detail-page.gdp-repo-blob-page .sb-head > #sidebar-toggle {
2979
3006
  grid-area: toggle;
2980
3007
  }
3008
+ body.gdp-repo-page .sb-title,
3009
+ body.gdp-file-detail-page.gdp-repo-blob-page .sb-title {
3010
+ grid-area: title;
3011
+ }
3012
+ body.gdp-repo-page #totals,
3013
+ body.gdp-file-detail-page.gdp-repo-blob-page #totals {
3014
+ grid-area: totals;
3015
+ }
2981
3016
  body.gdp-repo-page #repo-target-wrap,
2982
3017
  body.gdp-file-detail-page.gdp-repo-blob-page #repo-target-wrap {
2983
3018
  grid-area: ref;
@@ -2986,10 +3021,22 @@ body.gdp-file-detail-page.gdp-repo-blob-page #repo-target-wrap {
2986
3021
  body.gdp-repo-page .sb-actions,
2987
3022
  body.gdp-file-detail-page.gdp-repo-blob-page .sb-actions {
2988
3023
  grid-area: actions;
3024
+ margin-left: 0;
3025
+ justify-self: start;
2989
3026
  }
2990
3027
  body.gdp-repo-page .sb-view-seg,
2991
3028
  body.gdp-file-detail-page.gdp-repo-blob-page .sb-view-seg {
2992
3029
  grid-area: view;
3030
+ margin-left: auto;
3031
+ }
3032
+ body.gdp-repo-page .sb-filter-wrap,
3033
+ body.gdp-file-detail-page.gdp-repo-blob-page .sb-filter-wrap {
3034
+ grid-area: filter;
3035
+ position: static;
3036
+ width: 100%;
3037
+ margin: 0;
3038
+ padding: 6px 0 0;
3039
+ border-bottom: 0;
2993
3040
  }
2994
3041
  body.gdp-file-detail-page {
2995
3042
  --chrome-h: var(--global-header-h);