repoview 0.3.1 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repoview",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "GitHub-like repo browsing for local Git repositories (Markdown, live reload, broken link scanner).",
package/public/app.css CHANGED
@@ -564,6 +564,36 @@ a {
564
564
  text-size-adjust: 100%;
565
565
  }
566
566
 
567
+ .diff-actions {
568
+ display: flex;
569
+ gap: 8px;
570
+ padding: 10px 14px;
571
+ position: sticky;
572
+ top: 0;
573
+ z-index: 5;
574
+ background: var(--panel);
575
+ border-bottom: 1px solid var(--border);
576
+ }
577
+
578
+ .btn-sm {
579
+ font-weight: 600;
580
+ font-size: 12px;
581
+ padding: 3px 8px;
582
+ border-radius: 4px;
583
+ border: 1px solid var(--border);
584
+ background: var(--btn);
585
+ color: var(--text);
586
+ cursor: pointer;
587
+ }
588
+
589
+ .btn-sm:hover {
590
+ background: var(--btnHover);
591
+ }
592
+
593
+ .diff-truncated {
594
+ margin: 12px 14px;
595
+ }
596
+
567
597
  .diff-empty {
568
598
  margin: 12px;
569
599
  }
@@ -701,6 +731,16 @@ a {
701
731
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
702
732
  font-size: 13px;
703
733
  font-weight: 600;
734
+ white-space: normal;
735
+ word-break: break-all;
736
+ }
737
+
738
+ .diff-file-link {
739
+ color: var(--accent);
740
+ }
741
+
742
+ .diff-file-link:hover {
743
+ text-decoration: underline;
704
744
  }
705
745
 
706
746
  .d2h-file-header .d2h-tag {
@@ -763,7 +803,8 @@ a {
763
803
  transform: rotate(-90deg);
764
804
  }
765
805
 
766
- .d2h-file-diff[hidden] {
806
+ /* Diffs start hidden; JS adds .expanded to show them */
807
+ .d2h-file-diff:not(.expanded) {
767
808
  display: none;
768
809
  }
769
810
 
package/public/app.js CHANGED
@@ -184,17 +184,50 @@ function initDiffCollapse() {
184
184
  const wrappers = document.querySelectorAll(".d2h-file-wrapper");
185
185
  if (!wrappers.length) return;
186
186
 
187
+ // Diffs are hidden by CSS by default (.d2h-file-diff:not(.expanded)).
188
+ // Only expand when there are few files — avoids layout/paint cost for large diffs.
189
+ const shouldExpand = wrappers.length <= 8;
190
+
191
+ // Add expand/collapse all buttons
192
+ const diffWrap = document.querySelector(".diff-wrap");
193
+ if (diffWrap && wrappers.length > 1) {
194
+ const bar = document.createElement("div");
195
+ bar.className = "diff-actions";
196
+ bar.innerHTML = `<button type="button" class="btn btn-sm" id="expand-all">Expand all</button>
197
+ <button type="button" class="btn btn-sm" id="collapse-all">Collapse all</button>`;
198
+ diffWrap.insertBefore(bar, diffWrap.firstChild);
199
+ }
200
+
187
201
  for (const wrapper of wrappers) {
188
202
  const header = wrapper.querySelector(".d2h-file-header");
189
203
  const diff = wrapper.querySelector(".d2h-file-diff");
190
204
  if (!header || !diff) continue;
191
205
 
206
+ const fileNameEl = header.querySelector(".d2h-file-name");
207
+ if (fileNameEl && !fileNameEl.querySelector("a")) {
208
+ const rawName = fileNameEl.textContent.trim();
209
+ const name = rawName.includes(" → ") ? rawName.split(" → ").pop() : rawName;
210
+ const link = document.createElement("a");
211
+ link.className = "diff-file-link";
212
+ link.href = `/blob/${encodeURI(name)}`;
213
+ link.textContent = rawName;
214
+ fileNameEl.textContent = "";
215
+ fileNameEl.appendChild(link);
216
+ }
217
+
192
218
  const toggle = document.createElement("button");
193
219
  toggle.className = "diff-toggle";
194
220
  toggle.type = "button";
195
- toggle.setAttribute("aria-expanded", "true");
196
221
  toggle.setAttribute("aria-label", "Toggle file diff");
197
- toggle.textContent = "\u25BE";
222
+
223
+ if (shouldExpand) {
224
+ toggle.setAttribute("aria-expanded", "true");
225
+ toggle.textContent = "\u25BE";
226
+ diff.classList.add("expanded");
227
+ } else {
228
+ toggle.setAttribute("aria-expanded", "false");
229
+ toggle.textContent = "\u25B8";
230
+ }
198
231
  header.appendChild(toggle);
199
232
 
200
233
  header.addEventListener("click", (e) => {
@@ -202,9 +235,31 @@ function initDiffCollapse() {
202
235
  const expanded = toggle.getAttribute("aria-expanded") === "true";
203
236
  toggle.setAttribute("aria-expanded", String(!expanded));
204
237
  toggle.textContent = expanded ? "\u25B8" : "\u25BE";
205
- diff.hidden = expanded;
238
+ diff.classList.toggle("expanded");
206
239
  });
207
240
  }
241
+
242
+ document.getElementById("expand-all")?.addEventListener("click", () => {
243
+ for (const wrapper of wrappers) {
244
+ const toggle = wrapper.querySelector(".diff-toggle");
245
+ const diff = wrapper.querySelector(".d2h-file-diff");
246
+ if (!toggle || !diff) continue;
247
+ toggle.setAttribute("aria-expanded", "true");
248
+ toggle.textContent = "\u25BE";
249
+ diff.classList.add("expanded");
250
+ }
251
+ });
252
+
253
+ document.getElementById("collapse-all")?.addEventListener("click", () => {
254
+ for (const wrapper of wrappers) {
255
+ const toggle = wrapper.querySelector(".diff-toggle");
256
+ const diff = wrapper.querySelector(".d2h-file-diff");
257
+ if (!toggle || !diff) continue;
258
+ toggle.setAttribute("aria-expanded", "false");
259
+ toggle.textContent = "\u25B8";
260
+ diff.classList.remove("expanded");
261
+ }
262
+ });
208
263
  }
209
264
 
210
265
  function initBaseSelector() {
@@ -222,7 +277,7 @@ function initBaseSelector() {
222
277
  }
223
278
 
224
279
  window.addEventListener("load", () => {
225
- preserveQueryParamsOnInternalLinks(["ignored", "watch", "base"]);
280
+ preserveQueryParamsOnInternalLinks(["ignored", "watch", "base", "show_all"]);
226
281
  renderMath();
227
282
  renderMermaid();
228
283
  initTimezoneToggle();
package/src/server.js CHANGED
@@ -143,8 +143,7 @@ async function getGitDiffRaw(repoRootReal, base) {
143
143
  async function getGitInfo(repoRootReal) {
144
144
  const gitDir = path.join(repoRootReal, ".git");
145
145
  try {
146
- const stat = await fs.stat(gitDir);
147
- if (!stat.isDirectory()) return { branch: null, commit: null };
146
+ await fs.stat(gitDir);
148
147
  } catch {
149
148
  return { branch: null, commit: null };
150
149
  }
@@ -387,6 +386,7 @@ export async function startServer({ repoRoot, host, port, watch }) {
387
386
  if (req.query.watch === "0") query.set("watch", "0");
388
387
  if (req.query.ignored === "1") query.set("ignored", "1");
389
388
  if (base !== "HEAD") query.set("base", base);
389
+ if (req.query.show_all === "1") query.set("show_all", "1");
390
390
  const querySuffix = query.toString() ? `?${query.toString()}` : "";
391
391
 
392
392
  const [branches, tags, diffResult] = await Promise.all([
@@ -395,9 +395,17 @@ export async function startServer({ repoRoot, host, port, watch }) {
395
395
  getGitDiffRaw(repoRootReal, base),
396
396
  ]);
397
397
 
398
+ const MAX_DIFF_FILES = 30;
398
399
  let diffHtml = "";
400
+ let fileCount = 0;
401
+ const showAll = req.query.show_all === "1";
399
402
  if (!diffResult.tooLarge && diffResult.raw) {
400
- diffHtml = diff2html.html(diffResult.raw, {
403
+ const parsed = diff2html.parse(diffResult.raw);
404
+ fileCount = parsed.length;
405
+ const toRender = (!showAll && parsed.length > MAX_DIFF_FILES)
406
+ ? parsed.slice(0, MAX_DIFF_FILES)
407
+ : parsed;
408
+ diffHtml = diff2html.html(toRender, {
401
409
  outputFormat: "line-by-line",
402
410
  drawFileList: true,
403
411
  });
@@ -416,6 +424,8 @@ export async function startServer({ repoRoot, host, port, watch }) {
416
424
  diffHtml,
417
425
  tooLarge: diffResult.tooLarge,
418
426
  empty: !diffResult.raw,
427
+ fileCount,
428
+ showAll,
419
429
  }),
420
430
  );
421
431
  } catch (e) {
package/src/views.js CHANGED
@@ -293,6 +293,8 @@ export function renderDiffPage({
293
293
  diffHtml,
294
294
  tooLarge,
295
295
  empty,
296
+ fileCount,
297
+ showAll,
296
298
  }) {
297
299
  const branchOptions = branches
298
300
  .map((b) => {
@@ -323,6 +325,16 @@ export function renderDiffPage({
323
325
  content = diffHtml;
324
326
  }
325
327
 
328
+ const MAX_DIFF_FILES = 30;
329
+ let truncatedMsg = "";
330
+ if (fileCount > MAX_DIFF_FILES && !showAll) {
331
+ const hidden = fileCount - MAX_DIFF_FILES;
332
+ const showAllQuery = new URLSearchParams(querySuffix ? querySuffix.slice(1) : "");
333
+ showAllQuery.set("show_all", "1");
334
+ const showAllHref = `/diff?${showAllQuery.toString()}`;
335
+ truncatedMsg = `<div class="diff-truncated note">${hidden} more file${hidden === 1 ? "" : "s"} not shown. <a class="link" href="${showAllHref}">Show all ${fileCount} files</a></div>`;
336
+ }
337
+
326
338
  const body = `<section class="panel">
327
339
  <div class="panel-title">
328
340
  <span>Compare working tree against</span>
@@ -332,6 +344,7 @@ export function renderDiffPage({
332
344
  </div>
333
345
  <div class="diff-wrap">
334
346
  ${content}
347
+ ${truncatedMsg}
335
348
  </div>
336
349
  </section>`;
337
350