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.
- package/README.md +3 -3
- package/bin/cli.mjs +197 -80
- package/package.json +1 -1
- package/packaged-skill/SKILL.md +8 -7
- package/packaged-skill/references/scroll-guide.mdx +14 -0
- package/template/references/scroll-guide.mdx +14 -0
- package/template/src/app/App.jsx +77 -41
- package/template/src/app/search.js +14 -5
- package/template/src/app/use-appearance.js +29 -15
- package/template/src/components/MDXComponents.jsx +1029 -196
- package/template/src/components/SkinPicker.jsx +14 -6
- package/template/src/model/concept-schema.js +27 -12
- package/template/src/model/node-kinds.js +9 -9
- package/template/src/model/normalize-content.js +8 -8
- package/template/src/model/relation-types.js +37 -12
- package/template/src/model/validate-content.js +282 -59
- package/template/src/scroll-main.jsx +8 -1
- package/template/src/styles/core.css +191 -0
- package/template/src/styles/packs/elastic.css +37 -0
- package/template/src/styles/packs/manuscript.css +67 -0
- package/template/src/styles/packs/shadcn.css +33 -0
- package/template/src/views/NodeExplorer.jsx +364 -293
- package/template/src/views/RelationGraph.jsx +191 -118
- package/template/vite.config.js +32 -20
|
@@ -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 = () =>
|
|
17
|
-
|
|
18
|
-
|
|
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, {
|
|
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(
|
|
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)
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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({
|
|
130
|
-
|
|
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
|
|
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
|
|
140
|
-
return
|
|
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
|
|
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"
|
|
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({
|
|
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
|
|
212
|
-
{lineNumbers
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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) =>
|
|
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) =>
|
|
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">
|
|
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({
|
|
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
|
-
) :
|
|
483
|
+
) : (
|
|
484
|
+
children
|
|
485
|
+
)}
|
|
392
486
|
</section>
|
|
393
487
|
);
|
|
394
488
|
}
|
|
395
489
|
FrameworkModel.displayName = 'FrameworkModel';
|
|
396
490
|
|
|
397
|
-
export function MatrixModel({
|
|
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"
|
|
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
|
-
) :
|
|
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"
|
|
422
|
-
|
|
423
|
-
|
|
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"
|
|
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
|
|
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
|
-
) :
|
|
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"
|
|
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
|
|
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
|
-
) :
|
|
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
|
|
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 ?
|
|
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 = [
|
|
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 &&
|
|
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 &&
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
) :
|
|
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 &&
|
|
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>
|
|
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) =>
|
|
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
|
-
) :
|
|
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({
|
|
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
|
-
) :
|
|
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
|
|
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
|
-
) :
|
|
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 ?
|
|
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 ?
|
|
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
|
-
:
|
|
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({
|
|
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"
|
|
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"
|
|
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 &&
|
|
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) =>
|
|
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) =>
|
|
1106
|
+
{list.map((branch, index) => (
|
|
1107
|
+
<DecisionBranch key={index} node={branch} depth={0} />
|
|
1108
|
+
))}
|
|
863
1109
|
</ul>
|
|
864
|
-
) :
|
|
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 &&
|
|
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">
|
|
1144
|
+
<span className="feedback-return" aria-hidden="true">
|
|
1145
|
+
{polarity} ↺
|
|
1146
|
+
</span>
|
|
893
1147
|
</div>
|
|
894
|
-
) :
|
|
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 = () => {
|
|
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({
|
|
954
|
-
|
|
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)
|
|
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(
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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 () => {
|
|
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
|
-
|
|
1099
|
-
|
|
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({
|
|
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 () => {
|
|
1442
|
+
return () => {
|
|
1443
|
+
cancelled = true;
|
|
1444
|
+
};
|
|
1129
1445
|
}, [chart, id, appearanceKey]);
|
|
1130
1446
|
|
|
1131
1447
|
const canZoom = Boolean(chart.trim()) && !error;
|
|
1132
|
-
const openZoom = () => {
|
|
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
|
|
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
|
|
1147
|
-
|
|
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({
|
|
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
|
|
1174
|
-
|
|
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
|
-
) :
|
|
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 &&
|
|
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 &&
|
|
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
|
-
) :
|
|
1563
|
+
) : (
|
|
1564
|
+
children
|
|
1565
|
+
)}
|
|
1211
1566
|
</div>
|
|
1212
1567
|
);
|
|
1213
1568
|
}
|
|
1214
1569
|
RelationPath.displayName = 'RelationPath';
|
|
1215
1570
|
|
|
1216
|
-
export function Insight({
|
|
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
|
|
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({
|
|
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
|
|
1230
|
-
{
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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({
|
|
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 = [
|
|
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
|
|
1348
|
-
|
|
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}>
|
|
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
|
|
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(
|
|
1392
|
-
|
|
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
|
|
1398
|
-
|
|
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
|
-
:
|
|
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
|
|
1422
|
-
|
|
1423
|
-
|
|
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
|
|
1433
|
-
|
|
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
|
|
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
|
-
) :
|
|
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
|
|
1467
|
-
const
|
|
1468
|
-
|
|
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 => {
|
|
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') {
|
|
1489
|
-
|
|
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 = {
|
|
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 => {
|
|
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={() => {
|
|
1545
|
-
|
|
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
|
|
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 &&
|
|
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="缩小(−)"
|
|
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="放大(+)"
|
|
1556
|
-
|
|
1557
|
-
|
|
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({
|
|
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(() => {
|
|
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"
|
|
2155
|
+
<span className="figure-zoom-hint" aria-hidden="true">
|
|
2156
|
+
<ZoomIn size={12} />
|
|
2157
|
+
点击放大
|
|
2158
|
+
</span>
|
|
1604
2159
|
</button>
|
|
1605
|
-
) :
|
|
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 &&
|
|
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
|
-
|
|
1634
|
-
|
|
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}`}>
|
|
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(() => {
|
|
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"
|
|
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
|
|
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 &&
|
|
1669
|
-
|
|
1670
|
-
|
|
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
|
-
) :
|
|
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';
|