@youtyan/code-viewer 0.1.21 → 0.1.22

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 +620 -44
  3. package/web/style.css +38 -9
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.22",
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());
@@ -6884,7 +6921,11 @@ ${frontmatter.yaml}
6884
6921
  const VIRTUAL_SOURCE_SIZE_THRESHOLD = 1024 * 1024;
6885
6922
  const VIRTUAL_SOURCE_PAGE_SIZE = 2000;
6886
6923
  const VIRTUAL_SOURCE_ROW_HEIGHT = 20;
6924
+ const VIRTUAL_SIDEBAR_THRESHOLD = 3000;
6925
+ const VIRTUAL_SIDEBAR_ROW_HEIGHT = 29;
6926
+ const VIRTUAL_SIDEBAR_OVERSCAN = 16;
6887
6927
  const VIRTUAL_SOURCE_HIGHLIGHT_MAX_LINE_LENGTH = 2000;
6928
+ const TEST_RE = /(^|[/_.])(test|spec|__tests__)([/_.]|$)/i;
6888
6929
  let highlightLoadPromise = null;
6889
6930
  let sourceShikiLoadPromise = null;
6890
6931
  let highlightConfigured = false;
@@ -6894,6 +6935,12 @@ ${frontmatter.yaml}
6894
6935
  let REPO_SIDEBAR_LOAD = null;
6895
6936
  let SIDEBAR_FILES = [];
6896
6937
  let SIDEBAR_ON_FILE_CLICK;
6938
+ let SIDEBAR_TREE_ROOT = null;
6939
+ let SIDEBAR_TREE_ROWS = [];
6940
+ let SIDEBAR_VISIBLE_ROWS = [];
6941
+ let SIDEBAR_ROW_BY_PATH = new Map;
6942
+ let SIDEBAR_VIRTUAL_ACTIVE_PATH = "";
6943
+ const SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
6897
6944
  let SERVER_SCOPE_OMIT_DIRS_DEFAULT = [];
6898
6945
  let PENDING_G_SCOPE = null;
6899
6946
  let PENDING_G_UNTIL = 0;
@@ -7584,6 +7631,22 @@ ${frontmatter.yaml}
7584
7631
  attachSidebarToggle(restoreHost);
7585
7632
  else if (sidebarHead)
7586
7633
  attachSidebarToggle(sidebarHead);
7634
+ placeSidebarFilter();
7635
+ }
7636
+ function placeSidebarFilter() {
7637
+ const sidebarHead = document.querySelector(".sb-head");
7638
+ const filter = document.querySelector(".sb-filter-wrap");
7639
+ const list2 = document.querySelector("#filelist");
7640
+ if (!sidebarHead || !filter || !list2)
7641
+ return;
7642
+ const repoSidebar = isRepositorySidebarMode();
7643
+ if (repoSidebar && filter.parentElement !== sidebarHead) {
7644
+ sidebarHead.appendChild(filter);
7645
+ return;
7646
+ }
7647
+ if (!repoSidebar && filter.parentElement === sidebarHead) {
7648
+ sidebarHead.after(filter);
7649
+ }
7587
7650
  }
7588
7651
  function applySidebarHidden(hidden = STATE.sidebarHidden) {
7589
7652
  STATE.sidebarHidden = hidden;
@@ -7859,6 +7922,301 @@ ${frontmatter.yaml}
7859
7922
  }
7860
7923
  }
7861
7924
  }
7925
+ function treeNodeItems(node) {
7926
+ const cached = SIDEBAR_TREE_ITEMS_CACHE.get(node);
7927
+ if (cached)
7928
+ return cached;
7929
+ const items = [];
7930
+ for (const k of Object.keys(node.dirs)) {
7931
+ const d2 = node.dirs[k];
7932
+ items.push({ kind: "dir", sortKey: d2.minOrder, dir: d2 });
7933
+ }
7934
+ for (const f2 of node.files) {
7935
+ items.push({
7936
+ kind: "file",
7937
+ sortKey: f2.order != null ? f2.order : Infinity,
7938
+ file: f2
7939
+ });
7940
+ }
7941
+ items.sort((a2, b2) => a2.sortKey - b2.sortKey);
7942
+ SIDEBAR_TREE_ITEMS_CACHE.set(node, items);
7943
+ return items;
7944
+ }
7945
+ function createTreeDirRow(dir, depth, onFileClick) {
7946
+ const li = document.createElement("li");
7947
+ li.className = "tree-dir";
7948
+ li.tabIndex = -1;
7949
+ li.dataset.dirpath = dir.path;
7950
+ if (dir.explicit)
7951
+ li.dataset.explicit = "true";
7952
+ if (dir.children_omitted) {
7953
+ li.classList.add("children-omitted");
7954
+ li.classList.add(dir.children_omitted_reason === "heavy" ? "children-omitted-heavy" : "children-omitted-internal");
7955
+ 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";
7956
+ }
7957
+ li.style.setProperty("--lvl-pad", `${12 + depth * 14}px`);
7958
+ const chev = document.createElement("span");
7959
+ if (dir.children_omitted) {
7960
+ chev.className = "chev-spacer";
7961
+ chev.setAttribute("aria-hidden", "true");
7962
+ } else {
7963
+ chev.className = "chev";
7964
+ setChevronIcon(chev);
7965
+ }
7966
+ li.appendChild(chev);
7967
+ const dirIcon = document.createElement("span");
7968
+ dirIcon.className = "dir-icon";
7969
+ li.appendChild(dirIcon);
7970
+ const label = document.createElement("span");
7971
+ label.className = "dir-label";
7972
+ const dn = document.createElement("span");
7973
+ dn.className = "dir-name";
7974
+ dn.textContent = dir.name;
7975
+ dn.title = dir.path;
7976
+ label.appendChild(dn);
7977
+ if (dir.children_omitted) {
7978
+ const omitted = document.createElement("span");
7979
+ omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
7980
+ omitted.textContent = dir.children_omitted_reason === "heavy" ? "skipped" : "private";
7981
+ 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";
7982
+ label.appendChild(omitted);
7983
+ }
7984
+ li.appendChild(label);
7985
+ li.appendChild(createOpenPathButton(dir.path, "directory", "open this folder in OS"));
7986
+ const updateIcon = () => {
7987
+ setFolderIcon(dirIcon, li.classList.contains("collapsed"));
7988
+ };
7989
+ const toggleDir = (e2) => {
7990
+ e2.stopPropagation();
7991
+ li.classList.toggle("collapsed");
7992
+ updateIcon();
7993
+ if (li.classList.contains("collapsed"))
7994
+ STATE.collapsedDirs.add(dir.path);
7995
+ else
7996
+ STATE.collapsedDirs.delete(dir.path);
7997
+ localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
7998
+ rerenderVirtualSidebar();
7999
+ };
8000
+ li.classList.toggle("collapsed", STATE.collapsedDirs.has(dir.path));
8001
+ updateIcon();
8002
+ if (!dir.children_omitted) {
8003
+ chev.addEventListener("click", toggleDir);
8004
+ dirIcon.addEventListener("click", toggleDir);
8005
+ }
8006
+ if (onFileClick) {
8007
+ li.addEventListener("click", (e2) => {
8008
+ e2.stopPropagation();
8009
+ if (dir.children_omitted_reason === "internal" || dir.children_omitted_reason === "truncated")
8010
+ return;
8011
+ onFileClick({
8012
+ path: dir.path,
8013
+ display_path: dir.path,
8014
+ type: "tree",
8015
+ children_omitted: dir.children_omitted,
8016
+ children_omitted_reason: dir.children_omitted_reason
8017
+ });
8018
+ scheduleMainSurfaceFocus();
8019
+ });
8020
+ } else {
8021
+ li.addEventListener("click", toggleDir);
8022
+ }
8023
+ return li;
8024
+ }
8025
+ function createTreeFileRow(f2, depth, onFileClick) {
8026
+ const li = document.createElement("li");
8027
+ li.className = "tree-file";
8028
+ li.tabIndex = -1;
8029
+ li.dataset.path = f2.path;
8030
+ li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
8031
+ li.classList.toggle("hidden-by-tests", STATE.hideTests && TEST_RE.test(f2.path || ""));
8032
+ li.style.setProperty("--lvl-pad", `${12 + depth * 14}px`);
8033
+ const spacer = document.createElement("span");
8034
+ spacer.className = "chev-spacer";
8035
+ li.appendChild(spacer);
8036
+ if (f2.status) {
8037
+ li.appendChild(fileBadge(f2.status));
8038
+ } else {
8039
+ const icon = document.createElement("span");
8040
+ icon.className = "d2h-icon-wrapper";
8041
+ icon.innerHTML = fileEntryIcon();
8042
+ li.appendChild(icon);
8043
+ }
8044
+ const name = document.createElement("span");
8045
+ name.className = "name";
8046
+ name.textContent = f2.path.split("/").pop();
8047
+ name.title = f2.path;
8048
+ li.appendChild(name);
8049
+ li.addEventListener("click", () => {
8050
+ if (onFileClick)
8051
+ onFileClick(f2);
8052
+ else
8053
+ scrollToFile(f2.path);
8054
+ scheduleMainSurfaceFocus();
8055
+ });
8056
+ if (!onFileClick)
8057
+ li.addEventListener("mouseenter", () => prefetchByPath(f2.path), {
8058
+ passive: true
8059
+ });
8060
+ return li;
8061
+ }
8062
+ function buildSidebarTreeRows(root) {
8063
+ const rows = [];
8064
+ const byPath = new Map;
8065
+ const walk = (node, depth) => {
8066
+ for (const item of treeNodeItems(node)) {
8067
+ if (item.kind === "dir") {
8068
+ const row = {
8069
+ kind: "dir",
8070
+ path: item.dir.path,
8071
+ name: item.dir.name,
8072
+ depth,
8073
+ dir: item.dir
8074
+ };
8075
+ rows.push(row);
8076
+ byPath.set(row.path, row);
8077
+ walk(item.dir, depth + 1);
8078
+ } else {
8079
+ const row = {
8080
+ kind: "file",
8081
+ path: item.file.path,
8082
+ name: item.file.path.split("/").pop() || item.file.path,
8083
+ depth,
8084
+ file: item.file
8085
+ };
8086
+ rows.push(row);
8087
+ byPath.set(row.path, row);
8088
+ }
8089
+ }
8090
+ };
8091
+ walk(root, 0);
8092
+ SIDEBAR_TREE_ROWS = rows;
8093
+ SIDEBAR_ROW_BY_PATH = byPath;
8094
+ }
8095
+ function computeVirtualSidebarVisibleRows() {
8096
+ if (!SIDEBAR_TREE_ROOT) {
8097
+ SIDEBAR_VISIBLE_ROWS = [];
8098
+ return;
8099
+ }
8100
+ const input = $("#sb-filter");
8101
+ const filter = compileFileFilter(input.value);
8102
+ const invalid = filter.kind === "invalid";
8103
+ input.toggleAttribute("aria-invalid", invalid);
8104
+ input.title = invalid ? filter.error || "invalid regular expression" : "";
8105
+ const filterActive = filter.kind !== "empty" && !invalid;
8106
+ const matches = invalid ? () => true : filter.match;
8107
+ const walk = (node, depth) => {
8108
+ let subtreeVisible = false;
8109
+ const rows = [];
8110
+ for (const item of treeNodeItems(node)) {
8111
+ if (item.kind === "dir") {
8112
+ const dirMatches = filterActive && matches(item.dir.path);
8113
+ const expanded = !item.dir.children_omitted && (filterActive || !STATE.collapsedDirs.has(item.dir.path));
8114
+ const child = walk(item.dir, depth + 1);
8115
+ const visible = item.dir.explicit && !filterActive ? true : dirMatches || child.visible;
8116
+ if (visible) {
8117
+ rows.push({
8118
+ kind: "dir",
8119
+ path: item.dir.path,
8120
+ name: item.dir.name,
8121
+ depth,
8122
+ dir: item.dir
8123
+ });
8124
+ if (expanded)
8125
+ rows.push(...child.rows);
8126
+ }
8127
+ subtreeVisible = subtreeVisible || visible;
8128
+ } else {
8129
+ const testHidden = STATE.hideTests && TEST_RE.test(item.file.path || "");
8130
+ const visible = !testHidden && matches(item.file.path || "");
8131
+ if (visible) {
8132
+ rows.push({
8133
+ kind: "file",
8134
+ path: item.file.path,
8135
+ name: item.file.path.split("/").pop() || item.file.path,
8136
+ depth,
8137
+ file: item.file
8138
+ });
8139
+ }
8140
+ subtreeVisible = subtreeVisible || visible;
8141
+ }
8142
+ }
8143
+ return { visible: subtreeVisible, rows };
8144
+ };
8145
+ SIDEBAR_VISIBLE_ROWS = walk(SIDEBAR_TREE_ROOT, 0).rows;
8146
+ }
8147
+ function sidebarVirtualRange() {
8148
+ const sidebar = document.querySelector("#sidebar");
8149
+ const scrollTop = sidebar?.scrollTop || 0;
8150
+ const height = sidebar?.clientHeight || window.innerHeight;
8151
+ const start = Math.max(0, Math.floor(scrollTop / VIRTUAL_SIDEBAR_ROW_HEIGHT) - VIRTUAL_SIDEBAR_OVERSCAN);
8152
+ const end = Math.min(SIDEBAR_VISIBLE_ROWS.length, Math.ceil((scrollTop + height) / VIRTUAL_SIDEBAR_ROW_HEIGHT) + VIRTUAL_SIDEBAR_OVERSCAN);
8153
+ return { start, end };
8154
+ }
8155
+ function renderVirtualSidebarWindow() {
8156
+ const ul = $("#filelist");
8157
+ if (!ul.classList.contains("tree-virtual"))
8158
+ return;
8159
+ const { start, end } = sidebarVirtualRange();
8160
+ const fragment = document.createDocumentFragment();
8161
+ for (let i2 = start;i2 < end; i2++) {
8162
+ const row = SIDEBAR_VISIBLE_ROWS[i2];
8163
+ 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;
8164
+ if (!li)
8165
+ continue;
8166
+ li.classList.toggle("active", row.path === SIDEBAR_VIRTUAL_ACTIVE_PATH);
8167
+ li.style.position = "absolute";
8168
+ li.style.top = `${i2 * VIRTUAL_SIDEBAR_ROW_HEIGHT}px`;
8169
+ li.style.left = "0";
8170
+ li.style.right = "0";
8171
+ fragment.appendChild(li);
8172
+ }
8173
+ ul.replaceChildren(fragment);
8174
+ ul.style.height = `${SIDEBAR_VISIBLE_ROWS.length * VIRTUAL_SIDEBAR_ROW_HEIGHT}px`;
8175
+ }
8176
+ function scrollVirtualSidebarPathIntoView(path) {
8177
+ const index = SIDEBAR_VISIBLE_ROWS.findIndex((row) => row.path === path);
8178
+ if (index < 0)
8179
+ return;
8180
+ const sidebar = document.querySelector("#sidebar");
8181
+ if (!sidebar)
8182
+ return;
8183
+ const ul = $("#filelist");
8184
+ const top = index * VIRTUAL_SIDEBAR_ROW_HEIGHT;
8185
+ const bottom = top + VIRTUAL_SIDEBAR_ROW_HEIGHT;
8186
+ const sidebarRect = sidebar.getBoundingClientRect();
8187
+ const stickyBottom = Math.max(sidebarRect.top, document.querySelector(".sb-head")?.getBoundingClientRect().bottom || sidebarRect.top, document.querySelector(".sb-filter-wrap")?.getBoundingClientRect().bottom || sidebarRect.top);
8188
+ const topPadding = Math.max(8, stickyBottom - sidebarRect.top + 8);
8189
+ const bottomPadding = 14;
8190
+ const listTop = ul.offsetTop;
8191
+ const maxHeight = Number.parseFloat(getComputedStyle(sidebar).maxHeight);
8192
+ const visibleHeight = Number.isFinite(maxHeight) && maxHeight > 0 ? Math.min(sidebar.clientHeight, maxHeight) : sidebar.clientHeight;
8193
+ const visibleTop = sidebar.scrollTop + topPadding - listTop;
8194
+ const visibleBottom = sidebar.scrollTop + visibleHeight - bottomPadding - listTop;
8195
+ if (top < visibleTop)
8196
+ sidebar.scrollTop = Math.max(0, top + listTop - topPadding);
8197
+ else if (bottom > visibleBottom)
8198
+ sidebar.scrollTop = bottom + listTop - visibleHeight + bottomPadding;
8199
+ renderVirtualSidebarWindow();
8200
+ }
8201
+ function rerenderVirtualSidebar() {
8202
+ const ul = document.querySelector("#filelist");
8203
+ if (!ul?.classList.contains("tree-virtual"))
8204
+ return;
8205
+ computeVirtualSidebarVisibleRows();
8206
+ renderVirtualSidebarWindow();
8207
+ }
8208
+ function renderVirtualTreeSidebar(root) {
8209
+ const ul = $("#filelist");
8210
+ SIDEBAR_TREE_ROOT = root;
8211
+ buildSidebarTreeRows(root);
8212
+ ul.classList.add("tree-virtual");
8213
+ ul.style.position = "relative";
8214
+ computeVirtualSidebarVisibleRows();
8215
+ renderVirtualSidebarWindow();
8216
+ document.querySelector("#sidebar")?.addEventListener("scroll", renderVirtualSidebarWindow, {
8217
+ passive: true
8218
+ });
8219
+ }
7862
8220
  function renderFlat(files, ul, onFileClick) {
7863
8221
  files.forEach((f2, i2) => {
7864
8222
  const li = document.createElement("li");
@@ -7897,6 +8255,13 @@ ${frontmatter.yaml}
7897
8255
  const ul = $("#filelist");
7898
8256
  ul.innerHTML = "";
7899
8257
  ul.classList.toggle("tree", STATE.sbView === "tree");
8258
+ ul.classList.remove("tree-virtual");
8259
+ ul.style.removeProperty("height");
8260
+ ul.style.removeProperty("position");
8261
+ SIDEBAR_TREE_ROOT = null;
8262
+ SIDEBAR_TREE_ROWS = [];
8263
+ SIDEBAR_VISIBLE_ROWS = [];
8264
+ SIDEBAR_ROW_BY_PATH = new Map;
7900
8265
  STATE.files = files;
7901
8266
  SIDEBAR_FILES = files;
7902
8267
  SIDEBAR_ON_FILE_CLICK = onFileClick;
@@ -7904,7 +8269,10 @@ ${frontmatter.yaml}
7904
8269
  REPO_SIDEBAR_REF = null;
7905
8270
  if (STATE.sbView === "tree") {
7906
8271
  const root = buildTree(files);
7907
- renderTreeNode(root, 0, ul, onFileClick);
8272
+ if (onFileClick && files.length >= VIRTUAL_SIDEBAR_THRESHOLD)
8273
+ renderVirtualTreeSidebar(root);
8274
+ else
8275
+ renderTreeNode(root, 0, ul, onFileClick);
7908
8276
  } else {
7909
8277
  renderFlat(files, ul, onFileClick);
7910
8278
  }
@@ -7922,6 +8290,17 @@ ${frontmatter.yaml}
7922
8290
  function setAllSidebarDirsCollapsed(collapsed) {
7923
8291
  if (!collapsed)
7924
8292
  STATE.collapsedDirs.clear();
8293
+ if ($("#filelist").classList.contains("tree-virtual")) {
8294
+ if (collapsed) {
8295
+ for (const row of SIDEBAR_TREE_ROWS) {
8296
+ if (row.kind === "dir")
8297
+ STATE.collapsedDirs.add(row.path);
8298
+ }
8299
+ }
8300
+ localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
8301
+ rerenderVirtualSidebar();
8302
+ return;
8303
+ }
7925
8304
  $$("#filelist .tree-dir[data-dirpath]").forEach((li) => {
7926
8305
  const path = li.dataset.dirpath || "";
7927
8306
  if (!path)
@@ -8098,29 +8477,32 @@ ${frontmatter.yaml}
8098
8477
  }
8099
8478
  if (changed)
8100
8479
  localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
8480
+ rerenderVirtualSidebar();
8101
8481
  }
8102
8482
  function markActive(path, options = {}) {
8103
8483
  STATE.activeFile = path;
8484
+ SIDEBAR_VIRTUAL_ACTIVE_PATH = path;
8104
8485
  if (options.reveal && STATE.sbView === "tree")
8105
8486
  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
- });
8487
+ setActiveSidebarItem(sidebarItemByPath(path));
8488
+ if ($("#filelist").classList.contains("tree-virtual")) {
8489
+ renderVirtualSidebarWindow();
8490
+ scrollVirtualSidebarPathIntoView(path);
8491
+ return;
8492
+ }
8111
8493
  if (options.reveal) {
8112
- const active = document.querySelector("#filelist li.active[data-path], #filelist .tree-dir.active[data-dirpath]");
8494
+ const active = activeSidebarItem();
8113
8495
  if (active)
8114
8496
  requestAnimationFrame(() => scrollSidebarItemIntoView(active));
8115
8497
  }
8116
8498
  }
8117
8499
  function applyViewedState() {
8500
+ if (isRepositorySidebarMode())
8501
+ return;
8118
8502
  $$("#filelist li[data-path]").forEach((li) => {
8119
8503
  const path = li.dataset.path || "";
8120
- li.classList.toggle("viewed", !isRepositorySidebarMode() && STATE.viewedFiles.has(path));
8504
+ li.classList.toggle("viewed", STATE.viewedFiles.has(path));
8121
8505
  });
8122
- if (isRepositorySidebarMode())
8123
- return;
8124
8506
  $$(".gdp-file-shell[data-path]").forEach((card) => {
8125
8507
  const path = card.dataset.path || "";
8126
8508
  const viewed = STATE.viewedFiles.has(path);
@@ -8129,11 +8511,16 @@ ${frontmatter.yaml}
8129
8511
  }
8130
8512
  function applyFilter() {
8131
8513
  const input = $("#sb-filter");
8514
+ if ($("#filelist").classList.contains("tree-virtual")) {
8515
+ rerenderVirtualSidebar();
8516
+ return;
8517
+ }
8132
8518
  const filter = compileFileFilter(input.value);
8133
8519
  const invalid = filter.kind === "invalid";
8134
8520
  input.toggleAttribute("aria-invalid", invalid);
8135
8521
  input.title = invalid ? filter.error || "invalid regular expression" : "";
8136
8522
  const matches = invalid ? () => true : filter.match;
8523
+ const filterActive = filter.kind !== "empty" && !invalid;
8137
8524
  $$("#filelist li[data-path]").forEach((li) => {
8138
8525
  const match2 = matches(li.dataset.path || "");
8139
8526
  li.classList.toggle("hidden", !match2);
@@ -8144,21 +8531,51 @@ ${frontmatter.yaml}
8144
8531
  card.classList.toggle("hidden-by-filter", !match2);
8145
8532
  });
8146
8533
  }
8147
- updateTreeDirVisibility(matches, filter.kind !== "empty" && !invalid);
8148
- if (typeof applyViewedState === "function")
8534
+ updateTreeDirVisibility(matches, filterActive);
8535
+ if (!isRepositorySidebarMode() && typeof applyViewedState === "function")
8149
8536
  applyViewedState();
8150
8537
  }
8151
8538
  function updateTreeDirVisibility(dirMatches, filterActive = false) {
8152
- $$("#filelist .tree-dir").forEach((dir) => {
8539
+ const dirs = $$("#filelist .tree-dir");
8540
+ for (let i2 = dirs.length - 1;i2 >= 0; i2--) {
8541
+ const dir = dirs[i2];
8153
8542
  const childUl = dir.nextElementSibling;
8154
8543
  if (!childUl?.classList.contains("tree-children"))
8155
- return;
8156
- const anyVisible = !!childUl.querySelector(".tree-file:not(.hidden):not(.hidden-by-tests)");
8544
+ continue;
8545
+ let anyVisible = false;
8546
+ for (const child of childUl.children) {
8547
+ if (!(child instanceof HTMLElement))
8548
+ continue;
8549
+ if (child.classList.contains("tree-file") && !child.classList.contains("hidden") && !child.classList.contains("hidden-by-tests")) {
8550
+ anyVisible = true;
8551
+ break;
8552
+ }
8553
+ if (child.classList.contains("tree-dir") && !child.classList.contains("hidden") && !child.classList.contains("hidden-by-tests")) {
8554
+ anyVisible = true;
8555
+ break;
8556
+ }
8557
+ }
8157
8558
  const explicitVisible = dir.dataset.explicit === "true" && !filterActive;
8158
8559
  const selfMatches = filterActive && !!dirMatches && dirMatches(dir.dataset.dirpath || "");
8159
8560
  dir.classList.toggle("hidden", !anyVisible && !explicitVisible && !selfMatches);
8561
+ }
8562
+ }
8563
+ let SIDEBAR_FILTER_RAF = 0;
8564
+ function scheduleApplyFilter() {
8565
+ if (SIDEBAR_FILTER_RAF)
8566
+ cancelAnimationFrame(SIDEBAR_FILTER_RAF);
8567
+ SIDEBAR_FILTER_RAF = requestAnimationFrame(() => {
8568
+ SIDEBAR_FILTER_RAF = 0;
8569
+ applyFilter();
8160
8570
  });
8161
8571
  }
8572
+ function flushSidebarFilter() {
8573
+ if (!SIDEBAR_FILTER_RAF)
8574
+ return;
8575
+ cancelAnimationFrame(SIDEBAR_FILTER_RAF);
8576
+ SIDEBAR_FILTER_RAF = 0;
8577
+ applyFilter();
8578
+ }
8162
8579
  let SERVER_GENERATION = 0;
8163
8580
  let CLIENT_REQ_SEQ = 0;
8164
8581
  const LOAD_QUEUE = [];
@@ -11705,8 +12122,97 @@ ${frontmatter.yaml}
11705
12122
  }
11706
12123
  return true;
11707
12124
  }
12125
+ const SIDEBAR_ITEM_SELECTOR = "#filelist li[data-path], #filelist .tree-dir[data-dirpath]";
12126
+ const ACTIVE_SIDEBAR_ITEM_SELECTOR = "#filelist li.active[data-path], #filelist .tree-dir.active[data-dirpath]";
12127
+ function sidebarItemPath(item) {
12128
+ return item.dataset.path || item.dataset.dirpath || "";
12129
+ }
12130
+ function activeSidebarItem() {
12131
+ return document.querySelector(ACTIVE_SIDEBAR_ITEM_SELECTOR);
12132
+ }
12133
+ function sidebarItemByPath(path) {
12134
+ if (isVirtualSidebarActive() && SIDEBAR_ROW_BY_PATH.has(path)) {
12135
+ return document.querySelector(`#filelist li[data-path="${CSS.escape(path)}"], #filelist .tree-dir[data-dirpath="${CSS.escape(path)}"]`) || null;
12136
+ }
12137
+ const escaped = CSS.escape(path);
12138
+ return document.querySelector(`#filelist li[data-path="${escaped}"], #filelist .tree-dir[data-dirpath="${escaped}"]`);
12139
+ }
12140
+ function setActiveSidebarItem(target) {
12141
+ document.querySelectorAll(ACTIVE_SIDEBAR_ITEM_SELECTOR).forEach((item) => {
12142
+ if (item !== target)
12143
+ item.classList.remove("active");
12144
+ });
12145
+ target?.classList.add("active");
12146
+ }
11708
12147
  function visibleSidebarItems() {
11709
- return $$("#filelist li[data-path], #filelist .tree-dir[data-dirpath]").filter(isSidebarRowVisible);
12148
+ return $$(SIDEBAR_ITEM_SELECTOR).filter(isSidebarRowVisible);
12149
+ }
12150
+ function isVirtualSidebarActive() {
12151
+ return $("#filelist").classList.contains("tree-virtual");
12152
+ }
12153
+ function virtualSidebarActiveIndex() {
12154
+ const activePath = SIDEBAR_VIRTUAL_ACTIVE_PATH || STATE.activeFile || "";
12155
+ return SIDEBAR_VISIBLE_ROWS.findIndex((row) => row.path === activePath);
12156
+ }
12157
+ function selectVirtualSidebarIndex(index, options) {
12158
+ if (!SIDEBAR_VISIBLE_ROWS.length)
12159
+ return null;
12160
+ const safeIndex = Math.max(0, Math.min(SIDEBAR_VISIBLE_ROWS.length - 1, index));
12161
+ const row = SIDEBAR_VISIBLE_ROWS[safeIndex];
12162
+ if (!row)
12163
+ return null;
12164
+ markActive(row.path);
12165
+ scrollVirtualSidebarPathIntoView(row.path);
12166
+ if (options?.open) {
12167
+ if (row.kind === "dir" && row.dir && SIDEBAR_ON_FILE_CLICK) {
12168
+ SIDEBAR_ON_FILE_CLICK({
12169
+ path: row.dir.path,
12170
+ display_path: row.dir.path,
12171
+ type: "tree",
12172
+ children_omitted: row.dir.children_omitted,
12173
+ children_omitted_reason: row.dir.children_omitted_reason
12174
+ });
12175
+ } else if (row.file && SIDEBAR_ON_FILE_CLICK) {
12176
+ SIDEBAR_ON_FILE_CLICK(row.file);
12177
+ }
12178
+ }
12179
+ return row;
12180
+ }
12181
+ function visibleSidebarItemFrom(current, direction) {
12182
+ const root = document.querySelector("#filelist");
12183
+ if (!current.isConnected)
12184
+ return null;
12185
+ if (!root)
12186
+ return null;
12187
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
12188
+ acceptNode(node) {
12189
+ if (!(node instanceof HTMLElement))
12190
+ return NodeFilter.FILTER_SKIP;
12191
+ if (node.classList.contains("tree-children")) {
12192
+ const dir = node.previousElementSibling;
12193
+ if (dir?.classList.contains("collapsed") || dir?.classList.contains("hidden") || dir?.classList.contains("hidden-by-tests"))
12194
+ return NodeFilter.FILTER_REJECT;
12195
+ }
12196
+ if (!node.matches(SIDEBAR_ITEM_SELECTOR))
12197
+ return NodeFilter.FILTER_SKIP;
12198
+ return isSidebarRowVisible(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
12199
+ }
12200
+ });
12201
+ walker.currentNode = current;
12202
+ const next = direction === 1 ? walker.nextNode() : walker.previousNode();
12203
+ return next instanceof HTMLElement ? next : null;
12204
+ }
12205
+ function adjacentVisibleSidebarItem(direction) {
12206
+ const active = activeSidebarItem();
12207
+ if (!active) {
12208
+ const items = visibleSidebarItems();
12209
+ return direction === 1 ? items[0] || null : items[items.length - 1] || null;
12210
+ }
12211
+ if (!isSidebarRowVisible(active)) {
12212
+ const items = visibleSidebarItems();
12213
+ return direction === 1 ? items[0] || null : items[items.length - 1] || null;
12214
+ }
12215
+ return visibleSidebarItemFrom(active, direction) || active;
11710
12216
  }
11711
12217
  function scrollSidebarItemIntoView(item, block2 = "nearest") {
11712
12218
  const sidebar = document.querySelector("#sidebar");
@@ -11738,6 +12244,14 @@ ${frontmatter.yaml}
11738
12244
  return document.body.classList.contains("gdp-repo-page") || document.body.classList.contains("gdp-repo-blob-page");
11739
12245
  }
11740
12246
  function moveActiveSidebarItem(direction) {
12247
+ if (isVirtualSidebarActive()) {
12248
+ const current2 = virtualSidebarActiveIndex();
12249
+ const start = current2 < 0 ? direction === 1 ? 0 : SIDEBAR_VISIBLE_ROWS.length - 1 : current2 + direction;
12250
+ const row = selectVirtualSidebarIndex(start);
12251
+ if (row?.file)
12252
+ prefetchByPath(row.file.path);
12253
+ return;
12254
+ }
11741
12255
  const items = visibleSidebarItems();
11742
12256
  if (!items.length)
11743
12257
  return;
@@ -11754,6 +12268,16 @@ ${frontmatter.yaml}
11754
12268
  prefetchByPath(target.dataset.path);
11755
12269
  }
11756
12270
  function moveActiveSidebarPage(direction) {
12271
+ if (isVirtualSidebarActive()) {
12272
+ const sidebar2 = document.querySelector("#sidebar");
12273
+ const halfPageRows2 = Math.max(1, Math.floor((sidebar2?.clientHeight || window.innerHeight) / 2 / VIRTUAL_SIDEBAR_ROW_HEIGHT));
12274
+ const current2 = virtualSidebarActiveIndex();
12275
+ const start2 = current2 < 0 ? 0 : current2;
12276
+ const row = selectVirtualSidebarIndex(start2 + direction * halfPageRows2);
12277
+ if (row?.file)
12278
+ prefetchByPath(row.file.path);
12279
+ return;
12280
+ }
11757
12281
  const items = visibleSidebarItems();
11758
12282
  if (!items.length)
11759
12283
  return;
@@ -11776,6 +12300,12 @@ ${frontmatter.yaml}
11776
12300
  prefetchByPath(target.dataset.path);
11777
12301
  }
11778
12302
  function moveActiveSidebarToEdge(edge) {
12303
+ if (isVirtualSidebarActive()) {
12304
+ const row = selectVirtualSidebarIndex(edge === "top" ? 0 : SIDEBAR_VISIBLE_ROWS.length - 1);
12305
+ if (row?.file)
12306
+ prefetchByPath(row.file.path);
12307
+ return;
12308
+ }
11779
12309
  const items = visibleSidebarItems();
11780
12310
  const repoSidebar = isRepositorySidebarMode();
11781
12311
  const target = edge === "top" ? items[0] : items[items.length - 1];
@@ -11791,6 +12321,21 @@ ${frontmatter.yaml}
11791
12321
  prefetchByPath(target.dataset.path);
11792
12322
  }
11793
12323
  function setActiveSidebarDirectoryCollapsed(collapsed) {
12324
+ if (isVirtualSidebarActive()) {
12325
+ const row = SIDEBAR_VISIBLE_ROWS[virtualSidebarActiveIndex()];
12326
+ if (row?.kind !== "dir" || !row.dir || row.dir.children_omitted)
12327
+ return;
12328
+ if (STATE.collapsedDirs.has(row.path) === collapsed)
12329
+ return;
12330
+ if (collapsed)
12331
+ STATE.collapsedDirs.add(row.path);
12332
+ else
12333
+ STATE.collapsedDirs.delete(row.path);
12334
+ localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
12335
+ rerenderVirtualSidebar();
12336
+ scrollVirtualSidebarPathIntoView(row.path);
12337
+ return;
12338
+ }
11794
12339
  const active = document.querySelector("#filelist .tree-dir.active[data-dirpath]");
11795
12340
  if (!active)
11796
12341
  return;
@@ -11801,6 +12346,13 @@ ${frontmatter.yaml}
11801
12346
  control.click();
11802
12347
  }
11803
12348
  function toggleActiveSidebarDirectoryCollapsed() {
12349
+ if (isVirtualSidebarActive()) {
12350
+ const row = SIDEBAR_VISIBLE_ROWS[virtualSidebarActiveIndex()];
12351
+ if (row?.kind !== "dir" || !row.dir || row.dir.children_omitted)
12352
+ return;
12353
+ setActiveSidebarDirectoryCollapsed(!STATE.collapsedDirs.has(row.path));
12354
+ return;
12355
+ }
11804
12356
  const active = document.querySelector("#filelist .tree-dir.active[data-dirpath]");
11805
12357
  if (!active)
11806
12358
  return;
@@ -11809,11 +12361,23 @@ ${frontmatter.yaml}
11809
12361
  control.click();
11810
12362
  }
11811
12363
  function openActiveSidebarItem() {
12364
+ if (isVirtualSidebarActive()) {
12365
+ const index = virtualSidebarActiveIndex();
12366
+ if (index >= 0)
12367
+ selectVirtualSidebarIndex(index, { open: true });
12368
+ return;
12369
+ }
11812
12370
  const active = document.querySelector("#filelist li.active[data-path], #filelist .tree-dir.active[data-dirpath]");
11813
12371
  if (active && isSidebarRowVisible(active))
11814
12372
  active.click();
11815
12373
  }
11816
12374
  function jumpToActiveOrFirstFilteredItem() {
12375
+ if (isVirtualSidebarActive()) {
12376
+ const current = virtualSidebarActiveIndex();
12377
+ selectVirtualSidebarIndex(current >= 0 ? current : 0, { open: true });
12378
+ $("#sb-filter").blur();
12379
+ return;
12380
+ }
11817
12381
  const items = visibleSidebarItems();
11818
12382
  const active = items.find((li) => li.classList.contains("active"));
11819
12383
  const target = active || items[0];
@@ -11824,17 +12388,20 @@ ${frontmatter.yaml}
11824
12388
  }
11825
12389
  const sbFilter = $("#sb-filter");
11826
12390
  if (sbFilter) {
11827
- sbFilter.addEventListener("input", () => applyFilter());
12391
+ sbFilter.addEventListener("input", () => scheduleApplyFilter());
11828
12392
  sbFilter.addEventListener("keydown", (e2) => {
11829
12393
  if (e2.key === "Enter") {
11830
12394
  e2.preventDefault();
12395
+ flushSidebarFilter();
11831
12396
  jumpToActiveOrFirstFilteredItem();
11832
12397
  } else if (e2.key === "ArrowDown" || e2.key === "ArrowUp") {
11833
12398
  e2.preventDefault();
12399
+ flushSidebarFilter();
11834
12400
  moveActiveSidebarItem(e2.key === "ArrowDown" ? 1 : -1);
11835
12401
  } else if (e2.key === "Escape") {
11836
12402
  if (sbFilter.value) {
11837
12403
  sbFilter.value = "";
12404
+ flushSidebarFilter();
11838
12405
  applyFilter();
11839
12406
  } else {
11840
12407
  sbFilter.blur();
@@ -12322,16 +12889,24 @@ ${frontmatter.yaml}
12322
12889
  }
12323
12890
  if (action === "sidebar-next" || action === "sidebar-previous") {
12324
12891
  const repoSidebar = isRepositorySidebarMode();
12325
- const items = repoSidebar ? visibleSidebarItems() : $$("#filelist li[data-path]:not(.hidden):not(.hidden-by-tests)");
12326
- if (!items.length)
12892
+ const direction = action === "sidebar-next" ? 1 : -1;
12893
+ const diffItems = repoSidebar ? [] : $$("#filelist li[data-path]:not(.hidden):not(.hidden-by-tests)");
12894
+ let diffIndex = diffItems.findIndex((li) => li.classList.contains("active"));
12895
+ if (!repoSidebar)
12896
+ diffIndex = diffIndex < 0 ? 0 : Math.max(0, Math.min(diffItems.length - 1, diffIndex + direction));
12897
+ const target = repoSidebar ? isVirtualSidebarActive() ? null : adjacentVisibleSidebarItem(direction) : diffItems[diffIndex];
12898
+ if (repoSidebar && isVirtualSidebarActive()) {
12899
+ const current = virtualSidebarActiveIndex();
12900
+ const start = current < 0 ? direction === 1 ? 0 : SIDEBAR_VISIBLE_ROWS.length - 1 : current + direction;
12901
+ const row = selectVirtualSidebarIndex(start);
12902
+ const next = row ? SIDEBAR_VISIBLE_ROWS[Math.max(0, Math.min(SIDEBAR_VISIBLE_ROWS.length - 1, SIDEBAR_VISIBLE_ROWS.indexOf(row) + direction))] : null;
12903
+ if (!repeated && next?.file)
12904
+ prefetchByPath(next.file.path);
12327
12905
  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;
12906
+ }
12907
+ if (!target)
12908
+ return true;
12909
+ const path = sidebarItemPath(target);
12335
12910
  if (!repoSidebar && target) {
12336
12911
  target.click();
12337
12912
  scrollSidebarItemIntoView(target);
@@ -12339,9 +12914,8 @@ ${frontmatter.yaml}
12339
12914
  markActive(path);
12340
12915
  scrollSidebarItemIntoView(target);
12341
12916
  }
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)
12917
+ const nextItem = repoSidebar ? visibleSidebarItemFrom(target, direction) : diffItems[Math.max(0, Math.min(diffItems.length - 1, diffIndex + direction))];
12918
+ if (!repeated && nextItem && nextItem !== target && nextItem.dataset.path)
12345
12919
  prefetchByPath(nextItem.dataset.path);
12346
12920
  return true;
12347
12921
  }
@@ -12846,7 +13420,6 @@ ${frontmatter.yaml}
12846
13420
  if (e2.key === "gdp:syntax-highlight")
12847
13421
  setSyntaxHighlight(e2.newValue !== "0");
12848
13422
  });
12849
- const TEST_RE = /(^|[/_.])(test|spec|__tests__)([/_.]|$)/i;
12850
13423
  function applyHideTests() {
12851
13424
  const btn = $("#hide-tests");
12852
13425
  if (btn)
@@ -12859,7 +13432,10 @@ ${frontmatter.yaml}
12859
13432
  const isTest = TEST_RE.test(li.dataset.path || "");
12860
13433
  li.classList.toggle("hidden-by-tests", STATE.hideTests && isTest);
12861
13434
  });
12862
- updateTreeDirVisibility();
13435
+ if (isVirtualSidebarActive())
13436
+ rerenderVirtualSidebar();
13437
+ else
13438
+ updateTreeDirVisibility();
12863
13439
  if (typeof applyViewedState === "function")
12864
13440
  applyViewedState();
12865
13441
  }
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;
@@ -2958,26 +2968,33 @@ body.gdp-file-detail-page.gdp-repo-blob-page #sidebar-resizer {
2958
2968
  body.gdp-repo-page #sidebar-resizer {
2959
2969
  display: block;
2960
2970
  }
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
2971
  body.gdp-repo-page .sb-head,
2968
2972
  body.gdp-file-detail-page.gdp-repo-blob-page .sb-head {
2969
2973
  display: grid;
2970
2974
  grid-template:
2971
- "toggle ref actions view" auto
2972
- / 28px minmax(80px, 240px) auto auto;
2973
- justify-content: start;
2975
+ "toggle title totals ." auto
2976
+ "ref ref ref ref" auto
2977
+ "actions actions . view" auto
2978
+ "filter filter filter filter" auto
2979
+ / 28px auto minmax(0, 1fr) auto;
2980
+ justify-content: stretch;
2974
2981
  align-items: center;
2975
2982
  gap: 8px;
2983
+ top: var(--global-header-h);
2984
+ z-index: 5;
2976
2985
  }
2977
2986
  body.gdp-repo-page .sb-head > #sidebar-toggle,
2978
2987
  body.gdp-file-detail-page.gdp-repo-blob-page .sb-head > #sidebar-toggle {
2979
2988
  grid-area: toggle;
2980
2989
  }
2990
+ body.gdp-repo-page .sb-title,
2991
+ body.gdp-file-detail-page.gdp-repo-blob-page .sb-title {
2992
+ grid-area: title;
2993
+ }
2994
+ body.gdp-repo-page #totals,
2995
+ body.gdp-file-detail-page.gdp-repo-blob-page #totals {
2996
+ grid-area: totals;
2997
+ }
2981
2998
  body.gdp-repo-page #repo-target-wrap,
2982
2999
  body.gdp-file-detail-page.gdp-repo-blob-page #repo-target-wrap {
2983
3000
  grid-area: ref;
@@ -2986,10 +3003,22 @@ body.gdp-file-detail-page.gdp-repo-blob-page #repo-target-wrap {
2986
3003
  body.gdp-repo-page .sb-actions,
2987
3004
  body.gdp-file-detail-page.gdp-repo-blob-page .sb-actions {
2988
3005
  grid-area: actions;
3006
+ margin-left: 0;
3007
+ justify-self: start;
2989
3008
  }
2990
3009
  body.gdp-repo-page .sb-view-seg,
2991
3010
  body.gdp-file-detail-page.gdp-repo-blob-page .sb-view-seg {
2992
3011
  grid-area: view;
3012
+ margin-left: auto;
3013
+ }
3014
+ body.gdp-repo-page .sb-filter-wrap,
3015
+ body.gdp-file-detail-page.gdp-repo-blob-page .sb-filter-wrap {
3016
+ grid-area: filter;
3017
+ position: static;
3018
+ width: 100%;
3019
+ margin: 0;
3020
+ padding: 6px 0 0;
3021
+ border-bottom: 0;
2993
3022
  }
2994
3023
  body.gdp-file-detail-page {
2995
3024
  --chrome-h: var(--global-header-h);