concept-atlas-dense-explain 0.5.0 → 0.6.1
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 +33 -6
- package/package.json +1 -1
- package/skill/SKILL.md +10 -8
- package/template/guides/atlas-guide.mdx +2 -0
- package/template/index.html +42 -16
- package/template/scripts/build.mjs +64 -24
- package/template/scroll.html +26 -0
- package/template/src/app/App.jsx +232 -239
- package/template/src/app/use-appearance.js +82 -0
- package/template/src/components/MDXComponents.jsx +62 -13
- package/template/src/components/SkinPicker.jsx +90 -0
- package/template/src/model/concept-schema.js +169 -169
- package/template/src/model/relation-types.js +90 -90
- package/template/src/model/skins.js +44 -0
- package/template/src/model/validate-content.js +25 -0
- package/template/src/scroll-main.jsx +6 -8
- package/template/src/styles/concept-explain.css +266 -168
- package/template/src/styles/skins.css +194 -0
- package/template/src/styles/tokens.css +229 -0
- package/template/src/views/NodeExplorer.jsx +668 -668
- package/template/src/views/RelationGraph.jsx +722 -723
- package/template/vite.config.js +69 -0
package/bin/cli.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { existsSync } from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { build } from 'vite';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { validateMdxSource, countBySeverity, detectFeatures } from '../template/src/model/validate-content.js';
|
|
7
|
+
import { validateMdxSource, countBySeverity, detectFeatures, extractPageTitle } 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) {
|
|
@@ -142,6 +144,7 @@ if (command === 'create' || command === 'new') {
|
|
|
142
144
|
}
|
|
143
145
|
await mkdir(path.dirname(output), { recursive: true });
|
|
144
146
|
const template = mode === 'atlas' ? `\
|
|
147
|
+
{/* shell 的 title 会成为浏览器标签页标题;页面图标固定为 📃。请把“主题名称”改成真实标题。 */}
|
|
145
148
|
<ExplainPage id="topic-id" title="主题名称" summary="用一句话说明这个主题解决什么问题。">
|
|
146
149
|
<ConceptGraph root="root-node">
|
|
147
150
|
<ConceptNode id="root-node" title="核心概念" level="L0" summary="给读者建立整体认知。">
|
|
@@ -166,6 +169,7 @@ if (command === 'create' || command === 'new') {
|
|
|
166
169
|
</ConceptGraph>
|
|
167
170
|
</ExplainPage>
|
|
168
171
|
` : `\
|
|
172
|
+
{/* shell 的 title 会成为浏览器标签页标题;页面图标固定为 📃。请把“主题名称”改成真实标题。 */}
|
|
169
173
|
<ScrollDocument>
|
|
170
174
|
<ScrollHeader title="主题名称">用一两句话说明主题、背景和读者应该带走的判断。</ScrollHeader>
|
|
171
175
|
|
|
@@ -201,6 +205,25 @@ const force = parsed.flags.has('--force');
|
|
|
201
205
|
const linkAssets = parsed.flags.has('--link-assets');
|
|
202
206
|
const modeFlag = parsed.values.get('--mode') || null;
|
|
203
207
|
|
|
208
|
+
// Compile-time appearance defaults. Invalid values fail fast with the valid
|
|
209
|
+
// options instead of silently baking a broken default into every page.
|
|
210
|
+
let skinFlag = null;
|
|
211
|
+
if (parsed.values.has('--skin')) {
|
|
212
|
+
skinFlag = normalizeSkin(parsed.values.get('--skin'));
|
|
213
|
+
if (!skinFlag) fail(`Unknown skin: ${parsed.values.get('--skin')} (available: ${SKINS.map(skin => skin.id).join(', ')})`);
|
|
214
|
+
}
|
|
215
|
+
let defaultModeFlag = null;
|
|
216
|
+
if (parsed.values.has('--default-mode')) {
|
|
217
|
+
const raw = parsed.values.get('--default-mode');
|
|
218
|
+
if (!['dark', 'light', 'system'].includes(raw)) fail(`Invalid --default-mode: ${raw} (use dark, light or system)`);
|
|
219
|
+
defaultModeFlag = raw;
|
|
220
|
+
}
|
|
221
|
+
let styleFlag = null;
|
|
222
|
+
if (parsed.values.has('--style')) {
|
|
223
|
+
styleFlag = normalizeStyle(parsed.values.get('--style'));
|
|
224
|
+
if (!styleFlag) fail(`Unknown component style: ${parsed.values.get('--style')} (available: ${COMPONENT_STYLES.map(style => style.id).join(', ')})`);
|
|
225
|
+
}
|
|
226
|
+
|
|
204
227
|
if (command === 'validate') {
|
|
205
228
|
const target = parsed.positional[0] ? path.resolve(parsed.positional[0]) : null;
|
|
206
229
|
if (!target || path.extname(target).toLowerCase() !== '.mdx' || !(await exists(target))) {
|
|
@@ -279,7 +302,7 @@ const jobs = inputs.map((input, index) => {
|
|
|
279
302
|
if (linkAssets && path.resolve(path.dirname(outputs[index])) !== path.resolve(path.dirname(input))) {
|
|
280
303
|
console.error(`警告:--link-assets 下 ${outputs[index]} 不在 ${path.dirname(input)} 内,相对图片路径会失效。`);
|
|
281
304
|
}
|
|
282
|
-
return { input, output: outputs[index], mode, features: detectFeatures(sources[index]), linkAssets };
|
|
305
|
+
return { input, output: outputs[index], mode, title: extractPageTitle(sources[index]), features: detectFeatures(sources[index]), linkAssets };
|
|
283
306
|
});
|
|
284
307
|
|
|
285
308
|
const limit = clampConcurrency(parsed.values.get('--concurrency'), jobs.length);
|
|
@@ -295,7 +318,7 @@ if (failures.length) {
|
|
|
295
318
|
}
|
|
296
319
|
|
|
297
320
|
async function buildOne(job) {
|
|
298
|
-
const { input, output, mode, features, linkAssets: link } = job;
|
|
321
|
+
const { input, output, mode, title, features, linkAssets: link } = job;
|
|
299
322
|
const templateEntry = mode === 'atlas' ? 'index.html' : 'scroll.html';
|
|
300
323
|
// Each build gets its own scratch outDir: the template always writes
|
|
301
324
|
// `index.html`/`scroll.html`, so concurrent builds sharing a directory would
|
|
@@ -305,6 +328,10 @@ async function buildOne(job) {
|
|
|
305
328
|
await mkdir(scratch, { recursive: true });
|
|
306
329
|
const define = { __ATLAS_FEATURES__: JSON.stringify(features) };
|
|
307
330
|
if (link) define.__ATLAS_INLINE_ASSETS__ = 'false';
|
|
331
|
+
if (title) define.__ATLAS_PAGE_TITLE__ = JSON.stringify(title);
|
|
332
|
+
if (skinFlag) define.__ATLAS_DEFAULT_SKIN__ = JSON.stringify(skinFlag);
|
|
333
|
+
if (defaultModeFlag) define.__ATLAS_DEFAULT_MODE__ = JSON.stringify(defaultModeFlag);
|
|
334
|
+
if (styleFlag) define.__ATLAS_DEFAULT_STYLE__ = JSON.stringify(styleFlag);
|
|
308
335
|
try {
|
|
309
336
|
await build({
|
|
310
337
|
root: templateRoot,
|
|
@@ -319,7 +346,7 @@ async function buildOne(job) {
|
|
|
319
346
|
});
|
|
320
347
|
await rm(output, { force: true });
|
|
321
348
|
await rename(path.join(scratch, templateEntry), output);
|
|
322
|
-
console.log(`Built ${mode} HTML: ${output}${describeFeatures(features)}${link ? ' [figures linked]' : ''}`);
|
|
349
|
+
console.log(`Built ${mode} HTML: ${output}${title ? ` [tab: ${title}]` : ''}${describeFeatures(features)}${link ? ' [figures linked]' : ''}`);
|
|
323
350
|
return { ok: true, input, output };
|
|
324
351
|
} catch (error) {
|
|
325
352
|
return { ok: false, input, output, error };
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -17,7 +17,7 @@ If the user only wants the prompt/methodology and not files, still choose a shel
|
|
|
17
17
|
npx concept-atlas-dense-explain guide --mode atlas -o ./concept-atlas-atlas-guide.mdx
|
|
18
18
|
npx concept-atlas-dense-explain guide --mode scroll -o ./concept-atlas-scroll-guide.mdx
|
|
19
19
|
```
|
|
20
|
-
It is a real, compilable MDX file
|
|
20
|
+
It is a real, compilable MDX file that demonstrates the components for that shell and their exact props. Search it for a component name to copy the correct prop shape instead of guessing. Delete it when done.
|
|
21
21
|
3. Start from a skeleton when useful: `npx concept-atlas-dense-explain create <file>.mdx --mode atlas|scroll`. `create` and `guide` refuse to overwrite an existing file unless `--force` is passed.
|
|
22
22
|
4. Write the semantic MDX into the user's `.mdx` file (see Authoring rules).
|
|
23
23
|
5. Validate before rendering:
|
|
@@ -26,14 +26,16 @@ 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.
|
|
31
|
-
8.
|
|
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.
|
|
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
|
|
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.
|
|
38
|
+
- The browser tab comes from the shell, not the build flags: `ExplainPage title="..."` (atlas) or `ScrollHeader title="..."` (scroll) becomes the `<title>`, so give it a real, specific document name — never leave a placeholder like "主题名称". The favicon is a fixed 📃 document emoji on every generated page.
|
|
37
39
|
- Never make one MDX file both shells. When switching shells, convert only the outer structure.
|
|
38
40
|
|
|
39
41
|
## Component families
|
|
@@ -41,8 +43,8 @@ If the user only wants the prompt/methodology and not files, still choose a shel
|
|
|
41
43
|
- Node semantics: `Overview`, `Definition`, `Mechanism`, `Implementation`, `Boundary`, `Example`, `Counterexample`, `Prerequisite`, `Input`, `Output`, `Glossary`
|
|
42
44
|
- Argument and evidence: `Evidence`, `Invariant`, `FailureMode`, `Tradeoff`, `LearningObjectives`, `KeyQuestion`
|
|
43
45
|
- Information models: `Flow`, `Timeline`, `Compare`, `DecisionMatrix`, `FrameworkModel`, `MatrixModel`, `FormulaModel`, `PyramidModel`, `FunnelModel`
|
|
44
|
-
- Reading and layout: `Insight`, `Callout`, `Details`, `NoteGrid`, `Tabs`, `Columns`, `Stack`, `Grid`, `Split`, `ScrollGrid`
|
|
45
|
-
- Graphics and extensions: `Mermaid`, `RelationMap`, `RelationPath`, `Math`, `MathBlock`, `Chart`, `Figure
|
|
46
|
+
- Reading and layout: `Insight`, `Callout`, `Details`, `NoteGrid`, `Tabs`, `Columns`, `Stack`, `Grid`, `Split`, `ScrollGrid`, `ScrollPair`, `ScrollToc`
|
|
47
|
+
- Graphics and extensions: `Mermaid`, `RelationMap`, `RelationPath`, `Math`, `MathBlock`, `Chart`, `Figure` (alias `Image`), `Cite`, `References`
|
|
46
48
|
|
|
47
49
|
## Authoring rules
|
|
48
50
|
|
|
@@ -60,7 +62,7 @@ If the user only wants the prompt/methodology and not files, still choose a shel
|
|
|
60
62
|
|
|
61
63
|
## Validation diagnostics
|
|
62
64
|
|
|
63
|
-
`validate` and the build print `CODE line:column message`. Fix these `error`s before building: `UNKNOWN_COMPONENT`, `CARRIER_MISSING`, `CARRIER_CONFLICT`, `CARRIER_MODE_MISMATCH`, `NODE_MISSING_ID`, `DUPLICATE_NODE_ID`, `NODE_MISSING_TITLE`, `MISSING_PARENT`, `GRAPH_ROOT_UNRESOLVED`, `REF_UNRESOLVED`, `RELATION_FROM_UNRESOLVED`, `RELATION_TO_UNRESOLVED`. Warnings worth fixing: `NODE_MISSING_SUMMARY`, `NODE_NO_CORE_CONTENT`, `UNKNOWN_LEVEL`, `UNKNOWN_RELATION_TYPE`, `RELATION_MISSING_LABEL`, `PROP_EXPECTS_ARRAY`, `MATH_CHILDREN_BRACES`, `GRAPH_MISSING_ROOT`, `ASSET_MISSING
|
|
65
|
+
`validate` and the build print `CODE line:column message`. Fix these `error`s before building: `UNKNOWN_COMPONENT`, `CARRIER_MISSING`, `CARRIER_CONFLICT`, `CARRIER_MODE_MISMATCH`, `NODE_MISSING_ID`, `DUPLICATE_NODE_ID`, `NODE_MISSING_TITLE`, `MISSING_PARENT`, `GRAPH_ROOT_UNRESOLVED`, `REF_MISSING_ID`, `REF_UNRESOLVED`, `RELATION_FROM_UNRESOLVED`, `RELATION_TO_UNRESOLVED`. Warnings worth fixing: `NODE_MISSING_SUMMARY`, `NODE_NO_CORE_CONTENT`, `UNKNOWN_LEVEL`, `UNKNOWN_RELATION_TYPE`, `RELATION_MISSING_LABEL`, `RELATION_SELF`, `PROP_EXPECTS_ARRAY`, `MATH_CHILDREN_BRACES`, `PROSE_EXPRESSION`, `FRONTMATTER_UNSUPPORTED`, `GRAPH_MISSING_ROOT`, `FIGURE_MISSING_SRC`, `ASSET_MISSING`, `REF_SELF`; `NO_ROOT_LEVEL` and `MULTIPLE_ROOT_LEVEL` are warnings that `--strict` promotes to errors.
|
|
64
66
|
|
|
65
67
|
## Before you report
|
|
66
68
|
|
|
@@ -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>
|
package/template/index.html
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
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
|
-
<
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E📃%3C/text%3E%3C/svg%3E" />
|
|
7
|
+
<title>Concept Atlas · 概念缩放式知识图谱</title>
|
|
8
|
+
<script>
|
|
9
|
+
(function () {
|
|
10
|
+
var doc = document.documentElement;
|
|
11
|
+
var stored = null;
|
|
12
|
+
try { stored = JSON.parse(localStorage.getItem('concept_atlas_appearance') || 'null'); } catch (e) {}
|
|
13
|
+
var legacy = localStorage.getItem('concept_atlas_theme');
|
|
14
|
+
// Placeholders are replaced at build time when --skin/--default-mode
|
|
15
|
+
// (or CONCEPT_ATLAS_* env vars) are configured; otherwise the runtime
|
|
16
|
+
// fallbacks below keep the carrier defaults.
|
|
17
|
+
var skin = '__ATLAS_DEFAULT_SKIN__';
|
|
18
|
+
if (/^__.+__$/.test(skin)) skin = 'aurora';
|
|
19
|
+
var mode = '__ATLAS_DEFAULT_MODE__';
|
|
20
|
+
if (/^__.+__$/.test(mode)) mode = 'light';
|
|
21
|
+
var style = '__ATLAS_DEFAULT_STYLE__';
|
|
22
|
+
if (/^__.+__$/.test(style)) style = 'manuscript';
|
|
23
|
+
if (stored && (stored.mode === 'light' || stored.mode === 'dark')) {
|
|
24
|
+
mode = stored.mode;
|
|
25
|
+
} else if (mode !== 'light' && mode !== 'dark') {
|
|
26
|
+
mode = legacy === 'light' || legacy === 'dark' ? legacy : 'light';
|
|
27
|
+
}
|
|
28
|
+
doc.setAttribute('data-skin', stored && stored.skin ? stored.skin : skin);
|
|
29
|
+
doc.setAttribute('data-theme', mode);
|
|
30
|
+
doc.setAttribute('data-style', stored && stored.style ? stored.style : style);
|
|
31
|
+
})();
|
|
32
|
+
</script>
|
|
33
|
+
<!-- Google Fonts for Academic / Editorial Knowledge style -->
|
|
34
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
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">
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<div id="root"></div>
|
|
40
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -1,30 +1,70 @@
|
|
|
1
|
-
import { build } from 'vite';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
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
|
+
import { extractPageTitle } from '../src/model/validate-content.js';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Optional compile-time appearance defaults, read from the environment and
|
|
14
|
+
* forwarded as `define`s. The carrier HTML plugin replaces the placeholders
|
|
15
|
+
* inside the anti-flash inline script only when these are configured.
|
|
16
|
+
*/
|
|
17
|
+
function appearanceDefines() {
|
|
18
|
+
const define = {};
|
|
19
|
+
const skin = normalizeSkin(process.env.CONCEPT_ATLAS_SKIN || '');
|
|
20
|
+
const mode = process.env.CONCEPT_ATLAS_DEFAULT_MODE;
|
|
21
|
+
const style = normalizeStyle(process.env.CONCEPT_ATLAS_STYLE || '');
|
|
22
|
+
if (skin) define.__ATLAS_DEFAULT_SKIN__ = JSON.stringify(skin);
|
|
23
|
+
if (['dark', 'light', 'system'].includes(mode)) define.__ATLAS_DEFAULT_MODE__ = JSON.stringify(mode);
|
|
24
|
+
if (style) define.__ATLAS_DEFAULT_STYLE__ = JSON.stringify(style);
|
|
25
|
+
return define;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
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.
|
|
33
|
+
*/
|
|
34
|
+
function pageTitleFor(entry) {
|
|
35
|
+
const demos = entry === 'scroll.html'
|
|
36
|
+
? ['content/scroll-reading-demo.mdx']
|
|
37
|
+
: ['content/components-demo.mdx', 'content/compile-runtime.mdx'];
|
|
38
|
+
const demo = demos.map(name => path.resolve(rootDir, name)).find(file => fs.existsSync(file));
|
|
39
|
+
return demo ? extractPageTitle(fs.readFileSync(demo, 'utf8')) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
10
42
|
async function runBuild() {
|
|
11
43
|
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
|
-
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// 确保 dist 目录存在
|
|
47
|
+
const distDir = path.resolve(rootDir, 'dist');
|
|
48
|
+
if (!fs.existsSync(distDir)) {
|
|
49
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
20
52
|
const mode = process.env.CONCEPT_ATLAS_MODE;
|
|
21
53
|
const carriers = mode === 'atlas' ? ['index.html'] : mode === 'scroll' ? ['scroll.html'] : ['index.html', 'scroll.html'];
|
|
54
|
+
const define = appearanceDefines();
|
|
55
|
+
if (define.__ATLAS_DEFAULT_SKIN__ || define.__ATLAS_DEFAULT_MODE__) {
|
|
56
|
+
console.log(`🎨 默认外观:skin=${define.__ATLAS_DEFAULT_SKIN__ || '(carrier 默认)'} mode=${define.__ATLAS_DEFAULT_MODE__ || '(carrier 默认)'}`);
|
|
57
|
+
}
|
|
22
58
|
|
|
23
59
|
// vite-plugin-singlefile supports one HTML input per build. Build each
|
|
24
60
|
// requested carrier separately so every output remains a standalone file.
|
|
25
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}`);
|
|
26
65
|
await build({
|
|
27
66
|
root: rootDir,
|
|
67
|
+
define: entryDefine,
|
|
28
68
|
build: {
|
|
29
69
|
outDir: 'dist',
|
|
30
70
|
emptyOutDir: index === 0,
|
|
@@ -34,10 +74,10 @@ async function runBuild() {
|
|
|
34
74
|
}
|
|
35
75
|
|
|
36
76
|
console.log(`✅ 构建成功!产物已生成到 ${carriers.map(entry => `dist/${entry}`).join(' 和 ')}。`);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
console.error('❌ 构建失败:', err);
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
runBuild();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('❌ 构建失败:', err);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
runBuild();
|
package/template/scroll.html
CHANGED
|
@@ -3,7 +3,33 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E📃%3C/text%3E%3C/svg%3E" />
|
|
6
7
|
<title>Concept Atlas · 连续阅读示例</title>
|
|
8
|
+
<script>
|
|
9
|
+
(function () {
|
|
10
|
+
var doc = document.documentElement;
|
|
11
|
+
var stored = null;
|
|
12
|
+
try { stored = JSON.parse(localStorage.getItem('concept_atlas_appearance') || 'null'); } catch (e) {}
|
|
13
|
+
var legacy = localStorage.getItem('concept_atlas_scroll_theme');
|
|
14
|
+
// Placeholders are replaced at build time when --skin/--default-mode
|
|
15
|
+
// (or CONCEPT_ATLAS_* env vars) are configured; otherwise the runtime
|
|
16
|
+
// fallbacks below keep the carrier defaults.
|
|
17
|
+
var skin = '__ATLAS_DEFAULT_SKIN__';
|
|
18
|
+
if (/^__.+__$/.test(skin)) skin = 'aurora';
|
|
19
|
+
var mode = '__ATLAS_DEFAULT_MODE__';
|
|
20
|
+
if (/^__.+__$/.test(mode)) mode = 'light';
|
|
21
|
+
var style = '__ATLAS_DEFAULT_STYLE__';
|
|
22
|
+
if (/^__.+__$/.test(style)) style = 'manuscript';
|
|
23
|
+
if (stored && (stored.mode === 'light' || stored.mode === 'dark')) {
|
|
24
|
+
mode = stored.mode;
|
|
25
|
+
} else if (mode !== 'light' && mode !== 'dark') {
|
|
26
|
+
mode = legacy === 'light' || legacy === 'dark' ? legacy : 'light';
|
|
27
|
+
}
|
|
28
|
+
doc.setAttribute('data-skin', stored && stored.skin ? stored.skin : skin);
|
|
29
|
+
doc.setAttribute('data-theme', mode);
|
|
30
|
+
doc.setAttribute('data-style', stored && stored.style ? stored.style : style);
|
|
31
|
+
})();
|
|
32
|
+
</script>
|
|
7
33
|
</head>
|
|
8
34
|
<body>
|
|
9
35
|
<div id="root"></div>
|