@youtyan/code-viewer 0.1.24 → 0.1.25

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.
@@ -313,6 +313,13 @@ function objectByteSize(oid, cwd) {
313
313
  stderr: res.stderr
314
314
  };
315
315
  }
316
+ function lastCommitDateForPath(ref, path, cwd) {
317
+ const args = ["git", "log", "-1", "--format=%cI", ref, "--", path];
318
+ const res = run(args, cwd);
319
+ if (res.code !== 0)
320
+ return null;
321
+ return res.stdout.trim() || null;
322
+ }
316
323
  function objectId(ref, path, cwd) {
317
324
  const res = run(["git", "rev-parse", "--verify", `${ref}:${path}`], cwd);
318
325
  const oid = res.stdout.trim();
@@ -1694,6 +1701,71 @@ function parentRepoPath(path) {
1694
1701
  const parent = dirname2(path);
1695
1702
  return parent === "." ? "" : parent;
1696
1703
  }
1704
+ function isoDate(ms) {
1705
+ return ms && Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;
1706
+ }
1707
+ function worktreeFileMetadata(path, knownSize) {
1708
+ const full = safeWorktreePath(path);
1709
+ if (!full)
1710
+ return {};
1711
+ try {
1712
+ const stat = statSync(full);
1713
+ return {
1714
+ size: knownSize ?? stat.size,
1715
+ created_at: isoDate(stat.birthtimeMs),
1716
+ updated_at: isoDate(stat.mtimeMs)
1717
+ };
1718
+ } catch {
1719
+ return {};
1720
+ }
1721
+ }
1722
+ function gitFileMetadata(ref, path, knownSize) {
1723
+ const size = knownSize ?? rawFileSize(path, ref);
1724
+ const commitUpdatedAt = lastCommitDateForPath(ref, path, cwd) || undefined;
1725
+ return {
1726
+ size: size == null ? undefined : size,
1727
+ updated_at: commitUpdatedAt,
1728
+ commit_updated_at: commitUpdatedAt
1729
+ };
1730
+ }
1731
+ function directoryMetadata(target, path) {
1732
+ if (target === "worktree" || target === "") {
1733
+ const full = path === "" ? safeOpenWorktreePath("") : safeWorktreePath(path);
1734
+ if (!full)
1735
+ return {};
1736
+ try {
1737
+ const stat = statSync(full);
1738
+ return {
1739
+ created_at: isoDate(stat.birthtimeMs),
1740
+ updated_at: isoDate(stat.mtimeMs)
1741
+ };
1742
+ } catch {
1743
+ return {};
1744
+ }
1745
+ }
1746
+ const commitUpdatedAt = lastCommitDateForPath(target, path || ".", cwd) || undefined;
1747
+ return { updated_at: commitUpdatedAt, commit_updated_at: commitUpdatedAt };
1748
+ }
1749
+ function fileMetadataForTarget(target, path) {
1750
+ return target === "worktree" || target === "" ? worktreeFileMetadata(path) : gitFileMetadata(target, path);
1751
+ }
1752
+ function attachTreeEntryMetadata(target, entry) {
1753
+ if (entry.type === "tree")
1754
+ return { ...entry, ...directoryMetadata(target, entry.path) };
1755
+ if (entry.type !== "blob")
1756
+ return entry;
1757
+ return { ...entry, ...fileMetadataForTarget(target, entry.path) };
1758
+ }
1759
+ function fileMetadataHeaders(metadata) {
1760
+ const headers = {};
1761
+ if (metadata.created_at)
1762
+ headers["X-Code-Viewer-Created-At"] = metadata.created_at;
1763
+ if (metadata.updated_at)
1764
+ headers["X-Code-Viewer-Updated-At"] = metadata.updated_at;
1765
+ if (metadata.commit_updated_at)
1766
+ headers["X-Code-Viewer-Commit-Updated-At"] = metadata.commit_updated_at;
1767
+ return headers;
1768
+ }
1697
1769
  function readReadme(target, dirPath) {
1698
1770
  const candidates = ["README.md", "readme.md", "README.markdown", "README"];
1699
1771
  for (const name of candidates) {
@@ -1735,7 +1807,7 @@ function handleTree(url) {
1735
1807
  path,
1736
1808
  project: basename2(cwd),
1737
1809
  branch: currentBranch(cwd) || undefined,
1738
- entries,
1810
+ entries: recursive ? entries : entries.map((entry) => attachTreeEntryMetadata(target, entry)),
1739
1811
  readme: readReadme(target, path),
1740
1812
  upload_enabled: allowUpload && (target === "worktree" || target === "")
1741
1813
  });
@@ -2169,13 +2241,18 @@ function handleRawFile(req, url) {
2169
2241
  const size = rawFileSize(path, ref);
2170
2242
  if (size == null)
2171
2243
  return text("not in ref", 404);
2244
+ const metadata = gitFileMetadata(ref, path, size);
2172
2245
  if (req.method === "HEAD")
2173
- return new Response(null, { headers: rawFileHeaders(path, size) });
2246
+ return new Response(null, {
2247
+ headers: rawFileHeaders(path, size, undefined, metadata)
2248
+ });
2174
2249
  const res = showBytes(ref, path, cwd);
2175
2250
  if (res.code !== 0)
2176
2251
  return text("not in ref", 404);
2177
2252
  body = res.stdout.buffer.slice(res.stdout.byteOffset, res.stdout.byteOffset + res.stdout.byteLength);
2178
- return new Response(body, { headers: rawFileHeaders(path, size) });
2253
+ return new Response(body, {
2254
+ headers: rawFileHeaders(path, size, undefined, metadata)
2255
+ });
2179
2256
  } else {
2180
2257
  const full = safeWorktreePath(path);
2181
2258
  if (!full)
@@ -2183,12 +2260,13 @@ function handleRawFile(req, url) {
2183
2260
  const size = rawFileSize(path, ref);
2184
2261
  if (size == null)
2185
2262
  return text("not found", 404);
2263
+ const metadata = worktreeFileMetadata(path, size);
2186
2264
  const rangeResult = req.headers.get("range") ? parseHttpByteRange(req.headers.get("range"), size) : null;
2187
2265
  if (rangeResult?.kind === "unsatisfiable") {
2188
2266
  return new Response(null, {
2189
2267
  status: 416,
2190
2268
  headers: {
2191
- ...rawFileHeaders(path, size),
2269
+ ...rawFileHeaders(path, size, undefined, metadata),
2192
2270
  "Content-Range": `bytes */${size}`,
2193
2271
  "Content-Length": "0"
2194
2272
  }
@@ -2199,18 +2277,20 @@ function handleRawFile(req, url) {
2199
2277
  if (req.method === "HEAD") {
2200
2278
  return new Response(null, {
2201
2279
  status: 206,
2202
- headers: rawFileHeaders(path, size, range)
2280
+ headers: rawFileHeaders(path, size, range, metadata)
2203
2281
  });
2204
2282
  }
2205
2283
  return new Response(fileByteRangeResponseBody(full, range.start, range.end), {
2206
2284
  status: 206,
2207
- headers: rawFileHeaders(path, size, range)
2285
+ headers: rawFileHeaders(path, size, range, metadata)
2208
2286
  });
2209
2287
  }
2210
2288
  if (req.method === "HEAD")
2211
- return new Response(null, { headers: rawFileHeaders(path, size) });
2289
+ return new Response(null, {
2290
+ headers: rawFileHeaders(path, size, undefined, metadata)
2291
+ });
2212
2292
  return new Response(fileReadableStream(full), {
2213
- headers: rawFileHeaders(path, size)
2293
+ headers: rawFileHeaders(path, size, undefined, metadata)
2214
2294
  });
2215
2295
  }
2216
2296
  }
@@ -2230,7 +2310,7 @@ function rawFileSize(path, ref) {
2230
2310
  return null;
2231
2311
  }
2232
2312
  }
2233
- function rawFileHeaders(path, size = null, range) {
2313
+ function rawFileHeaders(path, size = null, range, metadata = {}) {
2234
2314
  const mime = {
2235
2315
  ".png": "image/png",
2236
2316
  ".jpg": "image/jpeg",
@@ -2263,6 +2343,9 @@ function rawFileHeaders(path, size = null, range) {
2263
2343
  } else if (size != null) {
2264
2344
  headers["Content-Length"] = String(size);
2265
2345
  }
2346
+ for (const [key, value] of Object.entries(fileMetadataHeaders(metadata))) {
2347
+ headers[key] = value;
2348
+ }
2266
2349
  return headers;
2267
2350
  }
2268
2351
  function isForbiddenUploadName(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -6942,6 +6942,10 @@ ${frontmatter.yaml}
6942
6942
  let SIDEBAR_ROW_BY_PATH = new Map;
6943
6943
  let SIDEBAR_VIRTUAL_ACTIVE_PATH = "";
6944
6944
  const SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
6945
+ let REPO_SORT = {
6946
+ key: "name",
6947
+ direction: "asc"
6948
+ };
6945
6949
  let SERVER_SCOPE_OMIT_DIRS_DEFAULT = [];
6946
6950
  let PENDING_G_SCOPE = null;
6947
6951
  let PENDING_G_UNTIL = 0;
@@ -9130,69 +9134,81 @@ ${frontmatter.yaml}
9130
9134
  if (meta.upload_enabled && (meta.ref === "worktree" || meta.ref === "")) {
9131
9135
  listWrapper.appendChild(createRepoUploadPanel(meta.path || ""));
9132
9136
  }
9137
+ const sortHost = document.createElement("div");
9138
+ sortHost.className = "gdp-repo-sort-host";
9133
9139
  const list2 = document.createElement("div");
9134
9140
  list2.className = "gdp-source-viewer gdp-repo-file-list";
9135
- if (meta.path) {
9136
- const parent = meta.path.split("/").slice(0, -1).join("/");
9137
- const row = document.createElement("button");
9138
- row.type = "button";
9139
- row.className = "gdp-repo-row parent";
9140
- const parentIcon = document.createElement("span");
9141
- parentIcon.className = "dir-icon";
9142
- setFolderIcon(parentIcon, false);
9143
- const parentName = document.createElement("span");
9144
- parentName.className = "name";
9145
- parentName.textContent = "..";
9146
- const parentKind = document.createElement("span");
9147
- parentKind.className = "kind";
9148
- parentKind.textContent = "parent";
9149
- row.append(parentIcon, parentName, parentKind);
9150
- row.addEventListener("click", () => {
9151
- setRoute(repoRoute(meta.ref, parent));
9152
- loadRepo();
9153
- });
9154
- list2.appendChild(row);
9155
- }
9156
- meta.entries.forEach((entry) => {
9157
- const row = document.createElement("button");
9158
- row.type = "button";
9159
- row.className = `gdp-repo-row ${entry.type}`;
9160
- const icon = document.createElement("span");
9161
- icon.className = entry.type === "tree" ? "dir-icon" : "d2h-icon-wrapper";
9162
- if (entry.type === "tree")
9163
- setFolderIcon(icon, true);
9164
- else
9165
- icon.innerHTML = fileEntryIcon();
9166
- const name = document.createElement("span");
9167
- name.className = "name";
9168
- name.textContent = entry.name;
9169
- const kind = document.createElement("span");
9170
- kind.className = "kind";
9171
- kind.textContent = entry.type === "tree" ? "directory" : entry.type === "commit" ? "submodule" : "file";
9172
- row.append(icon, name, kind);
9173
- row.addEventListener("click", () => {
9174
- if (entry.type === "tree") {
9175
- setRoute(repoRoute(meta.ref, entry.path));
9141
+ const renderRepoRows = (focusSortKey) => {
9142
+ sortHost.replaceChildren(createRepoSortHeader(renderRepoRows));
9143
+ if (focusSortKey) {
9144
+ sortHost.querySelector(`[data-repo-sort="${focusSortKey}"]`)?.focus();
9145
+ }
9146
+ list2.replaceChildren();
9147
+ if (meta.path) {
9148
+ const parent = meta.path.split("/").slice(0, -1).join("/");
9149
+ const row = document.createElement("button");
9150
+ row.type = "button";
9151
+ row.className = "gdp-repo-row parent";
9152
+ const parentIcon = document.createElement("span");
9153
+ parentIcon.className = "dir-icon";
9154
+ setFolderIcon(parentIcon, false);
9155
+ const parentName = document.createElement("span");
9156
+ parentName.className = "name";
9157
+ parentName.textContent = "..";
9158
+ const parentKind = document.createElement("span");
9159
+ parentKind.className = "meta";
9160
+ parentKind.textContent = "";
9161
+ const parentSize = document.createElement("span");
9162
+ parentSize.className = "size";
9163
+ row.append(parentIcon, parentName, parentKind, parentSize);
9164
+ row.addEventListener("click", () => {
9165
+ setRoute(repoRoute(meta.ref, parent));
9176
9166
  loadRepo();
9177
- } else if (entry.type === "blob") {
9178
- setRoute({
9179
- screen: "file",
9180
- path: entry.path,
9181
- ref: meta.ref,
9182
- view: "blob",
9183
- range: currentRange()
9184
- });
9185
- renderStandaloneSource({ path: entry.path, ref: meta.ref });
9186
- }
9167
+ });
9168
+ list2.appendChild(row);
9169
+ }
9170
+ sortedRepoEntries(meta.entries).forEach((entry) => {
9171
+ const row = document.createElement("button");
9172
+ row.type = "button";
9173
+ row.className = `gdp-repo-row ${entry.type}`;
9174
+ const icon = document.createElement("span");
9175
+ icon.className = entry.type === "tree" ? "dir-icon" : "d2h-icon-wrapper";
9176
+ if (entry.type === "tree")
9177
+ setFolderIcon(icon, true);
9178
+ else
9179
+ icon.innerHTML = fileEntryIcon();
9180
+ const name = document.createElement("span");
9181
+ name.className = "name";
9182
+ name.textContent = entry.name;
9183
+ const metaBlock = createRepoEntryMeta(entry);
9184
+ const size = createRepoEntrySize(entry);
9185
+ row.append(icon, name, metaBlock, size);
9186
+ row.addEventListener("click", () => {
9187
+ if (entry.type === "tree") {
9188
+ setRoute(repoRoute(meta.ref, entry.path));
9189
+ loadRepo();
9190
+ } else if (entry.type === "blob") {
9191
+ setRoute({
9192
+ screen: "file",
9193
+ path: entry.path,
9194
+ ref: meta.ref,
9195
+ view: "blob",
9196
+ range: currentRange()
9197
+ });
9198
+ renderStandaloneSource({ path: entry.path, ref: meta.ref });
9199
+ }
9200
+ });
9201
+ list2.appendChild(row);
9187
9202
  });
9188
- list2.appendChild(row);
9189
- });
9190
- if (!meta.entries.length) {
9191
- const empty = document.createElement("div");
9192
- empty.className = "gdp-repo-empty";
9193
- empty.textContent = "No files in this directory.";
9194
- list2.appendChild(empty);
9195
- }
9203
+ if (!meta.entries.length) {
9204
+ const empty = document.createElement("div");
9205
+ empty.className = "gdp-repo-empty";
9206
+ empty.textContent = "No files in this directory.";
9207
+ list2.appendChild(empty);
9208
+ }
9209
+ };
9210
+ listWrapper.appendChild(sortHost);
9211
+ renderRepoRows();
9196
9212
  listWrapper.appendChild(list2);
9197
9213
  listCard.appendChild(listWrapper);
9198
9214
  shell.appendChild(listCard);
@@ -10335,6 +10351,117 @@ ${frontmatter.yaml}
10335
10351
  }
10336
10352
  return (unit === 0 ? String(value) : value.toFixed(value >= 10 ? 1 : 2).replace(/\.0+$/, "")) + " " + units[unit];
10337
10353
  }
10354
+ function formatFileDate(value) {
10355
+ if (!value)
10356
+ return "";
10357
+ const date = new Date(value);
10358
+ if (Number.isNaN(date.getTime()))
10359
+ return "";
10360
+ return date.toLocaleString(undefined, {
10361
+ year: "numeric",
10362
+ month: "short",
10363
+ day: "numeric",
10364
+ hour: "2-digit",
10365
+ minute: "2-digit"
10366
+ });
10367
+ }
10368
+ function createRepoEntryMeta(entry) {
10369
+ const meta = document.createElement("span");
10370
+ meta.className = "meta";
10371
+ const updated = formatFileDate(entry.updated_at || entry.commit_updated_at);
10372
+ const created = formatFileDate(entry.created_at);
10373
+ if (entry.type === "tree" && updated) {
10374
+ meta.textContent = updated;
10375
+ if (created)
10376
+ meta.title = `Created ${created}`;
10377
+ return meta;
10378
+ }
10379
+ if (entry.type !== "blob") {
10380
+ meta.textContent = "-";
10381
+ return meta;
10382
+ }
10383
+ meta.textContent = updated ? updated : created ? created : "-";
10384
+ if (created)
10385
+ meta.title = `Created ${created}`;
10386
+ return meta;
10387
+ }
10388
+ function createRepoEntrySize(entry) {
10389
+ const size = document.createElement("span");
10390
+ size.className = "size";
10391
+ size.textContent = entry.type === "blob" && entry.size != null ? formatBytes(entry.size) : "";
10392
+ return size;
10393
+ }
10394
+ function repoEntryUpdatedTime(entry) {
10395
+ const raw = entry.updated_at || entry.commit_updated_at || entry.created_at;
10396
+ if (!raw)
10397
+ return -1;
10398
+ const time = new Date(raw).getTime();
10399
+ return Number.isNaN(time) ? -1 : time;
10400
+ }
10401
+ function sortedRepoEntries(entries) {
10402
+ const direction = REPO_SORT.direction === "asc" ? 1 : -1;
10403
+ return [...entries].sort((a2, b2) => {
10404
+ if (REPO_SORT.key === "name" && a2.type !== b2.type) {
10405
+ if (a2.type === "tree")
10406
+ return -1;
10407
+ if (b2.type === "tree")
10408
+ return 1;
10409
+ }
10410
+ let result = 0;
10411
+ if (REPO_SORT.key === "updated") {
10412
+ const aTime = repoEntryUpdatedTime(a2);
10413
+ const bTime = repoEntryUpdatedTime(b2);
10414
+ if (aTime < 0 && bTime >= 0)
10415
+ return 1;
10416
+ if (bTime < 0 && aTime >= 0)
10417
+ return -1;
10418
+ result = aTime - bTime;
10419
+ } else if (REPO_SORT.key === "size") {
10420
+ if (a2.size == null && b2.size != null)
10421
+ return 1;
10422
+ if (b2.size == null && a2.size != null)
10423
+ return -1;
10424
+ result = (a2.size ?? 0) - (b2.size ?? 0);
10425
+ } else {
10426
+ result = a2.name.localeCompare(b2.name);
10427
+ }
10428
+ if (result === 0)
10429
+ result = a2.name.localeCompare(b2.name);
10430
+ return result * direction;
10431
+ });
10432
+ }
10433
+ function createRepoSortHeader(onSortChange) {
10434
+ const header = document.createElement("div");
10435
+ header.className = "gdp-repo-sort-header";
10436
+ const spacer = document.createElement("span");
10437
+ spacer.className = "gdp-repo-sort-spacer";
10438
+ header.appendChild(spacer);
10439
+ const columns = [
10440
+ { key: "name", label: "Name" },
10441
+ { key: "updated", label: "Updated" },
10442
+ { key: "size", label: "Size" }
10443
+ ];
10444
+ columns.forEach((column) => {
10445
+ const button = document.createElement("button");
10446
+ button.type = "button";
10447
+ button.dataset.repoSort = column.key;
10448
+ button.textContent = column.label + (REPO_SORT.key === column.key ? REPO_SORT.direction === "asc" ? " ↑" : " ↓" : "");
10449
+ button.className = REPO_SORT.key === column.key ? "active" : "";
10450
+ button.addEventListener("click", () => {
10451
+ if (REPO_SORT.key === column.key) {
10452
+ REPO_SORT.direction = REPO_SORT.direction === "asc" ? "desc" : "asc";
10453
+ } else {
10454
+ REPO_SORT = {
10455
+ key: column.key,
10456
+ direction: column.key === "name" ? "asc" : "desc"
10457
+ };
10458
+ }
10459
+ onSortChange(column.key);
10460
+ });
10461
+ header.appendChild(button);
10462
+ });
10463
+ return header;
10464
+ }
10338
10465
  function humanFileKind(path, mime, fallback) {
10339
10466
  const ext = (path.split(".").pop() || "").toLowerCase();
10340
10467
  if (ext === "png")
@@ -10392,12 +10519,41 @@ ${frontmatter.yaml}
10392
10519
  const size = rawSize == null ? NaN : Number(rawSize);
10393
10520
  return {
10394
10521
  size: rawSize != null && Number.isFinite(size) ? size : undefined,
10395
- type: res.headers.get("content-type") || undefined
10522
+ type: res.headers.get("content-type") || undefined,
10523
+ created_at: res.headers.get("x-code-viewer-created-at") || undefined,
10524
+ updated_at: res.headers.get("x-code-viewer-updated-at") || undefined,
10525
+ commit_updated_at: res.headers.get("x-code-viewer-commit-updated-at") || undefined
10396
10526
  };
10397
10527
  } catch {
10398
10528
  return {};
10399
10529
  }
10400
10530
  }
10531
+ function createFileDetailMeta(target, meta) {
10532
+ const wrap = document.createElement("div");
10533
+ wrap.className = "gdp-file-detail-meta";
10534
+ const addItem = (label, value) => {
10535
+ if (!value)
10536
+ return;
10537
+ const item = document.createElement("span");
10538
+ item.className = "gdp-file-detail-meta-item";
10539
+ const labelEl = document.createElement("span");
10540
+ labelEl.className = "label";
10541
+ labelEl.textContent = label;
10542
+ const valueEl = document.createElement("span");
10543
+ valueEl.className = "value";
10544
+ valueEl.textContent = value;
10545
+ item.append(labelEl, valueEl);
10546
+ wrap.appendChild(item);
10547
+ };
10548
+ addItem("Size", meta.size == null ? "" : formatBytes(meta.size));
10549
+ addItem("Updated", formatFileDate(meta.updated_at || meta.commit_updated_at));
10550
+ addItem("Created", formatFileDate(meta.created_at));
10551
+ if (!wrap.childElementCount) {
10552
+ wrap.hidden = true;
10553
+ wrap.dataset.path = target.path;
10554
+ }
10555
+ return wrap;
10556
+ }
10401
10557
  function createSourceFileInfo(target, kind) {
10402
10558
  const info = document.createElement("div");
10403
10559
  info.className = "gdp-source-file-info";
@@ -11423,6 +11579,11 @@ ${frontmatter.yaml}
11423
11579
  name.appendChild(copy);
11424
11580
  name.appendChild(createOpenPathButton(target.path, "file-parent", "open parent folder in OS"));
11425
11581
  header.appendChild(name);
11582
+ loadRawFileInfo(target).then((meta) => {
11583
+ if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
11584
+ return;
11585
+ header.appendChild(createFileDetailMeta(target, meta));
11586
+ });
11426
11587
  if (!repoTarget) {
11427
11588
  const back = document.createElement("button");
11428
11589
  back.type = "button";
package/web/style.css CHANGED
@@ -2515,6 +2515,7 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2515
2515
  .gdp-file-detail-header {
2516
2516
  display: flex;
2517
2517
  align-items: center;
2518
+ flex-wrap: wrap;
2518
2519
  gap: 12px;
2519
2520
  min-height: 34px;
2520
2521
  margin-bottom: 0;
@@ -2589,6 +2590,39 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2589
2590
  flex-shrink: 0;
2590
2591
  color: var(--fg-muted);
2591
2592
  }
2593
+ .gdp-file-detail-meta {
2594
+ display: flex;
2595
+ align-items: center;
2596
+ flex-wrap: wrap;
2597
+ gap: 0;
2598
+ color: var(--fg-muted);
2599
+ font-size: 12px;
2600
+ line-height: 18px;
2601
+ }
2602
+ .gdp-file-detail-meta-item {
2603
+ display: inline-flex;
2604
+ align-items: baseline;
2605
+ gap: 6px;
2606
+ padding: 0 10px;
2607
+ border-right: 1px solid var(--border-muted);
2608
+ white-space: nowrap;
2609
+ }
2610
+ .gdp-file-detail-meta-item:first-child {
2611
+ padding-left: 0;
2612
+ }
2613
+ .gdp-file-detail-meta-item:last-child {
2614
+ border-right: 0;
2615
+ padding-right: 0;
2616
+ }
2617
+ .gdp-file-detail-meta-item .label {
2618
+ font-size: 11px;
2619
+ font-weight: 600;
2620
+ color: var(--fg-subtle);
2621
+ }
2622
+ .gdp-file-detail-meta-item .value {
2623
+ color: var(--fg);
2624
+ font-weight: 500;
2625
+ }
2592
2626
  .gdp-markdown-preview {
2593
2627
  width: 100%;
2594
2628
  max-width: none;
@@ -3124,10 +3158,46 @@ body.gdp-file-detail-page #empty {
3124
3158
  padding: 0;
3125
3159
  overflow: hidden;
3126
3160
  }
3161
+ .gdp-repo-sort-header {
3162
+ display: grid;
3163
+ grid-template-columns: 28px minmax(0, 1fr) minmax(150px, 220px) 80px;
3164
+ column-gap: 8px;
3165
+ align-items: center;
3166
+ min-height: 34px;
3167
+ padding: 0 14px;
3168
+ border-top: 1px solid var(--border-muted);
3169
+ border-bottom: 1px solid var(--border-muted);
3170
+ background: var(--bg-soft);
3171
+ }
3172
+ .gdp-repo-sort-spacer {
3173
+ display: block;
3174
+ }
3175
+ .gdp-repo-sort-header button {
3176
+ appearance: none;
3177
+ height: 100%;
3178
+ border: 0;
3179
+ background: transparent;
3180
+ color: var(--fg-muted);
3181
+ font: inherit;
3182
+ font-size: 12px;
3183
+ font-weight: 600;
3184
+ text-align: left;
3185
+ cursor: pointer;
3186
+ line-height: 20px;
3187
+ padding: 0;
3188
+ }
3189
+ .gdp-repo-sort-header button:hover,
3190
+ .gdp-repo-sort-header button.active {
3191
+ color: var(--fg);
3192
+ }
3193
+ .gdp-repo-sort-header button[data-repo-sort="size"] {
3194
+ text-align: right;
3195
+ }
3127
3196
  .gdp-repo-row {
3128
3197
  display: grid;
3129
- grid-template-columns: 28px minmax(0, 1fr) 120px;
3198
+ grid-template-columns: 28px minmax(0, 1fr) minmax(150px, 220px) 80px;
3130
3199
  align-items: center;
3200
+ column-gap: 8px;
3131
3201
  width: 100%;
3132
3202
  min-height: 42px;
3133
3203
  border: 0;
@@ -3173,6 +3243,20 @@ body.gdp-file-detail-page #empty {
3173
3243
  color: var(--fg-muted);
3174
3244
  font-size: 12px;
3175
3245
  }
3246
+ .gdp-repo-row .meta {
3247
+ overflow: hidden;
3248
+ text-overflow: ellipsis;
3249
+ white-space: nowrap;
3250
+ color: var(--fg-muted);
3251
+ font-size: 12px;
3252
+ }
3253
+ .gdp-repo-row .size {
3254
+ justify-self: end;
3255
+ min-width: 0;
3256
+ color: var(--fg-muted);
3257
+ font-size: 12px;
3258
+ white-space: nowrap;
3259
+ }
3176
3260
  .gdp-repo-empty {
3177
3261
  padding: 32px;
3178
3262
  color: var(--fg-muted);