kempo-ui 0.3.14 → 0.3.16

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 (82) hide show
  1. package/dist/components/Combobox.js +97 -0
  2. package/dist/components/Option.js +1 -0
  3. package/dist/components/ThemeSwitcher.js +77 -14
  4. package/docs/components/accordion.html +282 -94
  5. package/docs/components/aside.html +282 -94
  6. package/docs/components/card.html +282 -94
  7. package/docs/components/code-editor.html +282 -94
  8. package/docs/components/color-picker.html +282 -94
  9. package/docs/components/combobox.html +798 -0
  10. package/docs/components/content-slider.html +282 -94
  11. package/docs/components/context.html +282 -94
  12. package/docs/components/dialog.html +282 -94
  13. package/docs/components/dropdown.html +282 -94
  14. package/docs/components/filter-list.html +282 -94
  15. package/docs/components/focus-capture.html +282 -94
  16. package/docs/components/html-editor.html +282 -94
  17. package/docs/components/hybrid-component.html +282 -94
  18. package/docs/components/icon.html +282 -94
  19. package/docs/components/import.html +282 -94
  20. package/docs/components/light-component.html +282 -94
  21. package/docs/components/nav-spacer.html +282 -94
  22. package/docs/components/nav.html +282 -94
  23. package/docs/components/photo-viewer.html +282 -94
  24. package/docs/components/resize.html +282 -94
  25. package/docs/components/shadow-component.html +282 -94
  26. package/docs/components/show-more.html +282 -94
  27. package/docs/components/sortable.html +282 -94
  28. package/docs/components/spinner.html +282 -94
  29. package/docs/components/split.html +282 -94
  30. package/docs/components/table.html +282 -94
  31. package/docs/components/tableControls.html +282 -94
  32. package/docs/components/tableCustomFields.html +282 -94
  33. package/docs/components/tableFetchRecords.html +282 -94
  34. package/docs/components/tableFieldSortHide.html +282 -94
  35. package/docs/components/tablePagination.html +282 -94
  36. package/docs/components/tablePlaceholder.html +282 -94
  37. package/docs/components/tableRecordEditing.html +282 -94
  38. package/docs/components/tableRecordFiltering.html +282 -94
  39. package/docs/components/tableRecordHiding.html +282 -94
  40. package/docs/components/tableRecordSearching.html +282 -94
  41. package/docs/components/tableRecordSelection.html +282 -94
  42. package/docs/components/tableRowControls.html +282 -94
  43. package/docs/components/tableServerSync.html +282 -94
  44. package/docs/components/tableSorting.html +282 -94
  45. package/docs/components/tabs.html +282 -94
  46. package/docs/components/tags.html +282 -94
  47. package/docs/components/theme-select.html +282 -94
  48. package/docs/components/theme-switcher-aside.html +47 -0
  49. package/docs/components/theme-switcher.html +441 -101
  50. package/docs/components/timestamp.html +282 -94
  51. package/docs/components/toast.html +282 -94
  52. package/docs/components/toggle.html +282 -94
  53. package/docs/components/tree.html +282 -94
  54. package/docs/index.html +288 -94
  55. package/docs/src/components/Combobox.js +97 -0
  56. package/docs/src/components/Option.js +1 -0
  57. package/docs/src/components/ThemeSwitcher.js +77 -14
  58. package/docs/utils/context.html +282 -94
  59. package/docs/utils/cookie.html +282 -94
  60. package/docs/utils/debounce.html +282 -94
  61. package/docs/utils/drag.html +282 -94
  62. package/docs/utils/elevation.html +282 -94
  63. package/docs/utils/formatTimestamp.html +282 -94
  64. package/docs/utils/object.html +282 -94
  65. package/docs/utils/propConverters.html +282 -94
  66. package/docs/utils/string.html +282 -94
  67. package/docs/utils/theme.html +282 -94
  68. package/docs/utils/toTitleCase.html +282 -94
  69. package/docs/utils/type.html +282 -94
  70. package/docs/utils/wait.html +282 -94
  71. package/docs-src/components/combobox.page.html +358 -0
  72. package/docs-src/components/theme-switcher-aside.page.html +35 -0
  73. package/docs-src/components/theme-switcher.page.html +159 -7
  74. package/docs-src/index.page.html +6 -0
  75. package/docs-src/nav.fragment.html +283 -95
  76. package/llms.txt +2 -1
  77. package/package.json +1 -1
  78. package/src/components/Combobox.js +338 -0
  79. package/src/components/Option.js +19 -0
  80. package/src/components/ThemeSwitcher.js +171 -24
  81. package/tests/components/Combobox.browser-test.js +790 -0
  82. package/tests/components/ThemeSwitcher.browser-test.js +361 -33
@@ -0,0 +1,338 @@
1
+ import { html, css } from '../lit-all.min.js';
2
+ import ShadowComponent from './ShadowComponent.js';
3
+ import debounce from '../utils/debounce.js';
4
+ import './Option.js';
5
+
6
+ export default class Combobox extends ShadowComponent {
7
+ static formAssociated = true;
8
+
9
+ static properties = {
10
+ value: { type: String, reflect: true },
11
+ name: { type: String, reflect: true },
12
+ placeholder: { type: String },
13
+ opened: { type: Boolean, reflect: true },
14
+ searching: { type: Boolean, reflect: true },
15
+ required: { type: Boolean, reflect: true },
16
+ requireMatch: { type: Boolean, reflect: true, attribute: 'require-match' },
17
+ disabled: { type: Boolean, reflect: true },
18
+ debounceMs: { type: Number, attribute: 'debounce-ms' },
19
+ maxVisible: { type: Number, attribute: 'max-visible' }
20
+ };
21
+
22
+ #focusedIndex = -1;
23
+ #options = [];
24
+
25
+ /*
26
+ Lifecycle Callbacks
27
+ */
28
+ constructor() {
29
+ super();
30
+ this.internals = this.attachInternals();
31
+ this.value = '';
32
+ this.name = '';
33
+ this.placeholder = '';
34
+ this.opened = false;
35
+ this.searching = false;
36
+ this.required = false;
37
+ this.requireMatch = false;
38
+ this.disabled = false;
39
+ this.debounceMs = 300;
40
+ this.maxVisible = 8;
41
+ this.#setupDebounce(300);
42
+ }
43
+
44
+ connectedCallback() {
45
+ super.connectedCallback();
46
+ document.addEventListener('click', this.handleDocumentClick);
47
+ this.#syncOptions();
48
+ }
49
+
50
+ disconnectedCallback() {
51
+ super.disconnectedCallback();
52
+ document.removeEventListener('click', this.handleDocumentClick);
53
+ }
54
+
55
+ childrenUpdated() {
56
+ this.#syncOptions();
57
+ }
58
+
59
+ updated(changedProperties) {
60
+ super.updated(changedProperties);
61
+ if(changedProperties.has('debounceMs')) {
62
+ this.#setupDebounce(this.debounceMs);
63
+ }
64
+ if(changedProperties.has('value') || changedProperties.has('required') || changedProperties.has('requireMatch')) {
65
+ this.#updateFormState();
66
+ }
67
+ }
68
+
69
+ formResetCallback() {
70
+ this.value = '';
71
+ this.#updateFormState();
72
+ }
73
+
74
+ formStateRestoreCallback(state) {
75
+ this.value = state;
76
+ }
77
+
78
+ /*
79
+ Event Handlers
80
+ */
81
+ handleInput = e => {
82
+ this.value = e.target.value;
83
+ this.opened = true;
84
+ this.#focusedIndex = -1;
85
+ this.#debouncedSearch(this.value);
86
+ };
87
+
88
+ handleKeydown = e => {
89
+ const items = this.#visibleOptions;
90
+ if(e.key === 'ArrowDown') {
91
+ e.preventDefault();
92
+ if(!this.opened) {
93
+ this.opened = true;
94
+ return;
95
+ }
96
+ this.#focusedIndex = Math.min(this.#focusedIndex + 1, items.length - 1);
97
+ this.requestUpdate();
98
+ this.#scrollFocusedIntoView();
99
+ } else if(e.key === 'ArrowUp') {
100
+ e.preventDefault();
101
+ this.#focusedIndex = Math.max(this.#focusedIndex - 1, 0);
102
+ this.requestUpdate();
103
+ this.#scrollFocusedIntoView();
104
+ } else if(e.key === 'Enter') {
105
+ e.preventDefault();
106
+ if(this.#focusedIndex >= 0 && this.#focusedIndex < items.length) {
107
+ this.#selectOption(items[this.#focusedIndex]);
108
+ }
109
+ } else if(e.key === 'Escape') {
110
+ e.preventDefault();
111
+ this.opened = false;
112
+ this.#focusedIndex = -1;
113
+ } else if(e.key === 'Tab') {
114
+ this.opened = false;
115
+ this.#focusedIndex = -1;
116
+ }
117
+ };
118
+
119
+ handleDocumentClick = e => {
120
+ if(!e.composedPath().includes(this)) {
121
+ this.opened = false;
122
+ this.#focusedIndex = -1;
123
+ }
124
+ };
125
+
126
+ handleFocus = () => {
127
+ this.opened = true;
128
+ };
129
+
130
+ handleOptionClick = e => {
131
+ const index = Number(e.currentTarget.dataset.index);
132
+ this.#selectOption(this.#visibleOptions[index]);
133
+ };
134
+
135
+ /*
136
+ Public Methods
137
+ */
138
+ setOptions(options) {
139
+ [...this.querySelectorAll('k-option')].forEach(o => o.remove());
140
+ options.forEach(opt => {
141
+ const el = document.createElement('k-option');
142
+ if(typeof opt === 'object') {
143
+ el.setAttribute('value', opt.value ?? opt.label);
144
+ el.textContent = opt.label;
145
+ } else {
146
+ el.textContent = opt;
147
+ }
148
+ this.appendChild(el);
149
+ });
150
+ return this;
151
+ }
152
+
153
+ clear() {
154
+ this.value = '';
155
+ this.opened = false;
156
+ this.#focusedIndex = -1;
157
+ this.#updateFormState();
158
+ return this;
159
+ }
160
+
161
+ /*
162
+ Private Methods
163
+ */
164
+ #setupDebounce(ms) {
165
+ this.#debouncedSearch = debounce(value => {
166
+ this.dispatchEvent(new CustomEvent('search', {
167
+ detail: { value },
168
+ bubbles: true,
169
+ composed: true
170
+ }));
171
+ }, ms);
172
+ }
173
+
174
+ #debouncedSearch = () => {};
175
+
176
+ #syncOptions() {
177
+ this.#options = [...this.querySelectorAll('k-option')].map(el => ({
178
+ label: el.label,
179
+ value: el.value
180
+ }));
181
+ this.requestUpdate();
182
+ }
183
+
184
+ #selectOption(option) {
185
+ this.value = option.label;
186
+ this.opened = false;
187
+ this.#focusedIndex = -1;
188
+ this.#updateFormState();
189
+ this.dispatchEvent(new CustomEvent('select', {
190
+ detail: { value: option.value, label: option.label },
191
+ bubbles: true,
192
+ composed: true
193
+ }));
194
+ this.dispatchEvent(new CustomEvent('change', {
195
+ bubbles: true
196
+ }));
197
+ }
198
+
199
+ #updateFormState() {
200
+ const matchedOption = this.#options.find(o => o.label === this.value);
201
+ this.internals.setFormValue(matchedOption ? matchedOption.value : this.value);
202
+ if(this.required && !this.value) {
203
+ this.internals.setValidity({ valueMissing: true }, 'Please select a value.', this.shadowRoot?.querySelector('input'));
204
+ } else if(this.requireMatch && this.value && !matchedOption) {
205
+ this.internals.setValidity({ customError: true }, 'Please select a valid option.', this.shadowRoot?.querySelector('input'));
206
+ } else {
207
+ this.internals.setValidity({});
208
+ }
209
+ }
210
+
211
+ #scrollFocusedIntoView() {
212
+ this.updateComplete.then(() => {
213
+ this.shadowRoot?.querySelector('.option.focused')?.scrollIntoView({ block: 'nearest' });
214
+ });
215
+ }
216
+
217
+ get #filteredOptions() {
218
+ const term = (this.value || '').toLowerCase();
219
+ if(!term) return this.#options;
220
+ return this.#options.filter(opt => opt.label.toLowerCase().includes(term));
221
+ }
222
+
223
+ get #visibleOptions() {
224
+ return this.#filteredOptions.slice(0, this.maxVisible);
225
+ }
226
+
227
+ /*
228
+ Rendering
229
+ */
230
+ render() {
231
+ const visible = this.#visibleOptions;
232
+ const hasMore = this.#filteredOptions.length > this.maxVisible;
233
+ return html`
234
+ <slot style="display:none"></slot>
235
+ <input
236
+ type="text"
237
+ .value=${this.value}
238
+ placeholder=${this.placeholder}
239
+ ?disabled=${this.disabled}
240
+ @input=${this.handleInput}
241
+ @keydown=${this.handleKeydown}
242
+ @focus=${this.handleFocus}
243
+ autocomplete="off"
244
+ />
245
+ ${this.opened ? html`
246
+ <div id="menu">
247
+ ${visible.map((opt, i) => html`
248
+ <div
249
+ class="option ${i === this.#focusedIndex ? 'focused' : ''}"
250
+ data-index=${i}
251
+ @click=${this.handleOptionClick}
252
+ >${opt.label}</div>
253
+ `)}
254
+ ${hasMore && !this.searching ? html`
255
+ <div class="more">${this.#filteredOptions.length - this.maxVisible} more...</div>
256
+ ` : ''}
257
+ ${visible.length === 0 && !this.searching ? html`
258
+ <div class="no-results">No matches</div>
259
+ ` : ''}
260
+ ${this.searching ? html`
261
+ <div class="searching">
262
+ <k-spinner size="xs"></k-spinner>
263
+ <span>Searching...</span>
264
+ </div>
265
+ ` : ''}
266
+ </div>
267
+ ` : ''}
268
+ `;
269
+ }
270
+
271
+ /*
272
+ Styles
273
+ */
274
+ static styles = css`
275
+ :host {
276
+ display: block;
277
+ position: relative;
278
+ }
279
+ :host([disabled]) {
280
+ opacity: 0.5;
281
+ pointer-events: none;
282
+ }
283
+ input {
284
+ width: 100%;
285
+ box-sizing: border-box;
286
+ padding: var(--spacer_h) var(--spacer);
287
+ border: 1px solid var(--c_border);
288
+ border-radius: var(--radius);
289
+ background: var(--c_bg);
290
+ color: var(--tc);
291
+ font: inherit;
292
+ outline: none;
293
+ transition: border-color var(--animation_ms);
294
+ }
295
+ input:focus {
296
+ border-color: var(--c_primary);
297
+ }
298
+ #menu {
299
+ position: absolute;
300
+ top: 100%;
301
+ left: 0;
302
+ right: 0;
303
+ z-index: 70;
304
+ max-height: 20rem;
305
+ overflow-y: auto;
306
+ background: var(--c_bg);
307
+ border: 1px solid var(--c_border);
308
+ border-radius: var(--radius);
309
+ box-shadow: var(--drop_shadow);
310
+ margin-top: 0.25rem;
311
+ }
312
+ .option {
313
+ padding: var(--spacer_h) var(--spacer);
314
+ cursor: pointer;
315
+ transition: background var(--animation_ms);
316
+ }
317
+ .option:hover,
318
+ .option.focused {
319
+ background: var(--c_bg__alt);
320
+ }
321
+ .no-results,
322
+ .more {
323
+ padding: var(--spacer_h) var(--spacer);
324
+ color: var(--tc_muted);
325
+ font-style: italic;
326
+ }
327
+ .searching {
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 0.5rem;
331
+ padding: var(--spacer_h) var(--spacer);
332
+ color: var(--tc_muted);
333
+ border-top: 1px solid var(--c_border);
334
+ }
335
+ `;
336
+ }
337
+
338
+ customElements.define('k-combobox', Combobox);
@@ -0,0 +1,19 @@
1
+ export default class Option extends HTMLElement {
2
+ static get observedAttributes() {
3
+ return ['value'];
4
+ }
5
+
6
+ get value() {
7
+ return this.getAttribute('value') ?? this.textContent.trim();
8
+ }
9
+
10
+ set value(v) {
11
+ this.setAttribute('value', v);
12
+ }
13
+
14
+ get label() {
15
+ return this.textContent.trim();
16
+ }
17
+ }
18
+
19
+ customElements.define('k-option', Option);
@@ -1,28 +1,90 @@
1
1
  import ShadowComponent from './ShadowComponent.js';
2
- import { html, css } from '../lit-all.min.js';
2
+ import { html, css, nothing } from '../lit-all.min.js';
3
3
  import theme from '../utils/theme.js';
4
4
  import './Icon.js';
5
5
 
6
+ const ICON_MAP = {
7
+ auto: 'mode-auto',
8
+ light: 'mode-light',
9
+ dark: 'mode-dark'
10
+ };
11
+
12
+ const parseOptions = str => str.split(',').map(s => s.trim()).filter(Boolean);
13
+ const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
14
+
6
15
  export default class ThemeSwitcher extends ShadowComponent {
16
+
7
17
  /* Properties */
8
18
  static properties = {
9
- currentTheme: { type: String, reflect: true, attribute: 'current-theme' }
19
+ currentTheme: { type: String, reflect: true, attribute: 'current-theme' },
20
+ mode: { type: String, reflect: true },
21
+ options: { type: String, reflect: true },
22
+ labels: { type: String, reflect: true },
23
+ resolvedMode: { type: String, state: true },
24
+ resolvedLabels: { type: Array, state: true }
10
25
  };
11
26
 
12
27
  constructor() {
13
28
  super();
14
29
  this.currentTheme = theme.get();
30
+ this.mode = 'auto';
31
+ this.options = 'light, auto, dark';
32
+ this.labels = null;
33
+ this.resolvedMode = 'segmented';
34
+ this.resolvedLabels = null;
15
35
  }
16
36
 
37
+ /*
38
+ Aside Detection
39
+ */
40
+ #aside = null;
41
+
42
+ handleAsideStateChange = e => {
43
+ if(this.mode === 'auto') {
44
+ this.resolvedMode = e.detail.state === 'collapsed' ? 'toggle' : 'segmented';
45
+ }
46
+ this.resolveLabels();
47
+ };
48
+
49
+ resolveMode = () => {
50
+ if(this.mode === 'toggle' || this.mode === 'segmented'){
51
+ this.resolvedMode = this.mode;
52
+ return;
53
+ }
54
+ if(this.#aside){
55
+ this.resolvedMode = this.#aside.state === 'collapsed' ? 'toggle' : 'segmented';
56
+ } else {
57
+ this.resolvedMode = 'segmented';
58
+ }
59
+ };
60
+
61
+ resolveLabels = () => {
62
+ if(this.labels === null || this.labels === undefined || (this.#aside && this.#aside.state === 'collapsed')){
63
+ this.resolvedLabels = null;
64
+ return;
65
+ }
66
+ const opts = parseOptions(this.options);
67
+ if(this.labels === ''){
68
+ this.resolvedLabels = opts.map(capitalize);
69
+ } else {
70
+ const custom = parseOptions(this.labels);
71
+ this.resolvedLabels = opts.map((opt, i) => custom[i] ?? capitalize(opt));
72
+ }
73
+ };
74
+
17
75
  /*
18
76
  Event Handlers
19
77
  */
20
78
  handleClick = () => {
21
- const current = theme.get();
22
- if(current === 'auto') theme.set('light');
23
- if(current === 'light') theme.set('dark');
24
- if(current === 'dark') theme.set('auto');
25
- }
79
+ const opts = parseOptions(this.options);
80
+ if(opts.length === 0) return;
81
+ const idx = opts.indexOf(theme.get());
82
+ theme.set(opts[(idx + 1) % opts.length]);
83
+ };
84
+
85
+ handleSegmentClick = value => {
86
+ theme.set(value);
87
+ };
26
88
 
27
89
  /*
28
90
  Lifecycle Callbacks
@@ -32,11 +94,27 @@ export default class ThemeSwitcher extends ShadowComponent {
32
94
  this.unsubscribe = theme.subscribe(t => {
33
95
  this.currentTheme = t;
34
96
  });
97
+ this.#aside = this.closest('k-aside');
98
+ if(this.#aside){
99
+ this.#aside.addEventListener('aside_state_change', this.handleAsideStateChange);
100
+ }
101
+ this.resolveMode();
102
+ this.resolveLabels();
35
103
  }
36
104
 
37
105
  disconnectedCallback() {
38
106
  super.disconnectedCallback();
39
107
  if(this.unsubscribe) this.unsubscribe();
108
+ if(this.#aside){
109
+ this.#aside.removeEventListener('aside_state_change', this.handleAsideStateChange);
110
+ this.#aside = null;
111
+ }
112
+ }
113
+
114
+ updated(changed) {
115
+ super.updated(changed);
116
+ if(changed.has('mode')) this.resolveMode();
117
+ if(changed.has('labels') || changed.has('options')) this.resolveLabels();
40
118
  }
41
119
 
42
120
  /*
@@ -44,39 +122,108 @@ export default class ThemeSwitcher extends ShadowComponent {
44
122
  */
45
123
  static styles = css`
46
124
  :host {
47
- --padding: var(--spacer);
125
+ --padding: var(--spacer, 1rem);
126
+ --c_inactive: transparent;
127
+ --tc_inactive: inherit;
128
+ --c_inactive__hover: var(--c_bg__alt);
129
+ --tc_inactive__hover: inherit;
130
+ --c_active: var(--c_primary);
131
+ --tc_active: var(--tc_light);
132
+ --c_active__hover: var(--c_active);
133
+ --tc_active__hover: var(--tc_active);
134
+ --border: 1px solid var(--c_border);
135
+ }
136
+ button.no-btn {
137
+ padding: var(--padding);
138
+ border-radius: var(--radius);
48
139
  display: flex;
140
+ align-items: center;
141
+ gap: calc(var(--spacer, 1rem) * 0.35);
142
+ color: var(--tc_inactive);
143
+ }
144
+ button.no-btn:hover {
145
+ color: var(--tc_inactive__hover);
49
146
  }
50
- button {
147
+ .segmented {
148
+ display: inline-flex;
149
+ }
150
+ .segmented button {
51
151
  padding: var(--padding);
152
+ background: var(--c_inactive);
153
+ cursor: pointer;
154
+ color: var(--tc_inactive);
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ gap: calc(var(--spacer, 1rem) * 0.35);
159
+ border: var(--border);
160
+ border-right: none;
161
+ }
162
+ .segmented button:first-child {
163
+ border-radius: var(--radius) 0 0 var(--radius);
164
+ }
165
+ .segmented button:last-child {
166
+ border-radius: 0 var(--radius) var(--radius) 0;
167
+ border-right: var(--border);
168
+ }
169
+ .segmented button:not(:first-child):not(:last-child) {
170
+ border-radius: 0;
171
+ }
172
+ .segmented button.active {
173
+ background: var(--c_active);
174
+ color: var(--tc_active);
175
+ border-color: var(--c_active);
176
+ }
177
+ .segmented button.active + button {
178
+ border-left-color: var(--c_active);
179
+ }
180
+ .segmented button:not(.active):hover {
181
+ background: var(--c_inactive__hover);
182
+ color: var(--tc_inactive__hover);
183
+ }
184
+ .segmented button.active:hover {
185
+ background: var(--c_active__hover);
186
+ color: var(--tc_active__hover);
52
187
  }
53
188
  `;
54
189
 
55
190
  /*
56
191
  Rendering
57
192
  */
58
- render() {
59
- const iconName = this.currentTheme === 'auto' ? 'mode-auto' :
60
- this.currentTheme === 'light' ? 'mode-light' : 'mode-dark';
61
-
193
+ renderToggle() {
62
194
  return html`
63
195
  <button
64
196
  class="no-btn"
65
197
  @click=${this.handleClick}
66
198
  >
67
- <k-icon name=${iconName}></k-icon>
199
+ <k-icon name=${ICON_MAP[this.currentTheme] || 'mode-auto'}></k-icon>
200
+ ${this.resolvedLabels ? html`<span>${this.resolvedLabels[parseOptions(this.options).indexOf(this.currentTheme)] ?? capitalize(this.currentTheme)}</span>` : nothing}
68
201
  </button>
69
202
  `;
70
203
  }
71
- static styles = css`
72
- :host {
73
- --padding: var(--spacer, 1rem);
74
- }
75
- button.no-btn {
76
- padding: var(--padding);
77
- border-radius: var(--radius);
78
- }
79
- `;
204
+
205
+ renderSegmented() {
206
+ const opts = parseOptions(this.options);
207
+ return html`
208
+ <div class="segmented">
209
+ ${opts.map((opt, i) => html`
210
+ <button
211
+ class="no-style ${opt === this.currentTheme ? 'active' : ''}"
212
+ @click=${() => this.handleSegmentClick(opt)}
213
+ title=${opt}
214
+ >
215
+ <k-icon name=${ICON_MAP[opt] || 'mode-auto'}></k-icon>
216
+ ${this.resolvedLabels ? html`<span>${this.resolvedLabels[i]}</span>` : nothing}
217
+ </button>
218
+ `)}
219
+ </div>
220
+ `;
221
+ }
222
+
223
+ render() {
224
+ if(this.resolvedMode === 'toggle') return this.renderToggle();
225
+ return this.renderSegmented();
226
+ }
80
227
 
81
228
  /*
82
229
  Static Methods
@@ -94,4 +241,4 @@ export default class ThemeSwitcher extends ShadowComponent {
94
241
  }
95
242
  }
96
243
 
97
- customElements.define('k-theme-switcher', ThemeSwitcher);
244
+ customElements.define('k-theme-switcher', ThemeSwitcher);