concept-atlas-dense-explain 0.6.1 → 0.7.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/bin/cli.mjs CHANGED
@@ -14,7 +14,7 @@ let buildCounter = 0;
14
14
 
15
15
  function usage() {
16
16
  console.log('Usage:');
17
- console.log(' npx concept-atlas-dense-explain <input.mdx>... [--mode atlas|scroll] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [-o output.html|dir] [--force] [--concurrency N] [--link-assets] [--json] [--no-validate]');
17
+ console.log(' npx concept-atlas-dense-explain <input.mdx>... [--mode atlas|scroll] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [-o output.html|dir] [--force] [--concurrency N] [--link-assets] [--inline-mermaid] [--mermaid-cdn <url>] [--json] [--no-validate]');
18
18
  console.log(' npx concept-atlas-dense-explain render <input.mdx>... [-o output.html|dir]');
19
19
  console.log(' npx concept-atlas-dense-explain validate <input.mdx> [--mode atlas|scroll] [--strict] [--json]');
20
20
  console.log(' npx concept-atlas-dense-explain create <output.mdx> [--mode atlas|scroll] [--force]');
@@ -22,6 +22,7 @@ function usage() {
22
22
  console.log('');
23
23
  console.log(' Multiple inputs build in parallel (default 2 at a time, cap 4); -o is then a directory.');
24
24
  console.log(' --link-assets keeps figures as relative links instead of inlining them as base64.');
25
+ console.log(' Mermaid diagrams load from a CDN at runtime by default (fast builds, needs network); --inline-mermaid bakes Mermaid into the HTML for a fully offline single file; --mermaid-cdn overrides the CDN URL.');
25
26
  console.log(` --skin bakes a default palette (${SKINS.map(skin => skin.id).join(', ')}); --default-mode bakes a default dark/light mode; --style bakes a default component style (${COMPONENT_STYLES.map(style => style.id).join(', ')}). Readers can still switch in the UI.`);
26
27
  }
27
28
 
@@ -43,7 +44,7 @@ function fail(message) {
43
44
  process.exit(1);
44
45
  }
45
46
 
46
- const VALUE_FLAGS = new Set(['--mode', '-o', '--output', '--concurrency', '--skin', '--default-mode', '--style']);
47
+ const VALUE_FLAGS = new Set(['--mode', '-o', '--output', '--concurrency', '--skin', '--default-mode', '--style', '--mermaid-cdn']);
47
48
 
48
49
  /** Splits argv into flags, flag values and positional arguments. */
49
50
  function parseFlags(argv) {
@@ -203,6 +204,8 @@ const strict = parsed.flags.has('--strict');
203
204
  const skipValidate = parsed.flags.has('--no-validate');
204
205
  const force = parsed.flags.has('--force');
205
206
  const linkAssets = parsed.flags.has('--link-assets');
207
+ const inlineMermaid = parsed.flags.has('--inline-mermaid');
208
+ const mermaidCdn = parsed.values.get('--mermaid-cdn') || null;
206
209
  const modeFlag = parsed.values.get('--mode') || null;
207
210
 
208
211
  // Compile-time appearance defaults. Invalid values fail fast with the valid
@@ -332,6 +335,10 @@ async function buildOne(job) {
332
335
  if (skinFlag) define.__ATLAS_DEFAULT_SKIN__ = JSON.stringify(skinFlag);
333
336
  if (defaultModeFlag) define.__ATLAS_DEFAULT_MODE__ = JSON.stringify(defaultModeFlag);
334
337
  if (styleFlag) define.__ATLAS_DEFAULT_STYLE__ = JSON.stringify(styleFlag);
338
+ if (features.mermaid) {
339
+ define.__ATLAS_MERMAID_MODE__ = JSON.stringify(inlineMermaid ? 'inline' : 'cdn');
340
+ if (mermaidCdn) define.__ATLAS_MERMAID_CDN_URL__ = JSON.stringify(mermaidCdn);
341
+ }
335
342
  try {
336
343
  await build({
337
344
  root: templateRoot,
@@ -357,9 +364,10 @@ async function buildOne(job) {
357
364
 
358
365
  /** Saves the mermaid/KaTeX payload when a document never renders them. */
359
366
  function describeFeatures(features) {
360
- if (features.math && features.mermaid) return '';
361
- const dropped = [features.math ? null : 'KaTeX', features.mermaid ? null : 'Mermaid'].filter(Boolean);
362
- return ` [no ${dropped.join('/')}]`;
367
+ const tags = [];
368
+ if (features.mermaid) tags.push(inlineMermaid ? 'mermaid inline' : 'mermaid cdn');
369
+ if (features.math) tags.push('KaTeX');
370
+ return tags.length ? ` [${tags.join(', ')}]` : '';
363
371
  }
364
372
 
365
373
  function summarizeFeatures(jobs, results) {
@@ -368,7 +376,9 @@ function summarizeFeatures(jobs, results) {
368
376
  if (!pages.length) return '';
369
377
  const droppedMath = pages.filter(job => !job.features.math).length;
370
378
  const droppedMermaid = pages.filter(job => !job.features.mermaid).length;
379
+ const cdnMermaid = pages.filter(job => job.features.mermaid && !inlineMermaid).length;
371
380
  const parts = [];
381
+ if (cdnMermaid) parts.push(`Mermaid via CDN on ${cdnMermaid}/${pages.length}`);
372
382
  if (droppedMath) parts.push(`KaTeX dropped on ${droppedMath}/${pages.length}`);
373
383
  if (droppedMermaid) parts.push(`Mermaid dropped on ${droppedMermaid}/${pages.length}`);
374
384
  return parts.join(', ');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "concept-atlas-dense-explain",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Portable dense-explanation skill and MDX concept atlas template",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -26,9 +26,9 @@ If the user only wants the prompt/methodology and not files, still choose a shel
26
26
  npx concept-atlas-dense-explain validate <file>.mdx --json
27
27
  ```
28
28
  Every diagnostic is `CODE line:column message`. Fix all `error`s and re-run; warnings are quality signals you should also address when cheap.
29
- 6. Compile: `npx concept-atlas-dense-explain <file>.mdx --mode atlas|scroll [-o out.html] [--skin <id>] [--default-mode dark|light|system] [--style <id>]`. Output is a standalone HTML beside the MDX unless `-o` is given. Validation errors abort the build; use `--no-validate` only to force a knowingly broken build.
30
- 7. **Appearance (optional)**: pages ship with a reader-facing appearance menu — palette (`aurora` cool blue, `ember` warm gold), a dark/light toggle, and a component style pack (`manuscript` editorial marginalia, `classic` boxed cards). The shipped default is aurora × manuscript × light; choices persist in localStorage across both carriers. You can bake different compile-time defaults: `--skin ember --default-mode dark --style classic` (or env `CONCEPT_ATLAS_SKIN` / `CONCEPT_ATLAS_DEFAULT_MODE` / `CONCEPT_ATLAS_STYLE` on the repo build). `--default-mode` only honors `dark`/`light`; `system` is accepted by the CLI but resolves to the carrier default (`light`). Bake a default only when the user asks for one. Content MDX never sets appearance — it is carrier/tooling territory, not content.
31
- 8. For several documents, pass them all in one call: `npx concept-atlas-dense-explain a.mdx b.mdx c.mdx -o dist --force [--concurrency 3]`. `-o` is then a directory. The batch validates everything first and builds in parallel. Builds only bundle the heavy renderers the content uses: a page with no `<Math>`/`<Mermaid>` skips KaTeX (its ~1.4MB inlined fonts) and Mermaid, shrinking a typical scroll article from ~5MB to ~250KB. Do not add dummy `<Math>`/`<Mermaid>` nodes to "enable" them — write the components only when the content needs them. Add `--link-assets` when the page carries many screenshots and size matters.
29
+ 6. Compile: `npx concept-atlas-dense-explain <file>.mdx --mode atlas|scroll [-o out.html] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [--inline-mermaid] [--mermaid-cdn <url>]`. Output is a standalone HTML beside the MDX unless `-o` is given. Validation errors abort the build; use `--no-validate` only to force a knowingly broken build. Mermaid loads from a CDN at runtime by default (fast build, small HTML, needs network); pass `--inline-mermaid` when the user needs a fully offline single file.
30
+ 7. **Appearance (optional)**: pages ship with a reader-facing appearance menu — palette (`aurora` cool indigo, `ember` warm gold, `verdant` forest green, `sakura` pink-plum, `noir` achromatic ink), a dark/light toggle, and a component style pack (`manuscript` editorial marginalia, `classic` boxed cards, `shadcn` hairline-bordered minimal UI, `elastic` bordered observability panels). The shipped default is aurora × manuscript × light; choices persist in localStorage across both carriers. You can bake different compile-time defaults: `--skin ember --default-mode dark --style classic` (or env `CONCEPT_ATLAS_SKIN` / `CONCEPT_ATLAS_DEFAULT_MODE` / `CONCEPT_ATLAS_STYLE` on the repo build). `--default-mode` only honors `dark`/`light`; `system` is accepted by the CLI but resolves to the carrier default (`light`). Bake a default only when the user asks for one. Content MDX never sets appearance — it is carrier/tooling territory, not content.
31
+ 8. For several documents, pass them all in one call: `npx concept-atlas-dense-explain a.mdx b.mdx c.mdx -o dist --force [--concurrency 3]`. `-o` is then a directory. The batch validates everything first and builds in parallel. Builds only bundle the heavy renderers the content uses: a page with no `<Math>` skips KaTeX (its ~1.4MB inlined fonts), and Mermaid is served from a CDN by default rather than bundled. Do not add dummy `<Math>`/`<Mermaid>` nodes to "enable" them — write the components only when the content needs them. Add `--link-assets` when the page carries many screenshots and size matters.
32
32
  9. Report the shell, output path, validation result (errors/warnings), and limitations. Do not claim interactions you did not verify.
33
33
 
34
34
  ## Carriers
@@ -3,12 +3,21 @@ import path from 'path';
3
3
  import fs from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { normalizeSkin, normalizeStyle } from '../src/model/skins.js';
6
- import { extractPageTitle } from '../src/model/validate-content.js';
6
+ import { extractPageTitle, detectFeatures } from '../src/model/validate-content.js';
7
7
 
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
10
10
  const rootDir = path.resolve(__dirname, '..');
11
11
 
12
+ /**
13
+ * Mermaid is served from a CDN at runtime by default so its ~2100-module
14
+ * transform stays out of the build. Set CONCEPT_ATLAS_INLINE_MERMAID=1 to bake
15
+ * it back into the HTML for a fully offline single file; CONCEPT_ATLAS_MERMAID_CDN
16
+ * overrides the CDN URL.
17
+ */
18
+ const INLINE_MERMAID = ['1', 'true', 'yes'].includes((process.env.CONCEPT_ATLAS_INLINE_MERMAID || '').toLowerCase());
19
+ const MERMAID_CDN_URL = process.env.CONCEPT_ATLAS_MERMAID_CDN || '';
20
+
12
21
  /**
13
22
  * Optional compile-time appearance defaults, read from the environment and
14
23
  * forwarded as `define`s. The carrier HTML plugin replaces the placeholders
@@ -26,28 +35,65 @@ function appearanceDefines() {
26
35
  }
27
36
 
28
37
  /**
29
- * The tab title comes from the mounted demo document, not the carrier shell:
30
- * the repository mounts content/components-demo.mdx in index.html while the
31
- * npm template mounts content/compile-runtime.mdx, so both are probed and the
32
- * first existing file supplies its <ExplainPage>/<ScrollHeader> title.
38
+ * The demo document mounted by each carrier. Unlike the npm template (which
39
+ * aliases the user's MDX), the repository hardcodes its demos in main.jsx /
40
+ * scroll-main.jsx, so the build reads them to derive the tab title and the set
41
+ * of optional renderers actually needed.
33
42
  */
34
- function pageTitleFor(entry) {
43
+ function demoSourceFor(entry) {
35
44
  const demos = entry === 'scroll.html'
36
45
  ? ['content/scroll-reading-demo.mdx']
37
46
  : ['content/components-demo.mdx', 'content/compile-runtime.mdx'];
38
47
  const demo = demos.map(name => path.resolve(rootDir, name)).find(file => fs.existsSync(file));
39
- return demo ? extractPageTitle(fs.readFileSync(demo, 'utf8')) : null;
48
+ return demo ? { demo, source: fs.readFileSync(demo, 'utf8') } : null;
49
+ }
50
+
51
+ /**
52
+ * Builds one standalone carrier HTML. `__ATLAS_FEATURES__` mirrors the CLI's
53
+ * per-document stubbing so a demo that never uses <Math>/<Mermaid> skips the
54
+ * KaTeX fonts and Mermaid module graph instead of bundling them unconditionally.
55
+ */
56
+ async function buildCarrier(entry, baseDefine) {
57
+ const define = { ...baseDefine };
58
+ const demo = demoSourceFor(entry);
59
+ if (demo) {
60
+ const title = extractPageTitle(demo.source);
61
+ if (title) {
62
+ define.__ATLAS_PAGE_TITLE__ = JSON.stringify(title);
63
+ console.log(`🔖 ${entry} 标签页标题:${title}`);
64
+ }
65
+ const features = detectFeatures(demo.source);
66
+ define.__ATLAS_FEATURES__ = JSON.stringify(features);
67
+ if (features.mermaid) {
68
+ define.__ATLAS_MERMAID_MODE__ = JSON.stringify(INLINE_MERMAID ? 'inline' : 'cdn');
69
+ if (MERMAID_CDN_URL) define.__ATLAS_MERMAID_CDN_URL__ = JSON.stringify(MERMAID_CDN_URL);
70
+ if (!INLINE_MERMAID) console.log(`🌐 ${entry} Mermaid 运行时从 CDN 加载(--inline-mermaid 可内联)`);
71
+ }
72
+ const dropped = [features.math ? null : 'KaTeX', features.mermaid ? null : 'Mermaid'].filter(Boolean);
73
+ if (dropped.length) console.log(`⚡ ${entry} 省略未使用的渲染器:${dropped.join('、')}`);
74
+ }
75
+
76
+ await build({
77
+ root: rootDir,
78
+ define,
79
+ build: {
80
+ outDir: 'dist',
81
+ // dist is cleared once up front; parallel carriers must not wipe each
82
+ // other's output mid-build.
83
+ emptyOutDir: false,
84
+ rollupOptions: { input: path.resolve(rootDir, entry) },
85
+ }
86
+ });
40
87
  }
41
88
 
42
89
  async function runBuild() {
43
90
  console.log('🚀 开始构建 Concept Atlas 知识讲解页面...');
44
91
 
45
92
  try {
46
- // 确保 dist 目录存在
93
+ // Clear dist once, then let every carrier write into it concurrently.
47
94
  const distDir = path.resolve(rootDir, 'dist');
48
- if (!fs.existsSync(distDir)) {
49
- fs.mkdirSync(distDir, { recursive: true });
50
- }
95
+ fs.rmSync(distDir, { recursive: true, force: true });
96
+ fs.mkdirSync(distDir, { recursive: true });
51
97
 
52
98
  const mode = process.env.CONCEPT_ATLAS_MODE;
53
99
  const carriers = mode === 'atlas' ? ['index.html'] : mode === 'scroll' ? ['scroll.html'] : ['index.html', 'scroll.html'];
@@ -56,22 +102,10 @@ async function runBuild() {
56
102
  console.log(`🎨 默认外观:skin=${define.__ATLAS_DEFAULT_SKIN__ || '(carrier 默认)'} mode=${define.__ATLAS_DEFAULT_MODE__ || '(carrier 默认)'}`);
57
103
  }
58
104
 
59
- // vite-plugin-singlefile supports one HTML input per build. Build each
60
- // requested carrier separately so every output remains a standalone file.
61
- for (const [index, entry] of carriers.entries()) {
62
- const title = pageTitleFor(entry);
63
- const entryDefine = title ? { ...define, __ATLAS_PAGE_TITLE__: JSON.stringify(title) } : define;
64
- if (title) console.log(`🔖 ${entry} 标签页标题:${title}`);
65
- await build({
66
- root: rootDir,
67
- define: entryDefine,
68
- build: {
69
- outDir: 'dist',
70
- emptyOutDir: index === 0,
71
- rollupOptions: { input: path.resolve(rootDir, entry) },
72
- }
73
- });
74
- }
105
+ // vite-plugin-singlefile supports one HTML input per build and emits no
106
+ // shared assets, so the carriers are independent and build in parallel
107
+ // instead of one full pass after another.
108
+ await Promise.all(carriers.map(entry => buildCarrier(entry, define)));
75
109
 
76
110
  console.log(`✅ 构建成功!产物已生成到 ${carriers.map(entry => `dist/${entry}`).join(' 和 ')}。`);
77
111
  } catch (err) {
@@ -70,7 +70,7 @@ function configureMermaid() {
70
70
  lineColor: read('--mermaid-line', MERMAID_FALLBACKS.lineColor),
71
71
  secondaryColor: nodeBg,
72
72
  tertiaryColor: read('--mermaid-canvas', MERMAID_FALLBACKS.tertiaryColor),
73
- fontFamily: 'Plus Jakarta Sans, sans-serif',
73
+ fontFamily: read('--font-sans', "'Plus Jakarta Sans', sans-serif"),
74
74
  },
75
75
  });
76
76
  }
@@ -942,6 +942,7 @@ export function Chart({ title = '图表', type = 'bar', data = [], series = [],
942
942
  <g key={seriesIndex}>
943
943
  <polyline
944
944
  className="chart-line"
945
+ pathLength="1"
945
946
  style={chartColor(seriesIndex)}
946
947
  points={entry.values.map((value, idx) => `${xFor(idx)},${yFor(value)}`).join(' ')}
947
948
  />
@@ -13,6 +13,21 @@ export const SKINS = [
13
13
  id: 'ember',
14
14
  label: 'Ember · 炉火',
15
15
  swatch: { dark: '#1c1812', light: '#f6f1e7', accent: '#d99a4e' }
16
+ },
17
+ {
18
+ id: 'verdant',
19
+ label: 'Verdant · 苔原',
20
+ swatch: { dark: '#0f1813', light: '#f3f7f2', accent: '#10b981' }
21
+ },
22
+ {
23
+ id: 'sakura',
24
+ label: 'Sakura · 樱雾',
25
+ swatch: { dark: '#1e131d', light: '#fbf3f7', accent: '#ec4899' }
26
+ },
27
+ {
28
+ id: 'noir',
29
+ label: 'Noir · 墨白',
30
+ swatch: { dark: '#141416', light: '#f5f5f4', accent: '#e63946' }
16
31
  }
17
32
  ];
18
33
 
@@ -32,7 +47,9 @@ export function normalizeSkin(value) {
32
47
  */
33
48
  export const COMPONENT_STYLES = [
34
49
  { id: 'manuscript', label: 'Manuscript · 评注手稿' },
35
- { id: 'classic', label: 'Classic · 经典卡片' }
50
+ { id: 'classic', label: 'Classic · 经典卡片' },
51
+ { id: 'shadcn', label: 'shadcn · 极简界面' },
52
+ { id: 'elastic', label: 'Elastic · 观测面板' }
36
53
  ];
37
54
 
38
55
  export const DEFAULT_STYLE = 'manuscript';