sdocs-dev 1.1.1 → 1.1.2

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/public/index.html CHANGED
@@ -4,9 +4,6 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>SDocs</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
10
7
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32' fill='none'%3E%3Crect x='2' y='2' width='28' height='28' rx='7' fill='%232563EB' fill-opacity='.15'/%3E%3Crect x='2' y='2' width='28' height='28' rx='7' stroke='%232563EB' stroke-width='2'/%3E%3Cpath d='M9 11h14M9 16h11M9 21h8' stroke='%232563EB' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E">
11
8
  <link rel="stylesheet" href="/public/css/tokens.css">
12
9
  <link rel="stylesheet" href="/public/css/layout.css">
@@ -37,12 +34,12 @@
37
34
  <button class="btn active" id="btn-read" title="Read">
38
35
  <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="M12 7v14"/><path d="M16 12h2"/><path d="M16 8h2"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/><path d="M6 12h2"/><path d="M6 8h2"/></svg>
39
36
  </button>
40
- <button class="btn" id="btn-write" title="Write">
41
- <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="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
42
- </button>
43
37
  <button class="btn" id="btn-style" title="Style">
44
38
  <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="m14.622 17.897-10.68-2.913"/><path d="M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z"/><path d="M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15"/></svg>
45
39
  </button>
40
+ <button class="btn" id="btn-write" title="Write">
41
+ <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="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
42
+ </button>
46
43
  <button class="btn" id="btn-raw" title="Raw">
47
44
  <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="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
48
45
  </button>
@@ -245,6 +242,10 @@
245
242
  <label>Border</label>
246
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>
247
244
  </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>
248
+ </div>
248
249
  <div class="control-row">
249
250
  <label>Text</label>
250
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>
@@ -573,7 +574,7 @@
573
574
  </span>
574
575
  </div>
575
576
 
576
- <script src="https://cdn.jsdelivr.net/npm/marked@11/marked.min.js"></script>
577
+ <script src="/public/vendor/marked.min.js"></script>
577
578
  <script src="/public/sdocs-yaml.js"></script>
578
579
  <script src="/public/sdocs-styles.js"></script>
579
580
  <script src="/public/sdocs-state.js"></script>
@@ -582,5 +583,17 @@
582
583
  <script src="/public/sdocs-export.js"></script>
583
584
  <script src="/public/sdocs-write.js"></script>
584
585
  <script src="/public/sdocs-app.js"></script>
586
+ <script>
587
+ var APP_VERSION = '__APP_VERSION__';
588
+ if ('serviceWorker' in navigator) {
589
+ navigator.serviceWorker.register('/sw.js').then(function (reg) {
590
+ var msg = { type: 'check-update', version: APP_VERSION };
591
+ if (reg.active) reg.active.postMessage(msg);
592
+ navigator.serviceWorker.addEventListener('controllerchange', function () {
593
+ if (navigator.serviceWorker.controller) navigator.serviceWorker.controller.postMessage(msg);
594
+ });
595
+ });
596
+ }
597
+ </script>
585
598
  </body>
586
599
  </html>
@@ -263,7 +263,10 @@ function updateHash() {
263
263
  }
264
264
  var params = new URLSearchParams();
265
265
  if (!S._isDefaultState) {
266
- var meta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
266
+ var styles = SDocStyles.stripStyleDefaults(S.collectStyles());
267
+ var meta = Object.assign({}, S.currentMeta);
268
+ if (Object.keys(styles).length > 0) meta.styles = styles;
269
+ else delete meta.styles;
267
270
  var full = SDocYaml.serializeFrontMatter(meta) + '\n' + S.currentBody;
268
271
  var compressed = await compressText(full);
269
272
  params.set('md', compressed);
@@ -415,6 +418,10 @@ document.getElementById('factory-reset-styles').addEventListener('click', functi
415
418
 
416
419
  document.getElementById('toolbar-brand').addEventListener('click', function(e) {
417
420
  e.preventDefault();
421
+ if (window.location.hash && window.location.hash.indexOf('md=') !== -1) {
422
+ window.open(window.location.origin + window.location.pathname, '_blank');
423
+ return;
424
+ }
418
425
  S.resetAllStyles();
419
426
  loadText(DEFAULT_MD);
420
427
  S._isDefaultState = true;
@@ -476,19 +483,26 @@ S.loadText = loadText;
476
483
  // Sync theme tabs to initial theme
477
484
  S.updateThemeTabs(S.activeTheme);
478
485
 
479
- // ── Init ──────────────────────────────────
486
+ // ── Load document from URL hash ──────────────────────────────────
480
487
 
481
- (async function () {
482
- await _defaultReady;
483
- if (window.location.pathname === '/new') {
484
- startNewDocument();
485
- return;
486
- }
488
+ var _lastLoadedHash = null;
489
+
490
+ async function loadFromHash() {
487
491
  var hash = window.location.hash.slice(1);
492
+ if (hash === _lastLoadedHash) return;
493
+ _lastLoadedHash = hash;
494
+
495
+ clearTimeout(S._hashTimer);
496
+
488
497
  var params = hash ? new URLSearchParams(hash) : new URLSearchParams();
489
498
  var mdParam = params.get('md');
490
499
  var modeParam = params.get('mode');
491
500
  var stylesParam = params.get('styles');
501
+ var themeParam = params.get('theme');
502
+ var secParam = params.get('sec');
503
+
504
+ S.resetAllStyles();
505
+
492
506
  if (mdParam) {
493
507
  try {
494
508
  S._isDefaultState = false;
@@ -498,12 +512,29 @@ S.updateThemeTabs(S.activeTheme);
498
512
  console.warn('sdocs-dev: could not decode hash', e);
499
513
  }
500
514
  }
515
+
516
+ if (themeParam === 'light' || themeParam === 'dark') {
517
+ var savedPref = localStorage.getItem('sdocs-theme');
518
+ S.switchThemeAndUpdate(themeParam);
519
+ // Restore localStorage — URL theme is view-only, not persistent
520
+ if (savedPref) localStorage.setItem('sdocs-theme', savedPref);
521
+ else localStorage.removeItem('sdocs-theme');
522
+ } else {
523
+ // Restore user's actual preferred theme (previous hash may have overridden it)
524
+ var preferred = S.getPreferredTheme();
525
+ if (preferred !== S.activeTheme) {
526
+ S.switchThemeAndUpdate(preferred);
527
+ }
528
+ }
529
+
501
530
  if (modeParam && ['read', 'style', 'write', 'raw', 'export'].includes(modeParam)) {
502
531
  setMode(modeParam, true);
503
532
  } else {
504
533
  setMode('read', true);
505
534
  }
535
+
506
536
  if (!mdParam) {
537
+ S._isDefaultState = true;
507
538
  loadText(DEFAULT_MD);
508
539
  if (stylesParam) {
509
540
  try {
@@ -515,13 +546,11 @@ S.updateThemeTabs(S.activeTheme);
515
546
  }
516
547
  }
517
548
 
518
- var secParam = params.get('sec');
519
549
  if (secParam) {
520
550
  setTimeout(function() {
521
551
  var target = document.getElementById(secParam);
522
552
  if (!target) return;
523
553
 
524
- // Expand the target's own section body (its content)
525
554
  var ownSection = target.closest('.md-section');
526
555
  if (ownSection) {
527
556
  var ownBody = ownSection.querySelector(':scope > .md-section-body');
@@ -530,7 +559,6 @@ S.updateThemeTabs(S.activeTheme);
530
559
  if (ownToggle) { ownToggle.classList.add('open'); }
531
560
  }
532
561
 
533
- // Expand all ancestor section bodies
534
562
  var el = target.closest('.md-section-body');
535
563
  while (el) {
536
564
  el.classList.add('open');
@@ -542,7 +570,6 @@ S.updateThemeTabs(S.activeTheme);
542
570
  el = el.parentElement ? el.parentElement.closest('.md-section-body') : null;
543
571
  }
544
572
 
545
- // Add scroll spacer so the target can reach the top
546
573
  var spacerNeeded = contentArea.clientHeight - (contentArea.scrollHeight - target.offsetTop);
547
574
  if (spacerNeeded > 0) {
548
575
  var spacer = document.createElement('div');
@@ -554,8 +581,31 @@ S.updateThemeTabs(S.activeTheme);
554
581
  target.scrollIntoView({ behavior: 'smooth', block: 'start' });
555
582
  }, 200);
556
583
  }
584
+
585
+ if (!secParam) {
586
+ contentArea.scrollTop = 0;
587
+ }
588
+ }
589
+
590
+ // ── Init ──────────────────────────────────
591
+
592
+ (async function () {
593
+ await _defaultReady;
594
+ if (window.location.pathname === '/new') {
595
+ startNewDocument();
596
+ return;
597
+ }
598
+ await loadFromHash();
557
599
  }());
558
600
 
601
+ window.addEventListener('hashchange', function () {
602
+ loadFromHash();
603
+ });
604
+
605
+ window.addEventListener('popstate', function () {
606
+ loadFromHash();
607
+ });
608
+
559
609
  // ── Toolbar scroll hints (fade + bounce-peek) ──────
560
610
 
561
611
  function initScrollHint(el) {
@@ -51,7 +51,7 @@ var STANDALONE_COLOR_IDS = new Set(SDocStyles.STANDALONE_COLOR_IDS);
51
51
  'ctrl-h1-weight','ctrl-h2-weight','ctrl-h3-weight','ctrl-h4-weight',
52
52
  'ctrl-bg-color','ctrl-link-color','ctrl-link-decoration',
53
53
  'ctrl-code-font','ctrl-code-bg','ctrl-code-color',
54
- 'ctrl-bq-border-color','ctrl-bq-color',
54
+ 'ctrl-bq-border-color','ctrl-bq-bg','ctrl-bq-color',
55
55
  ].forEach(function(id) {
56
56
  var handler = function() { if (STANDALONE_COLOR_IDS.has(id)) S.overriddenColors.add(id); applyCtrl(id); };
57
57
  document.getElementById(id).addEventListener('input', handler);
@@ -97,7 +97,7 @@ document.getElementById('reset-h4-color').addEventListener('click', function()
97
97
  document.getElementById('reset-p-color').addEventListener('click', function() { resetColorValue('ctrl-p-color'); S.syncAll('controls'); });
98
98
  document.getElementById('reset-list-color').addEventListener('click', function() { resetColorValue('ctrl-list-color'); S.syncAll('controls'); });
99
99
 
100
- ['ctrl-bg-color','ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-color'].forEach(function(ctrlId) {
100
+ ['ctrl-bg-color','ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-bg','ctrl-bq-color'].forEach(function(ctrlId) {
101
101
  var btnId = 'reset-' + ctrlId.replace('ctrl-', '');
102
102
  document.getElementById(btnId).addEventListener('click', function() {
103
103
  var defaultVal = S.getStandaloneDefault(ctrlId);
@@ -165,54 +165,41 @@ function applyStylesFromMeta(s) {
165
165
  return;
166
166
  }
167
167
 
168
- // Legacy single-theme path: top-level colors go to light theme
168
+ // Legacy single-theme path: top-level colors go to light theme only,
169
+ // then loadThemeColors applies the correct theme's colors.
170
+ var lightOverridden = S.themeOverridden.light;
171
+ var lightColors = S.themeColors.light;
169
172
  var newOverridden = result.overriddenColors;
170
173
 
171
- S.overriddenColors.clear();
172
- setColorValue('ctrl-color', S.getColorDefault(), false);
174
+ lightOverridden.clear();
175
+ S.themeOverridden.dark.clear();
173
176
 
174
177
  newOverridden.forEach(function(id) {
175
- S.overriddenColors.add(id);
176
- setColorValue(id, controls[id], true);
178
+ lightOverridden.add(id);
179
+ lightColors[id] = controls[id];
177
180
  });
178
181
 
179
- // Also apply standalone colors from legacy format
180
- if (s.background) {
181
- S.overriddenColors.add('ctrl-bg-color');
182
- var elBg = document.getElementById('ctrl-bg-color');
183
- if (elBg) { elBg.value = s.background; applyCtrl('ctrl-bg-color'); }
184
- }
185
- if (s.link && s.link.color) {
186
- S.overriddenColors.add('ctrl-link-color');
187
- var el = document.getElementById('ctrl-link-color');
188
- if (el) { el.value = s.link.color; applyCtrl('ctrl-link-color'); }
189
- }
182
+ // Also store standalone colors from legacy format into light theme
183
+ var legacyStandalone = [];
184
+ if (s.background) legacyStandalone.push(['ctrl-bg-color', s.background]);
185
+ if (s.link && s.link.color) legacyStandalone.push(['ctrl-link-color', s.link.color]);
190
186
  if (s.code) {
191
- if (s.code.background) {
192
- S.overriddenColors.add('ctrl-code-bg');
193
- var el2 = document.getElementById('ctrl-code-bg');
194
- if (el2) { el2.value = s.code.background; applyCtrl('ctrl-code-bg'); }
195
- }
196
- if (s.code.color) {
197
- S.overriddenColors.add('ctrl-code-color');
198
- var el3 = document.getElementById('ctrl-code-color');
199
- if (el3) { el3.value = s.code.color; applyCtrl('ctrl-code-color'); }
200
- }
187
+ if (s.code.background) legacyStandalone.push(['ctrl-code-bg', s.code.background]);
188
+ if (s.code.color) legacyStandalone.push(['ctrl-code-color', s.code.color]);
201
189
  }
202
190
  if (s.blockquote) {
203
- if (s.blockquote.borderColor) {
204
- S.overriddenColors.add('ctrl-bq-border-color');
205
- var el4 = document.getElementById('ctrl-bq-border-color');
206
- if (el4) { el4.value = s.blockquote.borderColor; applyCtrl('ctrl-bq-border-color'); }
207
- }
208
- if (s.blockquote.color) {
209
- S.overriddenColors.add('ctrl-bq-color');
210
- var el5 = document.getElementById('ctrl-bq-color');
211
- if (el5) { el5.value = s.blockquote.color; applyCtrl('ctrl-bq-color'); }
212
- }
191
+ if (s.blockquote.borderColor) legacyStandalone.push(['ctrl-bq-border-color', s.blockquote.borderColor]);
192
+ if (s.blockquote.background) legacyStandalone.push(['ctrl-bq-bg', s.blockquote.background]);
193
+ if (s.blockquote.color) legacyStandalone.push(['ctrl-bq-color', s.blockquote.color]);
213
194
  }
195
+ legacyStandalone.forEach(function(pair) {
196
+ lightOverridden.add(pair[0]);
197
+ lightColors[pair[0]] = pair[1];
198
+ });
214
199
 
200
+ // Now apply the active theme's colors (light overrides or dark defaults)
215
201
  S._syncing = wasSyncing;
202
+ S.loadThemeColors(S.activeTheme);
216
203
  }
217
204
 
218
205
  function collectStyles() {
@@ -257,7 +244,7 @@ function resetAllStyles() {
257
244
  'ctrl-p-lh-num','ctrl-p-mb-num',
258
245
  'ctrl-link-color','ctrl-link-decoration',
259
246
  'ctrl-code-font','ctrl-code-bg','ctrl-code-color',
260
- 'ctrl-bq-border-color','ctrl-bq-bw-num','ctrl-bq-size-num','ctrl-bq-color',
247
+ 'ctrl-bq-border-color','ctrl-bq-bg','ctrl-bq-bw-num','ctrl-bq-size-num','ctrl-bq-color',
261
248
  'ctrl-list-spacing-num','ctrl-list-indent-num',
262
249
  ].forEach(function(id) { applyCtrl(id); });
263
250
  }
@@ -47,11 +47,12 @@ function buildExportCSS() {
47
47
  var preBG = cv('--md-pre-bg') || codeBG;
48
48
 
49
49
  var bqBorder = cv('--md-bq-border') || '3px solid #2563eb';
50
+ var bqBg = cv('--md-bq-bg') || '#f7f5f2';
50
51
  var bqColor = cv('--md-bq-color') || '#6b6560';
51
52
  var bqPad = cv('--md-bq-padding') || '0.5em 1em';
52
53
  var bqMargin = cv('--md-bq-margin') || '1.2em 0';
53
54
 
54
- return '\nbody {\n font-family: ' + fontFamily + ';\n font-size: ' + baseSize + ';\n color: ' + color + ';\n line-height: ' + lineHeight + ';\n background-color: ' + bgColor + ';\n max-width: 720px;\n margin: 0 auto;\n padding: 40px 48px 60px;\n -webkit-font-smoothing: antialiased;\n}\nh1 { font-family: ' + hFontFamily + '; font-size: ' + h1Size + '; color: ' + h1Color + '; font-weight: ' + h1Weight + '; letter-spacing: -0.02em; line-height: 1.2; margin: 0 0 ' + hMB + '; }\nh2 { font-family: ' + hFontFamily + '; font-size: ' + h2Size + '; color: ' + h2Color + '; font-weight: ' + h2Weight + '; letter-spacing: -0.015em; line-height: 1.3; margin: 1.4em 0 ' + hMB + '; padding-bottom: 0.3em; border-bottom: 1px solid #ede8e2; }\nh3 { font-family: ' + hFontFamily + '; font-size: ' + h3Size + '; color: ' + h3Color + '; font-weight: ' + h3Weight + '; letter-spacing: -0.01em; line-height: 1.4; margin: 1.2em 0 ' + hMB + '; }\nh4 { font-family: ' + hFontFamily + '; font-size: ' + h4Size + '; color: ' + h4Color + '; font-weight: ' + h4Weight + '; line-height: 1.5; margin: 1em 0 ' + hMB + '; }\np { color: ' + pColor + '; line-height: ' + pLH + '; margin: ' + pMargin + '; }\na { color: ' + linkColor + '; text-decoration: ' + linkDec + '; text-underline-offset: 2px; }\ncode { background: ' + codeBG + '; color: ' + codeColor + '; padding: 0.15em 0.45em; border-radius: 4px; font-family: ' + codeFont + '; font-size: 0.85em; }\npre { background: ' + preBG + '; padding: 1.1em 1.25em; border-radius: 8px; overflow-x: auto; margin: 1.2em 0; border: 1px solid #e7e2db; }\npre code { background: none; padding: 0; color: #3c3733; font-size: 0.88em; }\nblockquote { border-left: ' + bqBorder + '; color: ' + bqColor + '; padding: ' + bqPad + '; margin: ' + bqMargin + '; background: #f7f5f2; border-radius: 0 6px 6px 0; }\nblockquote p { margin: 0; color: inherit; }\nul, ol { padding-left: 1.6em; margin: 0.5em 0 1.1em; }\nli { margin-bottom: 0.3em; }\nhr { border: none; border-top: 1px solid #ede8e2; margin: 2em 0; }\ntable { border-collapse: collapse; width: 100%; margin: 1.2em 0; font-size: 0.92em; }\nth, td { border: 1px solid #e2ddd6; padding: 7px 12px; text-align: left; }\nth { background: #f4f1ed; font-weight: 600; }\ntr:nth-child(even) td { background: #fafaf8; }\nimg { max-width: 100%; border-radius: 8px; }\n';
55
+ return '\nbody {\n font-family: ' + fontFamily + ';\n font-size: ' + baseSize + ';\n color: ' + color + ';\n line-height: ' + lineHeight + ';\n background-color: ' + bgColor + ';\n max-width: 720px;\n margin: 0 auto;\n padding: 40px 48px 60px;\n -webkit-font-smoothing: antialiased;\n}\nh1 { font-family: ' + hFontFamily + '; font-size: ' + h1Size + '; color: ' + h1Color + '; font-weight: ' + h1Weight + '; letter-spacing: -0.02em; line-height: 1.2; margin: 0 0 ' + hMB + '; }\nh2 { font-family: ' + hFontFamily + '; font-size: ' + h2Size + '; color: ' + h2Color + '; font-weight: ' + h2Weight + '; letter-spacing: -0.015em; line-height: 1.3; margin: 1.4em 0 ' + hMB + '; padding-bottom: 0.3em; border-bottom: 1px solid #ede8e2; }\nh3 { font-family: ' + hFontFamily + '; font-size: ' + h3Size + '; color: ' + h3Color + '; font-weight: ' + h3Weight + '; letter-spacing: -0.01em; line-height: 1.4; margin: 1.2em 0 ' + hMB + '; }\nh4 { font-family: ' + hFontFamily + '; font-size: ' + h4Size + '; color: ' + h4Color + '; font-weight: ' + h4Weight + '; line-height: 1.5; margin: 1em 0 ' + hMB + '; }\np { color: ' + pColor + '; line-height: ' + pLH + '; margin: ' + pMargin + '; }\na { color: ' + linkColor + '; text-decoration: ' + linkDec + '; text-underline-offset: 2px; }\ncode { background: ' + codeBG + '; color: ' + codeColor + '; padding: 0.15em 0.45em; border-radius: 4px; font-family: ' + codeFont + '; font-size: 0.85em; }\npre { background: ' + preBG + '; padding: 1.1em 1.25em; border-radius: 8px; overflow-x: auto; margin: 1.2em 0; border: 1px solid #e7e2db; }\npre code { background: none; padding: 0; color: #3c3733; font-size: 0.88em; }\nblockquote { border-left: ' + bqBorder + '; color: ' + bqColor + '; padding: ' + bqPad + '; margin: ' + bqMargin + '; background: ' + bqBg + '; border-radius: 0 6px 6px 0; }\nblockquote p { margin: 0; color: inherit; }\nul, ol { padding-left: 1.6em; margin: 0.5em 0 1.1em; }\nli { margin-bottom: 0.3em; }\nhr { border: none; border-top: 1px solid #ede8e2; margin: 2em 0; }\ntable { border-collapse: collapse; width: 100%; margin: 1.2em 0; font-size: 0.92em; }\nth, td { border: 1px solid #e2ddd6; padding: 7px 12px; text-align: left; }\nth { background: #f4f1ed; font-weight: 600; }\ntr:nth-child(even) td { background: #fafaf8; }\nimg { max-width: 100%; border-radius: 8px; }\n';
55
56
  }
56
57
 
57
58
  function buildExportHTML() {
@@ -55,6 +55,7 @@ const CTRL_CSS_MAP = {
55
55
  'ctrl-code-color': { cssVar: '--md-code-color' },
56
56
  'ctrl-bq-border-color': { cssVar: '--md-bq-border', compound: 'bq-border' },
57
57
  'ctrl-bq-bw-num': { cssVar: '--md-bq-border', compound: 'bq-border' },
58
+ 'ctrl-bq-bg': { cssVar: '--md-bq-bg' },
58
59
  'ctrl-bq-size-num': { cssVar: '--md-bq-size', suffix: 'em' },
59
60
  'ctrl-bq-color': { cssVar: '--md-bq-color' },
60
61
  'ctrl-list-spacing-num': { cssVar: '--md-list-spacing', suffix: 'em' },
@@ -170,6 +171,7 @@ function collectStyles(values, overriddenColors) {
170
171
  blockquote: {
171
172
  borderColor: gv('ctrl-bq-border-color'),
172
173
  borderWidth: gn('ctrl-bq-bw-num'),
174
+ background: gv('ctrl-bq-bg'),
173
175
  fontSize: gn('ctrl-bq-size-num'),
174
176
  color: gv('ctrl-bq-color'),
175
177
  },
@@ -257,6 +259,7 @@ function stylesToControls(styles) {
257
259
  const bq = styles.blockquote || {};
258
260
  if (bq.borderColor) controls['ctrl-bq-border-color'] = bq.borderColor;
259
261
  if (bq.borderWidth) controls['ctrl-bq-bw-num'] = bq.borderWidth;
262
+ if (bq.background) controls['ctrl-bq-bg'] = bq.background;
260
263
  if (bq.fontSize) controls['ctrl-bq-size-num'] = bq.fontSize;
261
264
  if (bq.color) controls['ctrl-bq-color'] = bq.color;
262
265
 
@@ -276,7 +279,7 @@ function stylesToControls(styles) {
276
279
  // ═══════════════════════════════════════════════════════
277
280
 
278
281
  var STANDALONE_COLOR_IDS = [
279
- 'ctrl-bg-color','ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-color',
282
+ 'ctrl-bg-color','ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-bg','ctrl-bq-color',
280
283
  ];
281
284
 
282
285
  var CASCADE_COLOR_IDS = Object.keys(COLOR_VAR_MAP);
@@ -341,6 +344,7 @@ function parseThemeColorBlock(block) {
341
344
  }
342
345
  if (block.blockquote) {
343
346
  if (block.blockquote.borderColor) { colors['ctrl-bq-border-color'] = block.blockquote.borderColor; overridden.add('ctrl-bq-border-color'); }
347
+ if (block.blockquote.background) { colors['ctrl-bq-bg'] = block.blockquote.background; overridden.add('ctrl-bq-bg'); }
344
348
  if (block.blockquote.color) { colors['ctrl-bq-color'] = block.blockquote.color; overridden.add('ctrl-bq-color'); }
345
349
  }
346
350
 
@@ -397,6 +401,10 @@ function collectThemeColors(colorValues, overriddenSet) {
397
401
  if (!block.blockquote) block.blockquote = {};
398
402
  block.blockquote.borderColor = colorValues['ctrl-bq-border-color'];
399
403
  }
404
+ if (overriddenSet.has('ctrl-bq-bg')) {
405
+ if (!block.blockquote) block.blockquote = {};
406
+ block.blockquote.background = colorValues['ctrl-bq-bg'];
407
+ }
400
408
  if (overriddenSet.has('ctrl-bq-color')) {
401
409
  if (!block.blockquote) block.blockquote = {};
402
410
  block.blockquote.color = colorValues['ctrl-bq-color'];
@@ -438,6 +446,7 @@ function collectStylesDual(values, lightOverridden, darkOverridden, lightColors,
438
446
  borderWidth: gn('ctrl-bq-bw-num'),
439
447
  fontSize: gn('ctrl-bq-size-num'),
440
448
  },
449
+
441
450
  };
442
451
 
443
452
  styles.list = {
@@ -525,6 +534,79 @@ stylesToControls = function(styles) {
525
534
  };
526
535
  };
527
536
 
537
+ // ═══════════════════════════════════════════════════════
538
+ // STRIP STYLE DEFAULTS (for shorter URLs)
539
+ // ═══════════════════════════════════════════════════════
540
+
541
+ // Default values matching HTML control defaults in index.html.
542
+ // When a style value equals its default, it can be omitted from serialization
543
+ // because stylesToControls / the browser already falls back to these defaults.
544
+ var STYLE_DEFAULTS = {
545
+ fontFamily: 'Inter',
546
+ baseFontSize: 16,
547
+ lineHeight: 1.75,
548
+ headers: {
549
+ fontFamily: 'inherit',
550
+ scale: 1.0,
551
+ marginBottom: 0.4,
552
+ },
553
+ h1: { fontSize: 2.1, fontWeight: 700 },
554
+ h2: { fontSize: 1.55, fontWeight: 600 },
555
+ h3: { fontSize: 1.2, fontWeight: 600 },
556
+ h4: { fontSize: 1.0, fontWeight: 600 },
557
+ p: { lineHeight: 1.75, marginBottom: 1.1 },
558
+ link: { decoration: 'underline' },
559
+ code: { font: 'JetBrains Mono' },
560
+ blockquote: { borderWidth: 3, fontSize: 1.0 },
561
+ list: { spacing: 0.3, indent: 1.6 },
562
+ };
563
+
564
+ function numEq(a, b) {
565
+ var na = typeof a === 'number' ? a : parseFloat(a);
566
+ var nb = typeof b === 'number' ? b : parseFloat(b);
567
+ if (isNaN(na) || isNaN(nb)) return false;
568
+ return Math.abs(na - nb) < 0.001;
569
+ }
570
+
571
+ /**
572
+ * stripStyleDefaults(styles)
573
+ * Returns a new styles object with default-valued properties removed.
574
+ * light/dark color blocks are preserved as-is (they have no static defaults).
575
+ * Empty sub-objects are removed entirely.
576
+ */
577
+ function stripStyleDefaults(styles) {
578
+ if (!styles || typeof styles !== 'object') return styles;
579
+ var result = {};
580
+ for (var key in styles) {
581
+ if (!styles.hasOwnProperty(key)) continue;
582
+ var val = styles[key];
583
+ var def = STYLE_DEFAULTS[key];
584
+
585
+ // light/dark theme color blocks — always keep
586
+ if (key === 'light' || key === 'dark') {
587
+ result[key] = val;
588
+ continue;
589
+ }
590
+
591
+ if (typeof val === 'object' && val !== null) {
592
+ var sub = {};
593
+ var defObj = (typeof def === 'object' && def !== null) ? def : {};
594
+ for (var sk in val) {
595
+ if (!val.hasOwnProperty(sk)) continue;
596
+ var sv = val[sk];
597
+ var sd = defObj[sk];
598
+ if (sd !== undefined && (sv === sd || String(sv) === String(sd) || numEq(sv, sd))) continue;
599
+ sub[sk] = sv;
600
+ }
601
+ if (Object.keys(sub).length > 0) result[key] = sub;
602
+ } else {
603
+ if (def !== undefined && (val === def || String(val) === String(def) || numEq(val, def))) continue;
604
+ result[key] = val;
605
+ }
606
+ }
607
+ return result;
608
+ }
609
+
528
610
  // ═══════════════════════════════════════════════════════
529
611
  // EXPORTS
530
612
  // ═══════════════════════════════════════════════════════
@@ -544,6 +626,8 @@ exports.collectStylesDual = collectStylesDual;
544
626
  exports.collectThemeColors = collectThemeColors;
545
627
  exports.parseThemeColorBlock = parseThemeColorBlock;
546
628
  exports.stylesToControls = stylesToControls;
629
+ exports.STYLE_DEFAULTS = STYLE_DEFAULTS;
630
+ exports.stripStyleDefaults = stripStyleDefaults;
547
631
 
548
632
  // UMD tail: in Node (tests) this writes to module.exports; in the browser
549
633
  // it creates window.SDocStyles. We use this pattern instead of ES modules
@@ -55,16 +55,18 @@ const LIGHT_DEFAULTS = {
55
55
  codeColor: '#6b21a8',
56
56
  linkColor: '#2563eb',
57
57
  bqBorderColor: '#2563eb',
58
+ bqBg: '#f7f5f2',
58
59
  bqColor: '#6b6560',
59
60
  };
60
61
 
61
62
  const DARK_DEFAULTS = {
62
- bgColor: '#1c1a17',
63
+ bgColor: '#2c2a26',
63
64
  colorDefault: '#e7e5e2',
64
65
  codeBg: '#1a1816',
65
66
  codeColor: '#b8a99a',
66
67
  linkColor: '#60a5fa',
67
68
  bqBorderColor: '#60a5fa',
69
+ bqBg: '#252320',
68
70
  bqColor: '#a8a29e',
69
71
  };
70
72
 
@@ -142,6 +144,7 @@ function loadThemeColors(theme) {
142
144
  'ctrl-code-bg': defaults.codeBg,
143
145
  'ctrl-code-color': defaults.codeColor,
144
146
  'ctrl-bq-border-color': defaults.bqBorderColor,
147
+ 'ctrl-bq-bg': defaults.bqBg,
145
148
  'ctrl-bq-color': defaults.bqColor,
146
149
  };
147
150
  for (var ctrlId in standaloneMap) {
@@ -192,6 +195,7 @@ function updateDefaultColors() {
192
195
  'ctrl-code-bg': defaults.codeBg,
193
196
  'ctrl-code-color': defaults.codeColor,
194
197
  'ctrl-bq-border-color': defaults.bqBorderColor,
198
+ 'ctrl-bq-bg': defaults.bqBg,
195
199
  'ctrl-bq-color': defaults.bqColor,
196
200
  };
197
201
  for (const [ctrlId, val] of Object.entries(standaloneMap)) {
@@ -216,6 +220,7 @@ function getStandaloneDefault(ctrlId) {
216
220
  'ctrl-code-bg': d.codeBg,
217
221
  'ctrl-code-color': d.codeColor,
218
222
  'ctrl-bq-border-color': d.bqBorderColor,
223
+ 'ctrl-bq-bg': d.bqBg,
219
224
  'ctrl-bq-color': d.bqColor,
220
225
  };
221
226
  return map[ctrlId];
@@ -247,6 +252,7 @@ S.getStandaloneDefault = getStandaloneDefault;
247
252
  S.updateDefaultColors = updateDefaultColors;
248
253
  S.toggleTheme = toggleTheme;
249
254
  S.switchThemeAndUpdate = switchThemeAndUpdate;
255
+ S.getPreferredTheme = getPreferredTheme;
250
256
  S.saveCurrentThemeColors = saveCurrentThemeColors;
251
257
  S.loadThemeColors = loadThemeColors;
252
258
  S.updateThemeTabs = updateThemeTabs;
package/public/sw.js ADDED
@@ -0,0 +1,106 @@
1
+ // Service worker — stale-while-revalidate + version-gated cache bust
2
+ var CACHE_NAME = 'sdocs-cache';
3
+
4
+ var APP_SHELL = [
5
+ '/',
6
+ '/public/css/tokens.css',
7
+ '/public/css/layout.css',
8
+ '/public/css/rendered.css',
9
+ '/public/css/panel.css',
10
+ '/public/css/write.css',
11
+ '/public/css/mobile.css',
12
+ '/public/sdocs-yaml.js',
13
+ '/public/sdocs-styles.js',
14
+ '/public/sdocs-state.js',
15
+ '/public/sdocs-theme.js',
16
+ '/public/sdocs-controls.js',
17
+ '/public/sdocs-export.js',
18
+ '/public/sdocs-write.js',
19
+ '/public/sdocs-app.js',
20
+ '/public/vendor/marked.min.js',
21
+ '/public/fonts/inter-400.woff2',
22
+ '/public/fonts/inter-500.woff2',
23
+ '/public/fonts/inter-600.woff2',
24
+ '/public/default.md',
25
+ ];
26
+
27
+ // Pre-cache app shell on install
28
+ self.addEventListener('install', function (e) {
29
+ e.waitUntil(
30
+ caches.open(CACHE_NAME).then(function (cache) {
31
+ return cache.addAll(APP_SHELL);
32
+ }).then(function () {
33
+ return self.skipWaiting();
34
+ })
35
+ );
36
+ });
37
+
38
+ // Claim clients on activate
39
+ self.addEventListener('activate', function (e) {
40
+ e.waitUntil(self.clients.claim());
41
+ });
42
+
43
+ // Stale-while-revalidate for same-origin, cache-first for fonts
44
+ self.addEventListener('fetch', function (e) {
45
+ var url = new URL(e.request.url);
46
+
47
+ if (e.request.method !== 'GET') return;
48
+
49
+ // Version-check always hits network
50
+ if (url.pathname === '/version-check') return;
51
+
52
+ // Google Fonts: cache-first (they're immutable)
53
+ if (url.hostname === 'fonts.googleapis.com' || url.hostname === 'fonts.gstatic.com') {
54
+ e.respondWith(
55
+ caches.match(e.request).then(function (cached) {
56
+ if (cached) return cached;
57
+ return fetch(e.request).then(function (response) {
58
+ var clone = response.clone();
59
+ caches.open(CACHE_NAME).then(function (cache) { cache.put(e.request, clone); });
60
+ return response;
61
+ });
62
+ })
63
+ );
64
+ return;
65
+ }
66
+
67
+ // Same-origin: stale-while-revalidate
68
+ // Return cached immediately, fetch in background to update cache
69
+ if (url.origin === self.location.origin) {
70
+ e.respondWith(
71
+ caches.open(CACHE_NAME).then(function (cache) {
72
+ return cache.match(e.request).then(function (cached) {
73
+ var networkFetch = fetch(e.request).then(function (response) {
74
+ if (response.ok) {
75
+ cache.put(e.request, response.clone());
76
+ }
77
+ return response;
78
+ }).catch(function () {
79
+ return cached; // offline fallback
80
+ });
81
+
82
+ return cached || networkFetch;
83
+ });
84
+ })
85
+ );
86
+ return;
87
+ }
88
+ });
89
+
90
+ // Version check: if server version differs, purge and re-cache.
91
+ // No reload — stale-while-revalidate ensures next navigation gets fresh content.
92
+ self.addEventListener('message', function (e) {
93
+ if (e.data && e.data.type === 'check-update' && e.data.version) {
94
+ fetch('/version-check').then(function (res) {
95
+ return res.json();
96
+ }).then(function (data) {
97
+ if (data.version !== e.data.version) {
98
+ caches.delete(CACHE_NAME).then(function () {
99
+ return caches.open(CACHE_NAME).then(function (cache) {
100
+ return cache.addAll(APP_SHELL);
101
+ });
102
+ });
103
+ }
104
+ }).catch(function () { /* offline — ignore */ });
105
+ }
106
+ });