kempo-css 2.1.2 → 2.1.4

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.
Files changed (40) hide show
  1. package/.vscode/settings.json +2 -0
  2. package/CHANGELOG.md +7 -0
  3. package/dist/kempo.min.css +1 -1
  4. package/docs/borders-spacing.html +445 -0
  5. package/docs/buttons.html +261 -0
  6. package/docs/colors.html +198 -0
  7. package/docs/components/ThemePropertyInput.js +2 -2
  8. package/docs/elevation.html +508 -0
  9. package/docs/examples/responsive-grid.html +1 -1
  10. package/docs/index.html +236 -1477
  11. package/docs/inputs.html +251 -0
  12. package/docs/kempo.css +48 -32
  13. package/docs/kempo.min.css +1 -1
  14. package/docs/layout.html +310 -0
  15. package/docs/tables.html +261 -0
  16. package/docs/theme-editor.html +935 -802
  17. package/docs/typography.html +255 -0
  18. package/docs/utilities.html +171 -0
  19. package/docs-src/.config.js +26 -0
  20. package/docs-src/borders-spacing.page.html +297 -0
  21. package/docs-src/buttons.page.html +113 -0
  22. package/docs-src/colors.page.html +50 -0
  23. package/docs-src/default.template.html +22 -0
  24. package/docs-src/elevation.page.html +361 -0
  25. package/docs-src/examples/responsive-grid.html +33 -0
  26. package/docs-src/head.fragment.html +16 -0
  27. package/docs-src/index.page.html +95 -0
  28. package/docs-src/inputs.page.html +103 -0
  29. package/docs-src/layout.page.html +163 -0
  30. package/docs-src/nav.fragment.html +115 -0
  31. package/docs-src/tables.page.html +114 -0
  32. package/docs-src/theme-editor.page.html +850 -0
  33. package/docs-src/theme-editor.template.html +11 -0
  34. package/docs-src/typography.page.html +107 -0
  35. package/docs-src/utilities.page.html +26 -0
  36. package/package.json +3 -2
  37. package/scripts/build.js +7 -1
  38. package/src/kempo.css +48 -32
  39. package/docs/init.js +0 -4
  40. package/docs/nav.js +0 -33
@@ -0,0 +1,850 @@
1
+ <page
2
+ template="theme-editor"
3
+ title="Theme Editor - Kempo CSS"
4
+ >
5
+ <content location="styles">
6
+ <style>
7
+ html {
8
+ scrollbar-gutter: unset;
9
+ }
10
+
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ overflow: hidden;
15
+ height: 100vh;
16
+ }
17
+
18
+ #grid-container {
19
+ height: calc(100vh - 4rem);
20
+ }
21
+
22
+ k-split {
23
+ height: 100%;
24
+ }
25
+ </style>
26
+ </content>
27
+ <content>
28
+ <div id="grid-container">
29
+ <k-split style="--pane_1_size: 24rem;">
30
+ <aside
31
+ class="d-b p"
32
+ style="overflow-y: auto; height: 100%;"
33
+ >
34
+ <div class="mb">
35
+ <div class="d-f full btn-grp mb">
36
+ <button
37
+ id="downloadTheme"
38
+ class="primary flex"
39
+ >Download Theme</button>
40
+ <button
41
+ id="uploadThemeBtn"
42
+ class="secondary flex"
43
+ >Upload Theme</button>
44
+ </div>
45
+ <label class="d-b mb-q">
46
+ <strong>Editing Theme</strong>
47
+ <select
48
+ id="editingThemeSelect"
49
+ class="w-100"
50
+ >
51
+ <option value="light">Light</option>
52
+ <option value="dark">Dark</option>
53
+ </select>
54
+ </label>
55
+ </div>
56
+ <hr class="mb">
57
+ <div id="themeInputs"></div>
58
+ </aside>
59
+ <div
60
+ slot="right"
61
+ style="overflow-y: auto; height: 100%;"
62
+ >
63
+ <main style="padding: var(--spacer);">
64
+ <k-import src="./demo.inc.html"></k-import>
65
+ </main>
66
+ </div>
67
+ </k-split>
68
+ </div>
69
+
70
+ <script
71
+ type="module"
72
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Nav.js"
73
+ ></script>
74
+ <script
75
+ type="module"
76
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Aside.js"
77
+ ></script>
78
+ <script
79
+ type="module"
80
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Split.js"
81
+ ></script>
82
+ <script
83
+ type="module"
84
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Icon.js"
85
+ ></script>
86
+ <script
87
+ type="module"
88
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/ThemeSwitcher.js"
89
+ ></script>
90
+ <script
91
+ type="module"
92
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Import.js"
93
+ ></script>
94
+ <script
95
+ type="module"
96
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Resize.js"
97
+ ></script>
98
+ <script
99
+ type="module"
100
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Card.js"
101
+ ></script>
102
+ <script
103
+ type="module"
104
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/ColorPicker.js"
105
+ ></script>
106
+ <script
107
+ type="module"
108
+ src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/components/Dialog.js"
109
+ ></script>
110
+ <script
111
+ src="./components/ThemePropertyInput.js"
112
+ type="module"
113
+ ></script>
114
+ <script type="module">
115
+ import theme from 'https://cdn.jsdelivr.net/npm/kempo-ui@0.3/dist/utils/theme.js';
116
+
117
+ /*
118
+ Simple Context for storing theme CSS
119
+ */
120
+ const themeContext = {};
121
+ const setContext = (key, value) => { themeContext[key] = value; };
122
+ const getContext = key => themeContext[key];
123
+
124
+ const defaultTheme = {
125
+ "--ff_body": "\"Helvetica Neue\", Helvetica, Arial, sans-serif",
126
+ "--ff_heading": "\"Helvetica Neue\", Helvetica, Arial, sans-serif",
127
+ "--ff_mono": "Consolas, monaco, monospace",
128
+ "--fs_base": "16px",
129
+ "--fs_small": "calc(0.6 * var(--fs_base))",
130
+ "--fs_large": "calc(1.5 * var(--fs_base))",
131
+ "--fs_h6": "var(--fs_base)",
132
+ "--fs_h5": "calc(1.25 * var(--fs_base))",
133
+ "--fs_h4": "calc(1.5 * var(--fs_base))",
134
+ "--fs_h3": "calc(1.75 * var(--fs_base))",
135
+ "--fs_h2": "calc(2 * var(--fs_base))",
136
+ "--fs_h1": "calc(2.5 * var(--fs_base))",
137
+ "--fw_base": "400",
138
+ "--fw_bold": "700",
139
+ "--spacer": "1rem",
140
+ "--spacer_h": "calc(0.5 * var(--spacer))",
141
+ "--spacer_q": "calc(0.25 * var(--spacer))",
142
+ "--line-height": "1.35em",
143
+ "--container_width": "90rem",
144
+ "--animation_ms": "256ms",
145
+ "--radius": "0.25rem",
146
+ "--link_decoration": "underline",
147
+ "--input_padding": "var(--spacer_h) var(--spacer)",
148
+ "--input_border_width": "1px",
149
+ "--btn_padding": "var(--spacer_h) var(--spacer)",
150
+ "--c_bg": { light: "rgb(249, 249, 249)", dark: "rgb(51, 51, 51)" },
151
+ "--c_bg__inv": { light: "rgb(51, 51, 51)", dark: "rgb(249, 249, 249)" },
152
+ "--c_bg__alt": { light: "rgb(238, 238, 238)", dark: "rgb(34, 34, 34)" },
153
+ "--c_overscroll": { light: "rgb(255, 255, 255)", dark: "rgb(0, 0, 0)" },
154
+ "--c_border": { light: "rgb(204, 204, 204)", dark: "rgb(119, 119, 119)" },
155
+ "--c_border__inv": { light: "var(--d_c_bg_border)", dark: "rgb(204, 204, 204)" },
156
+ "--c_primary": "rgb(51, 102, 255)",
157
+ "--c_primary__hover": "rgb(17, 68, 221)",
158
+ "--c_secondary": "rgb(153, 51, 255)",
159
+ "--c_secondary__hover": "rgb(119, 17, 221)",
160
+ "--c_success": "rgb(0, 136, 0)",
161
+ "--c_success__hover": "rgb(0, 102, 0)",
162
+ "--c_warning": "rgb(255, 102, 0)",
163
+ "--c_warning__hover": "rgb(221, 68, 0)",
164
+ "--c_danger": "rgb(255, 0, 51)",
165
+ "--c_danger__hover": "rgb(221, 0, 17)",
166
+ "--c_input_accent": "rgb(51, 102, 255)",
167
+ "--c_input_border": "var(--c_border)",
168
+ "--c_highlight": { light: "rgba(41, 100, 210, 0.25)", dark: "rgba(0, 89, 255, 0.25)" },
169
+ "--tc": { light: "rgba(0, 0, 0, 0.93)", dark: "rgba(255, 255, 255, 0.93)" },
170
+ "--tc_dark": { light: "rgba(0, 0, 0, 0.93)", dark: "rgba(0, 0, 0, 0.93)" },
171
+ "--tc_light": { light: "rgba(255, 255, 255, 0.93)", dark: "rgba(255, 255, 255, 0.93)" },
172
+ "--tc_inv": { light: "rgba(255, 255, 255, 0.93)", dark: "rgba(0, 0, 0, 0.93)" },
173
+ "--tc_muted": { light: "rgba(0, 0, 0, 0.5)", dark: "rgba(255, 255, 255, 0.5)" },
174
+ "--tc_on_primary": { light: "rgba(255, 255, 255, 0.93)", dark: "rgba(255, 255, 255, 0.93)" },
175
+ "--tc_on_secondary": { light: "rgba(255, 255, 255, 0.93)", dark: "rgba(255, 255, 255, 0.93)" },
176
+ "--tc_on_success": { light: "rgba(255, 255, 255, 0.93)", dark: "rgba(255, 255, 255, 0.93)" },
177
+ "--tc_on_warning": { light: "rgba(255, 255, 255, 0.93)", dark: "rgba(255, 255, 255, 0.93)" },
178
+ "--tc_on_danger": { light: "rgba(255, 255, 255, 0.93)", dark: "rgba(255, 255, 255, 0.93)" },
179
+ "--c_overlay": "rgba(0, 0, 0, 0.5)",
180
+ "--tc_primary": { light: "#36f", dark: "rgb(138, 180, 248)" },
181
+ "--tc_secondary": { light: "#93f", dark: "rgb(187, 102, 255)" },
182
+ "--tc_success": { light: "#080", dark: "rgb(102, 187, 102)" },
183
+ "--tc_warning": { light: "#f60", dark: "rgb(255, 153, 51)" },
184
+ "--tc_danger": { light: "#f03", dark: "rgb(255, 85, 119)" },
185
+ "--btn_box_shadow": "0 0 0 transparent",
186
+ "--btn_box_shadow__hover": "0 0 0 transparent",
187
+ "--btn_border": "transparent",
188
+ "--btn_bg": { light: "rgb(221, 221, 221)", dark: "rgb(170, 170, 170)" },
189
+ "--btn_bg__hover": { light: "rgb(204, 204, 204)", dark: "rgb(187, 187, 187)" },
190
+ "--btn_tc": { light: "rgba(0, 0, 0, 0.93)", dark: "rgba(0, 0, 0, 0.93)" },
191
+ "--btn_transparent__hover": { light: "rgba(0, 0, 0, 0.05)", dark: "rgba(255, 255, 255, 0.05)" },
192
+ "--tc_link": "var(--tc_primary)",
193
+ "--tc_link__hover": "var(--tc_secondary)",
194
+ "--tc_link__inv": "var(--tc_primary__inv)",
195
+ "--tc_link__inv__hover": "var(--tc_secondary__inv)",
196
+ "--focus_shadow": "0 0 2px 2px var(--c_primary)",
197
+ "--focus_shadow_on_primary": "0 0 2px 2px var(--tc_on_primary)",
198
+ "--input_bg": { light: "white", dark: "var(--c_bg__alt)" },
199
+ "--input_tc": { light: "rgba(0, 0, 0, 0.93)", dark: "var(--tc)" },
200
+ "--shadow_color": "black",
201
+ "--shadow_size": "1px",
202
+ "--shadow_base_opacity": "0.08",
203
+ "--shadow_opacity_step": "0.01",
204
+ "--date_picker_icon_filter": { light: "invert(0)", dark: "invert(1)" }
205
+ };
206
+
207
+ /*
208
+ Current Theme State
209
+ */
210
+ const currentTheme = { light: {}, dark: {} };
211
+ const editingTheme = { value: theme.getCalculated() };
212
+ const availableProperties = Object.keys(defaultTheme);
213
+
214
+ /*
215
+ Initialize Theme State
216
+ */
217
+ Object.entries(defaultTheme).forEach(([prop, value]) => {
218
+ if (typeof value === 'object' && value.light) {
219
+ currentTheme.light[prop] = value.light;
220
+ currentTheme.dark[prop] = value.dark;
221
+ } else {
222
+ currentTheme.light[prop] = value;
223
+ currentTheme.dark[prop] = value;
224
+ }
225
+ });
226
+
227
+ /*
228
+ Render Theme Inputs
229
+ */
230
+ const themeInputsContainer = document.getElementById('themeInputs');
231
+
232
+ const propertyCategories = [
233
+ {
234
+ title: 'Theme Colors',
235
+ props: [
236
+ '--c_primary',
237
+ '--c_secondary',
238
+ '--c_success',
239
+ '--c_warning',
240
+ '--c_danger'
241
+ ]
242
+ },
243
+ {
244
+ title: 'Background & Neutral Colors',
245
+ props: [
246
+ '--c_bg', '--c_overscroll', '--c_border', '--c_overlay'
247
+ ]
248
+ },
249
+ {
250
+ title: 'Button Styles',
251
+ props: [
252
+ '--btn_bg', '--btn_tc', '--btn_transparent__hover',
253
+ '--btn_border', '--btn_box_shadow', '--btn_box_shadow__hover'
254
+ ]
255
+ },
256
+ {
257
+ title: 'Input Styles',
258
+ props: [
259
+ '--input_bg', '--input_tc'
260
+ ]
261
+ },
262
+ {
263
+ title: 'Focus & Effects',
264
+ props: [
265
+ '--focus_shadow', '--focus_shadow_on_primary',
266
+ '--date_picker_icon_filter'
267
+ ]
268
+ },
269
+ {
270
+ title: 'Elevation & Shadows',
271
+ props: [
272
+ '--shadow_color',
273
+ '--shadow_size',
274
+ '--shadow_base_opacity',
275
+ '--shadow_opacity_step'
276
+ ]
277
+ },
278
+ {
279
+ title: 'Typography',
280
+ props: [
281
+ '--ff_body', '--ff_heading', '--ff_mono',
282
+ '--fs_base', '--fs_small', '--fs_large',
283
+ '--fs_h1', '--fs_h2', '--fs_h3', '--fs_h4', '--fs_h5', '--fs_h6',
284
+ '--fw_base', '--fw_bold'
285
+ ]
286
+ },
287
+ {
288
+ title: 'Spacing & Layout',
289
+ props: [
290
+ '--spacer', '--line-height', '--container_width'
291
+ ]
292
+ },
293
+ {
294
+ title: 'Effects & Animation',
295
+ props: [
296
+ '--animation_ms', '--radius', '--link_decoration',
297
+ '--input_padding', '--input_border_width', '--btn_padding'
298
+ ]
299
+ }
300
+ ];
301
+
302
+ const isColorValue = (value) => {
303
+ if (!value || typeof value !== 'string') return false;
304
+ if (value.startsWith('var(')) return false;
305
+ if (value.startsWith('calc(')) return false;
306
+ if (value.includes('px') || value.includes('rem') || value.includes('em')) return false;
307
+ if (value.includes('ms') || value.includes('s')) return false;
308
+ if (/^[\d.]+$/.test(value)) return false;
309
+ if (value.startsWith('"') || value.startsWith("'")) return false;
310
+ return value.startsWith('#') ||
311
+ value.startsWith('rgb') ||
312
+ value.startsWith('hsl') ||
313
+ value.startsWith('hwb') ||
314
+ value.startsWith('lab') ||
315
+ value.startsWith('lch') ||
316
+ value.startsWith('oklab') ||
317
+ value.startsWith('oklch') ||
318
+ value === 'transparent' ||
319
+ value.includes('light-dark');
320
+ };
321
+
322
+ const normalizeColor = (color) => {
323
+ if (!color || typeof color !== 'string') return color;
324
+ if (color.startsWith('var(')) return color;
325
+ if (!isColorValue(color)) return color;
326
+ const testEl = document.createElement('div');
327
+ testEl.style.color = color;
328
+ document.body.appendChild(testEl);
329
+ const computed = window.getComputedStyle(testEl).color;
330
+ document.body.removeChild(testEl);
331
+ return computed || color;
332
+ };
333
+
334
+ const colorsEqual = (color1, color2) => {
335
+ if (color1 === color2) return true;
336
+ if (!isColorValue(color1) || !isColorValue(color2)) return color1 === color2;
337
+ return normalizeColor(color1) === normalizeColor(color2);
338
+ };
339
+
340
+ const getComputedValue = (varName) => {
341
+ const testEl = document.createElement('div');
342
+ testEl.style.color = varName;
343
+ document.body.appendChild(testEl);
344
+ const computed = window.getComputedStyle(testEl).color;
345
+ document.body.removeChild(testEl);
346
+ return computed;
347
+ };
348
+
349
+ const isColorProperty = (prop) => {
350
+ return prop.startsWith('--c_') || prop.startsWith('--tc_') || prop === '--tc' || (prop.startsWith('--btn_') && !prop.includes('shadow'));
351
+ };
352
+
353
+ const isFontWeightProperty = (prop) => {
354
+ return prop.startsWith('--fw_');
355
+ };
356
+
357
+ const propToLabel = (prop) => {
358
+ const withoutPrefix = prop.replace(/^--/, '');
359
+ const words = withoutPrefix.split('_').filter(word => {
360
+ return word !== 'c' && word !== 'tc';
361
+ }).map(word => {
362
+ const abbrevMap = {
363
+ 'bg': 'Background', 'inv': 'Inverse',
364
+ 'ff': 'Font Family', 'fs': 'Font Size', 'fw': 'Font Weight',
365
+ 'btn': 'Button', 'h1': 'H1', 'h2': 'H2', 'h3': 'H3', 'h4': 'H4', 'h5': 'H5', 'h6': 'H6',
366
+ 'ms': 'Duration', 'h': 'Half', 'q': 'Quarter'
367
+ };
368
+ if (abbrevMap[word]) return abbrevMap[word];
369
+ return word.charAt(0).toUpperCase() + word.slice(1);
370
+ });
371
+ return words.join(' ');
372
+ };
373
+
374
+ const titleToId = title => title.toLowerCase().replace(/\s*&\s*/g, '-').replace(/\s+/g, '-');
375
+
376
+ propertyCategories.forEach(({ title, props }) => {
377
+ const section = document.createElement('div');
378
+ section.className = 'mb';
379
+ section.id = titleToId(title);
380
+
381
+ const heading = document.createElement('h4');
382
+ heading.className = 'mb-h';
383
+ heading.textContent = title;
384
+ section.appendChild(heading);
385
+
386
+ props.forEach(prop => {
387
+ if (defaultTheme[prop] === undefined) return;
388
+
389
+ const value = currentTheme[editingTheme.value][prop];
390
+
391
+ if (isColorProperty(prop)) {
392
+ const input = document.createElement('k-theme-property-input');
393
+ input.setAttribute('prop-name', prop);
394
+ input.setAttribute('label', propToLabel(prop));
395
+ input.setAttribute('available-properties', JSON.stringify(availableProperties));
396
+ input.setAttribute('value', value);
397
+
398
+ if (isColorValue(value)) {
399
+ input.setAttribute('mode', 'color');
400
+ } else {
401
+ input.setAttribute('mode', 'var');
402
+ if (value.startsWith('var(')) {
403
+ const computedColor = getComputedValue(value);
404
+ if (computedColor && computedColor !== 'rgba(0, 0, 0, 0)' && !computedColor.includes('NaN')) {
405
+ input.setAttribute('initial-color', computedColor);
406
+ }
407
+ }
408
+ }
409
+
410
+ input.className = 'mb';
411
+ section.appendChild(input);
412
+ } else if (isFontWeightProperty(prop)) {
413
+ const wrapper = document.createElement('div');
414
+ wrapper.className = 'mb';
415
+
416
+ const label = document.createElement('label');
417
+ label.innerHTML = `${propToLabel(prop)} <small class="tc-muted"><code>${prop}</code></small>`;
418
+
419
+ const input = document.createElement('input');
420
+ input.setAttribute('type', 'number');
421
+ input.setAttribute('data-prop-name', prop);
422
+ input.setAttribute('min', '100');
423
+ input.setAttribute('max', '900');
424
+ input.setAttribute('step', '100');
425
+ input.value = value;
426
+ input.style.fontFamily = 'var(--ff_mono)';
427
+ input.style.fontSize = '0.875rem';
428
+
429
+ wrapper.appendChild(label);
430
+ wrapper.appendChild(input);
431
+ section.appendChild(wrapper);
432
+ } else {
433
+ const wrapper = document.createElement('div');
434
+ wrapper.className = 'mb';
435
+
436
+ const label = document.createElement('label');
437
+ label.innerHTML = `${propToLabel(prop)} <small class="tc-muted"><code>${prop}</code></small>`;
438
+
439
+ const input = document.createElement('input');
440
+ input.setAttribute('type', 'text');
441
+ input.setAttribute('data-prop-name', prop);
442
+ input.value = value;
443
+ input.style.fontFamily = 'var(--ff_mono)';
444
+ input.style.fontSize = '0.875rem';
445
+
446
+ wrapper.appendChild(label);
447
+ wrapper.appendChild(input);
448
+ section.appendChild(wrapper);
449
+ }
450
+ });
451
+
452
+ themeInputsContainer.appendChild(section);
453
+ });
454
+
455
+ /*
456
+ Handle Theme Switch
457
+ */
458
+ const editingThemeSelect = document.getElementById('editingThemeSelect');
459
+ editingThemeSelect.value = editingTheme.value;
460
+
461
+ const updateInputValues = () => {
462
+ document.querySelectorAll('k-theme-property-input').forEach(input => {
463
+ const prop = input.getAttribute('prop-name');
464
+ const value = currentTheme[editingTheme.value][prop];
465
+ input.setAttribute('value', value);
466
+ });
467
+
468
+ document.querySelectorAll('input[data-prop-name]').forEach(input => {
469
+ const prop = input.getAttribute('data-prop-name');
470
+ const value = currentTheme[editingTheme.value][prop];
471
+ input.value = value;
472
+ });
473
+ };
474
+
475
+ editingThemeSelect.addEventListener('change', () => {
476
+ editingTheme.value = editingThemeSelect.value;
477
+ theme.set(editingTheme.value);
478
+ updateInputValues();
479
+ });
480
+
481
+ theme.subscribe(() => {
482
+ editingTheme.value = theme.getCalculated();
483
+ editingThemeSelect.value = editingTheme.value;
484
+ updateInputValues();
485
+ });
486
+
487
+ /*
488
+ Change Tracking
489
+ */
490
+ const getThemeDiff = () => {
491
+ const diff = {};
492
+
493
+ ['light', 'dark'].forEach(themeMode => {
494
+ Object.entries(currentTheme[themeMode]).forEach(([prop, value]) => {
495
+ const defaultValue = typeof defaultTheme[prop] === 'object'
496
+ ? defaultTheme[prop][themeMode]
497
+ : defaultTheme[prop];
498
+
499
+ const isEqual = isColorProperty(prop)
500
+ ? colorsEqual(value, defaultValue)
501
+ : value === defaultValue;
502
+
503
+ if (!isEqual) {
504
+ if (!diff[prop]) diff[prop] = {};
505
+ diff[prop][themeMode] = value;
506
+ }
507
+ });
508
+ });
509
+
510
+ return diff;
511
+ };
512
+
513
+ const handlePropertyChange = (prop, value) => {
514
+ currentTheme[editingTheme.value][prop] = value;
515
+
516
+ const diff = getThemeDiff();
517
+ const css = generateThemeCSS(diff);
518
+ applyThemeCSS(css);
519
+ console.log('Theme CSS:', css);
520
+ };
521
+
522
+ /*
523
+ CSS Generation
524
+ */
525
+ const generateThemeCSS = (diff) => {
526
+ if (Object.keys(diff).length === 0) return '';
527
+
528
+ const cssLines = [];
529
+
530
+ Object.entries(diff).forEach(([prop, value]) => {
531
+ if (typeof value === 'object') {
532
+ // Property has light/dark variants
533
+ const hasLight = value.light !== undefined;
534
+ const hasDark = value.dark !== undefined;
535
+
536
+ if (hasLight && hasDark) {
537
+ // Both themes changed - use light-dark()
538
+ cssLines.push(` ${prop}: light-dark(${value.light}, ${value.dark});`);
539
+ } else if (hasLight) {
540
+ // Only light changed - need to get dark from current or default
541
+ const darkValue = currentTheme.dark[prop] !== undefined
542
+ ? currentTheme.dark[prop]
543
+ : (typeof defaultTheme[prop] === 'object' ? defaultTheme[prop].dark : defaultTheme[prop]);
544
+ cssLines.push(` ${prop}: light-dark(${value.light}, ${darkValue});`);
545
+ } else if (hasDark) {
546
+ // Only dark changed - need to get light from current or default
547
+ const lightValue = currentTheme.light[prop] !== undefined
548
+ ? currentTheme.light[prop]
549
+ : (typeof defaultTheme[prop] === 'object' ? defaultTheme[prop].light : defaultTheme[prop]);
550
+ cssLines.push(` ${prop}: light-dark(${lightValue}, ${value.dark});`);
551
+ }
552
+ } else {
553
+ // Single value (non-themed property)
554
+ cssLines.push(` ${prop}: ${value};`);
555
+ }
556
+ });
557
+
558
+ if (cssLines.length === 0) return '';
559
+
560
+ return `:root {\n${cssLines.join('\n')}\n}`;
561
+ };
562
+
563
+ const applyThemeCSS = (css) => {
564
+ let styleEl = document.getElementById('custom-theme');
565
+ if (!styleEl) {
566
+ styleEl = document.createElement('style');
567
+ styleEl.id = 'custom-theme';
568
+ document.head.appendChild(styleEl);
569
+ }
570
+ styleEl.textContent = css;
571
+
572
+ // Save to context for download
573
+ setContext('customThemeCSS', css);
574
+ };
575
+
576
+ /*
577
+ Event Listeners for Changes
578
+ */
579
+ document.addEventListener('value-change', (e) => {
580
+ const component = e.target;
581
+ if (component.tagName === 'K-THEME-PROPERTY-INPUT') {
582
+ const { propName, value } = e.detail;
583
+ handlePropertyChange(propName, value);
584
+ }
585
+ });
586
+
587
+ document.querySelectorAll('input[data-prop-name]').forEach(input => {
588
+ input.addEventListener('input', (e) => {
589
+ const prop = e.target.getAttribute('data-prop-name');
590
+ const value = e.target.value;
591
+ handlePropertyChange(prop, value);
592
+ });
593
+ });
594
+
595
+ /*
596
+ Setup color picker change listeners after components render
597
+ Listen directly to native color inputs since k-color-picker doesn't always re-dispatch events
598
+ */
599
+ const attachColorPickerListeners = () => {
600
+ document.querySelectorAll('k-theme-property-input').forEach(input => {
601
+ const colorPicker = input.shadowRoot?.querySelector('k-color-picker');
602
+ if (!colorPicker) return;
603
+
604
+ // Get the native color input inside k-color-picker
605
+ const nativeColorInput = colorPicker.shadowRoot?.querySelector('input[type="color"]');
606
+
607
+ if (nativeColorInput && !nativeColorInput._hasPageListener) {
608
+ nativeColorInput._hasPageListener = true;
609
+
610
+ const handleNativeChange = () => {
611
+ const propName = input.getAttribute('prop-name');
612
+ const newValue = nativeColorInput.value;
613
+ colorPicker.value = newValue;
614
+ input.value = newValue;
615
+ handlePropertyChange(propName, newValue);
616
+ };
617
+
618
+ // Listen to both input (while dragging) and change (on close)
619
+ nativeColorInput.addEventListener('input', handleNativeChange);
620
+ nativeColorInput.addEventListener('change', handleNativeChange);
621
+ }
622
+
623
+ // Also listen to k-color-picker events for text input changes
624
+ if (colorPicker && !colorPicker._hasPageListener) {
625
+ colorPicker._hasPageListener = true;
626
+ const handleChange = () => {
627
+ const propName = input.getAttribute('prop-name');
628
+ const newValue = colorPicker.value;
629
+ input.value = newValue;
630
+ handlePropertyChange(propName, newValue);
631
+ };
632
+ colorPicker.addEventListener('change', handleChange);
633
+ colorPicker.addEventListener('input', handleChange);
634
+ }
635
+ });
636
+ };
637
+
638
+ // Run after initial render and periodically to catch dynamically added components
639
+ setTimeout(attachColorPickerListeners, 100);
640
+ setTimeout(attachColorPickerListeners, 500);
641
+ setTimeout(attachColorPickerListeners, 1000);
642
+
643
+ /*
644
+ Download Theme Button
645
+ */
646
+ const downloadThemeBtn = document.getElementById('downloadTheme');
647
+ downloadThemeBtn.addEventListener('click', () => {
648
+ const css = getContext('customThemeCSS');
649
+
650
+ if (!css || css.trim() === '') {
651
+ alert('No theme changes to download. Make some changes first!');
652
+ return;
653
+ }
654
+
655
+ // Create blob and download link
656
+ const blob = new Blob([css], { type: 'text/css' });
657
+ const url = URL.createObjectURL(blob);
658
+ const a = document.createElement('a');
659
+ a.href = url;
660
+ a.download = 'kempo-theme.css';
661
+ document.body.appendChild(a);
662
+ a.click();
663
+ document.body.removeChild(a);
664
+ URL.revokeObjectURL(url);
665
+ });
666
+
667
+ /*
668
+ Upload Theme Button
669
+ */
670
+ const uploadThemeBtn = document.getElementById('uploadThemeBtn');
671
+ uploadThemeBtn.addEventListener('click', () => {
672
+ const dialogContent = document.createElement('div');
673
+ dialogContent.className = 'p';
674
+ dialogContent.innerHTML = `
675
+ <p class="mb">Upload a previously downloaded theme CSS file to restore your theme settings.</p>
676
+ <input type="file" id="themeFileInput" accept=".css" class="mb" />
677
+ <div id="uploadPreview" class="mb" style="display: none;">
678
+ <label><strong>Preview:</strong></label>
679
+ <pre style="max-height: 200px; overflow: auto; background: var(--c_bg__alt); padding: var(--spacer_h); border-radius: var(--radius); font-size: 0.8rem;"><code id="uploadPreviewCode"></code></pre>
680
+ </div>
681
+ <p id="uploadError" class="tc-danger mb" style="display: none;"></p>
682
+ `;
683
+
684
+ let parsedTheme = null;
685
+
686
+ const dialog = window.KDialog.create(dialogContent, {
687
+ title: 'Upload Theme',
688
+ confirmText: 'Apply Theme',
689
+ confirmClasses: 'primary ml',
690
+ cancelText: 'Cancel',
691
+ width: '32rem',
692
+ confirmAction: (e) => {
693
+ if (!parsedTheme) {
694
+ e.preventDefault();
695
+ const errorEl = dialogContent.querySelector('#uploadError');
696
+ errorEl.textContent = 'Please select a valid theme file first.';
697
+ errorEl.style.display = 'block';
698
+ return;
699
+ }
700
+ applyUploadedTheme(parsedTheme);
701
+ }
702
+ });
703
+
704
+ // Handle file selection
705
+ const fileInput = dialogContent.querySelector('#themeFileInput');
706
+ fileInput.addEventListener('change', (e) => {
707
+ const file = e.target.files[0];
708
+ if (!file) return;
709
+
710
+ const reader = new FileReader();
711
+ reader.onload = (evt) => {
712
+ const css = evt.target.result;
713
+ const previewEl = dialogContent.querySelector('#uploadPreview');
714
+ const previewCode = dialogContent.querySelector('#uploadPreviewCode');
715
+ const errorEl = dialogContent.querySelector('#uploadError');
716
+
717
+ try {
718
+ parsedTheme = parseThemeCSS(css);
719
+ if (Object.keys(parsedTheme).length === 0) {
720
+ throw new Error('No valid CSS custom properties found in file.');
721
+ }
722
+ previewCode.textContent = css;
723
+ previewEl.style.display = 'block';
724
+ errorEl.style.display = 'none';
725
+ } catch (err) {
726
+ parsedTheme = null;
727
+ previewEl.style.display = 'none';
728
+ errorEl.textContent = err.message;
729
+ errorEl.style.display = 'block';
730
+ }
731
+ };
732
+ reader.readAsText(file);
733
+ });
734
+ });
735
+
736
+ /*
737
+ Parse uploaded CSS
738
+ */
739
+ const parseThemeCSS = (css) => {
740
+ const result = {};
741
+
742
+ // Remove comments
743
+ css = css.replace(/\/\*[\s\S]*?\*\//g, '');
744
+
745
+ // Match :root block
746
+ const rootMatch = css.match(/:root\s*\{([^}]+)\}/);
747
+ if (!rootMatch) throw new Error('No :root block found in CSS file.');
748
+
749
+ const declarations = rootMatch[1];
750
+
751
+ // Match CSS custom properties
752
+ const propRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
753
+ let match;
754
+
755
+ while ((match = propRegex.exec(declarations)) !== null) {
756
+ const [, propName, value] = match;
757
+ const trimmedValue = value.trim();
758
+
759
+ // Check if it's a light-dark() value - need to handle nested parentheses
760
+ if (trimmedValue.startsWith('light-dark(')) {
761
+ // Find the comma that separates light and dark values
762
+ // by tracking parenthesis depth
763
+ const inner = trimmedValue.slice(11, -1); // Remove "light-dark(" and ")"
764
+ let depth = 0;
765
+ let splitIndex = -1;
766
+
767
+ for (let i = 0; i < inner.length; i++) {
768
+ if (inner[i] === '(') depth++;
769
+ else if (inner[i] === ')') depth--;
770
+ else if (inner[i] === ',' && depth === 0) {
771
+ splitIndex = i;
772
+ break;
773
+ }
774
+ }
775
+
776
+ if (splitIndex !== -1) {
777
+ result[propName] = {
778
+ light: inner.slice(0, splitIndex).trim(),
779
+ dark: inner.slice(splitIndex + 1).trim()
780
+ };
781
+ } else {
782
+ // Fallback if no comma found at depth 0
783
+ result[propName] = {
784
+ light: trimmedValue,
785
+ dark: trimmedValue
786
+ };
787
+ }
788
+ } else {
789
+ // Single value - apply to both themes
790
+ result[propName] = {
791
+ light: trimmedValue,
792
+ dark: trimmedValue
793
+ };
794
+ }
795
+ }
796
+
797
+ return result;
798
+ };
799
+
800
+ /*
801
+ Apply uploaded theme
802
+ */
803
+ const applyUploadedTheme = (parsedTheme) => {
804
+ // Reset currentTheme to defaults first
805
+ Object.keys(defaultTheme).forEach(prop => {
806
+ const defaultValue = defaultTheme[prop];
807
+ if (typeof defaultValue === 'object') {
808
+ currentTheme.light[prop] = defaultValue.light;
809
+ currentTheme.dark[prop] = defaultValue.dark;
810
+ } else {
811
+ currentTheme.light[prop] = defaultValue;
812
+ currentTheme.dark[prop] = defaultValue;
813
+ }
814
+ });
815
+
816
+ // Only apply values that differ from defaults
817
+ Object.entries(parsedTheme).forEach(([prop, value]) => {
818
+ if (currentTheme.light[prop] !== undefined) {
819
+ const defaultValue = defaultTheme[prop];
820
+ const defaultLight = typeof defaultValue === 'object' ? defaultValue.light : defaultValue;
821
+ const defaultDark = typeof defaultValue === 'object' ? defaultValue.dark : defaultValue;
822
+
823
+ // Only set if different from default
824
+ if (value.light !== defaultLight) {
825
+ currentTheme.light[prop] = value.light;
826
+ }
827
+ if (value.dark !== defaultDark) {
828
+ currentTheme.dark[prop] = value.dark;
829
+ }
830
+ }
831
+ });
832
+
833
+ // Update all input values
834
+ updateInputValues();
835
+
836
+ // Regenerate and apply CSS
837
+ const diff = getThemeDiff();
838
+ const css = generateThemeCSS(diff);
839
+ applyThemeCSS(css);
840
+
841
+ console.log('Uploaded theme applied, diff:', diff);
842
+ };
843
+
844
+ // Make Dialog available after component loads
845
+ customElements.whenDefined('k-dialog').then(() => {
846
+ window.KDialog = customElements.get('k-dialog');
847
+ });
848
+ </script>
849
+ </content>
850
+ </page>