@dxos/lit-theme-editor 0.8.2-main.10c050d

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 (87) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +3 -0
  3. package/dist/src/dx-theme-editor/dx-range-spinbutton.d.ts +16 -0
  4. package/dist/src/dx-theme-editor/dx-range-spinbutton.d.ts.map +1 -0
  5. package/dist/src/dx-theme-editor/dx-range-spinbutton.js +127 -0
  6. package/dist/src/dx-theme-editor/dx-range-spinbutton.js.map +1 -0
  7. package/dist/src/dx-theme-editor/dx-theme-editor-alias-colors.d.ts +21 -0
  8. package/dist/src/dx-theme-editor/dx-theme-editor-alias-colors.d.ts.map +1 -0
  9. package/dist/src/dx-theme-editor/dx-theme-editor-alias-colors.js +267 -0
  10. package/dist/src/dx-theme-editor/dx-theme-editor-alias-colors.js.map +1 -0
  11. package/dist/src/dx-theme-editor/dx-theme-editor-physical-colors.d.ts +19 -0
  12. package/dist/src/dx-theme-editor/dx-theme-editor-physical-colors.d.ts.map +1 -0
  13. package/dist/src/dx-theme-editor/dx-theme-editor-physical-colors.js +163 -0
  14. package/dist/src/dx-theme-editor/dx-theme-editor-physical-colors.js.map +1 -0
  15. package/dist/src/dx-theme-editor/dx-theme-editor-semantic-colors.d.ts +32 -0
  16. package/dist/src/dx-theme-editor/dx-theme-editor-semantic-colors.d.ts.map +1 -0
  17. package/dist/src/dx-theme-editor/dx-theme-editor-semantic-colors.js +474 -0
  18. package/dist/src/dx-theme-editor/dx-theme-editor-semantic-colors.js.map +1 -0
  19. package/dist/src/dx-theme-editor/dx-theme-editor.d.ts +16 -0
  20. package/dist/src/dx-theme-editor/dx-theme-editor.d.ts.map +1 -0
  21. package/dist/src/dx-theme-editor/dx-theme-editor.js +160 -0
  22. package/dist/src/dx-theme-editor/dx-theme-editor.js.map +1 -0
  23. package/dist/src/dx-theme-editor/dx-theme-editor.lit-stories.d.ts +22 -0
  24. package/dist/src/dx-theme-editor/dx-theme-editor.lit-stories.d.ts.map +1 -0
  25. package/dist/src/dx-theme-editor/dx-theme-editor.lit-stories.js +27 -0
  26. package/dist/src/dx-theme-editor/dx-theme-editor.lit-stories.js.map +1 -0
  27. package/dist/src/dx-theme-editor/index.d.ts +5 -0
  28. package/dist/src/dx-theme-editor/index.d.ts.map +1 -0
  29. package/dist/src/dx-theme-editor/index.js +8 -0
  30. package/dist/src/dx-theme-editor/index.js.map +1 -0
  31. package/dist/src/dx-theme-editor/util.d.ts +8 -0
  32. package/dist/src/dx-theme-editor/util.d.ts.map +1 -0
  33. package/dist/src/dx-theme-editor/util.js +61 -0
  34. package/dist/src/dx-theme-editor/util.js.map +1 -0
  35. package/dist/src/index.d.ts +2 -0
  36. package/dist/src/index.d.ts.map +1 -0
  37. package/dist/src/index.js +5 -0
  38. package/dist/src/index.js.map +1 -0
  39. package/dist/tsconfig.tsbuildinfo +1 -0
  40. package/dist/types/src/dx-theme-editor/dx-range-spinbutton.d.ts +16 -0
  41. package/dist/types/src/dx-theme-editor/dx-range-spinbutton.d.ts.map +1 -0
  42. package/dist/types/src/dx-theme-editor/dx-range-spinbutton.js +127 -0
  43. package/dist/types/src/dx-theme-editor/dx-range-spinbutton.js.map +1 -0
  44. package/dist/types/src/dx-theme-editor/dx-theme-editor-alias-colors.d.ts +21 -0
  45. package/dist/types/src/dx-theme-editor/dx-theme-editor-alias-colors.d.ts.map +1 -0
  46. package/dist/types/src/dx-theme-editor/dx-theme-editor-alias-colors.js +267 -0
  47. package/dist/types/src/dx-theme-editor/dx-theme-editor-alias-colors.js.map +1 -0
  48. package/dist/types/src/dx-theme-editor/dx-theme-editor-physical-colors.d.ts +19 -0
  49. package/dist/types/src/dx-theme-editor/dx-theme-editor-physical-colors.d.ts.map +1 -0
  50. package/dist/types/src/dx-theme-editor/dx-theme-editor-physical-colors.js +163 -0
  51. package/dist/types/src/dx-theme-editor/dx-theme-editor-physical-colors.js.map +1 -0
  52. package/dist/types/src/dx-theme-editor/dx-theme-editor-semantic-colors.d.ts +32 -0
  53. package/dist/types/src/dx-theme-editor/dx-theme-editor-semantic-colors.d.ts.map +1 -0
  54. package/dist/types/src/dx-theme-editor/dx-theme-editor-semantic-colors.js +474 -0
  55. package/dist/types/src/dx-theme-editor/dx-theme-editor-semantic-colors.js.map +1 -0
  56. package/dist/types/src/dx-theme-editor/dx-theme-editor.d.ts +16 -0
  57. package/dist/types/src/dx-theme-editor/dx-theme-editor.d.ts.map +1 -0
  58. package/dist/types/src/dx-theme-editor/dx-theme-editor.js +160 -0
  59. package/dist/types/src/dx-theme-editor/dx-theme-editor.js.map +1 -0
  60. package/dist/types/src/dx-theme-editor/dx-theme-editor.lit-stories.d.ts +22 -0
  61. package/dist/types/src/dx-theme-editor/dx-theme-editor.lit-stories.d.ts.map +1 -0
  62. package/dist/types/src/dx-theme-editor/dx-theme-editor.lit-stories.js +27 -0
  63. package/dist/types/src/dx-theme-editor/dx-theme-editor.lit-stories.js.map +1 -0
  64. package/dist/types/src/dx-theme-editor/index.d.ts +5 -0
  65. package/dist/types/src/dx-theme-editor/index.d.ts.map +1 -0
  66. package/dist/types/src/dx-theme-editor/index.js +8 -0
  67. package/dist/types/src/dx-theme-editor/index.js.map +1 -0
  68. package/dist/types/src/dx-theme-editor/util.d.ts +8 -0
  69. package/dist/types/src/dx-theme-editor/util.d.ts.map +1 -0
  70. package/dist/types/src/dx-theme-editor/util.js +61 -0
  71. package/dist/types/src/dx-theme-editor/util.js.map +1 -0
  72. package/dist/types/src/index.d.ts +2 -0
  73. package/dist/types/src/index.d.ts.map +1 -0
  74. package/dist/types/src/index.js +5 -0
  75. package/dist/types/src/index.js.map +1 -0
  76. package/dist/types/tsconfig.tsbuildinfo +1 -0
  77. package/package.json +40 -0
  78. package/src/dx-theme-editor/dx-range-spinbutton.ts +124 -0
  79. package/src/dx-theme-editor/dx-theme-editor-alias-colors.ts +305 -0
  80. package/src/dx-theme-editor/dx-theme-editor-physical-colors.ts +179 -0
  81. package/src/dx-theme-editor/dx-theme-editor-semantic-colors.ts +558 -0
  82. package/src/dx-theme-editor/dx-theme-editor.lit-stories.ts +37 -0
  83. package/src/dx-theme-editor/dx-theme-editor.pcss +299 -0
  84. package/src/dx-theme-editor/dx-theme-editor.ts +158 -0
  85. package/src/dx-theme-editor/index.ts +8 -0
  86. package/src/dx-theme-editor/util.ts +66 -0
  87. package/src/index.ts +5 -0
@@ -0,0 +1,558 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type AlphaLuminosity } from '@ch-ui/colors';
6
+ import { type TokenSet, parseAlphaLuminosity } from '@ch-ui/tokens';
7
+ import { LitElement, html } from 'lit';
8
+ import { customElement, state } from 'lit/decorators.js';
9
+ import { repeat } from 'lit/directives/repeat.js';
10
+ import { styleMap } from 'lit/directives/style-map.js';
11
+
12
+ import { debounce } from '@dxos/async';
13
+ import { makeId } from '@dxos/react-hooks';
14
+
15
+ import { restore, saveAndRender, tokenSetUpdateEvent } from './util';
16
+
17
+ import './dx-range-spinbutton';
18
+
19
+ export type DxThemeEditorSemanticColorsProps = {};
20
+
21
+ const isAlphaLuminosity = (value: any): value is AlphaLuminosity => {
22
+ return Number.isFinite(value) || (typeof value === 'string' && value.includes('/'));
23
+ };
24
+
25
+ @customElement('dx-theme-editor-semantic-colors')
26
+ export class DxThemeEditorSemanticColors extends LitElement {
27
+ @state()
28
+ tokenSet: TokenSet = restore();
29
+
30
+ @state()
31
+ searchTerm: string = '';
32
+
33
+ private debouncedSaveAndRender = debounce(() => {
34
+ saveAndRender(this.tokenSet);
35
+ }, 200);
36
+
37
+ private handleTokenSetUpdate = () => {
38
+ this.tokenSet = restore();
39
+ };
40
+
41
+ private getPhysicalColorSeries(): string[] {
42
+ if (!this.tokenSet.colors?.physical?.series) {
43
+ return [];
44
+ }
45
+
46
+ return Object.keys(this.tokenSet.colors.physical.series);
47
+ }
48
+
49
+ private getSemanticTokens(): [string, any][] {
50
+ if (!this.tokenSet.colors?.semantic?.sememes) {
51
+ return [];
52
+ }
53
+
54
+ return Object.entries(this.tokenSet.colors.semantic.sememes);
55
+ }
56
+
57
+ private updateSemanticToken(tokenName: string, condition: 'light' | 'dark', property: 0 | 1, value: any): void {
58
+ if (!this.tokenSet.colors?.semantic?.sememes?.[tokenName]?.[condition]) {
59
+ return;
60
+ }
61
+
62
+ // Create a deep copy of the tokenSet to avoid direct mutation
63
+ const updatedTokenSet = JSON.parse(JSON.stringify(this.tokenSet));
64
+
65
+ // Update the specific property
66
+ updatedTokenSet.colors.semantic.sememes[tokenName][condition][property] = value;
67
+
68
+ // Update the state
69
+ this.tokenSet = updatedTokenSet;
70
+
71
+ // Save and render changes
72
+ this.debouncedSaveAndRender();
73
+ }
74
+
75
+ private handleTokenNameChange(tokenName: string, newName: string): void {
76
+ if (!this.tokenSet.colors?.semantic?.sememes?.[tokenName]) {
77
+ return;
78
+ }
79
+
80
+ // Create a deep copy of the tokenSet to avoid direct mutation
81
+ const updatedTokenSet = JSON.parse(JSON.stringify(this.tokenSet));
82
+
83
+ // Get the token value
84
+ const tokenValue = updatedTokenSet.colors.semantic.sememes[tokenName];
85
+
86
+ // Delete the old token
87
+ delete updatedTokenSet.colors.semantic.sememes[tokenName];
88
+
89
+ // Add the token with the new name
90
+ updatedTokenSet.colors.semantic.sememes[newName] = tokenValue;
91
+
92
+ // Update the state
93
+ this.tokenSet = updatedTokenSet;
94
+
95
+ // Save and render changes
96
+ this.debouncedSaveAndRender();
97
+ }
98
+
99
+ private handleSeriesChange(tokenName: string, condition: 'light' | 'dark', value: string): void {
100
+ this.updateSemanticToken(tokenName, condition, 0, value);
101
+ }
102
+
103
+ private handleBothSeriesChange(tokenName: string, value: string): void {
104
+ // Update both light and dark series values
105
+ this.updateSemanticToken(tokenName, 'light', 0, value);
106
+ this.updateSemanticToken(tokenName, 'dark', 0, value);
107
+ }
108
+
109
+ private handleLuminosityChange(tokenName: string, condition: 'light' | 'dark', value: number): void {
110
+ // Get the current value to preserve alpha if it exists
111
+ const currentValue = this.tokenSet.colors?.semantic?.sememes?.[tokenName]?.[condition]?.[1];
112
+ if (!isAlphaLuminosity(currentValue)) {
113
+ return;
114
+ }
115
+
116
+ // Parse the current value to get the alpha component
117
+ const [, alpha] = parseAlphaLuminosity(currentValue);
118
+
119
+ // If alpha is defined and not 1, use the format "luminosity/alpha"
120
+ // Otherwise, just use the luminosity value
121
+ const newValue = alpha !== undefined && alpha !== 1 ? `${value}/${alpha}` : value;
122
+
123
+ this.updateSemanticToken(tokenName, condition, 1, newValue);
124
+ }
125
+
126
+ private handleAlphaChange(tokenName: string, value: number): void {
127
+ // Update both light and dark conditions
128
+ ['light', 'dark'].forEach((condition) => {
129
+ const currentValue = this.tokenSet.colors?.semantic?.sememes?.[tokenName]?.[condition as 'light' | 'dark']?.[1];
130
+ if (!isAlphaLuminosity(currentValue)) {
131
+ return;
132
+ }
133
+
134
+ // Parse the current value to get the luminosity component
135
+ const [luminosity] = parseAlphaLuminosity(currentValue);
136
+
137
+ // If alpha is 1 (default), just use the luminosity value
138
+ // Otherwise, use the format "luminosity/alpha"
139
+ const newValue = value === 1 ? luminosity : `${luminosity}/${value}`;
140
+
141
+ this.updateSemanticToken(tokenName, condition as 'light' | 'dark', 1, newValue);
142
+ });
143
+ }
144
+
145
+ private addSemanticToken(): void {
146
+ if (!this.tokenSet.colors?.semantic?.sememes) {
147
+ return;
148
+ }
149
+
150
+ // Create a deep copy of the tokenSet to avoid direct mutation
151
+ const updatedTokenSet = JSON.parse(JSON.stringify(this.tokenSet));
152
+
153
+ // Generate a random ID for the token name
154
+ const tokenName = makeId('sememe--');
155
+
156
+ // Create a new token with default values
157
+ updatedTokenSet.colors.semantic.sememes[tokenName] = {
158
+ light: ['neutral', 500],
159
+ dark: ['neutral', 500],
160
+ };
161
+
162
+ // Update the state
163
+ this.tokenSet = updatedTokenSet;
164
+
165
+ // Save and render changes
166
+ this.debouncedSaveAndRender();
167
+ }
168
+
169
+ private removeSemanticToken(tokenName: string): void {
170
+ if (!this.tokenSet.colors?.semantic?.sememes?.[tokenName]) {
171
+ return;
172
+ }
173
+
174
+ // Create a deep copy of the tokenSet to avoid direct mutation
175
+ const updatedTokenSet = JSON.parse(JSON.stringify(this.tokenSet));
176
+
177
+ // Delete the token
178
+ delete updatedTokenSet.colors.semantic.sememes[tokenName];
179
+
180
+ // Update the state
181
+ this.tokenSet = updatedTokenSet;
182
+
183
+ // Save and render changes
184
+ this.debouncedSaveAndRender();
185
+ }
186
+
187
+ private getAliasTokensForSemantic(tokenName: string): { condition: string; name: string }[] {
188
+ if (!this.tokenSet.colors?.alias?.aliases?.[tokenName]) {
189
+ return [];
190
+ }
191
+
192
+ const aliasTokens: { condition: string; name: string }[] = [];
193
+ const aliases = this.tokenSet.colors.alias.aliases[tokenName];
194
+
195
+ // Process each condition (root, attention)
196
+ Object.entries(aliases).forEach(([condition, names]) => {
197
+ names.forEach((name) => {
198
+ aliasTokens.push({ condition, name });
199
+ });
200
+ });
201
+
202
+ return aliasTokens;
203
+ }
204
+
205
+ private addAliasToken(tokenName: string): void {
206
+ if (!this.tokenSet.colors?.alias?.aliases) {
207
+ return;
208
+ }
209
+
210
+ // Create a deep copy of the tokenSet to avoid direct mutation
211
+ const updatedTokenSet = JSON.parse(JSON.stringify(this.tokenSet));
212
+
213
+ // Ensure the semantic token exists in the aliases structure
214
+ if (!updatedTokenSet.colors.alias.aliases[tokenName]) {
215
+ updatedTokenSet.colors.alias.aliases[tokenName] = {};
216
+ }
217
+
218
+ // Ensure the 'root' condition exists
219
+ if (!updatedTokenSet.colors.alias.aliases[tokenName].root) {
220
+ updatedTokenSet.colors.alias.aliases[tokenName].root = [];
221
+ }
222
+
223
+ // Generate a random ID for the alias name
224
+ const aliasName = makeId('alias--');
225
+
226
+ // Add the new alias to the 'root' condition
227
+ updatedTokenSet.colors.alias.aliases[tokenName].root.push(aliasName);
228
+
229
+ // Update the state
230
+ this.tokenSet = updatedTokenSet;
231
+
232
+ // Save and render changes
233
+ this.debouncedSaveAndRender();
234
+ }
235
+
236
+ private removeAliasToken(tokenName: string, condition: string, aliasName: string): void {
237
+ if (!this.tokenSet.colors?.alias?.aliases?.[tokenName]?.[condition]) {
238
+ return;
239
+ }
240
+
241
+ // Create a deep copy of the tokenSet to avoid direct mutation
242
+ const updatedTokenSet = JSON.parse(JSON.stringify(this.tokenSet));
243
+
244
+ // Find the index of the alias in the array
245
+ const aliasIndex = updatedTokenSet.colors.alias.aliases[tokenName][condition].indexOf(aliasName);
246
+ if (aliasIndex === -1) {
247
+ return;
248
+ }
249
+
250
+ // Remove the alias from the array
251
+ updatedTokenSet.colors.alias.aliases[tokenName][condition].splice(aliasIndex, 1);
252
+
253
+ // If the condition array is empty, remove it
254
+ if (updatedTokenSet.colors.alias.aliases[tokenName][condition].length === 0) {
255
+ delete updatedTokenSet.colors.alias.aliases[tokenName][condition];
256
+ }
257
+
258
+ // If the token has no more conditions, remove it from aliases
259
+ if (Object.keys(updatedTokenSet.colors.alias.aliases[tokenName]).length === 0) {
260
+ delete updatedTokenSet.colors.alias.aliases[tokenName];
261
+ }
262
+
263
+ // Update the state
264
+ this.tokenSet = updatedTokenSet;
265
+
266
+ // Save and render changes
267
+ this.debouncedSaveAndRender();
268
+ }
269
+
270
+ private updateAliasToken(
271
+ tokenName: string,
272
+ oldCondition: string,
273
+ oldName: string,
274
+ newCondition: string,
275
+ newName: string,
276
+ ): void {
277
+ if (!this.tokenSet.colors?.alias?.aliases?.[tokenName]?.[oldCondition]) {
278
+ return;
279
+ }
280
+
281
+ // Create a deep copy of the tokenSet to avoid direct mutation
282
+ const updatedTokenSet = JSON.parse(JSON.stringify(this.tokenSet));
283
+
284
+ // Find the index of the old alias in the array
285
+ const aliasIndex = updatedTokenSet.colors.alias.aliases[tokenName][oldCondition].indexOf(oldName);
286
+ if (aliasIndex === -1) {
287
+ return;
288
+ }
289
+
290
+ // Remove the old alias
291
+ updatedTokenSet.colors.alias.aliases[tokenName][oldCondition].splice(aliasIndex, 1);
292
+
293
+ // If the old condition array is empty, remove it
294
+ if (updatedTokenSet.colors.alias.aliases[tokenName][oldCondition].length === 0) {
295
+ delete updatedTokenSet.colors.alias.aliases[tokenName][oldCondition];
296
+ }
297
+
298
+ // Ensure the new condition exists
299
+ if (!updatedTokenSet.colors.alias.aliases[tokenName][newCondition]) {
300
+ updatedTokenSet.colors.alias.aliases[tokenName][newCondition] = [];
301
+ }
302
+
303
+ // Add the new alias to the new condition
304
+ updatedTokenSet.colors.alias.aliases[tokenName][newCondition].push(newName);
305
+
306
+ // Update the state
307
+ this.tokenSet = updatedTokenSet;
308
+
309
+ // Save and render changes
310
+ this.debouncedSaveAndRender();
311
+ }
312
+
313
+ private checkDuplicateAlias(tokenName: string, condition: string, aliasName: string): boolean {
314
+ if (!this.tokenSet.colors?.alias?.aliases) {
315
+ return false;
316
+ }
317
+
318
+ // Check if the alias exists in any other token with the same condition
319
+ for (const [currentTokenName, conditions] of Object.entries(this.tokenSet.colors.alias.aliases)) {
320
+ // Skip the current token
321
+ if (currentTokenName === tokenName) {
322
+ continue;
323
+ }
324
+
325
+ // Check if the condition exists and contains the alias name
326
+ if (conditions[condition] && conditions[condition].includes(aliasName)) {
327
+ return true;
328
+ }
329
+ }
330
+
331
+ return false;
332
+ }
333
+
334
+ private renderTokenControls(tokenName: string, tokenValue: any) {
335
+ const physicalColorSeries = this.getPhysicalColorSeries();
336
+ const lightSeries = tokenValue.light?.[0] || '';
337
+ const lightLuminosityValue = tokenValue.light?.[1] || 0;
338
+ const darkSeries = tokenValue.dark?.[0] || '';
339
+ const darkLuminosityValue = tokenValue.dark?.[1] || 0;
340
+
341
+ // Parse the luminosity values to extract the alpha components
342
+ const [lightLuminosity, lightAlpha] = parseAlphaLuminosity(lightLuminosityValue);
343
+ const [darkLuminosity, darkAlpha] = parseAlphaLuminosity(darkLuminosityValue);
344
+
345
+ // Use the same series for both light and dark
346
+ const currentSeries = lightSeries || darkSeries;
347
+
348
+ // Use the first defined alpha value, or default to 1
349
+ const currentAlpha = lightAlpha !== undefined ? lightAlpha : darkAlpha !== undefined ? darkAlpha : 1;
350
+
351
+ // Create unique IDs for headings to reference in aria-labelledby
352
+ const tokenHeadingId = `${tokenName}-heading`;
353
+ const lightHeadingId = `${tokenName}-light-heading`;
354
+ const darkHeadingId = `${tokenName}-dark-heading`;
355
+ const seriesSelectId = `${tokenName}-series`;
356
+ const contentId = `${tokenName}-content`;
357
+ const aliasListId = `${tokenName}-alias-list`;
358
+
359
+ // Toggle expanded/collapsed state
360
+ const toggleExpanded = (e: Event) => {
361
+ const button = e.currentTarget as HTMLButtonElement;
362
+ const container = button.closest('.collapsible-token') as HTMLElement;
363
+ const isExpanded = container.getAttribute('data-state') === 'expanded';
364
+ container.setAttribute('data-state', isExpanded ? 'collapsed' : 'expanded');
365
+ button.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
366
+ };
367
+
368
+ return html`
369
+ <div role="group" class="collapsible-token" data-state="collapsed">
370
+ <h3 id="${tokenHeadingId}" class="token-title">
371
+ <button
372
+ class="toggle-button dx-focus-ring dx-button"
373
+ @click=${toggleExpanded}
374
+ aria-expanded="false"
375
+ aria-controls="${contentId}"
376
+ >
377
+ <dx-icon icon="ph--caret-down--regular"></dx-icon>
378
+ <span class="sr-only">Toggle token controls</span>
379
+ </button>
380
+ <span class="static-token-name" @click=${toggleExpanded}>${tokenName}</span>
381
+ <input
382
+ type="text"
383
+ class="token-name-input dx-focus-ring"
384
+ .value=${tokenName}
385
+ @change=${(e: Event) => this.handleTokenNameChange(tokenName, (e.target as HTMLInputElement).value)}
386
+ aria-label="Token name"
387
+ />
388
+ <button
389
+ class="remove-token-button dx-focus-ring dx-button"
390
+ @click=${() => this.removeSemanticToken(tokenName)}
391
+ aria-label="Remove token"
392
+ >
393
+ <span class="sr-only">Remove token</span>
394
+ <dx-icon icon="ph--minus--regular" />
395
+ </button>
396
+ </h3>
397
+ <div id="${contentId}" class="token-config-content">
398
+ <div class="token-header">
399
+ <div class="token-series-select">
400
+ <label class="control-label" for="${seriesSelectId}">Palette:</label>
401
+ <select
402
+ id="${seriesSelectId}"
403
+ class="series-select dx-focus-ring"
404
+ .value=${currentSeries}
405
+ @change=${(e: Event) => this.handleBothSeriesChange(tokenName, (e.target as HTMLSelectElement).value)}
406
+ aria-labelledby="${tokenHeadingId}"
407
+ >
408
+ ${repeat(
409
+ physicalColorSeries,
410
+ (series) => series,
411
+ (series) => html`<option value="${series}" ?selected=${series === currentSeries}>${series}</option>`,
412
+ )}
413
+ </select>
414
+ </div>
415
+ <dx-range-spinbutton
416
+ label="Alpha"
417
+ min="0"
418
+ max="1"
419
+ step="0.01"
420
+ .value=${currentAlpha}
421
+ headingId=${tokenHeadingId}
422
+ @value-changed=${(e: CustomEvent) => this.handleAlphaChange(tokenName, e.detail.value)}
423
+ ></dx-range-spinbutton>
424
+ </div>
425
+ <div role="group" class="control-group">
426
+ <div role="none" class="control-group-item">
427
+ <div class="shade-preview dark">
428
+ <div class="shade" style="${styleMap({ backgroundColor: `var(--dx-${tokenName})` })}"></div>
429
+ </div>
430
+ <dx-range-spinbutton
431
+ label="Dark"
432
+ min="0"
433
+ max="1000"
434
+ step="1"
435
+ .value=${darkLuminosity}
436
+ headingId=${darkHeadingId}
437
+ @value-changed=${(e: CustomEvent) => this.handleLuminosityChange(tokenName, 'dark', e.detail.value)}
438
+ variant="reverse-range"
439
+ ></dx-range-spinbutton>
440
+ </div>
441
+ <div role="none" class="control-group-item">
442
+ <div class="shade-preview">
443
+ <div class="shade" style="${styleMap({ backgroundColor: `var(--dx-${tokenName})` })}"></div>
444
+ </div>
445
+ <dx-range-spinbutton
446
+ label="Light"
447
+ min="0"
448
+ max="1000"
449
+ step="1"
450
+ .value=${lightLuminosity}
451
+ headingId=${lightHeadingId}
452
+ @value-changed=${(e: CustomEvent) => this.handleLuminosityChange(tokenName, 'light', e.detail.value)}
453
+ variant="reverse-order"
454
+ ></dx-range-spinbutton>
455
+ </div>
456
+ </div>
457
+
458
+ <!-- Alias tokens -->
459
+ <div class="semantic-alias-token-section">
460
+ <ul id="${aliasListId}" class="semantic-alias-token-list">
461
+ ${repeat(
462
+ this.getAliasTokensForSemantic(tokenName),
463
+ (alias) => `${tokenName}-${alias.condition}-${alias.name}`,
464
+ (alias) => html`
465
+ <li class="alias-token-item">
466
+ <div role="none" class="condition-and-validation">
467
+ <p
468
+ class="alias-validation"
469
+ style=${styleMap({
470
+ display: this.checkDuplicateAlias(tokenName, alias.condition, alias.name) ? 'flex' : 'none',
471
+ })}
472
+ >
473
+ <dx-icon icon="ph--warning--duotone" size="6"></dx-icon>Duplicate
474
+ </p>
475
+ <select
476
+ class="alias-condition-select dx-focus-ring"
477
+ .value=${alias.condition}
478
+ @change=${(e: Event) => {
479
+ const newCondition = (e.target as HTMLSelectElement).value;
480
+ this.updateAliasToken(tokenName, alias.condition, alias.name, newCondition, alias.name);
481
+ }}
482
+ >
483
+ <option value="root">root</option>
484
+ <option value="attention">attention</option>
485
+ </select>
486
+ </div>
487
+ <input
488
+ type="text"
489
+ class="alias-name-input dx-focus-ring"
490
+ .value=${alias.name}
491
+ @change=${(e: Event) => {
492
+ const newName = (e.target as HTMLInputElement).value;
493
+ this.updateAliasToken(tokenName, alias.condition, alias.name, alias.condition, newName);
494
+ }}
495
+ />
496
+ <button
497
+ class="remove-alias-button dx-focus-ring dx-button"
498
+ @click=${() => this.removeAliasToken(tokenName, alias.condition, alias.name)}
499
+ >
500
+ <span class="sr-only">Remove alias token</span>
501
+ <dx-icon icon="ph--minus--regular" />
502
+ </button>
503
+ </li>
504
+ `,
505
+ )}
506
+ </ul>
507
+ <button class="add-alias-button dx-focus-ring dx-button" @click=${() => this.addAliasToken(tokenName)}>
508
+ Add alias
509
+ </button>
510
+ </div>
511
+ </div>
512
+ </div>
513
+ `;
514
+ }
515
+
516
+ override connectedCallback(): void {
517
+ super.connectedCallback();
518
+ saveAndRender(this.tokenSet);
519
+ window.addEventListener(tokenSetUpdateEvent, this.handleTokenSetUpdate);
520
+ }
521
+
522
+ override disconnectedCallback(): void {
523
+ super.disconnectedCallback();
524
+ window.removeEventListener(tokenSetUpdateEvent, this.handleTokenSetUpdate);
525
+ }
526
+
527
+ private handleSearchChange(e: Event): void {
528
+ this.searchTerm = (e.target as HTMLInputElement).value;
529
+ }
530
+
531
+ override render() {
532
+ const semanticTokens = this.getSemanticTokens();
533
+ const filteredTokens = semanticTokens.filter(([tokenName]) =>
534
+ tokenName.toLowerCase().includes(this.searchTerm.toLowerCase()),
535
+ );
536
+
537
+ return html`
538
+ <input
539
+ type="search"
540
+ class="token-search dx-focus-ring"
541
+ placeholder="Search semantic tokens…"
542
+ .value=${this.searchTerm}
543
+ @input=${this.handleSearchChange}
544
+ aria-label="Search tokens"
545
+ />
546
+ ${repeat(
547
+ filteredTokens,
548
+ ([tokenName]) => tokenName,
549
+ ([tokenName, tokenValue]) => this.renderTokenControls(tokenName, tokenValue),
550
+ )}
551
+ <button class="add-token-button dx-focus-ring dx-button" @click=${this.addSemanticToken}>Add token</button>
552
+ `;
553
+ }
554
+
555
+ override createRenderRoot(): this {
556
+ return this;
557
+ }
558
+ }
@@ -0,0 +1,37 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import './dx-theme-editor.ts';
6
+ import './dx-theme-editor-physical-colors.ts';
7
+ import './dx-theme-editor-semantic-colors.ts';
8
+ import './dx-theme-editor-alias-colors.ts';
9
+ import './dx-theme-editor.pcss';
10
+ import '@dxos/lit-ui';
11
+ import { html } from 'lit';
12
+
13
+ import { type DxThemeEditorProps } from './dx-theme-editor';
14
+ import { type DxThemeEditorAliasColorsProps } from './dx-theme-editor-alias-colors';
15
+ import { type DxThemeEditorPhysicalColorsProps } from './dx-theme-editor-physical-colors';
16
+ import { type DxThemeEditorSemanticColorsProps } from './dx-theme-editor-semantic-colors';
17
+
18
+ export default {
19
+ title: 'dx-theme-editor',
20
+ parameters: { layout: 'fullscreen' },
21
+ };
22
+
23
+ export const CombinedThemeEditor = (props: DxThemeEditorProps) => {
24
+ return html`<dx-theme-editor></dx-theme-editor>`;
25
+ };
26
+
27
+ export const PhysicalColors = (props: DxThemeEditorPhysicalColorsProps) => {
28
+ return html`<dx-theme-editor-physical-colors></dx-theme-editor-physical-colors>`;
29
+ };
30
+
31
+ export const SemanticColors = (props: DxThemeEditorSemanticColorsProps) => {
32
+ return html`<dx-theme-editor-semantic-colors></dx-theme-editor-semantic-colors>`;
33
+ };
34
+
35
+ export const AliasColors = (props: DxThemeEditorAliasColorsProps) => {
36
+ return html`<dx-theme-editor-alias-colors></dx-theme-editor-alias-colors>`;
37
+ };