sdocs-dev 1.1.2 → 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.
@@ -0,0 +1,12 @@
1
+ // sdocs-slugify.js — Slugify heading text into URL-safe IDs
2
+ // Shared by sdocs-app.js (browser) and test/test-slugify.js (Node)
3
+ (function (exports) {
4
+ 'use strict';
5
+
6
+ function slugify(text) {
7
+ return text.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
8
+ }
9
+
10
+ exports.slugify = slugify;
11
+
12
+ })(typeof module !== 'undefined' && module.exports ? module.exports : (window.SDocSlugify = {}));
@@ -8,6 +8,14 @@ window.SDocs = {
8
8
  currentBody: '',
9
9
  currentMeta: {},
10
10
 
11
+ // Runtime-only metadata (path, fullPath) passed via &local= URL param.
12
+ // Stripped from the URL on load so it isn't in anything the user copies/shares.
13
+ localMeta: {},
14
+
15
+ // Per-doc overrides for the file-info card. Set from `styles.fileinfo` in
16
+ // YAML front matter; emitted back via collectStyles so they round-trip.
17
+ fileinfoStyles: {},
18
+
11
19
  // Per-theme color state
12
20
  activeTheme: 'light',
13
21
  themeColors: { light: {}, dark: {} },
@@ -51,6 +59,12 @@ SDocs.contentAreaEl = document.getElementById('content-area');
51
59
  SDocs.setStyleVar = function(cssVar, value) {
52
60
  SDocs.renderedEl.style.setProperty(cssVar, value);
53
61
  if (SDocs.writeEl) SDocs.writeEl.style.setProperty(cssVar, value);
62
+ // File-info card is a sibling of #rendered, so it doesn't inherit vars set
63
+ // on #rendered. Mirror the relevant block-cascade vars to it directly.
64
+ var ficEl = document.getElementById('file-info-card');
65
+ if (ficEl && /^--md-(fic-|block-)/.test(cssVar)) {
66
+ ficEl.style.setProperty(cssVar, value);
67
+ }
54
68
  if (cssVar === '--md-bg') {
55
69
  SDocs.contentAreaEl.style.setProperty('background-color', value);
56
70
  }
@@ -11,21 +11,32 @@ const COLOR_DEFAULT = '#1c1917';
11
11
 
12
12
  // Control ID → CSS variable name (color controls only)
13
13
  const COLOR_VAR_MAP = {
14
- 'ctrl-color': '--md-color',
15
- 'ctrl-h-color': '--md-h-color',
16
- 'ctrl-h1-color': '--md-h1-color',
17
- 'ctrl-h2-color': '--md-h2-color',
18
- 'ctrl-h3-color': '--md-h3-color',
19
- 'ctrl-h4-color': '--md-h4-color',
20
- 'ctrl-p-color': '--md-p-color',
21
- 'ctrl-list-color': '--md-list-color',
14
+ 'ctrl-color': '--md-color',
15
+ 'ctrl-h-color': '--md-h-color',
16
+ 'ctrl-h1-color': '--md-h1-color',
17
+ 'ctrl-h2-color': '--md-h2-color',
18
+ 'ctrl-h3-color': '--md-h3-color',
19
+ 'ctrl-h4-color': '--md-h4-color',
20
+ 'ctrl-p-color': '--md-p-color',
21
+ 'ctrl-list-color': '--md-list-color',
22
+ // Block cascade
23
+ 'ctrl-block-bg': '--md-block-bg',
24
+ 'ctrl-block-text': '--md-block-text',
25
+ 'ctrl-code-bg': ['--md-code-bg', '--md-pre-bg'],
26
+ 'ctrl-code-color': '--md-code-color',
27
+ 'ctrl-bq-bg': '--md-bq-bg',
28
+ 'ctrl-bq-color': '--md-bq-color',
29
+ 'ctrl-chart-bg': '--md-chart-bg',
30
+ 'ctrl-chart-text': '--md-chart-text',
22
31
  };
23
32
 
24
33
  // Cascade tree: parent → direct children
25
34
  const COLOR_CASCADE = {
26
- 'ctrl-color': ['ctrl-h-color', 'ctrl-p-color'],
27
- 'ctrl-h-color': ['ctrl-h1-color', 'ctrl-h2-color', 'ctrl-h3-color', 'ctrl-h4-color'],
28
- 'ctrl-p-color': ['ctrl-list-color'],
35
+ 'ctrl-color': ['ctrl-h-color', 'ctrl-p-color'],
36
+ 'ctrl-h-color': ['ctrl-h1-color', 'ctrl-h2-color', 'ctrl-h3-color', 'ctrl-h4-color'],
37
+ 'ctrl-p-color': ['ctrl-list-color'],
38
+ 'ctrl-block-bg': ['ctrl-code-bg', 'ctrl-bq-bg', 'ctrl-chart-bg'],
39
+ 'ctrl-block-text': ['ctrl-code-color', 'ctrl-bq-color', 'ctrl-chart-text'],
29
40
  };
30
41
 
31
42
  // Control ID → { cssVar, suffix?, compound? }
@@ -51,15 +62,13 @@ const CTRL_CSS_MAP = {
51
62
  'ctrl-link-color': { cssVar: '--md-link-color' },
52
63
  'ctrl-link-decoration': { cssVar: '--md-link-decoration' },
53
64
  'ctrl-code-font': { cssVar: '--md-code-font' },
54
- 'ctrl-code-bg': { cssVar: ['--md-code-bg', '--md-pre-bg'] },
55
- 'ctrl-code-color': { cssVar: '--md-code-color' },
56
65
  'ctrl-bq-border-color': { cssVar: '--md-bq-border', compound: 'bq-border' },
57
66
  'ctrl-bq-bw-num': { cssVar: '--md-bq-border', compound: 'bq-border' },
58
- 'ctrl-bq-bg': { cssVar: '--md-bq-bg' },
59
67
  'ctrl-bq-size-num': { cssVar: '--md-bq-size', suffix: 'em' },
60
- 'ctrl-bq-color': { cssVar: '--md-bq-color' },
61
68
  'ctrl-list-spacing-num': { cssVar: '--md-list-spacing', suffix: 'em' },
62
69
  'ctrl-list-indent-num': { cssVar: '--md-list-indent', suffix: 'em' },
70
+ 'ctrl-chart-accent': { cssVar: '--md-chart-accent' },
71
+ 'ctrl-chart-palette': { cssVar: '--md-chart-palette' },
63
72
  };
64
73
 
65
74
  // Range ↔ Number input pairs
@@ -80,6 +89,93 @@ const RANGE_NUM_PAIRS = [
80
89
  ['ctrl-list-indent-range', 'ctrl-list-indent-num'],
81
90
  ];
82
91
 
92
+ // ═══════════════════════════════════════════════════════
93
+ // HSL COLOR UTILITIES
94
+ // ═══════════════════════════════════════════════════════
95
+
96
+ function hexToHsl(hex) {
97
+ if (!hex || hex.charAt(0) !== '#') return null;
98
+ var r = parseInt(hex.slice(1, 3), 16) / 255;
99
+ var g = parseInt(hex.slice(3, 5), 16) / 255;
100
+ var b = parseInt(hex.slice(5, 7), 16) / 255;
101
+ var max = Math.max(r, g, b), min = Math.min(r, g, b);
102
+ var h, s, l = (max + min) / 2;
103
+ if (max === min) { h = s = 0; }
104
+ else {
105
+ var d = max - min;
106
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
107
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
108
+ else if (max === g) h = ((b - r) / d + 2) / 6;
109
+ else h = ((r - g) / d + 4) / 6;
110
+ }
111
+ return [h * 360, s * 100, l * 100];
112
+ }
113
+
114
+ function hslToHex(h, s, l) {
115
+ h = ((h % 360) + 360) % 360;
116
+ s = Math.max(0, Math.min(100, s)) / 100;
117
+ l = Math.max(0, Math.min(100, l)) / 100;
118
+ var c = (1 - Math.abs(2 * l - 1)) * s;
119
+ var x = c * (1 - Math.abs((h / 60) % 2 - 1));
120
+ var m = l - c / 2;
121
+ var r, g, b;
122
+ if (h < 60) { r = c; g = x; b = 0; }
123
+ else if (h < 120) { r = x; g = c; b = 0; }
124
+ else if (h < 180) { r = 0; g = c; b = x; }
125
+ else if (h < 240) { r = 0; g = x; b = c; }
126
+ else if (h < 300) { r = x; g = 0; b = c; }
127
+ else { r = c; g = 0; b = x; }
128
+ var toH = function(v) { var h = Math.round((v + m) * 255).toString(16); return h.length < 2 ? '0' + h : h; };
129
+ return '#' + toH(r) + toH(g) + toH(b);
130
+ }
131
+
132
+ /**
133
+ * invertLightness(hex)
134
+ * Inverts a color's lightness for the opposite theme.
135
+ * Light colors (L>50) become dark, dark colors become light.
136
+ * Keeps hue and saturation, mirrors lightness around 50%.
137
+ * Slightly biased: dark bgs get very dark (L≈10-20), light text gets bright (L≈80-90).
138
+ */
139
+ /**
140
+ * invertLightness(hex)
141
+ * Generates a dark-theme counterpart for a light-theme color.
142
+ *
143
+ * Strategy: colors that look "right" in light mode get adapted for dark mode.
144
+ * - Very light colors (L>65): page/block backgrounds → make very dark
145
+ * - Very dark colors (L<20): already dark, likely intentional → keep as-is
146
+ * - Dark-ish colors (20<L<45): body text, headings → make light
147
+ * - Mid-range (45-65): accent colors → moderate shift
148
+ */
149
+ function invertLightness(hex) {
150
+ var hsl = hexToHsl(hex);
151
+ if (!hsl) return hex;
152
+ var h = hsl[0], s = hsl[1], l = hsl[2];
153
+ var invL, invS;
154
+
155
+ if (l > 80) {
156
+ // Very light background → very dark
157
+ invL = 10 + (100 - l) * 0.3; // L95→11, L85→14, L80→16
158
+ invS = s * 0.7;
159
+ } else if (l > 65) {
160
+ // Light background/accent → dark
161
+ invL = 12 + (100 - l) * 0.4; // L70→24, L65→26
162
+ invS = s * 0.75;
163
+ } else if (l < 20) {
164
+ // Already very dark → keep as-is (intentional dark bg like code blocks)
165
+ return hex;
166
+ } else if (l < 40) {
167
+ // Dark text → make light for dark theme readability
168
+ invL = 65 + (40 - l) * 0.7; // L10→86, L20→79, L35→68
169
+ invS = s * 0.8;
170
+ } else {
171
+ // Mid-range accent → moderate inversion
172
+ invL = 100 - l;
173
+ invS = s * 0.85;
174
+ }
175
+
176
+ return hslToHex(h, Math.max(0, invS), Math.max(0, Math.min(100, invL)));
177
+ }
178
+
83
179
  // ═══════════════════════════════════════════════════════
84
180
  // PURE FUNCTIONS
85
181
  // ═══════════════════════════════════════════════════════
@@ -91,7 +187,11 @@ const RANGE_NUM_PAIRS = [
91
187
  function controlToCssVars(ctrlId, value, allValues) {
92
188
  // Color controls go through the cascade system, not CTRL_CSS_MAP
93
189
  if (COLOR_VAR_MAP[ctrlId]) {
94
- return [{ cssVar: COLOR_VAR_MAP[ctrlId], value: value }];
190
+ var varName = COLOR_VAR_MAP[ctrlId];
191
+ if (Array.isArray(varName)) {
192
+ return varName.map(function(cv) { return { cssVar: cv, value: value }; });
193
+ }
194
+ return [{ cssVar: varName, value: value }];
95
195
  }
96
196
 
97
197
  const entry = CTRL_CSS_MAP[ctrlId];
@@ -165,15 +265,11 @@ function collectStyles(values, overriddenColors) {
165
265
  link: { color: gv('ctrl-link-color'), decoration: gv('ctrl-link-decoration') },
166
266
  code: {
167
267
  font: gv('ctrl-code-font').replace(/['"]/g, '').split(',')[0].trim(),
168
- background: gv('ctrl-code-bg'),
169
- color: gv('ctrl-code-color'),
170
268
  },
171
269
  blockquote: {
172
270
  borderColor: gv('ctrl-bq-border-color'),
173
271
  borderWidth: gn('ctrl-bq-bw-num'),
174
- background: gv('ctrl-bq-bg'),
175
272
  fontSize: gn('ctrl-bq-size-num'),
176
- color: gv('ctrl-bq-color'),
177
273
  },
178
274
  };
179
275
 
@@ -193,6 +289,25 @@ function collectStyles(values, overriddenColors) {
193
289
  if (overriddenColors.has('ctrl-p-color')) styles.p.color = gv('ctrl-p-color');
194
290
  if (overriddenColors.has('ctrl-list-color')) styles.list.color = gv('ctrl-list-color');
195
291
 
292
+ // Block cascade colors
293
+ if (overriddenColors.has('ctrl-block-bg') || overriddenColors.has('ctrl-block-text')) {
294
+ styles.blocks = {};
295
+ if (overriddenColors.has('ctrl-block-bg')) styles.blocks.background = gv('ctrl-block-bg');
296
+ if (overriddenColors.has('ctrl-block-text')) styles.blocks.color = gv('ctrl-block-text');
297
+ }
298
+ if (overriddenColors.has('ctrl-code-bg')) styles.code.background = gv('ctrl-code-bg');
299
+ if (overriddenColors.has('ctrl-code-color')) styles.code.color = gv('ctrl-code-color');
300
+ if (overriddenColors.has('ctrl-bq-bg')) styles.blockquote.background = gv('ctrl-bq-bg');
301
+ if (overriddenColors.has('ctrl-bq-color')) styles.blockquote.color = gv('ctrl-bq-color');
302
+
303
+ // Chart styles
304
+ var chartObj = {};
305
+ if (overriddenColors.has('ctrl-chart-accent')) chartObj.accent = gv('ctrl-chart-accent');
306
+ if (gv('ctrl-chart-palette') && gv('ctrl-chart-palette') !== 'monochrome') chartObj.palette = gv('ctrl-chart-palette');
307
+ if (overriddenColors.has('ctrl-chart-bg')) chartObj.background = gv('ctrl-chart-bg');
308
+ if (overriddenColors.has('ctrl-chart-text')) chartObj.textColor = gv('ctrl-chart-text');
309
+ if (Object.keys(chartObj).length) styles.chart = chartObj;
310
+
196
311
  return styles;
197
312
  }
198
313
 
@@ -251,17 +366,22 @@ function stylesToControls(styles) {
251
366
  if (lk.color) controls['ctrl-link-color'] = lk.color;
252
367
  if (lk.decoration) controls['ctrl-link-decoration'] = lk.decoration;
253
368
 
369
+ // Blocks cascade parent
370
+ const bl = styles.blocks || {};
371
+ if (bl.background) { controls['ctrl-block-bg'] = bl.background; overridden.add('ctrl-block-bg'); }
372
+ if (bl.color) { controls['ctrl-block-text'] = bl.color; overridden.add('ctrl-block-text'); }
373
+
254
374
  const cd = styles.code || {};
255
375
  if (cd.font) controls['ctrl-code-font'] = cd.font;
256
- if (cd.background) controls['ctrl-code-bg'] = cd.background;
257
- if (cd.color) controls['ctrl-code-color'] = cd.color;
376
+ if (cd.background) { controls['ctrl-code-bg'] = cd.background; overridden.add('ctrl-code-bg'); }
377
+ if (cd.color) { controls['ctrl-code-color'] = cd.color; overridden.add('ctrl-code-color'); }
258
378
 
259
379
  const bq = styles.blockquote || {};
260
380
  if (bq.borderColor) controls['ctrl-bq-border-color'] = bq.borderColor;
261
381
  if (bq.borderWidth) controls['ctrl-bq-bw-num'] = bq.borderWidth;
262
- if (bq.background) controls['ctrl-bq-bg'] = bq.background;
382
+ if (bq.background) { controls['ctrl-bq-bg'] = bq.background; overridden.add('ctrl-bq-bg'); }
263
383
  if (bq.fontSize) controls['ctrl-bq-size-num'] = bq.fontSize;
264
- if (bq.color) controls['ctrl-bq-color'] = bq.color;
384
+ if (bq.color) { controls['ctrl-bq-color'] = bq.color; overridden.add('ctrl-bq-color'); }
265
385
 
266
386
  const ll = styles.list || {};
267
387
  if (ll.spacing) controls['ctrl-list-spacing-num'] = ll.spacing;
@@ -271,6 +391,12 @@ function stylesToControls(styles) {
271
391
  overridden.add('ctrl-list-color');
272
392
  }
273
393
 
394
+ const ch = styles.chart || {};
395
+ if (ch.accent) { controls['ctrl-chart-accent'] = ch.accent; overridden.add('ctrl-chart-accent'); }
396
+ if (ch.palette) controls['ctrl-chart-palette'] = ch.palette;
397
+ if (ch.background) { controls['ctrl-chart-bg'] = ch.background; overridden.add('ctrl-chart-bg'); }
398
+ if (ch.textColor) { controls['ctrl-chart-text'] = ch.textColor; overridden.add('ctrl-chart-text'); }
399
+
274
400
  return { controls, overriddenColors: overridden };
275
401
  }
276
402
 
@@ -279,7 +405,9 @@ function stylesToControls(styles) {
279
405
  // ═══════════════════════════════════════════════════════
280
406
 
281
407
  var STANDALONE_COLOR_IDS = [
282
- 'ctrl-bg-color','ctrl-link-color','ctrl-code-bg','ctrl-code-color','ctrl-bq-border-color','ctrl-bq-bg','ctrl-bq-color',
408
+ 'ctrl-bg-color','ctrl-link-color',
409
+ 'ctrl-bq-border-color',
410
+ 'ctrl-chart-accent',
283
411
  ];
284
412
 
285
413
  var CASCADE_COLOR_IDS = Object.keys(COLOR_VAR_MAP);
@@ -291,249 +419,46 @@ var ALL_COLOR_IDS = CASCADE_COLOR_IDS.concat(STANDALONE_COLOR_IDS);
291
419
  // ═══════════════════════════════════════════════════════
292
420
 
293
421
  /**
294
- * parseThemeColorBlock(block)
295
- * Extracts { colors: {ctrlId: val}, overridden: Set } from a light:/dark: sub-object.
296
- * The block is an object like { color: "#1a1a2e", h1: { color: "#c0392b" }, link: { color: "#2563eb" } }
422
+ * parseDarkBlock(block)
423
+ * Extracts { ctrlId: colorValue } from a dark: sub-object in YAML front matter.
297
424
  */
298
- function parseThemeColorBlock(block) {
299
- if (!block) return { colors: {}, overridden: new Set() };
425
+ function parseDarkBlock(block) {
426
+ if (!block) return {};
300
427
  var colors = {};
301
- var overridden = new Set();
302
428
 
303
- // Background color
304
- if (block.background) {
305
- colors['ctrl-bg-color'] = block.background;
306
- overridden.add('ctrl-bg-color');
307
- }
429
+ if (block.background) colors['ctrl-bg-color'] = block.background;
430
+ if (block.color) colors['ctrl-color'] = block.color;
308
431
 
309
- // Top-level color (cascade root)
310
- if (block.color) {
311
- colors['ctrl-color'] = block.color;
312
- overridden.add('ctrl-color');
313
- }
314
-
315
- // Cascade colors nested in sub-objects
316
- if (block.headers && block.headers.color) {
317
- colors['ctrl-h-color'] = block.headers.color;
318
- overridden.add('ctrl-h-color');
319
- }
432
+ if (block.headers && block.headers.color) colors['ctrl-h-color'] = block.headers.color;
320
433
  ['h1','h2','h3','h4'].forEach(function(t) {
321
434
  var obj = block[t];
322
- if (obj && obj.color) {
323
- colors['ctrl-' + t + '-color'] = obj.color;
324
- overridden.add('ctrl-' + t + '-color');
325
- }
435
+ if (obj && obj.color) colors['ctrl-' + t + '-color'] = obj.color;
326
436
  });
327
- if (block.p && block.p.color) {
328
- colors['ctrl-p-color'] = block.p.color;
329
- overridden.add('ctrl-p-color');
330
- }
331
- if (block.list && block.list.color) {
332
- colors['ctrl-list-color'] = block.list.color;
333
- overridden.add('ctrl-list-color');
334
- }
437
+ if (block.p && block.p.color) colors['ctrl-p-color'] = block.p.color;
438
+ if (block.list && block.list.color) colors['ctrl-list-color'] = block.list.color;
439
+ if (block.link && block.link.color) colors['ctrl-link-color'] = block.link.color;
335
440
 
336
- // Standalone colors
337
- if (block.link && block.link.color) {
338
- colors['ctrl-link-color'] = block.link.color;
339
- overridden.add('ctrl-link-color');
441
+ if (block.blocks) {
442
+ if (block.blocks.background) colors['ctrl-block-bg'] = block.blocks.background;
443
+ if (block.blocks.color) colors['ctrl-block-text'] = block.blocks.color;
340
444
  }
341
445
  if (block.code) {
342
- if (block.code.background) { colors['ctrl-code-bg'] = block.code.background; overridden.add('ctrl-code-bg'); }
343
- if (block.code.color) { colors['ctrl-code-color'] = block.code.color; overridden.add('ctrl-code-color'); }
446
+ if (block.code.background) colors['ctrl-code-bg'] = block.code.background;
447
+ if (block.code.color) colors['ctrl-code-color'] = block.code.color;
344
448
  }
345
449
  if (block.blockquote) {
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'); }
348
- if (block.blockquote.color) { colors['ctrl-bq-color'] = block.blockquote.color; overridden.add('ctrl-bq-color'); }
349
- }
350
-
351
- return { colors: colors, overridden: overridden };
352
- }
353
-
354
- /**
355
- * collectThemeColors(colorValues, overriddenSet)
356
- * Builds the YAML sub-object for one theme (only emits overridden colors).
357
- */
358
- function collectThemeColors(colorValues, overriddenSet) {
359
- var block = {};
360
- if (overriddenSet.has('ctrl-bg-color')) block.background = colorValues['ctrl-bg-color'];
361
- if (overriddenSet.has('ctrl-color')) block.color = colorValues['ctrl-color'];
362
-
363
- // Heading colors
364
- var hasHColor = overriddenSet.has('ctrl-h-color');
365
- if (hasHColor) {
366
- if (!block.headers) block.headers = {};
367
- block.headers.color = colorValues['ctrl-h-color'];
368
- }
369
- ['h1','h2','h3','h4'].forEach(function(t) {
370
- var id = 'ctrl-' + t + '-color';
371
- if (overriddenSet.has(id)) {
372
- if (!block[t]) block[t] = {};
373
- block[t].color = colorValues[id];
374
- }
375
- });
376
-
377
- // Paragraph and list
378
- if (overriddenSet.has('ctrl-p-color')) {
379
- if (!block.p) block.p = {};
380
- block.p.color = colorValues['ctrl-p-color'];
381
- }
382
- if (overriddenSet.has('ctrl-list-color')) {
383
- if (!block.list) block.list = {};
384
- block.list.color = colorValues['ctrl-list-color'];
385
- }
386
-
387
- // Standalone colors
388
- if (overriddenSet.has('ctrl-link-color')) {
389
- if (!block.link) block.link = {};
390
- block.link.color = colorValues['ctrl-link-color'];
391
- }
392
- if (overriddenSet.has('ctrl-code-bg')) {
393
- if (!block.code) block.code = {};
394
- block.code.background = colorValues['ctrl-code-bg'];
395
- }
396
- if (overriddenSet.has('ctrl-code-color')) {
397
- if (!block.code) block.code = {};
398
- block.code.color = colorValues['ctrl-code-color'];
399
- }
400
- if (overriddenSet.has('ctrl-bq-border-color')) {
401
- if (!block.blockquote) block.blockquote = {};
402
- block.blockquote.borderColor = colorValues['ctrl-bq-border-color'];
450
+ if (block.blockquote.borderColor) colors['ctrl-bq-border-color'] = block.blockquote.borderColor;
451
+ if (block.blockquote.background) colors['ctrl-bq-bg'] = block.blockquote.background;
452
+ if (block.blockquote.color) colors['ctrl-bq-color'] = block.blockquote.color;
403
453
  }
404
- if (overriddenSet.has('ctrl-bq-bg')) {
405
- if (!block.blockquote) block.blockquote = {};
406
- block.blockquote.background = colorValues['ctrl-bq-bg'];
454
+ if (block.chart) {
455
+ if (block.chart.background) colors['ctrl-chart-bg'] = block.chart.background;
456
+ if (block.chart.textColor) colors['ctrl-chart-text'] = block.chart.textColor;
407
457
  }
408
- if (overriddenSet.has('ctrl-bq-color')) {
409
- if (!block.blockquote) block.blockquote = {};
410
- block.blockquote.color = colorValues['ctrl-bq-color'];
411
- }
412
-
413
- return block;
414
- }
415
-
416
- /**
417
- * collectStylesDual(values, lightOverridden, darkOverridden, lightColors, darkColors)
418
- * Builds full styles object with light:/dark: keys for colors, shared non-color props at top level.
419
- */
420
- function collectStylesDual(values, lightOverridden, darkOverridden, lightColors, darkColors) {
421
- var gv = function(id) { return values[id] || ''; };
422
- var gn = function(id) { return parseFloat(values[id]) || 0; };
423
-
424
- var styles = {
425
- fontFamily: gv('ctrl-font-family').replace(/['"]/g, '').split(',')[0].trim(),
426
- baseFontSize: gn('ctrl-base-size-num'),
427
- lineHeight: gn('ctrl-line-height-num'),
428
- headers: {
429
- fontFamily: gv('ctrl-h-font-family').replace(/['"]/g, '').split(',')[0].trim(),
430
- scale: gn('ctrl-h-scale-num'),
431
- marginBottom: gn('ctrl-h-mb-num'),
432
- },
433
- h1: { fontSize: gn('ctrl-h1-size-num'), fontWeight: parseInt(gv('ctrl-h1-weight')) || 0 },
434
- h2: { fontSize: gn('ctrl-h2-size-num'), fontWeight: parseInt(gv('ctrl-h2-weight')) || 0 },
435
- h3: { fontSize: gn('ctrl-h3-size-num'), fontWeight: parseInt(gv('ctrl-h3-weight')) || 0 },
436
- h4: { fontSize: gn('ctrl-h4-size-num'), fontWeight: parseInt(gv('ctrl-h4-weight')) || 0 },
437
- p: {
438
- lineHeight: gn('ctrl-p-lh-num'),
439
- marginBottom: gn('ctrl-p-mb-num'),
440
- },
441
- link: { decoration: gv('ctrl-link-decoration') },
442
- code: {
443
- font: gv('ctrl-code-font').replace(/['"]/g, '').split(',')[0].trim(),
444
- },
445
- blockquote: {
446
- borderWidth: gn('ctrl-bq-bw-num'),
447
- fontSize: gn('ctrl-bq-size-num'),
448
- },
449
-
450
- };
451
458
 
452
- styles.list = {
453
- spacing: gn('ctrl-list-spacing-num'),
454
- indent: gn('ctrl-list-indent-num'),
455
- };
456
-
457
- // Build theme color blocks
458
- var lightBlock = collectThemeColors(lightColors, lightOverridden);
459
- var darkBlock = collectThemeColors(darkColors, darkOverridden);
460
-
461
- // Only emit light/dark blocks if they have content
462
- if (Object.keys(lightBlock).length > 0) styles.light = lightBlock;
463
- if (Object.keys(darkBlock).length > 0) styles.dark = darkBlock;
464
-
465
- return styles;
459
+ return colors;
466
460
  }
467
461
 
468
- /**
469
- * stylesToControls(styles) — extended version
470
- * Detects styles.light/styles.dark and returns per-theme data when present.
471
- */
472
- var _origStylesToControls = stylesToControls;
473
-
474
- stylesToControls = function(styles) {
475
- if (!styles) return { controls: {}, overriddenColors: new Set(), hasThemeColors: false };
476
-
477
- var hasThemeColors = !!(styles.light || styles.dark);
478
-
479
- if (!hasThemeColors) {
480
- var result = _origStylesToControls(styles);
481
- result.hasThemeColors = false;
482
- return result;
483
- }
484
-
485
- // Parse non-color controls from top level (same as original, but skip colors)
486
- var controls = {};
487
- var overridden = new Set();
488
-
489
- if (styles.fontFamily) controls['ctrl-font-family'] = styles.fontFamily;
490
- if (styles.baseFontSize) controls['ctrl-base-size-num'] = styles.baseFontSize;
491
- if (styles.lineHeight) controls['ctrl-line-height-num'] = styles.lineHeight;
492
-
493
- var h = styles.headers || {};
494
- if (h.fontFamily) controls['ctrl-h-font-family'] = h.fontFamily;
495
- if (h.scale) controls['ctrl-h-scale-num'] = h.scale;
496
- if (h.marginBottom) controls['ctrl-h-mb-num'] = h.marginBottom;
497
-
498
- ['h1', 'h2', 'h3', 'h4'].forEach(function(t) {
499
- var hs = styles[t] || {};
500
- if (hs.fontSize) controls['ctrl-' + t + '-size-num'] = hs.fontSize;
501
- if (hs.fontWeight) controls['ctrl-' + t + '-weight'] = String(hs.fontWeight);
502
- });
503
-
504
- var p = styles.p || {};
505
- if (p.lineHeight) controls['ctrl-p-lh-num'] = p.lineHeight;
506
- if (p.marginBottom) controls['ctrl-p-mb-num'] = p.marginBottom;
507
-
508
- var lk = styles.link || {};
509
- if (lk.decoration) controls['ctrl-link-decoration'] = lk.decoration;
510
-
511
- var cd = styles.code || {};
512
- if (cd.font) controls['ctrl-code-font'] = cd.font;
513
-
514
- var bq = styles.blockquote || {};
515
- if (bq.borderWidth) controls['ctrl-bq-bw-num'] = bq.borderWidth;
516
- if (bq.fontSize) controls['ctrl-bq-size-num'] = bq.fontSize;
517
-
518
- var ll = styles.list || {};
519
- if (ll.spacing) controls['ctrl-list-spacing-num'] = ll.spacing;
520
- if (ll.indent) controls['ctrl-list-indent-num'] = ll.indent;
521
-
522
- // Parse per-theme color blocks
523
- var lightParsed = parseThemeColorBlock(styles.light);
524
- var darkParsed = parseThemeColorBlock(styles.dark);
525
-
526
- return {
527
- controls: controls,
528
- overriddenColors: overridden,
529
- hasThemeColors: true,
530
- lightColors: lightParsed.colors,
531
- lightOverridden: lightParsed.overridden,
532
- darkColors: darkParsed.colors,
533
- darkOverridden: darkParsed.overridden,
534
- };
535
- };
536
-
537
462
  // ═══════════════════════════════════════════════════════
538
463
  // STRIP STYLE DEFAULTS (for shorter URLs)
539
464
  // ═══════════════════════════════════════════════════════
@@ -619,12 +544,13 @@ exports.RANGE_NUM_PAIRS = RANGE_NUM_PAIRS;
619
544
  exports.STANDALONE_COLOR_IDS = STANDALONE_COLOR_IDS;
620
545
  exports.ALL_COLOR_IDS = ALL_COLOR_IDS;
621
546
 
547
+ exports.hexToHsl = hexToHsl;
548
+ exports.hslToHex = hslToHex;
622
549
  exports.controlToCssVars = controlToCssVars;
623
550
  exports.cascadeColor = cascadeColor;
551
+ exports.invertLightness = invertLightness;
624
552
  exports.collectStyles = collectStyles;
625
- exports.collectStylesDual = collectStylesDual;
626
- exports.collectThemeColors = collectThemeColors;
627
- exports.parseThemeColorBlock = parseThemeColorBlock;
553
+ exports.parseDarkBlock = parseDarkBlock;
628
554
  exports.stylesToControls = stylesToControls;
629
555
  exports.STYLE_DEFAULTS = STYLE_DEFAULTS;
630
556
  exports.stripStyleDefaults = stripStyleDefaults;
@@ -89,7 +89,7 @@ function getPreferredTheme() {
89
89
  }
90
90
 
91
91
  const SUN_ICON = '<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>';
92
- const MOON_ICON = '<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>';
92
+ const MOON_ICON = '<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" stroke-width="1.5"/>';
93
93
 
94
94
  function updateThemeIcon(theme) {
95
95
  const icon = document.getElementById('icon-theme');
@@ -120,32 +120,35 @@ function loadThemeColors(theme) {
120
120
  var overridden = S.themeOverridden[theme];
121
121
  var defaults = getThemeDefaultsFor(theme);
122
122
 
123
- // Cascade root first
124
- if (overridden.has('ctrl-color')) {
125
- S.setColorValue('ctrl-color', colors['ctrl-color'], true);
126
- } else {
127
- S.setColorValue('ctrl-color', defaults.colorDefault, false);
128
- }
123
+ // Cascade roots: text color and block bg/text
124
+ var cascadeRoots = [
125
+ ['ctrl-color', defaults.colorDefault],
126
+ ['ctrl-block-bg', defaults.codeBg],
127
+ ['ctrl-block-text', defaults.bqColor],
128
+ ];
129
+ cascadeRoots.forEach(function(pair) {
130
+ var id = pair[0], def = pair[1];
131
+ if (overridden.has(id)) {
132
+ S.setColorValue(id, colors[id], true);
133
+ } else {
134
+ S.setColorValue(id, def, false);
135
+ }
136
+ });
129
137
 
130
- // Then cascade children that are overridden
138
+ // Then cascade children that are overridden (overrides the parent cascade)
131
139
  var cascadeIds = Object.keys(SDocStyles.COLOR_VAR_MAP);
132
140
  cascadeIds.forEach(function(ctrlId) {
133
- if (ctrlId === 'ctrl-color') return; // already handled
141
+ if (ctrlId === 'ctrl-color' || ctrlId === 'ctrl-block-bg' || ctrlId === 'ctrl-block-text') return;
134
142
  if (overridden.has(ctrlId)) {
135
143
  S.setColorValue(ctrlId, colors[ctrlId], true);
136
144
  }
137
- // Non-overridden cascade children are already set by cascade from parent
138
145
  });
139
146
 
140
- // Standalone colors
147
+ // Standalone colors (not in cascade)
141
148
  var standaloneMap = {
142
149
  'ctrl-bg-color': defaults.bgColor,
143
150
  'ctrl-link-color': defaults.linkColor,
144
- 'ctrl-code-bg': defaults.codeBg,
145
- 'ctrl-code-color': defaults.codeColor,
146
151
  'ctrl-bq-border-color': defaults.bqBorderColor,
147
- 'ctrl-bq-bg': defaults.bqBg,
148
- 'ctrl-bq-color': defaults.bqColor,
149
152
  };
150
153
  for (var ctrlId in standaloneMap) {
151
154
  var el = document.getElementById(ctrlId);
@@ -217,11 +220,10 @@ function getStandaloneDefault(ctrlId) {
217
220
  const map = {
218
221
  'ctrl-bg-color': d.bgColor,
219
222
  'ctrl-link-color': d.linkColor,
220
- 'ctrl-code-bg': d.codeBg,
221
- 'ctrl-code-color': d.codeColor,
222
223
  'ctrl-bq-border-color': d.bqBorderColor,
223
- 'ctrl-bq-bg': d.bqBg,
224
- 'ctrl-bq-color': d.bqColor,
224
+ 'ctrl-block-bg': d.codeBg,
225
+ 'ctrl-block-text': d.bqColor,
226
+ 'ctrl-chart-accent': '#3b82f6',
225
227
  };
226
228
  return map[ctrlId];
227
229
  }