concept-atlas-dense-explain 0.9.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.9.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>
@@ -33,7 +33,7 @@
33
33
  <!-- Google Fonts for Academic / Editorial Knowledge style -->
34
34
  <link rel="preconnect" href="https://fonts.googleapis.com">
35
35
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
36
- <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Lora:ital,wght@0,500;0,600;0,700;1,400&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Noto+Serif+SC:wght@500;600;700;900&display=swap" rel="stylesheet">
36
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Lora:ital,wght@0,500;0,600;0,700;1,400&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Noto+Serif+SC:wght@500;600;700;900&display=swap" rel="stylesheet">
37
37
  </head>
38
38
  <body>
39
39
  <div id="root"></div>
@@ -173,8 +173,13 @@ Mechanism.displayName = 'Mechanism';
173
173
  export function Implementation({ language = 'text', title = '实现代码', children }) {
174
174
  return (
175
175
  <div className="semantic-implementation" data-language={language}>
176
- {title && <div className="impl-header">{title} <span className="lang-badge">{language}</span></div>}
177
- <pre><code>{typeof children === 'string' ? children.trim() : children}</code></pre>
176
+ {title && (
177
+ <div className="semantic-code-head impl-header">
178
+ <span className="semantic-code-title">{title}</span>
179
+ {language && <span className="lang-badge">{language}</span>}
180
+ </div>
181
+ )}
182
+ <pre className="code-block"><code>{typeof children === 'string' ? children.trim() : children}</code></pre>
178
183
  </div>
179
184
  );
180
185
  }
@@ -721,13 +726,48 @@ export function Tabs({ items = [], children }) {
721
726
  }
722
727
  Tabs.displayName = 'Tabs';
723
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
+
724
765
  export function Mermaid({ chart = '', title = '关系草图', width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
725
766
  const ref = React.useRef(null);
726
767
  const id = React.useId().replace(/:/g, '');
727
768
  const appearanceKey = useAppearanceKey();
728
769
  const [error, setError] = React.useState('');
729
- const [scale, setScale] = React.useState(1);
730
- const [pan, setPan] = React.useState({ x: 0, y: 0 });
770
+ const [zoomed, setZoomed] = React.useState(false);
731
771
 
732
772
  React.useEffect(() => {
733
773
  let cancelled = false;
@@ -749,15 +789,41 @@ export function Mermaid({ chart = '', title = '关系草图', width = 'auto', he
749
789
  return () => { cancelled = true; };
750
790
  }, [chart, id, appearanceKey]);
751
791
 
752
- const zoom = (delta) => setScale(value => Math.max(0.5, Math.min(3, +(value + delta).toFixed(2))));
753
- 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
+
754
801
  return (
755
802
  <div className={`semantic-mermaid ${widgetClass(position)}`} style={widgetStyle({ width, height, x, y, position })}>
756
803
  <div className="semantic-widget-head">
757
804
  <span>{title}</span>
758
- <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>
759
811
  </div>
760
- {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)} />}
761
827
  </div>
762
828
  );
763
829
  }
@@ -1042,12 +1108,12 @@ const ZOOM_MIN = 0.5;
1042
1108
  const ZOOM_MAX = 6;
1043
1109
 
1044
1110
  /**
1045
- * Full-viewport image viewer. Rendered through a portal so it escapes the
1046
- * transformed/rotated ancestor surfaces (`position: fixed` would otherwise be
1047
- * contained by them). Supports wheel zoom, drag to pan, double-click to toggle
1048
- * 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.
1049
1115
  */
1050
- function ImageZoom({ src, alt, caption, label, onClose }) {
1116
+ function ZoomOverlay({ title, caption, label, onClose, children }) {
1051
1117
  const [scale, setScale] = React.useState(1);
1052
1118
  const [offset, setOffset] = React.useState({ x: 0, y: 0 });
1053
1119
  const [dragging, setDragging] = React.useState(false);
@@ -1121,8 +1187,6 @@ function ImageZoom({ src, alt, caption, label, onClose }) {
1121
1187
  }
1122
1188
  };
1123
1189
 
1124
- const title = alt || caption || '图片';
1125
-
1126
1190
  return createPortal(
1127
1191
  <div
1128
1192
  className="image-zoom-overlay"
@@ -1141,14 +1205,11 @@ function ImageZoom({ src, alt, caption, label, onClose }) {
1141
1205
  onDoubleClick={() => { const next = scaleRef.current > 1 ? 1 : 2; setScale(next); settle(next); }}
1142
1206
  onClick={event => { if (movedRef.current) { event.stopPropagation(); movedRef.current = false; } }}
1143
1207
  >
1144
- <img
1145
- src={src}
1146
- alt={alt || ''}
1147
- draggable="false"
1148
- style={{ transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})` }}
1149
- />
1208
+ <div className="image-zoom-target" style={{ transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})` }}>
1209
+ {children}
1210
+ </div>
1150
1211
  </div>
1151
- {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>}
1152
1213
  <div className="image-zoom-toolbar">
1153
1214
  <button type="button" onClick={() => zoomBy(-0.25)} aria-label="缩小" title="缩小(−)">−</button>
1154
1215
  <span aria-live="polite">{Math.round(scale * 100)}%</span>
@@ -1161,6 +1222,15 @@ function ImageZoom({ src, alt, caption, label, onClose }) {
1161
1222
  );
1162
1223
  }
1163
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
+
1164
1234
  export function Figure({ src, alt = '', caption, label, width = 'auto', height = 'auto', x = 0, y = 0, position = 'flow' }) {
1165
1235
  const [zoomed, setZoomed] = React.useState(false);
1166
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 {
@@ -1026,26 +1060,37 @@ button.flow-box {
1026
1060
  border-radius: 10px;
1027
1061
  overflow-x: auto;
1028
1062
  font-family: var(--font-data);
1029
- font-size: 13px;
1030
- line-height: 1.6;
1063
+ /* A comfortable, taller setting for a mono face; ligatures stay off so
1064
+ operators render exactly as they were typed. */
1065
+ font-size: 14px;
1066
+ line-height: 1.7;
1067
+ font-variant-ligatures: none;
1068
+ font-feature-settings: 'liga' 0, 'calt' 0;
1031
1069
  color: var(--code-text);
1032
1070
  border: 1px solid var(--code-border);
1033
1071
  box-shadow: var(--shadow-inset);
1034
1072
  }
1035
1073
 
1036
- /* Generic CodeBlock. It reuses `.code-block` so every skin and style pack
1037
- keeps one code surface, and only adds an optional title bar, wrap and
1038
- line numbers on top. */
1039
- .semantic-code {
1040
- margin: 0;
1074
+ /* -- Code family: `.code-block` is the one shared dark inset used by the
1075
+ explorer, `Implementation` and `CodeBlock`. Both components wrap it with
1076
+ an optional head, caption and numbered lines, and they share the head +
1077
+ surface classes so every style pack only has to re-voice one grammar. -- */
1078
+ .semantic-code,
1079
+ .semantic-implementation {
1041
1080
  display: flex;
1042
1081
  flex-direction: column;
1043
1082
  gap: 8px;
1083
+ margin: 0;
1084
+ }
1085
+
1086
+ .semantic-code > pre,
1087
+ .semantic-implementation > pre {
1088
+ margin: 0;
1044
1089
  }
1045
1090
 
1046
1091
  .semantic-code-head {
1047
1092
  display: flex;
1048
- align-items: center;
1093
+ align-items: baseline;
1049
1094
  justify-content: space-between;
1050
1095
  gap: 10px;
1051
1096
  }
@@ -1057,6 +1102,17 @@ button.flow-box {
1057
1102
  color: var(--text-secondary);
1058
1103
  }
1059
1104
 
1105
+ /* The language tag rides every code head; a pack may dress it as a badge. */
1106
+ .lang-badge {
1107
+ font-family: var(--font-data);
1108
+ font-size: 10px;
1109
+ font-weight: 700;
1110
+ letter-spacing: 0.08em;
1111
+ text-transform: uppercase;
1112
+ color: var(--text-muted);
1113
+ white-space: nowrap;
1114
+ }
1115
+
1060
1116
  .semantic-code-caption {
1061
1117
  font-size: 12px;
1062
1118
  color: var(--text-muted);
@@ -1078,18 +1134,22 @@ button.flow-box {
1078
1134
  .code-block-numbered .code-line {
1079
1135
  position: relative;
1080
1136
  display: block;
1081
- min-height: 1.6em;
1082
- padding-left: 3em;
1137
+ min-height: 1.7em;
1138
+ padding-left: 3.6em;
1083
1139
  counter-increment: code-line;
1084
1140
  }
1085
1141
 
1086
1142
  .code-block-numbered .code-line::before {
1087
1143
  content: counter(code-line);
1088
1144
  position: absolute;
1145
+ top: 0;
1146
+ bottom: 0;
1089
1147
  left: 0;
1090
- width: 2em;
1148
+ width: 2.5em;
1149
+ padding-right: 0.85em;
1091
1150
  text-align: right;
1092
- color: rgba(148, 163, 184, 0.55);
1151
+ color: color-mix(in srgb, var(--code-text) 50%, transparent);
1152
+ border-right: 1px solid color-mix(in srgb, var(--code-text) 18%, transparent);
1093
1153
  user-select: none;
1094
1154
  }
1095
1155
 
@@ -2044,6 +2104,16 @@ button.flow-box {
2044
2104
  min-width: 0;
2045
2105
  }
2046
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
+
2047
2117
  .draft-board .custom-section {
2048
2118
  padding: 12px 14px;
2049
2119
  }
@@ -2093,10 +2163,10 @@ button.flow-box {
2093
2163
  .relation-path-node.tone-warning { border-top-color: var(--accent-amber); }
2094
2164
  .relation-path-node.tone-danger { border-top-color: var(--accent-rose); }
2095
2165
  .relation-path-kicker, .relation-path-node strong, .relation-path-node small { display: block; }
2096
- .relation-path-kicker { color: var(--text-dim); font: 700 10px 'Fira Code', monospace; margin-bottom: 4px; }
2166
+ .relation-path-kicker { color: var(--text-dim); font: 700 10px var(--font-data); margin-bottom: 4px; }
2097
2167
  .relation-path-node strong { color: var(--text-primary); font-size: 13px; }
2098
2168
  .relation-path-node small { margin-top: 4px; color: var(--text-muted); font-size: 11px; }
2099
- .relation-path-edge { flex: 0 0 72px; display: grid; place-items: center; position: relative; color: var(--text-accent); font: 700 10px 'Fira Code', monospace; }
2169
+ .relation-path-edge { flex: 0 0 72px; display: grid; place-items: center; position: relative; color: var(--text-accent); font: 700 10px var(--font-data); }
2100
2170
  .relation-path-edge::before { content: ''; position: absolute; left: 4px; right: 4px; top: 50%; border-top: 1px dashed var(--border-strong); }
2101
2171
  .relation-path-edge span { z-index: 1; padding: 2px 4px; background: var(--bg-surface); }
2102
2172
 
@@ -2104,9 +2174,10 @@ button.flow-box {
2104
2174
  .draft-board .semantic-relation-map {
2105
2175
  width: fit-content;
2106
2176
  max-width: 100%;
2107
- overflow: visible;
2108
2177
  }
2109
2178
 
2179
+ .draft-board .semantic-relation-map { overflow: visible; }
2180
+
2110
2181
  .draft-board .semantic-insight,
2111
2182
  .draft-board .semantic-note-grid {
2112
2183
  width: fit-content;
@@ -2130,22 +2201,18 @@ button.flow-box {
2130
2201
  font-size: 9px;
2131
2202
  }
2132
2203
 
2133
- .mermaid-canvas {
2134
- min-height: 100px;
2135
- overflow: auto;
2136
- padding: 4px;
2137
- max-width: 100%;
2138
- min-width: 0;
2139
- }
2140
-
2141
- .mermaid-canvas svg {
2142
- max-width: none;
2143
- min-width: 100%;
2144
- 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;
2145
2210
  }
2146
2211
 
2147
2212
  .mermaid-error {
2148
2213
  margin: 0;
2214
+ min-height: 0;
2215
+ overflow: auto;
2149
2216
  color: var(--accent-rose);
2150
2217
  font-size: 11px;
2151
2218
  white-space: pre-wrap;
@@ -3439,7 +3506,8 @@ html[data-carrier='scroll'] #root {
3439
3506
  .continuous-document .model-matrix-cell span,
3440
3507
  .continuous-document .model-pyramid-level span,
3441
3508
  .continuous-document .model-funnel-step span,
3442
- .continuous-document .tradeoff-option span {
3509
+ .continuous-document .tradeoff-option span,
3510
+ .continuous-document .code-block {
3443
3511
  font-size: calc(14px * var(--reading-ui-scale));
3444
3512
  }
3445
3513
 
@@ -3733,6 +3801,10 @@ html[data-carrier='scroll'] #root {
3733
3801
  place-items: center;
3734
3802
  padding: 28px;
3735
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;
3736
3808
  }
3737
3809
 
3738
3810
  .image-zoom-stage {
@@ -3740,13 +3812,22 @@ html[data-carrier='scroll'] #root {
3740
3812
  place-items: center;
3741
3813
  max-width: 100%;
3742
3814
  max-height: 100%;
3743
- overflow: hidden;
3815
+ overflow: visible;
3744
3816
  cursor: grab;
3745
3817
  touch-action: none;
3746
3818
  }
3747
3819
 
3748
3820
  .image-zoom-stage.is-dragging { cursor: grabbing; }
3749
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
+
3750
3831
  .image-zoom-stage img {
3751
3832
  display: block;
3752
3833
  width: auto;
@@ -3756,12 +3837,22 @@ html[data-carrier='scroll'] #root {
3756
3837
  border-radius: var(--radius-2);
3757
3838
  background: var(--bg-surface);
3758
3839
  box-shadow: var(--shadow-modal);
3759
- transition: transform 0.12s ease-out;
3760
- will-change: transform;
3761
3840
  user-select: none;
3762
3841
  }
3763
3842
 
3764
- .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
+ }
3765
3856
 
3766
3857
  .image-zoom-caption {
3767
3858
  position: absolute;
@@ -3820,14 +3911,14 @@ html[data-carrier='scroll'] #root {
3820
3911
  }
3821
3912
 
3822
3913
  @media (prefers-reduced-motion: reduce) {
3823
- .image-zoom-stage img { transition: none; }
3914
+ .image-zoom-target { transition: none; }
3824
3915
  }
3825
3916
 
3826
3917
  @media (max-width: 760px) {
3827
3918
  .image-zoom-overlay { padding: 12px; }
3828
3919
  .image-zoom-stage img { max-height: 74vh; }
3920
+ .mermaid-zoom-canvas svg { max-height: 74vh; }
3829
3921
  .image-zoom-caption { top: 10px; font-size: 12px; }
3830
- .image-zoom-hint { opacity: 0.9; }
3831
3922
  }
3832
3923
 
3833
3924
  .semantic-figure figcaption {
@@ -515,3 +515,60 @@
515
515
  [data-style='elastic'] .semantic-compare-table tbody tr {
516
516
  transition: background-color var(--elastic-motion);
517
517
  }
518
+
519
+ /* -- Code as a panel: a 6px bordered surface under a subdued header strip,
520
+ the well set flush inside and inset like a terminal. The whole panel
521
+ answers on the pack's 250ms curve with the same 1px lift as every other
522
+ elastic surface, and the caption closes as a dim mono footer. -- */
523
+ [data-style='elastic'] .semantic-code,
524
+ [data-style='elastic'] .semantic-implementation {
525
+ gap: 0;
526
+ border: 1px solid var(--border-medium);
527
+ border-radius: var(--radius-2);
528
+ background: var(--bg-surface);
529
+ box-shadow: var(--shadow-card);
530
+ overflow: hidden;
531
+ transition: border-color var(--elastic-motion), transform var(--elastic-motion);
532
+ }
533
+
534
+ [data-style='elastic'] .semantic-code:hover,
535
+ [data-style='elastic'] .semantic-implementation:hover {
536
+ border-color: var(--border-strong);
537
+ transform: translateY(-1px);
538
+ }
539
+
540
+ [data-style='elastic'] .semantic-code-head {
541
+ padding: 9px 12px;
542
+ background: var(--bg-subtle);
543
+ border-bottom: 1px solid var(--border-subtle);
544
+ }
545
+
546
+ [data-style='elastic'] .semantic-code-title {
547
+ font-family: var(--font-sans);
548
+ font-size: 13px;
549
+ font-weight: 600;
550
+ letter-spacing: -0.01em;
551
+ color: var(--text-primary);
552
+ }
553
+
554
+ [data-style='elastic'] .lang-badge {
555
+ padding: 2px 6px;
556
+ border: 1px solid var(--border-medium);
557
+ border-radius: var(--radius-1);
558
+ background: var(--bg-surface);
559
+ color: var(--text-secondary);
560
+ letter-spacing: 0.03em;
561
+ }
562
+
563
+ [data-style='elastic'] .semantic-code > .code-block,
564
+ [data-style='elastic'] .semantic-implementation > .code-block {
565
+ border: 0;
566
+ border-radius: 0;
567
+ box-shadow: var(--shadow-inset);
568
+ }
569
+
570
+ [data-style='elastic'] .semantic-code-caption {
571
+ padding: 8px 12px 10px;
572
+ border-top: 1px solid var(--border-subtle);
573
+ font-family: var(--font-sans);
574
+ }
@@ -424,3 +424,41 @@
424
424
  border-color: var(--primary);
425
425
  color: var(--primary);
426
426
  }
427
+
428
+ /* -- Listings: the code well keeps its ink, but the frame drops to a
429
+ near-square ruled inset under a heavier top rule. The head reads as a
430
+ printed listing caption in the heading face, the language tag as a quiet
431
+ mono folio, and the caption closes as an italic printer's note. Paper
432
+ answers the pointer through rule weight only — no nudging. -- */
433
+ [data-style='manuscript'] .code-block {
434
+ padding: 16px 18px;
435
+ border-radius: 2px;
436
+ border-top: 2px solid color-mix(in srgb, var(--code-text) 42%, transparent);
437
+ box-shadow: none;
438
+ transition: border-top-color var(--motion-base) var(--ease-standard);
439
+ }
440
+
441
+ [data-style='manuscript'] .code-block:hover {
442
+ border-top-color: color-mix(in srgb, var(--code-text) 62%, transparent);
443
+ }
444
+
445
+ [data-style='manuscript'] .semantic-code-title {
446
+ font-family: var(--font-heading);
447
+ font-size: 14px;
448
+ font-weight: 600;
449
+ color: var(--text-primary);
450
+ }
451
+
452
+ [data-style='manuscript'] .lang-badge {
453
+ color: var(--text-dim);
454
+ letter-spacing: 0.12em;
455
+ }
456
+
457
+ [data-style='manuscript'] .semantic-code-caption {
458
+ font-style: italic;
459
+ }
460
+
461
+ [data-style='manuscript'] .code-block-numbered .code-line::before {
462
+ color: color-mix(in srgb, var(--code-text) 40%, transparent);
463
+ border-right-color: color-mix(in srgb, var(--code-text) 16%, transparent);
464
+ }
@@ -515,3 +515,57 @@
515
515
  [data-style='shadcn'] .semantic-compare-table tbody tr {
516
516
  transition: background-color var(--shadcn-motion);
517
517
  }
518
+
519
+ /* -- Code as a card: hairline chassis, a muted divided header and the well set
520
+ flush inside it, closed by a caption footer. The language tag is an
521
+ outline badge, and the surface only answers on the shared 150ms curve. -- */
522
+ [data-style='shadcn'] .semantic-code,
523
+ [data-style='shadcn'] .semantic-implementation {
524
+ gap: 0;
525
+ border: 1px solid var(--border-subtle);
526
+ border-radius: var(--radius-3);
527
+ background: var(--bg-surface);
528
+ box-shadow: var(--shadow-sm);
529
+ overflow: hidden;
530
+ transition: border-color var(--shadcn-motion);
531
+ }
532
+
533
+ [data-style='shadcn'] .semantic-code:hover,
534
+ [data-style='shadcn'] .semantic-implementation:hover {
535
+ border-color: var(--border-medium);
536
+ }
537
+
538
+ [data-style='shadcn'] .semantic-code-head {
539
+ padding: 10px 14px;
540
+ background: var(--bg-subtle);
541
+ border-bottom: 1px solid var(--border-subtle);
542
+ }
543
+
544
+ [data-style='shadcn'] .semantic-code-title {
545
+ font-family: var(--font-sans);
546
+ font-size: 13px;
547
+ font-weight: 600;
548
+ letter-spacing: -0.01em;
549
+ color: var(--text-primary);
550
+ }
551
+
552
+ [data-style='shadcn'] .lang-badge {
553
+ padding: 2px 7px;
554
+ border: 1px solid var(--border-subtle);
555
+ border-radius: var(--radius-1);
556
+ background: var(--bg-surface);
557
+ color: var(--text-muted);
558
+ letter-spacing: 0.04em;
559
+ }
560
+
561
+ [data-style='shadcn'] .semantic-code > .code-block,
562
+ [data-style='shadcn'] .semantic-implementation > .code-block {
563
+ border: 0;
564
+ border-radius: 0;
565
+ box-shadow: none;
566
+ }
567
+
568
+ [data-style='shadcn'] .semantic-code-caption {
569
+ padding: 8px 14px 10px;
570
+ border-top: 1px solid var(--border-subtle);
571
+ }
@@ -262,7 +262,7 @@
262
262
  /* Family stacks */
263
263
  --font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", sans-serif;
264
264
  --font-serif: 'Lora', 'Noto Serif SC', Georgia, serif;
265
- --font-mono: 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
265
+ --font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
266
266
 
267
267
  /* Roles — the only families component styles should consume */
268
268
  --font-body: var(--font-sans);