@tensorix/metahub 0.2.2 → 0.3.1

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 (43) hide show
  1. package/dist/cli.js +1195 -267
  2. package/dist/cli.js.map +19 -15
  3. package/dist/core/databases.d.ts +5 -1
  4. package/dist/core/databases.d.ts.map +1 -1
  5. package/dist/core/documents.d.ts +19 -0
  6. package/dist/core/documents.d.ts.map +1 -1
  7. package/dist/core/md/grammar.d.ts +40 -0
  8. package/dist/core/md/grammar.d.ts.map +1 -0
  9. package/dist/core/md/heal.d.ts +2 -0
  10. package/dist/core/md/heal.d.ts.map +1 -0
  11. package/dist/core/md/inline.d.ts +20 -0
  12. package/dist/core/md/inline.d.ts.map +1 -0
  13. package/dist/core/schema-init.d.ts +13 -0
  14. package/dist/core/schema-init.d.ts.map +1 -1
  15. package/dist/core/schema.d.ts +1 -1
  16. package/dist/core/schema.d.ts.map +1 -1
  17. package/dist/core/sync/auth.d.ts +3 -1
  18. package/dist/core/sync/auth.d.ts.map +1 -1
  19. package/dist/core/sync/share-render.d.ts +3 -1
  20. package/dist/core/sync/share-render.d.ts.map +1 -1
  21. package/dist/db-worker.js +51 -41
  22. package/dist/index.js +1094 -254
  23. package/dist/index.js.map +15 -11
  24. package/dist/mh-runtime.js +1 -1
  25. package/dist/sw.js +1 -1
  26. package/dist/webui/fmt/manifest.d.ts +17 -0
  27. package/dist/webui/fmt/manifest.d.ts.map +1 -0
  28. package/dist/webui/server/assets.d.ts +5 -0
  29. package/dist/webui/server/assets.d.ts.map +1 -1
  30. package/dist/webui-fmt-clang.js +1 -0
  31. package/dist/webui-fmt-clang.wasm +0 -0
  32. package/dist/webui-fmt-gofmt.js +1 -0
  33. package/dist/webui-fmt-gofmt.wasm +0 -0
  34. package/dist/webui-fmt-lua.js +2 -0
  35. package/dist/webui-fmt-lua.wasm +0 -0
  36. package/dist/webui-fmt-ruff.js +2 -0
  37. package/dist/webui-fmt-ruff.wasm +0 -0
  38. package/dist/webui-fmt-sh.js +8 -0
  39. package/dist/webui-fmt-taplo.js +2 -0
  40. package/dist/webui-fmt-taplo.wasm +0 -0
  41. package/dist/webui-fmt.js +425 -0
  42. package/dist/webui.js +148 -75
  43. package/package.json +15 -1
package/dist/cli.js CHANGED
@@ -1443,7 +1443,7 @@ var init_crdt = __esm(() => {
1443
1443
  DOMAIN = {
1444
1444
  databases: {
1445
1445
  table: "databases",
1446
- cols: new Set(["name", "icon", "created_hlc", "__deleted"])
1446
+ cols: new Set(["name", "icon", "meta", "created_hlc", "__deleted"])
1447
1447
  },
1448
1448
  properties: {
1449
1449
  table: "properties",
@@ -2283,11 +2283,17 @@ var init_properties = __esm(() => {
2283
2283
  });
2284
2284
 
2285
2285
  // src/core/databases.ts
2286
+ function rowOut(r3) {
2287
+ if (!r3)
2288
+ return null;
2289
+ return { ...r3, meta: r3.meta === null ? null : JSON.parse(r3.meta) };
2290
+ }
2286
2291
  function getDatabase(db, id) {
2287
- return db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE id = ? AND __deleted = 0`).get(id);
2292
+ return rowOut(db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE id = ? AND __deleted = 0`).get(id));
2288
2293
  }
2289
2294
  function listDatabases(db) {
2290
- return db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE __deleted = 0 ORDER BY created_hlc`).all();
2295
+ const rows = db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE __deleted = 0 ORDER BY created_hlc`).all();
2296
+ return rows.map((r3) => rowOut(r3));
2291
2297
  }
2292
2298
  function liveChildren(db, table, col, parentId) {
2293
2299
  return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
@@ -2299,7 +2305,7 @@ var init_databases = __esm(() => {
2299
2305
  init_errors();
2300
2306
  init_properties();
2301
2307
  init_records();
2302
- DATABASE_COLS = ["id", "name", "icon", "created_hlc"];
2308
+ DATABASE_COLS = ["id", "name", "icon", "meta", "created_hlc"];
2303
2309
  DATABASE_SELECT = DATABASE_COLS.join(", ");
2304
2310
  createDatabase = grouped(function createDatabase2(db, opts) {
2305
2311
  const id = newId("db", opts.name);
@@ -2316,6 +2322,11 @@ var init_databases = __esm(() => {
2316
2322
  emit(db, "databases", id, "name", fields.name);
2317
2323
  if (fields.icon !== undefined)
2318
2324
  emit(db, "databases", id, "icon", fields.icon);
2325
+ if (fields.meta !== undefined) {
2326
+ if (fields.meta !== null && (typeof fields.meta !== "object" || Array.isArray(fields.meta)))
2327
+ throw new MhError("invalid_input", "meta must be a JSON object or null");
2328
+ emit(db, "databases", id, "meta", fields.meta);
2329
+ }
2319
2330
  return getDatabase(db, id);
2320
2331
  });
2321
2332
  duplicateDatabase = grouped(function duplicateDatabase2(db, id, opts = {}) {
@@ -2326,6 +2337,11 @@ var init_databases = __esm(() => {
2326
2337
  name: opts.name ?? src2.name,
2327
2338
  icon: opts.icon ?? src2.icon ?? undefined
2328
2339
  });
2340
+ if (src2.meta !== null) {
2341
+ const { collapsed: _hidden, ...meta } = src2.meta;
2342
+ if (Object.keys(meta).length)
2343
+ emit(db, "databases", dup.id, "meta", meta);
2344
+ }
2329
2345
  const propIdMap = new Map;
2330
2346
  for (const p of listProperties(db, id)) {
2331
2347
  const config = p.config?.database === id ? { ...p.config, database: dup.id } : p.config ?? undefined;
@@ -2966,7 +2982,7 @@ function countOccurrences(haystack, needle) {
2966
2982
  }
2967
2983
  return n2;
2968
2984
  }
2969
- 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;
2985
+ 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;
2970
2986
  var init_documents = __esm(() => {
2971
2987
  init_ids();
2972
2988
  init_crdt();
@@ -3096,6 +3112,33 @@ var init_documents = __esm(() => {
3096
3112
  reconcileBody(db, id, newBody);
3097
3113
  return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
3098
3114
  });
3115
+ editDocumentBatch = grouped(function editDocumentBatch2(db, id, opts) {
3116
+ if (!getDocument(db, id))
3117
+ throw new MhError("not_found", `no such document: ${id}`);
3118
+ if (opts.edits.length === 0)
3119
+ throw new MhError("invalid_input", "no edits given");
3120
+ if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
3121
+ throw new MhError("stale", `stale: document changed since ${opts.ifMatch}; re-read first`);
3122
+ ensureBlocks(db, id);
3123
+ const orig = serializeDocBlocks(liveBlocks(db, id).map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })));
3124
+ let body = orig;
3125
+ let total = 0;
3126
+ opts.edits.forEach((e2, i2) => {
3127
+ if (e2.old === "")
3128
+ throw new MhError("invalid_input", `edit[${i2}]: old must not be empty`);
3129
+ const count = countOccurrences(body, e2.old);
3130
+ if (count === 0)
3131
+ throw new MhError("invalid_input", `edit[${i2}]: anchor not found: no match for old`);
3132
+ if (count > 1 && !e2.replaceAll)
3133
+ throw new MhError("ambiguous", `edit[${i2}]: ${count} matches for old; add surrounding context or set replaceAll`);
3134
+ const repl = e2.new ?? "";
3135
+ body = e2.replaceAll ? body.replaceAll(e2.old, repl) : body.replace(e2.old, repl);
3136
+ total += e2.replaceAll ? count : 1;
3137
+ });
3138
+ if (body !== orig)
3139
+ reconcileBody(db, id, body);
3140
+ return { id, changed: body !== orig, replaced: total, version: documentVersion(db, id) };
3141
+ });
3099
3142
  appendDocument = grouped(function appendDocument2(db, id, body) {
3100
3143
  if (!getDocument(db, id))
3101
3144
  throw new MhError("not_found", `no such document: ${id}`);
@@ -19646,29 +19689,280 @@ var init_sites_serve = __esm(() => {
19646
19689
  init_sites();
19647
19690
  });
19648
19691
 
19692
+ // src/core/md/grammar.ts
19693
+ function matchQuoteLine(text) {
19694
+ const m3 = text.match(RE.quote);
19695
+ return m3 ? m3[1] : null;
19696
+ }
19697
+ function matchListLine(line, minIndent) {
19698
+ const indent = leadingIndent(line);
19699
+ if (indent < minIndent)
19700
+ return null;
19701
+ const text = stripIndent(line, indent);
19702
+ let m3 = text.match(/^[-*+]\s+\[([ xX])\][ \t](.*)$/);
19703
+ if (m3) {
19704
+ return {
19705
+ indent,
19706
+ type: "todo",
19707
+ checked: m3[1].toLowerCase() === "x",
19708
+ content: m3[2]
19709
+ };
19710
+ }
19711
+ m3 = text.match(/^(\d+)[.)][ \t]+(.*)$/);
19712
+ if (m3)
19713
+ return { indent, type: "numbered", content: m3[2], num: parseInt(m3[1], 10) };
19714
+ m3 = text.match(/^[-*+][ \t]+(.*)$/);
19715
+ if (m3)
19716
+ return { indent, type: "bullet", content: m3[1] };
19717
+ return null;
19718
+ }
19719
+ function leadingIndent(line) {
19720
+ let indent = 0;
19721
+ for (const ch of line) {
19722
+ if (ch === " ")
19723
+ indent++;
19724
+ else if (ch === "\t")
19725
+ indent += 4;
19726
+ else
19727
+ break;
19728
+ }
19729
+ return indent;
19730
+ }
19731
+ function stripIndent(line, columns) {
19732
+ let indent = 0;
19733
+ let i2 = 0;
19734
+ for (;i2 < line.length && indent < columns; i2++) {
19735
+ const ch = line[i2];
19736
+ if (ch === " ")
19737
+ indent++;
19738
+ else if (ch === "\t")
19739
+ indent += 4;
19740
+ else
19741
+ break;
19742
+ }
19743
+ return line.slice(i2);
19744
+ }
19745
+ function isFenceClose(line, char, len) {
19746
+ const trimmed = line.trim();
19747
+ if (!trimmed || trimmed[0] !== char)
19748
+ return false;
19749
+ for (const ch of trimmed) {
19750
+ if (ch !== char)
19751
+ return false;
19752
+ }
19753
+ return trimmed.length >= len;
19754
+ }
19755
+ function cleanLang(lang) {
19756
+ return lang.trim().replace(/[^A-Za-z0-9_+.#-]/g, "");
19757
+ }
19758
+ function looksLikeTableAt(lines, i2, minIndent) {
19759
+ const head = lines[i2];
19760
+ const delim = lines[i2 + 1];
19761
+ if (head == null || delim == null)
19762
+ return false;
19763
+ if (leadingIndent(head) < minIndent || leadingIndent(delim) < minIndent)
19764
+ return false;
19765
+ const headText = stripIndent(head, minIndent);
19766
+ const delimText = stripIndent(delim, minIndent);
19767
+ if (!headText.includes("|") || !delimText.includes("|"))
19768
+ return false;
19769
+ const cells = splitTableRow(delimText);
19770
+ return cells.length > 0 && cells.every((c3) => RE_DELIM_CELL.test(c3));
19771
+ }
19772
+ function splitTableRow(line) {
19773
+ const cells = [];
19774
+ let cur = "";
19775
+ for (let i2 = 0;i2 < line.length; i2++) {
19776
+ const ch = line[i2];
19777
+ if (ch === "\\" && line[i2 + 1] === "|") {
19778
+ cur += "|";
19779
+ i2++;
19780
+ continue;
19781
+ }
19782
+ if (ch === "|") {
19783
+ cells.push(cur);
19784
+ cur = "";
19785
+ continue;
19786
+ }
19787
+ cur += ch;
19788
+ }
19789
+ cells.push(cur);
19790
+ if (cells.length && cells[0].trim() === "")
19791
+ cells.shift();
19792
+ if (cells.length && cells[cells.length - 1].trim() === "")
19793
+ cells.pop();
19794
+ return cells.map((c3) => c3.trim());
19795
+ }
19796
+ var HTML_FENCE = "mh-html", RE, RE_DELIM_CELL;
19797
+ var init_grammar = __esm(() => {
19798
+ RE = {
19799
+ fenceOpen: /^\s*(`{3,}|~{3,})\s*([^\s`]*)?.*$/,
19800
+ h: /^(#{1,6})\s+(.*)$/,
19801
+ todo: /^\s*[-*+]\s+\[([ xX])\][ \t](.*)$/,
19802
+ bullet: /^\s*[-*+]\s+(.*)$/,
19803
+ numbered: /^\s*(\d+)[.)]\s+(.*)$/,
19804
+ quote: /^>[ \t](.*)$/,
19805
+ divider: /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/
19806
+ };
19807
+ RE_DELIM_CELL = /^\s*:?-+:?\s*$/;
19808
+ });
19809
+
19810
+ // src/core/md/inline.ts
19811
+ function tokenizeInline(text) {
19812
+ if (!text)
19813
+ return [];
19814
+ const cands = [];
19815
+ for (const p2 of PATTERNS) {
19816
+ p2.re.lastIndex = 0;
19817
+ let m3;
19818
+ while (m3 = p2.re.exec(text)) {
19819
+ const start = m3.index;
19820
+ const end = start + m3[0].length;
19821
+ let innerFrom;
19822
+ let innerTo;
19823
+ let url2;
19824
+ let alt;
19825
+ if (p2.kind === "link") {
19826
+ innerFrom = start + 1;
19827
+ innerTo = start + 1 + m3[1].length;
19828
+ url2 = m3[2];
19829
+ } else if (p2.kind === "image") {
19830
+ innerFrom = start + 2;
19831
+ innerTo = start + 2 + m3[1].length;
19832
+ url2 = m3[2];
19833
+ alt = m3[1];
19834
+ } else {
19835
+ innerFrom = start + p2.delim;
19836
+ innerTo = end - p2.delim;
19837
+ if (innerTo <= innerFrom)
19838
+ continue;
19839
+ }
19840
+ cands.push({ kind: p2.kind, start, end, innerFrom, innerTo, url: url2, alt, rank: p2.rank });
19841
+ }
19842
+ }
19843
+ cands.sort((a2, b3) => a2.start - b3.start || a2.rank - b3.rank || b3.end - a2.end);
19844
+ const out = [];
19845
+ let cursor = 0;
19846
+ for (const c3 of cands) {
19847
+ if (c3.start < cursor)
19848
+ continue;
19849
+ cursor = c3.end;
19850
+ out.push({
19851
+ kind: c3.kind,
19852
+ start: c3.start,
19853
+ end: c3.end,
19854
+ innerFrom: c3.innerFrom,
19855
+ innerTo: c3.innerTo,
19856
+ ...c3.url !== undefined ? { url: c3.url } : {},
19857
+ ...c3.alt !== undefined ? { alt: c3.alt } : {}
19858
+ });
19859
+ }
19860
+ return out;
19861
+ }
19862
+ var PATTERNS;
19863
+ var init_inline = __esm(() => {
19864
+ PATTERNS = [
19865
+ { kind: "code", re: /(?<!\\)`([^`\n]+)`/g, delim: 1, rank: 0 },
19866
+ { kind: "image", re: /(?<!\\)!\[([^\]\n]*)\]\(([^)\s]+)\)/g, delim: 0, rank: 1 },
19867
+ { kind: "strong", re: /(?<!\\)\*\*([^*\n]+?)(?<!\\)\*\*/g, delim: 2, rank: 2 },
19868
+ { kind: "strong", re: /(?<!\\)__([^_\n]+?)(?<!\\)__/g, delim: 2, rank: 2 },
19869
+ { kind: "del", re: /(?<!\\)~~([^~\n]+?)(?<!\\)~~/g, delim: 2, rank: 3 },
19870
+ { kind: "em", re: /(?<![*\w])(?<!\\)\*([^*\n]+?)(?<!\\)\*(?!\*)/g, delim: 1, rank: 4 },
19871
+ { kind: "em", re: /(?<![_\w])(?<!\\)_([^_\n]+?)(?<!\\)_(?!_)/g, delim: 1, rank: 4 },
19872
+ { kind: "link", re: /(?<!\\)\[([^\]\n]+)\]\(([^)\s]+)\)/g, delim: 0, rank: 5 }
19873
+ ];
19874
+ });
19875
+
19876
+ // src/core/md/heal.ts
19877
+ function fenceMask(lines) {
19878
+ const mask = new Array(lines.length).fill(false);
19879
+ for (let i2 = 0;i2 < lines.length; i2++) {
19880
+ const m3 = lines[i2].match(RE.fenceOpen);
19881
+ if (!m3)
19882
+ continue;
19883
+ const ch = m3[1][0];
19884
+ const len = m3[1].length;
19885
+ let close = -1;
19886
+ for (let j2 = i2 + 1;j2 < lines.length; j2++) {
19887
+ if (isFenceClose(lines[j2], ch, len)) {
19888
+ close = j2;
19889
+ break;
19890
+ }
19891
+ }
19892
+ if (close === -1)
19893
+ continue;
19894
+ for (let j2 = i2;j2 <= close; j2++)
19895
+ mask[j2] = true;
19896
+ i2 = close;
19897
+ }
19898
+ return mask;
19899
+ }
19900
+ function healLegacyMarkdown(src2) {
19901
+ if (!MAYBE_LEGACY.test(src2))
19902
+ return src2;
19903
+ const lines = src2.split(`
19904
+ `);
19905
+ const inFence = fenceMask(lines);
19906
+ const quoteish = (i2) => i2 >= 0 && i2 < lines.length && !inFence[i2] && matchQuoteLine(lines[i2].replace(/^[ \t]*/, "")) !== null;
19907
+ let changed = false;
19908
+ for (let i2 = 0;i2 < lines.length; i2++) {
19909
+ if (inFence[i2])
19910
+ continue;
19911
+ const line = lines[i2];
19912
+ if (LEGACY_EMPTY_TODO.test(line)) {
19913
+ lines[i2] = line + " ";
19914
+ changed = true;
19915
+ } else if (BARE_QUOTE.test(line) && (quoteish(i2 - 1) || quoteish(i2 + 1))) {
19916
+ lines[i2] = line + " ";
19917
+ changed = true;
19918
+ }
19919
+ }
19920
+ return changed ? lines.join(`
19921
+ `) : src2;
19922
+ }
19923
+ var LEGACY_EMPTY_TODO, BARE_QUOTE, MAYBE_LEGACY;
19924
+ var init_heal = __esm(() => {
19925
+ init_grammar();
19926
+ LEGACY_EMPTY_TODO = /^[ \t]*[-*+][ \t]+\[[ xX]\]$/;
19927
+ BARE_QUOTE = /^[ \t]*>$/;
19928
+ MAYBE_LEGACY = /^[ \t]*(?:[-*+][ \t]+\[[ xX]\]|>)$/m;
19929
+ });
19930
+
19649
19931
  // src/core/sync/share-render.ts
19650
19932
  function escapeHtml(s2) {
19651
19933
  return s2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19652
19934
  }
19653
19935
  function renderInline(src2, opts = {}) {
19654
- const codes = [];
19655
- let s2 = src2.replace(/`([^`]+)`/g, (_m, c3) => {
19656
- codes.push(c3);
19657
- return A4 + (codes.length - 1) + B3;
19658
- });
19659
- s2 = escapeHtml(s2);
19660
- s2 = s2.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_m, alt, u3) => {
19661
- const url2 = rewrite(String(u3), opts).replace(/"/g, "&quot;");
19662
- return `<img src="${url2}" alt="${escapeHtml(String(alt))}" loading="lazy">`;
19663
- });
19664
- s2 = s2.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, t2, u3) => {
19665
- const url2 = rewrite(String(u3), opts).replace(/"/g, "&quot;");
19666
- return `<a href="${url2}" target="_blank" rel="noreferrer noopener">${t2}</a>`;
19667
- });
19668
- s2 = s2.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
19669
- s2 = s2.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
19670
- s2 = s2.replace(RESTORE, (_m, i2) => `<code>${escapeHtml(codes[+i2] ?? "")}</code>`);
19671
- return s2;
19936
+ const tokens = tokenizeInline(src2);
19937
+ let out = "";
19938
+ let pos = 0;
19939
+ for (const t2 of tokens) {
19940
+ out += escapeHtml(src2.slice(pos, t2.start));
19941
+ const inner = src2.slice(t2.innerFrom, t2.innerTo);
19942
+ const url2 = rewrite(t2.url ?? "", opts).replace(/"/g, "&quot;");
19943
+ switch (t2.kind) {
19944
+ case "code":
19945
+ out += `<code>${escapeHtml(inner)}</code>`;
19946
+ break;
19947
+ case "strong":
19948
+ out += `<strong>${escapeHtml(inner)}</strong>`;
19949
+ break;
19950
+ case "em":
19951
+ out += `<em>${escapeHtml(inner)}</em>`;
19952
+ break;
19953
+ case "del":
19954
+ out += `<del>${escapeHtml(inner)}</del>`;
19955
+ break;
19956
+ case "link":
19957
+ out += `<a href="${url2}" target="_blank" rel="noreferrer noopener">${escapeHtml(inner)}</a>`;
19958
+ break;
19959
+ case "image":
19960
+ out += `<img src="${url2}" alt="${escapeHtml(t2.alt ?? "")}" loading="lazy">`;
19961
+ break;
19962
+ }
19963
+ pos = t2.end;
19964
+ }
19965
+ return out + escapeHtml(src2.slice(pos));
19672
19966
  }
19673
19967
  function rewrite(url2, opts) {
19674
19968
  if (opts.rewriteBlob && url2.startsWith("/blob/"))
@@ -19676,8 +19970,9 @@ function rewrite(url2, opts) {
19676
19970
  return url2;
19677
19971
  }
19678
19972
  function renderMarkdown(md, opts = {}) {
19679
- const lines = (md ?? "").replace(/\r\n?/g, `
19680
- `).split(`
19973
+ const healed = healLegacyMarkdown((md ?? "").replace(/\r\n?/g, `
19974
+ `));
19975
+ const lines = healed.split(`
19681
19976
  `);
19682
19977
  const out = [];
19683
19978
  let i2 = 0;
@@ -19690,38 +19985,39 @@ function renderMarkdown(md, opts = {}) {
19690
19985
  };
19691
19986
  while (i2 < lines.length) {
19692
19987
  const line = lines[i2];
19693
- const fence = line.match(FENCE);
19988
+ const fence = line.match(RE.fenceOpen);
19694
19989
  if (fence) {
19695
- flushPara();
19696
19990
  const marker = fence[1][0];
19697
- const info = (fence[2] ?? "").trim();
19698
- const body = [];
19699
- i2++;
19700
- while (i2 < lines.length) {
19701
- const m3 = lines[i2].match(/^\s*(`{3,}|~{3,})\s*$/);
19702
- if (m3 && m3[1][0] === marker) {
19703
- i2++;
19991
+ const len = fence[1].length;
19992
+ let close = -1;
19993
+ for (let j2 = i2 + 1;j2 < lines.length; j2++) {
19994
+ if (isFenceClose(lines[j2], marker, len)) {
19995
+ close = j2;
19704
19996
  break;
19705
19997
  }
19706
- body.push(lines[i2]);
19707
- i2++;
19708
19998
  }
19709
- if (info === "mh-html") {
19710
- out.push(`<iframe class="mh-embed" sandbox="allow-scripts allow-popups" srcdoc="${escapeHtml(body.join(`
19999
+ if (close !== -1) {
20000
+ flushPara();
20001
+ const info = cleanLang(fence[2] ?? "");
20002
+ const body = lines.slice(i2 + 1, close);
20003
+ i2 = close + 1;
20004
+ if (info === HTML_FENCE) {
20005
+ out.push(`<iframe class="mh-embed" sandbox="allow-scripts allow-popups" srcdoc="${escapeHtml(body.join(`
19711
20006
  `))}"></iframe>`);
19712
- } else {
19713
- const cls = info ? ` class="language-${escapeHtml(info)}"` : "";
19714
- out.push(`<pre><code${cls}>${escapeHtml(body.join(`
20007
+ } else {
20008
+ const cls = info ? ` class="language-${escapeHtml(info)}"` : "";
20009
+ out.push(`<pre><code${cls}>${escapeHtml(body.join(`
19715
20010
  `))}</code></pre>`);
20011
+ }
20012
+ continue;
19716
20013
  }
19717
- continue;
19718
20014
  }
19719
20015
  if (line.trim() === "") {
19720
20016
  flushPara();
19721
20017
  i2++;
19722
20018
  continue;
19723
20019
  }
19724
- const h3 = line.match(/^(#{1,6})\s+(.*)$/);
20020
+ const h3 = stripLead(line).match(RE.h);
19725
20021
  if (h3) {
19726
20022
  flushPara();
19727
20023
  const level = h3[1].length;
@@ -19729,30 +20025,33 @@ function renderMarkdown(md, opts = {}) {
19729
20025
  i2++;
19730
20026
  continue;
19731
20027
  }
19732
- if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
20028
+ if (RE.divider.test(line)) {
19733
20029
  flushPara();
19734
20030
  out.push("<hr>");
19735
20031
  i2++;
19736
20032
  continue;
19737
20033
  }
19738
- if (/^\s*>\s?/.test(line)) {
20034
+ if (matchQuoteLine(stripLead(line)) !== null) {
19739
20035
  flushPara();
19740
20036
  const q3 = [];
19741
- while (i2 < lines.length && /^\s*>\s?/.test(lines[i2])) {
19742
- q3.push(lines[i2].replace(/^\s*>\s?/, ""));
20037
+ while (i2 < lines.length) {
20038
+ const content = matchQuoteLine(stripLead(lines[i2]));
20039
+ if (content === null)
20040
+ break;
20041
+ q3.push(content);
19743
20042
  i2++;
19744
20043
  }
19745
20044
  out.push(`<blockquote>${renderMarkdown(q3.join(`
19746
20045
  `), opts)}</blockquote>`);
19747
20046
  continue;
19748
20047
  }
19749
- if (line.includes("|") && i2 + 1 < lines.length && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i2 + 1]) && lines[i2 + 1].includes("-")) {
20048
+ if (looksLikeTableAt(lines, i2, 0)) {
19750
20049
  flushPara();
19751
20050
  const rows = [];
19752
- const head = splitRow(line);
20051
+ const head = splitTableRow(stripLead(lines[i2]));
19753
20052
  i2 += 2;
19754
20053
  while (i2 < lines.length && lines[i2].includes("|") && lines[i2].trim() !== "") {
19755
- rows.push(splitRow(lines[i2]));
20054
+ rows.push(splitTableRow(stripLead(lines[i2])));
19756
20055
  i2++;
19757
20056
  }
19758
20057
  const th = head.map((c3) => `<th>${renderInline(c3, opts)}</th>`).join("");
@@ -19760,19 +20059,25 @@ function renderMarkdown(md, opts = {}) {
19760
20059
  out.push(`<table><thead><tr>${th}</tr></thead><tbody>${body}</tbody></table>`);
19761
20060
  continue;
19762
20061
  }
19763
- const ul = line.match(/^\s*[-*+]\s+/);
19764
- const ol = line.match(/^\s*\d+[.)]\s+/);
19765
- if (ul || ol) {
20062
+ const first = matchListLine(line, 0);
20063
+ if (first) {
19766
20064
  flushPara();
19767
- const ordered = !!ol;
19768
- const items = [];
19769
- const re = ordered ? /^\s*\d+[.)]\s+/ : /^\s*[-*+]\s+/;
19770
- while (i2 < lines.length && re.test(lines[i2])) {
19771
- items.push(lines[i2].replace(re, ""));
20065
+ const ordered = first.type === "numbered";
20066
+ const lis = [];
20067
+ while (i2 < lines.length) {
20068
+ const it3 = matchListLine(lines[i2], 0);
20069
+ if (!it3 || it3.type === "numbered" !== ordered)
20070
+ break;
20071
+ if (it3.type === "todo") {
20072
+ lis.push(`<li class="todo"><input type="checkbox" disabled${it3.checked ? " checked" : ""}> ${renderInline(it3.content, opts)}</li>`);
20073
+ } else if (it3.type === "numbered" && it3.num !== undefined) {
20074
+ lis.push(`<li value="${it3.num}">${renderInline(it3.content, opts)}</li>`);
20075
+ } else {
20076
+ lis.push(`<li>${renderInline(it3.content, opts)}</li>`);
20077
+ }
19772
20078
  i2++;
19773
20079
  }
19774
- const lis = items.map((t2) => `<li>${renderInline(t2, opts)}</li>`).join("");
19775
- out.push(ordered ? `<ol>${lis}</ol>` : `<ul>${lis}</ul>`);
20080
+ out.push(ordered ? `<ol>${lis.join("")}</ol>` : `<ul>${lis.join("")}</ul>`);
19776
20081
  continue;
19777
20082
  }
19778
20083
  if (/^\s*!\[[^\]]*\]\([^)\s]+\)\s*$/.test(line)) {
@@ -19788,20 +20093,13 @@ function renderMarkdown(md, opts = {}) {
19788
20093
  return out.join(`
19789
20094
  `);
19790
20095
  }
19791
- function splitRow(line) {
19792
- let s2 = line.trim();
19793
- if (s2.startsWith("|"))
19794
- s2 = s2.slice(1);
19795
- if (s2.endsWith("|"))
19796
- s2 = s2.slice(0, -1);
19797
- return s2.split("|").map((c3) => c3.trim());
20096
+ function stripLead(line) {
20097
+ return stripIndent(line, Number.MAX_SAFE_INTEGER);
19798
20098
  }
19799
- var A4, B3, RESTORE, FENCE;
19800
20099
  var init_share_render = __esm(() => {
19801
- A4 = String.fromCharCode(57344);
19802
- B3 = String.fromCharCode(57345);
19803
- RESTORE = new RegExp(A4 + "(\\d+)" + B3, "g");
19804
- FENCE = /^\s*(`{3,}|~{3,})(.*)$/;
20100
+ init_grammar();
20101
+ init_inline();
20102
+ init_heal();
19805
20103
  });
19806
20104
 
19807
20105
  // src/core/sync/share-serve.ts
@@ -21657,7 +21955,7 @@ async function runMain(cmd, opts = {}) {
21657
21955
  // package.json
21658
21956
  var package_default = {
21659
21957
  name: "@tensorix/metahub",
21660
- version: "0.2.2",
21958
+ version: "0.3.1",
21661
21959
  description: "Metahub core library and CLI",
21662
21960
  license: "AGPL-3.0-only",
21663
21961
  type: "module",
@@ -21687,16 +21985,21 @@ var package_default = {
21687
21985
  "build:shell": "bun run scripts/build-shell.ts",
21688
21986
  "build:share-viewer": "bun run scripts/build-share-viewer.ts",
21689
21987
  "build:binaries": "bun run scripts/compile-binaries.ts",
21988
+ "build:binaries:host": "HOST_ONLY=1 bun run scripts/compile-binaries.ts",
21690
21989
  prepublishOnly: "bun run build",
21691
21990
  test: "bun test",
21692
21991
  release: "bun run scripts/tag-release.ts"
21693
21992
  },
21694
21993
  dependencies: {
21695
21994
  "@clack/prompts": "^1.5.0",
21995
+ "@codemirror/commands": "6.10.4",
21996
+ "@codemirror/state": "6.7.0",
21997
+ "@codemirror/view": "6.43.4",
21696
21998
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
21697
21999
  aws4fetch: "^1.0.20",
21698
22000
  citty: "^0.1.6",
21699
22001
  "highlight.js": "^11.11.1",
22002
+ jsqr: "^1.4.0",
21700
22003
  preact: "^10.29.2",
21701
22004
  "qrcode-generator": "^2.0.4",
21702
22005
  turndown: "^7.2.4",
@@ -21705,8 +22008,17 @@ var package_default = {
21705
22008
  },
21706
22009
  devDependencies: {
21707
22010
  "@happy-dom/global-registrator": "^20.10.2",
22011
+ "@prettier/plugin-php": "^0.25.0",
21708
22012
  "@types/bun": "latest",
21709
22013
  "@types/turndown": "^5.0.6",
22014
+ "@wasm-fmt/clang-format": "^22.1.8",
22015
+ "@wasm-fmt/gofmt": "^0.7.3",
22016
+ "@wasm-fmt/lua_fmt": "^0.3.3",
22017
+ "@wasm-fmt/ruff_fmt": "^0.15.20",
22018
+ "@wasm-fmt/taplo_fmt": "^0.2.5",
22019
+ "mvdan-sh": "^0.10.1",
22020
+ prettier: "^3.6",
22021
+ "sql-formatter": "^15",
21710
22022
  typescript: "^5"
21711
22023
  }
21712
22024
  };
@@ -21850,6 +22162,7 @@ CREATE TABLE IF NOT EXISTS databases (
21850
22162
  id TEXT PRIMARY KEY,
21851
22163
  name TEXT,
21852
22164
  icon TEXT,
22165
+ meta TEXT, -- JSON: generic replicated metadata (e.g. UI flags), one LWW register
21853
22166
  created_hlc TEXT,
21854
22167
  __deleted INTEGER NOT NULL DEFAULT 0
21855
22168
  );
@@ -22132,9 +22445,25 @@ function migrateStoragePeerUrls(db) {
22132
22445
  });
22133
22446
  tx();
22134
22447
  }
22448
+ function migrateDatabases(db) {
22449
+ if (hasColumn(db, "databases", "meta"))
22450
+ return;
22451
+ db.exec("ALTER TABLE databases ADD COLUMN meta TEXT");
22452
+ db.exec(`
22453
+ UPDATE databases SET meta = (
22454
+ SELECT c.value FROM crdt_changes c
22455
+ WHERE c.dataset = 'databases' AND c.col = 'meta' AND c.row_id = databases.id
22456
+ ORDER BY c.hlc DESC LIMIT 1
22457
+ )
22458
+ WHERE id IN (
22459
+ SELECT DISTINCT row_id FROM crdt_changes WHERE dataset = 'databases' AND col = 'meta'
22460
+ )
22461
+ `);
22462
+ }
22135
22463
  function initSchema(db) {
22136
22464
  runSchema(db);
22137
22465
  migrateOplog(db);
22466
+ migrateDatabases(db);
22138
22467
  migrateCrdtChangesSeq(db);
22139
22468
  migrateRecords(db);
22140
22469
  migratePeers(db);
@@ -22378,6 +22707,18 @@ mh doc edit <ref> --old "old text" --new "new text"
22378
22707
  mh doc edit <ref> --old "TODO" --new "DONE" --replace-all
22379
22708
  mh doc edit <ref> --old "..." --new "..." --if-match <version-from-read>
22380
22709
 
22710
+ # EDIT (batch): apply several pairs in ONE atomic call \u2014 prefer this for many
22711
+ # scattered deltas. One --if-match check, one revision, no re-read between edits.
22712
+ # Pairs fold in order (a later "old" can match an earlier "new"); if ANY pair's
22713
+ # anchor is missing/ambiguous the whole batch aborts and the doc is untouched.
22714
+ mh doc edit <ref> --if-match <version> --edits @- <<'JSON'
22715
+ [{"old":"(draft)","new":""},
22716
+ {"old":"status: todo","new":"status: done"},
22717
+ {"old":"FIXME","new":"FIXED","replaceAll":true}]
22718
+ JSON
22719
+ # Keep anchors in prose. Don't let --old/--new (or a batch pair) cut into the
22720
+ # interior of a table, code fence, or media block, or the block won't round-trip.
22721
+
22381
22722
  # WRITE more: add blocks at the tail or head
22382
22723
  mh doc append <ref> --body "a new paragraph"
22383
22724
  mh doc prepend <ref> --body "## Summary"
@@ -22393,8 +22734,11 @@ Required agent loop for revising a document (treat \`--if-match\` as mandatory \
22393
22734
  without it a concurrent edit from another session/the WebUI is silently
22394
22735
  clobbered):
22395
22736
  1. \`mh doc read <ref>\` \u2192 capture \`body\` and \`version\`.
22396
- 2. Compute a minimal \`--old\`/\`--new\` pair (unique anchor text).
22397
- 3. \`mh doc edit <ref> --old ... --new ... --if-match <version>\`.
22737
+ 2. Compute the minimal \`--old\`/\`--new\` pair(s) (unique anchor text). Revising
22738
+ several spots at once? Collect them into one \`--edits\` batch instead of
22739
+ editing serially \u2014 the version bumps on the whole doc after any edit, so
22740
+ serial \`--if-match\` edits force a re-read between each.
22741
+ 3. \`mh doc edit <ref> --old ... --new ... --if-match <version>\` (or \`--edits\`).
22398
22742
  4. On exit 5 (\`stale\`), re-read and retry from step 1.
22399
22743
 
22400
22744
  ## Media & attachments (images / video / audio / files)
@@ -22554,24 +22898,45 @@ will prompt interactively** \u2014 always pass their flags explicitly:
22554
22898
  - **JSON by default** when piped; add \`--pretty\` only when a human will read it.
22555
22899
  `;
22556
22900
 
22557
- // src/cli/claude-skill.ts
22901
+ // src/cli/agent-skill.ts
22558
22902
  import { mkdirSync as mkdirSync2 } from "fs";
22559
22903
  import { homedir as homedir2 } from "os";
22560
22904
  import { join as join2 } from "path";
22905
+ var CODEX_OPENAI_YAML = `interface:
22906
+ display_name: "Metahub"
22907
+ short_description: "Use mh for durable agent memory"
22908
+ default_prompt: "Use $mh to store, recall, search, and edit durable working memory with metahub."
22909
+ `;
22561
22910
  function claudeHome() {
22562
22911
  return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
22563
22912
  }
22913
+ function codexHome() {
22914
+ return process.env.CODEX_HOME ?? join2(homedir2(), ".codex");
22915
+ }
22916
+ async function writeIfChanged(path, content) {
22917
+ const prev = await Bun.file(path).exists() ? await Bun.file(path).text() : null;
22918
+ if (prev === content)
22919
+ return false;
22920
+ await Bun.write(path, content);
22921
+ return true;
22922
+ }
22564
22923
  async function scaffoldClaudeSkill() {
22565
22924
  const dir = join2(claudeHome(), "skills", "mh");
22566
22925
  const path = join2(dir, "SKILL.md");
22567
- const prev = await Bun.file(path).exists() ? await Bun.file(path).text() : null;
22568
- const changed = prev !== SKILL_default;
22569
- if (changed) {
22570
- mkdirSync2(dir, { recursive: true });
22571
- await Bun.write(path, SKILL_default);
22572
- }
22926
+ mkdirSync2(dir, { recursive: true });
22927
+ const changed = await writeIfChanged(path, SKILL_default);
22573
22928
  return { path, changed };
22574
22929
  }
22930
+ async function scaffoldCodexSkill() {
22931
+ const dir = join2(codexHome(), "skills", "mh");
22932
+ const path = join2(dir, "SKILL.md");
22933
+ const metadataDir = join2(dir, "agents");
22934
+ const metadataPath = join2(metadataDir, "openai.yaml");
22935
+ mkdirSync2(metadataDir, { recursive: true });
22936
+ const skillChanged = await writeIfChanged(path, SKILL_default);
22937
+ const metadataChanged = await writeIfChanged(metadataPath, CODEX_OPENAI_YAML);
22938
+ return { path, metadataPath, changed: skillChanged || metadataChanged };
22939
+ }
22575
22940
 
22576
22941
  // src/cli/commands/init.ts
22577
22942
  var init_default = defineCommand({
@@ -22583,13 +22948,43 @@ var init_default = defineCommand({
22583
22948
  claude: {
22584
22949
  type: "boolean",
22585
22950
  description: "Install a Claude Code /mh skill into ~/.claude (does not touch ~/.metahub)"
22951
+ },
22952
+ codex: {
22953
+ type: "boolean",
22954
+ description: "Install a Codex $mh skill into ~/.codex (does not touch ~/.metahub)"
22586
22955
  }
22587
22956
  },
22588
22957
  async run({ args }) {
22589
- if (args.claude) {
22590
- const r3 = await scaffoldClaudeSkill();
22591
- print({ changed: r3.changed, skill: r3.path }, () => (r3.changed ? `Installed ${r3.path}` : `Up to date: ${r3.path}`) + `
22958
+ if (args.claude || args.codex) {
22959
+ const claude = args.claude ? await scaffoldClaudeSkill() : null;
22960
+ const codex = args.codex ? await scaffoldCodexSkill() : null;
22961
+ if (claude && !codex) {
22962
+ print({ changed: claude.changed, skill: claude.path }, () => (claude.changed ? `Installed ${claude.path}` : `Up to date: ${claude.path}`) + `
22963
+ Use /mh in Claude Code (any project).`);
22964
+ return;
22965
+ }
22966
+ if (codex && !claude) {
22967
+ print({ changed: codex.changed, skill: codex.path, metadata: codex.metadataPath }, () => (codex.changed ? `Installed ${codex.path}` : `Up to date: ${codex.path}`) + `
22968
+ Use $mh in Codex (restart Codex if it was already running).`);
22969
+ return;
22970
+ }
22971
+ print({
22972
+ changed: !!claude?.changed || !!codex?.changed,
22973
+ claude: claude ? { changed: claude.changed, skill: claude.path } : null,
22974
+ codex: codex ? { changed: codex.changed, skill: codex.path, metadata: codex.metadataPath } : null
22975
+ }, () => {
22976
+ const lines = [];
22977
+ if (claude) {
22978
+ lines.push((claude.changed ? `Installed ${claude.path}` : `Up to date: ${claude.path}`) + `
22592
22979
  Use /mh in Claude Code (any project).`);
22980
+ }
22981
+ if (codex) {
22982
+ lines.push((codex.changed ? `Installed ${codex.path}` : `Up to date: ${codex.path}`) + `
22983
+ Use $mh in Codex (restart Codex if it was already running).`);
22984
+ }
22985
+ return lines.join(`
22986
+ `);
22987
+ });
22593
22988
  return;
22594
22989
  }
22595
22990
  const db = openMetahub();
@@ -23756,23 +24151,54 @@ var read = defineCommand({
23756
24151
  }, () => body);
23757
24152
  })
23758
24153
  });
24154
+ function parseEditPairs(raw) {
24155
+ if (!Array.isArray(raw) || raw.length === 0)
24156
+ throw new MhError("invalid_input", "--edits must be a non-empty JSON array");
24157
+ return raw.map((e2, i2) => {
24158
+ if (typeof e2 !== "object" || e2 === null)
24159
+ throw new MhError("invalid_input", `--edits[${i2}] must be an object`);
24160
+ const { old, new: repl, replaceAll } = e2;
24161
+ if (typeof old !== "string" || old === "")
24162
+ throw new MhError("invalid_input", `--edits[${i2}].old must be a non-empty string`);
24163
+ if (repl !== undefined && typeof repl !== "string")
24164
+ throw new MhError("invalid_input", `--edits[${i2}].new must be a string`);
24165
+ if (replaceAll !== undefined && typeof replaceAll !== "boolean")
24166
+ throw new MhError("invalid_input", `--edits[${i2}].replaceAll must be a boolean`);
24167
+ return { old, new: repl, replaceAll };
24168
+ });
24169
+ }
23759
24170
  var edit = defineCommand({
23760
24171
  meta: {
23761
24172
  name: "edit",
23762
- description: "Anchored find/replace; --old must match exactly once unless --replace-all"
24173
+ description: "Anchored find/replace; --old must match exactly once unless --replace-all. --edits applies a JSON batch of pairs atomically"
23763
24174
  },
23764
24175
  args: {
23765
24176
  id: { type: "positional", required: true, description: "Document ref (id/prefix/title)" },
23766
- old: { type: "string", required: true, description: "Exact text to find (@file/@- ok)" },
24177
+ old: { type: "string", description: "Exact text to find (@file/@- ok)" },
23767
24178
  new: { type: "string", description: "Replacement text (@file/@- ok)" },
23768
24179
  "replace-all": { type: "boolean", description: "Replace every occurrence" },
24180
+ edits: {
24181
+ type: "string",
24182
+ description: 'Batch JSON: [{"old","new"?,"replaceAll"?}], applied in order (@file/@- ok)'
24183
+ },
23769
24184
  "if-match": { type: "string", description: "Version from `doc read`; reject if changed" }
23770
24185
  },
23771
24186
  run: guard(async (args) => {
23772
24187
  const db = openMetahub();
24188
+ const id = docId(db, args.id);
24189
+ if (args.edits != null) {
24190
+ if (args.old != null || args.new != null || args["replace-all"])
24191
+ throw new MhError("invalid_input", "use --old/--new/--replace-all or --edits, not both");
24192
+ const edits = parseEditPairs(await resolveJson(args.edits));
24193
+ const r4 = editDocumentBatch(db, id, { edits, ifMatch: args["if-match"] });
24194
+ print(r4, () => `edited ${r4.id} (${r4.replaced} replaced across ${edits.length} edit(s))`);
24195
+ return;
24196
+ }
24197
+ if (args.old == null)
24198
+ throw new MhError("invalid_input", "need --old or --edits");
23773
24199
  const oldText = await resolveValue2(args.old);
23774
24200
  const newText = await resolveValue2(args.new) ?? "";
23775
- const r3 = editDocument(db, docId(db, args.id), {
24201
+ const r3 = editDocument(db, id, {
23776
24202
  old: oldText,
23777
24203
  new: newText,
23778
24204
  replaceAll: args["replace-all"],
@@ -28356,56 +28782,196 @@ function wantsHtml(req) {
28356
28782
  return req.method === "GET" && (req.headers.get("accept") ?? "").includes("text/html");
28357
28783
  }
28358
28784
  var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
28785
+ 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"/>';
28786
+ 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"/>';
28787
+ 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"/>';
28788
+ var UNLOCK_CSS = `
28789
+ :root{color-scheme:light;
28790
+ --bg:#ffffff;--surface:#fbfbfa;--surface-2:#f1f1ef;--fg:#2c2c30;--fg-soft:#5b5b62;
28791
+ --muted:#75757f;--line:#ebebe8;--line-strong:#dededb;--accent:#4a55d6;--accent-fg:#ffffff;
28792
+ --accent-soft:#eef0ff;--danger:#d6473b;--danger-soft:#fdeceb;--code-bg:#f8f8f6}
28793
+ :root[data-resolved="dark"]{color-scheme:dark;
28794
+ --bg:#1a1a1c;--surface:#202022;--surface-2:#2a2a2d;--fg:#e6e6e9;--fg-soft:#b4b4bb;
28795
+ --muted:#8b8b95;--line:#2c2c30;--line-strong:#3a3a40;--accent:#7b86ff;--accent-fg:#14141a;
28796
+ --accent-soft:#23243a;--danger:#f87168;--danger-soft:#341e1c;--code-bg:#26262a}
28797
+ *{box-sizing:border-box}
28798
+ html,body{margin:0;min-height:100%}
28799
+ body{font-family:"Hanken Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
28800
+ color:var(--fg);background:var(--bg);line-height:1.55;
28801
+ display:flex;align-items:center;justify-content:center;min-height:100dvh;
28802
+ 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));
28803
+ -webkit-font-smoothing:antialiased;-webkit-tap-highlight-color:transparent;overflow:hidden}
28804
+ /* Ambient wash: two faint accent blooms behind the card. */
28805
+ body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
28806
+ background:
28807
+ radial-gradient(58vmax 58vmax at 14% -8%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 60%),
28808
+ radial-gradient(50vmax 50vmax at 108% 112%,color-mix(in srgb,var(--accent) 10%,transparent),transparent 55%)}
28809
+ .card{position:relative;z-index:1;width:100%;max-width:420px;background:var(--surface);
28810
+ border:1px solid var(--line);border-radius:18px;padding:32px 26px 26px;
28811
+ box-shadow:0 24px 60px rgba(20,20,40,.14),0 4px 14px rgba(20,20,40,.06);
28812
+ animation:rise .45s cubic-bezier(.2,.7,.3,1) both}
28813
+ @keyframes rise{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:none}}
28814
+ .brand{width:52px;height:52px;border-radius:14px;display:flex;align-items:center;justify-content:center;
28815
+ background:var(--accent-soft);color:var(--accent);margin-bottom:18px}
28816
+ .brand svg{width:28px;height:28px;fill:none;stroke:currentColor;stroke-width:1.6;
28817
+ stroke-linecap:round;stroke-linejoin:round}
28818
+ .brand path{stroke-dasharray:120;stroke-dashoffset:120;animation:draw .9s .12s ease forwards}
28819
+ @keyframes draw{to{stroke-dashoffset:0}}
28820
+ h1{font-size:21px;font-weight:650;letter-spacing:-.01em;margin:0 0 8px}
28821
+ .sub{color:var(--muted);font-size:13.5px;margin:0 0 22px;line-height:1.6}
28822
+ .sub code{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,monospace;font-size:12.5px;
28823
+ background:var(--code-bg);border:1px solid var(--line);border-radius:5px;padding:1px 6px;color:var(--fg-soft);
28824
+ white-space:nowrap}
28825
+ .field{position:relative;display:flex;align-items:center}
28826
+ input{width:100%;height:50px;padding:0 48px 0 15px;font-size:16px; /* 16px: never let iOS Safari zoom on focus */
28827
+ font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,monospace;letter-spacing:.01em;
28828
+ color:var(--fg);background:var(--bg);border:1.5px solid var(--line-strong);border-radius:12px;
28829
+ outline:none;transition:border-color .15s,box-shadow .15s}
28830
+ input::placeholder{font-family:var(--ui,inherit);letter-spacing:normal;color:var(--muted);opacity:.8}
28831
+ input:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
28832
+ .card.bad input{border-color:var(--danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--danger) 20%,transparent)}
28833
+ .card.bad .field{animation:shake .34s cubic-bezier(.36,.07,.19,.97)}
28834
+ @keyframes shake{10%,90%{transform:translateX(-1px)}20%,80%{transform:translateX(2px)}
28835
+ 30%,50%,70%{transform:translateX(-5px)}40%,60%{transform:translateX(5px)}}
28836
+ .peek{position:absolute;right:6px;width:40px;height:40px;border:0;background:none;cursor:pointer;
28837
+ color:var(--muted);display:flex;align-items:center;justify-content:center;border-radius:9px}
28838
+ .peek:hover{color:var(--fg-soft)}
28839
+ .peek svg{width:19px;height:19px;fill:none;stroke:currentColor;stroke-width:1.7;
28840
+ stroke-linecap:round;stroke-linejoin:round}
28841
+ .err{color:var(--danger);font-size:13px;font-weight:500;margin:11px 2px 0;
28842
+ display:flex;gap:6px;align-items:center;animation:fade .2s ease both}
28843
+ @keyframes fade{from{opacity:0}to{opacity:1}}
28844
+ button.go{width:100%;height:48px;margin-top:20px;border:0;border-radius:12px;cursor:pointer;
28845
+ background:var(--accent);color:var(--accent-fg);font-size:15px;font-weight:600;font-family:inherit;
28846
+ display:flex;align-items:center;justify-content:center;gap:9px;
28847
+ transition:filter .15s,transform .05s}
28848
+ button.go:hover{filter:brightness(1.06)}
28849
+ button.go:active{transform:translateY(1px)}
28850
+ button.go:disabled{opacity:.7;cursor:default}
28851
+ .spin{width:17px;height:17px;border:2px solid color-mix(in srgb,var(--accent-fg) 45%,transparent);
28852
+ border-top-color:var(--accent-fg);border-radius:50%;animation:spin .7s linear infinite}
28853
+ /* Full-page loader shown while a silent renewal is in flight. */
28854
+ .boot{position:fixed;inset:0;z-index:2;display:flex;align-items:center;justify-content:center;background:var(--bg)}
28855
+ .boot svg{width:40px;height:40px;fill:none;stroke:var(--accent);stroke-width:1.4;
28856
+ stroke-linecap:round;stroke-linejoin:round;animation:breathe 1.5s ease-in-out infinite}
28857
+ @keyframes breathe{0%,100%{opacity:.35;transform:scale(.94)}50%{opacity:1;transform:scale(1)}}
28858
+ @keyframes spin{to{transform:rotate(360deg)}}
28859
+ @media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important}
28860
+ .brand path{stroke-dashoffset:0}}
28861
+ `;
28359
28862
  function unlockPage() {
28360
- return `<!doctype html><html lang="en"><head>
28361
- <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
28362
- <title>Unlock</title>
28363
- <style>
28364
- body{font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
28365
- display:flex;min-height:100vh;margin:0;align-items:center;justify-content:center;
28366
- background:#0d1117;color:#e6edf3}
28367
- form{background:#161b22;border:1px solid #30363d;border-radius:10px;padding:28px;width:300px}
28368
- h1{font-size:16px;margin:0 0 14px}
28369
- input{width:100%;box-sizing:border-box;padding:8px;border-radius:6px;border:1px solid #30363d;
28370
- background:#0d1117;color:inherit;margin-bottom:12px}
28371
- button{width:100%;padding:8px;border:0;border-radius:6px;background:#238636;color:#fff;cursor:pointer}
28372
- .err{color:#f85149;margin:0 0 12px;font-size:13px}
28373
- </style></head><body>
28374
- <form id="f" style="display:none">
28375
- <h1>\uD83D\uDD12 This server requires a token</h1>
28376
- <p class="err" id="e" style="display:none">Token rejected \u2014 try again.</p>
28377
- <input id="t" type="password" placeholder="Token" autocomplete="current-password">
28378
- <button type="submit">Unlock</button>
28863
+ return `<!doctype html><html lang="zh-CN"><head>
28864
+ <meta charset="utf-8">
28865
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
28866
+ <meta name="theme-color" content="#ffffff">
28867
+ <title>\u9A8C\u8BC1\u8BBF\u95EE\u4EE4\u724C \xB7 metahub</title>
28868
+ <script>
28869
+ // FOUC-guard theme resolution, same key/logic as src/webui/theme.ts.
28870
+ try {
28871
+ var mt = localStorage.getItem("mh-theme");
28872
+ var dark = mt === "dark" || (mt !== "light" && matchMedia("(prefers-color-scheme: dark)").matches);
28873
+ document.documentElement.dataset.resolved = dark ? "dark" : "light";
28874
+ document.head.querySelector('meta[name=theme-color]').setAttribute("content", dark ? "#1a1a1c" : "#ffffff");
28875
+ } catch (e) {}
28876
+ </script>
28877
+ <style>${UNLOCK_CSS}</style></head><body>
28878
+ <div class="boot" id="boot"><svg viewBox="0 0 24 24">${CUBE_SVG}</svg></div>
28879
+ <form class="card" id="f" style="display:none" autocomplete="on">
28880
+ <div class="brand"><svg viewBox="0 0 24 24">${CUBE_SVG}</svg></div>
28881
+ <h1>\u8F93\u5165\u8BBF\u95EE\u4EE4\u724C</h1>
28882
+ <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>
28883
+ <div class="field">
28884
+ <input id="t" type="password" placeholder="\u7C98\u8D34\u8BBF\u95EE\u4EE4\u724C\u6216\u767B\u5F55\u94FE\u63A5"
28885
+ autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="go">
28886
+ <button type="button" class="peek" id="peek" aria-label="\u663E\u793A\u4EE4\u724C"><svg viewBox="0 0 24 24">${EYE_SVG}</svg></button>
28887
+ </div>
28888
+ <p class="err" id="e" style="display:none"></p>
28889
+ <button type="submit" class="go" id="go">\u89E3\u9501\u5E76\u8FDB\u5165</button>
28379
28890
  </form>
28380
28891
  <script>
28381
28892
  var KEY = "mh_token";
28893
+ var form = document.getElementById("f"), boot = document.getElementById("boot");
28894
+ var input = document.getElementById("t"), errEl = document.getElementById("e");
28895
+ var goBtn = document.getElementById("go"), peek = document.getElementById("peek");
28896
+
28382
28897
  function save(t) {
28383
28898
  localStorage.setItem(KEY, t);
28384
28899
  document.cookie = "${COOKIE}=" + encodeURIComponent(t) + "; ${COOKIE_ATTRS}" +
28385
28900
  (location.protocol === "https:" ? "; Secure" : "");
28386
28901
  }
28387
- function showForm() {
28388
- document.getElementById("f").style.display = "block";
28389
- // A leftover token means an attempt (or silent renewal) failed the gate.
28390
- if (localStorage.getItem(KEY)) document.getElementById("e").style.display = "block";
28391
- document.getElementById("t").focus();
28392
- }
28393
- document.getElementById("f").addEventListener("submit", function (ev) {
28902
+ // Accept a pasted "\u2026/?token=xxx" login link, not just the bare token.
28903
+ function normalize(v) {
28904
+ v = (v || "").trim();
28905
+ var m = /[?&#]token=([^&\\s]+)/.exec(v);
28906
+ if (m) { try { return decodeURIComponent(m[1]); } catch (e) { return m[1]; } }
28907
+ return v;
28908
+ }
28909
+ function showForm(msg) {
28910
+ boot.style.display = "none";
28911
+ form.style.display = "block";
28912
+ if (msg) { errEl.textContent = msg; errEl.style.display = "flex"; }
28913
+ input.focus();
28914
+ }
28915
+ function fail(msg) {
28916
+ goBtn.disabled = false;
28917
+ goBtn.innerHTML = "\u89E3\u9501\u5E76\u8FDB\u5165";
28918
+ errEl.textContent = msg;
28919
+ errEl.style.display = "flex";
28920
+ form.classList.remove("bad");
28921
+ void form.offsetWidth; // reflow so the shake animation restarts
28922
+ form.classList.add("bad");
28923
+ input.focus();
28924
+ input.select();
28925
+ }
28926
+ peek.addEventListener("click", function () {
28927
+ var pw = input.type === "password";
28928
+ input.type = pw ? "text" : "password";
28929
+ peek.innerHTML = '<svg viewBox="0 0 24 24">' + (pw ? ${JSON.stringify(EYE_OFF_SVG)} : ${JSON.stringify(EYE_SVG)}) + "</svg>";
28930
+ peek.setAttribute("aria-label", pw ? "\u9690\u85CF\u4EE4\u724C" : "\u663E\u793A\u4EE4\u724C");
28931
+ input.focus();
28932
+ });
28933
+ input.addEventListener("input", function () {
28934
+ if (errEl.style.display !== "none") { errEl.style.display = "none"; form.classList.remove("bad"); }
28935
+ });
28936
+
28937
+ form.addEventListener("submit", function (ev) {
28394
28938
  ev.preventDefault();
28395
- save(document.getElementById("t").value);
28396
- location.reload();
28939
+ var token = normalize(input.value);
28940
+ if (!token) { fail("\u8BF7\u8F93\u5165\u8BBF\u95EE\u4EE4\u724C\u3002"); return; }
28941
+ goBtn.disabled = true;
28942
+ goBtn.innerHTML = '<span class="spin"></span>\u9A8C\u8BC1\u4E2D\u2026';
28943
+ var hdr = { headers: { authorization: "Bearer " + token } };
28944
+ // Prefer the renewal endpoint (managed mode returns the canonical token,
28945
+ // also upgrading a pasted in-grace token). Fall back to a cheap
28946
+ // authenticated probe so we can reject a bad token *inline* instead of
28947
+ // storing it and reloading into another gate.
28948
+ fetch("${RENEW_PATH}", hdr)
28949
+ .then(function (r) { return r.ok ? r.json() : null; })
28950
+ .then(function (d) {
28951
+ if (d && d.token) { save(d.token); location.reload(); return; }
28952
+ return fetch("/api/version", hdr).then(function (r) {
28953
+ if (r.status === 401) { fail("\u4EE4\u724C\u65E0\u6548,\u8BF7\u68C0\u67E5\u540E\u91CD\u8BD5\u3002"); return; }
28954
+ // 200 (static-token mode) or no UI (404) \u2192 trust it and reload.
28955
+ save(token); location.reload();
28956
+ });
28957
+ })
28958
+ .catch(function () { save(token); location.reload(); }); // network hiccup \u2192 old behavior
28397
28959
  });
28398
- // Seamless renewal for top-level navigation: if we hold a token, try swapping
28399
- // it for the current one before prompting. Covers the case where the token
28400
- // rotated while this browser held the (in-grace) previous one.
28960
+
28961
+ // Silent renewal on load: a held token may just need swapping for the current
28962
+ // one (rotation while this browser held the in-grace previous). Show the boot
28963
+ // loader meanwhile; drop to the form (with a hint) if it can't be renewed.
28401
28964
  var stored = localStorage.getItem(KEY);
28402
28965
  if (stored) {
28403
28966
  fetch("${RENEW_PATH}", { headers: { authorization: "Bearer " + stored } })
28404
28967
  .then(function (r) { return r.ok ? r.json() : null; })
28405
- .then(function (d) { if (d && d.token) { save(d.token); location.reload(); } else { showForm(); } })
28406
- .catch(showForm);
28968
+ .then(function (d) {
28969
+ if (d && d.token) { save(d.token); location.reload(); }
28970
+ else showForm("\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u8F93\u5165\u4EE4\u724C\u3002");
28971
+ })
28972
+ .catch(function () { showForm(""); });
28407
28973
  } else {
28408
- showForm();
28974
+ showForm("");
28409
28975
  }
28410
28976
  </script>
28411
28977
  </body></html>`;
@@ -28681,6 +29247,11 @@ var styles_default = ` :root {
28681
29247
  svg { display: block; }
28682
29248
  .ico { width:16px; height:16px; stroke:currentColor; fill:none; stroke-width:1.75; stroke-linecap:round; stroke-linejoin:round; flex:none; }
28683
29249
  .ico.sm { width:14px; height:14px; }
29250
+ /* Every window (incl. quicknote) gets the same thin translucent scrollbar:
29251
+ transparent track, rounded translucent thumb. In Chromium 121+ the
29252
+ inherited html scrollbar-width/color renders it; the ::-webkit rules
29253
+ cover Safari. Both force classic (layout-consuming) mode \u2014 tight layouts
29254
+ pair scrollbar-gutter:stable with rebalanced insets (see .qn-body). */
28684
29255
  ::-webkit-scrollbar { width:10px; height:10px; }
28685
29256
  ::-webkit-scrollbar-thumb { background:var(--line-strong); border-radius:8px; border:3px solid transparent; background-clip:padding-box; }
28686
29257
  ::-webkit-scrollbar-thumb:hover { background:var(--muted); background-clip:padding-box; border:3px solid transparent; }
@@ -28729,12 +29300,45 @@ var styles_default = ` :root {
28729
29300
  .sb-search input { border:0; background:none; outline:none; color:var(--fg); width:100%; }
28730
29301
  .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; }
28731
29302
  .sb-scroll { flex:1; overflow-y:auto; padding:4px 8px 24px; }
28732
- .sb-section { margin-top:12px; }
28733
- .sb-section-head { display:flex; align-items:center; justify-content:space-between;
28734
- font-size:var(--fs-micro); font-weight:600; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); padding:4px 8px; }
28735
- .sb-section-head .add { opacity:0; padding:3px; border-radius:5px; color:var(--muted); transition:opacity .12s; }
28736
- .sb-section:hover .add { opacity:1; }
28737
- .sb-section-head .add:hover { background:var(--hover-2); color:var(--fg); }
29303
+ /* Notion-style list switcher: the active tab is an icon+label pill, inactive
29304
+ tabs are icon-only. One absolutely-positioned indicator slides & stretches
29305
+ under the active button; JS keeps it in sync via ResizeObserver so it
29306
+ chases the label's 0fr\u21921fr expansion instead of jumping to a stale width. */
29307
+ .sb-tabs { position:relative; display:flex; align-items:center; gap:2px; margin:2px 12px 8px; }
29308
+ .sb-tab { position:relative; z-index:1; display:flex; align-items:center;
29309
+ padding:5px 7px; border-radius:var(--radius-sm); color:var(--muted); cursor:pointer;
29310
+ font-size:var(--fs-ui); transition:color .18s; }
29311
+ .sb-tab:hover { color:var(--fg-soft); }
29312
+ .sb-tab.on { color:var(--fg); font-weight:600; }
29313
+ /* Label expansion: animating the grid track times correctly for any label
29314
+ width (max-width can't); the inner span clips during the ride. The
29315
+ icon-text gap rides the same curve so collapsing leaves no seam. */
29316
+ .sb-tab .tab-label { display:grid; grid-template-columns:0fr; opacity:0;
29317
+ transition:grid-template-columns .24s cubic-bezier(.22,1,.36,1), opacity .18s ease; }
29318
+ .sb-tab .tab-label > span { overflow:hidden; white-space:nowrap; min-width:0;
29319
+ transition:padding-left .24s cubic-bezier(.22,1,.36,1); }
29320
+ .sb-tab.on .tab-label { grid-template-columns:1fr; opacity:1; }
29321
+ .sb-tab.on .tab-label > span { padding-left:6px; }
29322
+ .sb-tab-ind { position:absolute; left:0; top:0; bottom:0; z-index:0; width:0;
29323
+ border-radius:var(--radius-sm); background:var(--hover-2);
29324
+ transition:transform .24s cubic-bezier(.22,1,.36,1), width .24s cubic-bezier(.22,1,.36,1); }
29325
+ .sb-tabs .add { margin-left:auto; padding:3px; border-radius:5px; color:var(--muted); }
29326
+ .sb-tabs .add:hover { background:var(--hover-2); color:var(--fg); }
29327
+ /* Pane swap: the entering list slides in from the side its tab lives on. */
29328
+ .sb-pane { margin-top:4px; }
29329
+ .sb-pane.in-r { animation:sb-pane-r .2s cubic-bezier(.22,1,.36,1); }
29330
+ .sb-pane.in-l { animation:sb-pane-l .2s cubic-bezier(.22,1,.36,1); }
29331
+ @keyframes sb-pane-r { from { opacity:0; transform:translateX(14px); } }
29332
+ @keyframes sb-pane-l { from { opacity:0; transform:translateX(-14px); } }
29333
+ .sb-subfold .chev { transition:transform .12s; }
29334
+ .sb-subfold .chev.open { transform:rotate(90deg); }
29335
+ /* Collapsed-databases group: a quiet tail row inside the \u6570\u636E\u8868 list for
29336
+ tables flagged meta.collapsed (site-facing / rarely browsed). */
29337
+ .sb-subfold { display:flex; align-items:center; gap:2px; width:100%; text-align:left;
29338
+ padding:4px 6px; margin-top:2px; border-radius:var(--radius-sm);
29339
+ color:var(--muted); font-size:var(--fs-micro); cursor:pointer; }
29340
+ .sb-subfold:hover { background:var(--hover-2); color:var(--fg-soft); }
29341
+ .navitem.dim { opacity:.62; }
28738
29342
  .navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; margin-bottom:2px; border-radius:var(--radius-sm);
28739
29343
  color:var(--fg-soft); font-size:var(--fs-ui); position:relative; white-space:nowrap; transition:background .1s; }
28740
29344
  .navitem:hover { background:var(--hover-2); }
@@ -28824,6 +29428,23 @@ var styles_default = ` :root {
28824
29428
  .set-block-title { font-size:14px; font-weight:650; color:var(--fg); letter-spacing:-.005em; }
28825
29429
  .set-block-desc { color:var(--fg-soft); font-size:var(--fs-sm); line-height:1.55; margin-bottom:14px; max-width:56ch; }
28826
29430
  .set-block-desc:last-child { margin-bottom:0; }
29431
+ /* title+desc on the left, a control (e.g. a switch) pinned right \u2014 used for
29432
+ single boolean settings where a whole card of chrome would be overkill */
29433
+ .set-toggle-row { display:flex; align-items:center; justify-content:space-between; gap:24px; }
29434
+ .set-toggle-text { min-width:0; }
29435
+ .set-toggle-text .set-block-head { margin-bottom:4px; }
29436
+ .set-toggle-text .set-block-desc { margin-bottom:0; }
29437
+ /* iOS-style switch: a hidden checkbox drives a sliding track+thumb */
29438
+ .set-switch { position:relative; flex:none; display:inline-flex; align-items:center; cursor:pointer; }
29439
+ .set-switch input { position:absolute; width:1px; height:1px; opacity:0; margin:0; }
29440
+ .set-switch-track { display:inline-flex; align-items:center; width:40px; height:23px; padding:0 2px;
29441
+ border-radius:999px; background:var(--line-strong); transition:background .2s ease; }
29442
+ .set-switch-thumb { width:19px; height:19px; border-radius:50%; background:#fff;
29443
+ box-shadow:0 1px 3px rgba(20,20,40,.28); transition:transform .2s cubic-bezier(.4,0,.2,1); }
29444
+ .set-switch input:checked + .set-switch-track { background:var(--accent); }
29445
+ .set-switch input:checked + .set-switch-track .set-switch-thumb { transform:translateX(17px); }
29446
+ .set-switch:hover .set-switch-track { filter:brightness(.97); }
29447
+ .set-switch input:focus-visible + .set-switch-track { outline:2px solid var(--accent); outline-offset:2px; }
28827
29448
  /* two-fact meta line under a block title (who publishes \xB7 where creds live) \u2014
28828
29449
  answers the questions hiding under "is this bucket for server or browser?" */
28829
29450
  .set-meta { display:flex; flex-wrap:wrap; gap:8px 16px; margin:0 0 12px; }
@@ -29023,10 +29644,19 @@ var styles_default = ` :root {
29023
29644
  .sidebar.collapsed { overflow:hidden; }
29024
29645
  .sidebar.collapsed .sb-resizer { pointer-events:none; }
29025
29646
  .crumb { display:flex; align-items:center; gap:7px; flex:1; min-width:0; font-size:var(--fs-ui); color:var(--fg); font-weight:500; }
29026
- .crumb .emoji { font-size:15px; } .crumb .sub { color:var(--muted); font-weight:400; }
29647
+ /* Long titles ellipsize on one line \u2014 never wrap and grow the topbar. */
29648
+ .crumb > span:not(.emoji) { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
29649
+ .crumb .emoji { font-size:15px; flex:none; } .crumb .sub { color:var(--muted); font-weight:400; }
29027
29650
  .iconbtn { width:30px; height:30px; display:grid; place-items:center; border-radius:var(--radius-sm); color:var(--fg-soft); transition:background .12s; }
29028
29651
  .iconbtn:hover { background:var(--hover-2); color:var(--fg); }
29029
- .content { flex:1; overflow:auto; }
29652
+ /* Reserve the classic-scrollbar gutter up front: view width never changes
29653
+ when content grows past one screen \u2014 no jump / rewrap flicker. stable
29654
+ ONLY (never both-edges): a left-edge gutter desyncs Chromium's composited
29655
+ scroll cull rect by the gutter width, leaving a permanent unpainted
29656
+ stripe over the rightmost 15px of the scrollport (content reaching into
29657
+ it gets visibly sliced \u2014 first seen slicing the quicknote file card).
29658
+ Inert on mobile, where .content becomes overflow:visible. */
29659
+ .content { flex:1; overflow:auto; scrollbar-gutter:stable; }
29030
29660
  .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; }
29031
29661
  .btn-ghost { color:var(--fg-soft); } .btn-ghost:hover { background:var(--hover-2); color:var(--fg); }
29032
29662
  .btn-secondary { background:var(--bg); border:1px solid var(--line-strong); color:var(--fg); box-shadow:var(--shadow-sm); }
@@ -29091,9 +29721,9 @@ var styles_default = ` :root {
29091
29721
  .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; }
29092
29722
  .doc-conflict-msg { color:var(--danger); font-weight:600; flex:1; min-width:180px; }
29093
29723
  .doc.source-mode { min-height:100%; }
29094
- .doc-source { display:block; width:100%; min-height:calc(100vh - 210px); min-height:calc(100dvh - 210px); resize:none; border:0;
29095
- background:transparent; color:var(--fg); outline:none; overflow:hidden; padding:3px 2px 36vh;
29096
- font-family:var(--ui); font-size:15px; line-height:1.7; tab-size:2; }
29724
+ /* wide mode (\u22EF menu): lift the column cap; the 60px side padding stays as the
29725
+ reading gutter and keeps the fixed .doc-toc ticks off the text. */
29726
+ .doc.wide-mode { max-width:none; }
29097
29727
  /* floating table of contents: a column of indented tick marks in the right
29098
29728
  gutter that expands to a labelled list on hover (Notion / Vercel-docs style).
29099
29729
  Fixed to the viewport so it never disturbs the centred .doc column. */
@@ -29132,49 +29762,33 @@ var styles_default = ` :root {
29132
29762
  /* not enough room for the gutter \u2192 hide entirely (also covers mobile) */
29133
29763
  @media (max-width:1100px) { .doc-toc { display:none; } }
29134
29764
 
29135
- .block-wrap { position:relative; }
29136
- .block-wrap.nested { padding-left:24px; }
29137
- /* Subtle indent guide only for true sub-lists; suppressed for code/other
29138
- nested content so code-in-list items don't stack a thicket of lines. */
29139
- .block-wrap.nested:has(> .block.b-bullet, > .block.b-numbered, > .block.b-todo)::before {
29140
- content:""; position:absolute; left:8px; top:2px; bottom:2px; width:1px; background:var(--line); }
29141
- .block { position:relative; display:flex; align-items:flex-start; padding:1px 0; border-radius:4px; scroll-margin-top:60px; }
29142
- .block .gutter { position:absolute; left:-52px; top:2px; display:flex; gap:1px; opacity:0; transition:opacity .12s; }
29143
- .block:hover .gutter, .block.drop-before .gutter, .block.drop-after .gutter, .block.dragging .gutter { opacity:1; }
29144
- .gutter button { width:22px; height:24px; display:grid; place-items:center; color:var(--muted); border-radius:5px; }
29145
- .gutter button:hover { background:var(--hover-2); color:var(--fg); }
29146
- .gutter .grip { cursor:grab; } .gutter .grip:active { cursor:grabbing; }
29147
- .block.dragging { opacity:.42; background:var(--hover); }
29148
- .block.drop-before::before, .block.drop-after::after { content:""; position:absolute; left:0; right:0; height:2px; background:var(--accent);
29149
- border-radius:2px; pointer-events:none; box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 18%,transparent); }
29150
- .block.drop-before::before { top:-1px; }
29151
- .block.drop-after::after { bottom:-1px; }
29152
- /* multi-block selection: whole-block tint + suppress stray native text highlight while dragging */
29153
- .block.selected { background:var(--accent-soft); }
29154
- .block.selected .editable::selection, .block.selected .editable *::selection { background:transparent; }
29155
- .doc.selecting, .doc.selecting * { user-select:none; -webkit-user-select:none; cursor:default; }
29156
- .block.list-code-host { height:0; min-height:0; padding:0; overflow:visible; }
29157
- .block-wrap:hover > .block.list-code-host .gutter { opacity:1; }
29158
- .block.list-code-host .marker { position:absolute; left:0; top:0; padding-top:4px; z-index:1; }
29159
- /* The list item's host gutter controls the whole unit, so suppress the nested
29160
- code block's own gutter (it would overlap the list marker on the same row). */
29161
- .block.list-code-host + .block-wrap.nested .gutter { display:none; }
29162
- .editable { outline:none; flex:1; min-width:0; padding:3px 2px; line-height:var(--note-lh); }
29163
- .editable:empty::before { content:attr(data-ph); color:var(--muted); pointer-events:none; }
29164
- /* The generic "/" prompt only shows on the focused line; idle blank lines (paragraphs and list items, any depth) stay blank. */
29165
- .editable[data-ph-hint="slash"]:empty:not(:focus)::before { content:""; }
29166
- .b-h1 .editable { font-size:28px; font-weight:700; letter-spacing:-.02em; padding-top:12px; }
29167
- .b-h2 .editable { font-size:22px; font-weight:650; letter-spacing:-.01em; padding-top:8px; }
29168
- .b-h3 .editable { font-size:18px; font-weight:600; padding-top:4px; }
29169
- .b-h4 .editable { font-size:16px; font-weight:600; padding-top:2px; }
29170
- .b-h5 .editable { font-size:14px; font-weight:600; }
29171
- .b-h6 .editable { font-size:13px; font-weight:600; color:var(--fg-soft); }
29172
- .b-h1 .gutter { top:22px; } .b-h2 .gutter { top:14px; } .b-h3 .gutter { top:7px; }
29173
- .b-h4 .gutter { top:4px; } .b-h5 .gutter { top:3px; } .b-h6 .gutter { top:2px; }
29174
- .b-quote .editable { border-left:3px solid var(--line-strong); padding:1px 0 1px 16px; color:var(--fg-soft); font-style:italic; }
29765
+ /* floating word count: a small pill in the bottom-right gutter (fixed to the
29766
+ viewport, aligned with .doc-toc's right edge). Collapsed it hugs the \u5B57\u6570;
29767
+ hover expands the whole pill (square-off + surface bg) and reveals the char
29768
+ count + reading estimate. Centre-aligned throughout, and the collapsed detail
29769
+ is width:0 so its wider text can't stretch the pill. Purely client-side. */
29770
+ .doc-wc { position:fixed; right:18px; bottom:calc(16px + env(safe-area-inset-bottom)); z-index:40;
29771
+ display:flex; flex-direction:column; align-items:center; justify-content:center;
29772
+ padding:4px 12px; border:1px solid var(--line); border-radius:999px; background:var(--bg);
29773
+ color:var(--muted); font-size:var(--fs-sm); line-height:1.2; user-select:none;
29774
+ box-shadow:var(--shadow-sm); opacity:.66;
29775
+ transition:opacity .18s ease, border-radius .2s ease, background .18s ease, box-shadow .2s ease, color .18s ease; }
29776
+ .doc-wc:hover { opacity:1; border-radius:var(--radius); background:var(--surface); border-color:var(--line-strong);
29777
+ box-shadow:var(--shadow-md); color:var(--fg-soft); }
29778
+ .doc-wc-num { font-variant-numeric:tabular-nums; white-space:nowrap; }
29779
+ .doc-wc.sel .doc-wc-num { color:var(--accent); font-weight:550; }
29780
+ /* detail rows: collapsed to zero size until hover. width:0 keeps them from
29781
+ widening the pill; no container gap (a phantom gap would push the collapsed
29782
+ count off-centre) \u2014 the spacing comes from margin-top only when expanded. */
29783
+ .doc-wc-detail { display:flex; flex-direction:column; align-items:center; gap:2px;
29784
+ width:0; max-height:0; margin-top:0; overflow:hidden; opacity:0; font-size:12px; color:var(--muted);
29785
+ transition:max-height .2s ease, opacity .18s ease, margin-top .2s ease; }
29786
+ .doc-wc-detail span { font-variant-numeric:tabular-nums; white-space:nowrap; }
29787
+ .doc-wc:hover .doc-wc-detail { width:auto; max-height:40px; margin-top:4px; opacity:1; }
29788
+ /* quick-note mini window has no room for it */
29789
+ .qn-body .doc-wc { display:none; }
29175
29790
 
29176
29791
  /* code block: transparent textarea over a highlight.js mirror */
29177
- .b-code .codeblock { flex:1; min-width:0; }
29178
29792
  .codeblock { position:relative; --code-fs:12.5px; --code-lh:1.55; --code-pad:8px; margin:2px 0;
29179
29793
  background:var(--code-bg); border:1px solid var(--line); border-radius:8px; overflow:hidden; box-shadow:var(--shadow-sm); }
29180
29794
  /* lang + copy float bottom-right, revealed on hover */
@@ -29183,27 +29797,57 @@ var styles_default = ` :root {
29183
29797
  background:color-mix(in srgb,var(--code-bg) 82%,transparent); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
29184
29798
  opacity:0; transform:translateY(3px); pointer-events:none; transition:opacity .12s,transform .12s; }
29185
29799
  .codeblock:hover .code-tools, .code-tools:focus-within { opacity:1; transform:none; pointer-events:auto; }
29800
+ /* hidden while editing (textarea focused) or whole-block selected \u2014 the pill would cover the
29801
+ bottom-right of the editing area; keep it if focus is on the pill itself (lang dropdown open) */
29802
+ .cm-void-code:focus-within .code-tools:not(:focus-within),
29803
+ .codeblock.ci-selected .code-tools:not(:focus-within) { opacity:0; transform:translateY(3px); pointer-events:none; }
29186
29804
  .code-lang { position:relative; display:inline-flex; align-items:center; }
29187
29805
  .code-lang select { appearance:none; -webkit-appearance:none; border:0; outline:none; background:none; cursor:pointer;
29188
29806
  font-family:var(--mono); font-size:10.5px; font-weight:500; color:var(--fg-soft); padding:3px 17px 3px 7px; border-radius:5px; }
29189
29807
  .code-lang select:hover { background:var(--hover-2); }
29190
29808
  .code-lang svg { position:absolute; right:3px; width:12px; height:12px; pointer-events:none; color:var(--muted); }
29191
- .code-copy { display:inline-flex; align-items:center; gap:4px; font-size:10.5px; font-weight:500; color:var(--muted);
29192
- padding:3px 7px; border-radius:5px; transition:background .12s,color .12s; }
29193
- .code-copy:hover { background:var(--hover-2); color:var(--fg-soft); }
29194
- .code-copy.ok { color:var(--accent); }
29195
- .code-body { display:flex; align-items:stretch; font-family:var(--mono); font-size:var(--code-fs); line-height:var(--code-lh); }
29809
+ .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);
29810
+ padding:3px 7px; border-radius:5px; transition:background .12s,color .12s; white-space:nowrap; }
29811
+ .code-copy:hover, .code-fmt:hover, .code-wrap-btn:hover { background:var(--hover-2); color:var(--fg-soft); }
29812
+ .code-copy.ok, .code-fmt.ok, .code-wrap-btn.on { color:var(--accent); }
29813
+ .code-fmt.err { color:var(--danger); }
29814
+ /* Default (no wrap): lines run long, the textarea scrolls horizontally and
29815
+ mirrors its scrollLeft onto the highlight layer; the gutter is a separate
29816
+ flex column of "1\\n2\\n3" (every logical line is exactly one row high). */
29817
+ .code-body { display:flex; align-items:stretch; font-family:var(--mono); font-size:var(--code-fs); line-height:var(--code-lh);
29818
+ --code-ln-ch:1ch; --code-gutter-w:calc(var(--code-ln-ch) + 20px); }
29196
29819
  .code-gutter { flex:none; text-align:right; white-space:pre; user-select:none; color:var(--muted); opacity:.55;
29197
29820
  padding:var(--code-pad) 8px var(--code-pad) 12px; border-right:1px solid var(--line); font-variant-numeric:tabular-nums; }
29198
29821
  .code-scroll { position:relative; flex:1; min-width:0; }
29199
29822
  .code-hl, .code-input { margin:0; font-family:var(--mono); font-size:var(--code-fs); line-height:var(--code-lh);
29200
29823
  padding:var(--code-pad) 16px; tab-size:2; -moz-tab-size:2; white-space:pre; letter-spacing:0; }
29201
29824
  .code-hl { position:absolute; inset:0; z-index:0; overflow:hidden; pointer-events:none; background:none; color:var(--fg); }
29202
- .code-hl code { display:block; font:inherit; background:none; padding:0; }
29825
+ .code-hl code { display:block; font:inherit; background:none; padding:0; counter-reset:code-ln; }
29826
+ .code-hl-line { counter-increment:code-ln;
29827
+ min-height:calc(var(--code-fs) * var(--code-lh)); /* empty logical line still occupies its row */ }
29203
29828
  .code-input { display:block; position:relative; z-index:1; width:100%; border:0; outline:none; resize:none;
29204
29829
  overflow-x:auto; overflow-y:hidden; background:transparent; color:transparent; caret-color:var(--fg);
29205
29830
  min-height:calc(var(--code-fs) * var(--code-lh) + var(--code-pad) * 2); }
29206
29831
  .code-input::placeholder { color:var(--muted); }
29832
+ /* Wrap mode (.wrap, global toggle in the tools pill, persisted): textarea and
29833
+ mirror share pre-wrap + break-word (the UA textarea wrap rules), so their
29834
+ wrap points match and the caret stays on the glyphs. The flex gutter would
29835
+ misalign (a logical line can span several visual rows), so it hides and
29836
+ line numbers become CSS counters on the mirror's per-logical-line blocks \u2014
29837
+ a wrapped line grows its block and the next number moves with it, no JS
29838
+ height sync. --code-ln-ch (digit count) is set by paint(). */
29839
+ .code-body.wrap { display:block; position:relative; }
29840
+ .code-body.wrap::before { content:""; position:absolute; left:var(--code-gutter-w); top:0; bottom:0;
29841
+ width:1px; background:var(--line); }
29842
+ .code-body.wrap .code-gutter { display:none; }
29843
+ .code-body.wrap .code-hl, .code-body.wrap .code-input {
29844
+ padding-left:calc(var(--code-gutter-w) + 17px);
29845
+ white-space:pre-wrap; overflow-wrap:break-word; }
29846
+ .code-body.wrap .code-input { overflow:hidden; }
29847
+ .code-body.wrap .code-hl-line { position:relative; }
29848
+ .code-body.wrap .code-hl-line::before { content:counter(code-ln); position:absolute; top:0;
29849
+ left:calc(-17px - var(--code-gutter-w)); width:calc(var(--code-gutter-w) - 8px);
29850
+ text-align:right; color:var(--muted); opacity:.55; font-variant-numeric:tabular-nums; }
29207
29851
 
29208
29852
  /* highlight.js token theme (cohesive with the app palette) */
29209
29853
  .hljs-comment, .hljs-quote { color:var(--hl-cmt); font-style:italic; }
@@ -29217,24 +29861,50 @@ var styles_default = ` :root {
29217
29861
  .hljs-params, .hljs-subst { color:var(--fg); }
29218
29862
  .hljs-emphasis { font-style:italic; }
29219
29863
  .hljs-strong { font-weight:700; }
29220
- .marker { width:24px; flex:none; text-align:center; color:var(--fg-soft); user-select:none; padding-top:4px; }
29221
- .b-todo .marker { box-sizing:content-box; padding-top:3px; height:calc(1em * var(--note-lh));
29222
- display:flex; align-items:center; justify-content:center; }
29223
- .b-todo .marker input { width:15px; height:15px; accent-color:var(--accent); cursor:pointer; }
29224
- .b-divider { padding:10px 0; }
29225
- .b-divider hr { border:0; border-top:1px solid var(--line-strong); margin:0; width:100%; }
29226
- .b-done .editable { color:var(--muted); text-decoration:line-through; }
29227
- .editable b, .editable strong { font-weight:700; }
29228
- .editable code { font-family:var(--mono); background:var(--surface-2); padding:1px 5px; border-radius:4px; font-size:.9em; }
29229
- .editable a { color:var(--accent); }
29230
- /* document images (pasted/dropped \u2192 /blob/<hash>); fetched on demand */
29231
- .editable img.doc-img { max-width:100%; height:auto; border-radius:8px; margin:6px 0; display:block; vertical-align:bottom; }
29232
29864
 
29233
- /* ---- block-level void embeds: image / video / audio / file / html ---- */
29234
- .void-host { flex:1; min-width:0; outline:none; padding:2px 0; }
29235
- .block.selected:has(.void-host) { background:transparent; }
29236
- .block.selected .void-block, .void-host:focus-visible .void-block { box-shadow:0 0 0 2px var(--accent); }
29865
+ /* CodeIsland (CM6 void widget): the tools pill (lang + copy) is the only
29866
+ focus-state chrome \u2014 an absolute overlay, so revealing it never changes the
29867
+ block's height (no layout shift on click). .ci-selected marks the island
29868
+ when the CM selection spans the whole void (Escape from inside / Backspace
29869
+ from the line below). */
29870
+ .cm-void-code .codeblock.ci-selected { box-shadow:0 0 0 2px var(--accent); }
29871
+
29872
+ /* Whole-void selected (the CM selection spans the block \u2014 click-to-select on
29873
+ media, Backspace-from-below, select-all): 2px accent ring, the old editor's
29874
+ \`.block.selected .void-block\` treatment. Code draws its own via .ci-selected. */
29875
+ /* Intrinsic-width firewall (same problem the doc table solved with
29876
+ contain:inline-size on .doc-table-wrap): a void widget's max-content \u2014
29877
+ e.g. a file card whose nowrap filename caps at 480px \u2014 must not propagate
29878
+ into .cm-content and push the whole editor wider than a narrow window
29879
+ (quick notes), shoving the card's right edge offscreen. width:0 zeroes the
29880
+ host's intrinsic contribution; min-width:100% restores the line width at
29881
+ layout time; the card's own min-width:0 + ellipsis then get to run.
29882
+ NOT contain:inline-size: Chromium 130's ink-overflow culling clips a
29883
+ size-contained host's paint at a stale boundary on 2x displays \u2014 the
29884
+ card's right border and selection ring were visibly sliced off. */
29885
+ .cm-void { box-sizing:border-box; width:0; min-width:100%; }
29886
+ .cm-void.selected .void-block { box-shadow:0 0 0 2px var(--accent); }
29887
+ .cm-void.selected table.doc-table { box-shadow:0 0 0 2px var(--accent), var(--shadow-sm); }
29888
+
29889
+ /* Editor text selection: one accent tint for BOTH the native ::selection and
29890
+ .cm-mk-sel, the class block-deco puts on marker widgets (bullet / ordered
29891
+ number / checkbox) whose marker range the selection covers. Native selection
29892
+ cannot paint replaced widgets, which left holes in the marker column during
29893
+ drag-select / select-all; unifying the two colors makes the line read as one
29894
+ continuous highlight (and tracks the theme in dark mode). */
29895
+ .cm-content ::selection { background: color-mix(in srgb, var(--accent) 24%, transparent); }
29896
+ .cm-content .cm-mk-sel { background: color-mix(in srgb, var(--accent) 24%, transparent); }
29897
+ /* bullet/number boxes are 24px + 2px margin-right; the margin can't take a
29898
+ background, so a same-color shadow fills the seam up to the content column */
29899
+ .cm-content .cm-bullet.cm-mk-sel, .cm-content .cm-ol-num.cm-mk-sel {
29900
+ box-shadow: 2px 0 0 color-mix(in srgb, var(--accent) 24%, transparent);
29901
+ }
29902
+ /* the glyph/number text inside a painted marker also sits in the DOM selection;
29903
+ without this it gets the ::selection tint ON TOP of the span background and
29904
+ reads darker than the rest of the line */
29905
+ .cm-content .cm-mk-sel::selection, .cm-content .cm-mk-sel ::selection { background: transparent; }
29237
29906
 
29907
+ /* ---- block-level void embeds: image / video / audio / file / html ---- */
29238
29908
  .void-image { position:relative; display:inline-block; max-width:100%; border-radius:8px; line-height:0; }
29239
29909
  .void-image img { display:block; width:100%; max-width:100%; height:auto; border-radius:8px; user-select:none; }
29240
29910
  .void-image .void-expand { position:absolute; top:8px; right:8px; width:28px; height:28px; display:grid; place-items:center;
@@ -29272,7 +29942,6 @@ var styles_default = ` :root {
29272
29942
  color:var(--fg-soft); font-size:var(--fs-micro); transition:background .12s; }
29273
29943
  .void-html .html-toggle:hover { background:var(--hover-2); color:var(--fg); }
29274
29944
  .void-html .html-toggle.on { background:var(--accent-soft); color:var(--accent); }
29275
- .void-html .html-source { width:100%; min-height:120px; padding:12px; border:0; background:var(--bg); resize:vertical; }
29276
29945
  .void-html .html-frame { display:block; width:100%; border:0; background:var(--bg); }
29277
29946
  .void-html .html-empty { padding:24px; text-align:center; color:var(--muted); cursor:text; font-size:var(--fs-ui); }
29278
29947
 
@@ -29283,30 +29952,47 @@ var styles_default = ` :root {
29283
29952
  .lightbox-toolbar { flex:none; display:flex; align-items:center; gap:6px; padding:10px 14px; color:#fff; }
29284
29953
  .lightbox-toolbar .lightbox-name { font-size:var(--fs-ui); opacity:.85; max-width:40vw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
29285
29954
  .lightbox-toolbar .lightbox-spacer { flex:1; }
29286
- .lightbox-toolbar button { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; color:#fff;
29955
+ .lightbox-toolbar > button { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; color:#fff;
29287
29956
  background:rgba(255,255,255,.08); transition:background .12s; }
29288
- .lightbox-toolbar button:hover { background:rgba(255,255,255,.2); }
29957
+ .lightbox-toolbar > button:hover { background:rgba(255,255,255,.2); }
29289
29958
  .lightbox-toolbar .lightbox-pct { width:auto; min-width:52px; font-size:var(--fs-micro); font-variant-numeric:tabular-nums; }
29959
+ .lightbox-toolbar .ico { width:16px; height:16px; stroke-width:2; }
29290
29960
  .lightbox-stage { flex:1; min-height:0; overflow:hidden; display:grid; place-items:center; }
29291
29961
  .lightbox-stage img { max-width:94vw; max-height:100%; will-change:transform; -webkit-user-drag:none; user-select:none; }
29292
29962
 
29293
29963
  /* ---- image annotator ---- */
29294
29964
  .annot { flex:1; min-height:0; display:flex; flex-direction:column; }
29295
- .annot-toolbar { flex:none; display:flex; flex-wrap:wrap; align-items:center; gap:14px; padding:8px 14px; }
29296
- .annot-tools, .annot-colors, .annot-actions { display:flex; align-items:center; gap:6px; }
29297
- .annot-actions { margin-left:auto; }
29298
- .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; }
29299
- .annot-tool:hover { background:rgba(255,255,255,.2); }
29300
- .annot-tool.on { background:var(--accent); color:var(--accent-fg); }
29301
- .annot-tool:disabled { opacity:.4; cursor:default; }
29302
- .annot-swatch { width:22px; height:22px; border-radius:50%; border:2px solid rgba(255,255,255,.5); transition:transform .1s; }
29303
- .annot-swatch.on { transform:scale(1.18); border-color:#fff; box-shadow:0 0 0 2px var(--accent); }
29965
+ /* Single-row chrome: filename \xB7 segmented tools \xB7 colors \xB7 undo/redo \xB7
29966
+ cancel/save \xB7 close. Wraps only as a narrow-window fallback. */
29967
+ .annot-bar { flex-wrap:wrap; row-gap:6px; }
29968
+ .annot-seg { display:flex; align-items:center; gap:2px; padding:2px; border-radius:9px; background:rgba(255,255,255,.1); }
29969
+ .annot-seg-btn { width:30px; height:26px; display:grid; place-items:center; border-radius:7px;
29970
+ color:rgba(255,255,255,.72); transition:background .12s, color .12s; }
29971
+ .annot-seg-btn:hover { color:#fff; background:rgba(255,255,255,.12); }
29972
+ .annot-seg-btn.on { background:#fff; color:#1c1c26; box-shadow:0 1px 2px rgba(0,0,0,.3); }
29973
+ .annot-div { flex:none; width:1px; height:18px; margin:0 5px; background:rgba(255,255,255,.18); }
29974
+ .annot-colors { display:flex; align-items:center; gap:8px; padding:0 2px; }
29975
+ .annot-swatch { width:15px; height:15px; border-radius:50%; box-shadow:inset 0 0 0 1px rgba(255,255,255,.3);
29976
+ transition:transform .12s, box-shadow .12s; }
29977
+ .annot-swatch:hover { transform:scale(1.18); }
29978
+ .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); }
29979
+ .annot-bar .annot-icon-btn { width:30px; height:30px; display:grid; place-items:center; border-radius:7px;
29980
+ color:rgba(255,255,255,.85); background:none; transition:background .12s; }
29981
+ .annot-bar .annot-icon-btn:hover { background:rgba(255,255,255,.14); }
29982
+ .annot-bar .annot-icon-btn:disabled { opacity:.35; cursor:default; background:none; }
29983
+ .annot-actions { display:flex; align-items:center; gap:8px; margin:0 2px 0 4px; }
29304
29984
  .annot-stage { flex:1; min-height:0; overflow:auto; display:grid; place-items:center; padding:8px 14px 18px; }
29305
29985
  .annot-img-wrap { position:relative; line-height:0; }
29306
29986
  .annot-img-wrap img { display:block; max-width:90vw; max-height:78vh; width:auto; height:auto; -webkit-user-drag:none; user-select:none; }
29307
- .annot-canvas { position:absolute; inset:0; width:100%; height:100%; }
29987
+ .annot-canvas { position:absolute; inset:0; width:100%; height:100%; touch-action:none; }
29308
29988
  .annot-canvas.tool-text { cursor:text; }
29309
29989
  .annot-canvas.tool-rect, .annot-canvas.tool-arrow { cursor:crosshair; }
29990
+ /* Inline text overlay: WYSIWYG twin of the flattened canvas text (same font,
29991
+ line-height and halo), plus an accent ring so the edit region reads. */
29992
+ .annot-text-input { position:absolute; z-index:2; min-width:60px; padding:2px 4px; margin:0; border:0; outline:none;
29993
+ resize:none; overflow:hidden; white-space:pre; background:transparent; border-radius:4px; caret-color:currentColor;
29994
+ font:600 20px/1.3 system-ui, -apple-system, sans-serif; text-shadow:0 1px 2px rgba(0,0,0,.55);
29995
+ box-shadow:0 0 0 1.5px var(--accent), 0 0 0 4px color-mix(in srgb, var(--accent) 25%, transparent); }
29310
29996
 
29311
29997
  /* ---- transient "uploading" placeholder + unavailable fallback ---- */
29312
29998
  .ico.spin { animation:mh-spin .8s linear infinite; }
@@ -29315,7 +30001,10 @@ var styles_default = ` :root {
29315
30001
  display:flex; align-items:center; gap:12px; margin:2px 0; max-width:480px; padding:12px 14px;
29316
30002
  border:1px solid var(--line-strong); border-radius:var(--radius); background:var(--surface); color:var(--fg-soft); }
29317
30003
  .void-uploading { border-style:dashed; }
29318
- .void-uploading .vu-name, .void-unavailable .vu-name { font-weight:600; color:var(--fg); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
30004
+ /* min-width:0: .void-uploading's vu-name is a direct flex item \u2014 without it a
30005
+ long nowrap filename sets the item's min-content floor and the placeholder
30006
+ card overflows a narrow window instead of ellipsizing. */
30007
+ .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; }
29319
30008
  .void-uploading .vu-sub, .void-unavailable .vu-sub { font-size:var(--fs-micro); color:var(--muted); }
29320
30009
  .void-unavailable { border-color:var(--danger-soft); color:var(--danger); }
29321
30010
  .void-unavailable .ico { color:var(--danger); flex:none; }
@@ -29361,24 +30050,53 @@ var styles_default = ` :root {
29361
30050
  /* Chrome rides the theme-tinted glass \u2192 theme-aware (var(--fg)), readable on a
29362
30051
  light or dark tint alike. */
29363
30052
  body.preview-window .lightbox-toolbar { color:var(--fg); }
29364
- body.preview-window .lightbox-toolbar button,
29365
- body.preview-window .annot-tool { color:var(--fg); background:color-mix(in srgb, var(--fg) 10%, transparent); }
29366
- body.preview-window .lightbox-toolbar button:hover,
29367
- body.preview-window .annot-tool:hover { background:color-mix(in srgb, var(--fg) 20%, transparent); }
29368
- body.preview-window .annot-tool.on { color:var(--accent-fg); background:var(--accent); }
29369
- body.preview-window .annot-swatch { border-color:color-mix(in srgb, var(--fg) 45%, transparent); }
30053
+ body.preview-window .lightbox-toolbar > button { color:var(--fg); background:color-mix(in srgb, var(--fg) 10%, transparent); }
30054
+ body.preview-window .lightbox-toolbar > button:hover { background:color-mix(in srgb, var(--fg) 20%, transparent); }
30055
+ body.preview-window .annot-seg { background:color-mix(in srgb, var(--fg) 8%, transparent); }
30056
+ body.preview-window .annot-seg-btn { color:color-mix(in srgb, var(--fg) 65%, transparent); }
30057
+ body.preview-window .annot-seg-btn:hover { color:var(--fg); background:color-mix(in srgb, var(--fg) 10%, transparent); }
30058
+ body.preview-window .annot-seg-btn.on { background:var(--bg); color:var(--fg); box-shadow:var(--shadow-sm); }
30059
+ body.preview-window .annot-div { background:color-mix(in srgb, var(--fg) 15%, transparent); }
30060
+ body.preview-window .annot-swatch { box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 25%, transparent); }
30061
+ body.preview-window .annot-swatch.on { box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 25%, transparent),
30062
+ 0 0 0 2px var(--bg), 0 0 0 3.5px color-mix(in srgb, var(--fg) 78%, transparent); }
30063
+ body.preview-window .annot-bar .annot-icon-btn { color:var(--fg); background:none; }
30064
+ body.preview-window .annot-bar .annot-icon-btn:hover { background:color-mix(in srgb, var(--fg) 12%, transparent); }
30065
+ body.preview-window .annot-bar .annot-icon-btn:disabled { background:none; }
29370
30066
  /* In the frameless window the toolbar doubles as the OS drag region; its
29371
30067
  buttons opt back out so they stay clickable. */
29372
30068
  .lightbox-toolbar.app-drag { -webkit-app-region:drag; }
29373
30069
  .lightbox-toolbar.app-drag button { -webkit-app-region:no-drag; }
29374
30070
 
29375
- /* document table (GFM pipe table block) */
29376
- .b-table .doc-table-wrap { flex:1; min-width:0; margin:4px 0; }
30071
+ /* document table (GFM pipe table block). contain:inline-size zeroes the
30072
+ widget's intrinsic-width contribution \u2014 otherwise the inner
30073
+ width:max-content propagates through overflow-x:auto into .cm-content's
30074
+ min-width:auto floor (intrinsic sizing ignores overflow) and pushes the
30075
+ whole page wider than the viewport on narrow screens. With the intrinsic
30076
+ loop broken, .doc-table-wrap resolves to the doc column width and the
30077
+ scroll box below finally gets to scroll. container-type additionally
30078
+ unlocks cqw for the sticky add-row bar (old engines fall back to contain);
30079
+ position:relative anchors the gutter row handle explicitly rather than
30080
+ leaning on container-type's implied containing block. */
30081
+ .doc-table-wrap { position:relative; contain:inline-size; container-type:inline-size; }
29377
30082
  .doc-table-scroll { overflow-x:auto; padding:2px 0; }
29378
- .doc-table-inner { width:max-content; }
30083
+ .doc-table-inner { position:relative; width:max-content; } /* anchors the drag outline / drop lines so they track horizontal scroll */
29379
30084
  .doc-table-row { display:flex; align-items:stretch; }
29380
30085
  table.doc-table { border-collapse:separate; border-spacing:0; table-layout:fixed; width:max-content;
29381
30086
  border:1px solid var(--line); border-radius:var(--radius-sm); overflow:hidden; box-shadow:var(--shadow-sm); }
30087
+ /* Default (no manual widths yet): browser auto table layout sizes columns by
30088
+ content, capped to the wrap (cqw works despite the max-content .doc-table-inner;
30089
+ 26px keeps the add-col button inside) so it wraps instead of scrolling. The
30090
+ first resizer drag freezes measured widths and drops this class (table-block.tsx). */
30091
+ table.doc-table.autofit { table-layout:auto; width:auto; max-width:calc(100cqw - 26px); }
30092
+ table.doc-table.autofit .doc-td { min-width:48px; } /* empty columns don't collapse */
30093
+ /* Code tokens are atomic: raising the column's min-content makes the auto
30094
+ algorithm shed width into wrappable prose columns instead of breaking
30095
+ \`file-names.md\` at hyphens; an over-long token falls back to the scroll
30096
+ box. Autofit only \u2014 in manual fixed layout nowrap can't grow the column,
30097
+ it would overflow the cell. Phones relax this (see the 560px block):
30098
+ there the atom IS the cap, and it crushes prose columns to their floor. */
30099
+ table.doc-table.autofit .doc-td code { white-space:nowrap; }
29382
30100
  table.doc-table td { position:relative; vertical-align:top; padding:0;
29383
30101
  border-right:1px solid var(--line); border-bottom:1px solid var(--line); }
29384
30102
  table.doc-table tr td:last-child { border-right:0; }
@@ -29390,24 +30108,59 @@ var styles_default = ` :root {
29390
30108
  .doc-td b, .doc-td strong { font-weight:700; }
29391
30109
  .doc-td code { font-family:var(--mono); background:var(--surface-2); padding:1px 5px; border-radius:4px; font-size:.9em; }
29392
30110
  .doc-td a { color:var(--accent); }
29393
- /* header cell: column menu + resize handle */
29394
- .doc-col-menu { position:absolute; top:4px; right:4px; width:18px; height:18px; display:grid; place-items:center;
29395
- color:var(--muted); border-radius:4px; opacity:0; transition:opacity .12s,background .12s; }
29396
- .doc-th:hover .doc-col-menu { opacity:1; }
29397
- .doc-col-menu:hover { background:var(--hover-2); color:var(--fg); }
29398
30111
  .doc-col-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:3; touch-action:none; }
29399
- /* per-row delete: small handle floating at the first cell on row hover */
29400
- .doc-row-del { position:absolute; top:3px; left:2px; width:18px; height:18px; display:grid; place-items:center;
29401
- color:var(--muted); background:var(--bg); border:1px solid var(--line); border-radius:5px; box-shadow:var(--shadow-sm);
29402
- opacity:0; transition:opacity .12s; }
29403
- table.doc-table tr:hover .doc-row-del { opacity:1; }
29404
- .doc-row-del:hover { background:var(--danger-soft); color:var(--danger); border-color:var(--danger-soft); }
30112
+ /* Row/column pill handles (Notion-style): one per axis, tracked onto the
30113
+ active cell (hovered on desktop, focused on touch) by JS. They live on
30114
+ .doc-table-wrap OUTSIDE .doc-table-scroll so overflow-x can't clip them,
30115
+ straddling the table's left/top border \u2014 never on cell text. Resting =
30116
+ quiet grey pill; pressed/dragging (.grab) = accent pill with light dots
30117
+ plus the source outline + drop line below. touch-action:none so a finger
30118
+ drag on the handle reorders instead of scrolling. Data rows only get a
30119
+ row pill (the block gutter's +/grip pins at the header row's y). */
30120
+ .doc-row-handle, .doc-col-handle { position:absolute; display:grid; place-items:center; z-index:7;
30121
+ color:var(--muted); background:var(--bg); border:1px solid var(--line); box-shadow:var(--shadow-sm);
30122
+ cursor:grab; touch-action:none; animation:gripFade .12s ease both;
30123
+ transition:background .12s,color .12s,border-color .12s; }
30124
+ .doc-row-handle { left:-8px; width:15px; height:28px; border-radius:8px; }
30125
+ .doc-col-handle { top:-6px; width:28px; height:15px; border-radius:8px; transform:translateX(-50%); }
30126
+ .doc-row-handle svg, .doc-col-handle svg { width:13px; height:13px; }
30127
+ .doc-row-handle:hover, .doc-col-handle:hover { background:var(--surface); color:var(--fg); }
30128
+ .doc-row-handle.grab, .doc-col-handle.grab { background:var(--accent); color:var(--accent-fg,#fff);
30129
+ border-color:var(--accent); cursor:grabbing; }
30130
+ .doc-row-handle::after { content:""; position:absolute; inset:-4px; }
30131
+ .doc-col-handle::after { content:""; position:absolute; inset:-4px; }
30132
+ /* drag-reorder overlays (inside .doc-table-inner, so they scroll with the
30133
+ table): accent outline on the source row/column, 2px line at the drop
30134
+ boundary (grid .rowdrop's cousin) */
30135
+ .doc-drag-outline { position:absolute; border:2px solid var(--accent); border-radius:6px;
30136
+ pointer-events:none; z-index:6; box-sizing:border-box; }
30137
+ .doc-rowdrop { position:absolute; height:2px; margin-top:-1px; background:var(--accent); border-radius:1px; pointer-events:none; z-index:6; }
30138
+ .doc-coldrop { position:absolute; width:2px; margin-left:-1px; background:var(--accent); border-radius:1px; pointer-events:none; z-index:6; }
30139
+ /* touch selection action bar: the OSK has no Delete/\u2318C, so the cell
30140
+ rectangle gets on-screen verbs; sits on the wrap layer above the selection */
30141
+ .doc-sel-bar { position:absolute; z-index:8; display:flex; gap:2px; padding:3px;
30142
+ background:var(--accent); border-radius:9px; box-shadow:var(--shadow-sm); animation:gripFade .12s ease both; }
30143
+ .doc-sel-bar button { color:var(--accent-fg,#fff); font-size:13px; line-height:1; padding:5px 9px; border-radius:6px; }
30144
+ .doc-sel-bar button:active { background:color-mix(in srgb, #000 18%, transparent); }
29405
30145
  /* add-column (right edge) and add-row (bottom edge) buttons */
29406
30146
  .doc-table-addcol { width:22px; margin-left:4px; flex:none; display:grid; place-items:center; color:var(--muted);
29407
- border:1px dashed var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); transition:background .12s,color .12s,border-color .12s; }
29408
- .doc-table-addrow { width:100%; height:22px; margin-top:4px; display:grid; place-items:center; color:var(--muted);
29409
- border:1px dashed var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); transition:background .12s,color .12s,border-color .12s; }
30147
+ border:1px dashed var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); transition:background .12s,color .12s,border-color .12s,opacity .12s; }
30148
+ /* sticky + 100cqw (the wrap is the container): on a wide table the bar pins
30149
+ to the scrollport at any horizontal offset; narrow tables never hit the
30150
+ max-width and keep their table-wide bar. Engines without cqw just ignore
30151
+ the cap and fall back to scrolling with the table. */
30152
+ .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);
30153
+ border:1px dashed var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); transition:background .12s,color .12s,border-color .12s,opacity .12s; }
29410
30154
  .doc-table-addcol:hover, .doc-table-addrow:hover { background:var(--accent-soft); color:var(--accent); border-color:var(--accent); }
30155
+ /* Notion-style: the add bars only materialize while the table is active \u2014
30156
+ hover or keyboard focus on desktop, an editing (focused) cell on touch.
30157
+ Opacity-only so nothing shifts; pointer-events gates off the invisible
30158
+ bars (a touch tap must not hit a bar it can't see). */
30159
+ .doc-table-addrow, .doc-table-addcol { opacity:0; pointer-events:none; }
30160
+ .doc-table-wrap:focus-within :is(.doc-table-addrow, .doc-table-addcol) { opacity:1; pointer-events:auto; }
30161
+ @media (hover:hover) and (pointer:fine) {
30162
+ .doc-table-wrap:hover :is(.doc-table-addrow, .doc-table-addcol) { opacity:1; pointer-events:auto; }
30163
+ }
29411
30164
  /* cell range selection (box select): soft fill per cell; the accent rectangle
29412
30165
  outline is drawn via inline inset box-shadow on the edge cells (see
29413
30166
  cell-select.ts edgeShadow). While selecting, suppress the caret + native
@@ -29602,7 +30355,7 @@ var styles_default = ` :root {
29602
30355
  .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); }
29603
30356
  .shv-search input { border:0; background:none; outline:none; color:var(--fg); font-size:13px; width:180px; }
29604
30357
 
29605
- .shv-list { display:flex; flex-direction:column; gap:8px; }
30358
+ .shv-list { display:flex; flex-direction:column; gap:8px; list-style:none; padding:0; margin:0; }
29606
30359
  .shv-row { display:flex; align-items:center; gap:13px; padding:13px 15px; background:var(--bg); border:1px solid var(--line);
29607
30360
  border-radius:var(--radius-lg); transition:border-color .12s, box-shadow .12s; }
29608
30361
  .shv-row:hover { border-color:var(--line-strong); box-shadow:var(--shadow-sm); }
@@ -29624,7 +30377,25 @@ var styles_default = ` :root {
29624
30377
  .shv-exp.dead { text-decoration:line-through; }
29625
30378
  .shv-actions { display:flex; align-items:center; gap:6px; flex:none; }
29626
30379
  .shv-actions .btn { padding:5px 10px; font-size:12px; }
29627
- @media (max-width:640px){ .shv-row{ flex-wrap:wrap; } .shv-actions{ width:100%; justify-content:flex-end; } }
30380
+ @media (max-width:640px){
30381
+ /* head stacks: title/desc full row, then segmented tabs, then search */
30382
+ .shares-page .db-head { flex-direction:column; align-items:stretch; gap:12px; }
30383
+ .shv-tools { margin-left:0; width:100%; }
30384
+ .shv-seg { display:flex; width:100%; }
30385
+ .shv-seg-btn { flex:1; text-align:center; }
30386
+ .shv-search { flex:1; }
30387
+ .shv-search input { width:100%; flex:1; }
30388
+
30389
+ /* cards: title clamps to two lines, meta wraps naturally */
30390
+ .shv-row { flex-wrap:wrap; }
30391
+ .shv-title { max-width:100%; white-space:normal; display:-webkit-box;
30392
+ -webkit-line-clamp:2; -webkit-box-orient:vertical; text-align:left; }
30393
+ .shv-meta { white-space:normal; overflow:visible; text-overflow:clip; }
30394
+
30395
+ /* actions: full row of evenly stretched touch targets */
30396
+ .shv-actions { width:100%; }
30397
+ .shv-actions .btn { flex:1; justify-content:center; padding:7px 10px; }
30398
+ }
29628
30399
 
29629
30400
  /* in-context "\u5DF2\u5206\u4EAB" badge on docs / tables / site cards */
29630
30401
  .share-badge { display:inline-grid; place-items:center; width:22px; height:22px; border-radius:6px; color:var(--accent);
@@ -29701,7 +30472,9 @@ var styles_default = ` :root {
29701
30472
  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; }
29702
30473
  .peek.open { transform:none; }
29703
30474
  .peek-head { display:flex; align-items:center; gap:4px; padding:12px 16px; border-bottom:1px solid var(--line); }
29704
- .peek-body { overflow:auto; padding:28px 32px; }
30475
+ /* Same stable-gutter treatment as .content (stable only \u2014 see .content on
30476
+ why both-edges is forbidden). */
30477
+ .peek-body { overflow:auto; scrollbar-gutter:stable; padding:28px 32px; }
29705
30478
  .peek h2 { font-size:27px; font-weight:700; letter-spacing:-.02em; margin:0 0 20px; outline:none; }
29706
30479
  .proprow { display:flex; gap:8px; align-items:flex-start; margin:1px 0; }
29707
30480
  .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); }
@@ -29758,7 +30531,7 @@ var styles_default = ` :root {
29758
30531
  .hist-restore { margin-top:8px; align-self:flex-start; }
29759
30532
 
29760
30533
  /* menu / popover */
29761
- .menu-layer { position:fixed; inset:0; z-index:90; }
30534
+ .menu-layer { position:fixed; inset:0; z-index:115; }
29762
30535
  .pop { position:fixed; background:var(--bg); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow-lg);
29763
30536
  padding:6px; min-width:232px; max-height:70vh; overflow:auto; animation:pop .12s ease; }
29764
30537
  @keyframes pop { from { opacity:0; transform:translateY(-4px); } }
@@ -29776,6 +30549,9 @@ var styles_default = ` :root {
29776
30549
  .pop .item .meta { flex:1; min-width:0; }
29777
30550
  .pop .item .t { font-size:var(--fs-ui); } .pop .item .d { font-size:var(--fs-xs); color:var(--muted); display:block; }
29778
30551
  .pop .item .chk { color:var(--accent); margin-left:auto; }
30552
+ /* shortcut badge, mirrors .sb-search kbd; margin-left:auto pins it right (and
30553
+ a trailing .chk, whose own auto then collapses, rides the gap beside it) */
30554
+ .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; }
29779
30555
  .pop .item.danger { color:var(--danger); } .pop .item.danger .lico { color:var(--danger); }
29780
30556
  .pop .sep { height:1px; background:var(--line); margin:5px 4px; }
29781
30557
  .pop .typegrid { display:grid; grid-template-columns:repeat(4,1fr); gap:4px; padding:2px 2px 4px; }
@@ -29895,24 +30671,28 @@ var styles_default = ` :root {
29895
30671
  .view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
29896
30672
  .ico.flip { transform:scaleX(-1); }
29897
30673
 
29898
- /* ---- find in document (Ctrl-F) ---- */
29899
- /* CSS Custom Highlight overlays \u2014 no DOM mutation, safe over contentEditable. */
29900
- ::highlight(mh-find-all) { background:color-mix(in srgb, var(--accent) 26%, transparent); border-radius:2px; }
29901
- ::highlight(mh-find-current) { background:var(--accent); color:var(--accent-fg); }
30674
+ /* ---- find in document (Ctrl-F, CM6 bar) ---- */
30675
+ /* Match highlights are CM6 Decoration.marks (.cm-find/.cm-find-cur, editor-theme.ts).
30676
+ Matches INSIDE void widgets (code mirror, table cells) can't be mark
30677
+ decorations \u2014 the block replace widget swallows them \u2014 so those paint via
30678
+ the CSS Custom Highlight API (find.ts collectMatches/applyHighlights). */
30679
+ ::highlight(mh-find-all) { background: color-mix(in srgb, var(--accent) 26%, transparent); border-radius: 2px; }
30680
+ ::highlight(mh-find-current) { background: var(--accent); color: var(--accent-fg); }
29902
30681
  .find-bar {
29903
- position:fixed; top:58px; right:18px; z-index:40; /* below the ~50px topbar */
30682
+ position:fixed; top:58px; right:18px; z-index:96; /* below the ~50px topbar */
29904
30683
  display:flex; align-items:center; gap:4px;
29905
30684
  padding:5px 6px; background:var(--bg); border:1px solid var(--line-strong);
29906
30685
  border-radius:var(--radius); box-shadow:var(--shadow-lg);
29907
30686
  }
29908
30687
  .find-bar .find-ico { color:var(--muted); margin:0 2px; flex:none; }
29909
30688
  .find-bar input {
29910
- width:200px; max-width:42vw; border:0; outline:0; background:transparent;
30689
+ width:200px; max-width:42vw; min-width:0; border:0; outline:0; background:transparent;
29911
30690
  color:var(--fg); font-size:13px; padding:3px 2px;
29912
30691
  }
29913
30692
  .find-bar input::placeholder { color:var(--muted); }
29914
- .find-count { font-size:12px; color:var(--muted); min-width:48px; text-align:right; padding:0 2px; font-variant-numeric:tabular-nums; }
30693
+ .find-count { font-size:12px; color:var(--muted); min-width:48px; text-align:right; padding:0 2px; font-variant-numeric:tabular-nums; flex:none; }
29915
30694
  .find-opt {
30695
+ flex:none; white-space:nowrap;
29916
30696
  border:1px solid var(--line-strong); background:transparent; color:var(--fg-soft);
29917
30697
  border-radius:var(--radius-sm); font-size:12px; line-height:1; padding:4px 6px; cursor:pointer;
29918
30698
  transition:background .12s, color .12s, border-color .12s;
@@ -29932,6 +30712,14 @@ var styles_default = ` :root {
29932
30712
  .find-bar { left:12px; right:12px; }
29933
30713
  .find-bar input { width:auto; flex:1; max-width:none; }
29934
30714
  }
30715
+ /* Very narrow windows (quicknote resizes down to 280px): drop the reserved
30716
+ count width and tighten gaps so the option buttons never get crushed. */
30717
+ @media (max-width:360px) {
30718
+ .find-bar { gap:2px; padding:5px 4px; }
30719
+ .find-count { min-width:0; }
30720
+ }
30721
+ /* Quicknote window: its qn-bar is ~34px, not the main ~50px topbar. */
30722
+ body.quicknote .find-bar { top:42px; }
29935
30723
 
29936
30724
  /* ---- board (kanban) view ---- */
29937
30725
  .board-wrap { margin-top:4px; }
@@ -30088,8 +30876,7 @@ var styles_default = ` :root {
30088
30876
  .navitem { padding:9px 8px; font-size:16px; }
30089
30877
  .navitem .emoji { font-size:18px; }
30090
30878
  .navitem .tw { width:26px; height:26px; }
30091
- .sb-section-head { font-size:12px; }
30092
- .sb-search kbd { display:none; } /* no hardware keyboard, no shortcut hint */
30879
+ .sb-search kbd, .pop .item kbd { display:none; } /* no hardware keyboard, no shortcut hint */
30093
30880
  /* Search box is hidden until the header search button reveals it; it then
30094
30881
  slides+fades down in. Desktop base (.sb-search{display:flex}) is untouched,
30095
30882
  so search stays always-on there. reduced-motion is handled globally. */
@@ -30104,17 +30891,26 @@ var styles_default = ` :root {
30104
30891
  /* Document: bigger body text, tighter side padding for narrow screens. */
30105
30892
  .doc { padding:24px 18px calc(36vh + env(safe-area-inset-bottom)); }
30106
30893
  .doc-title { font-size:30px; }
30107
- .doc .editable { font-size:16px; }
30894
+ /* CM6 editor (single-document): its .cm-editor inherits from here, so the whole
30895
+ body incl. table cells gets 16px on mobile \u2014 same iOS anti-zoom reason. */
30896
+ .doc .cm-doc-body { font-size:16px; }
30108
30897
  /* 16px (not 14): a focused code textarea below 16px makes iOS auto-zoom. */
30109
30898
  .codeblock { --code-fs:16px; }
30110
- .block .gutter { display:none; }
30111
- /* Anchor/TOC jumps: clear the mobile topbar (49px min + notch inset);
30112
- the desktop 60px estimate leaves blocks half-hidden behind it. */
30113
- .block { scroll-margin-top:calc(49px + env(safe-area-inset-top) + 8px); }
30899
+ /* CM6 hover gutter: no hover on touch, and -52px overlays would overflow
30900
+ the tight mobile margins \u2014 hide. */
30901
+ .cm-gutter-layer { display:none; }
30902
+ /* Autofit tables: on phones the atomic code span IS the width cap \u2014 one
30903
+ long \`file-name.md\` becomes the column's min-content, eats the whole
30904
+ 100cqw budget and crushes prose columns to their 48px floor. Let code
30905
+ wrap at its natural break points (hyphens/slashes) instead; wider
30906
+ layouts keep the atomic-code tradeoff (see the autofit rules). */
30907
+ table.doc-table.autofit .doc-td code { white-space:pre-wrap; overflow-wrap:normal; word-break:normal; }
30114
30908
  /* Find bar clears the taller mobile topbar (49px min + notch inset). */
30115
30909
  .find-bar { top:calc(49px + env(safe-area-inset-top) + 8px); }
30116
30910
  /* narrower handle gutter on phones; .db left padding must be \u2265 --grip-gutter */
30117
30911
  .db { padding:16px 14px 90px 24px; }
30912
+ /* pages reusing .db without row grips don't need the left gutter \u2014 keep padding symmetric */
30913
+ .shares-page.db { padding-left:14px; }
30118
30914
  .gridhost { --grip-gutter:24px; }
30119
30915
  .peek { width:100%; }
30120
30916
  /* history drawer: the side-by-side list/preview doesn't fit a phone \u2014 stack. */
@@ -30149,13 +30945,19 @@ var styles_default = ` :root {
30149
30945
  @media (pointer: coarse) {
30150
30946
  /* iOS Safari auto-zooms the page when a focused field is below 16px; the
30151
30947
  class selectors out-specify the element-level sizes set on these. */
30152
- input, textarea, .text-input, .doc-source, input.inlineedit { font-size:16px; }
30948
+ input, textarea, .text-input, input.inlineedit { font-size:16px; }
30153
30949
  .navitem .acts button { width:34px; height:34px; }
30950
+ .sb-tab { min-height:40px; padding:8px 10px; }
30951
+ .sb-tabs .add { width:40px; height:40px; display:grid; place-items:center; padding:0; }
30154
30952
  .iconbtn { width:40px; height:40px; }
30155
30953
  .btn { padding:9px 14px; min-height:40px; }
30156
30954
  .pop .item { padding:11px 10px; }
30157
30955
  /* Column/sidebar resize handles: 7px is a mouse affordance, not a finger's. */
30158
30956
  .col-resizer, .doc-col-resizer, .sb-resizer { width:16px; right:-8px; }
30957
+ /* cell-rectangle corner handle: big enough to grab with a finger (it is the
30958
+ only way to grow a touch selection \u2014 pan gestures stay scrolls) */
30959
+ .doc-cell-handle { width:12px; height:12px; right:-6px; bottom:-6px; }
30960
+ .doc-cell-handle::after { inset:-10px; }
30159
30961
  }
30160
30962
 
30161
30963
  /* No hover available (touch): controls that only materialize on :hover are
@@ -30164,9 +30966,11 @@ var styles_default = ` :root {
30164
30966
  @media (hover: none) {
30165
30967
  .code-tools { opacity:1; transform:none; pointer-events:auto; }
30166
30968
  .rowactions { opacity:1; pointer-events:auto; }
30167
- .doc-th .doc-col-menu, .cal-add, .filerow .facts,
30168
- .navitem .acts, .sb-section-head .add { opacity:1; }
30169
- table.doc-table .doc-row-del { opacity:1; }
30969
+ .cal-add, .filerow .facts,
30970
+ .navitem .acts { opacity:1; }
30971
+ /* the pill handles survive on touch (focus-driven, not hover-driven), but
30972
+ sticky tap-hover must not tint them */
30973
+ .doc-row-handle:hover, .doc-col-handle:hover { background:var(--bg); color:var(--muted); }
30170
30974
  .selcell input { opacity:1; }
30171
30975
  .tl-item .tl-h { opacity:1; }
30172
30976
  /* Neutralize the stickiest tap-residue tints (a tap "hovers" until the
@@ -30178,12 +30982,16 @@ var styles_default = ` :root {
30178
30982
  .filerow:hover { background:none; }
30179
30983
  .cal-day:hover { background:none; }
30180
30984
  .cal-day.dim:hover { background:var(--surface); }
30985
+ .doc-table-addcol:hover, .doc-table-addrow:hover { background:var(--surface); color:var(--muted); border-color:var(--line-strong); }
30181
30986
  }
30182
30987
 
30183
30988
  /* Quick Notes window (desktop, loaded at #quick). A transparent body lets the
30184
30989
  macOS vibrancy blur show through; a translucent panel keeps text readable.
30185
30990
  The top bar is the window drag region; its buttons opt back out. */
30186
- body.quicknote { background:transparent; }
30991
+ /* --pop-top-inset: editor popups (slash menu) clamp their upward growth below
30992
+ this line, so they never cover the drag bar / macOS traffic lights (bar =
30993
+ 26px actions + 2\xD74px padding). */
30994
+ body.quicknote { background:transparent; --pop-top-inset:38px; }
30187
30995
  /* html carries an opaque bg for the iOS 26 Safari toolbar tint; the vibrancy
30188
30996
  window needs the root transparent again or the blur is walled off. */
30189
30997
  html:has(body.quicknote) { background:transparent; }
@@ -30203,22 +31011,32 @@ var styles_default = ` :root {
30203
31011
  .qn-actions { display:flex; gap:2px; -webkit-app-region:no-drag; }
30204
31012
  .qn-actions .iconbtn { width:26px; height:26px; }
30205
31013
  .qn-actions .iconbtn.active { color:var(--accent); background:var(--hover-2); }
30206
- .qn-body { flex:1; overflow:auto; -webkit-app-region:no-drag; }
30207
- .qn-body .doc { max-width:none; margin:0; padding:14px 22px 30vh; }
31014
+ /* Quicknote scrolling: the app-wide thin translucent scrollbar (transparent
31015
+ track, so the reserved lane just reads as right margin). stable reserves
31016
+ the lane up front so width never jumps when the doc grows past one
31017
+ screen; the doc's right padding + lane \u2248 left padding, keeping the prose
31018
+ visually centered. stable only, NEVER both-edges \u2014 see .content: a left
31019
+ gutter desyncs Chromium's scroll cull rect and paints a blank stripe over
31020
+ the rightmost 15px of content (it sliced this window's file cards). */
31021
+ /* scrollbar-width does NOT inherit (unlike scrollbar-color): without an
31022
+ explicit thin here the lane is the classic 15px. thin = 11px, so the
31023
+ doc's 1px right padding + 11px lane mirrors the 12px left padding. */
31024
+ .qn-body { flex:1; overflow:auto; scrollbar-gutter:stable; scrollbar-width:thin; -webkit-app-region:no-drag; }
31025
+ .qn-body .doc { max-width:none; margin:0; padding:14px 1px 30vh 12px; }
30208
31026
  .qn-body .doc-title { font-size:24px; margin-bottom:2px; }
30209
31027
  .qn-body .doc-meta { display:none; }
30210
- /* The narrow-width gutter hide is now gated on (pointer:coarse), which this
30211
- desktop window never matches \u2014 re-hide explicitly: quick notes offer no
31028
+ /* The CM6 hover gutter never fits the compact window: quick notes offer no
30212
31029
  block insert/drag handles (Enter / slash commands cover insertion). */
30213
- body.quicknote .block .gutter { display:none; }
31030
+ body.quicknote .cm-gutter-layer { display:none; }
30214
31031
  /* A native checkbox paints an opaque white box when unchecked, which breaks
30215
31032
  the window's vibrancy blur. Draw a custom box so the unchecked background
30216
- stays transparent; checked falls back to the accent fill + checkmark. */
30217
- body.quicknote .b-todo .marker input {
31033
+ stays transparent; checked falls back to the accent fill + checkmark.
31034
+ (Same treatment main gave \`.b-todo .marker input\`.) */
31035
+ body.quicknote .cm-todo-box {
30218
31036
  appearance:none; -webkit-appearance:none; box-sizing:border-box;
30219
31037
  border:1.5px solid color-mix(in srgb, var(--fg-soft) 55%, transparent);
30220
31038
  border-radius:4px; background:transparent; }
30221
- body.quicknote .b-todo .marker input:checked {
31039
+ body.quicknote .cm-todo-box:checked {
30222
31040
  border-color:var(--accent);
30223
31041
  background:var(--accent) center/10px no-repeat
30224
31042
  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"); }
@@ -30240,8 +31058,11 @@ var styles_default = ` :root {
30240
31058
  .qn-set-main { flex:1; min-width:0; }
30241
31059
  .qn-set-name { font-weight:600; }
30242
31060
  .qn-set-desc { color:var(--muted); font-size:13px; margin-top:2px; }
30243
- .qn-shortcut { min-width:140px; justify-content:center; font-family:var(--mono); }
31061
+ .qn-shortcut { min-width:140px; justify-content:center; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif; }
30244
31062
  .qn-shortcut.capturing { outline:2px solid var(--accent); color:var(--accent); }
31063
+ .qn-keys { display:inline-flex; align-items:center; gap:6px; }
31064
+ .qn-key { line-height:1; }
31065
+ .qn-key.sym { font-size:1.15em; line-height:1; } /* \u2318 \u21E7 glyphs render smaller \u2014 bump to match letters */
30245
31066
 
30246
31067
  /* No-origin bootstrap: enroll screen + hydration splash (data-blind shell). */
30247
31068
  .enroll-splash { min-height:100dvh; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; padding:24px; }
@@ -30255,6 +31076,44 @@ var styles_default = ` :root {
30255
31076
  .enroll-err { color:var(--danger); font-size:13px; margin-top:12px; }
30256
31077
  .enroll-go { width:100%; justify-content:center; margin-top:18px; }
30257
31078
  .enroll-hint { font-size:12px; margin-top:14px; line-height:1.5; }
31079
+ .enroll-card { animation:enroll-in .35s ease both; }
31080
+ @keyframes enroll-in { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } }
31081
+
31082
+ /* Scan-first enroll: square viewfinder (corner frame + sweeping accent beam)
31083
+ over the live camera feed. The chip stays black in both themes \u2014 the feed
31084
+ brings its own colors; accent carries the beam and the success flash. */
31085
+ .enroll-scan { position:relative; aspect-ratio:1/1; margin:2px 0 12px; border-radius:16px; overflow:hidden; background:#0b0b0e; }
31086
+ .enroll-scan video { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
31087
+ .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); }
31088
+ .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; }
31089
+ .enroll-scan-beam { position:absolute; left:9%; right:9%; top:12%; height:2px; border-radius:2px; pointer-events:none; opacity:.85;
31090
+ background:linear-gradient(90deg, transparent, var(--accent), transparent);
31091
+ animation:enroll-beam 2.4s ease-in-out infinite alternate; }
31092
+ @keyframes enroll-beam { to { top:86%; } }
31093
+ .enroll-scan.hit .enroll-scan-frame { stroke:var(--accent); }
31094
+ .enroll-scan.hit::after { animation:enroll-hit .38s ease-out; }
31095
+ @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); } }
31096
+ .enroll-scan-wait { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:rgba(255,255,255,.45); }
31097
+ .enroll-scan-waitico { width:30px; height:30px; animation:enroll-wait 1.6s ease-in-out infinite; }
31098
+ @keyframes enroll-wait { 0%,100% { opacity:.4; } 50% { opacity:.9; } }
31099
+ .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; }
31100
+ .enroll-cam-err .muted { color:rgba(255,255,255,.55); font-size:12px; }
31101
+ .enroll-cam-errico { width:26px; height:26px; opacity:.7; }
31102
+ .enroll-cam-err .btn { margin-top:8px; }
31103
+ .enroll-paste { width:100%; font-size:13px; }
31104
+ .enroll-switch { display:flex; justify-content:center; align-items:center; gap:10px; margin-top:14px; }
31105
+ .enroll-switch-dot { color:var(--muted); }
31106
+ .enroll-link { background:none; border:0; padding:0; font:inherit; font-size:13px; color:var(--accent); cursor:pointer; }
31107
+ .enroll-link:hover { text-decoration:underline; }
31108
+ /* Unlock step: compact bucket summary chip above the passphrase field. */
31109
+ .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); }
31110
+ .enroll-bucket-ico { width:18px; height:18px; color:var(--muted); }
31111
+ .enroll-bucket-meta { flex:1; min-width:0; }
31112
+ .enroll-bucket-name { font-weight:600; font-size:14px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
31113
+ .enroll-bucket-host { font-family:var(--mono); font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
31114
+ .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;
31115
+ border:1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
31116
+ .enroll-bucket-badge .ico { width:12px; height:12px; }
30258
31117
 
30259
31118
  /* Enroll QR (settings \u2192 \u540C\u6B65\u5B58\u50A8 \u2192 \u5728\u624B\u673A\u4E0A\u6253\u5F00). The QR inverts with the
30260
31119
  theme: light = dark modules on a light chip, dark = light modules on a dark
@@ -30457,6 +31316,42 @@ var ICON_192 = "iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAYAAABS3GwHAAAAAXNSR0IArs4c6QAA
30457
31316
  var ICON_512 = "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";
30458
31317
  var ICON_180 = "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";
30459
31318
 
31319
+ // src/webui/fmt/manifest.ts
31320
+ var FMT_PROVIDERS = [
31321
+ { id: "core", js: "/webui-fmt.js", entry: "fmt/fmt-core.ts" },
31322
+ {
31323
+ id: "ruff",
31324
+ js: "/webui-fmt-ruff.js",
31325
+ entry: "fmt/fmt-ruff.ts",
31326
+ wasm: { route: "/webui-fmt-ruff.wasm", pkg: "@wasm-fmt/ruff_fmt", file: "ruff_fmt_bg.wasm" }
31327
+ },
31328
+ {
31329
+ id: "gofmt",
31330
+ js: "/webui-fmt-gofmt.js",
31331
+ entry: "fmt/fmt-gofmt.ts",
31332
+ wasm: { route: "/webui-fmt-gofmt.wasm", pkg: "@wasm-fmt/gofmt", file: "gofmt.wasm" }
31333
+ },
31334
+ {
31335
+ id: "clang",
31336
+ js: "/webui-fmt-clang.js",
31337
+ entry: "fmt/fmt-clang.ts",
31338
+ wasm: { route: "/webui-fmt-clang.wasm", pkg: "@wasm-fmt/clang-format", file: "clang-format.wasm" }
31339
+ },
31340
+ {
31341
+ id: "lua",
31342
+ js: "/webui-fmt-lua.js",
31343
+ entry: "fmt/fmt-lua.ts",
31344
+ wasm: { route: "/webui-fmt-lua.wasm", pkg: "@wasm-fmt/lua_fmt", file: "lua_fmt_bg.wasm" }
31345
+ },
31346
+ {
31347
+ id: "taplo",
31348
+ js: "/webui-fmt-taplo.js",
31349
+ entry: "fmt/fmt-taplo.ts",
31350
+ wasm: { route: "/webui-fmt-taplo.wasm", pkg: "@wasm-fmt/taplo_fmt", file: "taplo_fmt_bg.wasm" }
31351
+ },
31352
+ { id: "sh", js: "/webui-fmt-sh.js", entry: "fmt/fmt-sh.ts" }
31353
+ ];
31354
+
30460
31355
  // src/webui/server/assets.ts
30461
31356
  var RUNNING_FROM_SOURCE = import.meta.url.includes("/src/webui/");
30462
31357
  var HTML2 = `<!doctype html>
@@ -30501,6 +31396,7 @@ var injected = {
30501
31396
  sdk: null
30502
31397
  };
30503
31398
  var injectedWasm = null;
31399
+ var injectedFmtWasm = {};
30504
31400
  function newestSourceMtime(dir) {
30505
31401
  let newest = 0;
30506
31402
  for (const e3 of readdirSync4(dir, { withFileTypes: true })) {
@@ -30549,6 +31445,24 @@ function bundleGetter(opts) {
30549
31445
  }
30550
31446
  var getJs = bundleGetter({ key: "js", entry: "../app.tsx", dist: "webui.js" });
30551
31447
  var getSwRaw = bundleGetter({ key: "sw", entry: "../sw.ts", dist: "sw.js" });
31448
+ var fmtJs = {};
31449
+ for (const p2 of FMT_PROVIDERS) {
31450
+ injected[`fmt_${p2.id}`] = null;
31451
+ fmtJs[p2.js] = bundleGetter({ key: `fmt_${p2.id}`, entry: `../${p2.entry}`, dist: p2.js.slice(1) });
31452
+ }
31453
+ var fmtWasmByRoute = Object.fromEntries(FMT_PROVIDERS.filter((p2) => p2.wasm).map((p2) => [p2.wasm.route, p2]));
31454
+ async function getFmtWasm(p2) {
31455
+ const w4 = p2.wasm;
31456
+ const embedded = injectedFmtWasm[w4.route];
31457
+ if (embedded != null)
31458
+ return embedded;
31459
+ const path = RUNNING_FROM_SOURCE ? join9(dirname(Bun.resolveSync(w4.pkg, fileURLToPath(new URL(".", import.meta.url)))), w4.file) : fileURLToPath(new URL(`.${w4.route}`, import.meta.url));
31460
+ const f4 = Bun.file(path);
31461
+ if (!await f4.exists()) {
31462
+ throw new Error(`${w4.route} missing at ${path} \u2014 run \`bun install\` / \`bun run build\``);
31463
+ }
31464
+ return f4.arrayBuffer();
31465
+ }
30552
31466
  var getDbWorker = bundleGetter({ key: "dbWorker", entry: "../data/db-worker.ts", dist: "db-worker.js" });
30553
31467
  var getRuntime = bundleGetter({ key: "runtime", entry: "../runtime.ts", dist: "mh-runtime.js" });
30554
31468
  var getSdk = bundleGetter({ key: "sdk", entry: "../../sdk/client.ts", dist: "metahub-sdk.js", extraDirs: ["../../sdk"] });
@@ -30630,12 +31544,24 @@ async function serveWebui(req) {
30630
31544
  return new Response(`sqlite3.wasm unavailable: ${e3}`, { status: 500 });
30631
31545
  }
30632
31546
  }
31547
+ const fmtWasmProvider = fmtWasmByRoute[pathname];
31548
+ if (fmtWasmProvider) {
31549
+ try {
31550
+ return new Response(await getFmtWasm(fmtWasmProvider), {
31551
+ headers: { "content-type": "application/wasm", "cache-control": "public, max-age=86400" }
31552
+ });
31553
+ } catch (e3) {
31554
+ console.error(`[webui] failed to serve ${pathname} \u2014`, e3);
31555
+ return new Response(`${pathname} unavailable: ${e3}`, { status: 500 });
31556
+ }
31557
+ }
30633
31558
  const script = {
30634
31559
  "/webui.js": getJsStamped,
30635
31560
  "/sw.js": getSw,
30636
31561
  "/db-worker.js": getDbWorker,
30637
31562
  "/mh-runtime.js": getRuntime,
30638
- "/metahub-sdk.js": getSdk
31563
+ "/metahub-sdk.js": getSdk,
31564
+ ...fmtJs
30639
31565
  };
30640
31566
  const getter = script[pathname];
30641
31567
  if (getter) {
@@ -31800,6 +32726,7 @@ COMMANDS
31800
32726
  setup
31801
32727
  init Create ~/.metahub (SQLite db + cache dir)
31802
32728
  init --claude Install the /mh skill into ~/.claude (Claude Code)
32729
+ init --codex Install the $mh skill into ~/.codex (Codex)
31803
32730
  databases
31804
32731
  db create <name> [--icon] Create a database (table)
31805
32732
  db list | get <ref> | delete <ref>
@@ -31879,6 +32806,7 @@ var EXAMPLES = {
31879
32806
  init: [
31880
32807
  "mh init",
31881
32808
  "mh init --claude # install the /mh skill into ~/.claude (Claude Code)",
32809
+ "mh init --codex # install the $mh skill into ~/.codex (Codex)",
31882
32810
  "METAHUB_HOME=/tmp/mh mh init # use an alternate home"
31883
32811
  ],
31884
32812
  "db create": ['mh db create "Tasks"', 'mh db create "Notes" --icon \uD83D\uDCD3'],
@@ -32077,4 +33005,4 @@ if (argv2.includes("--server")) {
32077
33005
  runMain(main, { showUsage: showUsage2 });
32078
33006
  }
32079
33007
 
32080
- //# debugId=DFF4B33AB2E4208164756E2164756E21
33008
+ //# debugId=6B3DFB027376050D64756E2164756E21