@youtyan/code-viewer 0.1.26 → 0.1.28
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 +4 -13
- package/dist/code-viewer.js +281 -52
- package/package.json +1 -1
- package/web/app.js +304 -39
- package/web/index.html +2 -1
- package/web/style.css +61 -10
package/README.md
CHANGED
|
@@ -80,30 +80,21 @@ the full non-virtual view.
|
|
|
80
80
|
|
|
81
81
|
## Uploads
|
|
82
82
|
|
|
83
|
-
File uploads are
|
|
84
|
-
|
|
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
|
-
|
|
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
|
package/dist/code-viewer.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
closeSync,
|
|
6
6
|
constants,
|
|
7
7
|
existsSync as existsSync3,
|
|
8
|
-
lstatSync as
|
|
8
|
+
lstatSync as lstatSync4,
|
|
9
9
|
mkdirSync,
|
|
10
10
|
openSync,
|
|
11
11
|
readFileSync as readFileSync2,
|
|
@@ -17,7 +17,24 @@ import {
|
|
|
17
17
|
writeFileSync
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
|
-
import { basename as basename2, dirname as dirname2, extname, join as
|
|
20
|
+
import { basename as basename2, dirname as dirname2, extname, join as join5, relative as relative2 } from "node:path";
|
|
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
|
+
}
|
|
21
38
|
|
|
22
39
|
// web-src/routes.ts
|
|
23
40
|
var SPA_PATHS = ["/todif", "/todiff", "/file", "/help"];
|
|
@@ -1223,9 +1240,150 @@ function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames =
|
|
|
1223
1240
|
return parseRgOutput(normalized, max, omitDirNames, excludeNames);
|
|
1224
1241
|
}
|
|
1225
1242
|
|
|
1243
|
+
// web-src/server/worktree-watcher.ts
|
|
1244
|
+
import {
|
|
1245
|
+
lstatSync as lstatSync3,
|
|
1246
|
+
readdirSync as nodeReaddirSync,
|
|
1247
|
+
watch as nodeWatch
|
|
1248
|
+
} from "node:fs";
|
|
1249
|
+
import { join as join4, relative } from "node:path";
|
|
1250
|
+
function normalizeRelativePath(path) {
|
|
1251
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1252
|
+
}
|
|
1253
|
+
function isInsideRoot(root, path) {
|
|
1254
|
+
const rel = relative(root, path).replace(/\\/g, "/");
|
|
1255
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
|
|
1256
|
+
}
|
|
1257
|
+
function startWorktreeUpdateWatch(options) {
|
|
1258
|
+
const watch = options.watch || nodeWatch;
|
|
1259
|
+
const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
|
|
1260
|
+
const isDirectory = options.isDirectory || ((path) => {
|
|
1261
|
+
try {
|
|
1262
|
+
return lstatSync3(path).isDirectory();
|
|
1263
|
+
} catch {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
const directorySignature = options.directorySignature || ((path) => {
|
|
1268
|
+
try {
|
|
1269
|
+
const stats = lstatSync3(path);
|
|
1270
|
+
if (!stats.isDirectory())
|
|
1271
|
+
return null;
|
|
1272
|
+
return `${stats.dev}:${stats.ino}`;
|
|
1273
|
+
} catch {
|
|
1274
|
+
return null;
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
const setTimer = options.setTimeoutFn || setTimeout;
|
|
1278
|
+
const clearTimer = options.clearTimeoutFn || clearTimeout;
|
|
1279
|
+
const debounceMs = options.debounceMs ?? 250;
|
|
1280
|
+
const watchers = new Map;
|
|
1281
|
+
const signatures = new Map;
|
|
1282
|
+
let timer = null;
|
|
1283
|
+
const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
|
|
1284
|
+
const scheduleUpdate = () => {
|
|
1285
|
+
if (timer)
|
|
1286
|
+
clearTimer(timer);
|
|
1287
|
+
timer = setTimer(() => {
|
|
1288
|
+
timer = null;
|
|
1289
|
+
options.onUpdate();
|
|
1290
|
+
}, debounceMs);
|
|
1291
|
+
};
|
|
1292
|
+
const closeSubtree = (dir) => {
|
|
1293
|
+
for (const [watchedDir, watcher] of [...watchers]) {
|
|
1294
|
+
if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
|
|
1295
|
+
continue;
|
|
1296
|
+
try {
|
|
1297
|
+
watcher.close?.();
|
|
1298
|
+
} catch {}
|
|
1299
|
+
watchers.delete(watchedDir);
|
|
1300
|
+
signatures.delete(watchedDir);
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
const closeAll = () => {
|
|
1304
|
+
for (const watcher of [...watchers.values()]) {
|
|
1305
|
+
try {
|
|
1306
|
+
watcher.close?.();
|
|
1307
|
+
} catch {}
|
|
1308
|
+
}
|
|
1309
|
+
watchers.clear();
|
|
1310
|
+
signatures.clear();
|
|
1311
|
+
};
|
|
1312
|
+
const watchDirectory = (dir) => {
|
|
1313
|
+
if (watchers.has(dir))
|
|
1314
|
+
return;
|
|
1315
|
+
const rel = normalizeRelativePath(relative(options.root, dir));
|
|
1316
|
+
if (rel && ignored(rel))
|
|
1317
|
+
return;
|
|
1318
|
+
try {
|
|
1319
|
+
const watcher = watch(dir, { persistent: false }, (_event, filename) => {
|
|
1320
|
+
if (!filename) {
|
|
1321
|
+
scheduleUpdate();
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const changed = normalizeRelativePath(join4(rel, filename.toString()));
|
|
1325
|
+
if (ignored(changed))
|
|
1326
|
+
return;
|
|
1327
|
+
const fullChangedPath = join4(options.root, changed);
|
|
1328
|
+
if (!isInsideRoot(options.root, fullChangedPath))
|
|
1329
|
+
return;
|
|
1330
|
+
const known = watchers.has(fullChangedPath);
|
|
1331
|
+
if (isDirectory(fullChangedPath)) {
|
|
1332
|
+
if (known) {
|
|
1333
|
+
const signature2 = directorySignature(fullChangedPath);
|
|
1334
|
+
if (signature2 && signature2 !== signatures.get(fullChangedPath)) {
|
|
1335
|
+
closeSubtree(fullChangedPath);
|
|
1336
|
+
watchDirectory(fullChangedPath);
|
|
1337
|
+
}
|
|
1338
|
+
scheduleUpdate();
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
watchDirectory(fullChangedPath);
|
|
1342
|
+
} else if (known) {
|
|
1343
|
+
closeSubtree(fullChangedPath);
|
|
1344
|
+
}
|
|
1345
|
+
scheduleUpdate();
|
|
1346
|
+
}) || {};
|
|
1347
|
+
watchers.set(dir, watcher);
|
|
1348
|
+
const signature = directorySignature(dir);
|
|
1349
|
+
if (signature)
|
|
1350
|
+
signatures.set(dir, signature);
|
|
1351
|
+
watcher.on?.("error", () => {
|
|
1352
|
+
if (watchers.get(dir) === watcher) {
|
|
1353
|
+
watchers.delete(dir);
|
|
1354
|
+
signatures.delete(dir);
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
watcher.on?.("close", () => {
|
|
1358
|
+
if (watchers.get(dir) === watcher) {
|
|
1359
|
+
watchers.delete(dir);
|
|
1360
|
+
signatures.delete(dir);
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
options.onError?.(error);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
let entries;
|
|
1368
|
+
try {
|
|
1369
|
+
entries = readDirs(dir);
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
options.onError?.(error);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
for (const entry of entries) {
|
|
1375
|
+
if (!entry.isDirectory())
|
|
1376
|
+
continue;
|
|
1377
|
+
watchDirectory(join4(dir, entry.name));
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
watchDirectory(options.root);
|
|
1381
|
+
return { started: watchers.size > 0, close: closeAll };
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1226
1384
|
// web-src/server/preview.ts
|
|
1227
|
-
var WEB_ROOT =
|
|
1228
|
-
var VERSION = JSON.parse(readFileSync2(
|
|
1385
|
+
var WEB_ROOT = join5(ROOT, "web");
|
|
1386
|
+
var VERSION = JSON.parse(readFileSync2(join5(ROOT, "package.json"), "utf8")).version;
|
|
1229
1387
|
var DEFAULT_ARGS = ["HEAD"];
|
|
1230
1388
|
var PREVIEW_HUNKS_DEFAULT = 3;
|
|
1231
1389
|
var PREVIEW_LINES_DEFAULT = 1200;
|
|
@@ -1236,8 +1394,8 @@ var SIZE_LARGE = 20000;
|
|
|
1236
1394
|
var LINE_INDEX_MIN_START = 1e4;
|
|
1237
1395
|
var LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
1238
1396
|
var BLOB_LINE_CACHE_MAX_BYTES = 128 * 1024 * 1024;
|
|
1239
|
-
var MAX_UPLOAD_FILE_BYTES =
|
|
1240
|
-
var MAX_UPLOAD_TOTAL_BYTES =
|
|
1397
|
+
var MAX_UPLOAD_FILE_BYTES = 512 * 1024 * 1024;
|
|
1398
|
+
var MAX_UPLOAD_TOTAL_BYTES = 1024 * 1024 * 1024;
|
|
1241
1399
|
var MAX_UPLOAD_BODY_BYTES = MAX_UPLOAD_TOTAL_BYTES + 1024 * 1024;
|
|
1242
1400
|
var MAX_UPLOAD_FILES = 50;
|
|
1243
1401
|
var SAFE_UPLOAD_EXTENSIONS = new Set([
|
|
@@ -1255,7 +1413,19 @@ var SAFE_UPLOAD_EXTENSIONS = new Set([
|
|
|
1255
1413
|
".jpeg",
|
|
1256
1414
|
".gif",
|
|
1257
1415
|
".webp",
|
|
1416
|
+
".svg",
|
|
1258
1417
|
".pdf",
|
|
1418
|
+
".mp4",
|
|
1419
|
+
".mov",
|
|
1420
|
+
".m4v",
|
|
1421
|
+
".webm",
|
|
1422
|
+
".mp3",
|
|
1423
|
+
".wav",
|
|
1424
|
+
".m4a",
|
|
1425
|
+
".aac",
|
|
1426
|
+
".flac",
|
|
1427
|
+
".ogg",
|
|
1428
|
+
".zip",
|
|
1259
1429
|
".ts",
|
|
1260
1430
|
".tsx",
|
|
1261
1431
|
".js",
|
|
@@ -1268,12 +1438,11 @@ var generation = 1;
|
|
|
1268
1438
|
var cwd = repoRoot(process.cwd()) || process.cwd();
|
|
1269
1439
|
var cliArgs = DEFAULT_ARGS;
|
|
1270
1440
|
var listenPort = 0;
|
|
1271
|
-
var allowUpload = false;
|
|
1272
|
-
var uploadAllowedByCli = false;
|
|
1273
1441
|
var openAfterStart = false;
|
|
1274
1442
|
var scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
|
|
1275
1443
|
var scopeOmitDirCliOverride = null;
|
|
1276
1444
|
var scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
|
|
1445
|
+
var uploadDisabledByConfig = false;
|
|
1277
1446
|
var rgAvailableCache = null;
|
|
1278
1447
|
var enc = new TextEncoder;
|
|
1279
1448
|
var sseClients = new Set;
|
|
@@ -1326,10 +1495,7 @@ Examples:
|
|
|
1326
1495
|
listenPort = parsed;
|
|
1327
1496
|
} else if (arg === "--open") {
|
|
1328
1497
|
openAfterStart = true;
|
|
1329
|
-
} else if (arg === "--allow-upload") {
|
|
1330
|
-
allowUpload = true;
|
|
1331
|
-
uploadAllowedByCli = true;
|
|
1332
|
-
} else if (arg === "--scope-omit-dir") {
|
|
1498
|
+
} else if (arg === "--allow-upload") {} else if (arg === "--scope-omit-dir") {
|
|
1333
1499
|
const next = process.argv[++i];
|
|
1334
1500
|
if (!next) {
|
|
1335
1501
|
console.error("--scope-omit-dir requires a directory name");
|
|
@@ -1345,10 +1511,9 @@ Examples:
|
|
|
1345
1511
|
}
|
|
1346
1512
|
if (rest.length)
|
|
1347
1513
|
cliArgs = rest;
|
|
1348
|
-
if (!uploadAllowedByCli)
|
|
1349
|
-
allowUpload = loadProjectConfigUploadEnabled();
|
|
1350
1514
|
const configScopeOmitDirs = loadProjectConfigScopeOmitDirs();
|
|
1351
1515
|
const configScopeExcludeNames = loadProjectConfigScopeExcludeNames();
|
|
1516
|
+
uploadDisabledByConfig = loadProjectConfigUploadDisabled();
|
|
1352
1517
|
if (scopeOmitDirCliOverride) {
|
|
1353
1518
|
scopeOmitDirNames = scopeOmitDirCliOverride;
|
|
1354
1519
|
} else if (configScopeOmitDirs) {
|
|
@@ -1424,7 +1589,7 @@ function staticFile(pathname) {
|
|
|
1424
1589
|
const spec = map[pathname];
|
|
1425
1590
|
if (!spec)
|
|
1426
1591
|
return null;
|
|
1427
|
-
const full =
|
|
1592
|
+
const full = join5(WEB_ROOT, spec[0]);
|
|
1428
1593
|
if (!existsSync3(full))
|
|
1429
1594
|
return text("not found", 404);
|
|
1430
1595
|
return new Response(readFileSync2(full), {
|
|
@@ -1658,7 +1823,7 @@ function parseScopeExcludeNamesQuery(value) {
|
|
|
1658
1823
|
return normalizeScopeExcludeNames(names);
|
|
1659
1824
|
}
|
|
1660
1825
|
function loadProjectConfig() {
|
|
1661
|
-
const full =
|
|
1826
|
+
const full = join5(cwd, ".code-viewer.json");
|
|
1662
1827
|
if (!existsSync3(full))
|
|
1663
1828
|
return null;
|
|
1664
1829
|
let realCwd;
|
|
@@ -1680,9 +1845,9 @@ function loadProjectConfig() {
|
|
|
1680
1845
|
return null;
|
|
1681
1846
|
}
|
|
1682
1847
|
}
|
|
1683
|
-
function
|
|
1848
|
+
function loadProjectConfigUploadDisabled() {
|
|
1684
1849
|
const config = loadProjectConfig();
|
|
1685
|
-
return config?.upload?.enabled ===
|
|
1850
|
+
return config?.upload?.enabled === false;
|
|
1686
1851
|
}
|
|
1687
1852
|
function loadProjectConfigScopeOmitDirs() {
|
|
1688
1853
|
const config = loadProjectConfig();
|
|
@@ -1723,7 +1888,7 @@ function safeWorktreePath(path) {
|
|
|
1723
1888
|
return null;
|
|
1724
1889
|
if (isGitInternalPath(path))
|
|
1725
1890
|
return null;
|
|
1726
|
-
const full =
|
|
1891
|
+
const full = join5(cwd, path);
|
|
1727
1892
|
if (!existsSync3(full))
|
|
1728
1893
|
return null;
|
|
1729
1894
|
let realCwd;
|
|
@@ -1734,7 +1899,7 @@ function safeWorktreePath(path) {
|
|
|
1734
1899
|
} catch {
|
|
1735
1900
|
return null;
|
|
1736
1901
|
}
|
|
1737
|
-
const rel =
|
|
1902
|
+
const rel = relative2(realCwd, realFull);
|
|
1738
1903
|
if (rel === "" || rel.startsWith("..") || rel.startsWith("/") || rel.startsWith("\\"))
|
|
1739
1904
|
return null;
|
|
1740
1905
|
if (isGitInternalPath(rel))
|
|
@@ -1742,7 +1907,7 @@ function safeWorktreePath(path) {
|
|
|
1742
1907
|
return realFull;
|
|
1743
1908
|
}
|
|
1744
1909
|
function worktreePath(path) {
|
|
1745
|
-
return
|
|
1910
|
+
return join5(cwd, path);
|
|
1746
1911
|
}
|
|
1747
1912
|
function safeOpenWorktreePath(path) {
|
|
1748
1913
|
if (path === "") {
|
|
@@ -1873,7 +2038,7 @@ function handleTree(url) {
|
|
|
1873
2038
|
branch: currentBranch(cwd) || undefined,
|
|
1874
2039
|
entries: recursive ? entries : entries.map((entry) => attachTreeEntryMetadata(target, entry)),
|
|
1875
2040
|
readme: readReadme(target, path),
|
|
1876
|
-
upload_enabled:
|
|
2041
|
+
upload_enabled: !uploadDisabledByConfig && (target === "worktree" || target === "")
|
|
1877
2042
|
});
|
|
1878
2043
|
}
|
|
1879
2044
|
function handleSettings() {
|
|
@@ -1939,7 +2104,7 @@ function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
|
|
|
1939
2104
|
continue;
|
|
1940
2105
|
let stat;
|
|
1941
2106
|
try {
|
|
1942
|
-
stat =
|
|
2107
|
+
stat = lstatSync4(full);
|
|
1943
2108
|
} catch {
|
|
1944
2109
|
continue;
|
|
1945
2110
|
}
|
|
@@ -2430,24 +2595,26 @@ function isForbiddenUploadName(name) {
|
|
|
2430
2595
|
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
2596
|
}
|
|
2432
2597
|
function safeUploadFileName(name) {
|
|
2433
|
-
|
|
2598
|
+
const trimmed = name.trim();
|
|
2599
|
+
if (!trimmed || trimmed.length > 180 || trimmed.includes("\x00") || trimmed.includes("/") || trimmed.includes("\\") || Array.from(trimmed).some((char) => {
|
|
2600
|
+
const code = char.charCodeAt(0);
|
|
2601
|
+
return code < 32 || code === 127;
|
|
2602
|
+
}))
|
|
2434
2603
|
return null;
|
|
2435
|
-
if (
|
|
2604
|
+
if (trimmed === "." || trimmed === "..")
|
|
2436
2605
|
return null;
|
|
2437
|
-
if (
|
|
2606
|
+
if (isGitInternalPath(trimmed) || isForbiddenUploadName(trimmed))
|
|
2438
2607
|
return null;
|
|
2439
|
-
if (
|
|
2608
|
+
if (!SAFE_UPLOAD_EXTENSIONS.has(extname(trimmed).toLowerCase()))
|
|
2440
2609
|
return null;
|
|
2441
|
-
|
|
2442
|
-
return null;
|
|
2443
|
-
return name;
|
|
2610
|
+
return trimmed;
|
|
2444
2611
|
}
|
|
2445
2612
|
function uploadOpenFlags() {
|
|
2446
2613
|
return constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0);
|
|
2447
2614
|
}
|
|
2448
2615
|
async function handleUploadFiles(req) {
|
|
2449
|
-
if (
|
|
2450
|
-
return text("upload disabled", 403);
|
|
2616
|
+
if (uploadDisabledByConfig)
|
|
2617
|
+
return text("upload disabled by project config", 403);
|
|
2451
2618
|
if (req.method !== "POST")
|
|
2452
2619
|
return text("method not allowed", 405);
|
|
2453
2620
|
if (!sideEffectRequestAllowed(req))
|
|
@@ -2503,8 +2670,8 @@ async function handleUploadFiles(req) {
|
|
|
2503
2670
|
total += file.size;
|
|
2504
2671
|
if (total > MAX_UPLOAD_TOTAL_BYTES)
|
|
2505
2672
|
return text("upload too large", 413);
|
|
2506
|
-
const target =
|
|
2507
|
-
if (
|
|
2673
|
+
const target = join5(realDir, safeName);
|
|
2674
|
+
if (relative2(realDir, dirname2(target)) !== "")
|
|
2508
2675
|
return text("invalid filename", 400);
|
|
2509
2676
|
if (existsSync3(target))
|
|
2510
2677
|
return text("file exists", 409);
|
|
@@ -2531,10 +2698,7 @@ async function handleUploadFiles(req) {
|
|
|
2531
2698
|
return text("file exists", 409);
|
|
2532
2699
|
return text("upload failed", 500);
|
|
2533
2700
|
}
|
|
2534
|
-
|
|
2535
|
-
fileCache.clear();
|
|
2536
|
-
metaCache.clear();
|
|
2537
|
-
sendSse("update");
|
|
2701
|
+
triggerUpdate();
|
|
2538
2702
|
return json({
|
|
2539
2703
|
ok: true,
|
|
2540
2704
|
files: uploads.map((upload) => upload.name),
|
|
@@ -2621,10 +2785,15 @@ function clearMutableCaches() {
|
|
|
2621
2785
|
metaCache.clear();
|
|
2622
2786
|
fileListCache.clear();
|
|
2623
2787
|
}
|
|
2788
|
+
function triggerUpdate() {
|
|
2789
|
+
generation++;
|
|
2790
|
+
clearMutableCaches();
|
|
2791
|
+
sendSse("update");
|
|
2792
|
+
}
|
|
2624
2793
|
function moveMacPathIntoTrash(path) {
|
|
2625
|
-
const trashDir =
|
|
2794
|
+
const trashDir = join5(homedir(), ".Trash");
|
|
2626
2795
|
const base = basename2(path) || "code-viewer-trash-item";
|
|
2627
|
-
const target =
|
|
2796
|
+
const target = join5(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
2628
2797
|
try {
|
|
2629
2798
|
mkdirSync(trashDir, { recursive: true });
|
|
2630
2799
|
renameSync(path, target);
|
|
@@ -2634,7 +2803,7 @@ function moveMacPathIntoTrash(path) {
|
|
|
2634
2803
|
}
|
|
2635
2804
|
}
|
|
2636
2805
|
function movePathToTrash(path) {
|
|
2637
|
-
|
|
2806
|
+
lstatSync4(path);
|
|
2638
2807
|
if (process.platform === "darwin") {
|
|
2639
2808
|
return moveMacPathIntoTrash(path);
|
|
2640
2809
|
}
|
|
@@ -2666,8 +2835,8 @@ function restoreTrashPath(originalPath, trashPath) {
|
|
|
2666
2835
|
if (!existsSync3(trashPath))
|
|
2667
2836
|
return { ok: false, error: "trash item not found" };
|
|
2668
2837
|
try {
|
|
2669
|
-
const trashRoot =
|
|
2670
|
-
const trashRelative =
|
|
2838
|
+
const trashRoot = join5(homedir(), ".Trash");
|
|
2839
|
+
const trashRelative = relative2(trashRoot, trashPath);
|
|
2671
2840
|
if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
|
|
2672
2841
|
return { ok: false, error: "invalid trash handle" };
|
|
2673
2842
|
mkdirSync(dirname2(original), { recursive: true });
|
|
@@ -2773,11 +2942,62 @@ async function handleTrashPath(req) {
|
|
|
2773
2942
|
trashPath: moved.trashPath
|
|
2774
2943
|
}
|
|
2775
2944
|
};
|
|
2776
|
-
|
|
2777
|
-
clearMutableCaches();
|
|
2778
|
-
sendSse("update");
|
|
2945
|
+
triggerUpdate();
|
|
2779
2946
|
return json({ ok: true, generation, undo });
|
|
2780
2947
|
}
|
|
2948
|
+
async function handleCreateDirectory(req) {
|
|
2949
|
+
if (req.method !== "POST")
|
|
2950
|
+
return text("method not allowed", 405);
|
|
2951
|
+
if (!sideEffectRequestAllowed(req))
|
|
2952
|
+
return text("forbidden", 403);
|
|
2953
|
+
const contentType = req.headers.get("content-type") || "";
|
|
2954
|
+
if (!/^application\/json(?:;|$)/i.test(contentType))
|
|
2955
|
+
return text("unsupported media type", 415);
|
|
2956
|
+
const lengthHeader = req.headers.get("content-length");
|
|
2957
|
+
const length = Number(lengthHeader || "0");
|
|
2958
|
+
if (lengthHeader && (!Number.isFinite(length) || length < 0))
|
|
2959
|
+
return text("invalid content length", 400);
|
|
2960
|
+
if (length > 2048)
|
|
2961
|
+
return text("payload too large", 413);
|
|
2962
|
+
let body = {};
|
|
2963
|
+
try {
|
|
2964
|
+
const raw = await req.text();
|
|
2965
|
+
if (raw.length > 2048)
|
|
2966
|
+
return text("payload too large", 413);
|
|
2967
|
+
body = JSON.parse(raw);
|
|
2968
|
+
} catch {
|
|
2969
|
+
return text("invalid json", 400);
|
|
2970
|
+
}
|
|
2971
|
+
const dir = typeof body.dir === "string" ? body.dir.trim().replace(/^\/+|\/+$/g, "") : "";
|
|
2972
|
+
const name = normalizeNewDirectoryName(body.name);
|
|
2973
|
+
if (!safeRepoPath(dir))
|
|
2974
|
+
return text("invalid dir", 400);
|
|
2975
|
+
if (dir && isGitInternalPath(dir))
|
|
2976
|
+
return text("forbidden", 403);
|
|
2977
|
+
if (!name)
|
|
2978
|
+
return text("invalid name", 400);
|
|
2979
|
+
const parent = safeOpenWorktreePath(dir);
|
|
2980
|
+
if (!parent)
|
|
2981
|
+
return text("not found", 404);
|
|
2982
|
+
const stats = statSync(parent);
|
|
2983
|
+
if (!stats.isDirectory())
|
|
2984
|
+
return text("not a directory", 400);
|
|
2985
|
+
const targetPath = dir ? `${dir}/${name}` : name;
|
|
2986
|
+
if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
|
|
2987
|
+
return text("invalid target", 400);
|
|
2988
|
+
const target = join5(parent, name);
|
|
2989
|
+
if (existsSync3(target))
|
|
2990
|
+
return text("already exists", 409);
|
|
2991
|
+
try {
|
|
2992
|
+
mkdirSync(target, { recursive: false });
|
|
2993
|
+
} catch (error) {
|
|
2994
|
+
if (error.code === "EEXIST")
|
|
2995
|
+
return text("already exists", 409);
|
|
2996
|
+
return text("create failed", 500);
|
|
2997
|
+
}
|
|
2998
|
+
triggerUpdate();
|
|
2999
|
+
return json({ ok: true, path: targetPath, generation });
|
|
3000
|
+
}
|
|
2781
3001
|
async function handleRestoreTrash(req) {
|
|
2782
3002
|
if (req.method !== "POST")
|
|
2783
3003
|
return text("method not allowed", 405);
|
|
@@ -2807,9 +3027,7 @@ async function handleRestoreTrash(req) {
|
|
|
2807
3027
|
const restored = restoreTrashPath(originalPath, trashPath || undefined);
|
|
2808
3028
|
if (!restored.ok)
|
|
2809
3029
|
return text(restored.error || "undo failed", 409);
|
|
2810
|
-
|
|
2811
|
-
clearMutableCaches();
|
|
2812
|
-
sendSse("update");
|
|
3030
|
+
triggerUpdate();
|
|
2813
3031
|
return json({ ok: true, generation });
|
|
2814
3032
|
}
|
|
2815
3033
|
function sendSse(event, data = "tick") {
|
|
@@ -2864,6 +3082,8 @@ var server = await startServer({
|
|
|
2864
3082
|
return handleTrashPath(req);
|
|
2865
3083
|
if (url.pathname === "/_restore_trash")
|
|
2866
3084
|
return handleRestoreTrash(req);
|
|
3085
|
+
if (url.pathname === "/_create_directory")
|
|
3086
|
+
return handleCreateDirectory(req);
|
|
2867
3087
|
if (url.pathname === "/_upload_files")
|
|
2868
3088
|
return handleUploadFiles(req);
|
|
2869
3089
|
if (url.pathname === "/_refs")
|
|
@@ -2871,9 +3091,7 @@ var server = await startServer({
|
|
|
2871
3091
|
if (url.pathname === "/refresh" && req.method === "POST") {
|
|
2872
3092
|
if (!sideEffectRequestAllowed(req))
|
|
2873
3093
|
return text("forbidden", 403);
|
|
2874
|
-
|
|
2875
|
-
clearMutableCaches();
|
|
2876
|
-
sendSse("update");
|
|
3094
|
+
triggerUpdate();
|
|
2877
3095
|
return json({ ok: true, generation });
|
|
2878
3096
|
}
|
|
2879
3097
|
if (url.pathname === "/events") {
|
|
@@ -2924,5 +3142,16 @@ startDevAssetReload({
|
|
|
2924
3142
|
watch,
|
|
2925
3143
|
sendReload: () => sendSse("reload")
|
|
2926
3144
|
});
|
|
3145
|
+
startWorktreeUpdateWatch({
|
|
3146
|
+
root: cwd,
|
|
3147
|
+
omitDirNames: scopeOmitDirNames,
|
|
3148
|
+
excludeNames: scopeExcludeNames,
|
|
3149
|
+
watch,
|
|
3150
|
+
onUpdate: triggerUpdate,
|
|
3151
|
+
onError: (error) => {
|
|
3152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3153
|
+
console.warn(`code-viewer worktree watch skipped: ${message}`);
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
2927
3156
|
console.log(`GDP_LISTEN_URL=http://127.0.0.1:${server.port}/`);
|
|
2928
3157
|
console.log(`git-diff-preview serving ${cwd}`);
|
package/package.json
CHANGED
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 <=
|
|
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
|
|
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,
|
|
9417
|
+
await requestMoveToTrash(entry.path, onChanged, { focusReturnTarget });
|
|
9168
9418
|
});
|
|
9169
|
-
menu.
|
|
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-
|
|
9203
|
-
button.
|
|
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
|
|
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
|
-
|
|
9269
|
-
|
|
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
|
-
|
|
9287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
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:
|
|
286
|
+
font-size: 15px;
|
|
286
287
|
color: var(--fg);
|
|
287
|
-
|
|
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 {
|
|
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:
|
|
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:
|
|
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;
|