@tensorix/metahub 0.2.2 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +8 -1
  2. package/dist/cli.js +1309 -275
  3. package/dist/cli.js.map +21 -17
  4. package/dist/core/databases.d.ts +5 -1
  5. package/dist/core/databases.d.ts.map +1 -1
  6. package/dist/core/documents.d.ts +22 -0
  7. package/dist/core/documents.d.ts.map +1 -1
  8. package/dist/core/md/grammar.d.ts +77 -0
  9. package/dist/core/md/grammar.d.ts.map +1 -0
  10. package/dist/core/md/heal.d.ts +2 -0
  11. package/dist/core/md/heal.d.ts.map +1 -0
  12. package/dist/core/md/inline.d.ts +20 -0
  13. package/dist/core/md/inline.d.ts.map +1 -0
  14. package/dist/core/schema-init.d.ts +13 -0
  15. package/dist/core/schema-init.d.ts.map +1 -1
  16. package/dist/core/schema.d.ts +1 -1
  17. package/dist/core/schema.d.ts.map +1 -1
  18. package/dist/core/sync/auth.d.ts +3 -1
  19. package/dist/core/sync/auth.d.ts.map +1 -1
  20. package/dist/core/sync/share-render.d.ts +3 -1
  21. package/dist/core/sync/share-render.d.ts.map +1 -1
  22. package/dist/db-worker.js +51 -41
  23. package/dist/index.js +1201 -260
  24. package/dist/index.js.map +16 -12
  25. package/dist/mh-runtime.js +1 -1
  26. package/dist/sw.js +1 -1
  27. package/dist/webui/fmt/manifest.d.ts +17 -0
  28. package/dist/webui/fmt/manifest.d.ts.map +1 -0
  29. package/dist/webui/server/assets.d.ts +5 -0
  30. package/dist/webui/server/assets.d.ts.map +1 -1
  31. package/dist/webui-fmt-clang.js +1 -0
  32. package/dist/webui-fmt-clang.wasm +0 -0
  33. package/dist/webui-fmt-gofmt.js +1 -0
  34. package/dist/webui-fmt-gofmt.wasm +0 -0
  35. package/dist/webui-fmt-lua.js +2 -0
  36. package/dist/webui-fmt-lua.wasm +0 -0
  37. package/dist/webui-fmt-ruff.js +2 -0
  38. package/dist/webui-fmt-ruff.wasm +0 -0
  39. package/dist/webui-fmt-sh.js +8 -0
  40. package/dist/webui-fmt-taplo.js +2 -0
  41. package/dist/webui-fmt-taplo.wasm +0 -0
  42. package/dist/webui-fmt.js +425 -0
  43. package/dist/webui.js +148 -75
  44. package/package.json +15 -1
package/dist/index.js CHANGED
@@ -395,7 +395,7 @@ var init_crdt = __esm(() => {
395
395
  DOMAIN = {
396
396
  databases: {
397
397
  table: "databases",
398
- cols: new Set(["name", "icon", "created_hlc", "__deleted"])
398
+ cols: new Set(["name", "icon", "meta", "created_hlc", "__deleted"])
399
399
  },
400
400
  properties: {
401
401
  table: "properties",
@@ -576,11 +576,17 @@ var init_properties = __esm(() => {
576
576
  });
577
577
 
578
578
  // src/core/databases.ts
579
+ function rowOut(r) {
580
+ if (!r)
581
+ return null;
582
+ return { ...r, meta: r.meta === null ? null : JSON.parse(r.meta) };
583
+ }
579
584
  function getDatabase(db, id) {
580
- return db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE id = ? AND __deleted = 0`).get(id);
585
+ return rowOut(db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE id = ? AND __deleted = 0`).get(id));
581
586
  }
582
587
  function listDatabases(db) {
583
- return db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE __deleted = 0 ORDER BY created_hlc`).all();
588
+ const rows = db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE __deleted = 0 ORDER BY created_hlc`).all();
589
+ return rows.map((r) => rowOut(r));
584
590
  }
585
591
  function liveChildren(db, table, col, parentId) {
586
592
  return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r) => r.id);
@@ -592,7 +598,7 @@ var init_databases = __esm(() => {
592
598
  init_errors();
593
599
  init_properties();
594
600
  init_records();
595
- DATABASE_COLS = ["id", "name", "icon", "created_hlc"];
601
+ DATABASE_COLS = ["id", "name", "icon", "meta", "created_hlc"];
596
602
  DATABASE_SELECT = DATABASE_COLS.join(", ");
597
603
  createDatabase = grouped(function createDatabase2(db, opts) {
598
604
  const id = newId("db", opts.name);
@@ -609,6 +615,11 @@ var init_databases = __esm(() => {
609
615
  emit(db, "databases", id, "name", fields.name);
610
616
  if (fields.icon !== undefined)
611
617
  emit(db, "databases", id, "icon", fields.icon);
618
+ if (fields.meta !== undefined) {
619
+ if (fields.meta !== null && (typeof fields.meta !== "object" || Array.isArray(fields.meta)))
620
+ throw new MhError("invalid_input", "meta must be a JSON object or null");
621
+ emit(db, "databases", id, "meta", fields.meta);
622
+ }
612
623
  return getDatabase(db, id);
613
624
  });
614
625
  duplicateDatabase = grouped(function duplicateDatabase2(db, id, opts = {}) {
@@ -619,6 +630,11 @@ var init_databases = __esm(() => {
619
630
  name: opts.name ?? src.name,
620
631
  icon: opts.icon ?? src.icon ?? undefined
621
632
  });
633
+ if (src.meta !== null) {
634
+ const { collapsed: _hidden, ...meta } = src.meta;
635
+ if (Object.keys(meta).length)
636
+ emit(db, "databases", dup.id, "meta", meta);
637
+ }
622
638
  const propIdMap = new Map;
623
639
  for (const p of listProperties(db, id)) {
624
640
  const config = p.config?.database === id ? { ...p.config, database: dup.id } : p.config ?? undefined;
@@ -1259,7 +1275,7 @@ function countOccurrences(haystack, needle) {
1259
1275
  }
1260
1276
  return n;
1261
1277
  }
1262
- var DOCUMENT_COLS, DOCUMENT_SELECT, DOC_SUMMARY_COLS, DOC_SUMMARY_SELECT, createDocument, ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id", backfillDocumentOrderKeys, moveDocument, duplicateDocument, updateDocument, editDocument, appendDocument, prependDocument, deleteDocument;
1278
+ var DOCUMENT_COLS, DOCUMENT_SELECT, DOC_SUMMARY_COLS, DOC_SUMMARY_SELECT, createDocument, ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id", backfillDocumentOrderKeys, moveDocument, duplicateDocument, updateDocument, editDocument, editDocumentBatch, appendDocument, prependDocument, deleteDocument;
1263
1279
  var init_documents = __esm(() => {
1264
1280
  init_ids();
1265
1281
  init_crdt();
@@ -1376,19 +1392,52 @@ var init_documents = __esm(() => {
1376
1392
 
1377
1393
  `)) {
1378
1394
  const affected = blocks.filter((b) => b.text.includes(opts.old));
1379
- const next = affected.map((b) => opts.replaceAll ? b.text.replaceAll(opts.old, opts.new) : b.text.replace(opts.old, opts.new));
1395
+ const next = affected.map((b) => opts.replaceAll ? b.text.replaceAll(opts.old, () => opts.new) : b.text.replace(opts.old, () => opts.new));
1380
1396
  if (next.every((t) => t.length > 0 && !t.includes(`
1381
1397
 
1382
1398
  `))) {
1383
1399
  const targets = opts.replaceAll ? affected : affected.slice(0, 1);
1384
- targets.forEach((b, i) => emit(db, "doc_blocks", b.id, "text", next[i]));
1385
- return { id, changed: true, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
1400
+ let wrote = false;
1401
+ targets.forEach((b, i) => {
1402
+ if (next[i] !== b.text) {
1403
+ emit(db, "doc_blocks", b.id, "text", next[i]);
1404
+ wrote = true;
1405
+ }
1406
+ });
1407
+ return { id, changed: wrote, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
1386
1408
  }
1387
1409
  }
1388
- const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
1410
+ const newBody = opts.replaceAll ? body.replaceAll(opts.old, () => opts.new) : body.replace(opts.old, () => opts.new);
1389
1411
  reconcileBody(db, id, newBody);
1390
1412
  return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
1391
1413
  });
1414
+ editDocumentBatch = grouped(function editDocumentBatch2(db, id, opts) {
1415
+ if (!getDocument(db, id))
1416
+ throw new MhError("not_found", `no such document: ${id}`);
1417
+ if (opts.edits.length === 0)
1418
+ throw new MhError("invalid_input", "no edits given");
1419
+ if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
1420
+ throw new MhError("stale", `stale: document changed since ${opts.ifMatch}; re-read first`);
1421
+ ensureBlocks(db, id);
1422
+ const orig = serializeDocBlocks(liveBlocks(db, id).map((b) => ({ text: b.text, blankAfter: b.blank_after })));
1423
+ let body = orig;
1424
+ let total = 0;
1425
+ opts.edits.forEach((e, i) => {
1426
+ if (e.old === "")
1427
+ throw new MhError("invalid_input", `edit[${i}]: old must not be empty`);
1428
+ const count = countOccurrences(body, e.old);
1429
+ if (count === 0)
1430
+ throw new MhError("invalid_input", `edit[${i}]: anchor not found: no match for old`);
1431
+ if (count > 1 && !e.replaceAll)
1432
+ throw new MhError("ambiguous", `edit[${i}]: ${count} matches for old; add surrounding context or set replaceAll`);
1433
+ const repl = e.new ?? "";
1434
+ body = e.replaceAll ? body.replaceAll(e.old, () => repl) : body.replace(e.old, () => repl);
1435
+ total += e.replaceAll ? count : 1;
1436
+ });
1437
+ if (body !== orig)
1438
+ reconcileBody(db, id, body);
1439
+ return { id, changed: body !== orig, replaced: total, version: documentVersion(db, id) };
1440
+ });
1392
1441
  appendDocument = grouped(function appendDocument2(db, id, body) {
1393
1442
  if (!getDocument(db, id))
1394
1443
  throw new MhError("not_found", `no such document: ${id}`);
@@ -18759,38 +18808,382 @@ var init_shares = __esm(() => {
18759
18808
  SHARE_KINDS = new Set(["doc", "database", "site"]);
18760
18809
  });
18761
18810
 
18811
+ // src/core/md/grammar.ts
18812
+ function safeUrl(url2, opts = {}) {
18813
+ const probe = url2.replace(/[\u0000-\u0020]+/g, "");
18814
+ const m = /^([a-z][a-z0-9+.-]*):/i.exec(probe);
18815
+ if (!m)
18816
+ return url2;
18817
+ const scheme = m[1].toLowerCase();
18818
+ if (scheme === "http" || scheme === "https" || scheme === "mailto" || scheme === "blob")
18819
+ return url2;
18820
+ if (scheme === "data" && opts.allowData)
18821
+ return url2;
18822
+ return "#";
18823
+ }
18824
+ function extOf(url2) {
18825
+ const clean = url2.split(/[?#]/, 1)[0] ?? url2;
18826
+ const base = clean.slice(clean.lastIndexOf("/") + 1);
18827
+ const dot = base.lastIndexOf(".");
18828
+ return dot >= 0 ? base.slice(dot + 1).toLowerCase() : "";
18829
+ }
18830
+ function imageSyntaxKind(ext2) {
18831
+ if (VIDEO_EXTS.has(ext2))
18832
+ return "video";
18833
+ if (AUDIO_EXTS.has(ext2))
18834
+ return "audio";
18835
+ return "image";
18836
+ }
18837
+ function parseMediaUrl(url2) {
18838
+ const hash3 = url2.indexOf("#");
18839
+ const frag = hash3 >= 0 ? url2.slice(hash3) : "";
18840
+ const noFrag = hash3 >= 0 ? url2.slice(0, hash3) : url2;
18841
+ const q = noFrag.indexOf("?");
18842
+ if (q < 0)
18843
+ return { src: url2 };
18844
+ const base = noFrag.slice(0, q);
18845
+ let width;
18846
+ const kept = [];
18847
+ for (const p of noFrag.slice(q + 1).split("&").filter(Boolean)) {
18848
+ const m = p.match(/^w=(\d+)$/);
18849
+ if (m)
18850
+ width = parseInt(m[1], 10);
18851
+ else
18852
+ kept.push(p);
18853
+ }
18854
+ return { src: base + (kept.length ? "?" + kept.join("&") : "") + frag, width };
18855
+ }
18856
+ function matchMediaEmbed(line) {
18857
+ const t = line.trim();
18858
+ let m = t.match(/^!\[([^\]]*)\]\(([^\s)]+)\)$/);
18859
+ if (m) {
18860
+ const { src, width } = parseMediaUrl(m[2]);
18861
+ const kind = imageSyntaxKind(extOf(src));
18862
+ const r = { kind, src, name: m[1] };
18863
+ if (kind === "image" && width)
18864
+ r.width = width;
18865
+ return r;
18866
+ }
18867
+ m = t.match(/^\[([^\]]*)\]\(([^\s)]+)(?:\s+"([^"]*)")?\)$/);
18868
+ if (m && m[2].startsWith("/blob/")) {
18869
+ const r = { kind: "file", src: m[2], name: m[1] };
18870
+ if (m[3] != null && /^\d+$/.test(m[3]))
18871
+ r.size = parseInt(m[3], 10);
18872
+ return r;
18873
+ }
18874
+ return null;
18875
+ }
18876
+ function matchQuoteLine(text) {
18877
+ const m = text.match(RE.quote);
18878
+ return m ? m[1] : null;
18879
+ }
18880
+ function matchListLine(line, minIndent) {
18881
+ const indent = leadingIndent(line);
18882
+ if (indent < minIndent)
18883
+ return null;
18884
+ const text = stripIndent(line, indent);
18885
+ let m = text.match(/^[-*+]\s+\[([ xX])\][ \t](.*)$/);
18886
+ if (m) {
18887
+ return {
18888
+ indent,
18889
+ type: "todo",
18890
+ checked: m[1].toLowerCase() === "x",
18891
+ content: m[2]
18892
+ };
18893
+ }
18894
+ m = text.match(/^(\d+)[.)][ \t]+(.*)$/);
18895
+ if (m)
18896
+ return { indent, type: "numbered", content: m[2], num: parseInt(m[1], 10) };
18897
+ m = text.match(/^[-*+][ \t]+(.*)$/);
18898
+ if (m)
18899
+ return { indent, type: "bullet", content: m[1] };
18900
+ return null;
18901
+ }
18902
+ function leadingIndent(line) {
18903
+ let indent = 0;
18904
+ for (const ch of line) {
18905
+ if (ch === " ")
18906
+ indent++;
18907
+ else if (ch === "\t")
18908
+ indent += 4;
18909
+ else
18910
+ break;
18911
+ }
18912
+ return indent;
18913
+ }
18914
+ function stripIndent(line, columns) {
18915
+ let indent = 0;
18916
+ let i = 0;
18917
+ for (;i < line.length && indent < columns; i++) {
18918
+ const ch = line[i];
18919
+ if (ch === " ")
18920
+ indent++;
18921
+ else if (ch === "\t")
18922
+ indent += 4;
18923
+ else
18924
+ break;
18925
+ }
18926
+ return line.slice(i);
18927
+ }
18928
+ function isFenceClose(line, char, len) {
18929
+ const trimmed = line.trim();
18930
+ if (!trimmed || trimmed[0] !== char)
18931
+ return false;
18932
+ for (const ch of trimmed) {
18933
+ if (ch !== char)
18934
+ return false;
18935
+ }
18936
+ return trimmed.length >= len;
18937
+ }
18938
+ function cleanLang(lang) {
18939
+ return lang.trim().replace(/[^A-Za-z0-9_+.#-]/g, "");
18940
+ }
18941
+ function looksLikeTableAt(lines, i, minIndent) {
18942
+ const head = lines[i];
18943
+ const delim = lines[i + 1];
18944
+ if (head == null || delim == null)
18945
+ return false;
18946
+ if (leadingIndent(head) < minIndent || leadingIndent(delim) < minIndent)
18947
+ return false;
18948
+ const headText = stripIndent(head, minIndent);
18949
+ const delimText = stripIndent(delim, minIndent);
18950
+ if (!headText.includes("|") || !delimText.includes("|"))
18951
+ return false;
18952
+ const cells = splitTableRow(delimText);
18953
+ return cells.length > 0 && cells.every((c) => RE_DELIM_CELL.test(c));
18954
+ }
18955
+ function splitTableRow(line) {
18956
+ const cells = [];
18957
+ let cur = "";
18958
+ for (let i = 0;i < line.length; i++) {
18959
+ const ch = line[i];
18960
+ if (ch === "\\" && line[i + 1] === "|") {
18961
+ cur += "|";
18962
+ i++;
18963
+ continue;
18964
+ }
18965
+ if (ch === "|") {
18966
+ cells.push(cur);
18967
+ cur = "";
18968
+ continue;
18969
+ }
18970
+ cur += ch;
18971
+ }
18972
+ cells.push(cur);
18973
+ if (cells.length && cells[0].trim() === "")
18974
+ cells.shift();
18975
+ if (cells.length && cells[cells.length - 1].trim() === "")
18976
+ cells.pop();
18977
+ return cells.map((c) => c.trim());
18978
+ }
18979
+ var HTML_FENCE = "mh-html", RE, VIDEO_EXTS, AUDIO_EXTS, RE_DELIM_CELL;
18980
+ var init_grammar = __esm(() => {
18981
+ RE = {
18982
+ fenceOpen: /^\s*(`{3,}|~{3,})\s*([^\s`]*)?.*$/,
18983
+ h: /^(#{1,6})\s+(.*)$/,
18984
+ todo: /^\s*[-*+]\s+\[([ xX])\][ \t](.*)$/,
18985
+ bullet: /^\s*[-*+]\s+(.*)$/,
18986
+ numbered: /^\s*(\d+)[.)]\s+(.*)$/,
18987
+ quote: /^>[ \t](.*)$/,
18988
+ divider: /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/
18989
+ };
18990
+ VIDEO_EXTS = new Set(["mp4", "webm", "mov", "m4v", "ogv", "mkv", "ogg"]);
18991
+ AUDIO_EXTS = new Set(["mp3", "wav", "m4a", "aac", "flac", "opus", "oga", "weba"]);
18992
+ RE_DELIM_CELL = /^\s*:?-+:?\s*$/;
18993
+ });
18994
+
18995
+ // src/core/md/inline.ts
18996
+ function tokenizeInline(text) {
18997
+ if (!text)
18998
+ return [];
18999
+ const cands = [];
19000
+ for (const p of PATTERNS) {
19001
+ p.re.lastIndex = 0;
19002
+ let m;
19003
+ while (m = p.re.exec(text)) {
19004
+ const start = m.index;
19005
+ const end = start + m[0].length;
19006
+ let innerFrom;
19007
+ let innerTo;
19008
+ let url2;
19009
+ let alt;
19010
+ if (p.kind === "link") {
19011
+ innerFrom = start + 1;
19012
+ innerTo = start + 1 + m[1].length;
19013
+ url2 = m[2];
19014
+ } else if (p.kind === "image") {
19015
+ innerFrom = start + 2;
19016
+ innerTo = start + 2 + m[1].length;
19017
+ url2 = m[2];
19018
+ alt = m[1];
19019
+ } else {
19020
+ innerFrom = start + p.delim;
19021
+ innerTo = end - p.delim;
19022
+ if (innerTo <= innerFrom)
19023
+ continue;
19024
+ }
19025
+ cands.push({ kind: p.kind, start, end, innerFrom, innerTo, url: url2, alt, rank: p.rank });
19026
+ }
19027
+ }
19028
+ cands.sort((a, b) => a.start - b.start || a.rank - b.rank || b.end - a.end);
19029
+ const out = [];
19030
+ let cursor = 0;
19031
+ for (const c of cands) {
19032
+ if (c.start < cursor)
19033
+ continue;
19034
+ cursor = c.end;
19035
+ out.push({
19036
+ kind: c.kind,
19037
+ start: c.start,
19038
+ end: c.end,
19039
+ innerFrom: c.innerFrom,
19040
+ innerTo: c.innerTo,
19041
+ ...c.url !== undefined ? { url: c.url } : {},
19042
+ ...c.alt !== undefined ? { alt: c.alt } : {}
19043
+ });
19044
+ }
19045
+ return out;
19046
+ }
19047
+ var PATTERNS;
19048
+ var init_inline = __esm(() => {
19049
+ PATTERNS = [
19050
+ { kind: "code", re: /(?<!\\)`([^`\n]+)`/g, delim: 1, rank: 0 },
19051
+ { kind: "image", re: /(?<!\\)!\[([^\]\n]*)\]\(([^)\s]+)\)/g, delim: 0, rank: 1 },
19052
+ { kind: "strong", re: /(?<!\\)\*\*([^*\n]+?)(?<!\\)\*\*/g, delim: 2, rank: 2 },
19053
+ { kind: "strong", re: /(?<!\\)__([^_\n]+?)(?<!\\)__/g, delim: 2, rank: 2 },
19054
+ { kind: "del", re: /(?<!\\)~~([^~\n]+?)(?<!\\)~~/g, delim: 2, rank: 3 },
19055
+ { kind: "em", re: /(?<![*\w])(?<!\\)\*([^*\n]+?)(?<!\\)\*(?!\*)/g, delim: 1, rank: 4 },
19056
+ { kind: "em", re: /(?<![_\w])(?<!\\)_([^_\n]+?)(?<!\\)_(?!_)/g, delim: 1, rank: 4 },
19057
+ { kind: "link", re: /(?<!\\)\[([^\]\n]+)\]\(([^)\s]+)\)/g, delim: 0, rank: 5 }
19058
+ ];
19059
+ });
19060
+
19061
+ // src/core/md/heal.ts
19062
+ function fenceMask(lines) {
19063
+ const mask = new Array(lines.length).fill(false);
19064
+ for (let i = 0;i < lines.length; i++) {
19065
+ const m = lines[i].match(RE.fenceOpen);
19066
+ if (!m)
19067
+ continue;
19068
+ const ch = m[1][0];
19069
+ const len = m[1].length;
19070
+ let close = -1;
19071
+ for (let j = i + 1;j < lines.length; j++) {
19072
+ if (isFenceClose(lines[j], ch, len)) {
19073
+ close = j;
19074
+ break;
19075
+ }
19076
+ }
19077
+ if (close === -1)
19078
+ continue;
19079
+ for (let j = i;j <= close; j++)
19080
+ mask[j] = true;
19081
+ i = close;
19082
+ }
19083
+ return mask;
19084
+ }
19085
+ function healLegacyMarkdown(src) {
19086
+ if (!MAYBE_LEGACY.test(src))
19087
+ return src;
19088
+ const lines = src.split(`
19089
+ `);
19090
+ const inFence = fenceMask(lines);
19091
+ const quoteish = (i) => i >= 0 && i < lines.length && !inFence[i] && matchQuoteLine(lines[i].replace(/^[ \t]*/, "")) !== null;
19092
+ let changed = false;
19093
+ for (let i = 0;i < lines.length; i++) {
19094
+ if (inFence[i])
19095
+ continue;
19096
+ const line = lines[i];
19097
+ if (LEGACY_EMPTY_TODO.test(line)) {
19098
+ lines[i] = line + " ";
19099
+ changed = true;
19100
+ } else if (BARE_QUOTE.test(line) && (quoteish(i - 1) || quoteish(i + 1))) {
19101
+ lines[i] = line + " ";
19102
+ changed = true;
19103
+ }
19104
+ }
19105
+ for (let i = lines.length - 1;i >= 0; i--) {
19106
+ if (inFence[i])
19107
+ continue;
19108
+ const line = lines[i];
19109
+ if (BARE_QUOTE.test(line) && (quoteish(i - 1) || quoteish(i + 1))) {
19110
+ lines[i] = line + " ";
19111
+ changed = true;
19112
+ }
19113
+ }
19114
+ return changed ? lines.join(`
19115
+ `) : src;
19116
+ }
19117
+ var LEGACY_EMPTY_TODO, BARE_QUOTE, MAYBE_LEGACY;
19118
+ var init_heal = __esm(() => {
19119
+ init_grammar();
19120
+ LEGACY_EMPTY_TODO = /^[ \t]*[-*+][ \t]+\[[ xX]\]$/;
19121
+ BARE_QUOTE = /^[ \t]*>$/;
19122
+ MAYBE_LEGACY = /^[ \t]*(?:[-*+][ \t]+\[[ xX]\]|>)$/m;
19123
+ });
19124
+
18762
19125
  // src/core/sync/share-render.ts
18763
19126
  function escapeHtml(s) {
18764
19127
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18765
19128
  }
18766
19129
  function renderInline(src, opts = {}) {
18767
- const codes = [];
18768
- let s = src.replace(/`([^`]+)`/g, (_m, c) => {
18769
- codes.push(c);
18770
- return A + (codes.length - 1) + B;
18771
- });
18772
- s = escapeHtml(s);
18773
- s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_m, alt, u) => {
18774
- const url2 = rewrite(String(u), opts).replace(/"/g, "&quot;");
18775
- return `<img src="${url2}" alt="${escapeHtml(String(alt))}" loading="lazy">`;
18776
- });
18777
- s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, t, u) => {
18778
- const url2 = rewrite(String(u), opts).replace(/"/g, "&quot;");
18779
- return `<a href="${url2}" target="_blank" rel="noreferrer noopener">${t}</a>`;
18780
- });
18781
- s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
18782
- s = s.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
18783
- s = s.replace(RESTORE, (_m, i) => `<code>${escapeHtml(codes[+i] ?? "")}</code>`);
18784
- return s;
19130
+ const tokens = tokenizeInline(src);
19131
+ let out = "";
19132
+ let pos = 0;
19133
+ for (const t of tokens) {
19134
+ out += escapeHtml(src.slice(pos, t.start));
19135
+ const inner = src.slice(t.innerFrom, t.innerTo);
19136
+ const rawUrl = rewrite(t.url ?? "", opts);
19137
+ switch (t.kind) {
19138
+ case "code":
19139
+ out += `<code>${escapeHtml(inner)}</code>`;
19140
+ break;
19141
+ case "strong":
19142
+ out += `<strong>${escapeHtml(inner)}</strong>`;
19143
+ break;
19144
+ case "em":
19145
+ out += `<em>${escapeHtml(inner)}</em>`;
19146
+ break;
19147
+ case "del":
19148
+ out += `<del>${escapeHtml(inner)}</del>`;
19149
+ break;
19150
+ case "link": {
19151
+ const href = escapeHtml(safeUrl(rawUrl));
19152
+ out += `<a href="${href}" target="_blank" rel="noreferrer noopener">${escapeHtml(inner)}</a>`;
19153
+ break;
19154
+ }
19155
+ case "image": {
19156
+ const imgSrc = escapeHtml(safeUrl(rawUrl, { allowData: true }));
19157
+ out += `<img src="${imgSrc}" alt="${escapeHtml(t.alt ?? "")}" loading="lazy">`;
19158
+ break;
19159
+ }
19160
+ }
19161
+ pos = t.end;
19162
+ }
19163
+ return out + escapeHtml(src.slice(pos));
18785
19164
  }
18786
19165
  function rewrite(url2, opts) {
18787
19166
  if (opts.rewriteBlob && url2.startsWith("/blob/"))
18788
19167
  return opts.rewriteBlob(url2);
18789
19168
  return url2;
18790
19169
  }
19170
+ function renderMedia(m, opts) {
19171
+ const raw = rewrite(m.src, opts);
19172
+ if (m.kind === "video") {
19173
+ const src = escapeHtml(safeUrl(raw, { allowData: true }));
19174
+ return `<p class="mh-media"><video src="${src}" controls preload="metadata"></video></p>`;
19175
+ }
19176
+ if (m.kind === "audio") {
19177
+ const src = escapeHtml(safeUrl(raw, { allowData: true }));
19178
+ return `<p class="mh-media"><audio src="${src}" controls preload="metadata"></audio></p>`;
19179
+ }
19180
+ const href = escapeHtml(safeUrl(raw));
19181
+ return `<p class="mh-file"><a href="${href}" download>${escapeHtml(m.name || "file")}</a></p>`;
19182
+ }
18791
19183
  function renderMarkdown(md, opts = {}) {
18792
- const lines = (md ?? "").replace(/\r\n?/g, `
18793
- `).split(`
19184
+ const healed = healLegacyMarkdown((md ?? "").replace(/\r\n?/g, `
19185
+ `));
19186
+ const lines = healed.split(`
18794
19187
  `);
18795
19188
  const out = [];
18796
19189
  let i = 0;
@@ -18803,38 +19196,39 @@ function renderMarkdown(md, opts = {}) {
18803
19196
  };
18804
19197
  while (i < lines.length) {
18805
19198
  const line = lines[i];
18806
- const fence = line.match(FENCE);
19199
+ const fence = line.match(RE.fenceOpen);
18807
19200
  if (fence) {
18808
- flushPara();
18809
19201
  const marker = fence[1][0];
18810
- const info = (fence[2] ?? "").trim();
18811
- const body = [];
18812
- i++;
18813
- while (i < lines.length) {
18814
- const m = lines[i].match(/^\s*(`{3,}|~{3,})\s*$/);
18815
- if (m && m[1][0] === marker) {
18816
- i++;
19202
+ const len = fence[1].length;
19203
+ let close = -1;
19204
+ for (let j = i + 1;j < lines.length; j++) {
19205
+ if (isFenceClose(lines[j], marker, len)) {
19206
+ close = j;
18817
19207
  break;
18818
19208
  }
18819
- body.push(lines[i]);
18820
- i++;
18821
19209
  }
18822
- if (info === "mh-html") {
18823
- out.push(`<iframe class="mh-embed" sandbox="allow-scripts allow-popups" srcdoc="${escapeHtml(body.join(`
19210
+ if (close !== -1) {
19211
+ flushPara();
19212
+ const info = cleanLang(fence[2] ?? "");
19213
+ const body = lines.slice(i + 1, close);
19214
+ i = close + 1;
19215
+ if (info === HTML_FENCE) {
19216
+ out.push(`<iframe class="mh-embed" sandbox="allow-scripts allow-popups" srcdoc="${escapeHtml(body.join(`
18824
19217
  `))}"></iframe>`);
18825
- } else {
18826
- const cls = info ? ` class="language-${escapeHtml(info)}"` : "";
18827
- out.push(`<pre><code${cls}>${escapeHtml(body.join(`
19218
+ } else {
19219
+ const cls = info ? ` class="language-${escapeHtml(info)}"` : "";
19220
+ out.push(`<pre><code${cls}>${escapeHtml(body.join(`
18828
19221
  `))}</code></pre>`);
19222
+ }
19223
+ continue;
18829
19224
  }
18830
- continue;
18831
19225
  }
18832
19226
  if (line.trim() === "") {
18833
19227
  flushPara();
18834
19228
  i++;
18835
19229
  continue;
18836
19230
  }
18837
- const h = line.match(/^(#{1,6})\s+(.*)$/);
19231
+ const h = stripLead(line).match(RE.h);
18838
19232
  if (h) {
18839
19233
  flushPara();
18840
19234
  const level = h[1].length;
@@ -18842,30 +19236,33 @@ function renderMarkdown(md, opts = {}) {
18842
19236
  i++;
18843
19237
  continue;
18844
19238
  }
18845
- if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
19239
+ if (RE.divider.test(line)) {
18846
19240
  flushPara();
18847
19241
  out.push("<hr>");
18848
19242
  i++;
18849
19243
  continue;
18850
19244
  }
18851
- if (/^\s*>\s?/.test(line)) {
19245
+ if (matchQuoteLine(stripLead(line)) !== null) {
18852
19246
  flushPara();
18853
19247
  const q = [];
18854
- while (i < lines.length && /^\s*>\s?/.test(lines[i])) {
18855
- q.push(lines[i].replace(/^\s*>\s?/, ""));
19248
+ while (i < lines.length) {
19249
+ const content = matchQuoteLine(stripLead(lines[i]));
19250
+ if (content === null)
19251
+ break;
19252
+ q.push(content);
18856
19253
  i++;
18857
19254
  }
18858
19255
  out.push(`<blockquote>${renderMarkdown(q.join(`
18859
19256
  `), opts)}</blockquote>`);
18860
19257
  continue;
18861
19258
  }
18862
- if (line.includes("|") && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && lines[i + 1].includes("-")) {
19259
+ if (looksLikeTableAt(lines, i, 0)) {
18863
19260
  flushPara();
18864
19261
  const rows = [];
18865
- const head = splitRow(line);
19262
+ const head = splitTableRow(stripLead(lines[i]));
18866
19263
  i += 2;
18867
19264
  while (i < lines.length && lines[i].includes("|") && lines[i].trim() !== "") {
18868
- rows.push(splitRow(lines[i]));
19265
+ rows.push(splitTableRow(stripLead(lines[i])));
18869
19266
  i++;
18870
19267
  }
18871
19268
  const th = head.map((c) => `<th>${renderInline(c, opts)}</th>`).join("");
@@ -18873,24 +19270,31 @@ function renderMarkdown(md, opts = {}) {
18873
19270
  out.push(`<table><thead><tr>${th}</tr></thead><tbody>${body}</tbody></table>`);
18874
19271
  continue;
18875
19272
  }
18876
- const ul = line.match(/^\s*[-*+]\s+/);
18877
- const ol = line.match(/^\s*\d+[.)]\s+/);
18878
- if (ul || ol) {
19273
+ const first = matchListLine(line, 0);
19274
+ if (first) {
18879
19275
  flushPara();
18880
- const ordered = !!ol;
18881
- const items = [];
18882
- const re = ordered ? /^\s*\d+[.)]\s+/ : /^\s*[-*+]\s+/;
18883
- while (i < lines.length && re.test(lines[i])) {
18884
- items.push(lines[i].replace(re, ""));
19276
+ const ordered = first.type === "numbered";
19277
+ const lis = [];
19278
+ while (i < lines.length) {
19279
+ const it = matchListLine(lines[i], 0);
19280
+ if (!it || it.type === "numbered" !== ordered)
19281
+ break;
19282
+ if (it.type === "todo") {
19283
+ lis.push(`<li class="todo"><input type="checkbox" disabled${it.checked ? " checked" : ""}> ${renderInline(it.content, opts)}</li>`);
19284
+ } else if (it.type === "numbered" && it.num !== undefined) {
19285
+ lis.push(`<li value="${it.num}">${renderInline(it.content, opts)}</li>`);
19286
+ } else {
19287
+ lis.push(`<li>${renderInline(it.content, opts)}</li>`);
19288
+ }
18885
19289
  i++;
18886
19290
  }
18887
- const lis = items.map((t) => `<li>${renderInline(t, opts)}</li>`).join("");
18888
- out.push(ordered ? `<ol>${lis}</ol>` : `<ul>${lis}</ul>`);
19291
+ out.push(ordered ? `<ol>${lis.join("")}</ol>` : `<ul>${lis.join("")}</ul>`);
18889
19292
  continue;
18890
19293
  }
18891
- if (/^\s*!\[[^\]]*\]\([^)\s]+\)\s*$/.test(line)) {
19294
+ const media = matchMediaEmbed(line);
19295
+ if (media) {
18892
19296
  flushPara();
18893
- out.push(`<p class="mh-img">${renderInline(line.trim(), opts)}</p>`);
19297
+ out.push(media.kind === "image" ? `<p class="mh-img">${renderInline(line.trim(), opts)}</p>` : renderMedia(media, opts));
18894
19298
  i++;
18895
19299
  continue;
18896
19300
  }
@@ -18901,20 +19305,13 @@ function renderMarkdown(md, opts = {}) {
18901
19305
  return out.join(`
18902
19306
  `);
18903
19307
  }
18904
- function splitRow(line) {
18905
- let s = line.trim();
18906
- if (s.startsWith("|"))
18907
- s = s.slice(1);
18908
- if (s.endsWith("|"))
18909
- s = s.slice(0, -1);
18910
- return s.split("|").map((c) => c.trim());
19308
+ function stripLead(line) {
19309
+ return stripIndent(line, Number.MAX_SAFE_INTEGER);
18911
19310
  }
18912
- var A, B, RESTORE, FENCE;
18913
19311
  var init_share_render = __esm(() => {
18914
- A = String.fromCharCode(57344);
18915
- B = String.fromCharCode(57345);
18916
- RESTORE = new RegExp(A + "(\\d+)" + B, "g");
18917
- FENCE = /^\s*(`{3,}|~{3,})(.*)$/;
19312
+ init_grammar();
19313
+ init_inline();
19314
+ init_heal();
18918
19315
  });
18919
19316
 
18920
19317
  // src/core/sync/share-serve.ts
@@ -19332,6 +19729,8 @@ function pageShell(title, inner, opts = {}) {
19332
19729
  td[contenteditable]{outline:none} td.saving{opacity:.5} td.saved{background:rgba(46,160,67,.18)}
19333
19730
  .tag{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:999px;padding:1px 9px;font-size:12px}
19334
19731
  .muted{color:var(--muted)} .mh-img{text-align:center}
19732
+ .mh-media{text-align:center} .mh-media video,.mh-media audio{max-width:100%}
19733
+ .mh-file a{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px 14px;color:var(--accent);text-decoration:none}
19335
19734
  iframe.mh-embed{width:100%;border:1px solid var(--line);border-radius:8px;min-height:240px}
19336
19735
  textarea.mh-raw{width:100%;min-height:60vh;font:14px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--fg)}
19337
19736
  ul.files{list-style:none;padding:0} ul.files li{padding:4px 0} button.file{background:none;border:0;color:var(--accent);cursor:pointer;font-size:15px;padding:0}
@@ -19509,6 +19908,7 @@ CREATE TABLE IF NOT EXISTS databases (
19509
19908
  id TEXT PRIMARY KEY,
19510
19909
  name TEXT,
19511
19910
  icon TEXT,
19911
+ meta TEXT, -- JSON: generic replicated metadata (e.g. UI flags), one LWW register
19512
19912
  created_hlc TEXT,
19513
19913
  __deleted INTEGER NOT NULL DEFAULT 0
19514
19914
  );
@@ -19791,9 +20191,25 @@ function migrateStoragePeerUrls(db) {
19791
20191
  });
19792
20192
  tx();
19793
20193
  }
20194
+ function migrateDatabases(db) {
20195
+ if (hasColumn(db, "databases", "meta"))
20196
+ return;
20197
+ db.exec("ALTER TABLE databases ADD COLUMN meta TEXT");
20198
+ db.exec(`
20199
+ UPDATE databases SET meta = (
20200
+ SELECT c.value FROM crdt_changes c
20201
+ WHERE c.dataset = 'databases' AND c.col = 'meta' AND c.row_id = databases.id
20202
+ ORDER BY c.hlc DESC LIMIT 1
20203
+ )
20204
+ WHERE id IN (
20205
+ SELECT DISTINCT row_id FROM crdt_changes WHERE dataset = 'databases' AND col = 'meta'
20206
+ )
20207
+ `);
20208
+ }
19794
20209
  function initSchema(db) {
19795
20210
  runSchema(db);
19796
20211
  migrateOplog(db);
20212
+ migrateDatabases(db);
19797
20213
  migrateCrdtChangesSeq(db);
19798
20214
  migrateRecords(db);
19799
20215
  migratePeers(db);
@@ -20033,7 +20449,7 @@ init_config();
20033
20449
  // package.json
20034
20450
  var package_default = {
20035
20451
  name: "@tensorix/metahub",
20036
- version: "0.2.2",
20452
+ version: "0.3.2",
20037
20453
  description: "Metahub core library and CLI",
20038
20454
  license: "AGPL-3.0-only",
20039
20455
  type: "module",
@@ -20063,16 +20479,21 @@ var package_default = {
20063
20479
  "build:shell": "bun run scripts/build-shell.ts",
20064
20480
  "build:share-viewer": "bun run scripts/build-share-viewer.ts",
20065
20481
  "build:binaries": "bun run scripts/compile-binaries.ts",
20482
+ "build:binaries:host": "HOST_ONLY=1 bun run scripts/compile-binaries.ts",
20066
20483
  prepublishOnly: "bun run build",
20067
20484
  test: "bun test",
20068
20485
  release: "bun run scripts/tag-release.ts"
20069
20486
  },
20070
20487
  dependencies: {
20071
20488
  "@clack/prompts": "^1.5.0",
20489
+ "@codemirror/commands": "6.10.4",
20490
+ "@codemirror/state": "6.7.0",
20491
+ "@codemirror/view": "6.43.4",
20072
20492
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
20073
20493
  aws4fetch: "^1.0.20",
20074
20494
  citty: "^0.1.6",
20075
20495
  "highlight.js": "^11.11.1",
20496
+ jsqr: "^1.4.0",
20076
20497
  preact: "^10.29.2",
20077
20498
  "qrcode-generator": "^2.0.4",
20078
20499
  turndown: "^7.2.4",
@@ -20081,8 +20502,17 @@ var package_default = {
20081
20502
  },
20082
20503
  devDependencies: {
20083
20504
  "@happy-dom/global-registrator": "^20.10.2",
20505
+ "@prettier/plugin-php": "^0.25.0",
20084
20506
  "@types/bun": "latest",
20085
20507
  "@types/turndown": "^5.0.6",
20508
+ "@wasm-fmt/clang-format": "^22.1.8",
20509
+ "@wasm-fmt/gofmt": "^0.7.3",
20510
+ "@wasm-fmt/lua_fmt": "^0.3.3",
20511
+ "@wasm-fmt/ruff_fmt": "^0.15.20",
20512
+ "@wasm-fmt/taplo_fmt": "^0.2.5",
20513
+ "mvdan-sh": "^0.10.1",
20514
+ prettier: "^3.6",
20515
+ "sql-formatter": "^15",
20086
20516
  typescript: "^5"
20087
20517
  }
20088
20518
  };
@@ -20208,56 +20638,196 @@ function wantsHtml(req) {
20208
20638
  return req.method === "GET" && (req.headers.get("accept") ?? "").includes("text/html");
20209
20639
  }
20210
20640
  var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
20641
+ var CUBE_SVG = '<path d="M21 7.5 12 2 3 7.5v9L12 22l9-5.5z"/><path d="m3 7.5 9 5.5 9-5.5M12 22v-9"/>';
20642
+ var EYE_SVG = '<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/>';
20643
+ var EYE_OFF_SVG = '<path d="M4 4l16 16M9.5 9.5a3 3 0 0 0 4 4M6.5 6.7C4.5 8 3 10 2 12c2 4 6 7 10 7 1.6 0 3-.4 4.4-1.1M9.5 5.2A9.7 9.7 0 0 1 12 5c4 0 8 3 10 7-.6 1.3-1.5 2.5-2.5 3.5"/>';
20644
+ var UNLOCK_CSS = `
20645
+ :root{color-scheme:light;
20646
+ --bg:#ffffff;--surface:#fbfbfa;--surface-2:#f1f1ef;--fg:#2c2c30;--fg-soft:#5b5b62;
20647
+ --muted:#75757f;--line:#ebebe8;--line-strong:#dededb;--accent:#4a55d6;--accent-fg:#ffffff;
20648
+ --accent-soft:#eef0ff;--danger:#d6473b;--danger-soft:#fdeceb;--code-bg:#f8f8f6}
20649
+ :root[data-resolved="dark"]{color-scheme:dark;
20650
+ --bg:#1a1a1c;--surface:#202022;--surface-2:#2a2a2d;--fg:#e6e6e9;--fg-soft:#b4b4bb;
20651
+ --muted:#8b8b95;--line:#2c2c30;--line-strong:#3a3a40;--accent:#7b86ff;--accent-fg:#14141a;
20652
+ --accent-soft:#23243a;--danger:#f87168;--danger-soft:#341e1c;--code-bg:#26262a}
20653
+ *{box-sizing:border-box}
20654
+ html,body{margin:0;min-height:100%}
20655
+ body{font-family:"Hanken Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
20656
+ color:var(--fg);background:var(--bg);line-height:1.55;
20657
+ display:flex;align-items:center;justify-content:center;min-height:100dvh;
20658
+ padding:calc(24px + env(safe-area-inset-top)) calc(20px + env(safe-area-inset-right)) calc(24px + env(safe-area-inset-bottom)) calc(20px + env(safe-area-inset-left));
20659
+ -webkit-font-smoothing:antialiased;-webkit-tap-highlight-color:transparent;overflow:hidden}
20660
+ /* Ambient wash: two faint accent blooms behind the card. */
20661
+ body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
20662
+ background:
20663
+ radial-gradient(58vmax 58vmax at 14% -8%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 60%),
20664
+ radial-gradient(50vmax 50vmax at 108% 112%,color-mix(in srgb,var(--accent) 10%,transparent),transparent 55%)}
20665
+ .card{position:relative;z-index:1;width:100%;max-width:420px;background:var(--surface);
20666
+ border:1px solid var(--line);border-radius:18px;padding:32px 26px 26px;
20667
+ box-shadow:0 24px 60px rgba(20,20,40,.14),0 4px 14px rgba(20,20,40,.06);
20668
+ animation:rise .45s cubic-bezier(.2,.7,.3,1) both}
20669
+ @keyframes rise{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:none}}
20670
+ .brand{width:52px;height:52px;border-radius:14px;display:flex;align-items:center;justify-content:center;
20671
+ background:var(--accent-soft);color:var(--accent);margin-bottom:18px}
20672
+ .brand svg{width:28px;height:28px;fill:none;stroke:currentColor;stroke-width:1.6;
20673
+ stroke-linecap:round;stroke-linejoin:round}
20674
+ .brand path{stroke-dasharray:120;stroke-dashoffset:120;animation:draw .9s .12s ease forwards}
20675
+ @keyframes draw{to{stroke-dashoffset:0}}
20676
+ h1{font-size:21px;font-weight:650;letter-spacing:-.01em;margin:0 0 8px}
20677
+ .sub{color:var(--muted);font-size:13.5px;margin:0 0 22px;line-height:1.6}
20678
+ .sub code{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,monospace;font-size:12.5px;
20679
+ background:var(--code-bg);border:1px solid var(--line);border-radius:5px;padding:1px 6px;color:var(--fg-soft);
20680
+ white-space:nowrap}
20681
+ .field{position:relative;display:flex;align-items:center}
20682
+ input{width:100%;height:50px;padding:0 48px 0 15px;font-size:16px; /* 16px: never let iOS Safari zoom on focus */
20683
+ font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,monospace;letter-spacing:.01em;
20684
+ color:var(--fg);background:var(--bg);border:1.5px solid var(--line-strong);border-radius:12px;
20685
+ outline:none;transition:border-color .15s,box-shadow .15s}
20686
+ input::placeholder{font-family:var(--ui,inherit);letter-spacing:normal;color:var(--muted);opacity:.8}
20687
+ input:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
20688
+ .card.bad input{border-color:var(--danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--danger) 20%,transparent)}
20689
+ .card.bad .field{animation:shake .34s cubic-bezier(.36,.07,.19,.97)}
20690
+ @keyframes shake{10%,90%{transform:translateX(-1px)}20%,80%{transform:translateX(2px)}
20691
+ 30%,50%,70%{transform:translateX(-5px)}40%,60%{transform:translateX(5px)}}
20692
+ .peek{position:absolute;right:6px;width:40px;height:40px;border:0;background:none;cursor:pointer;
20693
+ color:var(--muted);display:flex;align-items:center;justify-content:center;border-radius:9px}
20694
+ .peek:hover{color:var(--fg-soft)}
20695
+ .peek svg{width:19px;height:19px;fill:none;stroke:currentColor;stroke-width:1.7;
20696
+ stroke-linecap:round;stroke-linejoin:round}
20697
+ .err{color:var(--danger);font-size:13px;font-weight:500;margin:11px 2px 0;
20698
+ display:flex;gap:6px;align-items:center;animation:fade .2s ease both}
20699
+ @keyframes fade{from{opacity:0}to{opacity:1}}
20700
+ button.go{width:100%;height:48px;margin-top:20px;border:0;border-radius:12px;cursor:pointer;
20701
+ background:var(--accent);color:var(--accent-fg);font-size:15px;font-weight:600;font-family:inherit;
20702
+ display:flex;align-items:center;justify-content:center;gap:9px;
20703
+ transition:filter .15s,transform .05s}
20704
+ button.go:hover{filter:brightness(1.06)}
20705
+ button.go:active{transform:translateY(1px)}
20706
+ button.go:disabled{opacity:.7;cursor:default}
20707
+ .spin{width:17px;height:17px;border:2px solid color-mix(in srgb,var(--accent-fg) 45%,transparent);
20708
+ border-top-color:var(--accent-fg);border-radius:50%;animation:spin .7s linear infinite}
20709
+ /* Full-page loader shown while a silent renewal is in flight. */
20710
+ .boot{position:fixed;inset:0;z-index:2;display:flex;align-items:center;justify-content:center;background:var(--bg)}
20711
+ .boot svg{width:40px;height:40px;fill:none;stroke:var(--accent);stroke-width:1.4;
20712
+ stroke-linecap:round;stroke-linejoin:round;animation:breathe 1.5s ease-in-out infinite}
20713
+ @keyframes breathe{0%,100%{opacity:.35;transform:scale(.94)}50%{opacity:1;transform:scale(1)}}
20714
+ @keyframes spin{to{transform:rotate(360deg)}}
20715
+ @media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important}
20716
+ .brand path{stroke-dashoffset:0}}
20717
+ `;
20211
20718
  function unlockPage() {
20212
- return `<!doctype html><html lang="en"><head>
20213
- <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
20214
- <title>Unlock</title>
20215
- <style>
20216
- body{font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
20217
- display:flex;min-height:100vh;margin:0;align-items:center;justify-content:center;
20218
- background:#0d1117;color:#e6edf3}
20219
- form{background:#161b22;border:1px solid #30363d;border-radius:10px;padding:28px;width:300px}
20220
- h1{font-size:16px;margin:0 0 14px}
20221
- input{width:100%;box-sizing:border-box;padding:8px;border-radius:6px;border:1px solid #30363d;
20222
- background:#0d1117;color:inherit;margin-bottom:12px}
20223
- button{width:100%;padding:8px;border:0;border-radius:6px;background:#238636;color:#fff;cursor:pointer}
20224
- .err{color:#f85149;margin:0 0 12px;font-size:13px}
20225
- </style></head><body>
20226
- <form id="f" style="display:none">
20227
- <h1>\uD83D\uDD12 This server requires a token</h1>
20228
- <p class="err" id="e" style="display:none">Token rejected \u2014 try again.</p>
20229
- <input id="t" type="password" placeholder="Token" autocomplete="current-password">
20230
- <button type="submit">Unlock</button>
20719
+ return `<!doctype html><html lang="zh-CN"><head>
20720
+ <meta charset="utf-8">
20721
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
20722
+ <meta name="theme-color" content="#ffffff">
20723
+ <title>\u9A8C\u8BC1\u8BBF\u95EE\u4EE4\u724C \xB7 metahub</title>
20724
+ <script>
20725
+ // FOUC-guard theme resolution, same key/logic as src/webui/theme.ts.
20726
+ try {
20727
+ var mt = localStorage.getItem("mh-theme");
20728
+ var dark = mt === "dark" || (mt !== "light" && matchMedia("(prefers-color-scheme: dark)").matches);
20729
+ document.documentElement.dataset.resolved = dark ? "dark" : "light";
20730
+ document.head.querySelector('meta[name=theme-color]').setAttribute("content", dark ? "#1a1a1c" : "#ffffff");
20731
+ } catch (e) {}
20732
+ </script>
20733
+ <style>${UNLOCK_CSS}</style></head><body>
20734
+ <div class="boot" id="boot"><svg viewBox="0 0 24 24">${CUBE_SVG}</svg></div>
20735
+ <form class="card" id="f" style="display:none" autocomplete="on">
20736
+ <div class="brand"><svg viewBox="0 0 24 24">${CUBE_SVG}</svg></div>
20737
+ <h1>\u8F93\u5165\u8BBF\u95EE\u4EE4\u724C</h1>
20738
+ <p class="sub">\u6B64\u670D\u52A1\u5668\u5DF2\u5F00\u542F\u8BBF\u95EE\u4FDD\u62A4\u3002\u5728\u5DF2\u767B\u5F55\u8BBE\u5907\u7684\u300C\u8BBE\u7F6E \u2192 \u8BBE\u5907\u4E0E\u6388\u6743\u300D\u83B7\u53D6\u767B\u5F55\u94FE\u63A5\u6216\u4E8C\u7EF4\u7801,\u6216\u5728\u670D\u52A1\u5668\u4E0A\u8FD0\u884C <code>mh token show</code>\u3002</p>
20739
+ <div class="field">
20740
+ <input id="t" type="password" placeholder="\u7C98\u8D34\u8BBF\u95EE\u4EE4\u724C\u6216\u767B\u5F55\u94FE\u63A5"
20741
+ autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="go">
20742
+ <button type="button" class="peek" id="peek" aria-label="\u663E\u793A\u4EE4\u724C"><svg viewBox="0 0 24 24">${EYE_SVG}</svg></button>
20743
+ </div>
20744
+ <p class="err" id="e" style="display:none"></p>
20745
+ <button type="submit" class="go" id="go">\u89E3\u9501\u5E76\u8FDB\u5165</button>
20231
20746
  </form>
20232
20747
  <script>
20233
20748
  var KEY = "mh_token";
20749
+ var form = document.getElementById("f"), boot = document.getElementById("boot");
20750
+ var input = document.getElementById("t"), errEl = document.getElementById("e");
20751
+ var goBtn = document.getElementById("go"), peek = document.getElementById("peek");
20752
+
20234
20753
  function save(t) {
20235
20754
  localStorage.setItem(KEY, t);
20236
20755
  document.cookie = "${COOKIE}=" + encodeURIComponent(t) + "; ${COOKIE_ATTRS}" +
20237
20756
  (location.protocol === "https:" ? "; Secure" : "");
20238
20757
  }
20239
- function showForm() {
20240
- document.getElementById("f").style.display = "block";
20241
- // A leftover token means an attempt (or silent renewal) failed the gate.
20242
- if (localStorage.getItem(KEY)) document.getElementById("e").style.display = "block";
20243
- document.getElementById("t").focus();
20244
- }
20245
- document.getElementById("f").addEventListener("submit", function (ev) {
20758
+ // Accept a pasted "\u2026/?token=xxx" login link, not just the bare token.
20759
+ function normalize(v) {
20760
+ v = (v || "").trim();
20761
+ var m = /[?&#]token=([^&\\s]+)/.exec(v);
20762
+ if (m) { try { return decodeURIComponent(m[1]); } catch (e) { return m[1]; } }
20763
+ return v;
20764
+ }
20765
+ function showForm(msg) {
20766
+ boot.style.display = "none";
20767
+ form.style.display = "block";
20768
+ if (msg) { errEl.textContent = msg; errEl.style.display = "flex"; }
20769
+ input.focus();
20770
+ }
20771
+ function fail(msg) {
20772
+ goBtn.disabled = false;
20773
+ goBtn.innerHTML = "\u89E3\u9501\u5E76\u8FDB\u5165";
20774
+ errEl.textContent = msg;
20775
+ errEl.style.display = "flex";
20776
+ form.classList.remove("bad");
20777
+ void form.offsetWidth; // reflow so the shake animation restarts
20778
+ form.classList.add("bad");
20779
+ input.focus();
20780
+ input.select();
20781
+ }
20782
+ peek.addEventListener("click", function () {
20783
+ var pw = input.type === "password";
20784
+ input.type = pw ? "text" : "password";
20785
+ peek.innerHTML = '<svg viewBox="0 0 24 24">' + (pw ? ${JSON.stringify(EYE_OFF_SVG)} : ${JSON.stringify(EYE_SVG)}) + "</svg>";
20786
+ peek.setAttribute("aria-label", pw ? "\u9690\u85CF\u4EE4\u724C" : "\u663E\u793A\u4EE4\u724C");
20787
+ input.focus();
20788
+ });
20789
+ input.addEventListener("input", function () {
20790
+ if (errEl.style.display !== "none") { errEl.style.display = "none"; form.classList.remove("bad"); }
20791
+ });
20792
+
20793
+ form.addEventListener("submit", function (ev) {
20246
20794
  ev.preventDefault();
20247
- save(document.getElementById("t").value);
20248
- location.reload();
20795
+ var token = normalize(input.value);
20796
+ if (!token) { fail("\u8BF7\u8F93\u5165\u8BBF\u95EE\u4EE4\u724C\u3002"); return; }
20797
+ goBtn.disabled = true;
20798
+ goBtn.innerHTML = '<span class="spin"></span>\u9A8C\u8BC1\u4E2D\u2026';
20799
+ var hdr = { headers: { authorization: "Bearer " + token } };
20800
+ // Prefer the renewal endpoint (managed mode returns the canonical token,
20801
+ // also upgrading a pasted in-grace token). Fall back to a cheap
20802
+ // authenticated probe so we can reject a bad token *inline* instead of
20803
+ // storing it and reloading into another gate.
20804
+ fetch("${RENEW_PATH}", hdr)
20805
+ .then(function (r) { return r.ok ? r.json() : null; })
20806
+ .then(function (d) {
20807
+ if (d && d.token) { save(d.token); location.reload(); return; }
20808
+ return fetch("/api/version", hdr).then(function (r) {
20809
+ if (r.status === 401) { fail("\u4EE4\u724C\u65E0\u6548,\u8BF7\u68C0\u67E5\u540E\u91CD\u8BD5\u3002"); return; }
20810
+ // 200 (static-token mode) or no UI (404) \u2192 trust it and reload.
20811
+ save(token); location.reload();
20812
+ });
20813
+ })
20814
+ .catch(function () { save(token); location.reload(); }); // network hiccup \u2192 old behavior
20249
20815
  });
20250
- // Seamless renewal for top-level navigation: if we hold a token, try swapping
20251
- // it for the current one before prompting. Covers the case where the token
20252
- // rotated while this browser held the (in-grace) previous one.
20816
+
20817
+ // Silent renewal on load: a held token may just need swapping for the current
20818
+ // one (rotation while this browser held the in-grace previous). Show the boot
20819
+ // loader meanwhile; drop to the form (with a hint) if it can't be renewed.
20253
20820
  var stored = localStorage.getItem(KEY);
20254
20821
  if (stored) {
20255
20822
  fetch("${RENEW_PATH}", { headers: { authorization: "Bearer " + stored } })
20256
20823
  .then(function (r) { return r.ok ? r.json() : null; })
20257
- .then(function (d) { if (d && d.token) { save(d.token); location.reload(); } else { showForm(); } })
20258
- .catch(showForm);
20824
+ .then(function (d) {
20825
+ if (d && d.token) { save(d.token); location.reload(); }
20826
+ else showForm("\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u8F93\u5165\u4EE4\u724C\u3002");
20827
+ })
20828
+ .catch(function () { showForm(""); });
20259
20829
  } else {
20260
- showForm();
20830
+ showForm("");
20261
20831
  }
20262
20832
  </script>
20263
20833
  </body></html>`;
@@ -20529,6 +21099,11 @@ var styles_default = ` :root {
20529
21099
  svg { display: block; }
20530
21100
  .ico { width:16px; height:16px; stroke:currentColor; fill:none; stroke-width:1.75; stroke-linecap:round; stroke-linejoin:round; flex:none; }
20531
21101
  .ico.sm { width:14px; height:14px; }
21102
+ /* Every window (incl. quicknote) gets the same thin translucent scrollbar:
21103
+ transparent track, rounded translucent thumb. In Chromium 121+ the
21104
+ inherited html scrollbar-width/color renders it; the ::-webkit rules
21105
+ cover Safari. Both force classic (layout-consuming) mode \u2014 tight layouts
21106
+ pair scrollbar-gutter:stable with rebalanced insets (see .qn-body). */
20532
21107
  ::-webkit-scrollbar { width:10px; height:10px; }
20533
21108
  ::-webkit-scrollbar-thumb { background:var(--line-strong); border-radius:8px; border:3px solid transparent; background-clip:padding-box; }
20534
21109
  ::-webkit-scrollbar-thumb:hover { background:var(--muted); background-clip:padding-box; border:3px solid transparent; }
@@ -20577,12 +21152,45 @@ var styles_default = ` :root {
20577
21152
  .sb-search input { border:0; background:none; outline:none; color:var(--fg); width:100%; }
20578
21153
  .sb-search kbd { font-family:var(--mono); font-size:10px; color:var(--muted); border:1px solid var(--line-strong); border-radius:4px; padding:1px 4px; }
20579
21154
  .sb-scroll { flex:1; overflow-y:auto; padding:4px 8px 24px; }
20580
- .sb-section { margin-top:12px; }
20581
- .sb-section-head { display:flex; align-items:center; justify-content:space-between;
20582
- font-size:var(--fs-micro); font-weight:600; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); padding:4px 8px; }
20583
- .sb-section-head .add { opacity:0; padding:3px; border-radius:5px; color:var(--muted); transition:opacity .12s; }
20584
- .sb-section:hover .add { opacity:1; }
20585
- .sb-section-head .add:hover { background:var(--hover-2); color:var(--fg); }
21155
+ /* Notion-style list switcher: the active tab is an icon+label pill, inactive
21156
+ tabs are icon-only. One absolutely-positioned indicator slides & stretches
21157
+ under the active button; JS keeps it in sync via ResizeObserver so it
21158
+ chases the label's 0fr\u21921fr expansion instead of jumping to a stale width. */
21159
+ .sb-tabs { position:relative; display:flex; align-items:center; gap:2px; margin:2px 12px 8px; }
21160
+ .sb-tab { position:relative; z-index:1; display:flex; align-items:center;
21161
+ padding:5px 7px; border-radius:var(--radius-sm); color:var(--muted); cursor:pointer;
21162
+ font-size:var(--fs-ui); transition:color .18s; }
21163
+ .sb-tab:hover { color:var(--fg-soft); }
21164
+ .sb-tab.on { color:var(--fg); font-weight:600; }
21165
+ /* Label expansion: animating the grid track times correctly for any label
21166
+ width (max-width can't); the inner span clips during the ride. The
21167
+ icon-text gap rides the same curve so collapsing leaves no seam. */
21168
+ .sb-tab .tab-label { display:grid; grid-template-columns:0fr; opacity:0;
21169
+ transition:grid-template-columns .24s cubic-bezier(.22,1,.36,1), opacity .18s ease; }
21170
+ .sb-tab .tab-label > span { overflow:hidden; white-space:nowrap; min-width:0;
21171
+ transition:padding-left .24s cubic-bezier(.22,1,.36,1); }
21172
+ .sb-tab.on .tab-label { grid-template-columns:1fr; opacity:1; }
21173
+ .sb-tab.on .tab-label > span { padding-left:6px; }
21174
+ .sb-tab-ind { position:absolute; left:0; top:0; bottom:0; z-index:0; width:0;
21175
+ border-radius:var(--radius-sm); background:var(--hover-2);
21176
+ transition:transform .24s cubic-bezier(.22,1,.36,1), width .24s cubic-bezier(.22,1,.36,1); }
21177
+ .sb-tabs .add { margin-left:auto; padding:3px; border-radius:5px; color:var(--muted); }
21178
+ .sb-tabs .add:hover { background:var(--hover-2); color:var(--fg); }
21179
+ /* Pane swap: the entering list slides in from the side its tab lives on. */
21180
+ .sb-pane { margin-top:4px; }
21181
+ .sb-pane.in-r { animation:sb-pane-r .2s cubic-bezier(.22,1,.36,1); }
21182
+ .sb-pane.in-l { animation:sb-pane-l .2s cubic-bezier(.22,1,.36,1); }
21183
+ @keyframes sb-pane-r { from { opacity:0; transform:translateX(14px); } }
21184
+ @keyframes sb-pane-l { from { opacity:0; transform:translateX(-14px); } }
21185
+ .sb-subfold .chev { transition:transform .12s; }
21186
+ .sb-subfold .chev.open { transform:rotate(90deg); }
21187
+ /* Collapsed-databases group: a quiet tail row inside the \u6570\u636E\u8868 list for
21188
+ tables flagged meta.collapsed (site-facing / rarely browsed). */
21189
+ .sb-subfold { display:flex; align-items:center; gap:2px; width:100%; text-align:left;
21190
+ padding:4px 6px; margin-top:2px; border-radius:var(--radius-sm);
21191
+ color:var(--muted); font-size:var(--fs-micro); cursor:pointer; }
21192
+ .sb-subfold:hover { background:var(--hover-2); color:var(--fg-soft); }
21193
+ .navitem.dim { opacity:.62; }
20586
21194
  .navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; margin-bottom:2px; border-radius:var(--radius-sm);
20587
21195
  color:var(--fg-soft); font-size:var(--fs-ui); position:relative; white-space:nowrap; transition:background .1s; }
20588
21196
  .navitem:hover { background:var(--hover-2); }
@@ -20672,6 +21280,23 @@ var styles_default = ` :root {
20672
21280
  .set-block-title { font-size:14px; font-weight:650; color:var(--fg); letter-spacing:-.005em; }
20673
21281
  .set-block-desc { color:var(--fg-soft); font-size:var(--fs-sm); line-height:1.55; margin-bottom:14px; max-width:56ch; }
20674
21282
  .set-block-desc:last-child { margin-bottom:0; }
21283
+ /* title+desc on the left, a control (e.g. a switch) pinned right \u2014 used for
21284
+ single boolean settings where a whole card of chrome would be overkill */
21285
+ .set-toggle-row { display:flex; align-items:center; justify-content:space-between; gap:24px; }
21286
+ .set-toggle-text { min-width:0; }
21287
+ .set-toggle-text .set-block-head { margin-bottom:4px; }
21288
+ .set-toggle-text .set-block-desc { margin-bottom:0; }
21289
+ /* iOS-style switch: a hidden checkbox drives a sliding track+thumb */
21290
+ .set-switch { position:relative; flex:none; display:inline-flex; align-items:center; cursor:pointer; }
21291
+ .set-switch input { position:absolute; width:1px; height:1px; opacity:0; margin:0; }
21292
+ .set-switch-track { display:inline-flex; align-items:center; width:40px; height:23px; padding:0 2px;
21293
+ border-radius:999px; background:var(--line-strong); transition:background .2s ease; }
21294
+ .set-switch-thumb { width:19px; height:19px; border-radius:50%; background:#fff;
21295
+ box-shadow:0 1px 3px rgba(20,20,40,.28); transition:transform .2s cubic-bezier(.4,0,.2,1); }
21296
+ .set-switch input:checked + .set-switch-track { background:var(--accent); }
21297
+ .set-switch input:checked + .set-switch-track .set-switch-thumb { transform:translateX(17px); }
21298
+ .set-switch:hover .set-switch-track { filter:brightness(.97); }
21299
+ .set-switch input:focus-visible + .set-switch-track { outline:2px solid var(--accent); outline-offset:2px; }
20675
21300
  /* two-fact meta line under a block title (who publishes \xB7 where creds live) \u2014
20676
21301
  answers the questions hiding under "is this bucket for server or browser?" */
20677
21302
  .set-meta { display:flex; flex-wrap:wrap; gap:8px 16px; margin:0 0 12px; }
@@ -20871,10 +21496,19 @@ var styles_default = ` :root {
20871
21496
  .sidebar.collapsed { overflow:hidden; }
20872
21497
  .sidebar.collapsed .sb-resizer { pointer-events:none; }
20873
21498
  .crumb { display:flex; align-items:center; gap:7px; flex:1; min-width:0; font-size:var(--fs-ui); color:var(--fg); font-weight:500; }
20874
- .crumb .emoji { font-size:15px; } .crumb .sub { color:var(--muted); font-weight:400; }
21499
+ /* Long titles ellipsize on one line \u2014 never wrap and grow the topbar. */
21500
+ .crumb > span:not(.emoji) { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
21501
+ .crumb .emoji { font-size:15px; flex:none; } .crumb .sub { color:var(--muted); font-weight:400; }
20875
21502
  .iconbtn { width:30px; height:30px; display:grid; place-items:center; border-radius:var(--radius-sm); color:var(--fg-soft); transition:background .12s; }
20876
21503
  .iconbtn:hover { background:var(--hover-2); color:var(--fg); }
20877
- .content { flex:1; overflow:auto; }
21504
+ /* Reserve the classic-scrollbar gutter up front: view width never changes
21505
+ when content grows past one screen \u2014 no jump / rewrap flicker. stable
21506
+ ONLY (never both-edges): a left-edge gutter desyncs Chromium's composited
21507
+ scroll cull rect by the gutter width, leaving a permanent unpainted
21508
+ stripe over the rightmost 15px of the scrollport (content reaching into
21509
+ it gets visibly sliced \u2014 first seen slicing the quicknote file card).
21510
+ Inert on mobile, where .content becomes overflow:visible. */
21511
+ .content { flex:1; overflow:auto; scrollbar-gutter:stable; }
20878
21512
  .btn { display:inline-flex; align-items:center; gap:6px; padding:6px 12px; border-radius:var(--radius-sm); font-size:13px; font-weight:500; transition:all .12s; white-space:nowrap; }
20879
21513
  .btn-ghost { color:var(--fg-soft); } .btn-ghost:hover { background:var(--hover-2); color:var(--fg); }
20880
21514
  .btn-secondary { background:var(--bg); border:1px solid var(--line-strong); color:var(--fg); box-shadow:var(--shadow-sm); }
@@ -20939,9 +21573,9 @@ var styles_default = ` :root {
20939
21573
  .doc-conflict { display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:10px 14px; margin-bottom:16px; border:1px solid var(--danger); border-radius:var(--radius-sm); background:var(--danger-soft); font-size:13px; }
20940
21574
  .doc-conflict-msg { color:var(--danger); font-weight:600; flex:1; min-width:180px; }
20941
21575
  .doc.source-mode { min-height:100%; }
20942
- .doc-source { display:block; width:100%; min-height:calc(100vh - 210px); min-height:calc(100dvh - 210px); resize:none; border:0;
20943
- background:transparent; color:var(--fg); outline:none; overflow:hidden; padding:3px 2px 36vh;
20944
- font-family:var(--ui); font-size:15px; line-height:1.7; tab-size:2; }
21576
+ /* wide mode (\u22EF menu): lift the column cap; the 60px side padding stays as the
21577
+ reading gutter and keeps the fixed .doc-toc ticks off the text. */
21578
+ .doc.wide-mode { max-width:none; }
20945
21579
  /* floating table of contents: a column of indented tick marks in the right
20946
21580
  gutter that expands to a labelled list on hover (Notion / Vercel-docs style).
20947
21581
  Fixed to the viewport so it never disturbs the centred .doc column. */
@@ -20980,49 +21614,33 @@ var styles_default = ` :root {
20980
21614
  /* not enough room for the gutter \u2192 hide entirely (also covers mobile) */
20981
21615
  @media (max-width:1100px) { .doc-toc { display:none; } }
20982
21616
 
20983
- .block-wrap { position:relative; }
20984
- .block-wrap.nested { padding-left:24px; }
20985
- /* Subtle indent guide only for true sub-lists; suppressed for code/other
20986
- nested content so code-in-list items don't stack a thicket of lines. */
20987
- .block-wrap.nested:has(> .block.b-bullet, > .block.b-numbered, > .block.b-todo)::before {
20988
- content:""; position:absolute; left:8px; top:2px; bottom:2px; width:1px; background:var(--line); }
20989
- .block { position:relative; display:flex; align-items:flex-start; padding:1px 0; border-radius:4px; scroll-margin-top:60px; }
20990
- .block .gutter { position:absolute; left:-52px; top:2px; display:flex; gap:1px; opacity:0; transition:opacity .12s; }
20991
- .block:hover .gutter, .block.drop-before .gutter, .block.drop-after .gutter, .block.dragging .gutter { opacity:1; }
20992
- .gutter button { width:22px; height:24px; display:grid; place-items:center; color:var(--muted); border-radius:5px; }
20993
- .gutter button:hover { background:var(--hover-2); color:var(--fg); }
20994
- .gutter .grip { cursor:grab; } .gutter .grip:active { cursor:grabbing; }
20995
- .block.dragging { opacity:.42; background:var(--hover); }
20996
- .block.drop-before::before, .block.drop-after::after { content:""; position:absolute; left:0; right:0; height:2px; background:var(--accent);
20997
- border-radius:2px; pointer-events:none; box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 18%,transparent); }
20998
- .block.drop-before::before { top:-1px; }
20999
- .block.drop-after::after { bottom:-1px; }
21000
- /* multi-block selection: whole-block tint + suppress stray native text highlight while dragging */
21001
- .block.selected { background:var(--accent-soft); }
21002
- .block.selected .editable::selection, .block.selected .editable *::selection { background:transparent; }
21003
- .doc.selecting, .doc.selecting * { user-select:none; -webkit-user-select:none; cursor:default; }
21004
- .block.list-code-host { height:0; min-height:0; padding:0; overflow:visible; }
21005
- .block-wrap:hover > .block.list-code-host .gutter { opacity:1; }
21006
- .block.list-code-host .marker { position:absolute; left:0; top:0; padding-top:4px; z-index:1; }
21007
- /* The list item's host gutter controls the whole unit, so suppress the nested
21008
- code block's own gutter (it would overlap the list marker on the same row). */
21009
- .block.list-code-host + .block-wrap.nested .gutter { display:none; }
21010
- .editable { outline:none; flex:1; min-width:0; padding:3px 2px; line-height:var(--note-lh); }
21011
- .editable:empty::before { content:attr(data-ph); color:var(--muted); pointer-events:none; }
21012
- /* The generic "/" prompt only shows on the focused line; idle blank lines (paragraphs and list items, any depth) stay blank. */
21013
- .editable[data-ph-hint="slash"]:empty:not(:focus)::before { content:""; }
21014
- .b-h1 .editable { font-size:28px; font-weight:700; letter-spacing:-.02em; padding-top:12px; }
21015
- .b-h2 .editable { font-size:22px; font-weight:650; letter-spacing:-.01em; padding-top:8px; }
21016
- .b-h3 .editable { font-size:18px; font-weight:600; padding-top:4px; }
21017
- .b-h4 .editable { font-size:16px; font-weight:600; padding-top:2px; }
21018
- .b-h5 .editable { font-size:14px; font-weight:600; }
21019
- .b-h6 .editable { font-size:13px; font-weight:600; color:var(--fg-soft); }
21020
- .b-h1 .gutter { top:22px; } .b-h2 .gutter { top:14px; } .b-h3 .gutter { top:7px; }
21021
- .b-h4 .gutter { top:4px; } .b-h5 .gutter { top:3px; } .b-h6 .gutter { top:2px; }
21022
- .b-quote .editable { border-left:3px solid var(--line-strong); padding:1px 0 1px 16px; color:var(--fg-soft); font-style:italic; }
21617
+ /* floating word count: a small pill in the bottom-right gutter (fixed to the
21618
+ viewport, aligned with .doc-toc's right edge). Collapsed it hugs the \u5B57\u6570;
21619
+ hover expands the whole pill (square-off + surface bg) and reveals the char
21620
+ count + reading estimate. Centre-aligned throughout, and the collapsed detail
21621
+ is width:0 so its wider text can't stretch the pill. Purely client-side. */
21622
+ .doc-wc { position:fixed; right:18px; bottom:calc(16px + env(safe-area-inset-bottom)); z-index:40;
21623
+ display:flex; flex-direction:column; align-items:center; justify-content:center;
21624
+ padding:4px 12px; border:1px solid var(--line); border-radius:999px; background:var(--bg);
21625
+ color:var(--muted); font-size:var(--fs-sm); line-height:1.2; user-select:none;
21626
+ box-shadow:var(--shadow-sm); opacity:.66;
21627
+ transition:opacity .18s ease, border-radius .2s ease, background .18s ease, box-shadow .2s ease, color .18s ease; }
21628
+ .doc-wc:hover { opacity:1; border-radius:var(--radius); background:var(--surface); border-color:var(--line-strong);
21629
+ box-shadow:var(--shadow-md); color:var(--fg-soft); }
21630
+ .doc-wc-num { font-variant-numeric:tabular-nums; white-space:nowrap; }
21631
+ .doc-wc.sel .doc-wc-num { color:var(--accent); font-weight:550; }
21632
+ /* detail rows: collapsed to zero size until hover. width:0 keeps them from
21633
+ widening the pill; no container gap (a phantom gap would push the collapsed
21634
+ count off-centre) \u2014 the spacing comes from margin-top only when expanded. */
21635
+ .doc-wc-detail { display:flex; flex-direction:column; align-items:center; gap:2px;
21636
+ width:0; max-height:0; margin-top:0; overflow:hidden; opacity:0; font-size:12px; color:var(--muted);
21637
+ transition:max-height .2s ease, opacity .18s ease, margin-top .2s ease; }
21638
+ .doc-wc-detail span { font-variant-numeric:tabular-nums; white-space:nowrap; }
21639
+ .doc-wc:hover .doc-wc-detail { width:auto; max-height:40px; margin-top:4px; opacity:1; }
21640
+ /* quick-note mini window has no room for it */
21641
+ .qn-body .doc-wc { display:none; }
21023
21642
 
21024
21643
  /* code block: transparent textarea over a highlight.js mirror */
21025
- .b-code .codeblock { flex:1; min-width:0; }
21026
21644
  .codeblock { position:relative; --code-fs:12.5px; --code-lh:1.55; --code-pad:8px; margin:2px 0;
21027
21645
  background:var(--code-bg); border:1px solid var(--line); border-radius:8px; overflow:hidden; box-shadow:var(--shadow-sm); }
21028
21646
  /* lang + copy float bottom-right, revealed on hover */
@@ -21031,27 +21649,57 @@ var styles_default = ` :root {
21031
21649
  background:color-mix(in srgb,var(--code-bg) 82%,transparent); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
21032
21650
  opacity:0; transform:translateY(3px); pointer-events:none; transition:opacity .12s,transform .12s; }
21033
21651
  .codeblock:hover .code-tools, .code-tools:focus-within { opacity:1; transform:none; pointer-events:auto; }
21652
+ /* hidden while editing (textarea focused) or whole-block selected \u2014 the pill would cover the
21653
+ bottom-right of the editing area; keep it if focus is on the pill itself (lang dropdown open) */
21654
+ .cm-void-code:focus-within .code-tools:not(:focus-within),
21655
+ .codeblock.ci-selected .code-tools:not(:focus-within) { opacity:0; transform:translateY(3px); pointer-events:none; }
21034
21656
  .code-lang { position:relative; display:inline-flex; align-items:center; }
21035
21657
  .code-lang select { appearance:none; -webkit-appearance:none; border:0; outline:none; background:none; cursor:pointer;
21036
21658
  font-family:var(--mono); font-size:10.5px; font-weight:500; color:var(--fg-soft); padding:3px 17px 3px 7px; border-radius:5px; }
21037
21659
  .code-lang select:hover { background:var(--hover-2); }
21038
21660
  .code-lang svg { position:absolute; right:3px; width:12px; height:12px; pointer-events:none; color:var(--muted); }
21039
- .code-copy { display:inline-flex; align-items:center; gap:4px; font-size:10.5px; font-weight:500; color:var(--muted);
21040
- padding:3px 7px; border-radius:5px; transition:background .12s,color .12s; }
21041
- .code-copy:hover { background:var(--hover-2); color:var(--fg-soft); }
21042
- .code-copy.ok { color:var(--accent); }
21043
- .code-body { display:flex; align-items:stretch; font-family:var(--mono); font-size:var(--code-fs); line-height:var(--code-lh); }
21661
+ .code-copy, .code-fmt, .code-wrap-btn { display:inline-flex; align-items:center; gap:4px; font-size:10.5px; font-weight:500; color:var(--muted);
21662
+ padding:3px 7px; border-radius:5px; transition:background .12s,color .12s; white-space:nowrap; }
21663
+ .code-copy:hover, .code-fmt:hover, .code-wrap-btn:hover { background:var(--hover-2); color:var(--fg-soft); }
21664
+ .code-copy.ok, .code-fmt.ok, .code-wrap-btn.on { color:var(--accent); }
21665
+ .code-fmt.err { color:var(--danger); }
21666
+ /* Default (no wrap): lines run long, the textarea scrolls horizontally and
21667
+ mirrors its scrollLeft onto the highlight layer; the gutter is a separate
21668
+ flex column of "1\\n2\\n3" (every logical line is exactly one row high). */
21669
+ .code-body { display:flex; align-items:stretch; font-family:var(--mono); font-size:var(--code-fs); line-height:var(--code-lh);
21670
+ --code-ln-ch:1ch; --code-gutter-w:calc(var(--code-ln-ch) + 20px); }
21044
21671
  .code-gutter { flex:none; text-align:right; white-space:pre; user-select:none; color:var(--muted); opacity:.55;
21045
21672
  padding:var(--code-pad) 8px var(--code-pad) 12px; border-right:1px solid var(--line); font-variant-numeric:tabular-nums; }
21046
21673
  .code-scroll { position:relative; flex:1; min-width:0; }
21047
21674
  .code-hl, .code-input { margin:0; font-family:var(--mono); font-size:var(--code-fs); line-height:var(--code-lh);
21048
21675
  padding:var(--code-pad) 16px; tab-size:2; -moz-tab-size:2; white-space:pre; letter-spacing:0; }
21049
21676
  .code-hl { position:absolute; inset:0; z-index:0; overflow:hidden; pointer-events:none; background:none; color:var(--fg); }
21050
- .code-hl code { display:block; font:inherit; background:none; padding:0; }
21677
+ .code-hl code { display:block; font:inherit; background:none; padding:0; counter-reset:code-ln; }
21678
+ .code-hl-line { counter-increment:code-ln;
21679
+ min-height:calc(var(--code-fs) * var(--code-lh)); /* empty logical line still occupies its row */ }
21051
21680
  .code-input { display:block; position:relative; z-index:1; width:100%; border:0; outline:none; resize:none;
21052
21681
  overflow-x:auto; overflow-y:hidden; background:transparent; color:transparent; caret-color:var(--fg);
21053
21682
  min-height:calc(var(--code-fs) * var(--code-lh) + var(--code-pad) * 2); }
21054
21683
  .code-input::placeholder { color:var(--muted); }
21684
+ /* Wrap mode (.wrap, global toggle in the tools pill, persisted): textarea and
21685
+ mirror share pre-wrap + break-word (the UA textarea wrap rules), so their
21686
+ wrap points match and the caret stays on the glyphs. The flex gutter would
21687
+ misalign (a logical line can span several visual rows), so it hides and
21688
+ line numbers become CSS counters on the mirror's per-logical-line blocks \u2014
21689
+ a wrapped line grows its block and the next number moves with it, no JS
21690
+ height sync. --code-ln-ch (digit count) is set by paint(). */
21691
+ .code-body.wrap { display:block; position:relative; }
21692
+ .code-body.wrap::before { content:""; position:absolute; left:var(--code-gutter-w); top:0; bottom:0;
21693
+ width:1px; background:var(--line); }
21694
+ .code-body.wrap .code-gutter { display:none; }
21695
+ .code-body.wrap .code-hl, .code-body.wrap .code-input {
21696
+ padding-left:calc(var(--code-gutter-w) + 17px);
21697
+ white-space:pre-wrap; overflow-wrap:break-word; }
21698
+ .code-body.wrap .code-input { overflow:hidden; }
21699
+ .code-body.wrap .code-hl-line { position:relative; }
21700
+ .code-body.wrap .code-hl-line::before { content:counter(code-ln); position:absolute; top:0;
21701
+ left:calc(-17px - var(--code-gutter-w)); width:calc(var(--code-gutter-w) - 8px);
21702
+ text-align:right; color:var(--muted); opacity:.55; font-variant-numeric:tabular-nums; }
21055
21703
 
21056
21704
  /* highlight.js token theme (cohesive with the app palette) */
21057
21705
  .hljs-comment, .hljs-quote { color:var(--hl-cmt); font-style:italic; }
@@ -21065,24 +21713,50 @@ var styles_default = ` :root {
21065
21713
  .hljs-params, .hljs-subst { color:var(--fg); }
21066
21714
  .hljs-emphasis { font-style:italic; }
21067
21715
  .hljs-strong { font-weight:700; }
21068
- .marker { width:24px; flex:none; text-align:center; color:var(--fg-soft); user-select:none; padding-top:4px; }
21069
- .b-todo .marker { box-sizing:content-box; padding-top:3px; height:calc(1em * var(--note-lh));
21070
- display:flex; align-items:center; justify-content:center; }
21071
- .b-todo .marker input { width:15px; height:15px; accent-color:var(--accent); cursor:pointer; }
21072
- .b-divider { padding:10px 0; }
21073
- .b-divider hr { border:0; border-top:1px solid var(--line-strong); margin:0; width:100%; }
21074
- .b-done .editable { color:var(--muted); text-decoration:line-through; }
21075
- .editable b, .editable strong { font-weight:700; }
21076
- .editable code { font-family:var(--mono); background:var(--surface-2); padding:1px 5px; border-radius:4px; font-size:.9em; }
21077
- .editable a { color:var(--accent); }
21078
- /* document images (pasted/dropped \u2192 /blob/<hash>); fetched on demand */
21079
- .editable img.doc-img { max-width:100%; height:auto; border-radius:8px; margin:6px 0; display:block; vertical-align:bottom; }
21080
21716
 
21081
- /* ---- block-level void embeds: image / video / audio / file / html ---- */
21082
- .void-host { flex:1; min-width:0; outline:none; padding:2px 0; }
21083
- .block.selected:has(.void-host) { background:transparent; }
21084
- .block.selected .void-block, .void-host:focus-visible .void-block { box-shadow:0 0 0 2px var(--accent); }
21717
+ /* CodeIsland (CM6 void widget): the tools pill (lang + copy) is the only
21718
+ focus-state chrome \u2014 an absolute overlay, so revealing it never changes the
21719
+ block's height (no layout shift on click). .ci-selected marks the island
21720
+ when the CM selection spans the whole void (Escape from inside / Backspace
21721
+ from the line below). */
21722
+ .cm-void-code .codeblock.ci-selected { box-shadow:0 0 0 2px var(--accent); }
21723
+
21724
+ /* Whole-void selected (the CM selection spans the block \u2014 click-to-select on
21725
+ media, Backspace-from-below, select-all): 2px accent ring, the old editor's
21726
+ \`.block.selected .void-block\` treatment. Code draws its own via .ci-selected. */
21727
+ /* Intrinsic-width firewall (same problem the doc table solved with
21728
+ contain:inline-size on .doc-table-wrap): a void widget's max-content \u2014
21729
+ e.g. a file card whose nowrap filename caps at 480px \u2014 must not propagate
21730
+ into .cm-content and push the whole editor wider than a narrow window
21731
+ (quick notes), shoving the card's right edge offscreen. width:0 zeroes the
21732
+ host's intrinsic contribution; min-width:100% restores the line width at
21733
+ layout time; the card's own min-width:0 + ellipsis then get to run.
21734
+ NOT contain:inline-size: Chromium 130's ink-overflow culling clips a
21735
+ size-contained host's paint at a stale boundary on 2x displays \u2014 the
21736
+ card's right border and selection ring were visibly sliced off. */
21737
+ .cm-void { box-sizing:border-box; width:0; min-width:100%; }
21738
+ .cm-void.selected .void-block { box-shadow:0 0 0 2px var(--accent); }
21739
+ .cm-void.selected table.doc-table { box-shadow:0 0 0 2px var(--accent), var(--shadow-sm); }
21740
+
21741
+ /* Editor text selection: one accent tint for BOTH the native ::selection and
21742
+ .cm-mk-sel, the class block-deco puts on marker widgets (bullet / ordered
21743
+ number / checkbox) whose marker range the selection covers. Native selection
21744
+ cannot paint replaced widgets, which left holes in the marker column during
21745
+ drag-select / select-all; unifying the two colors makes the line read as one
21746
+ continuous highlight (and tracks the theme in dark mode). */
21747
+ .cm-content ::selection { background: color-mix(in srgb, var(--accent) 24%, transparent); }
21748
+ .cm-content .cm-mk-sel { background: color-mix(in srgb, var(--accent) 24%, transparent); }
21749
+ /* bullet/number boxes are 24px + 2px margin-right; the margin can't take a
21750
+ background, so a same-color shadow fills the seam up to the content column */
21751
+ .cm-content .cm-bullet.cm-mk-sel, .cm-content .cm-ol-num.cm-mk-sel {
21752
+ box-shadow: 2px 0 0 color-mix(in srgb, var(--accent) 24%, transparent);
21753
+ }
21754
+ /* the glyph/number text inside a painted marker also sits in the DOM selection;
21755
+ without this it gets the ::selection tint ON TOP of the span background and
21756
+ reads darker than the rest of the line */
21757
+ .cm-content .cm-mk-sel::selection, .cm-content .cm-mk-sel ::selection { background: transparent; }
21085
21758
 
21759
+ /* ---- block-level void embeds: image / video / audio / file / html ---- */
21086
21760
  .void-image { position:relative; display:inline-block; max-width:100%; border-radius:8px; line-height:0; }
21087
21761
  .void-image img { display:block; width:100%; max-width:100%; height:auto; border-radius:8px; user-select:none; }
21088
21762
  .void-image .void-expand { position:absolute; top:8px; right:8px; width:28px; height:28px; display:grid; place-items:center;
@@ -21120,7 +21794,6 @@ var styles_default = ` :root {
21120
21794
  color:var(--fg-soft); font-size:var(--fs-micro); transition:background .12s; }
21121
21795
  .void-html .html-toggle:hover { background:var(--hover-2); color:var(--fg); }
21122
21796
  .void-html .html-toggle.on { background:var(--accent-soft); color:var(--accent); }
21123
- .void-html .html-source { width:100%; min-height:120px; padding:12px; border:0; background:var(--bg); resize:vertical; }
21124
21797
  .void-html .html-frame { display:block; width:100%; border:0; background:var(--bg); }
21125
21798
  .void-html .html-empty { padding:24px; text-align:center; color:var(--muted); cursor:text; font-size:var(--fs-ui); }
21126
21799
 
@@ -21131,30 +21804,47 @@ var styles_default = ` :root {
21131
21804
  .lightbox-toolbar { flex:none; display:flex; align-items:center; gap:6px; padding:10px 14px; color:#fff; }
21132
21805
  .lightbox-toolbar .lightbox-name { font-size:var(--fs-ui); opacity:.85; max-width:40vw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
21133
21806
  .lightbox-toolbar .lightbox-spacer { flex:1; }
21134
- .lightbox-toolbar button { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; color:#fff;
21807
+ .lightbox-toolbar > button { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; color:#fff;
21135
21808
  background:rgba(255,255,255,.08); transition:background .12s; }
21136
- .lightbox-toolbar button:hover { background:rgba(255,255,255,.2); }
21809
+ .lightbox-toolbar > button:hover { background:rgba(255,255,255,.2); }
21137
21810
  .lightbox-toolbar .lightbox-pct { width:auto; min-width:52px; font-size:var(--fs-micro); font-variant-numeric:tabular-nums; }
21811
+ .lightbox-toolbar .ico { width:16px; height:16px; stroke-width:2; }
21138
21812
  .lightbox-stage { flex:1; min-height:0; overflow:hidden; display:grid; place-items:center; }
21139
21813
  .lightbox-stage img { max-width:94vw; max-height:100%; will-change:transform; -webkit-user-drag:none; user-select:none; }
21140
21814
 
21141
21815
  /* ---- image annotator ---- */
21142
21816
  .annot { flex:1; min-height:0; display:flex; flex-direction:column; }
21143
- .annot-toolbar { flex:none; display:flex; flex-wrap:wrap; align-items:center; gap:14px; padding:8px 14px; }
21144
- .annot-tools, .annot-colors, .annot-actions { display:flex; align-items:center; gap:6px; }
21145
- .annot-actions { margin-left:auto; }
21146
- .annot-tool { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; color:#fff; background:rgba(255,255,255,.08); transition:background .12s; }
21147
- .annot-tool:hover { background:rgba(255,255,255,.2); }
21148
- .annot-tool.on { background:var(--accent); color:var(--accent-fg); }
21149
- .annot-tool:disabled { opacity:.4; cursor:default; }
21150
- .annot-swatch { width:22px; height:22px; border-radius:50%; border:2px solid rgba(255,255,255,.5); transition:transform .1s; }
21151
- .annot-swatch.on { transform:scale(1.18); border-color:#fff; box-shadow:0 0 0 2px var(--accent); }
21817
+ /* Single-row chrome: filename \xB7 segmented tools \xB7 colors \xB7 undo/redo \xB7
21818
+ cancel/save \xB7 close. Wraps only as a narrow-window fallback. */
21819
+ .annot-bar { flex-wrap:wrap; row-gap:6px; }
21820
+ .annot-seg { display:flex; align-items:center; gap:2px; padding:2px; border-radius:9px; background:rgba(255,255,255,.1); }
21821
+ .annot-seg-btn { width:30px; height:26px; display:grid; place-items:center; border-radius:7px;
21822
+ color:rgba(255,255,255,.72); transition:background .12s, color .12s; }
21823
+ .annot-seg-btn:hover { color:#fff; background:rgba(255,255,255,.12); }
21824
+ .annot-seg-btn.on { background:#fff; color:#1c1c26; box-shadow:0 1px 2px rgba(0,0,0,.3); }
21825
+ .annot-div { flex:none; width:1px; height:18px; margin:0 5px; background:rgba(255,255,255,.18); }
21826
+ .annot-colors { display:flex; align-items:center; gap:8px; padding:0 2px; }
21827
+ .annot-swatch { width:15px; height:15px; border-radius:50%; box-shadow:inset 0 0 0 1px rgba(255,255,255,.3);
21828
+ transition:transform .12s, box-shadow .12s; }
21829
+ .annot-swatch:hover { transform:scale(1.18); }
21830
+ .annot-swatch.on { box-shadow:inset 0 0 0 1px rgba(255,255,255,.3), 0 0 0 2px rgba(12,12,18,.9), 0 0 0 3.5px rgba(255,255,255,.92); }
21831
+ .annot-bar .annot-icon-btn { width:30px; height:30px; display:grid; place-items:center; border-radius:7px;
21832
+ color:rgba(255,255,255,.85); background:none; transition:background .12s; }
21833
+ .annot-bar .annot-icon-btn:hover { background:rgba(255,255,255,.14); }
21834
+ .annot-bar .annot-icon-btn:disabled { opacity:.35; cursor:default; background:none; }
21835
+ .annot-actions { display:flex; align-items:center; gap:8px; margin:0 2px 0 4px; }
21152
21836
  .annot-stage { flex:1; min-height:0; overflow:auto; display:grid; place-items:center; padding:8px 14px 18px; }
21153
21837
  .annot-img-wrap { position:relative; line-height:0; }
21154
21838
  .annot-img-wrap img { display:block; max-width:90vw; max-height:78vh; width:auto; height:auto; -webkit-user-drag:none; user-select:none; }
21155
- .annot-canvas { position:absolute; inset:0; width:100%; height:100%; }
21839
+ .annot-canvas { position:absolute; inset:0; width:100%; height:100%; touch-action:none; }
21156
21840
  .annot-canvas.tool-text { cursor:text; }
21157
21841
  .annot-canvas.tool-rect, .annot-canvas.tool-arrow { cursor:crosshair; }
21842
+ /* Inline text overlay: WYSIWYG twin of the flattened canvas text (same font,
21843
+ line-height and halo), plus an accent ring so the edit region reads. */
21844
+ .annot-text-input { position:absolute; z-index:2; min-width:60px; padding:2px 4px; margin:0; border:0; outline:none;
21845
+ resize:none; overflow:hidden; white-space:pre; background:transparent; border-radius:4px; caret-color:currentColor;
21846
+ font:600 20px/1.3 system-ui, -apple-system, sans-serif; text-shadow:0 1px 2px rgba(0,0,0,.55);
21847
+ box-shadow:0 0 0 1.5px var(--accent), 0 0 0 4px color-mix(in srgb, var(--accent) 25%, transparent); }
21158
21848
 
21159
21849
  /* ---- transient "uploading" placeholder + unavailable fallback ---- */
21160
21850
  .ico.spin { animation:mh-spin .8s linear infinite; }
@@ -21163,7 +21853,10 @@ var styles_default = ` :root {
21163
21853
  display:flex; align-items:center; gap:12px; margin:2px 0; max-width:480px; padding:12px 14px;
21164
21854
  border:1px solid var(--line-strong); border-radius:var(--radius); background:var(--surface); color:var(--fg-soft); }
21165
21855
  .void-uploading { border-style:dashed; }
21166
- .void-uploading .vu-name, .void-unavailable .vu-name { font-weight:600; color:var(--fg); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
21856
+ /* min-width:0: .void-uploading's vu-name is a direct flex item \u2014 without it a
21857
+ long nowrap filename sets the item's min-content floor and the placeholder
21858
+ card overflows a narrow window instead of ellipsizing. */
21859
+ .void-uploading .vu-name, .void-unavailable .vu-name { font-weight:600; color:var(--fg); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
21167
21860
  .void-uploading .vu-sub, .void-unavailable .vu-sub { font-size:var(--fs-micro); color:var(--muted); }
21168
21861
  .void-unavailable { border-color:var(--danger-soft); color:var(--danger); }
21169
21862
  .void-unavailable .ico { color:var(--danger); flex:none; }
@@ -21209,24 +21902,53 @@ var styles_default = ` :root {
21209
21902
  /* Chrome rides the theme-tinted glass \u2192 theme-aware (var(--fg)), readable on a
21210
21903
  light or dark tint alike. */
21211
21904
  body.preview-window .lightbox-toolbar { color:var(--fg); }
21212
- body.preview-window .lightbox-toolbar button,
21213
- body.preview-window .annot-tool { color:var(--fg); background:color-mix(in srgb, var(--fg) 10%, transparent); }
21214
- body.preview-window .lightbox-toolbar button:hover,
21215
- body.preview-window .annot-tool:hover { background:color-mix(in srgb, var(--fg) 20%, transparent); }
21216
- body.preview-window .annot-tool.on { color:var(--accent-fg); background:var(--accent); }
21217
- body.preview-window .annot-swatch { border-color:color-mix(in srgb, var(--fg) 45%, transparent); }
21905
+ body.preview-window .lightbox-toolbar > button { color:var(--fg); background:color-mix(in srgb, var(--fg) 10%, transparent); }
21906
+ body.preview-window .lightbox-toolbar > button:hover { background:color-mix(in srgb, var(--fg) 20%, transparent); }
21907
+ body.preview-window .annot-seg { background:color-mix(in srgb, var(--fg) 8%, transparent); }
21908
+ body.preview-window .annot-seg-btn { color:color-mix(in srgb, var(--fg) 65%, transparent); }
21909
+ body.preview-window .annot-seg-btn:hover { color:var(--fg); background:color-mix(in srgb, var(--fg) 10%, transparent); }
21910
+ body.preview-window .annot-seg-btn.on { background:var(--bg); color:var(--fg); box-shadow:var(--shadow-sm); }
21911
+ body.preview-window .annot-div { background:color-mix(in srgb, var(--fg) 15%, transparent); }
21912
+ body.preview-window .annot-swatch { box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 25%, transparent); }
21913
+ body.preview-window .annot-swatch.on { box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 25%, transparent),
21914
+ 0 0 0 2px var(--bg), 0 0 0 3.5px color-mix(in srgb, var(--fg) 78%, transparent); }
21915
+ body.preview-window .annot-bar .annot-icon-btn { color:var(--fg); background:none; }
21916
+ body.preview-window .annot-bar .annot-icon-btn:hover { background:color-mix(in srgb, var(--fg) 12%, transparent); }
21917
+ body.preview-window .annot-bar .annot-icon-btn:disabled { background:none; }
21218
21918
  /* In the frameless window the toolbar doubles as the OS drag region; its
21219
21919
  buttons opt back out so they stay clickable. */
21220
21920
  .lightbox-toolbar.app-drag { -webkit-app-region:drag; }
21221
21921
  .lightbox-toolbar.app-drag button { -webkit-app-region:no-drag; }
21222
21922
 
21223
- /* document table (GFM pipe table block) */
21224
- .b-table .doc-table-wrap { flex:1; min-width:0; margin:4px 0; }
21923
+ /* document table (GFM pipe table block). contain:inline-size zeroes the
21924
+ widget's intrinsic-width contribution \u2014 otherwise the inner
21925
+ width:max-content propagates through overflow-x:auto into .cm-content's
21926
+ min-width:auto floor (intrinsic sizing ignores overflow) and pushes the
21927
+ whole page wider than the viewport on narrow screens. With the intrinsic
21928
+ loop broken, .doc-table-wrap resolves to the doc column width and the
21929
+ scroll box below finally gets to scroll. container-type additionally
21930
+ unlocks cqw for the sticky add-row bar (old engines fall back to contain);
21931
+ position:relative anchors the gutter row handle explicitly rather than
21932
+ leaning on container-type's implied containing block. */
21933
+ .doc-table-wrap { position:relative; contain:inline-size; container-type:inline-size; }
21225
21934
  .doc-table-scroll { overflow-x:auto; padding:2px 0; }
21226
- .doc-table-inner { width:max-content; }
21935
+ .doc-table-inner { position:relative; width:max-content; } /* anchors the drag outline / drop lines so they track horizontal scroll */
21227
21936
  .doc-table-row { display:flex; align-items:stretch; }
21228
21937
  table.doc-table { border-collapse:separate; border-spacing:0; table-layout:fixed; width:max-content;
21229
21938
  border:1px solid var(--line); border-radius:var(--radius-sm); overflow:hidden; box-shadow:var(--shadow-sm); }
21939
+ /* Default (no manual widths yet): browser auto table layout sizes columns by
21940
+ content, capped to the wrap (cqw works despite the max-content .doc-table-inner;
21941
+ 26px keeps the add-col button inside) so it wraps instead of scrolling. The
21942
+ first resizer drag freezes measured widths and drops this class (table-block.tsx). */
21943
+ table.doc-table.autofit { table-layout:auto; width:auto; max-width:calc(100cqw - 26px); }
21944
+ table.doc-table.autofit .doc-td { min-width:48px; } /* empty columns don't collapse */
21945
+ /* Code tokens are atomic: raising the column's min-content makes the auto
21946
+ algorithm shed width into wrappable prose columns instead of breaking
21947
+ \`file-names.md\` at hyphens; an over-long token falls back to the scroll
21948
+ box. Autofit only \u2014 in manual fixed layout nowrap can't grow the column,
21949
+ it would overflow the cell. Phones relax this (see the 560px block):
21950
+ there the atom IS the cap, and it crushes prose columns to their floor. */
21951
+ table.doc-table.autofit .doc-td code { white-space:nowrap; }
21230
21952
  table.doc-table td { position:relative; vertical-align:top; padding:0;
21231
21953
  border-right:1px solid var(--line); border-bottom:1px solid var(--line); }
21232
21954
  table.doc-table tr td:last-child { border-right:0; }
@@ -21238,24 +21960,59 @@ var styles_default = ` :root {
21238
21960
  .doc-td b, .doc-td strong { font-weight:700; }
21239
21961
  .doc-td code { font-family:var(--mono); background:var(--surface-2); padding:1px 5px; border-radius:4px; font-size:.9em; }
21240
21962
  .doc-td a { color:var(--accent); }
21241
- /* header cell: column menu + resize handle */
21242
- .doc-col-menu { position:absolute; top:4px; right:4px; width:18px; height:18px; display:grid; place-items:center;
21243
- color:var(--muted); border-radius:4px; opacity:0; transition:opacity .12s,background .12s; }
21244
- .doc-th:hover .doc-col-menu { opacity:1; }
21245
- .doc-col-menu:hover { background:var(--hover-2); color:var(--fg); }
21246
21963
  .doc-col-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:3; touch-action:none; }
21247
- /* per-row delete: small handle floating at the first cell on row hover */
21248
- .doc-row-del { position:absolute; top:3px; left:2px; width:18px; height:18px; display:grid; place-items:center;
21249
- color:var(--muted); background:var(--bg); border:1px solid var(--line); border-radius:5px; box-shadow:var(--shadow-sm);
21250
- opacity:0; transition:opacity .12s; }
21251
- table.doc-table tr:hover .doc-row-del { opacity:1; }
21252
- .doc-row-del:hover { background:var(--danger-soft); color:var(--danger); border-color:var(--danger-soft); }
21964
+ /* Row/column pill handles (Notion-style): one per axis, tracked onto the
21965
+ active cell (hovered on desktop, focused on touch) by JS. They live on
21966
+ .doc-table-wrap OUTSIDE .doc-table-scroll so overflow-x can't clip them,
21967
+ straddling the table's left/top border \u2014 never on cell text. Resting =
21968
+ quiet grey pill; pressed/dragging (.grab) = accent pill with light dots
21969
+ plus the source outline + drop line below. touch-action:none so a finger
21970
+ drag on the handle reorders instead of scrolling. Data rows only get a
21971
+ row pill (the block gutter's +/grip pins at the header row's y). */
21972
+ .doc-row-handle, .doc-col-handle { position:absolute; display:grid; place-items:center; z-index:7;
21973
+ color:var(--muted); background:var(--bg); border:1px solid var(--line); box-shadow:var(--shadow-sm);
21974
+ cursor:grab; touch-action:none; animation:gripFade .12s ease both;
21975
+ transition:background .12s,color .12s,border-color .12s; }
21976
+ .doc-row-handle { left:-8px; width:15px; height:28px; border-radius:8px; }
21977
+ .doc-col-handle { top:-6px; width:28px; height:15px; border-radius:8px; transform:translateX(-50%); }
21978
+ .doc-row-handle svg, .doc-col-handle svg { width:13px; height:13px; }
21979
+ .doc-row-handle:hover, .doc-col-handle:hover { background:var(--surface); color:var(--fg); }
21980
+ .doc-row-handle.grab, .doc-col-handle.grab { background:var(--accent); color:var(--accent-fg,#fff);
21981
+ border-color:var(--accent); cursor:grabbing; }
21982
+ .doc-row-handle::after { content:""; position:absolute; inset:-4px; }
21983
+ .doc-col-handle::after { content:""; position:absolute; inset:-4px; }
21984
+ /* drag-reorder overlays (inside .doc-table-inner, so they scroll with the
21985
+ table): accent outline on the source row/column, 2px line at the drop
21986
+ boundary (grid .rowdrop's cousin) */
21987
+ .doc-drag-outline { position:absolute; border:2px solid var(--accent); border-radius:6px;
21988
+ pointer-events:none; z-index:6; box-sizing:border-box; }
21989
+ .doc-rowdrop { position:absolute; height:2px; margin-top:-1px; background:var(--accent); border-radius:1px; pointer-events:none; z-index:6; }
21990
+ .doc-coldrop { position:absolute; width:2px; margin-left:-1px; background:var(--accent); border-radius:1px; pointer-events:none; z-index:6; }
21991
+ /* touch selection action bar: the OSK has no Delete/\u2318C, so the cell
21992
+ rectangle gets on-screen verbs; sits on the wrap layer above the selection */
21993
+ .doc-sel-bar { position:absolute; z-index:8; display:flex; gap:2px; padding:3px;
21994
+ background:var(--accent); border-radius:9px; box-shadow:var(--shadow-sm); animation:gripFade .12s ease both; }
21995
+ .doc-sel-bar button { color:var(--accent-fg,#fff); font-size:13px; line-height:1; padding:5px 9px; border-radius:6px; }
21996
+ .doc-sel-bar button:active { background:color-mix(in srgb, #000 18%, transparent); }
21253
21997
  /* add-column (right edge) and add-row (bottom edge) buttons */
21254
21998
  .doc-table-addcol { width:22px; margin-left:4px; flex:none; display:grid; place-items:center; color:var(--muted);
21255
- border:1px dashed var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); transition:background .12s,color .12s,border-color .12s; }
21256
- .doc-table-addrow { width:100%; height:22px; margin-top:4px; display:grid; place-items:center; color:var(--muted);
21257
- border:1px dashed var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); transition:background .12s,color .12s,border-color .12s; }
21999
+ border:1px dashed var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); transition:background .12s,color .12s,border-color .12s,opacity .12s; }
22000
+ /* sticky + 100cqw (the wrap is the container): on a wide table the bar pins
22001
+ to the scrollport at any horizontal offset; narrow tables never hit the
22002
+ max-width and keep their table-wide bar. Engines without cqw just ignore
22003
+ the cap and fall back to scrolling with the table. */
22004
+ .doc-table-addrow { width:100%; max-width:100cqw; position:sticky; left:0; height:22px; margin-top:4px; display:grid; place-items:center; color:var(--muted);
22005
+ border:1px dashed var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); transition:background .12s,color .12s,border-color .12s,opacity .12s; }
21258
22006
  .doc-table-addcol:hover, .doc-table-addrow:hover { background:var(--accent-soft); color:var(--accent); border-color:var(--accent); }
22007
+ /* Notion-style: the add bars only materialize while the table is active \u2014
22008
+ hover or keyboard focus on desktop, an editing (focused) cell on touch.
22009
+ Opacity-only so nothing shifts; pointer-events gates off the invisible
22010
+ bars (a touch tap must not hit a bar it can't see). */
22011
+ .doc-table-addrow, .doc-table-addcol { opacity:0; pointer-events:none; }
22012
+ .doc-table-wrap:focus-within :is(.doc-table-addrow, .doc-table-addcol) { opacity:1; pointer-events:auto; }
22013
+ @media (hover:hover) and (pointer:fine) {
22014
+ .doc-table-wrap:hover :is(.doc-table-addrow, .doc-table-addcol) { opacity:1; pointer-events:auto; }
22015
+ }
21259
22016
  /* cell range selection (box select): soft fill per cell; the accent rectangle
21260
22017
  outline is drawn via inline inset box-shadow on the edge cells (see
21261
22018
  cell-select.ts edgeShadow). While selecting, suppress the caret + native
@@ -21450,7 +22207,7 @@ var styles_default = ` :root {
21450
22207
  .shv-search { display:flex; align-items:center; gap:6px; background:var(--bg); border:1px solid var(--line-strong); border-radius:var(--radius-sm); padding:5px 10px; color:var(--muted); }
21451
22208
  .shv-search input { border:0; background:none; outline:none; color:var(--fg); font-size:13px; width:180px; }
21452
22209
 
21453
- .shv-list { display:flex; flex-direction:column; gap:8px; }
22210
+ .shv-list { display:flex; flex-direction:column; gap:8px; list-style:none; padding:0; margin:0; }
21454
22211
  .shv-row { display:flex; align-items:center; gap:13px; padding:13px 15px; background:var(--bg); border:1px solid var(--line);
21455
22212
  border-radius:var(--radius-lg); transition:border-color .12s, box-shadow .12s; }
21456
22213
  .shv-row:hover { border-color:var(--line-strong); box-shadow:var(--shadow-sm); }
@@ -21472,7 +22229,25 @@ var styles_default = ` :root {
21472
22229
  .shv-exp.dead { text-decoration:line-through; }
21473
22230
  .shv-actions { display:flex; align-items:center; gap:6px; flex:none; }
21474
22231
  .shv-actions .btn { padding:5px 10px; font-size:12px; }
21475
- @media (max-width:640px){ .shv-row{ flex-wrap:wrap; } .shv-actions{ width:100%; justify-content:flex-end; } }
22232
+ @media (max-width:640px){
22233
+ /* head stacks: title/desc full row, then segmented tabs, then search */
22234
+ .shares-page .db-head { flex-direction:column; align-items:stretch; gap:12px; }
22235
+ .shv-tools { margin-left:0; width:100%; }
22236
+ .shv-seg { display:flex; width:100%; }
22237
+ .shv-seg-btn { flex:1; text-align:center; }
22238
+ .shv-search { flex:1; }
22239
+ .shv-search input { width:100%; flex:1; }
22240
+
22241
+ /* cards: title clamps to two lines, meta wraps naturally */
22242
+ .shv-row { flex-wrap:wrap; }
22243
+ .shv-title { max-width:100%; white-space:normal; display:-webkit-box;
22244
+ -webkit-line-clamp:2; -webkit-box-orient:vertical; text-align:left; }
22245
+ .shv-meta { white-space:normal; overflow:visible; text-overflow:clip; }
22246
+
22247
+ /* actions: full row of evenly stretched touch targets */
22248
+ .shv-actions { width:100%; }
22249
+ .shv-actions .btn { flex:1; justify-content:center; padding:7px 10px; }
22250
+ }
21476
22251
 
21477
22252
  /* in-context "\u5DF2\u5206\u4EAB" badge on docs / tables / site cards */
21478
22253
  .share-badge { display:inline-grid; place-items:center; width:22px; height:22px; border-radius:6px; color:var(--accent);
@@ -21549,7 +22324,9 @@ var styles_default = ` :root {
21549
22324
  box-shadow:var(--shadow-lg); z-index:71; transform:translateX(100%); transition:transform .24s cubic-bezier(.32,.72,0,1); display:flex; flex-direction:column; }
21550
22325
  .peek.open { transform:none; }
21551
22326
  .peek-head { display:flex; align-items:center; gap:4px; padding:12px 16px; border-bottom:1px solid var(--line); }
21552
- .peek-body { overflow:auto; padding:28px 32px; }
22327
+ /* Same stable-gutter treatment as .content (stable only \u2014 see .content on
22328
+ why both-edges is forbidden). */
22329
+ .peek-body { overflow:auto; scrollbar-gutter:stable; padding:28px 32px; }
21553
22330
  .peek h2 { font-size:27px; font-weight:700; letter-spacing:-.02em; margin:0 0 20px; outline:none; }
21554
22331
  .proprow { display:flex; gap:8px; align-items:flex-start; margin:1px 0; }
21555
22332
  .proprow .k { width:160px; flex:none; color:var(--muted); display:flex; align-items:center; gap:7px; font-size:13px; padding:7px 8px; border-radius:var(--radius-sm); }
@@ -21606,7 +22383,7 @@ var styles_default = ` :root {
21606
22383
  .hist-restore { margin-top:8px; align-self:flex-start; }
21607
22384
 
21608
22385
  /* menu / popover */
21609
- .menu-layer { position:fixed; inset:0; z-index:90; }
22386
+ .menu-layer { position:fixed; inset:0; z-index:115; }
21610
22387
  .pop { position:fixed; background:var(--bg); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow-lg);
21611
22388
  padding:6px; min-width:232px; max-height:70vh; overflow:auto; animation:pop .12s ease; }
21612
22389
  @keyframes pop { from { opacity:0; transform:translateY(-4px); } }
@@ -21624,6 +22401,9 @@ var styles_default = ` :root {
21624
22401
  .pop .item .meta { flex:1; min-width:0; }
21625
22402
  .pop .item .t { font-size:var(--fs-ui); } .pop .item .d { font-size:var(--fs-xs); color:var(--muted); display:block; }
21626
22403
  .pop .item .chk { color:var(--accent); margin-left:auto; }
22404
+ /* shortcut badge, mirrors .sb-search kbd; margin-left:auto pins it right (and
22405
+ a trailing .chk, whose own auto then collapses, rides the gap beside it) */
22406
+ .pop .item kbd { margin-left:auto; font-family:var(--mono); font-size:10px; color:var(--muted); border:1px solid var(--line-strong); border-radius:4px; padding:1px 4px; }
21627
22407
  .pop .item.danger { color:var(--danger); } .pop .item.danger .lico { color:var(--danger); }
21628
22408
  .pop .sep { height:1px; background:var(--line); margin:5px 4px; }
21629
22409
  .pop .typegrid { display:grid; grid-template-columns:repeat(4,1fr); gap:4px; padding:2px 2px 4px; }
@@ -21743,24 +22523,28 @@ var styles_default = ` :root {
21743
22523
  .view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
21744
22524
  .ico.flip { transform:scaleX(-1); }
21745
22525
 
21746
- /* ---- find in document (Ctrl-F) ---- */
21747
- /* CSS Custom Highlight overlays \u2014 no DOM mutation, safe over contentEditable. */
21748
- ::highlight(mh-find-all) { background:color-mix(in srgb, var(--accent) 26%, transparent); border-radius:2px; }
21749
- ::highlight(mh-find-current) { background:var(--accent); color:var(--accent-fg); }
22526
+ /* ---- find in document (Ctrl-F, CM6 bar) ---- */
22527
+ /* Match highlights are CM6 Decoration.marks (.cm-find/.cm-find-cur, editor-theme.ts).
22528
+ Matches INSIDE void widgets (code mirror, table cells) can't be mark
22529
+ decorations \u2014 the block replace widget swallows them \u2014 so those paint via
22530
+ the CSS Custom Highlight API (find.ts collectMatches/applyHighlights). */
22531
+ ::highlight(mh-find-all) { background: color-mix(in srgb, var(--accent) 26%, transparent); border-radius: 2px; }
22532
+ ::highlight(mh-find-current) { background: var(--accent); color: var(--accent-fg); }
21750
22533
  .find-bar {
21751
- position:fixed; top:58px; right:18px; z-index:40; /* below the ~50px topbar */
22534
+ position:fixed; top:58px; right:18px; z-index:96; /* below the ~50px topbar */
21752
22535
  display:flex; align-items:center; gap:4px;
21753
22536
  padding:5px 6px; background:var(--bg); border:1px solid var(--line-strong);
21754
22537
  border-radius:var(--radius); box-shadow:var(--shadow-lg);
21755
22538
  }
21756
22539
  .find-bar .find-ico { color:var(--muted); margin:0 2px; flex:none; }
21757
22540
  .find-bar input {
21758
- width:200px; max-width:42vw; border:0; outline:0; background:transparent;
22541
+ width:200px; max-width:42vw; min-width:0; border:0; outline:0; background:transparent;
21759
22542
  color:var(--fg); font-size:13px; padding:3px 2px;
21760
22543
  }
21761
22544
  .find-bar input::placeholder { color:var(--muted); }
21762
- .find-count { font-size:12px; color:var(--muted); min-width:48px; text-align:right; padding:0 2px; font-variant-numeric:tabular-nums; }
22545
+ .find-count { font-size:12px; color:var(--muted); min-width:48px; text-align:right; padding:0 2px; font-variant-numeric:tabular-nums; flex:none; }
21763
22546
  .find-opt {
22547
+ flex:none; white-space:nowrap;
21764
22548
  border:1px solid var(--line-strong); background:transparent; color:var(--fg-soft);
21765
22549
  border-radius:var(--radius-sm); font-size:12px; line-height:1; padding:4px 6px; cursor:pointer;
21766
22550
  transition:background .12s, color .12s, border-color .12s;
@@ -21780,6 +22564,14 @@ var styles_default = ` :root {
21780
22564
  .find-bar { left:12px; right:12px; }
21781
22565
  .find-bar input { width:auto; flex:1; max-width:none; }
21782
22566
  }
22567
+ /* Very narrow windows (quicknote resizes down to 280px): drop the reserved
22568
+ count width and tighten gaps so the option buttons never get crushed. */
22569
+ @media (max-width:360px) {
22570
+ .find-bar { gap:2px; padding:5px 4px; }
22571
+ .find-count { min-width:0; }
22572
+ }
22573
+ /* Quicknote window: its qn-bar is ~34px, not the main ~50px topbar. */
22574
+ body.quicknote .find-bar { top:42px; }
21783
22575
 
21784
22576
  /* ---- board (kanban) view ---- */
21785
22577
  .board-wrap { margin-top:4px; }
@@ -21936,8 +22728,7 @@ var styles_default = ` :root {
21936
22728
  .navitem { padding:9px 8px; font-size:16px; }
21937
22729
  .navitem .emoji { font-size:18px; }
21938
22730
  .navitem .tw { width:26px; height:26px; }
21939
- .sb-section-head { font-size:12px; }
21940
- .sb-search kbd { display:none; } /* no hardware keyboard, no shortcut hint */
22731
+ .sb-search kbd, .pop .item kbd { display:none; } /* no hardware keyboard, no shortcut hint */
21941
22732
  /* Search box is hidden until the header search button reveals it; it then
21942
22733
  slides+fades down in. Desktop base (.sb-search{display:flex}) is untouched,
21943
22734
  so search stays always-on there. reduced-motion is handled globally. */
@@ -21952,17 +22743,26 @@ var styles_default = ` :root {
21952
22743
  /* Document: bigger body text, tighter side padding for narrow screens. */
21953
22744
  .doc { padding:24px 18px calc(36vh + env(safe-area-inset-bottom)); }
21954
22745
  .doc-title { font-size:30px; }
21955
- .doc .editable { font-size:16px; }
22746
+ /* CM6 editor (single-document): its .cm-editor inherits from here, so the whole
22747
+ body incl. table cells gets 16px on mobile \u2014 same iOS anti-zoom reason. */
22748
+ .doc .cm-doc-body { font-size:16px; }
21956
22749
  /* 16px (not 14): a focused code textarea below 16px makes iOS auto-zoom. */
21957
22750
  .codeblock { --code-fs:16px; }
21958
- .block .gutter { display:none; }
21959
- /* Anchor/TOC jumps: clear the mobile topbar (49px min + notch inset);
21960
- the desktop 60px estimate leaves blocks half-hidden behind it. */
21961
- .block { scroll-margin-top:calc(49px + env(safe-area-inset-top) + 8px); }
22751
+ /* CM6 hover gutter: no hover on touch, and -52px overlays would overflow
22752
+ the tight mobile margins \u2014 hide. */
22753
+ .cm-gutter-layer { display:none; }
22754
+ /* Autofit tables: on phones the atomic code span IS the width cap \u2014 one
22755
+ long \`file-name.md\` becomes the column's min-content, eats the whole
22756
+ 100cqw budget and crushes prose columns to their 48px floor. Let code
22757
+ wrap at its natural break points (hyphens/slashes) instead; wider
22758
+ layouts keep the atomic-code tradeoff (see the autofit rules). */
22759
+ table.doc-table.autofit .doc-td code { white-space:pre-wrap; overflow-wrap:normal; word-break:normal; }
21962
22760
  /* Find bar clears the taller mobile topbar (49px min + notch inset). */
21963
22761
  .find-bar { top:calc(49px + env(safe-area-inset-top) + 8px); }
21964
22762
  /* narrower handle gutter on phones; .db left padding must be \u2265 --grip-gutter */
21965
22763
  .db { padding:16px 14px 90px 24px; }
22764
+ /* pages reusing .db without row grips don't need the left gutter \u2014 keep padding symmetric */
22765
+ .shares-page.db { padding-left:14px; }
21966
22766
  .gridhost { --grip-gutter:24px; }
21967
22767
  .peek { width:100%; }
21968
22768
  /* history drawer: the side-by-side list/preview doesn't fit a phone \u2014 stack. */
@@ -21997,13 +22797,19 @@ var styles_default = ` :root {
21997
22797
  @media (pointer: coarse) {
21998
22798
  /* iOS Safari auto-zooms the page when a focused field is below 16px; the
21999
22799
  class selectors out-specify the element-level sizes set on these. */
22000
- input, textarea, .text-input, .doc-source, input.inlineedit { font-size:16px; }
22800
+ input, textarea, .text-input, input.inlineedit { font-size:16px; }
22001
22801
  .navitem .acts button { width:34px; height:34px; }
22802
+ .sb-tab { min-height:40px; padding:8px 10px; }
22803
+ .sb-tabs .add { width:40px; height:40px; display:grid; place-items:center; padding:0; }
22002
22804
  .iconbtn { width:40px; height:40px; }
22003
22805
  .btn { padding:9px 14px; min-height:40px; }
22004
22806
  .pop .item { padding:11px 10px; }
22005
22807
  /* Column/sidebar resize handles: 7px is a mouse affordance, not a finger's. */
22006
22808
  .col-resizer, .doc-col-resizer, .sb-resizer { width:16px; right:-8px; }
22809
+ /* cell-rectangle corner handle: big enough to grab with a finger (it is the
22810
+ only way to grow a touch selection \u2014 pan gestures stay scrolls) */
22811
+ .doc-cell-handle { width:12px; height:12px; right:-6px; bottom:-6px; }
22812
+ .doc-cell-handle::after { inset:-10px; }
22007
22813
  }
22008
22814
 
22009
22815
  /* No hover available (touch): controls that only materialize on :hover are
@@ -22012,9 +22818,11 @@ var styles_default = ` :root {
22012
22818
  @media (hover: none) {
22013
22819
  .code-tools { opacity:1; transform:none; pointer-events:auto; }
22014
22820
  .rowactions { opacity:1; pointer-events:auto; }
22015
- .doc-th .doc-col-menu, .cal-add, .filerow .facts,
22016
- .navitem .acts, .sb-section-head .add { opacity:1; }
22017
- table.doc-table .doc-row-del { opacity:1; }
22821
+ .cal-add, .filerow .facts,
22822
+ .navitem .acts { opacity:1; }
22823
+ /* the pill handles survive on touch (focus-driven, not hover-driven), but
22824
+ sticky tap-hover must not tint them */
22825
+ .doc-row-handle:hover, .doc-col-handle:hover { background:var(--bg); color:var(--muted); }
22018
22826
  .selcell input { opacity:1; }
22019
22827
  .tl-item .tl-h { opacity:1; }
22020
22828
  /* Neutralize the stickiest tap-residue tints (a tap "hovers" until the
@@ -22026,12 +22834,16 @@ var styles_default = ` :root {
22026
22834
  .filerow:hover { background:none; }
22027
22835
  .cal-day:hover { background:none; }
22028
22836
  .cal-day.dim:hover { background:var(--surface); }
22837
+ .doc-table-addcol:hover, .doc-table-addrow:hover { background:var(--surface); color:var(--muted); border-color:var(--line-strong); }
22029
22838
  }
22030
22839
 
22031
22840
  /* Quick Notes window (desktop, loaded at #quick). A transparent body lets the
22032
22841
  macOS vibrancy blur show through; a translucent panel keeps text readable.
22033
22842
  The top bar is the window drag region; its buttons opt back out. */
22034
- body.quicknote { background:transparent; }
22843
+ /* --pop-top-inset: editor popups (slash menu) clamp their upward growth below
22844
+ this line, so they never cover the drag bar / macOS traffic lights (bar =
22845
+ 26px actions + 2\xD74px padding). */
22846
+ body.quicknote { background:transparent; --pop-top-inset:38px; }
22035
22847
  /* html carries an opaque bg for the iOS 26 Safari toolbar tint; the vibrancy
22036
22848
  window needs the root transparent again or the blur is walled off. */
22037
22849
  html:has(body.quicknote) { background:transparent; }
@@ -22051,22 +22863,32 @@ var styles_default = ` :root {
22051
22863
  .qn-actions { display:flex; gap:2px; -webkit-app-region:no-drag; }
22052
22864
  .qn-actions .iconbtn { width:26px; height:26px; }
22053
22865
  .qn-actions .iconbtn.active { color:var(--accent); background:var(--hover-2); }
22054
- .qn-body { flex:1; overflow:auto; -webkit-app-region:no-drag; }
22055
- .qn-body .doc { max-width:none; margin:0; padding:14px 22px 30vh; }
22866
+ /* Quicknote scrolling: the app-wide thin translucent scrollbar (transparent
22867
+ track, so the reserved lane just reads as right margin). stable reserves
22868
+ the lane up front so width never jumps when the doc grows past one
22869
+ screen; the doc's right padding + lane \u2248 left padding, keeping the prose
22870
+ visually centered. stable only, NEVER both-edges \u2014 see .content: a left
22871
+ gutter desyncs Chromium's scroll cull rect and paints a blank stripe over
22872
+ the rightmost 15px of content (it sliced this window's file cards). */
22873
+ /* scrollbar-width does NOT inherit (unlike scrollbar-color): without an
22874
+ explicit thin here the lane is the classic 15px. thin = 11px, so the
22875
+ doc's 1px right padding + 11px lane mirrors the 12px left padding. */
22876
+ .qn-body { flex:1; overflow:auto; scrollbar-gutter:stable; scrollbar-width:thin; -webkit-app-region:no-drag; }
22877
+ .qn-body .doc { max-width:none; margin:0; padding:14px 1px 30vh 12px; }
22056
22878
  .qn-body .doc-title { font-size:24px; margin-bottom:2px; }
22057
22879
  .qn-body .doc-meta { display:none; }
22058
- /* The narrow-width gutter hide is now gated on (pointer:coarse), which this
22059
- desktop window never matches \u2014 re-hide explicitly: quick notes offer no
22880
+ /* The CM6 hover gutter never fits the compact window: quick notes offer no
22060
22881
  block insert/drag handles (Enter / slash commands cover insertion). */
22061
- body.quicknote .block .gutter { display:none; }
22882
+ body.quicknote .cm-gutter-layer { display:none; }
22062
22883
  /* A native checkbox paints an opaque white box when unchecked, which breaks
22063
22884
  the window's vibrancy blur. Draw a custom box so the unchecked background
22064
- stays transparent; checked falls back to the accent fill + checkmark. */
22065
- body.quicknote .b-todo .marker input {
22885
+ stays transparent; checked falls back to the accent fill + checkmark.
22886
+ (Same treatment main gave \`.b-todo .marker input\`.) */
22887
+ body.quicknote .cm-todo-box {
22066
22888
  appearance:none; -webkit-appearance:none; box-sizing:border-box;
22067
22889
  border:1.5px solid color-mix(in srgb, var(--fg-soft) 55%, transparent);
22068
22890
  border-radius:4px; background:transparent; }
22069
- body.quicknote .b-todo .marker input:checked {
22891
+ body.quicknote .cm-todo-box:checked {
22070
22892
  border-color:var(--accent);
22071
22893
  background:var(--accent) center/10px no-repeat
22072
22894
  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l5 5L20 7'/%3E%3C/svg%3E"); }
@@ -22088,8 +22910,11 @@ var styles_default = ` :root {
22088
22910
  .qn-set-main { flex:1; min-width:0; }
22089
22911
  .qn-set-name { font-weight:600; }
22090
22912
  .qn-set-desc { color:var(--muted); font-size:13px; margin-top:2px; }
22091
- .qn-shortcut { min-width:140px; justify-content:center; font-family:var(--mono); }
22913
+ .qn-shortcut { min-width:140px; justify-content:center; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif; }
22092
22914
  .qn-shortcut.capturing { outline:2px solid var(--accent); color:var(--accent); }
22915
+ .qn-keys { display:inline-flex; align-items:center; gap:6px; }
22916
+ .qn-key { line-height:1; }
22917
+ .qn-key.sym { font-size:1.15em; line-height:1; } /* \u2318 \u21E7 glyphs render smaller \u2014 bump to match letters */
22093
22918
 
22094
22919
  /* No-origin bootstrap: enroll screen + hydration splash (data-blind shell). */
22095
22920
  .enroll-splash { min-height:100dvh; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; padding:24px; }
@@ -22103,6 +22928,44 @@ var styles_default = ` :root {
22103
22928
  .enroll-err { color:var(--danger); font-size:13px; margin-top:12px; }
22104
22929
  .enroll-go { width:100%; justify-content:center; margin-top:18px; }
22105
22930
  .enroll-hint { font-size:12px; margin-top:14px; line-height:1.5; }
22931
+ .enroll-card { animation:enroll-in .35s ease both; }
22932
+ @keyframes enroll-in { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } }
22933
+
22934
+ /* Scan-first enroll: square viewfinder (corner frame + sweeping accent beam)
22935
+ over the live camera feed. The chip stays black in both themes \u2014 the feed
22936
+ brings its own colors; accent carries the beam and the success flash. */
22937
+ .enroll-scan { position:relative; aspect-ratio:1/1; margin:2px 0 12px; border-radius:16px; overflow:hidden; background:#0b0b0e; }
22938
+ .enroll-scan video { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
22939
+ .enroll-scan::after { content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none; box-shadow:inset 0 0 44px rgba(0,0,0,.35); }
22940
+ .enroll-scan-frame { position:absolute; inset:0; width:100%; height:100%; fill:none; stroke:rgba(255,255,255,.92); stroke-width:2.5; stroke-linecap:round; pointer-events:none; }
22941
+ .enroll-scan-beam { position:absolute; left:9%; right:9%; top:12%; height:2px; border-radius:2px; pointer-events:none; opacity:.85;
22942
+ background:linear-gradient(90deg, transparent, var(--accent), transparent);
22943
+ animation:enroll-beam 2.4s ease-in-out infinite alternate; }
22944
+ @keyframes enroll-beam { to { top:86%; } }
22945
+ .enroll-scan.hit .enroll-scan-frame { stroke:var(--accent); }
22946
+ .enroll-scan.hit::after { animation:enroll-hit .38s ease-out; }
22947
+ @keyframes enroll-hit { from { box-shadow:inset 0 0 0 3px var(--accent), inset 0 0 60px rgba(0,0,0,.2); } to { box-shadow:inset 0 0 44px rgba(0,0,0,.35); } }
22948
+ .enroll-scan-wait { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:rgba(255,255,255,.45); }
22949
+ .enroll-scan-waitico { width:30px; height:30px; animation:enroll-wait 1.6s ease-in-out infinite; }
22950
+ @keyframes enroll-wait { 0%,100% { opacity:.4; } 50% { opacity:.9; } }
22951
+ .enroll-cam-err { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; padding:20px; text-align:center; color:rgba(255,255,255,.9); font-size:13px; }
22952
+ .enroll-cam-err .muted { color:rgba(255,255,255,.55); font-size:12px; }
22953
+ .enroll-cam-errico { width:26px; height:26px; opacity:.7; }
22954
+ .enroll-cam-err .btn { margin-top:8px; }
22955
+ .enroll-paste { width:100%; font-size:13px; }
22956
+ .enroll-switch { display:flex; justify-content:center; align-items:center; gap:10px; margin-top:14px; }
22957
+ .enroll-switch-dot { color:var(--muted); }
22958
+ .enroll-link { background:none; border:0; padding:0; font:inherit; font-size:13px; color:var(--accent); cursor:pointer; }
22959
+ .enroll-link:hover { text-decoration:underline; }
22960
+ /* Unlock step: compact bucket summary chip above the passphrase field. */
22961
+ .enroll-bucket-card { display:flex; align-items:center; gap:10px; padding:12px 14px; margin:2px 0 14px; border:1px solid var(--line); border-radius:12px; background:var(--bg); }
22962
+ .enroll-bucket-ico { width:18px; height:18px; color:var(--muted); }
22963
+ .enroll-bucket-meta { flex:1; min-width:0; }
22964
+ .enroll-bucket-name { font-weight:600; font-size:14px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
22965
+ .enroll-bucket-host { font-family:var(--mono); font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
22966
+ .enroll-bucket-badge { display:inline-flex; align-items:center; gap:4px; flex:none; font-size:11px; color:var(--accent); padding:3px 8px; border-radius:999px;
22967
+ border:1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
22968
+ .enroll-bucket-badge .ico { width:12px; height:12px; }
22106
22969
 
22107
22970
  /* Enroll QR (settings \u2192 \u540C\u6B65\u5B58\u50A8 \u2192 \u5728\u624B\u673A\u4E0A\u6253\u5F00). The QR inverts with the
22108
22971
  theme: light = dark modules on a light chip, dark = light modules on a dark
@@ -22305,6 +23168,42 @@ var ICON_192 = "iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAYAAABS3GwHAAAAAXNSR0IArs4c6QAA
22305
23168
  var ICON_512 = "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";
22306
23169
  var ICON_180 = "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";
22307
23170
 
23171
+ // src/webui/fmt/manifest.ts
23172
+ var FMT_PROVIDERS = [
23173
+ { id: "core", js: "/webui-fmt.js", entry: "fmt/fmt-core.ts" },
23174
+ {
23175
+ id: "ruff",
23176
+ js: "/webui-fmt-ruff.js",
23177
+ entry: "fmt/fmt-ruff.ts",
23178
+ wasm: { route: "/webui-fmt-ruff.wasm", pkg: "@wasm-fmt/ruff_fmt", file: "ruff_fmt_bg.wasm" }
23179
+ },
23180
+ {
23181
+ id: "gofmt",
23182
+ js: "/webui-fmt-gofmt.js",
23183
+ entry: "fmt/fmt-gofmt.ts",
23184
+ wasm: { route: "/webui-fmt-gofmt.wasm", pkg: "@wasm-fmt/gofmt", file: "gofmt.wasm" }
23185
+ },
23186
+ {
23187
+ id: "clang",
23188
+ js: "/webui-fmt-clang.js",
23189
+ entry: "fmt/fmt-clang.ts",
23190
+ wasm: { route: "/webui-fmt-clang.wasm", pkg: "@wasm-fmt/clang-format", file: "clang-format.wasm" }
23191
+ },
23192
+ {
23193
+ id: "lua",
23194
+ js: "/webui-fmt-lua.js",
23195
+ entry: "fmt/fmt-lua.ts",
23196
+ wasm: { route: "/webui-fmt-lua.wasm", pkg: "@wasm-fmt/lua_fmt", file: "lua_fmt_bg.wasm" }
23197
+ },
23198
+ {
23199
+ id: "taplo",
23200
+ js: "/webui-fmt-taplo.js",
23201
+ entry: "fmt/fmt-taplo.ts",
23202
+ wasm: { route: "/webui-fmt-taplo.wasm", pkg: "@wasm-fmt/taplo_fmt", file: "taplo_fmt_bg.wasm" }
23203
+ },
23204
+ { id: "sh", js: "/webui-fmt-sh.js", entry: "fmt/fmt-sh.ts" }
23205
+ ];
23206
+
22308
23207
  // src/webui/server/assets.ts
22309
23208
  var RUNNING_FROM_SOURCE = import.meta.url.includes("/src/webui/");
22310
23209
  var HTML2 = `<!doctype html>
@@ -22349,14 +23248,25 @@ var injected = {
22349
23248
  sdk: null
22350
23249
  };
22351
23250
  var injectedWasm = null;
23251
+ var injectedFmtWasm = {};
23252
+ function toArrayBuffer(w) {
23253
+ return w.byteOffset === 0 && w.byteLength === w.buffer.byteLength ? w.buffer : w.slice().buffer;
23254
+ }
22352
23255
  function setWebuiBundle(bundle) {
22353
23256
  injected.js = bundle.js;
22354
23257
  injected.sw = bundle.sw;
22355
23258
  injected.dbWorker = bundle.dbWorker;
22356
23259
  injected.runtime = bundle.runtime;
22357
23260
  injected.sdk = bundle.sdk;
22358
- const w = bundle.wasm;
22359
- injectedWasm = w.byteOffset === 0 && w.byteLength === w.buffer.byteLength ? w.buffer : w.slice().buffer;
23261
+ injectedWasm = toArrayBuffer(bundle.wasm);
23262
+ for (const p of FMT_PROVIDERS) {
23263
+ const js = bundle.fmt?.[p.js];
23264
+ if (js != null)
23265
+ injected[`fmt_${p.id}`] = js;
23266
+ const w = p.wasm && bundle.fmtWasm?.[p.wasm.route];
23267
+ if (p.wasm && w)
23268
+ injectedFmtWasm[p.wasm.route] = toArrayBuffer(w);
23269
+ }
22360
23270
  }
22361
23271
  function newestSourceMtime(dir) {
22362
23272
  let newest = 0;
@@ -22406,6 +23316,24 @@ function bundleGetter(opts) {
22406
23316
  }
22407
23317
  var getJs = bundleGetter({ key: "js", entry: "../app.tsx", dist: "webui.js" });
22408
23318
  var getSwRaw = bundleGetter({ key: "sw", entry: "../sw.ts", dist: "sw.js" });
23319
+ var fmtJs = {};
23320
+ for (const p of FMT_PROVIDERS) {
23321
+ injected[`fmt_${p.id}`] = null;
23322
+ fmtJs[p.js] = bundleGetter({ key: `fmt_${p.id}`, entry: `../${p.entry}`, dist: p.js.slice(1) });
23323
+ }
23324
+ var fmtWasmByRoute = Object.fromEntries(FMT_PROVIDERS.filter((p) => p.wasm).map((p) => [p.wasm.route, p]));
23325
+ async function getFmtWasm(p) {
23326
+ const w = p.wasm;
23327
+ const embedded = injectedFmtWasm[w.route];
23328
+ if (embedded != null)
23329
+ return embedded;
23330
+ const path = RUNNING_FROM_SOURCE ? join4(dirname(Bun.resolveSync(w.pkg, fileURLToPath(new URL(".", import.meta.url)))), w.file) : fileURLToPath(new URL(`.${w.route}`, import.meta.url));
23331
+ const f = Bun.file(path);
23332
+ if (!await f.exists()) {
23333
+ throw new Error(`${w.route} missing at ${path} \u2014 run \`bun install\` / \`bun run build\``);
23334
+ }
23335
+ return f.arrayBuffer();
23336
+ }
22409
23337
  var getDbWorker = bundleGetter({ key: "dbWorker", entry: "../data/db-worker.ts", dist: "db-worker.js" });
22410
23338
  var getRuntime = bundleGetter({ key: "runtime", entry: "../runtime.ts", dist: "mh-runtime.js" });
22411
23339
  var getSdk = bundleGetter({ key: "sdk", entry: "../../sdk/client.ts", dist: "metahub-sdk.js", extraDirs: ["../../sdk"] });
@@ -22487,12 +23415,24 @@ async function serveWebui(req) {
22487
23415
  return new Response(`sqlite3.wasm unavailable: ${e}`, { status: 500 });
22488
23416
  }
22489
23417
  }
23418
+ const fmtWasmProvider = fmtWasmByRoute[pathname];
23419
+ if (fmtWasmProvider) {
23420
+ try {
23421
+ return new Response(await getFmtWasm(fmtWasmProvider), {
23422
+ headers: { "content-type": "application/wasm", "cache-control": "public, max-age=86400" }
23423
+ });
23424
+ } catch (e) {
23425
+ console.error(`[webui] failed to serve ${pathname} \u2014`, e);
23426
+ return new Response(`${pathname} unavailable: ${e}`, { status: 500 });
23427
+ }
23428
+ }
22490
23429
  const script = {
22491
23430
  "/webui.js": getJsStamped,
22492
23431
  "/sw.js": getSw,
22493
23432
  "/db-worker.js": getDbWorker,
22494
23433
  "/mh-runtime.js": getRuntime,
22495
- "/metahub-sdk.js": getSdk
23434
+ "/metahub-sdk.js": getSdk,
23435
+ ...fmtJs
22496
23436
  };
22497
23437
  const getter = script[pathname];
22498
23438
  if (getter) {
@@ -24370,6 +25310,7 @@ export {
24370
25310
  ensureDirs,
24371
25311
  emitFields,
24372
25312
  emit,
25313
+ editDocumentBatch,
24373
25314
  editDocument,
24374
25315
  duplicateDocument,
24375
25316
  duplicateDatabase,
@@ -24422,4 +25363,4 @@ export {
24422
25363
  BLOB_HASH_HEX
24423
25364
  };
24424
25365
 
24425
- //# debugId=53C623352D26C68364756E2164756E21
25366
+ //# debugId=6391E3807369E20264756E2164756E21