@youtyan/code-viewer 0.9.0 → 0.9.2

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": "@youtyan/code-viewer",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -247,6 +247,22 @@ ${lines.join(`
247
247
  };
248
248
  }
249
249
 
250
+ // web-src/core/changed-paths.ts
251
+ function changedPathCoversPath(changed, path) {
252
+ return path === changed || path.startsWith(`${changed}/`);
253
+ }
254
+ function changedPathsCoverPath(changedPaths, path) {
255
+ if (!changedPaths)
256
+ return true;
257
+ if (changedPaths.has(path))
258
+ return true;
259
+ for (const changed of changedPaths) {
260
+ if (changedPathCoversPath(changed, path))
261
+ return true;
262
+ }
263
+ return false;
264
+ }
265
+
250
266
  // web-src/core/expand-logic.ts
251
267
  function initExpandState(prevHunkEndNew, hunkNewStart) {
252
268
  return {
@@ -23348,7 +23364,14 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23348
23364
  const span = document.createElement("span");
23349
23365
  span.className = `badge ${ch}`;
23350
23366
  span.textContent = ch;
23351
- span.title = { M: "modified", A: "added", D: "deleted", R: "renamed" }[ch] || ch;
23367
+ span.title = {
23368
+ M: "modified",
23369
+ A: "added",
23370
+ D: "deleted",
23371
+ R: "renamed",
23372
+ U: "untracked",
23373
+ I: "ignored"
23374
+ }[ch] || ch;
23352
23375
  return span;
23353
23376
  }
23354
23377
  function setFileViewed(path, viewed) {
@@ -23690,7 +23713,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23690
23713
  function invalidateLoadedCard(card, file, changedPaths, measuredHeight) {
23691
23714
  if (!card.classList.contains("loaded") && !card.classList.contains("loading"))
23692
23715
  return false;
23693
- if (changedPaths && !changedPaths.has(file.path))
23716
+ if (!changedPathsCoverPath(changedPaths, file.path))
23694
23717
  return false;
23695
23718
  card.dataset.reqId = String(++CLIENT_REQ_SEQ);
23696
23719
  card.style.minHeight = `${measuredHeight ?? card.offsetHeight}px`;
@@ -23780,13 +23803,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23780
23803
  const pathsUnknown = !changedPaths;
23781
23804
  const contentMayNeedReload = !listSame || pathsUnknown || (changedPaths ? newFiles.some((file) => {
23782
23805
  const key = fileKey(file);
23783
- return prevCardSignatures.get(key) !== newCardSigs.get(key) || changedPaths.has(file.path);
23806
+ return prevCardSignatures.get(key) !== newCardSigs.get(key) || changedPathsCoverPath(changedPaths, file.path);
23784
23807
  }) : false);
23785
23808
  if (contentMayNeedReload)
23786
23809
  resetLoadScheduling();
23787
23810
  let invalidatedCards = 0;
23788
23811
  if (listSame && domIntact) {
23789
- const pathsUnknown2 = !changedPaths;
23790
23812
  const cardsByKey = collectDiffCardsByKey(target);
23791
23813
  const measuredHeights = new Map;
23792
23814
  for (const f2 of newFiles) {
@@ -23801,7 +23823,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23801
23823
  const oldSig = prevCardSignatures.get(key);
23802
23824
  const newSig = newCardSigs.get(key) ?? computeCardSignature(f2);
23803
23825
  const sigChanged = oldSig !== newSig;
23804
- const pathHint = pathsUnknown2 || changedPaths.has(f2.path);
23826
+ const pathHint = changedPathsCoverPath(changedPaths, f2.path);
23805
23827
  if (!sigChanged && !pathHint) {
23806
23828
  continue;
23807
23829
  }
@@ -26425,7 +26447,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
26425
26447
  },
26426
26448
  {
26427
26449
  kind: "paragraph",
26428
- text: 'Symlinks show a distinct icon and a "→ target" label so they are never mistaken for a regular file or folder, and clicking one navigates to its resolved target. A broken symlink is visually flagged and disabled. Files with pending git changes (new, modified, renamed, deleted) show a status badge in the tree instead of the regular type icon.'
26450
+ text: 'Symlinks show a distinct icon and a "→ target" label so they are never mistaken for a regular file or folder, and clicking one navigates to its resolved target. A broken symlink is visually flagged and disabled.'
26451
+ },
26452
+ {
26453
+ kind: "paragraph",
26454
+ text: "Files with pending git changes show a status badge in the tree instead of the regular type icon: M (modified), A (added — staged for commit), D (deleted), R (renamed), U (untracked — in the worktree but not under version control yet), and I (ignored by a .gitignore rule). U and A stay separate so a file you have never run git add on does not look like one that is already staged. A wholly untracked or ignored directory is badged as a whole and keeps its folder icon, so it stays recognizable while collapsed; its contents inherit the badge unless an ignore rule names a file specifically."
26429
26455
  }
26430
26456
  ]
26431
26457
  },
@@ -27116,7 +27142,11 @@ code-viewer query agent-help`
27116
27142
  },
27117
27143
  {
27118
27144
  kind: "paragraph",
27119
- text: "シンボリックリンクは専用アイコンと「→ リンク先」ラベルで表示されるため通常のファイル/フォルダと区別でき、クリックするとリンク先に遷移します。リンク切れのシンボリックリンクは無効化されたことが分かる表示になります。未コミットの git 変更(新規・変更・リネーム・削除)があるファイルは、通常の種類アイコンの代わりにステータスバッジがツリーに表示されます。"
27145
+ text: "シンボリックリンクは専用アイコンと「→ リンク先」ラベルで表示されるため通常のファイル/フォルダと区別でき、クリックするとリンク先に遷移します。リンク切れのシンボリックリンクは無効化されたことが分かる表示になります。"
27146
+ },
27147
+ {
27148
+ kind: "paragraph",
27149
+ text: "git の状態があるファイルは、通常の種類アイコンの代わりにステータスバッジがツリーに表示されます。M(変更)、A(追加 — コミット予定としてステージ済み)、D(削除)、R(リネーム)、U(未追跡 — ワークツリーにあるがまだバージョン管理下にない)、I(.gitignore の対象) の 6 種類です。U と A を分けているのは、git add をまだ一度もしていないファイルが、既にステージ済みのファイルと同じ見た目にならないようにするためです。丸ごと未追跡・無視のディレクトリにはディレクトリ単位でバッジが付き、フォルダアイコンは残るので折りたたんだままでも判別できます。配下のファイルはそのバッジを引き継ぎますが、無視ルールが個別に名指ししているファイルはそちらが優先されます。"
27120
27150
  }
27121
27151
  ]
27122
27152
  },
@@ -31235,6 +31265,43 @@ code-viewer query agent-help`
31235
31265
  function toggleSidebarHidden() {
31236
31266
  applySidebarHidden(!STATE.sidebarHidden);
31237
31267
  }
31268
+ function applyDirEntryToNode(node, entry) {
31269
+ node.explicit = true;
31270
+ if (entry.children_omitted === true) {
31271
+ node.children_omitted = true;
31272
+ node.children_omitted_reason = entry.children_omitted_reason;
31273
+ }
31274
+ if (entry.is_symlink) {
31275
+ node.is_symlink = true;
31276
+ node.symlink_target = entry.symlink_target;
31277
+ node.resolved_path = entry.resolved_path;
31278
+ }
31279
+ if (entry.status)
31280
+ node.status = entry.status;
31281
+ }
31282
+ function createTreeDirLabel(dir) {
31283
+ const label = document.createElement("span");
31284
+ label.className = "dir-label";
31285
+ const dn = document.createElement("span");
31286
+ dn.className = "dir-name";
31287
+ dn.textContent = dir.name;
31288
+ dn.title = dir.path;
31289
+ label.appendChild(dn);
31290
+ if (dir.status)
31291
+ label.appendChild(fileBadge(dir.status));
31292
+ if (dir.children_omitted) {
31293
+ const omitted = document.createElement("span");
31294
+ omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
31295
+ const badge = omittedDirectoryBadge(dir.children_omitted_reason);
31296
+ omitted.textContent = badge.label;
31297
+ omitted.title = badge.title;
31298
+ label.appendChild(omitted);
31299
+ }
31300
+ const dirSymlinkLabel = symlinkTargetLabel(dir);
31301
+ if (dirSymlinkLabel)
31302
+ label.appendChild(dirSymlinkLabel);
31303
+ return label;
31304
+ }
31238
31305
  function buildTree(files) {
31239
31306
  const root = {
31240
31307
  name: "",
@@ -31266,16 +31333,7 @@ code-viewer query agent-help`
31266
31333
  node.minOrder = f2.order;
31267
31334
  }
31268
31335
  if (f2.type === "tree") {
31269
- node.explicit = true;
31270
- if (f2.children_omitted === true) {
31271
- node.children_omitted = true;
31272
- node.children_omitted_reason = f2.children_omitted_reason;
31273
- }
31274
- if (f2.is_symlink) {
31275
- node.is_symlink = true;
31276
- node.symlink_target = f2.symlink_target;
31277
- node.resolved_path = f2.resolved_path;
31278
- }
31336
+ applyDirEntryToNode(node, f2);
31279
31337
  continue;
31280
31338
  }
31281
31339
  node.files.push(f2);
@@ -31345,25 +31403,7 @@ code-viewer query agent-help`
31345
31403
  const dirIcon = document.createElement("span");
31346
31404
  dirIcon.className = "dir-icon";
31347
31405
  li.appendChild(dirIcon);
31348
- const label = document.createElement("span");
31349
- label.className = "dir-label";
31350
- const dn = document.createElement("span");
31351
- dn.className = "dir-name";
31352
- dn.textContent = dir.name;
31353
- dn.title = dir.path;
31354
- label.appendChild(dn);
31355
- if (dir.children_omitted) {
31356
- const omitted = document.createElement("span");
31357
- omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
31358
- const badge = omittedDirectoryBadge(dir.children_omitted_reason);
31359
- omitted.textContent = badge.label;
31360
- omitted.title = badge.title;
31361
- label.appendChild(omitted);
31362
- }
31363
- const dirSymlinkLabel = symlinkTargetLabel(dir);
31364
- if (dirSymlinkLabel)
31365
- label.appendChild(dirSymlinkLabel);
31366
- li.appendChild(label);
31406
+ li.appendChild(createTreeDirLabel(dir));
31367
31407
  li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
31368
31408
  const collapsed = STATE.collapsedDirs.has(dir.path);
31369
31409
  if (collapsed)
@@ -31460,16 +31500,7 @@ code-viewer query agent-help`
31460
31500
  node.minOrder = Math.min(node.minOrder, order);
31461
31501
  }
31462
31502
  if (entry.type === "tree") {
31463
- node.explicit = true;
31464
- if (entry.children_omitted === true) {
31465
- node.children_omitted = true;
31466
- node.children_omitted_reason = entry.children_omitted_reason;
31467
- }
31468
- if (entry.is_symlink) {
31469
- node.is_symlink = true;
31470
- node.symlink_target = entry.symlink_target;
31471
- node.resolved_path = entry.resolved_path;
31472
- }
31503
+ applyDirEntryToNode(node, entry);
31473
31504
  return;
31474
31505
  }
31475
31506
  if (!node.files.some((file) => file.path === entry.path))
@@ -31553,25 +31584,7 @@ code-viewer query agent-help`
31553
31584
  const dirIcon = document.createElement("span");
31554
31585
  dirIcon.className = "dir-icon";
31555
31586
  li.appendChild(dirIcon);
31556
- const label = document.createElement("span");
31557
- label.className = "dir-label";
31558
- const dn = document.createElement("span");
31559
- dn.className = "dir-name";
31560
- dn.textContent = dir.name;
31561
- dn.title = dir.path;
31562
- label.appendChild(dn);
31563
- if (dir.children_omitted) {
31564
- const omitted = document.createElement("span");
31565
- omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
31566
- const badge = omittedDirectoryBadge(dir.children_omitted_reason);
31567
- omitted.textContent = badge.label;
31568
- omitted.title = badge.title;
31569
- label.appendChild(omitted);
31570
- }
31571
- const dirSymlinkLabel = symlinkTargetLabel(dir);
31572
- if (dirSymlinkLabel)
31573
- label.appendChild(dirSymlinkLabel);
31574
- li.appendChild(label);
31587
+ li.appendChild(createTreeDirLabel(dir));
31575
31588
  li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
31576
31589
  const updateIcon = () => {
31577
31590
  setFolderIcon(dirIcon, li.classList.contains("collapsed"));
@@ -33209,10 +33222,17 @@ code-viewer query agent-help`
33209
33222
  row.className = nonBrowsableCommit ? `gdp-repo-row ${entry.type} gdp-repo-row-gitlink` : entry.is_symlink ? `gdp-repo-row ${entry.type} symlink-row${brokenSymlink ? " symlink-broken-row gdp-row-disabled" : ""}` : `gdp-repo-row ${entry.type}`;
33210
33223
  if (deletedEntry)
33211
33224
  row.classList.add("gdp-row-disabled");
33212
- const icon = entry.status ? fileBadge(entry.status) : repoEntryTypeIcon(entry, browsable, nonBrowsableCommit);
33225
+ const directoryRow = entry.type === "tree";
33226
+ const icon = entry.status && !directoryRow ? fileBadge(entry.status) : repoEntryTypeIcon(entry, browsable, nonBrowsableCommit);
33213
33227
  const name = document.createElement("span");
33214
33228
  name.className = "name";
33215
33229
  name.textContent = entry.name;
33230
+ let nameCell = name;
33231
+ if (directoryRow && entry.status) {
33232
+ nameCell = document.createElement("span");
33233
+ nameCell.className = "name-cell";
33234
+ nameCell.append(name, fileBadge(entry.status));
33235
+ }
33216
33236
  if (nonBrowsableCommit) {
33217
33237
  row.title = commitEntryMeta(entry.submodule).title;
33218
33238
  row.setAttribute("aria-disabled", "true");
@@ -33221,7 +33241,7 @@ code-viewer query agent-help`
33221
33241
  }
33222
33242
  const metaBlock = createRepoEntryMeta(entry, browsable);
33223
33243
  const size = createRepoEntrySize(entry);
33224
- row.append(icon, name, metaBlock, size);
33244
+ row.append(icon, nameCell, metaBlock, size);
33225
33245
  row.addEventListener("click", () => {
33226
33246
  if (brokenSymlink || deletedEntry)
33227
33247
  return;
@@ -36588,6 +36608,11 @@ code-viewer query agent-help`
36588
36608
  SERVER_SCOPE_WATCH_LIMIT_MIN = settings.scope.watch_limit_min;
36589
36609
  if (typeof settings.scope.watch_limit_max === "number")
36590
36610
  SERVER_SCOPE_WATCH_LIMIT_MAX = settings.scope.watch_limit_max;
36611
+ if (typeof settings.scope.watch_recursive === "boolean") {
36612
+ const watchSection = document.querySelector("#watch-settings-section");
36613
+ if (watchSection)
36614
+ watchSection.hidden = settings.scope.watch_recursive;
36615
+ }
36591
36616
  return settings;
36592
36617
  } catch {
36593
36618
  return null;
@@ -40074,7 +40099,7 @@ code-viewer query agent-help`
40074
40099
  return;
40075
40100
  if (isBlobOrBlameFileRoute(route)) {
40076
40101
  const viewingPath = route.path;
40077
- if (paths && viewingPath && !paths.has(viewingPath))
40102
+ if (viewingPath && !changedPathsCoverPath(paths, viewingPath))
40078
40103
  return;
40079
40104
  dispatchFileRoute(route, { refresh: true });
40080
40105
  return;
@@ -40122,7 +40147,7 @@ code-viewer query agent-help`
40122
40147
  const route = STATE.route;
40123
40148
  if (isBlobOrBlameFileRoute(route)) {
40124
40149
  const viewingPath = route.path;
40125
- if (paths && viewingPath && !paths.has(viewingPath))
40150
+ if (viewingPath && !changedPathsCoverPath(paths, viewingPath))
40126
40151
  return;
40127
40152
  }
40128
40153
  if (STATE.autoUpdate) {
package/web/index.html CHANGED
@@ -191,7 +191,7 @@
191
191
  </label>
192
192
  <p id="datastore-s3-tooltip-help" class="scope-settings-help"></p>
193
193
  </div>
194
- <div class="scope-settings-section">
194
+ <div id="watch-settings-section" class="scope-settings-section">
195
195
  <strong id="watch-section-title" class="scope-settings-section-title">File change watcher</strong>
196
196
  <label for="scope-watch-limit">Maximum directories to watch</label>
197
197
  <div class="scope-watch-limit-row">
package/web/style.css CHANGED
@@ -2721,11 +2721,19 @@ body.gdp-help-page #content {
2721
2721
  .badge.A { background: #ccffd866; color: var(--success); }
2722
2722
  .badge.D { background: #ffd7d566; color: var(--danger); }
2723
2723
  .badge.R { background: #d8b9ff66; color: var(--done); }
2724
+ /* Outside version control. U (untracked) takes the accent hue so it reads as
2725
+ "not in git yet" rather than as one more shade of the staged-green A it
2726
+ used to be collapsed into. I (ignored) stays deliberately colorless: an
2727
+ ignored file is present but irrelevant to the repository. */
2728
+ .badge.U { background: var(--accent-subtle); color: var(--accent); }
2729
+ .badge.I { background: var(--bg-emph); color: var(--fg-muted); }
2724
2730
 
2725
2731
  [data-theme="dark"] .badge.M { background: rgba(210,153,34,0.20); color: var(--attn); }
2726
2732
  [data-theme="dark"] .badge.A { background: rgba(46,160,67,0.20); color: var(--success); }
2727
2733
  [data-theme="dark"] .badge.D { background: rgba(248,81,73,0.20); color: var(--danger); }
2728
2734
  [data-theme="dark"] .badge.R { background: rgba(163,113,247,0.20); color: var(--done); }
2735
+ [data-theme="dark"] .badge.U { background: rgba(56,139,253,0.20); color: var(--accent); }
2736
+ [data-theme="dark"] .badge.I { background: rgba(145,152,161,0.15); color: var(--fg-muted); }
2729
2737
 
2730
2738
  /* Sidebar row marker for files worth a second look before diving in:
2731
2739
  heavy diffs, binary/media files, or Git commit entries. Same semantics as
@@ -4792,6 +4800,16 @@ body.gdp-file-detail-page #empty {
4792
4800
  white-space: nowrap;
4793
4801
  font-weight: 500;
4794
4802
  }
4803
+ /* Holds the name plus a directory's status badge inside the single name
4804
+ column of the row grid - same shape as the sidebar's .dir-label, which
4805
+ pairs a directory name with its badges. */
4806
+ .gdp-repo-row .name-cell {
4807
+ min-width: 0;
4808
+ display: flex;
4809
+ align-items: center;
4810
+ gap: 6px;
4811
+ overflow: hidden;
4812
+ }
4795
4813
  .gdp-repo-row .kind {
4796
4814
  justify-self: end;
4797
4815
  color: var(--fg-muted);