@waterwx/dsh-novel-forge 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/lib/client.js +2537 -1858
  2. package/lib/client.js.map +1 -1
  3. package/lib/index.js +657 -45
  4. package/lib/index.js.map +1 -1
  5. package/lib/types/client/api.d.ts +2 -0
  6. package/lib/types/client/locales.d.ts +0 -1
  7. package/lib/types/client/panel/FlowGuide.d.ts +21 -0
  8. package/lib/types/client/panel/MangaRoleLibrary.d.ts +18 -0
  9. package/lib/types/client/panel/MangaWorkspace.d.ts +2 -2
  10. package/lib/types/client/panel/StoryboardTab.d.ts +5 -1
  11. package/lib/types/engine.d.ts +15 -8
  12. package/lib/types/protocol.d.ts +124 -0
  13. package/package.json +2 -1
  14. package/src/client/api.ts +5 -0
  15. package/src/client/locales.ts +0 -1
  16. package/src/client/panel/AssetsTab.tsx +22 -22
  17. package/src/client/panel/AssistantTab.tsx +3 -3
  18. package/src/client/panel/BookshelfBar.tsx +1 -1
  19. package/src/client/panel/CreateBookView.tsx +10 -10
  20. package/src/client/panel/FlowGuide.tsx +83 -0
  21. package/src/client/panel/MangaRoleLibrary.tsx +810 -0
  22. package/src/client/panel/MangaWorkspace.tsx +359 -297
  23. package/src/client/panel/NovelPanel.tsx +107 -107
  24. package/src/client/panel/ReaderView.tsx +4 -4
  25. package/src/client/panel/RunPanel.tsx +8 -8
  26. package/src/client/panel/SceneLibrary.tsx +68 -67
  27. package/src/client/panel/StoryboardTab.tsx +105 -41
  28. package/src/client/panel/StyleCard.tsx +13 -13
  29. package/src/client/panel/StyleGallery.tsx +2 -2
  30. package/src/client/panel/WorldTab.tsx +5 -5
  31. package/src/client/panel/panel.module.css +861 -510
  32. package/src/client/panel/views.tsx +24 -24
  33. package/src/engine.ts +482 -51
  34. package/src/protocol.ts +125 -0
  35. package/src/routes.ts +203 -0
  36. package/src/client/panel/RoleVisualPanel.tsx +0 -485
@@ -18,7 +18,6 @@ import { RunPanel } from './RunPanel.tsx'
18
18
  import { CreateBookView } from './CreateBookView.tsx'
19
19
  import { ImportModal } from './ImportModal.tsx'
20
20
  import { WorldTab } from './WorldTab.tsx'
21
- import { StoryboardTab } from './StoryboardTab.tsx'
22
21
  import { MangaWorkspace } from './MangaWorkspace.tsx'
23
22
  import { AuditIssueRow, PlotlineCard, PlotlineHealthPanel, PlotlinePlanPanel, PlotlineSuggestionPanel, RoleCandidateRow, RoleCard, StatCell, TodoRow } from './views.tsx'
24
23
  import { extractDocxTextFromBuffer } from '../docx.ts'
@@ -87,7 +86,7 @@ const NAV_GROUPS: ReadonlyArray<{ id: string; label: string; items: ReadonlyArra
87
86
  label: '工具',
88
87
  items: [
89
88
  { id: 'assistant', label: tt('tab.assistant'), icon: <MessageSquare size={16} /> },
90
- { id: 'progress', label: 'AI进度', icon: <BarChart3 size={16} /> },
89
+ { id: 'progress', label: 'AI 进度', icon: <BarChart3 size={16} /> },
91
90
  { id: 'breakdown', label: '拆书分析', icon: <Search size={16} /> },
92
91
  { id: 'manhua', label: '漫剧工作台', icon: <Clapperboard size={16} /> },
93
92
  { id: 'run', label: '生产单', icon: <Factory size={16} /> },
@@ -226,7 +225,7 @@ function paragraphDiff(oldText: string, newText: string): DiffRow[] {
226
225
  function renderBeats(beats: string): ReactElement {
227
226
  const lines = beats.split('\n')
228
227
  return (
229
- <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
228
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-2)' }}>
230
229
  {lines.map((line, i) => {
231
230
  const trimmed = line.trim()
232
231
  const match = /^([^::]{2,14})[::]/.exec(trimmed)
@@ -382,14 +381,14 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
382
381
  setEditorFontSize(v)
383
382
  try { window.localStorage.setItem('dsh-novel-forge.editor.fontSize', String(v)) } catch { /* ignore */ }
384
383
  }
385
- /** 面板主题(localStorage 记忆):'liquid'=iOS 液态玻璃(绿) / 'classic'=经典毛玻璃(蓝) / 'neumorph'=新拟物(浅色)。 */
386
- const [panelTheme, setPanelTheme] = useState<'liquid' | 'classic' | 'neumorph'>(() => {
384
+ /** 面板主题(localStorage 记忆):'liquid'=iOS 液态玻璃(绿) / 'neumorph'=新拟物(浅色) / 'macos'=macOS 玻璃(蓝,随外观自动浅深) / 'clay'=粘土拟态。 */
385
+ const [panelTheme, setPanelTheme] = useState<'liquid' | 'neumorph' | 'macos' | 'clay'>(() => {
387
386
  try {
388
387
  const v = window.localStorage.getItem('dsh-novel-forge.theme')
389
- return v === 'classic' || v === 'neumorph' ? v : 'liquid'
388
+ return v === 'neumorph' || v === 'macos' || v === 'clay' ? v : 'liquid'
390
389
  } catch { return 'liquid' }
391
390
  })
392
- const changePanelTheme = (next: 'liquid' | 'classic' | 'neumorph'): void => {
391
+ const changePanelTheme = (next: 'liquid' | 'neumorph' | 'macos' | 'clay'): void => {
393
392
  setPanelTheme(next)
394
393
  try { window.localStorage.setItem('dsh-novel-forge.theme', next) } catch { /* ignore */ }
395
394
  }
@@ -2575,7 +2574,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
2575
2574
  <div className={css.card} style={{ borderColor: 'var(--nf-info)' }}>
2576
2575
  <div className={css.busyRow}>
2577
2576
  <span style={{ color: 'var(--nf-info)' }}>第 {draftNo} 章有未采纳的润色/修订草稿(原稿未被改动)</span>
2578
- <span style={{ display: 'flex', gap: 8 }}>
2577
+ <span style={{ display: 'flex', gap: 'var(--nf-space-8)' }}>
2579
2578
  <button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void openWorkspace(draftNo) }}>
2580
2579
  打开工作区
2581
2580
  </button>
@@ -2602,7 +2601,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
2602
2601
  )}
2603
2602
  <span className={css.cardTitle}>第 {workspace.no} 章《{workspace.title}》</span>
2604
2603
  <span className={css.meta}>{workspace.original.length} 字</span>
2605
- <span style={{ display: 'flex', gap: 4, alignItems: 'center', marginLeft: 'auto' }}>
2604
+ <span style={{ display: 'flex', gap: 'var(--nf-space-4)', alignItems: 'center', marginLeft: 'auto' }}>
2606
2605
  <button type="button" className={css.iconButton} title="减小字号" aria-label="减小字号" onClick={() => { changeEditorFontSize(editorFontSize - 1) }}>A−</button>
2607
2606
  <span className={css.meta}>{editorFontSize}px</span>
2608
2607
  <button type="button" className={css.iconButton} title="增大字号" aria-label="增大字号" onClick={() => { changeEditorFontSize(editorFontSize + 1) }}>A+</button>
@@ -2717,12 +2716,12 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
2717
2716
  </div>
2718
2717
  <div className={css.meta}><b>总评:</b>{wsCheckReport.verdict}</div>
2719
2718
  {wsCheckReport.issues.length > 0 && (
2720
- <ul style={{ margin: 0, paddingLeft: 16, display: 'flex', flexDirection: 'column', gap: 3, fontSize: editorFontSize - 1, maxHeight: '45vh', overflowY: 'auto' }}>
2719
+ <ul style={{ margin: 0, paddingLeft: 'var(--nf-space-16)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', fontSize: editorFontSize - 1, maxHeight: '45vh', overflowY: 'auto' }}>
2721
2720
  {wsCheckReport.issues.map((issue, i) => (
2722
- <li key={i} style={{ color: severityColor(issue.severity), display: 'flex', gap: 6, alignItems: 'flex-start' }}>
2721
+ <li key={i} style={{ color: severityColor(issue.severity), display: 'flex', gap: 'var(--nf-space-6)', alignItems: 'flex-start' }}>
2723
2722
  <input
2724
2723
  type="checkbox"
2725
- style={{ marginTop: 1 }}
2724
+ style={{ marginTop: 'var(--nf-space-2)' }}
2726
2725
  checked={wsChecked.includes(i)}
2727
2726
  onChange={e => {
2728
2727
  setWsChecked(prev => e.target.checked ? [...prev, i] : prev.filter(x => x !== i))
@@ -2748,7 +2747,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
2748
2747
  <div className={css.wsPreview} style={{ flex: 1, minHeight: 0 }}>
2749
2748
  <div className={css.busyRow}>
2750
2749
  <span className={css.meta}>优化预览({workspace.draft.length} 字)</span>
2751
- <span style={{ display: 'flex', gap: 8 }}>
2750
+ <span style={{ display: 'flex', gap: 'var(--nf-space-8)' }}>
2752
2751
  <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setWsShowDiff(v => !v) }}>
2753
2752
  {wsShowDiff ? '显示文本' : '查看对比'}
2754
2753
  </button>
@@ -2871,17 +2870,17 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
2871
2870
  </button>
2872
2871
  </div>
2873
2872
  {auditStatus?.status === 'running' && (
2874
- <div className={css.meta} style={{ marginTop: 8 }}>
2873
+ <div className={css.meta} style={{ marginTop: 'var(--nf-space-8)' }}>
2875
2874
  🔍 全书质检中:{auditStatus.completedBatches}/{auditStatus.totalBatches > 0 ? auditStatus.totalBatches : '…'} 批
2876
2875
  {auditStatus.totalBatches > 0 && (
2877
- <div className={css.dashJourneyBar} style={{ marginTop: 4 }}>
2876
+ <div className={css.dashJourneyBar} style={{ marginTop: 'var(--nf-space-4)' }}>
2878
2877
  <div className={css.dashJourneyFill} style={{ width: `${Math.round((auditStatus.completedBatches / auditStatus.totalBatches) * 100)}%` }} />
2879
2878
  </div>
2880
2879
  )}
2881
2880
  </div>
2882
2881
  )}
2883
2882
  {auditStatus?.status === 'error' && auditStatus.error !== undefined && (
2884
- <div className={css.meta} style={{ color: 'var(--nf-error)', marginTop: 8 }}>全书质检失败:{auditStatus.error}</div>
2883
+ <div className={css.meta} style={{ color: 'var(--nf-error)', marginTop: 'var(--nf-space-8)' }}>全书质检失败:{auditStatus.error}</div>
2885
2884
  )}
2886
2885
  <div className={css.assetGrid}>
2887
2886
  <StatCell
@@ -2913,7 +2912,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
2913
2912
  </div>
2914
2913
 
2915
2914
  {/* 剧情线进度 | 作者复盘趋势:精简摘要卡,并排(点击跳转详情页) */}
2916
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 14, alignItems: 'stretch' }}>
2915
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 'var(--nf-space-14)', alignItems: 'stretch' }}>
2917
2916
  <div className={css.card}>
2918
2917
  <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
2919
2918
  <span className={css.cardTitle}>🧵 {tt('plotlines.workflowTitle')}({(project?.plotlines ?? []).length} 条)</span>
@@ -2992,7 +2991,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
2992
2991
  </button>
2993
2992
  </div>
2994
2993
  {auditIssues.length > 0 && (
2995
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
2994
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
2996
2995
  {auditIssues.map((issue, i) => (
2997
2996
  <AuditIssueRow
2998
2997
  key={i}
@@ -3124,7 +3123,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3124
3123
  ) : (
3125
3124
  /* 只读展示:大纲是开书时的出生证明 */
3126
3125
  <>
3127
- <div className={css.meta} style={{ marginBottom: 6 }}>
3126
+ <div className={css.meta} style={{ marginBottom: 'var(--nf-space-6)' }}>
3128
3127
  大纲是本书的「出生证明」;已写章节可点右上角「🔄 反推大纲」从正文反推,或「更新大纲」手动修订。
3129
3128
  </div>
3130
3129
  <pre className={css.outlineReadonly}>{project.outline}</pre>
@@ -3187,7 +3186,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3187
3186
  </button>
3188
3187
  </div>
3189
3188
  {/* 封面 */}
3190
- <div className={css.row} style={{ alignItems: 'flex-start', gap: 14 }}>
3189
+ <div className={css.row} style={{ alignItems: 'flex-start', gap: 'var(--nf-space-14)' }}>
3191
3190
  <div className={css.coverPreview}>
3192
3191
  {coverDataUrl !== null ? (
3193
3192
  <img src={coverDataUrl} alt="封面" />
@@ -3195,7 +3194,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3195
3194
  <span className={css.coverPlaceholder}>暂无封面</span>
3196
3195
  )}
3197
3196
  </div>
3198
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
3197
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
3199
3198
  <div className={css.row}>
3200
3199
  <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy || project === null} onClick={() => { coverFileRef.current?.click() }}>
3201
3200
  📤 上传封面
@@ -3286,9 +3285,9 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3286
3285
  </div>
3287
3286
  <span className={css.meta}>{tt('sensitive.hint')}</span>
3288
3287
  {sensHits.length > 0 && (
3289
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 300, overflowY: 'auto', fontSize: 12 }}>
3288
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', maxHeight: 300, overflowY: 'auto', fontSize: 'var(--nf-fs-12)' }}>
3290
3289
  {sensHits.map((hit, i) => (
3291
- <div key={i} style={{ display: 'flex', gap: 8, alignItems: 'flex-start', border: '1px solid var(--nf-border)', borderRadius: 6, padding: '4px 8px' }}>
3290
+ <div key={i} style={{ display: 'flex', gap: 'var(--nf-space-8)', alignItems: 'flex-start', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-4) var(--nf-space-8)' }}>
3292
3291
  <span className={css.badge} style={{ borderColor: 'var(--nf-warn)', color: 'var(--nf-warn)', flex: 'none' }}>
3293
3292
  {hit.chapterNo > 0 ? `第${hit.chapterNo}章` : '文本'}
3294
3293
  </span>
@@ -3315,7 +3314,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3315
3314
  {chapters.length > 0 && (
3316
3315
  <div className={css.card}>
3317
3316
  <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
3318
- <div className={css.row} style={{ flexWrap: 'wrap', gap: 6 }}>
3317
+ <div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
3319
3318
  {(volumes !== undefined && volumes.length > 0) && (
3320
3319
  <>
3321
3320
  <button
@@ -3372,7 +3371,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3372
3371
  <span className={css.chapterNum}>{chapter.no}</span>
3373
3372
  <div className={css.chapterMain}>
3374
3373
  <div className={css.chapterTitle}>
3375
- <button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ padding: '1px 6px' }} onClick={() => { void handleToggleChapter(chapter.no) }}>
3374
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ padding: 'var(--nf-space-2) var(--nf-space-6)' }} onClick={() => { void handleToggleChapter(chapter.no) }}>
3376
3375
  {expanded ? '−' : '+'}
3377
3376
  </button>
3378
3377
  <span>{chapter.title}</span>
@@ -3383,7 +3382,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3383
3382
  </div>
3384
3383
  {!expanded && <div className={css.chapterBeats} title={chapter.beats}>{chapter.beats}</div>}
3385
3384
  {expanded && (
3386
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
3385
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
3387
3386
  <div className={css.meta}><b>{tt('plan.beats')}:</b></div>
3388
3387
  <div className={css.meta}>{renderBeats(chapter.beats)}</div>
3389
3388
  {chapter.summary !== undefined && chapter.summary !== '' && (
@@ -3400,7 +3399,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3400
3399
  </div>
3401
3400
  <div className={css.meta}><b>{tt('plan.reviewVerdict')}:</b> {review.verdict}</div>
3402
3401
  {review.issues.length > 0 && (
3403
- <ul style={{ margin: 0, paddingLeft: 18, fontSize: 12 }}>
3402
+ <ul style={{ margin: 0, paddingLeft: 'var(--nf-space-18)', fontSize: 'var(--nf-fs-12)' }}>
3404
3403
  {review.issues.map((issue, i) => (
3405
3404
  <li key={i} style={{ color: severityColor(issue.severity) }}>
3406
3405
  [{issue.severity}] {issue.item} → {issue.suggestion}
@@ -3446,7 +3445,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3446
3445
  </div>
3447
3446
  )}
3448
3447
  {(chapter.status === 'rejected' || chapter.status === 'written' || chapter.status === 'approved') && (
3449
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
3448
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
3450
3449
  <div className={css.meta} style={{ fontWeight: 600 }}>
3451
3450
  润色 / 修订 — 在右上角打开工作区:左栏原文可直接选中文字做局部修订,右栏输入指令后预览,确认后再应用(未应用不改动原稿)
3452
3451
  </div>
@@ -3527,16 +3526,16 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3527
3526
  )}
3528
3527
 
3529
3528
  {activeTab === 'book' && (
3530
- <div className={css.card} style={{ gap: 12 }}>
3529
+ <div className={css.card} style={{ gap: 'var(--nf-space-12)' }}>
3531
3530
  <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
3532
- <span className={css.cardTitle} style={{ fontSize: 17, fontWeight: 700 }}>📚 本书设定</span>
3531
+ <span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-16)', fontWeight: 700 }}>📚 本书设定</span>
3533
3532
  <span className={css.meta}>当前书的知识与资料(参考 AI-Novel-Writing-Assistant:世界观/角色准备收进书内)</span>
3534
3533
  </div>
3535
- <div className={css.row} style={{ flexWrap: 'wrap', gap: 6 }}>
3534
+ <div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
3536
3535
  <button
3537
3536
  type="button"
3538
3537
  className={`${css.button} ${bookTab === 'bible' ? css.buttonPrimary : ''}`}
3539
- style={{ fontSize: 14, flex: 1 }}
3538
+ style={{ fontSize: 'var(--nf-fs-14)', flex: 1 }}
3540
3539
  onClick={() => { changeBookTab('bible') }}
3541
3540
  title="设定库:题材 / 世界观规则 / 人物摘要 / 红线 / 文风"
3542
3541
  >
@@ -3545,7 +3544,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3545
3544
  <button
3546
3545
  type="button"
3547
3546
  className={`${css.button} ${bookTab === 'world' ? css.buttonPrimary : ''}`}
3548
- style={{ fontSize: 14, flex: 1 }}
3547
+ style={{ fontSize: 'var(--nf-fs-14)', flex: 1 }}
3549
3548
  onClick={() => { changeBookTab('world') }}
3550
3549
  title="大世界:境界体系 / 地理区域 / 势力分布(注入生成与审稿提示词)"
3551
3550
  >
@@ -3554,7 +3553,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3554
3553
  <button
3555
3554
  type="button"
3556
3555
  className={`${css.button} ${bookTab === 'roles' ? css.buttonPrimary : ''}`}
3557
- style={{ fontSize: 14, flex: 1 }}
3556
+ style={{ fontSize: 'var(--nf-fs-14)', flex: 1 }}
3558
3557
  onClick={() => { changeBookTab('roles') }}
3559
3558
  title="角色库:全书角色主表(定位 / 关系网 / 成长线 / 知情度)"
3560
3559
  >
@@ -3563,7 +3562,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3563
3562
  <button
3564
3563
  type="button"
3565
3564
  className={`${css.button} ${bookTab === 'facts' ? css.buttonPrimary : ''}`}
3566
- style={{ fontSize: 14, flex: 1 }}
3565
+ style={{ fontSize: 'var(--nf-fs-14)', flex: 1 }}
3567
3566
  onClick={() => { changeBookTab('facts') }}
3568
3567
  title="编年录与复盘记录"
3569
3568
  >
@@ -3584,13 +3583,13 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3584
3583
  {bible === undefined ? (
3585
3584
  <span className={css.meta}>{tt('bible.none')}</span>
3586
3585
  ) : (
3587
- <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
3586
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-10)' }}>
3588
3587
  {bible.genre !== '' && (
3589
3588
  <div><b>{tt('bible.genre')}:</b> <span className={css.meta}>{bible.genre}</span></div>
3590
3589
  )}
3591
3590
  {/* 世界观规则独立卡片(可编辑,每行一条) */}
3592
3591
  {bible.worldRules.length > 0 && (
3593
- <div style={{ border: '1px solid var(--nf-border)', borderRadius: 10, padding: '8px 10px' }}>
3592
+ <div style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)' }}>
3594
3593
  <div className={css.row} style={{ justifyContent: 'space-between' }}>
3595
3594
  <b>{tt('bible.worldRules')}({bible.worldRules.length})</b>
3596
3595
  <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setWorldRulesDraft(bible.worldRules.join('\n')) }}>
@@ -3598,7 +3597,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3598
3597
  </button>
3599
3598
  </div>
3600
3599
  {worldRulesDraft !== '' && (
3601
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 }}>
3600
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', marginTop: 'var(--nf-space-6)' }}>
3602
3601
  <textarea
3603
3602
  className={css.textarea}
3604
3603
  style={{ minHeight: 120 }}
@@ -3617,7 +3616,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3617
3616
  </div>
3618
3617
  )}
3619
3618
  {worldRulesDraft === '' && (
3620
- <ul style={{ margin: '4px 0 0', paddingLeft: 18, fontSize: 12 }}>{bible.worldRules.map((r, i) => <li key={i}>{r}</li>)}</ul>
3619
+ <ul style={{ margin: '4px 0 0', paddingLeft: 'var(--nf-space-18)', fontSize: 'var(--nf-fs-12)' }}>{bible.worldRules.map((r, i) => <li key={i}>{r}</li>)}</ul>
3621
3620
  )}
3622
3621
  </div>
3623
3622
  )}
@@ -3625,7 +3624,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3625
3624
  <div>
3626
3625
  <b>{tt('bible.characters')}({bible.characters.length})</b>
3627
3626
  {bible.characters.map(card => (
3628
- <div key={card.name} style={{ marginTop: 4, fontSize: 12 }}>
3627
+ <div key={card.name} style={{ marginTop: 'var(--nf-space-4)', fontSize: 'var(--nf-fs-12)' }}>
3629
3628
  <b>{card.name}</b> <span className={css.meta}>[{card.role}] {card.traits.join('、')}</span>
3630
3629
  {card.goals !== '' && <div className={css.meta}>目标:{card.goals}</div>}
3631
3630
  {card.relations !== '' && <div className={css.meta}>关系:{card.relations}</div>}
@@ -3636,13 +3635,13 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3636
3635
  {bible.redLines.length > 0 && (
3637
3636
  <div>
3638
3637
  <b>{tt('bible.redLines')}({bible.redLines.length})</b>
3639
- <ul style={{ margin: 0, paddingLeft: 18, fontSize: 12, color: 'var(--nf-error)' }}>{bible.redLines.map((r, i) => <li key={i}>{r}</li>)}</ul>
3638
+ <ul style={{ margin: 0, paddingLeft: 'var(--nf-space-18)', fontSize: 'var(--nf-fs-12)', color: 'var(--nf-error)' }}>{bible.redLines.map((r, i) => <li key={i}>{r}</li>)}</ul>
3640
3639
  </div>
3641
3640
  )}
3642
3641
  {bible.style.length > 0 && (
3643
3642
  <div>
3644
3643
  <b>{tt('bible.style')}({bible.style.length})</b>
3645
- <ul style={{ margin: 0, paddingLeft: 18, fontSize: 12 }}>{bible.style.map((r, i) => <li key={i}>{r}</li>)}</ul>
3644
+ <ul style={{ margin: 0, paddingLeft: 'var(--nf-space-18)', fontSize: 'var(--nf-fs-12)' }}>{bible.style.map((r, i) => <li key={i}>{r}</li>)}</ul>
3646
3645
  </div>
3647
3646
  )}
3648
3647
  </div>
@@ -3692,7 +3691,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3692
3691
  <span className={css.meta}>角色库是全书角色主表:定位(女主/女配/配角/反派)、身份、关系网、成长线、知情度——生成与审稿都会按定位规格刻画互动。点「从编年录刷新状态」可在每张卡上显示角色当前状态。</span>
3693
3692
 
3694
3693
  {roleCandidates !== null && (
3695
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, border: '1px solid var(--nf-info)', borderRadius: 12, padding: 10 }}>
3694
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', border: '1px solid var(--nf-info)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-10)' }}>
3696
3695
  <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
3697
3696
  <b>✨ AI 提炼候选({roleCandidates.length})</b>
3698
3697
  <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRoleCandidates(null) }}>收起</button>
@@ -3711,7 +3710,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3711
3710
  )}
3712
3711
 
3713
3712
  {roleDraft !== null && (
3714
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8, border: '1px solid var(--nf-accent)', borderRadius: 12, padding: 10 }}>
3713
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-10)' }}>
3715
3714
  <div className={css.row} style={{ flexWrap: 'wrap' }}>
3716
3715
  <div className={css.field} style={{ flex: 1, minWidth: 140 }}>
3717
3716
  <label className={css.fieldLabel}>角色名</label>
@@ -3761,7 +3760,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3761
3760
  {(project?.roles ?? []).length === 0 ? (
3762
3761
  <span className={css.meta}>角色库为空——点「✨ 从全书提炼角色」自动建立,或手动新增。</span>
3763
3762
  ) : (
3764
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
3763
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
3765
3764
  {(project?.roles ?? []).map(r => (
3766
3765
  <RoleCard
3767
3766
  key={r.name}
@@ -3783,18 +3782,18 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3783
3782
  {activeTab === 'settings' && configDraft !== null && (
3784
3783
  <>
3785
3784
  {/* 设置页内子导航(大容器) */}
3786
- <div className={`${css.card} ${css.settingsCard}`} style={{ padding: '20px 22px', gap: 14 }}>
3787
- <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
3788
- <span className={css.cardTitle} style={{ fontSize: 20, fontWeight: 700 }}>⚙️ 设置</span>
3785
+ <div className={`${css.card} ${css.settingsCard}`}>
3786
+ <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', alignItems: 'center', gap: 'var(--nf-space-8)' }}>
3787
+ <span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-20)', fontWeight: 700 }}>⚙️ 设置</span>
3789
3788
  <span className={css.meta}>当前:{config?.provider} / {config?.model} · {config?.outputDir}</span>
3790
3789
  </div>
3791
- <div className={css.row} style={{ gap: 8, flexWrap: 'wrap' }}>
3790
+ <div className={css.row} style={{ gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
3792
3791
  {SETTINGS_SECTIONS.map(s => (
3793
3792
  <button
3794
3793
  key={s.id}
3795
3794
  type="button"
3796
3795
  className={`${css.button} ${settingsTab === s.id ? css.buttonPrimary : ''}`}
3797
- style={{ fontSize: 14, padding: '8px 12px', flex: 1, minWidth: 104, justifyContent: 'center' }}
3796
+ style={{ fontSize: 'var(--nf-fs-14)', padding: 'var(--nf-space-8) var(--nf-space-12)', flex: 1, minWidth: 104, justifyContent: 'center' }}
3798
3797
  onClick={() => { changeSettingsTab(s.id) }}
3799
3798
  >
3800
3799
  {s.icon} {s.label}
@@ -3804,9 +3803,9 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3804
3803
  </div>
3805
3804
 
3806
3805
  {settingsTab === 'model' && (
3807
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 26 }}>
3806
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-24)' }}>
3808
3807
  <span className={css.cardTitle}><Brain size={18} style={{ verticalAlign: -3 }} /> 模型与推理</span>
3809
- <div style={{ display: 'flex', flexDirection: 'column', gap: 26 }}>
3808
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-24)' }}>
3810
3809
  <div className={css.field}>
3811
3810
  <label className={css.fieldLabel}>{tt('settings.provider')}</label>
3812
3811
  <input className={css.input} value={configDraft.provider} onChange={e => { setConfigDraft({ ...configDraft, provider: e.target.value }) }} />
@@ -3832,7 +3831,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3832
3831
  {modelCustomMode && (
3833
3832
  <input
3834
3833
  className={css.input}
3835
- style={{ marginTop: 6 }}
3834
+ style={{ marginTop: 'var(--nf-space-6)' }}
3836
3835
  value={configDraft.model}
3837
3836
  placeholder={tt('settings.modelCustomPlaceholder')}
3838
3837
  onChange={e => { setConfigDraft({ ...configDraft, model: e.target.value }) }}
@@ -3866,9 +3865,9 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3866
3865
  )}
3867
3866
 
3868
3867
  {settingsTab === 'writing' && (
3869
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 26 }}>
3868
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-24)' }}>
3870
3869
  <span className={css.cardTitle}><PenLine size={18} style={{ verticalAlign: -3 }} /> 写作与审稿</span>
3871
- <div style={{ display: 'flex', flexDirection: 'column', gap: 26 }}>
3870
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-24)' }}>
3872
3871
  <div className={css.field}>
3873
3872
  <label className={css.fieldLabel}>{tt('settings.chapterChars')}</label>
3874
3873
  <input className={css.input} type="number" min={1000} max={20000} value={configDraft.chapterChars} onChange={e => { setConfigDraft({ ...configDraft, chapterChars: Number(e.target.value) }) }} />
@@ -3935,10 +3934,10 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3935
3934
  )}
3936
3935
 
3937
3936
  {settingsTab === 'image' && (
3938
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 24 }}>
3937
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-24)' }}>
3939
3938
  <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', alignItems: 'center' }}>
3940
3939
  <span className={css.cardTitle}><Palette size={18} style={{ verticalAlign: -3 }} /> 生图模型</span>
3941
- <span className={css.row} style={{ gap: 8, alignItems: 'center' }}>
3940
+ <span className={css.row} style={{ gap: 'var(--nf-space-8)', alignItems: 'center' }}>
3942
3941
  <span className={css.meta}>启用生图功能</span>
3943
3942
  <button
3944
3943
  type="button"
@@ -3952,9 +3951,9 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3952
3951
  </span>
3953
3952
  </div>
3954
3953
  <span className={css.meta}>支持多套 OpenAI 兼容生图接口(豆包 / 即梦 / 其他平台);启用一条作为生成模型,保存后角色详情显示「AI 生图」按钮。</span>
3955
- <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
3954
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-20)' }}>
3956
3955
  {(configDraft.imageModels ?? []).map(m => (
3957
- <div key={m.id} style={{ border: m.enabled ? '1px solid var(--nf-accent)' : '1px solid var(--nf-border)', borderRadius: 12, padding: 14, display: 'flex', flexDirection: 'column', gap: 12, background: m.enabled ? 'var(--nf-accent-soft)' : 'transparent' }}>
3956
+ <div key={m.id} style={{ border: m.enabled ? '1px solid var(--nf-accent)' : '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-14)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-12)', background: m.enabled ? 'var(--nf-accent-soft)' : 'transparent' }}>
3958
3957
  <div className={css.row} style={{ alignItems: 'flex-end', flexWrap: 'wrap' }}>
3959
3958
  <div className={css.field} style={{ flex: 1, minWidth: 180 }}>
3960
3959
  <label className={css.fieldLabel}>名称</label>
@@ -3973,7 +3972,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3973
3972
  <label className={css.fieldLabel}>API Key</label>
3974
3973
  <input className={css.input} type="password" placeholder="ark-..." value={m.apiKey} onChange={e => { updateImageModel(m.id, { apiKey: e.target.value }) }} />
3975
3974
  </div>
3976
- <div className={css.row} style={{ gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
3975
+ <div className={css.row} style={{ gap: 'var(--nf-space-8)', flexWrap: 'wrap', alignItems: 'center' }}>
3977
3976
  <button type="button" className={`${css.button} ${css.buttonSmall} ${m.enabled ? css.buttonPrimary : ''}`} onClick={() => { enableImageModel(m.id) }}>
3978
3977
  {m.enabled ? '✓ 已启用' : '设为启用'}
3979
3978
  </button>
@@ -3986,8 +3985,8 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3986
3985
  const st = imageTestState[m.id]
3987
3986
  if (st === undefined || st.testing) return null
3988
3987
  return st.ok === true
3989
- ? <span style={{ color: 'var(--nf-success)', fontSize: 12, fontWeight: 600 }}>✓ 连通 · {st.ms}ms{st.modelFound === true ? '' : st.modelFound === false ? '(模型未在列表)' : ''}</span>
3990
- : <span style={{ color: 'var(--nf-error)', fontSize: 12 }}>✗ {st.message ?? '失败'}</span>
3988
+ ? <span style={{ color: 'var(--nf-success)', fontSize: 'var(--nf-fs-12)', fontWeight: 600 }}>✓ 连通 · {st.ms}ms{st.modelFound === true ? '' : st.modelFound === false ? '(模型未在列表)' : ''}</span>
3989
+ : <span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>✗ {st.message ?? '失败'}</span>
3991
3990
  })()}
3992
3991
  </div>
3993
3992
  </div>
@@ -4003,7 +4002,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4003
4002
  )}
4004
4003
 
4005
4004
  {settingsTab === 'files' && (
4006
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 24 }}>
4005
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-24)' }}>
4007
4006
  <span className={css.cardTitle}><Folder size={18} style={{ verticalAlign: -3 }} /> 路径与文件</span>
4008
4007
  <div className={css.field}>
4009
4008
  <label className={css.fieldLabel}>{tt('settings.outlinePath')}</label>
@@ -4019,7 +4018,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4019
4018
 
4020
4019
  <div style={{ borderTop: '1px solid var(--nf-border)', margin: '14px 0 12px' }} />
4021
4020
 
4022
- <span className={css.cardTitle} style={{ fontSize: 13 }}>💾 保存与导出</span>
4021
+ <span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-14)' }}>💾 保存与导出</span>
4023
4022
  <div className={css.row}>
4024
4023
  <button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleSaveConfig() }}>
4025
4024
  {tt('settings.save')}
@@ -4040,10 +4039,10 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4040
4039
  <div className={`${css.card} ${css.settingsCard}`}>
4041
4040
  <span className={css.cardTitle}><Sparkles size={18} style={{ verticalAlign: -3 }} /> 外观与主题</span>
4042
4041
  <span className={css.meta}>选择适合长时间创作的界面颜色和显示密度。主题只保存在当前设备,不影响小说内容和任务状态。</span>
4043
- <div style={{ display: 'flex', flexDirection: 'column', gap: 26 }}>
4042
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-24)' }}>
4044
4043
  <div className={css.field}>
4045
- <label className={css.fieldLabel} style={{ fontSize: 13 }}>显示模式</label>
4046
- <select className={css.input} style={{ padding: '10px 12px', fontSize: 14 }} value={themeMode} onChange={e => { changeThemeMode(e.target.value as 'system' | 'light' | 'dark') }}>
4044
+ <label className={css.fieldLabel} style={{ fontSize: 'var(--nf-fs-14)' }}>显示模式</label>
4045
+ <select className={css.input} style={{ padding: 'var(--nf-space-10) var(--nf-space-12)', fontSize: 'var(--nf-fs-14)' }} value={themeMode} onChange={e => { changeThemeMode(e.target.value as 'system' | 'light' | 'dark') }}>
4047
4046
  <option value="system">跟随系统</option>
4048
4047
  <option value="light">浅色</option>
4049
4048
  <option value="dark">深色</option>
@@ -4051,17 +4050,18 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4051
4050
  <span className={css.meta}>强制浅色/深色只作用于小说工坊面板</span>
4052
4051
  </div>
4053
4052
  <div className={css.field}>
4054
- <label className={css.fieldLabel} style={{ fontSize: 13 }}>主题风格</label>
4055
- <select className={css.input} style={{ padding: '10px 12px', fontSize: 14 }} value={panelTheme} onChange={e => { changePanelTheme(e.target.value as 'liquid' | 'classic' | 'neumorph') }}>
4053
+ <label className={css.fieldLabel} style={{ fontSize: 'var(--nf-fs-14)' }}>主题风格</label>
4054
+ <select className={css.input} style={{ padding: 'var(--nf-space-10) var(--nf-space-12)', fontSize: 'var(--nf-fs-14)' }} value={panelTheme} onChange={e => { changePanelTheme(e.target.value as 'liquid' | 'neumorph' | 'macos' | 'clay') }}>
4056
4055
  <option value="liquid">液态玻璃 · 清新绿(默认)</option>
4057
- <option value="classic">经典毛玻璃 · 沉静蓝</option>
4058
4056
  <option value="neumorph">新拟物 · 柔和浅色</option>
4057
+ <option value="macos">macOS · 玻璃(蓝,随外观浅/深)</option>
4058
+ <option value="clay">粘土拟态 · 柔和黏土</option>
4059
4059
  </select>
4060
4060
  <span className={css.meta}>{tt('settings.themeHint')}</span>
4061
4061
  </div>
4062
4062
  <div className={css.field}>
4063
- <label className={css.fieldLabel} style={{ fontSize: 13 }}>界面密度</label>
4064
- <select className={css.input} style={{ padding: '10px 12px', fontSize: 14 }} value={themeDensity} onChange={e => { changeThemeDensity(e.target.value as 'comfort' | 'compact' | 'spacious') }}>
4063
+ <label className={css.fieldLabel} style={{ fontSize: 'var(--nf-fs-14)' }}>界面密度</label>
4064
+ <select className={css.input} style={{ padding: 'var(--nf-space-10) var(--nf-space-12)', fontSize: 'var(--nf-fs-14)' }} value={themeDensity} onChange={e => { changeThemeDensity(e.target.value as 'comfort' | 'compact' | 'spacious') }}>
4065
4065
  <option value="comfort">舒适(默认)</option>
4066
4066
  <option value="compact">紧凑</option>
4067
4067
  <option value="spacious">宽松</option>
@@ -4070,7 +4070,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4070
4070
  </div>
4071
4071
  </div>
4072
4072
  <div className={css.row} style={{ justifyContent: 'flex-end' }}>
4073
- <button type="button" className={css.button} style={{ padding: '9px 18px', fontSize: 13.5 }} onClick={resetTheme}>
4073
+ <button type="button" className={css.button} style={{ padding: 'var(--nf-space-10) var(--nf-space-18)', fontSize: 'var(--nf-fs-14)' }} onClick={resetTheme}>
4074
4074
  <RotateCcw size={14} style={{ verticalAlign: -2 }} /> 恢复默认主题
4075
4075
  </button>
4076
4076
  </div>
@@ -4081,13 +4081,13 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4081
4081
  {activeTab === 'book' && bookTab === 'facts' && (
4082
4082
  <div className={css.card}>
4083
4083
  <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
4084
- <span className={css.cardTitle} style={{ fontSize: 17, fontWeight: 700 }}>📚 编年 / 复盘</span>
4084
+ <span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-16)', fontWeight: 700 }}>📚 编年 / 复盘</span>
4085
4085
  </div>
4086
- <div className={css.row} style={{ flexWrap: 'wrap', gap: 6 }}>
4086
+ <div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
4087
4087
  <button
4088
4088
  type="button"
4089
4089
  className={`${css.button} ${archiveTab === 'facts' ? css.buttonPrimary : ''}`}
4090
- style={{ fontSize: 14, flex: 1 }}
4090
+ style={{ fontSize: 'var(--nf-fs-14)', flex: 1 }}
4091
4091
  onClick={() => { changeArchiveTab('facts') }}
4092
4092
  title="编年录:客观事实流水(第 N 章 · 事件),生成/审稿查证用"
4093
4093
  >
@@ -4096,7 +4096,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4096
4096
  <button
4097
4097
  type="button"
4098
4098
  className={`${css.button} ${archiveTab === 'reviews' ? css.buttonPrimary : ''}`}
4099
- style={{ fontSize: 14, flex: 1 }}
4099
+ style={{ fontSize: 'var(--nf-fs-14)', flex: 1 }}
4100
4100
  onClick={() => { changeArchiveTab('reviews') }}
4101
4101
  title="复盘记录:每章作者复盘(钩子 / 推进 / 衔接 / 趋势)"
4102
4102
  >
@@ -4138,12 +4138,12 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4138
4138
  </span>
4139
4139
  )
4140
4140
  })()}
4141
- <div style={{ display: 'flex', flexDirection: 'column', gap: 10, overflowY: 'auto', flex: 1, minHeight: 0 }}>
4141
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-10)', overflowY: 'auto', flex: 1, minHeight: 0 }}>
4142
4142
  {(() => {
4143
4143
  const groupsWithReviews = chapterGroups.filter(group => group.chapters.some(c => c.authorReview !== undefined))
4144
4144
  if (groupsWithReviews.length === 0) return null
4145
4145
  return (
4146
- <div className={css.row} style={{ justifyContent: 'flex-end', gap: 6 }}>
4146
+ <div className={css.row} style={{ justifyContent: 'flex-end', gap: 'var(--nf-space-6)' }}>
4147
4147
  <button
4148
4148
  type="button"
4149
4149
  className={`${css.button} ${css.buttonSmall}`}
@@ -4166,10 +4166,10 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4166
4166
  if (groupReviewed.length === 0) return null
4167
4167
  const volumeExpanded = expandedVolumes[group.no] === true
4168
4168
  return (
4169
- <div key={group.no} style={{ border: '1px solid var(--nf-border)', borderRadius: 10, overflow: 'hidden' }}>
4169
+ <div key={group.no} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', overflow: 'hidden' }}>
4170
4170
  <button
4171
4171
  type="button"
4172
- style={{ width: '100%', textAlign: 'left', border: 'none', background: 'transparent', cursor: 'pointer', padding: '8px 12px', fontSize: 13, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}
4172
+ style={{ width: '100%', textAlign: 'left', border: 'none', background: 'transparent', cursor: 'pointer', padding: 'var(--nf-space-8) var(--nf-space-12)', fontSize: 'var(--nf-fs-14)', display: 'flex', alignItems: 'center', gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}
4173
4173
  onClick={() => { setExpandedVolumes(prev => ({ ...prev, [group.no]: !volumeExpanded })) }}
4174
4174
  >
4175
4175
  <span style={{ color: 'var(--nf-text-3)' }}>{volumeExpanded ? '▾' : '▸'}</span>
@@ -4177,25 +4177,25 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4177
4177
  <span className={css.meta}>已复盘 {groupReviewed.length}/{group.chapters.length} 章</span>
4178
4178
  </button>
4179
4179
  {volumeExpanded && (
4180
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '4px 8px 8px', borderTop: '1px solid var(--nf-border)' }}>
4180
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', padding: 'var(--nf-space-4) var(--nf-space-8) var(--nf-space-8)', borderTop: '1px solid var(--nf-border)' }}>
4181
4181
  {[...groupReviewed].reverse().map(c => {
4182
4182
  const ar = c.authorReview!
4183
4183
  const expanded = expandedReviewChapter === c.no
4184
4184
  return (
4185
- <div key={c.no} style={{ border: '1px solid var(--nf-border)', borderRadius: 8, overflow: 'hidden' }}>
4185
+ <div key={c.no} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-8)', overflow: 'hidden' }}>
4186
4186
  <button
4187
4187
  type="button"
4188
- style={{ width: '100%', textAlign: 'left', border: 'none', background: 'transparent', cursor: 'pointer', padding: '6px 10px', fontSize: 12, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}
4188
+ style={{ width: '100%', textAlign: 'left', border: 'none', background: 'transparent', cursor: 'pointer', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)', display: 'flex', alignItems: 'center', gap: 'var(--nf-space-6)', flexWrap: 'wrap' }}
4189
4189
  onClick={() => { setExpandedReviewChapter(expanded ? null : c.no) }}
4190
4190
  >
4191
4191
  <span style={{ color: 'var(--nf-text-3)' }}>{expanded ? '▾' : '▸'}</span>
4192
4192
  <b>第{c.no}章《{c.title}》</b>
4193
4193
  <span style={{ color: ar.hookHonored ? 'var(--nf-success)' : 'var(--nf-warn)' }}>钩子{ar.hookHonored ? '✓' : '✗'}</span>
4194
4194
  <span style={{ color: ar.endingHook >= 6 ? 'var(--nf-success)' : 'var(--nf-error)' }}>结尾钩子 {ar.endingHook}/10</span>
4195
- {ar.plotlineProgress !== '' && <span className={css.meta} style={{ marginLeft: 4 }}>{ar.plotlineProgress.slice(0, 40)}{ar.plotlineProgress.length > 40 ? '…' : ''}</span>}
4195
+ {ar.plotlineProgress !== '' && <span className={css.meta} style={{ marginLeft: 'var(--nf-space-4)' }}>{ar.plotlineProgress.slice(0, 40)}{ar.plotlineProgress.length > 40 ? '…' : ''}</span>}
4196
4196
  </button>
4197
4197
  {expanded && (
4198
- <div style={{ padding: '4px 10px 8px', fontSize: 12, display: 'flex', flexDirection: 'column', gap: 3, borderTop: '1px solid var(--nf-border)' }}>
4198
+ <div style={{ padding: 'var(--nf-space-4) var(--nf-space-10) var(--nf-space-8)', fontSize: 'var(--nf-fs-12)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', borderTop: '1px solid var(--nf-border)' }}>
4199
4199
  {ar.hookNote !== '' && <span className={css.meta}><b>钩子:</b>{ar.hookNote}</span>}
4200
4200
  {ar.plotlineProgress !== '' && <span className={css.meta}><b>推进:</b>{ar.plotlineProgress}</span>}
4201
4201
  {ar.continuity !== '' && <span className={css.meta}><b>衔接:</b>{ar.continuity}</span>}
@@ -4228,10 +4228,10 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4228
4228
  </div>
4229
4229
  <span className={css.meta}>{tt('facts.hint')}</span>
4230
4230
  {(project?.facts ?? []).length > 0 ? (
4231
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: '60vh', overflowY: 'auto', fontSize: 12 }}>
4231
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', maxHeight: '60vh', overflowY: 'auto', fontSize: 'var(--nf-fs-12)' }}>
4232
4232
  {[...(project?.facts ?? [])].reverse().map((fact, i) => (
4233
- <div key={i} style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
4234
- <span className={css.badge} style={{ borderColor: 'var(--nf-text-3)', color: 'var(--nf-text-3)', flex: 'none', marginTop: 1 }}>
4233
+ <div key={i} style={{ display: 'flex', gap: 'var(--nf-space-8)', alignItems: 'flex-start' }}>
4234
+ <span className={css.badge} style={{ borderColor: 'var(--nf-text-3)', color: 'var(--nf-text-3)', flex: 'none', marginTop: 'var(--nf-space-2)' }}>
4235
4235
  第 {fact.chapterNo} 章
4236
4236
  </span>
4237
4237
  <span className={css.meta}>{fact.text}</span>
@@ -4249,13 +4249,13 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4249
4249
  {activeTab === 'plotlines' && (
4250
4250
  <div className={css.card}>
4251
4251
  <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
4252
- <span className={css.cardTitle} style={{ fontSize: 17, fontWeight: 700 }}>📜 长线管理</span>
4252
+ <span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-16)', fontWeight: 700 }}>📜 长线管理</span>
4253
4253
  </div>
4254
- <div className={css.row} style={{ flexWrap: 'wrap', gap: 6 }}>
4254
+ <div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
4255
4255
  <button
4256
4256
  type="button"
4257
4257
  className={`${css.button} ${longlineTab === 'plotlines' ? css.buttonPrimary : ''}`}
4258
- style={{ fontSize: 14, flex: 1 }}
4258
+ style={{ fontSize: 'var(--nf-fs-14)', flex: 1 }}
4259
4259
  onClick={() => { changeLonglineTab('plotlines') }}
4260
4260
  title="剧情线:故事明线(主线 / 支线 / 人物 / 悬念)"
4261
4261
  >
@@ -4264,7 +4264,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4264
4264
  <button
4265
4265
  type="button"
4266
4266
  className={`${css.button} ${longlineTab === 'foreshadow' ? css.buttonPrimary : ''}`}
4267
- style={{ fontSize: 14, flex: 1 }}
4267
+ style={{ fontSize: 'var(--nf-fs-14)', flex: 1 }}
4268
4268
  onClick={() => { changeLonglineTab('foreshadow') }}
4269
4269
  title="伏笔:道具 / 事件级暗线(埋设 → 回收)"
4270
4270
  >
@@ -4300,7 +4300,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4300
4300
  </div>
4301
4301
  </div>
4302
4302
  <span className={css.badge} style={{ borderColor: statusColor, color: statusColor }}>{statusLabel}</span>
4303
- <div className={css.row} style={{ gap: 4 }}>
4303
+ <div className={css.row} style={{ gap: 'var(--nf-space-4)' }}>
4304
4304
  {f.status === 'planned' && (
4305
4305
  <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => {
4306
4306
  void api.foreshadow({ id: f.id, status: 'planted', plantedChapter: doneCount + 1 }).then(r => setProject(prev => prev === null ? prev : { ...prev, foreshadows: r.foreshadows }))
@@ -4395,7 +4395,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4395
4395
  )}
4396
4396
 
4397
4397
  {plotlineDraft !== null && (
4398
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8, border: '1px solid var(--nf-accent)', borderRadius: 12, padding: 10 }}>
4398
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-10)' }}>
4399
4399
  <div className={css.row} style={{ flexWrap: 'wrap' }}>
4400
4400
  <div className={css.field} style={{ flex: 2, minWidth: 160 }}>
4401
4401
  <label className={css.fieldLabel}>{tt('plotlines.name')}</label>
@@ -4442,7 +4442,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4442
4442
  {(project?.plotlines ?? []).length === 0 ? (
4443
4443
  <span className={css.meta}>{tt('plotlines.empty')}</span>
4444
4444
  ) : (
4445
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8, overflowY: 'auto', flex: 1, minHeight: 0 }}>
4445
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', overflowY: 'auto', flex: 1, minHeight: 0 }}>
4446
4446
  {(project?.plotlines ?? []).map(line => (
4447
4447
  <PlotlineCard
4448
4448
  key={line.id}
@@ -4510,7 +4510,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4510
4510
  <span className={css.meta}>选择范围与档位,点「开始拆书」对已写章节做整卷体检</span>
4511
4511
  </div>
4512
4512
  ) : (
4513
- <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
4513
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-10)' }}>
4514
4514
  <span className={css.meta}>
4515
4515
  分析 {breakdownResult.chaptersScanned} 章 · {breakdownResult.sections.length} 个小节 · 约 {breakdownResult.usedTokens} token
4516
4516
  </span>
@@ -4520,14 +4520,14 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4520
4520
  <b>{sec.title}</b>
4521
4521
  </div>
4522
4522
  {Object.keys(sec.structured).length > 0 && (
4523
- <div style={{ display: 'flex', flexDirection: 'column', gap: 2, fontSize: 12, marginBottom: 6 }}>
4523
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-2)', fontSize: 'var(--nf-fs-12)', marginBottom: 'var(--nf-space-6)' }}>
4524
4524
  {Object.entries(sec.structured).map(([k, v]) => {
4525
4525
  const label = (typeof v === 'string') ? v : Array.isArray(v) ? (v as string[]).join('、') : JSON.stringify(v)
4526
4526
  return <div key={k} className={css.meta}><b>{k}:</b>{label}</div>
4527
4527
  })}
4528
4528
  </div>
4529
4529
  )}
4530
- <div className={css.meta} style={{ whiteSpace: 'pre-wrap', fontSize: 12 }}>{sec.markdown}</div>
4530
+ <div className={css.meta} style={{ whiteSpace: 'pre-wrap', fontSize: 'var(--nf-fs-12)' }}>{sec.markdown}</div>
4531
4531
  </div>
4532
4532
  ))}
4533
4533
  </div>
@@ -4585,11 +4585,11 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4585
4585
  }}
4586
4586
  >
4587
4587
  <span>
4588
- 📊 AI进度
4588
+ 📊 AI 进度
4589
4589
  {busy && <span style={{ color: 'var(--nf-accent)' }}> 🟢 任务进行中</span>}
4590
4590
  <span className={css.meta}>(拖动标题栏移动 · 右下角拉大小)</span>
4591
4591
  </span>
4592
- <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
4592
+ <span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-4)' }}>
4593
4593
  {progress.length > 0 && (
4594
4594
  <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setProgress([]) }} title="清空活动记录">
4595
4595
  清空
@@ -4598,11 +4598,11 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4598
4598
  <button type="button" className={css.iconButton} title="关闭" aria-label="关闭AI进度" onClick={() => { setProgressOpen(false) }}>×</button>
4599
4599
  </span>
4600
4600
  </div>
4601
- <div className={css.assistantFloatBody} style={{ padding: 10, display: 'flex', flexDirection: 'column', gap: 8, overflow: 'hidden' }}>
4601
+ <div className={css.assistantFloatBody} style={{ padding: 'var(--nf-space-10)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', overflow: 'hidden' }}>
4602
4602
  {/* 当前任务大进度条:进行中显示 */}
4603
4603
  {(busy && (busyLabel !== '' || liveBar !== null)) && (
4604
- <div style={{ border: '1px solid var(--nf-accent)', borderRadius: 10, padding: '8px 12px', display: 'flex', flexDirection: 'column', gap: 5, background: 'color-mix(in srgb, var(--nf-accent) 6%, transparent)' }}>
4605
- <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--nf-accent)' }}>
4604
+ <div style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-12)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', background: 'color-mix(in srgb, var(--nf-accent) 6%, transparent)' }}>
4605
+ <span style={{ fontSize: 'var(--nf-fs-12)', fontWeight: 600, color: 'var(--nf-accent)' }}>
4606
4606
  ✍ {busyLabel !== '' ? busyLabel : (liveBar?.text ?? '任务进行中')}…
4607
4607
  </span>
4608
4608
  {liveBar?.ratio !== undefined && (
@@ -4614,10 +4614,10 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4614
4614
  </div>
4615
4615
  )}
4616
4616
  {/* 活动记录列表(标题行已并入窗口标题栏,清空按钮在右上角) */}
4617
- <div className={css.progress} style={{ flex: 1, minHeight: 0, overflowY: 'auto', border: '1px solid var(--nf-border)', borderRadius: 10, background: 'var(--nf-bg-inset)', padding: 8, display: 'flex', flexDirection: 'column' }}>
4617
+ <div className={css.progress} style={{ flex: 1, minHeight: 0, overflowY: 'auto', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', background: 'var(--nf-bg-inset)', padding: 'var(--nf-space-8)', display: 'flex', flexDirection: 'column' }}>
4618
4618
  {progress.length === 0 ? (
4619
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4, color: 'var(--nf-text-3)', fontSize: 12, minHeight: 120 }}>
4620
- <span style={{ fontSize: 22 }}>📭</span>
4619
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 'var(--nf-space-4)', color: 'var(--nf-text-3)', fontSize: 'var(--nf-fs-12)', minHeight: 120 }}>
4620
+ <span style={{ fontSize: 'var(--nf-fs-22)' }}>📭</span>
4621
4621
  <span>暂无活动记录</span>
4622
4622
  <span>生成、审稿等操作会显示在这里</span>
4623
4623
  </div>