@musnows/scriverse 0.3.8 → 0.3.10

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.
@@ -9,7 +9,7 @@
9
9
  <script src="/theme-init.js?v=20260720-route-skeleton"></script>
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
- <link rel="stylesheet" href="/styles.css?v=20260722-entity-editor-page">
12
+ <link rel="stylesheet" href="/styles.css?v=20260722-module-permissions-cr">
13
13
  </head>
14
14
  <body class="auth-pending">
15
15
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -316,6 +316,29 @@
316
316
  </form>
317
317
  </dialog>
318
318
 
319
+ <dialog id="import-mode-dialog" class="dialog import-mode-dialog" aria-labelledby="import-mode-dialog-title" aria-describedby="import-mode-dialog-description" data-testid="import-mode-dialog">
320
+ <form method="dialog">
321
+ <div class="dialog-header">
322
+ <div><span class="eyebrow">已有项目</span><h2 id="import-mode-dialog-title">选择正文导入方式</h2></div>
323
+ <button class="dialog-close" value="cancel" aria-label="取消导入" type="submit">×</button>
324
+ </div>
325
+ <div class="import-mode-body">
326
+ <p id="import-mode-dialog-description">即将把所选文件导入当前作品。请选择追加还是覆盖正文。</p>
327
+ <p id="import-mode-file-summary" class="import-mode-file-summary"></p>
328
+ <p id="import-mode-unsaved-warning" class="import-mode-unsaved-warning hidden" role="alert">当前章节有未保存修改,继续导入会丢失这些本地修改。</p>
329
+ <div class="import-mode-options" aria-label="导入方式说明">
330
+ <section><strong>追加正文</strong><span>保留现有卷章,把新文件解析出的卷章添加到目录末尾。</span></section>
331
+ <section><strong>覆盖正文</strong><span>删除现有正文目录后写入新文件;导入前会保留可恢复的版本快照。</span></section>
332
+ </div>
333
+ </div>
334
+ <div class="dialog-actions">
335
+ <button class="ghost-button" value="cancel" type="submit">取消</button>
336
+ <button id="import-mode-append" class="ghost-button" value="append" type="submit">追加正文</button>
337
+ <button id="import-mode-overwrite" class="primary-button" value="overwrite" type="submit">覆盖正文</button>
338
+ </div>
339
+ </form>
340
+ </dialog>
341
+
319
342
  <section id="entity-editor-view" class="entity-editor-view hidden" aria-label="全屏资料编辑">
320
343
  <form id="setting-editor-form" class="entity-editor-page hidden" aria-labelledby="setting-editor-title">
321
344
  <header class="entity-editor-header">
@@ -506,9 +529,22 @@
506
529
  </dialog>
507
530
 
508
531
  <dialog id="members-dialog" class="dialog wide-dialog" aria-labelledby="members-dialog-title">
509
- <div class="dialog-header"><div><span id="members-dialog-eyebrow" class="eyebrow">作品权限</span><h2 id="members-dialog-title">可访问人</h2></div><button id="members-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div>
532
+ <div class="dialog-header"><div><span id="members-dialog-eyebrow" class="eyebrow">作品权限</span><h2 id="members-dialog-title">成员模块权限</h2></div><button id="members-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div>
510
533
  <div class="access-dialog-body">
511
- <form id="member-invite-form" class="member-invite-form"><label>邀请已注册用户<select id="member-user-select" required></select></label><label>访问权限<select id="member-role-select" required><option value="viewer">仅查看</option><option value="editor">可编辑</option></select></label><button class="primary-button" type="submit">发送邀请</button></form>
534
+ <form id="member-permission-form" class="member-permission-form">
535
+ <label class="member-person-field">选择成员<select id="member-user-select" required></select><small>先选择已注册用户,再逐项配置模块权限。</small></label>
536
+ <fieldset id="member-permission-fieldset" disabled>
537
+ <legend>模块读写权限</legend>
538
+ <div class="member-permission-presets" aria-label="权限快捷设置">
539
+ <button type="button" data-permission-preset="none">全部关闭</button>
540
+ <button type="button" data-permission-preset="read">全部只读</button>
541
+ <button type="button" data-permission-preset="write">全部可编辑</button>
542
+ </div>
543
+ <div id="member-permission-grid" class="member-permission-grid"></div>
544
+ <small class="member-permission-help">“可编辑”自动包含读取权限;未授权模块不会显示,也不能通过 API 读取。</small>
545
+ <button id="member-permission-submit" class="primary-button" type="submit">保存成员权限</button>
546
+ </fieldset>
547
+ </form>
512
548
  <div id="members-list" class="access-list"></div>
513
549
  </div>
514
550
  </dialog>
@@ -575,6 +611,6 @@
575
611
  </dialog>
576
612
 
577
613
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
578
- <script type="module" src="/app.js?v=20260722-pagination-entity-editor-page"></script>
614
+ <script type="module" src="/app.js?v=20260722-module-permissions-cr"></script>
579
615
  </body>
580
616
  </html>
@@ -391,8 +391,8 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
391
391
  .settings-hub-card strong { align-self: end; font-size: 16px; }
392
392
  .settings-hub-card small { align-self: start; color: var(--muted); font-size: 10px; line-height: 1.5; }
393
393
  .settings-work-note { max-width: 1000px; margin: 18px auto 0; color: var(--muted); font-size: 11px; }
394
- .access-dialog-body { display: grid; gap: 16px; padding: 18px 24px 22px; }
395
- .access-list { display: grid; gap: 8px; max-height: min(52vh, 480px); overflow: auto; padding: 0; }
394
+ .access-dialog-body { display: grid; gap: 16px; max-height: calc(88vh - 82px); overflow-y: auto; padding: 18px 24px 22px; }
395
+ .access-list { display: grid; gap: 8px; max-height: min(34vh, 340px); overflow: auto; padding: 0; }
396
396
  .access-row {
397
397
  display: grid;
398
398
  grid-template-columns: minmax(0, 1fr) auto auto;
@@ -450,9 +450,10 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
450
450
  cursor: not-allowed;
451
451
  opacity: .46;
452
452
  }
453
- .member-invite-form { display: grid; grid-template-columns: minmax(0, 1fr) minmax(130px, .3fr) auto; align-items: end; gap: 10px; margin: 0; }
454
- .member-invite-form label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
455
- .member-invite-form select {
453
+ .member-permission-form { display: grid; gap: 12px; margin: 0; padding: 14px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-soft); }
454
+ .member-person-field { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
455
+ .member-person-field small, .member-permission-help { color: var(--muted); font-size: 10px; line-height: 1.5; }
456
+ .member-person-field select, .member-permission-row select {
456
457
  width: 100%;
457
458
  min-height: 38px;
458
459
  padding: 9px 10px;
@@ -462,16 +463,33 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
462
463
  color: var(--ink);
463
464
  font-size: 13px;
464
465
  }
465
- .member-invite-form .primary-button { min-height: 38px; white-space: nowrap; }
466
- .app-shell.view-only-mode:not(.shelf-mode) { grid-template-columns: var(--left-panel-width) minmax(480px, 1fr); }
467
- .app-shell.view-only-mode:not(.shelf-mode) .ai-panel { display: none; }
466
+ .member-permission-form fieldset { display: grid; gap: 12px; min-width: 0; margin: 0; padding: 14px; border: 1px solid var(--line); border-radius: 6px; }
467
+ .member-permission-form fieldset:disabled { opacity: .58; }
468
+ .member-permission-form legend { padding: 0 6px; color: var(--ink); font-size: 12px; font-weight: 650; }
469
+ .member-permission-presets { display: flex; flex-wrap: wrap; gap: 6px; }
470
+ .member-permission-presets button { padding: 6px 9px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--muted); font-size: 10px; }
471
+ .member-permission-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; }
472
+ .member-permission-row { display: grid; grid-template-columns: minmax(90px, 1fr) minmax(112px, .7fr); align-items: center; gap: 8px; color: var(--ink); font-size: 11px; }
473
+ .member-permission-row select { min-height: 34px; padding-block: 6px; font-size: 11px; }
474
+ .member-permission-form .primary-button { justify-self: end; min-height: 38px; white-space: nowrap; }
475
+ .permission-hidden { display: none !important; }
476
+ .app-shell.ai-hidden-mode:not(.shelf-mode) { grid-template-columns: var(--left-panel-width) minmax(480px, 1fr); }
477
+ .app-shell.prose-hidden-mode:not(.shelf-mode) .left-actions,
478
+ .app-shell.prose-hidden-mode:not(.shelf-mode) .panel-heading,
479
+ .app-shell.prose-hidden-mode:not(.shelf-mode) #novel-tree { display: none !important; }
480
+ .app-shell.prose-read-only-mode:not(.shelf-mode) .left-primary-actions .file-button,
481
+ .app-shell.prose-read-only-mode:not(.shelf-mode) #new-volume-button,
482
+ .app-shell.prose-read-only-mode:not(.shelf-mode) #new-chapter-button,
483
+ .app-shell.prose-read-only-mode:not(.shelf-mode) #tidy-blank-lines-button,
484
+ .app-shell.prose-read-only-mode:not(.shelf-mode) #save-button { display: none !important; }
485
+ .app-shell.prose-read-only-mode:not(.shelf-mode) .left-primary-actions { justify-content: flex-end; }
486
+ .app-shell.prose-read-only-mode:not(.shelf-mode) #chapter-title[readonly],
487
+ .app-shell.prose-read-only-mode:not(.shelf-mode) #chapter-content[readonly] { cursor: default; }
468
488
  .app-shell.view-only-mode:not(.shelf-mode) .left-primary-actions .file-button,
469
489
  .app-shell.view-only-mode:not(.shelf-mode) #new-volume-button,
470
490
  .app-shell.view-only-mode:not(.shelf-mode) #new-chapter-button,
471
491
  .app-shell.view-only-mode:not(.shelf-mode) #module-create-button,
472
492
  .app-shell.view-only-mode:not(.shelf-mode) [data-work-settings],
473
- .app-shell.view-only-mode:not(.shelf-mode) [data-module="tasks"],
474
- .app-shell.view-only-mode:not(.shelf-mode) [data-module="ai-settings"],
475
493
  .app-shell.view-only-mode:not(.shelf-mode) #tidy-blank-lines-button,
476
494
  .app-shell.view-only-mode:not(.shelf-mode) #save-button { display: none !important; }
477
495
  .app-shell.view-only-mode:not(.shelf-mode) .left-primary-actions { justify-content: flex-end; }
@@ -495,6 +513,8 @@ body.work-viewer-mode [data-edit-relationship],
495
513
  body.work-viewer-mode [data-character-relationship-edit],
496
514
  body.work-viewer-mode [data-character-relationship-create],
497
515
  body.work-viewer-mode [data-review-id],
516
+ body.work-viewer-mode [data-merge-review],
517
+ body.work-viewer-mode [data-keep-characters-separate],
498
518
  body.work-viewer-mode .task-auto-run-panel,
499
519
  body.work-viewer-mode [data-run-task],
500
520
  body.work-viewer-mode [data-cancel-task],
@@ -571,10 +591,12 @@ body.work-viewer-mode .character-editor-actions { display: none !important; }
571
591
  -webkit-box-orient: vertical;
572
592
  }
573
593
  .work-access-field { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-soft); }
574
- .work-access-field > div { display: grid; gap: 4px; min-width: 0; }
594
+ .work-access-field > div { display: grid; gap: 6px; min-width: 0; }
575
595
  .work-access-field strong { color: var(--ink); font-size: 12px; }
576
596
  .work-access-field small { color: var(--muted); font-size: 10px; line-height: 1.5; }
577
597
  .work-access-field .ghost-button { flex: 0 0 auto; white-space: nowrap; }
598
+ .work-access-options { display: flex; flex-wrap: wrap; gap: 5px; }
599
+ .work-access-options span { padding: 3px 7px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); color: var(--ink); font-size: 10px; line-height: 1.35; }
578
600
  .account-dialog { width: min(640px, 92vw); overflow: hidden; }
579
601
  .account-settings-body { display: grid; gap: 16px; max-height: calc(88vh - 86px); overflow-y: auto; padding: 20px 24px 24px; }
580
602
  .account-settings-section { padding: 18px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-soft); }
@@ -803,6 +825,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
803
825
  .character-duplicate-actions { flex-wrap: wrap; }
804
826
  .card-actions { display: flex; gap: 6px; margin-top: 15px; }
805
827
  .card-actions button { border: 1px solid var(--line); background: transparent; border-radius: 3px; font-size: 10px; padding: 5px 8px; }
828
+ .card-actions .danger-button { border-color: color-mix(in srgb, var(--accent) 52%, var(--line)); color: var(--accent-dark); }
829
+ .card-actions .danger-button:hover, .card-actions .danger-button:focus-visible { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 9%, transparent); }
806
830
  .card-actions .primary-button { border-color: var(--accent); background: var(--accent); color: #fff; padding: 8px 13px; font-size: 12px; }
807
831
  .card-actions .primary-button:hover, .card-actions .primary-button:focus-visible { background: var(--accent-dark); }
808
832
  .provider-card-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); }
@@ -1344,6 +1368,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1344
1368
  .dialog-fields label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
1345
1369
  .dialog-fields input, .dialog-fields textarea, .dialog-fields select { width: 100%; padding: 9px 10px; font-size: 13px; }
1346
1370
  .dialog-fields textarea { resize: vertical; min-height: 90px; line-height: 1.6; }
1371
+ .merge-dialog-note { margin: 0; padding: 10px 12px; border-left: 2px solid var(--accent); background: var(--surface-soft); color: var(--muted); font-size: 11px; line-height: 1.6; }
1347
1372
  .form-field { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
1348
1373
  .analysis-type-description { margin: 0; color: var(--muted); font-size: 10px; line-height: 1.55; }
1349
1374
  .item-list-rows { display: grid; gap: 7px; }
@@ -1384,6 +1409,19 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1384
1409
  .checkbox-field input { width: auto; }
1385
1410
  .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 14px 24px 20px; }
1386
1411
  .dialog-actions .reset-button { margin-right: auto; }
1412
+ .import-mode-dialog { width: min(560px, calc(100vw - 32px)); }
1413
+ .import-mode-body { display: grid; gap: 14px; padding: 20px 24px 6px; }
1414
+ .import-mode-body p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.65; }
1415
+ .import-mode-file-summary { overflow-wrap: anywhere; color: var(--ink) !important; font-weight: 600; }
1416
+ .import-mode-unsaved-warning { padding: 9px 11px; border: 1px solid color-mix(in srgb, var(--accent) 46%, var(--line)); border-radius: 4px; background: color-mix(in srgb, var(--accent) 8%, var(--surface)); color: var(--accent-dark) !important; }
1417
+ .import-mode-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
1418
+ .import-mode-options section { display: grid; gap: 5px; min-width: 0; padding: 12px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-soft); }
1419
+ .import-mode-options strong { color: var(--ink); font-size: 12px; }
1420
+ .import-mode-options span { color: var(--muted); font-size: 10px; line-height: 1.55; }
1421
+ @media (max-width: 560px) {
1422
+ .import-mode-options { grid-template-columns: 1fr; }
1423
+ .import-mode-dialog .dialog-actions { flex-wrap: wrap; }
1424
+ }
1387
1425
  .entity-editor-open { overflow: hidden; }
1388
1426
  .entity-editor-view { position: fixed; z-index: 700; inset: 0; overflow: hidden; background: var(--panel); color: var(--ink); }
1389
1427
  .entity-editor-view.hidden, .entity-editor-page.hidden { display: none; }
@@ -1556,7 +1594,12 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1556
1594
  .relationship-table .relationship-actions button + button { margin-left: 5px; }
1557
1595
 
1558
1596
  @media (max-width: 540px) {
1559
- .member-invite-form { grid-template-columns: minmax(0, 1fr); }
1597
+ .member-permission-grid { grid-template-columns: minmax(0, 1fr); }
1598
+ .member-permission-row { grid-template-columns: minmax(0, 1fr); }
1599
+ .member-permission-form .primary-button { justify-self: stretch; }
1600
+ .access-row { grid-template-columns: minmax(0, 1fr); align-items: stretch; }
1601
+ .access-row select, .access-row button { width: 100%; }
1602
+ .work-access-field { align-items: stretch; flex-direction: column; }
1560
1603
  .onboarding-popover { width: calc(100vw - 24px); }
1561
1604
  .onboarding-popover-header { padding-inline: 15px; }
1562
1605
  .onboarding-popover-body { padding: 16px 17px 18px; }
@@ -0,0 +1,14 @@
1
+ export type WorkPermissionModule = "prose" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "ai" | "ai-settings";
2
+ export type WorkUiModule = "editor" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "tasks" | "ai-settings";
3
+ export type WorkModuleAccess = "none" | "read" | "write";
4
+ export type WorkModulePermissions = Record<WorkPermissionModule, WorkModuleAccess>;
5
+ export type WorkPermissionAware = { accessRole?: string | null; modulePermissions?: WorkModulePermissions };
6
+
7
+ export const WORK_PERMISSION_MODULES: readonly Readonly<{ id: WorkPermissionModule; uiModule: WorkUiModule; label: string }>[];
8
+ export function emptyModulePermissions(): WorkModulePermissions;
9
+ export function normalizeModulePermissions(value: unknown, accessRole?: string): WorkModulePermissions;
10
+ export function permissionModuleForUiModule(uiModule: string): WorkPermissionModule;
11
+ export function canReadUiModule(work: WorkPermissionAware | null | undefined, uiModule: string): boolean;
12
+ export function canWriteUiModule(work: WorkPermissionAware | null | undefined, uiModule: string): boolean;
13
+ export function firstReadableUiModule(work: WorkPermissionAware | null | undefined): WorkUiModule | null;
14
+ export function permissionSummary(value: unknown): string;
@@ -0,0 +1,71 @@
1
+ export const WORK_PERMISSION_MODULES = Object.freeze([
2
+ { id: "prose", uiModule: "editor", label: "正文" },
3
+ { id: "settings", uiModule: "settings", label: "设定库" },
4
+ { id: "characters", uiModule: "characters", label: "角色" },
5
+ { id: "races", uiModule: "races", label: "种族" },
6
+ { id: "organizations", uiModule: "organizations", label: "组织" },
7
+ { id: "timeline", uiModule: "timeline", label: "时间轴" },
8
+ { id: "relationships", uiModule: "relationships", label: "关系" },
9
+ { id: "outlines", uiModule: "outlines", label: "大纲与伏笔" },
10
+ { id: "reviews", uiModule: "reviews", label: "审核" },
11
+ { id: "ai", uiModule: "tasks", label: "AI 对话与分析" },
12
+ { id: "ai-settings", uiModule: "ai-settings", label: "AI 设置" }
13
+ ]);
14
+
15
+ const permissionModuleByUiModule = new Map(WORK_PERMISSION_MODULES.map((item) => [item.uiModule, item.id]));
16
+ const validAccess = new Set(["none", "read", "write"]);
17
+
18
+ export function emptyModulePermissions() {
19
+ return Object.fromEntries(WORK_PERMISSION_MODULES.map((item) => [item.id, "none"]));
20
+ }
21
+
22
+ export function normalizeModulePermissions(value, accessRole = "viewer") {
23
+ if (value && typeof value === "object" && !Array.isArray(value)
24
+ && WORK_PERMISSION_MODULES.every((item) => validAccess.has(value[item.id]))) {
25
+ return Object.fromEntries(WORK_PERMISSION_MODULES.map((item) => [item.id, value[item.id]]));
26
+ }
27
+ const fallback = emptyModulePermissions();
28
+ for (const item of WORK_PERMISSION_MODULES) {
29
+ fallback[item.id] = accessRole === "editor" || accessRole === "admin" || accessRole === "owner"
30
+ ? "write"
31
+ : accessRole === "viewer" || accessRole === "settings-editor" ? "read" : "none";
32
+ }
33
+ if (accessRole === "settings-editor") {
34
+ fallback.prose = "read";
35
+ fallback.reviews = "read";
36
+ fallback.ai = "read";
37
+ fallback["ai-settings"] = "read";
38
+ }
39
+ return fallback;
40
+ }
41
+
42
+ export function permissionModuleForUiModule(uiModule) {
43
+ return permissionModuleByUiModule.get(String(uiModule)) ?? "prose";
44
+ }
45
+
46
+ export function canReadUiModule(work, uiModule) {
47
+ if (!work) return false;
48
+ const permissions = normalizeModulePermissions(work.modulePermissions, work.accessRole);
49
+ return permissions[permissionModuleForUiModule(uiModule)] !== "none";
50
+ }
51
+
52
+ export function canWriteUiModule(work, uiModule) {
53
+ if (!work) return false;
54
+ const permissions = normalizeModulePermissions(work.modulePermissions, work.accessRole);
55
+ return permissions[permissionModuleForUiModule(uiModule)] === "write";
56
+ }
57
+
58
+ export function firstReadableUiModule(work) {
59
+ return WORK_PERMISSION_MODULES.find((item) => canReadUiModule(work, item.uiModule))?.uiModule ?? null;
60
+ }
61
+
62
+ export function permissionSummary(value) {
63
+ const permissions = normalizeModulePermissions(value, "custom");
64
+ const writable = WORK_PERMISSION_MODULES.filter((item) => permissions[item.id] === "write").map((item) => item.label);
65
+ const readable = WORK_PERMISSION_MODULES.filter((item) => permissions[item.id] === "read").map((item) => item.label);
66
+ const parts = [];
67
+ if (writable.length) parts.push(`可编辑:${writable.join("、")}`);
68
+ if (readable.length) parts.push(`只读:${readable.join("、")}`);
69
+ if (!parts.length) parts.push("未授权任何模块");
70
+ return parts.join(";");
71
+ }