concept-atlas-dense-explain 0.5.0 → 0.6.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
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import { build } from 'vite';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { validateMdxSource, countBySeverity, detectFeatures } from '../template/src/model/validate-content.js';
8
+ import { SKINS, normalizeSkin, COMPONENT_STYLES, normalizeStyle } from '../template/src/model/skins.js';
8
9
 
9
10
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
11
  const templateRoot = path.join(packageRoot, 'template');
@@ -13,7 +14,7 @@ let buildCounter = 0;
13
14
 
14
15
  function usage() {
15
16
  console.log('Usage:');
16
- console.log(' npx concept-atlas-dense-explain <input.mdx>... [--mode atlas|scroll] [-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] [--json] [--no-validate]');
17
18
  console.log(' npx concept-atlas-dense-explain render <input.mdx>... [-o output.html|dir]');
18
19
  console.log(' npx concept-atlas-dense-explain validate <input.mdx> [--mode atlas|scroll] [--strict] [--json]');
19
20
  console.log(' npx concept-atlas-dense-explain create <output.mdx> [--mode atlas|scroll] [--force]');
@@ -21,6 +22,7 @@ function usage() {
21
22
  console.log('');
22
23
  console.log(' Multiple inputs build in parallel (default 2 at a time, cap 4); -o is then a directory.');
23
24
  console.log(' --link-assets keeps figures as relative links instead of inlining them as base64.');
25
+ 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.`);
24
26
  }
25
27
 
26
28
  async function exists(filePath) {
@@ -41,7 +43,7 @@ function fail(message) {
41
43
  process.exit(1);
42
44
  }
43
45
 
44
- const VALUE_FLAGS = new Set(['--mode', '-o', '--output', '--concurrency']);
46
+ const VALUE_FLAGS = new Set(['--mode', '-o', '--output', '--concurrency', '--skin', '--default-mode', '--style']);
45
47
 
46
48
  /** Splits argv into flags, flag values and positional arguments. */
47
49
  function parseFlags(argv) {
@@ -201,6 +203,25 @@ const force = parsed.flags.has('--force');
201
203
  const linkAssets = parsed.flags.has('--link-assets');
202
204
  const modeFlag = parsed.values.get('--mode') || null;
203
205
 
206
+ // Compile-time appearance defaults. Invalid values fail fast with the valid
207
+ // options instead of silently baking a broken default into every page.
208
+ let skinFlag = null;
209
+ if (parsed.values.has('--skin')) {
210
+ skinFlag = normalizeSkin(parsed.values.get('--skin'));
211
+ if (!skinFlag) fail(`Unknown skin: ${parsed.values.get('--skin')} (available: ${SKINS.map(skin => skin.id).join(', ')})`);
212
+ }
213
+ let defaultModeFlag = null;
214
+ if (parsed.values.has('--default-mode')) {
215
+ const raw = parsed.values.get('--default-mode');
216
+ if (!['dark', 'light', 'system'].includes(raw)) fail(`Invalid --default-mode: ${raw} (use dark, light or system)`);
217
+ defaultModeFlag = raw;
218
+ }
219
+ let styleFlag = null;
220
+ if (parsed.values.has('--style')) {
221
+ styleFlag = normalizeStyle(parsed.values.get('--style'));
222
+ if (!styleFlag) fail(`Unknown component style: ${parsed.values.get('--style')} (available: ${COMPONENT_STYLES.map(style => style.id).join(', ')})`);
223
+ }
224
+
204
225
  if (command === 'validate') {
205
226
  const target = parsed.positional[0] ? path.resolve(parsed.positional[0]) : null;
206
227
  if (!target || path.extname(target).toLowerCase() !== '.mdx' || !(await exists(target))) {
@@ -305,6 +326,9 @@ async function buildOne(job) {
305
326
  await mkdir(scratch, { recursive: true });
306
327
  const define = { __ATLAS_FEATURES__: JSON.stringify(features) };
307
328
  if (link) define.__ATLAS_INLINE_ASSETS__ = 'false';
329
+ if (skinFlag) define.__ATLAS_DEFAULT_SKIN__ = JSON.stringify(skinFlag);
330
+ if (defaultModeFlag) define.__ATLAS_DEFAULT_MODE__ = JSON.stringify(defaultModeFlag);
331
+ if (styleFlag) define.__ATLAS_DEFAULT_STYLE__ = JSON.stringify(styleFlag);
308
332
  try {
309
333
  await build({
310
334
  root: templateRoot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "concept-atlas-dense-explain",
3
- "version": "0.5.0",
3
+ "version": "0.6.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,14 +26,15 @@ 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]`. 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. 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.
31
- 8. Report the shell, output path, validation result (errors/warnings), and limitations. Do not claim interactions you did not verify.
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). 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.
32
+ 9. Report the shell, output path, validation result (errors/warnings), and limitations. Do not claim interactions you did not verify.
32
33
 
33
34
  ## Carriers
34
35
 
35
36
  - `atlas`: `ExplainPage` → `ConceptGraph` → `ConceptNode`, plus `Children`/`ConceptRef` and cross-branch `Relation`s. Exactly one `L0` root, several `L1` branches, depth to `L3`/`L4`. Shared components live inside nodes.
36
- - `scroll`: `ScrollDocument` → `ScrollHeader` + `ScrollSection` (+ `ScrollProse`, `ScrollGrid`). Shared components live inside sections. The shell auto-builds a a table of contents and reading progress from section titles — do not hand-build navigation.
37
+ - `scroll`: `ScrollDocument` → `ScrollHeader` + `ScrollSection` (+ `ScrollProse`, `ScrollGrid`). Shared components live inside sections. The shell auto-builds a table of contents and reading progress from section titles — do not hand-build navigation.
37
38
  - Never make one MDX file both shells. When switching shells, convert only the outer structure.
38
39
 
39
40
  ## Component families
@@ -89,10 +89,12 @@
89
89
  <Children><ConceptRef id="layout-demo" /><ConceptRef id="compact-demo" /></Children>
90
90
  </ConceptNode>
91
91
  <ConceptNode id="layout-demo" title="布局原语:Stack、Grid、Split、Columns 让信息有秩序" level="L2" parent="presentation-family" summary="布局组件只表达空间意图,具体样式由模板统一接管。">
92
+ <Definition>布局原语表达信息之间的空间关系;它们负责组织内容,不负责改变内容本身的语义。</Definition>
92
93
  <Columns><Split ratio="1fr 1fr"><Callout title="左列">结论与定义。</Callout><Callout title="右列">证据与边界。</Callout></Split></Columns>
93
94
  <Grid columns="auto" gap="sm"><Insight title="局部判断">Grid 中的短信息。</Insight><Insight title="另一判断" tone="warn">不要混用职责。</Insight></Grid>
94
95
  </ConceptNode>
95
96
  <ConceptNode id="compact-demo" title="压缩阅读:Tabs、Details、Callout、Insight、NoteGrid" level="L2" parent="presentation-family" summary="这些组件适合把结论、提示、细节和短信息压缩在首屏。">
97
+ <Definition>压缩组件通过折叠、分组和短提示降低首屏负担,同时保留继续阅读所需的上下文。</Definition>
96
98
  <Tabs items={[{label:'结论',content:'先展示最重要的判断。'},{label:'证据',content:'再展开可验证依据。'}]} />
97
99
  <Details summary="展开更多">Details 将次要信息收起,避免首屏过载。</Details>
98
100
  <Insight title="关键判断" tone="success">一个组件只承担一种信息组织方式。</Insight>
@@ -1,16 +1,41 @@
1
- <!doctype html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Concept Atlas · 概念缩放式知识图谱</title>
7
- <!-- Google Fonts for Academic / Editorial Knowledge style -->
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <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">
11
- </head>
12
- <body>
13
- <div id="root"></div>
14
- <script type="module" src="/src/main.jsx"></script>
15
- </body>
16
- </html>
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Concept Atlas · 概念缩放式知识图谱</title>
7
+ <script>
8
+ (function () {
9
+ var doc = document.documentElement;
10
+ var stored = null;
11
+ try { stored = JSON.parse(localStorage.getItem('concept_atlas_appearance') || 'null'); } catch (e) {}
12
+ var legacy = localStorage.getItem('concept_atlas_theme');
13
+ // Placeholders are replaced at build time when --skin/--default-mode
14
+ // (or CONCEPT_ATLAS_* env vars) are configured; otherwise the runtime
15
+ // fallbacks below keep the carrier defaults.
16
+ var skin = '__ATLAS_DEFAULT_SKIN__';
17
+ if (/^__.+__$/.test(skin)) skin = 'aurora';
18
+ var mode = '__ATLAS_DEFAULT_MODE__';
19
+ if (/^__.+__$/.test(mode)) mode = 'light';
20
+ var style = '__ATLAS_DEFAULT_STYLE__';
21
+ if (/^__.+__$/.test(style)) style = 'manuscript';
22
+ if (stored && (stored.mode === 'light' || stored.mode === 'dark')) {
23
+ mode = stored.mode;
24
+ } else if (mode !== 'light' && mode !== 'dark') {
25
+ mode = legacy === 'light' || legacy === 'dark' ? legacy : 'light';
26
+ }
27
+ doc.setAttribute('data-skin', stored && stored.skin ? stored.skin : skin);
28
+ doc.setAttribute('data-theme', mode);
29
+ doc.setAttribute('data-style', stored && stored.style ? stored.style : style);
30
+ })();
31
+ </script>
32
+ <!-- Google Fonts for Academic / Editorial Knowledge style -->
33
+ <link rel="preconnect" href="https://fonts.googleapis.com">
34
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
35
+ <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
+ </head>
37
+ <body>
38
+ <div id="root"></div>
39
+ <script type="module" src="/src/main.jsx"></script>
40
+ </body>
41
+ </html>
@@ -1,30 +1,52 @@
1
- import { build } from 'vite';
2
- import path from 'path';
3
- import fs from 'fs';
4
- import { fileURLToPath } from 'url';
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
- const rootDir = path.resolve(__dirname, '..');
9
-
1
+ import { build } from 'vite';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import { normalizeSkin, normalizeStyle } from '../src/model/skins.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const rootDir = path.resolve(__dirname, '..');
10
+
11
+ /**
12
+ * Optional compile-time appearance defaults, read from the environment and
13
+ * forwarded as `define`s. The carrier HTML plugin replaces the placeholders
14
+ * inside the anti-flash inline script only when these are configured.
15
+ */
16
+ function appearanceDefines() {
17
+ const define = {};
18
+ const skin = normalizeSkin(process.env.CONCEPT_ATLAS_SKIN || '');
19
+ const mode = process.env.CONCEPT_ATLAS_DEFAULT_MODE;
20
+ const style = normalizeStyle(process.env.CONCEPT_ATLAS_STYLE || '');
21
+ if (skin) define.__ATLAS_DEFAULT_SKIN__ = JSON.stringify(skin);
22
+ if (['dark', 'light', 'system'].includes(mode)) define.__ATLAS_DEFAULT_MODE__ = JSON.stringify(mode);
23
+ if (style) define.__ATLAS_DEFAULT_STYLE__ = JSON.stringify(style);
24
+ return define;
25
+ }
26
+
10
27
  async function runBuild() {
11
28
  console.log('🚀 开始构建 Concept Atlas 知识讲解页面...');
12
-
13
- try {
14
- // 确保 dist 目录存在
15
- const distDir = path.resolve(rootDir, 'dist');
16
- if (!fs.existsSync(distDir)) {
17
- fs.mkdirSync(distDir, { recursive: true });
18
- }
19
-
29
+
30
+ try {
31
+ // 确保 dist 目录存在
32
+ const distDir = path.resolve(rootDir, 'dist');
33
+ if (!fs.existsSync(distDir)) {
34
+ fs.mkdirSync(distDir, { recursive: true });
35
+ }
36
+
20
37
  const mode = process.env.CONCEPT_ATLAS_MODE;
21
38
  const carriers = mode === 'atlas' ? ['index.html'] : mode === 'scroll' ? ['scroll.html'] : ['index.html', 'scroll.html'];
39
+ const define = appearanceDefines();
40
+ if (define.__ATLAS_DEFAULT_SKIN__ || define.__ATLAS_DEFAULT_MODE__) {
41
+ console.log(`🎨 默认外观:skin=${define.__ATLAS_DEFAULT_SKIN__ || '(carrier 默认)'} mode=${define.__ATLAS_DEFAULT_MODE__ || '(carrier 默认)'}`);
42
+ }
22
43
 
23
44
  // vite-plugin-singlefile supports one HTML input per build. Build each
24
45
  // requested carrier separately so every output remains a standalone file.
25
46
  for (const [index, entry] of carriers.entries()) {
26
47
  await build({
27
48
  root: rootDir,
49
+ define,
28
50
  build: {
29
51
  outDir: 'dist',
30
52
  emptyOutDir: index === 0,
@@ -34,10 +56,10 @@ async function runBuild() {
34
56
  }
35
57
 
36
58
  console.log(`✅ 构建成功!产物已生成到 ${carriers.map(entry => `dist/${entry}`).join(' 和 ')}。`);
37
- } catch (err) {
38
- console.error('❌ 构建失败:', err);
39
- process.exit(1);
40
- }
41
- }
42
-
43
- runBuild();
59
+ } catch (err) {
60
+ console.error('❌ 构建失败:', err);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ runBuild();
@@ -4,6 +4,31 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Concept Atlas · 连续阅读示例</title>
7
+ <script>
8
+ (function () {
9
+ var doc = document.documentElement;
10
+ var stored = null;
11
+ try { stored = JSON.parse(localStorage.getItem('concept_atlas_appearance') || 'null'); } catch (e) {}
12
+ var legacy = localStorage.getItem('concept_atlas_scroll_theme');
13
+ // Placeholders are replaced at build time when --skin/--default-mode
14
+ // (or CONCEPT_ATLAS_* env vars) are configured; otherwise the runtime
15
+ // fallbacks below keep the carrier defaults.
16
+ var skin = '__ATLAS_DEFAULT_SKIN__';
17
+ if (/^__.+__$/.test(skin)) skin = 'aurora';
18
+ var mode = '__ATLAS_DEFAULT_MODE__';
19
+ if (/^__.+__$/.test(mode)) mode = 'light';
20
+ var style = '__ATLAS_DEFAULT_STYLE__';
21
+ if (/^__.+__$/.test(style)) style = 'manuscript';
22
+ if (stored && (stored.mode === 'light' || stored.mode === 'dark')) {
23
+ mode = stored.mode;
24
+ } else if (mode !== 'light' && mode !== 'dark') {
25
+ mode = legacy === 'light' || legacy === 'dark' ? legacy : 'light';
26
+ }
27
+ doc.setAttribute('data-skin', stored && stored.skin ? stored.skin : skin);
28
+ doc.setAttribute('data-theme', mode);
29
+ doc.setAttribute('data-style', stored && stored.style ? stored.style : style);
30
+ })();
31
+ </script>
7
32
  </head>
8
33
  <body>
9
34
  <div id="root"></div>