sdocs-dev 1.0.0 → 1.1.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.
@@ -44,14 +44,12 @@ function linkRangeNum(rangeId, numId) {
44
44
 
45
45
  SDocStyles.RANGE_NUM_PAIRS.forEach(function(pair) { linkRangeNum(pair[0], pair[1]); });
46
46
 
47
- var STANDALONE_COLOR_IDS = new Set([
48
- 'ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-color',
49
- ]);
47
+ var STANDALONE_COLOR_IDS = new Set(SDocStyles.STANDALONE_COLOR_IDS);
50
48
 
51
49
  [
52
50
  'ctrl-font-family','ctrl-h-font-family',
53
51
  'ctrl-h1-weight','ctrl-h2-weight','ctrl-h3-weight','ctrl-h4-weight',
54
- 'ctrl-link-color','ctrl-link-decoration',
52
+ 'ctrl-bg-color','ctrl-link-color','ctrl-link-decoration',
55
53
  'ctrl-code-font','ctrl-code-bg','ctrl-code-color',
56
54
  'ctrl-bq-border-color','ctrl-bq-color',
57
55
  ].forEach(function(id) {
@@ -99,7 +97,7 @@ document.getElementById('reset-h4-color').addEventListener('click', function()
99
97
  document.getElementById('reset-p-color').addEventListener('click', function() { resetColorValue('ctrl-p-color'); S.syncAll('controls'); });
100
98
  document.getElementById('reset-list-color').addEventListener('click', function() { resetColorValue('ctrl-list-color'); S.syncAll('controls'); });
101
99
 
102
- ['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-color'].forEach(function(ctrlId) {
103
101
  var btnId = 'reset-' + ctrlId.replace('ctrl-', '');
104
102
  document.getElementById(btnId).addEventListener('click', function() {
105
103
  var defaultVal = S.getStandaloneDefault(ctrlId);
@@ -133,7 +131,6 @@ function applyStylesFromMeta(s) {
133
131
 
134
132
  var result = SDocStyles.stylesToControls(s);
135
133
  var controls = result.controls;
136
- var newOverridden = result.overriddenColors;
137
134
 
138
135
  // Font family selects need special handling to match bare names against <option> values
139
136
  [['fontFamily', 'ctrl-font-family'], ['headers.fontFamily', 'ctrl-h-font-family']].forEach(function(pair) {
@@ -155,6 +152,22 @@ function applyStylesFromMeta(s) {
155
152
  setCtrl(id, controls[id]);
156
153
  });
157
154
 
155
+ if (result.hasThemeColors) {
156
+ // Per-theme color mode: populate both theme states
157
+ S.themeColors.light = result.lightColors || {};
158
+ S.themeColors.dark = result.darkColors || {};
159
+ S.themeOverridden.light = result.lightOverridden || new Set();
160
+ S.themeOverridden.dark = result.darkOverridden || new Set();
161
+
162
+ // Load the active theme's colors into controls
163
+ S._syncing = wasSyncing;
164
+ S.loadThemeColors(S.activeTheme);
165
+ return;
166
+ }
167
+
168
+ // Legacy single-theme path: top-level colors go to light theme
169
+ var newOverridden = result.overriddenColors;
170
+
158
171
  S.overriddenColors.clear();
159
172
  setColorValue('ctrl-color', S.getColorDefault(), false);
160
173
 
@@ -163,17 +176,68 @@ function applyStylesFromMeta(s) {
163
176
  setColorValue(id, controls[id], true);
164
177
  });
165
178
 
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
+ }
190
+ 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
+ }
201
+ }
202
+ 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
+ }
213
+ }
214
+
166
215
  S._syncing = wasSyncing;
167
216
  }
168
217
 
169
218
  function collectStyles() {
219
+ S.saveCurrentThemeColors();
220
+ var lightHas = S.themeOverridden.light.size > 0;
221
+ var darkHas = S.themeOverridden.dark.size > 0;
222
+ if (lightHas || darkHas) {
223
+ return SDocStyles.collectStylesDual(
224
+ readAllControlValues(),
225
+ S.themeOverridden.light, S.themeOverridden.dark,
226
+ S.themeColors.light, S.themeColors.dark
227
+ );
228
+ }
170
229
  return SDocStyles.collectStyles(readAllControlValues(), S.overriddenColors);
171
230
  }
172
231
 
173
232
  // ── Reset all styles ──────────────────────────────────
174
233
 
175
234
  function resetAllStyles() {
176
- S.overriddenColors.clear();
235
+ // Clear both theme states
236
+ S.themeOverridden.light.clear();
237
+ S.themeOverridden.dark.clear();
238
+ S.themeColors.light = {};
239
+ S.themeColors.dark = {};
240
+
177
241
  setColorValue('ctrl-color', S.getColorDefault(), false);
178
242
  // Set standalone color controls to theme-appropriate defaults
179
243
  STANDALONE_COLOR_IDS.forEach(function(ctrlId) {
@@ -186,7 +250,7 @@ function resetAllStyles() {
186
250
  else if (el.tagName === 'SELECT') el.selectedIndex = [].slice.call(el.options).findIndex(function(o) { return o.defaultSelected; });
187
251
  else if (el.type === 'color') el.value = el.defaultValue;
188
252
  });
189
- ['ctrl-font-family','ctrl-base-size-num','ctrl-line-height-num',
253
+ ['ctrl-bg-color','ctrl-font-family','ctrl-base-size-num','ctrl-line-height-num',
190
254
  'ctrl-h-font-family','ctrl-h-scale-num','ctrl-h-mb-num',
191
255
  'ctrl-h1-size-num','ctrl-h1-weight','ctrl-h2-size-num','ctrl-h2-weight',
192
256
  'ctrl-h3-size-num','ctrl-h3-weight','ctrl-h4-size-num','ctrl-h4-weight',
@@ -10,6 +10,7 @@ function cv(name) {
10
10
  }
11
11
 
12
12
  function buildExportCSS() {
13
+ var bgColor = cv('--md-bg') || '#ffffff';
13
14
  var fontFamily = cv('--md-font-family') || "'Inter', sans-serif";
14
15
  var baseSize = cv('--md-base-size') || '16px';
15
16
  var lineHeight = cv('--md-line-height') || '1.75';
@@ -50,7 +51,7 @@ function buildExportCSS() {
50
51
  var bqPad = cv('--md-bq-padding') || '0.5em 1em';
51
52
  var bqMargin = cv('--md-bq-margin') || '1.2em 0';
52
53
 
53
- return '\nbody {\n font-family: ' + fontFamily + ';\n font-size: ' + baseSize + ';\n color: ' + color + ';\n line-height: ' + lineHeight + ';\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';
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';
54
55
  }
55
56
 
56
57
  function buildExportHTML() {
@@ -149,17 +150,34 @@ document.getElementById('exp-styled').addEventListener('click', function() {
149
150
 
150
151
  // ── Save as default styles ──────────────────────────────
151
152
 
153
+ function formatStyleValue(k, v) {
154
+ if (typeof v !== 'object' || v === null) {
155
+ return k + ': ' + JSON.stringify(v);
156
+ }
157
+ // light/dark theme blocks: serialize as nested block (not inline)
158
+ if (k === 'light' || k === 'dark') {
159
+ var blockLines = [k + ':'];
160
+ Object.keys(v).forEach(function(sk) {
161
+ var sv = v[sk];
162
+ if (typeof sv === 'object' && sv !== null) {
163
+ var inner = Object.keys(sv).map(function(a) { return a + ': ' + JSON.stringify(sv[a]); }).join(', ');
164
+ blockLines.push(' ' + sk + ': { ' + inner + ' }');
165
+ } else {
166
+ blockLines.push(' ' + sk + ': ' + JSON.stringify(sv));
167
+ }
168
+ });
169
+ return blockLines.join('\n');
170
+ }
171
+ // Default: inline object
172
+ var inner = Object.keys(v).map(function(a) { return a + ': ' + JSON.stringify(v[a]); }).join(', ');
173
+ return k + ': { ' + inner + ' }';
174
+ }
175
+
152
176
  function buildStylesYaml() {
153
177
  var styles = S.collectStyles();
154
178
  var lines = [];
155
179
  Object.keys(styles).forEach(function(k) {
156
- var v = styles[k];
157
- if (typeof v === 'object' && v !== null) {
158
- var inner = Object.keys(v).map(function(a) { return a + ': ' + JSON.stringify(v[a]); }).join(', ');
159
- lines.push(k + ': { ' + inner + ' }');
160
- } else {
161
- lines.push(k + ': ' + JSON.stringify(v));
162
- }
180
+ lines.push(formatStyleValue(k, styles[k]));
163
181
  });
164
182
  return lines.join('\n');
165
183
  }
@@ -8,8 +8,10 @@ window.SDocs = {
8
8
  currentBody: '',
9
9
  currentMeta: {},
10
10
 
11
- // Color override tracking
12
- overriddenColors: new Set(),
11
+ // Per-theme color state
12
+ activeTheme: 'light',
13
+ themeColors: { light: {}, dark: {} },
14
+ themeOverridden: { light: new Set(), dark: new Set() },
13
15
 
14
16
  // DOM references
15
17
  renderedEl: document.getElementById('rendered'),
@@ -28,16 +30,30 @@ window.SDocs = {
28
30
 
29
31
  // Cross-module functions (registered by defining module)
30
32
  // Theme: toggleTheme, getThemeDefaults, getColorDefault, getStandaloneDefault,
31
- // loadGoogleFont, updateDefaultColors, GOOGLE_FONTS
33
+ // loadGoogleFont, updateDefaultColors, GOOGLE_FONTS,
34
+ // switchThemeAndUpdate, saveCurrentThemeColors, loadThemeColors
32
35
  // Controls: setColorValue, readAllControlValues, collectStyles,
33
36
  // applyStylesFromMeta, resetAllStyles, STANDALONE_COLOR_IDS
34
37
  // App: syncAll, setStatus, setMode, render, loadText
35
38
  // Write: enterWriteMode, exitWriteMode
36
39
  };
37
40
 
41
+ // Backwards compat: S.overriddenColors delegates to active theme's set
42
+ Object.defineProperty(SDocs, 'overriddenColors', {
43
+ get: function() { return SDocs.themeOverridden[SDocs.activeTheme]; },
44
+ set: function(v) { SDocs.themeOverridden[SDocs.activeTheme] = v; },
45
+ enumerable: true,
46
+ configurable: true,
47
+ });
48
+
49
+ SDocs.contentAreaEl = document.getElementById('content-area');
50
+
38
51
  SDocs.setStyleVar = function(cssVar, value) {
39
52
  SDocs.renderedEl.style.setProperty(cssVar, value);
40
53
  if (SDocs.writeEl) SDocs.writeEl.style.setProperty(cssVar, value);
54
+ if (cssVar === '--md-bg') {
55
+ SDocs.contentAreaEl.style.setProperty('background-color', value);
56
+ }
41
57
  };
42
58
 
43
59
  })();
@@ -31,6 +31,7 @@ const COLOR_CASCADE = {
31
31
  // Control ID → { cssVar, suffix?, compound? }
32
32
  // Maps every non-color control to its CSS variable and optional unit suffix
33
33
  const CTRL_CSS_MAP = {
34
+ 'ctrl-bg-color': { cssVar: '--md-bg' },
34
35
  'ctrl-font-family': { cssVar: '--md-font-family' },
35
36
  'ctrl-base-size-num': { cssVar: '--md-base-size', suffix: 'px' },
36
37
  'ctrl-line-height-num': { cssVar: '--md-line-height' },
@@ -179,7 +180,8 @@ function collectStyles(values, overriddenColors) {
179
180
  indent: gn('ctrl-list-indent-num'),
180
181
  };
181
182
 
182
- // Only emit cascade colors that were explicitly overridden
183
+ // Only emit colors that were explicitly overridden
184
+ if (overriddenColors.has('ctrl-bg-color')) styles.background = gv('ctrl-bg-color');
183
185
  if (overriddenColors.has('ctrl-color')) styles.color = gv('ctrl-color');
184
186
  if (overriddenColors.has('ctrl-h-color')) styles.headers.color = gv('ctrl-h-color');
185
187
  if (overriddenColors.has('ctrl-h1-color')) styles.h1.color = gv('ctrl-h1-color');
@@ -207,6 +209,10 @@ function stylesToControls(styles) {
207
209
  if (styles.baseFontSize) controls['ctrl-base-size-num'] = styles.baseFontSize;
208
210
  if (styles.lineHeight) controls['ctrl-line-height-num'] = styles.lineHeight;
209
211
 
212
+ if (styles.background) {
213
+ controls['ctrl-bg-color'] = styles.background;
214
+ overridden.add('ctrl-bg-color');
215
+ }
210
216
  if (styles.color) {
211
217
  controls['ctrl-color'] = styles.color;
212
218
  overridden.add('ctrl-color');
@@ -265,6 +271,260 @@ function stylesToControls(styles) {
265
271
  return { controls, overriddenColors: overridden };
266
272
  }
267
273
 
274
+ // ═══════════════════════════════════════════════════════
275
+ // STANDALONE & ALL COLOR IDS
276
+ // ═══════════════════════════════════════════════════════
277
+
278
+ var STANDALONE_COLOR_IDS = [
279
+ 'ctrl-bg-color','ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-color',
280
+ ];
281
+
282
+ var CASCADE_COLOR_IDS = Object.keys(COLOR_VAR_MAP);
283
+
284
+ var ALL_COLOR_IDS = CASCADE_COLOR_IDS.concat(STANDALONE_COLOR_IDS);
285
+
286
+ // ═══════════════════════════════════════════════════════
287
+ // PER-THEME FUNCTIONS
288
+ // ═══════════════════════════════════════════════════════
289
+
290
+ /**
291
+ * parseThemeColorBlock(block)
292
+ * Extracts { colors: {ctrlId: val}, overridden: Set } from a light:/dark: sub-object.
293
+ * The block is an object like { color: "#1a1a2e", h1: { color: "#c0392b" }, link: { color: "#2563eb" } }
294
+ */
295
+ function parseThemeColorBlock(block) {
296
+ if (!block) return { colors: {}, overridden: new Set() };
297
+ var colors = {};
298
+ var overridden = new Set();
299
+
300
+ // Background color
301
+ if (block.background) {
302
+ colors['ctrl-bg-color'] = block.background;
303
+ overridden.add('ctrl-bg-color');
304
+ }
305
+
306
+ // Top-level color (cascade root)
307
+ if (block.color) {
308
+ colors['ctrl-color'] = block.color;
309
+ overridden.add('ctrl-color');
310
+ }
311
+
312
+ // Cascade colors nested in sub-objects
313
+ if (block.headers && block.headers.color) {
314
+ colors['ctrl-h-color'] = block.headers.color;
315
+ overridden.add('ctrl-h-color');
316
+ }
317
+ ['h1','h2','h3','h4'].forEach(function(t) {
318
+ var obj = block[t];
319
+ if (obj && obj.color) {
320
+ colors['ctrl-' + t + '-color'] = obj.color;
321
+ overridden.add('ctrl-' + t + '-color');
322
+ }
323
+ });
324
+ if (block.p && block.p.color) {
325
+ colors['ctrl-p-color'] = block.p.color;
326
+ overridden.add('ctrl-p-color');
327
+ }
328
+ if (block.list && block.list.color) {
329
+ colors['ctrl-list-color'] = block.list.color;
330
+ overridden.add('ctrl-list-color');
331
+ }
332
+
333
+ // Standalone colors
334
+ if (block.link && block.link.color) {
335
+ colors['ctrl-link-color'] = block.link.color;
336
+ overridden.add('ctrl-link-color');
337
+ }
338
+ if (block.code) {
339
+ if (block.code.background) { colors['ctrl-code-bg'] = block.code.background; overridden.add('ctrl-code-bg'); }
340
+ if (block.code.color) { colors['ctrl-code-color'] = block.code.color; overridden.add('ctrl-code-color'); }
341
+ }
342
+ if (block.blockquote) {
343
+ if (block.blockquote.borderColor) { colors['ctrl-bq-border-color'] = block.blockquote.borderColor; overridden.add('ctrl-bq-border-color'); }
344
+ if (block.blockquote.color) { colors['ctrl-bq-color'] = block.blockquote.color; overridden.add('ctrl-bq-color'); }
345
+ }
346
+
347
+ return { colors: colors, overridden: overridden };
348
+ }
349
+
350
+ /**
351
+ * collectThemeColors(colorValues, overriddenSet)
352
+ * Builds the YAML sub-object for one theme (only emits overridden colors).
353
+ */
354
+ function collectThemeColors(colorValues, overriddenSet) {
355
+ var block = {};
356
+ if (overriddenSet.has('ctrl-bg-color')) block.background = colorValues['ctrl-bg-color'];
357
+ if (overriddenSet.has('ctrl-color')) block.color = colorValues['ctrl-color'];
358
+
359
+ // Heading colors
360
+ var hasHColor = overriddenSet.has('ctrl-h-color');
361
+ if (hasHColor) {
362
+ if (!block.headers) block.headers = {};
363
+ block.headers.color = colorValues['ctrl-h-color'];
364
+ }
365
+ ['h1','h2','h3','h4'].forEach(function(t) {
366
+ var id = 'ctrl-' + t + '-color';
367
+ if (overriddenSet.has(id)) {
368
+ if (!block[t]) block[t] = {};
369
+ block[t].color = colorValues[id];
370
+ }
371
+ });
372
+
373
+ // Paragraph and list
374
+ if (overriddenSet.has('ctrl-p-color')) {
375
+ if (!block.p) block.p = {};
376
+ block.p.color = colorValues['ctrl-p-color'];
377
+ }
378
+ if (overriddenSet.has('ctrl-list-color')) {
379
+ if (!block.list) block.list = {};
380
+ block.list.color = colorValues['ctrl-list-color'];
381
+ }
382
+
383
+ // Standalone colors
384
+ if (overriddenSet.has('ctrl-link-color')) {
385
+ if (!block.link) block.link = {};
386
+ block.link.color = colorValues['ctrl-link-color'];
387
+ }
388
+ if (overriddenSet.has('ctrl-code-bg')) {
389
+ if (!block.code) block.code = {};
390
+ block.code.background = colorValues['ctrl-code-bg'];
391
+ }
392
+ if (overriddenSet.has('ctrl-code-color')) {
393
+ if (!block.code) block.code = {};
394
+ block.code.color = colorValues['ctrl-code-color'];
395
+ }
396
+ if (overriddenSet.has('ctrl-bq-border-color')) {
397
+ if (!block.blockquote) block.blockquote = {};
398
+ block.blockquote.borderColor = colorValues['ctrl-bq-border-color'];
399
+ }
400
+ if (overriddenSet.has('ctrl-bq-color')) {
401
+ if (!block.blockquote) block.blockquote = {};
402
+ block.blockquote.color = colorValues['ctrl-bq-color'];
403
+ }
404
+
405
+ return block;
406
+ }
407
+
408
+ /**
409
+ * collectStylesDual(values, lightOverridden, darkOverridden, lightColors, darkColors)
410
+ * Builds full styles object with light:/dark: keys for colors, shared non-color props at top level.
411
+ */
412
+ function collectStylesDual(values, lightOverridden, darkOverridden, lightColors, darkColors) {
413
+ var gv = function(id) { return values[id] || ''; };
414
+ var gn = function(id) { return parseFloat(values[id]) || 0; };
415
+
416
+ var styles = {
417
+ fontFamily: gv('ctrl-font-family').replace(/['"]/g, '').split(',')[0].trim(),
418
+ baseFontSize: gn('ctrl-base-size-num'),
419
+ lineHeight: gn('ctrl-line-height-num'),
420
+ headers: {
421
+ fontFamily: gv('ctrl-h-font-family').replace(/['"]/g, '').split(',')[0].trim(),
422
+ scale: gn('ctrl-h-scale-num'),
423
+ marginBottom: gn('ctrl-h-mb-num'),
424
+ },
425
+ h1: { fontSize: gn('ctrl-h1-size-num'), fontWeight: parseInt(gv('ctrl-h1-weight')) || 0 },
426
+ h2: { fontSize: gn('ctrl-h2-size-num'), fontWeight: parseInt(gv('ctrl-h2-weight')) || 0 },
427
+ h3: { fontSize: gn('ctrl-h3-size-num'), fontWeight: parseInt(gv('ctrl-h3-weight')) || 0 },
428
+ h4: { fontSize: gn('ctrl-h4-size-num'), fontWeight: parseInt(gv('ctrl-h4-weight')) || 0 },
429
+ p: {
430
+ lineHeight: gn('ctrl-p-lh-num'),
431
+ marginBottom: gn('ctrl-p-mb-num'),
432
+ },
433
+ link: { decoration: gv('ctrl-link-decoration') },
434
+ code: {
435
+ font: gv('ctrl-code-font').replace(/['"]/g, '').split(',')[0].trim(),
436
+ },
437
+ blockquote: {
438
+ borderWidth: gn('ctrl-bq-bw-num'),
439
+ fontSize: gn('ctrl-bq-size-num'),
440
+ },
441
+ };
442
+
443
+ styles.list = {
444
+ spacing: gn('ctrl-list-spacing-num'),
445
+ indent: gn('ctrl-list-indent-num'),
446
+ };
447
+
448
+ // Build theme color blocks
449
+ var lightBlock = collectThemeColors(lightColors, lightOverridden);
450
+ var darkBlock = collectThemeColors(darkColors, darkOverridden);
451
+
452
+ // Only emit light/dark blocks if they have content
453
+ if (Object.keys(lightBlock).length > 0) styles.light = lightBlock;
454
+ if (Object.keys(darkBlock).length > 0) styles.dark = darkBlock;
455
+
456
+ return styles;
457
+ }
458
+
459
+ /**
460
+ * stylesToControls(styles) — extended version
461
+ * Detects styles.light/styles.dark and returns per-theme data when present.
462
+ */
463
+ var _origStylesToControls = stylesToControls;
464
+
465
+ stylesToControls = function(styles) {
466
+ if (!styles) return { controls: {}, overriddenColors: new Set(), hasThemeColors: false };
467
+
468
+ var hasThemeColors = !!(styles.light || styles.dark);
469
+
470
+ if (!hasThemeColors) {
471
+ var result = _origStylesToControls(styles);
472
+ result.hasThemeColors = false;
473
+ return result;
474
+ }
475
+
476
+ // Parse non-color controls from top level (same as original, but skip colors)
477
+ var controls = {};
478
+ var overridden = new Set();
479
+
480
+ if (styles.fontFamily) controls['ctrl-font-family'] = styles.fontFamily;
481
+ if (styles.baseFontSize) controls['ctrl-base-size-num'] = styles.baseFontSize;
482
+ if (styles.lineHeight) controls['ctrl-line-height-num'] = styles.lineHeight;
483
+
484
+ var h = styles.headers || {};
485
+ if (h.fontFamily) controls['ctrl-h-font-family'] = h.fontFamily;
486
+ if (h.scale) controls['ctrl-h-scale-num'] = h.scale;
487
+ if (h.marginBottom) controls['ctrl-h-mb-num'] = h.marginBottom;
488
+
489
+ ['h1', 'h2', 'h3', 'h4'].forEach(function(t) {
490
+ var hs = styles[t] || {};
491
+ if (hs.fontSize) controls['ctrl-' + t + '-size-num'] = hs.fontSize;
492
+ if (hs.fontWeight) controls['ctrl-' + t + '-weight'] = String(hs.fontWeight);
493
+ });
494
+
495
+ var p = styles.p || {};
496
+ if (p.lineHeight) controls['ctrl-p-lh-num'] = p.lineHeight;
497
+ if (p.marginBottom) controls['ctrl-p-mb-num'] = p.marginBottom;
498
+
499
+ var lk = styles.link || {};
500
+ if (lk.decoration) controls['ctrl-link-decoration'] = lk.decoration;
501
+
502
+ var cd = styles.code || {};
503
+ if (cd.font) controls['ctrl-code-font'] = cd.font;
504
+
505
+ var bq = styles.blockquote || {};
506
+ if (bq.borderWidth) controls['ctrl-bq-bw-num'] = bq.borderWidth;
507
+ if (bq.fontSize) controls['ctrl-bq-size-num'] = bq.fontSize;
508
+
509
+ var ll = styles.list || {};
510
+ if (ll.spacing) controls['ctrl-list-spacing-num'] = ll.spacing;
511
+ if (ll.indent) controls['ctrl-list-indent-num'] = ll.indent;
512
+
513
+ // Parse per-theme color blocks
514
+ var lightParsed = parseThemeColorBlock(styles.light);
515
+ var darkParsed = parseThemeColorBlock(styles.dark);
516
+
517
+ return {
518
+ controls: controls,
519
+ overriddenColors: overridden,
520
+ hasThemeColors: true,
521
+ lightColors: lightParsed.colors,
522
+ lightOverridden: lightParsed.overridden,
523
+ darkColors: darkParsed.colors,
524
+ darkOverridden: darkParsed.overridden,
525
+ };
526
+ };
527
+
268
528
  // ═══════════════════════════════════════════════════════
269
529
  // EXPORTS
270
530
  // ═══════════════════════════════════════════════════════
@@ -274,10 +534,16 @@ exports.COLOR_CASCADE = COLOR_CASCADE;
274
534
  exports.CTRL_CSS_MAP = CTRL_CSS_MAP;
275
535
  exports.RANGE_NUM_PAIRS = RANGE_NUM_PAIRS;
276
536
 
277
- exports.controlToCssVars = controlToCssVars;
278
- exports.cascadeColor = cascadeColor;
279
- exports.collectStyles = collectStyles;
280
- exports.stylesToControls = stylesToControls;
537
+ exports.STANDALONE_COLOR_IDS = STANDALONE_COLOR_IDS;
538
+ exports.ALL_COLOR_IDS = ALL_COLOR_IDS;
539
+
540
+ exports.controlToCssVars = controlToCssVars;
541
+ exports.cascadeColor = cascadeColor;
542
+ exports.collectStyles = collectStyles;
543
+ exports.collectStylesDual = collectStylesDual;
544
+ exports.collectThemeColors = collectThemeColors;
545
+ exports.parseThemeColorBlock = parseThemeColorBlock;
546
+ exports.stylesToControls = stylesToControls;
281
547
 
282
548
  // UMD tail: in Node (tests) this writes to module.exports; in the browser
283
549
  // it creates window.SDocStyles. We use this pattern instead of ES modules