sdocs-dev 1.2.0 → 1.3.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.
@@ -26,6 +26,13 @@
26
26
  #write { padding: 24px 20px 40px; max-width: 100%; }
27
27
  #raw { padding: 20px 16px; min-height: calc(100dvh - 36px); font-size: 16px; }
28
28
 
29
+ /* ── File-info card: inset 20px from viewport edges ── */
30
+ #file-info-card {
31
+ max-width: none;
32
+ margin: 16px 20px 0;
33
+ padding: 8px 12px;
34
+ }
35
+
29
36
  /* ── Write toolbar: sticky below left-toolbar ── */
30
37
  body.write-mode #write-toolbar { position: sticky; top: 36px; z-index: 98; justify-content: flex-start; }
31
38
 
@@ -383,7 +383,7 @@ body.export-mode #export-panel {
383
383
  }
384
384
  #statusbar span {
385
385
  font-size: 11px;
386
- color: var(--text-3);
386
+ color: var(--text-2);
387
387
  }
388
388
  #statusbar .dot {
389
389
  width: 6px; height: 6px;
@@ -104,6 +104,7 @@
104
104
  color: var(--md-link-color);
105
105
  text-decoration: var(--md-link-decoration);
106
106
  text-underline-offset: 2px;
107
+ cursor: pointer;
107
108
  }
108
109
  :is(#rendered, #write) a:hover { opacity: .8; }
109
110
 
@@ -129,7 +130,15 @@
129
130
  #rendered .section-toggle.open {
130
131
  transform: rotate(90deg);
131
132
  }
132
- #rendered .section-toggle:hover {
133
+ #rendered .section-toggle:hover,
134
+ #rendered h1:hover > .section-toggle,
135
+ #rendered .md-section > h2:hover > .section-toggle,
136
+ #rendered .md-section > h3:hover > .section-toggle,
137
+ #rendered .md-section > h4:hover > .section-toggle,
138
+ #rendered > h1:hover ~ .md-section .section-toggle,
139
+ #rendered .md-section > h2:hover ~ .md-section-body .section-toggle,
140
+ #rendered .md-section > h3:hover ~ .md-section-body .section-toggle,
141
+ #rendered .md-section > h4:hover ~ .md-section-body .section-toggle {
133
142
  color: var(--text-2);
134
143
  }
135
144
  #rendered .md-section-body:not(.open):not(:has(.md-section-body.open)) > :not(.md-section) {
@@ -254,6 +263,81 @@
254
263
  white-space: pre;
255
264
  word-break: normal;
256
265
  }
266
+ /* ── Charts ── */
267
+ .sdoc-chart {
268
+ margin: 1.2em 0;
269
+ padding: 16px;
270
+ border-radius: var(--radius, 8px);
271
+ background: var(--md-chart-bg, var(--md-block-bg, #F5F2EE));
272
+ max-width: 100%;
273
+ position: relative;
274
+ }
275
+ .sdoc-chart canvas { max-height: 400px; }
276
+ .sdoc-chart-error { outline: 2px dashed #ef4444; }
277
+
278
+ /* Chart menu */
279
+ .chart-menu-btn {
280
+ position: absolute; top: 6px; right: 6px;
281
+ background: var(--md-chart-bg, var(--md-block-bg, #F5F2EE));
282
+ border: 1px solid var(--md-copy-btn-border, rgba(0,0,0,0.1));
283
+ border-radius: 4px;
284
+ padding: 3px 5px;
285
+ cursor: pointer; opacity: 0.7;
286
+ transition: opacity .15s;
287
+ color: var(--md-chart-text, var(--text-3, #999));
288
+ font-size: 12px; line-height: 1;
289
+ display: flex; align-items: center; justify-content: center;
290
+ z-index: 2;
291
+ }
292
+ .chart-menu-btn:hover { opacity: 1; background: var(--md-copy-btn-hover, rgba(0,0,0,0.05)); }
293
+
294
+ .chart-menu {
295
+ display: none; position: absolute; top: 36px; right: 8px;
296
+ min-width: 160px; width: max-content; background: var(--bg-surface, #fff);
297
+ border: 1px solid var(--border, #e0dcd7);
298
+ border-radius: var(--radius, 8px);
299
+ box-shadow: 0 4px 16px rgba(0,0,0,0.12);
300
+ padding: 6px 0; z-index: 10; font-size: 13px; color: var(--text, #333);
301
+ }
302
+ .chart-menu.open { display: block; }
303
+
304
+ .chart-menu-item {
305
+ display: block; width: 100%; padding: 6px 14px;
306
+ border: none; background: none; text-align: left;
307
+ cursor: pointer; font-size: 13px; color: var(--text, #333);
308
+ }
309
+ .chart-menu-item:hover { background: var(--bg-hover, #f5f3f0); }
310
+
311
+ .chart-menu-sep { height: 1px; background: var(--border, #e0dcd7); margin: 4px 0; }
312
+
313
+ .chart-menu-toggle {
314
+ display: flex; align-items: center; gap: 6px;
315
+ padding: 5px 14px; cursor: pointer; font-size: 13px; color: var(--text-2, #666);
316
+ }
317
+ .chart-menu-toggle:hover { background: var(--bg-hover, #f5f3f0); }
318
+ .chart-menu-toggle input { margin: 0; }
319
+
320
+ .chart-menu-types { display: flex; gap: 3px; padding: 4px 10px; flex-wrap: wrap; }
321
+ .chart-type-btn {
322
+ padding: 3px 10px; border: 1px solid var(--border, #e0dcd7);
323
+ background: none; border-radius: var(--radius-sm, 4px);
324
+ font-size: 11px; cursor: pointer; color: var(--text-2, #666);
325
+ white-space: nowrap;
326
+ }
327
+ .chart-type-btn:hover { background: var(--bg-hover, #f5f3f0); }
328
+ .chart-type-btn.active {
329
+ background: var(--bg-input, #fff); color: var(--text, #333);
330
+ border-color: var(--border-strong, #c0bbb5); box-shadow: 0 1px 2px rgba(0,0,0,0.06);
331
+ }
332
+
333
+ .chart-menu-input {
334
+ display: block; width: calc(100% - 28px); margin: 4px 14px;
335
+ padding: 4px 8px; border: 1px solid var(--border, #e0dcd7);
336
+ border-radius: var(--radius-sm, 4px); font-size: 12px;
337
+ background: var(--bg-input, #fff); color: var(--text, #333);
338
+ }
339
+ .chart-menu-input:focus { outline: none; border-color: var(--accent, #3b82f6); }
340
+
257
341
  :is(#rendered, #write) blockquote {
258
342
  border-left: var(--md-bq-border);
259
343
  font-size: var(--md-bq-size);
@@ -293,3 +377,102 @@
293
377
  }
294
378
  :is(#rendered, #write) tr:nth-child(even) td { background: var(--md-table-even-bg); }
295
379
  :is(#rendered, #write) img { max-width: 100%; border-radius: var(--radius-md); }
380
+
381
+ /* ── File-info card (shown when front matter has `file:`) ─────────── */
382
+ /* Inherits the block cascade: --md-fic-bg / --md-fic-text fall back to
383
+ --md-block-bg / --md-block-text so the card blends in with code blocks,
384
+ blockquotes, and charts. Override per-doc via `styles.fileinfo` in YAML. */
385
+ #file-info-card[hidden] { display: none; }
386
+ #file-info-card {
387
+ --fic-bg: var(--md-fic-bg, var(--md-block-bg, var(--bg)));
388
+ --fic-text: var(--md-fic-text, var(--md-block-text, var(--text)));
389
+ --fic-muted: color-mix(in srgb, var(--fic-text) 60%, transparent);
390
+ --fic-border: color-mix(in srgb, var(--fic-text) 15%, transparent);
391
+ --fic-hover: color-mix(in srgb, var(--fic-text) 8%, transparent);
392
+
393
+ max-width: 680px;
394
+ margin: 24px auto 0;
395
+ padding: 10px 16px;
396
+ background: var(--fic-bg);
397
+ border: 1px solid var(--fic-border);
398
+ border-radius: var(--radius-md);
399
+ box-shadow: var(--shadow-sm);
400
+ font-family: var(--font-ui);
401
+ font-size: 13px;
402
+ color: var(--fic-text);
403
+ display: flex;
404
+ flex-direction: column;
405
+ gap: 6px;
406
+ }
407
+ #file-info-card .fic-rows { display: flex; flex-direction: column; gap: 4px; }
408
+ #file-info-card .fic-row {
409
+ display: flex;
410
+ align-items: center;
411
+ gap: 8px;
412
+ padding: 2px 0;
413
+ border-radius: var(--radius-sm);
414
+ }
415
+ #file-info-card .fic-label {
416
+ color: var(--fic-muted);
417
+ font-size: 11px;
418
+ font-weight: 600;
419
+ text-transform: uppercase;
420
+ letter-spacing: 0.04em;
421
+ min-width: 52px;
422
+ }
423
+ #file-info-card .fic-value {
424
+ flex: 1;
425
+ min-width: 0;
426
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
427
+ font-size: 12.5px;
428
+ color: var(--fic-text);
429
+ overflow-wrap: anywhere;
430
+ word-break: break-word;
431
+ }
432
+ #file-info-card .fic-local-pill {
433
+ font-size: 10px;
434
+ font-weight: 600;
435
+ text-transform: uppercase;
436
+ letter-spacing: 0.04em;
437
+ color: var(--fic-muted);
438
+ background: var(--fic-hover);
439
+ border: 1px solid var(--fic-border);
440
+ border-radius: 999px;
441
+ padding: 2px 7px;
442
+ }
443
+ #file-info-card .fic-copy,
444
+ #file-info-card .fic-action {
445
+ display: inline-flex;
446
+ align-items: center;
447
+ gap: 6px;
448
+ background: transparent;
449
+ border: 1px solid var(--fic-border);
450
+ color: var(--fic-text);
451
+ border-radius: var(--radius-sm);
452
+ padding: 4px 8px;
453
+ font-family: var(--font-ui);
454
+ font-size: 12px;
455
+ cursor: pointer;
456
+ transition: background .1s, border-color .1s, color .1s;
457
+ }
458
+ #file-info-card .fic-copy { padding: 3px 6px; }
459
+ #file-info-card .fic-copy:hover,
460
+ #file-info-card .fic-action:hover {
461
+ background: var(--fic-hover);
462
+ color: var(--fic-text);
463
+ }
464
+ #file-info-card .fic-footer {
465
+ display: flex;
466
+ align-items: center;
467
+ gap: 10px;
468
+ margin-top: 2px;
469
+ }
470
+ #file-info-card .fic-privacy-note {
471
+ margin-left: auto;
472
+ display: inline-flex;
473
+ align-items: center;
474
+ gap: 6px;
475
+ font-size: 11px;
476
+ color: var(--fic-muted);
477
+ }
478
+ #file-info-card .fic-privacy-note[hidden] { display: none; }
package/public/index.html CHANGED
@@ -11,7 +11,7 @@
11
11
  <link rel="stylesheet" href="/public/css/panel.css">
12
12
  <link rel="stylesheet" href="/public/css/write.css">
13
13
  <link rel="stylesheet" href="/public/css/mobile.css">
14
- <script>
14
+ <script nonce="__CSP_NONCE__">
15
15
  (function(){
16
16
  var t = localStorage.getItem('sdocs-theme');
17
17
  if (!t) t = window.matchMedia('(prefers-color-scheme:dark)').matches ? 'dark' : 'light';
@@ -46,22 +46,14 @@
46
46
  <button class="btn" id="btn-export" title="Export">
47
47
  <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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
48
48
  </button>
49
- <span class="write-tb-sep"></span>
50
49
  <button class="btn" id="btn-new" title="New document">
51
50
  <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="M11.35 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v5.35"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M14 19h6"/><path d="M17 16v6"/></svg>
52
51
  </button>
52
+ <span class="write-tb-sep"></span>
53
+ <button class="btn" id="btn-theme" title="Toggle dark mode" aria-label="Toggle dark mode">
54
+ <svg id="icon-theme" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" stroke-width="2"/></svg>
55
+ </button>
53
56
  </div>
54
- <button id="btn-expand-all" class="toolbar-icon-btn" title="Expand all sections" aria-label="Expand all sections">
55
- <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M15 6V11C15 13.21 13.21 15 11 15H6C5.26 15 4.62 14.6 4.27 14H11C12.65 14 14 12.65 14 11V4.27C14.6 4.62 15 5.26 15 6ZM11 13H4C2.897 13 2 12.103 2 11V4C2 2.897 2.897 2 4 2H11C12.103 2 13 2.897 13 4V11C13 12.103 12.103 13 11 13ZM4 12H11C11.551 12 12 11.552 12 11V4C12 3.449 11.551 3 11 3H4C3.449 3 3 3.449 3 4V11C3 11.552 3.449 12 4 12ZM9.5 7H8V5.5C8 5.224 7.776 5 7.5 5C7.224 5 7 5.224 7 5.5V7H5.5C5.224 7 5 7.224 5 7.5C5 7.776 5.224 8 5.5 8H7V9.5C7 9.776 7.224 10 7.5 10C7.776 10 8 9.776 8 9.5V8H9.5C9.776 8 10 7.776 10 7.5C10 7.224 9.776 7 9.5 7Z"/></svg>
56
- </button>
57
- <button id="btn-collapse-all" class="toolbar-icon-btn" title="Collapse all sections" aria-label="Collapse all sections">
58
- <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M14 4.27051C14.5999 4.62053 15 5.26009 15 6V11C15 13.21 13.21 15 11 15H6C5.26009 15 4.62053 14.5999 4.27051 14H11C12.65 14 14 12.65 14 11V4.27051Z"/><path d="M9.5 7C9.776 7 10 7.224 10 7.5C10 7.776 9.776 8 9.5 8H5.5C5.224 8 5 7.776 5 7.5C5 7.224 5.224 7 5.5 7H9.5Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M11 2C12.103 2 13 2.897 13 4V11C13 12.103 12.103 13 11 13H4C2.897 13 2 12.103 2 11V4C2 2.897 2.897 2 4 2H11ZM4 3C3.449 3 3 3.449 3 4V11C3 11.552 3.449 12 4 12H11C11.551 12 12 11.552 12 11V4C12 3.449 11.551 3 11 3H4Z"/></svg>
59
- </button>
60
- <button id="btn-theme" title="Toggle dark mode" aria-label="Toggle dark mode">
61
- <svg id="icon-theme" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
62
- <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
63
- </svg>
64
- </button>
65
57
  <span id="drop-hint">
66
58
  <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M6 1v7M3.5 5.5L6 8l2.5-2.5M2 10h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
67
59
  Drop .md to load
@@ -105,7 +97,17 @@
105
97
  <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="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/><line x1="4" y1="20" x2="20" y2="4" stroke-width="1.5" opacity="0.5"/></svg>
106
98
  </button>
107
99
  </div>
108
- <div id="content-area">
100
+ <div id="content-area" tabindex="0">
101
+ <div id="file-info-card" hidden>
102
+ <div class="fic-rows"></div>
103
+ <div class="fic-footer">
104
+ <button class="fic-action" id="fic-copy-all" title="Copy the full markdown (body + front matter)">
105
+ <svg width="13" height="13" 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" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
106
+ <span>Copy whole file</span>
107
+ </button>
108
+ <span class="fic-privacy-note" hidden><span class="fic-local-pill">local only</span> rows aren't included in shared sdocs</span>
109
+ </div>
110
+ </div>
109
111
  <div id="rendered"></div>
110
112
  <div id="write" contenteditable="true" spellcheck="true"></div>
111
113
  <textarea id="raw" spellcheck="false"></textarea>
@@ -218,37 +220,87 @@
218
220
  </div>
219
221
 
220
222
  <div class="sub-section">
221
- <div class="sub-header" data-target="sub-colors-code">
222
- <span class="arrow">▶</span> Code
223
+ <div class="sub-header" data-target="sub-colors-blocks">
224
+ <span class="arrow">▶</span> Blocks
223
225
  </div>
224
- <div class="sub-body" id="sub-colors-code">
226
+ <div class="sub-body" id="sub-colors-blocks">
227
+ <p style="font-size:11px; color:var(--text-3); margin:0 0 8px; line-height:1.5;">Default for code blocks, blockquotes, and charts. Override individually below.</p>
225
228
  <div class="control-row">
226
229
  <label>Background</label>
227
- <div class="color-pair"><input type="color" id="ctrl-code-bg" value="#f4f1ed"><button class="reset-btn" id="reset-code-bg" title="Reset to default">↺</button></div>
230
+ <div class="color-pair"><input type="color" id="ctrl-block-bg" value="#f4f1ed"><button class="reset-btn" id="reset-block-bg" title="Reset to default">↺</button></div>
228
231
  </div>
229
232
  <div class="control-row">
230
233
  <label>Text</label>
231
- <div class="color-pair"><input type="color" id="ctrl-code-color" value="#6b21a8"><button class="reset-btn" id="reset-code-color" title="Reset to default">↺</button></div>
234
+ <div class="color-pair"><input type="color" id="ctrl-block-text" value="#6b6560"><button class="reset-btn" id="reset-block-text" title="Reset to default">↺</button></div>
232
235
  </div>
233
- </div>
234
- </div>
235
236
 
236
- <div class="sub-section">
237
- <div class="sub-header" data-target="sub-colors-bq">
238
- <span class="arrow">▶</span> Blockquote
239
- </div>
240
- <div class="sub-body" id="sub-colors-bq">
241
- <div class="control-row">
242
- <label>Border</label>
243
- <div class="color-pair"><input type="color" id="ctrl-bq-border-color" value="#2563eb"><button class="reset-btn" id="reset-bq-border-color" title="Reset to default">↺</button></div>
237
+ <div class="sub-section">
238
+ <div class="sub-header" data-target="sub-colors-code">
239
+ <span class="arrow">▶</span> Code
240
+ </div>
241
+ <div class="sub-body" id="sub-colors-code">
242
+ <div class="control-row">
243
+ <label>Background</label>
244
+ <div class="color-pair"><input type="color" id="ctrl-code-bg" value="#f4f1ed"><button class="reset-btn" id="reset-code-bg" title="Follow block background">↺</button></div>
245
+ </div>
246
+ <div class="control-row">
247
+ <label>Text</label>
248
+ <div class="color-pair"><input type="color" id="ctrl-code-color" value="#6b21a8"><button class="reset-btn" id="reset-code-color" title="Follow block text">↺</button></div>
249
+ </div>
250
+ </div>
244
251
  </div>
245
- <div class="control-row">
246
- <label>Background</label>
247
- <div class="color-pair"><input type="color" id="ctrl-bq-bg" value="#f7f5f2"><button class="reset-btn" id="reset-bq-bg" title="Reset to default">↺</button></div>
252
+
253
+ <div class="sub-section">
254
+ <div class="sub-header" data-target="sub-colors-bq">
255
+ <span class="arrow">▶</span> Blockquote
256
+ </div>
257
+ <div class="sub-body" id="sub-colors-bq">
258
+ <div class="control-row">
259
+ <label>Border</label>
260
+ <div class="color-pair"><input type="color" id="ctrl-bq-border-color" value="#2563eb"><button class="reset-btn" id="reset-bq-border-color" title="Reset to default">↺</button></div>
261
+ </div>
262
+ <div class="control-row">
263
+ <label>Background</label>
264
+ <div class="color-pair"><input type="color" id="ctrl-bq-bg" value="#f7f5f2"><button class="reset-btn" id="reset-bq-bg" title="Follow block background">↺</button></div>
265
+ </div>
266
+ <div class="control-row">
267
+ <label>Text</label>
268
+ <div class="color-pair"><input type="color" id="ctrl-bq-color" value="#6b6560"><button class="reset-btn" id="reset-bq-color" title="Follow block text">↺</button></div>
269
+ </div>
270
+ </div>
248
271
  </div>
249
- <div class="control-row">
250
- <label>Text</label>
251
- <div class="color-pair"><input type="color" id="ctrl-bq-color" value="#6b6560"><button class="reset-btn" id="reset-bq-color" title="Reset to default">↺</button></div>
272
+
273
+ <div class="sub-section">
274
+ <div class="sub-header" data-target="sub-colors-charts">
275
+ <span class="arrow">▶</span> Charts
276
+ </div>
277
+ <div class="sub-body" id="sub-colors-charts">
278
+ <div class="control-row">
279
+ <label>Background</label>
280
+ <div class="color-pair"><input type="color" id="ctrl-chart-bg" value="#f4f1ed"><button class="reset-btn" id="reset-chart-bg" title="Follow block background">↺</button></div>
281
+ </div>
282
+ <div class="control-row">
283
+ <label>Text</label>
284
+ <div class="color-pair"><input type="color" id="ctrl-chart-text" value="#78716c"><button class="reset-btn" id="reset-chart-text" title="Follow block text">↺</button></div>
285
+ </div>
286
+ <div class="control-row">
287
+ <label>Accent</label>
288
+ <div class="color-pair"><input type="color" id="ctrl-chart-accent" value="#3b82f6"><button class="reset-btn" id="reset-chart-accent" title="Reset to default palette">↺</button></div>
289
+ </div>
290
+ <div class="control-row">
291
+ <label>Palette</label>
292
+ <select id="ctrl-chart-palette">
293
+ <option value="monochrome" selected>Monochrome</option>
294
+ <option value="complementary">Complementary</option>
295
+ <option value="analogous">Analogous</option>
296
+ <option value="triadic">Triadic</option>
297
+ <option value="pastel">Pastel</option>
298
+ <option value="warm">Warm</option>
299
+ <option value="cool">Cool</option>
300
+ <option value="earth">Earth</option>
301
+ </select>
302
+ </div>
303
+ </div>
252
304
  </div>
253
305
  </div>
254
306
  </div>
@@ -567,28 +619,70 @@
567
619
 
568
620
  <!-- Status bar -->
569
621
  <div id="statusbar">
570
- <span class="dot"></span>
571
- <span id="status-text">Ready</span>
572
- <span style="margin-left:auto; font-size:10.5px; color:var(--text-3)">
573
- YAML front matter &middot; drag to load
622
+ <span id="status-text"></span>
623
+ <span style="margin-left:auto; font-size:10.5px; color:var(--text-2); display:flex; align-items:center; gap:4px; flex-wrap:wrap; justify-content:flex-end">
624
+ <span>100% private (<a href="https://sdocs.dev/#sec=privacy" target="_blank" rel="noopener" style="color:var(--text-2); text-decoration:underline">learn why</a>)</span>
625
+ · CLI for you &amp; your agents: <span id="cli-install-wrap" style="display:inline-flex; align-items:center; gap:3px; cursor:pointer" title="Click to copy install command"><code id="cli-install-cmd" style="background:var(--bg-surface); border:1px solid var(--md-link-color, #2563eb); border-radius:3px; padding:1px 5px; font-size:10px; color:var(--md-link-color, #2563eb)">npm i -g sdocs-dev</code><button id="cli-copy-btn" style="background:none; border:none; cursor:pointer; color:var(--md-link-color, #2563eb); padding:0; vertical-align:middle; opacity:0.5; transition:opacity .15s"><svg width="11" height="11" 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></button></span>
626
+ · <a href="https://github.com/JoshInLisbon/SDocs" target="_blank" rel="noopener" style="color:var(--text-2); text-decoration:underline">GitHub</a>
574
627
  </span>
575
628
  </div>
629
+ <style>
630
+ #cli-install-wrap:hover #cli-copy-btn { opacity: 1 !important; }
631
+ </style>
632
+ <script nonce="__CSP_NONCE__">
633
+ (function () {
634
+ var COPY_ICON = '<svg width="11" height="11" 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>';
635
+ var CHECK_ICON = '<svg width="11" height="11" 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>';
636
+ var wrap = document.getElementById('cli-install-wrap');
637
+ var btn = document.getElementById('cli-copy-btn');
638
+ if (wrap && btn) wrap.addEventListener('click', function () {
639
+ navigator.clipboard.writeText('npm i -g sdocs-dev').then(function () {
640
+ btn.innerHTML = CHECK_ICON;
641
+ setTimeout(function () { btn.innerHTML = COPY_ICON; }, 1500);
642
+ });
643
+ });
644
+ })();
645
+ </script>
576
646
 
577
- <script src="/public/brotli-wasm.js"></script>
647
+ <script src="/public/brotli-wasm-v1.js"></script>
578
648
  <script src="/public/vendor/marked.min.js"></script>
649
+ <script src="/public/vendor/purify.min.js"></script>
579
650
  <script src="/public/sdocs-yaml.js"></script>
580
651
  <script src="/public/sdocs-styles.js"></script>
581
652
  <script src="/public/sdocs-state.js"></script>
653
+ <script src="/public/sdocs-slugify.js"></script>
582
654
  <script src="/public/sdocs-theme.js"></script>
583
655
  <script src="/public/sdocs-controls.js"></script>
584
656
  <script src="/public/sdocs-export.js"></script>
585
657
  <script src="/public/sdocs-write.js"></script>
658
+ <script src="/public/sdocs-charts.js"></script>
586
659
  <script src="/public/sdocs-app.js"></script>
587
- <script>
660
+ <script nonce="__CSP_NONCE__">
588
661
  var APP_VERSION = '__APP_VERSION__';
589
- if ('serviceWorker' in navigator) {
662
+ var SDOCS_DEV = '__SDOCS_DEV__' === '1';
663
+ // Cohort: store the ISO week of first visit for retention tracking.
664
+ // Not a unique ID — groups all visitors who first came the same week.
665
+ if (!localStorage.getItem('sdocs_cohort')) {
666
+ try {
667
+ var _d = new Date(), _u = new Date(Date.UTC(_d.getFullYear(), _d.getMonth(), _d.getDate()));
668
+ var _dn = _u.getUTCDay() || 7;
669
+ _u.setUTCDate(_u.getUTCDate() + 4 - _dn);
670
+ var _ys = new Date(Date.UTC(_u.getUTCFullYear(), 0, 1));
671
+ var _wn = Math.ceil((((_u - _ys) / 86400000) + 1) / 7);
672
+ localStorage.setItem('sdocs_cohort', _u.getUTCFullYear() + '-W' + String(_wn).padStart(2, '0'));
673
+ } catch (e) { /* private browsing or storage disabled */ }
674
+ }
675
+ if (SDOCS_DEV && 'serviceWorker' in navigator) {
676
+ // Dev mode: unregister any existing SW and clear caches so edits are picked up instantly.
677
+ navigator.serviceWorker.getRegistrations().then(function (regs) {
678
+ regs.forEach(function (r) { r.unregister(); });
679
+ });
680
+ if (window.caches) caches.keys().then(function (ks) { ks.forEach(function (k) { caches.delete(k); }); });
681
+ } else if ('serviceWorker' in navigator) {
682
+ var _cohort = '';
683
+ try { _cohort = localStorage.getItem('sdocs_cohort') || ''; } catch (e) {}
590
684
  navigator.serviceWorker.register('/sw.js').then(function (reg) {
591
- var msg = { type: 'check-update', version: APP_VERSION };
685
+ var msg = { type: 'check-update', version: APP_VERSION, cohort: _cohort };
592
686
  if (reg.active) reg.active.postMessage(msg);
593
687
  navigator.serviceWorker.addEventListener('controllerchange', function () {
594
688
  if (navigator.serviceWorker.controller) navigator.serviceWorker.controller.postMessage(msg);