kempo-css 1.0.7 → 1.0.9

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