@tensorix/metahub 0.2.2 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +8 -1
  2. package/dist/cli.js +1309 -275
  3. package/dist/cli.js.map +21 -17
  4. package/dist/core/databases.d.ts +5 -1
  5. package/dist/core/databases.d.ts.map +1 -1
  6. package/dist/core/documents.d.ts +22 -0
  7. package/dist/core/documents.d.ts.map +1 -1
  8. package/dist/core/md/grammar.d.ts +77 -0
  9. package/dist/core/md/grammar.d.ts.map +1 -0
  10. package/dist/core/md/heal.d.ts +2 -0
  11. package/dist/core/md/heal.d.ts.map +1 -0
  12. package/dist/core/md/inline.d.ts +20 -0
  13. package/dist/core/md/inline.d.ts.map +1 -0
  14. package/dist/core/schema-init.d.ts +13 -0
  15. package/dist/core/schema-init.d.ts.map +1 -1
  16. package/dist/core/schema.d.ts +1 -1
  17. package/dist/core/schema.d.ts.map +1 -1
  18. package/dist/core/sync/auth.d.ts +3 -1
  19. package/dist/core/sync/auth.d.ts.map +1 -1
  20. package/dist/core/sync/share-render.d.ts +3 -1
  21. package/dist/core/sync/share-render.d.ts.map +1 -1
  22. package/dist/db-worker.js +51 -41
  23. package/dist/index.js +1201 -260
  24. package/dist/index.js.map +16 -12
  25. package/dist/mh-runtime.js +1 -1
  26. package/dist/sw.js +1 -1
  27. package/dist/webui/fmt/manifest.d.ts +17 -0
  28. package/dist/webui/fmt/manifest.d.ts.map +1 -0
  29. package/dist/webui/server/assets.d.ts +5 -0
  30. package/dist/webui/server/assets.d.ts.map +1 -1
  31. package/dist/webui-fmt-clang.js +1 -0
  32. package/dist/webui-fmt-clang.wasm +0 -0
  33. package/dist/webui-fmt-gofmt.js +1 -0
  34. package/dist/webui-fmt-gofmt.wasm +0 -0
  35. package/dist/webui-fmt-lua.js +2 -0
  36. package/dist/webui-fmt-lua.wasm +0 -0
  37. package/dist/webui-fmt-ruff.js +2 -0
  38. package/dist/webui-fmt-ruff.wasm +0 -0
  39. package/dist/webui-fmt-sh.js +8 -0
  40. package/dist/webui-fmt-taplo.js +2 -0
  41. package/dist/webui-fmt-taplo.wasm +0 -0
  42. package/dist/webui-fmt.js +425 -0
  43. package/dist/webui.js +148 -75
  44. package/package.json +15 -1
package/dist/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();
@@ -3083,19 +3099,52 @@ var init_documents = __esm(() => {
3083
3099
 
3084
3100
  `)) {
3085
3101
  const affected = blocks.filter((b2) => b2.text.includes(opts.old));
3086
- const next = affected.map((b2) => opts.replaceAll ? b2.text.replaceAll(opts.old, opts.new) : b2.text.replace(opts.old, opts.new));
3102
+ const next = affected.map((b2) => opts.replaceAll ? b2.text.replaceAll(opts.old, () => opts.new) : b2.text.replace(opts.old, () => opts.new));
3087
3103
  if (next.every((t2) => t2.length > 0 && !t2.includes(`
3088
3104
 
3089
3105
  `))) {
3090
3106
  const targets = opts.replaceAll ? affected : affected.slice(0, 1);
3091
- targets.forEach((b2, i2) => emit(db, "doc_blocks", b2.id, "text", next[i2]));
3092
- return { id, changed: true, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
3107
+ let wrote = false;
3108
+ targets.forEach((b2, i2) => {
3109
+ if (next[i2] !== b2.text) {
3110
+ emit(db, "doc_blocks", b2.id, "text", next[i2]);
3111
+ wrote = true;
3112
+ }
3113
+ });
3114
+ return { id, changed: wrote, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
3093
3115
  }
3094
3116
  }
3095
- const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
3117
+ const newBody = opts.replaceAll ? body.replaceAll(opts.old, () => opts.new) : body.replace(opts.old, () => opts.new);
3096
3118
  reconcileBody(db, id, newBody);
3097
3119
  return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
3098
3120
  });
3121
+ editDocumentBatch = grouped(function editDocumentBatch2(db, id, opts) {
3122
+ if (!getDocument(db, id))
3123
+ throw new MhError("not_found", `no such document: ${id}`);
3124
+ if (opts.edits.length === 0)
3125
+ throw new MhError("invalid_input", "no edits given");
3126
+ if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
3127
+ throw new MhError("stale", `stale: document changed since ${opts.ifMatch}; re-read first`);
3128
+ ensureBlocks(db, id);
3129
+ const orig = serializeDocBlocks(liveBlocks(db, id).map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })));
3130
+ let body = orig;
3131
+ let total = 0;
3132
+ opts.edits.forEach((e2, i2) => {
3133
+ if (e2.old === "")
3134
+ throw new MhError("invalid_input", `edit[${i2}]: old must not be empty`);
3135
+ const count = countOccurrences(body, e2.old);
3136
+ if (count === 0)
3137
+ throw new MhError("invalid_input", `edit[${i2}]: anchor not found: no match for old`);
3138
+ if (count > 1 && !e2.replaceAll)
3139
+ throw new MhError("ambiguous", `edit[${i2}]: ${count} matches for old; add surrounding context or set replaceAll`);
3140
+ const repl = e2.new ?? "";
3141
+ body = e2.replaceAll ? body.replaceAll(e2.old, () => repl) : body.replace(e2.old, () => repl);
3142
+ total += e2.replaceAll ? count : 1;
3143
+ });
3144
+ if (body !== orig)
3145
+ reconcileBody(db, id, body);
3146
+ return { id, changed: body !== orig, replaced: total, version: documentVersion(db, id) };
3147
+ });
3099
3148
  appendDocument = grouped(function appendDocument2(db, id, body) {
3100
3149
  if (!getDocument(db, id))
3101
3150
  throw new MhError("not_found", `no such document: ${id}`);
@@ -19646,38 +19695,382 @@ var init_sites_serve = __esm(() => {
19646
19695
  init_sites();
19647
19696
  });
19648
19697
 
19698
+ // src/core/md/grammar.ts
19699
+ function safeUrl(url2, opts = {}) {
19700
+ const probe = url2.replace(/[\u0000-\u0020]+/g, "");
19701
+ const m3 = /^([a-z][a-z0-9+.-]*):/i.exec(probe);
19702
+ if (!m3)
19703
+ return url2;
19704
+ const scheme = m3[1].toLowerCase();
19705
+ if (scheme === "http" || scheme === "https" || scheme === "mailto" || scheme === "blob")
19706
+ return url2;
19707
+ if (scheme === "data" && opts.allowData)
19708
+ return url2;
19709
+ return "#";
19710
+ }
19711
+ function extOf(url2) {
19712
+ const clean = url2.split(/[?#]/, 1)[0] ?? url2;
19713
+ const base = clean.slice(clean.lastIndexOf("/") + 1);
19714
+ const dot = base.lastIndexOf(".");
19715
+ return dot >= 0 ? base.slice(dot + 1).toLowerCase() : "";
19716
+ }
19717
+ function imageSyntaxKind(ext2) {
19718
+ if (VIDEO_EXTS.has(ext2))
19719
+ return "video";
19720
+ if (AUDIO_EXTS.has(ext2))
19721
+ return "audio";
19722
+ return "image";
19723
+ }
19724
+ function parseMediaUrl(url2) {
19725
+ const hash3 = url2.indexOf("#");
19726
+ const frag = hash3 >= 0 ? url2.slice(hash3) : "";
19727
+ const noFrag = hash3 >= 0 ? url2.slice(0, hash3) : url2;
19728
+ const q3 = noFrag.indexOf("?");
19729
+ if (q3 < 0)
19730
+ return { src: url2 };
19731
+ const base = noFrag.slice(0, q3);
19732
+ let width;
19733
+ const kept = [];
19734
+ for (const p2 of noFrag.slice(q3 + 1).split("&").filter(Boolean)) {
19735
+ const m3 = p2.match(/^w=(\d+)$/);
19736
+ if (m3)
19737
+ width = parseInt(m3[1], 10);
19738
+ else
19739
+ kept.push(p2);
19740
+ }
19741
+ return { src: base + (kept.length ? "?" + kept.join("&") : "") + frag, width };
19742
+ }
19743
+ function matchMediaEmbed(line) {
19744
+ const t2 = line.trim();
19745
+ let m3 = t2.match(/^!\[([^\]]*)\]\(([^\s)]+)\)$/);
19746
+ if (m3) {
19747
+ const { src: src2, width } = parseMediaUrl(m3[2]);
19748
+ const kind = imageSyntaxKind(extOf(src2));
19749
+ const r3 = { kind, src: src2, name: m3[1] };
19750
+ if (kind === "image" && width)
19751
+ r3.width = width;
19752
+ return r3;
19753
+ }
19754
+ m3 = t2.match(/^\[([^\]]*)\]\(([^\s)]+)(?:\s+"([^"]*)")?\)$/);
19755
+ if (m3 && m3[2].startsWith("/blob/")) {
19756
+ const r3 = { kind: "file", src: m3[2], name: m3[1] };
19757
+ if (m3[3] != null && /^\d+$/.test(m3[3]))
19758
+ r3.size = parseInt(m3[3], 10);
19759
+ return r3;
19760
+ }
19761
+ return null;
19762
+ }
19763
+ function matchQuoteLine(text) {
19764
+ const m3 = text.match(RE.quote);
19765
+ return m3 ? m3[1] : null;
19766
+ }
19767
+ function matchListLine(line, minIndent) {
19768
+ const indent = leadingIndent(line);
19769
+ if (indent < minIndent)
19770
+ return null;
19771
+ const text = stripIndent(line, indent);
19772
+ let m3 = text.match(/^[-*+]\s+\[([ xX])\][ \t](.*)$/);
19773
+ if (m3) {
19774
+ return {
19775
+ indent,
19776
+ type: "todo",
19777
+ checked: m3[1].toLowerCase() === "x",
19778
+ content: m3[2]
19779
+ };
19780
+ }
19781
+ m3 = text.match(/^(\d+)[.)][ \t]+(.*)$/);
19782
+ if (m3)
19783
+ return { indent, type: "numbered", content: m3[2], num: parseInt(m3[1], 10) };
19784
+ m3 = text.match(/^[-*+][ \t]+(.*)$/);
19785
+ if (m3)
19786
+ return { indent, type: "bullet", content: m3[1] };
19787
+ return null;
19788
+ }
19789
+ function leadingIndent(line) {
19790
+ let indent = 0;
19791
+ for (const ch of line) {
19792
+ if (ch === " ")
19793
+ indent++;
19794
+ else if (ch === "\t")
19795
+ indent += 4;
19796
+ else
19797
+ break;
19798
+ }
19799
+ return indent;
19800
+ }
19801
+ function stripIndent(line, columns) {
19802
+ let indent = 0;
19803
+ let i2 = 0;
19804
+ for (;i2 < line.length && indent < columns; i2++) {
19805
+ const ch = line[i2];
19806
+ if (ch === " ")
19807
+ indent++;
19808
+ else if (ch === "\t")
19809
+ indent += 4;
19810
+ else
19811
+ break;
19812
+ }
19813
+ return line.slice(i2);
19814
+ }
19815
+ function isFenceClose(line, char, len) {
19816
+ const trimmed = line.trim();
19817
+ if (!trimmed || trimmed[0] !== char)
19818
+ return false;
19819
+ for (const ch of trimmed) {
19820
+ if (ch !== char)
19821
+ return false;
19822
+ }
19823
+ return trimmed.length >= len;
19824
+ }
19825
+ function cleanLang(lang) {
19826
+ return lang.trim().replace(/[^A-Za-z0-9_+.#-]/g, "");
19827
+ }
19828
+ function looksLikeTableAt(lines, i2, minIndent) {
19829
+ const head = lines[i2];
19830
+ const delim = lines[i2 + 1];
19831
+ if (head == null || delim == null)
19832
+ return false;
19833
+ if (leadingIndent(head) < minIndent || leadingIndent(delim) < minIndent)
19834
+ return false;
19835
+ const headText = stripIndent(head, minIndent);
19836
+ const delimText = stripIndent(delim, minIndent);
19837
+ if (!headText.includes("|") || !delimText.includes("|"))
19838
+ return false;
19839
+ const cells = splitTableRow(delimText);
19840
+ return cells.length > 0 && cells.every((c3) => RE_DELIM_CELL.test(c3));
19841
+ }
19842
+ function splitTableRow(line) {
19843
+ const cells = [];
19844
+ let cur = "";
19845
+ for (let i2 = 0;i2 < line.length; i2++) {
19846
+ const ch = line[i2];
19847
+ if (ch === "\\" && line[i2 + 1] === "|") {
19848
+ cur += "|";
19849
+ i2++;
19850
+ continue;
19851
+ }
19852
+ if (ch === "|") {
19853
+ cells.push(cur);
19854
+ cur = "";
19855
+ continue;
19856
+ }
19857
+ cur += ch;
19858
+ }
19859
+ cells.push(cur);
19860
+ if (cells.length && cells[0].trim() === "")
19861
+ cells.shift();
19862
+ if (cells.length && cells[cells.length - 1].trim() === "")
19863
+ cells.pop();
19864
+ return cells.map((c3) => c3.trim());
19865
+ }
19866
+ var HTML_FENCE = "mh-html", RE, VIDEO_EXTS, AUDIO_EXTS, RE_DELIM_CELL;
19867
+ var init_grammar = __esm(() => {
19868
+ RE = {
19869
+ fenceOpen: /^\s*(`{3,}|~{3,})\s*([^\s`]*)?.*$/,
19870
+ h: /^(#{1,6})\s+(.*)$/,
19871
+ todo: /^\s*[-*+]\s+\[([ xX])\][ \t](.*)$/,
19872
+ bullet: /^\s*[-*+]\s+(.*)$/,
19873
+ numbered: /^\s*(\d+)[.)]\s+(.*)$/,
19874
+ quote: /^>[ \t](.*)$/,
19875
+ divider: /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/
19876
+ };
19877
+ VIDEO_EXTS = new Set(["mp4", "webm", "mov", "m4v", "ogv", "mkv", "ogg"]);
19878
+ AUDIO_EXTS = new Set(["mp3", "wav", "m4a", "aac", "flac", "opus", "oga", "weba"]);
19879
+ RE_DELIM_CELL = /^\s*:?-+:?\s*$/;
19880
+ });
19881
+
19882
+ // src/core/md/inline.ts
19883
+ function tokenizeInline(text) {
19884
+ if (!text)
19885
+ return [];
19886
+ const cands = [];
19887
+ for (const p2 of PATTERNS) {
19888
+ p2.re.lastIndex = 0;
19889
+ let m3;
19890
+ while (m3 = p2.re.exec(text)) {
19891
+ const start = m3.index;
19892
+ const end = start + m3[0].length;
19893
+ let innerFrom;
19894
+ let innerTo;
19895
+ let url2;
19896
+ let alt;
19897
+ if (p2.kind === "link") {
19898
+ innerFrom = start + 1;
19899
+ innerTo = start + 1 + m3[1].length;
19900
+ url2 = m3[2];
19901
+ } else if (p2.kind === "image") {
19902
+ innerFrom = start + 2;
19903
+ innerTo = start + 2 + m3[1].length;
19904
+ url2 = m3[2];
19905
+ alt = m3[1];
19906
+ } else {
19907
+ innerFrom = start + p2.delim;
19908
+ innerTo = end - p2.delim;
19909
+ if (innerTo <= innerFrom)
19910
+ continue;
19911
+ }
19912
+ cands.push({ kind: p2.kind, start, end, innerFrom, innerTo, url: url2, alt, rank: p2.rank });
19913
+ }
19914
+ }
19915
+ cands.sort((a2, b3) => a2.start - b3.start || a2.rank - b3.rank || b3.end - a2.end);
19916
+ const out = [];
19917
+ let cursor = 0;
19918
+ for (const c3 of cands) {
19919
+ if (c3.start < cursor)
19920
+ continue;
19921
+ cursor = c3.end;
19922
+ out.push({
19923
+ kind: c3.kind,
19924
+ start: c3.start,
19925
+ end: c3.end,
19926
+ innerFrom: c3.innerFrom,
19927
+ innerTo: c3.innerTo,
19928
+ ...c3.url !== undefined ? { url: c3.url } : {},
19929
+ ...c3.alt !== undefined ? { alt: c3.alt } : {}
19930
+ });
19931
+ }
19932
+ return out;
19933
+ }
19934
+ var PATTERNS;
19935
+ var init_inline = __esm(() => {
19936
+ PATTERNS = [
19937
+ { kind: "code", re: /(?<!\\)`([^`\n]+)`/g, delim: 1, rank: 0 },
19938
+ { kind: "image", re: /(?<!\\)!\[([^\]\n]*)\]\(([^)\s]+)\)/g, delim: 0, rank: 1 },
19939
+ { kind: "strong", re: /(?<!\\)\*\*([^*\n]+?)(?<!\\)\*\*/g, delim: 2, rank: 2 },
19940
+ { kind: "strong", re: /(?<!\\)__([^_\n]+?)(?<!\\)__/g, delim: 2, rank: 2 },
19941
+ { kind: "del", re: /(?<!\\)~~([^~\n]+?)(?<!\\)~~/g, delim: 2, rank: 3 },
19942
+ { kind: "em", re: /(?<![*\w])(?<!\\)\*([^*\n]+?)(?<!\\)\*(?!\*)/g, delim: 1, rank: 4 },
19943
+ { kind: "em", re: /(?<![_\w])(?<!\\)_([^_\n]+?)(?<!\\)_(?!_)/g, delim: 1, rank: 4 },
19944
+ { kind: "link", re: /(?<!\\)\[([^\]\n]+)\]\(([^)\s]+)\)/g, delim: 0, rank: 5 }
19945
+ ];
19946
+ });
19947
+
19948
+ // src/core/md/heal.ts
19949
+ function fenceMask(lines) {
19950
+ const mask = new Array(lines.length).fill(false);
19951
+ for (let i2 = 0;i2 < lines.length; i2++) {
19952
+ const m3 = lines[i2].match(RE.fenceOpen);
19953
+ if (!m3)
19954
+ continue;
19955
+ const ch = m3[1][0];
19956
+ const len = m3[1].length;
19957
+ let close = -1;
19958
+ for (let j2 = i2 + 1;j2 < lines.length; j2++) {
19959
+ if (isFenceClose(lines[j2], ch, len)) {
19960
+ close = j2;
19961
+ break;
19962
+ }
19963
+ }
19964
+ if (close === -1)
19965
+ continue;
19966
+ for (let j2 = i2;j2 <= close; j2++)
19967
+ mask[j2] = true;
19968
+ i2 = close;
19969
+ }
19970
+ return mask;
19971
+ }
19972
+ function healLegacyMarkdown(src2) {
19973
+ if (!MAYBE_LEGACY.test(src2))
19974
+ return src2;
19975
+ const lines = src2.split(`
19976
+ `);
19977
+ const inFence = fenceMask(lines);
19978
+ const quoteish = (i2) => i2 >= 0 && i2 < lines.length && !inFence[i2] && matchQuoteLine(lines[i2].replace(/^[ \t]*/, "")) !== null;
19979
+ let changed = false;
19980
+ for (let i2 = 0;i2 < lines.length; i2++) {
19981
+ if (inFence[i2])
19982
+ continue;
19983
+ const line = lines[i2];
19984
+ if (LEGACY_EMPTY_TODO.test(line)) {
19985
+ lines[i2] = line + " ";
19986
+ changed = true;
19987
+ } else if (BARE_QUOTE.test(line) && (quoteish(i2 - 1) || quoteish(i2 + 1))) {
19988
+ lines[i2] = line + " ";
19989
+ changed = true;
19990
+ }
19991
+ }
19992
+ for (let i2 = lines.length - 1;i2 >= 0; i2--) {
19993
+ if (inFence[i2])
19994
+ continue;
19995
+ const line = lines[i2];
19996
+ if (BARE_QUOTE.test(line) && (quoteish(i2 - 1) || quoteish(i2 + 1))) {
19997
+ lines[i2] = line + " ";
19998
+ changed = true;
19999
+ }
20000
+ }
20001
+ return changed ? lines.join(`
20002
+ `) : src2;
20003
+ }
20004
+ var LEGACY_EMPTY_TODO, BARE_QUOTE, MAYBE_LEGACY;
20005
+ var init_heal = __esm(() => {
20006
+ init_grammar();
20007
+ LEGACY_EMPTY_TODO = /^[ \t]*[-*+][ \t]+\[[ xX]\]$/;
20008
+ BARE_QUOTE = /^[ \t]*>$/;
20009
+ MAYBE_LEGACY = /^[ \t]*(?:[-*+][ \t]+\[[ xX]\]|>)$/m;
20010
+ });
20011
+
19649
20012
  // src/core/sync/share-render.ts
19650
20013
  function escapeHtml(s2) {
19651
20014
  return s2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19652
20015
  }
19653
20016
  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;
20017
+ const tokens = tokenizeInline(src2);
20018
+ let out = "";
20019
+ let pos = 0;
20020
+ for (const t2 of tokens) {
20021
+ out += escapeHtml(src2.slice(pos, t2.start));
20022
+ const inner = src2.slice(t2.innerFrom, t2.innerTo);
20023
+ const rawUrl = rewrite(t2.url ?? "", opts);
20024
+ switch (t2.kind) {
20025
+ case "code":
20026
+ out += `<code>${escapeHtml(inner)}</code>`;
20027
+ break;
20028
+ case "strong":
20029
+ out += `<strong>${escapeHtml(inner)}</strong>`;
20030
+ break;
20031
+ case "em":
20032
+ out += `<em>${escapeHtml(inner)}</em>`;
20033
+ break;
20034
+ case "del":
20035
+ out += `<del>${escapeHtml(inner)}</del>`;
20036
+ break;
20037
+ case "link": {
20038
+ const href = escapeHtml(safeUrl(rawUrl));
20039
+ out += `<a href="${href}" target="_blank" rel="noreferrer noopener">${escapeHtml(inner)}</a>`;
20040
+ break;
20041
+ }
20042
+ case "image": {
20043
+ const imgSrc = escapeHtml(safeUrl(rawUrl, { allowData: true }));
20044
+ out += `<img src="${imgSrc}" alt="${escapeHtml(t2.alt ?? "")}" loading="lazy">`;
20045
+ break;
20046
+ }
20047
+ }
20048
+ pos = t2.end;
20049
+ }
20050
+ return out + escapeHtml(src2.slice(pos));
19672
20051
  }
19673
20052
  function rewrite(url2, opts) {
19674
20053
  if (opts.rewriteBlob && url2.startsWith("/blob/"))
19675
20054
  return opts.rewriteBlob(url2);
19676
20055
  return url2;
19677
20056
  }
20057
+ function renderMedia(m3, opts) {
20058
+ const raw = rewrite(m3.src, opts);
20059
+ if (m3.kind === "video") {
20060
+ const src2 = escapeHtml(safeUrl(raw, { allowData: true }));
20061
+ return `<p class="mh-media"><video src="${src2}" controls preload="metadata"></video></p>`;
20062
+ }
20063
+ if (m3.kind === "audio") {
20064
+ const src2 = escapeHtml(safeUrl(raw, { allowData: true }));
20065
+ return `<p class="mh-media"><audio src="${src2}" controls preload="metadata"></audio></p>`;
20066
+ }
20067
+ const href = escapeHtml(safeUrl(raw));
20068
+ return `<p class="mh-file"><a href="${href}" download>${escapeHtml(m3.name || "file")}</a></p>`;
20069
+ }
19678
20070
  function renderMarkdown(md, opts = {}) {
19679
- const lines = (md ?? "").replace(/\r\n?/g, `
19680
- `).split(`
20071
+ const healed = healLegacyMarkdown((md ?? "").replace(/\r\n?/g, `
20072
+ `));
20073
+ const lines = healed.split(`
19681
20074
  `);
19682
20075
  const out = [];
19683
20076
  let i2 = 0;
@@ -19690,38 +20083,39 @@ function renderMarkdown(md, opts = {}) {
19690
20083
  };
19691
20084
  while (i2 < lines.length) {
19692
20085
  const line = lines[i2];
19693
- const fence = line.match(FENCE);
20086
+ const fence = line.match(RE.fenceOpen);
19694
20087
  if (fence) {
19695
- flushPara();
19696
20088
  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++;
20089
+ const len = fence[1].length;
20090
+ let close = -1;
20091
+ for (let j2 = i2 + 1;j2 < lines.length; j2++) {
20092
+ if (isFenceClose(lines[j2], marker, len)) {
20093
+ close = j2;
19704
20094
  break;
19705
20095
  }
19706
- body.push(lines[i2]);
19707
- i2++;
19708
20096
  }
19709
- if (info === "mh-html") {
19710
- out.push(`<iframe class="mh-embed" sandbox="allow-scripts allow-popups" srcdoc="${escapeHtml(body.join(`
20097
+ if (close !== -1) {
20098
+ flushPara();
20099
+ const info = cleanLang(fence[2] ?? "");
20100
+ const body = lines.slice(i2 + 1, close);
20101
+ i2 = close + 1;
20102
+ if (info === HTML_FENCE) {
20103
+ out.push(`<iframe class="mh-embed" sandbox="allow-scripts allow-popups" srcdoc="${escapeHtml(body.join(`
19711
20104
  `))}"></iframe>`);
19712
- } else {
19713
- const cls = info ? ` class="language-${escapeHtml(info)}"` : "";
19714
- out.push(`<pre><code${cls}>${escapeHtml(body.join(`
20105
+ } else {
20106
+ const cls = info ? ` class="language-${escapeHtml(info)}"` : "";
20107
+ out.push(`<pre><code${cls}>${escapeHtml(body.join(`
19715
20108
  `))}</code></pre>`);
20109
+ }
20110
+ continue;
19716
20111
  }
19717
- continue;
19718
20112
  }
19719
20113
  if (line.trim() === "") {
19720
20114
  flushPara();
19721
20115
  i2++;
19722
20116
  continue;
19723
20117
  }
19724
- const h3 = line.match(/^(#{1,6})\s+(.*)$/);
20118
+ const h3 = stripLead(line).match(RE.h);
19725
20119
  if (h3) {
19726
20120
  flushPara();
19727
20121
  const level = h3[1].length;
@@ -19729,30 +20123,33 @@ function renderMarkdown(md, opts = {}) {
19729
20123
  i2++;
19730
20124
  continue;
19731
20125
  }
19732
- if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
20126
+ if (RE.divider.test(line)) {
19733
20127
  flushPara();
19734
20128
  out.push("<hr>");
19735
20129
  i2++;
19736
20130
  continue;
19737
20131
  }
19738
- if (/^\s*>\s?/.test(line)) {
20132
+ if (matchQuoteLine(stripLead(line)) !== null) {
19739
20133
  flushPara();
19740
20134
  const q3 = [];
19741
- while (i2 < lines.length && /^\s*>\s?/.test(lines[i2])) {
19742
- q3.push(lines[i2].replace(/^\s*>\s?/, ""));
20135
+ while (i2 < lines.length) {
20136
+ const content = matchQuoteLine(stripLead(lines[i2]));
20137
+ if (content === null)
20138
+ break;
20139
+ q3.push(content);
19743
20140
  i2++;
19744
20141
  }
19745
20142
  out.push(`<blockquote>${renderMarkdown(q3.join(`
19746
20143
  `), opts)}</blockquote>`);
19747
20144
  continue;
19748
20145
  }
19749
- if (line.includes("|") && i2 + 1 < lines.length && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i2 + 1]) && lines[i2 + 1].includes("-")) {
20146
+ if (looksLikeTableAt(lines, i2, 0)) {
19750
20147
  flushPara();
19751
20148
  const rows = [];
19752
- const head = splitRow(line);
20149
+ const head = splitTableRow(stripLead(lines[i2]));
19753
20150
  i2 += 2;
19754
20151
  while (i2 < lines.length && lines[i2].includes("|") && lines[i2].trim() !== "") {
19755
- rows.push(splitRow(lines[i2]));
20152
+ rows.push(splitTableRow(stripLead(lines[i2])));
19756
20153
  i2++;
19757
20154
  }
19758
20155
  const th = head.map((c3) => `<th>${renderInline(c3, opts)}</th>`).join("");
@@ -19760,24 +20157,31 @@ function renderMarkdown(md, opts = {}) {
19760
20157
  out.push(`<table><thead><tr>${th}</tr></thead><tbody>${body}</tbody></table>`);
19761
20158
  continue;
19762
20159
  }
19763
- const ul = line.match(/^\s*[-*+]\s+/);
19764
- const ol = line.match(/^\s*\d+[.)]\s+/);
19765
- if (ul || ol) {
20160
+ const first = matchListLine(line, 0);
20161
+ if (first) {
19766
20162
  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, ""));
20163
+ const ordered = first.type === "numbered";
20164
+ const lis = [];
20165
+ while (i2 < lines.length) {
20166
+ const it3 = matchListLine(lines[i2], 0);
20167
+ if (!it3 || it3.type === "numbered" !== ordered)
20168
+ break;
20169
+ if (it3.type === "todo") {
20170
+ lis.push(`<li class="todo"><input type="checkbox" disabled${it3.checked ? " checked" : ""}> ${renderInline(it3.content, opts)}</li>`);
20171
+ } else if (it3.type === "numbered" && it3.num !== undefined) {
20172
+ lis.push(`<li value="${it3.num}">${renderInline(it3.content, opts)}</li>`);
20173
+ } else {
20174
+ lis.push(`<li>${renderInline(it3.content, opts)}</li>`);
20175
+ }
19772
20176
  i2++;
19773
20177
  }
19774
- const lis = items.map((t2) => `<li>${renderInline(t2, opts)}</li>`).join("");
19775
- out.push(ordered ? `<ol>${lis}</ol>` : `<ul>${lis}</ul>`);
20178
+ out.push(ordered ? `<ol>${lis.join("")}</ol>` : `<ul>${lis.join("")}</ul>`);
19776
20179
  continue;
19777
20180
  }
19778
- if (/^\s*!\[[^\]]*\]\([^)\s]+\)\s*$/.test(line)) {
20181
+ const media = matchMediaEmbed(line);
20182
+ if (media) {
19779
20183
  flushPara();
19780
- out.push(`<p class="mh-img">${renderInline(line.trim(), opts)}</p>`);
20184
+ out.push(media.kind === "image" ? `<p class="mh-img">${renderInline(line.trim(), opts)}</p>` : renderMedia(media, opts));
19781
20185
  i2++;
19782
20186
  continue;
19783
20187
  }
@@ -19788,20 +20192,13 @@ function renderMarkdown(md, opts = {}) {
19788
20192
  return out.join(`
19789
20193
  `);
19790
20194
  }
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());
20195
+ function stripLead(line) {
20196
+ return stripIndent(line, Number.MAX_SAFE_INTEGER);
19798
20197
  }
19799
- var A4, B3, RESTORE, FENCE;
19800
20198
  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,})(.*)$/;
20199
+ init_grammar();
20200
+ init_inline();
20201
+ init_heal();
19805
20202
  });
19806
20203
 
19807
20204
  // src/core/sync/share-serve.ts
@@ -20219,6 +20616,8 @@ function pageShell(title, inner, opts = {}) {
20219
20616
  td[contenteditable]{outline:none} td.saving{opacity:.5} td.saved{background:rgba(46,160,67,.18)}
20220
20617
  .tag{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:999px;padding:1px 9px;font-size:12px}
20221
20618
  .muted{color:var(--muted)} .mh-img{text-align:center}
20619
+ .mh-media{text-align:center} .mh-media video,.mh-media audio{max-width:100%}
20620
+ .mh-file a{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px 14px;color:var(--accent);text-decoration:none}
20222
20621
  iframe.mh-embed{width:100%;border:1px solid var(--line);border-radius:8px;min-height:240px}
20223
20622
  textarea.mh-raw{width:100%;min-height:60vh;font:14px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--fg)}
20224
20623
  ul.files{list-style:none;padding:0} ul.files li{padding:4px 0} button.file{background:none;border:0;color:var(--accent);cursor:pointer;font-size:15px;padding:0}
@@ -21657,7 +22056,7 @@ async function runMain(cmd, opts = {}) {
21657
22056
  // package.json
21658
22057
  var package_default = {
21659
22058
  name: "@tensorix/metahub",
21660
- version: "0.2.2",
22059
+ version: "0.3.2",
21661
22060
  description: "Metahub core library and CLI",
21662
22061
  license: "AGPL-3.0-only",
21663
22062
  type: "module",
@@ -21687,16 +22086,21 @@ var package_default = {
21687
22086
  "build:shell": "bun run scripts/build-shell.ts",
21688
22087
  "build:share-viewer": "bun run scripts/build-share-viewer.ts",
21689
22088
  "build:binaries": "bun run scripts/compile-binaries.ts",
22089
+ "build:binaries:host": "HOST_ONLY=1 bun run scripts/compile-binaries.ts",
21690
22090
  prepublishOnly: "bun run build",
21691
22091
  test: "bun test",
21692
22092
  release: "bun run scripts/tag-release.ts"
21693
22093
  },
21694
22094
  dependencies: {
21695
22095
  "@clack/prompts": "^1.5.0",
22096
+ "@codemirror/commands": "6.10.4",
22097
+ "@codemirror/state": "6.7.0",
22098
+ "@codemirror/view": "6.43.4",
21696
22099
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
21697
22100
  aws4fetch: "^1.0.20",
21698
22101
  citty: "^0.1.6",
21699
22102
  "highlight.js": "^11.11.1",
22103
+ jsqr: "^1.4.0",
21700
22104
  preact: "^10.29.2",
21701
22105
  "qrcode-generator": "^2.0.4",
21702
22106
  turndown: "^7.2.4",
@@ -21705,8 +22109,17 @@ var package_default = {
21705
22109
  },
21706
22110
  devDependencies: {
21707
22111
  "@happy-dom/global-registrator": "^20.10.2",
22112
+ "@prettier/plugin-php": "^0.25.0",
21708
22113
  "@types/bun": "latest",
21709
22114
  "@types/turndown": "^5.0.6",
22115
+ "@wasm-fmt/clang-format": "^22.1.8",
22116
+ "@wasm-fmt/gofmt": "^0.7.3",
22117
+ "@wasm-fmt/lua_fmt": "^0.3.3",
22118
+ "@wasm-fmt/ruff_fmt": "^0.15.20",
22119
+ "@wasm-fmt/taplo_fmt": "^0.2.5",
22120
+ "mvdan-sh": "^0.10.1",
22121
+ prettier: "^3.6",
22122
+ "sql-formatter": "^15",
21710
22123
  typescript: "^5"
21711
22124
  }
21712
22125
  };
@@ -21850,6 +22263,7 @@ CREATE TABLE IF NOT EXISTS databases (
21850
22263
  id TEXT PRIMARY KEY,
21851
22264
  name TEXT,
21852
22265
  icon TEXT,
22266
+ meta TEXT, -- JSON: generic replicated metadata (e.g. UI flags), one LWW register
21853
22267
  created_hlc TEXT,
21854
22268
  __deleted INTEGER NOT NULL DEFAULT 0
21855
22269
  );
@@ -22132,9 +22546,25 @@ function migrateStoragePeerUrls(db) {
22132
22546
  });
22133
22547
  tx();
22134
22548
  }
22549
+ function migrateDatabases(db) {
22550
+ if (hasColumn(db, "databases", "meta"))
22551
+ return;
22552
+ db.exec("ALTER TABLE databases ADD COLUMN meta TEXT");
22553
+ db.exec(`
22554
+ UPDATE databases SET meta = (
22555
+ SELECT c.value FROM crdt_changes c
22556
+ WHERE c.dataset = 'databases' AND c.col = 'meta' AND c.row_id = databases.id
22557
+ ORDER BY c.hlc DESC LIMIT 1
22558
+ )
22559
+ WHERE id IN (
22560
+ SELECT DISTINCT row_id FROM crdt_changes WHERE dataset = 'databases' AND col = 'meta'
22561
+ )
22562
+ `);
22563
+ }
22135
22564
  function initSchema(db) {
22136
22565
  runSchema(db);
22137
22566
  migrateOplog(db);
22567
+ migrateDatabases(db);
22138
22568
  migrateCrdtChangesSeq(db);
22139
22569
  migrateRecords(db);
22140
22570
  migratePeers(db);
@@ -22378,6 +22808,18 @@ mh doc edit <ref> --old "old text" --new "new text"
22378
22808
  mh doc edit <ref> --old "TODO" --new "DONE" --replace-all
22379
22809
  mh doc edit <ref> --old "..." --new "..." --if-match <version-from-read>
22380
22810
 
22811
+ # EDIT (batch): apply several pairs in ONE atomic call \u2014 prefer this for many
22812
+ # scattered deltas. One --if-match check, one revision, no re-read between edits.
22813
+ # Pairs fold in order (a later "old" can match an earlier "new"); if ANY pair's
22814
+ # anchor is missing/ambiguous the whole batch aborts and the doc is untouched.
22815
+ mh doc edit <ref> --if-match <version> --edits @- <<'JSON'
22816
+ [{"old":"(draft)","new":""},
22817
+ {"old":"status: todo","new":"status: done"},
22818
+ {"old":"FIXME","new":"FIXED","replaceAll":true}]
22819
+ JSON
22820
+ # Keep anchors in prose. Don't let --old/--new (or a batch pair) cut into the
22821
+ # interior of a table, code fence, or media block, or the block won't round-trip.
22822
+
22381
22823
  # WRITE more: add blocks at the tail or head
22382
22824
  mh doc append <ref> --body "a new paragraph"
22383
22825
  mh doc prepend <ref> --body "## Summary"
@@ -22393,8 +22835,11 @@ Required agent loop for revising a document (treat \`--if-match\` as mandatory \
22393
22835
  without it a concurrent edit from another session/the WebUI is silently
22394
22836
  clobbered):
22395
22837
  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>\`.
22838
+ 2. Compute the minimal \`--old\`/\`--new\` pair(s) (unique anchor text). Revising
22839
+ several spots at once? Collect them into one \`--edits\` batch instead of
22840
+ editing serially \u2014 the version bumps on the whole doc after any edit, so
22841
+ serial \`--if-match\` edits force a re-read between each.
22842
+ 3. \`mh doc edit <ref> --old ... --new ... --if-match <version>\` (or \`--edits\`).
22398
22843
  4. On exit 5 (\`stale\`), re-read and retry from step 1.
22399
22844
 
22400
22845
  ## Media & attachments (images / video / audio / files)
@@ -22554,24 +22999,45 @@ will prompt interactively** \u2014 always pass their flags explicitly:
22554
22999
  - **JSON by default** when piped; add \`--pretty\` only when a human will read it.
22555
23000
  `;
22556
23001
 
22557
- // src/cli/claude-skill.ts
23002
+ // src/cli/agent-skill.ts
22558
23003
  import { mkdirSync as mkdirSync2 } from "fs";
22559
23004
  import { homedir as homedir2 } from "os";
22560
23005
  import { join as join2 } from "path";
23006
+ var CODEX_OPENAI_YAML = `interface:
23007
+ display_name: "Metahub"
23008
+ short_description: "Use mh for durable agent memory"
23009
+ default_prompt: "Use $mh to store, recall, search, and edit durable working memory with metahub."
23010
+ `;
22561
23011
  function claudeHome() {
22562
23012
  return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
22563
23013
  }
23014
+ function codexHome() {
23015
+ return process.env.CODEX_HOME ?? join2(homedir2(), ".codex");
23016
+ }
23017
+ async function writeIfChanged(path, content) {
23018
+ const prev = await Bun.file(path).exists() ? await Bun.file(path).text() : null;
23019
+ if (prev === content)
23020
+ return false;
23021
+ await Bun.write(path, content);
23022
+ return true;
23023
+ }
22564
23024
  async function scaffoldClaudeSkill() {
22565
23025
  const dir = join2(claudeHome(), "skills", "mh");
22566
23026
  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
- }
23027
+ mkdirSync2(dir, { recursive: true });
23028
+ const changed = await writeIfChanged(path, SKILL_default);
22573
23029
  return { path, changed };
22574
23030
  }
23031
+ async function scaffoldCodexSkill() {
23032
+ const dir = join2(codexHome(), "skills", "mh");
23033
+ const path = join2(dir, "SKILL.md");
23034
+ const metadataDir = join2(dir, "agents");
23035
+ const metadataPath = join2(metadataDir, "openai.yaml");
23036
+ mkdirSync2(metadataDir, { recursive: true });
23037
+ const skillChanged = await writeIfChanged(path, SKILL_default);
23038
+ const metadataChanged = await writeIfChanged(metadataPath, CODEX_OPENAI_YAML);
23039
+ return { path, metadataPath, changed: skillChanged || metadataChanged };
23040
+ }
22575
23041
 
22576
23042
  // src/cli/commands/init.ts
22577
23043
  var init_default = defineCommand({
@@ -22583,13 +23049,43 @@ var init_default = defineCommand({
22583
23049
  claude: {
22584
23050
  type: "boolean",
22585
23051
  description: "Install a Claude Code /mh skill into ~/.claude (does not touch ~/.metahub)"
23052
+ },
23053
+ codex: {
23054
+ type: "boolean",
23055
+ description: "Install a Codex $mh skill into ~/.codex (does not touch ~/.metahub)"
22586
23056
  }
22587
23057
  },
22588
23058
  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}`) + `
23059
+ if (args.claude || args.codex) {
23060
+ const claude = args.claude ? await scaffoldClaudeSkill() : null;
23061
+ const codex = args.codex ? await scaffoldCodexSkill() : null;
23062
+ if (claude && !codex) {
23063
+ print({ changed: claude.changed, skill: claude.path }, () => (claude.changed ? `Installed ${claude.path}` : `Up to date: ${claude.path}`) + `
22592
23064
  Use /mh in Claude Code (any project).`);
23065
+ return;
23066
+ }
23067
+ if (codex && !claude) {
23068
+ print({ changed: codex.changed, skill: codex.path, metadata: codex.metadataPath }, () => (codex.changed ? `Installed ${codex.path}` : `Up to date: ${codex.path}`) + `
23069
+ Use $mh in Codex (restart Codex if it was already running).`);
23070
+ return;
23071
+ }
23072
+ print({
23073
+ changed: !!claude?.changed || !!codex?.changed,
23074
+ claude: claude ? { changed: claude.changed, skill: claude.path } : null,
23075
+ codex: codex ? { changed: codex.changed, skill: codex.path, metadata: codex.metadataPath } : null
23076
+ }, () => {
23077
+ const lines = [];
23078
+ if (claude) {
23079
+ lines.push((claude.changed ? `Installed ${claude.path}` : `Up to date: ${claude.path}`) + `
23080
+ Use /mh in Claude Code (any project).`);
23081
+ }
23082
+ if (codex) {
23083
+ lines.push((codex.changed ? `Installed ${codex.path}` : `Up to date: ${codex.path}`) + `
23084
+ Use $mh in Codex (restart Codex if it was already running).`);
23085
+ }
23086
+ return lines.join(`
23087
+ `);
23088
+ });
22593
23089
  return;
22594
23090
  }
22595
23091
  const db = openMetahub();
@@ -23283,6 +23779,7 @@ var get_default = defineCommand({
23283
23779
  init_properties();
23284
23780
 
23285
23781
  // src/cli/input.ts
23782
+ init_errors();
23286
23783
  async function resolveValue2(raw) {
23287
23784
  if (raw == null)
23288
23785
  return;
@@ -23296,7 +23793,11 @@ async function resolveJson(raw) {
23296
23793
  const text = await resolveValue2(raw);
23297
23794
  if (text == null)
23298
23795
  return;
23299
- return JSON.parse(text);
23796
+ try {
23797
+ return JSON.parse(text);
23798
+ } catch (e2) {
23799
+ throw new MhError("invalid_input", `invalid JSON: ${e2 instanceof Error ? e2.message : String(e2)}`);
23800
+ }
23300
23801
  }
23301
23802
 
23302
23803
  // src/cli/commands/prop.ts
@@ -23756,29 +24257,60 @@ var read = defineCommand({
23756
24257
  }, () => body);
23757
24258
  })
23758
24259
  });
24260
+ function parseEditPairs(raw) {
24261
+ if (!Array.isArray(raw) || raw.length === 0)
24262
+ throw new MhError("invalid_input", "--edits must be a non-empty JSON array");
24263
+ return raw.map((e2, i2) => {
24264
+ if (typeof e2 !== "object" || e2 === null)
24265
+ throw new MhError("invalid_input", `--edits[${i2}] must be an object`);
24266
+ const { old, new: repl, replaceAll } = e2;
24267
+ if (typeof old !== "string" || old === "")
24268
+ throw new MhError("invalid_input", `--edits[${i2}].old must be a non-empty string`);
24269
+ if (repl !== undefined && typeof repl !== "string")
24270
+ throw new MhError("invalid_input", `--edits[${i2}].new must be a string`);
24271
+ if (replaceAll !== undefined && typeof replaceAll !== "boolean")
24272
+ throw new MhError("invalid_input", `--edits[${i2}].replaceAll must be a boolean`);
24273
+ return { old, new: repl, replaceAll };
24274
+ });
24275
+ }
23759
24276
  var edit = defineCommand({
23760
24277
  meta: {
23761
24278
  name: "edit",
23762
- description: "Anchored find/replace; --old must match exactly once unless --replace-all"
24279
+ description: "Anchored find/replace; --old must match exactly once unless --replace-all. --edits applies a JSON batch of pairs atomically"
23763
24280
  },
23764
24281
  args: {
23765
24282
  id: { type: "positional", required: true, description: "Document ref (id/prefix/title)" },
23766
- old: { type: "string", required: true, description: "Exact text to find (@file/@- ok)" },
24283
+ old: { type: "string", description: "Exact text to find (@file/@- ok)" },
23767
24284
  new: { type: "string", description: "Replacement text (@file/@- ok)" },
23768
24285
  "replace-all": { type: "boolean", description: "Replace every occurrence" },
24286
+ edits: {
24287
+ type: "string",
24288
+ description: 'Batch JSON: [{"old","new"?,"replaceAll"?}], applied in order (@file/@- ok)'
24289
+ },
23769
24290
  "if-match": { type: "string", description: "Version from `doc read`; reject if changed" }
23770
24291
  },
23771
24292
  run: guard(async (args) => {
23772
24293
  const db = openMetahub();
24294
+ const id = docId(db, args.id);
24295
+ if (args.edits != null) {
24296
+ if (args.old != null || args.new != null || args["replace-all"])
24297
+ throw new MhError("invalid_input", "use --old/--new/--replace-all or --edits, not both");
24298
+ const edits = parseEditPairs(await resolveJson(args.edits));
24299
+ const r4 = editDocumentBatch(db, id, { edits, ifMatch: args["if-match"] });
24300
+ print(r4, () => r4.changed ? `edited ${r4.id} (${r4.replaced} replaced across ${edits.length} edit(s))` : `no change: ${r4.id}`);
24301
+ return;
24302
+ }
24303
+ if (args.old == null)
24304
+ throw new MhError("invalid_input", "need --old or --edits");
23773
24305
  const oldText = await resolveValue2(args.old);
23774
24306
  const newText = await resolveValue2(args.new) ?? "";
23775
- const r3 = editDocument(db, docId(db, args.id), {
24307
+ const r3 = editDocument(db, id, {
23776
24308
  old: oldText,
23777
24309
  new: newText,
23778
24310
  replaceAll: args["replace-all"],
23779
24311
  ifMatch: args["if-match"]
23780
24312
  });
23781
- print(r3, () => `edited ${r3.id} (${r3.replaced} replaced)`);
24313
+ print(r3, () => r3.changed ? `edited ${r3.id} (${r3.replaced} replaced)` : `no change: ${r3.id}`);
23782
24314
  })
23783
24315
  });
23784
24316
  var append = defineCommand({
@@ -28356,56 +28888,196 @@ function wantsHtml(req) {
28356
28888
  return req.method === "GET" && (req.headers.get("accept") ?? "").includes("text/html");
28357
28889
  }
28358
28890
  var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
28891
+ 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"/>';
28892
+ 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"/>';
28893
+ 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"/>';
28894
+ var UNLOCK_CSS = `
28895
+ :root{color-scheme:light;
28896
+ --bg:#ffffff;--surface:#fbfbfa;--surface-2:#f1f1ef;--fg:#2c2c30;--fg-soft:#5b5b62;
28897
+ --muted:#75757f;--line:#ebebe8;--line-strong:#dededb;--accent:#4a55d6;--accent-fg:#ffffff;
28898
+ --accent-soft:#eef0ff;--danger:#d6473b;--danger-soft:#fdeceb;--code-bg:#f8f8f6}
28899
+ :root[data-resolved="dark"]{color-scheme:dark;
28900
+ --bg:#1a1a1c;--surface:#202022;--surface-2:#2a2a2d;--fg:#e6e6e9;--fg-soft:#b4b4bb;
28901
+ --muted:#8b8b95;--line:#2c2c30;--line-strong:#3a3a40;--accent:#7b86ff;--accent-fg:#14141a;
28902
+ --accent-soft:#23243a;--danger:#f87168;--danger-soft:#341e1c;--code-bg:#26262a}
28903
+ *{box-sizing:border-box}
28904
+ html,body{margin:0;min-height:100%}
28905
+ body{font-family:"Hanken Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
28906
+ color:var(--fg);background:var(--bg);line-height:1.55;
28907
+ display:flex;align-items:center;justify-content:center;min-height:100dvh;
28908
+ 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));
28909
+ -webkit-font-smoothing:antialiased;-webkit-tap-highlight-color:transparent;overflow:hidden}
28910
+ /* Ambient wash: two faint accent blooms behind the card. */
28911
+ body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
28912
+ background:
28913
+ radial-gradient(58vmax 58vmax at 14% -8%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 60%),
28914
+ radial-gradient(50vmax 50vmax at 108% 112%,color-mix(in srgb,var(--accent) 10%,transparent),transparent 55%)}
28915
+ .card{position:relative;z-index:1;width:100%;max-width:420px;background:var(--surface);
28916
+ border:1px solid var(--line);border-radius:18px;padding:32px 26px 26px;
28917
+ box-shadow:0 24px 60px rgba(20,20,40,.14),0 4px 14px rgba(20,20,40,.06);
28918
+ animation:rise .45s cubic-bezier(.2,.7,.3,1) both}
28919
+ @keyframes rise{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:none}}
28920
+ .brand{width:52px;height:52px;border-radius:14px;display:flex;align-items:center;justify-content:center;
28921
+ background:var(--accent-soft);color:var(--accent);margin-bottom:18px}
28922
+ .brand svg{width:28px;height:28px;fill:none;stroke:currentColor;stroke-width:1.6;
28923
+ stroke-linecap:round;stroke-linejoin:round}
28924
+ .brand path{stroke-dasharray:120;stroke-dashoffset:120;animation:draw .9s .12s ease forwards}
28925
+ @keyframes draw{to{stroke-dashoffset:0}}
28926
+ h1{font-size:21px;font-weight:650;letter-spacing:-.01em;margin:0 0 8px}
28927
+ .sub{color:var(--muted);font-size:13.5px;margin:0 0 22px;line-height:1.6}
28928
+ .sub code{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,monospace;font-size:12.5px;
28929
+ background:var(--code-bg);border:1px solid var(--line);border-radius:5px;padding:1px 6px;color:var(--fg-soft);
28930
+ white-space:nowrap}
28931
+ .field{position:relative;display:flex;align-items:center}
28932
+ input{width:100%;height:50px;padding:0 48px 0 15px;font-size:16px; /* 16px: never let iOS Safari zoom on focus */
28933
+ font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,monospace;letter-spacing:.01em;
28934
+ color:var(--fg);background:var(--bg);border:1.5px solid var(--line-strong);border-radius:12px;
28935
+ outline:none;transition:border-color .15s,box-shadow .15s}
28936
+ input::placeholder{font-family:var(--ui,inherit);letter-spacing:normal;color:var(--muted);opacity:.8}
28937
+ input:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
28938
+ .card.bad input{border-color:var(--danger);box-shadow:0 0 0 3px color-mix(in srgb,var(--danger) 20%,transparent)}
28939
+ .card.bad .field{animation:shake .34s cubic-bezier(.36,.07,.19,.97)}
28940
+ @keyframes shake{10%,90%{transform:translateX(-1px)}20%,80%{transform:translateX(2px)}
28941
+ 30%,50%,70%{transform:translateX(-5px)}40%,60%{transform:translateX(5px)}}
28942
+ .peek{position:absolute;right:6px;width:40px;height:40px;border:0;background:none;cursor:pointer;
28943
+ color:var(--muted);display:flex;align-items:center;justify-content:center;border-radius:9px}
28944
+ .peek:hover{color:var(--fg-soft)}
28945
+ .peek svg{width:19px;height:19px;fill:none;stroke:currentColor;stroke-width:1.7;
28946
+ stroke-linecap:round;stroke-linejoin:round}
28947
+ .err{color:var(--danger);font-size:13px;font-weight:500;margin:11px 2px 0;
28948
+ display:flex;gap:6px;align-items:center;animation:fade .2s ease both}
28949
+ @keyframes fade{from{opacity:0}to{opacity:1}}
28950
+ button.go{width:100%;height:48px;margin-top:20px;border:0;border-radius:12px;cursor:pointer;
28951
+ background:var(--accent);color:var(--accent-fg);font-size:15px;font-weight:600;font-family:inherit;
28952
+ display:flex;align-items:center;justify-content:center;gap:9px;
28953
+ transition:filter .15s,transform .05s}
28954
+ button.go:hover{filter:brightness(1.06)}
28955
+ button.go:active{transform:translateY(1px)}
28956
+ button.go:disabled{opacity:.7;cursor:default}
28957
+ .spin{width:17px;height:17px;border:2px solid color-mix(in srgb,var(--accent-fg) 45%,transparent);
28958
+ border-top-color:var(--accent-fg);border-radius:50%;animation:spin .7s linear infinite}
28959
+ /* Full-page loader shown while a silent renewal is in flight. */
28960
+ .boot{position:fixed;inset:0;z-index:2;display:flex;align-items:center;justify-content:center;background:var(--bg)}
28961
+ .boot svg{width:40px;height:40px;fill:none;stroke:var(--accent);stroke-width:1.4;
28962
+ stroke-linecap:round;stroke-linejoin:round;animation:breathe 1.5s ease-in-out infinite}
28963
+ @keyframes breathe{0%,100%{opacity:.35;transform:scale(.94)}50%{opacity:1;transform:scale(1)}}
28964
+ @keyframes spin{to{transform:rotate(360deg)}}
28965
+ @media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important}
28966
+ .brand path{stroke-dashoffset:0}}
28967
+ `;
28359
28968
  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>
28969
+ return `<!doctype html><html lang="zh-CN"><head>
28970
+ <meta charset="utf-8">
28971
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
28972
+ <meta name="theme-color" content="#ffffff">
28973
+ <title>\u9A8C\u8BC1\u8BBF\u95EE\u4EE4\u724C \xB7 metahub</title>
28974
+ <script>
28975
+ // FOUC-guard theme resolution, same key/logic as src/webui/theme.ts.
28976
+ try {
28977
+ var mt = localStorage.getItem("mh-theme");
28978
+ var dark = mt === "dark" || (mt !== "light" && matchMedia("(prefers-color-scheme: dark)").matches);
28979
+ document.documentElement.dataset.resolved = dark ? "dark" : "light";
28980
+ document.head.querySelector('meta[name=theme-color]').setAttribute("content", dark ? "#1a1a1c" : "#ffffff");
28981
+ } catch (e) {}
28982
+ </script>
28983
+ <style>${UNLOCK_CSS}</style></head><body>
28984
+ <div class="boot" id="boot"><svg viewBox="0 0 24 24">${CUBE_SVG}</svg></div>
28985
+ <form class="card" id="f" style="display:none" autocomplete="on">
28986
+ <div class="brand"><svg viewBox="0 0 24 24">${CUBE_SVG}</svg></div>
28987
+ <h1>\u8F93\u5165\u8BBF\u95EE\u4EE4\u724C</h1>
28988
+ <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>
28989
+ <div class="field">
28990
+ <input id="t" type="password" placeholder="\u7C98\u8D34\u8BBF\u95EE\u4EE4\u724C\u6216\u767B\u5F55\u94FE\u63A5"
28991
+ autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="go">
28992
+ <button type="button" class="peek" id="peek" aria-label="\u663E\u793A\u4EE4\u724C"><svg viewBox="0 0 24 24">${EYE_SVG}</svg></button>
28993
+ </div>
28994
+ <p class="err" id="e" style="display:none"></p>
28995
+ <button type="submit" class="go" id="go">\u89E3\u9501\u5E76\u8FDB\u5165</button>
28379
28996
  </form>
28380
28997
  <script>
28381
28998
  var KEY = "mh_token";
28999
+ var form = document.getElementById("f"), boot = document.getElementById("boot");
29000
+ var input = document.getElementById("t"), errEl = document.getElementById("e");
29001
+ var goBtn = document.getElementById("go"), peek = document.getElementById("peek");
29002
+
28382
29003
  function save(t) {
28383
29004
  localStorage.setItem(KEY, t);
28384
29005
  document.cookie = "${COOKIE}=" + encodeURIComponent(t) + "; ${COOKIE_ATTRS}" +
28385
29006
  (location.protocol === "https:" ? "; Secure" : "");
28386
29007
  }
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) {
29008
+ // Accept a pasted "\u2026/?token=xxx" login link, not just the bare token.
29009
+ function normalize(v) {
29010
+ v = (v || "").trim();
29011
+ var m = /[?&#]token=([^&\\s]+)/.exec(v);
29012
+ if (m) { try { return decodeURIComponent(m[1]); } catch (e) { return m[1]; } }
29013
+ return v;
29014
+ }
29015
+ function showForm(msg) {
29016
+ boot.style.display = "none";
29017
+ form.style.display = "block";
29018
+ if (msg) { errEl.textContent = msg; errEl.style.display = "flex"; }
29019
+ input.focus();
29020
+ }
29021
+ function fail(msg) {
29022
+ goBtn.disabled = false;
29023
+ goBtn.innerHTML = "\u89E3\u9501\u5E76\u8FDB\u5165";
29024
+ errEl.textContent = msg;
29025
+ errEl.style.display = "flex";
29026
+ form.classList.remove("bad");
29027
+ void form.offsetWidth; // reflow so the shake animation restarts
29028
+ form.classList.add("bad");
29029
+ input.focus();
29030
+ input.select();
29031
+ }
29032
+ peek.addEventListener("click", function () {
29033
+ var pw = input.type === "password";
29034
+ input.type = pw ? "text" : "password";
29035
+ peek.innerHTML = '<svg viewBox="0 0 24 24">' + (pw ? ${JSON.stringify(EYE_OFF_SVG)} : ${JSON.stringify(EYE_SVG)}) + "</svg>";
29036
+ peek.setAttribute("aria-label", pw ? "\u9690\u85CF\u4EE4\u724C" : "\u663E\u793A\u4EE4\u724C");
29037
+ input.focus();
29038
+ });
29039
+ input.addEventListener("input", function () {
29040
+ if (errEl.style.display !== "none") { errEl.style.display = "none"; form.classList.remove("bad"); }
29041
+ });
29042
+
29043
+ form.addEventListener("submit", function (ev) {
28394
29044
  ev.preventDefault();
28395
- save(document.getElementById("t").value);
28396
- location.reload();
29045
+ var token = normalize(input.value);
29046
+ if (!token) { fail("\u8BF7\u8F93\u5165\u8BBF\u95EE\u4EE4\u724C\u3002"); return; }
29047
+ goBtn.disabled = true;
29048
+ goBtn.innerHTML = '<span class="spin"></span>\u9A8C\u8BC1\u4E2D\u2026';
29049
+ var hdr = { headers: { authorization: "Bearer " + token } };
29050
+ // Prefer the renewal endpoint (managed mode returns the canonical token,
29051
+ // also upgrading a pasted in-grace token). Fall back to a cheap
29052
+ // authenticated probe so we can reject a bad token *inline* instead of
29053
+ // storing it and reloading into another gate.
29054
+ fetch("${RENEW_PATH}", hdr)
29055
+ .then(function (r) { return r.ok ? r.json() : null; })
29056
+ .then(function (d) {
29057
+ if (d && d.token) { save(d.token); location.reload(); return; }
29058
+ return fetch("/api/version", hdr).then(function (r) {
29059
+ if (r.status === 401) { fail("\u4EE4\u724C\u65E0\u6548,\u8BF7\u68C0\u67E5\u540E\u91CD\u8BD5\u3002"); return; }
29060
+ // 200 (static-token mode) or no UI (404) \u2192 trust it and reload.
29061
+ save(token); location.reload();
29062
+ });
29063
+ })
29064
+ .catch(function () { save(token); location.reload(); }); // network hiccup \u2192 old behavior
28397
29065
  });
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.
29066
+
29067
+ // Silent renewal on load: a held token may just need swapping for the current
29068
+ // one (rotation while this browser held the in-grace previous). Show the boot
29069
+ // loader meanwhile; drop to the form (with a hint) if it can't be renewed.
28401
29070
  var stored = localStorage.getItem(KEY);
28402
29071
  if (stored) {
28403
29072
  fetch("${RENEW_PATH}", { headers: { authorization: "Bearer " + stored } })
28404
29073
  .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);
29074
+ .then(function (d) {
29075
+ if (d && d.token) { save(d.token); location.reload(); }
29076
+ else showForm("\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u8F93\u5165\u4EE4\u724C\u3002");
29077
+ })
29078
+ .catch(function () { showForm(""); });
28407
29079
  } else {
28408
- showForm();
29080
+ showForm("");
28409
29081
  }
28410
29082
  </script>
28411
29083
  </body></html>`;
@@ -28681,6 +29353,11 @@ var styles_default = ` :root {
28681
29353
  svg { display: block; }
28682
29354
  .ico { width:16px; height:16px; stroke:currentColor; fill:none; stroke-width:1.75; stroke-linecap:round; stroke-linejoin:round; flex:none; }
28683
29355
  .ico.sm { width:14px; height:14px; }
29356
+ /* Every window (incl. quicknote) gets the same thin translucent scrollbar:
29357
+ transparent track, rounded translucent thumb. In Chromium 121+ the
29358
+ inherited html scrollbar-width/color renders it; the ::-webkit rules
29359
+ cover Safari. Both force classic (layout-consuming) mode \u2014 tight layouts
29360
+ pair scrollbar-gutter:stable with rebalanced insets (see .qn-body). */
28684
29361
  ::-webkit-scrollbar { width:10px; height:10px; }
28685
29362
  ::-webkit-scrollbar-thumb { background:var(--line-strong); border-radius:8px; border:3px solid transparent; background-clip:padding-box; }
28686
29363
  ::-webkit-scrollbar-thumb:hover { background:var(--muted); background-clip:padding-box; border:3px solid transparent; }
@@ -28729,12 +29406,45 @@ var styles_default = ` :root {
28729
29406
  .sb-search input { border:0; background:none; outline:none; color:var(--fg); width:100%; }
28730
29407
  .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
29408
  .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); }
29409
+ /* Notion-style list switcher: the active tab is an icon+label pill, inactive
29410
+ tabs are icon-only. One absolutely-positioned indicator slides & stretches
29411
+ under the active button; JS keeps it in sync via ResizeObserver so it
29412
+ chases the label's 0fr\u21921fr expansion instead of jumping to a stale width. */
29413
+ .sb-tabs { position:relative; display:flex; align-items:center; gap:2px; margin:2px 12px 8px; }
29414
+ .sb-tab { position:relative; z-index:1; display:flex; align-items:center;
29415
+ padding:5px 7px; border-radius:var(--radius-sm); color:var(--muted); cursor:pointer;
29416
+ font-size:var(--fs-ui); transition:color .18s; }
29417
+ .sb-tab:hover { color:var(--fg-soft); }
29418
+ .sb-tab.on { color:var(--fg); font-weight:600; }
29419
+ /* Label expansion: animating the grid track times correctly for any label
29420
+ width (max-width can't); the inner span clips during the ride. The
29421
+ icon-text gap rides the same curve so collapsing leaves no seam. */
29422
+ .sb-tab .tab-label { display:grid; grid-template-columns:0fr; opacity:0;
29423
+ transition:grid-template-columns .24s cubic-bezier(.22,1,.36,1), opacity .18s ease; }
29424
+ .sb-tab .tab-label > span { overflow:hidden; white-space:nowrap; min-width:0;
29425
+ transition:padding-left .24s cubic-bezier(.22,1,.36,1); }
29426
+ .sb-tab.on .tab-label { grid-template-columns:1fr; opacity:1; }
29427
+ .sb-tab.on .tab-label > span { padding-left:6px; }
29428
+ .sb-tab-ind { position:absolute; left:0; top:0; bottom:0; z-index:0; width:0;
29429
+ border-radius:var(--radius-sm); background:var(--hover-2);
29430
+ transition:transform .24s cubic-bezier(.22,1,.36,1), width .24s cubic-bezier(.22,1,.36,1); }
29431
+ .sb-tabs .add { margin-left:auto; padding:3px; border-radius:5px; color:var(--muted); }
29432
+ .sb-tabs .add:hover { background:var(--hover-2); color:var(--fg); }
29433
+ /* Pane swap: the entering list slides in from the side its tab lives on. */
29434
+ .sb-pane { margin-top:4px; }
29435
+ .sb-pane.in-r { animation:sb-pane-r .2s cubic-bezier(.22,1,.36,1); }
29436
+ .sb-pane.in-l { animation:sb-pane-l .2s cubic-bezier(.22,1,.36,1); }
29437
+ @keyframes sb-pane-r { from { opacity:0; transform:translateX(14px); } }
29438
+ @keyframes sb-pane-l { from { opacity:0; transform:translateX(-14px); } }
29439
+ .sb-subfold .chev { transition:transform .12s; }
29440
+ .sb-subfold .chev.open { transform:rotate(90deg); }
29441
+ /* Collapsed-databases group: a quiet tail row inside the \u6570\u636E\u8868 list for
29442
+ tables flagged meta.collapsed (site-facing / rarely browsed). */
29443
+ .sb-subfold { display:flex; align-items:center; gap:2px; width:100%; text-align:left;
29444
+ padding:4px 6px; margin-top:2px; border-radius:var(--radius-sm);
29445
+ color:var(--muted); font-size:var(--fs-micro); cursor:pointer; }
29446
+ .sb-subfold:hover { background:var(--hover-2); color:var(--fg-soft); }
29447
+ .navitem.dim { opacity:.62; }
28738
29448
  .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
29449
  color:var(--fg-soft); font-size:var(--fs-ui); position:relative; white-space:nowrap; transition:background .1s; }
28740
29450
  .navitem:hover { background:var(--hover-2); }
@@ -28824,6 +29534,23 @@ var styles_default = ` :root {
28824
29534
  .set-block-title { font-size:14px; font-weight:650; color:var(--fg); letter-spacing:-.005em; }
28825
29535
  .set-block-desc { color:var(--fg-soft); font-size:var(--fs-sm); line-height:1.55; margin-bottom:14px; max-width:56ch; }
28826
29536
  .set-block-desc:last-child { margin-bottom:0; }
29537
+ /* title+desc on the left, a control (e.g. a switch) pinned right \u2014 used for
29538
+ single boolean settings where a whole card of chrome would be overkill */
29539
+ .set-toggle-row { display:flex; align-items:center; justify-content:space-between; gap:24px; }
29540
+ .set-toggle-text { min-width:0; }
29541
+ .set-toggle-text .set-block-head { margin-bottom:4px; }
29542
+ .set-toggle-text .set-block-desc { margin-bottom:0; }
29543
+ /* iOS-style switch: a hidden checkbox drives a sliding track+thumb */
29544
+ .set-switch { position:relative; flex:none; display:inline-flex; align-items:center; cursor:pointer; }
29545
+ .set-switch input { position:absolute; width:1px; height:1px; opacity:0; margin:0; }
29546
+ .set-switch-track { display:inline-flex; align-items:center; width:40px; height:23px; padding:0 2px;
29547
+ border-radius:999px; background:var(--line-strong); transition:background .2s ease; }
29548
+ .set-switch-thumb { width:19px; height:19px; border-radius:50%; background:#fff;
29549
+ box-shadow:0 1px 3px rgba(20,20,40,.28); transition:transform .2s cubic-bezier(.4,0,.2,1); }
29550
+ .set-switch input:checked + .set-switch-track { background:var(--accent); }
29551
+ .set-switch input:checked + .set-switch-track .set-switch-thumb { transform:translateX(17px); }
29552
+ .set-switch:hover .set-switch-track { filter:brightness(.97); }
29553
+ .set-switch input:focus-visible + .set-switch-track { outline:2px solid var(--accent); outline-offset:2px; }
28827
29554
  /* two-fact meta line under a block title (who publishes \xB7 where creds live) \u2014
28828
29555
  answers the questions hiding under "is this bucket for server or browser?" */
28829
29556
  .set-meta { display:flex; flex-wrap:wrap; gap:8px 16px; margin:0 0 12px; }
@@ -29023,10 +29750,19 @@ var styles_default = ` :root {
29023
29750
  .sidebar.collapsed { overflow:hidden; }
29024
29751
  .sidebar.collapsed .sb-resizer { pointer-events:none; }
29025
29752
  .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; }
29753
+ /* Long titles ellipsize on one line \u2014 never wrap and grow the topbar. */
29754
+ .crumb > span:not(.emoji) { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
29755
+ .crumb .emoji { font-size:15px; flex:none; } .crumb .sub { color:var(--muted); font-weight:400; }
29027
29756
  .iconbtn { width:30px; height:30px; display:grid; place-items:center; border-radius:var(--radius-sm); color:var(--fg-soft); transition:background .12s; }
29028
29757
  .iconbtn:hover { background:var(--hover-2); color:var(--fg); }
29029
- .content { flex:1; overflow:auto; }
29758
+ /* Reserve the classic-scrollbar gutter up front: view width never changes
29759
+ when content grows past one screen \u2014 no jump / rewrap flicker. stable
29760
+ ONLY (never both-edges): a left-edge gutter desyncs Chromium's composited
29761
+ scroll cull rect by the gutter width, leaving a permanent unpainted
29762
+ stripe over the rightmost 15px of the scrollport (content reaching into
29763
+ it gets visibly sliced \u2014 first seen slicing the quicknote file card).
29764
+ Inert on mobile, where .content becomes overflow:visible. */
29765
+ .content { flex:1; overflow:auto; scrollbar-gutter:stable; }
29030
29766
  .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
29767
  .btn-ghost { color:var(--fg-soft); } .btn-ghost:hover { background:var(--hover-2); color:var(--fg); }
29032
29768
  .btn-secondary { background:var(--bg); border:1px solid var(--line-strong); color:var(--fg); box-shadow:var(--shadow-sm); }
@@ -29091,9 +29827,9 @@ var styles_default = ` :root {
29091
29827
  .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
29828
  .doc-conflict-msg { color:var(--danger); font-weight:600; flex:1; min-width:180px; }
29093
29829
  .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; }
29830
+ /* wide mode (\u22EF menu): lift the column cap; the 60px side padding stays as the
29831
+ reading gutter and keeps the fixed .doc-toc ticks off the text. */
29832
+ .doc.wide-mode { max-width:none; }
29097
29833
  /* floating table of contents: a column of indented tick marks in the right
29098
29834
  gutter that expands to a labelled list on hover (Notion / Vercel-docs style).
29099
29835
  Fixed to the viewport so it never disturbs the centred .doc column. */
@@ -29132,49 +29868,33 @@ var styles_default = ` :root {
29132
29868
  /* not enough room for the gutter \u2192 hide entirely (also covers mobile) */
29133
29869
  @media (max-width:1100px) { .doc-toc { display:none; } }
29134
29870
 
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; }
29871
+ /* floating word count: a small pill in the bottom-right gutter (fixed to the
29872
+ viewport, aligned with .doc-toc's right edge). Collapsed it hugs the \u5B57\u6570;
29873
+ hover expands the whole pill (square-off + surface bg) and reveals the char
29874
+ count + reading estimate. Centre-aligned throughout, and the collapsed detail
29875
+ is width:0 so its wider text can't stretch the pill. Purely client-side. */
29876
+ .doc-wc { position:fixed; right:18px; bottom:calc(16px + env(safe-area-inset-bottom)); z-index:40;
29877
+ display:flex; flex-direction:column; align-items:center; justify-content:center;
29878
+ padding:4px 12px; border:1px solid var(--line); border-radius:999px; background:var(--bg);
29879
+ color:var(--muted); font-size:var(--fs-sm); line-height:1.2; user-select:none;
29880
+ box-shadow:var(--shadow-sm); opacity:.66;
29881
+ transition:opacity .18s ease, border-radius .2s ease, background .18s ease, box-shadow .2s ease, color .18s ease; }
29882
+ .doc-wc:hover { opacity:1; border-radius:var(--radius); background:var(--surface); border-color:var(--line-strong);
29883
+ box-shadow:var(--shadow-md); color:var(--fg-soft); }
29884
+ .doc-wc-num { font-variant-numeric:tabular-nums; white-space:nowrap; }
29885
+ .doc-wc.sel .doc-wc-num { color:var(--accent); font-weight:550; }
29886
+ /* detail rows: collapsed to zero size until hover. width:0 keeps them from
29887
+ widening the pill; no container gap (a phantom gap would push the collapsed
29888
+ count off-centre) \u2014 the spacing comes from margin-top only when expanded. */
29889
+ .doc-wc-detail { display:flex; flex-direction:column; align-items:center; gap:2px;
29890
+ width:0; max-height:0; margin-top:0; overflow:hidden; opacity:0; font-size:12px; color:var(--muted);
29891
+ transition:max-height .2s ease, opacity .18s ease, margin-top .2s ease; }
29892
+ .doc-wc-detail span { font-variant-numeric:tabular-nums; white-space:nowrap; }
29893
+ .doc-wc:hover .doc-wc-detail { width:auto; max-height:40px; margin-top:4px; opacity:1; }
29894
+ /* quick-note mini window has no room for it */
29895
+ .qn-body .doc-wc { display:none; }
29175
29896
 
29176
29897
  /* code block: transparent textarea over a highlight.js mirror */
29177
- .b-code .codeblock { flex:1; min-width:0; }
29178
29898
  .codeblock { position:relative; --code-fs:12.5px; --code-lh:1.55; --code-pad:8px; margin:2px 0;
29179
29899
  background:var(--code-bg); border:1px solid var(--line); border-radius:8px; overflow:hidden; box-shadow:var(--shadow-sm); }
29180
29900
  /* lang + copy float bottom-right, revealed on hover */
@@ -29183,27 +29903,57 @@ var styles_default = ` :root {
29183
29903
  background:color-mix(in srgb,var(--code-bg) 82%,transparent); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
29184
29904
  opacity:0; transform:translateY(3px); pointer-events:none; transition:opacity .12s,transform .12s; }
29185
29905
  .codeblock:hover .code-tools, .code-tools:focus-within { opacity:1; transform:none; pointer-events:auto; }
29906
+ /* hidden while editing (textarea focused) or whole-block selected \u2014 the pill would cover the
29907
+ bottom-right of the editing area; keep it if focus is on the pill itself (lang dropdown open) */
29908
+ .cm-void-code:focus-within .code-tools:not(:focus-within),
29909
+ .codeblock.ci-selected .code-tools:not(:focus-within) { opacity:0; transform:translateY(3px); pointer-events:none; }
29186
29910
  .code-lang { position:relative; display:inline-flex; align-items:center; }
29187
29911
  .code-lang select { appearance:none; -webkit-appearance:none; border:0; outline:none; background:none; cursor:pointer;
29188
29912
  font-family:var(--mono); font-size:10.5px; font-weight:500; color:var(--fg-soft); padding:3px 17px 3px 7px; border-radius:5px; }
29189
29913
  .code-lang select:hover { background:var(--hover-2); }
29190
29914
  .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); }
29915
+ .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);
29916
+ padding:3px 7px; border-radius:5px; transition:background .12s,color .12s; white-space:nowrap; }
29917
+ .code-copy:hover, .code-fmt:hover, .code-wrap-btn:hover { background:var(--hover-2); color:var(--fg-soft); }
29918
+ .code-copy.ok, .code-fmt.ok, .code-wrap-btn.on { color:var(--accent); }
29919
+ .code-fmt.err { color:var(--danger); }
29920
+ /* Default (no wrap): lines run long, the textarea scrolls horizontally and
29921
+ mirrors its scrollLeft onto the highlight layer; the gutter is a separate
29922
+ flex column of "1\\n2\\n3" (every logical line is exactly one row high). */
29923
+ .code-body { display:flex; align-items:stretch; font-family:var(--mono); font-size:var(--code-fs); line-height:var(--code-lh);
29924
+ --code-ln-ch:1ch; --code-gutter-w:calc(var(--code-ln-ch) + 20px); }
29196
29925
  .code-gutter { flex:none; text-align:right; white-space:pre; user-select:none; color:var(--muted); opacity:.55;
29197
29926
  padding:var(--code-pad) 8px var(--code-pad) 12px; border-right:1px solid var(--line); font-variant-numeric:tabular-nums; }
29198
29927
  .code-scroll { position:relative; flex:1; min-width:0; }
29199
29928
  .code-hl, .code-input { margin:0; font-family:var(--mono); font-size:var(--code-fs); line-height:var(--code-lh);
29200
29929
  padding:var(--code-pad) 16px; tab-size:2; -moz-tab-size:2; white-space:pre; letter-spacing:0; }
29201
29930
  .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; }
29931
+ .code-hl code { display:block; font:inherit; background:none; padding:0; counter-reset:code-ln; }
29932
+ .code-hl-line { counter-increment:code-ln;
29933
+ min-height:calc(var(--code-fs) * var(--code-lh)); /* empty logical line still occupies its row */ }
29203
29934
  .code-input { display:block; position:relative; z-index:1; width:100%; border:0; outline:none; resize:none;
29204
29935
  overflow-x:auto; overflow-y:hidden; background:transparent; color:transparent; caret-color:var(--fg);
29205
29936
  min-height:calc(var(--code-fs) * var(--code-lh) + var(--code-pad) * 2); }
29206
29937
  .code-input::placeholder { color:var(--muted); }
29938
+ /* Wrap mode (.wrap, global toggle in the tools pill, persisted): textarea and
29939
+ mirror share pre-wrap + break-word (the UA textarea wrap rules), so their
29940
+ wrap points match and the caret stays on the glyphs. The flex gutter would
29941
+ misalign (a logical line can span several visual rows), so it hides and
29942
+ line numbers become CSS counters on the mirror's per-logical-line blocks \u2014
29943
+ a wrapped line grows its block and the next number moves with it, no JS
29944
+ height sync. --code-ln-ch (digit count) is set by paint(). */
29945
+ .code-body.wrap { display:block; position:relative; }
29946
+ .code-body.wrap::before { content:""; position:absolute; left:var(--code-gutter-w); top:0; bottom:0;
29947
+ width:1px; background:var(--line); }
29948
+ .code-body.wrap .code-gutter { display:none; }
29949
+ .code-body.wrap .code-hl, .code-body.wrap .code-input {
29950
+ padding-left:calc(var(--code-gutter-w) + 17px);
29951
+ white-space:pre-wrap; overflow-wrap:break-word; }
29952
+ .code-body.wrap .code-input { overflow:hidden; }
29953
+ .code-body.wrap .code-hl-line { position:relative; }
29954
+ .code-body.wrap .code-hl-line::before { content:counter(code-ln); position:absolute; top:0;
29955
+ left:calc(-17px - var(--code-gutter-w)); width:calc(var(--code-gutter-w) - 8px);
29956
+ text-align:right; color:var(--muted); opacity:.55; font-variant-numeric:tabular-nums; }
29207
29957
 
29208
29958
  /* highlight.js token theme (cohesive with the app palette) */
29209
29959
  .hljs-comment, .hljs-quote { color:var(--hl-cmt); font-style:italic; }
@@ -29217,24 +29967,50 @@ var styles_default = ` :root {
29217
29967
  .hljs-params, .hljs-subst { color:var(--fg); }
29218
29968
  .hljs-emphasis { font-style:italic; }
29219
29969
  .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
29970
 
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); }
29971
+ /* CodeIsland (CM6 void widget): the tools pill (lang + copy) is the only
29972
+ focus-state chrome \u2014 an absolute overlay, so revealing it never changes the
29973
+ block's height (no layout shift on click). .ci-selected marks the island
29974
+ when the CM selection spans the whole void (Escape from inside / Backspace
29975
+ from the line below). */
29976
+ .cm-void-code .codeblock.ci-selected { box-shadow:0 0 0 2px var(--accent); }
29977
+
29978
+ /* Whole-void selected (the CM selection spans the block \u2014 click-to-select on
29979
+ media, Backspace-from-below, select-all): 2px accent ring, the old editor's
29980
+ \`.block.selected .void-block\` treatment. Code draws its own via .ci-selected. */
29981
+ /* Intrinsic-width firewall (same problem the doc table solved with
29982
+ contain:inline-size on .doc-table-wrap): a void widget's max-content \u2014
29983
+ e.g. a file card whose nowrap filename caps at 480px \u2014 must not propagate
29984
+ into .cm-content and push the whole editor wider than a narrow window
29985
+ (quick notes), shoving the card's right edge offscreen. width:0 zeroes the
29986
+ host's intrinsic contribution; min-width:100% restores the line width at
29987
+ layout time; the card's own min-width:0 + ellipsis then get to run.
29988
+ NOT contain:inline-size: Chromium 130's ink-overflow culling clips a
29989
+ size-contained host's paint at a stale boundary on 2x displays \u2014 the
29990
+ card's right border and selection ring were visibly sliced off. */
29991
+ .cm-void { box-sizing:border-box; width:0; min-width:100%; }
29992
+ .cm-void.selected .void-block { box-shadow:0 0 0 2px var(--accent); }
29993
+ .cm-void.selected table.doc-table { box-shadow:0 0 0 2px var(--accent), var(--shadow-sm); }
29994
+
29995
+ /* Editor text selection: one accent tint for BOTH the native ::selection and
29996
+ .cm-mk-sel, the class block-deco puts on marker widgets (bullet / ordered
29997
+ number / checkbox) whose marker range the selection covers. Native selection
29998
+ cannot paint replaced widgets, which left holes in the marker column during
29999
+ drag-select / select-all; unifying the two colors makes the line read as one
30000
+ continuous highlight (and tracks the theme in dark mode). */
30001
+ .cm-content ::selection { background: color-mix(in srgb, var(--accent) 24%, transparent); }
30002
+ .cm-content .cm-mk-sel { background: color-mix(in srgb, var(--accent) 24%, transparent); }
30003
+ /* bullet/number boxes are 24px + 2px margin-right; the margin can't take a
30004
+ background, so a same-color shadow fills the seam up to the content column */
30005
+ .cm-content .cm-bullet.cm-mk-sel, .cm-content .cm-ol-num.cm-mk-sel {
30006
+ box-shadow: 2px 0 0 color-mix(in srgb, var(--accent) 24%, transparent);
30007
+ }
30008
+ /* the glyph/number text inside a painted marker also sits in the DOM selection;
30009
+ without this it gets the ::selection tint ON TOP of the span background and
30010
+ reads darker than the rest of the line */
30011
+ .cm-content .cm-mk-sel::selection, .cm-content .cm-mk-sel ::selection { background: transparent; }
29237
30012
 
30013
+ /* ---- block-level void embeds: image / video / audio / file / html ---- */
29238
30014
  .void-image { position:relative; display:inline-block; max-width:100%; border-radius:8px; line-height:0; }
29239
30015
  .void-image img { display:block; width:100%; max-width:100%; height:auto; border-radius:8px; user-select:none; }
29240
30016
  .void-image .void-expand { position:absolute; top:8px; right:8px; width:28px; height:28px; display:grid; place-items:center;
@@ -29272,7 +30048,6 @@ var styles_default = ` :root {
29272
30048
  color:var(--fg-soft); font-size:var(--fs-micro); transition:background .12s; }
29273
30049
  .void-html .html-toggle:hover { background:var(--hover-2); color:var(--fg); }
29274
30050
  .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
30051
  .void-html .html-frame { display:block; width:100%; border:0; background:var(--bg); }
29277
30052
  .void-html .html-empty { padding:24px; text-align:center; color:var(--muted); cursor:text; font-size:var(--fs-ui); }
29278
30053
 
@@ -29283,30 +30058,47 @@ var styles_default = ` :root {
29283
30058
  .lightbox-toolbar { flex:none; display:flex; align-items:center; gap:6px; padding:10px 14px; color:#fff; }
29284
30059
  .lightbox-toolbar .lightbox-name { font-size:var(--fs-ui); opacity:.85; max-width:40vw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
29285
30060
  .lightbox-toolbar .lightbox-spacer { flex:1; }
29286
- .lightbox-toolbar button { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; color:#fff;
30061
+ .lightbox-toolbar > button { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; color:#fff;
29287
30062
  background:rgba(255,255,255,.08); transition:background .12s; }
29288
- .lightbox-toolbar button:hover { background:rgba(255,255,255,.2); }
30063
+ .lightbox-toolbar > button:hover { background:rgba(255,255,255,.2); }
29289
30064
  .lightbox-toolbar .lightbox-pct { width:auto; min-width:52px; font-size:var(--fs-micro); font-variant-numeric:tabular-nums; }
30065
+ .lightbox-toolbar .ico { width:16px; height:16px; stroke-width:2; }
29290
30066
  .lightbox-stage { flex:1; min-height:0; overflow:hidden; display:grid; place-items:center; }
29291
30067
  .lightbox-stage img { max-width:94vw; max-height:100%; will-change:transform; -webkit-user-drag:none; user-select:none; }
29292
30068
 
29293
30069
  /* ---- image annotator ---- */
29294
30070
  .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); }
30071
+ /* Single-row chrome: filename \xB7 segmented tools \xB7 colors \xB7 undo/redo \xB7
30072
+ cancel/save \xB7 close. Wraps only as a narrow-window fallback. */
30073
+ .annot-bar { flex-wrap:wrap; row-gap:6px; }
30074
+ .annot-seg { display:flex; align-items:center; gap:2px; padding:2px; border-radius:9px; background:rgba(255,255,255,.1); }
30075
+ .annot-seg-btn { width:30px; height:26px; display:grid; place-items:center; border-radius:7px;
30076
+ color:rgba(255,255,255,.72); transition:background .12s, color .12s; }
30077
+ .annot-seg-btn:hover { color:#fff; background:rgba(255,255,255,.12); }
30078
+ .annot-seg-btn.on { background:#fff; color:#1c1c26; box-shadow:0 1px 2px rgba(0,0,0,.3); }
30079
+ .annot-div { flex:none; width:1px; height:18px; margin:0 5px; background:rgba(255,255,255,.18); }
30080
+ .annot-colors { display:flex; align-items:center; gap:8px; padding:0 2px; }
30081
+ .annot-swatch { width:15px; height:15px; border-radius:50%; box-shadow:inset 0 0 0 1px rgba(255,255,255,.3);
30082
+ transition:transform .12s, box-shadow .12s; }
30083
+ .annot-swatch:hover { transform:scale(1.18); }
30084
+ .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); }
30085
+ .annot-bar .annot-icon-btn { width:30px; height:30px; display:grid; place-items:center; border-radius:7px;
30086
+ color:rgba(255,255,255,.85); background:none; transition:background .12s; }
30087
+ .annot-bar .annot-icon-btn:hover { background:rgba(255,255,255,.14); }
30088
+ .annot-bar .annot-icon-btn:disabled { opacity:.35; cursor:default; background:none; }
30089
+ .annot-actions { display:flex; align-items:center; gap:8px; margin:0 2px 0 4px; }
29304
30090
  .annot-stage { flex:1; min-height:0; overflow:auto; display:grid; place-items:center; padding:8px 14px 18px; }
29305
30091
  .annot-img-wrap { position:relative; line-height:0; }
29306
30092
  .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%; }
30093
+ .annot-canvas { position:absolute; inset:0; width:100%; height:100%; touch-action:none; }
29308
30094
  .annot-canvas.tool-text { cursor:text; }
29309
30095
  .annot-canvas.tool-rect, .annot-canvas.tool-arrow { cursor:crosshair; }
30096
+ /* Inline text overlay: WYSIWYG twin of the flattened canvas text (same font,
30097
+ line-height and halo), plus an accent ring so the edit region reads. */
30098
+ .annot-text-input { position:absolute; z-index:2; min-width:60px; padding:2px 4px; margin:0; border:0; outline:none;
30099
+ resize:none; overflow:hidden; white-space:pre; background:transparent; border-radius:4px; caret-color:currentColor;
30100
+ font:600 20px/1.3 system-ui, -apple-system, sans-serif; text-shadow:0 1px 2px rgba(0,0,0,.55);
30101
+ box-shadow:0 0 0 1.5px var(--accent), 0 0 0 4px color-mix(in srgb, var(--accent) 25%, transparent); }
29310
30102
 
29311
30103
  /* ---- transient "uploading" placeholder + unavailable fallback ---- */
29312
30104
  .ico.spin { animation:mh-spin .8s linear infinite; }
@@ -29315,7 +30107,10 @@ var styles_default = ` :root {
29315
30107
  display:flex; align-items:center; gap:12px; margin:2px 0; max-width:480px; padding:12px 14px;
29316
30108
  border:1px solid var(--line-strong); border-radius:var(--radius); background:var(--surface); color:var(--fg-soft); }
29317
30109
  .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; }
30110
+ /* min-width:0: .void-uploading's vu-name is a direct flex item \u2014 without it a
30111
+ long nowrap filename sets the item's min-content floor and the placeholder
30112
+ card overflows a narrow window instead of ellipsizing. */
30113
+ .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
30114
  .void-uploading .vu-sub, .void-unavailable .vu-sub { font-size:var(--fs-micro); color:var(--muted); }
29320
30115
  .void-unavailable { border-color:var(--danger-soft); color:var(--danger); }
29321
30116
  .void-unavailable .ico { color:var(--danger); flex:none; }
@@ -29361,24 +30156,53 @@ var styles_default = ` :root {
29361
30156
  /* Chrome rides the theme-tinted glass \u2192 theme-aware (var(--fg)), readable on a
29362
30157
  light or dark tint alike. */
29363
30158
  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); }
30159
+ body.preview-window .lightbox-toolbar > button { color:var(--fg); background:color-mix(in srgb, var(--fg) 10%, transparent); }
30160
+ body.preview-window .lightbox-toolbar > button:hover { background:color-mix(in srgb, var(--fg) 20%, transparent); }
30161
+ body.preview-window .annot-seg { background:color-mix(in srgb, var(--fg) 8%, transparent); }
30162
+ body.preview-window .annot-seg-btn { color:color-mix(in srgb, var(--fg) 65%, transparent); }
30163
+ body.preview-window .annot-seg-btn:hover { color:var(--fg); background:color-mix(in srgb, var(--fg) 10%, transparent); }
30164
+ body.preview-window .annot-seg-btn.on { background:var(--bg); color:var(--fg); box-shadow:var(--shadow-sm); }
30165
+ body.preview-window .annot-div { background:color-mix(in srgb, var(--fg) 15%, transparent); }
30166
+ body.preview-window .annot-swatch { box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 25%, transparent); }
30167
+ body.preview-window .annot-swatch.on { box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 25%, transparent),
30168
+ 0 0 0 2px var(--bg), 0 0 0 3.5px color-mix(in srgb, var(--fg) 78%, transparent); }
30169
+ body.preview-window .annot-bar .annot-icon-btn { color:var(--fg); background:none; }
30170
+ body.preview-window .annot-bar .annot-icon-btn:hover { background:color-mix(in srgb, var(--fg) 12%, transparent); }
30171
+ body.preview-window .annot-bar .annot-icon-btn:disabled { background:none; }
29370
30172
  /* In the frameless window the toolbar doubles as the OS drag region; its
29371
30173
  buttons opt back out so they stay clickable. */
29372
30174
  .lightbox-toolbar.app-drag { -webkit-app-region:drag; }
29373
30175
  .lightbox-toolbar.app-drag button { -webkit-app-region:no-drag; }
29374
30176
 
29375
- /* document table (GFM pipe table block) */
29376
- .b-table .doc-table-wrap { flex:1; min-width:0; margin:4px 0; }
30177
+ /* document table (GFM pipe table block). contain:inline-size zeroes the
30178
+ widget's intrinsic-width contribution \u2014 otherwise the inner
30179
+ width:max-content propagates through overflow-x:auto into .cm-content's
30180
+ min-width:auto floor (intrinsic sizing ignores overflow) and pushes the
30181
+ whole page wider than the viewport on narrow screens. With the intrinsic
30182
+ loop broken, .doc-table-wrap resolves to the doc column width and the
30183
+ scroll box below finally gets to scroll. container-type additionally
30184
+ unlocks cqw for the sticky add-row bar (old engines fall back to contain);
30185
+ position:relative anchors the gutter row handle explicitly rather than
30186
+ leaning on container-type's implied containing block. */
30187
+ .doc-table-wrap { position:relative; contain:inline-size; container-type:inline-size; }
29377
30188
  .doc-table-scroll { overflow-x:auto; padding:2px 0; }
29378
- .doc-table-inner { width:max-content; }
30189
+ .doc-table-inner { position:relative; width:max-content; } /* anchors the drag outline / drop lines so they track horizontal scroll */
29379
30190
  .doc-table-row { display:flex; align-items:stretch; }
29380
30191
  table.doc-table { border-collapse:separate; border-spacing:0; table-layout:fixed; width:max-content;
29381
30192
  border:1px solid var(--line); border-radius:var(--radius-sm); overflow:hidden; box-shadow:var(--shadow-sm); }
30193
+ /* Default (no manual widths yet): browser auto table layout sizes columns by
30194
+ content, capped to the wrap (cqw works despite the max-content .doc-table-inner;
30195
+ 26px keeps the add-col button inside) so it wraps instead of scrolling. The
30196
+ first resizer drag freezes measured widths and drops this class (table-block.tsx). */
30197
+ table.doc-table.autofit { table-layout:auto; width:auto; max-width:calc(100cqw - 26px); }
30198
+ table.doc-table.autofit .doc-td { min-width:48px; } /* empty columns don't collapse */
30199
+ /* Code tokens are atomic: raising the column's min-content makes the auto
30200
+ algorithm shed width into wrappable prose columns instead of breaking
30201
+ \`file-names.md\` at hyphens; an over-long token falls back to the scroll
30202
+ box. Autofit only \u2014 in manual fixed layout nowrap can't grow the column,
30203
+ it would overflow the cell. Phones relax this (see the 560px block):
30204
+ there the atom IS the cap, and it crushes prose columns to their floor. */
30205
+ table.doc-table.autofit .doc-td code { white-space:nowrap; }
29382
30206
  table.doc-table td { position:relative; vertical-align:top; padding:0;
29383
30207
  border-right:1px solid var(--line); border-bottom:1px solid var(--line); }
29384
30208
  table.doc-table tr td:last-child { border-right:0; }
@@ -29390,24 +30214,59 @@ var styles_default = ` :root {
29390
30214
  .doc-td b, .doc-td strong { font-weight:700; }
29391
30215
  .doc-td code { font-family:var(--mono); background:var(--surface-2); padding:1px 5px; border-radius:4px; font-size:.9em; }
29392
30216
  .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
30217
  .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); }
30218
+ /* Row/column pill handles (Notion-style): one per axis, tracked onto the
30219
+ active cell (hovered on desktop, focused on touch) by JS. They live on
30220
+ .doc-table-wrap OUTSIDE .doc-table-scroll so overflow-x can't clip them,
30221
+ straddling the table's left/top border \u2014 never on cell text. Resting =
30222
+ quiet grey pill; pressed/dragging (.grab) = accent pill with light dots
30223
+ plus the source outline + drop line below. touch-action:none so a finger
30224
+ drag on the handle reorders instead of scrolling. Data rows only get a
30225
+ row pill (the block gutter's +/grip pins at the header row's y). */
30226
+ .doc-row-handle, .doc-col-handle { position:absolute; display:grid; place-items:center; z-index:7;
30227
+ color:var(--muted); background:var(--bg); border:1px solid var(--line); box-shadow:var(--shadow-sm);
30228
+ cursor:grab; touch-action:none; animation:gripFade .12s ease both;
30229
+ transition:background .12s,color .12s,border-color .12s; }
30230
+ .doc-row-handle { left:-8px; width:15px; height:28px; border-radius:8px; }
30231
+ .doc-col-handle { top:-6px; width:28px; height:15px; border-radius:8px; transform:translateX(-50%); }
30232
+ .doc-row-handle svg, .doc-col-handle svg { width:13px; height:13px; }
30233
+ .doc-row-handle:hover, .doc-col-handle:hover { background:var(--surface); color:var(--fg); }
30234
+ .doc-row-handle.grab, .doc-col-handle.grab { background:var(--accent); color:var(--accent-fg,#fff);
30235
+ border-color:var(--accent); cursor:grabbing; }
30236
+ .doc-row-handle::after { content:""; position:absolute; inset:-4px; }
30237
+ .doc-col-handle::after { content:""; position:absolute; inset:-4px; }
30238
+ /* drag-reorder overlays (inside .doc-table-inner, so they scroll with the
30239
+ table): accent outline on the source row/column, 2px line at the drop
30240
+ boundary (grid .rowdrop's cousin) */
30241
+ .doc-drag-outline { position:absolute; border:2px solid var(--accent); border-radius:6px;
30242
+ pointer-events:none; z-index:6; box-sizing:border-box; }
30243
+ .doc-rowdrop { position:absolute; height:2px; margin-top:-1px; background:var(--accent); border-radius:1px; pointer-events:none; z-index:6; }
30244
+ .doc-coldrop { position:absolute; width:2px; margin-left:-1px; background:var(--accent); border-radius:1px; pointer-events:none; z-index:6; }
30245
+ /* touch selection action bar: the OSK has no Delete/\u2318C, so the cell
30246
+ rectangle gets on-screen verbs; sits on the wrap layer above the selection */
30247
+ .doc-sel-bar { position:absolute; z-index:8; display:flex; gap:2px; padding:3px;
30248
+ background:var(--accent); border-radius:9px; box-shadow:var(--shadow-sm); animation:gripFade .12s ease both; }
30249
+ .doc-sel-bar button { color:var(--accent-fg,#fff); font-size:13px; line-height:1; padding:5px 9px; border-radius:6px; }
30250
+ .doc-sel-bar button:active { background:color-mix(in srgb, #000 18%, transparent); }
29405
30251
  /* add-column (right edge) and add-row (bottom edge) buttons */
29406
30252
  .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; }
30253
+ border:1px dashed var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); transition:background .12s,color .12s,border-color .12s,opacity .12s; }
30254
+ /* sticky + 100cqw (the wrap is the container): on a wide table the bar pins
30255
+ to the scrollport at any horizontal offset; narrow tables never hit the
30256
+ max-width and keep their table-wide bar. Engines without cqw just ignore
30257
+ the cap and fall back to scrolling with the table. */
30258
+ .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);
30259
+ 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
30260
  .doc-table-addcol:hover, .doc-table-addrow:hover { background:var(--accent-soft); color:var(--accent); border-color:var(--accent); }
30261
+ /* Notion-style: the add bars only materialize while the table is active \u2014
30262
+ hover or keyboard focus on desktop, an editing (focused) cell on touch.
30263
+ Opacity-only so nothing shifts; pointer-events gates off the invisible
30264
+ bars (a touch tap must not hit a bar it can't see). */
30265
+ .doc-table-addrow, .doc-table-addcol { opacity:0; pointer-events:none; }
30266
+ .doc-table-wrap:focus-within :is(.doc-table-addrow, .doc-table-addcol) { opacity:1; pointer-events:auto; }
30267
+ @media (hover:hover) and (pointer:fine) {
30268
+ .doc-table-wrap:hover :is(.doc-table-addrow, .doc-table-addcol) { opacity:1; pointer-events:auto; }
30269
+ }
29411
30270
  /* cell range selection (box select): soft fill per cell; the accent rectangle
29412
30271
  outline is drawn via inline inset box-shadow on the edge cells (see
29413
30272
  cell-select.ts edgeShadow). While selecting, suppress the caret + native
@@ -29602,7 +30461,7 @@ var styles_default = ` :root {
29602
30461
  .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
30462
  .shv-search input { border:0; background:none; outline:none; color:var(--fg); font-size:13px; width:180px; }
29604
30463
 
29605
- .shv-list { display:flex; flex-direction:column; gap:8px; }
30464
+ .shv-list { display:flex; flex-direction:column; gap:8px; list-style:none; padding:0; margin:0; }
29606
30465
  .shv-row { display:flex; align-items:center; gap:13px; padding:13px 15px; background:var(--bg); border:1px solid var(--line);
29607
30466
  border-radius:var(--radius-lg); transition:border-color .12s, box-shadow .12s; }
29608
30467
  .shv-row:hover { border-color:var(--line-strong); box-shadow:var(--shadow-sm); }
@@ -29624,7 +30483,25 @@ var styles_default = ` :root {
29624
30483
  .shv-exp.dead { text-decoration:line-through; }
29625
30484
  .shv-actions { display:flex; align-items:center; gap:6px; flex:none; }
29626
30485
  .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; } }
30486
+ @media (max-width:640px){
30487
+ /* head stacks: title/desc full row, then segmented tabs, then search */
30488
+ .shares-page .db-head { flex-direction:column; align-items:stretch; gap:12px; }
30489
+ .shv-tools { margin-left:0; width:100%; }
30490
+ .shv-seg { display:flex; width:100%; }
30491
+ .shv-seg-btn { flex:1; text-align:center; }
30492
+ .shv-search { flex:1; }
30493
+ .shv-search input { width:100%; flex:1; }
30494
+
30495
+ /* cards: title clamps to two lines, meta wraps naturally */
30496
+ .shv-row { flex-wrap:wrap; }
30497
+ .shv-title { max-width:100%; white-space:normal; display:-webkit-box;
30498
+ -webkit-line-clamp:2; -webkit-box-orient:vertical; text-align:left; }
30499
+ .shv-meta { white-space:normal; overflow:visible; text-overflow:clip; }
30500
+
30501
+ /* actions: full row of evenly stretched touch targets */
30502
+ .shv-actions { width:100%; }
30503
+ .shv-actions .btn { flex:1; justify-content:center; padding:7px 10px; }
30504
+ }
29628
30505
 
29629
30506
  /* in-context "\u5DF2\u5206\u4EAB" badge on docs / tables / site cards */
29630
30507
  .share-badge { display:inline-grid; place-items:center; width:22px; height:22px; border-radius:6px; color:var(--accent);
@@ -29701,7 +30578,9 @@ var styles_default = ` :root {
29701
30578
  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
30579
  .peek.open { transform:none; }
29703
30580
  .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; }
30581
+ /* Same stable-gutter treatment as .content (stable only \u2014 see .content on
30582
+ why both-edges is forbidden). */
30583
+ .peek-body { overflow:auto; scrollbar-gutter:stable; padding:28px 32px; }
29705
30584
  .peek h2 { font-size:27px; font-weight:700; letter-spacing:-.02em; margin:0 0 20px; outline:none; }
29706
30585
  .proprow { display:flex; gap:8px; align-items:flex-start; margin:1px 0; }
29707
30586
  .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 +30637,7 @@ var styles_default = ` :root {
29758
30637
  .hist-restore { margin-top:8px; align-self:flex-start; }
29759
30638
 
29760
30639
  /* menu / popover */
29761
- .menu-layer { position:fixed; inset:0; z-index:90; }
30640
+ .menu-layer { position:fixed; inset:0; z-index:115; }
29762
30641
  .pop { position:fixed; background:var(--bg); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow-lg);
29763
30642
  padding:6px; min-width:232px; max-height:70vh; overflow:auto; animation:pop .12s ease; }
29764
30643
  @keyframes pop { from { opacity:0; transform:translateY(-4px); } }
@@ -29776,6 +30655,9 @@ var styles_default = ` :root {
29776
30655
  .pop .item .meta { flex:1; min-width:0; }
29777
30656
  .pop .item .t { font-size:var(--fs-ui); } .pop .item .d { font-size:var(--fs-xs); color:var(--muted); display:block; }
29778
30657
  .pop .item .chk { color:var(--accent); margin-left:auto; }
30658
+ /* shortcut badge, mirrors .sb-search kbd; margin-left:auto pins it right (and
30659
+ a trailing .chk, whose own auto then collapses, rides the gap beside it) */
30660
+ .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
30661
  .pop .item.danger { color:var(--danger); } .pop .item.danger .lico { color:var(--danger); }
29780
30662
  .pop .sep { height:1px; background:var(--line); margin:5px 4px; }
29781
30663
  .pop .typegrid { display:grid; grid-template-columns:repeat(4,1fr); gap:4px; padding:2px 2px 4px; }
@@ -29895,24 +30777,28 @@ var styles_default = ` :root {
29895
30777
  .view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
29896
30778
  .ico.flip { transform:scaleX(-1); }
29897
30779
 
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); }
30780
+ /* ---- find in document (Ctrl-F, CM6 bar) ---- */
30781
+ /* Match highlights are CM6 Decoration.marks (.cm-find/.cm-find-cur, editor-theme.ts).
30782
+ Matches INSIDE void widgets (code mirror, table cells) can't be mark
30783
+ decorations \u2014 the block replace widget swallows them \u2014 so those paint via
30784
+ the CSS Custom Highlight API (find.ts collectMatches/applyHighlights). */
30785
+ ::highlight(mh-find-all) { background: color-mix(in srgb, var(--accent) 26%, transparent); border-radius: 2px; }
30786
+ ::highlight(mh-find-current) { background: var(--accent); color: var(--accent-fg); }
29902
30787
  .find-bar {
29903
- position:fixed; top:58px; right:18px; z-index:40; /* below the ~50px topbar */
30788
+ position:fixed; top:58px; right:18px; z-index:96; /* below the ~50px topbar */
29904
30789
  display:flex; align-items:center; gap:4px;
29905
30790
  padding:5px 6px; background:var(--bg); border:1px solid var(--line-strong);
29906
30791
  border-radius:var(--radius); box-shadow:var(--shadow-lg);
29907
30792
  }
29908
30793
  .find-bar .find-ico { color:var(--muted); margin:0 2px; flex:none; }
29909
30794
  .find-bar input {
29910
- width:200px; max-width:42vw; border:0; outline:0; background:transparent;
30795
+ width:200px; max-width:42vw; min-width:0; border:0; outline:0; background:transparent;
29911
30796
  color:var(--fg); font-size:13px; padding:3px 2px;
29912
30797
  }
29913
30798
  .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; }
30799
+ .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
30800
  .find-opt {
30801
+ flex:none; white-space:nowrap;
29916
30802
  border:1px solid var(--line-strong); background:transparent; color:var(--fg-soft);
29917
30803
  border-radius:var(--radius-sm); font-size:12px; line-height:1; padding:4px 6px; cursor:pointer;
29918
30804
  transition:background .12s, color .12s, border-color .12s;
@@ -29932,6 +30818,14 @@ var styles_default = ` :root {
29932
30818
  .find-bar { left:12px; right:12px; }
29933
30819
  .find-bar input { width:auto; flex:1; max-width:none; }
29934
30820
  }
30821
+ /* Very narrow windows (quicknote resizes down to 280px): drop the reserved
30822
+ count width and tighten gaps so the option buttons never get crushed. */
30823
+ @media (max-width:360px) {
30824
+ .find-bar { gap:2px; padding:5px 4px; }
30825
+ .find-count { min-width:0; }
30826
+ }
30827
+ /* Quicknote window: its qn-bar is ~34px, not the main ~50px topbar. */
30828
+ body.quicknote .find-bar { top:42px; }
29935
30829
 
29936
30830
  /* ---- board (kanban) view ---- */
29937
30831
  .board-wrap { margin-top:4px; }
@@ -30088,8 +30982,7 @@ var styles_default = ` :root {
30088
30982
  .navitem { padding:9px 8px; font-size:16px; }
30089
30983
  .navitem .emoji { font-size:18px; }
30090
30984
  .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 */
30985
+ .sb-search kbd, .pop .item kbd { display:none; } /* no hardware keyboard, no shortcut hint */
30093
30986
  /* Search box is hidden until the header search button reveals it; it then
30094
30987
  slides+fades down in. Desktop base (.sb-search{display:flex}) is untouched,
30095
30988
  so search stays always-on there. reduced-motion is handled globally. */
@@ -30104,17 +30997,26 @@ var styles_default = ` :root {
30104
30997
  /* Document: bigger body text, tighter side padding for narrow screens. */
30105
30998
  .doc { padding:24px 18px calc(36vh + env(safe-area-inset-bottom)); }
30106
30999
  .doc-title { font-size:30px; }
30107
- .doc .editable { font-size:16px; }
31000
+ /* CM6 editor (single-document): its .cm-editor inherits from here, so the whole
31001
+ body incl. table cells gets 16px on mobile \u2014 same iOS anti-zoom reason. */
31002
+ .doc .cm-doc-body { font-size:16px; }
30108
31003
  /* 16px (not 14): a focused code textarea below 16px makes iOS auto-zoom. */
30109
31004
  .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); }
31005
+ /* CM6 hover gutter: no hover on touch, and -52px overlays would overflow
31006
+ the tight mobile margins \u2014 hide. */
31007
+ .cm-gutter-layer { display:none; }
31008
+ /* Autofit tables: on phones the atomic code span IS the width cap \u2014 one
31009
+ long \`file-name.md\` becomes the column's min-content, eats the whole
31010
+ 100cqw budget and crushes prose columns to their 48px floor. Let code
31011
+ wrap at its natural break points (hyphens/slashes) instead; wider
31012
+ layouts keep the atomic-code tradeoff (see the autofit rules). */
31013
+ table.doc-table.autofit .doc-td code { white-space:pre-wrap; overflow-wrap:normal; word-break:normal; }
30114
31014
  /* Find bar clears the taller mobile topbar (49px min + notch inset). */
30115
31015
  .find-bar { top:calc(49px + env(safe-area-inset-top) + 8px); }
30116
31016
  /* narrower handle gutter on phones; .db left padding must be \u2265 --grip-gutter */
30117
31017
  .db { padding:16px 14px 90px 24px; }
31018
+ /* pages reusing .db without row grips don't need the left gutter \u2014 keep padding symmetric */
31019
+ .shares-page.db { padding-left:14px; }
30118
31020
  .gridhost { --grip-gutter:24px; }
30119
31021
  .peek { width:100%; }
30120
31022
  /* history drawer: the side-by-side list/preview doesn't fit a phone \u2014 stack. */
@@ -30149,13 +31051,19 @@ var styles_default = ` :root {
30149
31051
  @media (pointer: coarse) {
30150
31052
  /* iOS Safari auto-zooms the page when a focused field is below 16px; the
30151
31053
  class selectors out-specify the element-level sizes set on these. */
30152
- input, textarea, .text-input, .doc-source, input.inlineedit { font-size:16px; }
31054
+ input, textarea, .text-input, input.inlineedit { font-size:16px; }
30153
31055
  .navitem .acts button { width:34px; height:34px; }
31056
+ .sb-tab { min-height:40px; padding:8px 10px; }
31057
+ .sb-tabs .add { width:40px; height:40px; display:grid; place-items:center; padding:0; }
30154
31058
  .iconbtn { width:40px; height:40px; }
30155
31059
  .btn { padding:9px 14px; min-height:40px; }
30156
31060
  .pop .item { padding:11px 10px; }
30157
31061
  /* Column/sidebar resize handles: 7px is a mouse affordance, not a finger's. */
30158
31062
  .col-resizer, .doc-col-resizer, .sb-resizer { width:16px; right:-8px; }
31063
+ /* cell-rectangle corner handle: big enough to grab with a finger (it is the
31064
+ only way to grow a touch selection \u2014 pan gestures stay scrolls) */
31065
+ .doc-cell-handle { width:12px; height:12px; right:-6px; bottom:-6px; }
31066
+ .doc-cell-handle::after { inset:-10px; }
30159
31067
  }
30160
31068
 
30161
31069
  /* No hover available (touch): controls that only materialize on :hover are
@@ -30164,9 +31072,11 @@ var styles_default = ` :root {
30164
31072
  @media (hover: none) {
30165
31073
  .code-tools { opacity:1; transform:none; pointer-events:auto; }
30166
31074
  .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; }
31075
+ .cal-add, .filerow .facts,
31076
+ .navitem .acts { opacity:1; }
31077
+ /* the pill handles survive on touch (focus-driven, not hover-driven), but
31078
+ sticky tap-hover must not tint them */
31079
+ .doc-row-handle:hover, .doc-col-handle:hover { background:var(--bg); color:var(--muted); }
30170
31080
  .selcell input { opacity:1; }
30171
31081
  .tl-item .tl-h { opacity:1; }
30172
31082
  /* Neutralize the stickiest tap-residue tints (a tap "hovers" until the
@@ -30178,12 +31088,16 @@ var styles_default = ` :root {
30178
31088
  .filerow:hover { background:none; }
30179
31089
  .cal-day:hover { background:none; }
30180
31090
  .cal-day.dim:hover { background:var(--surface); }
31091
+ .doc-table-addcol:hover, .doc-table-addrow:hover { background:var(--surface); color:var(--muted); border-color:var(--line-strong); }
30181
31092
  }
30182
31093
 
30183
31094
  /* Quick Notes window (desktop, loaded at #quick). A transparent body lets the
30184
31095
  macOS vibrancy blur show through; a translucent panel keeps text readable.
30185
31096
  The top bar is the window drag region; its buttons opt back out. */
30186
- body.quicknote { background:transparent; }
31097
+ /* --pop-top-inset: editor popups (slash menu) clamp their upward growth below
31098
+ this line, so they never cover the drag bar / macOS traffic lights (bar =
31099
+ 26px actions + 2\xD74px padding). */
31100
+ body.quicknote { background:transparent; --pop-top-inset:38px; }
30187
31101
  /* html carries an opaque bg for the iOS 26 Safari toolbar tint; the vibrancy
30188
31102
  window needs the root transparent again or the blur is walled off. */
30189
31103
  html:has(body.quicknote) { background:transparent; }
@@ -30203,22 +31117,32 @@ var styles_default = ` :root {
30203
31117
  .qn-actions { display:flex; gap:2px; -webkit-app-region:no-drag; }
30204
31118
  .qn-actions .iconbtn { width:26px; height:26px; }
30205
31119
  .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; }
31120
+ /* Quicknote scrolling: the app-wide thin translucent scrollbar (transparent
31121
+ track, so the reserved lane just reads as right margin). stable reserves
31122
+ the lane up front so width never jumps when the doc grows past one
31123
+ screen; the doc's right padding + lane \u2248 left padding, keeping the prose
31124
+ visually centered. stable only, NEVER both-edges \u2014 see .content: a left
31125
+ gutter desyncs Chromium's scroll cull rect and paints a blank stripe over
31126
+ the rightmost 15px of content (it sliced this window's file cards). */
31127
+ /* scrollbar-width does NOT inherit (unlike scrollbar-color): without an
31128
+ explicit thin here the lane is the classic 15px. thin = 11px, so the
31129
+ doc's 1px right padding + 11px lane mirrors the 12px left padding. */
31130
+ .qn-body { flex:1; overflow:auto; scrollbar-gutter:stable; scrollbar-width:thin; -webkit-app-region:no-drag; }
31131
+ .qn-body .doc { max-width:none; margin:0; padding:14px 1px 30vh 12px; }
30208
31132
  .qn-body .doc-title { font-size:24px; margin-bottom:2px; }
30209
31133
  .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
31134
+ /* The CM6 hover gutter never fits the compact window: quick notes offer no
30212
31135
  block insert/drag handles (Enter / slash commands cover insertion). */
30213
- body.quicknote .block .gutter { display:none; }
31136
+ body.quicknote .cm-gutter-layer { display:none; }
30214
31137
  /* A native checkbox paints an opaque white box when unchecked, which breaks
30215
31138
  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 {
31139
+ stays transparent; checked falls back to the accent fill + checkmark.
31140
+ (Same treatment main gave \`.b-todo .marker input\`.) */
31141
+ body.quicknote .cm-todo-box {
30218
31142
  appearance:none; -webkit-appearance:none; box-sizing:border-box;
30219
31143
  border:1.5px solid color-mix(in srgb, var(--fg-soft) 55%, transparent);
30220
31144
  border-radius:4px; background:transparent; }
30221
- body.quicknote .b-todo .marker input:checked {
31145
+ body.quicknote .cm-todo-box:checked {
30222
31146
  border-color:var(--accent);
30223
31147
  background:var(--accent) center/10px no-repeat
30224
31148
  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 +31164,11 @@ var styles_default = ` :root {
30240
31164
  .qn-set-main { flex:1; min-width:0; }
30241
31165
  .qn-set-name { font-weight:600; }
30242
31166
  .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); }
31167
+ .qn-shortcut { min-width:140px; justify-content:center; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif; }
30244
31168
  .qn-shortcut.capturing { outline:2px solid var(--accent); color:var(--accent); }
31169
+ .qn-keys { display:inline-flex; align-items:center; gap:6px; }
31170
+ .qn-key { line-height:1; }
31171
+ .qn-key.sym { font-size:1.15em; line-height:1; } /* \u2318 \u21E7 glyphs render smaller \u2014 bump to match letters */
30245
31172
 
30246
31173
  /* No-origin bootstrap: enroll screen + hydration splash (data-blind shell). */
30247
31174
  .enroll-splash { min-height:100dvh; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; padding:24px; }
@@ -30255,6 +31182,44 @@ var styles_default = ` :root {
30255
31182
  .enroll-err { color:var(--danger); font-size:13px; margin-top:12px; }
30256
31183
  .enroll-go { width:100%; justify-content:center; margin-top:18px; }
30257
31184
  .enroll-hint { font-size:12px; margin-top:14px; line-height:1.5; }
31185
+ .enroll-card { animation:enroll-in .35s ease both; }
31186
+ @keyframes enroll-in { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } }
31187
+
31188
+ /* Scan-first enroll: square viewfinder (corner frame + sweeping accent beam)
31189
+ over the live camera feed. The chip stays black in both themes \u2014 the feed
31190
+ brings its own colors; accent carries the beam and the success flash. */
31191
+ .enroll-scan { position:relative; aspect-ratio:1/1; margin:2px 0 12px; border-radius:16px; overflow:hidden; background:#0b0b0e; }
31192
+ .enroll-scan video { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
31193
+ .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); }
31194
+ .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; }
31195
+ .enroll-scan-beam { position:absolute; left:9%; right:9%; top:12%; height:2px; border-radius:2px; pointer-events:none; opacity:.85;
31196
+ background:linear-gradient(90deg, transparent, var(--accent), transparent);
31197
+ animation:enroll-beam 2.4s ease-in-out infinite alternate; }
31198
+ @keyframes enroll-beam { to { top:86%; } }
31199
+ .enroll-scan.hit .enroll-scan-frame { stroke:var(--accent); }
31200
+ .enroll-scan.hit::after { animation:enroll-hit .38s ease-out; }
31201
+ @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); } }
31202
+ .enroll-scan-wait { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:rgba(255,255,255,.45); }
31203
+ .enroll-scan-waitico { width:30px; height:30px; animation:enroll-wait 1.6s ease-in-out infinite; }
31204
+ @keyframes enroll-wait { 0%,100% { opacity:.4; } 50% { opacity:.9; } }
31205
+ .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; }
31206
+ .enroll-cam-err .muted { color:rgba(255,255,255,.55); font-size:12px; }
31207
+ .enroll-cam-errico { width:26px; height:26px; opacity:.7; }
31208
+ .enroll-cam-err .btn { margin-top:8px; }
31209
+ .enroll-paste { width:100%; font-size:13px; }
31210
+ .enroll-switch { display:flex; justify-content:center; align-items:center; gap:10px; margin-top:14px; }
31211
+ .enroll-switch-dot { color:var(--muted); }
31212
+ .enroll-link { background:none; border:0; padding:0; font:inherit; font-size:13px; color:var(--accent); cursor:pointer; }
31213
+ .enroll-link:hover { text-decoration:underline; }
31214
+ /* Unlock step: compact bucket summary chip above the passphrase field. */
31215
+ .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); }
31216
+ .enroll-bucket-ico { width:18px; height:18px; color:var(--muted); }
31217
+ .enroll-bucket-meta { flex:1; min-width:0; }
31218
+ .enroll-bucket-name { font-weight:600; font-size:14px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
31219
+ .enroll-bucket-host { font-family:var(--mono); font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
31220
+ .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;
31221
+ border:1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
31222
+ .enroll-bucket-badge .ico { width:12px; height:12px; }
30258
31223
 
30259
31224
  /* Enroll QR (settings \u2192 \u540C\u6B65\u5B58\u50A8 \u2192 \u5728\u624B\u673A\u4E0A\u6253\u5F00). The QR inverts with the
30260
31225
  theme: light = dark modules on a light chip, dark = light modules on a dark
@@ -30457,6 +31422,42 @@ var ICON_192 = "iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAYAAABS3GwHAAAAAXNSR0IArs4c6QAA
30457
31422
  var ICON_512 = "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";
30458
31423
  var ICON_180 = "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";
30459
31424
 
31425
+ // src/webui/fmt/manifest.ts
31426
+ var FMT_PROVIDERS = [
31427
+ { id: "core", js: "/webui-fmt.js", entry: "fmt/fmt-core.ts" },
31428
+ {
31429
+ id: "ruff",
31430
+ js: "/webui-fmt-ruff.js",
31431
+ entry: "fmt/fmt-ruff.ts",
31432
+ wasm: { route: "/webui-fmt-ruff.wasm", pkg: "@wasm-fmt/ruff_fmt", file: "ruff_fmt_bg.wasm" }
31433
+ },
31434
+ {
31435
+ id: "gofmt",
31436
+ js: "/webui-fmt-gofmt.js",
31437
+ entry: "fmt/fmt-gofmt.ts",
31438
+ wasm: { route: "/webui-fmt-gofmt.wasm", pkg: "@wasm-fmt/gofmt", file: "gofmt.wasm" }
31439
+ },
31440
+ {
31441
+ id: "clang",
31442
+ js: "/webui-fmt-clang.js",
31443
+ entry: "fmt/fmt-clang.ts",
31444
+ wasm: { route: "/webui-fmt-clang.wasm", pkg: "@wasm-fmt/clang-format", file: "clang-format.wasm" }
31445
+ },
31446
+ {
31447
+ id: "lua",
31448
+ js: "/webui-fmt-lua.js",
31449
+ entry: "fmt/fmt-lua.ts",
31450
+ wasm: { route: "/webui-fmt-lua.wasm", pkg: "@wasm-fmt/lua_fmt", file: "lua_fmt_bg.wasm" }
31451
+ },
31452
+ {
31453
+ id: "taplo",
31454
+ js: "/webui-fmt-taplo.js",
31455
+ entry: "fmt/fmt-taplo.ts",
31456
+ wasm: { route: "/webui-fmt-taplo.wasm", pkg: "@wasm-fmt/taplo_fmt", file: "taplo_fmt_bg.wasm" }
31457
+ },
31458
+ { id: "sh", js: "/webui-fmt-sh.js", entry: "fmt/fmt-sh.ts" }
31459
+ ];
31460
+
30460
31461
  // src/webui/server/assets.ts
30461
31462
  var RUNNING_FROM_SOURCE = import.meta.url.includes("/src/webui/");
30462
31463
  var HTML2 = `<!doctype html>
@@ -30501,6 +31502,7 @@ var injected = {
30501
31502
  sdk: null
30502
31503
  };
30503
31504
  var injectedWasm = null;
31505
+ var injectedFmtWasm = {};
30504
31506
  function newestSourceMtime(dir) {
30505
31507
  let newest = 0;
30506
31508
  for (const e3 of readdirSync4(dir, { withFileTypes: true })) {
@@ -30549,6 +31551,24 @@ function bundleGetter(opts) {
30549
31551
  }
30550
31552
  var getJs = bundleGetter({ key: "js", entry: "../app.tsx", dist: "webui.js" });
30551
31553
  var getSwRaw = bundleGetter({ key: "sw", entry: "../sw.ts", dist: "sw.js" });
31554
+ var fmtJs = {};
31555
+ for (const p2 of FMT_PROVIDERS) {
31556
+ injected[`fmt_${p2.id}`] = null;
31557
+ fmtJs[p2.js] = bundleGetter({ key: `fmt_${p2.id}`, entry: `../${p2.entry}`, dist: p2.js.slice(1) });
31558
+ }
31559
+ var fmtWasmByRoute = Object.fromEntries(FMT_PROVIDERS.filter((p2) => p2.wasm).map((p2) => [p2.wasm.route, p2]));
31560
+ async function getFmtWasm(p2) {
31561
+ const w4 = p2.wasm;
31562
+ const embedded = injectedFmtWasm[w4.route];
31563
+ if (embedded != null)
31564
+ return embedded;
31565
+ 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));
31566
+ const f4 = Bun.file(path);
31567
+ if (!await f4.exists()) {
31568
+ throw new Error(`${w4.route} missing at ${path} \u2014 run \`bun install\` / \`bun run build\``);
31569
+ }
31570
+ return f4.arrayBuffer();
31571
+ }
30552
31572
  var getDbWorker = bundleGetter({ key: "dbWorker", entry: "../data/db-worker.ts", dist: "db-worker.js" });
30553
31573
  var getRuntime = bundleGetter({ key: "runtime", entry: "../runtime.ts", dist: "mh-runtime.js" });
30554
31574
  var getSdk = bundleGetter({ key: "sdk", entry: "../../sdk/client.ts", dist: "metahub-sdk.js", extraDirs: ["../../sdk"] });
@@ -30630,12 +31650,24 @@ async function serveWebui(req) {
30630
31650
  return new Response(`sqlite3.wasm unavailable: ${e3}`, { status: 500 });
30631
31651
  }
30632
31652
  }
31653
+ const fmtWasmProvider = fmtWasmByRoute[pathname];
31654
+ if (fmtWasmProvider) {
31655
+ try {
31656
+ return new Response(await getFmtWasm(fmtWasmProvider), {
31657
+ headers: { "content-type": "application/wasm", "cache-control": "public, max-age=86400" }
31658
+ });
31659
+ } catch (e3) {
31660
+ console.error(`[webui] failed to serve ${pathname} \u2014`, e3);
31661
+ return new Response(`${pathname} unavailable: ${e3}`, { status: 500 });
31662
+ }
31663
+ }
30633
31664
  const script = {
30634
31665
  "/webui.js": getJsStamped,
30635
31666
  "/sw.js": getSw,
30636
31667
  "/db-worker.js": getDbWorker,
30637
31668
  "/mh-runtime.js": getRuntime,
30638
- "/metahub-sdk.js": getSdk
31669
+ "/metahub-sdk.js": getSdk,
31670
+ ...fmtJs
30639
31671
  };
30640
31672
  const getter = script[pathname];
30641
31673
  if (getter) {
@@ -31800,6 +32832,7 @@ COMMANDS
31800
32832
  setup
31801
32833
  init Create ~/.metahub (SQLite db + cache dir)
31802
32834
  init --claude Install the /mh skill into ~/.claude (Claude Code)
32835
+ init --codex Install the $mh skill into ~/.codex (Codex)
31803
32836
  databases
31804
32837
  db create <name> [--icon] Create a database (table)
31805
32838
  db list | get <ref> | delete <ref>
@@ -31879,6 +32912,7 @@ var EXAMPLES = {
31879
32912
  init: [
31880
32913
  "mh init",
31881
32914
  "mh init --claude # install the /mh skill into ~/.claude (Claude Code)",
32915
+ "mh init --codex # install the $mh skill into ~/.codex (Codex)",
31882
32916
  "METAHUB_HOME=/tmp/mh mh init # use an alternate home"
31883
32917
  ],
31884
32918
  "db create": ['mh db create "Tasks"', 'mh db create "Notes" --icon \uD83D\uDCD3'],
@@ -32077,4 +33111,4 @@ if (argv2.includes("--server")) {
32077
33111
  runMain(main, { showUsage: showUsage2 });
32078
33112
  }
32079
33113
 
32080
- //# debugId=DFF4B33AB2E4208164756E2164756E21
33114
+ //# debugId=65C9A394BDB4F1CE64756E2164756E21