dsh-novel-write-engine 0.2.8 → 0.2.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.
package/lib/client.js CHANGED
@@ -5,9 +5,12 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
+ let react_dom = require("react-dom");
8
9
  // src/constants.ts
9
10
  const PANEL_ID = "novel-workshop";
10
11
  const PANEL_ROUTE = "/api/novel-write-engine/panel";
12
+ /** User-visible panel/tutorial version; keep in sync with package.json. */
13
+ const PANEL_VERSION = "0.2.10";
11
14
  // src/next-reasons.ts
12
15
  /**
13
16
  * Chinese one-line explanation for the tool each engine state recommends.
@@ -37,6 +40,9 @@ window.__ModuleLoader__.load({
37
40
  novel_rollback: "回退到更早阶段重建当前章。",
38
41
  novel_amend: "按明确的修复理由修正一份状态文档。",
39
42
  novel_audit: "先查看事务日志,确认并恢复损坏的历史。",
43
+ novel_restore: "按备份清单恢复损坏的状态文档或当前章。",
44
+ novel_state: "查看状态文档、大纲、世界观或历史原文。",
45
+ novel_pipeline: "按当前阶段领取下一步工具与参数。",
40
46
  novel_export: "导出成书或书级提案。",
41
47
  novel_diff: "对比当前正文与上一版或给定文本。",
42
48
  novel_word_count: "按引擎字数口径核对正文长度。"
@@ -115,6 +121,7 @@ window.__ModuleLoader__.load({
115
121
  damaged: false,
116
122
  stageLabel: "读取中",
117
123
  finished: false,
124
+ chapter: 0,
118
125
  revisionLabel: "",
119
126
  phases: [],
120
127
  rows: [],
@@ -152,7 +159,7 @@ window.__ModuleLoader__.load({
152
159
  },
153
160
  quality: {
154
161
  averageScore: 0,
155
- lastTenAverage: 0,
162
+ lastTenAverage: null,
156
163
  bookMedian: 0,
157
164
  bookMin: 0,
158
165
  bookMax: 0,
@@ -304,15 +311,15 @@ window.__ModuleLoader__.load({
304
311
  hint: item.hint ?? ""
305
312
  }));
306
313
  const rows = chapterRows.map((row) => {
307
- const scoreValue = Number.isFinite(row.score) ? Math.max(0, Math.min(150, row.score)) : 0;
314
+ const scoreValue = row.score === null ? null : Math.max(0, Math.min(150, Number.isFinite(row.score) ? row.score : 0));
308
315
  return {
309
316
  num: String(row.num),
310
317
  title: row.title,
311
318
  words: row.wordCount.toLocaleString("zh-CN"),
312
319
  wordCount: row.wordCount,
313
- score: `${scoreValue}/150`,
320
+ score: scoreValue === null ? "—" : `${scoreValue}/150`,
314
321
  scoreValue,
315
- scorePercent: Math.round(scoreValue / 150 * 100),
322
+ scorePercent: scoreValue === null ? null : Math.round(scoreValue / 150 * 100),
316
323
  badge: row.status === "passed" ? "已归档" : row.status === "blocked" ? "阻塞" : "草稿",
317
324
  tone: row.status === "passed" ? "ok" : row.status === "blocked" ? "bad" : "muted",
318
325
  details: row.details ?? "",
@@ -328,8 +335,8 @@ window.__ModuleLoader__.load({
328
335
  archivedAt: row.archivedAt ?? null
329
336
  };
330
337
  });
331
- const scored = rows.filter((row) => row.tone === "ok");
332
- const rowAverage = scored.length === 0 ? 0 : scored.reduce((sum, row) => sum + row.scoreValue, 0) / scored.length;
338
+ const scored = rows.filter((row) => row.tone === "ok" && row.scoreValue !== null);
339
+ const rowAverage = scored.length === 0 ? 0 : scored.reduce((sum, row) => sum + (row.scoreValue ?? 0), 0) / scored.length;
333
340
  const average = summary.averageScore !== void 0 && Number.isFinite(summary.averageScore) ? summary.averageScore : rowAverage;
334
341
  const next = summary.next;
335
342
  const progress = summary.progress;
@@ -348,6 +355,7 @@ window.__ModuleLoader__.load({
348
355
  subtitle: `${summary.genre} · ${platformLabel(summary.platform)} · ${progressLine}${volumeLabel(summary)}`,
349
356
  stageLabel: doneLabel,
350
357
  finished,
358
+ chapter: summary.chapter,
351
359
  revisionLabel: `第 ${summary.bookData?.current.rounds.revise ?? summary.revision} 次修订`,
352
360
  phases: summary.phases.map((phase) => ({
353
361
  ...phase,
@@ -396,7 +404,7 @@ window.__ModuleLoader__.load({
396
404
  },
397
405
  quality: {
398
406
  averageScore: summary.bookData?.quality.average ?? summary.averageScore ?? average,
399
- lastTenAverage: summary.bookData?.quality.last_ten_average ?? summary.quality?.averageScore ?? average,
407
+ lastTenAverage: summary.bookData?.quality.last_ten_average ?? summary.quality?.last_ten_average ?? null,
400
408
  bookMedian: summary.bookData?.quality.median ?? 0,
401
409
  bookMin: summary.bookData?.quality.min ?? 0,
402
410
  bookMax: summary.bookData?.quality.max ?? 0,
@@ -405,7 +413,7 @@ window.__ModuleLoader__.load({
405
413
  trend: summary.bookData?.quality.trend ?? summary.quality?.trend ?? null,
406
414
  trendText: (summary.bookData?.quality.trend ?? summary.quality?.trend) === null || (summary.bookData?.quality.trend ?? summary.quality?.trend) === void 0 ? summary.quality?.trend_basis === "insufficient_samples" || summary.bookData?.quality.trend_basis === "insufficient_samples" ? "样本不足" : summary.quality?.trend_basis === "insufficient_history" || summary.bookData?.quality.trend_basis === "insufficient_history" ? "历史不足" : "—" : `${(summary.bookData?.quality.trend ?? summary.quality.trend) >= 0 ? "+" : ""}${summary.bookData?.quality.trend ?? summary.quality.trend}`,
407
415
  samples: summary.bookData?.quality.samples ?? summary.quality?.samples ?? 0,
408
- attempts: Math.max(summary.bookData?.quality.attempts ?? summary.quality?.attempts ?? 0, summary.bookData?.quality.samples ?? summary.quality?.samples ?? 0)
416
+ attempts: summary.bookData?.quality.attempts ?? summary.quality?.attempts ?? 0
409
417
  },
410
418
  health: {
411
419
  ok: summary.health?.ok ?? blocking.length === 0,
@@ -432,12 +440,20 @@ window.__ModuleLoader__.load({
432
440
  };
433
441
  }
434
442
  /** Fetch the panel payload; failures become a plain message, never a throw. */
435
- async function fetchPanel(fetcher, path, signal) {
443
+ async function fetchPanel(fetcher, path, signal, etag) {
436
444
  try {
437
445
  const response = await fetcher(path, {
438
- headers: { accept: "application/json" },
446
+ headers: {
447
+ accept: "application/json",
448
+ ...etag === void 0 ? {} : { "if-none-match": etag }
449
+ },
439
450
  ...signal === void 0 ? {} : { signal }
440
451
  });
452
+ if (response.status === 304) return {
453
+ payload: null,
454
+ error: null,
455
+ notModified: true
456
+ };
441
457
  if (!response.ok) {
442
458
  let detail = `宿主返回 ${response.status}`;
443
459
  try {
@@ -468,7 +484,9 @@ window.__ModuleLoader__.load({
468
484
  --nwe-surface: var(--dsw-alias-bg-layer-1, #171a20);
469
485
  --nwe-raised: var(--dsw-alias-bg-layer-2, #1e222a);
470
486
  --nwe-border: var(--dsw-alias-border-l1, #ffffff14);
471
- --nwe-gold: color-mix(in srgb, #b7862f 82%, var(--nwe-ink));
487
+ --nwe-gold: var(--dsw-alias-label-accent, color-mix(in srgb, var(--nwe-ink) 58%, var(--nwe-raised)));
488
+ --nwe-accent: var(--nwe-gold);
489
+ --nwe-font: var(--dsw-font-family, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif);
472
490
  --nwe-jade: color-mix(in srgb, #4f9f82 68%, var(--nwe-ink));
473
491
  --nwe-rose: color-mix(in srgb, #c96a6a 68%, var(--nwe-ink));
474
492
  position: relative;
@@ -485,7 +503,7 @@ window.__ModuleLoader__.load({
485
503
  margin: 0 auto;
486
504
  padding: clamp(18px, 3.2vw, 38px);
487
505
  color: var(--nwe-ink);
488
- font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
506
+ font-family: var(--nwe-font);
489
507
  line-height: 1.55;
490
508
  }
491
509
  .nwe-root * { box-sizing: border-box; }
@@ -512,7 +530,7 @@ window.__ModuleLoader__.load({
512
530
  }
513
531
  .nwe-eyebrow {
514
532
  color: var(--nwe-gold);
515
- font-size: 10px;
533
+ font-size: 12px;
516
534
  font-weight: 600;
517
535
  letter-spacing: .22em;
518
536
  text-transform: uppercase;
@@ -541,7 +559,7 @@ window.__ModuleLoader__.load({
541
559
  border: 1px solid color-mix(in srgb, var(--nwe-gold) 50%, transparent);
542
560
  border-radius: 999px;
543
561
  color: var(--nwe-gold);
544
- font-size: 11px;
562
+ font-size: 12px;
545
563
  font-weight: 600;
546
564
  }
547
565
  .nwe-stage::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
@@ -565,7 +583,7 @@ window.__ModuleLoader__.load({
565
583
  .nwe-refresh:disabled { cursor: wait; opacity: .55; }
566
584
  .nwe-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; flex-wrap: wrap; }
567
585
  .nwe-project { display: flex; align-items: center; gap: 6px; min-width: 0; }
568
- .nwe-project-label { color: var(--nwe-muted); font-size: 11px; white-space: nowrap; }
586
+ .nwe-project-label { color: var(--nwe-muted); font-size: 12px; white-space: nowrap; }
569
587
  .nwe-project-select {
570
588
  max-width: 260px;
571
589
  min-height: 36px;
@@ -577,7 +595,7 @@ window.__ModuleLoader__.load({
577
595
  font: inherit;
578
596
  font-size: 12px;
579
597
  }
580
- .nwe-rootline { margin-top: 5px; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; overflow-wrap: anywhere; }
598
+ .nwe-rootline { margin-top: 5px; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; overflow-wrap: anywhere; }
581
599
  .nwe-damaged { margin-top: 10px; padding: 9px 12px; border: 1px solid color-mix(in srgb, var(--nwe-rose) 36%, var(--nwe-border)); border-radius: 10px; color: var(--nwe-rose); background: color-mix(in srgb, var(--nwe-rose) 7%, transparent); font-size: 12px; }
582
600
  .nwe-help {
583
601
  display: inline-flex;
@@ -606,7 +624,7 @@ window.__ModuleLoader__.load({
606
624
  align-items: center;
607
625
  justify-content: center;
608
626
  padding: clamp(12px, 3.5vw, 42px);
609
- background: color-mix(in srgb, #080a0d 76%, transparent);
627
+ background: color-mix(in srgb, #000 76%, transparent);
610
628
  backdrop-filter: blur(7px);
611
629
  }
612
630
  .nwe-modal {
@@ -670,27 +688,49 @@ window.__ModuleLoader__.load({
670
688
  .nwe-tutorial-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
671
689
  .nwe-tutorial-card { padding: 11px 12px; border: 1px solid var(--nwe-border); border-radius: 11px; background: color-mix(in srgb, var(--nwe-raised) 52%, transparent); }
672
690
  .nwe-tutorial-card strong { display: block; color: var(--nwe-gold); font-size: 12px; }
673
- .nwe-tutorial-card span { display: block; margin-top: 4px; color: var(--nwe-muted); font-size: 11px; line-height: 1.65; }
691
+ .nwe-tutorial-card span { display: block; margin-top: 4px; color: var(--nwe-muted); font-size: 12px; line-height: 1.65; }
674
692
  .nwe-steps { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; }
675
693
  .nwe-step { padding: 12px 14px; border: 1px solid var(--nwe-border); border-left: 2px solid color-mix(in srgb, var(--nwe-gold) 58%, var(--nwe-border)); border-radius: 11px; background: color-mix(in srgb, var(--nwe-surface) 86%, var(--nwe-raised) 14%); }
676
694
  .nwe-step-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
677
- .nwe-step-no { color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; letter-spacing: .06em; }
695
+ .nwe-step-no { color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; letter-spacing: .06em; }
678
696
  .nwe-step h4 { margin: 0; font-size: 13px; font-weight: 600; }
679
- .nwe-step-tool { margin-left: auto; padding: 2px 6px; border: 1px solid var(--nwe-border); border-radius: 5px; color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; white-space: nowrap; }
697
+ .nwe-step-tool { margin-left: auto; padding: 2px 6px; border: 1px solid var(--nwe-border); border-radius: 5px; color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; white-space: nowrap; }
680
698
  .nwe-step p { margin: 6px 0 0; color: var(--nwe-muted); font-size: 12px; line-height: 1.7; }
681
699
  .nwe-step p strong { color: var(--nwe-ink); font-weight: 600; }
682
- .nwe-tutorial-note { margin: 12px 0 0; padding: 10px 12px; border-radius: 9px; background: color-mix(in srgb, var(--nwe-gold) 8%, transparent); color: var(--nwe-muted); font-size: 11px; line-height: 1.7; }
700
+ .nwe-tutorial-note { margin: 12px 0 0; padding: 10px 12px; border-radius: 9px; background: color-mix(in srgb, var(--nwe-gold) 8%, transparent); color: var(--nwe-muted); font-size: 12px; line-height: 1.7; }
683
701
  .nwe-tutorial-note strong { color: var(--nwe-gold); }
684
702
  .nwe-faq { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; }
685
703
  .nwe-faq li { padding: 10px 12px; border: 1px solid var(--nwe-border); border-radius: 10px; }
686
704
  .nwe-faq strong { display: block; color: var(--nwe-ink); font-size: 12px; }
687
705
  .nwe-faq p { margin: 4px 0 0; color: var(--nwe-muted); font-size: 12px; line-height: 1.65; }
688
- .nwe-prompt { overflow-x: auto; margin: 0; padding: 12px 14px; border: 1px solid var(--nwe-border); border-radius: 10px; background: color-mix(in srgb, #080a0d 24%, var(--nwe-surface)); color: var(--nwe-ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 1.75; white-space: pre-wrap; }
706
+ .nwe-copy-block { margin-top: 0; }
707
+ .nwe-copy-row { display: flex; align-items: center; gap: 10px; margin-top: 7px; }
708
+ .nwe-copy-status { color: var(--nwe-muted); font-size: 12px; }
709
+ .nwe-prompt { overflow-x: auto; margin: 0; padding: 12px 14px; border: 1px solid var(--nwe-border); border-radius: 10px; background: color-mix(in srgb, #000 24%, var(--nwe-surface)); color: var(--nwe-ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.75; white-space: pre-wrap; }
689
710
  .nwe-checklist { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
690
711
  .nwe-checklist li { display: flex; gap: 7px; color: var(--nwe-muted); font-size: 12px; line-height: 1.6; }
691
712
  .nwe-checklist li::before { content: "✓"; color: var(--nwe-jade); font-weight: 700; }
692
- .nwe-modal-foot { flex: 0 0 auto; padding: 10px 24px; border-top: 1px solid var(--nwe-border); color: var(--nwe-muted); font-size: 10px; }
713
+ .nwe-modal-foot { flex: 0 0 auto; padding: 10px 24px; border-top: 1px solid var(--nwe-border); color: var(--nwe-muted); font-size: 12px; }
693
714
  .nwe-help-label { white-space: nowrap; }
715
+ .nwe-tabs { display: flex; flex-wrap: wrap; gap: 4px; margin: 18px 0 0; padding: 4px; border: 1px solid var(--nwe-border); border-radius: 12px; background: color-mix(in srgb, var(--nwe-raised) 65%, transparent); }
716
+ .nwe-tab { padding: 7px 13px; border: 0; border-radius: 8px; background: transparent; color: var(--nwe-muted); font: inherit; font-size: 12px; cursor: pointer; }
717
+ .nwe-tab.is-active { background: color-mix(in srgb, var(--nwe-gold) 14%, transparent); color: var(--nwe-ink); }
718
+ .nwe-tab:focus-visible { outline: 2px solid var(--nwe-accent); outline-offset: 2px; }
719
+ .nwe-section-list { display: grid; gap: 7px; }
720
+ .nwe-data-row { border: 1px solid var(--nwe-border); border-radius: 9px; background: color-mix(in srgb, var(--nwe-raised) 55%, transparent); }
721
+ .nwe-data-row > summary { cursor: pointer; padding: 9px 11px; font-size: 12px; overflow-wrap: anywhere; }
722
+ .nwe-data-json { margin: 0; padding: 0 11px 11px; color: var(--nwe-muted); font-size: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }
723
+ .nwe-subsection { margin: 14px 0 0; }
724
+ .nwe-subsection > h3 { margin: 0 0 6px; color: var(--nwe-muted); font-size: 12px; font-weight: 600; letter-spacing: .04em; }
725
+ .nwe-kv { display: grid; grid-template-columns: minmax(72px, max-content) minmax(0, 1fr); gap: 4px 12px; margin: 0; padding: 0 11px 11px; font-size: 12px; }
726
+ .nwe-kv dt { color: var(--nwe-muted); }
727
+ .nwe-kv dd { margin: 0; overflow-wrap: anywhere; }
728
+ .nwe-quality-summary { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-bottom: 6px; color: var(--nwe-muted); font-size: 12px; }
729
+ .nwe-dim { display: grid; grid-template-columns: 72px minmax(0, 1fr) 76px; align-items: center; gap: 9px; font-size: 12px; }
730
+ .nwe-dim > span:last-child { color: var(--nwe-muted); text-align: right; font-variant-numeric: tabular-nums; }
731
+ .nwe-dim .nwe-v6-bar { margin: 0; }
732
+ .nwe-health-flags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
733
+ .nwe-empty { padding: 18px; border: 1px dashed var(--nwe-border); border-radius: 10px; color: var(--nwe-muted); font-size: 12px; }
694
734
  .nwe-stats {
695
735
  display: grid;
696
736
  grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -703,7 +743,7 @@ window.__ModuleLoader__.load({
703
743
  border-radius: 14px;
704
744
  background: color-mix(in srgb, var(--nwe-surface) 88%, var(--nwe-gold) 4%);
705
745
  }
706
- .nwe-stat-label { color: var(--nwe-muted); font-size: 11px; letter-spacing: .04em; }
746
+ .nwe-stat-label { color: var(--nwe-muted); font-size: 12px; letter-spacing: .04em; }
707
747
  .nwe-stat-value {
708
748
  margin-top: 3px;
709
749
  font-size: 22px;
@@ -711,7 +751,21 @@ window.__ModuleLoader__.load({
711
751
  font-weight: 600;
712
752
  letter-spacing: -.02em;
713
753
  }
714
- .nwe-stat-unit { margin-left: 2px; color: var(--nwe-muted); font-size: 11px; font-weight: 400; }
754
+ .nwe-stat-unit { margin-left: 2px; color: var(--nwe-muted); font-size: 12px; font-weight: 400; }
755
+ .nwe-overview-charts { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(260px, .9fr); gap: 12px; margin-top: 14px; }
756
+ .nwe-viz-card { min-width: 0; padding: 13px 15px 11px; border: 1px solid var(--nwe-border); border-radius: 12px; background: linear-gradient(145deg, color-mix(in srgb, var(--nwe-gold) 6%, var(--nwe-surface)), var(--nwe-surface) 58%); }
757
+ .nwe-viz-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
758
+ .nwe-viz-title { display: flex; align-items: center; gap: 8px; color: var(--nwe-ink); font-size: 13px; font-weight: 650; }
759
+ .nwe-viz-title svg { color: var(--nwe-gold); }
760
+ .nwe-viz-note { color: var(--nwe-muted); font-size: 11px; font-variant-numeric: tabular-nums; text-align: right; }
761
+ .nwe-viz-empty { display: grid; place-items: center; min-height: 196px; color: var(--nwe-muted); font-size: 12px; text-align: center; }
762
+ .nwe-trend-chart { display: block; width: 100%; height: 196px; margin-top: 2px; overflow: visible; }
763
+ .nwe-word-chart { display: flex; align-items: flex-end; gap: 7px; height: 196px; margin-top: 2px; padding: 6px 2px 0; border-bottom: 1px solid var(--nwe-border); background: repeating-linear-gradient(to top, transparent 0, transparent calc(25% - 1px), var(--nwe-border) 25%); }
764
+ .nwe-word-col { display: flex; flex: 1 1 0; flex-direction: column; align-items: center; justify-content: flex-end; min-width: 0; height: 100%; gap: 4px; }
765
+ .nwe-word-value { height: 12px; color: var(--nwe-muted); font-size: 10px; font-variant-numeric: tabular-nums; white-space: nowrap; }
766
+ .nwe-word-track { display: flex; align-items: flex-end; justify-content: center; flex: 0 0 142px; width: 100%; height: 142px; min-height: 0; }
767
+ .nwe-word-track > span { display: block; width: min(72%, 30px); min-height: 3px; border-radius: 4px 4px 2px 2px; background: linear-gradient(180deg, var(--nwe-gold), color-mix(in srgb, var(--nwe-gold) 28%, transparent)); }
768
+ .nwe-word-label { color: var(--nwe-muted); font-size: 10px; font-variant-numeric: tabular-nums; white-space: nowrap; }
715
769
  .nwe-section { margin-top: 26px; }
716
770
  .nwe-section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
717
771
  .nwe-section-title {
@@ -720,7 +774,7 @@ window.__ModuleLoader__.load({
720
774
  font-size: 17px;
721
775
  font-weight: 600;
722
776
  }
723
- .nwe-section-note { color: var(--nwe-muted); font-size: 11px; }
777
+ .nwe-section-note { color: var(--nwe-muted); font-size: 12px; }
724
778
  .nwe-phases {
725
779
  display: grid;
726
780
  grid-template-columns: repeat(9, minmax(0, 1fr));
@@ -754,37 +808,37 @@ window.__ModuleLoader__.load({
754
808
  border-radius: 50%;
755
809
  background: var(--nwe-surface);
756
810
  color: transparent;
757
- font-size: 9px;
811
+ font-size: 12px;
758
812
  font-weight: 700;
759
813
  }
760
814
  .nwe-phase.is-done .nwe-dot { border-color: color-mix(in srgb, var(--nwe-jade) 70%, transparent); background: color-mix(in srgb, var(--nwe-jade) 18%, var(--nwe-surface)); color: var(--nwe-jade); }
761
815
  .nwe-phase.is-active .nwe-dot { border-color: var(--nwe-gold); background: var(--nwe-gold); color: #19150d; box-shadow: 0 0 0 4px color-mix(in srgb, var(--nwe-gold) 16%, transparent); }
762
- .nwe-phase-label { overflow: hidden; max-width: 100%; color: var(--nwe-muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
816
+ .nwe-phase-label { overflow: hidden; max-width: 100%; color: var(--nwe-muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
763
817
  .nwe-phase.is-active .nwe-phase-label { color: var(--nwe-ink); font-weight: 600; }
764
818
  .nwe-book-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 10px; margin-top: 12px; }
765
819
  .nwe-book-card { padding: 14px 16px; border: 1px solid var(--nwe-border); border-radius: 14px; background: color-mix(in srgb, var(--nwe-surface) 92%, transparent); }
766
820
  .nwe-book-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
767
821
  .nwe-book-title { font-family: "Noto Serif SC", "Songti SC", Georgia, serif; font-size: 14px; font-weight: 600; }
768
- .nwe-book-note { color: var(--nwe-muted); font-size: 10px; }
822
+ .nwe-book-note { color: var(--nwe-muted); font-size: 12px; }
769
823
  .nwe-progress { overflow: hidden; height: 7px; margin-top: 10px; border-radius: 99px; background: var(--nwe-border); }
770
824
  .nwe-progress > span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--nwe-jade), var(--nwe-gold)); }
771
- .nwe-progress-legend { display: flex; justify-content: space-between; margin-top: 5px; color: var(--nwe-muted); font-size: 10px; font-variant-numeric: tabular-nums; }
825
+ .nwe-progress-legend { display: flex; justify-content: space-between; margin-top: 5px; color: var(--nwe-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
772
826
  .nwe-volumes { max-height: 190px; overflow-y: auto; overscroll-behavior: contain; padding-right: 3px; }
773
- .nwe-volume { display: flex; justify-content: space-between; gap: 8px; margin-top: 7px; color: var(--nwe-muted); font-size: 11px; }
827
+ .nwe-volume { display: flex; justify-content: space-between; gap: 8px; margin-top: 7px; color: var(--nwe-muted); font-size: 12px; }
774
828
  .nwe-volume strong { overflow: hidden; color: var(--nwe-ink); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
775
829
  .nwe-volume.is-current strong { color: var(--nwe-gold); }
776
- .nwe-copy { margin-top: 8px; padding: 6px 10px; border: 1px solid var(--nwe-border); border-radius: 8px; background: var(--nwe-raised); color: var(--nwe-muted); font: inherit; font-size: 11px; cursor: pointer; }
830
+ .nwe-copy { margin-top: 8px; padding: 6px 10px; border: 1px solid var(--nwe-border); border-radius: 8px; background: var(--nwe-raised); color: var(--nwe-muted); font: inherit; font-size: 12px; cursor: pointer; }
777
831
  .nwe-copy:hover { border-color: color-mix(in srgb, var(--nwe-gold) 65%, var(--nwe-border)); color: var(--nwe-ink); }
778
- .nwe-resume-fallback { margin-top: 6px; padding: 8px 10px; border: 1px solid var(--nwe-border); border-radius: 8px; background: color-mix(in srgb, #080a0d 24%, var(--nwe-surface)); color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; line-height: 1.6; white-space: pre-wrap; }
832
+ .nwe-resume-fallback { margin-top: 6px; padding: 8px 10px; border: 1px solid var(--nwe-border); border-radius: 8px; background: color-mix(in srgb, #000 24%, var(--nwe-surface)); color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
779
833
  .nwe-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
780
- .nwe-chip { padding: 3px 8px; border: 1px solid var(--nwe-border); border-radius: 999px; color: var(--nwe-muted); font-size: 10px; font-variant-numeric: tabular-nums; }
834
+ .nwe-chip { padding: 3px 8px; border: 1px solid var(--nwe-border); border-radius: 999px; color: var(--nwe-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
781
835
  .nwe-bars { display: flex; align-items: flex-end; gap: 5px; height: 54px; margin-top: 10px; }
782
836
  .nwe-bar { flex: 1 1 0; min-width: 4px; border-radius: 4px 4px 2px 2px; background: linear-gradient(180deg, var(--nwe-gold), color-mix(in srgb, var(--nwe-gold) 35%, transparent)); }
783
- .nwe-health { display: flex; align-items: center; gap: 8px; margin-top: 10px; color: var(--nwe-muted); font-size: 11px; }
837
+ .nwe-health { display: flex; align-items: center; gap: 8px; margin-top: 10px; color: var(--nwe-muted); font-size: 12px; }
784
838
  .nwe-health.is-bad { color: var(--nwe-rose); }
785
839
  .nwe-activity { display: flex; flex-direction: column; gap: 6px; margin: 10px 0 0; padding: 0; list-style: none; }
786
- .nwe-activity li { display: grid; grid-template-columns: 96px minmax(0, 1fr) auto; gap: 8px; color: var(--nwe-muted); font-size: 11px; }
787
- .nwe-activity code { color: var(--nwe-ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }
840
+ .nwe-activity li { display: grid; grid-template-columns: 96px minmax(0, 1fr) auto; gap: 8px; color: var(--nwe-muted); font-size: 12px; }
841
+ .nwe-activity code { color: var(--nwe-ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
788
842
  .nwe-next {
789
843
  display: flex;
790
844
  align-items: flex-start;
@@ -797,11 +851,27 @@ window.__ModuleLoader__.load({
797
851
  background: linear-gradient(90deg, color-mix(in srgb, var(--nwe-gold) 9%, transparent), transparent 72%), var(--nwe-surface);
798
852
  }
799
853
  .nwe-next-icon { display: grid; place-items: center; flex: 0 0 auto; width: 28px; height: 28px; border-radius: 9px; background: color-mix(in srgb, var(--nwe-gold) 15%, transparent); color: var(--nwe-gold); }
800
- .nwe-next-kicker { color: var(--nwe-gold); font-size: 10px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; }
854
+ .nwe-next-kicker { color: var(--nwe-gold); font-size: 12px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; }
801
855
  .nwe-next-title { margin-top: 2px; font-size: 15px; font-weight: 600; }
802
856
  .nwe-next-detail { margin-top: 2px; color: var(--nwe-muted); font-size: 12px; }
803
- .nwe-next-tool { margin-left: auto; align-self: center; flex: 0 0 auto; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }
857
+ .nwe-next-tool { margin-left: auto; align-self: center; flex: 0 0 auto; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
804
858
  .nwe-chapters { display: flex; flex-direction: column; gap: 9px; margin: 12px 0 0; padding: 0; list-style: none; }
859
+ .nwe-chapter-toolbar { display: flex; flex-wrap: wrap; align-items: end; gap: 8px; margin: 12px 0 0; padding: 10px 12px; border: 1px solid var(--nwe-border); border-radius: 10px; background: color-mix(in srgb, var(--nwe-raised) 55%, transparent); }
860
+ .nwe-chapter-toolbar label { display: grid; gap: 3px; color: var(--nwe-muted); font-size: 12px; }
861
+ .nwe-chapter-toolbar input, .nwe-chapter-toolbar select { min-height: 30px; padding: 4px 7px; border: 1px solid var(--nwe-border); border-radius: 7px; color: var(--nwe-ink); background: var(--nwe-surface); font: inherit; font-size: 12px; }
862
+ .nwe-chapter-toolbar input { min-width: 150px; }
863
+ .nwe-section-search { display: flex; align-items: center; gap: 8px; margin: 12px 0 0; color: var(--nwe-muted); font-size: 12px; }
864
+ .nwe-section-search input { min-height: 30px; min-width: 180px; padding: 4px 8px; border: 1px solid var(--nwe-border); border-radius: 7px; color: var(--nwe-ink); background: var(--nwe-surface); font: inherit; font-size: 12px; }
865
+ .nwe-table-wrap { overflow-x: auto; margin-top: 10px; border: 1px solid var(--nwe-border); border-radius: 10px; }
866
+ .nwe-chapter-detail { margin-top: 12px; padding: 12px; border: 1px solid var(--nwe-border); border-radius: 10px; background: color-mix(in srgb, var(--nwe-raised) 45%, transparent); }
867
+ .nwe-detail-text { max-height: 360px; overflow: auto; margin: 8px 0 0; padding: 10px; border-radius: 8px; background: var(--nwe-surface); color: var(--nwe-ink); font: 12px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
868
+ .nwe-chapter-table { width: 100%; min-width: 640px; border-collapse: collapse; font-size: 12px; }
869
+ .nwe-chapter-table th, .nwe-chapter-table td { padding: 8px 10px; border-bottom: 1px solid var(--nwe-border); text-align: left; vertical-align: top; }
870
+ .nwe-chapter-table th { color: var(--nwe-muted); font-weight: 600; background: color-mix(in srgb, var(--nwe-raised) 55%, transparent); }
871
+ .nwe-chapter-table tr:last-child td { border-bottom: 0; }
872
+ .nwe-chapter-table td.is-num { color: var(--nwe-gold); font-variant-numeric: tabular-nums; white-space: nowrap; }
873
+ .nwe-chapter-table .nwe-table-title { font-weight: 600; overflow-wrap: anywhere; }
874
+ .nwe-pager { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; color: var(--nwe-muted); font-size: 12px; }
805
875
  .nwe-chapter {
806
876
  overflow: hidden;
807
877
  border: 1px solid var(--nwe-border);
@@ -823,18 +893,18 @@ window.__ModuleLoader__.load({
823
893
  list-style: none;
824
894
  }
825
895
  .nwe-chapter summary::-webkit-details-marker { display: none; }
826
- .nwe-chapter-num { color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; letter-spacing: .04em; }
896
+ .nwe-chapter-num { color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; letter-spacing: .04em; }
827
897
  .nwe-chapter-title { overflow: hidden; font-size: 14px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
828
- .nwe-chapter-stage { margin-top: 2px; color: var(--nwe-muted); font-size: 10px; }
898
+ .nwe-chapter-stage { margin-top: 2px; color: var(--nwe-muted); font-size: 12px; }
829
899
  .nwe-chevron { color: var(--nwe-muted); font-size: 16px; line-height: 1; transition: transform .18s ease; }
830
900
  .nwe-chapter[open] .nwe-chevron { transform: rotate(90deg); }
831
- .nwe-chapter-meta { display: flex; align-items: center; gap: 10px; justify-content: flex-end; color: var(--nwe-muted); font-size: 11px; font-variant-numeric: tabular-nums; white-space: nowrap; }
901
+ .nwe-chapter-meta { display: flex; align-items: center; gap: 10px; justify-content: flex-end; color: var(--nwe-muted); font-size: 12px; font-variant-numeric: tabular-nums; white-space: nowrap; }
832
902
  .nwe-score { display: flex; flex-direction: column; gap: 4px; width: 92px; }
833
903
  .nwe-score-line { display: flex; justify-content: space-between; gap: 6px; }
834
- .nwe-score-value { color: var(--nwe-ink); font-size: 11px; }
904
+ .nwe-score-value { color: var(--nwe-ink); font-size: 12px; }
835
905
  .nwe-scorebar { overflow: hidden; height: 4px; border-radius: 99px; background: var(--nwe-border); }
836
906
  .nwe-scorebar > span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, color-mix(in srgb, var(--nwe-gold) 65%, var(--nwe-jade)), var(--nwe-gold)); }
837
- .nwe-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border: 1px solid var(--nwe-border); border-radius: 999px; font-size: 10px; white-space: nowrap; }
907
+ .nwe-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border: 1px solid var(--nwe-border); border-radius: 999px; font-size: 12px; white-space: nowrap; }
838
908
  .nwe-badge.is-ok { border-color: color-mix(in srgb, var(--nwe-jade) 45%, transparent); color: var(--nwe-jade); }
839
909
  .nwe-badge.is-bad { border-color: color-mix(in srgb, var(--nwe-rose) 45%, transparent); color: var(--nwe-rose); }
840
910
  .nwe-badge.is-muted { color: var(--nwe-muted); }
@@ -857,19 +927,20 @@ window.__ModuleLoader__.load({
857
927
  .nwe-project-choices { display: flex; flex-direction: column; gap: 8px; width: min(560px, 100%); margin-top: 16px; }
858
928
  .nwe-project-choice { display: flex; flex-direction: column; gap: 3px; padding: 10px 13px; border: 1px solid var(--nwe-border); border-radius: 10px; background: var(--nwe-raised); color: var(--nwe-ink); font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
859
929
  .nwe-project-choice:hover { border-color: color-mix(in srgb, var(--nwe-gold) 65%, var(--nwe-border)); }
860
- .nwe-project-choice span { overflow-wrap: anywhere; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }
930
+ .nwe-project-choice span { overflow-wrap: anywhere; color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
861
931
  .nwe-skeleton { display: grid; gap: 10px; margin-top: 12px; }
862
932
  .nwe-skeleton-line { height: 16px; border-radius: 8px; background: linear-gradient(90deg, var(--nwe-border), color-mix(in srgb, var(--nwe-border) 40%, transparent), var(--nwe-border)); background-size: 200% 100%; animation: nwe-shimmer 1.6s linear infinite; }
863
933
  .nwe-skeleton-line.is-wide { height: 72px; }
864
934
  .nwe-skeleton-line.is-short { width: 42%; }
865
935
  .nwe-blocking { display: flex; flex-direction: column; gap: 8px; margin: 12px 0 0; padding: 0; list-style: none; }
866
936
  .nwe-block { display: flex; gap: 11px; align-items: flex-start; padding: 13px 15px; border: 1px solid var(--nwe-border); border-left: 2px solid var(--nwe-rose); border-radius: 12px; background: color-mix(in srgb, var(--nwe-surface) 92%, transparent); }
867
- .nwe-block-icon { display: grid; place-items: center; flex: 0 0 auto; width: 22px; height: 22px; border-radius: 7px; background: color-mix(in srgb, var(--nwe-rose) 14%, transparent); color: var(--nwe-rose); font-size: 11px; font-weight: 700; }
868
- .nwe-block-code { color: var(--nwe-rose); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }
937
+ .nwe-block-icon { display: grid; place-items: center; flex: 0 0 auto; width: 22px; height: 22px; border-radius: 7px; background: color-mix(in srgb, var(--nwe-rose) 14%, transparent); color: var(--nwe-rose); font-size: 12px; font-weight: 700; }
938
+ .nwe-block-code { color: var(--nwe-rose); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
869
939
  .nwe-block-detail { margin-top: 2px; font-size: 12px; }
870
- .nwe-block-hint { margin-top: 3px; color: var(--nwe-muted); font-size: 11px; }
940
+ .nwe-block-hint { margin-top: 3px; color: var(--nwe-muted); font-size: 12px; }
871
941
  .nwe-pass { display: flex; align-items: center; gap: 9px; margin-top: 12px; padding: 13px 15px; border: 1px solid color-mix(in srgb, var(--nwe-jade) 30%, var(--nwe-border)); border-radius: 12px; color: var(--nwe-jade); font-size: 12px; background: color-mix(in srgb, var(--nwe-jade) 6%, var(--nwe-surface)); }
872
- .nwe-footer { display: flex; justify-content: space-between; gap: 10px; margin-top: 26px; padding-top: 12px; border-top: 1px solid var(--nwe-border); color: var(--nwe-muted); font-size: 10px; }
942
+ .nwe-footer { display: flex; justify-content: space-between; gap: 10px; margin-top: 26px; padding-top: 12px; border-top: 1px solid var(--nwe-border); color: var(--nwe-muted); font-size: 12px; }
943
+ .nwe-footer-actions { display: flex; align-items: center; gap: 8px; }
873
944
  .nwe-footer code { color: var(--nwe-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
874
945
  @keyframes nwe-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
875
946
  @media (max-width: 760px) {
@@ -916,7 +987,7 @@ window.__ModuleLoader__.load({
916
987
  box-shadow: none;
917
988
  }
918
989
  .nwe-hero-title { min-width: 0; }
919
- .nwe-eyebrow { color: var(--nwe-muted); font-size: 9px; letter-spacing: .16em; }
990
+ .nwe-eyebrow { color: var(--nwe-muted); font-size: 12px; letter-spacing: .16em; }
920
991
  .nwe-title {
921
992
  margin: 4px 0 4px;
922
993
  font-family: inherit;
@@ -924,17 +995,17 @@ window.__ModuleLoader__.load({
924
995
  font-weight: 650;
925
996
  letter-spacing: -.015em;
926
997
  }
927
- .nwe-meta { gap: 7px; font-size: 11px; }
998
+ .nwe-meta { gap: 7px; font-size: 12px; }
928
999
  .nwe-actions { gap: 7px; }
929
1000
  .nwe-project { flex-wrap: wrap; gap: 5px; }
930
- .nwe-project-label { font-size: 10px; }
1001
+ .nwe-project-label { font-size: 12px; }
931
1002
  .nwe-project-select {
932
1003
  max-width: 310px;
933
1004
  min-height: 32px;
934
1005
  padding: 5px 26px 5px 9px;
935
1006
  border-radius: 8px;
936
1007
  background: var(--nwe-surface);
937
- font-size: 11px;
1008
+ font-size: 12px;
938
1009
  }
939
1010
  .nwe-project-search {
940
1011
  width: 124px;
@@ -945,19 +1016,19 @@ window.__ModuleLoader__.load({
945
1016
  background: var(--nwe-surface);
946
1017
  color: var(--nwe-ink);
947
1018
  font: inherit;
948
- font-size: 11px;
1019
+ font-size: 12px;
949
1020
  }
950
1021
  .nwe-project-toggle {
951
1022
  display: inline-flex;
952
1023
  align-items: center;
953
1024
  gap: 4px;
954
1025
  color: var(--nwe-muted);
955
- font-size: 10px;
1026
+ font-size: 12px;
956
1027
  cursor: pointer;
957
1028
  white-space: nowrap;
958
1029
  }
959
1030
  .nwe-refresh, .nwe-help { min-height: 32px; padding: 6px 10px; border-radius: 8px; }
960
- .nwe-rootline { font-size: 9px; margin-top: 3px; }
1031
+ .nwe-rootline { font-size: 12px; margin-top: 3px; }
961
1032
  .nwe-stats {
962
1033
  display: grid;
963
1034
  grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -975,11 +1046,11 @@ window.__ModuleLoader__.load({
975
1046
  background: transparent;
976
1047
  }
977
1048
  .nwe-stat:last-child { border-right: 0; }
978
- .nwe-stat-label { font-size: 10px; }
1049
+ .nwe-stat-label { font-size: 12px; }
979
1050
  .nwe-stat-value { margin-top: 1px; font-size: 18px; font-weight: 600; }
980
1051
  .nwe-section { margin-top: 18px; }
981
1052
  .nwe-section-title { font-family: inherit; font-size: 14px; font-weight: 650; }
982
- .nwe-section-note { font-size: 10px; }
1053
+ .nwe-section-note { font-size: 12px; }
983
1054
  .nwe-next {
984
1055
  margin-top: 12px;
985
1056
  padding: 14px 16px;
@@ -991,8 +1062,8 @@ window.__ModuleLoader__.load({
991
1062
  .nwe-next-icon { width: 24px; height: 24px; border-radius: 7px; }
992
1063
  .nwe-next-body { min-width: 0; }
993
1064
  .nwe-next-title { font-size: 14px; }
994
- .nwe-next-detail { font-size: 11px; }
995
- .nwe-next-tool { font-size: 9px; }
1065
+ .nwe-next-detail { font-size: 12px; }
1066
+ .nwe-next-tool { font-size: 12px; }
996
1067
  .nwe-desk {
997
1068
  display: grid;
998
1069
  grid-template-columns: minmax(0, 1.2fr) minmax(300px, .8fr);
@@ -1008,8 +1079,8 @@ window.__ModuleLoader__.load({
1008
1079
  gap: 8px;
1009
1080
  margin-top: 16px;
1010
1081
  }
1011
- .nwe-desk-kicker { color: var(--nwe-muted); font-size: 9px; font-weight: 650; letter-spacing: .14em; text-transform: uppercase; }
1012
- .nwe-desk-note { color: var(--nwe-gold); font-size: 10px; }
1082
+ .nwe-desk-kicker { color: var(--nwe-muted); font-size: 12px; font-weight: 650; letter-spacing: .14em; text-transform: uppercase; }
1083
+ .nwe-desk-note { color: var(--nwe-gold); font-size: 12px; }
1013
1084
  .nwe-desk-main .nwe-phases { margin-top: 8px; }
1014
1085
  .nwe-desk-side .nwe-book-grid { grid-template-columns: 1fr; }
1015
1086
  .nwe-desk-side .nwe-section { margin-top: 16px; }
@@ -1020,27 +1091,27 @@ window.__ModuleLoader__.load({
1020
1091
  gap: 8px;
1021
1092
  align-items: baseline;
1022
1093
  padding: 7px 9px;
1023
- border: 1px solid color-mix(in srgb, #c9a227 24%, var(--nwe-border));
1094
+ border: 1px solid color-mix(in srgb, var(--nwe-gold) 24%, var(--nwe-border));
1024
1095
  border-radius: 7px;
1025
- background: color-mix(in srgb, #c9a227 5%, transparent);
1026
- font-size: 11px;
1096
+ background: color-mix(in srgb, var(--nwe-gold) 5%, transparent);
1097
+ font-size: 12px;
1027
1098
  }
1028
- .nwe-advisory-code { color: #b08a1e; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9px; white-space: nowrap; }
1099
+ .nwe-advisory-code { color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; white-space: nowrap; }
1029
1100
  .nwe-advisory-detail { color: var(--nwe-ink); line-height: 1.5; }
1030
- .nwe-advisory-hint { color: var(--nwe-muted); font-size: 9px; white-space: nowrap; }
1101
+ .nwe-advisory-hint { color: var(--nwe-muted); font-size: 12px; white-space: nowrap; }
1031
1102
  .nwe-advisory-empty {
1032
1103
  margin-top: 8px;
1033
1104
  padding: 8px 10px;
1034
- border: 1px solid color-mix(in srgb, #c9a227 18%, var(--nwe-border));
1105
+ border: 1px solid color-mix(in srgb, var(--nwe-gold) 18%, var(--nwe-border));
1035
1106
  border-radius: 7px;
1036
1107
  color: var(--nwe-muted);
1037
- font-size: 11px;
1108
+ font-size: 12px;
1038
1109
  }
1039
1110
 
1040
- .nwe-pending-section { border-left: 3px solid #c9a227; padding-left: 10px; }
1111
+ .nwe-pending-section { border-left: 3px solid var(--nwe-gold); padding-left: 10px; }
1041
1112
  .nwe-pending-error { border-left-color: #b3261e; }
1042
- .nwe-pending-detail { margin: 8px 0 0; color: var(--nwe-ink); font-size: 11px; line-height: 1.6; }
1043
- .nwe-pending-hint { margin: 4px 0 0; color: var(--nwe-muted); font-size: 10px; line-height: 1.5; }
1113
+ .nwe-pending-detail { margin: 8px 0 0; color: var(--nwe-ink); font-size: 12px; line-height: 1.6; }
1114
+ .nwe-pending-hint { margin: 4px 0 0; color: var(--nwe-muted); font-size: 12px; line-height: 1.5; }
1044
1115
 
1045
1116
  .nwe-v6-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
1046
1117
  .nwe-v6-card { border: 1px solid var(--nwe-border); border-radius: 8px; padding: 10px; background: var(--nwe-surface); }
@@ -1048,16 +1119,17 @@ window.__ModuleLoader__.load({
1048
1119
  .nwe-v6-card > summary { cursor: pointer; font: 600 12px/1.4 var(--nwe-font); color: var(--nwe-ink); }
1049
1120
  .nwe-v6-card > summary:focus-visible { outline: 2px solid var(--nwe-accent); outline-offset: 2px; }
1050
1121
  .nwe-v6-list { margin: 8px 0 0; padding: 0; list-style: none; display: grid; gap: 4px; }
1051
- .nwe-v6-list li { display: grid; grid-template-columns: 7.5em minmax(0, 1fr); gap: 8px; font-size: 10px; line-height: 1.55; color: var(--nwe-muted); }
1122
+ .nwe-v6-list li, .nwe-v6-list b, .nwe-v6-list span { min-width: 0; overflow-wrap: anywhere; }
1123
+ .nwe-v6-list li { display: grid; grid-template-columns: 7.5em minmax(0, 1fr); gap: 8px; font-size: 12px; line-height: 1.55; color: var(--nwe-muted); }
1052
1124
  .nwe-v6-list b { color: var(--nwe-ink); font-weight: 600; overflow-wrap: anywhere; }
1053
1125
  .nwe-v6-bars { display: grid; gap: 4px; margin-top: 8px; }
1054
- .nwe-v6-bar { display: grid; grid-template-columns: 7em minmax(0, 1fr) 3em; gap: 6px; align-items: center; font-size: 10px; color: var(--nwe-muted); }
1126
+ .nwe-v6-bar { display: grid; grid-template-columns: 7em minmax(0, 1fr) 3em; gap: 6px; align-items: center; font-size: 12px; color: var(--nwe-muted); }
1055
1127
  .nwe-v6-bar span { height: 4px; border-radius: 2px; background: var(--nwe-border); overflow: hidden; }
1056
1128
  .nwe-v6-bar span i { display: block; height: 100%; background: var(--nwe-accent); }
1057
- .nwe-v6-note { margin: 8px 0 0; font-size: 10px; line-height: 1.5; color: var(--nwe-muted); }
1129
+ .nwe-v6-note { margin: 8px 0 0; font-size: 12px; line-height: 1.5; color: var(--nwe-muted); }
1058
1130
  .nwe-v6-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
1059
- .nwe-v6-tags span { border: 1px solid var(--nwe-border); border-radius: 999px; padding: 2px 7px; font-size: 9px; color: var(--nwe-muted); }
1060
- .nwe-chapter-extra { margin-top: 6px; font-size: 10px; color: var(--nwe-muted); line-height: 1.5; overflow-wrap: anywhere; }
1131
+ .nwe-v6-tags span { border: 1px solid var(--nwe-border); border-radius: 999px; padding: 2px 7px; font-size: 12px; color: var(--nwe-muted); }
1132
+ .nwe-chapter-extra { margin-top: 6px; font-size: 12px; color: var(--nwe-muted); line-height: 1.5; overflow-wrap: anywhere; }
1061
1133
  @media (max-width: 640px) { .nwe-v6-grid { grid-template-columns: 1fr; } .nwe-v6-card[open] { grid-column: auto; } }
1062
1134
  .nwe-legacy {
1063
1135
  display: flex;
@@ -1067,10 +1139,10 @@ window.__ModuleLoader__.load({
1067
1139
  border: 1px dashed var(--nwe-border);
1068
1140
  border-radius: 7px;
1069
1141
  color: var(--nwe-muted);
1070
- font-size: 10px;
1142
+ font-size: 12px;
1071
1143
  line-height: 1.5;
1072
1144
  }
1073
- .nwe-legacy code { color: var(--nwe-muted); font-size: 9px; white-space: nowrap; }
1145
+ .nwe-legacy code { color: var(--nwe-muted); font-size: 12px; white-space: nowrap; }
1074
1146
  .nwe-chapters-section { margin-top: 20px; }
1075
1147
  .nwe-desk-main .nwe-chapters {
1076
1148
  max-height: 460px;
@@ -1103,6 +1175,718 @@ window.__ModuleLoader__.load({
1103
1175
  .nwe-skeleton-line { animation: none; }
1104
1176
  .nwe-refresh, .nwe-chapter { transition: none; }
1105
1177
  }
1178
+
1179
+ /* v8 workbench: quiet chrome, editorial hierarchy, content over ornament. */
1180
+ .nwe-root {
1181
+ --nwe-accent: color-mix(in srgb, #b4471f 82%, var(--nwe-ink));
1182
+ --nwe-accent-soft: color-mix(in srgb, var(--nwe-accent) 9%, transparent);
1183
+ --nwe-ok: color-mix(in srgb, #2f7d5b 82%, var(--nwe-ink));
1184
+ --nwe-warn: color-mix(in srgb, #9a6700 82%, var(--nwe-ink));
1185
+ --nwe-bad: color-mix(in srgb, #b42318 82%, var(--nwe-ink));
1186
+ --nwe-gold: var(--nwe-accent);
1187
+ --nwe-jade: var(--nwe-ok);
1188
+ --nwe-rose: var(--nwe-bad);
1189
+ max-width: 1240px;
1190
+ padding: clamp(22px, 2.8vw, 38px) clamp(20px, 3.4vw, 46px) 30px;
1191
+ font-size: 13px;
1192
+ line-height: 1.65;
1193
+ }
1194
+ .nwe-hero {
1195
+ align-items: flex-end;
1196
+ gap: 32px;
1197
+ padding: 0 0 22px;
1198
+ border: 0;
1199
+ border-bottom: 1px solid var(--nwe-border);
1200
+ border-radius: 0;
1201
+ background: none;
1202
+ box-shadow: none;
1203
+ }
1204
+ .nwe-brand { display: flex; align-items: flex-start; gap: 14px; min-width: 0; }
1205
+ .nwe-brand-mark {
1206
+ display: grid;
1207
+ place-items: center;
1208
+ flex: 0 0 auto;
1209
+ width: 40px;
1210
+ height: 40px;
1211
+ border-radius: 11px;
1212
+ background: var(--nwe-accent);
1213
+ color: #fffaf4;
1214
+ font-size: 18px;
1215
+ font-weight: 700;
1216
+ box-shadow: inset 0 1px 0 #ffffff2e;
1217
+ }
1218
+ .nwe-eyebrow { color: var(--nwe-muted); font-size: 11px; font-weight: 700; letter-spacing: .16em; }
1219
+ .nwe-title {
1220
+ margin: 5px 0 6px;
1221
+ font-family: inherit;
1222
+ font-size: clamp(26px, 3vw, 34px);
1223
+ font-weight: 700;
1224
+ line-height: 1.12;
1225
+ letter-spacing: -.04em;
1226
+ }
1227
+ .nwe-meta { gap: 6px 10px; color: var(--nwe-muted); font-size: 12px; }
1228
+ .nwe-stage {
1229
+ border: 0;
1230
+ border-radius: 5px;
1231
+ padding: 3px 7px;
1232
+ background: var(--nwe-accent-soft);
1233
+ color: var(--nwe-accent);
1234
+ font-size: 11px;
1235
+ font-weight: 650;
1236
+ }
1237
+ .nwe-stage::before { display: none; }
1238
+ .nwe-rootline {
1239
+ margin-top: 4px;
1240
+ color: color-mix(in srgb, var(--nwe-ink) 46%, transparent);
1241
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1242
+ font-size: 11px;
1243
+ }
1244
+ .nwe-revision { padding-left: 9px; border-left: 1px solid var(--nwe-border); }
1245
+ .nwe-actions { align-items: center; gap: 8px; }
1246
+ .nwe-project-label { font-size: 11px; color: var(--nwe-muted); }
1247
+ .nwe-project-select,
1248
+ .nwe-project-search {
1249
+ min-height: 34px;
1250
+ border: 1px solid var(--nwe-border);
1251
+ border-radius: 8px;
1252
+ background: var(--nwe-surface);
1253
+ color: var(--nwe-ink);
1254
+ font-size: 12px;
1255
+ }
1256
+ .nwe-project-select { max-width: 300px; padding: 6px 28px 6px 10px; }
1257
+ .nwe-project-search { width: 132px; padding: 6px 9px; }
1258
+ .nwe-refresh,
1259
+ .nwe-help {
1260
+ min-height: 34px;
1261
+ padding: 7px 11px;
1262
+ border: 1px solid var(--nwe-border);
1263
+ border-radius: 8px;
1264
+ background: transparent;
1265
+ color: var(--nwe-ink);
1266
+ font: inherit;
1267
+ font-size: 12px;
1268
+ }
1269
+ .nwe-refresh {
1270
+ border-color: var(--nwe-border);
1271
+ background: transparent;
1272
+ color: var(--nwe-ink);
1273
+ }
1274
+ .nwe-refresh:hover:not(:disabled) {
1275
+ border-color: var(--nwe-accent);
1276
+ background: var(--nwe-accent-soft);
1277
+ transform: none;
1278
+ }
1279
+ .nwe-help { background: color-mix(in srgb, var(--nwe-surface) 70%, transparent); color: var(--nwe-muted); }
1280
+ .nwe-help:hover { border-color: var(--nwe-accent); color: var(--nwe-ink); transform: none; }
1281
+ .nwe-refresh:disabled { cursor: wait; opacity: .55; }
1282
+ .nwe-next {
1283
+ display: grid;
1284
+ grid-template-columns: 36px minmax(0, 1fr) auto;
1285
+ gap: 14px;
1286
+ align-items: start;
1287
+ margin-top: 22px;
1288
+ padding: 18px 20px;
1289
+ border: 1px solid color-mix(in srgb, var(--nwe-accent) 22%, var(--nwe-border));
1290
+ border-left: 3px solid var(--nwe-accent);
1291
+ border-radius: 11px;
1292
+ background: color-mix(in srgb, var(--nwe-accent) 5%, var(--nwe-surface));
1293
+ }
1294
+ .nwe-next-icon {
1295
+ display: grid;
1296
+ place-items: center;
1297
+ width: 36px;
1298
+ height: 36px;
1299
+ border-radius: 9px;
1300
+ background: var(--nwe-accent);
1301
+ color: #fffaf4;
1302
+ }
1303
+ .nwe-next-kicker { color: var(--nwe-accent); font-size: 11px; font-weight: 700; letter-spacing: .14em; }
1304
+ .nwe-next-title { margin: 2px 0 3px; font-size: 19px; font-weight: 650; line-height: 1.25; letter-spacing: -.025em; }
1305
+ .nwe-next-detail { max-width: 62ch; color: var(--nwe-muted); font-size: 12.5px; line-height: 1.6; }
1306
+ .nwe-next-meta {
1307
+ display: flex;
1308
+ flex-direction: column;
1309
+ align-items: flex-end;
1310
+ gap: 4px;
1311
+ padding-top: 2px;
1312
+ color: var(--nwe-muted);
1313
+ font-size: 11px;
1314
+ text-align: right;
1315
+ white-space: nowrap;
1316
+ }
1317
+ .nwe-next-tool {
1318
+ margin: 1px 0 0;
1319
+ padding: 4px 7px;
1320
+ border: 1px solid var(--nwe-border);
1321
+ border-radius: 5px;
1322
+ background: var(--nwe-surface);
1323
+ color: var(--nwe-muted);
1324
+ font-size: 11px;
1325
+ }
1326
+ .nwe-next .nwe-copy {
1327
+ margin-top: 10px;
1328
+ padding: 6px 11px;
1329
+ border: 1px solid var(--nwe-accent);
1330
+ border-radius: 6px;
1331
+ background: var(--nwe-accent);
1332
+ color: #fffaf4;
1333
+ font-size: 11px;
1334
+ }
1335
+ .nwe-next .nwe-copy:hover { border-color: color-mix(in srgb, var(--nwe-accent) 84%, var(--nwe-ink)); background: color-mix(in srgb, var(--nwe-accent) 84%, var(--nwe-ink)); color: #fffaf4; }
1336
+ .nwe-tabs {
1337
+ gap: 25px;
1338
+ flex-wrap: nowrap;
1339
+ overflow-x: auto;
1340
+ margin-top: 20px;
1341
+ padding: 0;
1342
+ border: 0;
1343
+ border-bottom: 1px solid var(--nwe-border);
1344
+ border-radius: 0;
1345
+ background: transparent;
1346
+ scrollbar-width: none;
1347
+ }
1348
+ .nwe-tabs::-webkit-scrollbar { display: none; }
1349
+ .nwe-tab {
1350
+ position: relative;
1351
+ padding: 10px 1px 11px;
1352
+ border-radius: 0;
1353
+ color: var(--nwe-muted);
1354
+ font-size: 13px;
1355
+ white-space: nowrap;
1356
+ }
1357
+ .nwe-tab:hover { color: var(--nwe-ink); }
1358
+ .nwe-tab.is-active { background: transparent; color: var(--nwe-ink); font-weight: 650; }
1359
+ .nwe-tab.is-active::after {
1360
+ content: "";
1361
+ position: absolute;
1362
+ right: 0;
1363
+ bottom: -1px;
1364
+ left: 0;
1365
+ height: 2px;
1366
+ border-radius: 2px;
1367
+ background: var(--nwe-accent);
1368
+ }
1369
+ .nwe-stats {
1370
+ display: grid;
1371
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1372
+ gap: 0;
1373
+ margin-top: 20px;
1374
+ padding: 0 0 18px;
1375
+ border: 0;
1376
+ border-bottom: 1px solid var(--nwe-border);
1377
+ }
1378
+ .nwe-stat {
1379
+ min-width: 0;
1380
+ padding: 0 22px;
1381
+ border: 0;
1382
+ border-right: 1px solid var(--nwe-border);
1383
+ border-radius: 0;
1384
+ background: transparent;
1385
+ }
1386
+ .nwe-stat:first-child { padding-left: 0; }
1387
+ .nwe-stat:last-child { padding-right: 0; border-right: 0; }
1388
+ .nwe-stat-label { color: var(--nwe-muted); font-size: 11px; font-weight: 500; letter-spacing: .03em; }
1389
+ .nwe-stat-value {
1390
+ margin-top: 3px;
1391
+ font-size: 30px;
1392
+ font-weight: 650;
1393
+ line-height: 1.05;
1394
+ letter-spacing: -.045em;
1395
+ }
1396
+ .nwe-stat-unit { margin-left: 4px; color: var(--nwe-muted); font-size: 11px; font-weight: 400; letter-spacing: 0; }
1397
+ .nwe-stat-unit.is-note { display: block; margin: 5px 0 0; font-size: 11px; line-height: 1.45; }
1398
+ .nwe-desk {
1399
+ grid-template-columns: minmax(0, 1.55fr) minmax(290px, .75fr);
1400
+ gap: 40px;
1401
+ margin-top: 24px;
1402
+ }
1403
+ .nwe-desk-side { padding-left: 32px; border-left: 1px solid var(--nwe-border); }
1404
+ .nwe-desk-heading { margin: 0 0 10px; }
1405
+ .nwe-desk-kicker { color: var(--nwe-muted); font-size: 11px; font-weight: 700; letter-spacing: .12em; }
1406
+ .nwe-desk-note { color: var(--nwe-muted); font-size: 11px; }
1407
+ .nwe-section { margin-top: 25px; }
1408
+ .nwe-section-head { align-items: center; margin-bottom: 10px; }
1409
+ .nwe-section-title { font-family: inherit; font-size: 14px; font-weight: 650; letter-spacing: -.01em; }
1410
+ .nwe-section-note { color: var(--nwe-muted); font-size: 11px; }
1411
+ .nwe-phases {
1412
+ margin-top: 8px;
1413
+ padding: 13px 2px 7px;
1414
+ border: 0;
1415
+ border-top: 1px solid var(--nwe-border);
1416
+ border-bottom: 1px solid var(--nwe-border);
1417
+ border-radius: 0;
1418
+ background: transparent;
1419
+ }
1420
+ .nwe-phase { gap: 5px; }
1421
+ .nwe-phase:not(:last-child)::after { top: 8px; left: calc(50% + 12px); width: calc(100% - 24px); background: var(--nwe-border); }
1422
+ .nwe-dot {
1423
+ width: 17px;
1424
+ height: 17px;
1425
+ border: 1.5px solid color-mix(in srgb, var(--nwe-ink) 24%, transparent);
1426
+ background: var(--nwe-surface);
1427
+ font-size: 11px;
1428
+ }
1429
+ .nwe-phase.is-done .nwe-dot { border-color: var(--nwe-ok); background: color-mix(in srgb, var(--nwe-ok) 13%, var(--nwe-surface)); color: var(--nwe-ok); }
1430
+ .nwe-phase.is-active .nwe-dot { border-color: var(--nwe-accent); background: var(--nwe-accent); color: #fffaf4; box-shadow: 0 0 0 3px var(--nwe-accent-soft); }
1431
+ .nwe-phase-label { margin-top: 1px; font-size: 11px; }
1432
+ .nwe-phase.is-active .nwe-phase-label { color: var(--nwe-ink); }
1433
+ .nwe-pass {
1434
+ margin-top: 6px;
1435
+ padding: 0;
1436
+ border: 0;
1437
+ border-radius: 0;
1438
+ background: transparent;
1439
+ color: var(--nwe-ok);
1440
+ font-size: 12px;
1441
+ }
1442
+ .nwe-block {
1443
+ padding: 11px 13px;
1444
+ border: 1px solid color-mix(in srgb, var(--nwe-bad) 20%, var(--nwe-border));
1445
+ border-left: 3px solid var(--nwe-bad);
1446
+ border-radius: 7px;
1447
+ background: color-mix(in srgb, var(--nwe-bad) 4%, var(--nwe-surface));
1448
+ }
1449
+ .nwe-block-icon { width: 20px; height: 20px; border-radius: 5px; }
1450
+ .nwe-advisory {
1451
+ padding: 8px 0;
1452
+ border: 0;
1453
+ border-bottom: 1px solid var(--nwe-border);
1454
+ border-radius: 0;
1455
+ background: transparent;
1456
+ }
1457
+ .nwe-advisory:last-child { border-bottom: 0; }
1458
+ .nwe-advisory-code { color: var(--nwe-warn); }
1459
+ .nwe-advisory-hint { color: var(--nwe-muted); font-size: 11px; }
1460
+ .nwe-chapters { gap: 0; margin-top: 8px; border-top: 1px solid var(--nwe-border); }
1461
+ .nwe-chapter {
1462
+ border: 0;
1463
+ border-bottom: 1px solid var(--nwe-border);
1464
+ border-radius: 0;
1465
+ background: transparent;
1466
+ }
1467
+ .nwe-chapter.is-ok, .nwe-chapter.is-bad { border-left: 0; }
1468
+ .nwe-chapter:hover { background: color-mix(in srgb, var(--nwe-accent) 4%, transparent); }
1469
+ .nwe-chapter summary {
1470
+ grid-template-columns: 48px minmax(0, 1fr) auto 14px;
1471
+ gap: 12px;
1472
+ padding: 13px 4px;
1473
+ }
1474
+ .nwe-chapter-num { color: var(--nwe-accent); font-size: 11px; font-weight: 650; letter-spacing: .02em; }
1475
+ .nwe-chapter-title { display: block; font-size: 14px; font-weight: 600; }
1476
+ .nwe-chapter-stage { display: block; margin-top: 1px; color: var(--nwe-muted); font-size: 11px; }
1477
+ .nwe-chapter-meta { gap: 12px; color: var(--nwe-muted); font-size: 11px; }
1478
+ .nwe-score { width: 104px; }
1479
+ .nwe-score-value { font-size: 11px; }
1480
+ .nwe-scorebar { height: 3px; }
1481
+ .nwe-scorebar > span { background: var(--nwe-accent); }
1482
+ .nwe-badge { padding: 2px 6px; border-radius: 5px; font-size: 11px; }
1483
+ .nwe-badge.is-ok { border-color: color-mix(in srgb, var(--nwe-ok) 34%, transparent); color: var(--nwe-ok); }
1484
+ .nwe-badge.is-bad { border-color: color-mix(in srgb, var(--nwe-bad) 34%, transparent); color: var(--nwe-bad); }
1485
+ .nwe-chapter-extra {
1486
+ margin: 0;
1487
+ padding: 0 4px 12px 64px;
1488
+ color: var(--nwe-muted);
1489
+ font-size: 11px;
1490
+ line-height: 1.55;
1491
+ }
1492
+ .nwe-chapter-detail {
1493
+ margin: 0 4px 10px 64px;
1494
+ padding: 10px 12px;
1495
+ border: 0;
1496
+ border-radius: 6px;
1497
+ background: var(--nwe-surface);
1498
+ }
1499
+ .nwe-glance {
1500
+ padding: 15px 16px 14px;
1501
+ border: 1px solid var(--nwe-border);
1502
+ border-radius: 9px;
1503
+ background: color-mix(in srgb, var(--nwe-surface) 76%, var(--nwe-raised));
1504
+ }
1505
+ .nwe-glance-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
1506
+ .nwe-glance-title { margin: 8px 0 4px; font-size: 17px; font-weight: 650; line-height: 1.35; letter-spacing: -.02em; }
1507
+ .nwe-glance-text {
1508
+ display: -webkit-box;
1509
+ overflow: hidden;
1510
+ margin: 0;
1511
+ color: var(--nwe-muted);
1512
+ font-size: 12.5px;
1513
+ line-height: 1.65;
1514
+ -webkit-box-orient: vertical;
1515
+ -webkit-line-clamp: 3;
1516
+ }
1517
+ .nwe-glance-bar { overflow: hidden; height: 6px; margin-top: 14px; border-radius: 99px; background: var(--nwe-border); }
1518
+ .nwe-glance-bar > span { display: block; height: 100%; border-radius: inherit; background: var(--nwe-accent); }
1519
+ .nwe-glance-legend { display: flex; justify-content: space-between; margin-top: 5px; color: var(--nwe-muted); font-size: 11px; font-variant-numeric: tabular-nums; }
1520
+ .nwe-glance-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; margin: 15px 0 0; }
1521
+ .nwe-glance-facts > div { padding: 0 12px; border-left: 1px solid var(--nwe-border); }
1522
+ .nwe-glance-facts > div:first-child { padding-left: 0; border-left: 0; }
1523
+ .nwe-glance-facts dt { color: var(--nwe-muted); font-size: 11px; letter-spacing: .05em; }
1524
+ .nwe-glance-facts dd { margin: 2px 0 0; font-size: 15px; font-weight: 650; font-variant-numeric: tabular-nums; }
1525
+ .nwe-activity { gap: 0; margin-top: 18px; }
1526
+ .nwe-activity li { grid-template-columns: 62px minmax(0, 1fr) auto; gap: 8px; padding: 8px 0; border-bottom: 1px solid color-mix(in srgb, var(--nwe-border) 65%, transparent); font-size: 11px; }
1527
+ .nwe-activity li:last-child { border-bottom: 0; }
1528
+ .nwe-dossier { margin-top: 32px; padding-top: 13px; border-top: 1px solid var(--nwe-border); }
1529
+ .nwe-dossier > summary {
1530
+ display: flex;
1531
+ align-items: center;
1532
+ gap: 8px;
1533
+ padding: 4px 0;
1534
+ cursor: pointer;
1535
+ list-style: none;
1536
+ font-size: 14px;
1537
+ font-weight: 650;
1538
+ }
1539
+ .nwe-dossier > summary::-webkit-details-marker { display: none; }
1540
+ .nwe-dossier > summary::before { content: "+"; color: var(--nwe-accent); font-size: 16px; line-height: 1; }
1541
+ .nwe-dossier[open] > summary::before { content: "−"; }
1542
+ .nwe-dossier-note { margin-left: auto; color: var(--nwe-muted); font-size: 11px; font-weight: 400; }
1543
+ .nwe-dossier .nwe-section { margin-top: 12px; }
1544
+ .nwe-dossier .nwe-section-head { display: none; }
1545
+ .nwe-dossier .nwe-v6-grid {
1546
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1547
+ gap: 1px;
1548
+ overflow: hidden;
1549
+ border: 1px solid var(--nwe-border);
1550
+ border-radius: 8px;
1551
+ background: var(--nwe-border);
1552
+ }
1553
+ .nwe-dossier .nwe-v6-card { padding: 14px 16px; border: 0; border-radius: 0; background: var(--nwe-surface); }
1554
+ .nwe-dossier .nwe-v6-card[open] { grid-column: span 3; }
1555
+ .nwe-dossier .nwe-v6-card > summary { font-size: 12px; }
1556
+ .nwe-footer {
1557
+ margin-top: 36px;
1558
+ padding-top: 13px;
1559
+ border-top: 1px solid var(--nwe-border);
1560
+ color: var(--nwe-muted);
1561
+ font-size: 11px;
1562
+ }
1563
+ .nwe-footer code { font-size: 11px; }
1564
+ .nwe-empty { min-height: 0; padding: 24px; border-style: dashed; border-radius: 8px; background: color-mix(in srgb, var(--nwe-surface) 60%, transparent); }
1565
+ .nwe-state { min-height: 320px; border: 0; background: color-mix(in srgb, var(--nwe-surface) 55%, transparent); }
1566
+ .nwe-state-title { font-family: inherit; font-size: 20px; font-weight: 650; }
1567
+ .nwe-state-icon { width: 38px; height: 38px; border-radius: 11px; background: var(--nwe-accent-soft); color: var(--nwe-accent); }
1568
+ .nwe-modal { border-color: var(--nwe-border); border-radius: 14px; background: var(--nwe-surface); }
1569
+ .nwe-modal-head { border-bottom-color: var(--nwe-border); background: var(--nwe-surface); }
1570
+ .nwe-modal-title { font-family: inherit; font-size: 22px; font-weight: 650; letter-spacing: -.025em; }
1571
+ .nwe-tutorial-block h3 { font-family: inherit; font-size: 15px; font-weight: 650; }
1572
+ .nwe-step { border: 1px solid var(--nwe-border); border-left: 2px solid var(--nwe-accent); border-radius: 8px; background: transparent; }
1573
+ .nwe-step-no, .nwe-step-tool { color: var(--nwe-accent); }
1574
+ .nwe-tutorial-card { border-radius: 8px; background: color-mix(in srgb, var(--nwe-surface) 80%, var(--nwe-raised)); }
1575
+ .nwe-tutorial-card strong { color: var(--nwe-accent); }
1576
+ .nwe-tutorial-note strong { color: var(--nwe-accent); }
1577
+ .nwe-copy-block .nwe-copy { border-radius: 6px; }
1578
+ .nwe-chapter-toolbar, .nwe-section-search { border-radius: 7px; }
1579
+ .nwe-chapter-toolbar input, .nwe-chapter-toolbar select, .nwe-section-search input { border-radius: 6px; }
1580
+ .nwe-data-row { border-radius: 6px; background: transparent; }
1581
+ .nwe-quality-summary { font-size: 11px; }
1582
+ .nwe-dim { grid-template-columns: 68px minmax(0, 1fr) 52px; font-size: 11px; }
1583
+ @media (max-width: 980px) {
1584
+ .nwe-desk { grid-template-columns: 1fr; gap: 0; }
1585
+ .nwe-desk-side { display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, .7fr); gap: 24px; padding-left: 0; border-left: 0; border-top: 1px solid var(--nwe-border); margin-top: 24px; padding-top: 20px; }
1586
+ .nwe-dossier .nwe-v6-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1587
+ .nwe-dossier .nwe-v6-card[open] { grid-column: span 2; }
1588
+ }
1589
+ @media (max-width: 760px) {
1590
+ .nwe-root { padding: 16px 15px 24px; }
1591
+ .nwe-hero { align-items: flex-start; flex-direction: column; gap: 16px; padding-bottom: 17px; }
1592
+ .nwe-brand { gap: 11px; }
1593
+ .nwe-brand-mark { width: 34px; height: 34px; border-radius: 9px; font-size: 15px; }
1594
+ .nwe-title { font-size: 25px; }
1595
+ .nwe-revision { padding-left: 0; border-left: 0; }
1596
+ .nwe-actions { width: 100%; flex-wrap: wrap; min-width: 0; }
1597
+ .nwe-project { order: 0; flex: 1 0 100%; width: 100%; min-width: 0; }
1598
+ .nwe-project-select { flex: 1; max-width: none; min-width: 0; width: 100%; }
1599
+ .nwe-help, .nwe-refresh { flex: 1 1 0; min-width: 0; }
1600
+ .nwe-project-search { flex: 1 1 100%; width: auto; }
1601
+ .nwe-project-toggle { flex: 1 1 100%; }
1602
+ .nwe-refresh { flex: 1; justify-content: center; }
1603
+ .nwe-help { flex: 0 0 auto; }
1604
+ .nwe-next { grid-template-columns: 32px minmax(0, 1fr); gap: 11px; padding: 15px; }
1605
+ .nwe-next-icon { width: 32px; height: 32px; border-radius: 8px; }
1606
+ .nwe-next-title { font-size: 17px; }
1607
+ .nwe-next-tool { display: none; }
1608
+ .nwe-next-meta { display: none; }
1609
+ .nwe-next .nwe-copy { margin-top: 9px; }
1610
+ .nwe-tabs { gap: 20px; margin-top: 16px; }
1611
+ .nwe-tab { padding: 9px 0 10px; font-size: 12px; }
1612
+ .nwe-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 15px 0; padding-bottom: 15px; }
1613
+ .nwe-stat { padding: 0 14px; }
1614
+ .nwe-stat:nth-child(2) { border-right: 0; }
1615
+ .nwe-stat:nth-child(3) { padding-left: 0; }
1616
+ .nwe-stat:nth-child(4) { padding-right: 0; }
1617
+ .nwe-stat-value { font-size: 24px; }
1618
+ .nwe-desk-side { display: block; }
1619
+ .nwe-glance { padding: 14px; }
1620
+ .nwe-phases { grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: 13px; padding: 12px 0 8px; }
1621
+ .nwe-phase:nth-child(3n)::after { display: none; }
1622
+ .nwe-chapter summary { grid-template-columns: 40px minmax(0, 1fr) 14px; gap: 9px; padding: 12px 2px; }
1623
+ .nwe-chapter-meta { grid-column: 2 / 4; grid-row: 2; display: flex; justify-content: flex-start; flex-wrap: wrap; gap: 7px 12px; margin-top: 4px; }
1624
+ .nwe-chapter-meta > .nwe-badge { order: 4; }
1625
+ .nwe-score { width: 88px; }
1626
+ .nwe-chapter-extra { padding: 0 2px 11px 49px; }
1627
+ .nwe-chapter-detail { margin: 0 2px 9px 49px; }
1628
+ .nwe-dossier { margin-top: 25px; }
1629
+ .nwe-dossier-note { display: none; }
1630
+ .nwe-dossier .nwe-v6-grid { grid-template-columns: 1fr; }
1631
+ .nwe-dossier .nwe-v6-card[open] { grid-column: auto; }
1632
+ .nwe-footer { margin-top: 26px; flex-direction: column; }
1633
+ }
1634
+
1635
+ /* v9 card wall: every read-model tab owns the full canvas. */
1636
+ .nwe-root { max-width: 1460px; }
1637
+ .nwe-tab-intro {
1638
+ display: grid;
1639
+ grid-template-columns: 46px minmax(0, 1fr) auto;
1640
+ gap: 15px;
1641
+ align-items: start;
1642
+ margin: 24px 0 2px;
1643
+ padding: 0 0 18px;
1644
+ border-bottom: 1px solid var(--nwe-border);
1645
+ }
1646
+ .nwe-tab-intro-icon {
1647
+ display: grid;
1648
+ place-items: center;
1649
+ width: 46px;
1650
+ height: 46px;
1651
+ border: 1px solid color-mix(in srgb, var(--nwe-accent) 20%, var(--nwe-border));
1652
+ border-radius: 12px;
1653
+ background: var(--nwe-accent-soft);
1654
+ color: var(--nwe-accent);
1655
+ }
1656
+ .nwe-tab-intro-copy { min-width: 0; }
1657
+ .nwe-tab-intro-desc { max-width: 76ch; margin: 0; color: var(--nwe-muted); font-size: 12.5px; line-height: 1.65; overflow-wrap: anywhere; }
1658
+ .nwe-tab-intro-kicker { color: var(--nwe-muted); font-size: 10px; font-weight: 700; letter-spacing: .16em; }
1659
+ .nwe-tab-intro-title { margin: 2px 0 3px; font-size: 25px; font-weight: 700; line-height: 1.2; letter-spacing: -.035em; }
1660
+ .nwe-tab-intro-desc { max-width: 76ch; margin: 0; color: var(--nwe-muted); font-size: 12.5px; line-height: 1.65; overflow-wrap: anywhere; }
1661
+ .nwe-tab-intro-count {
1662
+ align-self: start;
1663
+ padding: 4px 8px;
1664
+ border: 1px solid var(--nwe-border);
1665
+ border-radius: 5px;
1666
+ color: var(--nwe-muted);
1667
+ font-size: 11px;
1668
+ font-variant-numeric: tabular-nums;
1669
+ white-space: nowrap;
1670
+ }
1671
+ .nwe-tab-panel { margin-top: 0; }
1672
+ .nwe-desk-single { grid-template-columns: minmax(0, 1fr); }
1673
+ .nwe-card-grid {
1674
+ display: grid;
1675
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1676
+ gap: 10px;
1677
+ align-items: start;
1678
+ margin-top: 9px;
1679
+ }
1680
+ .nwe-data-card {
1681
+ overflow: hidden;
1682
+ border: 1px solid var(--nwe-border);
1683
+ border-radius: 10px;
1684
+ background: color-mix(in srgb, var(--nwe-surface) 88%, var(--nwe-raised));
1685
+ transition: border-color .16s ease, background .16s ease, box-shadow .16s ease;
1686
+ }
1687
+ .nwe-data-card:hover { border-color: color-mix(in srgb, var(--nwe-accent) 30%, var(--nwe-border)); background: var(--nwe-surface); }
1688
+ .nwe-data-card[open] {
1689
+ border-color: color-mix(in srgb, var(--nwe-accent) 38%, var(--nwe-border));
1690
+ background: var(--nwe-surface);
1691
+ box-shadow: 0 12px 28px -24px color-mix(in srgb, #000 55%, transparent);
1692
+ }
1693
+ .nwe-data-card-head {
1694
+ display: grid;
1695
+ grid-template-columns: 36px minmax(0, 1fr) 15px;
1696
+ gap: 11px;
1697
+ align-items: start;
1698
+ padding: 13px 14px;
1699
+ cursor: pointer;
1700
+ list-style: none;
1701
+ }
1702
+ .nwe-data-card-head::-webkit-details-marker { display: none; }
1703
+ .nwe-data-card-head:focus-visible { outline: 2px solid var(--nwe-accent); outline-offset: -2px; }
1704
+ .nwe-data-card-icon {
1705
+ display: grid;
1706
+ place-items: center;
1707
+ width: 36px;
1708
+ height: 36px;
1709
+ border-radius: 9px;
1710
+ background: var(--nwe-accent-soft);
1711
+ color: var(--nwe-accent);
1712
+ }
1713
+ .nwe-data-card-icon.is-seed, .nwe-data-card-icon.is-clock { background: color-mix(in srgb, var(--nwe-warn) 10%, transparent); color: var(--nwe-warn); }
1714
+ .nwe-data-card-icon.is-link, .nwe-data-card-icon.is-check { background: color-mix(in srgb, var(--nwe-ok) 10%, transparent); color: var(--nwe-ok); }
1715
+ .nwe-data-card-icon.is-alert { background: color-mix(in srgb, var(--nwe-bad) 10%, transparent); color: var(--nwe-bad); }
1716
+ .nwe-data-card-heading { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
1717
+ .nwe-data-card-heading > strong { overflow-wrap: anywhere; font-size: 13px; font-weight: 650; line-height: 1.4; }
1718
+ .nwe-data-card-note {
1719
+ display: -webkit-box;
1720
+ overflow: hidden;
1721
+ color: var(--nwe-muted);
1722
+ font-size: 11.5px;
1723
+ line-height: 1.55;
1724
+ -webkit-box-orient: vertical;
1725
+ -webkit-line-clamp: 2;
1726
+ }
1727
+ .nwe-data-card-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
1728
+ .nwe-data-card-chips span {
1729
+ padding: 1px 5px;
1730
+ border: 1px solid color-mix(in srgb, var(--nwe-border) 80%, transparent);
1731
+ border-radius: 4px;
1732
+ color: var(--nwe-muted);
1733
+ font-size: 10.5px;
1734
+ line-height: 1.5;
1735
+ }
1736
+ .nwe-data-card-caret { color: var(--nwe-muted); font-size: 16px; line-height: 1; transition: transform .16s ease; }
1737
+ .nwe-data-card[open] .nwe-data-card-caret { transform: rotate(45deg); color: var(--nwe-accent); }
1738
+ .nwe-data-card-body {
1739
+ padding: 10px 14px 12px;
1740
+ border-top: 1px solid var(--nwe-border);
1741
+ background: color-mix(in srgb, var(--nwe-raised) 42%, transparent);
1742
+ }
1743
+ .nwe-data-card-body .nwe-kv { grid-template-columns: 84px minmax(0, 1fr); padding: 0; font-size: 11.5px; gap: 5px 11px; }
1744
+ .nwe-group-head { display: flex; align-items: center; gap: 8px; margin: 21px 0 8px; }
1745
+ .nwe-group-icon {
1746
+ display: grid;
1747
+ place-items: center;
1748
+ width: 26px;
1749
+ height: 26px;
1750
+ border: 1px solid var(--nwe-border);
1751
+ border-radius: 7px;
1752
+ background: var(--nwe-surface);
1753
+ color: var(--nwe-accent);
1754
+ }
1755
+ .nwe-group-head h3 { margin: 0; font-size: 13px; font-weight: 650; letter-spacing: -.01em; }
1756
+ .nwe-subsection { margin: 0; }
1757
+ .nwe-worldbook-layout, .nwe-leads-layout { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 22px; align-items: start; }
1758
+ .nwe-worldbook-layout > .nwe-subsection:first-child,
1759
+ .nwe-leads-layout > .nwe-subsection:first-child { grid-column: 1 / -1; }
1760
+ .nwe-worldbook-layout > .nwe-subsection .nwe-card-grid,
1761
+ .nwe-leads-layout > .nwe-subsection .nwe-card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1762
+ .nwe-worldbook-layout > .nwe-subsection:first-child .nwe-card-grid,
1763
+ .nwe-leads-layout > .nwe-subsection:first-child .nwe-card-grid { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
1764
+ .nwe-quality-layout { display: block; }
1765
+ .nwe-quality-kpis {
1766
+ display: grid;
1767
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1768
+ overflow: hidden;
1769
+ margin-top: 16px;
1770
+ border: 1px solid var(--nwe-border);
1771
+ border-radius: 11px;
1772
+ background: var(--nwe-surface);
1773
+ }
1774
+ .nwe-quality-kpi { padding: 15px 17px; border-right: 1px solid var(--nwe-border); }
1775
+ .nwe-quality-kpi:last-child { border-right: 0; }
1776
+ .nwe-quality-kpi-label { display: block; color: var(--nwe-muted); font-size: 10.5px; font-weight: 600; letter-spacing: .08em; }
1777
+ .nwe-quality-kpi-value { display: block; margin-top: 3px; font-size: 29px; font-weight: 650; line-height: 1.1; letter-spacing: -.045em; }
1778
+ .nwe-quality-kpi-value small { margin-left: 3px; color: var(--nwe-muted); font-size: 10px; font-weight: 400; letter-spacing: 0; }
1779
+ .nwe-quality-toolbar { display: flex; justify-content: flex-end; margin: 11px 0; }
1780
+ .nwe-dim-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 8px; margin-top: 14px; }
1781
+ .nwe-dim-card { padding: 12px 14px; border: 1px solid var(--nwe-border); border-radius: 8px; background: var(--nwe-surface); }
1782
+ .nwe-dim-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
1783
+ .nwe-dim-card-head span { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10.5px; color: var(--nwe-muted); }
1784
+ .nwe-dim-card-head strong { font-size: 11.5px; font-weight: 650; font-variant-numeric: tabular-nums; }
1785
+ .nwe-dim-card .nwe-v6-bar { display: block; margin-top: 8px; }
1786
+ .nwe-dim-card .nwe-v6-bar > span { display: block; height: 5px; border-radius: 99px; background: var(--nwe-border); }
1787
+ .nwe-dim-card .nwe-v6-bar i { display: block; height: 100%; border-radius: inherit; background: var(--nwe-accent); }
1788
+ .nwe-health-layout { display: block; }
1789
+ .nwe-health-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; margin-top: 16px; }
1790
+ .nwe-health-card {
1791
+ display: grid;
1792
+ grid-template-columns: 34px minmax(0, 1fr) auto;
1793
+ gap: 10px;
1794
+ align-items: start;
1795
+ padding: 13px 14px;
1796
+ border: 1px solid var(--nwe-border);
1797
+ border-radius: 9px;
1798
+ background: var(--nwe-surface);
1799
+ }
1800
+ .nwe-health-card.is-ok { border-color: color-mix(in srgb, var(--nwe-ok) 25%, var(--nwe-border)); }
1801
+ .nwe-health-card.is-bad { border-color: color-mix(in srgb, var(--nwe-bad) 28%, var(--nwe-border)); }
1802
+ .nwe-health-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 8px; background: color-mix(in srgb, var(--nwe-ok) 10%, transparent); color: var(--nwe-ok); }
1803
+ .nwe-health-card.is-bad .nwe-health-icon { background: color-mix(in srgb, var(--nwe-bad) 10%, transparent); color: var(--nwe-bad); }
1804
+ .nwe-health-copy { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
1805
+ .nwe-health-copy strong { font-size: 12.5px; }
1806
+ .nwe-health-copy span { color: var(--nwe-muted); font-size: 11px; line-height: 1.5; }
1807
+ .nwe-health-state { color: var(--nwe-ok); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9.5px; letter-spacing: .08em; }
1808
+ .nwe-health-card.is-bad .nwe-health-state { color: var(--nwe-bad); }
1809
+ .nwe-effort-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
1810
+ .nwe-effort-card { padding: 12px 14px; border: 1px solid var(--nwe-border); border-radius: 8px; background: var(--nwe-surface); }
1811
+ .nwe-effort-card span { display: block; color: var(--nwe-muted); font-size: 10.5px; }
1812
+ .nwe-effort-card strong { display: block; margin-top: 2px; font-size: 22px; font-weight: 650; line-height: 1.1; letter-spacing: -.035em; }
1813
+ .nwe-section-search {
1814
+ display: flex;
1815
+ align-items: center;
1816
+ gap: 8px;
1817
+ justify-content: flex-start;
1818
+ margin: 14px 0 0;
1819
+ padding: 7px 9px;
1820
+ border: 1px solid var(--nwe-border);
1821
+ border-radius: 7px;
1822
+ background: var(--nwe-surface);
1823
+ }
1824
+ .nwe-search-icon { display: grid; place-items: center; width: 20px; color: var(--nwe-muted); }
1825
+ .nwe-search-label { color: var(--nwe-muted); font-size: 11px; white-space: nowrap; }
1826
+ .nwe-section-search input { flex: 1 1 auto; max-width: 360px; min-height: 28px; border: 0; background: transparent; }
1827
+ .nwe-section-search input:focus-visible { outline: 2px solid var(--nwe-accent); outline-offset: 2px; }
1828
+ .nwe-chapter-cards-mobile { display: none; }
1829
+ .nwe-mobile-chapter {
1830
+ display: grid;
1831
+ grid-template-columns: 42px minmax(0, 1fr) auto;
1832
+ grid-template-rows: auto auto;
1833
+ gap: 3px 9px;
1834
+ align-items: center;
1835
+ width: 100%;
1836
+ padding: 11px 12px;
1837
+ border: 1px solid var(--nwe-border);
1838
+ border-radius: 8px;
1839
+ background: var(--nwe-surface);
1840
+ }
1841
+ .nwe-mobile-chapter-num { grid-row: 1 / 3; color: var(--nwe-accent); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; font-weight: 650; }
1842
+ .nwe-mobile-chapter-body { display: flex; grid-column: 2; grid-row: 1; flex-direction: column; gap: 2px; min-width: 0; }
1843
+ .nwe-mobile-chapter-body strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
1844
+ .nwe-mobile-chapter-body span { overflow: hidden; color: var(--nwe-muted); font-size: 10.5px; text-overflow: ellipsis; white-space: nowrap; }
1845
+ .nwe-mobile-chapter-score { grid-column: 3; grid-row: 1; color: var(--nwe-ink); font-size: 13px; font-weight: 650; font-variant-numeric: tabular-nums; }
1846
+ .nwe-mobile-chapter .nwe-refresh { grid-column: 2 / 4; grid-row: 2; justify-self: start; min-height: 26px; margin-top: 2px; padding: 3px 9px; font-size: 11px; }
1847
+ .nwe-chapter-table { border-collapse: separate; border-spacing: 0 6px; min-width: 720px; }
1848
+ .nwe-chapter-table thead th { padding: 4px 11px 7px; border: 0; background: transparent; font-size: 10.5px; letter-spacing: .04em; }
1849
+ .nwe-chapter-table tbody tr { background: var(--nwe-surface); box-shadow: 0 0 0 1px var(--nwe-border); }
1850
+ .nwe-chapter-table tbody tr:hover { box-shadow: 0 0 0 1px color-mix(in srgb, var(--nwe-accent) 35%, var(--nwe-border)); }
1851
+ .nwe-chapter-table tbody td { padding: 12px 11px; border: 0; }
1852
+ .nwe-chapter-table tbody td:first-child { border-radius: 7px 0 0 7px; border-left: 2px solid var(--nwe-accent); font-weight: 650; }
1853
+ .nwe-chapter-table tbody td:last-child { border-radius: 0 7px 7px 0; }
1854
+ .nwe-chapter-table tbody td.is-num { font-size: 11px; }
1855
+ @media (max-width: 980px) {
1856
+ .nwe-overview-charts { grid-template-columns: 1fr 1fr; }
1857
+ .nwe-tab-intro { grid-template-columns: 40px minmax(0, 1fr); }
1858
+ .nwe-tab-intro-icon { width: 40px; height: 40px; }
1859
+ .nwe-tab-intro-count { grid-column: 2; justify-self: start; }
1860
+ .nwe-quality-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1861
+ .nwe-quality-kpi:nth-child(2) { border-right: 0; }
1862
+ .nwe-quality-kpi:nth-child(-n + 2) { border-bottom: 1px solid var(--nwe-border); }
1863
+ .nwe-health-grid { grid-template-columns: 1fr; }
1864
+ }
1865
+ @media (max-width: 760px) {
1866
+ .nwe-tab-intro { gap: 11px; margin-top: 19px; padding-bottom: 15px; }
1867
+ .nwe-tab-intro-title { font-size: 21px; }
1868
+ .nwe-tab-intro-desc { font-size: 12px; }
1869
+ .nwe-overview-charts { grid-template-columns: 1fr; }
1870
+ .nwe-trend-chart, .nwe-word-chart { height: 170px; }
1871
+ .nwe-viz-empty { min-height: 170px; }
1872
+ .nwe-word-track { flex-basis: 120px; height: 120px; }
1873
+ .nwe-card-grid { grid-template-columns: 1fr; }
1874
+ .nwe-quality-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1875
+ .nwe-quality-kpi { padding: 13px 14px; }
1876
+ .nwe-quality-kpi-value { font-size: 25px; }
1877
+ .nwe-dim-grid { grid-template-columns: 1fr; }
1878
+ .nwe-effort-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1879
+ .nwe-worldbook-layout, .nwe-leads-layout { grid-template-columns: 1fr; gap: 0; }
1880
+ .nwe-worldbook-layout > .nwe-subsection:first-child,
1881
+ .nwe-leads-layout > .nwe-subsection:first-child { grid-column: auto; }
1882
+ .nwe-worldbook-layout > .nwe-subsection .nwe-card-grid,
1883
+ .nwe-leads-layout > .nwe-subsection .nwe-card-grid,
1884
+ .nwe-worldbook-layout > .nwe-subsection:first-child .nwe-card-grid,
1885
+ .nwe-leads-layout > .nwe-subsection:first-child .nwe-card-grid { grid-template-columns: 1fr; }
1886
+ .nwe-chapter-table { border-spacing: 0 7px; min-width: 680px; }
1887
+ .nwe-chapters-section .nwe-table-wrap { display: none; }
1888
+ .nwe-chapter-cards-mobile { display: grid; gap: 8px; margin-top: 10px; }
1889
+ }
1106
1890
  `;
1107
1891
  const icon = (size, path) => (0, react.createElement)("svg", {
1108
1892
  width: size,
@@ -1139,6 +1923,21 @@ window.__ModuleLoader__.load({
1139
1923
  strokeLinecap: "round",
1140
1924
  strokeLinejoin: "round"
1141
1925
  }));
1926
+ const GLYPH_PATHS = {
1927
+ spark: "M12 3.5l1.8 5.1 5.2 1.9-5.2 1.8L12 17.5l-1.8-5.2-5.2-1.8 5.2-1.9L12 3.5Z",
1928
+ person: "M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm-7 9a7 7 0 0 1 14 0",
1929
+ seed: "M12 21V9m0 0c0-3 2-5 5-5 0 3-2 5-5 5Zm0 4c0-2-2-4-5-4 0 3 2 4 5 4Z",
1930
+ clock: "M12 7v5l3 2M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z",
1931
+ link: "M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1",
1932
+ map: "m3 6 6-3 6 3 6-3v15l-6 3-6-3-6 3V6Zm6-3v15m6-12v15",
1933
+ book: "M4 5.5A2.5 2.5 0 0 1 6.5 3H20v15H6.5A2.5 2.5 0 0 0 4 20.5V5.5Zm0 15A2.5 2.5 0 0 1 6.5 18H20",
1934
+ chart: "M4 20V10m6 10V4m6 16v-7m4 7H2",
1935
+ shield: "M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3Z",
1936
+ heart: "M20.8 8.6c0 5-8.8 10.1-8.8 10.1S3.2 13.6 3.2 8.6A4.6 4.6 0 0 1 12 6a4.6 4.6 0 0 1 8.8 2.6ZM12 8v8m-4-4h8",
1937
+ check: "m5 12 4 4L19 6",
1938
+ alert: "M12 4 3 20h18L12 4Zm0 5v5m0 3h.01"
1939
+ };
1940
+ const sectionGlyph = (name, size = 18) => icon(size, GLYPH_PATHS[name]);
1142
1941
  const TUTORIAL_STEPS = [
1143
1942
  {
1144
1943
  title: "建一本书的项目",
@@ -1222,7 +2021,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1222
2021
  正文只通过 write_draft/revise 写;不要直接改 state/、chapters/ 或 outline/。
1223
2022
  每次提交前先领取当前阶段任务书,失败时按返回的 code 和 blocking 一次改完。`;
1224
2023
  const TUTORIAL_NEXT_PROMPT = `继续写当前项目的下一章。先调用 novel_pipeline(或 novel_status)确认当前阶段、项目根目录和下一步工具,不要猜阶段。
1225
- 如果当前会话还没绑定或不确定是哪本书:先用 /novel current 看会话绑定,再调用 novel_status 看 selection;需要切换时用 /novel use <路径|书名|.>,不要靠 active 猜。
2024
+ 如果当前会话还没绑定或不确定是哪本书:先让用户执行 /novel current 或 /novel use(Agent 不执行 slash 命令),同时调用 novel_status 看 selection;不要靠 active 猜。
1226
2025
  第一次带 project_root 的项目调用会成为当前会话的持久绑定;后续无 root 调用会继续这本书。纯文本的 novel_word_count 和 novel_diff 不读取项目,也不会建立绑定。
1227
2026
 
1228
2027
  如果下一章还没有章卡:用 novel_outline 只提交下一章的卡(按 num upsert),省略的已有卡会保持不动;不要重交全量,也不要用空数组清空。要删卡必须显式传 remove。
@@ -1234,18 +2033,18 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1234
2033
  const stat = (label, value, unit) => (0, react.createElement)("div", {
1235
2034
  className: "nwe-stat",
1236
2035
  key: label
1237
- }, (0, react.createElement)("div", { className: "nwe-stat-label" }, label), (0, react.createElement)("div", { className: "nwe-stat-value" }, value, (0, react.createElement)("span", { className: "nwe-stat-unit" }, unit)));
1238
- const chapterCard = (row) => (0, react.createElement)("li", { key: row.num }, (0, react.createElement)("details", { className: `nwe-chapter is-${row.tone}` }, (0, react.createElement)("summary", { "aria-label": `第 ${row.num} 章 ${row.title},${row.badge},${row.score}` }, (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "nwe-chapter-num" }, `CH ${row.num.padStart(2, "0")}`)), (0, react.createElement)("div", { style: { minWidth: 0 } }, (0, react.createElement)("div", { className: "nwe-chapter-title" }, row.title), (0, react.createElement)("div", { className: "nwe-chapter-stage" }, row.stageLabel)), (0, react.createElement)("div", { className: "nwe-chapter-meta" }, (0, react.createElement)("span", null, `${row.words} 字`), (0, react.createElement)("div", { className: "nwe-score" }, (0, react.createElement)("div", { className: "nwe-score-line" }, (0, react.createElement)("span", { className: "nwe-score-value" }, row.score), (0, react.createElement)("span", { className: "nwe-chapter-stage" }, `${row.scorePercent}%`)), (0, react.createElement)("div", {
2036
+ }, (0, react.createElement)("div", { className: "nwe-stat-label" }, label), (0, react.createElement)("div", { className: "nwe-stat-value" }, value, (0, react.createElement)("span", { className: `nwe-stat-unit${unit.length > 8 ? " is-note" : ""}` }, unit)));
2037
+ const chapterCard = (row) => (0, react.createElement)("li", { key: row.num }, (0, react.createElement)("details", { className: `nwe-chapter is-${row.tone}` }, (0, react.createElement)("summary", { "aria-label": `第 ${row.num} 章 ${row.title},${row.badge},${row.score}` }, (0, react.createElement)("span", null, (0, react.createElement)("span", { className: "nwe-chapter-num" }, `CH ${row.num.padStart(2, "0")}`)), (0, react.createElement)("span", { style: { minWidth: 0 } }, (0, react.createElement)("span", { className: "nwe-chapter-title" }, row.title), (0, react.createElement)("span", { className: "nwe-chapter-stage" }, row.stageLabel)), (0, react.createElement)("span", { className: "nwe-chapter-meta" }, (0, react.createElement)("span", null, `${row.words} 字`), (0, react.createElement)("span", { className: "nwe-score" }, (0, react.createElement)("span", { className: "nwe-score-line" }, (0, react.createElement)("span", { className: "nwe-score-value" }, row.score), (0, react.createElement)("span", { className: "nwe-chapter-stage" }, row.scorePercent === null ? "未评审" : `${row.scorePercent}%`)), row.scoreValue === null ? null : (0, react.createElement)("span", {
1239
2038
  className: "nwe-scorebar",
1240
2039
  role: "progressbar",
1241
2040
  "aria-valuemin": 0,
1242
2041
  "aria-valuemax": 150,
1243
2042
  "aria-valuenow": row.scoreValue,
1244
2043
  "aria-label": `${row.title} 评分`
1245
- }, (0, react.createElement)("span", { style: { width: `${row.scorePercent}%` } }))), (0, react.createElement)("span", { className: `nwe-badge is-${row.tone}` }, row.badge)), (0, react.createElement)("span", {
2044
+ }, (0, react.createElement)("span", { style: { width: `${row.scorePercent ?? 0}%` } }))), (0, react.createElement)("span", { className: `nwe-badge is-${row.tone}` }, row.badge)), (0, react.createElement)("span", {
1246
2045
  className: "nwe-chevron",
1247
2046
  "aria-hidden": "true"
1248
- }, "›")), row.details === "" ? null : (0, react.createElement)("div", { className: "nwe-chapter-detail" }, row.details), (0, react.createElement)("div", { className: "nwe-chapter-extra" }, `${row.sceneType || "—"} · 对话 ${row.dialogueRatio === null ? "—" : `${Math.round(row.dialogueRatio * 100)}%`} · 钩子 ${row.hookStrength ?? "—"} · 终检 ${row.finalCheck ?? "—"} · 评 ${row.reviews} / 修 ${row.revisions} / 检 ${row.checks} / 回滚 ${row.rollbacks} · 归档 ${row.archivedAt ?? "—"}`)));
2047
+ }, "›")), row.details === "" ? null : (0, react.createElement)("div", { className: "nwe-chapter-detail" }, row.details), (0, react.createElement)("div", { className: "nwe-chapter-extra" }, `${row.sceneType || "—"} · 对话 ${row.dialogueRatio === null ? "—" : `${Math.round(row.dialogueRatio * 100)}%`} · 钩子 ${row.hookStrength ?? "—"} · 终检 ${finalCheckLabel(row.finalCheck)} · 评 ${row.reviews} / 修 ${row.revisions} / 检 ${row.checks} / 回滚 ${row.rollbacks} · 归档 ${localTime(row.archivedAt)}`)));
1249
2048
  const phases = (view) => (0, react.createElement)("ol", {
1250
2049
  className: "nwe-phases",
1251
2050
  "aria-label": "创作阶段"
@@ -1294,7 +2093,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1294
2093
  }, `${label} ${value}`))), (0, react.createElement)("div", {
1295
2094
  className: "nwe-book-head",
1296
2095
  style: { marginTop: 12 }
1297
- }, (0, react.createElement)("span", { className: "nwe-book-title" }, "质量趋势"), (0, react.createElement)("span", { className: "nwe-book-note" }, bars.length === 0 ? "—" : `${view.quality.lastTenAverage.toFixed(1)} 近十均分 · 趋势 ${view.quality.trendText} · ${view.quality.samples} 章 / ${view.quality.attempts} 评`)), (0, react.createElement)("div", {
2096
+ }, (0, react.createElement)("span", { className: "nwe-book-title" }, "质量趋势"), (0, react.createElement)("span", { className: "nwe-book-note" }, bars.length === 0 ? "—" : `${view.quality.lastTenAverage === null ? "—" : view.quality.lastTenAverage.toFixed(1)} 近十均分 · 趋势 ${view.quality.samples === 0 ? "见质量页" : view.quality.trendText} · ${view.quality.samples === 0 ? "可读评审 —" : `${view.quality.samples} 章 / ${view.quality.attempts} 评`}`)), (0, react.createElement)("div", {
1298
2097
  className: "nwe-bars",
1299
2098
  role: "img",
1300
2099
  "aria-label": "最近十章评分"
@@ -1305,9 +2104,19 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1305
2104
  title: `${score}/150`
1306
2105
  }))), (0, react.createElement)("div", { className: `nwe-health${view.health.ok ? "" : " is-bad"}` }, (0, react.createElement)("span", { "aria-hidden": "true" }, view.health.ok ? "✓" : "!"), (0, react.createElement)("span", null, view.health.ok ? "项目健康:没有阻塞或损坏状态" : `需要处理:${view.health.blocking} 条门控 · ${view.health.stateErrors} 个损坏文档${view.health.pending ? " · 有 pending 事务" : ""}`)))));
1307
2106
  };
2107
+ const localTime = (value) => {
2108
+ if (value === null || value === void 0 || value === "") return "—";
2109
+ const parsed = new Date(value);
2110
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString("zh-CN", {
2111
+ month: "2-digit",
2112
+ day: "2-digit",
2113
+ hour: "2-digit",
2114
+ minute: "2-digit"
2115
+ });
2116
+ };
1308
2117
  const activitySection = (view) => {
1309
2118
  if (view.activity.length === 0) return null;
1310
- return (0, react.createElement)("section", { className: "nwe-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "最近活动"), (0, react.createElement)("span", { className: "nwe-section-note" }, "来自事务日志")), (0, react.createElement)("ul", { className: "nwe-activity" }, ...view.activity.slice(-5).map((row, index) => (0, react.createElement)("li", { key: `${row.at}-${row.op}-${index}` }, (0, react.createElement)("span", null, row.at.slice(0, 16).replace("T", " ")), (0, react.createElement)("code", null, row.op), (0, react.createElement)("span", null, row.chapter === 0 ? row.status : `第 ${row.chapter} 章 · ${row.status}`)))));
2119
+ return (0, react.createElement)("section", { className: "nwe-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "最近活动"), (0, react.createElement)("span", { className: "nwe-section-note" }, "来自事务日志")), (0, react.createElement)("ul", { className: "nwe-activity" }, ...view.activity.slice(-5).map((row, index) => (0, react.createElement)("li", { key: `${row.at}-${row.op}-${index}` }, (0, react.createElement)("time", { dateTime: row.at }, localTime(row.at)), (0, react.createElement)("code", null, row.op), (0, react.createElement)("span", null, row.chapter === 0 ? statusLabel(row.status) : `第 ${row.chapter} 章 · ${statusLabel(row.status)}`)))));
1311
2120
  };
1312
2121
  const advisoriesSection = (view) => {
1313
2122
  if (view.advisories.length === 0 && view.acknowledgedAdvisories === 0) return null;
@@ -1326,7 +2135,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1326
2135
  return (0, react.createElement)("section", {
1327
2136
  className: `nwe-section nwe-pending-section${partial ? " nwe-pending-error" : ""}`,
1328
2137
  "aria-label": "待恢复事务"
1329
- }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, partial ? "事务部分完成" : "待恢复事务"), (0, react.createElement)("span", { className: "nwe-section-note" }, view.pending.state)), (0, react.createElement)("p", { className: "nwe-pending-detail" }, view.pending.detail), (0, react.createElement)("p", { className: "nwe-pending-hint" }, partial ? `检查 backups 与 .staging/${view.pending.txid}/before/,不要整项目恢复。` : "下一次写操作会自动收尾;只读面不会改动事务。"));
2138
+ }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, partial ? "事务部分完成" : "待恢复事务"), (0, react.createElement)("span", { className: "nwe-section-note" }, partial ? "部分完成" : "待恢复")), (0, react.createElement)("p", { className: "nwe-pending-detail" }, view.pending.detail), (0, react.createElement)("p", { className: "nwe-pending-hint" }, partial ? `检查 backups 与 .staging/${view.pending.txid}/before/,不要整项目恢复。` : "下一次写操作会自动收尾;只读面不会改动事务。"));
1330
2139
  };
1331
2140
  const legacySection = (view) => {
1332
2141
  if (view.legacy.length === 0) return null;
@@ -1338,7 +2147,14 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1338
2147
  key: `${item.code}-${item.detail}`
1339
2148
  }, (0, react.createElement)("code", null, item.code), (0, react.createElement)("span", null, item.detail)))));
1340
2149
  };
1341
- const footer = () => (0, react.createElement)("footer", { className: "nwe-footer" }, (0, react.createElement)("span", null, "只读面板 · 数据来自引擎 · 每 15 秒自动同步"), (0, react.createElement)("code", null, "novel-workshop"));
2150
+ const footer = (view, onRefresh) => (0, react.createElement)("footer", { className: "nwe-footer" }, (0, react.createElement)("span", {
2151
+ role: "status",
2152
+ "aria-live": "polite"
2153
+ }, view.syncError !== void 0 && view.syncError !== null && view.syncError !== "" ? `同步失败:${view.syncError}` : view.lastSync !== void 0 && view.lastSync !== null ? `只读面板 · 数据来自引擎 · 更新于 ${new Date(view.lastSync).toLocaleTimeString("zh-CN")}` : "只读面板 · 数据来自引擎 · 等待首次同步"), (0, react.createElement)("span", { className: "nwe-footer-actions" }, view.syncError !== void 0 && view.syncError !== null && view.syncError !== "" ? (0, react.createElement)("button", {
2154
+ type: "button",
2155
+ className: "nwe-copy",
2156
+ onClick: onRefresh
2157
+ }, "重试") : null, (0, react.createElement)("code", null, "novel-workshop")));
1342
2158
  const loadingState = () => (0, react.createElement)("div", {
1343
2159
  className: "nwe-section",
1344
2160
  role: "status",
@@ -1405,7 +2221,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1405
2221
  onChange: (event) => {
1406
2222
  onSelect(event.target.value === "" ? null : event.target.value);
1407
2223
  }
1408
- }, view.selectedRoot === null ? (0, react.createElement)("option", { value: "" }, "选择项目…") : null, ...groups.filter((group) => group.items.length > 0).map((group) => (0, react.createElement)("optgroup", {
2224
+ }, (0, react.createElement)("option", { value: "" }, view.selectedRoot === null ? "跟随当前项目(自动)" : "跟随当前项目"), ...groups.filter((group) => group.items.length > 0).map((group) => (0, react.createElement)("optgroup", {
1409
2225
  key: group.key,
1410
2226
  label: group.label
1411
2227
  }, ...group.items.map((project) => (0, react.createElement)("option", {
@@ -1420,10 +2236,10 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1420
2236
  const choosePanel = (view, onSelect) => {
1421
2237
  const preferred = view.projects.filter((project) => project.group !== "missing");
1422
2238
  const shown = preferred.length > 0 ? preferred : view.projects;
1423
- return (0, react.createElement)("div", {
1424
- className: "nwe-state",
2239
+ return (0, react.createElement)("div", { className: "nwe-state" }, (0, react.createElement)("div", { className: "nwe-state-icon" }, stateIcon()), (0, react.createElement)("div", { className: "nwe-state-title" }, "发现多本书,先选一本"), (0, react.createElement)("div", {
2240
+ className: "nwe-state-text",
1425
2241
  role: "status"
1426
- }, (0, react.createElement)("div", { className: "nwe-state-icon" }, stateIcon()), (0, react.createElement)("div", { className: "nwe-state-title" }, "发现多本书,先选一本"), (0, react.createElement)("div", { className: "nwe-state-text" }, view.subtitle), (0, react.createElement)("div", { className: "nwe-project-choices" }, ...shown.map((project) => (0, react.createElement)("button", {
2242
+ }, view.subtitle), (0, react.createElement)("div", { className: "nwe-project-choices" }, ...shown.map((project) => (0, react.createElement)("button", {
1427
2243
  key: project.root,
1428
2244
  type: "button",
1429
2245
  className: "nwe-project-choice",
@@ -1481,6 +2297,28 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1481
2297
  className: "nwe-step",
1482
2298
  key: step.title
1483
2299
  }, (0, react.createElement)("div", { className: "nwe-step-head" }, (0, react.createElement)("span", { className: "nwe-step-no" }, String(index + 1).padStart(2, "0")), (0, react.createElement)("h4", null, step.title), (0, react.createElement)("code", { className: "nwe-step-tool" }, step.tool)), (0, react.createElement)("p", null, step.body), (0, react.createElement)("p", null, (0, react.createElement)("strong", null, "注意:"), step.note));
2300
+ const CopyPrompt = ({ text, label }) => {
2301
+ const [status, setStatus] = (0, react.useState)("");
2302
+ const copy = (0, react.useCallback)(async () => {
2303
+ try {
2304
+ const clipboard = globalThis.navigator?.clipboard;
2305
+ if (clipboard?.writeText === void 0) throw new Error("clipboard unavailable");
2306
+ await clipboard.writeText(text);
2307
+ setStatus("已复制到剪贴板");
2308
+ } catch {
2309
+ setStatus("复制失败:请手动选择下方文本,或检查浏览器剪贴板权限");
2310
+ }
2311
+ }, [text]);
2312
+ return (0, react.createElement)("div", { className: "nwe-copy-block" }, (0, react.createElement)("pre", { className: "nwe-prompt" }, text), (0, react.createElement)("div", { className: "nwe-copy-row" }, (0, react.createElement)("button", {
2313
+ type: "button",
2314
+ className: "nwe-copy",
2315
+ onClick: () => void copy()
2316
+ }, label), (0, react.createElement)("span", {
2317
+ className: "nwe-copy-status",
2318
+ role: "status",
2319
+ "aria-live": "polite"
2320
+ }, status)));
2321
+ };
1484
2322
  const tutorialDialog = (onClose, closeRef) => (0, react.createElement)("div", {
1485
2323
  className: "nwe-modal-backdrop",
1486
2324
  onMouseDown: (event) => {
@@ -1519,7 +2357,13 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1519
2357
  ref: closeRef,
1520
2358
  onClick: onClose,
1521
2359
  "aria-label": "关闭使用教程"
1522
- }, (0, react.createElement)("span", { "aria-hidden": "true" }, "×"))), (0, react.createElement)("div", { className: "nwe-modal-body" }, (0, react.createElement)("div", { className: "nwe-tutorial-intro" }, (0, react.createElement)("p", null, "网文工坊不是一个“帮你想剧情”的按钮,而是一台有状态的写作引擎:你提供内容,引擎负责阶段、字数、门控、评审、归档和文件。"), (0, react.createElement)("p", null, "每一步都先领取当前阶段的任务书,再提交;提交后看引擎返回的 blocking 与下一步。面板只读,真正的写操作都在对话里的工具调用中。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "开始前:记住三件事"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "只用引擎入口写"), (0, react.createElement)("span", null, "正文只走 novel_write_draft / novel_revise;状态只走工具。手改 chapters/、state/、outline/ 会破坏事务与校验。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "每一步先领 brief"), (0, react.createElement)("span", null, "任务书携带当前上下文和 digest;跳过它会撞 PG-01。任务书过期就重新领取,不要复用旧上下文。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "失败先读 code"), (0, react.createElement)("span", null, "参数错改参数,门控没过改正文,系统故障查目录/稍后重试。blocking 会一次列全,一次改完再交。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "用哪个会话:"), " 想获得完整的引擎保证,请新开一个「网文工坊」预设会话;普通会话里模型可能还挂着文件工具,能绕过引擎直接改稿。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一本书的完整流程"), (0, react.createElement)("p", null, "前四步是开书准备,后面是每章循环。第一次先按顺序走完一章,熟悉后再批量推进整本。"), (0, react.createElement)("ol", { className: "nwe-steps" }, ...TUTORIAL_STEPS.map((step, index) => tutorialStep(step, index)))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "面板怎么看"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "顶部统计"), (0, react.createElement)("span", null, "已归档章、总字数、平均分、待处理门控。数字来自引擎,不是正文估算。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "九阶段轨与下一步"), (0, react.createElement)("span", null, "亮起的阶段是当前进度,“下一步”卡会直接给出该调用的工具名。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "章节与门控"), (0, react.createElement)("span", null, "章节卡展开可看字数、评分与状态;门控区按 code/detail/hint 列出所有阻塞。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "刷新:"), " 面板每 15 秒静默同步一次,也可以点右上角刷新。它是只读投影;不会替你改稿、归档或修状态。"), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "项目状态:"), "选择器里 choose 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;不是项目表示目录还在但没有 novel.yaml 或项目状态;项目不可读表示权限或 IO 错误,先修权限或换一本。面板选择只影响查看,不改变任何会话的写作绑定。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "卡住时先看这里"), (0, react.createElement)("ul", { className: "nwe-faq" }, (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "为什么一直让我先领 brief?"), (0, react.createElement)("p", null, "这是正常门控。任务书决定当前阶段可用的字段与上下文 digest;先按返回值调用 novel_brief,再继续。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "门控失败,改一条还是全改?"), (0, react.createElement)("p", null, "一次看完所有 blocking,再一起改。逐条试会反复烧掉阶段轮次;错误里的 code 会告诉你属于参数、门控还是系统故障。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "字数和评分为什么对不上?"), (0, react.createElement)("p", null, "字数以 engine 返回为准,可用 novel_word_count 核对;review 的 total 必须等于 150 减全部扣分,不能另算一套。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "P4/P6 过不去怎么办?"), (0, react.createElement)("p", null, "P4 有 blocker 就领 P5 任务书修订,再回 P4 重评;P6 失败同样回修订。不要 waive blocker,也不要只跑半个 scope 就归档。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "想改已经归档的章节?"), (0, react.createElement)("p", null, "先 novel_reopen,带非空 reason。它会从归档备份恢复,重开后重新走 P3→P8;不要直接编辑归档卡或正文文件。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "状态文档坏了能手工修吗?"), (0, react.createElement)("p", null, "先用 novel_state 看 raw 和 next;需要修复时用 novel_amend,并在 reason 写清原因。手改 YAML 会丢失事务保护。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "有多个项目,面板看的是哪本?"), (0, react.createElement)("p", null, "面板用顶部项目选择器切换查看对象;选择只改 ?root= 和浏览器 localStorage,不调用 activate、不写会话绑定。写作项目请用 /novel use 或显式 project_root。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "选错书、想清掉绑定怎么办?"), (0, react.createElement)("p", null, "先用 /novel current 看当前会话绑定;要换书用 /novel use <路径|书名|.>,要清掉只影响当前会话的绑定用 /novel unbind。不要靠 active 猜,也不要手改 state/session-bindings.yaml。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "扫描到了,为什么还没切过去?"), (0, react.createElement)("p", null, "scan 只读发现;scan --register 只登记项目,不选择、不改 active、不绑会话。真正切换写作项目要用 /novel use,或第一次调用显式传 project_root。")))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "可以直接复制给模型的起手话术"), (0, react.createElement)("pre", { className: "nwe-prompt" }, TUTORIAL_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "续写下一章时,直接复制这段给 Agent"), (0, react.createElement)("p", null, "这段会先确认状态,再处理缺章卡和目标章数已满,最后按 P1→P8 推进;归档前会要求你确认 state_delta。"), (0, react.createElement)("pre", { className: "nwe-prompt" }, TUTORIAL_NEXT_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一章完成检查清单"), (0, react.createElement)("ul", { className: "nwe-checklist" }, (0, react.createElement)("li", null, "本章 P1/P2 计划已提交,P3 正文已通过格式与字数门控。"), (0, react.createElement)("li", null, "P4 评审 total 对账无误,没有未修 blocker。"), (0, react.createElement)("li", null, "P6 五个语义项都有不同句子的正文引用,完整 scope 通过。"), (0, react.createElement)("li", null, "P8 任务书当前,novel_archive 带完整 state_delta 成功。"), (0, react.createElement)("li", null, "面板出现已归档章节,门控区没有新的阻塞。")))), (0, react.createElement)("div", { className: "nwe-modal-foot" }, "教程基于 0.2.8 · 具体字段以当前任务书和工具返回为准 · 面板只读")));
2360
+ }, (0, react.createElement)("span", { "aria-hidden": "true" }, "×"))), (0, react.createElement)("div", { className: "nwe-modal-body" }, (0, react.createElement)("div", { className: "nwe-tutorial-intro" }, (0, react.createElement)("p", null, "网文工坊不是一个“帮你想剧情”的按钮,而是一台有状态的写作引擎:你提供内容,引擎负责阶段、字数、门控、评审、归档和文件。"), (0, react.createElement)("p", null, "每一步都先领取当前阶段的任务书,再提交;提交后看引擎返回的 blocking 与下一步。面板只读,真正的写操作都在对话里的工具调用中。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "开始前:记住三件事"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "只用引擎入口写"), (0, react.createElement)("span", null, "正文只走 novel_write_draft / novel_revise;状态只走工具。手改 chapters/、state/、outline/ 会破坏事务与校验。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "每一步先领 brief"), (0, react.createElement)("span", null, "任务书携带当前上下文和 digest;跳过它会撞 PG-01。任务书过期就重新领取,不要复用旧上下文。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "失败先读 code"), (0, react.createElement)("span", null, "参数错改参数,门控没过改正文,系统故障查目录/稍后重试。blocking 会一次列全,一次改完再交。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "用哪个会话:"), " 想获得完整的引擎保证,请新开一个「网文工坊」预设会话;普通会话里模型可能还挂着文件工具,能绕过引擎直接改稿。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一本书的完整流程"), (0, react.createElement)("p", null, "前四步是开书准备,后面是每章循环。第一次先按顺序走完一章,熟悉后再批量推进整本。"), (0, react.createElement)("ol", { className: "nwe-steps" }, ...TUTORIAL_STEPS.map((step, index) => tutorialStep(step, index)))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "面板怎么看"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "顶部统计"), (0, react.createElement)("span", null, "已归档章、总字数、平均分、待处理门控。数字来自引擎,不是正文估算。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "九阶段轨与下一步"), (0, react.createElement)("span", null, "亮起的阶段是当前进度,“下一步”卡会直接给出该调用的工具名。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "章节与门控"), (0, react.createElement)("span", null, "章节卡展开可看字数、评分与状态;门控区按 code/detail/hint 列出所有阻塞。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "刷新:"), " 面板每 15 秒静默同步一次,也可以点右上角刷新。它是只读投影;不会替你改稿、归档或修状态。"), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "项目状态:"), "选择器里 choose 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;不是项目表示目录还在但没有 novel.yaml 或项目状态;项目不可读表示权限或 IO 错误,先修权限或换一本。面板选择只影响查看,不改变任何会话的写作绑定。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "卡住时先看这里"), (0, react.createElement)("ul", { className: "nwe-faq" }, (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "为什么一直让我先领 brief?"), (0, react.createElement)("p", null, "这是正常门控。任务书决定当前阶段可用的字段与上下文 digest;先按返回值调用 novel_brief,再继续。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "门控失败,改一条还是全改?"), (0, react.createElement)("p", null, "一次看完所有 blocking,再一起改。逐条试会反复烧掉阶段轮次;错误里的 code 会告诉你属于参数、门控还是系统故障。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "字数和评分为什么对不上?"), (0, react.createElement)("p", null, "字数以 engine 返回为准,可用 novel_word_count 核对;review 的 total 必须等于 150 减全部扣分,不能另算一套。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "P4/P6 过不去怎么办?"), (0, react.createElement)("p", null, "P4 有 blocker 就领 P5 任务书修订,再回 P4 重评;P6 失败同样回修订。不要 waive blocker,也不要只跑半个 scope 就归档。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "想改已经归档的章节?"), (0, react.createElement)("p", null, "先 novel_reopen,带非空 reason。它会从归档备份恢复,重开后重新走 P3→P8;不要直接编辑归档卡或正文文件。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "状态文档坏了能手工修吗?"), (0, react.createElement)("p", null, "先用 novel_state 看 raw 和 next;需要修复时用 novel_amend,并在 reason 写清原因。手改 YAML 会丢失事务保护。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "有多个项目,面板看的是哪本?"), (0, react.createElement)("p", null, "面板用顶部项目选择器切换查看对象;选择只改 ?root= 和浏览器 localStorage,不调用 activate、不写会话绑定。写作项目请用 /novel use 或显式 project_root。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "选错书、想清掉绑定怎么办?"), (0, react.createElement)("p", null, "先用 /novel current 看当前会话绑定;要换书用 /novel use <路径|书名|.>,要清掉只影响当前会话的绑定用 /novel unbind。不要靠 active 猜,也不要手改 state/session-bindings.yaml。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "扫描到了,为什么还没切过去?"), (0, react.createElement)("p", null, "scan 只读发现;scan --register 只登记项目,不选择、不改 active、不绑会话。真正切换写作项目要用 /novel use,或第一次调用显式传 project_root。")))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "可以直接复制给模型的起手话术"), (0, react.createElement)(CopyPrompt, {
2361
+ text: TUTORIAL_PROMPT,
2362
+ label: "复制起手话术"
2363
+ })), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "续写下一章时,直接复制这段给 Agent"), (0, react.createElement)("p", null, "这段会先确认状态,再处理缺章卡和目标章数已满,最后按 P1→P8 推进;归档前会要求你确认 state_delta。"), (0, react.createElement)(CopyPrompt, {
2364
+ text: TUTORIAL_NEXT_PROMPT,
2365
+ label: "复制续写指令"
2366
+ })), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一章完成检查清单"), (0, react.createElement)("ul", { className: "nwe-checklist" }, (0, react.createElement)("li", null, "本章 P1/P2 计划已提交,P3 正文已通过格式与字数门控。"), (0, react.createElement)("li", null, "P4 评审 total 对账无误,没有未修 blocker。"), (0, react.createElement)("li", null, "P6 五个语义项都有不同句子的正文引用,完整 scope 通过。"), (0, react.createElement)("li", null, "P8 任务书当前,novel_archive 带完整 state_delta 成功。"), (0, react.createElement)("li", null, "面板出现已归档章节,门控区没有新的阻塞。")))), (0, react.createElement)("div", { className: "nwe-modal-foot" }, `教程基于 ${PANEL_VERSION} · 具体字段以当前任务书和工具返回为准 · 面板只读`)));
1523
2367
  const NovelHelp = () => {
1524
2368
  const [open, setOpen] = (0, react.useState)(false);
1525
2369
  const triggerRef = (0, react.useRef)(null);
@@ -1539,6 +2383,18 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1539
2383
  wasOpen.current = true;
1540
2384
  const previousOverflow = doc.body.style.overflow;
1541
2385
  doc.body.style.overflow = "hidden";
2386
+ const panelRoot = triggerRef.current?.closest("[data-plugin=\"dsh-novel-write-engine\"]") ?? doc.querySelector("[data-plugin=\"dsh-novel-write-engine\"]");
2387
+ const inerted = [];
2388
+ if (panelRoot !== null) for (const child of Array.from(panelRoot.children)) {
2389
+ if (child.classList.contains("nwe-modal-backdrop")) continue;
2390
+ inerted.push({
2391
+ element: child,
2392
+ inert: child.getAttribute("inert"),
2393
+ aria: child.getAttribute("aria-hidden")
2394
+ });
2395
+ child.setAttribute("inert", "");
2396
+ child.setAttribute("aria-hidden", "true");
2397
+ }
1542
2398
  const onKeyDown = (event) => {
1543
2399
  if (event.key === "Escape") {
1544
2400
  event.preventDefault();
@@ -1550,8 +2406,18 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1550
2406
  return () => {
1551
2407
  doc.removeEventListener("keydown", onKeyDown);
1552
2408
  doc.body.style.overflow = previousOverflow;
2409
+ for (const item of inerted) {
2410
+ if (item.inert === null) item.element.removeAttribute("inert");
2411
+ else item.element.setAttribute("inert", item.inert);
2412
+ if (item.aria === null) item.element.removeAttribute("aria-hidden");
2413
+ else item.element.setAttribute("aria-hidden", item.aria);
2414
+ }
1553
2415
  };
1554
2416
  }, [open]);
2417
+ const doc = globalThis.document;
2418
+ const panelRoot = triggerRef.current?.closest("[data-plugin=\"dsh-novel-write-engine\"]") ?? doc?.querySelector("[data-plugin=\"dsh-novel-write-engine\"]") ?? null;
2419
+ const dialog = open ? tutorialDialog(close, closeRef) : null;
2420
+ const overlay = dialog !== null && panelRoot !== null ? (0, react_dom.createPortal)(dialog, panelRoot) : dialog;
1555
2421
  return (0, react.createElement)(react.Fragment, null, (0, react.createElement)("button", {
1556
2422
  type: "button",
1557
2423
  className: "nwe-help",
@@ -1561,12 +2427,153 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1561
2427
  "aria-haspopup": "dialog",
1562
2428
  "aria-expanded": open,
1563
2429
  title: "插件使用教程"
1564
- }, helpIcon(), (0, react.createElement)("span", { className: "nwe-help-label" }, "教程")), open ? tutorialDialog(close, closeRef) : null);
2430
+ }, helpIcon(), (0, react.createElement)("span", { className: "nwe-help-label" }, "教程")), overlay);
2431
+ };
2432
+ const trendChart = (view) => {
2433
+ const values = view.quality.scores.filter((score) => Number.isFinite(score)).slice(-10);
2434
+ const width = 720;
2435
+ const height = 196;
2436
+ const left = 38;
2437
+ const innerWidth = 666;
2438
+ const innerHeight = 150;
2439
+ const max = 150;
2440
+ const x = (index) => values.length <= 1 ? 371 : left + index / (values.length - 1) * innerWidth;
2441
+ const y = (score) => 166 - Math.max(0, Math.min(max, score)) / max * innerHeight;
2442
+ const points = values.map((score, index) => ({
2443
+ x: x(index),
2444
+ y: y(score),
2445
+ score
2446
+ }));
2447
+ const line = points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(" ");
2448
+ const area = points.length === 0 ? "" : `M ${points[0].x.toFixed(1)} ${166 .toFixed(1)} L ${line.replaceAll(" ", " L ")} L ${points[points.length - 1].x.toFixed(1)} ${166 .toFixed(1)} Z`;
2449
+ return (0, react.createElement)("section", {
2450
+ className: "nwe-viz-card nwe-viz-trend",
2451
+ "aria-label": "章节评分趋势"
2452
+ }, (0, react.createElement)("div", { className: "nwe-viz-head" }, (0, react.createElement)("div", { className: "nwe-viz-title" }, sectionGlyph("chart", 16), (0, react.createElement)("span", null, "评分趋势")), (0, react.createElement)("span", { className: "nwe-viz-note" }, values.length === 0 ? "还没有评审" : `最近 ${values.length} 章 · 满分 ${max}`)), values.length === 0 ? (0, react.createElement)("div", { className: "nwe-viz-empty" }, "完成 P4 评审后,这里会出现逐章评分曲线。") : (0, react.createElement)("div", { className: "nwe-trend-wrap" }, (0, react.createElement)("svg", {
2453
+ className: "nwe-trend-chart",
2454
+ viewBox: `0 0 ${width} ${height}`,
2455
+ role: "img",
2456
+ "aria-label": `最近 ${values.length} 章评分趋势,最低 ${Math.min(...values)},最高 ${Math.max(...values)},满分 150`
2457
+ }, (0, react.createElement)("defs", null, (0, react.createElement)("linearGradient", {
2458
+ id: "nwe-trend-fill",
2459
+ x1: "0",
2460
+ y1: "0",
2461
+ x2: "0",
2462
+ y2: "1"
2463
+ }, (0, react.createElement)("stop", {
2464
+ offset: "0%",
2465
+ style: {
2466
+ stopColor: "var(--nwe-gold)",
2467
+ stopOpacity: .3
2468
+ }
2469
+ }), (0, react.createElement)("stop", {
2470
+ offset: "100%",
2471
+ style: {
2472
+ stopColor: "var(--nwe-gold)",
2473
+ stopOpacity: 0
2474
+ }
2475
+ }))), ...[
2476
+ 0,
2477
+ .5,
2478
+ 1
2479
+ ].map((ratio) => {
2480
+ const gridY = 166 - ratio * innerHeight;
2481
+ return (0, react.createElement)("g", { key: `grid-${ratio}` }, (0, react.createElement)("line", {
2482
+ x1: left,
2483
+ x2: 704,
2484
+ y1: gridY,
2485
+ y2: gridY,
2486
+ style: {
2487
+ stroke: "var(--nwe-border)",
2488
+ strokeDasharray: ratio === 0 ? void 0 : "3 5"
2489
+ }
2490
+ }), (0, react.createElement)("text", {
2491
+ x: 29,
2492
+ y: gridY + 4,
2493
+ textAnchor: "end",
2494
+ style: {
2495
+ fill: "var(--nwe-muted)",
2496
+ fontSize: 10
2497
+ }
2498
+ }, String(Math.round(max * ratio))));
2499
+ }), (0, react.createElement)("path", {
2500
+ d: area,
2501
+ style: { fill: "url(#nwe-trend-fill)" }
2502
+ }), (0, react.createElement)("polyline", {
2503
+ points: line,
2504
+ style: {
2505
+ fill: "none",
2506
+ stroke: "var(--nwe-gold)",
2507
+ strokeWidth: 2.4,
2508
+ strokeLinejoin: "round",
2509
+ strokeLinecap: "round"
2510
+ }
2511
+ }), ...points.map((point, index) => (0, react.createElement)("g", { key: `point-${index}` }, (0, react.createElement)("circle", {
2512
+ cx: point.x,
2513
+ cy: point.y,
2514
+ r: 3.5,
2515
+ style: {
2516
+ fill: "var(--nwe-surface)",
2517
+ stroke: "var(--nwe-gold)",
2518
+ strokeWidth: 2
2519
+ }
2520
+ }), (0, react.createElement)("title", null, `第 ${index + 1} 个样本 · ${point.score} / 150`))), (0, react.createElement)("text", {
2521
+ x: left,
2522
+ y: 188,
2523
+ style: {
2524
+ fill: "var(--nwe-muted)",
2525
+ fontSize: 10
2526
+ }
2527
+ }, `最近 ${values.length} 章`))));
2528
+ };
2529
+ const wordChart = (view) => {
2530
+ const rows = view.rows.slice(-8);
2531
+ const max = Math.max(1, ...rows.map((row) => row.wordCount));
2532
+ return (0, react.createElement)("section", {
2533
+ className: "nwe-viz-card nwe-viz-words",
2534
+ "aria-label": "章节字数分布"
2535
+ }, (0, react.createElement)("div", { className: "nwe-viz-head" }, (0, react.createElement)("div", { className: "nwe-viz-title" }, sectionGlyph("book", 16), (0, react.createElement)("span", null, "章节字数")), (0, react.createElement)("span", { className: "nwe-viz-note" }, rows.length === 0 ? "还没有章节" : `最近 ${rows.length} 章 · 峰值 ${max.toLocaleString("zh-CN")}`)), rows.length === 0 ? (0, react.createElement)("div", { className: "nwe-viz-empty" }, "归档第一章后,这里会出现字数柱状图。") : (0, react.createElement)("div", {
2536
+ className: "nwe-word-chart",
2537
+ role: "img",
2538
+ "aria-label": `最近 ${rows.length} 章字数分布,峰值 ${max.toLocaleString("zh-CN")} 字`
2539
+ }, ...rows.map((row) => {
2540
+ return (0, react.createElement)("div", {
2541
+ className: "nwe-word-col",
2542
+ key: `word-${row.num}`
2543
+ }, (0, react.createElement)("span", { className: "nwe-word-value" }, row.wordCount.toLocaleString("zh-CN")), (0, react.createElement)("div", {
2544
+ className: "nwe-word-track",
2545
+ title: `第 ${row.num} 章 · ${row.wordCount.toLocaleString("zh-CN")} 字`
2546
+ }, (0, react.createElement)("span", { style: {
2547
+ display: "block",
2548
+ flex: "0 0 auto",
2549
+ width: "24px",
2550
+ height: `${Math.max(4, Math.round(row.wordCount / max * 142))}px`,
2551
+ maxHeight: "100%",
2552
+ backgroundColor: "var(--nwe-gold)",
2553
+ borderRadius: "4px 4px 2px 2px"
2554
+ } })), (0, react.createElement)("span", { className: "nwe-word-label" }, `第${row.num}章`));
2555
+ })));
2556
+ };
2557
+ const overviewCharts = (view) => (0, react.createElement)("div", { className: "nwe-overview-charts" }, trendChart(view), wordChart(view));
2558
+ const bookGlance = (view) => {
2559
+ const target = view.book.target ?? view.book.outlineTotal;
2560
+ const percent = view.book.archivedCoverage === null ? target === 0 ? 0 : Math.round(view.book.archivedCards / target * 100) : Math.round(view.book.archivedCoverage * 100);
2561
+ return (0, react.createElement)("section", {
2562
+ className: "nwe-glance",
2563
+ "aria-label": "全书概览"
2564
+ }, (0, react.createElement)("div", { className: "nwe-glance-head" }, (0, react.createElement)("span", { className: "nwe-desk-kicker" }, "全书"), (0, react.createElement)("span", { className: "nwe-desk-note" }, view.book.present ? "已规划" : "未规划")), (0, react.createElement)("h2", { className: "nwe-glance-title" }, view.book.premise || "这本书还没有蓝图"), (0, react.createElement)("p", { className: "nwe-glance-text" }, view.book.logline || "补全书级蓝图、卷卡和世界观后,写作会更稳定。"), (0, react.createElement)("div", {
2565
+ className: "nwe-glance-bar",
2566
+ role: "progressbar",
2567
+ "aria-valuemin": 0,
2568
+ "aria-valuemax": 100,
2569
+ "aria-valuenow": Math.min(100, Math.max(0, percent)),
2570
+ "aria-label": "全书归档进度"
2571
+ }, (0, react.createElement)("span", { style: { width: `${Math.min(100, Math.max(0, percent))}%` } })), (0, react.createElement)("div", { className: "nwe-glance-legend" }, (0, react.createElement)("span", null, `${view.book.archivedCards} / ${target === 0 ? "—" : target} 章`), (0, react.createElement)("span", null, `${percent}% 已归档`)), (0, react.createElement)("dl", { className: "nwe-glance-facts" }, (0, react.createElement)("div", null, (0, react.createElement)("dt", null, "计划卡"), (0, react.createElement)("dd", null, String(view.book.planned))), (0, react.createElement)("div", null, (0, react.createElement)("dt", null, "卷"), (0, react.createElement)("dd", null, String(view.book.volumes.length))), (0, react.createElement)("div", null, (0, react.createElement)("dt", null, "待处理"), (0, react.createElement)("dd", null, String(view.health.blocking)))));
1565
2572
  };
1566
2573
  const nextCard = (view) => (0, react.createElement)("section", {
1567
2574
  className: "nwe-next",
1568
2575
  "aria-label": "下一步"
1569
- }, (0, react.createElement)("div", { className: "nwe-next-icon" }, arrowIcon()), (0, react.createElement)("div", { className: "nwe-next-body" }, (0, react.createElement)("div", { className: "nwe-next-kicker" }, "CURRENT CHAPTER · 下一步"), (0, react.createElement)("div", { className: "nwe-next-title" }, view.nextTitle), (0, react.createElement)("div", { className: "nwe-next-detail" }, view.nextDetail), (0, react.createElement)(CopyResumeButton, { text: view.resumeText })), view.nextTool === "" ? null : (0, react.createElement)("code", { className: "nwe-next-tool" }, view.nextTool));
2576
+ }, (0, react.createElement)("div", { className: "nwe-next-icon" }, arrowIcon()), (0, react.createElement)("div", { className: "nwe-next-body" }, (0, react.createElement)("div", { className: "nwe-next-kicker" }, "当前阶段 · 下一步"), (0, react.createElement)("div", { className: "nwe-next-title" }, view.nextTitle), (0, react.createElement)("div", { className: "nwe-next-detail" }, view.nextDetail), (0, react.createElement)(CopyResumeButton, { text: view.resumeText })), (0, react.createElement)("div", { className: "nwe-next-meta" }, (0, react.createElement)("span", null, view.chapter > 0 ? `第 ${view.chapter} 章` : "未开始"), (0, react.createElement)("span", null, view.stageLabel), view.nextTool === "" ? null : (0, react.createElement)("code", { className: "nwe-next-tool" }, view.nextTool)));
1570
2577
  const blockingSection = (view) => (0, react.createElement)("section", { className: "nwe-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "门控"), (0, react.createElement)("span", { className: "nwe-section-note" }, view.blockingTitle)), view.blocking.length === 0 ? (0, react.createElement)("div", {
1571
2578
  className: "nwe-pass",
1572
2579
  role: "status"
@@ -1580,7 +2587,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1580
2587
  className: "nwe-block-icon",
1581
2588
  "aria-hidden": "true"
1582
2589
  }, "!"), (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "nwe-block-code" }, item.code), (0, react.createElement)("div", { className: "nwe-block-detail" }, item.detail), item.hint === "" ? null : (0, react.createElement)("div", { className: "nwe-block-hint" }, item.hint))))));
1583
- const chaptersSection = (view) => (0, react.createElement)("section", { className: "nwe-section nwe-chapters-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, `${view.stats.chapters} 条记录${view.stats.chapters > view.rows.filter((row) => row.tone === "ok").length ? ` · 显示最近 ${view.rows.length} 条` : ""}`)), view.rows.length === 0 ? (0, react.createElement)("div", {
2590
+ const chaptersSection = (view) => (0, react.createElement)("section", { className: "nwe-section nwe-chapters-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, `${view.stats.chapters} 已归档 · 显示 ${view.rows.length} 条${view.stats.chapters > view.rows.filter((row) => row.tone === "ok").length ? `(最近 ${view.rows.length} 条)` : ""}`)), view.rows.length === 0 ? (0, react.createElement)("div", {
1584
2591
  className: "nwe-empty",
1585
2592
  style: {
1586
2593
  marginTop: 12,
@@ -1600,7 +2607,13 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1600
2607
  const dims = data.quality.dimensions.map((dimension) => (0, react.createElement)("div", {
1601
2608
  className: "nwe-v6-bar",
1602
2609
  key: dimension.id
1603
- }, (0, react.createElement)("span", null, dimension.id), (0, react.createElement)("span", null, (0, react.createElement)("i", { style: { width: `${Math.min(100, Math.max(0, dimension.average / 10 * 100))}%` } })), (0, react.createElement)("span", null, dimension.samples === 0 ? "—" : dimension.average.toFixed(1))));
2610
+ }, (0, react.createElement)("span", null, dimension.id), (0, react.createElement)("span", {
2611
+ role: "progressbar",
2612
+ "aria-label": `${dimension.id} 得分`,
2613
+ "aria-valuemin": 0,
2614
+ "aria-valuemax": dimension.max === void 0 || dimension.max <= 0 ? 10 : dimension.max,
2615
+ "aria-valuenow": dimension.average
2616
+ }, (0, react.createElement)("i", { style: { width: `${Math.min(100, Math.max(0, dimension.average / (dimension.max === void 0 || dimension.max <= 0 ? 10 : dimension.max) * 100))}%` } })), (0, react.createElement)("span", null, dimension.samples === 0 ? "—" : dimension.average.toFixed(1))));
1604
2617
  const worldEntries = (entries) => entries.map((entry) => (0, react.createElement)("li", { key: entry.name }, (0, react.createElement)("b", null, entry.name), (0, react.createElement)("span", null, entry.description), entry.anchors === void 0 || entry.anchors.length === 0 ? null : (0, react.createElement)("span", { className: "nwe-v6-note" }, `锚点:${entry.anchors.join(" / ")}`)));
1605
2618
  return (0, react.createElement)("section", {
1606
2619
  className: "nwe-section",
@@ -1608,7 +2621,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1608
2621
  }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "这本书"), (0, react.createElement)("span", { className: "nwe-section-note" }, "只读投影 · 不含正文")), (0, react.createElement)("div", { className: "nwe-v6-grid" }, v6Card("bp", "书脊与蓝图", [v6Kv([
1609
2622
  ["作者", data.identity.author || "未填写"],
1610
2623
  ["简介", data.identity.description || "未填写"],
1611
- ["创建", data.identity.created_at || "—"],
2624
+ ["创建", localTime(data.identity.created_at)],
1612
2625
  ["目标", `${data.identity.targets.words_per_chapter[0]}–${data.identity.targets.words_per_chapter[1]} 字 / 章 · ${data.identity.targets.total_chapters ?? "不限"} 章`],
1613
2626
  ["主题", data.blueprint.themes.join(" / ") || "—"],
1614
2627
  ["基调 / POV", `${data.blueprint.tone || "—"} / ${data.blueprint.pov || "—"}`],
@@ -1617,12 +2630,12 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1617
2630
  ]), (0, react.createElement)("p", { className: "nwe-v6-note" }, data.blueprint.logline || data.blueprint.premise || "还没有蓝图摘要")], true), v6Card("progress", "进度与当前章", [v6Kv([
1618
2631
  ["已归档", `${data.progress.archived} / ${data.progress.target ?? "不限"} 章`],
1619
2632
  ["字数", `归档 ${data.progress.words_archived} · 含草稿 ${data.progress.words_with_draft}`],
1620
- ["剩余", data.progress.remaining_words === null ? "—" : `${data.progress.remaining_words} 字(${data.progress.remaining_words_basis})`],
2633
+ ["剩余", data.progress.remaining_words === null ? "—" : `${data.progress.remaining_words} 字(${remainingBasisLabel(data.progress.remaining_words_basis)})`],
1621
2634
  ["当前章", `${currentLabel} · ${data.current.card?.title ?? "无章卡"}`],
1622
2635
  ["轮次", `评 ${data.current.rounds.review} / 修 ${data.current.rounds.revise} / 检 ${data.current.rounds.check}`],
1623
2636
  ["正文 hash", data.current.manuscript_hash ?? "—"],
1624
2637
  ["当前字数", data.current.metrics === null ? "—" : String(data.current.metrics.total_word_count)]
1625
- ]), data.current.card === null ? null : (0, react.createElement)("div", { className: "nwe-v6-tags" }, ...data.current.card.cast.map((name) => (0, react.createElement)("span", { key: name }, name)))]), v6Card("cast", "人物与关系", [v6Kv([["角色总数", String(data.cast.total)], ["关系", `${data.relationships.total} 条 · ${Object.entries(data.relationships.types).map(([type, count]) => `${type} ${count}`).join(" / ") || "—"}`]]), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.cast.top.map((person) => (0, react.createElement)("li", { key: person.name }, (0, react.createElement)("b", null, person.name), (0, react.createElement)("span", null, `${person.role} · 出场 ${person.appearances} · 最近第 ${person.last_seen} 章`))))]), v6Card("world", "世界与设定", [
2638
+ ]), data.current.card === null ? null : (0, react.createElement)("div", { className: "nwe-v6-tags" }, ...data.current.card.cast.map((name) => (0, react.createElement)("span", { key: name }, name)))]), v6Card("volumes", "卷进度", [view.book.volumes.length === 0 ? (0, react.createElement)("p", { className: "nwe-v6-note" }, "还没有卷") : (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...view.book.volumes.map((volume) => (0, react.createElement)("li", { key: `${volume.title}-${volume.range}` }, (0, react.createElement)("b", null, volume.title), (0, react.createElement)("span", null, `${volume.range} · ${volume.planned} 计划 / ${volume.archived} 归档`))))]), v6Card("cast", "人物与关系", [v6Kv([["角色总数", String(data.cast.total)], ["关系", `${data.relationships.total} 条 · ${Object.entries(data.relationships.types).map(([type, count]) => `${type} ${count}`).join(" / ") || "—"}`]]), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.cast.top.map((person) => (0, react.createElement)("li", { key: person.name }, (0, react.createElement)("b", null, person.name), (0, react.createElement)("span", null, `${person.role} · 出场 ${person.appearances} · 最近第 ${person.last_seen} 章`))))]), v6Card("world", "世界与设定", [
1626
2639
  v6Kv([
1627
2640
  ["地点 / 势力", `${data.worldbook.counts.locations} / ${data.worldbook.counts.factions}`],
1628
2641
  ["规则 / 术语", `${data.worldbook.counts.rules} / ${data.worldbook.counts.glossary}`],
@@ -1638,43 +2651,689 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1638
2651
  ]), v6Card("foreshadow", "伏笔与时间线", [v6Kv([["伏笔", `未解 ${data.foreshadows.open} · 休眠 ${data.foreshadows.dormant} · 逾期 ${data.foreshadows.overdue} · 已解 ${data.foreshadows.resolved}`], ["时间线", `${data.timeline.events} 个事件 · 第 ${data.timeline.first_chapter ?? "—"}–${data.timeline.last_chapter ?? "—"} 章`]]), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.foreshadows.oldest.map((item) => (0, react.createElement)("li", { key: item.id }, (0, react.createElement)("b", null, item.id), (0, react.createElement)("span", null, `${item.title} · 已 ${item.age} 章`))))]), v6Card("quality", "质量与节奏", [
1639
2652
  v6Kv([
1640
2653
  ["全书均分", `${data.quality.average.toFixed(1)} · 中位 ${data.quality.median.toFixed(1)} · ${data.quality.min}–${data.quality.max}`],
1641
- ["近十均分", data.quality.last_ten_average.toFixed(1)],
1642
- ["样本 / 评审", `${data.quality.samples} / ${data.quality.attempts}`],
1643
- ["趋势", data.quality.trend === null ? data.quality.trend_basis : `${data.quality.trend >= 0 ? "+" : ""}${data.quality.trend}`],
1644
- ["节奏", JSON.stringify(data.pacing.scene_types)],
1645
- ["钩子 / 推进", `${JSON.stringify(data.pacing.hook_strength)} / ${JSON.stringify(data.pacing.plot_advancement)}`],
2654
+ ["近十均分", data.quality.last_ten_average === null ? "—" : data.quality.last_ten_average.toFixed(1)],
2655
+ ["样本 / 评审", data.quality.samples === 0 ? "见质量页" : `${data.quality.samples} / ${data.quality.attempts}`],
2656
+ ["趋势", data.quality.trend === null ? data.quality.samples === 0 ? "见质量页" : data.quality.trend_basis === "insufficient_samples" ? "样本不足" : data.quality.trend_basis === "insufficient_history" ? "历史不足" : "最近五章对比前五章" : `${data.quality.trend >= 0 ? "+" : ""}${data.quality.trend}`],
2657
+ ["节奏", countsText(data.pacing.scene_types)],
2658
+ ["钩子 / 推进", `${countsText(data.pacing.hook_strength, hookLabel)} / ${countsText(data.pacing.plot_advancement, plotLabel)}`],
1646
2659
  ["最长 / 最短", data.pacing.longest === null ? "—" : `第 ${data.pacing.longest.chapter} 章 ${data.pacing.longest.words} 字 / 第 ${data.pacing.shortest?.chapter ?? "—"} 章 ${data.pacing.shortest?.words ?? "—"} 字`],
1647
2660
  ["返工", `评 ${data.effort.reviews} · 修 ${data.effort.revisions} · 检 ${data.effort.checks} · 回滚 ${data.effort.rollbacks}`]
1648
2661
  ]),
1649
- (0, react.createElement)("div", {
2662
+ dims.length === 0 ? (0, react.createElement)("div", { className: "nwe-v6-note" }, "质量维度请切换到「质量」页查看;概览只取轻量摘要。") : (0, react.createElement)("div", {
1650
2663
  className: "nwe-v6-bars",
1651
2664
  "aria-label": "十五维质量"
1652
2665
  }, ...dims),
1653
- (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "逐章 effort"), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.effort.chapters.map((chapter) => (0, react.createElement)("li", { key: chapter.num }, (0, react.createElement)("b", null, `第 ${chapter.num} 章`), (0, react.createElement)("span", null, `评 ${chapter.reviews} / 修 ${chapter.revisions} / 检 ${chapter.checks} / 回滚 ${chapter.rollbacks} · ${chapter.final_check ?? "—"}`)))))
2666
+ (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "逐章 effort"), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.effort.chapters.map((chapter) => (0, react.createElement)("li", { key: chapter.num }, (0, react.createElement)("b", null, `第 ${chapter.num} 章`), (0, react.createElement)("span", null, `评 ${chapter.reviews} / 修 ${chapter.revisions} / 检 ${chapter.checks} / 回滚 ${chapter.rollbacks} · ${finalCheckLabel(chapter.final_check)}`)))))
1654
2667
  ]), v6Card("health", "健康与恢复", [
1655
2668
  v6Kv([
1656
2669
  ["大纲", data.health.outline.present ? "可读" : data.health.outline.error === null ? "还没有大纲" : "损坏"],
1657
2670
  ["世界观", data.health.worldbook.present ? "可读" : data.health.worldbook.error === null ? "还没有设定" : "损坏"],
1658
- ["pending", data.health.pending === null ? "无" : `${data.health.pending.state} · ${data.health.pending.detail}`]
2671
+ ["pending", data.health.pending === null ? "无" : `${statusLabel(data.health.pending.state)} · ${data.health.pending.detail}`]
1659
2672
  ]),
1660
2673
  data.health.outline.error === null ? null : (0, react.createElement)("p", { className: "nwe-v6-note" }, data.health.outline.error),
1661
2674
  data.health.worldbook.error === null ? null : (0, react.createElement)("p", { className: "nwe-v6-note" }, data.health.worldbook.error),
1662
2675
  (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.health.state_errors.map((error) => (0, react.createElement)("li", { key: `${error.path}:${error.code}` }, (0, react.createElement)("b", null, error.code), (0, react.createElement)("span", null, `${error.path}: ${error.message}`))))
1663
2676
  ])));
1664
2677
  };
2678
+ const bookDossier = (view) => {
2679
+ const body = bookDataSection(view) ?? bookSection(view);
2680
+ if (body === null) return null;
2681
+ return (0, react.createElement)("details", {
2682
+ className: "nwe-dossier",
2683
+ id: "nwe-dossier"
2684
+ }, (0, react.createElement)("summary", null, (0, react.createElement)("span", null, "全书资料"), (0, react.createElement)("span", { className: "nwe-dossier-note" }, "蓝图 · 卷卡 · 世界 · 质量 · 健康")), body);
2685
+ };
2686
+ const PANEL_TABS = [
2687
+ {
2688
+ id: "overview",
2689
+ label: "概览"
2690
+ },
2691
+ {
2692
+ id: "chapters",
2693
+ label: "章节"
2694
+ },
2695
+ {
2696
+ id: "characters",
2697
+ label: "人物"
2698
+ },
2699
+ {
2700
+ id: "foreshadows",
2701
+ label: "线索"
2702
+ },
2703
+ {
2704
+ id: "worldbook",
2705
+ label: "设定"
2706
+ },
2707
+ {
2708
+ id: "quality",
2709
+ label: "质量"
2710
+ },
2711
+ {
2712
+ id: "health",
2713
+ label: "健康"
2714
+ }
2715
+ ];
2716
+ const EMPTY_CHAPTER_QUERY = {
2717
+ search: "",
2718
+ status: "all",
2719
+ scene: "",
2720
+ sort: "num_desc",
2721
+ cursor: ""
2722
+ };
2723
+ const TAB_META = {
2724
+ chapters: {
2725
+ title: "章节台账",
2726
+ desc: "按章查看状态、字数、评分与归档结果;点开详情可读正文和归档 plan。",
2727
+ glyph: "book"
2728
+ },
2729
+ characters: {
2730
+ title: "人物档案",
2731
+ desc: "在场人物、首次出场、最近状态与成长轨迹,按档案卡分组呈现。",
2732
+ glyph: "person"
2733
+ },
2734
+ foreshadows: {
2735
+ title: "线索网络",
2736
+ desc: "伏笔、时间线与人物关系同屏对照,避免线索在长书里失联。",
2737
+ glyph: "seed"
2738
+ },
2739
+ worldbook: {
2740
+ title: "世界设定",
2741
+ desc: "蓝图、地点、势力、规则与数值事实的统一索引,所有条目可展开。",
2742
+ glyph: "map"
2743
+ },
2744
+ quality: {
2745
+ title: "质量剖面",
2746
+ desc: "全书评分、可读评审、十五维得分与节奏数据,定位需要返工的章节。",
2747
+ glyph: "chart"
2748
+ },
2749
+ health: {
2750
+ title: "项目健康",
2751
+ desc: "大纲、设定与状态文件的完整性检查,把可修复的问题前置。",
2752
+ glyph: "shield"
2753
+ }
2754
+ };
2755
+ const sectionItemCount = (section) => {
2756
+ const payload = section?.payload;
2757
+ if (payload === null || payload === void 0) return void 0;
2758
+ if (Array.isArray(payload.items)) return payload.items.length;
2759
+ const row = asRow(payload.items);
2760
+ if (row !== null) {
2761
+ const count = Object.values(row).reduce((total, value) => total + (Array.isArray(value) ? value.length : 1), 0);
2762
+ if (count > 0) return count;
2763
+ }
2764
+ return payload.total ?? payload.returned;
2765
+ };
2766
+ const tabIntro = (tab, view, section) => {
2767
+ const meta = TAB_META[tab];
2768
+ const total = tab === "chapters" ? view.stats.chapters : sectionItemCount(section);
2769
+ const note = total === void 0 ? "只读投影" : tab === "chapters" ? `${total} 章` : `${total} 条记录`;
2770
+ return (0, react.createElement)("header", { className: "nwe-tab-intro" }, (0, react.createElement)("span", {
2771
+ className: "nwe-tab-intro-icon",
2772
+ "aria-hidden": "true"
2773
+ }, sectionGlyph(meta.glyph, 22)), (0, react.createElement)("div", { className: "nwe-tab-intro-copy" }, (0, react.createElement)("span", { className: "nwe-tab-intro-kicker" }, "WORKBENCH / READ MODEL"), (0, react.createElement)("h2", { className: "nwe-tab-intro-title" }, meta.title), (0, react.createElement)("p", { className: "nwe-tab-intro-desc" }, meta.desc)), (0, react.createElement)("span", { className: "nwe-tab-intro-count" }, note));
2774
+ };
2775
+ const tabBar = (tab, onTab) => (0, react.createElement)("nav", {
2776
+ className: "nwe-tabs",
2777
+ role: "tablist",
2778
+ "aria-label": "网文工坊栏目"
2779
+ }, ...PANEL_TABS.map((item) => (0, react.createElement)("button", {
2780
+ key: item.id,
2781
+ type: "button",
2782
+ role: "tab",
2783
+ id: `nwe-tab-${item.id}`,
2784
+ "aria-controls": `nwe-panel-${item.id}`,
2785
+ "aria-selected": tab === item.id,
2786
+ className: `nwe-tab${tab === item.id ? " is-active" : ""}`,
2787
+ onClick: () => onTab(item.id)
2788
+ }, item.label)));
2789
+ const asRow = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
2790
+ const textOf = (value, fallback = "—") => typeof value === "string" && value !== "" ? value : typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
2791
+ const roleLabel = (value) => value === "protagonist" ? "主角" : value === "supporting" ? "配角" : value === "extra" ? "龙套" : textOf(value);
2792
+ const STATUS_LABELS = {
2793
+ planted: "已埋",
2794
+ advanced: "已推进",
2795
+ resolved: "已回收",
2796
+ archived: "已归档",
2797
+ draft: "草稿",
2798
+ dormant: "休眠",
2799
+ active: "在场",
2800
+ planned: "计划",
2801
+ writing: "写作中",
2802
+ done: "完成",
2803
+ open: "待处理",
2804
+ fixed: "已修复",
2805
+ waived: "已豁免",
2806
+ committed: "已提交",
2807
+ partial: "不完整",
2808
+ complete: "已完成",
2809
+ untouched: "未开始",
2810
+ recoverable: "可恢复",
2811
+ abortable: "可中止"
2812
+ };
2813
+ const statusLabel = (value) => typeof value === "string" ? STATUS_LABELS[value] ?? value : textOf(value);
2814
+ const finalCheckLabel = (value) => value === "pass" ? "通过" : value === "fail" ? "未过" : "—";
2815
+ const remainingBasisLabel = (value) => value === "actual_average" ? "按已归档章均字" : value === "minimum_target" ? "按最低目标" : value === "none" ? "无" : value;
2816
+ const countsText = (value, label = (key) => key) => {
2817
+ const row = asRow(value);
2818
+ if (row === null) return textOf(value);
2819
+ const entries = Object.entries(row).filter(([, count]) => typeof count === "number");
2820
+ return entries.length === 0 ? "—" : entries.map(([key, count]) => `${label(key)} ${count}`).join(" · ");
2821
+ };
2822
+ const hookLabel = (key) => key === "1" ? "弱" : key === "2" ? "较弱" : key === "3" ? "中" : key === "4" ? "较强" : key === "5" ? "强" : key;
2823
+ const plotLabel = (key) => key === "high" ? "高" : key === "mid" ? "中" : key === "low" ? "低" : key;
2824
+ const sectionKv = (row) => {
2825
+ const fields = [
2826
+ ["角色", roleLabel(row.role)],
2827
+ ["状态", statusLabel(row.status)],
2828
+ ["首次出场", textOf(row.first_appearance ?? row.first)],
2829
+ ["最近出场", textOf(row.last)],
2830
+ ["出场次数", textOf(row.appearances)],
2831
+ ["类型", textOf(row.type)],
2832
+ ["地点", textOf(row.location)],
2833
+ ["双方", row.a === void 0 ? "—" : `${textOf(row.a)} ↔ ${textOf(row.b)}`],
2834
+ ["别名", textOf(row.alias)],
2835
+ ["数值", textOf(row.value)],
2836
+ ["单位", textOf(row.unit)],
2837
+ ["描述", textOf(row.description)],
2838
+ ["锚点", textOf(row.anchor)],
2839
+ ["容忍", textOf(row.tolerance)],
2840
+ ["备注", textOf(row.note)],
2841
+ ["推进记录", Array.isArray(row.advance_log) ? `${row.advance_log.length} 条` : "—"],
2842
+ ["成长", Array.isArray(row.growth) ? `${row.growth.length} 条` : "—"],
2843
+ ["当前状态", textOf(row.current_state)],
2844
+ ["人物档案", textOf(row.profile)],
2845
+ ["章龄", textOf(row.age)],
2846
+ ["术语", textOf(row.term)],
2847
+ ["释义", textOf(row.definition)],
2848
+ ["时间", textOf(row.time)],
2849
+ ["事件", textOf(row.event)]
2850
+ ];
2851
+ const used = /* @__PURE__ */ new Set([
2852
+ "role",
2853
+ "status",
2854
+ "first_appearance",
2855
+ "first",
2856
+ "last",
2857
+ "appearances",
2858
+ "type",
2859
+ "location",
2860
+ "a",
2861
+ "b",
2862
+ "alias",
2863
+ "value",
2864
+ "unit",
2865
+ "description",
2866
+ "anchor",
2867
+ "tolerance",
2868
+ "note",
2869
+ "advance_log",
2870
+ "growth"
2871
+ ]);
2872
+ const extraLabels = {
2873
+ average: "平均",
2874
+ samples: "可读评审",
2875
+ median: "中位",
2876
+ min: "最小",
2877
+ max: "最大",
2878
+ trend: "趋势",
2879
+ attempts: "评审轮次",
2880
+ reviews: "评",
2881
+ revisions: "修",
2882
+ checks: "检",
2883
+ rollbacks: "回滚",
2884
+ chapters: "章节",
2885
+ words: "字数",
2886
+ total: "总数",
2887
+ present: "存在",
2888
+ error: "错误",
2889
+ path: "路径",
2890
+ code: "代码",
2891
+ message: "信息",
2892
+ chapter: "章",
2893
+ field: "字段",
2894
+ planned: "计划",
2895
+ actual: "实际",
2896
+ state: "状态",
2897
+ detail: "详情",
2898
+ txid: "事务",
2899
+ num: "章号",
2900
+ scene_type: "场景",
2901
+ goal: "目标",
2902
+ conflict: "冲突",
2903
+ hook: "钩子",
2904
+ notes: "备注",
2905
+ story_time: "故事时间",
2906
+ since: "始于",
2907
+ word_count: "字数",
2908
+ score: "评分",
2909
+ archived_at: "归档时间",
2910
+ characters: "人物",
2911
+ evolution: "演变"
2912
+ };
2913
+ for (const [key, value] of Object.entries(row)) {
2914
+ if (used.has(key) || key === "title" || key === "name" || key === "id") continue;
2915
+ const label = extraLabels[key];
2916
+ if (key === "archived_at") {
2917
+ fields.push([label, localTime(typeof value === "string" ? value : null)]);
2918
+ continue;
2919
+ }
2920
+ if (label !== void 0 && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")) fields.push([label, textOf(value)]);
2921
+ }
2922
+ const visible = fields.filter(([, value]) => value !== "—");
2923
+ return (0, react.createElement)("dl", { className: "nwe-kv" }, ...visible.flatMap(([label, value]) => [(0, react.createElement)("dt", { key: `dt-${label}` }, label), (0, react.createElement)("dd", { key: `dd-${label}` }, value)]));
2924
+ };
2925
+ const SECTION_GLYPHS = {
2926
+ characters: "person",
2927
+ foreshadows: "seed",
2928
+ timeline: "clock",
2929
+ relationships: "link",
2930
+ rules: "book",
2931
+ factions: "shield",
2932
+ locations: "map",
2933
+ glossary: "book",
2934
+ backstory: "clock",
2935
+ facts: "chart",
2936
+ themes: "spark",
2937
+ outline_cards: "book",
2938
+ drift: "alert",
2939
+ blueprint: "map",
2940
+ "state-errors": "alert",
2941
+ effort: "heart",
2942
+ pacing: "chart"
2943
+ };
2944
+ const sectionList = (title, items, kind = "spark") => {
2945
+ if (!Array.isArray(items) || items.length === 0) return (0, react.createElement)("div", { className: "nwe-empty" }, `${title}:暂无数据`);
2946
+ return (0, react.createElement)("div", { className: "nwe-card-grid" }, ...items.map((item, index) => {
2947
+ const row = asRow(item);
2948
+ const explicitLabel = row === null ? void 0 : row.a !== void 0 && row.b !== void 0 ? `${textOf(row.a)} ↔ ${textOf(row.b)}` : row.title ?? row.name ?? row.id ?? row.alias ?? row.term ?? row.event ?? row.a;
2949
+ const label = row === null ? String(item) : textOf(row.chapter === void 0 ? explicitLabel : `第 ${textOf(row.chapter)} 章${row.time === void 0 ? "" : ` · ${textOf(row.time)}`}`, explicitLabel === void 0 ? title : textOf(explicitLabel));
2950
+ const note = row === null ? "" : textOf(row.current_state ?? row.profile ?? row.description ?? row.definition ?? row.layout ?? row.goal ?? row.note ?? (row.value === void 0 ? row.event !== void 0 && row.time !== void 0 ? row.time : void 0 : `${textOf(row.value)}${row.unit === void 0 ? "" : ` ${textOf(row.unit)}`}`), "");
2951
+ const chips = row === null ? [] : [
2952
+ row.role === void 0 ? "" : roleLabel(row.role),
2953
+ row.status === void 0 ? "" : statusLabel(row.status),
2954
+ row.first_appearance === void 0 ? "" : `首次 ${textOf(row.first_appearance)}`,
2955
+ row.last === void 0 ? "" : `最近 ${textOf(row.last)}`,
2956
+ row.appearances === void 0 ? "" : `${textOf(row.appearances)} 次登场`,
2957
+ row.type === void 0 ? "" : textOf(row.type)
2958
+ ].filter((value) => value !== "");
2959
+ return (0, react.createElement)("details", {
2960
+ className: "nwe-data-card",
2961
+ key: `${label}-${index}`
2962
+ }, (0, react.createElement)("summary", { className: "nwe-data-card-head" }, (0, react.createElement)("span", {
2963
+ className: `nwe-data-card-icon is-${kind}`,
2964
+ "aria-hidden": "true"
2965
+ }, sectionGlyph(kind)), (0, react.createElement)("span", { className: "nwe-data-card-heading" }, (0, react.createElement)("strong", null, label), note === "" ? null : (0, react.createElement)("span", { className: "nwe-data-card-note" }, note), chips.length === 0 ? null : (0, react.createElement)("span", { className: "nwe-data-card-chips" }, ...chips.map((chip) => (0, react.createElement)("span", { key: chip }, chip)))), (0, react.createElement)("span", {
2966
+ className: "nwe-data-card-caret",
2967
+ "aria-hidden": "true"
2968
+ }, "+")), row === null ? (0, react.createElement)("div", { className: "nwe-data-card-body" }, String(item)) : (0, react.createElement)("div", { className: "nwe-data-card-body" }, sectionKv(row)));
2969
+ }));
2970
+ };
2971
+ const sectionBlock = (title, items, key) => {
2972
+ const list = Array.isArray(items) ? items : [];
2973
+ if (list.length === 0) return null;
2974
+ const glyph = SECTION_GLYPHS[key] ?? "spark";
2975
+ return (0, react.createElement)("section", {
2976
+ className: "nwe-subsection",
2977
+ key
2978
+ }, (0, react.createElement)("div", { className: "nwe-group-head" }, (0, react.createElement)("span", {
2979
+ className: `nwe-group-icon is-${glyph}`,
2980
+ "aria-hidden": "true"
2981
+ }, sectionGlyph(glyph, 16)), (0, react.createElement)("h3", null, `${title}(${list.length})`)), sectionList(title, list, glyph));
2982
+ };
2983
+ const worldbookSection = (items) => {
2984
+ const row = asRow(items);
2985
+ if (row === null) return sectionList("设定", items);
2986
+ const groups = [
2987
+ ["rules", "规则"],
2988
+ ["factions", "势力"],
2989
+ ["locations", "地点"],
2990
+ ["characters", "人物小传"],
2991
+ ["glossary", "术语"],
2992
+ ["backstory", "背景"],
2993
+ ["facts", "数值事实"],
2994
+ ["themes", "主题"],
2995
+ ["outline_cards", "章卡"],
2996
+ ["drift", "大纲漂移"]
2997
+ ];
2998
+ const ledger = asRow(row.ledger);
2999
+ const blueprint = asRow(row.blueprint);
3000
+ const volumes = Array.isArray(row.volumes) ? row.volumes : [];
3001
+ const intro = [
3002
+ typeof row.premise === "string" && row.premise !== "" ? {
3003
+ title: "前提",
3004
+ description: row.premise
3005
+ } : null,
3006
+ typeof row.setting === "string" && row.setting !== "" ? {
3007
+ title: "设定",
3008
+ description: row.setting
3009
+ } : null,
3010
+ blueprint === null ? null : {
3011
+ title: "蓝图",
3012
+ description: [
3013
+ Array.isArray(blueprint.themes) ? `主题 ${blueprint.themes.join(" / ")}` : "",
3014
+ typeof blueprint.tone === "string" ? `基调 ${blueprint.tone}` : "",
3015
+ typeof blueprint.pov === "string" ? `POV ${blueprint.pov}` : "",
3016
+ typeof blueprint.ending === "string" ? `结局 ${blueprint.ending}` : ""
3017
+ ].filter((part) => part !== "").join(" · ")
3018
+ },
3019
+ volumes.length === 0 ? null : {
3020
+ title: "卷",
3021
+ description: volumes.map((volume) => {
3022
+ const item = asRow(volume);
3023
+ return item === null ? "" : `${textOf(item.title)} ${textOf(item.start_chapter)}–${textOf(item.end_chapter)}`;
3024
+ }).filter((part) => part !== "").join(" / ")
3025
+ }
3026
+ ].filter((entry) => entry !== null);
3027
+ const ledgerGroups = [
3028
+ ["locations", "台账地点"],
3029
+ ["abilities", "台账能力"],
3030
+ ["artifacts", "台账物品"],
3031
+ ["factions", "台账势力"],
3032
+ ["rules", "台账规则"]
3033
+ ];
3034
+ const blocks = [
3035
+ ...intro.length === 0 ? [] : [sectionBlock("蓝图", intro, "blueprint")],
3036
+ ...groups.map(([key, label]) => sectionBlock(label, row[key], key)),
3037
+ ...ledgerGroups.map(([key, label]) => sectionBlock(label, ledger?.[key], `ledger-${key}`))
3038
+ ].filter((block) => block !== null);
3039
+ return blocks.length === 0 ? (0, react.createElement)("div", { className: "nwe-empty" }, "设定:暂无数据") : (0, react.createElement)("div", { className: "nwe-worldbook-layout" }, ...blocks);
3040
+ };
3041
+ const qualitySection = (items, chapter = 0, detail = null, onDetail = () => void 0) => {
3042
+ const row = asRow(items);
3043
+ if (row === null) return sectionList("质量", items);
3044
+ const quality = asRow(row.quality);
3045
+ const dimensions = Array.isArray(row.dimensions) ? row.dimensions : [];
3046
+ const summary = quality === null || Number(quality.samples ?? 0) === 0 ? (0, react.createElement)("div", { className: "nwe-empty" }, "质量:暂无评审") : (0, react.createElement)("div", { className: "nwe-quality-kpis" }, ...[
3047
+ [
3048
+ "全书均分",
3049
+ textOf(quality.average),
3050
+ "/ 150"
3051
+ ],
3052
+ [
3053
+ "可读评审",
3054
+ textOf(quality.samples),
3055
+ "章"
3056
+ ],
3057
+ [
3058
+ "中位分",
3059
+ textOf(quality.median),
3060
+ ""
3061
+ ],
3062
+ [
3063
+ "评分区间",
3064
+ `${textOf(quality.min)}–${textOf(quality.max)}`,
3065
+ ""
3066
+ ]
3067
+ ].map(([label, value, unit]) => (0, react.createElement)("article", {
3068
+ className: "nwe-quality-kpi",
3069
+ key: label
3070
+ }, (0, react.createElement)("span", { className: "nwe-quality-kpi-label" }, label), (0, react.createElement)("strong", { className: "nwe-quality-kpi-value" }, value, unit === "" ? null : (0, react.createElement)("small", null, unit)))));
3071
+ const bars = dimensions.map((entry, index) => {
3072
+ const dimension = asRow(entry);
3073
+ if (dimension === null) return null;
3074
+ const average = Number(dimension.average);
3075
+ const max = Number(dimension.max);
3076
+ const id = textOf(dimension.id, `维度 ${index + 1}`);
3077
+ const percent = max > 0 ? Math.max(0, Math.min(100, average / max * 100)) : 0;
3078
+ return (0, react.createElement)("article", {
3079
+ className: "nwe-dim-card",
3080
+ key: id
3081
+ }, (0, react.createElement)("div", { className: "nwe-dim-card-head" }, (0, react.createElement)("span", null, id), (0, react.createElement)("strong", null, `${Number.isFinite(average) ? average : "—"} / ${max > 0 ? max : "—"}`)), (0, react.createElement)("div", {
3082
+ className: "nwe-v6-bar",
3083
+ role: "progressbar",
3084
+ "aria-label": `${id} 得分`,
3085
+ "aria-valuemin": 0,
3086
+ "aria-valuemax": max,
3087
+ "aria-valuenow": Number.isFinite(average) ? average : 0
3088
+ }, (0, react.createElement)("span", null, (0, react.createElement)("i", { style: { width: `${percent}%` } }))));
3089
+ });
3090
+ const pacing = asRow(row.pacing);
3091
+ const longest = asRow(pacing?.longest);
3092
+ const shortest = asRow(pacing?.shortest);
3093
+ const pacingRows = pacing === null ? [] : [
3094
+ {
3095
+ title: "节奏",
3096
+ description: countsText(pacing.scene_types)
3097
+ },
3098
+ {
3099
+ title: "钩子 / 推进",
3100
+ description: `${countsText(pacing.hook_strength, hookLabel)} / ${countsText(pacing.plot_advancement, plotLabel)}`
3101
+ },
3102
+ {
3103
+ title: "最长 / 最短",
3104
+ description: pacing.longest === null ? "—" : `第 ${textOf(longest?.chapter)} 章 ${textOf(longest?.words)} 字 / 第 ${textOf(shortest?.chapter)} 章 ${textOf(shortest?.words)} 字`
3105
+ }
3106
+ ];
3107
+ const effort = asRow(row.effort);
3108
+ const effortBlock = effort === null ? null : (0, react.createElement)("section", { className: "nwe-subsection" }, (0, react.createElement)("div", { className: "nwe-group-head" }, (0, react.createElement)("span", {
3109
+ className: "nwe-group-icon is-heart",
3110
+ "aria-hidden": "true"
3111
+ }, sectionGlyph("heart", 16)), (0, react.createElement)("h3", null, "返工投入")), (0, react.createElement)("div", { className: "nwe-effort-grid" }, ...[
3112
+ ["评审", effort.reviews],
3113
+ ["修订", effort.revisions],
3114
+ ["校验", effort.checks],
3115
+ ["回滚", effort.rollbacks]
3116
+ ].map(([label, value]) => (0, react.createElement)("article", {
3117
+ className: "nwe-effort-card",
3118
+ key: label
3119
+ }, (0, react.createElement)("span", null, label), (0, react.createElement)("strong", null, textOf(value))))));
3120
+ const extras = [pacingRows.length === 0 ? null : sectionBlock("节奏", pacingRows, "pacing"), effortBlock].filter((block) => block !== null);
3121
+ return (0, react.createElement)("div", { className: "nwe-quality-layout" }, summary, chapter > 0 ? (0, react.createElement)("div", { className: "nwe-quality-toolbar" }, (0, react.createElement)("button", {
3122
+ type: "button",
3123
+ className: "nwe-refresh",
3124
+ onClick: () => onDetail(chapter)
3125
+ }, "查看本章质量详情")) : null, (0, react.createElement)("div", { className: "nwe-dim-grid" }, ...bars), ...extras, qualityDetailPanel(detail, onDetail));
3126
+ };
3127
+ const healthSection = (items) => {
3128
+ const row = asRow(items);
3129
+ if (row === null) return sectionList("健康", items);
3130
+ const flags = ["outline", "worldbook"].map((key) => {
3131
+ const value = asRow(row[key]);
3132
+ const present = value?.present === true;
3133
+ const existed = value?.file_existed === true;
3134
+ const label = key === "outline" ? "大纲" : "设定";
3135
+ return (0, react.createElement)("article", {
3136
+ className: `nwe-health-card ${present ? "is-ok" : "is-bad"}`,
3137
+ key
3138
+ }, (0, react.createElement)("span", {
3139
+ className: "nwe-health-icon",
3140
+ "aria-hidden": "true"
3141
+ }, sectionGlyph(present ? "check" : "alert", 18)), (0, react.createElement)("div", { className: "nwe-health-copy" }, (0, react.createElement)("strong", null, `${label}:${present ? "可读" : existed ? "损坏" : "缺失"}`), (0, react.createElement)("span", null, present ? "结构完整,可安全读取" : existed ? "文件存在但解析失败" : "尚未生成该文件")), (0, react.createElement)("span", { className: "nwe-health-state" }, present ? "PASS" : "CHECK"));
3142
+ });
3143
+ const stateErrors = Array.isArray(row.state_errors) ? row.state_errors : [];
3144
+ const drift = Array.isArray(row.drift) ? row.drift : [];
3145
+ const blocks = [sectionBlock("状态错误", stateErrors, "state-errors"), sectionBlock("大纲漂移", drift, "drift")].filter((block) => block !== null);
3146
+ return (0, react.createElement)("div", { className: "nwe-health-layout" }, (0, react.createElement)("div", { className: "nwe-health-grid" }, ...flags), ...blocks);
3147
+ };
3148
+ const chapterToolbar = (query, onQuery) => {
3149
+ return (0, react.createElement)("div", {
3150
+ className: "nwe-chapter-toolbar",
3151
+ role: "search",
3152
+ "aria-label": "筛选章节"
3153
+ }, (0, react.createElement)("label", null, "搜索标题", (0, react.createElement)("input", {
3154
+ type: "search",
3155
+ value: query.search,
3156
+ placeholder: "输入章节标题",
3157
+ onChange: (event) => onQuery({
3158
+ search: event.target.value,
3159
+ cursor: ""
3160
+ })
3161
+ })), (0, react.createElement)("label", null, "状态", (0, react.createElement)("select", {
3162
+ value: query.status,
3163
+ onChange: (event) => onQuery({
3164
+ status: event.target.value,
3165
+ cursor: ""
3166
+ })
3167
+ }, (0, react.createElement)("option", { value: "all" }, "全部"), (0, react.createElement)("option", { value: "archived" }, "已归档"), (0, react.createElement)("option", { value: "draft" }, "草稿"))), (0, react.createElement)("label", null, "场景", (0, react.createElement)("select", {
3168
+ value: query.scene,
3169
+ onChange: (event) => onQuery({
3170
+ scene: event.target.value,
3171
+ cursor: ""
3172
+ })
3173
+ }, (0, react.createElement)("option", { value: "" }, "全部"), ...[
3174
+ "战斗/动作章",
3175
+ "日常过渡章",
3176
+ "情感爆发章",
3177
+ "纯叙事章"
3178
+ ].map((scene) => (0, react.createElement)("option", {
3179
+ value: scene,
3180
+ key: scene
3181
+ }, scene)))), (0, react.createElement)("label", null, "排序", (0, react.createElement)("select", {
3182
+ value: query.sort,
3183
+ onChange: (event) => onQuery({
3184
+ sort: event.target.value,
3185
+ cursor: ""
3186
+ })
3187
+ }, (0, react.createElement)("option", { value: "num_desc" }, "章号倒序"), (0, react.createElement)("option", { value: "num_asc" }, "章号正序"))));
3188
+ };
3189
+ const chapterDetailPanel = (detail, onClose) => {
3190
+ if (detail === null) return null;
3191
+ const payload = detail.section.payload;
3192
+ const items = asRow(payload?.items);
3193
+ const manuscript = typeof items?.manuscript === "string" ? items.manuscript : null;
3194
+ const plan = typeof items?.plan === "string" ? items.plan : null;
3195
+ const worldPlan = typeof items?.world_plan === "string" ? items.world_plan : null;
3196
+ const detailRow = asRow(items?.row);
3197
+ const rawSummaryEvents = items?.summary_events ?? detailRow?.summary_events;
3198
+ const summaryEvents = Array.isArray(rawSummaryEvents) ? rawSummaryEvents.filter((event) => typeof event === "string") : [];
3199
+ return (0, react.createElement)("section", {
3200
+ className: "nwe-chapter-detail",
3201
+ "aria-label": `第 ${detail.chapter} 章详情`
3202
+ }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h3", { className: "nwe-section-title" }, `第 ${detail.chapter} 章详情`), (0, react.createElement)("button", {
3203
+ type: "button",
3204
+ className: "nwe-refresh",
3205
+ onClick: () => onClose(null)
3206
+ }, "收起")), detail.section.loading && payload === null ? (0, react.createElement)("div", { className: "nwe-section-note" }, "正在读取章节详情…") : detail.section.error !== null ? (0, react.createElement)("div", { className: "nwe-empty" }, `章节详情读取失败:${detail.section.error}`) : (0, react.createElement)(react.Fragment, null, manuscript === null ? (0, react.createElement)("div", { className: "nwe-empty" }, "正文尚未写入。") : (0, react.createElement)("details", { open: true }, (0, react.createElement)("summary", null, `正文 · ${Array.from(manuscript).length} 字`), (0, react.createElement)("pre", { className: "nwe-detail-text" }, manuscript)), plan === null ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "归档 plan"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, plan)), worldPlan === null ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "世界计划"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, worldPlan)), summaryEvents.length === 0 ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "本章摘要"), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...summaryEvents.map((event, index) => (0, react.createElement)("li", { key: `${index}-${event}` }, event))))));
3207
+ };
3208
+ const qualityDetailPanel = (detail, onClose) => {
3209
+ if (detail === null) return null;
3210
+ const payload = detail.section.payload;
3211
+ const items = asRow(payload?.items);
3212
+ const review = typeof items?.review === "string" ? items.review : null;
3213
+ const check = typeof items?.check === "string" ? items.check : null;
3214
+ return (0, react.createElement)("section", {
3215
+ className: "nwe-chapter-detail",
3216
+ "aria-label": `第 ${detail.chapter} 章质量详情`
3217
+ }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h3", { className: "nwe-section-title" }, `第 ${detail.chapter} 章质量详情`), (0, react.createElement)("button", {
3218
+ type: "button",
3219
+ className: "nwe-refresh",
3220
+ onClick: () => onClose(null)
3221
+ }, "收起")), detail.section.loading && payload === null ? (0, react.createElement)("div", { className: "nwe-section-note" }, "正在读取质量详情…") : detail.section.error !== null ? (0, react.createElement)("div", { className: "nwe-empty" }, `质量详情读取失败:${detail.section.error}`) : review === null && check === null ? (0, react.createElement)("div", { className: "nwe-empty" }, "还没有评审或检查文档。") : (0, react.createElement)(react.Fragment, null, review === null ? null : (0, react.createElement)("details", { open: true }, (0, react.createElement)("summary", null, "评审 review"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, review)), check === null ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "检查 check"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, check))));
3222
+ };
3223
+ const chapterTable = (section, view, query, onQuery, detail = null, onDetail = () => void 0) => {
3224
+ if (section === null || section.loading && section.payload === null) return chaptersSection(view);
3225
+ if (section.error !== null && section.payload === null) return (0, react.createElement)(react.Fragment, null, (0, react.createElement)("div", { className: "nwe-empty" }, `章节读取失败:${section.error}`), chaptersSection(view));
3226
+ const payload = section.payload;
3227
+ const items = payload?.items;
3228
+ if (!Array.isArray(items)) return chaptersSection(view);
3229
+ const rows = items.map(asRow).filter((row) => row !== null);
3230
+ const total = payload?.total ?? rows.length;
3231
+ const cursor = query.cursor;
3232
+ const mobileCards = rows.length === 0 ? null : (0, react.createElement)("div", { className: "nwe-chapter-cards-mobile" }, ...rows.map((row, index) => {
3233
+ const num = textOf(row.num, String(index + 1));
3234
+ const score = typeof row.score === "number" ? String(row.score) : "—";
3235
+ return (0, react.createElement)("article", {
3236
+ className: "nwe-mobile-chapter",
3237
+ key: `${num}-${index}`
3238
+ }, (0, react.createElement)("span", { className: "nwe-mobile-chapter-num" }, `CH ${num.padStart(2, "0")}`), (0, react.createElement)("div", { className: "nwe-mobile-chapter-body" }, (0, react.createElement)("strong", null, textOf(row.title)), (0, react.createElement)("span", null, `${row.status === "draft" ? "草稿" : "已归档"} · ${textOf(row.word_count)} 字 · ${textOf(row.scene_type)}`)), (0, react.createElement)("span", { className: "nwe-mobile-chapter-score" }, score), (0, react.createElement)("button", {
3239
+ type: "button",
3240
+ className: "nwe-refresh",
3241
+ onClick: () => onDetail(Number(num) || index + 1)
3242
+ }, "详情"));
3243
+ }));
3244
+ return (0, react.createElement)("section", {
3245
+ className: "nwe-chapters-section",
3246
+ "aria-label": "章节表"
3247
+ }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, payload?.truncated === true && rows.length < total ? `显示 ${rows.length} / 共 ${total}` : payload?.truncated === true ? `共 ${total} 章 · 部分文本已截断` : `共 ${total} 章`)), rows.length === 0 ? (0, react.createElement)("div", { className: "nwe-empty" }, "没有匹配的章节。") : (0, react.createElement)("div", { className: "nwe-table-wrap" }, (0, react.createElement)("table", { className: "nwe-chapter-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, ...[
3248
+ "章",
3249
+ "标题",
3250
+ "状态",
3251
+ "字数",
3252
+ "评分",
3253
+ "场景",
3254
+ "终检",
3255
+ "归档",
3256
+ "详情"
3257
+ ].map((label) => (0, react.createElement)("th", {
3258
+ key: label,
3259
+ scope: "col"
3260
+ }, label)))), (0, react.createElement)("tbody", null, ...rows.map((row, index) => {
3261
+ const num = textOf(row.num, String(index + 1));
3262
+ const score = typeof row.score === "number" ? String(row.score) : "—";
3263
+ return (0, react.createElement)("tr", { key: `${num}-${index}` }, (0, react.createElement)("td", { className: "is-num" }, `第 ${num} 章`), (0, react.createElement)("td", { className: "nwe-table-title" }, textOf(row.title)), (0, react.createElement)("td", null, row.status === "draft" ? "草稿" : "已归档"), (0, react.createElement)("td", null, textOf(row.word_count)), (0, react.createElement)("td", null, score), (0, react.createElement)("td", null, textOf(row.scene_type)), (0, react.createElement)("td", null, finalCheckLabel(row.final_check)), (0, react.createElement)("td", null, localTime(typeof row.archived_at === "string" ? row.archived_at : null)), (0, react.createElement)("td", null, (0, react.createElement)("button", {
3264
+ type: "button",
3265
+ className: "nwe-refresh",
3266
+ onClick: () => onDetail(Number(num) || index + 1)
3267
+ }, "详情")));
3268
+ })))), mobileCards, (0, react.createElement)("div", { className: "nwe-pager" }, (0, react.createElement)("span", null, payload?.next_cursor !== null && payload?.next_cursor !== void 0 ? "已截断,可继续翻页" : payload?.truncated === true ? "部分文本已截断" : "已显示全部"), (0, react.createElement)("span", null, cursor === "" ? null : (0, react.createElement)("button", {
3269
+ type: "button",
3270
+ className: "nwe-refresh",
3271
+ onClick: () => onQuery({ cursor: "" })
3272
+ }, "回到第一页"), payload?.next_cursor === null || payload?.next_cursor === void 0 ? null : (0, react.createElement)("button", {
3273
+ type: "button",
3274
+ className: "nwe-refresh",
3275
+ onClick: () => onQuery({ cursor: String(payload.next_cursor) })
3276
+ }, "下一页"))), chapterDetailPanel(detail, onDetail));
3277
+ };
3278
+ const leadsSection = (items) => {
3279
+ const row = asRow(items);
3280
+ if (row === null) return sectionList("线索", items);
3281
+ const blocks = [
3282
+ sectionBlock("伏笔", row.foreshadows, "foreshadows"),
3283
+ sectionBlock("时间线", row.timeline, "timeline"),
3284
+ sectionBlock("关系", row.relationships, "relationships")
3285
+ ].filter((block) => block !== null);
3286
+ return blocks.length === 0 ? (0, react.createElement)("div", { className: "nwe-empty" }, "线索:暂无数据") : (0, react.createElement)("div", { className: "nwe-leads-layout" }, ...blocks);
3287
+ };
3288
+ const sectionMeta = (section) => {
3289
+ const payload = section.payload;
3290
+ if (payload === null || payload.total === void 0) return null;
3291
+ const returned = payload.returned ?? 0;
3292
+ const text = payload.truncated !== true ? `共 ${payload.total}` : returned < payload.total ? `显示 ${returned} / 共 ${payload.total}` : `共 ${payload.total} · 部分文本已截断`;
3293
+ return (0, react.createElement)("div", {
3294
+ className: "nwe-section-note",
3295
+ role: "status"
3296
+ }, text);
3297
+ };
3298
+ const sectionPanel = (tab, section, search = "", onSearch = () => void 0, chapter = 0, qualityDetail = null, onQualityDetail = () => void 0) => {
3299
+ if (section === null || section.loading && section.payload === null) return (0, react.createElement)("div", { className: "nwe-section-note" }, "正在读取栏目…");
3300
+ if (section.error !== null && section.payload === null) return (0, react.createElement)("div", { className: "nwe-empty" }, `栏目读取失败:${section.error}`);
3301
+ const items = section.payload?.items;
3302
+ const searchLabel = tab === "characters" ? "搜索人物" : tab === "foreshadows" ? "搜索线索" : tab === "worldbook" ? "搜索设定" : "";
3303
+ const withSearch = (content) => {
3304
+ return (0, react.createElement)(react.Fragment, null, (0, react.createElement)("label", { className: "nwe-section-search" }, (0, react.createElement)("span", {
3305
+ className: "nwe-search-icon",
3306
+ "aria-hidden": "true"
3307
+ }, sectionGlyph(tab === "characters" ? "person" : tab === "foreshadows" ? "seed" : "map", 16)), (0, react.createElement)("span", { className: "nwe-search-label" }, searchLabel), (0, react.createElement)("input", {
3308
+ type: "search",
3309
+ value: search,
3310
+ placeholder: "输入关键词",
3311
+ onChange: (event) => onSearch(event.target.value)
3312
+ })), sectionMeta(section), content);
3313
+ };
3314
+ if (tab === "characters") return withSearch(sectionList("人物", items, "person"));
3315
+ if (tab === "foreshadows") return withSearch(leadsSection(items));
3316
+ if (tab === "worldbook") return withSearch(worldbookSection(items));
3317
+ if (tab === "quality") return qualitySection(items, chapter, qualityDetail, onQualityDetail);
3318
+ if (tab === "health") return healthSection(items);
3319
+ return sectionList("数据", Array.isArray(items) ? items : [items]);
3320
+ };
1665
3321
  /**
1666
3322
  * Pure view: given a view model, return the element tree. Keeping the hooks in
1667
3323
  * `NovelPanel` makes every visual state renderable without a browser.
1668
3324
  */
1669
- function panelElement(view, loading, onRefresh, onSelectProject = () => void 0) {
3325
+ function panelElement(view, loading, onRefresh, onSelectProject = () => void 0, tab = "overview", onTab = () => void 0, section = null, chapterQuery = EMPTY_CHAPTER_QUERY, onChapterQuery = () => void 0, sectionSearch = "", onSectionSearch = () => void 0, chapterDetail = null, onChapterDetail = () => void 0, qualityDetail = null, onQualityDetail = () => void 0) {
1670
3326
  const ready = view.state === "ready";
1671
- const selectable = ready || view.state === "choose" || view.state === "missing" || view.state === "loading" && view.projects.length > 0;
3327
+ const selectable = view.projects.length > 0 || ready || view.state === "choose" || view.state === "missing" || view.state === "error" || view.state === "outdated" || view.state === "empty";
1672
3328
  return (0, react.createElement)("section", {
1673
3329
  "data-plugin": "dsh-novel-write-engine",
1674
3330
  className: "nwe-root",
1675
3331
  "aria-busy": loading,
1676
3332
  "data-state": view.state
1677
- }, (0, react.createElement)("style", { key: "nwe-style" }, CSS), (0, react.createElement)("header", { className: "nwe-hero" }, (0, react.createElement)("div", { className: "nwe-hero-title" }, (0, react.createElement)("div", { className: "nwe-eyebrow" }, "NOVEL STUDIO · 网文工坊"), (0, react.createElement)("h1", { className: "nwe-title" }, view.title), (0, react.createElement)("div", { className: "nwe-meta" }, (0, react.createElement)("span", { className: `nwe-stage${view.finished ? " is-done" : ""}` }, view.stageLabel), ready ? (0, react.createElement)("span", null, view.subtitle) : null, ready ? (0, react.createElement)("span", { "aria-hidden": "true" }, "·") : null, ready ? (0, react.createElement)("span", null, view.revisionLabel) : null, view.state === "choose" || view.state === "missing" || view.state === "loading" ? (0, react.createElement)("span", null, view.subtitle) : null), ready && view.root !== "" ? (0, react.createElement)("div", { className: "nwe-rootline" }, view.root) : null), (0, react.createElement)("div", { className: "nwe-actions" }, selectable ? (0, react.createElement)(ProjectSelector, {
3333
+ }, (0, react.createElement)("style", { key: "nwe-style" }, CSS), (0, react.createElement)("header", { className: "nwe-hero" }, (0, react.createElement)("div", { className: "nwe-brand" }, (0, react.createElement)("span", {
3334
+ className: "nwe-brand-mark",
3335
+ "aria-hidden": "true"
3336
+ }, "文"), (0, react.createElement)("div", { className: "nwe-hero-title" }, (0, react.createElement)("div", { className: "nwe-eyebrow" }, "网文工坊 · NOVEL WORKBENCH"), (0, react.createElement)("h1", { className: "nwe-title" }, view.title), (0, react.createElement)("div", { className: "nwe-meta" }, (0, react.createElement)("span", { className: `nwe-stage${view.finished ? " is-done" : ""}` }, view.stageLabel), ready ? (0, react.createElement)("span", null, view.subtitle) : null, ready ? (0, react.createElement)("span", { className: "nwe-revision" }, view.revisionLabel) : null, view.state === "choose" || view.state === "missing" || view.state === "loading" ? (0, react.createElement)("span", null, view.subtitle) : null), ready && view.root !== "" ? (0, react.createElement)("div", { className: "nwe-rootline" }, view.root) : null)), (0, react.createElement)("div", { className: "nwe-actions" }, selectable ? (0, react.createElement)(ProjectSelector, {
1678
3337
  view,
1679
3338
  onSelect: onSelectProject
1680
3339
  }) : null, (0, react.createElement)(NovelHelp), (0, react.createElement)("button", {
@@ -1686,14 +3345,29 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1686
3345
  }, refreshIcon(), loading ? "刷新中…" : "刷新"))), ready && view.damaged ? (0, react.createElement)("div", {
1687
3346
  className: "nwe-damaged",
1688
3347
  role: "status"
1689
- }, "项目定义不可读;先用 novel_state 查看,再用 novel_amend 按 next 修复。面板不会把它伪装成正常项目。") : null, ready && view.nextTitle !== "" ? nextCard(view) : null, ready ? (0, react.createElement)("div", {
3348
+ }, "项目定义不可读;先用 novel_state 查看,再用 novel_amend 按 next 修复。面板不会把它伪装成正常项目。") : null, ready && view.nextTitle !== "" ? nextCard(view) : null, ready ? tabBar(tab, onTab) : null, ready && tab === "overview" ? (0, react.createElement)("div", {
1690
3349
  className: "nwe-stats",
1691
3350
  role: "group",
1692
3351
  "aria-label": "项目统计"
1693
- }, stat("已归档", String(view.stats.chapters), "章"), stat("总字数", view.stats.words.toLocaleString("zh-CN"), (view.stats.draftWords ?? 0) > 0 ? `字(归档 ${(view.stats.archivedWords ?? 0).toLocaleString("zh-CN")} + 草稿 ${view.stats.draftWords.toLocaleString("zh-CN")})` : "字"), stat("全书均分", view.stats.chapters === 0 ? "—" : String(view.stats.averageScore), "/150"), stat("待处理", String(view.stats.blocking), "项")) : null, ready ? bookDataSection(view) : null, view.state === "loading" ? loadingState() : null, view.state === "choose" ? choosePanel(view, onSelectProject) : null, view.state === "missing" ? missingPanel(view) : null, view.state === "error" || view.state === "empty" || view.state === "outdated" ? statePanel(view.state, view, loading, onRefresh) : null, ready ? (0, react.createElement)("div", { className: "nwe-desk" }, (0, react.createElement)("div", { className: "nwe-desk-main" }, (0, react.createElement)("div", { className: "nwe-desk-heading" }, (0, react.createElement)("span", { className: "nwe-desk-kicker" }, "当前一章"), (0, react.createElement)("span", { className: "nwe-desk-note" }, view.stageLabel)), phases(view), blockingSection(view), advisoriesSection(view), pendingSection(view), legacySection(view), chaptersSection(view)), (0, react.createElement)("aside", {
3352
+ }, stat("已归档", String(view.stats.chapters), "章"), stat("总字数", view.stats.words.toLocaleString("zh-CN"), (view.stats.draftWords ?? 0) > 0 ? `字(归档 ${(view.stats.archivedWords ?? 0).toLocaleString("zh-CN")} + 草稿 ${view.stats.draftWords.toLocaleString("zh-CN")})` : "字"), stat("全书均分", view.stats.chapters === 0 ? "—" : String(view.stats.averageScore), "/150"), stat("待处理", String(view.stats.blocking), "项")) : null, ready && tab === "overview" ? overviewCharts(view) : null, view.state === "loading" ? loadingState() : null, view.state === "choose" ? choosePanel(view, onSelectProject) : null, view.state === "missing" ? missingPanel(view) : null, view.state === "error" || view.state === "empty" || view.state === "outdated" ? statePanel(view.state, view, loading, onRefresh) : null, ready && tab === "overview" ? (0, react.createElement)("div", {
3353
+ className: "nwe-desk",
3354
+ role: "tabpanel",
3355
+ id: "nwe-panel-overview",
3356
+ "aria-labelledby": "nwe-tab-overview"
3357
+ }, (0, react.createElement)("div", { className: "nwe-desk-main" }, (0, react.createElement)("div", { className: "nwe-desk-heading" }, (0, react.createElement)("span", { className: "nwe-desk-kicker" }, "当前一章"), (0, react.createElement)("span", { className: "nwe-desk-note" }, view.stageLabel)), phases(view), blockingSection(view), advisoriesSection(view), pendingSection(view), legacySection(view), chaptersSection(view)), (0, react.createElement)("aside", {
1694
3358
  className: "nwe-desk-side",
1695
3359
  "aria-label": "全书与活动"
1696
- }, bookSection(view), activitySection(view))) : null, footer());
3360
+ }, bookGlance(view), activitySection(view))) : null, ready && tab === "overview" ? bookDossier(view) : null, ready && tab === "chapters" ? (0, react.createElement)("div", {
3361
+ className: "nwe-desk nwe-desk-single",
3362
+ role: "tabpanel",
3363
+ id: "nwe-panel-chapters",
3364
+ "aria-labelledby": "nwe-tab-chapters"
3365
+ }, (0, react.createElement)("div", { className: "nwe-desk-main" }, tabIntro("chapters", view, section), blockingSection(view), chapterToolbar(chapterQuery, onChapterQuery), chapterTable(section, view, chapterQuery, onChapterQuery, chapterDetail, onChapterDetail))) : null, ready && tab !== "overview" && tab !== "chapters" ? (0, react.createElement)("section", {
3366
+ className: "nwe-section nwe-tab-panel",
3367
+ role: "tabpanel",
3368
+ id: `nwe-panel-${tab}`,
3369
+ "aria-labelledby": `nwe-tab-${tab}`
3370
+ }, tabIntro(tab, view, section), tab === "health" ? (0, react.createElement)(react.Fragment, null, blockingSection(view), advisoriesSection(view), pendingSection(view), legacySection(view)) : null, sectionPanel(tab, section, sectionSearch, onSectionSearch, view.chapter, qualityDetail, onQualityDetail)) : null, footer(view, onRefresh));
1697
3371
  }
1698
3372
  const PANEL_ROOT_KEY = "nwe.panel-root.v1";
1699
3373
  const storage = () => globalThis.localStorage;
@@ -1715,22 +3389,42 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1715
3389
  };
1716
3390
  const panelPath = (root) => {
1717
3391
  const base = PANEL_ROUTE.replace(/^\/+/, "");
1718
- return root === null ? base : `${base}?root=${encodeURIComponent(root)}`;
3392
+ return root === null ? `${base}?lean=1` : `${base}?root=${encodeURIComponent(root)}&lean=1`;
1719
3393
  };
1720
3394
  /** Stateful wrapper: fetch once, refresh on demand and after returning to the tab. */
1721
3395
  function NovelPanel() {
1722
3396
  const [view, setView] = (0, react.useState)(() => buildView(null));
1723
3397
  const [loading, setLoading] = (0, react.useState)(true);
1724
3398
  const [switching, setSwitching] = (0, react.useState)(false);
3399
+ const [sync, setSync] = (0, react.useState)({
3400
+ lastSync: null,
3401
+ syncError: null
3402
+ });
3403
+ const [tab, setTab] = (0, react.useState)("overview");
3404
+ const [section, setSection] = (0, react.useState)(null);
3405
+ const [chapterQuery, setChapterQuery] = (0, react.useState)(EMPTY_CHAPTER_QUERY);
3406
+ const [sectionSearch, setSectionSearch] = (0, react.useState)("");
3407
+ const [chapterDetail, setChapterDetail] = (0, react.useState)(null);
3408
+ const [qualityDetail, setQualityDetail] = (0, react.useState)(null);
3409
+ const detailController = (0, react.useRef)(null);
3410
+ const [revisionTick, setRevisionTick] = (0, react.useState)(0);
3411
+ const syncFailures = (0, react.useRef)(0);
3412
+ const nextSyncAt = (0, react.useRef)(0);
1725
3413
  const [selectedRoot, setSelectedRoot] = (0, react.useState)(() => readStoredRoot());
1726
3414
  const alive = (0, react.useRef)(true);
1727
3415
  const requestId = (0, react.useRef)(0);
1728
3416
  const visibleRequest = (0, react.useRef)(0);
1729
3417
  const controllerRef = (0, react.useRef)(null);
1730
3418
  const queryRootRef = (0, react.useRef)(selectedRoot);
3419
+ const sectionEtagRef = (0, react.useRef)(null);
3420
+ const bundleEtagRef = (0, react.useRef)(null);
3421
+ const lastSilentAt = (0, react.useRef)(0);
3422
+ const quietPolls = (0, react.useRef)(0);
1731
3423
  const loadRef = (0, react.useRef)(async () => void 0);
1732
3424
  const load = (0, react.useCallback)(async (silent = false, root = queryRootRef.current) => {
1733
3425
  if (silent && visibleRequest.current !== 0) return;
3426
+ if (silent && Date.now() - lastSilentAt.current < 1e3) return;
3427
+ if (silent) lastSilentAt.current = Date.now();
1734
3428
  const id = ++requestId.current;
1735
3429
  controllerRef.current?.abort();
1736
3430
  const controller = new AbortController();
@@ -1744,8 +3438,36 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1744
3438
  const anySignal = timeout === void 0 ? controller.signal : typeof any === "function" ? any([controller.signal, timeout]) : controller.signal;
1745
3439
  const timeoutTimer = timeout === void 0 || typeof any !== "function" ? setTimeout(() => controller.abort(), 1e4) : void 0;
1746
3440
  try {
1747
- const result = await fetchPanel(fetch, panelPath(root), anySignal);
3441
+ const rootKey = root ?? "";
3442
+ const cachedBundleEtag = bundleEtagRef.current?.root === rootKey ? bundleEtagRef.current.etag : void 0;
3443
+ const result = await fetchPanel(fetch, panelPath(root), anySignal, cachedBundleEtag);
1748
3444
  if (!alive.current || id !== requestId.current) return;
3445
+ if (result.notModified === true) {
3446
+ syncFailures.current = 0;
3447
+ quietPolls.current += 1;
3448
+ nextSyncAt.current = Date.now() + (quietPolls.current >= 2 ? 6e4 : 3e4);
3449
+ setSync({
3450
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
3451
+ syncError: null
3452
+ });
3453
+ return;
3454
+ }
3455
+ if (result.error === null) {
3456
+ syncFailures.current = 0;
3457
+ quietPolls.current = 0;
3458
+ nextSyncAt.current = 0;
3459
+ setSync({
3460
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
3461
+ syncError: null
3462
+ });
3463
+ } else {
3464
+ syncFailures.current += 1;
3465
+ nextSyncAt.current = Date.now() + (syncFailures.current === 1 ? 3e4 : 6e4);
3466
+ setSync((previous) => ({
3467
+ ...previous,
3468
+ syncError: result.error
3469
+ }));
3470
+ }
1749
3471
  const payload = result.payload;
1750
3472
  if (payload !== null && payload.missing?.reason === "not_registered" && root !== null) {
1751
3473
  queryRootRef.current = null;
@@ -1754,7 +3476,14 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1754
3476
  await loadRef.current(false, null);
1755
3477
  return;
1756
3478
  }
1757
- if (payload !== null) setSelectedRoot(payload.selection.root);
3479
+ if (payload !== null) {
3480
+ if (payload.etag !== void 0) bundleEtagRef.current = {
3481
+ root: rootKey,
3482
+ etag: payload.etag
3483
+ };
3484
+ setSelectedRoot(payload.selection.root);
3485
+ setRevisionTick((tick) => tick + 1);
3486
+ }
1758
3487
  setView((previous) => {
1759
3488
  const next = buildView(payload, result.error);
1760
3489
  if (silent && result.error !== null && previous.projects.length > 0) return previous;
@@ -1770,11 +3499,81 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1770
3499
  }
1771
3500
  }, []);
1772
3501
  loadRef.current = load;
3502
+ (0, react.useEffect)(() => {
3503
+ if (tab === "overview" || view.state !== "ready") {
3504
+ setSection(null);
3505
+ return;
3506
+ }
3507
+ const root = queryRootRef.current;
3508
+ const base = panelPath(root);
3509
+ const params = new URLSearchParams({ section: tab });
3510
+ if (tab === "chapters") {
3511
+ if (chapterQuery.search !== "") params.set("search", chapterQuery.search);
3512
+ if (chapterQuery.status !== "all") params.set("status", chapterQuery.status);
3513
+ if (chapterQuery.scene !== "") params.set("scene", chapterQuery.scene);
3514
+ params.set("sort", chapterQuery.sort);
3515
+ if (chapterQuery.cursor !== "") params.set("cursor", chapterQuery.cursor);
3516
+ } else if (tab === "characters" || tab === "foreshadows" || tab === "worldbook") {
3517
+ if (sectionSearch.trim() !== "") params.set("search", sectionSearch.trim());
3518
+ }
3519
+ const path = `${base}${base.includes("?") ? "&" : "?"}${params.toString()}`;
3520
+ const queryKey = `${tab}|${root ?? ""}|${JSON.stringify(chapterQuery)}|${sectionSearch}`;
3521
+ const cachedEtag = sectionEtagRef.current?.key === queryKey ? sectionEtagRef.current.etag : void 0;
3522
+ const controller = new AbortController();
3523
+ setSection((previous) => previous === null ? {
3524
+ loading: true,
3525
+ error: null,
3526
+ payload: null
3527
+ } : {
3528
+ ...previous,
3529
+ loading: true,
3530
+ error: null
3531
+ });
3532
+ fetchPanel(fetch, path, controller.signal, cachedEtag).then((result) => {
3533
+ if (controller.signal.aborted) return;
3534
+ if (result.notModified === true) {
3535
+ setSection((previous) => previous === null ? {
3536
+ loading: false,
3537
+ error: null,
3538
+ payload: null
3539
+ } : {
3540
+ ...previous,
3541
+ loading: false,
3542
+ error: null
3543
+ });
3544
+ return;
3545
+ }
3546
+ if (result.payload?.etag !== void 0) sectionEtagRef.current = {
3547
+ key: queryKey,
3548
+ etag: result.payload.etag
3549
+ };
3550
+ setSection({
3551
+ loading: false,
3552
+ error: result.error,
3553
+ payload: result.payload
3554
+ });
3555
+ });
3556
+ return () => controller.abort();
3557
+ }, [
3558
+ tab,
3559
+ view.state,
3560
+ selectedRoot,
3561
+ chapterQuery,
3562
+ revisionTick,
3563
+ sectionSearch
3564
+ ]);
1773
3565
  const selectProject = (0, react.useCallback)((root) => {
1774
3566
  queryRootRef.current = root;
1775
3567
  storeRoot(root);
1776
3568
  setSelectedRoot(root);
3569
+ setChapterDetail(null);
3570
+ setQualityDetail(null);
3571
+ detailController.current?.abort();
1777
3572
  setSwitching(true);
3573
+ setChapterQuery((previous) => ({
3574
+ ...previous,
3575
+ cursor: ""
3576
+ }));
1778
3577
  setView((previous) => ({
1779
3578
  ...previous,
1780
3579
  state: "loading",
@@ -1799,7 +3598,9 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1799
3598
  const doc = globalThis.document;
1800
3599
  if (win === void 0 || doc === void 0) return;
1801
3600
  const refreshWhenVisible = () => {
1802
- if (doc.visibilityState !== "hidden") load(true);
3601
+ if (doc.visibilityState === "hidden") return;
3602
+ if (Date.now() < nextSyncAt.current) return;
3603
+ load(true);
1803
3604
  };
1804
3605
  const timer = win.setInterval(refreshWhenVisible, 15e3);
1805
3606
  win.addEventListener("focus", refreshWhenVisible);
@@ -1807,15 +3608,55 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1807
3608
  return () => {
1808
3609
  alive.current = false;
1809
3610
  controllerRef.current?.abort();
3611
+ detailController.current?.abort();
1810
3612
  win.clearInterval(timer);
1811
3613
  win.removeEventListener("focus", refreshWhenVisible);
1812
3614
  doc.removeEventListener("visibilitychange", refreshWhenVisible);
1813
3615
  };
1814
3616
  }, [load]);
3617
+ const loadDetail = (section, chapter, setter) => {
3618
+ detailController.current?.abort();
3619
+ if (chapter === null) {
3620
+ setter(null);
3621
+ return;
3622
+ }
3623
+ const root = queryRootRef.current;
3624
+ const controller = new AbortController();
3625
+ detailController.current = controller;
3626
+ const path = `${panelPath(root)}&section=${section}&chapter=${encodeURIComponent(String(chapter))}`;
3627
+ setter({
3628
+ chapter,
3629
+ section: {
3630
+ loading: true,
3631
+ error: null,
3632
+ payload: null
3633
+ }
3634
+ });
3635
+ fetchPanel(fetch, path, controller.signal).then((result) => {
3636
+ if (!alive.current || controller.signal.aborted || queryRootRef.current !== root) return;
3637
+ setter({
3638
+ chapter,
3639
+ section: {
3640
+ loading: false,
3641
+ error: result.error,
3642
+ payload: result.payload
3643
+ }
3644
+ });
3645
+ });
3646
+ };
3647
+ const openChapterDetail = (chapter) => loadDetail("chapter_detail", chapter, setChapterDetail);
3648
+ const openQualityDetail = (chapter) => loadDetail("quality_detail", chapter, setQualityDetail);
1815
3649
  return panelElement({
1816
3650
  ...view,
1817
- selectedRoot
1818
- }, loading || switching, () => void load(false, queryRootRef.current), selectProject);
3651
+ selectedRoot,
3652
+ ...sync
3653
+ }, loading || switching, () => {
3654
+ nextSyncAt.current = 0;
3655
+ load(false, queryRootRef.current);
3656
+ }, selectProject, tab, setTab, section, chapterQuery, (patch) => setChapterQuery((previous) => ({
3657
+ ...previous,
3658
+ ...patch
3659
+ })), sectionSearch, setSectionSearch, chapterDetail, openChapterDetail, qualityDetail, openQualityDetail);
1819
3660
  }
1820
3661
  /** Sidebar icon: a small book mark, inheriting the current text colour. */
1821
3662
  function NovelIcon({ size }) {