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.
@@ -1,16 +1,21 @@
1
1
  import React from 'react';
2
- import { ChevronRight, ArrowUpRight, CornerDownRight, ArrowLeft, Network, CornerLeftUp, ZoomIn, ZoomOut, RotateCcw, Link as LinkIcon } from 'lucide-react';
2
+ import {
3
+ ChevronRight,
4
+ ArrowUpRight,
5
+ CornerDownRight,
6
+ ArrowLeft,
7
+ Network,
8
+ CornerLeftUp,
9
+ ZoomIn,
10
+ ZoomOut,
11
+ RotateCcw
12
+ } from 'lucide-react';
3
13
  import { getAncestorPath, getSiblingNodes } from '../model/concept-schema.js';
4
14
  import { LEVEL_DEFS } from '../model/relation-types.js';
5
15
  import { NODE_KINDS } from '../model/node-kinds.js';
6
16
  import { FigureScopeProvider } from '../components/FigureScope.jsx';
7
17
 
8
- export function NodeExplorer({
9
- graph,
10
- currentNodeId,
11
- onSelectNode,
12
- onSwitchView,
13
- }) {
18
+ export function NodeExplorer({ graph, currentNodeId, onSelectNode, onSwitchView }) {
14
19
  const { nodes, relations } = graph;
15
20
  const [canvasScale, setCanvasScale] = React.useState(1);
16
21
  const [canvasPan, setCanvasPan] = React.useState({ x: 0, y: 0 });
@@ -24,13 +29,13 @@ export function NodeExplorer({
24
29
  canvasScaleRef.current = canvasScale;
25
30
  canvasPanRef.current = canvasPan;
26
31
 
27
- const updateScale = (nextScale) => {
32
+ const updateScale = nextScale => {
28
33
  const scale = Math.max(0.65, Math.min(1.6, +nextScale.toFixed(2)));
29
34
  setCanvasScale(scale);
30
35
  };
31
36
 
32
37
  // Keep the board from being scrolled entirely out of view.
33
- const clampCanvasPan = (x, y, scale = canvasScale) => {
38
+ const clampCanvasPan = React.useCallback((x, y, scale = canvasScaleRef.current) => {
34
39
  const viewport = viewportRef.current;
35
40
  const board = boardRef.current;
36
41
  if (!viewport || !board) return { x, y };
@@ -39,9 +44,9 @@ export function NodeExplorer({
39
44
  Math.max(Math.min(view - content, 0) - margin, Math.min(margin, value));
40
45
  return {
41
46
  x: clampAxis(x, board.offsetWidth * scale, viewport.clientWidth),
42
- y: clampAxis(y, board.offsetHeight * scale, viewport.clientHeight),
47
+ y: clampAxis(y, board.offsetHeight * scale, viewport.clientHeight)
43
48
  };
44
- };
49
+ }, []);
45
50
 
46
51
  // Wheel scrolls the canvas vertically; Ctrl/Cmd + wheel zooms around the
47
52
  // pointer. React's onWheel is passive, so bind a native listener instead.
@@ -49,7 +54,7 @@ export function NodeExplorer({
49
54
  const viewport = viewportRef.current;
50
55
  if (!viewport) return undefined;
51
56
 
52
- const handleWheel = (event) => {
57
+ const handleWheel = event => {
53
58
  if (event.ctrlKey || event.metaKey) {
54
59
  event.preventDefault();
55
60
  const scale = canvasScaleRef.current;
@@ -62,7 +67,9 @@ export function NodeExplorer({
62
67
  const contentX = (focusX - pan.x) / scale;
63
68
  const contentY = (focusY - pan.y) / scale;
64
69
  setCanvasScale(nextScale);
65
- setCanvasPan(clampCanvasPan(focusX - contentX * nextScale, focusY - contentY * nextScale, nextScale));
70
+ setCanvasPan(
71
+ clampCanvasPan(focusX - contentX * nextScale, focusY - contentY * nextScale, nextScale)
72
+ );
66
73
  return;
67
74
  }
68
75
 
@@ -75,9 +82,9 @@ export function NodeExplorer({
75
82
 
76
83
  viewport.addEventListener('wheel', handleWheel, { passive: false });
77
84
  return () => viewport.removeEventListener('wheel', handleWheel);
78
- }, []);
85
+ }, [clampCanvasPan]);
79
86
 
80
- const handleCanvasPointerDown = (event) => {
87
+ const handleCanvasPointerDown = event => {
81
88
  if (event.target.closest('button, a, input, select, textarea')) return;
82
89
  dragRef.current = {
83
90
  pointerId: event.pointerId,
@@ -85,26 +92,31 @@ export function NodeExplorer({
85
92
  startY: event.clientY,
86
93
  panX: canvasPan.x,
87
94
  panY: canvasPan.y,
88
- moved: false,
95
+ moved: false
89
96
  };
90
97
  setIsDragging(true);
91
98
  event.currentTarget.setPointerCapture(event.pointerId);
92
99
  };
93
100
 
94
- const handleCanvasPointerMove = (event) => {
101
+ const handleCanvasPointerMove = event => {
95
102
  if (!dragRef.current || dragRef.current.pointerId !== event.pointerId) return;
96
103
  event.preventDefault();
97
- const distance = Math.hypot(event.clientX - dragRef.current.startX, event.clientY - dragRef.current.startY);
104
+ const distance = Math.hypot(
105
+ event.clientX - dragRef.current.startX,
106
+ event.clientY - dragRef.current.startY
107
+ );
98
108
  if (distance < 6) return;
99
109
  dragRef.current.moved = true;
100
- setCanvasPan(clampCanvasPan(
101
- dragRef.current.panX + event.clientX - dragRef.current.startX,
102
- dragRef.current.panY + event.clientY - dragRef.current.startY,
103
- canvasScale
104
- ));
110
+ setCanvasPan(
111
+ clampCanvasPan(
112
+ dragRef.current.panX + event.clientX - dragRef.current.startX,
113
+ dragRef.current.panY + event.clientY - dragRef.current.startY,
114
+ canvasScale
115
+ )
116
+ );
105
117
  };
106
118
 
107
- const handleCanvasPointerUp = (event) => {
119
+ const handleCanvasPointerUp = event => {
108
120
  if (dragRef.current?.pointerId === event.pointerId) {
109
121
  suppressClickRef.current = dragRef.current.moved;
110
122
  dragRef.current = null;
@@ -120,17 +132,18 @@ export function NodeExplorer({
120
132
 
121
133
  const ancestorPath = getAncestorPath(nodes, currentNode.id);
122
134
  const siblings = getSiblingNodes(nodes, currentNode.id);
123
- const parentNode = currentNode.parent ? nodes.get(currentNode.parent) : null;
124
- const childNodes = currentNode.children
125
- .filter(id => nodes.has(id))
126
- .map(id => nodes.get(id));
135
+ const childNodes = currentNode.children.filter(id => nodes.has(id)).map(id => nodes.get(id));
127
136
 
128
137
  // Node-specific relations
129
138
  const outgoingRelations = relations.filter(r => r.from === currentNode.id);
130
139
  const incomingRelations = relations.filter(r => r.to === currentNode.id);
131
140
 
132
141
  // Level definition
133
- const levelInfo = LEVEL_DEFS[currentNode.level] || { name: currentNode.level, tag: currentNode.level, color: 'var(--level-l0)' };
142
+ const levelInfo = LEVEL_DEFS[currentNode.level] || {
143
+ name: currentNode.level,
144
+ tag: currentNode.level,
145
+ color: 'var(--level-l0)'
146
+ };
134
147
  const kindDef = currentNode.kind ? NODE_KINDS[currentNode.kind] || null : null;
135
148
 
136
149
  return (
@@ -151,11 +164,16 @@ export function NodeExplorer({
151
164
  title={node.title}
152
165
  aria-current={isCurrent ? 'page' : undefined}
153
166
  >
154
- <span className="bc-level-pill" style={{ borderColor: LEVEL_DEFS[node.level]?.color }}>
167
+ <span
168
+ className="bc-level-pill"
169
+ style={{ borderColor: LEVEL_DEFS[node.level]?.color }}
170
+ >
155
171
  {node.level}
156
172
  </span>
157
173
  <span className="bc-title">{node.title}</span>
158
- {index < ancestorPath.length - 1 && <ChevronRight size={14} className="bc-arrow" />}
174
+ {index < ancestorPath.length - 1 && (
175
+ <ChevronRight size={14} className="bc-arrow" />
176
+ )}
159
177
  </button>
160
178
  );
161
179
  })}
@@ -167,12 +185,11 @@ export function NodeExplorer({
167
185
  <div className="sibling-list">
168
186
  {siblings.length > 0 ? (
169
187
  siblings.map(sib => (
170
- <button
171
- key={sib.id}
172
- className="sibling-btn"
173
- onClick={() => onSelectNode(sib.id)}
174
- >
175
- <span className="sib-dot" style={{ backgroundColor: LEVEL_DEFS[sib.level]?.color }} />
188
+ <button key={sib.id} className="sibling-btn" onClick={() => onSelectNode(sib.id)}>
189
+ <span
190
+ className="sib-dot"
191
+ style={{ backgroundColor: LEVEL_DEFS[sib.level]?.color }}
192
+ />
176
193
  <span className="sib-title">{sib.title}</span>
177
194
  <span className="sib-lvl">{sib.level}</span>
178
195
  </button>
@@ -193,251 +210,261 @@ export function NodeExplorer({
193
210
 
194
211
  {/* 2. Middle Column: Current Node Explanation Card */}
195
212
  <FigureScopeProvider scope={currentNode.id}>
196
- <main className="explorer-center">
197
- <HierarchyStrip
198
- ancestorPath={ancestorPath}
199
- currentNode={currentNode}
200
- childNodes={childNodes}
201
- onSelectNode={onSelectNode}
202
- />
203
- <div className="center-scrollable">
204
- <div
205
- ref={viewportRef}
206
- className={`draft-viewport ${isDragging ? 'is-dragging' : ''}`}
207
- onPointerDown={handleCanvasPointerDown}
208
- onPointerMove={handleCanvasPointerMove}
209
- onPointerUp={handleCanvasPointerUp}
210
- onPointerCancel={handleCanvasPointerUp}
211
- onClickCapture={(event) => {
212
- if (!suppressClickRef.current) return;
213
- event.preventDefault();
214
- event.stopPropagation();
215
- suppressClickRef.current = false;
216
- }}
217
- >
218
- <div className="draft-floating-tools">
219
- <button
220
- className="view-graph-btn"
221
- onClick={() => onSwitchView('graph')}
222
- title="在图谱中聚焦此节点"
223
- >
224
- <Network size={14} />
225
- <span>查看全局关系</span>
226
- </button>
227
- <div className="draft-zoom-controls" aria-label="草稿缩放">
228
- <button onClick={() => updateScale(canvasScale - 0.1)} title="缩小">
229
- <ZoomOut size={14} />
230
- </button>
231
- <span>{Math.round(canvasScale * 100)}%</span>
232
- <button onClick={() => updateScale(canvasScale + 0.1)} title="放大">
233
- <ZoomIn size={14} />
234
- </button>
235
- <button onClick={() => { setCanvasScale(1); setCanvasPan({ x: 0, y: 0 }); }} title="重置画布">
236
- <RotateCcw size={13} />
237
- </button>
238
- </div>
239
- </div>
213
+ <main className="explorer-center">
214
+ <HierarchyStrip
215
+ ancestorPath={ancestorPath}
216
+ currentNode={currentNode}
217
+ childNodes={childNodes}
218
+ onSelectNode={onSelectNode}
219
+ />
220
+ <div className="center-scrollable">
240
221
  <div
241
- ref={boardRef}
242
- className="draft-board"
243
- style={{
244
- transform: `translate(${canvasPan.x}px, ${canvasPan.y}px) scale(${canvasScale})`,
222
+ ref={viewportRef}
223
+ className={`draft-viewport ${isDragging ? 'is-dragging' : ''}`}
224
+ onPointerDown={handleCanvasPointerDown}
225
+ onPointerMove={handleCanvasPointerMove}
226
+ onPointerUp={handleCanvasPointerUp}
227
+ onPointerCancel={handleCanvasPointerUp}
228
+ onClickCapture={event => {
229
+ if (!suppressClickRef.current) return;
230
+ event.preventDefault();
231
+ event.stopPropagation();
232
+ suppressClickRef.current = false;
245
233
  }}
246
234
  >
247
- {/* Compact concept note header */}
248
- <article className="concept-hero-card">
249
- <div className="hero-level-banner" style={{ color: levelInfo.color }}>
250
- <span className="badge">{levelInfo.tag}</span>
251
- {kindDef && (
252
- <span
253
- className="kind-badge"
254
- style={{ color: kindDef.tone, borderColor: kindDef.tone }}
255
- title={kindDef.description}
235
+ <div className="draft-floating-tools">
236
+ <button
237
+ className="view-graph-btn"
238
+ onClick={() => onSwitchView('graph')}
239
+ title="在图谱中聚焦此节点"
256
240
  >
257
- {kindDef.label}
258
- </span>
259
- )}
260
- <span className="desc">{levelInfo.desc}</span>
261
- </div>
262
-
263
- <h1 className="hero-title">{currentNode.title}</h1>
264
-
265
- {currentNode.summary && (
266
- <p className="hero-summary">{currentNode.summary}</p>
267
- )}
268
-
269
- {/* I/O and Path chips */}
270
- <div className="hero-meta-chips">
271
- {currentNode.input && (
272
- <div className="meta-chip">
273
- <span className="chip-label">输入</span>
274
- <span className="chip-value">{currentNode.input}</span>
275
- </div>
276
- )}
277
- {currentNode.output && (
278
- <div className="meta-chip">
279
- <span className="chip-label">输出</span>
280
- <span className="chip-value">{currentNode.output}</span>
281
- </div>
282
- )}
283
- {currentNode.invariants.length > 0 && (
284
- <div className="meta-chip">
285
- <span className="chip-label">不变量</span>
286
- <span className="chip-value">{currentNode.invariants.length}</span>
287
- </div>
288
- )}
289
- {currentNode.evidence.length > 0 && (
290
- <div className="meta-chip">
291
- <span className="chip-label">证据</span>
292
- <span className="chip-value">{currentNode.evidence.length}</span>
293
- </div>
294
- )}
295
- {currentNode.failureModes.length > 0 && (
296
- <div className="meta-chip">
297
- <span className="chip-label">故障模式</span>
298
- <span className="chip-value">{currentNode.failureModes.length}</span>
241
+ <Network size={14} />
242
+ <span>查看全局关系</span>
243
+ </button>
244
+ <div className="draft-zoom-controls" aria-label="草稿缩放">
245
+ <button onClick={() => updateScale(canvasScale - 0.1)} title="缩小">
246
+ <ZoomOut size={14} />
247
+ </button>
248
+ <span>{Math.round(canvasScale * 100)}%</span>
249
+ <button onClick={() => updateScale(canvasScale + 0.1)} title="放大">
250
+ <ZoomIn size={14} />
251
+ </button>
252
+ <button
253
+ onClick={() => {
254
+ setCanvasScale(1);
255
+ setCanvasPan({ x: 0, y: 0 });
256
+ }}
257
+ title="重置画布"
258
+ >
259
+ <RotateCcw size={13} />
260
+ </button>
299
261
  </div>
300
- )}
301
- <div className="meta-chip">
302
- <span className="chip-label">完整路径</span>
303
- <span className="chip-value">{ancestorPath.map(n => n.title).join(' / ')}</span>
304
262
  </div>
305
- </div>
306
-
307
- </article>
308
-
309
- {/* Core Mechanism / Definition Section */}
310
- <div className="content-blocks">
311
- {currentNode.overview && (
312
- <section className="node-block">
313
- <div className="block-head">
314
- <h2>概览理解</h2>
315
- <small>一句话宏观认知</small>
316
- </div>
317
- <div className="block-body">{currentNode.overview}</div>
318
- </section>
319
- )}
320
-
321
- {currentNode.definition && (
322
- <section className="node-block">
323
- <div className="block-head">
324
- <h2>概念定义</h2>
325
- <small>准确定义与本质属性</small>
326
- </div>
327
- <div className="block-body">{currentNode.definition}</div>
328
- </section>
329
- )}
330
-
331
- {currentNode.mechanism && (
332
- <section className="node-block">
333
- <div className="block-head">
334
- <h2>核心机制</h2>
335
- <small>工作原理与状态流转</small>
336
- </div>
337
- <div className="block-body">{currentNode.mechanism}</div>
338
- </section>
339
- )}
340
-
341
- {/* Implementation code if present */}
342
- {currentNode.implementation && (
343
- <section className="node-block">
344
- <div className="block-head">
345
- <h2>{currentNode.implementation.title}</h2>
346
- <span className="impl-lang">{currentNode.implementation.language}</span>
347
- </div>
348
- <div className="block-body">
349
- <pre className="code-block">
350
- <code>{currentNode.implementation.code}</code>
351
- </pre>
352
- </div>
353
- </section>
354
- )}
355
-
356
- {/* Examples & Counterexamples */}
357
- {currentNode.examples.length > 0 && (
358
- <section className="node-block">
359
- <div className="block-head">
360
- <h2>典型示例</h2>
361
- <small>具象化阐释</small>
362
- </div>
363
- <div className="block-body">
364
- {currentNode.examples.map((ex, i) => (
365
- <div key={i} className="example-item">
366
- <div className="ex-title">{ex.title}</div>
367
- <div className="ex-content">{ex.content}</div>
368
- </div>
369
- ))}
370
- </div>
371
- </section>
372
- )}
373
-
374
- {currentNode.counterexamples.length > 0 && (
375
- <section className="node-block highlight-warn">
376
- <div className="block-head">
377
- <h2>反例与常见误区</h2>
378
- <small>加深概念边界的辨析</small>
379
- </div>
380
- <div className="block-body">
381
- {currentNode.counterexamples.map((cex, i) => (
382
- <div key={i} className="counterexample-item">
383
- <div className="cex-title">✕ {cex.title}</div>
384
- <div className="cex-content">{cex.content}</div>
263
+ <div
264
+ ref={boardRef}
265
+ className="draft-board"
266
+ style={{
267
+ transform: `translate(${canvasPan.x}px, ${canvasPan.y}px) scale(${canvasScale})`
268
+ }}
269
+ >
270
+ {/* Compact concept note header */}
271
+ <article className="concept-hero-card">
272
+ <div className="hero-level-banner" style={{ color: levelInfo.color }}>
273
+ <span className="badge">{levelInfo.tag}</span>
274
+ {kindDef && (
275
+ <span
276
+ className="kind-badge"
277
+ style={{ color: kindDef.tone, borderColor: kindDef.tone }}
278
+ title={kindDef.description}
279
+ >
280
+ {kindDef.label}
281
+ </span>
282
+ )}
283
+ <span className="desc">{levelInfo.desc}</span>
284
+ </div>
285
+
286
+ <h1 className="hero-title">{currentNode.title}</h1>
287
+
288
+ {currentNode.summary && <p className="hero-summary">{currentNode.summary}</p>}
289
+
290
+ {/* I/O and Path chips */}
291
+ <div className="hero-meta-chips">
292
+ {currentNode.input && (
293
+ <div className="meta-chip">
294
+ <span className="chip-label">输入</span>
295
+ <span className="chip-value">{currentNode.input}</span>
296
+ </div>
297
+ )}
298
+ {currentNode.output && (
299
+ <div className="meta-chip">
300
+ <span className="chip-label">输出</span>
301
+ <span className="chip-value">{currentNode.output}</span>
302
+ </div>
303
+ )}
304
+ {currentNode.invariants.length > 0 && (
305
+ <div className="meta-chip">
306
+ <span className="chip-label">不变量</span>
307
+ <span className="chip-value">{currentNode.invariants.length}</span>
308
+ </div>
309
+ )}
310
+ {currentNode.evidence.length > 0 && (
311
+ <div className="meta-chip">
312
+ <span className="chip-label">证据</span>
313
+ <span className="chip-value">{currentNode.evidence.length}</span>
314
+ </div>
315
+ )}
316
+ {currentNode.failureModes.length > 0 && (
317
+ <div className="meta-chip">
318
+ <span className="chip-label">故障模式</span>
319
+ <span className="chip-value">{currentNode.failureModes.length}</span>
320
+ </div>
321
+ )}
322
+ <div className="meta-chip">
323
+ <span className="chip-label">完整路径</span>
324
+ <span className="chip-value">
325
+ {ancestorPath.map(n => n.title).join(' / ')}
326
+ </span>
385
327
  </div>
386
- ))}
387
- </div>
388
- </section>
389
- )}
390
-
391
- {/* Custom presentation sections (Compare, Flow, etc.) */}
392
- {currentNode.customSections.map((sec, i) => (
393
- sec?.props?.position === 'absolute' ? (
394
- <React.Fragment key={i}>{sec}</React.Fragment>
395
- ) : (
396
- <section key={i} className="node-block custom-section">
397
- {sec}
398
- </section>
399
- )
400
- ))}
401
-
402
- {/* Sub-node Exploration Cards (Drill Down Entrance) */}
403
- {childNodes.length > 0 && (
404
- <section className="node-block drill-down-section">
405
- <div className="block-head">
406
- <h2>深入下钻:子概念节点</h2>
407
- <small>点击卡片探索更深机制</small>
408
- </div>
409
- <div className="subnodes-grid">
410
- {childNodes.map(child => (
411
- <button
412
- key={child.id}
413
- className="subnode-card"
414
- onClick={() => onSelectNode(child.id)}
415
- >
416
- <div className="sn-header">
417
- <span className="sn-level" style={{ color: LEVEL_DEFS[child.level]?.color }}>
418
- {child.level}
419
- </span>
420
- <ArrowUpRight size={16} className="sn-arrow" />
328
+ </div>
329
+ </article>
330
+
331
+ {/* Core Mechanism / Definition Section */}
332
+ <div className="content-blocks">
333
+ {currentNode.overview && (
334
+ <section className="node-block">
335
+ <div className="block-head">
336
+ <h2>概览理解</h2>
337
+ <small>一句话宏观认知</small>
338
+ </div>
339
+ <div className="block-body">{currentNode.overview}</div>
340
+ </section>
341
+ )}
342
+
343
+ {currentNode.definition && (
344
+ <section className="node-block">
345
+ <div className="block-head">
346
+ <h2>概念定义</h2>
347
+ <small>准确定义与本质属性</small>
348
+ </div>
349
+ <div className="block-body">{currentNode.definition}</div>
350
+ </section>
351
+ )}
352
+
353
+ {currentNode.mechanism && (
354
+ <section className="node-block">
355
+ <div className="block-head">
356
+ <h2>核心机制</h2>
357
+ <small>工作原理与状态流转</small>
358
+ </div>
359
+ <div className="block-body">{currentNode.mechanism}</div>
360
+ </section>
361
+ )}
362
+
363
+ {/* Implementation code if present */}
364
+ {currentNode.implementation && (
365
+ <section className="node-block">
366
+ <div className="block-head">
367
+ <h2>{currentNode.implementation.title}</h2>
368
+ <span className="impl-lang">{currentNode.implementation.language}</span>
369
+ </div>
370
+ <div className="block-body">
371
+ <pre className="code-block">
372
+ <code>{currentNode.implementation.code}</code>
373
+ </pre>
374
+ </div>
375
+ </section>
376
+ )}
377
+
378
+ {/* Examples & Counterexamples */}
379
+ {currentNode.examples.length > 0 && (
380
+ <section className="node-block">
381
+ <div className="block-head">
382
+ <h2>典型示例</h2>
383
+ <small>具象化阐释</small>
384
+ </div>
385
+ <div className="block-body">
386
+ {currentNode.examples.map((ex, i) => (
387
+ <div key={i} className="example-item">
388
+ <div className="ex-title">{ex.title}</div>
389
+ <div className="ex-content">{ex.content}</div>
390
+ </div>
391
+ ))}
392
+ </div>
393
+ </section>
394
+ )}
395
+
396
+ {currentNode.counterexamples.length > 0 && (
397
+ <section className="node-block highlight-warn">
398
+ <div className="block-head">
399
+ <h2>反例与常见误区</h2>
400
+ <small>加深概念边界的辨析</small>
401
+ </div>
402
+ <div className="block-body">
403
+ {currentNode.counterexamples.map((cex, i) => (
404
+ <div key={i} className="counterexample-item">
405
+ <div className="cex-title">✕ {cex.title}</div>
406
+ <div className="cex-content">{cex.content}</div>
407
+ </div>
408
+ ))}
421
409
  </div>
422
- <h3 className="sn-title">{child.title}</h3>
423
- <p className="sn-summary">{child.summary || '点击进入该概念下钻探索…'}</p>
424
- </button>
425
- ))}
410
+ </section>
411
+ )}
412
+
413
+ {/* Custom presentation sections (Compare, Flow, etc.) */}
414
+ {currentNode.customSections.map((sec, i) =>
415
+ sec?.props?.position === 'absolute' ? (
416
+ <React.Fragment key={i}>{sec}</React.Fragment>
417
+ ) : (
418
+ <section key={i} className="node-block custom-section">
419
+ {sec}
420
+ </section>
421
+ )
422
+ )}
423
+
424
+ {/* Sub-node Exploration Cards (Drill Down Entrance) */}
425
+ {childNodes.length > 0 && (
426
+ <section className="node-block drill-down-section">
427
+ <div className="block-head">
428
+ <h2>深入下钻:子概念节点</h2>
429
+ <small>点击卡片探索更深机制</small>
430
+ </div>
431
+ <div className="subnodes-grid">
432
+ {childNodes.map(child => (
433
+ <button
434
+ key={child.id}
435
+ className="subnode-card"
436
+ onClick={() => onSelectNode(child.id)}
437
+ >
438
+ <div className="sn-header">
439
+ <span
440
+ className="sn-level"
441
+ style={{ color: LEVEL_DEFS[child.level]?.color }}
442
+ >
443
+ {child.level}
444
+ </span>
445
+ <ArrowUpRight size={16} className="sn-arrow" />
446
+ </div>
447
+ <h3 className="sn-title">{child.title}</h3>
448
+ <p className="sn-summary">
449
+ {child.summary || '点击进入该概念下钻探索…'}
450
+ </p>
451
+ </button>
452
+ ))}
453
+ </div>
454
+ </section>
455
+ )}
426
456
  </div>
427
- </section>
428
- )}
429
- </div>
430
- <NodeInspector
431
- currentNode={currentNode}
432
- nodes={nodes}
433
- outgoingRelations={outgoingRelations}
434
- incomingRelations={incomingRelations}
435
- onSelectNode={onSelectNode}
436
- />
457
+ <NodeInspector
458
+ currentNode={currentNode}
459
+ nodes={nodes}
460
+ outgoingRelations={outgoingRelations}
461
+ incomingRelations={incomingRelations}
462
+ onSelectNode={onSelectNode}
463
+ />
464
+ </div>
437
465
  </div>
438
466
  </div>
439
- </div>
440
- </main>
467
+ </main>
441
468
  </FigureScopeProvider>
442
469
  </div>
443
470
  );
@@ -451,12 +478,21 @@ function HierarchyStrip({ ancestorPath, currentNode, childNodes, onSelectNode })
451
478
  <div className="hierarchy-strip-main">
452
479
  <span className="hierarchy-strip-label">当前位置</span>
453
480
  <div className="hierarchy-strip-path">
454
- {ancestorPath.slice(0, -1).map(node => (
455
- <button key={node.id} onClick={() => onSelectNode(node.id)} title={`返回 ${node.title}`}>
456
- <span>{node.level}</span>{node.title}<ChevronRight size={11} />
457
- </button>
458
- ))}
459
- <strong><span>{currentNode.level}</span>{currentNode.title}</strong>
481
+ {ancestorPath.slice(0, -1).map(node => (
482
+ <button
483
+ key={node.id}
484
+ onClick={() => onSelectNode(node.id)}
485
+ title={`返回 ${node.title}`}
486
+ >
487
+ <span>{node.level}</span>
488
+ {node.title}
489
+ <ChevronRight size={11} />
490
+ </button>
491
+ ))}
492
+ <strong>
493
+ <span>{currentNode.level}</span>
494
+ {currentNode.title}
495
+ </strong>
460
496
  </div>
461
497
  {parent && (
462
498
  <button className="hierarchy-parent-link" onClick={() => onSelectNode(parent.id)}>
@@ -464,14 +500,23 @@ function HierarchyStrip({ ancestorPath, currentNode, childNodes, onSelectNode })
464
500
  </button>
465
501
  )}
466
502
  </div>
467
- {childNodes.length > 0 && <div className="hierarchy-strip-children">
468
- <span className="hierarchy-strip-label">继续下钻</span>
469
- <div className="hierarchy-child-scroll">{childNodes.map(child => (
470
- <button key={child.id} onClick={() => onSelectNode(child.id)} title={`进入 ${child.title}`}>
471
- {child.title}<ChevronRight size={12} />
472
- </button>
473
- ))}</div>
474
- </div>}
503
+ {childNodes.length > 0 && (
504
+ <div className="hierarchy-strip-children">
505
+ <span className="hierarchy-strip-label">继续下钻</span>
506
+ <div className="hierarchy-child-scroll">
507
+ {childNodes.map(child => (
508
+ <button
509
+ key={child.id}
510
+ onClick={() => onSelectNode(child.id)}
511
+ title={`进入 ${child.title}`}
512
+ >
513
+ {child.title}
514
+ <ChevronRight size={12} />
515
+ </button>
516
+ ))}
517
+ </div>
518
+ </div>
519
+ )}
475
520
  </nav>
476
521
  );
477
522
  }
@@ -480,8 +525,14 @@ function NodeInspector({ currentNode, nodes, outgoingRelations, incomingRelation
480
525
  const relationCard = (rel, targetId, direction) => {
481
526
  const target = nodes.get(targetId);
482
527
  return (
483
- <button className="inline-relation" key={`${direction}-${rel.from}-${rel.to}-${rel.type}`} onClick={() => target && onSelectNode(target.id)}>
484
- <span className="inline-relation-type" style={{ color: rel.typeInfo?.color }}>{rel.typeLabel}</span>
528
+ <button
529
+ className="inline-relation"
530
+ key={`${direction}-${rel.from}-${rel.to}-${rel.type}`}
531
+ onClick={() => target && onSelectNode(target.id)}
532
+ >
533
+ <span className="inline-relation-type" style={{ color: rel.typeInfo?.color }}>
534
+ {rel.typeLabel}
535
+ </span>
485
536
  <span className="inline-relation-main">
486
537
  <b>{target ? target.title : targetId}</b>
487
538
  {direction === 'out' ? <CornerDownRight size={13} /> : <ArrowLeft size={13} />}
@@ -498,14 +549,20 @@ function NodeInspector({ currentNode, nodes, outgoingRelations, incomingRelation
498
549
  <span className="side-label">当前节点的延伸笔记</span>
499
550
  <h2>关联、边界与前置知识</h2>
500
551
  </div>
501
- <span className="inline-inspector-count">{outgoingRelations.length + incomingRelations.length} 条关系</span>
552
+ <span className="inline-inspector-count">
553
+ {outgoingRelations.length + incomingRelations.length} 条关系
554
+ </span>
502
555
  </div>
503
556
  <div className="inline-inspector-grid">
504
557
  {currentNode.prerequisites.length > 0 && (
505
558
  <div className="inline-note-block">
506
559
  <span className="insp-label">前置知识</span>
507
560
  <div className="inline-pill-flow">
508
- {currentNode.prerequisites.map((item, index) => <span className="insp-pill" key={index}>{item}</span>)}
561
+ {currentNode.prerequisites.map((item, index) => (
562
+ <span className="insp-pill" key={index}>
563
+ {item}
564
+ </span>
565
+ ))}
509
566
  </div>
510
567
  </div>
511
568
  )}
@@ -513,17 +570,28 @@ function NodeInspector({ currentNode, nodes, outgoingRelations, incomingRelation
513
570
  <div className="inline-note-block">
514
571
  <span className="insp-label">边界条件</span>
515
572
  {currentNode.boundaries.map((boundary, index) => (
516
- <div className="inline-boundary" key={index}><b>{boundary.title}</b><span>{boundary.content}</span></div>
573
+ <div className="inline-boundary" key={index}>
574
+ <b>{boundary.title}</b>
575
+ <span>{boundary.content}</span>
576
+ </div>
517
577
  ))}
518
578
  </div>
519
579
  )}
520
580
  <div className="inline-note-block relation-column">
521
581
  <span className="insp-label">延伸关系 · 出</span>
522
- {outgoingRelations.length > 0 ? outgoingRelations.map(rel => relationCard(rel, rel.to, 'out')) : <span className="empty-subtext">暂无向外关联</span>}
582
+ {outgoingRelations.length > 0 ? (
583
+ outgoingRelations.map(rel => relationCard(rel, rel.to, 'out'))
584
+ ) : (
585
+ <span className="empty-subtext">暂无向外关联</span>
586
+ )}
523
587
  </div>
524
588
  <div className="inline-note-block relation-column">
525
589
  <span className="insp-label">前驱关系 · 入</span>
526
- {incomingRelations.length > 0 ? incomingRelations.map(rel => relationCard(rel, rel.from, 'in')) : <span className="empty-subtext">暂无前驱来源</span>}
590
+ {incomingRelations.length > 0 ? (
591
+ incomingRelations.map(rel => relationCard(rel, rel.from, 'in'))
592
+ ) : (
593
+ <span className="empty-subtext">暂无前驱来源</span>
594
+ )}
527
595
  </div>
528
596
  {currentNode.glossary.length > 0 && (
529
597
  <div className="inline-note-block">
@@ -561,7 +629,10 @@ function renderNavTree(nodes, rootId, currentNodeId, onSelectNode) {
561
629
  style={{ paddingLeft: `${12 + depth * 14}px` }}
562
630
  onClick={() => onSelectNode(id)}
563
631
  >
564
- <span className="node-bullet" style={{ backgroundColor: LEVEL_DEFS[node.level]?.color || 'var(--level-l0)' }} />
632
+ <span
633
+ className="node-bullet"
634
+ style={{ backgroundColor: LEVEL_DEFS[node.level]?.color || 'var(--level-l0)' }}
635
+ />
565
636
  <span className="tree-title">{node.title}</span>
566
637
  <span className="tree-level">{node.level}</span>
567
638
  </button>