repoview 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repoview",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
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,34 @@ 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;
744
+ }
745
+
746
+ .diff-copy-btn {
747
+ display: inline-flex;
748
+ align-items: center;
749
+ justify-content: center;
750
+ margin-left: 6px;
751
+ padding: 2px;
752
+ border: none;
753
+ background: none;
754
+ color: var(--muted);
755
+ cursor: pointer;
756
+ border-radius: 3px;
757
+ vertical-align: middle;
758
+ }
759
+ .diff-copy-btn:hover {
760
+ color: var(--accent);
761
+ background: var(--btnHover);
704
762
  }
705
763
 
706
764
  .d2h-file-header .d2h-tag {
@@ -763,7 +821,8 @@ a {
763
821
  transform: rotate(-90deg);
764
822
  }
765
823
 
766
- .d2h-file-diff[hidden] {
824
+ /* Diffs start hidden; JS adds .expanded to show them */
825
+ .d2h-file-diff:not(.expanded) {
767
826
  display: none;
768
827
  }
769
828
 
package/public/app.js CHANGED
@@ -180,21 +180,93 @@ function initTimezoneToggle() {
180
180
  update();
181
181
  }
182
182
 
183
+ function fallbackCopy(text) {
184
+ const ta = document.createElement("textarea");
185
+ ta.value = text;
186
+ ta.style.position = "fixed";
187
+ ta.style.opacity = "0";
188
+ document.body.appendChild(ta);
189
+ ta.select();
190
+ document.execCommand("copy");
191
+ document.body.removeChild(ta);
192
+ }
193
+
183
194
  function initDiffCollapse() {
184
195
  const wrappers = document.querySelectorAll(".d2h-file-wrapper");
185
196
  if (!wrappers.length) return;
186
197
 
198
+ // Diffs are hidden by CSS by default (.d2h-file-diff:not(.expanded)).
199
+ // Only expand when there are few files — avoids layout/paint cost for large diffs.
200
+ const shouldExpand = wrappers.length <= 8;
201
+
202
+ // Add expand/collapse all buttons
203
+ const diffWrap = document.querySelector(".diff-wrap");
204
+ if (diffWrap && wrappers.length > 1) {
205
+ const bar = document.createElement("div");
206
+ bar.className = "diff-actions";
207
+ bar.innerHTML = `<button type="button" class="btn btn-sm" id="expand-all">Expand all</button>
208
+ <button type="button" class="btn btn-sm" id="collapse-all">Collapse all</button>`;
209
+ diffWrap.insertBefore(bar, diffWrap.firstChild);
210
+ }
211
+
187
212
  for (const wrapper of wrappers) {
188
213
  const header = wrapper.querySelector(".d2h-file-header");
189
214
  const diff = wrapper.querySelector(".d2h-file-diff");
190
215
  if (!header || !diff) continue;
191
216
 
217
+ const fileNameEl = header.querySelector(".d2h-file-name");
218
+ if (fileNameEl && !fileNameEl.querySelector("a")) {
219
+ const rawName = fileNameEl.textContent.trim();
220
+ const name = rawName.includes(" → ") ? rawName.split(" → ").pop() : rawName;
221
+ const link = document.createElement("a");
222
+ link.className = "diff-file-link";
223
+ link.href = `/blob/${encodeURI(name)}`;
224
+ link.textContent = rawName;
225
+ fileNameEl.textContent = "";
226
+ fileNameEl.appendChild(link);
227
+
228
+ const copyBtn = document.createElement("button");
229
+ copyBtn.className = "diff-copy-btn";
230
+ copyBtn.type = "button";
231
+ copyBtn.setAttribute("aria-label", "Copy filename");
232
+ copyBtn.innerHTML =
233
+ '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">' +
234
+ '<rect x="5" y="5" width="9" height="9" rx="1.5"/>' +
235
+ '<path d="M3 11V2.5A1.5 1.5 0 0 1 4.5 1H11"/>' +
236
+ "</svg>";
237
+ const svgIcon = copyBtn.innerHTML;
238
+ copyBtn.addEventListener("click", (e) => {
239
+ e.stopPropagation();
240
+ const showSuccess = () => {
241
+ copyBtn.textContent = "\u2713";
242
+ setTimeout(() => { copyBtn.innerHTML = svgIcon; }, 1500);
243
+ };
244
+ if (navigator.clipboard?.writeText) {
245
+ navigator.clipboard.writeText(name).then(showSuccess).catch(() => {
246
+ fallbackCopy(name);
247
+ showSuccess();
248
+ });
249
+ } else {
250
+ fallbackCopy(name);
251
+ showSuccess();
252
+ }
253
+ });
254
+ fileNameEl.appendChild(copyBtn);
255
+ }
256
+
192
257
  const toggle = document.createElement("button");
193
258
  toggle.className = "diff-toggle";
194
259
  toggle.type = "button";
195
- toggle.setAttribute("aria-expanded", "true");
196
260
  toggle.setAttribute("aria-label", "Toggle file diff");
197
- toggle.textContent = "\u25BE";
261
+
262
+ if (shouldExpand) {
263
+ toggle.setAttribute("aria-expanded", "true");
264
+ toggle.textContent = "\u25BE";
265
+ diff.classList.add("expanded");
266
+ } else {
267
+ toggle.setAttribute("aria-expanded", "false");
268
+ toggle.textContent = "\u25B8";
269
+ }
198
270
  header.appendChild(toggle);
199
271
 
200
272
  header.addEventListener("click", (e) => {
@@ -202,9 +274,31 @@ function initDiffCollapse() {
202
274
  const expanded = toggle.getAttribute("aria-expanded") === "true";
203
275
  toggle.setAttribute("aria-expanded", String(!expanded));
204
276
  toggle.textContent = expanded ? "\u25B8" : "\u25BE";
205
- diff.hidden = expanded;
277
+ diff.classList.toggle("expanded");
206
278
  });
207
279
  }
280
+
281
+ document.getElementById("expand-all")?.addEventListener("click", () => {
282
+ for (const wrapper of wrappers) {
283
+ const toggle = wrapper.querySelector(".diff-toggle");
284
+ const diff = wrapper.querySelector(".d2h-file-diff");
285
+ if (!toggle || !diff) continue;
286
+ toggle.setAttribute("aria-expanded", "true");
287
+ toggle.textContent = "\u25BE";
288
+ diff.classList.add("expanded");
289
+ }
290
+ });
291
+
292
+ document.getElementById("collapse-all")?.addEventListener("click", () => {
293
+ for (const wrapper of wrappers) {
294
+ const toggle = wrapper.querySelector(".diff-toggle");
295
+ const diff = wrapper.querySelector(".d2h-file-diff");
296
+ if (!toggle || !diff) continue;
297
+ toggle.setAttribute("aria-expanded", "false");
298
+ toggle.textContent = "\u25B8";
299
+ diff.classList.remove("expanded");
300
+ }
301
+ });
208
302
  }
209
303
 
210
304
  function initBaseSelector() {
@@ -222,7 +316,7 @@ function initBaseSelector() {
222
316
  }
223
317
 
224
318
  window.addEventListener("load", () => {
225
- preserveQueryParamsOnInternalLinks(["ignored", "watch", "base"]);
319
+ preserveQueryParamsOnInternalLinks(["ignored", "watch", "base", "show_all"]);
226
320
  renderMath();
227
321
  renderMermaid();
228
322
  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