sdocs-dev 1.0.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/bin/sdocs-dev.js +443 -0
- package/package.json +40 -0
- package/public/css/layout.css +237 -0
- package/public/css/mobile.css +189 -0
- package/public/css/panel.css +348 -0
- package/public/css/rendered.css +278 -0
- package/public/css/tokens.css +112 -0
- package/public/css/write.css +128 -0
- package/public/default.md +129 -0
- package/public/index.html +533 -0
- package/public/sdocs-app.js +595 -0
- package/public/sdocs-controls.js +221 -0
- package/public/sdocs-export.js +193 -0
- package/public/sdocs-state.js +43 -0
- package/public/sdocs-styles.js +285 -0
- package/public/sdocs-theme.js +168 -0
- package/public/sdocs-write.js +737 -0
- package/public/sdocs-yaml.js +78 -0
- package/server.js +65 -0
|
@@ -0,0 +1,595 @@
|
|
|
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 section = heading.closest('.md-section');
|
|
151
|
+
var body = section.querySelector('.md-section-body');
|
|
152
|
+
var toggle = section.querySelector('.section-toggle');
|
|
153
|
+
var isOpen = body.classList.toggle('open');
|
|
154
|
+
toggle.classList.toggle('open', isOpen);
|
|
155
|
+
body.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.toggle('open', isOpen); });
|
|
156
|
+
body.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.toggle('open', isOpen); });
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Status ──────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function setStatus(msg) {
|
|
164
|
+
document.getElementById('status-text').textContent = msg;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Load content ──────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function loadText(text, filename) {
|
|
170
|
+
var parsed = SDocYaml.parseFrontMatter(text);
|
|
171
|
+
S.currentMeta = parsed.meta;
|
|
172
|
+
S.currentBody = parsed.body;
|
|
173
|
+
render();
|
|
174
|
+
if (parsed.meta.styles) S.applyStylesFromMeta(parsed.meta.styles);
|
|
175
|
+
// Re-apply theme defaults for standalone colors (front matter may have
|
|
176
|
+
// theme-specific values that don't match the viewer's current theme)
|
|
177
|
+
S.STANDALONE_COLOR_IDS.forEach(function(ctrlId) {
|
|
178
|
+
if (!S.overriddenColors.has(ctrlId)) {
|
|
179
|
+
var val = S.getStandaloneDefault(ctrlId);
|
|
180
|
+
var el = document.getElementById(ctrlId);
|
|
181
|
+
if (el) {
|
|
182
|
+
el.value = val;
|
|
183
|
+
var allVals = S.readAllControlValues();
|
|
184
|
+
SDocStyles.controlToCssVars(ctrlId, val, allVals)
|
|
185
|
+
.forEach(function(a) { S.setStyleVar(a.cssVar, a.value); });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
190
|
+
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
191
|
+
setStatus(filename ? 'Loaded: ' + filename : 'Ready');
|
|
192
|
+
syncAll('load');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Compression helpers (deflate-raw + base64url) ──
|
|
196
|
+
|
|
197
|
+
function toBase64Url(bytes) {
|
|
198
|
+
var bin = Array.from(new Uint8Array(bytes), function(b) { return String.fromCharCode(b); }).join('');
|
|
199
|
+
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function fromBase64Url(str) {
|
|
203
|
+
var b64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
204
|
+
var bin = atob(b64);
|
|
205
|
+
var bytes = new Uint8Array(bin.length);
|
|
206
|
+
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
207
|
+
return bytes;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function compressText(text) {
|
|
211
|
+
var encoded = new TextEncoder().encode(text);
|
|
212
|
+
var cs = new CompressionStream('deflate-raw');
|
|
213
|
+
var writer = cs.writable.getWriter();
|
|
214
|
+
writer.write(encoded);
|
|
215
|
+
writer.close();
|
|
216
|
+
var chunks = [];
|
|
217
|
+
var reader = cs.readable.getReader();
|
|
218
|
+
while (true) {
|
|
219
|
+
var result = await reader.read();
|
|
220
|
+
if (result.done) break;
|
|
221
|
+
chunks.push(result.value);
|
|
222
|
+
}
|
|
223
|
+
var total = chunks.reduce(function(n, c) { return n + c.length; }, 0);
|
|
224
|
+
var buf = new Uint8Array(total);
|
|
225
|
+
var offset = 0;
|
|
226
|
+
for (var i = 0; i < chunks.length; i++) { buf.set(chunks[i], offset); offset += chunks[i].length; }
|
|
227
|
+
return toBase64Url(buf);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function decompressText(b64url) {
|
|
231
|
+
var bytes = fromBase64Url(b64url);
|
|
232
|
+
var ds = new DecompressionStream('deflate-raw');
|
|
233
|
+
var writer = ds.writable.getWriter();
|
|
234
|
+
writer.write(bytes);
|
|
235
|
+
writer.close();
|
|
236
|
+
var chunks = [];
|
|
237
|
+
var reader = ds.readable.getReader();
|
|
238
|
+
while (true) {
|
|
239
|
+
var result = await reader.read();
|
|
240
|
+
if (result.done) break;
|
|
241
|
+
chunks.push(result.value);
|
|
242
|
+
}
|
|
243
|
+
var total = chunks.reduce(function(n, c) { return n + c.length; }, 0);
|
|
244
|
+
var buf = new Uint8Array(total);
|
|
245
|
+
var offset = 0;
|
|
246
|
+
for (var i = 0; i < chunks.length; i++) { buf.set(chunks[i], offset); offset += chunks[i].length; }
|
|
247
|
+
return new TextDecoder().decode(buf);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Auto-save to URL hash ──────────────────────────
|
|
251
|
+
|
|
252
|
+
function updateHash() {
|
|
253
|
+
clearTimeout(S._hashTimer);
|
|
254
|
+
S._hashTimer = setTimeout(async function() {
|
|
255
|
+
if (S._isDefaultState && S.currentMode === 'read') {
|
|
256
|
+
history.replaceState(null, '', window.location.pathname === '/new' ? '/' : window.location.pathname);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
var params = new URLSearchParams();
|
|
260
|
+
if (!S._isDefaultState) {
|
|
261
|
+
var meta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
262
|
+
var full = SDocYaml.serializeFrontMatter(meta) + '\n' + S.currentBody;
|
|
263
|
+
var compressed = await compressText(full);
|
|
264
|
+
params.set('md', compressed);
|
|
265
|
+
}
|
|
266
|
+
if (S.currentMode !== 'read') {
|
|
267
|
+
params.set('mode', S.currentMode);
|
|
268
|
+
}
|
|
269
|
+
var basePath = window.location.pathname === '/new' ? '/' : window.location.pathname;
|
|
270
|
+
history.replaceState(null, '', basePath + '#' + params.toString());
|
|
271
|
+
}, 400);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── State sync ──────────────────────────────────
|
|
275
|
+
|
|
276
|
+
function syncAll(source) {
|
|
277
|
+
if (S._syncing) return;
|
|
278
|
+
S._syncing = true;
|
|
279
|
+
try {
|
|
280
|
+
if (source === 'controls') {
|
|
281
|
+
S._isDefaultState = false;
|
|
282
|
+
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
283
|
+
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
284
|
+
updateHash();
|
|
285
|
+
} else if (source === 'raw') {
|
|
286
|
+
S._isDefaultState = false;
|
|
287
|
+
var parsed = SDocYaml.parseFrontMatter(S.rawEl.value);
|
|
288
|
+
S.currentMeta = parsed.meta;
|
|
289
|
+
S.currentBody = parsed.body;
|
|
290
|
+
render();
|
|
291
|
+
if (parsed.meta.styles) S.applyStylesFromMeta(parsed.meta.styles);
|
|
292
|
+
updateHash();
|
|
293
|
+
} else if (source === 'write') {
|
|
294
|
+
S._isDefaultState = false;
|
|
295
|
+
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
296
|
+
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
297
|
+
updateHash();
|
|
298
|
+
} else if (source === 'load') {
|
|
299
|
+
updateHash();
|
|
300
|
+
}
|
|
301
|
+
} finally {
|
|
302
|
+
S._syncing = false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── Drag & drop ──────────────────────────────────
|
|
307
|
+
|
|
308
|
+
var contentArea = document.getElementById('content-area');
|
|
309
|
+
|
|
310
|
+
contentArea.addEventListener('dragover', function(e) {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
contentArea.classList.add('drag-over');
|
|
313
|
+
});
|
|
314
|
+
['dragleave','dragend'].forEach(function(ev) {
|
|
315
|
+
contentArea.addEventListener(ev, function() { contentArea.classList.remove('drag-over'); });
|
|
316
|
+
});
|
|
317
|
+
contentArea.addEventListener('drop', function(e) {
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
contentArea.classList.remove('drag-over');
|
|
320
|
+
var file = e.dataTransfer.files[0];
|
|
321
|
+
if (!file) return;
|
|
322
|
+
var reader = new FileReader();
|
|
323
|
+
reader.onload = function(ev) { S._isDefaultState = false; loadText(ev.target.result, file.name); };
|
|
324
|
+
reader.readAsText(file);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
S.rawEl.addEventListener('input', function() {
|
|
328
|
+
clearTimeout(S._rawSyncTimer);
|
|
329
|
+
S._rawSyncTimer = setTimeout(function() { syncAll('raw'); }, 300);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ── Mode toggle (read / style / raw / export) ──────────────────
|
|
333
|
+
|
|
334
|
+
function setMode(mode, skipHash) {
|
|
335
|
+
var prev = S.currentMode;
|
|
336
|
+
S.currentMode = mode;
|
|
337
|
+
|
|
338
|
+
// Exit write mode — extract markdown back
|
|
339
|
+
if (prev === 'write' && mode !== 'write') {
|
|
340
|
+
S.exitWriteMode();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
S.renderedEl.style.display = (mode === 'raw' || mode === 'write') ? 'none' : '';
|
|
344
|
+
S.rawEl.style.display = mode === 'raw' ? 'block' : 'none';
|
|
345
|
+
|
|
346
|
+
document.getElementById('btn-read').classList.toggle('active', mode === 'read');
|
|
347
|
+
document.getElementById('btn-style').classList.toggle('active', mode === 'style');
|
|
348
|
+
document.getElementById('btn-write').classList.toggle('active', mode === 'write');
|
|
349
|
+
document.getElementById('btn-raw').classList.toggle('active', mode === 'raw');
|
|
350
|
+
document.getElementById('btn-export').classList.toggle('active', mode === 'export');
|
|
351
|
+
|
|
352
|
+
document.body.classList.toggle('style-mode', mode === 'style');
|
|
353
|
+
document.body.classList.toggle('read-mode', mode === 'read');
|
|
354
|
+
document.body.classList.toggle('write-mode', mode === 'write');
|
|
355
|
+
document.body.classList.toggle('raw-mode', mode === 'raw');
|
|
356
|
+
document.body.classList.toggle('export-mode', mode === 'export');
|
|
357
|
+
document.body.classList.remove('mobile-sheet-open');
|
|
358
|
+
document.body.classList.remove('mobile-export-open');
|
|
359
|
+
|
|
360
|
+
// Enter write mode — populate contentEditable
|
|
361
|
+
if (mode === 'write') {
|
|
362
|
+
S.enterWriteMode();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!skipHash) updateHash();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
document.getElementById('btn-theme').addEventListener('click', function() { S.toggleTheme(); });
|
|
369
|
+
document.getElementById('btn-read').addEventListener('click', function() { setMode('read'); });
|
|
370
|
+
document.getElementById('btn-style').addEventListener('click', function() { setMode('style'); });
|
|
371
|
+
document.getElementById('btn-write').addEventListener('click', function() { setMode('write'); });
|
|
372
|
+
document.getElementById('btn-raw').addEventListener('click', function() { setMode('raw'); });
|
|
373
|
+
document.getElementById('btn-export').addEventListener('click', function() { setMode('export'); });
|
|
374
|
+
|
|
375
|
+
document.getElementById('btn-new').addEventListener('click', function() {
|
|
376
|
+
history.replaceState(null, '', '/new');
|
|
377
|
+
startNewDocument();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
document.getElementById('btn-expand-all').addEventListener('click', function() {
|
|
381
|
+
S.renderedEl.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.add('open'); });
|
|
382
|
+
S.renderedEl.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.add('open'); });
|
|
383
|
+
});
|
|
384
|
+
document.getElementById('btn-collapse-all').addEventListener('click', function() {
|
|
385
|
+
S.renderedEl.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.remove('open'); });
|
|
386
|
+
S.renderedEl.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.remove('open'); });
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
document.getElementById('right-header').addEventListener('click', function() {
|
|
390
|
+
if (window.innerWidth <= 768) {
|
|
391
|
+
document.body.classList.toggle('mobile-sheet-open');
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
document.getElementById('export-panel-header').addEventListener('click', function() {
|
|
396
|
+
if (window.innerWidth <= 768) {
|
|
397
|
+
document.body.classList.toggle('mobile-export-open');
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
document.getElementById('factory-reset-styles').addEventListener('click', function() {
|
|
402
|
+
S.resetAllStyles();
|
|
403
|
+
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
404
|
+
S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
405
|
+
render();
|
|
406
|
+
syncAll('load');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
document.getElementById('toolbar-brand').addEventListener('click', function(e) {
|
|
410
|
+
e.preventDefault();
|
|
411
|
+
S.resetAllStyles();
|
|
412
|
+
loadText(DEFAULT_MD);
|
|
413
|
+
S._isDefaultState = true;
|
|
414
|
+
clearTimeout(S._hashTimer);
|
|
415
|
+
history.replaceState(null, '', window.location.pathname);
|
|
416
|
+
setMode('read');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ── Collapsible panels ──────────────────────────────
|
|
420
|
+
|
|
421
|
+
document.querySelectorAll('.panel-header').forEach(function(h) {
|
|
422
|
+
h.addEventListener('click', function() {
|
|
423
|
+
var body = document.getElementById(h.dataset.target);
|
|
424
|
+
var open = body.classList.toggle('open');
|
|
425
|
+
h.classList.toggle('open', open);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
document.querySelectorAll('.sub-header').forEach(function(h) {
|
|
429
|
+
h.addEventListener('click', function() {
|
|
430
|
+
var body = document.getElementById(h.dataset.target);
|
|
431
|
+
var open = body.classList.toggle('open');
|
|
432
|
+
h.classList.toggle('open', open);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// ── Default content ──────────────────────────────────
|
|
437
|
+
|
|
438
|
+
var DEFAULT_MD = '';
|
|
439
|
+
var _defaultReady = fetch('/public/default.md').then(function(r) { return r.text(); }).then(function(t) { DEFAULT_MD = t; });
|
|
440
|
+
|
|
441
|
+
// ── Register on SDocs for cross-module access ──────────
|
|
442
|
+
|
|
443
|
+
function startNewDocument() {
|
|
444
|
+
S.resetAllStyles();
|
|
445
|
+
S.currentBody = '';
|
|
446
|
+
S.currentMeta = {};
|
|
447
|
+
S._isDefaultState = false;
|
|
448
|
+
clearTimeout(S._hashTimer);
|
|
449
|
+
render();
|
|
450
|
+
setMode('write', true);
|
|
451
|
+
var w = S.writeEl;
|
|
452
|
+
w.innerHTML = '<h1><br></h1>';
|
|
453
|
+
w.focus();
|
|
454
|
+
var range = document.createRange();
|
|
455
|
+
range.selectNodeContents(w.querySelector('h1'));
|
|
456
|
+
range.collapse(false);
|
|
457
|
+
var sel = window.getSelection();
|
|
458
|
+
sel.removeAllRanges();
|
|
459
|
+
sel.addRange(range);
|
|
460
|
+
setTimeout(function() { S.updateToolbarState(); }, 0);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
S.syncAll = syncAll;
|
|
464
|
+
S.setStatus = setStatus;
|
|
465
|
+
S.setMode = setMode;
|
|
466
|
+
S.render = render;
|
|
467
|
+
S.loadText = loadText;
|
|
468
|
+
|
|
469
|
+
// ── Init ──────────────────────────────────
|
|
470
|
+
|
|
471
|
+
(async function () {
|
|
472
|
+
await _defaultReady;
|
|
473
|
+
if (window.location.pathname === '/new') {
|
|
474
|
+
startNewDocument();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
var hash = window.location.hash.slice(1);
|
|
478
|
+
var params = hash ? new URLSearchParams(hash) : new URLSearchParams();
|
|
479
|
+
var mdParam = params.get('md');
|
|
480
|
+
var modeParam = params.get('mode');
|
|
481
|
+
var stylesParam = params.get('styles');
|
|
482
|
+
if (mdParam) {
|
|
483
|
+
try {
|
|
484
|
+
S._isDefaultState = false;
|
|
485
|
+
var text = await decompressText(mdParam);
|
|
486
|
+
loadText(text, 'document.md');
|
|
487
|
+
} catch (e) {
|
|
488
|
+
console.warn('sdocs-dev: could not decode hash', e);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (modeParam && ['read', 'style', 'write', 'raw', 'export'].includes(modeParam)) {
|
|
492
|
+
setMode(modeParam, true);
|
|
493
|
+
} else {
|
|
494
|
+
setMode('read', true);
|
|
495
|
+
}
|
|
496
|
+
if (!mdParam) {
|
|
497
|
+
loadText(DEFAULT_MD);
|
|
498
|
+
if (stylesParam) {
|
|
499
|
+
try {
|
|
500
|
+
var styles = JSON.parse(atob(decodeURIComponent(stylesParam)));
|
|
501
|
+
S.applyStylesFromMeta(styles);
|
|
502
|
+
} catch (e) {
|
|
503
|
+
console.warn('sdocs-dev: could not decode #styles hash', e);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
var secParam = params.get('sec');
|
|
509
|
+
if (secParam) {
|
|
510
|
+
setTimeout(function() {
|
|
511
|
+
var target = document.getElementById(secParam);
|
|
512
|
+
if (!target) return;
|
|
513
|
+
|
|
514
|
+
// Expand the target's own section body (its content)
|
|
515
|
+
var ownSection = target.closest('.md-section');
|
|
516
|
+
if (ownSection) {
|
|
517
|
+
var ownBody = ownSection.querySelector(':scope > .md-section-body');
|
|
518
|
+
if (ownBody) { ownBody.classList.add('open'); }
|
|
519
|
+
var ownToggle = ownSection.querySelector(':scope > h1 > .section-toggle, :scope > h2 > .section-toggle, :scope > h3 > .section-toggle, :scope > h4 > .section-toggle');
|
|
520
|
+
if (ownToggle) { ownToggle.classList.add('open'); }
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Expand all ancestor section bodies
|
|
524
|
+
var el = target.closest('.md-section-body');
|
|
525
|
+
while (el) {
|
|
526
|
+
el.classList.add('open');
|
|
527
|
+
var parentSection = el.closest('.md-section');
|
|
528
|
+
if (parentSection) {
|
|
529
|
+
var toggle = parentSection.querySelector(':scope > h2 > .section-toggle, :scope > h3 > .section-toggle, :scope > h4 > .section-toggle');
|
|
530
|
+
if (toggle) toggle.classList.add('open');
|
|
531
|
+
}
|
|
532
|
+
el = el.parentElement ? el.parentElement.closest('.md-section-body') : null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Add scroll spacer so the target can reach the top
|
|
536
|
+
var spacerNeeded = contentArea.clientHeight - (contentArea.scrollHeight - target.offsetTop);
|
|
537
|
+
if (spacerNeeded > 0) {
|
|
538
|
+
var spacer = document.createElement('div');
|
|
539
|
+
spacer.className = 'sec-scroll-spacer';
|
|
540
|
+
spacer.style.height = spacerNeeded + 'px';
|
|
541
|
+
S.renderedEl.appendChild(spacer);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
545
|
+
}, 200);
|
|
546
|
+
}
|
|
547
|
+
}());
|
|
548
|
+
|
|
549
|
+
// ── Toolbar scroll hints (fade + bounce-peek) ──────
|
|
550
|
+
|
|
551
|
+
function initScrollHint(el) {
|
|
552
|
+
function update() {
|
|
553
|
+
var hasOverflow = el.scrollWidth > el.clientWidth + 1;
|
|
554
|
+
el.classList.toggle('has-overflow', hasOverflow);
|
|
555
|
+
if (hasOverflow) {
|
|
556
|
+
var atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 2;
|
|
557
|
+
el.classList.toggle('scrolled-end', atEnd);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
el.addEventListener('scroll', update, { passive: true });
|
|
562
|
+
window.addEventListener('resize', update);
|
|
563
|
+
|
|
564
|
+
// Bounce-peek: briefly scroll right then back on first show
|
|
565
|
+
var peeked = false;
|
|
566
|
+
function peek() {
|
|
567
|
+
if (peeked) return;
|
|
568
|
+
if (el.scrollWidth <= el.clientWidth + 1) return;
|
|
569
|
+
peeked = true;
|
|
570
|
+
el.scrollTo({ left: 28, behavior: 'smooth' });
|
|
571
|
+
setTimeout(function() { el.scrollTo({ left: 0, behavior: 'smooth' }); }, 400);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Run initial check; peek after a short delay
|
|
575
|
+
update();
|
|
576
|
+
return { update: update, peek: peek };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
var leftHint = initScrollHint(document.getElementById('left-toolbar'));
|
|
580
|
+
var writeHint = initScrollHint(document.getElementById('write-toolbar'));
|
|
581
|
+
|
|
582
|
+
// Re-check and peek when entering write mode
|
|
583
|
+
var _origSetMode = setMode;
|
|
584
|
+
setMode = function(mode, skipHash) {
|
|
585
|
+
_origSetMode(mode, skipHash);
|
|
586
|
+
if (mode === 'write') {
|
|
587
|
+
setTimeout(function() { writeHint.update(); writeHint.peek(); }, 100);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
S.setMode = setMode;
|
|
591
|
+
|
|
592
|
+
// Check left toolbar on load
|
|
593
|
+
setTimeout(function() { leftHint.update(); leftHint.peek(); }, 500);
|
|
594
|
+
|
|
595
|
+
})();
|