concept-atlas-dense-explain 2.0.0 → 3.0.0

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.
@@ -13,9 +13,10 @@ import { useFigureScope } from './FigureScope.jsx';
13
13
  // attributes on <html> change.
14
14
  const appearanceStore = (() => {
15
15
  const listeners = new Set();
16
- const readKey = () => (typeof document === 'undefined'
17
- ? ''
18
- : `${document.documentElement.getAttribute('data-skin') || ''}:${document.documentElement.getAttribute('data-theme') || ''}`);
16
+ const readKey = () =>
17
+ typeof document === 'undefined'
18
+ ? ''
19
+ : `${document.documentElement.getAttribute('data-skin') || ''}:${document.documentElement.getAttribute('data-theme') || ''}`;
19
20
  let observer = null;
20
21
  let lastKey = '';
21
22
  return {
@@ -30,7 +31,10 @@ const appearanceStore = (() => {
30
31
  listeners.forEach(fn => fn());
31
32
  }
32
33
  });
33
- observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-skin', 'data-theme'] });
34
+ observer.observe(document.documentElement, {
35
+ attributes: true,
36
+ attributeFilter: ['data-skin', 'data-theme']
37
+ });
34
38
  }
35
39
  return () => {
36
40
  listeners.delete(listener);
@@ -40,12 +44,16 @@ const appearanceStore = (() => {
40
44
  }
41
45
  };
42
46
  },
43
- getKey: readKey,
47
+ getKey: readKey
44
48
  };
45
49
  })();
46
50
 
47
51
  function useAppearanceKey() {
48
- return React.useSyncExternalStore(appearanceStore.subscribe, appearanceStore.getKey, appearanceStore.getKey);
52
+ return React.useSyncExternalStore(
53
+ appearanceStore.subscribe,
54
+ appearanceStore.getKey,
55
+ appearanceStore.getKey
56
+ );
49
57
  }
50
58
 
51
59
  const MERMAID_FALLBACKS = {
@@ -53,7 +61,7 @@ const MERMAID_FALLBACKS = {
53
61
  primaryTextColor: '#e0f2fe',
54
62
  primaryBorderColor: '#38bdf8',
55
63
  lineColor: '#64748b',
56
- tertiaryColor: '#0f172a',
64
+ tertiaryColor: '#0f172a'
57
65
  };
58
66
 
59
67
  function configureMermaid() {
@@ -72,8 +80,8 @@ function configureMermaid() {
72
80
  lineColor: read('--mermaid-line', MERMAID_FALLBACKS.lineColor),
73
81
  secondaryColor: nodeBg,
74
82
  tertiaryColor: read('--mermaid-canvas', MERMAID_FALLBACKS.tertiaryColor),
75
- fontFamily: read('--font-sans', "'Plus Jakarta Sans', sans-serif"),
76
- },
83
+ fontFamily: read('--font-sans', "'Plus Jakarta Sans', sans-serif")
84
+ }
77
85
  });
78
86
  }
79
87
 
@@ -83,18 +91,21 @@ function childrenToText(children) {
83
91
  if (children === null || children === undefined || typeof children === 'boolean') return '';
84
92
  if (typeof children === 'string' || typeof children === 'number') return String(children);
85
93
  if (Array.isArray(children)) return children.map(childrenToText).join('');
86
- if (React.isValidElement(children) && children.props) return childrenToText(children.props.children);
94
+ if (React.isValidElement(children) && children.props)
95
+ return childrenToText(children.props.children);
87
96
  return '';
88
97
  }
89
98
 
90
99
  function slugify(text) {
91
- return String(text)
92
- .trim()
93
- .toLowerCase()
94
- .replace(/\s+/g, '-')
95
- .replace(/[^\w\u4e00-\u9fa5-]/g, '')
96
- .replace(/-+/g, '-')
97
- .replace(/^-|-$/g, '') || 'section';
100
+ return (
101
+ String(text)
102
+ .trim()
103
+ .toLowerCase()
104
+ .replace(/\s+/g, '-')
105
+ .replace(/[^\w\u4e00-\u9fa5-]/g, '')
106
+ .replace(/-+/g, '-')
107
+ .replace(/^-|-$/g, '') || 'section'
108
+ );
98
109
  }
99
110
 
100
111
  const katexCache = new Map();
@@ -109,7 +120,7 @@ function renderTex(tex, displayMode) {
109
120
  throwOnError: false,
110
121
  errorColor: 'var(--accent-rose)',
111
122
  strict: 'ignore',
112
- trust: false,
123
+ trust: false
113
124
  });
114
125
  } catch {
115
126
  html = `<code class="math-error">${tex.replace(/[<>&]/g, '')}</code>`;
@@ -122,22 +133,54 @@ const FONT_PRESETS = {
122
133
  compact: { scale: 0.95, lineHeight: 1.7 },
123
134
  normal: { scale: 1, lineHeight: 1.8 },
124
135
  large: { scale: 1.15, lineHeight: 1.85 },
125
- xlarge: { scale: 1.3, lineHeight: 1.9 },
136
+ xlarge: { scale: 1.3, lineHeight: 1.9 }
126
137
  };
127
138
 
128
139
  // Data Layer Components
129
- export function ExplainPage({ id, title, summary, layout = 'editorial', density = 'reading', children }) {
130
- return <div data-component="ExplainPage" data-id={id} data-title={title} data-summary={summary} data-layout={layout} data-density={density}>{children}</div>;
140
+ export function ExplainPage({
141
+ id,
142
+ title,
143
+ summary,
144
+ layout = 'editorial',
145
+ density = 'reading',
146
+ children
147
+ }) {
148
+ return (
149
+ <div
150
+ data-component="ExplainPage"
151
+ data-id={id}
152
+ data-title={title}
153
+ data-summary={summary}
154
+ data-layout={layout}
155
+ data-density={density}
156
+ >
157
+ {children}
158
+ </div>
159
+ );
131
160
  }
132
161
  ExplainPage.displayName = 'ExplainPage';
133
162
 
134
163
  export function ConceptGraph({ root, children }) {
135
- return <div data-component="ConceptGraph" data-root={root}>{children}</div>;
164
+ return (
165
+ <div data-component="ConceptGraph" data-root={root}>
166
+ {children}
167
+ </div>
168
+ );
136
169
  }
137
170
  ConceptGraph.displayName = 'ConceptGraph';
138
171
 
139
- export function ConceptNode({ id, title, level = 'L2', parent = null, children, input, output, summary }) {
140
- return <div data-component="ConceptNode" data-id={id} data-title={title} data-level={level} data-parent={parent}>{children}</div>;
172
+ export function ConceptNode({ id, title, level = 'L2', parent = null, children }) {
173
+ return (
174
+ <div
175
+ data-component="ConceptNode"
176
+ data-id={id}
177
+ data-title={title}
178
+ data-level={level}
179
+ data-parent={parent}
180
+ >
181
+ {children}
182
+ </div>
183
+ );
141
184
  }
142
185
  ConceptNode.displayName = 'ConceptNode';
143
186
 
@@ -152,7 +195,17 @@ export function Children({ children }) {
152
195
  Children.displayName = 'Children';
153
196
 
154
197
  export function Relation({ from, to, type = 'depends-on', label, description, children }) {
155
- return <div data-component="Relation" data-from={from} data-to={to} data-type={type} data-label={label}>{children || description}</div>;
198
+ return (
199
+ <div
200
+ data-component="Relation"
201
+ data-from={from}
202
+ data-to={to}
203
+ data-type={type}
204
+ data-label={label}
205
+ >
206
+ {children || description}
207
+ </div>
208
+ );
156
209
  }
157
210
  Relation.displayName = 'Relation';
158
211
 
@@ -181,7 +234,9 @@ export function Implementation({ language = 'text', title = '实现代码', chil
181
234
  {language && <span className="lang-badge">{language}</span>}
182
235
  </div>
183
236
  )}
184
- <pre className="code-block"><code>{typeof children === 'string' ? children.trim() : children}</code></pre>
237
+ <pre className="code-block">
238
+ <code>{typeof children === 'string' ? children.trim() : children}</code>
239
+ </pre>
185
240
  </div>
186
241
  );
187
242
  }
@@ -194,7 +249,15 @@ Implementation.displayName = 'Implementation';
194
249
  * Pass the code as a string through `code` (or as children) so MDX parsing and
195
250
  * the validator never confuse code with markup.
196
251
  */
197
- export function CodeBlock({ code, language = 'text', title, caption, lineNumbers = false, wrap = false, children }) {
252
+ export function CodeBlock({
253
+ code,
254
+ language = 'text',
255
+ title,
256
+ caption,
257
+ lineNumbers = false,
258
+ wrap = false,
259
+ children
260
+ }) {
198
261
  const raw = typeof code === 'string' ? code : childrenToText(children);
199
262
  const text = typeof raw === 'string' ? raw.replace(/^\n+|\s+$/g, '') : '';
200
263
  if (!text) return null;
@@ -208,10 +271,20 @@ export function CodeBlock({ code, language = 'text', title, caption, lineNumbers
208
271
  {showLanguage && <span className="lang-badge">{language}</span>}
209
272
  </div>
210
273
  )}
211
- <pre className={`code-block${lineNumbers ? ' code-block-numbered' : ''}${wrap ? ' code-block-wrap' : ''}`}>
212
- {lineNumbers
213
- ? <code>{text.split('\n').map((line, index) => <span className="code-line" key={index}>{line}</span>)}</code>
214
- : <code>{text}</code>}
274
+ <pre
275
+ className={`code-block${lineNumbers ? ' code-block-numbered' : ''}${wrap ? ' code-block-wrap' : ''}`}
276
+ >
277
+ {lineNumbers ? (
278
+ <code>
279
+ {text.split('\n').map((line, index) => (
280
+ <span className="code-line" key={index}>
281
+ {line}
282
+ </span>
283
+ ))}
284
+ </code>
285
+ ) : (
286
+ <code>{text}</code>
287
+ )}
215
288
  </pre>
216
289
  {caption && <figcaption className="semantic-code-caption">{caption}</figcaption>}
217
290
  </figure>
@@ -288,16 +361,16 @@ export function Compare({ items = [], children }) {
288
361
  </tr>
289
362
  </thead>
290
363
  <tbody>
291
- {items[0]?.rows ? (
292
- items[0].rows.map((rowKey, rIdx) => (
293
- <tr key={rIdx}>
294
- <td className="compare-row-label">{rowKey}</td>
295
- {items.map((it, idx) => (
296
- <td key={idx}>{it.values?.[rIdx] || '-'}</td>
297
- ))}
298
- </tr>
299
- ))
300
- ) : null}
364
+ {items[0]?.rows
365
+ ? items[0].rows.map((rowKey, rIdx) => (
366
+ <tr key={rIdx}>
367
+ <td className="compare-row-label">{rowKey}</td>
368
+ {items.map((it, idx) => (
369
+ <td key={idx}>{it.values?.[rIdx] || '-'}</td>
370
+ ))}
371
+ </tr>
372
+ ))
373
+ : null}
301
374
  </tbody>
302
375
  </table>
303
376
  </div>
@@ -315,13 +388,17 @@ export function DecisionMatrix({ title = '权衡矩阵', headers = [], rows = []
315
388
  <table className="matrix-table">
316
389
  <thead>
317
390
  <tr>
318
- {headers.map((h, i) => <th key={i}>{h}</th>)}
391
+ {headers.map((h, i) => (
392
+ <th key={i}>{h}</th>
393
+ ))}
319
394
  </tr>
320
395
  </thead>
321
396
  <tbody>
322
397
  {rows.map((row, i) => (
323
398
  <tr key={i}>
324
- {row.map((cell, j) => <td key={j}>{cell}</td>)}
399
+ {row.map((cell, j) => (
400
+ <td key={j}>{cell}</td>
401
+ ))}
325
402
  </tr>
326
403
  ))}
327
404
  </tbody>
@@ -340,7 +417,16 @@ export function Flow({ steps = [], children }) {
340
417
  {steps.map((step, idx) => (
341
418
  <div key={idx} className="flow-step">
342
419
  <span className="step-num">{idx + 1}</span>
343
- <span className="step-text">{typeof step === 'object' ? <><strong>{step.title || step.label || `步骤 ${idx + 1}`}</strong>{step.description && <small>{step.description}</small>}</> : step}</span>
420
+ <span className="step-text">
421
+ {typeof step === 'object' ? (
422
+ <>
423
+ <strong>{step.title || step.label || `步骤 ${idx + 1}`}</strong>
424
+ {step.description && <small>{step.description}</small>}
425
+ </>
426
+ ) : (
427
+ step
428
+ )}
429
+ </span>
344
430
  {idx < steps.length - 1 && <span className="step-arrow">→</span>}
345
431
  </div>
346
432
  ))}
@@ -369,7 +455,13 @@ export function Timeline({ events = [], children }) {
369
455
  }
370
456
  Timeline.displayName = 'Timeline';
371
457
 
372
- export function FrameworkModel({ title = '结构化模型', type = 'elements', elements = [], question, children }) {
458
+ export function FrameworkModel({
459
+ title = '结构化模型',
460
+ type = 'elements',
461
+ elements = [],
462
+ question,
463
+ children
464
+ }) {
373
465
  const labels = { elements: '要素', stages: '阶段', layers: '层级', cycle: '循环' };
374
466
  return (
375
467
  <section className={`semantic-framework-model model-${type}`}>
@@ -388,16 +480,26 @@ export function FrameworkModel({ title = '结构化模型', type = 'elements', e
388
480
  </div>
389
481
  ))}
390
482
  </div>
391
- ) : children}
483
+ ) : (
484
+ children
485
+ )}
392
486
  </section>
393
487
  );
394
488
  }
395
489
  FrameworkModel.displayName = 'FrameworkModel';
396
490
 
397
- export function MatrixModel({ title = '二维矩阵', xLabel = '横轴', yLabel = '纵轴', cells = [], children }) {
491
+ export function MatrixModel({
492
+ title = '二维矩阵',
493
+ xLabel = '横轴',
494
+ yLabel = '纵轴',
495
+ cells = [],
496
+ children
497
+ }) {
398
498
  return (
399
499
  <section className="semantic-model-matrix">
400
- <div className="framework-model-head"><span className="semantic-tag">▦ {title}</span></div>
500
+ <div className="framework-model-head">
501
+ <span className="semantic-tag">▦ {title}</span>
502
+ </div>
401
503
  {cells.length > 0 ? (
402
504
  <div className="model-matrix-grid">
403
505
  <div className="model-matrix-axis model-matrix-y">{yLabel}</div>
@@ -409,7 +511,9 @@ export function MatrixModel({ title = '二维矩阵', xLabel = '横轴', yLabel
409
511
  </div>
410
512
  ))}
411
513
  </div>
412
- ) : children}
514
+ ) : (
515
+ children
516
+ )}
413
517
  </section>
414
518
  );
415
519
  }
@@ -418,9 +522,26 @@ MatrixModel.displayName = 'MatrixModel';
418
522
  export function FormulaModel({ title = '公式模型', formula, variables = [], children }) {
419
523
  return (
420
524
  <section className="semantic-model-formula">
421
- <div className="framework-model-head"><span className="semantic-tag">∑ {title}</span></div>
422
- {formula && <div className="model-formula-expression"><code>{formula}</code></div>}
423
- {variables.length > 0 ? <dl className="model-formula-variables">{variables.map((variable, index) => <React.Fragment key={index}><dt>{variable.symbol || variable.name}</dt><dd>{variable.description || variable.value}</dd></React.Fragment>)}</dl> : children}
525
+ <div className="framework-model-head">
526
+ <span className="semantic-tag">∑ {title}</span>
527
+ </div>
528
+ {formula && (
529
+ <div className="model-formula-expression">
530
+ <code>{formula}</code>
531
+ </div>
532
+ )}
533
+ {variables.length > 0 ? (
534
+ <dl className="model-formula-variables">
535
+ {variables.map((variable, index) => (
536
+ <React.Fragment key={index}>
537
+ <dt>{variable.symbol || variable.name}</dt>
538
+ <dd>{variable.description || variable.value}</dd>
539
+ </React.Fragment>
540
+ ))}
541
+ </dl>
542
+ ) : (
543
+ children
544
+ )}
424
545
  </section>
425
546
  );
426
547
  }
@@ -429,17 +550,25 @@ FormulaModel.displayName = 'FormulaModel';
429
550
  export function PyramidModel({ title = '金字塔模型', levels = [], children }) {
430
551
  return (
431
552
  <section className="semantic-model-pyramid">
432
- <div className="framework-model-head"><span className="semantic-tag">△ {title}</span></div>
553
+ <div className="framework-model-head">
554
+ <span className="semantic-tag">△ {title}</span>
555
+ </div>
433
556
  {levels.length > 0 ? (
434
557
  <div className="model-pyramid-levels">
435
558
  {levels.map((level, index) => (
436
- <div className="model-pyramid-level" key={index} style={{ '--pyramid-width': `${Math.max(38, 100 - index * 12)}%` }}>
559
+ <div
560
+ className="model-pyramid-level"
561
+ key={index}
562
+ style={{ '--pyramid-width': `${Math.max(38, 100 - index * 12)}%` }}
563
+ >
437
564
  <strong>{level.title || level.label}</strong>
438
565
  {level.description && <span>{level.description}</span>}
439
566
  </div>
440
567
  ))}
441
568
  </div>
442
- ) : children}
569
+ ) : (
570
+ children
571
+ )}
443
572
  </section>
444
573
  );
445
574
  }
@@ -448,17 +577,25 @@ PyramidModel.displayName = 'PyramidModel';
448
577
  export function FunnelModel({ title = '漏斗模型', steps = [], children }) {
449
578
  return (
450
579
  <section className="semantic-model-funnel">
451
- <div className="framework-model-head"><span className="semantic-tag">▽ {title}</span></div>
580
+ <div className="framework-model-head">
581
+ <span className="semantic-tag">▽ {title}</span>
582
+ </div>
452
583
  {steps.length > 0 ? (
453
584
  <div className="model-funnel-steps">
454
585
  {steps.map((step, index) => (
455
- <div className="model-funnel-step" key={index} style={{ '--funnel-width': `${Math.max(40, 100 - index * 12)}%` }}>
586
+ <div
587
+ className="model-funnel-step"
588
+ key={index}
589
+ style={{ '--funnel-width': `${Math.max(40, 100 - index * 12)}%` }}
590
+ >
456
591
  <strong>{step.title || step.label}</strong>
457
592
  {step.description && <span>{step.description}</span>}
458
593
  </div>
459
594
  ))}
460
595
  </div>
461
- ) : children}
596
+ ) : (
597
+ children
598
+ )}
462
599
  </section>
463
600
  );
464
601
  }
@@ -467,7 +604,10 @@ FunnelModel.displayName = 'FunnelModel';
467
604
  export function Callout({ type = 'info', title, children }) {
468
605
  const tone = ['info', 'success', 'warn', 'danger'].includes(type) ? type : 'info';
469
606
  return (
470
- <aside className={`semantic-callout callout-${tone}${title ? ' has-callout-title' : ''}`} data-tone={tone}>
607
+ <aside
608
+ className={`semantic-callout callout-${tone}${title ? ' has-callout-title' : ''}`}
609
+ data-tone={tone}
610
+ >
471
611
  {title && <div className="callout-title">{title}</div>}
472
612
  <div className="callout-content">{children}</div>
473
613
  </aside>
@@ -490,7 +630,15 @@ export function LearningObjectives({ items = [], children }) {
490
630
  return (
491
631
  <section className="semantic-learning-objectives">
492
632
  <div className="semantic-tag">◎ 学习目标</div>
493
- {goals.length > 0 ? <ul>{goals.map((item, index) => <li key={index}>{item}</li>)}</ul> : children}
633
+ {goals.length > 0 ? (
634
+ <ul>
635
+ {goals.map((item, index) => (
636
+ <li key={index}>{item}</li>
637
+ ))}
638
+ </ul>
639
+ ) : (
640
+ children
641
+ )}
494
642
  </section>
495
643
  );
496
644
  }
@@ -528,11 +676,25 @@ export function Invariant({ title = '不变量', children }) {
528
676
  Invariant.displayName = 'Invariant';
529
677
 
530
678
  export function FailureMode({ symptom, cause, evidence, remedy, children }) {
531
- const rows = [['现象', symptom], ['原因', cause], ['证据', evidence], ['建议', remedy]].filter(([, value]) => value);
679
+ const rows = [
680
+ ['现象', symptom],
681
+ ['原因', cause],
682
+ ['证据', evidence],
683
+ ['建议', remedy]
684
+ ].filter(([, value]) => value);
532
685
  return (
533
686
  <div className="semantic-failure-mode">
534
687
  <div className="semantic-tag">⚠ 故障模式</div>
535
- {rows.length > 0 && <dl>{rows.map(([label, value]) => <React.Fragment key={label}><dt>{label}</dt><dd>{value}</dd></React.Fragment>)}</dl>}
688
+ {rows.length > 0 && (
689
+ <dl>
690
+ {rows.map(([label, value]) => (
691
+ <React.Fragment key={label}>
692
+ <dt>{label}</dt>
693
+ <dd>{value}</dd>
694
+ </React.Fragment>
695
+ ))}
696
+ </dl>
697
+ )}
536
698
  {children && <div className="failure-details">{children}</div>}
537
699
  </div>
538
700
  );
@@ -548,13 +710,30 @@ export function Tradeoff({ title = '工程权衡', options = [], children }) {
548
710
  {options.map((option, index) => (
549
711
  <div className="tradeoff-option" key={index}>
550
712
  <strong>{option.name || option.label || `方案 ${index + 1}`}</strong>
551
- {option.benefit && <span><b>收益</b>{option.benefit}</span>}
552
- {option.cost && <span><b>代价</b>{option.cost}</span>}
553
- {option.when && <span><b>适用</b>{option.when}</span>}
713
+ {option.benefit && (
714
+ <span>
715
+ <b>收益</b>
716
+ {option.benefit}
717
+ </span>
718
+ )}
719
+ {option.cost && (
720
+ <span>
721
+ <b>代价</b>
722
+ {option.cost}
723
+ </span>
724
+ )}
725
+ {option.when && (
726
+ <span>
727
+ <b>适用</b>
728
+ {option.when}
729
+ </span>
730
+ )}
554
731
  </div>
555
732
  ))}
556
733
  </div>
557
- ) : children}
734
+ ) : (
735
+ children
736
+ )}
558
737
  </div>
559
738
  );
560
739
  }
@@ -589,7 +768,12 @@ export function Step({ number, title, reason, children }) {
589
768
  <div className="worked-step-body">
590
769
  {title && <strong className="worked-step-title">{title}</strong>}
591
770
  {children && <div className="worked-step-content">{children}</div>}
592
- {reason && <div className="worked-step-reason"><span>为什么</span>{reason}</div>}
771
+ {reason && (
772
+ <div className="worked-step-reason">
773
+ <span>为什么</span>
774
+ {reason}
775
+ </div>
776
+ )}
593
777
  </div>
594
778
  </li>
595
779
  );
@@ -617,19 +801,27 @@ export function DataTable({ title, caption, headers = [], rows = [], children })
617
801
  <table className="data-table">
618
802
  {head.length > 0 && (
619
803
  <thead>
620
- <tr>{head.map((cell, index) => <th key={index}>{cell}</th>)}</tr>
804
+ <tr>
805
+ {head.map((cell, index) => (
806
+ <th key={index}>{cell}</th>
807
+ ))}
808
+ </tr>
621
809
  </thead>
622
810
  )}
623
811
  <tbody>
624
812
  {body.map((row, rowIndex) => (
625
813
  <tr key={rowIndex}>
626
- {(Array.isArray(row) ? row : [row]).map((cell, cellIndex) => <td key={cellIndex}>{cell}</td>)}
814
+ {(Array.isArray(row) ? row : [row]).map((cell, cellIndex) => (
815
+ <td key={cellIndex}>{cell}</td>
816
+ ))}
627
817
  </tr>
628
818
  ))}
629
819
  </tbody>
630
820
  </table>
631
821
  </div>
632
- ) : children}
822
+ ) : (
823
+ children
824
+ )}
633
825
  {caption && <figcaption className="data-table-caption">{caption}</figcaption>}
634
826
  </figure>
635
827
  );
@@ -640,7 +832,13 @@ DataTable.displayName = 'DataTable';
640
832
  * States plus transitions. `Flow` is linear and `Timeline` is chronological;
641
833
  * a state machine expresses cycles, guarded transitions and terminal states.
642
834
  */
643
- export function StateMachine({ title = '状态机', initial, states = [], transitions = [], children }) {
835
+ export function StateMachine({
836
+ title = '状态机',
837
+ initial,
838
+ states = [],
839
+ transitions = [],
840
+ children
841
+ }) {
644
842
  const list = Array.isArray(states) ? states.filter(Boolean) : [];
645
843
  const edges = Array.isArray(transitions) ? transitions.filter(Boolean) : [];
646
844
  return (
@@ -675,7 +873,9 @@ export function StateMachine({ title = '状态机', initial, states = [], transi
675
873
  </ul>
676
874
  )}
677
875
  </>
678
- ) : children}
876
+ ) : (
877
+ children
878
+ )}
679
879
  </section>
680
880
  );
681
881
  }
@@ -692,7 +892,12 @@ export function Quiz({ question, answer, children, tone = 'info' }) {
692
892
  {question && <p className="quiz-question">{question}</p>}
693
893
  {hasAnswer ? (
694
894
  <>
695
- <button type="button" className="quiz-toggle" aria-expanded={shown} onClick={() => setShown(value => !value)}>
895
+ <button
896
+ type="button"
897
+ className="quiz-toggle"
898
+ aria-expanded={shown}
899
+ onClick={() => setShown(value => !value)}
900
+ >
696
901
  {shown ? '隐藏答案' : '显示答案'}
697
902
  </button>
698
903
  {shown && (
@@ -702,7 +907,9 @@ export function Quiz({ question, answer, children, tone = 'info' }) {
702
907
  </div>
703
908
  )}
704
909
  </>
705
- ) : children}
910
+ ) : (
911
+ children
912
+ )}
706
913
  </aside>
707
914
  );
708
915
  }
@@ -714,7 +921,7 @@ const SOURCE_KIND_LABELS = {
714
921
  implementation: '实现',
715
922
  experiment: '实测',
716
923
  experience: '经验',
717
- reference: '来源',
924
+ reference: '来源'
718
925
  };
719
926
 
720
927
  /**
@@ -729,7 +936,13 @@ export function Source({ kind = 'reference', label, href, children }) {
729
936
  return (
730
937
  <span className={`semantic-source source-${safeKind}`} data-kind={safeKind}>
731
938
  <span className="source-kind">{SOURCE_KIND_LABELS[safeKind]}</span>
732
- {href ? <a href={href} target="_blank" rel="noreferrer">{text}</a> : <span>{text}</span>}
939
+ {href ? (
940
+ <a href={href} target="_blank" rel="noreferrer">
941
+ {text}
942
+ </a>
943
+ ) : (
944
+ <span>{text}</span>
945
+ )}
733
946
  </span>
734
947
  );
735
948
  }
@@ -758,7 +971,15 @@ export function KeyTakeaways({ title = '关键要点', items = [], children }) {
758
971
  return (
759
972
  <section className="semantic-key-takeaways">
760
973
  <div className="semantic-tag">✓ {title}</div>
761
- {list.length > 0 ? <ul>{list.map((item, index) => <li key={index}>{item}</li>)}</ul> : children}
974
+ {list.length > 0 ? (
975
+ <ul>
976
+ {list.map((item, index) => (
977
+ <li key={index}>{item}</li>
978
+ ))}
979
+ </ul>
980
+ ) : (
981
+ children
982
+ )}
762
983
  </section>
763
984
  );
764
985
  }
@@ -768,7 +989,11 @@ KeyTakeaways.displayName = 'KeyTakeaways';
768
989
  export function Metric({ label, value, unit, delta, trend, note }) {
769
990
  const safeTrend = ['up', 'down', 'flat'].includes(trend)
770
991
  ? trend
771
- : (typeof delta === 'string' && delta.trim().startsWith('-') ? 'down' : (delta ? 'up' : null));
992
+ : typeof delta === 'string' && delta.trim().startsWith('-')
993
+ ? 'down'
994
+ : delta
995
+ ? 'up'
996
+ : null;
772
997
  return (
773
998
  <div className="semantic-metric">
774
999
  {label && <div className="metric-label">{label}</div>}
@@ -784,7 +1009,15 @@ export function Metric({ label, value, unit, delta, trend, note }) {
784
1009
  Metric.displayName = 'Metric';
785
1010
 
786
1011
  /** Before/after code comparison, for explaining a fix or a refactor. */
787
- export function CodeDiff({ title = '代码对比', language = 'text', before, after, beforeLabel = '修改前', afterLabel = '修改后', caption }) {
1012
+ export function CodeDiff({
1013
+ title = '代码对比',
1014
+ language = 'text',
1015
+ before,
1016
+ after,
1017
+ beforeLabel = '修改前',
1018
+ afterLabel = '修改后',
1019
+ caption
1020
+ }) {
788
1021
  const beforeText = typeof before === 'string' ? before.replace(/^\n+|\s+$/g, '') : '';
789
1022
  const afterText = typeof after === 'string' ? after.replace(/^\n+|\s+$/g, '') : '';
790
1023
  if (!beforeText && !afterText) return null;
@@ -798,11 +1031,15 @@ export function CodeDiff({ title = '代码对比', language = 'text', before, af
798
1031
  <div className="code-diff-panes">
799
1032
  <div className="code-diff-pane pane-before">
800
1033
  <div className="code-diff-label">{beforeLabel}</div>
801
- <pre className="code-block"><code>{beforeText}</code></pre>
1034
+ <pre className="code-block">
1035
+ <code>{beforeText}</code>
1036
+ </pre>
802
1037
  </div>
803
1038
  <div className="code-diff-pane pane-after">
804
1039
  <div className="code-diff-label">{afterLabel}</div>
805
- <pre className="code-block"><code>{afterText}</code></pre>
1040
+ <pre className="code-block">
1041
+ <code>{afterText}</code>
1042
+ </pre>
806
1043
  </div>
807
1044
  </div>
808
1045
  {caption && <figcaption className="semantic-code-caption">{caption}</figcaption>}
@@ -820,9 +1057,14 @@ export function Term({ name, definition, children }) {
820
1057
  const description = definition || (name ? childrenToText(children) : '');
821
1058
  if (!term) return null;
822
1059
  return (
1060
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- focus reveals the definition tooltip for keyboard users
823
1061
  <span className="semantic-term" tabIndex={0} data-term={term}>
824
1062
  {term}
825
- {description && <span className="term-popover" role="tooltip">{description}</span>}
1063
+ {description && (
1064
+ <span className="term-popover" role="tooltip">
1065
+ {description}
1066
+ </span>
1067
+ )}
826
1068
  </span>
827
1069
  );
828
1070
  }
@@ -840,7 +1082,9 @@ function DecisionBranch({ node, depth }) {
840
1082
  </div>
841
1083
  {kids.length > 0 && (
842
1084
  <ul className="decision-children">
843
- {kids.map((child, index) => <DecisionBranch key={index} node={child} depth={depth + 1} />)}
1085
+ {kids.map((child, index) => (
1086
+ <DecisionBranch key={index} node={child} depth={depth + 1} />
1087
+ ))}
844
1088
  </ul>
845
1089
  )}
846
1090
  </li>
@@ -859,9 +1103,13 @@ export function DecisionTree({ title = '决策路径', question, branches = [],
859
1103
  {question && <div className="decision-question">{question}</div>}
860
1104
  {list.length > 0 ? (
861
1105
  <ul className="decision-tree-root">
862
- {list.map((branch, index) => <DecisionBranch key={index} node={branch} depth={0} />)}
1106
+ {list.map((branch, index) => (
1107
+ <DecisionBranch key={index} node={branch} depth={0} />
1108
+ ))}
863
1109
  </ul>
864
- ) : children}
1110
+ ) : (
1111
+ children
1112
+ )}
865
1113
  </section>
866
1114
  );
867
1115
  }
@@ -886,12 +1134,20 @@ export function FeedbackLoop({ title = '反馈回路', type = 'reinforcing', nod
886
1134
  <strong>{node.label || node.title}</strong>
887
1135
  {node.description && <span>{node.description}</span>}
888
1136
  </div>
889
- {index < list.length - 1 && <span className="feedback-arrow" aria-hidden="true">{polarity}</span>}
1137
+ {index < list.length - 1 && (
1138
+ <span className="feedback-arrow" aria-hidden="true">
1139
+ {polarity}
1140
+ </span>
1141
+ )}
890
1142
  </React.Fragment>
891
1143
  ))}
892
- <span className="feedback-return" aria-hidden="true">{polarity} ↺</span>
1144
+ <span className="feedback-return" aria-hidden="true">
1145
+ {polarity} ↺
1146
+ </span>
893
1147
  </div>
894
- ) : children}
1148
+ ) : (
1149
+ children
1150
+ )}
895
1151
  </section>
896
1152
  );
897
1153
  }
@@ -915,7 +1171,9 @@ function ReadingProgress() {
915
1171
  const ratio = total > 0 ? Math.min(1, Math.max(0, window.scrollY / total)) : 0;
916
1172
  if (barRef.current) barRef.current.style.transform = `scaleX(${ratio})`;
917
1173
  };
918
- const onScroll = () => { if (!frame) frame = window.requestAnimationFrame(update); };
1174
+ const onScroll = () => {
1175
+ if (!frame) frame = window.requestAnimationFrame(update);
1176
+ };
919
1177
  update();
920
1178
  window.addEventListener('scroll', onScroll, { passive: true });
921
1179
  window.addEventListener('resize', onScroll);
@@ -950,8 +1208,18 @@ export function ScrollToc({ items = [], activeId = null, title = '目录' }) {
950
1208
  }
951
1209
  ScrollToc.displayName = 'ScrollToc';
952
1210
 
953
- export function ScrollDocument({ spacing = 'comfortable', fontSize = 'normal', scale, lineHeight, toc = true, progress = true, children }) {
954
- const safeSpacing = ['compact', 'comfortable', 'airy'].includes(spacing) ? spacing : 'comfortable';
1211
+ export function ScrollDocument({
1212
+ spacing = 'comfortable',
1213
+ fontSize = 'normal',
1214
+ scale,
1215
+ lineHeight,
1216
+ toc = true,
1217
+ progress = true,
1218
+ children
1219
+ }) {
1220
+ const safeSpacing = ['compact', 'comfortable', 'airy'].includes(spacing)
1221
+ ? spacing
1222
+ : 'comfortable';
955
1223
  const preset = FONT_PRESETS[fontSize] || FONT_PRESETS.normal;
956
1224
  const [items, setItems] = React.useState([]);
957
1225
  const [activeId, setActiveId] = React.useState(null);
@@ -961,7 +1229,10 @@ export function ScrollDocument({ spacing = 'comfortable', fontSize = 'normal', s
961
1229
  const register = React.useCallback(item => {
962
1230
  setItems(previous => {
963
1231
  const existing = previous.find(entry => entry.id === item.id);
964
- if (existing) return existing.title === item.title ? previous : previous.map(entry => (entry.id === item.id ? item : entry));
1232
+ if (existing)
1233
+ return existing.title === item.title
1234
+ ? previous
1235
+ : previous.map(entry => (entry.id === item.id ? item : entry));
965
1236
  return [...previous, item];
966
1237
  });
967
1238
  return () => setItems(previous => previous.filter(entry => entry.id !== item.id));
@@ -969,12 +1240,15 @@ export function ScrollDocument({ spacing = 'comfortable', fontSize = 'normal', s
969
1240
 
970
1241
  React.useEffect(() => {
971
1242
  if (items.length < 2 || typeof IntersectionObserver === 'undefined') return undefined;
972
- const observer = new IntersectionObserver(entries => {
973
- const visible = entries
974
- .filter(entry => entry.isIntersecting)
975
- .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
976
- if (visible[0]) setActiveId(visible[0].target.id);
977
- }, { rootMargin: '-15% 0px -75% 0px', threshold: 0 });
1243
+ const observer = new IntersectionObserver(
1244
+ entries => {
1245
+ const visible = entries
1246
+ .filter(entry => entry.isIntersecting)
1247
+ .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
1248
+ if (visible[0]) setActiveId(visible[0].target.id);
1249
+ },
1250
+ { rootMargin: '-15% 0px -75% 0px', threshold: 0 }
1251
+ );
978
1252
  items.forEach(item => {
979
1253
  const node = document.getElementById(item.id);
980
1254
  if (node) observer.observe(node);
@@ -985,19 +1259,21 @@ export function ScrollDocument({ spacing = 'comfortable', fontSize = 'normal', s
985
1259
  const outline = React.useMemo(() => ({ register }), [register]);
986
1260
  const style = {
987
1261
  '--reading-scale': String(scale ?? preset.scale),
988
- '--reading-line-height': String(lineHeight ?? preset.lineHeight),
1262
+ '--reading-line-height': String(lineHeight ?? preset.lineHeight)
989
1263
  };
990
1264
 
991
1265
  const showToc = toc && items.length >= 2;
992
1266
 
993
1267
  return (
994
- <article className={`continuous-document continuous-spacing-${safeSpacing}${showToc ? ' has-toc' : ''}`} data-font-size={fontSize} style={style}>
1268
+ <article
1269
+ className={`continuous-document continuous-spacing-${safeSpacing}${showToc ? ' has-toc' : ''}`}
1270
+ data-font-size={fontSize}
1271
+ style={style}
1272
+ >
995
1273
  {progress && <ReadingProgress />}
996
1274
  <ScrollOutlineContext.Provider value={outline}>
997
1275
  {showToc && <ScrollToc items={items} activeId={activeId} />}
998
- <div className="continuous-content">
999
- {children}
1000
- </div>
1276
+ <div className="continuous-content">{children}</div>
1001
1277
  </ScrollOutlineContext.Provider>
1002
1278
  </article>
1003
1279
  );
@@ -1005,14 +1281,22 @@ export function ScrollDocument({ spacing = 'comfortable', fontSize = 'normal', s
1005
1281
  ScrollDocument.displayName = 'ScrollDocument';
1006
1282
 
1007
1283
  export function ScrollHeader({ label = '连续阅读', title, children }) {
1008
- return <header className="continuous-header"><p>{label}</p>{title && <h1>{title}</h1>}<div>{children}</div></header>;
1284
+ return (
1285
+ <header className="continuous-header">
1286
+ <p>{label}</p>
1287
+ {title && <h1>{title}</h1>}
1288
+ <div>{children}</div>
1289
+ </header>
1290
+ );
1009
1291
  }
1010
1292
  ScrollHeader.displayName = 'ScrollHeader';
1011
1293
 
1012
1294
  export function ScrollSection({ title, id, wide = false, spacing = 'inherit', children }) {
1013
1295
  const outline = React.useContext(ScrollOutlineContext);
1014
1296
  const anchor = id || (title ? slugify(title) : '');
1015
- const safeSpacing = ['compact', 'comfortable', 'airy'].includes(spacing) ? ` continuous-spacing-${spacing}` : '';
1297
+ const safeSpacing = ['compact', 'comfortable', 'airy'].includes(spacing)
1298
+ ? ` continuous-spacing-${spacing}`
1299
+ : '';
1016
1300
 
1017
1301
  React.useEffect(() => {
1018
1302
  if (!outline || !title || !anchor) return undefined;
@@ -1020,7 +1304,10 @@ export function ScrollSection({ title, id, wide = false, spacing = 'inherit', ch
1020
1304
  }, [outline, anchor, title]);
1021
1305
 
1022
1306
  return (
1023
- <section id={anchor || undefined} className={`continuous-section${wide ? ' continuous-section-wide' : ''}${safeSpacing}`}>
1307
+ <section
1308
+ id={anchor || undefined}
1309
+ className={`continuous-section${wide ? ' continuous-section-wide' : ''}${safeSpacing}`}
1310
+ >
1024
1311
  {title && <h2>{title}</h2>}
1025
1312
  {children}
1026
1313
  </section>
@@ -1056,12 +1343,27 @@ export function Grid({ columns = 'auto', gap = 'md', children }) {
1056
1343
  Grid.displayName = 'Grid';
1057
1344
 
1058
1345
  export function Split({ ratio = '1fr 1fr', children }) {
1059
- return <div className="semantic-split" style={{ '--split-ratio': ratio }}>{children}</div>;
1346
+ return (
1347
+ <div className="semantic-split" style={{ '--split-ratio': ratio }}>
1348
+ {children}
1349
+ </div>
1350
+ );
1060
1351
  }
1061
1352
  Split.displayName = 'Split';
1062
1353
 
1063
1354
  export function Tabs({ items = [], children }) {
1064
- return <div className="semantic-tabs" data-tab-count={items.length || undefined}>{items.length ? items.map((item, i) => <details key={i} open={i === 0}><summary>{item.label || item.title}</summary><div>{item.content}</div></details>) : children}</div>;
1355
+ return (
1356
+ <div className="semantic-tabs" data-tab-count={items.length || undefined}>
1357
+ {items.length
1358
+ ? items.map((item, i) => (
1359
+ <details key={i} open={i === 0}>
1360
+ <summary>{item.label || item.title}</summary>
1361
+ <div>{item.content}</div>
1362
+ </details>
1363
+ ))
1364
+ : children}
1365
+ </div>
1366
+ );
1065
1367
  }
1066
1368
  Tabs.displayName = 'Tabs';
1067
1369
 
@@ -1089,19 +1391,31 @@ function MermaidZoom({ chart, title, onClose }) {
1089
1391
  }
1090
1392
  }
1091
1393
  renderZoom();
1092
- return () => { cancelled = true; };
1394
+ return () => {
1395
+ cancelled = true;
1396
+ };
1093
1397
  }, [chart, appearanceKey]);
1094
1398
 
1095
1399
  return (
1096
1400
  <ZoomOverlay title={title || '关系草图'} caption={title} onClose={onClose}>
1097
- {error
1098
- ? <pre className="mermaid-error">{error}</pre>
1099
- : <div className="mermaid-zoom-canvas" ref={ref} />}
1401
+ {error ? (
1402
+ <pre className="mermaid-error">{error}</pre>
1403
+ ) : (
1404
+ <div className="mermaid-zoom-canvas" ref={ref} />
1405
+ )}
1100
1406
  </ZoomOverlay>
1101
1407
  );
1102
1408
  }
1103
1409
 
1104
- export function Mermaid({ chart = '', title = '关系草图', width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
1410
+ export function Mermaid({
1411
+ chart = '',
1412
+ title = '关系草图',
1413
+ width = 'auto',
1414
+ height = 'auto',
1415
+ x = 0,
1416
+ y = 0,
1417
+ position = 'flow'
1418
+ }) {
1105
1419
  const ref = React.useRef(null);
1106
1420
  const id = React.useId().replace(/:/g, '');
1107
1421
  const appearanceKey = useAppearanceKey();
@@ -1125,11 +1439,15 @@ export function Mermaid({ chart = '', title = '关系草图', width = 'auto', he
1125
1439
  }
1126
1440
  }
1127
1441
  renderChart();
1128
- return () => { cancelled = true; };
1442
+ return () => {
1443
+ cancelled = true;
1444
+ };
1129
1445
  }, [chart, id, appearanceKey]);
1130
1446
 
1131
1447
  const canZoom = Boolean(chart.trim()) && !error;
1132
- const openZoom = () => { if (canZoom) setZoomed(true); };
1448
+ const openZoom = () => {
1449
+ if (canZoom) setZoomed(true);
1450
+ };
1133
1451
  const onKeyDown = event => {
1134
1452
  if (event.key === 'Enter' || event.key === ' ') {
1135
1453
  event.preventDefault();
@@ -1138,13 +1456,22 @@ export function Mermaid({ chart = '', title = '关系草图', width = 'auto', he
1138
1456
  };
1139
1457
 
1140
1458
  return (
1141
- <div className={`semantic-mermaid ${widgetClass(position)}`} style={widgetStyle({ width, height, x, y, position })}>
1459
+ <div
1460
+ className={`semantic-mermaid ${widgetClass(position)}`}
1461
+ style={widgetStyle({ width, height, x, y, position })}
1462
+ >
1142
1463
  <div className="semantic-widget-head">
1143
1464
  <span>{title}</span>
1144
1465
  <div className="mermaid-tools">
1145
1466
  <code>MERMAID</code>
1146
- <button type="button" onClick={openZoom} disabled={!canZoom} aria-label={`放大阅读:${title}`}>
1147
- <ZoomIn size={12} />放大阅读
1467
+ <button
1468
+ type="button"
1469
+ onClick={openZoom}
1470
+ disabled={!canZoom}
1471
+ aria-label={`放大阅读:${title}`}
1472
+ >
1473
+ <ZoomIn size={12} />
1474
+ 放大阅读
1148
1475
  </button>
1149
1476
  </div>
1150
1477
  </div>
@@ -1168,10 +1495,25 @@ export function Mermaid({ chart = '', title = '关系草图', width = 'auto', he
1168
1495
  }
1169
1496
  Mermaid.displayName = 'Mermaid';
1170
1497
 
1171
- export function RelationMap({ title = '关系速览', items = [], children, width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
1498
+ export function RelationMap({
1499
+ title = '关系速览',
1500
+ items = [],
1501
+ children,
1502
+ width = 'auto',
1503
+ height = 'auto',
1504
+ x = 0,
1505
+ y = 0,
1506
+ position = 'flow'
1507
+ }) {
1172
1508
  return (
1173
- <div className={`semantic-relation-map ${widgetClass(position)}`} style={widgetStyle({ width, height, x, y, position })}>
1174
- <div className="semantic-widget-head"><span>{title}</span><code>RELATIONS</code></div>
1509
+ <div
1510
+ className={`semantic-relation-map ${widgetClass(position)}`}
1511
+ style={widgetStyle({ width, height, x, y, position })}
1512
+ >
1513
+ <div className="semantic-widget-head">
1514
+ <span>{title}</span>
1515
+ <code>RELATIONS</code>
1516
+ </div>
1175
1517
  {items.length > 0 ? (
1176
1518
  <div className="relation-map-grid">
1177
1519
  {items.map((item, index) => (
@@ -1183,7 +1525,9 @@ export function RelationMap({ title = '关系速览', items = [], children, widt
1183
1525
  </div>
1184
1526
  ))}
1185
1527
  </div>
1186
- ) : children}
1528
+ ) : (
1529
+ children
1530
+ )}
1187
1531
  </div>
1188
1532
  );
1189
1533
  }
@@ -1193,7 +1537,12 @@ export function RelationPath({ title = '关系链', steps = [], children }) {
1193
1537
  const safeSteps = Array.isArray(steps) ? steps.filter(Boolean) : [];
1194
1538
  return (
1195
1539
  <div className="semantic-relation-path">
1196
- {title && <div className="semantic-widget-head"><span>{title}</span><code>PATH</code></div>}
1540
+ {title && (
1541
+ <div className="semantic-widget-head">
1542
+ <span>{title}</span>
1543
+ <code>PATH</code>
1544
+ </div>
1545
+ )}
1197
1546
  {safeSteps.length > 0 ? (
1198
1547
  <div className="relation-path-steps">
1199
1548
  {safeSteps.map((step, index) => (
@@ -1203,20 +1552,39 @@ export function RelationPath({ title = '关系链', steps = [], children }) {
1203
1552
  <strong>{step.node || step.title}</strong>
1204
1553
  {step.note && <small>{step.note}</small>}
1205
1554
  </div>
1206
- {index < safeSteps.length - 1 && <div className="relation-path-edge"><span>{safeSteps[index].relation || '→'}</span></div>}
1555
+ {index < safeSteps.length - 1 && (
1556
+ <div className="relation-path-edge">
1557
+ <span>{safeSteps[index].relation || '→'}</span>
1558
+ </div>
1559
+ )}
1207
1560
  </React.Fragment>
1208
1561
  ))}
1209
1562
  </div>
1210
- ) : children}
1563
+ ) : (
1564
+ children
1565
+ )}
1211
1566
  </div>
1212
1567
  );
1213
1568
  }
1214
1569
  RelationPath.displayName = 'RelationPath';
1215
1570
 
1216
- export function Insight({ title = '关键判断', tone = 'info', children, width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
1571
+ export function Insight({
1572
+ title = '关键判断',
1573
+ tone = 'info',
1574
+ children,
1575
+ width = 'auto',
1576
+ height = 'auto',
1577
+ x = 0,
1578
+ y = 0,
1579
+ position = 'flow'
1580
+ }) {
1217
1581
  const safeTone = ['info', 'success', 'warn', 'danger'].includes(tone) ? tone : 'info';
1218
1582
  return (
1219
- <aside className={`semantic-insight insight-${safeTone} ${widgetClass(position)}`} data-tone={safeTone} style={widgetStyle({ width, height, x, y, position })}>
1583
+ <aside
1584
+ className={`semantic-insight insight-${safeTone} ${widgetClass(position)}`}
1585
+ data-tone={safeTone}
1586
+ style={widgetStyle({ width, height, x, y, position })}
1587
+ >
1220
1588
  <div className="insight-kicker">{title}</div>
1221
1589
  <div className="insight-body">{children}</div>
1222
1590
  </aside>
@@ -1224,15 +1592,28 @@ export function Insight({ title = '关键判断', tone = 'info', children, width
1224
1592
  }
1225
1593
  Insight.displayName = 'Insight';
1226
1594
 
1227
- export function NoteGrid({ notes = [], children, width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
1595
+ export function NoteGrid({
1596
+ notes = [],
1597
+ children,
1598
+ width = 'auto',
1599
+ height = 'auto',
1600
+ x = 0,
1601
+ y = 0,
1602
+ position = 'flow'
1603
+ }) {
1228
1604
  return (
1229
- <div className={`semantic-note-grid ${widgetClass(position)}`} style={widgetStyle({ width, height, x, y, position })}>
1230
- {notes.length > 0 ? notes.map((note, index) => (
1231
- <div className="semantic-note" key={index}>
1232
- <strong>{note.title || note.label}</strong>
1233
- <span>{note.content || note.text || note.description}</span>
1234
- </div>
1235
- )) : children}
1605
+ <div
1606
+ className={`semantic-note-grid ${widgetClass(position)}`}
1607
+ style={widgetStyle({ width, height, x, y, position })}
1608
+ >
1609
+ {notes.length > 0
1610
+ ? notes.map((note, index) => (
1611
+ <div className="semantic-note" key={index}>
1612
+ <strong>{note.title || note.label}</strong>
1613
+ <span>{note.content || note.text || note.description}</span>
1614
+ </div>
1615
+ ))
1616
+ : children}
1236
1617
  </div>
1237
1618
  );
1238
1619
  }
@@ -1246,7 +1627,9 @@ NoteGrid.displayName = 'NoteGrid';
1246
1627
  function MathInline({ formula, children }) {
1247
1628
  const tex = (formula || childrenToText(children)).trim();
1248
1629
  if (!tex) return null;
1249
- return <span className="semantic-math" dangerouslySetInnerHTML={{ __html: renderTex(tex, false) }} />;
1630
+ return (
1631
+ <span className="semantic-math" dangerouslySetInnerHTML={{ __html: renderTex(tex, false) }} />
1632
+ );
1250
1633
  }
1251
1634
  MathInline.displayName = 'Math';
1252
1635
  export { MathInline as Math };
@@ -1260,12 +1643,19 @@ export function MathBlock({ title = '公式', formula, variables = [], children
1260
1643
  <span className="semantic-tag">∑ {title}</span>
1261
1644
  <span className="framework-model-type">公式</span>
1262
1645
  </div>
1263
- <div className="math-block-expression" dangerouslySetInnerHTML={{ __html: renderTex(tex, true) }} />
1646
+ <div
1647
+ className="math-block-expression"
1648
+ dangerouslySetInnerHTML={{ __html: renderTex(tex, true) }}
1649
+ />
1264
1650
  {variables.length > 0 && (
1265
1651
  <dl className="model-formula-variables">
1266
1652
  {variables.map((variable, index) => (
1267
1653
  <React.Fragment key={index}>
1268
- <dt dangerouslySetInnerHTML={{ __html: renderTex(variable.symbol || variable.name || '', false) }} />
1654
+ <dt
1655
+ dangerouslySetInnerHTML={{
1656
+ __html: renderTex(variable.symbol || variable.name || '', false)
1657
+ }}
1658
+ />
1269
1659
  <dd>{variable.description || variable.value}</dd>
1270
1660
  </React.Fragment>
1271
1661
  ))}
@@ -1290,7 +1680,10 @@ function normalizePoints(data) {
1290
1680
  if (typeof point === 'number') return { label: String(index + 1), value: point };
1291
1681
  if (!point || typeof point !== 'object') return null;
1292
1682
  const value = Number(point.value ?? point.y ?? 0);
1293
- return { label: String(point.label ?? point.x ?? index + 1), value: Number.isFinite(value) ? value : 0 };
1683
+ return {
1684
+ label: String(point.label ?? point.x ?? index + 1),
1685
+ value: Number.isFinite(value) ? value : 0
1686
+ };
1294
1687
  })
1295
1688
  .filter(Boolean);
1296
1689
  }
@@ -1301,14 +1694,30 @@ function niceMax(value) {
1301
1694
  return Math.ceil(value / magnitude) * magnitude;
1302
1695
  }
1303
1696
 
1304
- export function Chart({ title = '图表', type = 'bar', data = [], series = [], labels = [], unit = '', showValues = true, width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
1697
+ export function Chart({
1698
+ title = '图表',
1699
+ type = 'bar',
1700
+ data = [],
1701
+ series = [],
1702
+ labels = [],
1703
+ unit = '',
1704
+ showValues = true,
1705
+ width = 'auto',
1706
+ height = 'auto',
1707
+ x = 0,
1708
+ y = 0,
1709
+ position = 'flow'
1710
+ }) {
1305
1711
  const points = normalizePoints(data);
1306
1712
  const seriesList = (Array.isArray(series) ? series : []).filter(Boolean).map((entry, index) => ({
1307
1713
  name: entry.name || entry.label || `系列 ${index + 1}`,
1308
1714
  colorIndex: index + 1,
1309
- values: (Array.isArray(entry.values) ? entry.values : []).map(value => Number(value) || 0),
1715
+ values: (Array.isArray(entry.values) ? entry.values : []).map(value => Number(value) || 0)
1310
1716
  }));
1311
- const allValues = [...points.map(point => point.value), ...seriesList.flatMap(entry => entry.values)];
1717
+ const allValues = [
1718
+ ...points.map(point => point.value),
1719
+ ...seriesList.flatMap(entry => entry.values)
1720
+ ];
1312
1721
  const max = niceMax(Math.max(1, ...allValues));
1313
1722
 
1314
1723
  const VIEW_W = 600;
@@ -1344,8 +1753,16 @@ export function Chart({ title = '图表', type = 'bar', data = [], series = [],
1344
1753
  <g className="chart-legend">
1345
1754
  {points.map((point, index) => (
1346
1755
  <g key={index} transform={`translate(${PAD.left + 8}, ${PAD.top + index * 22})`}>
1347
- <rect className="chart-legend-swatch" style={chartColor(index)} width="10" height="10" rx="2" />
1348
- <text className="chart-legend-label" x="18" y="9">{point.label} · {Math.round((Math.max(0, point.value) / total) * 100)}%</text>
1756
+ <rect
1757
+ className="chart-legend-swatch"
1758
+ style={chartColor(index)}
1759
+ width="10"
1760
+ height="10"
1761
+ rx="2"
1762
+ />
1763
+ <text className="chart-legend-label" x="18" y="9">
1764
+ {point.label} · {Math.round((Math.max(0, point.value) / total) * 100)}%
1765
+ </text>
1349
1766
  </g>
1350
1767
  ))}
1351
1768
  </g>
@@ -1360,7 +1777,9 @@ export function Chart({ title = '图表', type = 'bar', data = [], series = [],
1360
1777
  return (
1361
1778
  <g key={index}>
1362
1779
  <line x1={PAD.left} y1={yPos} x2={PAD.left + innerW} y2={yPos} />
1363
- <text className="chart-axis-value" x={PAD.left - 8} y={yPos + 3}>{Math.round(tick.value)}</text>
1780
+ <text className="chart-axis-value" x={PAD.left - 8} y={yPos + 3}>
1781
+ {Math.round(tick.value)}
1782
+ </text>
1364
1783
  </g>
1365
1784
  );
1366
1785
  })}
@@ -1384,18 +1803,43 @@ export function Chart({ title = '图表', type = 'bar', data = [], series = [],
1384
1803
  points={entry.values.map((value, idx) => `${xFor(idx)},${yFor(value)}`).join(' ')}
1385
1804
  />
1386
1805
  {entry.values.map((value, idx) => (
1387
- <circle key={idx} className="chart-point" style={chartColor(seriesIndex)} cx={xFor(idx)} cy={yFor(value)} r="3" />
1806
+ <circle
1807
+ key={idx}
1808
+ className="chart-point"
1809
+ style={chartColor(seriesIndex)}
1810
+ cx={xFor(idx)}
1811
+ cy={yFor(value)}
1812
+ r="3"
1813
+ />
1388
1814
  ))}
1389
1815
  </g>
1390
1816
  ))}
1391
- {(labels.length ? labels : seriesList[0]?.values.map((_, i) => String(i + 1)) || []).map((label, idx) => (
1392
- <text key={idx} className="chart-axis-label" x={xFor(idx)} y={VIEW_H - PAD.bottom + 20} textAnchor="middle">{label}</text>
1393
- ))}
1817
+ {(labels.length ? labels : seriesList[0]?.values.map((_, i) => String(i + 1)) || []).map(
1818
+ (label, idx) => (
1819
+ <text
1820
+ key={idx}
1821
+ className="chart-axis-label"
1822
+ x={xFor(idx)}
1823
+ y={VIEW_H - PAD.bottom + 20}
1824
+ textAnchor="middle"
1825
+ >
1826
+ {label}
1827
+ </text>
1828
+ )
1829
+ )}
1394
1830
  <g className="chart-legend">
1395
1831
  {seriesList.map((entry, index) => (
1396
1832
  <g key={index} transform={`translate(${PAD.left + index * 130}, ${PAD.top - 10})`}>
1397
- <rect className="chart-legend-swatch" style={chartColor(index)} width="10" height="10" rx="2" />
1398
- <text className="chart-legend-label" x="16" y="9">{entry.name}</text>
1833
+ <rect
1834
+ className="chart-legend-swatch"
1835
+ style={chartColor(index)}
1836
+ width="10"
1837
+ height="10"
1838
+ rx="2"
1839
+ />
1840
+ <text className="chart-legend-label" x="16" y="9">
1841
+ {entry.name}
1842
+ </text>
1399
1843
  </g>
1400
1844
  ))}
1401
1845
  </g>
@@ -1405,7 +1849,10 @@ export function Chart({ title = '图表', type = 'bar', data = [], series = [],
1405
1849
 
1406
1850
  const bars = points.length
1407
1851
  ? points
1408
- : (seriesList[0]?.values.map((value, index) => ({ label: labels[index] || String(index + 1), value })) || []);
1852
+ : seriesList[0]?.values.map((value, index) => ({
1853
+ label: labels[index] || String(index + 1),
1854
+ value
1855
+ })) || [];
1409
1856
  const barCount = Math.max(1, bars.length);
1410
1857
  const slot = innerW / barCount;
1411
1858
  const barWidth = Math.min(56, slot * 0.6);
@@ -1418,9 +1865,33 @@ export function Chart({ title = '图表', type = 'bar', data = [], series = [],
1418
1865
  const yPos = PAD.top + innerH - barHeight;
1419
1866
  return (
1420
1867
  <g key={index}>
1421
- <rect className="chart-bar" style={chartColor(index)} x={xPos} y={yPos} width={barWidth} height={Math.max(0, barHeight)} rx="3" />
1422
- {showValues && <text className="chart-bar-value" x={xPos + barWidth / 2} y={yPos - 6} textAnchor="middle">{bar.value}</text>}
1423
- <text className="chart-axis-label" x={xPos + barWidth / 2} y={VIEW_H - PAD.bottom + 20} textAnchor="middle">{bar.label}</text>
1868
+ <rect
1869
+ className="chart-bar"
1870
+ style={chartColor(index)}
1871
+ x={xPos}
1872
+ y={yPos}
1873
+ width={barWidth}
1874
+ height={Math.max(0, barHeight)}
1875
+ rx="3"
1876
+ />
1877
+ {showValues && (
1878
+ <text
1879
+ className="chart-bar-value"
1880
+ x={xPos + barWidth / 2}
1881
+ y={yPos - 6}
1882
+ textAnchor="middle"
1883
+ >
1884
+ {bar.value}
1885
+ </text>
1886
+ )}
1887
+ <text
1888
+ className="chart-axis-label"
1889
+ x={xPos + barWidth / 2}
1890
+ y={VIEW_H - PAD.bottom + 20}
1891
+ textAnchor="middle"
1892
+ >
1893
+ {bar.label}
1894
+ </text>
1424
1895
  </g>
1425
1896
  );
1426
1897
  })}
@@ -1429,13 +1900,30 @@ export function Chart({ title = '图表', type = 'bar', data = [], series = [],
1429
1900
  })();
1430
1901
 
1431
1902
  return (
1432
- <div className={`semantic-chart ${widgetClass(position)}`} style={widgetStyle({ width, height, x, y, position })}>
1433
- <div className="semantic-widget-head"><span>{title}</span><code>CHART · {type.toUpperCase()}{unit ? ` · ${unit}` : ''}</code></div>
1903
+ <div
1904
+ className={`semantic-chart ${widgetClass(position)}`}
1905
+ style={widgetStyle({ width, height, x, y, position })}
1906
+ >
1907
+ <div className="semantic-widget-head">
1908
+ <span>{title}</span>
1909
+ <code>
1910
+ CHART · {type.toUpperCase()}
1911
+ {unit ? ` · ${unit}` : ''}
1912
+ </code>
1913
+ </div>
1434
1914
  {body ? (
1435
- <svg className="chart-canvas" viewBox={`0 0 ${VIEW_W} ${VIEW_H}`} role="img" aria-label={title} preserveAspectRatio="xMidYMid meet">
1915
+ <svg
1916
+ className="chart-canvas"
1917
+ viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
1918
+ role="img"
1919
+ aria-label={title}
1920
+ preserveAspectRatio="xMidYMid meet"
1921
+ >
1436
1922
  {body}
1437
1923
  </svg>
1438
- ) : <div className="chart-empty">没有可绘制的数据</div>}
1924
+ ) : (
1925
+ <div className="chart-empty">没有可绘制的数据</div>
1926
+ )}
1439
1927
  </div>
1440
1928
  );
1441
1929
  }
@@ -1445,6 +1933,9 @@ Chart.displayName = 'Chart';
1445
1933
 
1446
1934
  const ZOOM_MIN = 0.5;
1447
1935
  const ZOOM_MAX = 6;
1936
+ // Module scope: only reads the ZOOM_* constants, so it is stable across renders
1937
+ // and can be a useCallback dependency without re-creating the callback.
1938
+ const clampScale = value => Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, +value.toFixed(3)));
1448
1939
 
1449
1940
  /**
1450
1941
  * Full-viewport zoom viewer shared by images and diagrams. Rendered through a
@@ -1463,11 +1954,15 @@ function ZoomOverlay({ title, caption, label, onClose, children }) {
1463
1954
  const scaleRef = React.useRef(scale);
1464
1955
  scaleRef.current = scale;
1465
1956
 
1466
- const clampScale = value => Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, +value.toFixed(3)));
1467
- const zoomBy = delta => setScale(value => clampScale(value + delta));
1468
- const reset = () => { setScale(1); setOffset({ x: 0, y: 0 }); };
1957
+ const zoomBy = React.useCallback(delta => setScale(value => clampScale(value + delta)), []);
1958
+ const reset = React.useCallback(() => {
1959
+ setScale(1);
1960
+ setOffset({ x: 0, y: 0 });
1961
+ }, []);
1469
1962
  // Cursor-anchored zoom feels wrong at 1x; recentre whenever we return to it.
1470
- const settle = value => { if (value <= 1) setOffset({ x: 0, y: 0 }); };
1963
+ const settle = value => {
1964
+ if (value <= 1) setOffset({ x: 0, y: 0 });
1965
+ };
1471
1966
 
1472
1967
  React.useEffect(() => {
1473
1968
  const stage = stageRef.current;
@@ -1485,8 +1980,10 @@ function ZoomOverlay({ title, caption, label, onClose, children }) {
1485
1980
 
1486
1981
  React.useEffect(() => {
1487
1982
  const onKeyDown = event => {
1488
- if (event.key === 'Escape') { event.stopPropagation(); onClose(); }
1489
- else if (event.key === '+' || event.key === '=') zoomBy(0.25);
1983
+ if (event.key === 'Escape') {
1984
+ event.stopPropagation();
1985
+ onClose();
1986
+ } else if (event.key === '+' || event.key === '=') zoomBy(0.25);
1490
1987
  else if (event.key === '-' || event.key === '_') zoomBy(-0.25);
1491
1988
  else if (event.key === '0') reset();
1492
1989
  };
@@ -1498,11 +1995,17 @@ function ZoomOverlay({ title, caption, label, onClose, children }) {
1498
1995
  window.removeEventListener('keydown', onKeyDown, true);
1499
1996
  document.body.style.overflow = overflow;
1500
1997
  };
1501
- }, [onClose]);
1998
+ }, [onClose, zoomBy, reset]);
1502
1999
 
1503
2000
  const onPointerDown = event => {
1504
2001
  if (event.target.closest('button')) return;
1505
- dragRef.current = { id: event.pointerId, x: event.clientX, y: event.clientY, ox: offset.x, oy: offset.y };
2002
+ dragRef.current = {
2003
+ id: event.pointerId,
2004
+ x: event.clientX,
2005
+ y: event.clientY,
2006
+ ox: offset.x,
2007
+ oy: offset.y
2008
+ };
1506
2009
  movedRef.current = false;
1507
2010
  setDragging(true);
1508
2011
  event.currentTarget.setPointerCapture(event.pointerId);
@@ -1527,13 +2030,17 @@ function ZoomOverlay({ title, caption, label, onClose, children }) {
1527
2030
  };
1528
2031
 
1529
2032
  return createPortal(
2033
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions -- clicking the backdrop closes the overlay; keyboard users close with the ✕ button or Esc
1530
2034
  <div
1531
2035
  className="image-zoom-overlay"
1532
2036
  role="dialog"
1533
2037
  aria-modal="true"
1534
2038
  aria-label={`放大查看:${title}`}
1535
- onClick={event => { if (event.target === event.currentTarget && !movedRef.current) onClose(); }}
2039
+ onClick={event => {
2040
+ if (event.target === event.currentTarget && !movedRef.current) onClose();
2041
+ }}
1536
2042
  >
2043
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- pan/zoom stage driven by pointer; the toolbar buttons are the keyboard path */}
1537
2044
  <div
1538
2045
  ref={stageRef}
1539
2046
  className={`image-zoom-stage${dragging ? ' is-dragging' : ''}`}
@@ -1541,23 +2048,54 @@ function ZoomOverlay({ title, caption, label, onClose, children }) {
1541
2048
  onPointerMove={onPointerMove}
1542
2049
  onPointerUp={onPointerUp}
1543
2050
  onPointerCancel={onPointerUp}
1544
- onDoubleClick={() => { const next = scaleRef.current > 1 ? 1 : 2; setScale(next); settle(next); }}
1545
- onClick={event => { if (movedRef.current) { event.stopPropagation(); movedRef.current = false; } }}
2051
+ onDoubleClick={() => {
2052
+ const next = scaleRef.current > 1 ? 1 : 2;
2053
+ setScale(next);
2054
+ settle(next);
2055
+ }}
2056
+ onClick={event => {
2057
+ if (movedRef.current) {
2058
+ event.stopPropagation();
2059
+ movedRef.current = false;
2060
+ }
2061
+ }}
1546
2062
  >
1547
- <div className="image-zoom-target" style={{ transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})` }}>
2063
+ <div
2064
+ className="image-zoom-target"
2065
+ style={{ transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})` }}
2066
+ >
1548
2067
  {children}
1549
2068
  </div>
1550
2069
  </div>
1551
- {title && <p className="image-zoom-caption">{label && <b>{label}</b>}{caption}</p>}
2070
+ {title && (
2071
+ <p className="image-zoom-caption">
2072
+ {label && <b>{label}</b>}
2073
+ {caption}
2074
+ </p>
2075
+ )}
1552
2076
  <div className="image-zoom-toolbar">
1553
- <button type="button" onClick={() => zoomBy(-0.25)} aria-label="缩小" title="缩小(−)">−</button>
2077
+ <button type="button" onClick={() => zoomBy(-0.25)} aria-label="缩小" title="缩小(−)">
2078
+ −
2079
+ </button>
1554
2080
  <span aria-live="polite">{Math.round(scale * 100)}%</span>
1555
- <button type="button" onClick={() => zoomBy(0.25)} aria-label="放大" title="放大(+)">+</button>
1556
- <button type="button" onClick={reset} aria-label="重置缩放" title="重置(0)">↺</button>
1557
- <button type="button" ref={closeRef} onClick={onClose} aria-label="关闭" title="关闭(Esc)">✕</button>
2081
+ <button type="button" onClick={() => zoomBy(0.25)} aria-label="放大" title="放大(+)">
2082
+ +
2083
+ </button>
2084
+ <button type="button" onClick={reset} aria-label="重置缩放" title="重置(0)">
2085
+ ↺
2086
+ </button>
2087
+ <button
2088
+ type="button"
2089
+ ref={closeRef}
2090
+ onClick={onClose}
2091
+ aria-label="关闭"
2092
+ title="关闭(Esc)"
2093
+ >
2094
+ ✕
2095
+ </button>
1558
2096
  </div>
1559
2097
  </div>,
1560
- document.body,
2098
+ document.body
1561
2099
  );
1562
2100
  }
1563
2101
 
@@ -1570,7 +2108,19 @@ function ImageZoom({ src, alt, caption, label, onClose }) {
1570
2108
  );
1571
2109
  }
1572
2110
 
1573
- export function Figure({ id, src, alt = '', caption, label, inline, width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
2111
+ export function Figure({
2112
+ id,
2113
+ src,
2114
+ alt = '',
2115
+ caption,
2116
+ label,
2117
+ inline,
2118
+ width = 'auto',
2119
+ height = 'auto',
2120
+ x = 0,
2121
+ y = 0,
2122
+ position = 'flow'
2123
+ }) {
1574
2124
  const scope = useFigureScope();
1575
2125
  // `inline` is a build-time hint consumed by vite's asset plugin; binding it
1576
2126
  // here keeps it out of the DOM and documents that the component accepts it.
@@ -1578,9 +2128,11 @@ export function Figure({ id, src, alt = '', caption, label, inline, width = 'aut
1578
2128
  const number = React.useSyncExternalStore(
1579
2129
  subscribeFigures,
1580
2130
  () => getFigureNumber(scope, id),
1581
- () => getFigureNumber(scope, id),
2131
+ () => getFigureNumber(scope, id)
1582
2132
  );
1583
- React.useEffect(() => { registerFigure(scope, id); }, [scope, id]);
2133
+ React.useEffect(() => {
2134
+ registerFigure(scope, id);
2135
+ }, [scope, id]);
1584
2136
  const [zoomed, setZoomed] = React.useState(false);
1585
2137
  const title = alt || caption || '图片';
1586
2138
  // An explicit `label` wins; otherwise a named figure numbers itself.
@@ -1600,16 +2152,29 @@ export function Figure({ id, src, alt = '', caption, label, inline, width = 'aut
1600
2152
  aria-label={`放大查看:${title}`}
1601
2153
  >
1602
2154
  <img src={src} alt={alt} loading="lazy" />
1603
- <span className="figure-zoom-hint" aria-hidden="true"><ZoomIn size={12} />点击放大</span>
2155
+ <span className="figure-zoom-hint" aria-hidden="true">
2156
+ <ZoomIn size={12} />
2157
+ 点击放大
2158
+ </span>
1604
2159
  </button>
1605
- ) : <div className="figure-placeholder">缺少图片 src</div>}
2160
+ ) : (
2161
+ <div className="figure-placeholder">缺少图片 src</div>
2162
+ )}
1606
2163
  {(caption || displayLabel) && (
1607
2164
  <figcaption>
1608
2165
  {displayLabel && <span className="figure-label">{displayLabel}</span>}
1609
2166
  {caption}
1610
2167
  </figcaption>
1611
2168
  )}
1612
- {zoomed && <ImageZoom src={src} alt={alt} caption={caption} label={displayLabel} onClose={() => setZoomed(false)} />}
2169
+ {zoomed && (
2170
+ <ImageZoom
2171
+ src={src}
2172
+ alt={alt}
2173
+ caption={caption}
2174
+ label={displayLabel}
2175
+ onClose={() => setZoomed(false)}
2176
+ />
2177
+ )}
1613
2178
  </figure>
1614
2179
  );
1615
2180
  }
@@ -1626,12 +2191,18 @@ export function FigureRef({ id, children }) {
1626
2191
  const number = React.useSyncExternalStore(
1627
2192
  subscribeFigures,
1628
2193
  () => getFigureNumber(scope, id),
1629
- () => getFigureNumber(scope, id),
2194
+ () => getFigureNumber(scope, id)
1630
2195
  );
1631
2196
  const text = children ?? (number ? `图 ${number}` : '图 ?');
1632
- return id
1633
- ? <a className="semantic-figure-ref" href={`#fig-${id}`}>{text}</a>
1634
- : <span className="semantic-figure-ref" data-missing="true">{text}</span>;
2197
+ return id ? (
2198
+ <a className="semantic-figure-ref" href={`#fig-${id}`}>
2199
+ {text}
2200
+ </a>
2201
+ ) : (
2202
+ <span className="semantic-figure-ref" data-missing="true">
2203
+ {text}
2204
+ </span>
2205
+ );
1635
2206
  }
1636
2207
  FigureRef.displayName = 'FigureRef';
1637
2208
 
@@ -1641,12 +2212,14 @@ export function Cite({ id, children }) {
1641
2212
  const index = React.useSyncExternalStore(
1642
2213
  subscribeReferences,
1643
2214
  () => getReferenceIndex(id),
1644
- () => null,
2215
+ () => null
1645
2216
  );
1646
2217
  const uid = React.useId().replace(/:/g, '');
1647
2218
  return (
1648
2219
  <sup className="semantic-cite" data-missing={index ? undefined : 'true'}>
1649
- <a href={`#ref-${id}`} id={`cite-${id}-${uid}`}>{children ?? (index ? `[${index}]` : '[?]')}</a>
2220
+ <a href={`#ref-${id}`} id={`cite-${id}-${uid}`}>
2221
+ {children ?? (index ? `[${index}]` : '[?]')}
2222
+ </a>
1650
2223
  </sup>
1651
2224
  );
1652
2225
  }
@@ -1654,26 +2227,45 @@ Cite.displayName = 'Cite';
1654
2227
 
1655
2228
  export function References({ title = '参考文献', items = [], children }) {
1656
2229
  const key = JSON.stringify(items || []);
1657
- React.useEffect(() => { registerReferences(items); }, [key]);
2230
+ React.useEffect(() => {
2231
+ registerReferences(items);
2232
+ // Registration is keyed on the serialized content, not the array identity,
2233
+ // so an equal-but-new `items` array does not re-register on every render.
2234
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2235
+ }, [key]);
1658
2236
  const list = Array.isArray(items) ? items.filter(Boolean) : [];
1659
2237
  return (
1660
2238
  <section className="semantic-references">
1661
- <div className="framework-model-head"><span className="semantic-tag">❡ {title}</span><code>REFERENCES</code></div>
2239
+ <div className="framework-model-head">
2240
+ <span className="semantic-tag">❡ {title}</span>
2241
+ <code>REFERENCES</code>
2242
+ </div>
1662
2243
  {list.length > 0 ? (
1663
2244
  <ol className="reference-list">
1664
2245
  {list.map((item, index) => (
1665
- <li key={item.id || index} id={item.id ? `ref-${item.id}` : undefined} className="reference-item">
2246
+ <li
2247
+ key={item.id || index}
2248
+ id={item.id ? `ref-${item.id}` : undefined}
2249
+ className="reference-item"
2250
+ >
1666
2251
  {item.authors && <span className="reference-authors">{item.authors}</span>}
1667
2252
  {item.year && <span className="reference-year">{item.year}</span>}
1668
- {item.title && (item.url
1669
- ? <a className="reference-title" href={item.url} target="_blank" rel="noreferrer">{item.title}</a>
1670
- : <span className="reference-title">{item.title}</span>)}
2253
+ {item.title &&
2254
+ (item.url ? (
2255
+ <a className="reference-title" href={item.url} target="_blank" rel="noreferrer">
2256
+ {item.title}
2257
+ </a>
2258
+ ) : (
2259
+ <span className="reference-title">{item.title}</span>
2260
+ ))}
1671
2261
  {item.source && <span className="reference-source">{item.source}</span>}
1672
2262
  {item.note && <span className="reference-note">{item.note}</span>}
1673
2263
  </li>
1674
2264
  ))}
1675
2265
  </ol>
1676
- ) : children}
2266
+ ) : (
2267
+ children
2268
+ )}
1677
2269
  </section>
1678
2270
  );
1679
2271
  }
@@ -1696,3 +2288,244 @@ function widgetStyle({ width, height, x, y, position }) {
1696
2288
  function widgetClass(position) {
1697
2289
  return position === 'absolute' ? 'semantic-widget-absolute' : '';
1698
2290
  }
2291
+
2292
+ // Argument, evidence and reference extensions --------------------------------
2293
+
2294
+ /** An attributed quotation: the body plus an optional author/source citation. */
2295
+ export function Quote({ author, source, href, children }) {
2296
+ const citation = source && href ? <a href={href}>{source}</a> : source;
2297
+ return (
2298
+ <blockquote className="semantic-quote">
2299
+ <div className="quote-body">{children}</div>
2300
+ {(author || source) && (
2301
+ <footer className="quote-attribution">
2302
+ <cite>
2303
+ {author}
2304
+ {author && source ? ' · ' : ''}
2305
+ {citation}
2306
+ </cite>
2307
+ </footer>
2308
+ )}
2309
+ </blockquote>
2310
+ );
2311
+ }
2312
+ Quote.displayName = 'Quote';
2313
+
2314
+ const CHECKLIST_MARKS = { pass: '✓', fail: '✕', unknown: '?', todo: '○' };
2315
+
2316
+ /** A verification checklist; each item carries a pass/fail/unknown/todo status. */
2317
+ export function Checklist({ title = '验证清单', items = [], children }) {
2318
+ const entries = Array.isArray(items) ? items.filter(Boolean) : [];
2319
+ return (
2320
+ <section className="semantic-checklist">
2321
+ <div className="semantic-tag">☑ {title}</div>
2322
+ {entries.length > 0 ? (
2323
+ <ul className="checklist-items">
2324
+ {entries.map((item, index) => {
2325
+ const entry = typeof item === 'string' ? { text: item } : item;
2326
+ const status = Object.hasOwn(CHECKLIST_MARKS, entry.status) ? entry.status : 'unknown';
2327
+ return (
2328
+ <li className="checklist-item" data-status={status} key={index}>
2329
+ <span className="checklist-mark" aria-hidden="true">
2330
+ {CHECKLIST_MARKS[status]}
2331
+ </span>
2332
+ <span className="checklist-text">
2333
+ {entry.text}
2334
+ {entry.note && <small className="checklist-note">{entry.note}</small>}
2335
+ </span>
2336
+ </li>
2337
+ );
2338
+ })}
2339
+ </ul>
2340
+ ) : (
2341
+ children
2342
+ )}
2343
+ </section>
2344
+ );
2345
+ }
2346
+ Checklist.displayName = 'Checklist';
2347
+
2348
+ /** The review date/reviewer stamp for a claim or page. */
2349
+ export function LastReviewed({ date, by, note }) {
2350
+ if (!date && !by && !note) return null;
2351
+ return (
2352
+ <div className="semantic-last-reviewed">
2353
+ <span className="reviewed-label">审阅</span>
2354
+ {date && (
2355
+ <time className="reviewed-date" dateTime={date}>
2356
+ {date}
2357
+ </time>
2358
+ )}
2359
+ {by && <span className="reviewed-by">{by}</span>}
2360
+ {note && <span className="reviewed-note">{note}</span>}
2361
+ </div>
2362
+ );
2363
+ }
2364
+ LastReviewed.displayName = 'LastReviewed';
2365
+
2366
+ /** A definition-style name/value list for specifications and parameters. */
2367
+ export function PropertyList({ title, items = [], children }) {
2368
+ const entries = Array.isArray(items) ? items.filter(Boolean) : [];
2369
+ return (
2370
+ <section className="semantic-property-list">
2371
+ {title && <div className="semantic-tag">≡ {title}</div>}
2372
+ {entries.length > 0 ? (
2373
+ <dl className="property-list">
2374
+ {entries.map((item, index) => {
2375
+ const entry = typeof item === 'string' ? { name: item } : item;
2376
+ return (
2377
+ <div className="property-row" key={index}>
2378
+ <dt className="property-name">{entry.name ?? entry.label ?? entry.key}</dt>
2379
+ <dd className="property-value">
2380
+ {entry.value}
2381
+ {entry.note && <small className="property-note">{entry.note}</small>}
2382
+ </dd>
2383
+ </div>
2384
+ );
2385
+ })}
2386
+ </dl>
2387
+ ) : (
2388
+ children
2389
+ )}
2390
+ </section>
2391
+ );
2392
+ }
2393
+ PropertyList.displayName = 'PropertyList';
2394
+
2395
+ /** An explicit nested hierarchy, distinct from the relational graph views. */
2396
+ function TreeBranch({ node, depth }) {
2397
+ if (!node) return null;
2398
+ const children = Array.isArray(node.children) ? node.children.filter(Boolean) : [];
2399
+ return (
2400
+ <li className="tree-branch" data-depth={depth}>
2401
+ <div className="tree-node">
2402
+ <span className="tree-label">{node.label ?? node.title}</span>
2403
+ {node.description && <span className="tree-desc">{node.description}</span>}
2404
+ </div>
2405
+ {children.length > 0 && (
2406
+ <ul className="tree-children">
2407
+ {children.map((child, index) => (
2408
+ <TreeBranch node={child} depth={depth + 1} key={index} />
2409
+ ))}
2410
+ </ul>
2411
+ )}
2412
+ </li>
2413
+ );
2414
+ }
2415
+
2416
+ export function TreeView({ title, items = [], children }) {
2417
+ const roots = Array.isArray(items) ? items.filter(Boolean) : [];
2418
+ return (
2419
+ <section className="semantic-tree" data-component="TreeView">
2420
+ {title && <div className="semantic-tag">⌗ {title}</div>}
2421
+ {roots.length > 0 ? (
2422
+ <ul className="tree-root">
2423
+ {roots.map((node, index) => (
2424
+ <TreeBranch node={node} depth={0} key={index} />
2425
+ ))}
2426
+ </ul>
2427
+ ) : (
2428
+ children
2429
+ )}
2430
+ </section>
2431
+ );
2432
+ }
2433
+ TreeView.displayName = 'TreeView';
2434
+
2435
+ // Code extensions ------------------------------------------------------------
2436
+
2437
+ /** One code block per variant (language or before/after), toggled like Tabs. */
2438
+ export function CodeTabs({ items = [], title, caption, lineNumbers = false }) {
2439
+ const tabs = Array.isArray(items) ? items.filter(Boolean) : [];
2440
+ if (tabs.length === 0) return null;
2441
+ return (
2442
+ <figure className="semantic-code-tabs">
2443
+ {title && <figcaption className="code-tabs-caption">{title}</figcaption>}
2444
+ <div className="code-tabs">
2445
+ {tabs.map((tab, index) => {
2446
+ const text = typeof tab.code === 'string' ? tab.code.replace(/^\n+|\s+$/g, '') : '';
2447
+ const showLang = Boolean(tab.language) && tab.language !== 'text';
2448
+ return (
2449
+ <details key={index} open={index === 0}>
2450
+ <summary>
2451
+ <span className="code-tab-label">
2452
+ {tab.label || tab.language || `代码 ${index + 1}`}
2453
+ </span>
2454
+ {showLang && <span className="lang-badge">{tab.language}</span>}
2455
+ </summary>
2456
+ <pre className={`code-block${lineNumbers ? ' code-block-numbered' : ''}`}>
2457
+ {lineNumbers ? (
2458
+ <code>
2459
+ {text.split('\n').map((line, lineIndex) => (
2460
+ <span className="code-line" key={lineIndex}>
2461
+ {line}
2462
+ </span>
2463
+ ))}
2464
+ </code>
2465
+ ) : (
2466
+ <code>{text}</code>
2467
+ )}
2468
+ </pre>
2469
+ </details>
2470
+ );
2471
+ })}
2472
+ </div>
2473
+ {caption && <p className="code-tabs-note">{caption}</p>}
2474
+ </figure>
2475
+ );
2476
+ }
2477
+ CodeTabs.displayName = 'CodeTabs';
2478
+
2479
+ /** A numbered listing whose specific lines are called out underneath. */
2480
+ export function AnnotatedCode({ code, language = 'text', title, caption, notes = [] }) {
2481
+ const raw = typeof code === 'string' ? code : childrenToText(code);
2482
+ const text = typeof raw === 'string' ? raw.replace(/^\n+|\s+$/g, '') : '';
2483
+ if (!text) return null;
2484
+ const annotated = new Map();
2485
+ (Array.isArray(notes) ? notes : []).forEach(note => {
2486
+ if (note && Number.isInteger(note.line)) {
2487
+ const list = annotated.get(note.line) || [];
2488
+ list.push(note);
2489
+ annotated.set(note.line, list);
2490
+ }
2491
+ });
2492
+ const showLang = Boolean(language) && language !== 'text';
2493
+ return (
2494
+ <figure className="semantic-annotated-code" data-language={language}>
2495
+ {(title || showLang) && (
2496
+ <div className="semantic-code-head">
2497
+ {title && <span className="semantic-code-title">{title}</span>}
2498
+ {showLang && <span className="lang-badge">{language}</span>}
2499
+ </div>
2500
+ )}
2501
+ <pre className="code-block code-block-numbered annotated-code">
2502
+ <code>
2503
+ {text.split('\n').map((line, index) => (
2504
+ <span
2505
+ className={`code-line${annotated.has(index + 1) ? ' is-annotated' : ''}`}
2506
+ key={index}
2507
+ >
2508
+ {line}
2509
+ </span>
2510
+ ))}
2511
+ </code>
2512
+ </pre>
2513
+ {annotated.size > 0 && (
2514
+ <ol className="annotated-code-notes">
2515
+ {[...annotated.entries()]
2516
+ .sort((a, b) => a[0] - b[0])
2517
+ .flatMap(([line, list]) =>
2518
+ list.map((note, noteIndex) => (
2519
+ <li className="annotated-code-note" key={`${line}-${noteIndex}`}>
2520
+ <span className="note-line">L{line}</span>
2521
+ <span className="note-text">{note.text}</span>
2522
+ </li>
2523
+ ))
2524
+ )}
2525
+ </ol>
2526
+ )}
2527
+ {caption && <figcaption className="semantic-code-caption">{caption}</figcaption>}
2528
+ </figure>
2529
+ );
2530
+ }
2531
+ AnnotatedCode.displayName = 'AnnotatedCode';