@musnows/scriverse 0.5.10 → 0.5.12

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.
@@ -1,6 +1,7 @@
1
1
  import { levelLabel, relationshipCategoryLabel } from "./display-labels.js?v=20260725-enum-labels-zh";
2
2
 
3
3
  export const VERSIONED_ENTITY_LABELS = Object.freeze({
4
+ draft: "创作草稿",
4
5
  setting: "世界观设定",
5
6
  race: "种族档案",
6
7
  organization: "组织档案",
@@ -24,6 +25,7 @@ export function entityVersionSourceLabel(source) {
24
25
  }
25
26
 
26
27
  export function entityVersionSnapshotSummary(type, snapshot = {}) {
28
+ if (type === "draft") return `${snapshot.draftType === "setting" ? "设定草稿" : "正文草稿"} · ${snapshot.title || "未命名草稿"}`;
27
29
  if (type === "setting") return `${snapshot.category || "未分类"} · ${snapshot.title || "未命名设定"}`;
28
30
  if (type === "race") return `${snapshot.name || "未命名种族"} · ${(snapshot.memberIds ?? []).length} 位角色`;
29
31
  if (type === "organization") return `${snapshot.name || "未命名组织"} · ${(snapshot.memberIds ?? []).length} 位成员`;
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
13
- <link rel="stylesheet" href="/styles.css?v=20260729-analysis-rerun-vditor-ui-v1">
13
+ <link rel="stylesheet" href="/styles.css?v=20260730-drafts-stream-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -123,6 +123,7 @@
123
123
  </div>
124
124
  <nav id="module-nav" class="module-nav" aria-label="作品模块">
125
125
  <button type="button" data-module="editor" class="active"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>正文</button>
126
+ <button type="button" data-module="drafts"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h4l11-11a2.8 2.8 0 0 0-4-4L4 16v4Z"/><path d="m13.5 6.5 4 4"/></svg>草稿</button>
126
127
  <button type="button" data-module="settings"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg>设定库</button>
127
128
  <button type="button" data-module="characters"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>角色</button>
128
129
  <button type="button" data-module="organizations"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>组织</button>
@@ -333,6 +334,12 @@
333
334
  <button id="add-line-annotation" type="button" role="menuitem">添加批注</button>
334
335
  <button id="add-line-todo" type="button" role="menuitem">添加待办</button>
335
336
  </div>
337
+ <div id="markdown-table-menu" class="markdown-table-menu hidden" role="menu" aria-label="Markdown 表格显示方式">
338
+ <button id="markdown-table-wrap-toggle" type="button" role="menuitemcheckbox" aria-checked="false">
339
+ <span>表格内容自动换行</span>
340
+ <small>关闭时可横向滚动</small>
341
+ </button>
342
+ </div>
336
343
  <input id="new-import-file" class="hidden" type="file" accept=".txt,.docx">
337
344
  <input id="cover-file" class="hidden" type="file" accept="image/png,image/jpeg,image/webp">
338
345
  <input id="avatar-file" class="hidden" type="file" accept="image/png,image/jpeg,image/webp">
@@ -713,6 +720,7 @@
713
720
  <legend>可分页模块单页数量</legend>
714
721
  <small>以下模块独立设置,允许 10–100 条;默认均为 30 条。种族层级始终完整展示。</small>
715
722
  <div class="pagination-settings-grid">
723
+ <label>草稿列表<input id="page-size-drafts" name="pageSizeDrafts" type="number" min="10" max="100" step="1" required aria-label="草稿列表单页数量"></label>
716
724
  <label>设定库<input id="page-size-settings" name="pageSizeSettings" type="number" min="10" max="100" step="1" required aria-label="设定库单页数量"></label>
717
725
  <label>角色列表<input id="page-size-characters" name="pageSizeCharacters" type="number" min="10" max="100" step="1" required aria-label="角色列表单页数量"></label>
718
726
  <label>组织列表<input id="page-size-organizations" name="pageSizeOrganizations" type="number" min="10" max="100" step="1" required aria-label="组织列表单页数量"></label>
@@ -876,6 +884,6 @@
876
884
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
877
885
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
878
886
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
879
- <script type="module" src="/app.js?v=20260729-analysis-rerun-vditor-ui-v1"></script>
887
+ <script type="module" src="/app.js?v=20260730-volume-lazy-module-cache-v1"></script>
880
888
  </body>
881
889
  </html>
@@ -128,7 +128,12 @@ function tableAlignments(cells) {
128
128
  }
129
129
 
130
130
  function renderMarkdownTable(headers, alignments, rows) {
131
- const renderCell = (tag, value, alignment) => `<${tag} class="markdown-align-${alignment}">${renderInlineMarkdown(value)}</${tag}>`;
131
+ const renderCell = (tag, value, alignment) => {
132
+ const interaction = tag === "th"
133
+ ? ' data-markdown-table-header tabindex="0" aria-haspopup="menu" title="右键或按 Shift+F10 设置表格换行"'
134
+ : "";
135
+ return `<${tag} class="markdown-align-${alignment}"${interaction}>${renderInlineMarkdown(value)}</${tag}>`;
136
+ };
132
137
  const header = headers.map((cell, index) => renderCell("th", cell, alignments[index])).join("");
133
138
  const body = rows.map((row) => {
134
139
  const cells = headers.map((_header, index) => renderCell("td", row[index] ?? "", alignments[index])).join("");
@@ -0,0 +1,17 @@
1
+ export interface ModuleRequestOptions {
2
+ refresh?: boolean;
3
+ }
4
+
5
+ export interface ModuleRequestCache {
6
+ request<T>(
7
+ workId: string,
8
+ module: string,
9
+ requestKey: string,
10
+ loader: () => Promise<T> | T,
11
+ options?: ModuleRequestOptions
12
+ ): Promise<T>;
13
+ invalidate(workId: string, module: string): void;
14
+ clear(): void;
15
+ }
16
+
17
+ export function createModuleRequestCache(): ModuleRequestCache;
@@ -0,0 +1,35 @@
1
+ export function createModuleRequestCache() {
2
+ const scopes = new Map();
3
+
4
+ const scopeKey = (workId, module) => `${String(workId)}\u0000${String(module)}`;
5
+ const cloneResult = (value) => structuredClone(value);
6
+
7
+ function request(workId, module, requestKey, loader, { refresh = false } = {}) {
8
+ const key = scopeKey(workId, module);
9
+ let scope = scopes.get(key);
10
+ if (!scope) {
11
+ scope = new Map();
12
+ scopes.set(key, scope);
13
+ }
14
+ if (refresh) scope.delete(requestKey);
15
+ if (scope.has(requestKey)) return scope.get(requestKey).then(cloneResult);
16
+
17
+ const pending = Promise.resolve().then(loader);
18
+ scope.set(requestKey, pending);
19
+ pending.catch(() => {
20
+ if (scope.get(requestKey) === pending) scope.delete(requestKey);
21
+ if (scope.size === 0) scopes.delete(key);
22
+ });
23
+ return pending.then(cloneResult);
24
+ }
25
+
26
+ function invalidate(workId, module) {
27
+ scopes.delete(scopeKey(workId, module));
28
+ }
29
+
30
+ function clear() {
31
+ scopes.clear();
32
+ }
33
+
34
+ return { request, invalidate, clear };
35
+ }
@@ -1,4 +1,4 @@
1
- export type RestorableModule = "settings" | "characters" | "races" | "organizations" | "timeline" | "outlines" | "relationships" | "reviews" | "tasks" | "ai-settings";
1
+ export type RestorableModule = "drafts" | "settings" | "characters" | "races" | "organizations" | "timeline" | "outlines" | "relationships" | "reviews" | "tasks" | "ai-settings";
2
2
  export type PageRoute =
3
3
  | { view: "shelf" }
4
4
  | { view: "editor"; workId: string; chapterId: string | null }
@@ -1,4 +1,5 @@
1
1
  export const RESTORABLE_MODULES = Object.freeze([
2
+ "drafts",
2
3
  "settings",
3
4
  "characters",
4
5
  "races",
@@ -0,0 +1,19 @@
1
+ export type StreamTypewriterProgress = {
2
+ visibleCharacters: number;
3
+ pendingCharacters: number;
4
+ };
5
+
6
+ export type StreamTypewriter = {
7
+ append(value: unknown): void;
8
+ finish(): Promise<string>;
9
+ reveal(): string;
10
+ };
11
+
12
+ export function streamTypewriterBatchSize(pendingCharacters: number, finishing?: boolean): number;
13
+
14
+ export function createStreamTypewriter<FrameHandle = number>(options: {
15
+ onRender: (text: string, progress: StreamTypewriterProgress) => void;
16
+ scheduleFrame?: (callback: () => void) => FrameHandle;
17
+ cancelFrame?: (handle: FrameHandle) => void;
18
+ reducedMotion?: boolean;
19
+ }): StreamTypewriter;
@@ -0,0 +1,83 @@
1
+ const STREAMING_CHARACTERS_PER_FRAME = 1;
2
+ const FINISHING_CHARACTERS_PER_FRAME = 2;
3
+ const FINISHING_ACCELERATION = 0.9;
4
+ const MAX_STREAMING_CHARACTERS_PER_FRAME = 12;
5
+ const MAX_FINISHING_CHARACTERS_PER_FRAME = 24;
6
+
7
+ export function streamTypewriterBatchSize(pendingCharacters, finishing = false) {
8
+ const pending = Math.max(0, Math.floor(Number(pendingCharacters) || 0));
9
+ if (pending === 0) return 0;
10
+ const minimum = finishing ? FINISHING_CHARACTERS_PER_FRAME : STREAMING_CHARACTERS_PER_FRAME;
11
+ const maximum = finishing ? MAX_FINISHING_CHARACTERS_PER_FRAME : MAX_STREAMING_CHARACTERS_PER_FRAME;
12
+ const adaptive = finishing
13
+ ? Math.ceil(Math.sqrt(pending) * FINISHING_ACCELERATION)
14
+ : Math.ceil(pending / 30);
15
+ return Math.min(pending, maximum, Math.max(minimum, adaptive));
16
+ }
17
+
18
+ export function createStreamTypewriter({
19
+ onRender,
20
+ scheduleFrame = (callback) => window.requestAnimationFrame(callback),
21
+ cancelFrame = (handle) => window.cancelAnimationFrame(handle),
22
+ reducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true
23
+ }) {
24
+ if (typeof onRender !== "function") throw new TypeError("onRender must be a function");
25
+
26
+ const visibleCharacters = [];
27
+ const pendingCharacters = [];
28
+ const idleResolvers = [];
29
+ let scheduledFrame = null;
30
+ let finishing = false;
31
+
32
+ const snapshot = () => visibleCharacters.join("");
33
+ const resolveIdle = () => {
34
+ if (pendingCharacters.length || scheduledFrame !== null) return;
35
+ const value = snapshot();
36
+ for (const resolve of idleResolvers.splice(0)) resolve(value);
37
+ };
38
+ const render = () => {
39
+ onRender(snapshot(), {
40
+ visibleCharacters: visibleCharacters.length,
41
+ pendingCharacters: pendingCharacters.length
42
+ });
43
+ };
44
+ const schedule = () => {
45
+ if (scheduledFrame !== null || pendingCharacters.length === 0) return;
46
+ scheduledFrame = scheduleFrame(() => {
47
+ scheduledFrame = null;
48
+ const batchSize = reducedMotion
49
+ ? pendingCharacters.length
50
+ : streamTypewriterBatchSize(pendingCharacters.length, finishing);
51
+ visibleCharacters.push(...pendingCharacters.splice(0, batchSize));
52
+ render();
53
+ if (pendingCharacters.length) schedule();
54
+ else resolveIdle();
55
+ });
56
+ };
57
+
58
+ return {
59
+ append(value) {
60
+ const characters = Array.from(String(value ?? ""));
61
+ if (!characters.length) return;
62
+ pendingCharacters.push(...characters);
63
+ schedule();
64
+ },
65
+ finish() {
66
+ if (!pendingCharacters.length && scheduledFrame === null) return Promise.resolve(snapshot());
67
+ finishing = true;
68
+ schedule();
69
+ return new Promise((resolve) => idleResolvers.push(resolve));
70
+ },
71
+ reveal() {
72
+ if (scheduledFrame !== null) {
73
+ cancelFrame(scheduledFrame);
74
+ scheduledFrame = null;
75
+ }
76
+ visibleCharacters.push(...pendingCharacters.splice(0));
77
+ finishing = false;
78
+ render();
79
+ resolveIdle();
80
+ return snapshot();
81
+ }
82
+ };
83
+ }
@@ -1100,7 +1100,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1100
1100
  .module-nav .ai-analysis-entry,
1101
1101
  .module-nav .ai-analysis-entry:hover,
1102
1102
  .module-nav .ai-analysis-entry.active { background: transparent; color: var(--accent-dark); font-weight: 650; }
1103
- #module-more-button { grid-column: 1 / -1; color: var(--accent-dark); text-align: center; }
1103
+ #module-more-button { grid-column: 2; color: var(--accent-dark); text-align: center; }
1104
1104
  .panel-heading { display: flex; align-items: center; gap: 8px; padding: 15px 0 9px 7px; color: var(--muted); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
1105
1105
  .panel-heading-actions { display: flex; align-items: center; gap: 7px; margin-left: auto; }
1106
1106
  .chapter-batch-button { display: grid; flex: none; place-items: center; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
@@ -1275,6 +1275,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1275
1275
  .character-filter-toolbar-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; min-height: 38px; }
1276
1276
  .character-filter-result-count { color: var(--muted); font-size: 11px; white-space: nowrap; }
1277
1277
  .character-filter-toolbar-actions button:disabled { cursor: default; opacity: .45; }
1278
+ .draft-filter-toolbar { display: flex; align-items: end; gap: 10px; margin-bottom: 18px; padding: 12px 14px; border: 1px solid var(--line); background: var(--surface-soft); }
1279
+ .draft-filter-toolbar label { color: var(--muted); font-size: 11px; }
1280
+ .draft-filter-toolbar select { min-width: 180px; min-height: 38px; padding: 8px 10px; background: var(--surface); font-size: 11px; }
1281
+ .draft-filter-toolbar span { margin-left: auto; align-self: center; color: var(--muted); font-size: 11px; }
1278
1282
  .character-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1279
1283
  .character-duplicate-review { grid-column: 1 / -1; }
1280
1284
  .character-duplicate-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 14px 0; }
@@ -1343,10 +1347,11 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1343
1347
  border-radius: 5px;
1344
1348
  background: var(--surface-soft);
1345
1349
  }
1346
- .task-auto-run-copy { display: grid; grid-column: 1; gap: 4px; }
1350
+ .task-auto-run-header { display: grid; grid-column: 1; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 12px; }
1351
+ .task-auto-run-copy { display: grid; gap: 4px; }
1347
1352
  .task-auto-run-copy strong { font-size: 13px; }
1348
1353
  .task-auto-run-copy small { color: var(--muted); font-size: 10px; line-height: 1.5; }
1349
- .task-auto-run-actions { display: flex; grid-column: 2; grid-row: 1; align-items: start; justify-content: flex-end; min-height: 32px; }
1354
+ .task-auto-run-actions { display: flex; align-items: start; justify-content: flex-end; min-height: 32px; }
1350
1355
  .task-auto-run-actions:empty { display: none; }
1351
1356
  .task-auto-run-controls {
1352
1357
  display: flex;
@@ -1897,6 +1902,40 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1897
1902
  .outline-summary strong { font-size: 26px; }.outline-summary span { color: var(--muted); font-size: 10px; }
1898
1903
  .planning-section + .planning-section { margin-top: 42px; }.section-title { display: flex; justify-content: space-between; margin-bottom: 15px; }.section-title h2 { margin: 0; font-size: 21px; }
1899
1904
  .foreshadow-card.is-overdue { border-color: #b55743; box-shadow: inset 3px 0 #b55743; }.foreshadow-links { margin-top: 13px; }
1905
+ .foreshadow-editor-section { display: grid; gap: 14px; padding: 18px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
1906
+ .foreshadow-editor-section > header, .foreshadow-occurrence-toolbar > div { display: grid; gap: 4px; }
1907
+ .foreshadow-editor-section h3 { margin: 0; color: var(--ink); font-size: 14px; font-weight: 650; }
1908
+ .foreshadow-editor-section header p, .foreshadow-occurrence-toolbar p { margin: 0; color: var(--muted); font-size: 10px; line-height: 1.5; }
1909
+ .foreshadow-overview-grid { display: grid; grid-template-columns: minmax(220px, 1.5fr) repeat(2, minmax(130px, .65fr)) minmax(220px, 1.2fr); gap: 12px; }
1910
+ .foreshadow-overview-description textarea, .foreshadow-resolution-field textarea { min-height: 108px; }
1911
+ .foreshadow-occurrence-field { gap: 16px; }
1912
+ .foreshadow-occurrence-toolbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
1913
+ .foreshadow-occurrence-toolbar button, .foreshadow-occurrence-remove { flex: 0 0 auto; border: 1px solid var(--line); background: var(--surface); color: var(--ink); }
1914
+ .foreshadow-occurrence-rows { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; align-items: start; }
1915
+ .foreshadow-occurrence-row { --occurrence-color: var(--accent); position: relative; min-width: 0; display: grid; gap: 12px; margin: 0; padding: 14px 14px 15px; border: 1px solid color-mix(in srgb, var(--occurrence-color) 35%, var(--line)); border-left: 4px solid var(--occurrence-color); border-radius: 6px; background: color-mix(in srgb, var(--occurrence-color) 5%, var(--surface)); }
1916
+ .foreshadow-occurrence-row[data-occurrence-role="setup"] { --occurrence-color: #9a6547; }
1917
+ .foreshadow-occurrence-row[data-occurrence-role="reminder"] { --occurrence-color: #80713d; }
1918
+ .foreshadow-occurrence-row[data-occurrence-role="payoff"] { --occurrence-color: #49745c; }
1919
+ .foreshadow-occurrence-row legend { display: flex; max-width: calc(100% - 94px); align-items: center; gap: 7px; padding: 0 7px; color: var(--ink); }
1920
+ .foreshadow-occurrence-row legend strong { font-size: 11px; }
1921
+ .foreshadow-occurrence-row legend small { min-width: 0; overflow: hidden; color: var(--muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
1922
+ .foreshadow-occurrence-role-badge { flex: 0 0 auto; padding: 3px 7px; border: 1px solid color-mix(in srgb, var(--occurrence-color) 55%, var(--line)); border-radius: 12px; background: color-mix(in srgb, var(--occurrence-color) 12%, var(--surface)); color: var(--occurrence-color); font-size: 9px; font-weight: 700; }
1923
+ .foreshadow-occurrence-remove { position: absolute; top: 9px; right: 12px; padding: 5px 8px; font-size: 10px; }
1924
+ .foreshadow-occurrence-grid { display: grid; grid-template-columns: minmax(110px, .55fr) minmax(180px, 1.45fr); gap: 10px; }
1925
+ .dialog-fields .foreshadow-occurrence-row textarea { min-height: 68px; }
1926
+ .foreshadow-occurrence-empty { margin: 0; padding: 12px; border: 1px dashed var(--line); color: var(--muted); font-size: 11px; text-align: center; }
1927
+ @media (max-width: 900px) {
1928
+ .foreshadow-overview-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1929
+ .foreshadow-occurrence-rows { grid-template-columns: minmax(0, 1fr); }
1930
+ }
1931
+ @media (max-width: 620px) {
1932
+ .foreshadow-occurrence-toolbar { align-items: stretch; flex-direction: column; }
1933
+ .foreshadow-occurrence-toolbar button { align-self: flex-start; }
1934
+ .foreshadow-overview-grid { grid-template-columns: minmax(0, 1fr); }
1935
+ .foreshadow-occurrence-grid { grid-template-columns: minmax(0, 1fr); }
1936
+ .foreshadow-occurrence-row legend { max-width: calc(100% - 78px); flex-wrap: wrap; }
1937
+ .foreshadow-occurrence-row legend small { flex-basis: 100%; }
1938
+ }
1900
1939
  .outline-list { display: grid; gap: 8px; }.outline-row { display: grid; grid-template-columns: minmax(150px, 1.1fr) repeat(3, minmax(130px, 1fr)) auto; gap: 16px; align-items: start; padding: 15px; border: 1px solid var(--line); background: var(--surface-soft); }
1901
1940
  .outline-row small { color: var(--accent); font-size: 9px; }.outline-row h3 { margin: 4px 0 0; font-size: 14px; }.outline-row b { font-size: 10px; }.outline-row p { margin: 5px 0 0; color: var(--muted); font-size: 10px; line-height: 1.45; white-space: pre-wrap; }
1902
1941
  .outline-row.is-complete { opacity: .66; }.outline-actions { display: grid; gap: 7px; justify-items: end; }.outline-actions span { color: var(--accent); font-size: 9px; }.outline-actions button { border: 1px solid var(--line); background: transparent; border-radius: 3px; padding: 5px 8px; font-size: 9px; }
@@ -2017,6 +2056,11 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2017
2056
  .message-body .markdown-table-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
2018
2057
  .message-body .markdown-table-scroll table { width: max-content; min-width: 100%; border-collapse: collapse; }
2019
2058
  .message-body .markdown-table-scroll th, .message-body .markdown-table-scroll td { padding: 7px 9px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); overflow-wrap: normal; white-space: nowrap; }
2059
+ .message-body .markdown-table-scroll th { cursor: context-menu; }
2060
+ .message-body .markdown-table-scroll th:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
2061
+ .message-body .markdown-table-scroll.is-wrapping { overflow-x: hidden; scrollbar-gutter: auto; }
2062
+ .message-body .markdown-table-scroll.is-wrapping table { width: 100%; table-layout: fixed; }
2063
+ .message-body .markdown-table-scroll.is-wrapping th, .message-body .markdown-table-scroll.is-wrapping td { overflow-wrap: anywhere; white-space: normal; word-break: break-word; }
2020
2064
  .message-body .markdown-table-scroll th:last-child, .message-body .markdown-table-scroll td:last-child { border-right: 0; }
2021
2065
  .message-body .markdown-table-scroll tbody tr:last-child td { border-bottom: 0; }
2022
2066
  .message-body .markdown-table-scroll th { background: var(--surface-soft); font-weight: 600; }
@@ -2203,6 +2247,13 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2203
2247
  .line-citation-menu { position: fixed; z-index: 82; display: grid; min-width: 230px; padding: 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); box-shadow: 0 14px 38px rgba(49,42,32,.22); }
2204
2248
  .line-citation-menu strong { padding: 5px 7px 8px; color: var(--muted); font-size: 9px; }
2205
2249
  .line-citation-menu button { border: 0; border-radius: 4px; padding: 8px; background: transparent; text-align: left; font-size: 11px; }.line-citation-menu button:hover { background: var(--paper-deep); color: var(--accent-dark); }
2250
+ .markdown-table-menu { position: fixed; z-index: 84; min-width: 210px; padding: 7px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); box-shadow: 0 14px 38px rgba(49,42,32,.22); }
2251
+ .markdown-table-menu button { position: relative; display: grid; width: 100%; gap: 2px; padding: 8px 9px 8px 31px; border: 0; border-radius: 4px; background: transparent; color: var(--ink); text-align: left; }
2252
+ .markdown-table-menu button::before { content: ""; position: absolute; top: 12px; left: 10px; width: 11px; height: 11px; border: 1px solid var(--muted); border-radius: 2px; background: var(--surface); }
2253
+ .markdown-table-menu button[aria-checked="true"]::before { border-color: var(--accent); background: var(--accent); box-shadow: inset 0 0 0 3px var(--surface); }
2254
+ .markdown-table-menu button:hover, .markdown-table-menu button:focus-visible { background: var(--paper-deep); color: var(--accent-dark); }
2255
+ .markdown-table-menu button span { font-size: 11px; font-weight: 600; }
2256
+ .markdown-table-menu button small { color: var(--muted); font-size: 9px; }
2206
2257
  .chapter-annotations-dialog { width: min(820px, 94vw); }
2207
2258
  .chapter-annotations-list { display: grid; gap: 10px; max-height: 62vh; overflow: auto; padding: 18px 24px; }
2208
2259
  .chapter-annotation-card { display: grid; gap: 8px; padding: 13px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
@@ -2223,6 +2274,11 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2223
2274
  .wide-dialog { width: min(800px, 92vw); }
2224
2275
  .trace-dialog { width: min(1180px, 94vw); }
2225
2276
  .trace-dialog .dialog-fields { max-height: 68vh; }
2277
+ .large-dialog { width: min(1120px, 94vw); max-height: 94dvh; }
2278
+ .large-dialog .dialog-fields { max-height: 72dvh; padding: 24px 28px; }
2279
+ .editor-dialog { width: min(1180px, 94vw); max-height: calc(100dvh - 16px); }
2280
+ .editor-dialog .dialog-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); max-height: 76dvh; padding: 24px 28px; }
2281
+ .editor-dialog .dialog-fields > .form-field-note, .editor-dialog .dialog-fields > .markdown-editor-field { grid-column: 1 / -1; }
2226
2282
  .dialog::backdrop { background: rgba(34,30,25,.42); backdrop-filter: blur(3px); }
2227
2283
  .dialog-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 22px 24px 16px; border-bottom: 1px solid var(--line); }
2228
2284
  .dialog-header h2 { margin: 0; font-size: 23px; font-weight: 500; }
@@ -2247,6 +2303,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2247
2303
  outline: none;
2248
2304
  }
2249
2305
  .dialog-fields { display: grid; gap: 14px; padding: 22px 24px; max-height: 58vh; overflow-y: auto; }
2306
+ .dialog-fields .form-field-note { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.65; }
2250
2307
  .dialog-fields label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
2251
2308
  .dialog-fields input, .dialog-fields textarea, .dialog-fields select { width: 100%; padding: 9px 10px; font-size: 13px; }
2252
2309
  .dialog-fields textarea { resize: vertical; min-height: 90px; line-height: 1.6; }
@@ -2444,6 +2501,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2444
2501
  .import-mode-dialog .dialog-actions { flex-wrap: wrap; }
2445
2502
  .character-filter-toolbar { grid-template-columns: minmax(0, 1fr); }
2446
2503
  .character-filter-toolbar-actions { justify-content: flex-start; }
2504
+ .draft-filter-toolbar { align-items: stretch; flex-direction: column; }
2505
+ .draft-filter-toolbar select { width: 100%; min-width: 0; }
2506
+ .draft-filter-toolbar span { margin-left: 0; }
2447
2507
  }
2448
2508
  .entity-editor-open { overflow: hidden; }
2449
2509
  .preview-record-card { cursor: pointer; }
@@ -2494,7 +2554,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2494
2554
  .setting-markdown-heading, .markdown-editor-field-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 24px; color: var(--ink); font-size: 14px; }
2495
2555
  .setting-markdown-heading small, .markdown-editor-field-heading small { color: var(--muted); font-size: 10px; }
2496
2556
  .markdown-editor-field { display: grid; gap: 10px; min-width: 0; }
2557
+ .markdown-editor-field > span { color: var(--muted); font-size: 11px; }
2497
2558
  .markdown-editor-field .vditor-editor-host { min-height: 360px; }
2559
+ .editor-dialog .markdown-editor-field .vditor-editor-host.vditor { min-height: clamp(420px, 56dvh, 640px) !important; }
2498
2560
  .vditor-editor-host { width: 100%; height: 100%; min-width: 0; min-height: 0; }
2499
2561
  .vditor-editor-host.vditor { height: 100% !important; min-height: 0; overflow: hidden; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); }
2500
2562
  .vditor-editor-host.vditor:not(.vditor--dark) { --border-color: var(--line); --panel-background-color: var(--surface); --toolbar-background-color: var(--surface-soft); --toolbar-icon-color: var(--muted); --toolbar-icon-hover-color: var(--accent-dark); --textarea-background-color: var(--surface); --textarea-text-color: var(--ink); --heading-border-color: var(--line); --blockquote-color: var(--muted); }
@@ -2553,6 +2615,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2553
2615
  .character-editor-section-fields textarea { min-height: 120px; resize: vertical; line-height: 1.65; }
2554
2616
  .character-editor-section-fields .section-list-row textarea { min-height: 180px; }
2555
2617
  .character-editor-section-fields .member-chip { position: relative; display: inline-flex; width: auto; cursor: pointer; }
2618
+ .character-editor-section-fields .member-chip input[type="checkbox"] { position: absolute; width: 1px !important; min-width: 1px; height: 1px; padding: 0; border: 0; opacity: 0; }
2556
2619
  .character-editor-section-fields .item-list-row button, .character-editor-section-fields .structured-list-row button { min-width: 54px; }
2557
2620
  .knowledge-markdown-sections { display: grid; grid-column: 1 / -1; gap: 14px; min-width: 0; }
2558
2621
  .knowledge-markdown-list-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
@@ -2730,8 +2793,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2730
2793
  .top-actions .ghost-button:not(.settings-button) { display: none; }
2731
2794
  .book-shelf { grid-template-columns: repeat(3, minmax(180px, 1fr)); }
2732
2795
  .task-auto-run-panel { grid-template-columns: minmax(0, 1fr); column-gap: 0; }
2733
- .task-auto-run-copy, .task-auto-run-actions, .task-auto-run-controls, .task-auto-run-meta { grid-column: 1; }
2734
- .task-auto-run-actions { grid-row: auto; }
2796
+ .task-auto-run-header, .task-auto-run-controls, .task-auto-run-meta { grid-column: 1; }
2735
2797
  .task-auto-run-help, .task-auto-run-alert { grid-column: 1; }
2736
2798
  .task-auto-run-progress { grid-column: 1; grid-row: auto; justify-self: stretch; width: 100%; }
2737
2799
  .task-auto-run-progress-ring { display: none; }
@@ -2755,6 +2817,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2755
2817
  .setting-markdown-field { height: 70dvh; }
2756
2818
  .setting-markdown-heading, .markdown-editor-field-heading { align-items: flex-start; flex-direction: column; gap: 4px; }
2757
2819
  .setting-markdown-field > .vditor-editor-host, .markdown-editor-field .vditor-editor-host { min-height: 42dvh; }
2820
+ .editor-dialog .dialog-fields { grid-template-columns: minmax(0, 1fr); max-height: 76dvh; }
2821
+ .editor-dialog .markdown-editor-field .vditor-editor-host.vditor { min-height: 52dvh !important; }
2758
2822
  .character-editor-workspace, .character-editor-workspace.history-open { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
2759
2823
  .character-editor-nav { flex-direction: row; overflow-x: auto; padding: 8px 10px; border-right: 0; border-bottom: 1px solid var(--line); }
2760
2824
  .character-editor-nav button { flex: 0 0 130px; }
@@ -2970,7 +3034,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2970
3034
  .settings-layout-toggle, .module-layout-toggle { width: 100%; }
2971
3035
  .settings-layout-toggle button { flex: 1; }
2972
3036
 
2973
- .dialog, .wide-dialog, .trace-dialog, .account-dialog {
3037
+ .dialog, .wide-dialog, .trace-dialog, .large-dialog, .account-dialog {
2974
3038
  width: calc(100vw - 20px);
2975
3039
  max-width: calc(100vw - 20px);
2976
3040
  max-height: calc(100dvh - 20px);
@@ -1,5 +1,5 @@
1
- export type WorkPermissionModule = "prose" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "ai-chat" | "ai-analysis" | "ai-settings";
2
- export type WorkUiModule = "editor" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "tasks" | "ai-settings";
1
+ export type WorkPermissionModule = "prose" | "drafts" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "ai-chat" | "ai-analysis" | "ai-settings";
2
+ export type WorkUiModule = "editor" | "drafts" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "tasks" | "ai-settings";
3
3
  export type WorkModuleAccess = "none" | "read" | "write";
4
4
  export type WorkModulePermissions = Record<WorkPermissionModule, WorkModuleAccess>;
5
5
  export type WorkPermissionAware = { accessRole?: string | null; modulePermissions?: WorkModulePermissions };
@@ -1,5 +1,6 @@
1
1
  export const WORK_PERMISSION_MODULES = Object.freeze([
2
2
  { id: "prose", uiModule: "editor", label: "正文" },
3
+ { id: "drafts", uiModule: "drafts", label: "草稿" },
3
4
  { id: "settings", uiModule: "settings", label: "设定库" },
4
5
  { id: "characters", uiModule: "characters", label: "角色" },
5
6
  { id: "races", uiModule: "races", label: "种族" },
@@ -21,6 +22,13 @@ const validAccess = new Set(["none", "read", "write"]);
21
22
  function migrateLegacyModulePermissions(value) {
22
23
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
23
24
  const migrated = { ...value };
25
+ if (!validAccess.has(migrated.drafts)) {
26
+ const prose = validAccess.has(migrated.prose) ? migrated.prose : "none";
27
+ const settings = validAccess.has(migrated.settings) ? migrated.settings : "none";
28
+ migrated.drafts = prose === "write" && settings === "write"
29
+ ? "write"
30
+ : prose === "none" || settings === "none" ? "none" : "read";
31
+ }
24
32
  const legacyAi = validAccess.has(migrated.ai) ? migrated.ai : null;
25
33
  if (!validAccess.has(migrated["ai-chat"]) && legacyAi) migrated["ai-chat"] = legacyAi;
26
34
  if (!validAccess.has(migrated["ai-analysis"]) && validAccess.has(migrated.ai)) {