荒境环抱无处可逃,逸散灵魂如影随梢
-
+
地图构建:Mol1Sama feat. Roser [Original]
@@ -70,7 +70,7 @@ sidebar_position: 1
> 原浮空城地块,因多起高空事故而封闭
-
+
地图构建:Mol1Sama [Original]
@@ -87,7 +87,7 @@ sidebar_position: 1
> 有堂有庭,有桥有船。有书有酒,有歌有弦。
-
+
地图原筑:[苍园](https://www.bilibili.com/video/BV1KgBEYYEDh) by 苍蓝之水手
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/3. \345\234\260\345\233\276\345\257\274\350\247\210.tiptap.json" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/3. \345\234\260\345\233\276\345\257\274\350\247\210.tiptap.json"
index 9ee271f..ad1bb98 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/3. \345\234\260\345\233\276\345\257\274\350\247\210.tiptap.json"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/3. \345\234\260\345\233\276\345\257\274\350\247\210.tiptap.json"
@@ -1,6 +1,6 @@
{
"format": "tiptap-v1",
- "frontmatter": "---\nsidebar_position: 1\n---\n",
+ "frontmatter": "---\ndescription: 午夜灵魂 6 张内置地图——聚光圣殿、镇灵塔楼、山间湖谷、心火特区、昔居浮屿、苏园夜游——的俯视图、数据与机制,附虚空惩罚与自定义地图说明。\n---\n",
"content": {
"type": "doc",
"content": [
@@ -76,7 +76,7 @@
{
"type": "image",
"attrs": {
- "src": "/img/maps/聚光圣殿.PNG",
+ "src": "/img/maps/聚光圣殿.webp",
"alt": "",
"title": null,
"width": null,
@@ -523,7 +523,7 @@
{
"type": "image",
"attrs": {
- "src": "/img/maps/镇灵塔楼.PNG",
+ "src": "/img/maps/镇灵塔楼.webp",
"alt": "",
"title": null,
"width": null,
@@ -1002,7 +1002,7 @@
{
"type": "image",
"attrs": {
- "src": "/img/maps/山间湖谷.png",
+ "src": "/img/maps/山间湖谷.webp",
"alt": null,
"title": null,
"width": null,
@@ -1429,7 +1429,7 @@
{
"type": "image",
"attrs": {
- "src": "/img/maps/心火特区.png",
+ "src": "/img/maps/心火特区.webp",
"alt": null,
"title": null,
"width": null,
@@ -1895,7 +1895,7 @@
{
"type": "image",
"attrs": {
- "src": "/img/maps/昔居浮屿.PNG",
+ "src": "/img/maps/昔居浮屿.webp",
"alt": null,
"title": null,
"width": null,
@@ -2361,7 +2361,7 @@
{
"type": "image",
"attrs": {
- "src": "/img/maps/苏园夜游.PNG",
+ "src": "/img/maps/苏园夜游.webp",
"alt": null,
"title": null,
"width": null,
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/4. \350\203\275\345\212\233\344\270\200\350\247\210.md" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/4. \350\203\275\345\212\233\344\270\200\350\247\210.md"
index c162099..5cbd4f7 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/4. \350\203\275\345\212\233\344\270\200\350\247\210.md"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/4. \350\203\275\345\212\233\344\270\200\350\247\210.md"
@@ -1,5 +1,5 @@
---
-sidebar_position: 1
+description: 午夜灵魂全部 10 个技能、14 个天赋与 7 件灵魂宝物的冷却时间、持续时间与效果数据,按灵魂方与灵魂守卫者方分列。
---
# 能力一览
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/4. \350\203\275\345\212\233\344\270\200\350\247\210.tiptap.json" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/4. \350\203\275\345\212\233\344\270\200\350\247\210.tiptap.json"
index cc54b12..08bcad6 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/4. \350\203\275\345\212\233\344\270\200\350\247\210.tiptap.json"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/4. \350\203\275\345\212\233\344\270\200\350\247\210.tiptap.json"
@@ -1,6 +1,6 @@
{
"format": "tiptap-v1",
- "frontmatter": "---\nsidebar_position: 1\n---\n",
+ "frontmatter": "---\ndescription: 午夜灵魂全部 10 个技能、14 个天赋与 7 件灵魂宝物的冷却时间、持续时间与效果数据,按灵魂方与灵魂守卫者方分列。\n---\n",
"content": {
"type": "doc",
"content": [
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/5. \345\233\236\345\223\215\350\256\260\345\275\225.md" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/5. \345\233\236\345\223\215\350\256\260\345\275\225.md"
index 4e2e781..09d1e97 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/5. \345\233\236\345\223\215\350\256\260\345\275\225.md"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/5. \345\233\236\345\223\215\350\256\260\345\275\225.md"
@@ -1,5 +1,5 @@
---
-sidebar_position: 1
+description: 午夜灵魂全部回响的详细信息:强制性引导、灵魂鉴碎片、开盒得技能、天幕降落雷、命运交响曲等,含各自的触发条件与效果改动。
---
# 回响记录
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/5. \345\233\236\345\223\215\350\256\260\345\275\225.tiptap.json" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/5. \345\233\236\345\223\215\350\256\260\345\275\225.tiptap.json"
index d31d4b5..1f3901e 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/5. \345\233\236\345\223\215\350\256\260\345\275\225.tiptap.json"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/5. \345\233\236\345\223\215\350\256\260\345\275\225.tiptap.json"
@@ -1,6 +1,6 @@
{
"format": "tiptap-v1",
- "frontmatter": "---\nsidebar_position: 1\n---\n",
+ "frontmatter": "---\ndescription: 午夜灵魂全部回响的详细信息:强制性引导、灵魂鉴碎片、开盒得技能、天幕降落雷、命运交响曲等,含各自的触发条件与效果改动。\n---\n",
"content": {
"type": "doc",
"content": [
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/6. \350\277\233\345\272\246\347\242\221\345\210\273.md" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/6. \350\277\233\345\272\246\347\242\221\345\210\273.md"
index d730863..ed638c3 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/6. \350\277\233\345\272\246\347\242\221\345\210\273.md"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/6. \350\277\233\345\272\246\347\242\221\345\210\273.md"
@@ -1,5 +1,5 @@
---
-sidebar_position: 1
+description: 午夜灵魂全部进度成就的解锁条件、描述与进度点奖励,涵盖教程、装饰品解锁,以及沉浸、得胜、高光、捡拾、处决五类历程进度。
---
# 进度碑刻
@@ -27,7 +27,7 @@ sidebar_position: 1
['**五号屠场**', '完成 5 场游戏', '您初出茅庐。', '5', '-'],
['**二十首情诗**', '完成 20 场游戏', '您渐入佳境。', '5', '-'],
['**五十度灰**', '完成 50 场游戏', '您驾轻就熟。', '10', '-'],
- ['**s百年孤独**', '完成 100 场游戏', '您持之以恒。', '10', '-'],
+ ['**百年孤独**', '完成 100 场游戏', '您持之以恒。', '10', '-'],
[{t:'**二百周年的人**',bg:'#E5E6E9',op:0.55}, {t:'完成 200 场游戏',bg:'#E5E6E9',op:0.55}, {t:'您历久弥新。',bg:'#E5E6E9',op:0.55}, {t:'20',bg:'#E5E6E9',op:0.55}, {t:'-',bg:'#E5E6E9',op:0.55}],
[{t:'**五百罗汉**',bg:'#E5E6E9',op:0.55}, {t:'完成 500 场游戏',bg:'#E5E6E9',op:0.55}, {t:'您已臻化境。',bg:'#E5E6E9',op:0.55}, {t:'20',bg:'#E5E6E9',op:0.55}, {t:'-',bg:'#E5E6E9',op:0.55}],
]}/>
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/6. \350\277\233\345\272\246\347\242\221\345\210\273.tiptap.json" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/6. \350\277\233\345\272\246\347\242\221\345\210\273.tiptap.json"
index fe3c4a8..d3da170 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/6. \350\277\233\345\272\246\347\242\221\345\210\273.tiptap.json"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/6. \350\277\233\345\272\246\347\242\221\345\210\273.tiptap.json"
@@ -1,6 +1,6 @@
{
"format": "tiptap-v1",
- "frontmatter": "---\nsidebar_position: 1\n---\n",
+ "frontmatter": "---\ndescription: 午夜灵魂全部进度成就的解锁条件、描述与进度点奖励,涵盖教程、装饰品解锁,以及沉浸、得胜、高光、捡拾、处决五类历程进度。\n---\n",
"content": {
"type": "doc",
"content": [
@@ -862,7 +862,7 @@
"type": "bold"
}
],
- "text": "s百年孤独"
+ "text": "百年孤独"
}
]
}
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/7. \351\245\260\345\223\201\351\233\206\345\206\214.md" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/7. \351\245\260\345\223\201\351\233\206\345\206\214.md"
index 2a3ea1d..5203c25 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/7. \351\245\260\345\223\201\351\233\206\345\206\214.md"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/7. \351\245\260\345\223\201\351\233\206\345\206\214.md"
@@ -1,5 +1,5 @@
---
-sidebar_position: 1
+description: 午夜灵魂装饰品总览:粒子效果、武器样式与文本套组的完整列表和获取方法,以及饰品装配规则。
---
# 饰品集册
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/7. \351\245\260\345\223\201\351\233\206\345\206\214.tiptap.json" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/7. \351\245\260\345\223\201\351\233\206\345\206\214.tiptap.json"
index 29cd529..4939110 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/7. \351\245\260\345\223\201\351\233\206\345\206\214.tiptap.json"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/7. \351\245\260\345\223\201\351\233\206\345\206\214.tiptap.json"
@@ -1,6 +1,6 @@
{
"format": "tiptap-v1",
- "frontmatter": "---\nsidebar_position: 1\n---\n",
+ "frontmatter": "---\ndescription: 午夜灵魂装饰品总览:粒子效果、武器样式与文本套组的完整列表和获取方法,以及饰品装配规则。\n---\n",
"content": {
"type": "doc",
"content": [
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/8. \351\245\260\345\223\201\351\233\206\345\206\214-\346\226\207\346\234\254\345\245\227\347\273\204\350\257\246\350\247\210.md" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/8. \351\245\260\345\223\201\351\233\206\345\206\214-\346\226\207\346\234\254\345\245\227\347\273\204\350\257\246\350\247\210.md"
index dcb0419..2de4e68 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/8. \351\245\260\345\223\201\351\233\206\345\206\214-\346\226\207\346\234\254\345\245\227\347\273\204\350\257\246\350\247\210.md"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/8. \351\245\260\345\223\201\351\233\206\345\206\214-\346\226\207\346\234\254\345\245\227\347\273\204\350\257\246\350\247\210.md"
@@ -1,5 +1,5 @@
---
-sidebar_position: 1
+description: 午夜灵魂文本套组四大系列——趋光、溯回、闲嬉、灵兽——的全部文本内容与解锁方式。
---
# 饰品集册/文本套组详览
diff --git "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/8. \351\245\260\345\223\201\351\233\206\345\206\214-\346\226\207\346\234\254\345\245\227\347\273\204\350\257\246\350\247\210.tiptap.json" "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/8. \351\245\260\345\223\201\351\233\206\345\206\214-\346\226\207\346\234\254\345\245\227\347\273\204\350\257\246\350\247\210.tiptap.json"
index 928ea30..cf2bb94 100644
--- "a/docs/\345\215\210\345\244\234\347\201\265\351\255\202/8. \351\245\260\345\223\201\351\233\206\345\206\214-\346\226\207\346\234\254\345\245\227\347\273\204\350\257\246\350\247\210.tiptap.json"
+++ "b/docs/\345\215\210\345\244\234\347\201\265\351\255\202/8. \351\245\260\345\223\201\351\233\206\345\206\214-\346\226\207\346\234\254\345\245\227\347\273\204\350\257\246\350\247\210.tiptap.json"
@@ -1,6 +1,6 @@
{
"format": "tiptap-v1",
- "frontmatter": "---\nsidebar_position: 1\n---\n",
+ "frontmatter": "---\ndescription: 午夜灵魂文本套组四大系列——趋光、溯回、闲嬉、灵兽——的全部文本内容与解锁方式。\n---\n",
"content": {
"type": "doc",
"content": [
diff --git a/docusaurus.config.ts b/docusaurus.config.ts
index 76a3c3a..a8824b2 100644
--- a/docusaurus.config.ts
+++ b/docusaurus.config.ts
@@ -105,7 +105,6 @@ const config: Config = {
language: ["zh"],
indexBlog: false,
docsRouteBasePath: "/wiki",
- searchBarShortcutHint: false,
},
],
],
@@ -154,7 +153,8 @@ const config: Config = {
minHeadingLevel: 2,
maxHeadingLevel: 4,
},
- image: "img/MidSoul-Logo.png",
+ // 分享卡片按 1.91:1 裁切,直接用 3.24:1 的 logo 会被裁掉两头。
+ image: "img/og-cover.jpg",
colorMode: {
defaultMode: "dark",
respectPrefersColorScheme: true,
@@ -164,11 +164,13 @@ const config: Config = {
hideOnScroll: true,
logo: {
alt: "MidSoul Wiki",
- src: "img/MidSoul-Logo-Dark.png",
- srcDark: "img/MidSoul-Logo.png",
+ src: "img/MidSoul-Logo-Dark.webp",
+ srcDark: "img/MidSoul-Logo.webp",
href: "/",
- width: 120,
- height: 36,
+ // 原图 2400×741(3.24:1),导航栏按 2rem 高显示;写成 120×36(3.33:1)
+ // 会让浏览器按错误比例预留占位。这里按真实比例给 104×32。
+ width: 104,
+ height: 32,
},
items: [
{
diff --git a/package.json b/package.json
index a2443af..b9eb580 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,10 @@
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
- "typecheck": "tsc"
+ "typecheck": "tsc",
+ "fonts": "node scripts/build-font-subset.mjs",
+ "test": "node src/components/editor/fromMarkdown.test.mjs && node src/components/editor/toMarkdown.test.mjs && node src/components/editor/tableIO.test.mjs",
+ "check:assets": "node scripts/check-assets.mjs"
},
"dependencies": {
"@docusaurus/core": "3.10.2",
@@ -49,6 +52,7 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.10.2",
+ "@docusaurus/plugin-content-docs": "3.10.2",
"@docusaurus/tsconfig": "3.10.2",
"@docusaurus/types": "3.10.2",
"@types/mdx": "^2.0.14",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e4a56ac..f61c2cf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -105,6 +105,9 @@ importers:
'@docusaurus/module-type-aliases':
specifier: 3.10.2
version: 3.10.2(@swc/core@1.15.47)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@docusaurus/plugin-content-docs':
+ specifier: 3.10.2
+ version: 3.10.2(@docusaurus/faster@3.10.2(@docusaurus/types@3.10.2(@swc/core@1.15.47)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(postcss@8.5.26))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@rspack/core@1.7.12)(@swc/core@1.15.47)(debug@4.4.3)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3)
'@docusaurus/tsconfig':
specifier: 3.10.2
version: 3.10.2
diff --git a/scripts/build-font-subset.mjs b/scripts/build-font-subset.mjs
new file mode 100644
index 0000000..9cd6c32
--- /dev/null
+++ b/scripts/build-font-subset.mjs
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+/**
+ * 生成自托管的 Noto Serif SC 子集。
+ *
+ * 站点原先从 fonts.googleapis.com 加载 Noto Serif SC / Noto Sans SC。中文
+ * 受众多半在国内,那个域名基本不可达,且 @import 写在 CSS 首行是阻塞渲染
+ * 的。正文已回落系统字体栈(PingFang SC / 微软雅黑),标题衬线体是设计
+ * 主特征,改成自托管子集。
+ *
+ * pnpm fonts
+ *
+ * 需要 uv(https://docs.astral.sh/uv/)——用它跑 fonttools,不污染系统
+ * Python。源字体从 jsDelivr 拉,缓存在 .cache-fonts/(已 gitignore)。
+ */
+import { execFileSync } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+import { ROOT, CHARSET_MANIFEST, collectCharset } from './charset.mjs';
+
+const CACHE = path.join(ROOT, '.cache-fonts');
+// 放在 src/css 下而不是 static/:@font-face 用相对 url() 交给打包器处理,
+// 才能自动带上 baseUrl(/midsoul/)并加内容哈希。
+const OUT = path.join(ROOT, 'src', 'css', 'fonts');
+const SOURCE = 'https://cdn.jsdelivr.net/gh/notofonts/noto-cjk@main/Serif/SubsetOTF/SC';
+
+// 400:h1/h2/h3、页脚标题、翻页标签、首页大标题。
+// 700:Infima 的 --ifm-heading-font-weight,作用于 h4~h6 与博客标题。
+// 600 全站没有任何规则用到,不生成。
+const WEIGHTS = [
+ { weight: 400, source: 'NotoSerifSC-Regular.otf' },
+ { weight: 700, source: 'NotoSerifSC-Bold.otf' },
+];
+
+function download(name) {
+ const target = path.join(CACHE, name);
+ if (fs.existsSync(target)) return target;
+ fs.mkdirSync(CACHE, { recursive: true });
+ console.log(`下载 ${name}(约 11MB,只需一次)…`);
+ execFileSync('curl', ['-sfL', '--max-time', '300', `${SOURCE}/${name}`, '-o', target]);
+ return target;
+}
+
+const charset = collectCharset();
+fs.mkdirSync(OUT, { recursive: true });
+fs.mkdirSync(CACHE, { recursive: true });
+// CI 用这份清单比对:仓库里出现了子集外的字就提示重新生成,不必解析字体。
+fs.writeFileSync(CHARSET_MANIFEST, charset);
+const charsetFile = path.join(CACHE, 'charset.txt');
+fs.writeFileSync(charsetFile, charset);
+console.log(`字符集:${charset.length} 个中日韩字符 + 拉丁基本区`);
+
+for (const { weight, source } of WEIGHTS) {
+ const input = download(source);
+ const output = path.join(OUT, `NotoSerifSC-${weight}.woff2`);
+ execFileSync('uvx', [
+ '--quiet', '--from', 'fonttools[woff]', 'pyftsubset', input,
+ `--output-file=${output}`,
+ '--flavor=woff2',
+ `--text-file=${charsetFile}`,
+ // 拉丁基本区 + 常用符号:标题里混排英文与数字。
+ '--unicodes=U+0020-007E,U+00A0-00FF,U+2010-2027,U+2030-205E',
+ '--no-hinting',
+ '--desubroutinize',
+ '--drop-tables+=DSIG',
+ ], { stdio: 'inherit' });
+ console.log(`✓ NotoSerifSC-${weight}.woff2 ${(fs.statSync(output).size / 1024).toFixed(1)} KB`);
+}
diff --git a/scripts/charset.mjs b/scripts/charset.mjs
new file mode 100644
index 0000000..21d2dc5
--- /dev/null
+++ b/scripts/charset.mjs
@@ -0,0 +1,43 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+export const ROOT = path.resolve(import.meta.dirname, '..');
+export const CHARSET_MANIFEST = path.join(import.meta.dirname, 'font-charset.txt');
+
+/** 标题可能用到的文字来源:文档与博客正文,以及配置和首页里写死的中文串。 */
+const CONTENT_DIRS = [
+ { dir: 'docs', match: /\.mdx?$/ },
+ { dir: 'blog', match: /\.mdx?$/ },
+ { dir: 'src/pages', match: /\.tsx$/ },
+];
+const CONTENT_FILES = ['docusaurus.config.ts'];
+
+const CJK = /[ -〿㐀-䶿一-鿿-]/;
+
+function walk(dir, match, found = []) {
+ if (!fs.existsSync(dir)) return found;
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) walk(full, match, found);
+ else if (match.test(entry.name)) found.push(full);
+ }
+ return found;
+}
+
+/**
+ * 全站出现过的中日韩字符,排序去重。
+ *
+ * 取整站用字而不是只取标题用字:标题是会变的,用整站词汇留余量,否则新
+ * 标题里的字一旦落在子集外,就会逐字回落系统宋体,同一行出现两种字形。
+ */
+export function collectCharset() {
+ const files = [
+ ...CONTENT_DIRS.flatMap(({ dir, match }) => walk(path.join(ROOT, dir), match)),
+ ...CONTENT_FILES.map((file) => path.join(ROOT, file)),
+ ];
+ const chars = new Set();
+ for (const file of files) {
+ for (const char of fs.readFileSync(file, 'utf8')) if (CJK.test(char)) chars.add(char);
+ }
+ return [...chars].sort().join('');
+}
diff --git a/scripts/check-assets.mjs b/scripts/check-assets.mjs
new file mode 100644
index 0000000..1230187
--- /dev/null
+++ b/scripts/check-assets.mjs
@@ -0,0 +1,61 @@
+#!/usr/bin/env node
+/**
+ * 资源护栏,CI 里跑。
+ *
+ * 1. static/img 单文件不得超过 MAX_IMAGE_BYTES。站点曾经因为一张 2400×741
+ * 的 logo(860KB,实际只渲染 104×32)和 6 张未压缩的地图 PNG(合计
+ * 8.3MB)而首屏极重。压完之后需要一道闸,否则下次有人直接拖张原图进来
+ * 就白做了。
+ * 2. 仓库用字必须落在 scripts/font-charset.txt 之内。超出的字在标题里会
+ * 逐字回落系统宋体,同一行出现两种字形——提示跑 `pnpm fonts` 重生成。
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import { ROOT, CHARSET_MANIFEST, collectCharset } from './charset.mjs';
+
+const MAX_IMAGE_BYTES = 500 * 1024;
+const IMAGE_DIR = path.join(ROOT, 'static', 'img');
+const IMAGE_FILE = /\.(?:avif|gif|jpe?g|png|svg|webp)$/i;
+
+const problems = [];
+
+function checkImages(dir) {
+ if (!fs.existsSync(dir)) return;
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) { checkImages(full); continue; }
+ if (!IMAGE_FILE.test(entry.name)) continue;
+ const size = fs.statSync(full).size;
+ if (size > MAX_IMAGE_BYTES) {
+ problems.push(
+ `图片过大:${path.relative(ROOT, full)} 为 ${(size / 1024).toFixed(0)}KB,` +
+ `上限 ${MAX_IMAGE_BYTES / 1024}KB。请压缩后再提交(截图类内容建议 cwebp -q 82)。`,
+ );
+ }
+ }
+}
+
+function checkCharset() {
+ if (!fs.existsSync(CHARSET_MANIFEST)) {
+ problems.push('缺少 scripts/font-charset.txt,请运行 `pnpm fonts` 生成。');
+ return;
+ }
+ const covered = new Set(fs.readFileSync(CHARSET_MANIFEST, 'utf8'));
+ const missing = [...collectCharset()].filter((char) => !covered.has(char));
+ if (missing.length > 0) {
+ problems.push(
+ `字体子集缺少 ${missing.length} 个字:${missing.slice(0, 30).join('')}` +
+ `${missing.length > 30 ? '…' : ''}\n 这些字出现在标题里时会回落系统宋体。请运行 \`pnpm fonts\` 重新生成子集。`,
+ );
+ }
+}
+
+checkImages(IMAGE_DIR);
+checkCharset();
+
+if (problems.length > 0) {
+ console.error('资源检查未通过:\n');
+ for (const problem of problems) console.error(` ✗ ${problem}\n`);
+ process.exit(1);
+}
+console.log('✓ 资源检查通过');
diff --git a/scripts/font-charset.txt b/scripts/font-charset.txt
new file mode 100644
index 0000000..97a3bfc
--- /dev/null
+++ b/scripts/font-charset.txt
@@ -0,0 +1 @@
+、。《》「」一万丈三上下不与且世丘丛东丢两严个中为主久么义之乐乘九也书了予事二于互五些亡交亦产享亮人什仅介仍从仓他仙代令以们件任伏众优会传伤伴伺似但位低住体何余作你佳使例供依侦侧侵保信修俯倍倒候值假偏停偶储傲僻儿允元充先光免入全八公六共关其具兽内册再冒冕写军冠冥冰冲决况冷净准减凝出击刀刃分切划列则初判利别到制刷刺刻削前剑剧剩力功加务动助劫励势勇包化区匿十升午半华协单占卡卫印即却厅历压原厨去参又及友双反发取受变叠句另叩只召可史右叶号吃各合同名后向否吧含听启吸告员呢周味命和咒咔品哈响哦唤唯啊啸喜喵器噬四回因园困围固国图圣在地场均坏块坪垂型垠埋城基堂塑塔境墙增墨士壮声壳处复外多夜大天失头夺奇奋奏奔奖套好如妨始娱嬉子字存孤它守安完定宝实室害家容宿寂寒寝寸对寻导封射将小少尔尖尘尚尝就尺尽尾局层居屈屏展属屠屡山屿岁岸峡巡工左差己已布带帮常幅幕干平年并幻幽庄序庐库应底度庭延建开异弃式引张弥弦弱弹强归当录形彩影彻彼往征径待律得循微心必志快念态怎思急性总恒恢恭息恶恻悉您悬情惊惜惩想愈意感愿慎懈戏成我戒或战截戮戴所扉手才打扣执扫扬扭找承技把抑抗抢护报抱拉拓拖招拥拦拨择拼拾持指按挑振捕损捡换据掉掌排探接控推描提援搭播操擦支收改攻放故效敌敏救教散数整文斗料斩断新方施旁旅无日旧时明昔星春是显晓晚普景晶暂暗暴曲更替最月有服期木未末本机杀杂权材村束条来板极构果枯架某染查标栏样核根格桃桑档桥桶梢梦棍棒楼概模横樱次欢欺歌止正此步武死殊残段殿毁每比毕毛气氤氲水永求汉汝池污汤汲沉没沦沧油治法波注泰活流浆测浏浓浮海浸消涉涵淡淬深添清渊渐渲渴游湎湖源溢溯滞满滴演漫潜澜瀚火灭灯灰灵灼炮炸点炽烁烈烙烛烟热焚焰然照熔熟燃爆爱父片版牌物牵特状狂独猎猫献率玩环现理瑟璀璨甜生用由甲画界留略疗疾痕痛瘪白百的皆盈盏盒盖盘目直相看真眠眼着睡瞥瞩瞬知短石破砸础确碍碎碑示社祈祛祝神福离种科秒秘秦称移程稿空穿窒窟立竞章端竹笔符第笼等筏筑策签简算管篇类粉粒粗粹精系素索紫累繁红约级纪纳线组细终绍绎经结绘给绚络绝统继绩绪续绯维综绽绿编缚缩网罗罚罩置署群翻者而耗聆聊联聚肉背胜胡能脉脑脚脱腾自至致臻舍航般船艘良艰色节芒花芽苍苏若苦英范茅草荒荫获萤营落著蓄蓝蔓薪藏虔虚虫虽蚀血行衡表被裁装西要见观规觅视览觉角解触言誉警计认让议讯记许论设证评诅译试诗诚询该详语误说请读谁调谋谢谨谷象负败质贯资赋赐赛赢走赴赶起超越趋足跃跑距跨路跳踏踪蹈蹑蹲身躯躲转轻载较辅辉辑输辛辰边达迁过迎运近还这进远连迭述迷迹追退送适逃逆选透逐递途通速造逮逸逻遇遏道遮遵避那邦郁部都配酒酷醒释里重量金鉴针钟钮钻铜铤铭银铸铺锁锋锐错锚锤锦键镇长门闪闭问闲间闸闹阅阈阎阑队防阳阴阵阶阻附际降限除陨险陷随隐隔障难雄雅集雨零雷雾需霜露霾静非靠面音韵页顶顷项顺须预题颜额风飘飞食饮饰首驻驾骁验骤骨髓高魂魄魔鱼鳕鸣鸿黄黑默齐!(),:;>?~
\ No newline at end of file
diff --git a/src/components/Callout.module.css b/src/components/Callout.module.css
index 03eb6ac..2061c87 100644
--- a/src/components/Callout.module.css
+++ b/src/components/Callout.module.css
@@ -4,13 +4,20 @@
gap: 0.75rem;
margin: 1.5rem 0;
padding: 0.9rem 1.1rem;
- border: 1px solid var(--callout-border);
+ border: 1px solid var(--callout-border-l);
border-radius: 8px;
- background: var(--callout-background);
- color: var(--callout-ink);
+ background: var(--callout-bg-l);
+ color: var(--callout-ink-l);
line-height: 1.8;
}
+/* 暗色值随组件一起发出,切换主题纯靠 CSS,不等 React 重渲染。 */
+:global([data-theme='dark']) .callout {
+ border-color: var(--callout-border-d);
+ background: var(--callout-bg-d);
+ color: var(--callout-ink-d);
+}
+
.emoji {
flex: none;
font-size: 1.15em;
diff --git a/src/components/Callout.tsx b/src/components/Callout.tsx
index 34a1084..7d61d0a 100644
--- a/src/components/Callout.tsx
+++ b/src/components/Callout.tsx
@@ -1,18 +1,20 @@
import React, { type CSSProperties, type ReactNode } from 'react';
-import { useColorMode } from '@docusaurus/theme-common';
import { CALLOUT, INK, type CalloutColor } from './theme';
import styles from './Callout.module.css';
export type CalloutProps = { color?: CalloutColor | string; emoji?: ReactNode; children?: ReactNode };
export default function Callout({ color = 'green', emoji, children }: CalloutProps) {
- const { colorMode } = useColorMode();
- const isDark = colorMode === 'dark';
- const palette = (CALLOUT[color as CalloutColor] ?? CALLOUT.green)[isDark ? 'dark' : 'light'];
+ // 明暗两套都发出去,由 Callout.module.css 挑;在 JS 里二选一会让 SSR
+ // 按 defaultMode 烤死一种,另一种主题的读者首屏会闪一下。
+ const palette = CALLOUT[color as CalloutColor] ?? CALLOUT.green;
const style = {
- '--callout-border': palette.border,
- '--callout-background': palette.bg,
- '--callout-ink': isDark ? INK.dark : INK.light,
+ '--callout-border-l': palette.light.border,
+ '--callout-border-d': palette.dark.border,
+ '--callout-bg-l': palette.light.bg,
+ '--callout-bg-d': palette.dark.bg,
+ '--callout-ink-l': INK.light,
+ '--callout-ink-d': INK.dark,
} as CSSProperties & Record<`--${string}`, string>;
return
{emoji && {emoji}}
diff --git a/src/components/ColorTable.tsx b/src/components/ColorTable.tsx
index 92170ce..1e3246f 100644
--- a/src/components/ColorTable.tsx
+++ b/src/components/ColorTable.tsx
@@ -1,5 +1,4 @@
import React, { useId, type ReactNode } from 'react';
-import { useColorMode } from '@docusaurus/theme-common';
import { darkBg } from './colorUtils';
import { INK } from './theme';
@@ -15,15 +14,21 @@ function parse(value: ColorValue): { bg?: string; text?: string } | null {
}
export default function ColorTable({ header, cols, rows, cells, hideHeader, noFirstCol, children }: ColorTableProps) {
- const { colorMode } = useColorMode();
- const isDark = colorMode === 'dark';
const id = useId().replace(/:/g, '');
const selector = `#t${id}`;
const rules: string[] = [];
+ // 明暗两套规则一起写进