sdocs-dev 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/bin/sdocs-dev.js +282 -38
- package/package.json +13 -11
- package/public/sdocs-slugify.js +12 -0
- package/public/sdocs-styles.js +279 -353
- package/public/brotli-wasm.js +0 -519
- package/public/brotli_wasm_bg.wasm +0 -0
- package/public/css/layout.css +0 -241
- package/public/css/mobile.css +0 -189
- package/public/css/panel.css +0 -393
- package/public/css/rendered.css +0 -295
- package/public/css/tokens.css +0 -116
- package/public/css/write.css +0 -128
- package/public/default.md +0 -255
- package/public/fonts/inter-400.woff2 +0 -0
- package/public/fonts/inter-500.woff2 +0 -0
- package/public/fonts/inter-600.woff2 +0 -0
- package/public/images/examples.png +0 -0
- package/public/index.html +0 -600
- package/public/sdocs-app.js +0 -653
- package/public/sdocs-controls.js +0 -272
- package/public/sdocs-export.js +0 -212
- package/public/sdocs-state.js +0 -59
- package/public/sdocs-theme.js +0 -260
- package/public/sdocs-write.js +0 -737
- package/public/sw.js +0 -108
- package/public/vendor/marked.min.js +0 -6
- package/server.js +0 -128
package/public/sdocs-app.js
DELETED
|
@@ -1,653 +0,0 @@
|
|
|
1
|
-
// sdocs-app.js — Core app: render, sync, modes, drag/drop, init
|
|
2
|
-
(function () {
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
var S = SDocs;
|
|
6
|
-
|
|
7
|
-
// ── SVG icons ──────────────────────────────────
|
|
8
|
-
|
|
9
|
-
var LINK_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
|
|
10
|
-
var COPY_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
11
|
-
var CHECK_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
12
|
-
|
|
13
|
-
// ── Slugify + section helpers ──────────────────────
|
|
14
|
-
|
|
15
|
-
function slugify(text) {
|
|
16
|
-
return text.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function buildSectionUrl(slug) {
|
|
20
|
-
var base = window.location.origin + window.location.pathname;
|
|
21
|
-
var hash = window.location.hash.slice(1);
|
|
22
|
-
var params = hash ? new URLSearchParams(hash) : new URLSearchParams();
|
|
23
|
-
params.delete('sec');
|
|
24
|
-
params.set('sec', slug);
|
|
25
|
-
if (S.currentMode !== 'read' && S.currentMode !== 'raw') {
|
|
26
|
-
params.set('mode', S.currentMode);
|
|
27
|
-
}
|
|
28
|
-
return base + '#' + params.toString();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getSectionMarkdown(headingIndex) {
|
|
32
|
-
var lines = S.currentBody.split('\n');
|
|
33
|
-
var headings = [];
|
|
34
|
-
var inFence = false;
|
|
35
|
-
for (var i = 0; i < lines.length; i++) {
|
|
36
|
-
if (/^(`{3,}|~{3,})/.test(lines[i])) { inFence = !inFence; continue; }
|
|
37
|
-
if (inFence) continue;
|
|
38
|
-
var m = lines[i].match(/^(#{1,4})\s/);
|
|
39
|
-
if (m) headings.push({ line: i, level: m[1].length });
|
|
40
|
-
}
|
|
41
|
-
if (headingIndex < 0 || headingIndex >= headings.length) return '';
|
|
42
|
-
var target = headings[headingIndex];
|
|
43
|
-
var endLine = lines.length;
|
|
44
|
-
for (var j = headingIndex + 1; j < headings.length; j++) {
|
|
45
|
-
if (headings[j].level <= target.level) {
|
|
46
|
-
endLine = headings[j].line;
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return lines.slice(target.line, endLine).join('\n').trimEnd();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ── Render ──────────────────────────────────
|
|
54
|
-
|
|
55
|
-
function render() {
|
|
56
|
-
var oldSpacer = S.renderedEl.querySelector('.sec-scroll-spacer');
|
|
57
|
-
if (oldSpacer) oldSpacer.remove();
|
|
58
|
-
S.renderedEl.innerHTML = marked.parse(S.currentBody);
|
|
59
|
-
|
|
60
|
-
var slugCounts = {};
|
|
61
|
-
var allHeadings = [].slice.call(S.renderedEl.querySelectorAll('h1, h2, h3, h4'));
|
|
62
|
-
allHeadings.forEach(function(h, idx) {
|
|
63
|
-
var slug = slugify(h.textContent);
|
|
64
|
-
if (!slug) slug = 'section';
|
|
65
|
-
if (slugCounts[slug] != null) {
|
|
66
|
-
slugCounts[slug]++;
|
|
67
|
-
slug = slug + '-' + slugCounts[slug];
|
|
68
|
-
} else {
|
|
69
|
-
slugCounts[slug] = 0;
|
|
70
|
-
}
|
|
71
|
-
h.id = slug;
|
|
72
|
-
|
|
73
|
-
var anchor = document.createElement('a');
|
|
74
|
-
anchor.className = 'header-anchor';
|
|
75
|
-
anchor.innerHTML = LINK_SVG;
|
|
76
|
-
anchor.title = 'Copy link to section';
|
|
77
|
-
anchor.addEventListener('click', function(e) {
|
|
78
|
-
e.preventDefault();
|
|
79
|
-
navigator.clipboard.writeText(buildSectionUrl(slug)).then(function() {
|
|
80
|
-
anchor.innerHTML = CHECK_SVG;
|
|
81
|
-
setTimeout(function() { anchor.innerHTML = LINK_SVG; }, 1500);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
h.appendChild(anchor);
|
|
85
|
-
|
|
86
|
-
var copyBtn = document.createElement('button');
|
|
87
|
-
copyBtn.className = 'header-copy-btn';
|
|
88
|
-
copyBtn.innerHTML = COPY_SVG;
|
|
89
|
-
copyBtn.title = 'Copy section';
|
|
90
|
-
var hIdx = idx;
|
|
91
|
-
copyBtn.addEventListener('click', function(e) {
|
|
92
|
-
e.stopPropagation();
|
|
93
|
-
e.preventDefault();
|
|
94
|
-
var md = getSectionMarkdown(hIdx);
|
|
95
|
-
navigator.clipboard.writeText(md).then(function() {
|
|
96
|
-
copyBtn.innerHTML = CHECK_SVG;
|
|
97
|
-
setTimeout(function() { copyBtn.innerHTML = COPY_SVG; }, 1500);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
h.appendChild(copyBtn);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
S.renderedEl.querySelectorAll('pre').forEach(function(pre) {
|
|
104
|
-
var wrapper = document.createElement('div');
|
|
105
|
-
wrapper.className = 'pre-wrapper';
|
|
106
|
-
pre.parentNode.insertBefore(wrapper, pre);
|
|
107
|
-
wrapper.appendChild(pre);
|
|
108
|
-
var btn = document.createElement('button');
|
|
109
|
-
btn.className = 'copy-btn';
|
|
110
|
-
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
111
|
-
btn.title = 'Copy code';
|
|
112
|
-
btn.addEventListener('click', function() {
|
|
113
|
-
var code = pre.querySelector('code');
|
|
114
|
-
navigator.clipboard.writeText(code ? code.textContent : pre.textContent).then(function() {
|
|
115
|
-
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
116
|
-
setTimeout(function() {
|
|
117
|
-
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
118
|
-
}, 1500);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
wrapper.appendChild(btn);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Collapsible heading sections (H2, H3, H4)
|
|
125
|
-
var SECTION_CHEVRON = '<span class="section-toggle"><svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M3 2l4 3-4 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></span>';
|
|
126
|
-
var SECTION_LEVELS = { H2: 2, H3: 3, H4: 4 };
|
|
127
|
-
var children = [].slice.call(S.renderedEl.children);
|
|
128
|
-
var bodyStack = [S.renderedEl];
|
|
129
|
-
children.forEach(function(child) {
|
|
130
|
-
var level = SECTION_LEVELS[child.tagName];
|
|
131
|
-
if (level) {
|
|
132
|
-
while (bodyStack.length > level - 1) bodyStack.pop();
|
|
133
|
-
var sectionDiv = document.createElement('div');
|
|
134
|
-
sectionDiv.className = 'md-section';
|
|
135
|
-
var sectionBody = document.createElement('div');
|
|
136
|
-
sectionBody.className = 'md-section-body';
|
|
137
|
-
child.insertAdjacentHTML('afterbegin', SECTION_CHEVRON);
|
|
138
|
-
bodyStack[bodyStack.length - 1].appendChild(sectionDiv);
|
|
139
|
-
sectionDiv.appendChild(child);
|
|
140
|
-
sectionDiv.appendChild(sectionBody);
|
|
141
|
-
bodyStack.push(sectionBody);
|
|
142
|
-
} else {
|
|
143
|
-
bodyStack[bodyStack.length - 1].appendChild(child);
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
S.renderedEl.querySelectorAll('.md-section > h2, .md-section > h3, .md-section > h4').forEach(function(heading) {
|
|
148
|
-
heading.addEventListener('click', function(e) {
|
|
149
|
-
if (e.target.closest('.header-anchor') || e.target.closest('.header-copy-btn')) return;
|
|
150
|
-
var yBefore = heading.getBoundingClientRect().top;
|
|
151
|
-
var section = heading.closest('.md-section');
|
|
152
|
-
var body = section.querySelector('.md-section-body');
|
|
153
|
-
var toggle = section.querySelector('.section-toggle');
|
|
154
|
-
var isOpen = body.classList.toggle('open');
|
|
155
|
-
toggle.classList.toggle('open', isOpen);
|
|
156
|
-
body.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.toggle('open', isOpen); });
|
|
157
|
-
body.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.toggle('open', isOpen); });
|
|
158
|
-
var yAfter = heading.getBoundingClientRect().top;
|
|
159
|
-
if (yAfter !== yBefore) {
|
|
160
|
-
contentArea.scrollTop += yAfter - yBefore;
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── Status ──────────────────────────────────
|
|
167
|
-
|
|
168
|
-
function setStatus(msg) {
|
|
169
|
-
document.getElementById('status-text').textContent = msg;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ── Load content ──────────────────────────────────
|
|
173
|
-
|
|
174
|
-
function loadText(text, filename) {
|
|
175
|
-
var parsed = SDocYaml.parseFrontMatter(text);
|
|
176
|
-
S.currentMeta = parsed.meta;
|
|
177
|
-
S.currentBody = parsed.body;
|
|
178
|
-
render();
|
|
179
|
-
if (parsed.meta.styles) S.applyStylesFromMeta(parsed.meta.styles);
|
|
180
|
-
// Re-apply theme defaults for standalone colors (front matter may have
|
|
181
|
-
// theme-specific values that don't match the viewer's current theme)
|
|
182
|
-
S.STANDALONE_COLOR_IDS.forEach(function(ctrlId) {
|
|
183
|
-
if (!S.overriddenColors.has(ctrlId)) {
|
|
184
|
-
var val = S.getStandaloneDefault(ctrlId);
|
|
185
|
-
var el = document.getElementById(ctrlId);
|
|
186
|
-
if (el) {
|
|
187
|
-
el.value = val;
|
|
188
|
-
var allVals = S.readAllControlValues();
|
|
189
|
-
SDocStyles.controlToCssVars(ctrlId, val, allVals)
|
|
190
|
-
.forEach(function(a) { S.setStyleVar(a.cssVar, a.value); });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
195
|
-
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
196
|
-
setStatus(filename ? 'Loaded: ' + filename : 'Ready');
|
|
197
|
-
syncAll('load');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ── Compression helpers (brotli + base64url) ──
|
|
201
|
-
|
|
202
|
-
function toBase64Url(bytes) {
|
|
203
|
-
var bin = Array.from(new Uint8Array(bytes), function(b) { return String.fromCharCode(b); }).join('');
|
|
204
|
-
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function fromBase64Url(str) {
|
|
208
|
-
var b64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
209
|
-
var bin = atob(b64);
|
|
210
|
-
var bytes = new Uint8Array(bin.length);
|
|
211
|
-
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
212
|
-
return bytes;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async function compressText(text) {
|
|
216
|
-
await BrotliWasm.ready;
|
|
217
|
-
var encoded = new TextEncoder().encode(text);
|
|
218
|
-
var compressed = BrotliWasm.compress(encoded, { quality: 11 });
|
|
219
|
-
return toBase64Url(compressed);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function decompressDeflate(bytes) {
|
|
223
|
-
var ds = new DecompressionStream('deflate-raw');
|
|
224
|
-
var writer = ds.writable.getWriter();
|
|
225
|
-
writer.write(bytes);
|
|
226
|
-
writer.close();
|
|
227
|
-
var chunks = [];
|
|
228
|
-
var reader = ds.readable.getReader();
|
|
229
|
-
while (true) {
|
|
230
|
-
var result = await reader.read();
|
|
231
|
-
if (result.done) break;
|
|
232
|
-
chunks.push(result.value);
|
|
233
|
-
}
|
|
234
|
-
var total = chunks.reduce(function(n, c) { return n + c.length; }, 0);
|
|
235
|
-
var buf = new Uint8Array(total);
|
|
236
|
-
var offset = 0;
|
|
237
|
-
for (var i = 0; i < chunks.length; i++) { buf.set(chunks[i], offset); offset += chunks[i].length; }
|
|
238
|
-
return new TextDecoder().decode(buf);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function decompressText(b64url) {
|
|
242
|
-
await BrotliWasm.ready;
|
|
243
|
-
var bytes = fromBase64Url(b64url);
|
|
244
|
-
// Try brotli first, fall back to deflate for old URLs
|
|
245
|
-
try {
|
|
246
|
-
var decompressed = BrotliWasm.decompress(bytes);
|
|
247
|
-
return new TextDecoder().decode(decompressed);
|
|
248
|
-
} catch (_) {
|
|
249
|
-
return decompressDeflate(bytes);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ── Auto-save to URL hash ──────────────────────────
|
|
254
|
-
|
|
255
|
-
function updateHash() {
|
|
256
|
-
clearTimeout(S._hashTimer);
|
|
257
|
-
S._hashTimer = setTimeout(async function() {
|
|
258
|
-
if (S._isDefaultState && S.currentMode === 'read') {
|
|
259
|
-
history.replaceState(null, '', window.location.pathname === '/new' ? '/' : window.location.pathname);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
var params = new URLSearchParams();
|
|
263
|
-
if (!S._isDefaultState) {
|
|
264
|
-
var styles = SDocStyles.stripStyleDefaults(S.collectStyles());
|
|
265
|
-
var meta = Object.assign({}, S.currentMeta);
|
|
266
|
-
if (Object.keys(styles).length > 0) meta.styles = styles;
|
|
267
|
-
else delete meta.styles;
|
|
268
|
-
var full = SDocYaml.serializeFrontMatter(meta) + '\n' + S.currentBody;
|
|
269
|
-
var compressed = await compressText(full);
|
|
270
|
-
params.set('md', compressed);
|
|
271
|
-
}
|
|
272
|
-
if (S.currentMode !== 'read') {
|
|
273
|
-
params.set('mode', S.currentMode);
|
|
274
|
-
}
|
|
275
|
-
var basePath = window.location.pathname === '/new' ? '/' : window.location.pathname;
|
|
276
|
-
history.replaceState(null, '', basePath + '#' + params.toString());
|
|
277
|
-
}, 400);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ── State sync ──────────────────────────────────
|
|
281
|
-
|
|
282
|
-
function syncAll(source) {
|
|
283
|
-
if (S._syncing) return;
|
|
284
|
-
S._syncing = true;
|
|
285
|
-
try {
|
|
286
|
-
if (source === 'controls') {
|
|
287
|
-
S._isDefaultState = false;
|
|
288
|
-
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
289
|
-
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
290
|
-
updateHash();
|
|
291
|
-
} else if (source === 'raw') {
|
|
292
|
-
S._isDefaultState = false;
|
|
293
|
-
var parsed = SDocYaml.parseFrontMatter(S.rawEl.value);
|
|
294
|
-
S.currentMeta = parsed.meta;
|
|
295
|
-
S.currentBody = parsed.body;
|
|
296
|
-
render();
|
|
297
|
-
if (parsed.meta.styles) S.applyStylesFromMeta(parsed.meta.styles);
|
|
298
|
-
updateHash();
|
|
299
|
-
} else if (source === 'write') {
|
|
300
|
-
S._isDefaultState = false;
|
|
301
|
-
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
302
|
-
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
303
|
-
updateHash();
|
|
304
|
-
} else if (source === 'load') {
|
|
305
|
-
updateHash();
|
|
306
|
-
}
|
|
307
|
-
} finally {
|
|
308
|
-
S._syncing = false;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ── Drag & drop ──────────────────────────────────
|
|
313
|
-
|
|
314
|
-
var contentArea = document.getElementById('content-area');
|
|
315
|
-
|
|
316
|
-
contentArea.addEventListener('dragover', function(e) {
|
|
317
|
-
e.preventDefault();
|
|
318
|
-
contentArea.classList.add('drag-over');
|
|
319
|
-
});
|
|
320
|
-
['dragleave','dragend'].forEach(function(ev) {
|
|
321
|
-
contentArea.addEventListener(ev, function() { contentArea.classList.remove('drag-over'); });
|
|
322
|
-
});
|
|
323
|
-
contentArea.addEventListener('drop', function(e) {
|
|
324
|
-
e.preventDefault();
|
|
325
|
-
contentArea.classList.remove('drag-over');
|
|
326
|
-
var file = e.dataTransfer.files[0];
|
|
327
|
-
if (!file) return;
|
|
328
|
-
var reader = new FileReader();
|
|
329
|
-
reader.onload = function(ev) { S._isDefaultState = false; loadText(ev.target.result, file.name); };
|
|
330
|
-
reader.readAsText(file);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
S.rawEl.addEventListener('input', function() {
|
|
334
|
-
clearTimeout(S._rawSyncTimer);
|
|
335
|
-
S._rawSyncTimer = setTimeout(function() { syncAll('raw'); }, 300);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// ── Mode toggle (read / style / raw / export) ──────────────────
|
|
339
|
-
|
|
340
|
-
function setMode(mode, skipHash) {
|
|
341
|
-
var prev = S.currentMode;
|
|
342
|
-
S.currentMode = mode;
|
|
343
|
-
|
|
344
|
-
// Exit write mode — extract markdown back
|
|
345
|
-
if (prev === 'write' && mode !== 'write') {
|
|
346
|
-
S.exitWriteMode();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
S.renderedEl.style.display = (mode === 'raw' || mode === 'write') ? 'none' : '';
|
|
350
|
-
S.rawEl.style.display = mode === 'raw' ? 'block' : 'none';
|
|
351
|
-
|
|
352
|
-
document.getElementById('btn-read').classList.toggle('active', mode === 'read');
|
|
353
|
-
document.getElementById('btn-style').classList.toggle('active', mode === 'style');
|
|
354
|
-
document.getElementById('btn-write').classList.toggle('active', mode === 'write');
|
|
355
|
-
document.getElementById('btn-raw').classList.toggle('active', mode === 'raw');
|
|
356
|
-
document.getElementById('btn-export').classList.toggle('active', mode === 'export');
|
|
357
|
-
|
|
358
|
-
document.body.classList.toggle('style-mode', mode === 'style');
|
|
359
|
-
document.body.classList.toggle('read-mode', mode === 'read');
|
|
360
|
-
document.body.classList.toggle('write-mode', mode === 'write');
|
|
361
|
-
document.body.classList.toggle('raw-mode', mode === 'raw');
|
|
362
|
-
document.body.classList.toggle('export-mode', mode === 'export');
|
|
363
|
-
document.body.classList.remove('mobile-sheet-open');
|
|
364
|
-
document.body.classList.remove('mobile-export-open');
|
|
365
|
-
|
|
366
|
-
// Enter write mode — populate contentEditable
|
|
367
|
-
if (mode === 'write') {
|
|
368
|
-
S.enterWriteMode();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (!skipHash) updateHash();
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
document.getElementById('btn-theme').addEventListener('click', function() { S.toggleTheme(); });
|
|
375
|
-
document.getElementById('theme-tab-light').addEventListener('click', function() { S.switchThemeAndUpdate('light'); });
|
|
376
|
-
document.getElementById('theme-tab-dark').addEventListener('click', function() { S.switchThemeAndUpdate('dark'); });
|
|
377
|
-
document.getElementById('btn-read').addEventListener('click', function() { setMode('read'); });
|
|
378
|
-
document.getElementById('btn-style').addEventListener('click', function() { setMode('style'); });
|
|
379
|
-
document.getElementById('btn-write').addEventListener('click', function() { setMode('write'); });
|
|
380
|
-
document.getElementById('btn-raw').addEventListener('click', function() { setMode('raw'); });
|
|
381
|
-
document.getElementById('btn-export').addEventListener('click', function() { setMode('export'); });
|
|
382
|
-
|
|
383
|
-
document.getElementById('btn-new').addEventListener('click', function() {
|
|
384
|
-
history.replaceState(null, '', '/new');
|
|
385
|
-
startNewDocument();
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
document.getElementById('btn-expand-all').addEventListener('click', function() {
|
|
389
|
-
S.renderedEl.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.add('open'); });
|
|
390
|
-
S.renderedEl.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.add('open'); });
|
|
391
|
-
});
|
|
392
|
-
document.getElementById('btn-collapse-all').addEventListener('click', function() {
|
|
393
|
-
S.renderedEl.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.remove('open'); });
|
|
394
|
-
S.renderedEl.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.remove('open'); });
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
document.getElementById('right-header').addEventListener('click', function() {
|
|
398
|
-
if (window.innerWidth <= 768) {
|
|
399
|
-
document.body.classList.toggle('mobile-sheet-open');
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
document.getElementById('export-panel-header').addEventListener('click', function() {
|
|
404
|
-
if (window.innerWidth <= 768) {
|
|
405
|
-
document.body.classList.toggle('mobile-export-open');
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
document.getElementById('factory-reset-styles').addEventListener('click', function() {
|
|
410
|
-
S.resetAllStyles();
|
|
411
|
-
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
412
|
-
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
413
|
-
render();
|
|
414
|
-
syncAll('load');
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
document.getElementById('toolbar-brand').addEventListener('click', function(e) {
|
|
418
|
-
e.preventDefault();
|
|
419
|
-
if (window.location.hash && window.location.hash.indexOf('md=') !== -1) {
|
|
420
|
-
window.open(window.location.origin + window.location.pathname, '_blank');
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
S.resetAllStyles();
|
|
424
|
-
loadText(DEFAULT_MD);
|
|
425
|
-
S._isDefaultState = true;
|
|
426
|
-
clearTimeout(S._hashTimer);
|
|
427
|
-
history.replaceState(null, '', window.location.pathname);
|
|
428
|
-
setMode('read');
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// ── Collapsible panels ──────────────────────────────
|
|
432
|
-
|
|
433
|
-
document.querySelectorAll('.panel-header').forEach(function(h) {
|
|
434
|
-
h.addEventListener('click', function() {
|
|
435
|
-
var body = document.getElementById(h.dataset.target);
|
|
436
|
-
var open = body.classList.toggle('open');
|
|
437
|
-
h.classList.toggle('open', open);
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
document.querySelectorAll('.sub-header').forEach(function(h) {
|
|
441
|
-
h.addEventListener('click', function() {
|
|
442
|
-
var body = document.getElementById(h.dataset.target);
|
|
443
|
-
var open = body.classList.toggle('open');
|
|
444
|
-
h.classList.toggle('open', open);
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
// ── Default content ──────────────────────────────────
|
|
449
|
-
|
|
450
|
-
var DEFAULT_MD = '';
|
|
451
|
-
var _defaultReady = fetch('/public/default.md').then(function(r) { return r.text(); }).then(function(t) { DEFAULT_MD = t; });
|
|
452
|
-
|
|
453
|
-
// ── Register on SDocs for cross-module access ──────────
|
|
454
|
-
|
|
455
|
-
function startNewDocument() {
|
|
456
|
-
S.resetAllStyles();
|
|
457
|
-
S.currentBody = '';
|
|
458
|
-
S.currentMeta = {};
|
|
459
|
-
S._isDefaultState = false;
|
|
460
|
-
clearTimeout(S._hashTimer);
|
|
461
|
-
render();
|
|
462
|
-
setMode('write', true);
|
|
463
|
-
var w = S.writeEl;
|
|
464
|
-
w.innerHTML = '<h1><br></h1>';
|
|
465
|
-
w.focus();
|
|
466
|
-
var range = document.createRange();
|
|
467
|
-
range.selectNodeContents(w.querySelector('h1'));
|
|
468
|
-
range.collapse(false);
|
|
469
|
-
var sel = window.getSelection();
|
|
470
|
-
sel.removeAllRanges();
|
|
471
|
-
sel.addRange(range);
|
|
472
|
-
setTimeout(function() { S.updateToolbarState(); }, 0);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
S.syncAll = syncAll;
|
|
476
|
-
S.setStatus = setStatus;
|
|
477
|
-
S.setMode = setMode;
|
|
478
|
-
S.render = render;
|
|
479
|
-
S.loadText = loadText;
|
|
480
|
-
|
|
481
|
-
// Sync theme tabs to initial theme
|
|
482
|
-
S.updateThemeTabs(S.activeTheme);
|
|
483
|
-
|
|
484
|
-
// ── Load document from URL hash ──────────────────────────────────
|
|
485
|
-
|
|
486
|
-
var _lastLoadedHash = null;
|
|
487
|
-
|
|
488
|
-
async function loadFromHash() {
|
|
489
|
-
var hash = window.location.hash.slice(1);
|
|
490
|
-
if (hash === _lastLoadedHash) return;
|
|
491
|
-
_lastLoadedHash = hash;
|
|
492
|
-
|
|
493
|
-
clearTimeout(S._hashTimer);
|
|
494
|
-
|
|
495
|
-
var params = hash ? new URLSearchParams(hash) : new URLSearchParams();
|
|
496
|
-
var mdParam = params.get('md');
|
|
497
|
-
var modeParam = params.get('mode');
|
|
498
|
-
var stylesParam = params.get('styles');
|
|
499
|
-
var themeParam = params.get('theme');
|
|
500
|
-
var secParam = params.get('sec');
|
|
501
|
-
|
|
502
|
-
S.resetAllStyles();
|
|
503
|
-
|
|
504
|
-
if (mdParam) {
|
|
505
|
-
try {
|
|
506
|
-
S._isDefaultState = false;
|
|
507
|
-
var text = await decompressText(mdParam);
|
|
508
|
-
loadText(text, 'document.md');
|
|
509
|
-
} catch (e) {
|
|
510
|
-
console.warn('sdocs-dev: could not decode hash', e);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (themeParam === 'light' || themeParam === 'dark') {
|
|
515
|
-
var savedPref = localStorage.getItem('sdocs-theme');
|
|
516
|
-
S.switchThemeAndUpdate(themeParam);
|
|
517
|
-
// Restore localStorage — URL theme is view-only, not persistent
|
|
518
|
-
if (savedPref) localStorage.setItem('sdocs-theme', savedPref);
|
|
519
|
-
else localStorage.removeItem('sdocs-theme');
|
|
520
|
-
} else {
|
|
521
|
-
// Restore user's actual preferred theme (previous hash may have overridden it)
|
|
522
|
-
var preferred = S.getPreferredTheme();
|
|
523
|
-
if (preferred !== S.activeTheme) {
|
|
524
|
-
S.switchThemeAndUpdate(preferred);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (modeParam && ['read', 'style', 'write', 'raw', 'export'].includes(modeParam)) {
|
|
529
|
-
setMode(modeParam, true);
|
|
530
|
-
} else {
|
|
531
|
-
setMode('read', true);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (!mdParam) {
|
|
535
|
-
S._isDefaultState = true;
|
|
536
|
-
loadText(DEFAULT_MD);
|
|
537
|
-
if (stylesParam) {
|
|
538
|
-
try {
|
|
539
|
-
var styles = JSON.parse(atob(decodeURIComponent(stylesParam)));
|
|
540
|
-
S.applyStylesFromMeta(styles);
|
|
541
|
-
} catch (e) {
|
|
542
|
-
console.warn('sdocs-dev: could not decode #styles hash', e);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
if (secParam) {
|
|
548
|
-
setTimeout(function() {
|
|
549
|
-
var target = document.getElementById(secParam);
|
|
550
|
-
if (!target) return;
|
|
551
|
-
|
|
552
|
-
var ownSection = target.closest('.md-section');
|
|
553
|
-
if (ownSection) {
|
|
554
|
-
var ownBody = ownSection.querySelector(':scope > .md-section-body');
|
|
555
|
-
if (ownBody) { ownBody.classList.add('open'); }
|
|
556
|
-
var ownToggle = ownSection.querySelector(':scope > h1 > .section-toggle, :scope > h2 > .section-toggle, :scope > h3 > .section-toggle, :scope > h4 > .section-toggle');
|
|
557
|
-
if (ownToggle) { ownToggle.classList.add('open'); }
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
var el = target.closest('.md-section-body');
|
|
561
|
-
while (el) {
|
|
562
|
-
el.classList.add('open');
|
|
563
|
-
var parentSection = el.closest('.md-section');
|
|
564
|
-
if (parentSection) {
|
|
565
|
-
var toggle = parentSection.querySelector(':scope > h2 > .section-toggle, :scope > h3 > .section-toggle, :scope > h4 > .section-toggle');
|
|
566
|
-
if (toggle) toggle.classList.add('open');
|
|
567
|
-
}
|
|
568
|
-
el = el.parentElement ? el.parentElement.closest('.md-section-body') : null;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
var spacerNeeded = contentArea.clientHeight - (contentArea.scrollHeight - target.offsetTop);
|
|
572
|
-
if (spacerNeeded > 0) {
|
|
573
|
-
var spacer = document.createElement('div');
|
|
574
|
-
spacer.className = 'sec-scroll-spacer';
|
|
575
|
-
spacer.style.height = spacerNeeded + 'px';
|
|
576
|
-
S.renderedEl.appendChild(spacer);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
580
|
-
}, 200);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
if (!secParam) {
|
|
584
|
-
contentArea.scrollTop = 0;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// ── Init ──────────────────────────────────
|
|
589
|
-
|
|
590
|
-
(async function () {
|
|
591
|
-
await _defaultReady;
|
|
592
|
-
if (window.location.pathname === '/new') {
|
|
593
|
-
startNewDocument();
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
await loadFromHash();
|
|
597
|
-
}());
|
|
598
|
-
|
|
599
|
-
window.addEventListener('hashchange', function () {
|
|
600
|
-
loadFromHash();
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
window.addEventListener('popstate', function () {
|
|
604
|
-
loadFromHash();
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
// ── Toolbar scroll hints (fade + bounce-peek) ──────
|
|
608
|
-
|
|
609
|
-
function initScrollHint(el) {
|
|
610
|
-
function update() {
|
|
611
|
-
var hasOverflow = el.scrollWidth > el.clientWidth + 1;
|
|
612
|
-
el.classList.toggle('has-overflow', hasOverflow);
|
|
613
|
-
if (hasOverflow) {
|
|
614
|
-
var atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 2;
|
|
615
|
-
el.classList.toggle('scrolled-end', atEnd);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
el.addEventListener('scroll', update, { passive: true });
|
|
620
|
-
window.addEventListener('resize', update);
|
|
621
|
-
|
|
622
|
-
// Bounce-peek: briefly scroll right then back on first show
|
|
623
|
-
var peeked = false;
|
|
624
|
-
function peek() {
|
|
625
|
-
if (peeked) return;
|
|
626
|
-
if (el.scrollWidth <= el.clientWidth + 1) return;
|
|
627
|
-
peeked = true;
|
|
628
|
-
el.scrollTo({ left: 28, behavior: 'smooth' });
|
|
629
|
-
setTimeout(function() { el.scrollTo({ left: 0, behavior: 'smooth' }); }, 400);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Run initial check; peek after a short delay
|
|
633
|
-
update();
|
|
634
|
-
return { update: update, peek: peek };
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
var leftHint = initScrollHint(document.getElementById('left-toolbar'));
|
|
638
|
-
var writeHint = initScrollHint(document.getElementById('write-toolbar'));
|
|
639
|
-
|
|
640
|
-
// Re-check and peek when entering write mode
|
|
641
|
-
var _origSetMode = setMode;
|
|
642
|
-
setMode = function(mode, skipHash) {
|
|
643
|
-
_origSetMode(mode, skipHash);
|
|
644
|
-
if (mode === 'write') {
|
|
645
|
-
setTimeout(function() { writeHint.update(); writeHint.peek(); }, 100);
|
|
646
|
-
}
|
|
647
|
-
};
|
|
648
|
-
S.setMode = setMode;
|
|
649
|
-
|
|
650
|
-
// Check left toolbar on load
|
|
651
|
-
setTimeout(function() { leftHint.update(); leftHint.peek(); }, 500);
|
|
652
|
-
|
|
653
|
-
})();
|