@youtyan/code-viewer 0.1.26 → 0.1.27

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/README.md CHANGED
@@ -80,30 +80,21 @@ the full non-virtual view.
80
80
 
81
81
  ## Uploads
82
82
 
83
- File uploads are disabled by default. Enable them only for trusted local
84
- worktrees:
85
-
86
- ```sh
87
- code-viewer --cwd /path/to/repo --allow-upload
88
- ```
83
+ File uploads are available for the local worktree target. Git tree views remain
84
+ read-only.
89
85
 
90
- Or place `.code-viewer.json` at the repository root:
86
+ Place `.code-viewer.json` at the repository root to configure repository scope
87
+ defaults:
91
88
 
92
89
  ```json
93
90
  {
94
91
  "version": 1,
95
- "upload": {
96
- "enabled": true
97
- },
98
92
  "scope": {
99
93
  "omitDirs": ["node_modules", "dist", "build"]
100
94
  }
101
95
  }
102
96
  ```
103
97
 
104
- Uploads are accepted only for the worktree target. Git tree views remain
105
- read-only.
106
-
107
98
  Repository scope settings control recursive repository browsing and search scope
108
99
  for the left tree, Ctrl+K file palette, and Ctrl+G grep palette. The in-app Scope
109
100
  Settings popover stores only a browser-local override in localStorage; edit
@@ -19,6 +19,23 @@ import {
19
19
  import { homedir } from "node:os";
20
20
  import { basename as basename2, dirname as dirname2, extname, join as join4, relative } from "node:path";
21
21
 
22
+ // web-src/directory-name.ts
23
+ function normalizeNewDirectoryName(name) {
24
+ if (typeof name !== "string")
25
+ return null;
26
+ const trimmed = name.trim();
27
+ if (!trimmed || trimmed.length > 180)
28
+ return null;
29
+ if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("\x00") || Array.from(trimmed).some((char) => {
30
+ const code = char.charCodeAt(0);
31
+ return code < 32 || code === 127;
32
+ }))
33
+ return null;
34
+ if (trimmed === "." || trimmed === ".." || trimmed.toLowerCase() === ".git")
35
+ return null;
36
+ return trimmed;
37
+ }
38
+
22
39
  // web-src/routes.ts
23
40
  var SPA_PATHS = ["/todif", "/todiff", "/file", "/help"];
24
41
  var APP_ENTRY_PATHS = ["/", "/index.html"];
@@ -1236,8 +1253,8 @@ var SIZE_LARGE = 20000;
1236
1253
  var LINE_INDEX_MIN_START = 1e4;
1237
1254
  var LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
1238
1255
  var BLOB_LINE_CACHE_MAX_BYTES = 128 * 1024 * 1024;
1239
- var MAX_UPLOAD_FILE_BYTES = 10 * 1024 * 1024;
1240
- var MAX_UPLOAD_TOTAL_BYTES = 50 * 1024 * 1024;
1256
+ var MAX_UPLOAD_FILE_BYTES = 512 * 1024 * 1024;
1257
+ var MAX_UPLOAD_TOTAL_BYTES = 1024 * 1024 * 1024;
1241
1258
  var MAX_UPLOAD_BODY_BYTES = MAX_UPLOAD_TOTAL_BYTES + 1024 * 1024;
1242
1259
  var MAX_UPLOAD_FILES = 50;
1243
1260
  var SAFE_UPLOAD_EXTENSIONS = new Set([
@@ -1255,7 +1272,19 @@ var SAFE_UPLOAD_EXTENSIONS = new Set([
1255
1272
  ".jpeg",
1256
1273
  ".gif",
1257
1274
  ".webp",
1275
+ ".svg",
1258
1276
  ".pdf",
1277
+ ".mp4",
1278
+ ".mov",
1279
+ ".m4v",
1280
+ ".webm",
1281
+ ".mp3",
1282
+ ".wav",
1283
+ ".m4a",
1284
+ ".aac",
1285
+ ".flac",
1286
+ ".ogg",
1287
+ ".zip",
1259
1288
  ".ts",
1260
1289
  ".tsx",
1261
1290
  ".js",
@@ -1268,12 +1297,11 @@ var generation = 1;
1268
1297
  var cwd = repoRoot(process.cwd()) || process.cwd();
1269
1298
  var cliArgs = DEFAULT_ARGS;
1270
1299
  var listenPort = 0;
1271
- var allowUpload = false;
1272
- var uploadAllowedByCli = false;
1273
1300
  var openAfterStart = false;
1274
1301
  var scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
1275
1302
  var scopeOmitDirCliOverride = null;
1276
1303
  var scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
1304
+ var uploadDisabledByConfig = false;
1277
1305
  var rgAvailableCache = null;
1278
1306
  var enc = new TextEncoder;
1279
1307
  var sseClients = new Set;
@@ -1326,10 +1354,7 @@ Examples:
1326
1354
  listenPort = parsed;
1327
1355
  } else if (arg === "--open") {
1328
1356
  openAfterStart = true;
1329
- } else if (arg === "--allow-upload") {
1330
- allowUpload = true;
1331
- uploadAllowedByCli = true;
1332
- } else if (arg === "--scope-omit-dir") {
1357
+ } else if (arg === "--allow-upload") {} else if (arg === "--scope-omit-dir") {
1333
1358
  const next = process.argv[++i];
1334
1359
  if (!next) {
1335
1360
  console.error("--scope-omit-dir requires a directory name");
@@ -1345,10 +1370,9 @@ Examples:
1345
1370
  }
1346
1371
  if (rest.length)
1347
1372
  cliArgs = rest;
1348
- if (!uploadAllowedByCli)
1349
- allowUpload = loadProjectConfigUploadEnabled();
1350
1373
  const configScopeOmitDirs = loadProjectConfigScopeOmitDirs();
1351
1374
  const configScopeExcludeNames = loadProjectConfigScopeExcludeNames();
1375
+ uploadDisabledByConfig = loadProjectConfigUploadDisabled();
1352
1376
  if (scopeOmitDirCliOverride) {
1353
1377
  scopeOmitDirNames = scopeOmitDirCliOverride;
1354
1378
  } else if (configScopeOmitDirs) {
@@ -1680,9 +1704,9 @@ function loadProjectConfig() {
1680
1704
  return null;
1681
1705
  }
1682
1706
  }
1683
- function loadProjectConfigUploadEnabled() {
1707
+ function loadProjectConfigUploadDisabled() {
1684
1708
  const config = loadProjectConfig();
1685
- return config?.upload?.enabled === true;
1709
+ return config?.upload?.enabled === false;
1686
1710
  }
1687
1711
  function loadProjectConfigScopeOmitDirs() {
1688
1712
  const config = loadProjectConfig();
@@ -1873,7 +1897,7 @@ function handleTree(url) {
1873
1897
  branch: currentBranch(cwd) || undefined,
1874
1898
  entries: recursive ? entries : entries.map((entry) => attachTreeEntryMetadata(target, entry)),
1875
1899
  readme: readReadme(target, path),
1876
- upload_enabled: allowUpload && (target === "worktree" || target === "")
1900
+ upload_enabled: !uploadDisabledByConfig && (target === "worktree" || target === "")
1877
1901
  });
1878
1902
  }
1879
1903
  function handleSettings() {
@@ -2430,24 +2454,26 @@ function isForbiddenUploadName(name) {
2430
2454
  return lower.startsWith(".") || lower === "package.json" || lower === "package-lock.json" || lower === "bun.lock" || lower === "bun.lockb" || lower === "yarn.lock" || lower === "pnpm-lock.yaml" || lower === "makefile" || lower === "dockerfile" || lower.endsWith(".dockerfile") || /^(tsconfig|jsconfig|bunfig|vercel|netlify|wrangler|next|vite|webpack|rollup|esbuild|astro|svelte|tailwind|postcss|babel|prettier|eslint)\./.test(lower) || lower.endsWith(".config.js") || lower.endsWith(".config.jsx") || lower.endsWith(".config.ts") || lower.endsWith(".config.tsx") || lower.endsWith(".config.mjs") || lower.endsWith(".config.cjs") || lower.includes("credential") || lower.includes("secret") || lower.endsWith(".exe") || lower.endsWith(".dll") || lower.endsWith(".dylib") || lower.endsWith(".so") || lower.endsWith(".sh") || lower.endsWith(".bash") || lower.endsWith(".zsh") || lower.endsWith(".fish") || lower.endsWith(".ps1") || lower.endsWith(".bat") || lower.endsWith(".cmd");
2431
2455
  }
2432
2456
  function safeUploadFileName(name) {
2433
- if (!name || name.includes("\x00") || name.includes("/") || name.includes("\\"))
2457
+ const trimmed = name.trim();
2458
+ if (!trimmed || trimmed.length > 180 || trimmed.includes("\x00") || trimmed.includes("/") || trimmed.includes("\\") || Array.from(trimmed).some((char) => {
2459
+ const code = char.charCodeAt(0);
2460
+ return code < 32 || code === 127;
2461
+ }))
2434
2462
  return null;
2435
- if (name === "." || name === "..")
2463
+ if (trimmed === "." || trimmed === "..")
2436
2464
  return null;
2437
- if (!/^[A-Za-z0-9][A-Za-z0-9._ -]{0,180}$/.test(name))
2465
+ if (isGitInternalPath(trimmed) || isForbiddenUploadName(trimmed))
2438
2466
  return null;
2439
- if (isGitInternalPath(name) || isForbiddenUploadName(name))
2467
+ if (!SAFE_UPLOAD_EXTENSIONS.has(extname(trimmed).toLowerCase()))
2440
2468
  return null;
2441
- if (!SAFE_UPLOAD_EXTENSIONS.has(extname(name).toLowerCase()))
2442
- return null;
2443
- return name;
2469
+ return trimmed;
2444
2470
  }
2445
2471
  function uploadOpenFlags() {
2446
2472
  return constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0);
2447
2473
  }
2448
2474
  async function handleUploadFiles(req) {
2449
- if (!allowUpload)
2450
- return text("upload disabled", 403);
2475
+ if (uploadDisabledByConfig)
2476
+ return text("upload disabled by project config", 403);
2451
2477
  if (req.method !== "POST")
2452
2478
  return text("method not allowed", 405);
2453
2479
  if (!sideEffectRequestAllowed(req))
@@ -2778,6 +2804,61 @@ async function handleTrashPath(req) {
2778
2804
  sendSse("update");
2779
2805
  return json({ ok: true, generation, undo });
2780
2806
  }
2807
+ async function handleCreateDirectory(req) {
2808
+ if (req.method !== "POST")
2809
+ return text("method not allowed", 405);
2810
+ if (!sideEffectRequestAllowed(req))
2811
+ return text("forbidden", 403);
2812
+ const contentType = req.headers.get("content-type") || "";
2813
+ if (!/^application\/json(?:;|$)/i.test(contentType))
2814
+ return text("unsupported media type", 415);
2815
+ const lengthHeader = req.headers.get("content-length");
2816
+ const length = Number(lengthHeader || "0");
2817
+ if (lengthHeader && (!Number.isFinite(length) || length < 0))
2818
+ return text("invalid content length", 400);
2819
+ if (length > 2048)
2820
+ return text("payload too large", 413);
2821
+ let body = {};
2822
+ try {
2823
+ const raw = await req.text();
2824
+ if (raw.length > 2048)
2825
+ return text("payload too large", 413);
2826
+ body = JSON.parse(raw);
2827
+ } catch {
2828
+ return text("invalid json", 400);
2829
+ }
2830
+ const dir = typeof body.dir === "string" ? body.dir.trim().replace(/^\/+|\/+$/g, "") : "";
2831
+ const name = normalizeNewDirectoryName(body.name);
2832
+ if (!safeRepoPath(dir))
2833
+ return text("invalid dir", 400);
2834
+ if (dir && isGitInternalPath(dir))
2835
+ return text("forbidden", 403);
2836
+ if (!name)
2837
+ return text("invalid name", 400);
2838
+ const parent = safeOpenWorktreePath(dir);
2839
+ if (!parent)
2840
+ return text("not found", 404);
2841
+ const stats = statSync(parent);
2842
+ if (!stats.isDirectory())
2843
+ return text("not a directory", 400);
2844
+ const targetPath = dir ? `${dir}/${name}` : name;
2845
+ if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
2846
+ return text("invalid target", 400);
2847
+ const target = join4(parent, name);
2848
+ if (existsSync3(target))
2849
+ return text("already exists", 409);
2850
+ try {
2851
+ mkdirSync(target, { recursive: false });
2852
+ } catch (error) {
2853
+ if (error.code === "EEXIST")
2854
+ return text("already exists", 409);
2855
+ return text("create failed", 500);
2856
+ }
2857
+ generation++;
2858
+ clearMutableCaches();
2859
+ sendSse("update");
2860
+ return json({ ok: true, path: targetPath, generation });
2861
+ }
2781
2862
  async function handleRestoreTrash(req) {
2782
2863
  if (req.method !== "POST")
2783
2864
  return text("method not allowed", 405);
@@ -2864,6 +2945,8 @@ var server = await startServer({
2864
2945
  return handleTrashPath(req);
2865
2946
  if (url.pathname === "/_restore_trash")
2866
2947
  return handleRestoreTrash(req);
2948
+ if (url.pathname === "/_create_directory")
2949
+ return handleCreateDirectory(req);
2867
2950
  if (url.pathname === "/_upload_files")
2868
2951
  return handleUploadFiles(req);
2869
2952
  if (url.pathname === "/_refs")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -36,6 +36,23 @@
36
36
  };
37
37
  }
38
38
 
39
+ // web-src/directory-name.ts
40
+ function normalizeNewDirectoryName(name) {
41
+ if (typeof name !== "string")
42
+ return null;
43
+ const trimmed = name.trim();
44
+ if (!trimmed || trimmed.length > 180)
45
+ return null;
46
+ if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("\x00") || Array.from(trimmed).some((char) => {
47
+ const code = char.charCodeAt(0);
48
+ return code < 32 || code === 127;
49
+ }))
50
+ return null;
51
+ if (trimmed === "." || trimmed === ".." || trimmed.toLowerCase() === ".git")
52
+ return null;
53
+ return trimmed;
54
+ }
55
+
39
56
  // web-src/expand-logic.ts
40
57
  function initExpandState(prevHunkEndNew, hunkNewStart) {
41
58
  return {
@@ -145,6 +162,12 @@
145
162
  function filePathClipboardText(path) {
146
163
  return path || "";
147
164
  }
165
+ function fileNameClipboardText(path) {
166
+ if (!path)
167
+ return "";
168
+ const parts = path.split("/").filter(Boolean);
169
+ return parts[parts.length - 1] || "";
170
+ }
148
171
 
149
172
  // web-src/focus-scope.ts
150
173
  function isEditableKeyTarget(target) {
@@ -6578,8 +6601,8 @@ ${frontmatter.yaml}
6578
6601
  if (!section)
6579
6602
  return;
6580
6603
  e2.preventDefault();
6581
- section.scrollIntoView({ block: "start", behavior: "smooth" });
6582
6604
  history.replaceState(history.state, "", `#${encodeURIComponent(section.id)}`);
6605
+ scrollMarkdownSectionIntoView(section, "smooth");
6583
6606
  });
6584
6607
  const controller = new AbortController;
6585
6608
  const scrollRoot = document.scrollingElement || document.documentElement;
@@ -6596,8 +6619,9 @@ ${frontmatter.yaml}
6596
6619
  return;
6597
6620
  }
6598
6621
  let active = entries[0];
6622
+ const activeThreshold = markdownAnchorOffset() + 40;
6599
6623
  for (const entry of entries) {
6600
- if (entry.target.getBoundingClientRect().top <= 96)
6624
+ if (entry.target.getBoundingClientRect().top <= activeThreshold)
6601
6625
  active = entry;
6602
6626
  else
6603
6627
  break;
@@ -6622,9 +6646,43 @@ ${frontmatter.yaml}
6622
6646
  setTimeout(() => {
6623
6647
  if (!root.isConnected)
6624
6648
  return;
6649
+ scrollInitialMarkdownHash(root);
6625
6650
  update();
6626
6651
  }, 0);
6627
6652
  }
6653
+ function scrollInitialMarkdownHash(root) {
6654
+ if (!location.hash)
6655
+ return;
6656
+ const id = decodeHashFragment(location.hash);
6657
+ const section = root.querySelector(`#${CSS.escape(id)}`);
6658
+ if (!section)
6659
+ return;
6660
+ scrollMarkdownSectionIntoView(section, "auto");
6661
+ }
6662
+ function decodeHashFragment(hash) {
6663
+ const value = hash.startsWith("#") ? hash.slice(1) : hash;
6664
+ try {
6665
+ return decodeURIComponent(value);
6666
+ } catch {
6667
+ return value;
6668
+ }
6669
+ }
6670
+ function scrollMarkdownSectionIntoView(section, behavior) {
6671
+ const top = section.getBoundingClientRect().top + window.scrollY - markdownAnchorOffset() - 12;
6672
+ window.scrollTo({ top: Math.max(0, top), behavior });
6673
+ }
6674
+ function markdownAnchorOffset() {
6675
+ const bottoms = Array.from(document.querySelectorAll("#global-header, .gdp-file-detail-sticky")).map((element) => {
6676
+ const style = getComputedStyle(element);
6677
+ if (style.display === "none" || style.visibility === "hidden")
6678
+ return 0;
6679
+ const rect = element.getBoundingClientRect();
6680
+ if (rect.height <= 0)
6681
+ return 0;
6682
+ return rect.bottom > 0 ? rect.bottom : 0;
6683
+ }).filter((bottom) => Number.isFinite(bottom));
6684
+ return Math.max(0, ...bottoms);
6685
+ }
6628
6686
  function keepTocLinkVisible(toc, link2) {
6629
6687
  if (toc.scrollHeight <= toc.clientHeight)
6630
6688
  return;
@@ -6894,6 +6952,8 @@ ${frontmatter.yaml}
6894
6952
  ];
6895
6953
  const FILE_16_PATH = "M2 1.75C2 .784 2.784 0 3.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 12.25 16h-8.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 8 4.25V1.5Zm5.75.062V4.25c0 .138.112.25.25.25h2.688Z";
6896
6954
  const OPEN_EXTERNAL_16_PATH = "M3.75 2A1.75 1.75 0 0 0 2 3.75v8.5C2 13.216 2.784 14 3.75 14h8.5A1.75 1.75 0 0 0 14 12.25v-3.5a.75.75 0 0 0-1.5 0v3.5a.25.25 0 0 1-.25.25h-8.5a.25.25 0 0 1-.25-.25v-8.5a.25.25 0 0 1 .25-.25h3.5a.75.75 0 0 0 0-1.5h-3.5Zm6.5 0a.75.75 0 0 0 0 1.5h1.19L7.72 7.22a.749.749 0 1 0 1.06 1.06l3.72-3.72v1.19a.75.75 0 0 0 1.5 0v-3A.75.75 0 0 0 13.25 2h-3Z";
6955
+ const PLUS_16_PATH = "M7.75 2a.75.75 0 0 1 .75.75V7.5h4.75a.75.75 0 0 1 0 1.5H8.5v4.75a.75.75 0 0 1-1.5 0V9H2.25a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z";
6956
+ const TRASH_16_PATH = "M6.5 1.75A1.75 1.75 0 0 1 8.25 0h1.5A1.75 1.75 0 0 1 11.5 1.75V2h3.75a.75.75 0 0 1 0 1.5h-.75v10.75A1.75 1.75 0 0 1 12.75 16h-9.5A1.75 1.75 0 0 1 1.5 14.25V3.5H.75a.75.75 0 0 1 0-1.5H4.5v-.25ZM6 2h4v-.25a.25.25 0 0 0-.25-.25h-1.5a.25.25 0 0 0-.25.25V2Zm-3 1.5v10.75c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V3.5Zm3 2.25a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0v-5.5A.75.75 0 0 1 6 5.75Zm4 0a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0v-5.5A.75.75 0 0 1 10 5.75Z";
6897
6957
  const GIT_BRANCH_16_PATH = "M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z";
6898
6958
  const TRIANGLE_DOWN_16_PATH = "m4.427 7.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427Z";
6899
6959
  const SIDEBAR_SHOW_16_PATHS = [
@@ -6931,6 +6991,7 @@ ${frontmatter.yaml}
6931
6991
  let sourceShikiLoadPromise = null;
6932
6992
  let highlightConfigured = false;
6933
6993
  let PROJECT_NAME = "";
6994
+ let creatingDirectory = false;
6934
6995
  let REPO_SIDEBAR_REF = null;
6935
6996
  let REPO_SIDEBAR_LOAD_REF = null;
6936
6997
  let REPO_SIDEBAR_LOAD = null;
@@ -7276,6 +7337,11 @@ ${frontmatter.yaml}
7276
7337
  return;
7277
7338
  PROJECT_NAME = project;
7278
7339
  document.title = `${project} - code viewer`;
7340
+ const projectTitle = document.querySelector("#project-title");
7341
+ if (projectTitle) {
7342
+ projectTitle.textContent = project;
7343
+ projectTitle.title = project;
7344
+ }
7279
7345
  }
7280
7346
  function savedScopeOmitDirs() {
7281
7347
  const raw = localStorage.getItem(scopeOmitDirsStorageKey());
@@ -7850,6 +7916,7 @@ ${frontmatter.yaml}
7850
7916
  li.className = "tree-dir";
7851
7917
  li.tabIndex = -1;
7852
7918
  li.dataset.dirpath = dir.path;
7919
+ li.dataset.type = "tree";
7853
7920
  if (dir.children_omitted_reason)
7854
7921
  li.dataset.childrenOmittedReason = dir.children_omitted_reason;
7855
7922
  if (dir.explicit)
@@ -7937,6 +8004,7 @@ ${frontmatter.yaml}
7937
8004
  li.className = "tree-file";
7938
8005
  li.tabIndex = -1;
7939
8006
  li.dataset.path = f2.path;
8007
+ li.dataset.type = "blob";
7940
8008
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
7941
8009
  li.style.setProperty("--lvl-pad", `${12 + depth * 14}px`);
7942
8010
  const spacer = document.createElement("span");
@@ -7995,6 +8063,7 @@ ${frontmatter.yaml}
7995
8063
  li.className = "tree-dir";
7996
8064
  li.tabIndex = -1;
7997
8065
  li.dataset.dirpath = dir.path;
8066
+ li.dataset.type = "tree";
7998
8067
  if (dir.children_omitted_reason)
7999
8068
  li.dataset.childrenOmittedReason = dir.children_omitted_reason;
8000
8069
  if (dir.explicit)
@@ -8077,6 +8146,7 @@ ${frontmatter.yaml}
8077
8146
  li.className = "tree-file";
8078
8147
  li.tabIndex = -1;
8079
8148
  li.dataset.path = f2.path;
8149
+ li.dataset.type = "blob";
8080
8150
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
8081
8151
  li.classList.toggle("hidden-by-tests", STATE.hideTests && TEST_RE.test(f2.path || ""));
8082
8152
  li.style.setProperty("--lvl-pad", `${12 + depth * 14}px`);
@@ -9084,6 +9154,102 @@ ${frontmatter.yaml}
9084
9154
  createTrashDialog("Trash failed", message, [ok]);
9085
9155
  ok.focus();
9086
9156
  }
9157
+ function showCreateDirectoryError(message) {
9158
+ const ok = document.createElement("button");
9159
+ ok.type = "button";
9160
+ ok.className = "gdp-btn gdp-btn-sm";
9161
+ ok.textContent = "OK";
9162
+ ok.addEventListener("click", closeTrashDialog);
9163
+ createTrashDialog("New folder failed", message, [ok]);
9164
+ ok.focus();
9165
+ }
9166
+ function askNewDirectoryName(path, focusReturnTarget) {
9167
+ return new Promise((resolve) => {
9168
+ const previousFocus = focusReturnTarget || document.activeElement;
9169
+ const cancel = document.createElement("button");
9170
+ cancel.type = "button";
9171
+ cancel.className = "gdp-btn gdp-btn-sm";
9172
+ cancel.textContent = "Cancel";
9173
+ const create = document.createElement("button");
9174
+ create.type = "button";
9175
+ create.className = "gdp-btn gdp-btn-sm";
9176
+ create.textContent = "Create";
9177
+ const input = document.createElement("input");
9178
+ input.className = "gdp-create-dir-input";
9179
+ input.type = "text";
9180
+ input.autocomplete = "off";
9181
+ input.placeholder = "Folder name";
9182
+ input.setAttribute("aria-label", "Folder name");
9183
+ const error2 = document.createElement("div");
9184
+ error2.className = "gdp-create-dir-error";
9185
+ error2.setAttribute("role", "alert");
9186
+ const syncValidity = () => {
9187
+ const valid = !!normalizeNewDirectoryName(input.value);
9188
+ create.disabled = !valid;
9189
+ error2.textContent = input.value && !valid ? "Use a folder name without slashes, control characters, . or .." : "";
9190
+ return valid;
9191
+ };
9192
+ const done = (name) => {
9193
+ document.removeEventListener("keydown", onKeydown);
9194
+ closeTrashDialog();
9195
+ previousFocus?.focus?.();
9196
+ resolve(name);
9197
+ };
9198
+ const submit = () => {
9199
+ const name = normalizeNewDirectoryName(input.value);
9200
+ if (!name) {
9201
+ syncValidity();
9202
+ input.focus();
9203
+ return;
9204
+ }
9205
+ done(name);
9206
+ };
9207
+ const onKeydown = (event) => {
9208
+ if (event.key === "Escape") {
9209
+ event.preventDefault();
9210
+ event.stopPropagation();
9211
+ done(null);
9212
+ return;
9213
+ }
9214
+ if (event.isComposing || event.keyCode === 229)
9215
+ return;
9216
+ if (event.key === "Enter") {
9217
+ event.preventDefault();
9218
+ submit();
9219
+ return;
9220
+ }
9221
+ if (event.key !== "Tab")
9222
+ return;
9223
+ const focusables = [input, cancel, create];
9224
+ const index = focusables.indexOf(document.activeElement);
9225
+ if (index < 0) {
9226
+ event.preventDefault();
9227
+ focusables[0].focus();
9228
+ return;
9229
+ }
9230
+ if (event.shiftKey && index <= 0) {
9231
+ event.preventDefault();
9232
+ focusables[focusables.length - 1].focus();
9233
+ } else if (!event.shiftKey && index === focusables.length - 1) {
9234
+ event.preventDefault();
9235
+ focusables[0].focus();
9236
+ }
9237
+ };
9238
+ cancel.addEventListener("click", () => done(null));
9239
+ create.addEventListener("click", submit);
9240
+ input.addEventListener("input", syncValidity);
9241
+ create.disabled = true;
9242
+ const backdrop = createTrashDialog("New Folder", `Create a folder in "${path || PROJECT_NAME || "repository"}".`, [cancel, create]);
9243
+ const body = backdrop.querySelector(".gdp-trash-dialog-body");
9244
+ body?.append(input, error2);
9245
+ backdrop.addEventListener("pointerdown", (event) => {
9246
+ if (event.target === backdrop)
9247
+ done(null);
9248
+ });
9249
+ document.addEventListener("keydown", onKeydown);
9250
+ input.focus();
9251
+ });
9252
+ }
9087
9253
  async function moveRepoPathToTrash(path) {
9088
9254
  const res = await fetch("/_trash_path", {
9089
9255
  method: "POST",
@@ -9102,6 +9268,32 @@ ${frontmatter.yaml}
9102
9268
  UNDO_STACK.unshift(body.undo);
9103
9269
  return true;
9104
9270
  }
9271
+ async function requestCreateDirectory(path, onCreated, options = {}) {
9272
+ if (creatingDirectory)
9273
+ return;
9274
+ const name = await askNewDirectoryName(path, options.focusReturnTarget);
9275
+ if (!name)
9276
+ return;
9277
+ creatingDirectory = true;
9278
+ try {
9279
+ const res = await fetch("/_create_directory", {
9280
+ method: "POST",
9281
+ headers: {
9282
+ "Content-Type": "application/json",
9283
+ "X-Code-Viewer-Action": "1"
9284
+ },
9285
+ body: JSON.stringify({ dir: path, name })
9286
+ });
9287
+ if (!res.ok) {
9288
+ showCreateDirectoryError(`Failed to create "${name}": ${await res.text()}`);
9289
+ return;
9290
+ }
9291
+ const body = await res.json();
9292
+ onCreated(body.path || (path ? `${path}/${name}` : name));
9293
+ } finally {
9294
+ creatingDirectory = false;
9295
+ }
9296
+ }
9105
9297
  async function runUndoAction(action) {
9106
9298
  if (action.type !== "trash")
9107
9299
  return false;
@@ -9140,13 +9332,47 @@ ${frontmatter.yaml}
9140
9332
  function canTrashWorktreeRef(ref) {
9141
9333
  return ref === "worktree" || ref === "";
9142
9334
  }
9143
- function showRepoContextMenu(event, entry, ref, onDeleted) {
9335
+ function parentRepoPath(path) {
9336
+ return path.split("/").slice(0, -1).join("/");
9337
+ }
9338
+ async function copyRepoContextText(text2) {
9339
+ if (!text2)
9340
+ return;
9341
+ await navigator.clipboard.writeText(text2);
9342
+ }
9343
+ function createCopyPathButton(path) {
9344
+ const button = document.createElement("button");
9345
+ button.type = "button";
9346
+ button.className = "gdp-file-header-icon gdp-copy-path";
9347
+ button.title = "copy folder path";
9348
+ button.setAttribute("aria-label", "copy folder path");
9349
+ button.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
9350
+ button.addEventListener("click", async (event) => {
9351
+ event.stopPropagation();
9352
+ try {
9353
+ await navigator.clipboard.writeText(filePathClipboardText(path));
9354
+ button.classList.add("copied");
9355
+ setTimeout(() => {
9356
+ button.classList.remove("copied");
9357
+ }, 1200);
9358
+ } catch {
9359
+ button.classList.add("failed");
9360
+ setTimeout(() => {
9361
+ button.classList.remove("failed");
9362
+ }, 1200);
9363
+ }
9364
+ });
9365
+ return button;
9366
+ }
9367
+ function showRepoContextMenu(event, entry, ref, onChanged) {
9144
9368
  if (document.querySelector(".gdp-trash-dialog-backdrop"))
9145
9369
  return false;
9146
9370
  if (!canTrashWorktreeRef(ref))
9147
9371
  return false;
9148
9372
  if (entry.children_omitted_reason === "internal")
9149
9373
  return false;
9374
+ if (entry.type !== "tree" && entry.type !== "blob")
9375
+ return false;
9150
9376
  event.preventDefault();
9151
9377
  closeRepoContextMenu();
9152
9378
  const menu = document.createElement("div");
@@ -9158,15 +9384,39 @@ ${frontmatter.yaml}
9158
9384
  const anchorY = event.clientY > 0 ? event.clientY : anchorRect?.bottom || window.innerHeight / 2;
9159
9385
  menu.style.left = `${anchorX}px`;
9160
9386
  menu.style.top = `${anchorY}px`;
9387
+ const copyPath = document.createElement("button");
9388
+ copyPath.type = "button";
9389
+ copyPath.textContent = "Copy Path";
9390
+ copyPath.addEventListener("click", async () => {
9391
+ closeRepoContextMenu();
9392
+ await copyRepoContextText(filePathClipboardText(entry.path));
9393
+ });
9394
+ const copyName = document.createElement("button");
9395
+ copyName.type = "button";
9396
+ copyName.textContent = "Copy Name";
9397
+ copyName.addEventListener("click", async () => {
9398
+ closeRepoContextMenu();
9399
+ await copyRepoContextText(fileNameClipboardText(entry.path));
9400
+ });
9401
+ const createDir = document.createElement("button");
9402
+ createDir.type = "button";
9403
+ createDir.textContent = "New Folder...";
9404
+ createDir.addEventListener("click", async () => {
9405
+ closeRepoContextMenu();
9406
+ const targetPath = entry.type === "blob" ? parentRepoPath(entry.path) : entry.path;
9407
+ await requestCreateDirectory(targetPath, onChanged, {
9408
+ focusReturnTarget
9409
+ });
9410
+ });
9161
9411
  const trash = document.createElement("button");
9162
9412
  trash.type = "button";
9163
9413
  trash.className = "danger";
9164
9414
  trash.textContent = "Move to Trash...";
9165
9415
  trash.addEventListener("click", async () => {
9166
9416
  closeRepoContextMenu();
9167
- await requestMoveToTrash(entry.path, onDeleted, { focusReturnTarget });
9417
+ await requestMoveToTrash(entry.path, onChanged, { focusReturnTarget });
9168
9418
  });
9169
- menu.appendChild(trash);
9419
+ menu.append(copyPath, copyName, createDir, trash);
9170
9420
  document.body.appendChild(menu);
9171
9421
  const rect = menu.getBoundingClientRect();
9172
9422
  const left = Math.min(anchorX, window.innerWidth - rect.width - 8);
@@ -9186,6 +9436,7 @@ ${frontmatter.yaml}
9186
9436
  return null;
9187
9437
  return {
9188
9438
  path,
9439
+ type: row.dataset.type,
9189
9440
  children_omitted_reason: row.dataset.childrenOmittedReason
9190
9441
  };
9191
9442
  }
@@ -9199,14 +9450,31 @@ ${frontmatter.yaml}
9199
9450
  function createMoveToTrashButton(path, onDeleted) {
9200
9451
  const button = document.createElement("button");
9201
9452
  button.type = "button";
9202
- button.className = "gdp-btn gdp-btn-sm gdp-trash-path";
9203
- button.textContent = "Move to Trash";
9453
+ button.className = "gdp-file-header-icon gdp-trash-path";
9454
+ button.title = "move folder to Trash";
9455
+ button.setAttribute("aria-label", "move folder to Trash");
9456
+ button.innerHTML = iconSvg("octicon-trash", TRASH_16_PATH);
9204
9457
  button.addEventListener("click", async (event) => {
9205
9458
  event.stopPropagation();
9206
9459
  await requestMoveToTrash(path, onDeleted, { focusReturnTarget: button });
9207
9460
  });
9208
9461
  return button;
9209
9462
  }
9463
+ function createNewFolderButton(path, onCreated) {
9464
+ const button = document.createElement("button");
9465
+ button.type = "button";
9466
+ button.className = "gdp-file-header-icon gdp-create-dir";
9467
+ button.title = "new folder";
9468
+ button.setAttribute("aria-label", "new folder");
9469
+ button.innerHTML = iconSvg("octicon-plus", PLUS_16_PATH);
9470
+ button.addEventListener("click", async (event) => {
9471
+ event.stopPropagation();
9472
+ await requestCreateDirectory(path, onCreated, {
9473
+ focusReturnTarget: button
9474
+ });
9475
+ });
9476
+ return button;
9477
+ }
9210
9478
  function createOpenPathButton(path, kind, title = "open folder in OS") {
9211
9479
  const button = document.createElement("button");
9212
9480
  button.type = "button";
@@ -9257,7 +9525,10 @@ ${frontmatter.yaml}
9257
9525
  button.className = "gdp-btn gdp-btn-sm";
9258
9526
  button.textContent = "Upload files";
9259
9527
  button.addEventListener("click", () => input.click());
9260
- const fail = () => {
9528
+ const error2 = document.createElement("div");
9529
+ error2.className = "gdp-upload-error";
9530
+ const fail = (message = "Upload failed") => {
9531
+ error2.textContent = message;
9261
9532
  dropPanel.classList.add("failed");
9262
9533
  setTimeout(() => dropPanel.classList.remove("failed"), 1600);
9263
9534
  };
@@ -9265,8 +9536,9 @@ ${frontmatter.yaml}
9265
9536
  try {
9266
9537
  if (input.files?.length)
9267
9538
  await uploadFiles(path, input.files);
9268
- } catch {
9269
- fail();
9539
+ error2.textContent = "";
9540
+ } catch (uploadError) {
9541
+ fail(uploadError instanceof Error ? uploadError.message : "Upload failed");
9270
9542
  } finally {
9271
9543
  input.value = "";
9272
9544
  }
@@ -9283,11 +9555,12 @@ ${frontmatter.yaml}
9283
9555
  const files = event.dataTransfer?.files;
9284
9556
  if (files?.length)
9285
9557
  await uploadFiles(path, files);
9286
- } catch {
9287
- fail();
9558
+ error2.textContent = "";
9559
+ } catch (uploadError) {
9560
+ fail(uploadError instanceof Error ? uploadError.message : "Upload failed");
9288
9561
  }
9289
9562
  });
9290
- dropPanel.append(copy, button, input);
9563
+ dropPanel.append(copy, button, input, error2);
9291
9564
  return dropPanel;
9292
9565
  }
9293
9566
  function repoRoute(ref, path) {
@@ -9362,39 +9635,31 @@ ${frontmatter.yaml}
9362
9635
  const target = $("#diff");
9363
9636
  const shell = document.createElement("section");
9364
9637
  shell.className = "gdp-repo-shell";
9365
- const { wrap: targetPickerWrap, input: targetPicker } = createRefSelectorInput({
9366
- id: "repo-ref",
9367
- placeholder: "ref...",
9368
- title: "repository ref",
9369
- value: meta.ref || "worktree"
9370
- });
9371
- wireRefSelectorInput(targetPicker, (ref) => {
9372
- setRoute(repoRoute(ref, ""));
9373
- loadRepo();
9374
- });
9375
9638
  const toolbar = document.createElement("div");
9376
9639
  toolbar.className = "gdp-file-detail-header gdp-repo-toolbar";
9377
- toolbar.append(targetPickerWrap, createRepoBreadcrumb(meta.ref, meta.path || ""), createOpenPathButton(meta.path || "", "directory", "open this folder in OS"));
9640
+ const pathHeader = document.createElement("div");
9641
+ pathHeader.className = "gdp-file-detail-path";
9642
+ pathHeader.appendChild(createRepoBreadcrumb(meta.ref, meta.path || ""));
9643
+ if (meta.path)
9644
+ pathHeader.appendChild(createCopyPathButton(meta.path));
9645
+ pathHeader.appendChild(createOpenPathButton(meta.path || "", "directory", "open this folder in OS"));
9646
+ toolbar.appendChild(pathHeader);
9647
+ if (canTrashWorktreeRef(meta.ref)) {
9648
+ toolbar.appendChild(createNewFolderButton(meta.path || "", () => loadRepo()));
9649
+ if (meta.path) {
9650
+ toolbar.appendChild(createMoveToTrashButton(meta.path, () => {
9651
+ const parent = meta.path.split("/").slice(0, -1).join("/");
9652
+ setRoute(repoRoute(meta.ref, parent));
9653
+ loadRepo();
9654
+ }));
9655
+ }
9656
+ }
9378
9657
  shell.appendChild(toolbar);
9379
9658
  const listCard = document.createElement("section");
9380
9659
  listCard.className = "gdp-file-shell loaded gdp-repo-list-shell";
9381
9660
  const listWrapper = document.createElement("div");
9382
9661
  listWrapper.className = "d2h-file-wrapper";
9383
- const listHeader = document.createElement("div");
9384
- listHeader.className = "d2h-file-header";
9385
- const listName = document.createElement("div");
9386
- listName.className = "d2h-file-name-wrapper";
9387
- const listIcon = document.createElement("span");
9388
- listIcon.className = "dir-icon";
9389
- setFolderIcon(listIcon, false);
9390
- const listTitle = document.createElement("span");
9391
- listTitle.className = "d2h-file-name";
9392
- listTitle.textContent = meta.path || meta.project || "Files";
9393
- listName.append(listIcon, listTitle);
9394
- listHeader.appendChild(listName);
9395
- listHeader.appendChild(createOpenPathButton(meta.path || "", "directory", "open this folder in OS"));
9396
- listWrapper.appendChild(listHeader);
9397
- if (meta.upload_enabled && (meta.ref === "worktree" || meta.ref === "")) {
9662
+ if (meta.ref === "worktree" || meta.ref === "") {
9398
9663
  listWrapper.appendChild(createRepoUploadPanel(meta.path || ""));
9399
9664
  }
9400
9665
  const sortHost = document.createElement("div");
package/web/index.html CHANGED
@@ -21,7 +21,7 @@
21
21
  <header id="global-header">
22
22
  <a class="brand" href="/" aria-label="Repository home">
23
23
  <img class="brand-icon" src="/favicon.png" alt="" width="20" height="20" />
24
- <span class="title">code viewer</span>
24
+ <span class="title" id="project-title">repository</span>
25
25
  </a>
26
26
  <nav class="app-menu" aria-label="Views">
27
27
  <a class="app-menu-item active" data-route="repo" href="/">Repository</a>
@@ -32,6 +32,7 @@
32
32
  <button id="theme" title="toggle theme">🌗</button>
33
33
  <span id="status" class="status"></span>
34
34
  <a class="global-help-link" data-route="help" href="/help">Help</a>
35
+ <span class="product-label" aria-hidden="true">code viewer</span>
35
36
  </div>
36
37
  </header>
37
38
  <header id="topbar">
package/web/style.css CHANGED
@@ -101,6 +101,7 @@ html, body {
101
101
  margin: 0; padding: 0;
102
102
  background: var(--bg); color: var(--fg);
103
103
  --code-font-size: 12px;
104
+ --markdown-font-size: calc(var(--code-font-size) + 2px);
104
105
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
105
106
  Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
106
107
  font-size: 14px;
@@ -282,9 +283,11 @@ body[data-code-font-size="xlarge"] { --code-font-size: 15px; }
282
283
  .brand {
283
284
  display: flex; align-items: center; gap: 9px;
284
285
  font-weight: 600;
285
- font-size: 14px;
286
+ font-size: 15px;
286
287
  color: var(--fg);
287
- flex-shrink: 0;
288
+ min-width: 0;
289
+ max-width: min(34vw, 420px);
290
+ flex: 0 1 auto;
288
291
  text-decoration: none;
289
292
  }
290
293
  .brand:hover { color: var(--fg); text-decoration: none; }
@@ -294,7 +297,12 @@ body[data-code-font-size="xlarge"] { --code-font-size: 15px; }
294
297
  display: block;
295
298
  border-radius: 5px;
296
299
  }
297
- .brand .title { letter-spacing: -0.005em; }
300
+ .brand .title {
301
+ min-width: 0;
302
+ overflow: hidden;
303
+ text-overflow: ellipsis;
304
+ white-space: nowrap;
305
+ }
298
306
 
299
307
  .app-menu {
300
308
  display: flex;
@@ -366,6 +374,13 @@ body[data-code-font-size="xlarge"] { --code-font-size: 15px; }
366
374
  gap: 8px;
367
375
  flex: 0 0 auto;
368
376
  }
377
+ .product-label {
378
+ color: var(--fg-muted);
379
+ font-size: 12px;
380
+ font-weight: 600;
381
+ line-height: 1;
382
+ white-space: nowrap;
383
+ }
369
384
  .global-actions #theme,
370
385
  .global-icon-action {
371
386
  display: inline-flex;
@@ -2298,6 +2313,12 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2298
2313
  .gdp-upload-panel.failed {
2299
2314
  border-color: var(--danger);
2300
2315
  }
2316
+ .gdp-upload-error {
2317
+ min-height: 18px;
2318
+ color: var(--danger);
2319
+ font-size: 12px;
2320
+ line-height: 18px;
2321
+ }
2301
2322
  .gdp-upload-copy {
2302
2323
  min-width: 0;
2303
2324
  color: var(--fg-muted);
@@ -2542,6 +2563,7 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2542
2563
  display: flex;
2543
2564
  align-items: center;
2544
2565
  min-width: 0;
2566
+ flex: 0 1 auto;
2545
2567
  gap: 4px;
2546
2568
  padding-top: 2px;
2547
2569
  overflow: hidden;
@@ -2595,6 +2617,9 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2595
2617
  flex-shrink: 0;
2596
2618
  color: var(--danger);
2597
2619
  }
2620
+ .gdp-file-detail-header .gdp-create-dir {
2621
+ flex-shrink: 0;
2622
+ }
2598
2623
  .gdp-file-detail-meta {
2599
2624
  display: flex;
2600
2625
  align-items: center;
@@ -2634,7 +2659,7 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2634
2659
  padding: 24px 32px 40px;
2635
2660
  color: var(--fg);
2636
2661
  overflow-wrap: break-word;
2637
- font-size: 16px;
2662
+ font-size: var(--markdown-font-size);
2638
2663
  line-height: 1.75;
2639
2664
  }
2640
2665
  .gdp-html-preview {
@@ -2697,7 +2722,7 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2697
2722
  margin: 0;
2698
2723
  }
2699
2724
  .gdp-markdown-toc a {
2700
- display: -webkit-box;
2725
+ display: block;
2701
2726
  min-height: 28px;
2702
2727
  padding: 5px 10px 5px 12px;
2703
2728
  border-left: 3px solid transparent;
@@ -2706,8 +2731,6 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2706
2731
  overflow: hidden;
2707
2732
  overflow-wrap: anywhere;
2708
2733
  text-decoration: none;
2709
- -webkit-box-orient: vertical;
2710
- -webkit-line-clamp: 2;
2711
2734
  transition:
2712
2735
  background-color 0.06s ease,
2713
2736
  border-color 0.06s ease,
@@ -2741,6 +2764,14 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2741
2764
  font-weight: 600;
2742
2765
  }
2743
2766
  .gdp-markdown-preview h1,
2767
+ .gdp-markdown-preview h2,
2768
+ .gdp-markdown-preview h3,
2769
+ .gdp-markdown-preview h4,
2770
+ .gdp-markdown-preview h5,
2771
+ .gdp-markdown-preview h6 {
2772
+ scroll-margin-top: calc(var(--global-header-h) + 120px);
2773
+ }
2774
+ .gdp-markdown-preview h1,
2744
2775
  .gdp-markdown-preview h2 {
2745
2776
  padding-bottom: 0.3em;
2746
2777
  border-bottom: 1px solid var(--border-muted);
@@ -3141,9 +3172,6 @@ body.gdp-file-detail-page #empty {
3141
3172
  padding-left: 0;
3142
3173
  padding-right: 0;
3143
3174
  }
3144
- .gdp-repo-breadcrumb {
3145
- flex: 1;
3146
- }
3147
3175
  .gdp-repo-breadcrumb button {
3148
3176
  max-width: 260px;
3149
3177
  border: 0;
@@ -3324,6 +3352,29 @@ body.gdp-file-detail-page #empty {
3324
3352
  line-height: 1.5;
3325
3353
  overflow-wrap: anywhere;
3326
3354
  }
3355
+ .gdp-create-dir-input {
3356
+ display: block;
3357
+ width: 100%;
3358
+ height: 32px;
3359
+ margin-top: 10px;
3360
+ padding: 0 10px;
3361
+ border: 1px solid var(--border);
3362
+ border-radius: 6px;
3363
+ background: var(--bg);
3364
+ color: var(--fg);
3365
+ font: inherit;
3366
+ }
3367
+ .gdp-create-dir-input:focus {
3368
+ outline: 2px solid var(--accent-subtle);
3369
+ border-color: var(--accent);
3370
+ }
3371
+ .gdp-create-dir-error {
3372
+ min-height: 18px;
3373
+ margin-top: 6px;
3374
+ color: var(--danger);
3375
+ font-size: 12px;
3376
+ line-height: 18px;
3377
+ }
3327
3378
  .gdp-trash-dialog-actions {
3328
3379
  display: flex;
3329
3380
  justify-content: flex-end;