docnest 0.1.12 → 0.1.14
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/README.md +17 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +15 -0
- package/dist/server.js +13 -2
- package/docnest.config.example.mjs +2 -0
- package/package.json +1 -1
- package/server/static/css/styles.css +14 -5
- package/server/static/css/themes.css +5 -87
- package/server/static/js/diagram/diagram-viewer.js +42 -31
- package/server/static/js/diagram/diagram-zoom.js +2 -3
package/README.md
CHANGED
|
@@ -78,6 +78,8 @@ export default defineConfig({
|
|
|
78
78
|
auth: {
|
|
79
79
|
enabled: true,
|
|
80
80
|
passphrase: '请替换为项目自己的长口令',
|
|
81
|
+
// 服务通过反向代理挂载在子路径时填写,例如 /documents;默认跳转到 /。
|
|
82
|
+
redirectPrefix: '',
|
|
81
83
|
stateFile: '.docnest/auth.json',
|
|
82
84
|
localStorageKey: 'my-project:auth-passphrase',
|
|
83
85
|
sessionTtlMinutes: 24 * 60,
|
|
@@ -91,6 +93,8 @@ export default defineConfig({
|
|
|
91
93
|
`localStorageKey` 可选,用于为不同宿主项目隔离浏览器存储键。内置授权不提供“修改口令”或“退出”按钮;需要更换口令时,停止服务、删除 `stateFile`,修改配置中的 `passphrase` 后重新启动。
|
|
92
94
|
建议把 `.docnest/` 加入宿主项目的 `.gitignore`,并为口令状态文件设置仅当前用户可读写的权限。
|
|
93
95
|
|
|
96
|
+
如果 DocNest 由反向代理挂载在 `/documents/` 等子路径下,将 `auth.redirectPrefix` 配置为 `/documents`。登录页会把原始文档目标写成带此前缀的路径,口令登录后的服务端跳转也会使用同一前缀;不配置时保持跳转到 `/` 的默认行为。
|
|
97
|
+
|
|
94
98
|
授权开启但既没有状态文件,也没有初始 `passphrase` 时,DocNest 会拒绝启动,避免意外以空口令开放文档。
|
|
95
99
|
|
|
96
100
|
如需显式指定端口、文档目录或监听地址:
|
|
@@ -136,3 +140,16 @@ npm run pack:check
|
|
|
136
140
|
```
|
|
137
141
|
|
|
138
142
|
最低支持 Node.js 20,验证 Node.js 20 和 22。
|
|
143
|
+
|
|
144
|
+
## 发布规范
|
|
145
|
+
|
|
146
|
+
DocNest 的 GitHub Release 和 npm 发布统一由 GitHub Actions 完成,不在本地直接执行 `npm publish`。
|
|
147
|
+
|
|
148
|
+
发布新版本时:
|
|
149
|
+
|
|
150
|
+
1. 更新 `package.json` 和 `package-lock.json` 的版本号,并运行 `npm test` 与 `npm run pack:check`。
|
|
151
|
+
2. 将版本提交推送到 `main`。
|
|
152
|
+
3. 在 `main` 上创建与包版本一致的标签,例如 `v0.1.13`。
|
|
153
|
+
4. 推送该标签:`git push origin v0.1.13`。
|
|
154
|
+
|
|
155
|
+
标签推送后,`.github/workflows/publish.yml` 会校验标签与包版本一致,使用 npm OIDC 发布公开包,并自动创建名为 `DocNest v0.1.13` 的 GitHub Release。远程仓库只保留 `main` 分支,发布标签不需要推送临时开发分支。
|
package/dist/config.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ export type DocNestConfig = {
|
|
|
28
28
|
auth?: {
|
|
29
29
|
enabled?: boolean;
|
|
30
30
|
passphrase?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Prefix to add to authentication return targets when the service is
|
|
33
|
+
* mounted below a reverse-proxy path, for example `/documents`.
|
|
34
|
+
*/
|
|
35
|
+
redirectPrefix?: string;
|
|
31
36
|
stateFile?: string;
|
|
32
37
|
localStorageKey?: string;
|
|
33
38
|
sessionTtlMinutes?: number;
|
|
@@ -49,6 +54,7 @@ export type ResolvedDocNestConfig = Required<DocNestConfig> & {
|
|
|
49
54
|
auth: {
|
|
50
55
|
enabled: boolean;
|
|
51
56
|
passphrase: string;
|
|
57
|
+
redirectPrefix: string;
|
|
52
58
|
stateFile: string;
|
|
53
59
|
localStorageKey: string;
|
|
54
60
|
sessionTtlMinutes: number;
|
package/dist/config.js
CHANGED
|
@@ -45,6 +45,20 @@ function normalizePositiveNumber(value, fallback) {
|
|
|
45
45
|
const normalized = Number(value);
|
|
46
46
|
return Number.isFinite(normalized) && normalized > 0 ? normalized : fallback;
|
|
47
47
|
}
|
|
48
|
+
function normalizeRedirectPrefix(value) {
|
|
49
|
+
if (typeof value !== 'string')
|
|
50
|
+
return '';
|
|
51
|
+
const normalized = value.trim();
|
|
52
|
+
if (!normalized ||
|
|
53
|
+
normalized === '/' ||
|
|
54
|
+
!normalized.startsWith('/') ||
|
|
55
|
+
normalized.startsWith('//') ||
|
|
56
|
+
normalized.includes('//') ||
|
|
57
|
+
normalized.includes('\\') ||
|
|
58
|
+
/[\r\n?#]/.test(normalized))
|
|
59
|
+
return '';
|
|
60
|
+
return normalized.replace(/\/+$/, '') || '';
|
|
61
|
+
}
|
|
48
62
|
function normalizeThemeName(value) {
|
|
49
63
|
if (value === 'current-docs')
|
|
50
64
|
return 'slate-modern';
|
|
@@ -113,6 +127,7 @@ async function loadConfig(projectRoot = process.cwd()) {
|
|
|
113
127
|
auth: {
|
|
114
128
|
enabled: auth.enabled ?? false,
|
|
115
129
|
passphrase: typeof auth.passphrase === 'string' ? auth.passphrase : '',
|
|
130
|
+
redirectPrefix: normalizeRedirectPrefix(auth.redirectPrefix),
|
|
116
131
|
stateFile: node_path_1.default.resolve(projectRoot, auth.stateFile?.trim() || '.docnest/auth.json'),
|
|
117
132
|
localStorageKey: authLocalStorageKey,
|
|
118
133
|
sessionTtlMinutes: normalizePositiveNumber(auth.sessionTtlMinutes, 24 * 60),
|
package/dist/server.js
CHANGED
|
@@ -280,6 +280,17 @@ function safeNextPath(value) {
|
|
|
280
280
|
return '/';
|
|
281
281
|
return value;
|
|
282
282
|
}
|
|
283
|
+
function authRedirectTarget(value) {
|
|
284
|
+
const nextPath = safeNextPath(value);
|
|
285
|
+
const prefix = typeof AUTH_CONFIG.redirectPrefix === 'string' ? AUTH_CONFIG.redirectPrefix : '';
|
|
286
|
+
if (!prefix ||
|
|
287
|
+
nextPath === prefix ||
|
|
288
|
+
nextPath.startsWith(`${prefix}/`) ||
|
|
289
|
+
nextPath.startsWith(`${prefix}?`) ||
|
|
290
|
+
nextPath.startsWith(`${prefix}#`))
|
|
291
|
+
return nextPath;
|
|
292
|
+
return nextPath === '/' ? `${prefix}/` : `${prefix}${nextPath}`;
|
|
293
|
+
}
|
|
283
294
|
function setAuthCookie(res, token) {
|
|
284
295
|
const maxAge = AUTH_MANAGER.sessionMaxAgeSeconds;
|
|
285
296
|
res.setHeader('Set-Cookie', `${AUTH_MANAGER.sessionCookieName}=${encodeURIComponent(token)}; Max-Age=${maxAge}; Path=/; HttpOnly; SameSite=Lax`);
|
|
@@ -292,7 +303,7 @@ function wantsJson(req) {
|
|
|
292
303
|
}
|
|
293
304
|
function renderLoginPage(res, nextPath, error, status = 200) {
|
|
294
305
|
return res.status(status).render('login', {
|
|
295
|
-
nextPath:
|
|
306
|
+
nextPath: authRedirectTarget(nextPath),
|
|
296
307
|
error: error || '',
|
|
297
308
|
assetVersion: ASSET_VERSION,
|
|
298
309
|
});
|
|
@@ -316,7 +327,7 @@ app.post('/auth/verify', (req, res) => {
|
|
|
316
327
|
app.post('/auth/login', (req, res) => {
|
|
317
328
|
if (!BUILT_IN_AUTH_ENABLED)
|
|
318
329
|
return res.redirect('/');
|
|
319
|
-
const nextPath =
|
|
330
|
+
const nextPath = authRedirectTarget(req.body?.next);
|
|
320
331
|
const token = AUTH_MANAGER.createSession(String(req.body?.passphrase || ''));
|
|
321
332
|
if (!token)
|
|
322
333
|
return renderLoginPage(res, nextPath, '口令不正确,请重试。', 401);
|
|
@@ -36,6 +36,8 @@ export default defineConfig({
|
|
|
36
36
|
enabled: false,
|
|
37
37
|
// 仅首次启动且授权状态文件不存在时使用;更换口令需手动删除状态文件后重启。
|
|
38
38
|
passphrase: '',
|
|
39
|
+
// 通过反向代理挂载在子路径时,为登录成功后的目标补此前缀;默认空值跳转到根路径。
|
|
40
|
+
redirectPrefix: '',
|
|
39
41
|
stateFile: '.docnest/auth.json',
|
|
40
42
|
// 前端 localStorage 的键;口令会永久保存在浏览器本地,直到手动清除或口令失效。
|
|
41
43
|
localStorageKey: 'my-project:auth-passphrase',
|
package/package.json
CHANGED
|
@@ -522,6 +522,7 @@ body[data-theme="dark"] .dir-toggle.active {
|
|
|
522
522
|
/* 主内容区 - 白底、留白 */
|
|
523
523
|
.main-content {
|
|
524
524
|
flex: 1;
|
|
525
|
+
min-width: 0;
|
|
525
526
|
overflow-y: auto;
|
|
526
527
|
overflow-x: hidden;
|
|
527
528
|
padding: 2rem 2.5rem;
|
|
@@ -535,6 +536,7 @@ body[data-theme="dark"] .dir-toggle.active {
|
|
|
535
536
|
align-items: flex-start;
|
|
536
537
|
gap: 24px;
|
|
537
538
|
width: min(1400px, 100%);
|
|
539
|
+
min-width: 0;
|
|
538
540
|
margin: 0 auto;
|
|
539
541
|
}
|
|
540
542
|
|
|
@@ -1032,7 +1034,8 @@ body[data-theme="dark"] .dir-toggle.active {
|
|
|
1032
1034
|
/* Markdown 内容 - 限宽、舒适行距;普通显示器上更宽,超宽屏用 92% 避免单行过长 */
|
|
1033
1035
|
.markdown-body {
|
|
1034
1036
|
flex: 1;
|
|
1035
|
-
width:
|
|
1037
|
+
min-width: 0;
|
|
1038
|
+
width: min(var(--doc-reading-width, 1024px), 100%);
|
|
1036
1039
|
max-width: 100%;
|
|
1037
1040
|
margin: 0;
|
|
1038
1041
|
box-sizing: border-box;
|
|
@@ -1050,7 +1053,7 @@ body[data-theme="dark"] .dir-toggle.active {
|
|
|
1050
1053
|
}
|
|
1051
1054
|
|
|
1052
1055
|
.doc-layout {
|
|
1053
|
-
width: min(1024px, 100%);
|
|
1056
|
+
width: min(var(--doc-reading-width, 1024px), 100%);
|
|
1054
1057
|
}
|
|
1055
1058
|
}
|
|
1056
1059
|
|
|
@@ -1254,6 +1257,9 @@ body[data-theme="dark"] .not-found-path {
|
|
|
1254
1257
|
text-align: center;
|
|
1255
1258
|
background: var(--linear-bg-subtle);
|
|
1256
1259
|
padding: 16px;
|
|
1260
|
+
min-width: 0;
|
|
1261
|
+
max-width: 100%;
|
|
1262
|
+
box-sizing: border-box;
|
|
1257
1263
|
border-radius: var(--linear-radius);
|
|
1258
1264
|
border: 1px solid var(--linear-border);
|
|
1259
1265
|
overflow-x: auto;
|
|
@@ -1301,9 +1307,12 @@ body[data-theme="dark"] .markdown-body .mermaid::after {
|
|
|
1301
1307
|
|
|
1302
1308
|
/* 确保 Mermaid SVG 正确显示 */
|
|
1303
1309
|
.markdown-body .mermaid svg {
|
|
1304
|
-
|
|
1305
|
-
|
|
1310
|
+
width: 100% !important;
|
|
1311
|
+
max-width: 100% !important;
|
|
1312
|
+
min-width: 0 !important;
|
|
1313
|
+
height: auto !important;
|
|
1306
1314
|
display: block;
|
|
1315
|
+
margin: 0 auto;
|
|
1307
1316
|
}
|
|
1308
1317
|
|
|
1309
1318
|
/* Mermaid 下载按钮 */
|
|
@@ -2079,7 +2088,7 @@ body[data-theme="dark"] .tree-nav::-webkit-scrollbar-thumb:hover {
|
|
|
2079
2088
|
.markdown-body .mermaid svg {
|
|
2080
2089
|
max-width: 100% !important;
|
|
2081
2090
|
max-height: 75vh !important; /* 高流程图缩放到单页内 */
|
|
2082
|
-
width:
|
|
2091
|
+
width: 100% !important;
|
|
2083
2092
|
height: auto !important;
|
|
2084
2093
|
display: block !important;
|
|
2085
2094
|
margin: 0 auto !important;
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* DocNest theme profiles
|
|
3
3
|
*
|
|
4
4
|
* One semantic DOM, five visual systems. The original DocNest rules remain
|
|
5
|
-
* the Slate Modern baseline
|
|
6
|
-
*
|
|
5
|
+
* the Slate Modern baseline for page geometry (main-content padding,
|
|
6
|
+
* doc-layout max-width/gap, markdown reading width). This file owns theme
|
|
7
|
+
* tokens and visual character without forking templates or client behavior.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
:root {
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
--doc-code-bg: #f6f7f8;
|
|
20
21
|
--doc-code-text: #24262b;
|
|
21
22
|
--doc-header-height: auto;
|
|
23
|
+
/* Shared with Slate Modern; theme profiles must not override page geometry. */
|
|
22
24
|
--doc-reading-width: 1024px;
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -405,7 +407,6 @@ body[data-doc-theme="editorial-atlas"] {
|
|
|
405
407
|
"Microsoft YaHei", sans-serif;
|
|
406
408
|
--doc-font-reading: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
|
|
407
409
|
--doc-font-display: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
|
|
408
|
-
--doc-reading-width: 860px;
|
|
409
410
|
}
|
|
410
411
|
|
|
411
412
|
body[data-doc-theme="editorial-atlas"][data-theme="dark"] {
|
|
@@ -452,18 +453,7 @@ body[data-doc-theme="editorial-atlas"] .sidebar {
|
|
|
452
453
|
border-right-color: var(--doc-rule-strong);
|
|
453
454
|
}
|
|
454
455
|
|
|
455
|
-
body[data-doc-theme="editorial-atlas"] .main-content {
|
|
456
|
-
padding: clamp(2rem, 4vw, 4.75rem) clamp(1.5rem, 5vw, 5.5rem);
|
|
457
|
-
background: var(--linear-bg);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
body[data-doc-theme="editorial-atlas"] .doc-layout {
|
|
461
|
-
width: min(1240px, 100%);
|
|
462
|
-
gap: 42px;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
456
|
body[data-doc-theme="editorial-atlas"] .markdown-body {
|
|
466
|
-
width: min(var(--doc-reading-width), 100%);
|
|
467
457
|
background: transparent;
|
|
468
458
|
font-size: 1rem;
|
|
469
459
|
line-height: 1.9;
|
|
@@ -520,7 +510,6 @@ body[data-doc-theme="precision-index"] {
|
|
|
520
510
|
"Microsoft YaHei", sans-serif;
|
|
521
511
|
--doc-font-reading: var(--doc-font-ui);
|
|
522
512
|
--doc-font-display: var(--doc-font-ui);
|
|
523
|
-
--doc-reading-width: 980px;
|
|
524
513
|
}
|
|
525
514
|
|
|
526
515
|
body[data-doc-theme="precision-index"][data-theme="dark"] {
|
|
@@ -597,28 +586,17 @@ body[data-doc-theme="precision-index"] .file-link.active {
|
|
|
597
586
|
background: var(--doc-surface-raised);
|
|
598
587
|
}
|
|
599
588
|
|
|
600
|
-
body[data-doc-theme="precision-index"] .main-content {
|
|
601
|
-
padding: 0;
|
|
602
|
-
background: var(--linear-bg);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
589
|
body[data-doc-theme="precision-index"] .breadcrumbs {
|
|
606
|
-
margin: 0;
|
|
607
|
-
padding: 0.75rem 1.5rem;
|
|
608
590
|
border-bottom: 1px solid var(--doc-rule-strong);
|
|
609
591
|
background: var(--linear-bg-subtle);
|
|
610
592
|
font-size: 0.6875rem;
|
|
611
593
|
}
|
|
612
594
|
|
|
613
595
|
body[data-doc-theme="precision-index"] .doc-layout {
|
|
614
|
-
width: 100%;
|
|
615
|
-
gap: 0;
|
|
616
596
|
background: var(--doc-surface-raised);
|
|
617
597
|
}
|
|
618
598
|
|
|
619
599
|
body[data-doc-theme="precision-index"] .markdown-body {
|
|
620
|
-
width: min(var(--doc-reading-width), 100%);
|
|
621
|
-
padding: clamp(2.5rem, 5vw, 5rem);
|
|
622
600
|
border-right: 1px solid var(--doc-rule-strong);
|
|
623
601
|
font-size: 0.9rem;
|
|
624
602
|
line-height: 1.75;
|
|
@@ -651,11 +629,6 @@ body[data-doc-theme="precision-index"] .markdown-body h2 {
|
|
|
651
629
|
}
|
|
652
630
|
|
|
653
631
|
body[data-doc-theme="precision-index"] .doc-toc {
|
|
654
|
-
top: 0;
|
|
655
|
-
width: 216px;
|
|
656
|
-
flex-basis: 216px;
|
|
657
|
-
max-height: calc(100vh - 54px);
|
|
658
|
-
padding: 1.25rem;
|
|
659
632
|
border: 0;
|
|
660
633
|
border-radius: 0;
|
|
661
634
|
background: var(--linear-bg-subtle);
|
|
@@ -666,12 +639,6 @@ body[data-doc-theme="precision-index"] .doc-toc-title {
|
|
|
666
639
|
text-transform: uppercase;
|
|
667
640
|
}
|
|
668
641
|
|
|
669
|
-
body[data-doc-theme="precision-index"] .dir-listing-page,
|
|
670
|
-
body[data-doc-theme="precision-index"] .welcome {
|
|
671
|
-
margin: 0;
|
|
672
|
-
padding: clamp(2rem, 5vw, 5rem);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
642
|
/* Theme 04: Archive Room */
|
|
676
643
|
body[data-doc-theme="archive-room"] {
|
|
677
644
|
--linear-bg: #f3efe4;
|
|
@@ -693,7 +660,6 @@ body[data-doc-theme="archive-room"] {
|
|
|
693
660
|
"Microsoft YaHei", sans-serif;
|
|
694
661
|
--doc-font-reading: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
|
|
695
662
|
--doc-font-display: "Songti SC", SimSun, "Noto Serif CJK SC", serif;
|
|
696
|
-
--doc-reading-width: 900px;
|
|
697
663
|
}
|
|
698
664
|
|
|
699
665
|
body[data-doc-theme="archive-room"][data-theme="dark"] {
|
|
@@ -761,7 +727,6 @@ body[data-doc-theme="archive-room"][data-theme="dark"] .file-link.active {
|
|
|
761
727
|
}
|
|
762
728
|
|
|
763
729
|
body[data-doc-theme="archive-room"] .main-content {
|
|
764
|
-
padding: clamp(2rem, 4vw, 4.5rem);
|
|
765
730
|
background-color: var(--linear-bg);
|
|
766
731
|
background-image:
|
|
767
732
|
linear-gradient(rgba(93, 96, 85, 0.045) 1px, transparent 1px),
|
|
@@ -769,14 +734,8 @@ body[data-doc-theme="archive-room"] .main-content {
|
|
|
769
734
|
background-size: 24px 24px;
|
|
770
735
|
}
|
|
771
736
|
|
|
772
|
-
body[data-doc-theme="archive-room"] .doc-layout {
|
|
773
|
-
width: min(1280px, 100%);
|
|
774
|
-
gap: 34px;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
737
|
body[data-doc-theme="archive-room"] .markdown-body {
|
|
778
|
-
|
|
779
|
-
padding: clamp(1.5rem, 3vw, 3rem);
|
|
738
|
+
padding: 1.5rem 1.5rem 3rem;
|
|
780
739
|
border: 1px solid var(--doc-rule-strong);
|
|
781
740
|
background: var(--doc-surface-raised);
|
|
782
741
|
box-shadow: 7px 7px 0 var(--linear-border);
|
|
@@ -836,7 +795,6 @@ body[data-doc-theme="swiss-manual"] {
|
|
|
836
795
|
"Microsoft YaHei", sans-serif;
|
|
837
796
|
--doc-font-reading: var(--doc-font-ui);
|
|
838
797
|
--doc-font-display: var(--doc-font-ui);
|
|
839
|
-
--doc-reading-width: 960px;
|
|
840
798
|
}
|
|
841
799
|
|
|
842
800
|
body[data-doc-theme="swiss-manual"][data-theme="dark"] {
|
|
@@ -939,27 +897,13 @@ body[data-doc-theme="swiss-manual"] .file-link.active {
|
|
|
939
897
|
font-weight: 800;
|
|
940
898
|
}
|
|
941
899
|
|
|
942
|
-
body[data-doc-theme="swiss-manual"] .main-content {
|
|
943
|
-
padding: 0;
|
|
944
|
-
background: var(--doc-surface-raised);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
900
|
body[data-doc-theme="swiss-manual"] .breadcrumbs {
|
|
948
|
-
margin: 0;
|
|
949
|
-
padding: 0.85rem 1.5rem;
|
|
950
901
|
border-bottom: 1px solid var(--doc-rule-strong);
|
|
951
902
|
background: var(--linear-bg-subtle);
|
|
952
903
|
font-size: 0.65rem;
|
|
953
904
|
}
|
|
954
905
|
|
|
955
|
-
body[data-doc-theme="swiss-manual"] .doc-layout {
|
|
956
|
-
width: 100%;
|
|
957
|
-
gap: 0;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
906
|
body[data-doc-theme="swiss-manual"] .markdown-body {
|
|
961
|
-
width: min(var(--doc-reading-width), 100%);
|
|
962
|
-
padding: clamp(2.5rem, 6vw, 6rem);
|
|
963
907
|
border-right: 1px solid var(--doc-rule-strong);
|
|
964
908
|
font-size: 0.95rem;
|
|
965
909
|
line-height: 1.72;
|
|
@@ -1000,22 +944,11 @@ body[data-doc-theme="swiss-manual"] .markdown-body blockquote {
|
|
|
1000
944
|
}
|
|
1001
945
|
|
|
1002
946
|
body[data-doc-theme="swiss-manual"] .doc-toc {
|
|
1003
|
-
top: 0;
|
|
1004
|
-
width: 220px;
|
|
1005
|
-
flex-basis: 220px;
|
|
1006
|
-
max-height: calc(100vh - 72px);
|
|
1007
|
-
padding: 1.25rem;
|
|
1008
947
|
border: 0;
|
|
1009
948
|
border-radius: 0;
|
|
1010
949
|
background: var(--linear-bg-subtle);
|
|
1011
950
|
}
|
|
1012
951
|
|
|
1013
|
-
body[data-doc-theme="swiss-manual"] .dir-listing-page,
|
|
1014
|
-
body[data-doc-theme="swiss-manual"] .welcome {
|
|
1015
|
-
margin: 0;
|
|
1016
|
-
padding: clamp(2rem, 6vw, 6rem);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
952
|
/* Shared component refinements for the four independent profiles. */
|
|
1020
953
|
body:not([data-doc-theme="slate-modern"]) .markdown-body code:not(pre code) {
|
|
1021
954
|
border: 1px solid var(--linear-border);
|
|
@@ -1206,26 +1139,11 @@ body[data-doc-theme="swiss-manual"].not-found-page .deep-space-404__code {
|
|
|
1206
1139
|
overflow-y: auto;
|
|
1207
1140
|
}
|
|
1208
1141
|
|
|
1209
|
-
body[data-doc-theme="editorial-atlas"] .main-content,
|
|
1210
|
-
body[data-doc-theme="archive-room"] .main-content {
|
|
1211
|
-
padding: 1.25rem;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
1142
|
body[data-doc-theme="archive-room"] .markdown-body {
|
|
1215
1143
|
padding: 1.25rem;
|
|
1216
1144
|
box-shadow: 4px 4px 0 var(--linear-border);
|
|
1217
1145
|
}
|
|
1218
1146
|
|
|
1219
|
-
body[data-doc-theme="precision-index"] .main-content,
|
|
1220
|
-
body[data-doc-theme="swiss-manual"] .main-content {
|
|
1221
|
-
padding: 0;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
body[data-doc-theme="precision-index"] .markdown-body,
|
|
1225
|
-
body[data-doc-theme="swiss-manual"] .markdown-body {
|
|
1226
|
-
padding: 1.25rem;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
1147
|
body[data-doc-theme="precision-index"] .markdown-body h1,
|
|
1230
1148
|
body[data-doc-theme="swiss-manual"] .markdown-body h1 {
|
|
1231
1149
|
font-size: clamp(2rem, 12vw, 3.25rem);
|
|
@@ -8,6 +8,41 @@ function isDocNestRestrictedMode() {
|
|
|
8
8
|
return !!(window.DOCNEST_CONFIG && window.DOCNEST_CONFIG.restrictedMode === true);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function parseSvgAbsoluteDimension(value) {
|
|
12
|
+
if (typeof value !== 'string' || !/^\s*\d*\.?\d+(?:e[+-]?\d+)?(?:px)?\s*$/i.test(value)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const dimension = Number.parseFloat(value);
|
|
17
|
+
return Number.isFinite(dimension) && dimension > 0 ? dimension : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDiagramIntrinsicDimensions(svgElement) {
|
|
21
|
+
const viewBox = svgElement.getAttribute('viewBox');
|
|
22
|
+
if (viewBox) {
|
|
23
|
+
const parts = viewBox.trim().split(/[\s,]+/).map(Number);
|
|
24
|
+
const viewBoxWidth = parts[2];
|
|
25
|
+
const viewBoxHeight = parts[3];
|
|
26
|
+
if (
|
|
27
|
+
parts.length === 4 &&
|
|
28
|
+
parts.every(Number.isFinite) &&
|
|
29
|
+
viewBoxWidth > 0 &&
|
|
30
|
+
viewBoxHeight > 0
|
|
31
|
+
) {
|
|
32
|
+
// viewBox 是图表的实际坐标范围,优先于 Mermaid 的 width="100%"。
|
|
33
|
+
return { width: viewBoxWidth, height: viewBoxHeight };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const width = parseSvgAbsoluteDimension(svgElement.getAttribute('width'));
|
|
38
|
+
const height = parseSvgAbsoluteDimension(svgElement.getAttribute('height'));
|
|
39
|
+
if (width && height) return { width, height };
|
|
40
|
+
if (width) return { width, height: width * 0.75 };
|
|
41
|
+
if (height) return { width: height * (4 / 3), height };
|
|
42
|
+
|
|
43
|
+
return { width: 800, height: 600 };
|
|
44
|
+
}
|
|
45
|
+
|
|
11
46
|
function setupDiagramViewer() {
|
|
12
47
|
if (isDocNestRestrictedMode()) return;
|
|
13
48
|
const diagrams = document.querySelectorAll('.markdown-body .mermaid');
|
|
@@ -49,39 +84,16 @@ function openDiagramViewer(svgElement) {
|
|
|
49
84
|
|
|
50
85
|
if (!viewer || !container) return;
|
|
51
86
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
let originalHeight = parseFloat(svgElement.getAttribute('height'));
|
|
55
|
-
|
|
56
|
-
// 如果没有 width/height,尝试从 viewBox 获取
|
|
57
|
-
if (!originalWidth || !originalHeight || isNaN(originalWidth) || isNaN(originalHeight)) {
|
|
58
|
-
const viewBox = svgElement.getAttribute('viewBox');
|
|
59
|
-
if (viewBox) {
|
|
60
|
-
const parts = viewBox.split(/\s+/);
|
|
61
|
-
if (parts.length >= 4) {
|
|
62
|
-
originalWidth = parseFloat(parts[2]) || 800;
|
|
63
|
-
originalHeight = parseFloat(parts[3]) || 600;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 如果还是没有,使用默认值
|
|
69
|
-
if (!originalWidth || !originalHeight || isNaN(originalWidth) || isNaN(originalHeight)) {
|
|
70
|
-
originalWidth = 800;
|
|
71
|
-
originalHeight = 600;
|
|
72
|
-
}
|
|
87
|
+
// viewBox 才是图表的实际尺寸;不能把 Mermaid 的 width="100%" 解析成 100。
|
|
88
|
+
const { width: originalWidth, height: originalHeight } = getDiagramIntrinsicDimensions(svgElement);
|
|
73
89
|
|
|
74
90
|
// 设置 SVG 样式,确保充分利用空间
|
|
75
91
|
svgElement.setAttribute('class', 'diagram-viewer-svg');
|
|
76
92
|
// 清除之前的样式,但保留必要的属性
|
|
77
93
|
svgElement.removeAttribute('style');
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
if (!svgElement.getAttribute('height')) {
|
|
83
|
-
svgElement.setAttribute('height', originalHeight);
|
|
84
|
-
}
|
|
94
|
+
// 查看器需要固定的像素基准;内嵌图仍然保持响应式,不会被这个克隆影响。
|
|
95
|
+
svgElement.setAttribute('width', originalWidth);
|
|
96
|
+
svgElement.setAttribute('height', originalHeight);
|
|
85
97
|
|
|
86
98
|
// 包装元素本身还包含边界 padding,因此适配比例必须把容器 padding 和图表边界一并算入。
|
|
87
99
|
const padding = 40;
|
|
@@ -205,9 +217,8 @@ function resetDiagramZoom() {
|
|
|
205
217
|
const wrapper = container.querySelector('.diagram-viewer-svg-wrapper');
|
|
206
218
|
if (!wrapper) return;
|
|
207
219
|
|
|
208
|
-
//
|
|
209
|
-
const originalWidth
|
|
210
|
-
const originalHeight = parseFloat(currentSvg.style.height) || parseFloat(currentSvg.getAttribute('height')) || 600;
|
|
220
|
+
// 始终从统一的尺寸解析逻辑获取原始尺寸,避免缩放/重置再次误读百分比宽度。
|
|
221
|
+
const { width: originalWidth, height: originalHeight } = getDiagramIntrinsicDimensions(currentSvg);
|
|
211
222
|
|
|
212
223
|
const padding = 40; // 边界 padding 大小(像素)
|
|
213
224
|
currentZoom = defaultZoom;
|
|
@@ -8,9 +8,8 @@ function zoomDiagram(direction) {
|
|
|
8
8
|
const wrapper = container.querySelector('.diagram-viewer-svg-wrapper');
|
|
9
9
|
if (!wrapper) return;
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
const originalWidth
|
|
13
|
-
const originalHeight = parseFloat(currentSvg.style.height) || parseFloat(currentSvg.getAttribute('height')) || 600;
|
|
11
|
+
// 与查看器使用同一套尺寸解析逻辑,不能把 width="100%" 解析成 100。
|
|
12
|
+
const { width: originalWidth, height: originalHeight } = getDiagramIntrinsicDimensions(currentSvg);
|
|
14
13
|
|
|
15
14
|
// 边界 padding 大小(与 diagram-viewer.js 中保持一致)
|
|
16
15
|
const padding = 40;
|