@musnows/scriverse 0.4.2 → 0.4.3

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.
@@ -29,5 +29,5 @@ export function characterVersionSourceLabel(source) {
29
29
  race: "种族变更",
30
30
  organization: "组织变更",
31
31
  migration: "历史基线"
32
- })[String(source)] ?? String(source || "未知来源");
32
+ })[String(source)] ?? "其他来源";
33
33
  }
@@ -0,0 +1,16 @@
1
+ export function settingStatusLabel(value: unknown): string;
2
+ export function characterVisibilityLabel(value: unknown): string;
3
+ export function timelineStatusLabel(value: unknown): string;
4
+ export function levelLabel(value: unknown): string;
5
+ export function foreshadowStatusLabel(value: unknown): string;
6
+ export function outlineStatusLabel(value: unknown): string;
7
+ export function relationshipCategoryLabel(value: unknown): string;
8
+ export function relationshipConfirmationLabel(value: unknown): string;
9
+ export function reviewItemTypeLabel(value: unknown): string;
10
+ export function reviewStatusLabel(value: unknown): string;
11
+ export function taskScopeLabel(value: unknown): string;
12
+ export function providerStatusLabel(value: unknown): string;
13
+ export function providerConnectionLabel(value: unknown): string;
14
+ export function chapterVersionSourceLabel(value: unknown): string;
15
+ export function occurrenceRoleLabel(value: unknown): string;
16
+ export function searchResultTypeLabel(value: unknown): string;
@@ -0,0 +1,81 @@
1
+ function enumLabel(labels, value, fallback) {
2
+ const normalized = String(value ?? "").trim();
3
+ if (Object.hasOwn(labels, normalized)) return labels[normalized];
4
+ if (!normalized || /^[a-z][a-z0-9_-]*$/iu.test(normalized)) return fallback;
5
+ return normalized;
6
+ }
7
+
8
+ export function settingStatusLabel(value) {
9
+ return enumLabel({ draft: "草稿", pending: "待确认", confirmed: "已确认", deprecated: "已弃用" }, value, "未知状态");
10
+ }
11
+
12
+ export function characterVisibilityLabel(value) {
13
+ return enumLabel({ public: "公开", author: "仅作者", collaborators: "协作者可见" }, value, "未知范围");
14
+ }
15
+
16
+ export function timelineStatusLabel(value) {
17
+ return enumLabel({ candidate: "候选", pending: "待确认", confirmed: "已确认", deprecated: "已弃用" }, value, "未知状态");
18
+ }
19
+
20
+ export function levelLabel(value) {
21
+ return enumLabel({ low: "低", medium: "中", high: "高" }, value, "未知等级");
22
+ }
23
+
24
+ export function foreshadowStatusLabel(value) {
25
+ return enumLabel({ planned: "计划中", planted: "已埋设", resolved: "已回收", abandoned: "已放弃" }, value, "未知状态");
26
+ }
27
+
28
+ export function outlineStatusLabel(value) {
29
+ return enumLabel({ draft: "草稿", ready: "可执行", completed: "已完成" }, value, "未知状态");
30
+ }
31
+
32
+ export function relationshipCategoryLabel(value) {
33
+ return enumLabel({ family: "亲属", social: "社交", emotional: "情感", conflict: "冲突", uncertain: "未确定" }, value, "其他关系");
34
+ }
35
+
36
+ export function relationshipConfirmationLabel(value) {
37
+ return enumLabel({ pending: "待确认", confirmed: "已确认", rejected: "已拒绝" }, value, "未知状态");
38
+ }
39
+
40
+ export function reviewItemTypeLabel(value) {
41
+ return enumLabel({
42
+ consistency: "一致性问题",
43
+ "character-duplicate": "角色重复",
44
+ "timeline-conflict": "时间线冲突",
45
+ "setting-conflict": "设定冲突",
46
+ "relationship-conflict": "关系冲突",
47
+ "character-conflict": "角色冲突",
48
+ "plot-hole": "剧情漏洞",
49
+ "low-confidence": "低置信度结论",
50
+ chronology: "时间顺序问题",
51
+ factual: "事实问题"
52
+ }, value, "其他审核问题");
53
+ }
54
+
55
+ export function reviewStatusLabel(value) {
56
+ return enumLabel({ pending: "待处理", ignored: "已忽略", fixing: "处理中", fixed: "已修复", exception: "例外保留" }, value, "未知状态");
57
+ }
58
+
59
+ export function taskScopeLabel(value) {
60
+ return enumLabel({ none: "无上下文", selection: "选中文本", chapter: "当前章节", volume: "当前分卷", book: "全书", entities: "指定资料" }, value, "未指定范围");
61
+ }
62
+
63
+ export function providerStatusLabel(value) {
64
+ return enumLabel({ enabled: "已启用", disabled: "已停用", error: "异常" }, value, "未知状态");
65
+ }
66
+
67
+ export function providerConnectionLabel(value) {
68
+ return enumLabel({ unchecked: "未测试", success: "连接正常", failed: "连接失败" }, value, "未知连接状态");
69
+ }
70
+
71
+ export function chapterVersionSourceLabel(value) {
72
+ return enumLabel({ manual: "人工保存", auto: "自动保存", "ai-suggestion": "AI 建议", restore: "历史恢复", import: "文件导入", create: "初始版本" }, value, "其他来源");
73
+ }
74
+
75
+ export function occurrenceRoleLabel(value) {
76
+ return enumLabel({ setup: "埋设", reminder: "提醒", payoff: "回收" }, value, "其他节点");
77
+ }
78
+
79
+ export function searchResultTypeLabel(value) {
80
+ return enumLabel({ chapter: "章节", setting: "设定", character: "角色", race: "种族", organization: "组织" }, value, "其他资料");
81
+ }
@@ -1,3 +1,5 @@
1
+ import { levelLabel, relationshipCategoryLabel } from "./display-labels.js?v=20260725-enum-labels-zh";
2
+
1
3
  export const VERSIONED_ENTITY_LABELS = Object.freeze({
2
4
  setting: "世界观设定",
3
5
  race: "种族档案",
@@ -18,7 +20,7 @@ export function entityVersionSourceLabel(source) {
18
20
  analysis: "AI 分析",
19
21
  merge: "事件合并",
20
22
  split: "事件拆分"
21
- })[source] ?? source;
23
+ })[source] ?? "其他来源";
22
24
  }
23
25
 
24
26
  export function entityVersionSnapshotSummary(type, snapshot = {}) {
@@ -27,8 +29,8 @@ export function entityVersionSnapshotSummary(type, snapshot = {}) {
27
29
  if (type === "organization") return `${snapshot.name || "未命名组织"} · ${(snapshot.memberIds ?? []).length} 位成员`;
28
30
  if (type === "timeline-track") return `${snapshot.name || "未命名时间轴"} · 排序 ${snapshot.sortOrder ?? 0}`;
29
31
  if (type === "timeline-event") return `${snapshot.timeLabel || "时间待定"} · ${snapshot.name || "未命名事件"}`;
30
- if (type === "relationship") return `${snapshot.category || "未分类"} / ${snapshot.subtype || "未细分"} · ${Math.round(Number(snapshot.confidence ?? 0) * 100)}%`;
32
+ if (type === "relationship") return `${relationshipCategoryLabel(snapshot.category)} / ${snapshot.subtype || "未细分"} · ${Math.round(Number(snapshot.confidence ?? 0) * 100)}%`;
31
33
  if (type === "chapter-outline") return `目标:${snapshot.goal || "未填写"}`;
32
- if (type === "foreshadow") return `${snapshot.importance || "medium"} · ${snapshot.title || "未命名伏笔"}`;
34
+ if (type === "foreshadow") return `${levelLabel(snapshot.importance || "medium")} · ${snapshot.title || "未命名伏笔"}`;
33
35
  return "历史快照";
34
36
  }
@@ -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=20260724-account-menu-identity">
13
+ <link rel="stylesheet" href="/styles.css?v=20260725-ui-collaboration">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -73,6 +73,13 @@
73
73
  <div id="work-meta" class="work-meta">尚未选择作品</div>
74
74
  <div class="top-actions">
75
75
  <span id="save-state" class="save-state">就绪</span>
76
+ <div id="presence-control" class="presence-control hidden">
77
+ <button id="presence-button" class="presence-button" type="button" aria-expanded="false" aria-controls="presence-panel"><span class="presence-status-dot" aria-hidden="true"></span><span id="presence-count">1 人在线</span></button>
78
+ <section id="presence-panel" class="presence-panel hidden" aria-label="当前作品在线协作者">
79
+ <header><strong>正在访问这部作品</strong><small>离开后状态会自动过期</small></header>
80
+ <div id="presence-list" class="presence-list" aria-live="polite"></div>
81
+ </section>
82
+ </div>
76
83
  <button id="account-button" class="account-button" type="button" aria-haspopup="menu" aria-expanded="false"><span id="account-avatar" class="user-avatar account-avatar" aria-hidden="true"><span class="user-avatar-fallback">作</span></span><span id="account-name">账户</span></button>
77
84
  <div id="account-menu" class="account-menu hidden" role="menu">
78
85
  <strong id="account-menu-name" class="account-menu-name"><span id="account-menu-display-name">账户</span><span id="account-menu-username" class="account-menu-username">@account</span></strong><small id="account-menu-role">普通用户</small>
@@ -321,7 +328,7 @@
321
328
  <dialog id="form-dialog" class="dialog">
322
329
  <form id="dynamic-form" method="dialog">
323
330
  <div class="dialog-header">
324
- <div><span id="dialog-eyebrow" class="eyebrow">新增</span><h2 id="dialog-title">创建记录</h2></div>
331
+ <div><span id="dialog-eyebrow" class="eyebrow">新增</span><h2 id="dialog-title">创建记录</h2><p id="dialog-meta" class="dialog-header-meta hidden"></p></div>
325
332
  <button class="dialog-close" value="cancel" aria-label="关闭" type="submit">×</button>
326
333
  </div>
327
334
  <div id="dialog-fields" class="dialog-fields"></div>
@@ -365,23 +372,25 @@
365
372
  <header class="entity-editor-header setting-editor-header">
366
373
  <button id="setting-editor-back" class="entity-editor-back" type="button">返回设定库</button>
367
374
  <div class="entity-editor-heading">
368
- <span id="setting-editor-eyebrow" class="eyebrow">作者事实</span>
375
+ <span id="setting-editor-eyebrow" class="eyebrow">新建设定</span>
369
376
  <input id="setting-editor-name" class="setting-editor-title-input" name="title" maxlength="200" placeholder="新建设定" aria-label="设定标题">
370
377
  <div class="setting-editor-header-fields">
371
378
  <label>分类<select id="setting-editor-category" name="category">
372
379
  <option value="世界规则">世界规则</option><option value="历史与年代">历史与年代</option><option value="地点与地图">地点与地图</option><option value="组织与阵营">组织与阵营</option><option value="物种与族群">物种与族群</option><option value="科技与物品">科技与物品</option><option value="术语与称谓">术语与称谓</option><option value="创作约束">创作约束</option>
373
380
  </select></label>
374
381
  <label class="setting-editor-lock"><input id="setting-editor-locked" name="locked" type="checkbox"><span><b>锁定为 AI 硬约束</b><small>锁定后,AI 续写与校对不能擅自覆盖这条设定。</small></span></label>
375
- <label id="setting-change-note-field" class="setting-editor-change-note">版本说明<input id="setting-change-note" name="changeNote" maxlength="500" placeholder="可选,例如:补充纪元历法限制"></label>
376
382
  </div>
377
383
  </div>
378
- <button id="setting-editor-submit" class="primary-button" type="submit">保存设定</button>
384
+ <div class="entity-editor-mode-actions">
385
+ <button id="setting-editor-edit" class="ghost-button hidden" type="button">编辑设定</button>
386
+ <button id="setting-editor-confirm" class="ghost-button hidden" type="button">确认候选</button>
387
+ <button id="setting-editor-deprecate" class="ghost-button hidden" type="button">弃用</button>
388
+ <button id="setting-editor-history" class="ghost-button hidden" type="button">版本历史</button>
389
+ <button id="setting-editor-delete" class="danger-button hidden" type="button">删除设定</button>
390
+ <button id="setting-editor-submit" class="primary-button" type="submit">保存设定</button>
391
+ </div>
379
392
  </header>
380
393
  <div class="setting-editor-workspace">
381
- <section id="setting-editor-management" class="entity-editor-management hidden" aria-label="设定档案操作">
382
- <div><strong>档案操作</strong><small>版本历史和高风险操作集中在编辑面板内。</small></div>
383
- <div class="entity-editor-management-actions"><button id="setting-editor-confirm" class="ghost-button hidden" type="button">确认候选</button><button id="setting-editor-deprecate" class="ghost-button hidden" type="button">弃用</button><button id="setting-editor-history" class="ghost-button" type="button">版本历史</button><button id="setting-editor-delete" class="danger-button" type="button">删除设定</button></div>
384
- </section>
385
394
  <main class="setting-editor-content">
386
395
  <div class="setting-markdown-field" data-vditor-editor-field>
387
396
  <div id="setting-editor-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
@@ -399,6 +408,7 @@
399
408
  <div class="character-editor-title-row"><h1 id="character-editor-title">新建角色</h1><span id="character-editor-version" class="character-version-badge">新档案</span></div>
400
409
  </div>
401
410
  <div class="character-editor-header-actions">
411
+ <button id="character-editor-edit" class="primary-button hidden" type="button">编辑角色</button>
402
412
  <button id="character-history-button" class="ghost-button" type="button" aria-controls="character-history-panel" aria-expanded="false">版本历史</button>
403
413
  <button id="character-merge-button" class="ghost-button hidden" type="button">合并角色</button>
404
414
  <button id="character-delete-button" class="danger-button hidden" type="button">删除角色</button>
@@ -433,6 +443,7 @@
433
443
  </div>
434
444
  <div class="character-editor-header-actions">
435
445
  <span id="knowledge-editor-header-note" class="knowledge-editor-header-note">独立资料页</span>
446
+ <button id="knowledge-editor-edit" class="primary-button hidden" type="button">编辑档案</button>
436
447
  <button id="knowledge-editor-history" class="ghost-button hidden" type="button">版本历史</button>
437
448
  <button id="knowledge-editor-merge" class="ghost-button hidden" type="button">合并档案</button>
438
449
  <button id="knowledge-editor-delete" class="danger-button hidden" type="button">删除档案</button>
@@ -683,6 +694,6 @@
683
694
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
684
695
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
685
696
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
686
- <script type="module" src="/app.js?v=20260724-account-menu-identity"></script>
697
+ <script type="module" src="/app.js?v=20260725-ui-collaboration"></script>
687
698
  </body>
688
699
  </html>
@@ -47,6 +47,7 @@ export function serializePageRoute(route = {}) {
47
47
  params.set("work", workId);
48
48
  params.set("entity", route.entity);
49
49
  if (route.entityId) params.set("id", String(route.entityId));
50
+ if (route.entityMode === "read") params.set("mode", "read");
50
51
  } else if (view === "welcome" && workId) {
51
52
  params.set("view", "welcome");
52
53
  params.set("work", workId);
@@ -80,7 +81,8 @@ export function parsePageRoute(hash = "") {
80
81
  const entity = value(params, "entity");
81
82
  if (!entityEditorSet.has(entity)) return { view: "shelf" };
82
83
  const entityId = value(params, "id");
83
- return { view, workId, entity, entityId: entityId || null };
84
+ const entityMode = value(params, "mode") === "read" ? "read" : "edit";
85
+ return { view, workId, entity, entityId: entityId || null, entityMode };
84
86
  }
85
87
  if (view === "welcome" && workId) return { view, workId };
86
88
  if (view === "settings" || view === "platform-ai") {
@@ -285,6 +285,45 @@ html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
285
285
  button, input, textarea, select { font: inherit; color: inherit; }
286
286
  button { cursor: pointer; }
287
287
 
288
+ input[type="checkbox"] {
289
+ appearance: none;
290
+ -webkit-appearance: none;
291
+ display: inline-grid;
292
+ flex: 0 0 16px;
293
+ place-content: center;
294
+ width: 16px;
295
+ height: 16px;
296
+ min-width: 16px;
297
+ margin: 0;
298
+ border: 1px solid var(--line);
299
+ border-radius: 4px;
300
+ background: var(--surface);
301
+ cursor: pointer;
302
+ transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
303
+ }
304
+
305
+ input[type="checkbox"]::before {
306
+ content: "";
307
+ width: 8px;
308
+ height: 5px;
309
+ border: solid #fff;
310
+ border-width: 0 0 2px 2px;
311
+ transform: translateY(-1px) rotate(-45deg) scale(0);
312
+ transform-origin: center;
313
+ transition: transform .12s ease;
314
+ }
315
+
316
+ input[type="checkbox"]:hover:not(:disabled) { border-color: var(--accent); }
317
+ input[type="checkbox"]:checked { border-color: var(--accent); background: var(--accent); }
318
+ input[type="checkbox"]:checked::before { transform: translateY(-1px) rotate(-45deg) scale(1); }
319
+ input[type="checkbox"]:focus-visible {
320
+ outline: none;
321
+ border-color: var(--accent);
322
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
323
+ }
324
+ input[type="checkbox"]:disabled { cursor: not-allowed; opacity: .48; }
325
+ input[type="checkbox"][hidden] { display: none; }
326
+
288
327
  .app-shell {
289
328
  display: grid;
290
329
  grid-template-columns: var(--left-panel-width) minmax(480px, 1fr) var(--ai-panel-width);
@@ -312,7 +351,24 @@ button { cursor: pointer; }
312
351
  .brand-block strong { font-size: 16px; letter-spacing: .16em; }
313
352
  .brand-block small { color: var(--muted); font-size: 10px; margin-top: 2px; }
314
353
  .work-meta { font-size: 13px; color: var(--muted); padding-left: 26px; }
315
- .top-actions { display: flex; justify-content: flex-end; gap: 10px; align-items: center; padding-right: 18px; }
354
+ .top-actions { position: relative; display: flex; justify-content: flex-end; gap: 10px; align-items: center; padding-right: 18px; }
355
+ .presence-control { position: relative; }
356
+ .presence-button { display: flex; align-items: center; gap: 7px; min-height: 34px; padding: 0 11px; border: 1px solid var(--line); border-radius: 18px; background: transparent; color: var(--muted); font-size: 11px; }
357
+ .presence-button:hover, .presence-button:focus-visible, .presence-button[aria-expanded="true"] { border-color: var(--accent); background: var(--paper-deep); color: var(--accent-dark); outline: none; }
358
+ .presence-status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 16%, transparent); }
359
+ .presence-panel { position: absolute; top: calc(100% + 10px); right: 0; z-index: 12; width: min(340px, calc(100vw - 24px)); padding: 14px; border: 1px solid var(--line); border-radius: 6px; background: var(--paper); box-shadow: 0 14px 38px rgba(36, 30, 24, .16); }
360
+ .presence-panel > header { display: grid; gap: 3px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
361
+ .presence-panel > header strong { font-size: 12px; }
362
+ .presence-panel > header small { color: var(--muted); font-size: 10px; }
363
+ .presence-list { display: grid; gap: 5px; padding-top: 8px; }
364
+ .presence-person { display: grid; grid-template-columns: 30px minmax(0, 1fr) auto; align-items: center; gap: 9px; padding: 7px; border-radius: 4px; }
365
+ .presence-person.is-same-page { background: color-mix(in srgb, var(--accent) 8%, transparent); }
366
+ .presence-person .user-avatar { width: 30px; height: 30px; }
367
+ .presence-person-copy { min-width: 0; }
368
+ .presence-person-copy strong, .presence-person-copy small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
369
+ .presence-person-copy strong { font-size: 11px; }
370
+ .presence-person-copy small { margin-top: 3px; color: var(--muted); font-size: 10px; }
371
+ .presence-same-page { color: var(--accent-dark); font-size: 9px; white-space: nowrap; }
316
372
  .theme-toggle, .settings-button, .topbar-icon-button {
317
373
  display: grid;
318
374
  flex: 0 0 34px;
@@ -450,7 +506,7 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
450
506
 
451
507
  .shelf-view { display: flex; flex-direction: column; height: 100%; overflow-y: auto; padding: 48px clamp(32px, 7vw, 110px) 90px; }
452
508
  #shelf-view, #settings-hub-view { padding-bottom: 24px; }
453
- .shelf-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 30px; max-width: 1400px; margin: 0 auto 38px; }
509
+ .shelf-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 30px; width: 100%; max-width: 1400px; margin: 0 auto 38px; }
454
510
  .shelf-header h1 { margin: 0 0 10px; font-size: clamp(30px, 4vw, 52px); font-weight: 560; letter-spacing: -.04em; }
455
511
  .shelf-header p { margin: 0; color: var(--muted); }
456
512
  .settings-hub-header { align-items: center; }
@@ -896,9 +952,11 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
896
952
  .record-card p { color: var(--muted); font-size: 12px; line-height: 1.55; margin: 0; white-space: pre-wrap; }
897
953
  .record-card small { color: var(--accent); font-size: 9px; letter-spacing: .1em; text-transform: uppercase; }
898
954
  .settings-layout-toolbar, .module-layout-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-bottom: 0; }
899
- .settings-layout-hint, .module-layout-hint { color: var(--muted); font-size: 10px; }
955
+ .settings-layout-hint { color: var(--muted); font-size: 10px; }
900
956
  .settings-layout-toggle, .module-layout-toggle { display: inline-flex; border: 1px solid var(--line); border-radius: 4px; overflow: hidden; background: var(--surface); }
901
- .settings-layout-toggle button, .module-layout-toggle button { min-height: 34px; padding: 0 12px; border: 0; border-right: 1px solid var(--line); background: transparent; color: var(--muted); font-size: 11px; }
957
+ .settings-layout-toggle button { min-height: 34px; padding: 0 12px; border: 0; border-right: 1px solid var(--line); background: transparent; color: var(--muted); font-size: 11px; }
958
+ .module-layout-toggle button { display: inline-grid; place-items: center; width: 36px; min-height: 34px; padding: 0; border: 0; border-right: 1px solid var(--line); background: transparent; color: var(--muted); }
959
+ .module-layout-toggle svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
902
960
  .settings-layout-toggle button:last-child, .module-layout-toggle button:last-child { border-right: 0; }
903
961
  .settings-layout-toggle button:hover, .settings-layout-toggle button:focus-visible,
904
962
  .module-layout-toggle button:hover, .module-layout-toggle button:focus-visible { color: var(--ink); background: var(--paper-deep); }
@@ -908,7 +966,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
908
966
  .setting-row, .module-row { display: grid; grid-template-columns: minmax(108px, .2fr) minmax(120px, .24fr) minmax(0, 1fr) auto; gap: 10px 16px; align-items: center; min-height: 0; padding: 12px 16px; }
909
967
  .setting-row h3, .module-row h3 { margin: 0; font-size: 14px; line-height: 1.35; }
910
968
  .setting-row p, .module-row p, .module-row-preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
911
- .module-row-preview { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.45; }
969
+ .module-row .module-row-preview { display: -webkit-box; max-height: 2.9em; margin: 0; overflow: hidden; color: var(--muted); font-size: 12px; line-height: 1.45; white-space: normal; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
912
970
  .module-row-path { display: block; margin-top: 2px; color: var(--accent); font-family: var(--font-latin), monospace; font-size: 11px; font-weight: 500; }
913
971
  .module-row-span { grid-column: 1 / -1; }
914
972
  .setting-row .card-actions, .module-row .card-actions { margin-top: 0; flex-wrap: wrap; justify-content: flex-end; }
@@ -929,6 +987,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
929
987
  .character-duplicate-actions { flex-wrap: wrap; }
930
988
  .card-actions { display: flex; gap: 6px; margin-top: 15px; }
931
989
  .card-actions button { border: 1px solid var(--line); background: transparent; border-radius: 3px; font-size: 10px; padding: 5px 8px; }
990
+ .card-actions .record-card-edit { padding: 0; }
932
991
  .danger-button { border: 1px solid color-mix(in srgb, var(--accent) 52%, var(--line)); border-radius: 4px; background: transparent; color: var(--accent-dark); padding: 8px 13px; font-size: 12px; }
933
992
  .danger-button:hover, .danger-button:focus-visible { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 9%, transparent); outline: none; }
934
993
  .card-actions .danger-button { border-color: color-mix(in srgb, var(--accent) 52%, var(--line)); color: var(--accent-dark); }
@@ -958,14 +1017,14 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
958
1017
  .race-settings .pill { display: inline-flex; align-items: center; gap: 5px; }
959
1018
  .race-settings .pill small { color: inherit; font-size: 8px; letter-spacing: 0; text-transform: none; opacity: .72; }
960
1019
  .race-settings .pill.inherited { border-style: dashed; }
961
- .entity-dialog-management, .entity-editor-management { display: grid; gap: 10px; padding: 14px; border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--line)); border-radius: 5px; background: color-mix(in srgb, var(--accent) 4%, var(--surface)); }
962
- .entity-dialog-management > div:first-child, .entity-editor-management > div:first-child { display: grid; gap: 4px; }
963
- .entity-dialog-management strong, .entity-editor-management strong { color: var(--ink); font-size: 11px; }
964
- .entity-dialog-management small, .entity-editor-management small { color: var(--muted); font-size: 10px; line-height: 1.5; }
965
- .entity-dialog-management-actions, .entity-editor-management-actions { display: flex; flex-wrap: wrap; gap: 7px; }
966
- .entity-dialog-management-actions button, .entity-editor-management-actions button { min-height: 32px; }
1020
+ .entity-dialog-management { display: grid; gap: 10px; padding: 14px; border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--line)); border-radius: 5px; background: color-mix(in srgb, var(--accent) 4%, var(--surface)); }
1021
+ .entity-dialog-management > div:first-child { display: grid; gap: 4px; }
1022
+ .entity-dialog-management strong { color: var(--ink); font-size: 11px; }
1023
+ .entity-dialog-management small { color: var(--muted); font-size: 10px; line-height: 1.5; }
1024
+ .entity-dialog-management-actions { display: flex; flex-wrap: wrap; gap: 7px; }
1025
+ .entity-dialog-management-actions button { min-height: 32px; }
967
1026
  .danger-confirm-field { display: grid !important; grid-template-columns: auto minmax(0, 1fr); align-items: start; gap: 9px !important; padding: 12px; border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line)); border-radius: 5px; background: color-mix(in srgb, var(--accent) 5%, var(--surface)); }
968
- .danger-confirm-field input { width: auto !important; margin-top: 3px; }
1027
+ .danger-confirm-field input { width: 16px !important; margin-top: 3px; }
969
1028
  .danger-confirm-field span { display: grid; gap: 4px; }
970
1029
  .danger-confirm-field strong { color: var(--ink); font-size: 11px; }
971
1030
  .danger-confirm-field small { color: var(--muted); font-size: 10px; line-height: 1.5; }
@@ -1027,7 +1086,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1027
1086
  .config-section-header p { margin: 0; color: var(--muted); font-size: 11px; }
1028
1087
  .ai-agent-tools { display: grid; gap: 8px; }
1029
1088
  .ai-agent-tools label { display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: 8px; align-items: start; color: var(--muted); font-size: 11px; line-height: 1.45; }
1030
- .ai-agent-tools input { width: auto; margin-top: 2px; }
1089
+ .ai-agent-tools input { width: 16px; margin-top: 2px; }
1031
1090
  .ai-agent-tools strong { color: var(--ink); font-size: 12px; font-weight: 600; }
1032
1091
  .ai-agent-tools small { display: block; margin-top: 2px; font-size: 10px; }
1033
1092
  .default-model-select { min-width: 260px; padding: 7px 9px; font-size: 11px; }
@@ -1504,6 +1563,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1504
1563
  }
1505
1564
  .toast-confirmation strong { font-size: 12px; }
1506
1565
  .toast-confirmation p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.55; white-space: pre-line; }
1566
+ .toast-input { width: 100%; min-height: 34px; padding: 7px 9px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 11px; }
1507
1567
  .toast-confirmation-actions { display: flex; justify-content: flex-end; gap: 7px; margin-top: 3px; }
1508
1568
  .toast-confirmation-actions button { min-height: 30px; padding: 6px 10px; }
1509
1569
  .chapter-type-menu { position: fixed; z-index: 80; width: 176px; padding: 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); box-shadow: 0 14px 38px rgba(49,42,32,.22); }
@@ -1522,6 +1582,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1522
1582
  .dialog::backdrop { background: rgba(34,30,25,.42); backdrop-filter: blur(3px); }
1523
1583
  .dialog-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 22px 24px 16px; border-bottom: 1px solid var(--line); }
1524
1584
  .dialog-header h2 { margin: 0; font-size: 23px; font-weight: 500; }
1585
+ .dialog-header-meta { margin: 7px 0 0; color: var(--muted); font-size: 10px; line-height: 1.5; }
1525
1586
  .dialog-close {
1526
1587
  display: grid;
1527
1588
  place-items: center;
@@ -1586,12 +1647,12 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1586
1647
  .character-section-count { display: block; margin-top: 8px; color: var(--accent-dark); }
1587
1648
  .chip-picker { display: flex; flex-wrap: wrap; gap: 7px; max-height: 190px; overflow-y: auto; padding: 9px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface-soft); }
1588
1649
  .dialog-fields .member-chip { position: relative; display: inline-flex !important; width: auto; cursor: pointer; }
1589
- .member-chip input { position: absolute; width: 1px !important; height: 1px; opacity: 0; }
1650
+ .member-chip input { position: absolute; width: 1px !important; min-width: 1px; height: 1px; opacity: 0; }
1590
1651
  .member-chip span { display: inline-flex; align-items: center; min-height: 30px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 16px; background: var(--panel); color: var(--muted); font-size: 10px; transition: background .14s ease, border-color .14s ease, color .14s ease; }
1591
1652
  .member-chip input:checked + span { border-color: var(--accent); background: var(--accent); color: #fff; }
1592
1653
  .member-chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
1593
1654
  .checkbox-field { display: flex !important; align-items: center; grid-template-columns: 18px 1fr; }
1594
- .checkbox-field input { width: auto; }
1655
+ .checkbox-field input { width: 16px; }
1595
1656
  .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 14px 24px 20px; }
1596
1657
  .dialog-actions .reset-button { margin-right: auto; }
1597
1658
  .import-mode-dialog { width: min(560px, calc(100vw - 32px)); }
@@ -1618,6 +1679,14 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1618
1679
  .import-mode-dialog .dialog-actions { flex-wrap: wrap; }
1619
1680
  }
1620
1681
  .entity-editor-open { overflow: hidden; }
1682
+ .preview-record-card { cursor: pointer; }
1683
+ .preview-record-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1684
+ .entity-editor-mode-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; }
1685
+ .entity-editor-mode-actions button { min-height: 36px; }
1686
+ .module-pagination { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px 0 4px; }
1687
+ .module-pagination span { color: var(--muted); font-size: 10px; }
1688
+ .module-pagination button { min-width: 58px; min-height: 28px; padding: 5px 9px; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); font-size: 10px; }
1689
+ .module-pagination button:disabled { cursor: default; opacity: .46; }
1621
1690
  .entity-editor-view { position: fixed; z-index: 700; inset: 0; overflow: hidden; background: var(--panel); color: var(--ink); }
1622
1691
  .entity-editor-view.hidden, .entity-editor-page.hidden { display: none; }
1623
1692
  .entity-editor-page { width: 100%; height: 100%; min-height: 0; }
@@ -1638,16 +1707,16 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1638
1707
  .setting-editor-title-input:focus { border-color: var(--accent); box-shadow: none; }
1639
1708
  .setting-editor-header-fields { display: flex; align-items: center; justify-content: center; gap: 14px; min-width: 0; }
1640
1709
  .setting-editor-header-fields > label:not(.setting-editor-lock) { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 10px; white-space: nowrap; }
1641
- .setting-editor-header-fields select, .setting-editor-change-note input { min-width: 0; padding: 5px 8px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 11px; }
1710
+ .setting-editor-header-fields select { min-width: 0; padding: 5px 8px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 11px; }
1642
1711
  .setting-editor-lock { display: inline-flex; grid-template-columns: none; align-items: center; gap: 6px; padding: 0; border: 0; background: transparent; white-space: nowrap; }
1643
- .setting-editor-lock input { width: auto; margin: 0; }
1644
- .setting-editor-lock span { display: block; }
1645
- .setting-editor-lock b { color: var(--ink); font-size: 10px; }
1712
+ .setting-editor-lock input { width: 16px; margin: 0; }
1713
+ .setting-editor-lock span { display: inline-flex; align-items: center; min-height: 16px; line-height: 16px; }
1714
+ .setting-editor-lock b { display: block; color: var(--ink); font-size: 10px; line-height: 16px; }
1646
1715
  .setting-editor-lock small { display: none; }
1647
- .setting-editor-change-note { align-items: center; gap: 6px; }
1648
- .setting-editor-workspace { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; overflow: hidden; }
1716
+ .setting-editor-workspace { display: grid; grid-template-rows: minmax(0, 1fr); min-height: 0; overflow: hidden; }
1649
1717
  .setting-editor-content { height: 100%; min-height: 0; padding: 16px clamp(16px, 2vw, 32px) 24px; overflow: hidden; }
1650
1718
  .setting-markdown-field { display: grid; grid-template-rows: minmax(0, 1fr); height: 100%; min-height: 0; }
1719
+ .entity-editor-page.is-read-only .vditor-ir pre.vditor-reset[contenteditable="false"], .entity-editor-page.is-read-only .vditor-wysiwyg pre.vditor-reset[contenteditable="false"], .entity-editor-page.is-read-only .vditor-sv[contenteditable="false"] { opacity: 1; cursor: default; }
1651
1720
  .setting-markdown-heading, .markdown-editor-field-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 24px; color: var(--ink); font-size: 14px; }
1652
1721
  .setting-markdown-heading small, .markdown-editor-field-heading small { color: var(--muted); font-size: 10px; }
1653
1722
  .markdown-editor-field { display: grid; gap: 10px; min-width: 0; }
package/dist/user-auth.js CHANGED
@@ -700,6 +700,8 @@ function workModuleRequirements(request, write) {
700
700
  return write ? { ownerOnly: true } : {};
701
701
  if (/^\/api\/works\/[^/]+\/members(?:\/[^/]+)?$/u.test(pathname))
702
702
  return { ownerOnly: true };
703
+ if (/^\/api\/works\/[^/]+\/presence$/u.test(pathname))
704
+ return {};
703
705
  if (/^\/api\/works\/[^/]+\/audit-logs$/u.test(pathname))
704
706
  return { ownerOnly: true };
705
707
  if (/^\/api\/works\/[^/]+\/models$/u.test(pathname))