arcshape 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/README.md +91 -2
- package/bin/arcshape.mjs +93 -0
- package/bin/confluence-pages.mjs +569 -0
- package/bin/confluence.mjs +263 -0
- package/bin/init.mjs +160 -0
- package/bin/product-citations.mjs +104 -0
- package/bin/session.mjs +753 -0
- package/bin/site.mjs +59 -0
- package/bin/sync.mjs +352 -0
- package/bin/validate-links.mjs +98 -0
- package/bin/validate.mjs +126 -0
- package/package.json +62 -4
- package/templates/architecture/01-introduction-and-goals.md +13 -0
- package/templates/architecture/02-constraints.md +13 -0
- package/templates/architecture/03-context-and-scope.md +11 -0
- package/templates/architecture/04-solution-strategy.md +13 -0
- package/templates/architecture/05-building-block-view.md +11 -0
- package/templates/architecture/06-runtime-view.md +9 -0
- package/templates/architecture/07-deployment-view.md +11 -0
- package/templates/architecture/08-crosscutting-concepts.md +13 -0
- package/templates/architecture/09-architecture-decisions.md +9 -0
- package/templates/architecture/10-quality-requirements.md +11 -0
- package/templates/architecture/11-risks-and-technical-debt.md +11 -0
- package/templates/architecture/12-glossary.md +10 -0
- package/templates/architecture/README.md +20 -0
- package/templates/assistants/.agents/skills/arcshape-adr/SKILL.md +16 -0
- package/templates/assistants/.agents/skills/arcshape-architecture/SKILL.md +35 -0
- package/templates/assistants/.agents/skills/arcshape-c4/SKILL.md +13 -0
- package/templates/assistants/.agents/skills/arcshape-docs-sync/SKILL.md +12 -0
- package/templates/assistants/.claude/commands/arcshape/adr.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/c4.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/document.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/reverse-engineer.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/sync.md +5 -0
- package/templates/assistants/.claude/skills/arcshape-adr/SKILL.md +16 -0
- package/templates/assistants/.claude/skills/arcshape-architecture/SKILL.md +35 -0
- package/templates/assistants/.claude/skills/arcshape-c4/SKILL.md +13 -0
- package/templates/assistants/.claude/skills/arcshape-docs-sync/SKILL.md +12 -0
- package/tools/arc42-site/LICENSE +21 -0
- package/tools/arc42-site/NOTICE.md +3 -0
- package/tools/arc42-site/README.md +3 -0
- package/tools/arc42-site/browser-options.mjs +14 -0
- package/tools/arc42-site/capture-evidence.mjs +84 -0
- package/tools/arc42-site/generate.mjs +227 -0
- package/tools/arc42-site/loadDocs.mjs +196 -0
- package/tools/arc42-site/render.mjs +411 -0
- package/tools/arc42-site/shell.mjs +761 -0
- package/tools/arc42-site/validate-diagrams.mjs +27 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shell.mjs — build the single self-contained HTML page.
|
|
3
|
+
*
|
|
4
|
+
* Structure mirrors productshape's buildSnapshotHtml:
|
|
5
|
+
* - one inline <style> with light + dark design tokens;
|
|
6
|
+
* - one inert JSON data block;
|
|
7
|
+
* - one ES5 application script with a single hash router;
|
|
8
|
+
* - no external resources, no forms, nothing persisted outside the address.
|
|
9
|
+
*/
|
|
10
|
+
import { escapeHtml as esc } from "./render.mjs";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
/* ------------------------------------------------------------------ */
|
|
14
|
+
/* Design tokens: a calm engineering instrument in two appearances. */
|
|
15
|
+
/* ------------------------------------------------------------------ */
|
|
16
|
+
const lightBlock = ` --bg: #fbfbfa;
|
|
17
|
+
--panel: #f2f2ef;
|
|
18
|
+
--raise: #ffffff;
|
|
19
|
+
--ink: #1c1e21;
|
|
20
|
+
--text: #33373d;
|
|
21
|
+
--muted: #6b7078;
|
|
22
|
+
--line: #dcdcd6;
|
|
23
|
+
--line-strong: #b9bab2;
|
|
24
|
+
--accent: #0b5cad;
|
|
25
|
+
--accent-soft: #e3edf7;
|
|
26
|
+
--mark: #ffe9a8;
|
|
27
|
+
--code-bg: #f0efe9;`;
|
|
28
|
+
|
|
29
|
+
const darkBlock = ` --bg: #16181b;
|
|
30
|
+
--panel: #1d2024;
|
|
31
|
+
--raise: #24272c;
|
|
32
|
+
--ink: #e8eaed;
|
|
33
|
+
--text: #c6cad0;
|
|
34
|
+
--muted: #8d939c;
|
|
35
|
+
--line: #33373d;
|
|
36
|
+
--line-strong: #4a4f57;
|
|
37
|
+
--accent: #6cb2eb;
|
|
38
|
+
--accent-soft: #22303c;
|
|
39
|
+
--mark: #6a5b1e;
|
|
40
|
+
--code-bg: #22252a;
|
|
41
|
+
--cat-quality: #f27ec2;
|
|
42
|
+
--cat-views: #43c4da;
|
|
43
|
+
--cat-concepts: #e3b544;
|
|
44
|
+
--cat-other: #a9b2c5;`;
|
|
45
|
+
|
|
46
|
+
const lightBlockWithSectionColors = `${lightBlock}
|
|
47
|
+
--cat-quality: #b0287a;
|
|
48
|
+
--cat-views: #0b7285;
|
|
49
|
+
--cat-concepts: #8a6100;
|
|
50
|
+
--cat-other: #56607a;`;
|
|
51
|
+
|
|
52
|
+
const SECTION_ICONS = [
|
|
53
|
+
'<circle cx="8" cy="8" r="5.5"/><circle cx="8" cy="8" r="2"/><path d="M8 1v2M15 8h-2M8 15v-2M1 8h2"/>',
|
|
54
|
+
'<path d="M3 4h10M3 8h10M3 12h10"/><circle cx="6" cy="4" r="1.4"/><circle cx="10" cy="8" r="1.4"/><circle cx="7" cy="12" r="1.4"/>',
|
|
55
|
+
'<rect x="2" y="3" width="5" height="5" rx="1"/><rect x="9" y="8" width="5" height="5" rx="1"/><path d="M7 5.5h2v5H9"/>',
|
|
56
|
+
'<circle cx="8" cy="8" r="6"/><path d="m8 2 1.2 5.2L14 8l-5.2 1.2L8 14l-1.2-4.8L2 8l4.8-.8z"/>',
|
|
57
|
+
'<path d="m2 5 6-3 6 3-6 3zM2 5v6l6 3 6-3V5M8 8v6"/>',
|
|
58
|
+
'<circle cx="3" cy="8" r="1.5"/><circle cx="13" cy="4" r="1.5"/><circle cx="13" cy="12" r="1.5"/><path d="m4.5 7.5 7-3M4.5 8.5l7 3"/>',
|
|
59
|
+
'<rect x="2" y="2" width="12" height="4" rx="1"/><rect x="2" y="7" width="12" height="4" rx="1"/><rect x="2" y="12" width="12" height="2" rx="1"/>',
|
|
60
|
+
'<path d="m8 2 6 3-6 3-6-3zM2 8l6 3 6-3M2 11l6 3 6-3"/>',
|
|
61
|
+
'<path d="M3 3v10h10M5 10l3-3 2 2 4-5M11 4h3v3"/>',
|
|
62
|
+
'<path d="M2 12a6 6 0 1 1 12 0M8 10l3-4M4 12h8"/><circle cx="8" cy="10" r="1"/>',
|
|
63
|
+
'<path d="m8 2 6 11H2zM8 6v3.5M8 11.5v.1"/>',
|
|
64
|
+
'<path d="M3 2.5h7l3 3V14H3zM10 2.5V6h3M5 8h6M5 10h6M5 12h4"/>',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const SECTION_CODES = [
|
|
68
|
+
"GOAL",
|
|
69
|
+
"CONS",
|
|
70
|
+
"CTX",
|
|
71
|
+
"STRAT",
|
|
72
|
+
"BLDG",
|
|
73
|
+
"RUN",
|
|
74
|
+
"DEP",
|
|
75
|
+
"CONC",
|
|
76
|
+
"ADR",
|
|
77
|
+
"QUAL",
|
|
78
|
+
"RISK",
|
|
79
|
+
"TERM",
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
function sectionCategory(number) {
|
|
83
|
+
const n = Number(number);
|
|
84
|
+
if (n <= 3 || n === 10) return "quality";
|
|
85
|
+
if (n >= 5 && n <= 7) return "views";
|
|
86
|
+
if (n >= 4 && n <= 9) return "concepts";
|
|
87
|
+
return "other";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sectionIcon(number) {
|
|
91
|
+
const index = Number(number) - 1;
|
|
92
|
+
return `<svg viewBox="0 0 16 16" aria-hidden="true">${SECTION_ICONS[index] ?? SECTION_ICONS[11]}</svg>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const style = `
|
|
96
|
+
:root {
|
|
97
|
+
${lightBlockWithSectionColors}
|
|
98
|
+
--sans: system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif;
|
|
99
|
+
--mono: ui-monospace, SFMono-Regular, 'SF Mono', 'Cascadia Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
100
|
+
color-scheme: light;
|
|
101
|
+
}
|
|
102
|
+
@media (prefers-color-scheme: dark) {
|
|
103
|
+
:root:not([data-appearance='light']) {
|
|
104
|
+
${darkBlock}
|
|
105
|
+
color-scheme: dark;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
:root[data-appearance='dark'] {
|
|
109
|
+
${darkBlock}
|
|
110
|
+
color-scheme: dark;
|
|
111
|
+
}
|
|
112
|
+
* { box-sizing: border-box; }
|
|
113
|
+
[hidden] { display: none !important; }
|
|
114
|
+
html, body { height: 100%; }
|
|
115
|
+
body {
|
|
116
|
+
margin: 0; display: flex; flex-direction: column; overflow: hidden;
|
|
117
|
+
font: 15px/1.6 var(--sans); color: var(--text); background: var(--bg);
|
|
118
|
+
-webkit-text-size-adjust: 100%;
|
|
119
|
+
}
|
|
120
|
+
h1, h2, h3, h4, h5, h6 { color: var(--ink); line-height: 1.3; }
|
|
121
|
+
a { color: var(--accent); text-decoration: none; }
|
|
122
|
+
a:hover { text-decoration: underline; }
|
|
123
|
+
button { font: inherit; color: inherit; }
|
|
124
|
+
code, kbd, .rev { font-family: var(--mono); font-size: 0.88em; }
|
|
125
|
+
code { background: var(--code-bg); border: 1px solid var(--line); border-radius: 3px; padding: 0.05em 0.35em; }
|
|
126
|
+
pre { background: var(--panel); border: 1px solid var(--line); border-radius: 4px; padding: 0.6rem 0.8rem; overflow-x: auto; font-size: 0.85rem; line-height: 1.5; }
|
|
127
|
+
pre code { background: none; border: none; padding: 0; font-size: inherit; }
|
|
128
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
129
|
+
|
|
130
|
+
.skip {
|
|
131
|
+
position: absolute; left: -9999px; top: 0; z-index: 20;
|
|
132
|
+
background: var(--raise); color: var(--ink); padding: 0.4rem 0.8rem;
|
|
133
|
+
border: 1px solid var(--line-strong);
|
|
134
|
+
}
|
|
135
|
+
.skip:focus { left: 0.5rem; top: 0.5rem; }
|
|
136
|
+
|
|
137
|
+
/* ---- Header ---- */
|
|
138
|
+
header.site {
|
|
139
|
+
display: flex; align-items: center; gap: 0.9rem; flex: none;
|
|
140
|
+
padding: 0.45rem 1rem; border-bottom: 1px solid var(--line);
|
|
141
|
+
background: var(--raise);
|
|
142
|
+
}
|
|
143
|
+
header.site h1 { margin: 0; min-width: 0; font-size: 0.98rem; overflow-wrap: anywhere; }
|
|
144
|
+
.revshort {
|
|
145
|
+
font-family: var(--mono); font-size: 0.74rem; color: var(--muted);
|
|
146
|
+
border: 1px solid var(--line); border-radius: 3px; padding: 0.05rem 0.4rem; white-space: nowrap;
|
|
147
|
+
}
|
|
148
|
+
header.site .spacer { flex: 1; }
|
|
149
|
+
.iconbtn {
|
|
150
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
151
|
+
flex: none;
|
|
152
|
+
width: 1.7rem; height: 1.7rem; border: 1px solid var(--line-strong); border-radius: 4px;
|
|
153
|
+
background: var(--raise); cursor: pointer; color: var(--muted);
|
|
154
|
+
}
|
|
155
|
+
.iconbtn:hover { color: var(--ink); border-color: var(--accent); }
|
|
156
|
+
.appearance { display: inline-flex; flex: none; border: 1px solid var(--line-strong); border-radius: 4px; overflow: hidden; }
|
|
157
|
+
.appearance button {
|
|
158
|
+
border: none; background: var(--raise); color: var(--muted); cursor: pointer;
|
|
159
|
+
padding: 0.15rem 0.5rem; font-size: 0.78rem;
|
|
160
|
+
}
|
|
161
|
+
.appearance button + button { border-left: 1px solid var(--line); }
|
|
162
|
+
.appearance button[aria-pressed='true'] { background: var(--accent-soft); color: var(--ink); }
|
|
163
|
+
|
|
164
|
+
/* ---- Layout ---- */
|
|
165
|
+
#shell { flex: 1 1 auto; min-height: 0; display: flex; }
|
|
166
|
+
aside.nav {
|
|
167
|
+
position:relative; flex:none; width:17.5rem; overflow-y:auto; border-right:1px solid var(--line);
|
|
168
|
+
background: var(--panel); padding: 0.9rem 0.75rem;
|
|
169
|
+
transition:width .18s ease,padding .18s ease;
|
|
170
|
+
}
|
|
171
|
+
main#main { flex: 1 1 auto; min-width: 0; overflow-y: auto; padding: 1.4rem 2.2rem 4rem; scroll-behavior: smooth; }
|
|
172
|
+
.doc { max-width: 78ch; margin: 0 auto; }
|
|
173
|
+
.doc > h2.doc-title { margin: 0 0 0.2rem; font-size: 1.45rem; }
|
|
174
|
+
p.doc-desc { margin: 0 0 1.2rem; color: var(--muted); font-size: 0.9rem; }
|
|
175
|
+
|
|
176
|
+
/* ---- Sidebar ---- */
|
|
177
|
+
aside.nav .nav-heading { display:flex; align-items:center; justify-content:space-between; gap:.5rem; margin:0 0 .5rem; }
|
|
178
|
+
aside.nav h2 { margin:0; font-size:.72rem; text-transform:uppercase; letter-spacing:.08em; color:var(--muted); }
|
|
179
|
+
aside.nav ol { list-style: none; margin: 0; padding: 0; }
|
|
180
|
+
aside.nav li { margin: 0; border-bottom: 1px solid var(--line); }
|
|
181
|
+
aside.nav a {
|
|
182
|
+
display:flex; gap:.55rem; align-items:center; padding:.32rem .4rem;
|
|
183
|
+
color: var(--text); border-left: 2px solid transparent;
|
|
184
|
+
}
|
|
185
|
+
aside.nav a:hover { color: var(--ink); text-decoration: none; background: var(--raise); }
|
|
186
|
+
aside.nav a[aria-current='page'] { color: var(--ink); border-left-color: var(--accent); background: var(--raise); }
|
|
187
|
+
aside.nav .nav-icon { display:inline-flex; flex:none; width:1rem; height:1rem; color:var(--category); }
|
|
188
|
+
aside.nav .nav-icon svg, .prevnext svg { width:100%; height:100%; fill:none; stroke:currentColor; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
|
|
189
|
+
aside.nav a[data-category="quality"], .prevnext a[data-category="quality"] { --category:var(--cat-quality); }
|
|
190
|
+
aside.nav a[data-category="views"], .prevnext a[data-category="views"] { --category:var(--cat-views); }
|
|
191
|
+
aside.nav a[data-category="concepts"], .prevnext a[data-category="concepts"] { --category:var(--cat-concepts); }
|
|
192
|
+
aside.nav a[data-category="other"], .prevnext a[data-category="other"] { --category:var(--cat-other); }
|
|
193
|
+
aside.nav .nav-code { display:none; font:600 .58rem/1 var(--mono); color:var(--category); }
|
|
194
|
+
aside.nav .t { flex:1 1 auto; min-width:0; font-size:.86rem; }
|
|
195
|
+
.nav-foot { margin-top: 1rem; padding-top: 0.6rem; border-top: 1px solid var(--line); font-size: 0.78rem; color: var(--muted); }
|
|
196
|
+
.nav-foot .prevnext { display: flex; justify-content: space-between; gap: 0.5rem; }
|
|
197
|
+
.nav-foot .prevnext a { font-size: 0.78rem; }
|
|
198
|
+
.prevnext a { display:flex; align-items:flex-start; gap:.35rem; max-width:48%; color:var(--category); }
|
|
199
|
+
.prevnext .nav-icon { display:inline-flex; width:.9rem; height:.9rem; flex:none; margin-top:.15rem; }
|
|
200
|
+
body.nav-collapsed aside.nav { width:4.25rem; padding:.9rem .55rem; }
|
|
201
|
+
body.nav-collapsed aside.nav .nav-heading { justify-content:center; }
|
|
202
|
+
body.nav-collapsed aside.nav h2, body.nav-collapsed aside.nav .nav-title, body.nav-collapsed aside.nav .nav-foot { display:none; }
|
|
203
|
+
body.nav-collapsed aside.nav a { flex-direction:column; gap:.12rem; align-items:center; padding:.48rem .15rem; }
|
|
204
|
+
body.nav-collapsed aside.nav .nav-icon { width:1.2rem; height:1.2rem; }
|
|
205
|
+
body.nav-collapsed aside.nav .nav-code { display:block; }
|
|
206
|
+
.mobile-nav-toggle, #nav-close, #nav-backdrop { display:none; }
|
|
207
|
+
|
|
208
|
+
/* ---- Content ---- */
|
|
209
|
+
.doc h3 { margin: 1.6rem 0 0.5rem; font-size: 1.12rem; border-bottom: 1px solid var(--line); padding-bottom: 0.25rem; }
|
|
210
|
+
.doc h4 { margin: 1.35rem 0 0.35rem; font-size: 1rem; }
|
|
211
|
+
.doc h5 { margin: 1.1rem 0 0.3rem; font-size: 0.9rem; }
|
|
212
|
+
.doc h6 { margin: 1rem 0 0.3rem; font-size: 0.85rem; }
|
|
213
|
+
.doc p, .doc ul, .doc ol { margin: 0.55rem 0; }
|
|
214
|
+
.doc ul, .doc ol { padding-left: 1.4rem; }
|
|
215
|
+
.doc li { margin: 0.18rem 0; }
|
|
216
|
+
.doc table { border-collapse: collapse; margin: 0.8rem 0; font-size: 0.88rem; display: block; overflow-x: auto; max-width: 100%; }
|
|
217
|
+
.doc th, .doc td { border: 1px solid var(--line); padding: 0.32rem 0.6rem; text-align: left; vertical-align: top; }
|
|
218
|
+
.doc th { background: var(--panel); color: var(--ink); }
|
|
219
|
+
.doc blockquote { margin: 0.6rem 0; padding: 0.15rem 0.9rem; border-left: 3px solid var(--line-strong); color: var(--muted); }
|
|
220
|
+
.doc hr { border: none; border-top: 1px solid var(--line); margin: 1.4rem 0; }
|
|
221
|
+
.xref-dead code { word-break: break-all; }
|
|
222
|
+
figure.diagram {
|
|
223
|
+
margin: 1rem 0; padding: 0.85rem; border: 1px solid var(--line); border-radius: 6px;
|
|
224
|
+
background: #ffffff; color: #183b56; overflow-x: auto; overscroll-behavior-x: contain;
|
|
225
|
+
}
|
|
226
|
+
figure.diagram svg { display: block; width: auto; max-width: min(100%, 72rem); height: auto; margin-inline: auto; }
|
|
227
|
+
.diagram-missing { color: var(--muted); font-style: italic; }
|
|
228
|
+
mark { background: var(--mark); color: inherit; border-radius: 2px; padding: 0 0.05em; }
|
|
229
|
+
|
|
230
|
+
/* ---- Search dialog ---- */
|
|
231
|
+
.dlg { position: fixed; inset: 0; background: rgba(0,0,0,0.35); display: flex; align-items: flex-start; justify-content: center; padding-top: 12vh; z-index: 30; }
|
|
232
|
+
.dlgbox {
|
|
233
|
+
width: min(38rem, 92vw); background: var(--raise); border: 1px solid var(--line-strong);
|
|
234
|
+
border-radius: 6px; box-shadow: 0 12px 40px rgba(0,0,0,0.25); overflow: hidden;
|
|
235
|
+
}
|
|
236
|
+
.dlgbox input {
|
|
237
|
+
width: 100%; border: none; border-bottom: 1px solid var(--line); padding: 0.7rem 0.9rem;
|
|
238
|
+
font: inherit; color: var(--ink); background: var(--raise); outline: none;
|
|
239
|
+
}
|
|
240
|
+
.dlgbox ul { list-style: none; margin: 0; padding: 0; max-height: 50vh; overflow-y: auto; }
|
|
241
|
+
.dlgbox li a { display: block; padding: 0.45rem 0.9rem; color: var(--text); border-left: 2px solid transparent; }
|
|
242
|
+
.dlgbox li a:hover, .dlgbox li a.sel { background: var(--accent-soft); color: var(--ink); border-left-color: var(--accent); text-decoration: none; }
|
|
243
|
+
.dlgbox .where { display: block; font-size: 0.76rem; color: var(--muted); }
|
|
244
|
+
.dlgbox .empty { padding: 0.7rem 0.9rem; color: var(--muted); font-size: 0.88rem; }
|
|
245
|
+
kbd {
|
|
246
|
+
border: 1px solid var(--line-strong); border-bottom-width: 2px; border-radius: 3px;
|
|
247
|
+
padding: 0 0.3em; font-size: 0.8em; background: var(--raise); color: var(--muted);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* ---- Keyboard help ---- */
|
|
251
|
+
.keysbox { width: min(28rem, 92vw); }
|
|
252
|
+
.keysbox dl { margin: 0; padding: 0.6rem 0.9rem 0.9rem; }
|
|
253
|
+
.keysbox dt { font-weight: 650; color: var(--ink); margin-top: 0.55rem; }
|
|
254
|
+
.keysbox dd { margin: 0.1rem 0 0; color: var(--muted); font-size: 0.88rem; }
|
|
255
|
+
|
|
256
|
+
/* ---- Toast ---- */
|
|
257
|
+
.toast {
|
|
258
|
+
position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%);
|
|
259
|
+
background: var(--raise); border: 1px solid var(--line-strong); border-radius: 4px;
|
|
260
|
+
padding: 0.35rem 0.8rem; font-size: 0.85rem; color: var(--ink); z-index: 40;
|
|
261
|
+
opacity: 0; transition: opacity 0.15s; pointer-events: none;
|
|
262
|
+
}
|
|
263
|
+
.toast.show { opacity: 1; }
|
|
264
|
+
|
|
265
|
+
/* ---- Narrow viewports ---- */
|
|
266
|
+
@media (max-width: 900px) {
|
|
267
|
+
header.site .mobile-nav-toggle { display:inline-flex; }
|
|
268
|
+
#shell { position:relative; }
|
|
269
|
+
aside.nav { position:absolute; z-index:25; inset:0 auto 0 0; width:min(19rem,86vw); transform:translateX(-105%); transition:transform .2s ease; box-shadow:0 12px 36px rgba(0,0,0,.28); }
|
|
270
|
+
body.mobile-nav-open aside.nav { transform:translateX(0); }
|
|
271
|
+
#nav-close { display:inline-flex; }
|
|
272
|
+
#nav-backdrop { position:absolute; z-index:24; inset:0; display:block; border:0; background:rgba(0,0,0,.48); }
|
|
273
|
+
body.nav-collapsed aside.nav { width:min(19rem,86vw); padding:.9rem .75rem; }
|
|
274
|
+
body.nav-collapsed aside.nav .nav-heading { justify-content:space-between; }
|
|
275
|
+
body.nav-collapsed aside.nav h2, body.nav-collapsed aside.nav .nav-title, body.nav-collapsed aside.nav .nav-foot { display:block; }
|
|
276
|
+
body.nav-collapsed aside.nav a { flex-direction:row; gap:.55rem; align-items:center; padding:.32rem .4rem; }
|
|
277
|
+
body.nav-collapsed aside.nav .nav-icon { width:1rem; height:1rem; }
|
|
278
|
+
body.nav-collapsed aside.nav .nav-code { display:none; }
|
|
279
|
+
main#main { padding:1rem 1rem 3rem; }
|
|
280
|
+
}
|
|
281
|
+
@media (max-width: 520px) {
|
|
282
|
+
header.site { flex-wrap: wrap; gap: 0.5rem; }
|
|
283
|
+
header.site h1 { flex: 1 1 calc(100% - 6rem); }
|
|
284
|
+
header.site .spacer { display: none; }
|
|
285
|
+
header.site .appearance { margin-left: auto; }
|
|
286
|
+
}
|
|
287
|
+
.overview-intro { max-width:62rem; margin:0 auto 1.6rem; }
|
|
288
|
+
.overview-intro h2 { margin:0 0 .4rem; font-size:clamp(1.7rem,3vw,2.5rem); }
|
|
289
|
+
.overview-intro p { margin:0; color:var(--muted); font-size:1.05rem; }
|
|
290
|
+
.section-cards { display:grid; grid-auto-flow:column; grid-template-columns:repeat(2,minmax(0,1fr)); grid-template-rows:repeat(6,minmax(5rem,auto)); gap:.7rem; max-width:75rem; margin:auto; }
|
|
291
|
+
.section-card { display:flex; align-items:flex-start; gap:.85rem; min-height:5rem; padding:.9rem 1rem; color:var(--ink); border:1px solid var(--line); border-radius:7px; background:color-mix(in srgb,var(--category) 9%,var(--raise)); }
|
|
292
|
+
.section-card:hover { border-color:var(--category); text-decoration:none; }
|
|
293
|
+
.section-card[data-category="quality"] { --category:var(--cat-quality); }
|
|
294
|
+
.section-card[data-category="views"] { --category:var(--cat-views); }
|
|
295
|
+
.section-card[data-category="concepts"] { --category:var(--cat-concepts); }
|
|
296
|
+
.section-card[data-category="other"] { --category:var(--cat-other); }
|
|
297
|
+
.section-card .card-number { display:grid; place-items:center; width:2rem; height:2rem; flex:none; border-radius:50%; background:var(--category); color:#fff; font-weight:700; }
|
|
298
|
+
@media (prefers-color-scheme: dark) {
|
|
299
|
+
:root:not([data-appearance='light']) .section-card .card-number { color:#16181b; }
|
|
300
|
+
}
|
|
301
|
+
:root[data-appearance='dark'] .section-card .card-number { color:#16181b; }
|
|
302
|
+
.section-card strong { display:block; color:var(--ink); font-size:1rem; }
|
|
303
|
+
.section-card small { display:block; margin-top:.2rem; color:var(--muted); font-size:.83rem; }
|
|
304
|
+
@media(max-width:650px) { .section-cards { grid-auto-flow:row; grid-template-columns:1fr; grid-template-rows:none; } }
|
|
305
|
+
@media print {
|
|
306
|
+
body { overflow: visible; }
|
|
307
|
+
aside.nav, header.site .appearance, header.site .iconbtn { display: none; }
|
|
308
|
+
main#main { overflow: visible; }
|
|
309
|
+
}
|
|
310
|
+
@media (prefers-reduced-motion: reduce) {
|
|
311
|
+
* { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; }
|
|
312
|
+
}
|
|
313
|
+
`.trim();
|
|
314
|
+
|
|
315
|
+
/* ------------------------------------------------------------------ */
|
|
316
|
+
/* The application: one router, views rendered from inert data. */
|
|
317
|
+
/* ------------------------------------------------------------------ */
|
|
318
|
+
const script = String.raw`
|
|
319
|
+
(function () {
|
|
320
|
+
'use strict';
|
|
321
|
+
var doc = document;
|
|
322
|
+
var root = doc.documentElement;
|
|
323
|
+
var dataEl = doc.getElementById('arc42-data');
|
|
324
|
+
if (!dataEl) return;
|
|
325
|
+
var DATA = JSON.parse(dataEl.textContent || '{}');
|
|
326
|
+
var README = DATA.readme || {};
|
|
327
|
+
var SECTIONS = DATA.sections || [];
|
|
328
|
+
var byNumber = {};
|
|
329
|
+
for (var i = 0; i < SECTIONS.length; i += 1) byNumber[SECTIONS[i].number] = SECTIONS[i];
|
|
330
|
+
|
|
331
|
+
var el = function (tag, cls, text) {
|
|
332
|
+
var node = doc.createElement(tag);
|
|
333
|
+
if (cls) node.className = cls;
|
|
334
|
+
if (text !== undefined && text !== null) node.textContent = String(text);
|
|
335
|
+
return node;
|
|
336
|
+
};
|
|
337
|
+
var clear = function (node) { while (node.firstChild) node.removeChild(node.firstChild); };
|
|
338
|
+
|
|
339
|
+
/* ---------- Search index (built lazily, offline) ---------- */
|
|
340
|
+
var index = null;
|
|
341
|
+
function buildIndex() {
|
|
342
|
+
index = [];
|
|
343
|
+
var push = function (number, route, where, text) {
|
|
344
|
+
var plain = text.replace(/<[^>]+>/g, ' ').replace(/&[a-z]+;/g, ' ');
|
|
345
|
+
index.push({ number: number, route: route, where: where, text: plain });
|
|
346
|
+
};
|
|
347
|
+
push('', '#/', 'Overview', (README.title || '') + ' ' + (README.html || ''));
|
|
348
|
+
for (var i = 0; i < SECTIONS.length; i += 1) {
|
|
349
|
+
var s = SECTIONS[i];
|
|
350
|
+
push(s.number, '#/section/' + s.number, s.title, s.title + ' ' + s.description + ' ' + (s.html || ''));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function search(query) {
|
|
354
|
+
if (!index) buildIndex();
|
|
355
|
+
var terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
356
|
+
if (terms.length === 0) return [];
|
|
357
|
+
var results = [];
|
|
358
|
+
for (var i = 0; i < index.length; i += 1) {
|
|
359
|
+
var entry = index[i];
|
|
360
|
+
var hay = entry.text.toLowerCase();
|
|
361
|
+
var score = 0;
|
|
362
|
+
var all = true;
|
|
363
|
+
for (var t = 0; t < terms.length; t += 1) {
|
|
364
|
+
var term = terms[t];
|
|
365
|
+
var inTitle = entry.where.toLowerCase().indexOf(term) !== -1;
|
|
366
|
+
var hits = hay.split(term).length - 1;
|
|
367
|
+
if (hits === 0 && !inTitle) { all = false; break; }
|
|
368
|
+
score += inTitle ? 12 : 0;
|
|
369
|
+
score += Math.min(hits, 8);
|
|
370
|
+
}
|
|
371
|
+
if (all) results.push({ score: score, entry: entry });
|
|
372
|
+
}
|
|
373
|
+
results.sort(function (a, b) { return b.score - a.score || (a.entry.number < b.entry.number ? -1 : 1); });
|
|
374
|
+
return results.slice(0, 24);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* ---------- Views ---------- */
|
|
378
|
+
function renderDoc(host, title, description, html) {
|
|
379
|
+
clear(host);
|
|
380
|
+
var article = el('article', 'doc');
|
|
381
|
+
var h = el('h2', 'doc-title', title);
|
|
382
|
+
article.appendChild(h);
|
|
383
|
+
if (description) article.appendChild(el('p', 'doc-desc', description));
|
|
384
|
+
var body = el('div', 'doc-body');
|
|
385
|
+
body.innerHTML = html; /* generator-escaped markdown output only */
|
|
386
|
+
article.appendChild(body);
|
|
387
|
+
host.appendChild(article);
|
|
388
|
+
doc.getElementById('main').scrollTop = 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function showOverview(host) {
|
|
392
|
+
clear(host);
|
|
393
|
+
var intro = el('section', 'overview-intro');
|
|
394
|
+
intro.appendChild(el('h2', null, README.title || 'Raccord Architecture'));
|
|
395
|
+
if (README.description) intro.appendChild(el('p', null, README.description));
|
|
396
|
+
host.appendChild(intro);
|
|
397
|
+
var cards = el('div', 'section-cards');
|
|
398
|
+
for (var i = 0; i < SECTIONS.length; i += 1) {
|
|
399
|
+
var section = SECTIONS[i];
|
|
400
|
+
var card = doc.createElement('a');
|
|
401
|
+
card.className = 'section-card';
|
|
402
|
+
card.href = '#/section/' + section.number;
|
|
403
|
+
card.setAttribute('data-category', section.category);
|
|
404
|
+
card.appendChild(el('span', 'card-number', String(Number(section.number))));
|
|
405
|
+
var copy = el('span');
|
|
406
|
+
copy.appendChild(el('strong', null, section.title));
|
|
407
|
+
copy.appendChild(el('small', null, section.description || 'Explore this architecture section.'));
|
|
408
|
+
card.appendChild(copy);
|
|
409
|
+
cards.appendChild(card);
|
|
410
|
+
}
|
|
411
|
+
host.appendChild(cards);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function showSection(host, number) {
|
|
415
|
+
var section = byNumber[number];
|
|
416
|
+
if (!section) {
|
|
417
|
+
clear(host);
|
|
418
|
+
var box = el('div', 'doc');
|
|
419
|
+
box.appendChild(el('h2', 'doc-title', 'Section not found'));
|
|
420
|
+
var p = el('p');
|
|
421
|
+
p.appendChild(doc.createTextNode('This snapshot does not contain section ' + number + '.'));
|
|
422
|
+
box.appendChild(p);
|
|
423
|
+
var ways = el('p');
|
|
424
|
+
var home = doc.createElement('a');
|
|
425
|
+
home.href = '#/';
|
|
426
|
+
home.textContent = 'Open the overview';
|
|
427
|
+
ways.appendChild(home);
|
|
428
|
+
ways.appendChild(doc.createTextNode(' or '));
|
|
429
|
+
var list = doc.createElement('a');
|
|
430
|
+
list.href = '#/section/' + (SECTIONS[0] ? SECTIONS[0].number : '01');
|
|
431
|
+
list.textContent = 'start at the first section';
|
|
432
|
+
ways.appendChild(list);
|
|
433
|
+
ways.appendChild(doc.createTextNode('.'));
|
|
434
|
+
box.appendChild(ways);
|
|
435
|
+
host.appendChild(box);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
renderDoc(host, section.title, section.description, section.html || '');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* ---------- Sidebar state ---------- */
|
|
442
|
+
function syncNav(route) {
|
|
443
|
+
var links = doc.querySelectorAll('aside.nav a[data-route]');
|
|
444
|
+
for (var i = 0; i < links.length; i += 1) {
|
|
445
|
+
var current = links[i].getAttribute('data-route') === route;
|
|
446
|
+
if (current) links[i].setAttribute('aria-current', 'page');
|
|
447
|
+
else links[i].removeAttribute('aria-current');
|
|
448
|
+
}
|
|
449
|
+
var m = /^#\/section\/(\d+)/.exec(route);
|
|
450
|
+
var number = m ? m[1] : null;
|
|
451
|
+
var prev = doc.getElementById('nav-prev');
|
|
452
|
+
var next = doc.getElementById('nav-next');
|
|
453
|
+
var set = function (a, target, direction) {
|
|
454
|
+
clear(a);
|
|
455
|
+
if (!target) { a.style.visibility = 'hidden'; return; }
|
|
456
|
+
a.style.visibility = 'visible';
|
|
457
|
+
a.href = '#/section/' + target.number;
|
|
458
|
+
a.setAttribute('data-category', target.category);
|
|
459
|
+
var icon = el('span', 'nav-icon');
|
|
460
|
+
icon.innerHTML = direction === 'previous'
|
|
461
|
+
? '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="m10 3-5 5 5 5"/></svg>'
|
|
462
|
+
: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="m6 3 5 5-5 5"/></svg>';
|
|
463
|
+
a.appendChild(icon);
|
|
464
|
+
a.appendChild(el('span', null, target.title));
|
|
465
|
+
};
|
|
466
|
+
if (number) {
|
|
467
|
+
var pos = -1;
|
|
468
|
+
for (var s = 0; s < SECTIONS.length; s += 1) if (SECTIONS[s].number === number) pos = s;
|
|
469
|
+
set(prev, pos > 0 ? SECTIONS[pos - 1] : null, 'previous');
|
|
470
|
+
set(next, pos >= 0 && pos < SECTIONS.length - 1 ? SECTIONS[pos + 1] : null, 'next');
|
|
471
|
+
} else {
|
|
472
|
+
set(prev, null); set(next, null);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* ---------- Router ---------- */
|
|
477
|
+
function go() {
|
|
478
|
+
var hash = location.hash || '#/';
|
|
479
|
+
var host = doc.getElementById('content');
|
|
480
|
+
var route = hash.split('?')[0];
|
|
481
|
+
var m = /^#\/section\/(\d{2})(?:\/([^/]*))?$/.exec(route);
|
|
482
|
+
if (m) showSection(host, m[1]);
|
|
483
|
+
else showOverview(host);
|
|
484
|
+
syncNav(m ? '#/section/' + m[1] : '#/');
|
|
485
|
+
var anchor = m ? m[2] : /^#\/([^/]*)$/.exec(route);
|
|
486
|
+
if (!m) anchor = anchor ? anchor[1] : '';
|
|
487
|
+
if (anchor) {
|
|
488
|
+
try {
|
|
489
|
+
var heading = doc.getElementById(decodeURIComponent(anchor));
|
|
490
|
+
if (heading && host.contains(heading)) heading.scrollIntoView({ block: 'start' });
|
|
491
|
+
} catch (error) { /* malformed anchors leave the document at its start */ }
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* ---------- Appearance: carried in the address only ---------- */
|
|
496
|
+
function applyAppearance() {
|
|
497
|
+
var m = /[?&]a=(light|dark)/.exec(location.hash);
|
|
498
|
+
var choice = m ? m[1] : 'auto';
|
|
499
|
+
if (choice !== 'auto') root.setAttribute('data-appearance', choice);
|
|
500
|
+
else root.removeAttribute('data-appearance');
|
|
501
|
+
var buttons = doc.querySelectorAll('.appearance button');
|
|
502
|
+
for (var i = 0; i < buttons.length; i += 1) {
|
|
503
|
+
buttons[i].setAttribute('aria-pressed', String(buttons[i].getAttribute('data-appearance-set') === choice));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function setAppearance(choice) {
|
|
507
|
+
var hash = (location.hash || '#/').split('?')[0];
|
|
508
|
+
var next = choice === 'auto' ? hash : hash + (hash.indexOf('?') === -1 ? '?' : '&') + 'a=' + choice;
|
|
509
|
+
location.hash = next;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* ---------- Search dialog ---------- */
|
|
513
|
+
var findState = { sel: 0, results: [] };
|
|
514
|
+
function openFind() {
|
|
515
|
+
var dlg = doc.getElementById('find');
|
|
516
|
+
var input = doc.getElementById('find-input');
|
|
517
|
+
dlg.hidden = false;
|
|
518
|
+
input.value = '';
|
|
519
|
+
runSearch('');
|
|
520
|
+
input.focus();
|
|
521
|
+
}
|
|
522
|
+
function closeFind() {
|
|
523
|
+
doc.getElementById('find').hidden = true;
|
|
524
|
+
doc.getElementById('ov-find').focus();
|
|
525
|
+
}
|
|
526
|
+
function runSearch(query) {
|
|
527
|
+
var list = doc.getElementById('find-results');
|
|
528
|
+
clear(list);
|
|
529
|
+
findState.results = search(query);
|
|
530
|
+
findState.sel = 0;
|
|
531
|
+
if (query.trim() === '') {
|
|
532
|
+
var li = el('li');
|
|
533
|
+
li.appendChild(el('div', 'empty', 'Type to search across the overview and all ' + SECTIONS.length + ' sections.'));
|
|
534
|
+
list.appendChild(li);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (findState.results.length === 0) {
|
|
538
|
+
var li2 = el('li');
|
|
539
|
+
li2.appendChild(el('div', 'empty', 'No matches.'));
|
|
540
|
+
list.appendChild(li2);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
for (var i = 0; i < findState.results.length; i += 1) {
|
|
544
|
+
var r = findState.results[i].entry;
|
|
545
|
+
var a = doc.createElement('a');
|
|
546
|
+
a.href = r.route;
|
|
547
|
+
a.addEventListener('click', closeFind);
|
|
548
|
+
var where = el('span', null, r.where);
|
|
549
|
+
a.appendChild(where);
|
|
550
|
+
a.appendChild(el('span', 'where', r.number ? 'section ' + r.number : 'overview'));
|
|
551
|
+
list.appendChild(a);
|
|
552
|
+
}
|
|
553
|
+
var first = list.querySelector('a');
|
|
554
|
+
if (first) first.classList.add('sel');
|
|
555
|
+
}
|
|
556
|
+
function moveSel(delta) {
|
|
557
|
+
var links = doc.querySelectorAll('#find-results a');
|
|
558
|
+
if (links.length === 0) return;
|
|
559
|
+
links[findState.sel].classList.remove('sel');
|
|
560
|
+
findState.sel = Math.min(Math.max(findState.sel + delta, 0), links.length - 1);
|
|
561
|
+
links[findState.sel].classList.add('sel');
|
|
562
|
+
links[findState.sel].scrollIntoView({ block: 'nearest' });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/* ---------- Keyboard shortcuts ---------- */
|
|
566
|
+
function shortcuts(event) {
|
|
567
|
+
if (event.defaultPrevented) return;
|
|
568
|
+
var target = event.target;
|
|
569
|
+
var typing = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
|
|
570
|
+
var findOpen = !doc.getElementById('find').hidden;
|
|
571
|
+
if (findOpen) {
|
|
572
|
+
if (event.key === 'Escape') { closeFind(); event.preventDefault(); }
|
|
573
|
+
else if (event.key === 'ArrowDown') { moveSel(1); event.preventDefault(); }
|
|
574
|
+
else if (event.key === 'ArrowUp') { moveSel(-1); event.preventDefault(); }
|
|
575
|
+
else if (event.key === 'Enter') {
|
|
576
|
+
var links = doc.querySelectorAll('#find-results a');
|
|
577
|
+
if (links[findState.sel]) { location.hash = links[findState.sel].getAttribute('href'); closeFind(); }
|
|
578
|
+
event.preventDefault();
|
|
579
|
+
}
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (event.key === 'Escape' && doc.body.classList.contains('mobile-nav-open')) { closeMobileNav(); event.preventDefault(); return; }
|
|
583
|
+
if (typing) return;
|
|
584
|
+
if (event.key === '/') { openFind(); event.preventDefault(); }
|
|
585
|
+
else if (event.key === '?') { doc.getElementById('keys').hidden = false; event.preventDefault(); }
|
|
586
|
+
else if (event.key === 'Escape') { doc.getElementById('keys').hidden = true; }
|
|
587
|
+
else if (event.key === 'ArrowRight' || event.key === 'PageDown') {
|
|
588
|
+
var nx = doc.getElementById('nav-next');
|
|
589
|
+
if (nx && nx.style.visibility !== 'hidden') location.hash = nx.getAttribute('href');
|
|
590
|
+
}
|
|
591
|
+
else if (event.key === 'ArrowLeft' || event.key === 'PageUp') {
|
|
592
|
+
var pv = doc.getElementById('nav-prev');
|
|
593
|
+
if (pv && pv.style.visibility !== 'hidden') location.hash = pv.getAttribute('href');
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function closeMobileNav() {
|
|
598
|
+
doc.body.classList.remove('mobile-nav-open');
|
|
599
|
+
var toggle = doc.getElementById('mobile-nav-toggle');
|
|
600
|
+
if (toggle) toggle.setAttribute('aria-expanded', 'false');
|
|
601
|
+
doc.getElementById('nav-backdrop').hidden = true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/* ---------- Toast ---------- */
|
|
605
|
+
var toastTimer = null;
|
|
606
|
+
function toast(message) {
|
|
607
|
+
var t = doc.getElementById('toast');
|
|
608
|
+
t.textContent = message;
|
|
609
|
+
t.classList.add('show');
|
|
610
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
611
|
+
toastTimer = setTimeout(function () { t.classList.remove('show'); }, 1600);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* ---------- Wire up ---------- */
|
|
615
|
+
window.addEventListener('hashchange', function () { applyAppearance(); go(); });
|
|
616
|
+
doc.addEventListener('keydown', shortcuts);
|
|
617
|
+
doc.getElementById('ov-find').addEventListener('click', openFind);
|
|
618
|
+
doc.getElementById('keys-open').addEventListener('click', function () { doc.getElementById('keys').hidden = false; });
|
|
619
|
+
doc.getElementById('keys-close').addEventListener('click', function () { doc.getElementById('keys').hidden = true; });
|
|
620
|
+
doc.getElementById('keys').addEventListener('click', function (event) { if (event.target === this) this.hidden = true; });
|
|
621
|
+
doc.getElementById('nav-collapse').addEventListener('click', function () {
|
|
622
|
+
doc.body.classList.toggle('nav-collapsed');
|
|
623
|
+
var expanded = !doc.body.classList.contains('nav-collapsed');
|
|
624
|
+
this.setAttribute('aria-expanded', String(expanded));
|
|
625
|
+
this.setAttribute('aria-label', expanded ? 'Collapse navigation' : 'Expand navigation');
|
|
626
|
+
this.title = expanded ? 'Collapse navigation' : 'Expand navigation';
|
|
627
|
+
});
|
|
628
|
+
doc.getElementById('mobile-nav-toggle').addEventListener('click', function () {
|
|
629
|
+
doc.body.classList.add('mobile-nav-open'); this.setAttribute('aria-expanded', 'true');
|
|
630
|
+
doc.getElementById('nav-backdrop').hidden = false;
|
|
631
|
+
});
|
|
632
|
+
doc.getElementById('nav-close').addEventListener('click', closeMobileNav);
|
|
633
|
+
doc.getElementById('nav-backdrop').addEventListener('click', closeMobileNav);
|
|
634
|
+
doc.querySelector('aside.nav ol').addEventListener('click', function (event) {
|
|
635
|
+
if (event.target.closest('a')) closeMobileNav();
|
|
636
|
+
});
|
|
637
|
+
var findInput = doc.getElementById('find-input');
|
|
638
|
+
findInput.addEventListener('input', function () { runSearch(findInput.value); });
|
|
639
|
+
var appearanceButtons = doc.querySelectorAll('.appearance button');
|
|
640
|
+
for (var b = 0; b < appearanceButtons.length; b += 1) {
|
|
641
|
+
appearanceButtons[b].addEventListener('click', function () { setAppearance(this.getAttribute('data-appearance-set')); });
|
|
642
|
+
}
|
|
643
|
+
applyAppearance();
|
|
644
|
+
go();
|
|
645
|
+
})();
|
|
646
|
+
`;
|
|
647
|
+
|
|
648
|
+
/* ------------------------------------------------------------------ */
|
|
649
|
+
/* Snapshot assembly */
|
|
650
|
+
/* ------------------------------------------------------------------ */
|
|
651
|
+
|
|
652
|
+
/** Stable JSON: sorted keys, no incidental whitespace variation. */
|
|
653
|
+
function stableJson(value) {
|
|
654
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* @param {object} input
|
|
659
|
+
* @param {{title: string, description: string, html: string}} input.readme
|
|
660
|
+
* @param {Array<{number: string, title: string, description: string, html: string}>} input.sections
|
|
661
|
+
* @param {string} input.stackName
|
|
662
|
+
* @param {string|undefined} input.revision
|
|
663
|
+
*/
|
|
664
|
+
export function buildSnapshotHtml({ readme, sections, stackName, revision }) {
|
|
665
|
+
const revisionShort = revision
|
|
666
|
+
? `<span class="revshort" title="revision ${esc(revision)}">${esc(revision.slice(0, 7))}</span>`
|
|
667
|
+
: '<span class="revshort" title="Not generated from a Git checkout">no revision</span>';
|
|
668
|
+
|
|
669
|
+
const navItems = sections
|
|
670
|
+
.map(
|
|
671
|
+
(s) =>
|
|
672
|
+
` <li><a href="#/section/${s.number}" data-route="#/section/${s.number}" data-category="${sectionCategory(s.number)}"><span class="nav-icon">${sectionIcon(s.number)}</span><span class="nav-title t">${esc(s.title)}</span><span class="nav-code">${SECTION_CODES[Number(s.number) - 1]}</span></a></li>`,
|
|
673
|
+
)
|
|
674
|
+
.join("\n");
|
|
675
|
+
|
|
676
|
+
const data = {
|
|
677
|
+
readme: {
|
|
678
|
+
title: readme.title,
|
|
679
|
+
description: readme.description,
|
|
680
|
+
html: readme.html,
|
|
681
|
+
},
|
|
682
|
+
sections: sections.map((s) => ({
|
|
683
|
+
number: s.number,
|
|
684
|
+
title: s.title,
|
|
685
|
+
description: s.description,
|
|
686
|
+
html: s.html,
|
|
687
|
+
category: sectionCategory(s.number),
|
|
688
|
+
})),
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const lines = [
|
|
692
|
+
"<!DOCTYPE html>",
|
|
693
|
+
'<html lang="en">',
|
|
694
|
+
"<head>",
|
|
695
|
+
'<meta charset="utf-8">',
|
|
696
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
697
|
+
`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-${createHash("sha256").update(`\n${script}\n`).digest("base64")}'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">`,
|
|
698
|
+
'<meta name="color-scheme" content="light dark">',
|
|
699
|
+
`<title>${esc(stackName)} — arc42 Architecture Snapshot</title>`,
|
|
700
|
+
`<style>\n${style}\n</style>`,
|
|
701
|
+
"</head>",
|
|
702
|
+
"<body>",
|
|
703
|
+
'<a class="skip" href="#main">Skip to content</a>',
|
|
704
|
+
'<header class="site">',
|
|
705
|
+
`<h1>${esc(stackName)} · arc42</h1>`,
|
|
706
|
+
revisionShort,
|
|
707
|
+
'<span class="spacer"></span>',
|
|
708
|
+
'<div class="appearance" role="group" aria-label="Appearance">',
|
|
709
|
+
' <button type="button" data-appearance-set="auto" aria-pressed="true">Auto</button>',
|
|
710
|
+
' <button type="button" data-appearance-set="light" aria-pressed="false">Light</button>',
|
|
711
|
+
' <button type="button" data-appearance-set="dark" aria-pressed="false">Dark</button>',
|
|
712
|
+
"</div>",
|
|
713
|
+
'<button type="button" class="iconbtn" id="ov-find" aria-haspopup="dialog" aria-label="Search (/)" title="Search (/)">\u2315</button>',
|
|
714
|
+
'<button type="button" class="iconbtn mobile-nav-toggle" id="mobile-nav-toggle" aria-controls="arc42-nav" aria-expanded="false" aria-label="Open sections navigation" title="Open navigation">☰</button>',
|
|
715
|
+
'<button type="button" class="iconbtn" id="keys-open" aria-haspopup="dialog" aria-label="Keyboard shortcuts (?)" title="Keyboard shortcuts (?)">?</button>',
|
|
716
|
+
"</header>",
|
|
717
|
+
'<div id="shell">',
|
|
718
|
+
'<aside class="nav" id="arc42-nav" aria-label="arc42 sections">',
|
|
719
|
+
' <div class="nav-heading"><h2>Sections</h2><button type="button" class="iconbtn" id="nav-collapse" aria-label="Collapse navigation" aria-expanded="true" title="Collapse navigation">◧</button><button type="button" class="iconbtn" id="nav-close" aria-label="Close navigation" title="Close navigation">×</button></div>',
|
|
720
|
+
" <ol>",
|
|
721
|
+
` <li><a href="#/" data-route="#/" data-category="other"><span class="nav-icon"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="m2 7 6-5 6 5v7H9v-4H7v4H2z"/></svg></span><span class="nav-title t">Overview</span><span class="nav-code">HOME</span></a></li>`,
|
|
722
|
+
navItems,
|
|
723
|
+
" </ol>",
|
|
724
|
+
' <div class="nav-foot">',
|
|
725
|
+
' <div class="prevnext">',
|
|
726
|
+
' <a id="nav-prev" href="#/" aria-label="Previous section"></a>',
|
|
727
|
+
' <a id="nav-next" href="#/" aria-label="Next section"></a>',
|
|
728
|
+
" </div>",
|
|
729
|
+
" </div>",
|
|
730
|
+
"</aside>",
|
|
731
|
+
'<button type="button" id="nav-backdrop" aria-label="Close navigation" hidden></button>',
|
|
732
|
+
'<main id="main">',
|
|
733
|
+
' <div id="content"></div>',
|
|
734
|
+
"</main>",
|
|
735
|
+
"</div>",
|
|
736
|
+
'<div class="dlg" id="find" role="dialog" aria-modal="true" aria-labelledby="find-title" hidden>',
|
|
737
|
+
'<div class="dlgbox">',
|
|
738
|
+
` <input id="find-input" type="text" placeholder="Search ${esc(stackName)} docs…" aria-label="Search query" autocomplete="off">`,
|
|
739
|
+
' <ul id="find-results"></ul>',
|
|
740
|
+
"</div>",
|
|
741
|
+
"</div>",
|
|
742
|
+
'<div class="dlg" id="keys" role="dialog" aria-modal="true" aria-labelledby="keys-title" hidden>',
|
|
743
|
+
'<div class="dlgbox keysbox">',
|
|
744
|
+
' <input type="text" value="Keyboard shortcuts" aria-label="Keyboard shortcuts" readonly>',
|
|
745
|
+
" <dl>",
|
|
746
|
+
" <dt><kbd>/</kbd></dt><dd>Search the overview and all sections</dd>",
|
|
747
|
+
" <dt><kbd>←</kbd> / <kbd>→</kbd></dt><dd>Previous / next section</dd>",
|
|
748
|
+
" <dt><kbd>?</kbd></dt><dd>Show this list</dd>",
|
|
749
|
+
" <dt><kbd>Esc</kbd></dt><dd>Close dialogs</dd>",
|
|
750
|
+
" </dl>",
|
|
751
|
+
' <p style="margin:0; padding:0.6rem 0.9rem 0.9rem; text-align:right;"><button type="button" id="keys-close">Close</button></p>',
|
|
752
|
+
"</div>",
|
|
753
|
+
"</div>",
|
|
754
|
+
'<p class="toast" id="toast" role="status" aria-live="polite"></p>',
|
|
755
|
+
`<script id="arc42-data" type="application/json">${stableJson(data)}</script>`,
|
|
756
|
+
`<script>\n${script}\n</script>`,
|
|
757
|
+
"</body>",
|
|
758
|
+
"</html>",
|
|
759
|
+
];
|
|
760
|
+
return `${lines.filter((l) => l !== "").join("\n")}\n`;
|
|
761
|
+
}
|