concept-atlas-dense-explain 0.10.0 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "concept-atlas-dense-explain",
3
- "version": "0.10.0",
3
+ "version": "1.0.0",
4
4
  "description": "Portable dense-explanation skill and MDX concept atlas template",
5
5
  "type": "module",
6
6
  "bin": {
@@ -100,6 +100,38 @@ npm run dev`}</CodeBlock>
100
100
  <RelationMap title="关系速览" items={[{from:'MDX',type:'produces',to:'Graph',note:'生成关系图'},{from:'Node',type:'uses',to:'Mermaid',note:'展示局部流程'}]} />
101
101
  </ScrollSection>
102
102
 
103
+ <ScrollSection title="附录:大图的框内适配与放大阅读">
104
+ <ScrollProse>较大的流程图会被等比收进框内,而不是画出边框。点击图面或标题栏的「放大阅读」可以打开全屏视图,用滚轮缩放、拖拽平移、Esc 关闭。</ScrollProse>
105
+ <Mermaid title="图谱生成管线" width="100%" height="360px" chart={`flowchart TD
106
+ S[MDX 源文件] --> P[解析与校验]
107
+ P --> G[概念图谱]
108
+ P --> F[图表与公式]
109
+ G --> D[节点探索]
110
+ G --> R[关系图谱]
111
+ D --> M[Mermaid 局部流程图]
112
+ R --> M
113
+ F --> M
114
+ M --> Z[框内自适应缩放]
115
+ Z --> O[点击放大阅读]
116
+ O --> W[拖拽平移]
117
+ O --> K[滚轮缩放]
118
+ O --> E[Esc 关闭]
119
+ style S fill:#172554,stroke:#38bdf8,color:#e0f2fe
120
+ style Z fill:#14532d,stroke:#34d399,color:#ecfdf5
121
+ style O fill:#78350f,stroke:#fbbf24,color:#fffbeb`} />
122
+ <ScrollGrid columns="2">
123
+ <Mermaid title="窄栏中的纵向长图" width="100%" height="220px" chart={`flowchart TD
124
+ A[读取] --> B[解析]
125
+ B --> C[建模]
126
+ C --> D[渲染]
127
+ D --> E[校验]
128
+ E --> F[发布]
129
+ F --> G[归档]
130
+ G --> H[复盘]`} />
131
+ <RelationPath title="阅读路径" steps={[{level:'L0',node:'总览',note:'建立直觉',tone:'info'},{level:'L1',node:'机制',note:'理解过程',tone:'success'},{level:'L2',node:'边界',note:'检查反例',tone:'warn'}]} />
132
+ </ScrollGrid>
133
+ </ScrollSection>
134
+
103
135
  <ScrollSection title="参考文献">
104
136
  <References title="本页引用" items={[{id:'tufte1983',authors:'Tufte, E. R.',year:'1983',title:'The Visual Display of Quantitative Information',source:'Graphics Press',note:'关于以图形压缩与呈现证据的经典论述。'}]} />
105
137
  </ScrollSection>
@@ -726,13 +726,48 @@ export function Tabs({ items = [], children }) {
726
726
  }
727
727
  Tabs.displayName = 'Tabs';
728
728
 
729
+ /**
730
+ * Enlarged reader for a diagram. It re-renders the chart under a fresh id so
731
+ * the full-size copy never collides with the inline SVG, then hands the result
732
+ * to the shared zoom overlay (wheel zoom, drag to pan, Esc to close).
733
+ */
734
+ function MermaidZoom({ chart, title, onClose }) {
735
+ const ref = React.useRef(null);
736
+ const appearanceKey = useAppearanceKey();
737
+ const [error, setError] = React.useState('');
738
+
739
+ React.useEffect(() => {
740
+ let cancelled = false;
741
+ async function renderZoom() {
742
+ if (!ref.current || !chart.trim()) return;
743
+ try {
744
+ configureMermaid();
745
+ const renderId = `mermaid-zoom-${Math.random().toString(36).slice(2, 10)}`;
746
+ const { svg } = await mermaid.render(renderId, chart.trim());
747
+ if (!cancelled && ref.current) ref.current.innerHTML = svg;
748
+ } catch (err) {
749
+ if (!cancelled) setError(err.message || 'Mermaid 图表语法错误');
750
+ }
751
+ }
752
+ renderZoom();
753
+ return () => { cancelled = true; };
754
+ }, [chart, appearanceKey]);
755
+
756
+ return (
757
+ <ZoomOverlay title={title || '关系草图'} caption={title} onClose={onClose}>
758
+ {error
759
+ ? <pre className="mermaid-error">{error}</pre>
760
+ : <div className="mermaid-zoom-canvas" ref={ref} />}
761
+ </ZoomOverlay>
762
+ );
763
+ }
764
+
729
765
  export function Mermaid({ chart = '', title = '关系草图', width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
730
766
  const ref = React.useRef(null);
731
767
  const id = React.useId().replace(/:/g, '');
732
768
  const appearanceKey = useAppearanceKey();
733
769
  const [error, setError] = React.useState('');
734
- const [scale, setScale] = React.useState(1);
735
- const [pan, setPan] = React.useState({ x: 0, y: 0 });
770
+ const [zoomed, setZoomed] = React.useState(false);
736
771
 
737
772
  React.useEffect(() => {
738
773
  let cancelled = false;
@@ -754,15 +789,41 @@ export function Mermaid({ chart = '', title = '关系草图', width = 'auto', he
754
789
  return () => { cancelled = true; };
755
790
  }, [chart, id, appearanceKey]);
756
791
 
757
- const zoom = (delta) => setScale(value => Math.max(0.5, Math.min(3, +(value + delta).toFixed(2))));
758
- const reset = () => { setScale(1); setPan({ x: 0, y: 0 }); };
792
+ const canZoom = Boolean(chart.trim()) && !error;
793
+ const openZoom = () => { if (canZoom) setZoomed(true); };
794
+ const onKeyDown = event => {
795
+ if (event.key === 'Enter' || event.key === ' ') {
796
+ event.preventDefault();
797
+ openZoom();
798
+ }
799
+ };
800
+
759
801
  return (
760
802
  <div className={`semantic-mermaid ${widgetClass(position)}`} style={widgetStyle({ width, height, x, y, position })}>
761
803
  <div className="semantic-widget-head">
762
804
  <span>{title}</span>
763
- <div className="mermaid-tools"><code>MERMAID</code><button type="button" onClick={() => zoom(-0.1)} aria-label="缩小图表">−</button><span>{Math.round(scale * 100)}%</span><button type="button" onClick={() => zoom(0.1)} aria-label="放大图表">+</button><button type="button" onClick={reset} aria-label="重置图表">↺</button></div>
805
+ <div className="mermaid-tools">
806
+ <code>MERMAID</code>
807
+ <button type="button" onClick={openZoom} disabled={!canZoom} aria-label={`放大阅读:${title}`}>
808
+ <ZoomIn size={12} />放大阅读
809
+ </button>
810
+ </div>
764
811
  </div>
765
- {error ? <pre className="mermaid-error">{error}</pre> : <div className="mermaid-canvas" onWheel={(event) => { event.preventDefault(); zoom(event.deltaY < 0 ? 0.1 : -0.1); }}><div className="mermaid-canvas-inner" style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})` }} ref={ref} /></div>}
812
+ {error ? (
813
+ <pre className="mermaid-error">{error}</pre>
814
+ ) : (
815
+ <div
816
+ className="mermaid-canvas"
817
+ role="button"
818
+ tabIndex={0}
819
+ aria-label={`放大阅读:${title}`}
820
+ onClick={openZoom}
821
+ onKeyDown={onKeyDown}
822
+ >
823
+ <div className="mermaid-canvas-inner" ref={ref} />
824
+ </div>
825
+ )}
826
+ {zoomed && <MermaidZoom chart={chart} title={title} onClose={() => setZoomed(false)} />}
766
827
  </div>
767
828
  );
768
829
  }
@@ -1047,12 +1108,12 @@ const ZOOM_MIN = 0.5;
1047
1108
  const ZOOM_MAX = 6;
1048
1109
 
1049
1110
  /**
1050
- * Full-viewport image viewer. Rendered through a portal so it escapes the
1051
- * transformed/rotated ancestor surfaces (`position: fixed` would otherwise be
1052
- * contained by them). Supports wheel zoom, drag to pan, double-click to toggle
1053
- * 1x/2x, and Escape to close.
1111
+ * Full-viewport zoom viewer shared by images and diagrams. Rendered through a
1112
+ * portal so it escapes the transformed/rotated ancestor surfaces (`position:
1113
+ * fixed` would otherwise be contained by them). Supports wheel zoom, drag to
1114
+ * pan, double-click to toggle 1x/2x, and Escape to close.
1054
1115
  */
1055
- function ImageZoom({ src, alt, caption, label, onClose }) {
1116
+ function ZoomOverlay({ title, caption, label, onClose, children }) {
1056
1117
  const [scale, setScale] = React.useState(1);
1057
1118
  const [offset, setOffset] = React.useState({ x: 0, y: 0 });
1058
1119
  const [dragging, setDragging] = React.useState(false);
@@ -1126,8 +1187,6 @@ function ImageZoom({ src, alt, caption, label, onClose }) {
1126
1187
  }
1127
1188
  };
1128
1189
 
1129
- const title = alt || caption || '图片';
1130
-
1131
1190
  return createPortal(
1132
1191
  <div
1133
1192
  className="image-zoom-overlay"
@@ -1146,14 +1205,11 @@ function ImageZoom({ src, alt, caption, label, onClose }) {
1146
1205
  onDoubleClick={() => { const next = scaleRef.current > 1 ? 1 : 2; setScale(next); settle(next); }}
1147
1206
  onClick={event => { if (movedRef.current) { event.stopPropagation(); movedRef.current = false; } }}
1148
1207
  >
1149
- <img
1150
- src={src}
1151
- alt={alt || ''}
1152
- draggable="false"
1153
- style={{ transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})` }}
1154
- />
1208
+ <div className="image-zoom-target" style={{ transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})` }}>
1209
+ {children}
1210
+ </div>
1155
1211
  </div>
1156
- {title && <p className="image-zoom-caption">{label && <b>{label}</b>}{caption || alt}</p>}
1212
+ {title && <p className="image-zoom-caption">{label && <b>{label}</b>}{caption}</p>}
1157
1213
  <div className="image-zoom-toolbar">
1158
1214
  <button type="button" onClick={() => zoomBy(-0.25)} aria-label="缩小" title="缩小(−)">−</button>
1159
1215
  <span aria-live="polite">{Math.round(scale * 100)}%</span>
@@ -1166,6 +1222,15 @@ function ImageZoom({ src, alt, caption, label, onClose }) {
1166
1222
  );
1167
1223
  }
1168
1224
 
1225
+ function ImageZoom({ src, alt, caption, label, onClose }) {
1226
+ const title = alt || caption || '图片';
1227
+ return (
1228
+ <ZoomOverlay title={title} caption={caption || alt} label={label} onClose={onClose}>
1229
+ <img src={src} alt={alt || ''} draggable="false" />
1230
+ </ZoomOverlay>
1231
+ );
1232
+ }
1233
+
1169
1234
  export function Figure({ src, alt = '', caption, label, width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
1170
1235
  const [zoomed, setZoomed] = React.useState(false);
1171
1236
  const title = alt || caption || '图片';
@@ -94,11 +94,45 @@ button:focus-visible, a:focus-visible, [role="button"]:focus-visible {
94
94
  .gap-sm { gap: 8px; } .gap-md { gap: 16px; } .gap-lg { gap: 28px; }
95
95
  .semantic-tabs details { border-bottom: 1px solid var(--border-subtle); padding: 8px 0; }
96
96
  .semantic-tabs summary { cursor: pointer; font-weight: 700; color: var(--text-primary); }
97
- .mermaid-tools { display: inline-flex; align-items: center; gap: 5px; }
98
- .mermaid-tools button { width: 22px; height: 22px; border: 1px solid var(--border-medium); border-radius: 4px; background: var(--bg-subtle); font-weight: 700; }
97
+ .mermaid-tools { display: inline-flex; align-items: center; gap: 6px; }
98
+ .mermaid-tools button {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ gap: 4px;
102
+ height: 22px;
103
+ padding: 0 8px;
104
+ border: 1px solid var(--border-medium);
105
+ border-radius: var(--radius-1);
106
+ background: var(--bg-subtle);
107
+ color: var(--text-secondary);
108
+ font-size: 11px;
109
+ font-weight: 700;
110
+ }
99
111
  .mermaid-tools button:hover { color: var(--primary); border-color: var(--primary); }
100
- .mermaid-canvas { overflow: hidden; min-height: 120px; display: grid; place-items: center; cursor: grab; }
101
- .mermaid-canvas-inner { display: inline-block; min-width: 100%; transform-origin: center center; transition: transform .15s ease; }
112
+ .mermaid-tools button:disabled { opacity: .5; cursor: not-allowed; }
113
+
114
+ /* The diagram frame is a bounded viewport, not a loose box: the head keeps its
115
+ own height and the canvas takes the rest, so a fixed `height` prop contains
116
+ the drawing instead of letting it paint past the border. The SVG is scaled to
117
+ fit with its own preserveAspectRatio; clicking opens the full-size viewer. */
118
+ .mermaid-canvas {
119
+ position: relative;
120
+ flex: 1 1 auto;
121
+ min-height: 0;
122
+ min-width: 0;
123
+ max-width: 100%;
124
+ overflow: hidden;
125
+ padding: 4px;
126
+ cursor: zoom-in;
127
+ }
128
+ .mermaid-canvas-inner {
129
+ display: grid;
130
+ place-items: center;
131
+ width: 100%;
132
+ height: 100%;
133
+ transform-origin: center center;
134
+ }
135
+ .mermaid-canvas svg { display: block; width: 100%; height: 100%; max-width: 100%; max-height: 100%; }
102
136
 
103
137
  /* Scrollbar beautification */
104
138
  ::-webkit-scrollbar {
@@ -2070,6 +2104,16 @@ button.flow-box {
2070
2104
  min-width: 0;
2071
2105
  }
2072
2106
 
2107
+ /* The diagram frame clips and hands its remaining height to the canvas, so a
2108
+ fixed `height` prop can never let the drawing escape the border. */
2109
+ .semantic-mermaid {
2110
+ display: flex;
2111
+ flex-direction: column;
2112
+ overflow: hidden;
2113
+ }
2114
+
2115
+ .semantic-mermaid > .semantic-widget-head { flex: 0 0 auto; }
2116
+
2073
2117
  .draft-board .custom-section {
2074
2118
  padding: 12px 14px;
2075
2119
  }
@@ -2130,9 +2174,10 @@ button.flow-box {
2130
2174
  .draft-board .semantic-relation-map {
2131
2175
  width: fit-content;
2132
2176
  max-width: 100%;
2133
- overflow: visible;
2134
2177
  }
2135
2178
 
2179
+ .draft-board .semantic-relation-map { overflow: visible; }
2180
+
2136
2181
  .draft-board .semantic-insight,
2137
2182
  .draft-board .semantic-note-grid {
2138
2183
  width: fit-content;
@@ -2156,22 +2201,18 @@ button.flow-box {
2156
2201
  font-size: 9px;
2157
2202
  }
2158
2203
 
2159
- .mermaid-canvas {
2160
- min-height: 100px;
2161
- overflow: auto;
2162
- padding: 4px;
2163
- max-width: 100%;
2164
- min-width: 0;
2165
- }
2166
-
2167
- .mermaid-canvas svg {
2168
- max-width: none;
2169
- min-width: 100%;
2170
- height: auto;
2204
+ /* The inline diagram is a preview: clicking it, or the head button, opens the
2205
+ full-size viewer. The cursor is the only affordance on the drawing itself, so
2206
+ nothing competes with the diagram for attention. */
2207
+ .mermaid-canvas:focus-visible {
2208
+ outline: 2px solid var(--accent-cyan);
2209
+ outline-offset: 2px;
2171
2210
  }
2172
2211
 
2173
2212
  .mermaid-error {
2174
2213
  margin: 0;
2214
+ min-height: 0;
2215
+ overflow: auto;
2175
2216
  color: var(--accent-rose);
2176
2217
  font-size: 11px;
2177
2218
  white-space: pre-wrap;
@@ -3760,6 +3801,10 @@ html[data-carrier='scroll'] #root {
3760
3801
  place-items: center;
3761
3802
  padding: 28px;
3762
3803
  background: var(--overlay-scrim);
3804
+ /* Clip at the viewport, not at the media box: the zoom transform does not
3805
+ change layout size, so a stage-sized clip would only ever reveal what the
3806
+ un-zoomed element already covered. */
3807
+ overflow: hidden;
3763
3808
  }
3764
3809
 
3765
3810
  .image-zoom-stage {
@@ -3767,13 +3812,22 @@ html[data-carrier='scroll'] #root {
3767
3812
  place-items: center;
3768
3813
  max-width: 100%;
3769
3814
  max-height: 100%;
3770
- overflow: hidden;
3815
+ overflow: visible;
3771
3816
  cursor: grab;
3772
3817
  touch-action: none;
3773
3818
  }
3774
3819
 
3775
3820
  .image-zoom-stage.is-dragging { cursor: grabbing; }
3776
3821
 
3822
+ /* The pan/zoom transform rides a wrapper so the viewer can hold an <img> or a
3823
+ rendered diagram without duplicating the gesture logic. */
3824
+ .image-zoom-target {
3825
+ display: block;
3826
+ transition: transform 0.12s ease-out;
3827
+ will-change: transform;
3828
+ user-select: none;
3829
+ }
3830
+
3777
3831
  .image-zoom-stage img {
3778
3832
  display: block;
3779
3833
  width: auto;
@@ -3783,12 +3837,22 @@ html[data-carrier='scroll'] #root {
3783
3837
  border-radius: var(--radius-2);
3784
3838
  background: var(--bg-surface);
3785
3839
  box-shadow: var(--shadow-modal);
3786
- transition: transform 0.12s ease-out;
3787
- will-change: transform;
3788
3840
  user-select: none;
3789
3841
  }
3790
3842
 
3791
- .image-zoom-stage.is-dragging img { transition: none; }
3843
+ .image-zoom-stage.is-dragging .image-zoom-target { transition: none; }
3844
+
3845
+ /* A diagram keeps its natural aspect inside the viewer and paints on the
3846
+ Mermaid canvas tone, so labels stay legible over the dark scrim. */
3847
+ .mermaid-zoom-canvas svg {
3848
+ display: block;
3849
+ width: min(94vw, 1800px);
3850
+ height: auto;
3851
+ max-height: 84vh;
3852
+ border-radius: var(--radius-3);
3853
+ background: var(--mermaid-canvas);
3854
+ box-shadow: var(--shadow-modal);
3855
+ }
3792
3856
 
3793
3857
  .image-zoom-caption {
3794
3858
  position: absolute;
@@ -3847,14 +3911,14 @@ html[data-carrier='scroll'] #root {
3847
3911
  }
3848
3912
 
3849
3913
  @media (prefers-reduced-motion: reduce) {
3850
- .image-zoom-stage img { transition: none; }
3914
+ .image-zoom-target { transition: none; }
3851
3915
  }
3852
3916
 
3853
3917
  @media (max-width: 760px) {
3854
3918
  .image-zoom-overlay { padding: 12px; }
3855
3919
  .image-zoom-stage img { max-height: 74vh; }
3920
+ .mermaid-zoom-canvas svg { max-height: 74vh; }
3856
3921
  .image-zoom-caption { top: 10px; font-size: 12px; }
3857
- .image-zoom-hint { opacity: 0.9; }
3858
3922
  }
3859
3923
 
3860
3924
  .semantic-figure figcaption {