finch-markdown-editor 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -204,12 +204,55 @@ function substituteFinchFileImagesForBm(markdown) {
204
204
  });
205
205
  return { markdown: substituted, urls };
206
206
  }
207
+ var MERMAID_THEME_BY_STYLE = {
208
+ kami: "solarized-light",
209
+ bauhaus: "github-light",
210
+ blueprint: "nord",
211
+ botanical: "catppuccin-latte",
212
+ newsprint: "github-light",
213
+ retro: "solarized-dark",
214
+ sketch: "github-light",
215
+ terminal: "one-dark"
216
+ };
217
+ var MERMAID_THEME_COLORS = {
218
+ "zinc-dark": { bg: "#18181B", fg: "#FAFAFA" },
219
+ "tokyo-night": { bg: "#1a1b26", fg: "#a9b1d6", line: "#3d59a1", accent: "#7aa2f7", muted: "#565f89" },
220
+ "tokyo-night-storm": { bg: "#24283b", fg: "#a9b1d6", line: "#3d59a1", accent: "#7aa2f7", muted: "#565f89" },
221
+ "tokyo-night-light": { bg: "#d5d6db", fg: "#343b58", line: "#34548a", accent: "#34548a", muted: "#9699a3" },
222
+ "catppuccin-mocha": { bg: "#1e1e2e", fg: "#cdd6f4", line: "#585b70", accent: "#cba6f7", muted: "#6c7086" },
223
+ "catppuccin-latte": { bg: "#eff1f5", fg: "#4c4f69", line: "#9ca0b0", accent: "#8839ef", muted: "#9ca0b0" },
224
+ nord: { bg: "#2e3440", fg: "#d8dee9", line: "#4c566a", accent: "#88c0d0", muted: "#616e88" },
225
+ "nord-light": { bg: "#eceff4", fg: "#2e3440", line: "#aab1c0", accent: "#5e81ac", muted: "#7b88a1" },
226
+ dracula: { bg: "#282a36", fg: "#f8f8f2", line: "#6272a4", accent: "#bd93f9", muted: "#6272a4" },
227
+ "github-light": { bg: "#ffffff", fg: "#1f2328", line: "#d1d9e0", accent: "#0969da", muted: "#59636e" },
228
+ "github-dark": { bg: "#0d1117", fg: "#e6edf3", line: "#3d444d", accent: "#4493f8", muted: "#9198a1" },
229
+ "solarized-light": { bg: "#fdf6e3", fg: "#657b83", line: "#93a1a1", accent: "#268bd2", muted: "#93a1a1" },
230
+ "solarized-dark": { bg: "#002b36", fg: "#839496", line: "#586e75", accent: "#268bd2", muted: "#586e75" },
231
+ "one-dark": { bg: "#282c34", fg: "#abb2bf", line: "#4b5263", accent: "#c678dd", muted: "#5c6370" }
232
+ };
233
+ var MERMAID_FIGURE_SVG_STYLE_RE = /(<figure class="figure-mermaid"[^>]*>\s*<svg\b[^>]*?\sstyle=")/g;
234
+ function applyMermaidThemeVars(html, themeId) {
235
+ const colors = MERMAID_THEME_COLORS[themeId];
236
+ if (!colors) return html;
237
+ const vars = [
238
+ `--bg:${colors.bg};`,
239
+ `--fg:${colors.fg};`,
240
+ colors.line ? `--line:${colors.line};` : "",
241
+ colors.accent ? `--accent:${colors.accent};` : "",
242
+ colors.muted ? `--muted:${colors.muted};` : ""
243
+ ].filter(Boolean).join("");
244
+ return html.replace(MERMAID_FIGURE_SVG_STYLE_RE, (_match, prefix) => `${prefix}${vars}`);
245
+ }
207
246
  async function renderWithBm(markdown, markdownStyle, customCss) {
208
- const args = ["render", "--platform", "wechat", "--markdown-style", markdownStyle || "kami"];
247
+ const style = markdownStyle || "kami";
248
+ const args = ["render", "--platform", "wechat", "--markdown-style", style];
249
+ const mermaidTheme = MERMAID_THEME_BY_STYLE[style];
250
+ if (mermaidTheme) args.push("--mermaid-theme", mermaidTheme);
209
251
  if (customCss && customCss.trim()) args.push("--custom-css", customCss);
210
252
  const sized = prepareObsidianImageWidths(markdown);
211
253
  const prepared = substituteFinchFileImagesForBm(sized.markdown);
212
254
  let html = await runBmmd(args, prepared.markdown);
255
+ if (mermaidTheme) html = applyMermaidThemeVars(html, mermaidTheme);
213
256
  html = applyObsidianImageWidths(html, sized.markers);
214
257
  for (const [placeholder, originalUrl] of prepared.urls) html = html.split(placeholder).join(originalUrl);
215
258
  return html;
@@ -273,6 +316,7 @@ async function openMarkdownImagePreview(ctx, rawUrl) {
273
316
  var STYLE_SLOT_COUNT = 3;
274
317
  var WRITING_STYLE_IDS = /* @__PURE__ */ new Set(["kami", "bauhaus", "blueprint", "botanical", "newsprint", "retro", "sketch", "terminal", "custom"]);
275
318
  var MAX_CUSTOM_STYLE_CSS_LENGTH = 2e5;
319
+ var MAX_LIBRARY_GROUP_IDS = 500;
276
320
  function result(message, isError = false) {
277
321
  return { content: [{ type: "text", text: message }], isError };
278
322
  }
@@ -285,6 +329,33 @@ function styleSlotsFile(ctx) {
285
329
  function writingPreferencesFile(ctx) {
286
330
  return path3.join(ctx.storagePath, "writing-preferences.json");
287
331
  }
332
+ function libraryGroupsFile(ctx) {
333
+ return path3.join(ctx.storagePath, "library-groups.json");
334
+ }
335
+ function normalizeLibraryGroups(raw) {
336
+ const value = raw && typeof raw === "object" ? raw : {};
337
+ const order = Array.isArray(value.order) ? value.order.filter((id) => typeof id === "string").slice(0, MAX_LIBRARY_GROUP_IDS) : [];
338
+ const collapsedRaw = value.collapsed && typeof value.collapsed === "object" ? value.collapsed : {};
339
+ const collapsed = {};
340
+ Object.keys(collapsedRaw).slice(0, MAX_LIBRARY_GROUP_IDS).forEach((id) => {
341
+ collapsed[id] = collapsedRaw[id] === true;
342
+ });
343
+ return { order, collapsed };
344
+ }
345
+ async function readLibraryGroups(ctx) {
346
+ try {
347
+ const raw = await readFile2(libraryGroupsFile(ctx), "utf8");
348
+ return normalizeLibraryGroups(JSON.parse(raw));
349
+ } catch {
350
+ return void 0;
351
+ }
352
+ }
353
+ async function writeLibraryGroups(ctx, raw) {
354
+ const state = normalizeLibraryGroups(raw);
355
+ await mkdir2(ctx.storagePath, { recursive: true });
356
+ await writeFile2(libraryGroupsFile(ctx), JSON.stringify(state), "utf8");
357
+ return state;
358
+ }
288
359
  function normalizeWritingPreferences(raw) {
289
360
  const value = raw && typeof raw === "object" ? raw : {};
290
361
  const fontSize = value.fontSize === 16 || value.fontSize === 18 ? value.fontSize : 14;
@@ -684,9 +755,10 @@ async function getAssistantName(ctx) {
684
755
  }
685
756
  async function sendReady(ctx, panel) {
686
757
  const pickFileSupported = ctx.api.supports("ui.pickFile");
687
- const [styleSlots, writingPreferences, assistantName] = await Promise.all([
758
+ const [styleSlots, writingPreferences, libraryGroups, assistantName] = await Promise.all([
688
759
  readStyleSlots(ctx),
689
760
  readWritingPreferences(ctx),
761
+ readLibraryGroups(ctx),
690
762
  getAssistantName(ctx)
691
763
  ]);
692
764
  ctx.logger.info(`sending ready to panel; pickFileSupported = ${pickFileSupported}`);
@@ -696,6 +768,7 @@ async function sendReady(ctx, panel) {
696
768
  pickFileSupported,
697
769
  styleSlots,
698
770
  writingPreferences,
771
+ libraryGroups,
699
772
  assistantName,
700
773
  // So the page can render `cwd` the OS-friendly way (`~/…`) without a
701
774
  // round trip — it never needs the raw value for anything but display.
@@ -717,6 +790,22 @@ async function revealInFileManager(ctx, targetPath) {
717
790
  ctx.logger.warn(`Could not open file manager for ${targetPath}: ${String(error)}`);
718
791
  }
719
792
  }
793
+ async function rememberLastAiSession(ctx, sourcePath, sessionId) {
794
+ try {
795
+ await mutateState(ctx, (state) => {
796
+ state.lastAiSessions = { ...state.lastAiSessions, [sourcePath]: { sessionId, at: Date.now() } };
797
+ });
798
+ } catch (error) {
799
+ ctx.logger.warn(`Could not persist last AI session: ${String(error)}`);
800
+ }
801
+ }
802
+ async function readLastAiSession(ctx, sourcePath) {
803
+ const state = await readLastPathState(ctx);
804
+ const id = state.lastAiSessions?.[sourcePath]?.sessionId;
805
+ if (!id) return void 0;
806
+ const session = await ctx.sessions.get(id).catch(() => void 0);
807
+ return session ? id : void 0;
808
+ }
720
809
  async function readRewriteSession(ctx, sourcePath) {
721
810
  const state = await readLastPathState(ctx);
722
811
  const id = state.rewriteSessions?.[sourcePath];
@@ -780,6 +869,36 @@ async function notifyRewritePanels(sourcePath, message) {
780
869
  await Promise.all(targets.map((panel) => panel.postMessage(message).catch(() => {
781
870
  })));
782
871
  }
872
+ async function restoreInFlightOperations(ctx, panel, sourcePath) {
873
+ const state = await readLastPathState(ctx);
874
+ const operation = state.rewriteOperations?.[sourcePath];
875
+ if (operation) await panel.postMessage({
876
+ type: "rewriteSessionStarted",
877
+ sessionId: operation.sessionId,
878
+ turnId: operation.turnId,
879
+ startLine: operation.startLine,
880
+ endLine: operation.endLine,
881
+ rewriteMode: operation.rewriteMode
882
+ });
883
+ const styleOperation = state.styleOperations?.[sourcePath];
884
+ if (styleOperation) await panel.postMessage({ type: "styleSessionStarted", sessionId: styleOperation.sessionId });
885
+ const pendingStyle = state.pendingStyles?.[sourcePath];
886
+ if (pendingStyle) await panel.postMessage({ type: "customStyleSet", css: pendingStyle.css, label: pendingStyle.label });
887
+ const lastSessionId = await readLastAiSession(ctx, sourcePath);
888
+ await panel.postMessage({ type: "lastSessionInfo", sessionId: lastSessionId ?? null });
889
+ }
890
+ async function cancelRewriteSession(ctx, panel, message) {
891
+ const sourcePath = String(message.path ?? "").trim();
892
+ const sessionId = String(message.sessionId ?? "").trim();
893
+ const turnId = String(message.turnId ?? "").trim();
894
+ if (panel.view !== "appView" || !path3.isAbsolute(sourcePath) || !sessionId || !turnId) return;
895
+ const operation = (await readLastPathState(ctx)).rewriteOperations?.[sourcePath];
896
+ if (!operation || operation.sessionId !== sessionId || operation.turnId !== turnId) return;
897
+ const accepted = await ctx.sessions.cancelTurn(sessionId, turnId);
898
+ if (accepted) {
899
+ await notifyRewritePanels(sourcePath, { type: "rewriteCancellationRequested", sessionId, turnId });
900
+ }
901
+ }
783
902
  async function startRewriteSession(ctx, panel, message) {
784
903
  const sourcePath = String(message.path ?? "").trim();
785
904
  const selectedText = String(message.selectedText ?? "").trim();
@@ -796,12 +915,13 @@ async function startRewriteSession(ctx, panel, message) {
796
915
  const session = await ctx.sessions.create({
797
916
  ...scope.spaceId ? { space: { spaceId: scope.spaceId } } : {},
798
917
  title: `\u6539\u5199\uFF1A${path3.basename(sourcePath)}`,
799
- activity: "interactive",
918
+ activity: "background",
800
919
  permissionMode: "acceptCalls"
801
920
  });
802
921
  sessionId = session.sessionId;
803
922
  await rememberRewriteSession(ctx, sourcePath, sessionId);
804
923
  }
924
+ await rememberLastAiSession(ctx, sourcePath, sessionId);
805
925
  const lineText = message.startLine ? `\u4F4D\u7F6E\uFF1A\u7B2C ${message.startLine}${message.endLine && message.endLine !== message.startLine ? `\u2013${message.endLine}` : ""} \u884C\u3002` : "";
806
926
  const prompt = rewriteMode === "continue" ? `\u8BF7\u5728\u4E0B\u9762\u8FD9\u4EFD Markdown \u6587\u4EF6\u7684\u6307\u5B9A\u4F4D\u7F6E\u7EED\u5199\u5185\u5BB9\uFF0C\u5E76\u628A\u7ED3\u679C\u5199\u56DE\u6587\u4EF6\u3002
807
927
 
@@ -839,6 +959,7 @@ ${selectedText}
839
959
  await notifyRewritePanels(sourcePath, {
840
960
  type: "rewriteSessionStarted",
841
961
  sessionId,
962
+ turnId: receipt.turnId,
842
963
  spaceName: scope.spaceName,
843
964
  title: `${rewriteMode === "continue" ? "\u7EED\u5199" : "\u6539\u5199"}\uFF1A${path3.basename(sourcePath)}`,
844
965
  startLine: operation.startLine,
@@ -851,6 +972,7 @@ ${selectedText}
851
972
  await notifyRewritePanels(sourcePath, {
852
973
  type: result2.state === "completed" ? "rewriteSessionFinished" : "rewriteSessionFailed",
853
974
  sessionId,
975
+ turnId: receipt.turnId,
854
976
  message: result2.state === "completed" ? `${verb}\u5DF2\u5B8C\u6210\u3002` : result2.state === "timeout" ? `${verb}\u4ECD\u5728\u4F1A\u8BDD\u4E2D\u7EE7\u7EED\u3002` : `${verb}\u4F1A\u8BDD\u672A\u5B8C\u6210\u3002`
855
977
  });
856
978
  });
@@ -886,12 +1008,13 @@ async function startStyleSession(ctx, panel, message) {
886
1008
  const session = await ctx.sessions.create({
887
1009
  ...scope.spaceId ? { space: { spaceId: scope.spaceId } } : {},
888
1010
  title: `\u8BBE\u8BA1\u6392\u7248\uFF1A${path3.basename(sourcePath)}`,
889
- activity: "interactive",
1011
+ activity: "background",
890
1012
  permissionMode: "acceptCalls"
891
1013
  });
892
1014
  sessionId = session.sessionId;
893
1015
  await rememberStyleSession(ctx, sourcePath, sessionId);
894
1016
  }
1017
+ await rememberLastAiSession(ctx, sourcePath, sessionId);
895
1018
  const prompt = `\u8BF7\u4E3A\u8FD9\u7BC7\u516C\u4F17\u53F7\u6587\u7AE0\u8BBE\u8BA1\u4E00\u5957\u81EA\u5B9A\u4E49\u6392\u7248 CSS\u3002${baseNote ? baseNote + "\uFF0C" : ""}\u4F60\u7684 CSS \u4F1A\u53E0\u52A0\u5728\u57FA\u7840\u98CE\u683C\u4E4B\u4E0A\u3002\u8981\u6C42\uFF1A\u53EA\u5199\u666E\u901A CSS \u89C4\u5219\uFF0C\u9009\u62E9\u5668\u9650\u5B9A\u5728 #bm-md \u4E0B\u7684\u6807\u7B7E/\u7ED3\u6784\uFF08\u5982 #bm-md h1\u3001#bm-md p\u3001#bm-md blockquote\u3001#bm-md pre code\u3001#bm-md a\u3001#bm-md strong\u3001#bm-md table \u7B49\uFF09\uFF0C\u4E0D\u8981\u4F7F\u7528 class\uFF0C\u5FC5\u8981\u65F6\u7528 !important \u8986\u76D6\u57FA\u7840\u98CE\u683C\u3002\u53EF\u53C2\u8003 bm.md \u5185\u7F6E\u98CE\u683C\u7684\u8BBE\u8BA1\u8BED\u8A00\uFF1Akami\uFF08\u6696\u8272\u7EB8\u611F\uFF09\u3001bauhaus\uFF08\u51E0\u4F55\u649E\u8272\uFF09\u3001blueprint\uFF08\u6280\u672F\u84DD\u56FE\u7F51\u683C\uFF09\u3001botanical\uFF08\u6E05\u65B0\u7EFF\u610F\uFF09\u3001newsprint\uFF08\u62A5\u520A\u886C\u7EBF\uFF09\u3001retro\uFF08\u590D\u53E4\u6000\u65E7\uFF09\u3001sketch\uFF08\u624B\u7ED8\u98CE\uFF09\u3001terminal\uFF08\u7B49\u5BBD\u6697\u8272\u7EC8\u7AEF\u98CE\uFF09\u3002\u6587\u7AE0\u8DEF\u5F84\uFF1A${sourcePath}\u3002\u8981\u6C42\uFF1A${requirement}\u3002\u8BBE\u8BA1\u597D\u540E\u76F4\u63A5\u8C03\u7528 markdown_editor_document \u7684 set_style\uFF08\u4F20 path="${sourcePath}"\uFF0Ccss \u548C\u7B80\u77ED label\uFF0C\u4E0D\u8981\u4F20 slot\u2014\u2014\u4F20 path \u662F\u4E3A\u4E86\u8BA9\u5B83\u80FD\u627E\u5230\u8FD9\u7BC7\u6587\u6863\u5BF9\u5E94\u7684\u9884\u89C8\u7A97\u53E3\uFF0C\u5373\u4F7F\u7528\u6237\u5DF2\u7ECF\u5207\u6362\u5230\u522B\u7684\u754C\u9762\uFF09\uFF0C\u8BA9\u5B83\u5E94\u7528\u5230\u9884\u89C8\uFF1B\u4E0D\u8981\u5728\u8FD9\u91CC\u8BE2\u95EE\u8981\u8986\u76D6\u54EA\u4E2A\u69FD\u4F4D\u2014\u2014\u9762\u677F\u4F1A\u81EA\u5DF1\u7ED9\u7528\u6237\u4E00\u4E2A\u8F7B\u91CF\u7684\u201C\u4FDD\u5B58\u4E3A\u81EA\u5B9A\u4E49\u98CE\u683C\u201D\u6309\u94AE\uFF0C\u7528\u6237\u56DE\u5230\u8FD9\u7BC7\u6587\u6863\u65F6\u4E5F\u8FD8\u80FD\u770B\u5230\u3002\u5B8C\u6210\u540E\u7528\u4E00\u4E24\u53E5\u8BDD\u7B80\u77ED\u8BF4\u660E\u8BBE\u8BA1\u601D\u8DEF\u5373\u53EF\u3002`;
896
1019
  const receipt = await ctx.sessions.send(sessionId, {
897
1020
  text: prompt,
@@ -936,21 +1059,7 @@ async function handleMessage(ctx, panel, raw) {
936
1059
  await panel.postMessage({ type: "lastFileUnavailable" });
937
1060
  }
938
1061
  const currentPath = livePanelDocuments.get(panel.id)?.path;
939
- if (currentPath) {
940
- const state = await readLastPathState(ctx);
941
- const operation = state.rewriteOperations?.[currentPath];
942
- if (operation) await panel.postMessage({
943
- type: "rewriteSessionStarted",
944
- sessionId: operation.sessionId,
945
- startLine: operation.startLine,
946
- endLine: operation.endLine,
947
- rewriteMode: operation.rewriteMode
948
- });
949
- const styleOperation = state.styleOperations?.[currentPath];
950
- if (styleOperation) await panel.postMessage({ type: "styleSessionStarted", sessionId: styleOperation.sessionId });
951
- const pendingStyle = state.pendingStyles?.[currentPath];
952
- if (pendingStyle) await panel.postMessage({ type: "customStyleSet", css: pendingStyle.css, label: pendingStyle.label });
953
- }
1062
+ if (currentPath) await restoreInFlightOperations(ctx, panel, currentPath);
954
1063
  return;
955
1064
  }
956
1065
  case "openImage": {
@@ -1000,6 +1109,7 @@ async function handleMessage(ctx, panel, raw) {
1000
1109
  watchSource(ctx, panel, sourcePath);
1001
1110
  await rememberLastPath(ctx, panel, sourcePath);
1002
1111
  await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath), draftRestored, draftConflict, diskMarkdown });
1112
+ await restoreInFlightOperations(ctx, panel, sourcePath);
1003
1113
  } catch (error) {
1004
1114
  ctx.logger.error(`pickFile() threw: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
1005
1115
  await panel.postMessage({
@@ -1021,6 +1131,7 @@ async function handleMessage(ctx, panel, raw) {
1021
1131
  watchSource(ctx, panel, sourcePath);
1022
1132
  await rememberLastPath(ctx, panel, sourcePath);
1023
1133
  await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath), draftRestored, draftConflict, diskMarkdown });
1134
+ await restoreInFlightOperations(ctx, panel, sourcePath);
1024
1135
  } catch (error) {
1025
1136
  await panel.postMessage({ type: "error", message: `Cannot read file: ${error instanceof Error ? error.message : String(error)}` });
1026
1137
  }
@@ -1077,6 +1188,10 @@ async function handleMessage(ctx, panel, raw) {
1077
1188
  await startRewriteSession(ctx, panel, message);
1078
1189
  return;
1079
1190
  }
1191
+ case "cancelRewrite": {
1192
+ await cancelRewriteSession(ctx, panel, message);
1193
+ return;
1194
+ }
1080
1195
  case "requestStyleSession": {
1081
1196
  await startStyleSession(ctx, panel, message);
1082
1197
  return;
@@ -1102,6 +1217,14 @@ async function handleMessage(ctx, panel, raw) {
1102
1217
  }
1103
1218
  return;
1104
1219
  }
1220
+ case "saveLibraryGroups": {
1221
+ try {
1222
+ await writeLibraryGroups(ctx, message.libraryGroups);
1223
+ } catch (error) {
1224
+ ctx.logger.warn(`Could not save library group order: ${String(error)}`);
1225
+ }
1226
+ return;
1227
+ }
1105
1228
  case "saveDraft": {
1106
1229
  const sourcePath = String(message.path ?? "").trim();
1107
1230
  if (!path3.isAbsolute(sourcePath)) return;
package/dist/panel.css CHANGED
@@ -138,7 +138,7 @@ body.app-view .status-inline-btn:hover{background:var(--finch-bg-hover,var(--bg)
138
138
  @media (prefers-reduced-motion:reduce){.confirm-overlay,.confirm-card{animation:none}}
139
139
  .confirm-card p{margin:0 0 16px;font-size:13px;line-height:1.6;color:var(--text)}
140
140
  .confirm-actions{display:flex;justify-content:flex-end;gap:8px}
141
- .confirm-actions button{font:inherit;font-size:13px;border:1px solid var(--border);background:transparent;color:var(--text);border-radius:7px;padding:6px 12px;cursor:pointer}
141
+ .confirm-actions button{flex:none;white-space:nowrap;font:inherit;font-size:13px;border:1px solid var(--border);background:transparent;color:var(--text);border-radius:7px;padding:6px 12px;cursor:pointer}
142
142
  .confirm-actions button:hover{background:var(--bg)}
143
143
  .confirm-actions button.primary{border-color:var(--accent);background:var(--accent);color:#fff}
144
144
  .confirm-actions button.primary:hover{filter:brightness(1.06)}
@@ -192,7 +192,7 @@ body.app-view .app-toolbar{display:flex!important}
192
192
  .app-tool svg{display:block;width:17px;height:17px;stroke-width:1.8}.app-tool:hover:not(:disabled){background:var(--finch-bg-hover,var(--bg))}
193
193
  .app-tool:disabled{opacity:.4;cursor:default}.app-tool.checked{color:var(--text);background:var(--finch-bg-active,color-mix(in srgb,var(--text) 10%,transparent))}.app-tool.dirty{background:transparent}
194
194
  .app-tool-label{min-width:auto;padding:0 9px}.ic-save,.ic-save-check{display:inline-flex;align-items:center}.ic-save[hidden],.ic-save-check[hidden]{display:none}.app-menu-wrap{position:relative}.app-menu{position:absolute;z-index:32;top:calc(100% + 7px);right:0;min-width:174px;padding:5px;border:1px solid var(--border);border-radius:9px;background:var(--card);box-shadow:0 2px 8px rgba(0,0,0,.12)}
195
- .app-menu button{display:flex;width:100%;align-items:center;justify-content:space-between;gap:12px;padding:7px 9px;border:0;border-radius:6px;background:transparent;color:var(--text);font:12px var(--finch-font-body,system-ui);text-align:left;cursor:pointer}.app-menu button:hover{background:var(--finch-bg-hover,var(--bg))}.app-menu button.checked{color:var(--text);background:var(--finch-bg-active,color-mix(in srgb,var(--text) 10%,transparent));font-weight:600}.app-menu hr{margin:5px 3px;border:0;border-top:1px solid var(--border)}
195
+ .app-menu button{display:flex;width:100%;align-items:center;justify-content:space-between;gap:12px;padding:7px 9px;border:0;border-radius:6px;background:transparent;color:var(--text);font:12px var(--finch-font-body,system-ui);text-align:left;cursor:pointer}.app-menu button:hover{background:var(--finch-bg-hover,var(--bg))}.app-menu button.checked{color:var(--text);background:var(--finch-bg-active,color-mix(in srgb,var(--text) 10%,transparent));font-weight:600}.app-menu button:disabled{color:var(--muted);cursor:default;opacity:.5}.app-menu button:disabled:hover{background:transparent}.app-menu hr{margin:5px 3px;border:0;border-top:1px solid var(--border)}
196
196
  .app-document-meta{min-width:0;display:flex;flex-direction:column;gap:1px}.app-document-meta strong,.app-document-meta span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.app-document-meta strong{font-size:13px}.app-document-meta span{max-width:34vw;color:var(--muted);font-size:11px}
197
197
  body.app-view .stage{display:grid;grid-template-columns:minmax(0,1fr) 6px var(--preview-width,480px);background:var(--bg)}
198
198
  body.app-view .pane{position:relative;inset:auto;min-width:0;min-height:0}
@@ -219,7 +219,7 @@ body.app-view .empty{grid-column:1/-1;background:var(--bg);padding:54px 38px}
219
219
  body.app-view .home{max-width:1040px}body.app-view #emptyNew{display:none}
220
220
  body.app-view .actions{right:28px;bottom:28px}
221
221
  .library-backdrop{position:fixed;z-index:39;inset:0;background:rgba(0,0,0,.24)}
222
- .library-drawer{position:fixed;z-index:40;top:0;bottom:0;left:0;width:min(390px,88vw);padding:0;background:var(--card);border-right:1px solid var(--border);transform:translateX(-102%);transition:transform .18s ease;display:flex;flex-direction:column}
222
+ .library-drawer{position:fixed;z-index:40;top:0;bottom:0;left:0;width:min(310px,88vw);padding:0;background:var(--card);border-right:1px solid var(--border);transform:translateX(-102%);transition:transform .18s ease;display:flex;flex-direction:column}
223
223
  .library-drawer.open{transform:translateX(0)}.library-drawer>header{height:58px;display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--border)}
224
224
  .library-drawer>header div{display:flex;flex-direction:column}.library-drawer>header span{font-size:11px;color:var(--muted)}.library-drawer>header>button{border:0;background:transparent;color:var(--muted);font-size:24px;cursor:pointer}
225
225
  /* Specificity note: `.library-drawer>header div` above sets
@@ -248,7 +248,7 @@ body.app-view .actions{right:28px;bottom:28px}
248
248
  path + time metadata instead of cramming all three into one text run. */
249
249
  .library-groups{flex:1;overflow:auto;padding:10px 12px 20px}
250
250
  .library-group{margin:0 0 8px;border:0;border-radius:9px}.library-group[open]{background:color-mix(in srgb,var(--text) 2%,transparent)}
251
- .library-group summary{display:flex;align-items:center;gap:7px;min-height:32px;padding:0 9px;list-style:none;color:var(--muted);font-size:12px;font-weight:600;cursor:pointer;user-select:none;touch-action:none}.library-group summary::-webkit-details-marker{display:none}.library-group summary::before{content:'›';color:var(--muted);font-size:17px;font-weight:400;line-height:1;transform:rotate(0deg);transition:transform .14s ease}.library-group[open] summary::before{transform:rotate(90deg)}.library-group summary:hover{background:var(--finch-bg-hover,var(--bg));border-radius:8px}.library-group[data-draggable="true"] summary{cursor:grab}.library-group.dragging{opacity:.32}
251
+ .library-group summary{display:flex;align-items:center;gap:7px;min-height:32px;padding:0 9px;list-style:none;color:var(--muted);font-size:12px;font-weight:600;cursor:pointer;user-select:none;touch-action:none}.library-group summary::-webkit-details-marker{display:none}.library-group summary::before{content:'›';color:var(--muted);font-size:17px;font-weight:400;line-height:1;transform:rotate(0deg);transition:transform .14s ease}.library-group[open] summary::before{transform:rotate(90deg)}.library-group summary:hover{background:var(--finch-bg-hover,var(--bg));border-radius:8px}.library-group.dragging{opacity:.32}
252
252
  .library-group-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted)}.library-group-count{margin-left:auto;color:var(--muted);font-size:11px;font-weight:400}.library-items{padding:0 4px 4px}
253
253
  /* Custom drag ghost: a floating clone that follows the pointer, replacing
254
254
  the browser's native (unstylable) HTML5 drag screenshot. Collapsed