@d0nj/dsh-ui-custom 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/README.md +82 -2
- package/cordis.patch.yml +22 -0
- package/dist/client/client.js +1313 -0
- package/dist/client/index.d.ts +69 -0
- package/dist/client/index.js +324 -0
- package/dist/client/panel.d.ts +85 -0
- package/dist/client/panel.js +294 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +143 -0
- package/dist/resolve.d.ts +116 -0
- package/dist/resolve.js +149 -0
- package/dist/spec.d.ts +101 -0
- package/dist/spec.js +509 -0
- package/icon.svg +15 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +71 -3
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@d0nj/dsh-ui-custom',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const __modules = new Map()
|
|
5
|
+
function __require(id) {
|
|
6
|
+
if (__modules.has(id)) return __modules.get(id)
|
|
7
|
+
const factory = __factories[id]
|
|
8
|
+
if (factory === undefined) throw new Error('ui-custom: unknown module ' + id)
|
|
9
|
+
const module = { exports: {} }
|
|
10
|
+
__modules.set(id, module.exports)
|
|
11
|
+
factory(module, module.exports, __require)
|
|
12
|
+
__modules.set(id, module.exports)
|
|
13
|
+
return module.exports
|
|
14
|
+
}
|
|
15
|
+
const __factories = {
|
|
16
|
+
"./client/index.js": (module, exports, __require) => {
|
|
17
|
+
const { PRESETS, TOKEN_SPECS } = __require('./spec.js');
|
|
18
|
+
const { resolveCustomization } = __require('./resolve.js');
|
|
19
|
+
const { ThemeStudio } = __require('./client/panel.js');
|
|
20
|
+
/** Layer identity; the runtime keeps one layer per source. */
|
|
21
|
+
const OVERRIDE_SOURCE = 'd0nj-ui-custom';
|
|
22
|
+
/** Services required before `apply` runs. */
|
|
23
|
+
const inject = ['theme', 'configForms', 'slots'];
|
|
24
|
+
/**
|
|
25
|
+
* The React namespace, handed in by the module envelope.
|
|
26
|
+
*
|
|
27
|
+
* The browser loader resolves React through its own module table, so a client
|
|
28
|
+
* half cannot import it. The stamped envelope calls useReact before apply,
|
|
29
|
+
* which keeps the dependency explicit instead of reading an ambient global.
|
|
30
|
+
*/
|
|
31
|
+
let ReactImpl;
|
|
32
|
+
/**
|
|
33
|
+
* Provide the React namespace to this plugin.
|
|
34
|
+
* @param react - the namespace the loader resolved.
|
|
35
|
+
*/
|
|
36
|
+
function useReact(react) {
|
|
37
|
+
ReactImpl = react;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Flatten a preset's per-mode tokens to the single value the `register` arm
|
|
41
|
+
* expects, picking the mode that matches the preset's own base palette.
|
|
42
|
+
*
|
|
43
|
+
* `register` takes a flat `tokens` map rather than the two-mode shape
|
|
44
|
+
* `overrideTokens` accepts, so the mapping has to happen somewhere; doing it
|
|
45
|
+
* here keeps the colorScheme and the chosen values from disagreeing.
|
|
46
|
+
*
|
|
47
|
+
* @param preset - one shipped preset.
|
|
48
|
+
* @returns the flat token map for `register`.
|
|
49
|
+
*/
|
|
50
|
+
function flatPresetTokens(preset) {
|
|
51
|
+
const tokens = {};
|
|
52
|
+
for (const [name, modes] of Object.entries(preset.tokens)) {
|
|
53
|
+
tokens[name] = preset.colorScheme === 'dark' ? modes.dark : modes.light;
|
|
54
|
+
}
|
|
55
|
+
return tokens;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Normalize the stored value into a document.
|
|
59
|
+
*
|
|
60
|
+
* The settings snapshot carries the parsed namespace value, which is already
|
|
61
|
+
* shaped like the config; this narrows it without trusting the transport.
|
|
62
|
+
* @param value - snapshot value.
|
|
63
|
+
* @returns the document to resolve.
|
|
64
|
+
*/
|
|
65
|
+
function asDocument(value) {
|
|
66
|
+
const fallback = {
|
|
67
|
+
basePreset: 'none',
|
|
68
|
+
tokens: {},
|
|
69
|
+
typography: {
|
|
70
|
+
textFontFamily: '',
|
|
71
|
+
codeFontFamily: '',
|
|
72
|
+
terminalFontFamily: '',
|
|
73
|
+
fontSize: 14,
|
|
74
|
+
codeFontSize: 11,
|
|
75
|
+
terminalFontSize: 13,
|
|
76
|
+
},
|
|
77
|
+
geometry: { radius: 8, density: 'comfortable' },
|
|
78
|
+
};
|
|
79
|
+
if (value === null || typeof value !== 'object')
|
|
80
|
+
return fallback;
|
|
81
|
+
const record = value;
|
|
82
|
+
const typography = record.typography;
|
|
83
|
+
const geometry = record.geometry;
|
|
84
|
+
return {
|
|
85
|
+
basePreset: typeof record.basePreset === 'string' ? record.basePreset : 'none',
|
|
86
|
+
tokens: record.tokens !== null && typeof record.tokens === 'object'
|
|
87
|
+
? record.tokens
|
|
88
|
+
: {},
|
|
89
|
+
typography: {
|
|
90
|
+
...fallback.typography,
|
|
91
|
+
...(typography !== null && typeof typography === 'object'
|
|
92
|
+
? typography
|
|
93
|
+
: {}),
|
|
94
|
+
},
|
|
95
|
+
geometry: {
|
|
96
|
+
...fallback.geometry,
|
|
97
|
+
...(geometry !== null && typeof geometry === 'object'
|
|
98
|
+
? geometry
|
|
99
|
+
: {}),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Client plugin body.
|
|
105
|
+
* @param ctx - client cordis context with `theme` and `configForms` injected.
|
|
106
|
+
*/
|
|
107
|
+
function apply(ctx) {
|
|
108
|
+
// A throw here fails the whole web boot (1 entry did not activate), which takes
|
|
109
|
+
// the UI down for a customization plugin. Every step is therefore guarded: a
|
|
110
|
+
// problem is reported once and the rest of the harness keeps working.
|
|
111
|
+
try {
|
|
112
|
+
applyInner(ctx);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
reportFailure(ctx, error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Report one fatal setup failure without rethrowing.
|
|
120
|
+
* @param ctx - client context.
|
|
121
|
+
* @param error - the failure.
|
|
122
|
+
*/
|
|
123
|
+
function reportFailure(ctx, error) {
|
|
124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
125
|
+
const logger = ctx.logger;
|
|
126
|
+
logger?.('ui-custom').error("theme customization disabled: " + message);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The real client body, called under a guard by {@link apply}.
|
|
130
|
+
* @param ctx - client context.
|
|
131
|
+
*/
|
|
132
|
+
function applyInner(ctx) {
|
|
133
|
+
const theme = ctx.theme;
|
|
134
|
+
const forms = ctx.configForms;
|
|
135
|
+
// Presets register once, as real themes, so they appear among the normal
|
|
136
|
+
// choices and survive a reload even if this plugin's override layer is empty.
|
|
137
|
+
for (const preset of PRESETS) {
|
|
138
|
+
try {
|
|
139
|
+
ctx.effect(() => theme.register({
|
|
140
|
+
id: preset.id,
|
|
141
|
+
colorScheme: preset.colorScheme,
|
|
142
|
+
tokens: flatPresetTokens(preset),
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
// A preset id that is already taken (the built-in light/dark pair, or a
|
|
147
|
+
// re-registration after HMR) must not cost the user the editor.
|
|
148
|
+
reportFailure(ctx, error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const form = forms.get('ui-custom');
|
|
152
|
+
let disposeLayer;
|
|
153
|
+
// The last typography this plugin applied, so it writes only real changes and
|
|
154
|
+
// never fights the shipped Appearance rows on every snapshot.
|
|
155
|
+
let appliedTypography = '';
|
|
156
|
+
/**
|
|
157
|
+
* Recompute the layer from the stored document.
|
|
158
|
+
*/
|
|
159
|
+
const refresh = () => {
|
|
160
|
+
const snapshot = form.getSnapshot();
|
|
161
|
+
if (snapshot.value === undefined)
|
|
162
|
+
return;
|
|
163
|
+
const resolved = resolveCustomization(asDocument(snapshot.value));
|
|
164
|
+
disposeLayer?.();
|
|
165
|
+
disposeLayer = theme.overrideTokens(OVERRIDE_SOURCE, resolved.tokens);
|
|
166
|
+
// Typography goes through the theme service's own entries, so the built-in
|
|
167
|
+
// rows and this panel share one source of truth instead of shadowing each
|
|
168
|
+
// other. Sizes are only written when they differ, because each write is a
|
|
169
|
+
// settings mutation.
|
|
170
|
+
const typography = resolved.typography;
|
|
171
|
+
const key = JSON.stringify(typography);
|
|
172
|
+
if (key !== appliedTypography) {
|
|
173
|
+
appliedTypography = key;
|
|
174
|
+
const roles = [
|
|
175
|
+
['text', typography.textFontFamily, typography.fontSize],
|
|
176
|
+
['code', typography.codeFontFamily, typography.codeFontSize],
|
|
177
|
+
['terminal', typography.terminalFontFamily, typography.terminalFontSize],
|
|
178
|
+
];
|
|
179
|
+
for (const [role, family, size] of roles) {
|
|
180
|
+
try {
|
|
181
|
+
theme.setFontFamily(role, family);
|
|
182
|
+
theme.setFontSize(role, size);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// A font name the runtime rejects must not stop the colour layer from
|
|
186
|
+
// applying; the panel surfaces the rejected value instead.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
ctx.effect(() => form.subscribe(refresh));
|
|
192
|
+
ctx.effect(() => () => {
|
|
193
|
+
disposeLayer?.();
|
|
194
|
+
});
|
|
195
|
+
refresh();
|
|
196
|
+
try {
|
|
197
|
+
registerPanel(ctx, () => panelState(form), panelActions(form, readDocument), form);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
reportFailure(ctx, error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Register the Theme Studio page into the settings section list.
|
|
205
|
+
*
|
|
206
|
+
* Kept separate from `apply` so the section registration can be exercised in a
|
|
207
|
+
* unit test with a stub slots service, without a browser.
|
|
208
|
+
*
|
|
209
|
+
* @param ctx - client context.
|
|
210
|
+
* @param getState - supplies the current panel state.
|
|
211
|
+
* @param actions - the reactions the panel invokes.
|
|
212
|
+
*/
|
|
213
|
+
function registerPanel(ctx, getState, actions, settingsForm) {
|
|
214
|
+
const slots = ctx.slots;
|
|
215
|
+
if (ReactImpl === undefined)
|
|
216
|
+
return;
|
|
217
|
+
const React = ReactImpl;
|
|
218
|
+
/**
|
|
219
|
+
* The slot component.
|
|
220
|
+
*
|
|
221
|
+
* It is a real component rather than a direct call, so hooks belong to it and a
|
|
222
|
+
* settings change re-renders it. The subscription is what makes a panel write
|
|
223
|
+
* visible without reopening the page: `getState()` reads the same form the
|
|
224
|
+
* action just wrote, and the subscription fires when the Host echoes it back.
|
|
225
|
+
*
|
|
226
|
+
* @returns the Theme Studio element.
|
|
227
|
+
*/
|
|
228
|
+
const Panel = () => {
|
|
229
|
+
const [, force] = React.useState(0);
|
|
230
|
+
React.useEffect(() => {
|
|
231
|
+
// Re-render when the Host publishes a new section for this namespace.
|
|
232
|
+
let tick = 0;
|
|
233
|
+
const dispose = settingsForm.subscribe(() => {
|
|
234
|
+
tick += 1;
|
|
235
|
+
force(tick);
|
|
236
|
+
});
|
|
237
|
+
return dispose;
|
|
238
|
+
}, []);
|
|
239
|
+
return ThemeStudio(React, { state: getState(), actions, presets: PRESETS });
|
|
240
|
+
};
|
|
241
|
+
ctx.effect(() => {
|
|
242
|
+
slots.inject('settings.section', () => slots.register({ name: 'settings.section', id: 'ui-custom', order: 11, label: () => 'Theme Studio' }, Panel));
|
|
243
|
+
return () => { };
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Read the stored customization document.
|
|
248
|
+
* @param form - the settings form for this plugin's namespace.
|
|
249
|
+
* @returns the document the panel edits.
|
|
250
|
+
*/
|
|
251
|
+
function readDocument(form) {
|
|
252
|
+
return asDocument(form.getSnapshot().value);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The document the panel last rendered, so its actions can merge one field
|
|
256
|
+
* without restating the whole section from a possibly stale snapshot.
|
|
257
|
+
*/
|
|
258
|
+
let currentDocument;
|
|
259
|
+
/**
|
|
260
|
+
* Supply the current document for the panel's reads.
|
|
261
|
+
* @param form - the settings form.
|
|
262
|
+
* @returns the document.
|
|
263
|
+
*/
|
|
264
|
+
function getDocument(form) {
|
|
265
|
+
currentDocument = readDocument(form);
|
|
266
|
+
return currentDocument;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Build the panel's state from the stored document.
|
|
270
|
+
* @param form - the settings form.
|
|
271
|
+
* @returns the state the panel renders.
|
|
272
|
+
*/
|
|
273
|
+
function panelState(form) {
|
|
274
|
+
const document = getDocument(form);
|
|
275
|
+
const resolved = resolveCustomization(document);
|
|
276
|
+
const overrides = document.tokens;
|
|
277
|
+
return {
|
|
278
|
+
basePreset: document.basePreset,
|
|
279
|
+
tokens: TOKEN_SPECS.map((spec) => ({
|
|
280
|
+
name: spec.name,
|
|
281
|
+
label: spec.label,
|
|
282
|
+
description: spec.description,
|
|
283
|
+
override: overrides[spec.name],
|
|
284
|
+
})),
|
|
285
|
+
typography: resolved.typography,
|
|
286
|
+
geometry: document.geometry,
|
|
287
|
+
warnings: resolved.warnings,
|
|
288
|
+
writable: form.getSnapshot().writable,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Build the panel's write actions.
|
|
293
|
+
*
|
|
294
|
+
* Every action writes a whole field through the settings form, which serializes
|
|
295
|
+
* the mutation against the namespace revision. Tokens are written as one field
|
|
296
|
+
* (`tokens`) rather than per-name, because the schema models them as a dict and
|
|
297
|
+
* a partial write would drop the others.
|
|
298
|
+
*
|
|
299
|
+
* @param form - the settings form.
|
|
300
|
+
* @param document - reads the document the action should start from.
|
|
301
|
+
* @returns the actions the panel invokes.
|
|
302
|
+
*/
|
|
303
|
+
function panelActions(form, document) {
|
|
304
|
+
const write = (field, value) => {
|
|
305
|
+
void form.set(field, value);
|
|
306
|
+
};
|
|
307
|
+
const withTokens = (mutate) => {
|
|
308
|
+
const base = document(form);
|
|
309
|
+
write('tokens', mutate({ ...base.tokens }));
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
setPreset: (id) => write('basePreset', id),
|
|
313
|
+
setToken: (name, light, dark) => withTokens((tokens) => ({ ...tokens, [name]: { light, dark } })),
|
|
314
|
+
clearToken: (name) => withTokens((tokens) => {
|
|
315
|
+
const next = { ...tokens };
|
|
316
|
+
delete next[name];
|
|
317
|
+
return next;
|
|
318
|
+
}),
|
|
319
|
+
setFont: (role, value) => {
|
|
320
|
+
const base = document(form);
|
|
321
|
+
const key = role === 'text' ? 'textFontFamily' : role === 'code' ? 'codeFontFamily' : 'terminalFontFamily';
|
|
322
|
+
write('typography', { ...base.typography, [key]: value });
|
|
323
|
+
},
|
|
324
|
+
setFontSize: (role, px) => {
|
|
325
|
+
const base = document(form);
|
|
326
|
+
const key = role === 'text' ? 'fontSize' : role === 'code' ? 'codeFontSize' : 'terminalFontSize';
|
|
327
|
+
write('typography', { ...base.typography, [key]: px });
|
|
328
|
+
},
|
|
329
|
+
setRadius: (px) => {
|
|
330
|
+
const base = document(form);
|
|
331
|
+
write('geometry', { ...base.geometry, radius: px });
|
|
332
|
+
},
|
|
333
|
+
setDensity: (density) => {
|
|
334
|
+
const base = document(form);
|
|
335
|
+
write('geometry', { ...base.geometry, density: density });
|
|
336
|
+
},
|
|
337
|
+
resetAll: () => write('tokens', {}),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=index.js.map
|
|
341
|
+
Object.assign(exports, { useReact: useReact, apply: apply, registerPanel: registerPanel, OVERRIDE_SOURCE: OVERRIDE_SOURCE, inject: inject });
|
|
342
|
+
|
|
343
|
+
},
|
|
344
|
+
"./spec.js": (module, exports, __require) => {
|
|
345
|
+
/**
|
|
346
|
+
* The token catalog and the shipped presets.
|
|
347
|
+
*
|
|
348
|
+
* Every override must supply **both** palette modes. The theme runtime requires
|
|
349
|
+
* it (`ThemeTokenModes`), because a token defined for only one scheme goes
|
|
350
|
+
* illegible when the user flips to the other — so this module represents that
|
|
351
|
+
* requirement in the type rather than trusting each caller to remember it.
|
|
352
|
+
*
|
|
353
|
+
* @module @d0nj/dsh-ui-custom/spec
|
|
354
|
+
*/
|
|
355
|
+
/** Groups in display order, with their headings. */
|
|
356
|
+
const TOKEN_GROUPS = [
|
|
357
|
+
{ id: 'surface', label: 'Surfaces' },
|
|
358
|
+
{ id: 'border', label: 'Borders' },
|
|
359
|
+
{ id: 'text', label: 'Text' },
|
|
360
|
+
{ id: 'accent', label: 'Accent' },
|
|
361
|
+
{ id: 'state', label: 'State colours' },
|
|
362
|
+
];
|
|
363
|
+
/**
|
|
364
|
+
* The documented alias tokens.
|
|
365
|
+
*
|
|
366
|
+
* These are the tokens the theme Inspect provider reports for this build, so the
|
|
367
|
+
* editor never offers a row that would silently do nothing.
|
|
368
|
+
*/
|
|
369
|
+
const TOKEN_SPECS = [
|
|
370
|
+
{
|
|
371
|
+
name: '--dsw-alias-bg-base',
|
|
372
|
+
label: 'Base background',
|
|
373
|
+
group: 'surface',
|
|
374
|
+
kind: 'color',
|
|
375
|
+
description: 'Application base background.',
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: '--dsw-alias-bg-layer-1',
|
|
379
|
+
label: 'Raised surface',
|
|
380
|
+
group: 'surface',
|
|
381
|
+
kind: 'color',
|
|
382
|
+
description: 'Primary raised surface background.',
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: '--dsw-alias-bg-layer-2',
|
|
386
|
+
label: 'Nested surface',
|
|
387
|
+
group: 'surface',
|
|
388
|
+
kind: 'color',
|
|
389
|
+
description: 'Secondary nested surface background.',
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: '--dsw-alias-bg-overlay',
|
|
393
|
+
label: 'Overlay',
|
|
394
|
+
group: 'surface',
|
|
395
|
+
kind: 'color',
|
|
396
|
+
description: 'Overlay and popover background.',
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: '--dsw-specific-sidebar-fill',
|
|
400
|
+
label: 'Sidebar',
|
|
401
|
+
group: 'surface',
|
|
402
|
+
kind: 'color',
|
|
403
|
+
description: 'Sidebar column and title-row background.',
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: '--dsw-alias-border-l1',
|
|
407
|
+
label: 'Border, subtle',
|
|
408
|
+
group: 'border',
|
|
409
|
+
kind: 'color',
|
|
410
|
+
description: 'Primary subtle border.',
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: '--dsw-alias-border-l2',
|
|
414
|
+
label: 'Border, strong',
|
|
415
|
+
group: 'border',
|
|
416
|
+
kind: 'color',
|
|
417
|
+
description: 'Secondary stronger border.',
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: '--dsw-alias-label-primary',
|
|
421
|
+
label: 'Primary text',
|
|
422
|
+
group: 'text',
|
|
423
|
+
kind: 'color',
|
|
424
|
+
description: 'Primary text colour.',
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: '--dsw-alias-label-secondary',
|
|
428
|
+
label: 'Secondary text',
|
|
429
|
+
group: 'text',
|
|
430
|
+
kind: 'color',
|
|
431
|
+
description: 'Secondary text colour.',
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: '--dsw-alias-brand-primary',
|
|
435
|
+
label: 'Brand accent',
|
|
436
|
+
group: 'accent',
|
|
437
|
+
kind: 'color',
|
|
438
|
+
description: 'Primary brand accent.',
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: '--dsw-alias-state-error-primary',
|
|
442
|
+
label: 'Error',
|
|
443
|
+
group: 'state',
|
|
444
|
+
kind: 'color',
|
|
445
|
+
description: 'Primary error state colour.',
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: '--dsw-alias-state-success-primary',
|
|
449
|
+
label: 'Success',
|
|
450
|
+
group: 'state',
|
|
451
|
+
kind: 'color',
|
|
452
|
+
description: 'Primary success state colour.',
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: '--dsw-alias-state-warn-primary',
|
|
456
|
+
label: 'Warning',
|
|
457
|
+
group: 'state',
|
|
458
|
+
kind: 'color',
|
|
459
|
+
description: 'Primary warning state colour.',
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: '--dsw-alias-state-idle-primary',
|
|
463
|
+
label: 'Idle',
|
|
464
|
+
group: 'state',
|
|
465
|
+
kind: 'color',
|
|
466
|
+
description: 'Primary inactive state colour.',
|
|
467
|
+
},
|
|
468
|
+
];
|
|
469
|
+
/** Every documented token name, for validation. */
|
|
470
|
+
const TOKEN_NAMES = TOKEN_SPECS.map((spec) => spec.name);
|
|
471
|
+
/**
|
|
472
|
+
* Look up one token's specification.
|
|
473
|
+
* @param name - CSS custom property name.
|
|
474
|
+
* @returns the specification, or `undefined` when the token is not documented.
|
|
475
|
+
*/
|
|
476
|
+
function tokenSpec(name) {
|
|
477
|
+
return TOKEN_SPECS.find((spec) => spec.name === name);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Shipped presets.
|
|
481
|
+
*
|
|
482
|
+
* Each is a complete token set rather than a partial overlay, so selecting one
|
|
483
|
+
* gives a predictable result no matter what the generated base palette does.
|
|
484
|
+
* The colours are chosen to keep body text at or above WCAG AA contrast against
|
|
485
|
+
* their own surfaces, which the tests check.
|
|
486
|
+
*/
|
|
487
|
+
const PRESETS = [
|
|
488
|
+
{
|
|
489
|
+
id: 'midnight',
|
|
490
|
+
label: 'Midnight',
|
|
491
|
+
description: 'Deep blue-black surfaces with a cool accent.',
|
|
492
|
+
colorScheme: 'dark',
|
|
493
|
+
tokens: {
|
|
494
|
+
'--dsw-alias-bg-base': { light: '#f4f6fb', dark: '#0b1020' },
|
|
495
|
+
'--dsw-alias-bg-layer-1': { light: '#ffffff', dark: '#141a2e' },
|
|
496
|
+
'--dsw-alias-bg-layer-2': { light: '#eef1f8', dark: '#1c2440' },
|
|
497
|
+
'--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#1f2745' },
|
|
498
|
+
'--dsw-specific-sidebar-fill': { light: '#e9edf6', dark: '#080d1a' },
|
|
499
|
+
'--dsw-alias-border-l1': { light: '#d9dfec', dark: '#232c48' },
|
|
500
|
+
'--dsw-alias-border-l2': { light: '#c2cade', dark: '#33405f' },
|
|
501
|
+
'--dsw-alias-label-primary': { light: '#111827', dark: '#e8ecf7' },
|
|
502
|
+
'--dsw-alias-label-secondary': { light: '#4b5563', dark: '#9aa5bf' },
|
|
503
|
+
'--dsw-alias-brand-primary': { light: '#2563eb', dark: '#7aa2ff' },
|
|
504
|
+
'--dsw-alias-state-error-primary': { light: '#b91c1c', dark: '#ff8f8f' },
|
|
505
|
+
'--dsw-alias-state-success-primary': { light: '#047857', dark: '#5fd4a0' },
|
|
506
|
+
'--dsw-alias-state-warn-primary': { light: '#a16207', dark: '#f5c26b' },
|
|
507
|
+
'--dsw-alias-state-idle-primary': { light: '#6b7280', dark: '#8792ab' },
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
id: 'paper',
|
|
512
|
+
label: 'Paper',
|
|
513
|
+
description: 'Warm off-white surfaces with ink-black text.',
|
|
514
|
+
colorScheme: 'light',
|
|
515
|
+
tokens: {
|
|
516
|
+
'--dsw-alias-bg-base': { light: '#faf7f0', dark: '#17150f' },
|
|
517
|
+
'--dsw-alias-bg-layer-1': { light: '#fffdf8', dark: '#221f16' },
|
|
518
|
+
'--dsw-alias-bg-layer-2': { light: '#f3efe4', dark: '#2b2619' },
|
|
519
|
+
'--dsw-alias-bg-overlay': { light: '#fffdf8', dark: '#2f2a1c' },
|
|
520
|
+
'--dsw-specific-sidebar-fill': { light: '#f2ecdf', dark: '#12100a' },
|
|
521
|
+
'--dsw-alias-border-l1': { light: '#e2dcc9', dark: '#3a3323' },
|
|
522
|
+
'--dsw-alias-border-l2': { light: '#cfc7ad', dark: '#4d442e' },
|
|
523
|
+
'--dsw-alias-label-primary': { light: '#1c1a14', dark: '#f0ebdd' },
|
|
524
|
+
'--dsw-alias-label-secondary': { light: '#5c5545', dark: '#b3aa92' },
|
|
525
|
+
'--dsw-alias-brand-primary': { light: '#8a4b12', dark: '#e0a061' },
|
|
526
|
+
'--dsw-alias-state-error-primary': { light: '#9b1c1c', dark: '#f08a8a' },
|
|
527
|
+
'--dsw-alias-state-success-primary': { light: '#3f6212', dark: '#9fcb63' },
|
|
528
|
+
'--dsw-alias-state-warn-primary': { light: '#8a5a00', dark: '#e8c063' },
|
|
529
|
+
'--dsw-alias-state-idle-primary': { light: '#6f6757', dark: '#8e856f' },
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
id: 'high-contrast',
|
|
534
|
+
label: 'High contrast',
|
|
535
|
+
description: 'Maximum separation for low-vision use.',
|
|
536
|
+
colorScheme: 'dark',
|
|
537
|
+
tokens: {
|
|
538
|
+
'--dsw-alias-bg-base': { light: '#ffffff', dark: '#000000' },
|
|
539
|
+
'--dsw-alias-bg-layer-1': { light: '#ffffff', dark: '#0a0a0a' },
|
|
540
|
+
'--dsw-alias-bg-layer-2': { light: '#f2f2f2', dark: '#141414' },
|
|
541
|
+
'--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#141414' },
|
|
542
|
+
'--dsw-specific-sidebar-fill': { light: '#ebebeb', dark: '#000000' },
|
|
543
|
+
'--dsw-alias-border-l1': { light: '#767676', dark: '#8a8a8a' },
|
|
544
|
+
'--dsw-alias-border-l2': { light: '#000000', dark: '#ffffff' },
|
|
545
|
+
'--dsw-alias-label-primary': { light: '#000000', dark: '#ffffff' },
|
|
546
|
+
'--dsw-alias-label-secondary': { light: '#3d3d3d', dark: '#d6d6d6' },
|
|
547
|
+
'--dsw-alias-brand-primary': { light: '#0033cc', dark: '#8fb0ff' },
|
|
548
|
+
'--dsw-alias-state-error-primary': { light: '#a10000', dark: '#ff9b9b' },
|
|
549
|
+
'--dsw-alias-state-success-primary': { light: '#005c2b', dark: '#7dffb0' },
|
|
550
|
+
'--dsw-alias-state-warn-primary': { light: '#6b4500', dark: '#ffd479' },
|
|
551
|
+
'--dsw-alias-state-idle-primary': { light: '#4a4a4a', dark: '#b8b8b8' },
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
// Palette: https://draculatheme.com — bg #282a36, fg #f8f8f2, comment #6272a4.
|
|
556
|
+
id: 'dracula',
|
|
557
|
+
label: 'Dracula',
|
|
558
|
+
description: 'The classic purple-on-charcoal editor theme.',
|
|
559
|
+
colorScheme: 'dark',
|
|
560
|
+
tokens: {
|
|
561
|
+
'--dsw-alias-bg-base': { light: '#fffbeb', dark: '#282a36' },
|
|
562
|
+
'--dsw-alias-bg-layer-1': { light: '#ffffff', dark: '#21222c' },
|
|
563
|
+
'--dsw-alias-bg-layer-2': { light: '#f2ecd9', dark: '#343746' },
|
|
564
|
+
'--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#44475a' },
|
|
565
|
+
'--dsw-specific-sidebar-fill': { light: '#f2ecd9', dark: '#21222c' },
|
|
566
|
+
'--dsw-alias-border-l1': { light: '#dcd6c3', dark: '#44475a' },
|
|
567
|
+
'--dsw-alias-border-l2': { light: '#c4bda6', dark: '#6272a4' },
|
|
568
|
+
'--dsw-alias-label-primary': { light: '#1f1f1f', dark: '#f8f8f2' },
|
|
569
|
+
'--dsw-alias-label-secondary': { light: '#6c664b', dark: '#8693bd' },
|
|
570
|
+
'--dsw-alias-brand-primary': { light: '#644ac9', dark: '#bd93f9' },
|
|
571
|
+
'--dsw-alias-state-error-primary': { light: '#cb3a2a', dark: '#ff5555' },
|
|
572
|
+
'--dsw-alias-state-success-primary': { light: '#14710a', dark: '#50fa7b' },
|
|
573
|
+
'--dsw-alias-state-warn-primary': { light: '#846e15', dark: '#f1fa8c' },
|
|
574
|
+
'--dsw-alias-state-idle-primary': { light: '#6c664b', dark: '#6272a4' },
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
// Palette: https://www.nordtheme.com — nord0 #2e3440, nord4 #d8dee9, nord8 #88c0d0.
|
|
579
|
+
id: 'nord',
|
|
580
|
+
label: 'Nord',
|
|
581
|
+
description: 'Arctic, north-bluish, with muted frost accents.',
|
|
582
|
+
colorScheme: 'dark',
|
|
583
|
+
tokens: {
|
|
584
|
+
'--dsw-alias-bg-base': { light: '#eceff4', dark: '#2e3440' },
|
|
585
|
+
'--dsw-alias-bg-layer-1': { light: '#ffffff', dark: '#3b4252' },
|
|
586
|
+
'--dsw-alias-bg-layer-2': { light: '#e5e9f0', dark: '#434c5e' },
|
|
587
|
+
'--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#4c566a' },
|
|
588
|
+
'--dsw-specific-sidebar-fill': { light: '#e5e9f0', dark: '#292e39' },
|
|
589
|
+
'--dsw-alias-border-l1': { light: '#d8dee9', dark: '#434c5e' },
|
|
590
|
+
'--dsw-alias-border-l2': { light: '#b8c2d1', dark: '#4c566a' },
|
|
591
|
+
'--dsw-alias-label-primary': { light: '#2e3440', dark: '#eceff4' },
|
|
592
|
+
'--dsw-alias-label-secondary': { light: '#4c566a', dark: '#a3adc0' },
|
|
593
|
+
'--dsw-alias-brand-primary': { light: '#5e81ac', dark: '#88c0d0' },
|
|
594
|
+
'--dsw-alias-state-error-primary': { light: '#a33f4a', dark: '#bf616a' },
|
|
595
|
+
'--dsw-alias-state-success-primary': { light: '#5a7d52', dark: '#a3be8c' },
|
|
596
|
+
'--dsw-alias-state-warn-primary': { light: '#8a6a2f', dark: '#ebcb8b' },
|
|
597
|
+
'--dsw-alias-state-idle-primary': { light: '#6d7b8c', dark: '#7b88a1' },
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
// Palette: https://github.com/catppuccin/palette — Mocha base #1e1e2e, text #cdd6f4, mauve #cba6f7.
|
|
602
|
+
id: 'catppuccin-mocha',
|
|
603
|
+
label: 'Catppuccin Mocha',
|
|
604
|
+
description: 'Soothing pastels on the darkest Catppuccin flavour.',
|
|
605
|
+
colorScheme: 'dark',
|
|
606
|
+
tokens: {
|
|
607
|
+
'--dsw-alias-bg-base': { light: '#eff1f5', dark: '#1e1e2e' },
|
|
608
|
+
'--dsw-alias-bg-layer-1': { light: '#ffffff', dark: '#181825' },
|
|
609
|
+
'--dsw-alias-bg-layer-2': { light: '#e6e9ef', dark: '#313244' },
|
|
610
|
+
'--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#45475a' },
|
|
611
|
+
'--dsw-specific-sidebar-fill': { light: '#e6e9ef', dark: '#11111b' },
|
|
612
|
+
'--dsw-alias-border-l1': { light: '#ccd0da', dark: '#313244' },
|
|
613
|
+
'--dsw-alias-border-l2': { light: '#acb0be', dark: '#45475a' },
|
|
614
|
+
'--dsw-alias-label-primary': { light: '#4c4f69', dark: '#cdd6f4' },
|
|
615
|
+
'--dsw-alias-label-secondary': { light: '#6c6f85', dark: '#a6adc8' },
|
|
616
|
+
'--dsw-alias-brand-primary': { light: '#8839ef', dark: '#cba6f7' },
|
|
617
|
+
'--dsw-alias-state-error-primary': { light: '#d20f39', dark: '#f38ba8' },
|
|
618
|
+
'--dsw-alias-state-success-primary': { light: '#40a02b', dark: '#a6e3a1' },
|
|
619
|
+
'--dsw-alias-state-warn-primary': { light: '#df8e1d', dark: '#f9e2af' },
|
|
620
|
+
'--dsw-alias-state-idle-primary': { light: '#8c8fa1', dark: '#6c7086' },
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
// Palette: https://github.com/morhetz/gruvbox — dark0 #282828, fg1 #ebdbb2, orange #fe8019.
|
|
625
|
+
id: 'gruvbox',
|
|
626
|
+
label: 'Gruvbox',
|
|
627
|
+
description: 'Retro groove warmth with earthy amber accents.',
|
|
628
|
+
colorScheme: 'dark',
|
|
629
|
+
tokens: {
|
|
630
|
+
'--dsw-alias-bg-base': { light: '#fbf1c7', dark: '#282828' },
|
|
631
|
+
'--dsw-alias-bg-layer-1': { light: '#f9f5d7', dark: '#1d2021' },
|
|
632
|
+
'--dsw-alias-bg-layer-2': { light: '#f2e5bc', dark: '#3c3836' },
|
|
633
|
+
'--dsw-alias-bg-overlay': { light: '#f9f5d7', dark: '#504945' },
|
|
634
|
+
'--dsw-specific-sidebar-fill': { light: '#f2e5bc', dark: '#1d2021' },
|
|
635
|
+
'--dsw-alias-border-l1': { light: '#ebdbb2', dark: '#3c3836' },
|
|
636
|
+
'--dsw-alias-border-l2': { light: '#d5c4a1', dark: '#504945' },
|
|
637
|
+
'--dsw-alias-label-primary': { light: '#3c3836', dark: '#ebdbb2' },
|
|
638
|
+
'--dsw-alias-label-secondary': { light: '#665c54', dark: '#a89984' },
|
|
639
|
+
'--dsw-alias-brand-primary': { light: '#af3a03', dark: '#fe8019' },
|
|
640
|
+
'--dsw-alias-state-error-primary': { light: '#9d0006', dark: '#fb4934' },
|
|
641
|
+
'--dsw-alias-state-success-primary': { light: '#79740e', dark: '#b8bb26' },
|
|
642
|
+
'--dsw-alias-state-warn-primary': { light: '#b57614', dark: '#fabd2f' },
|
|
643
|
+
'--dsw-alias-state-idle-primary': { light: '#928374', dark: '#928374' },
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
// Palette: https://github.com/atom/one-dark-syntax — bg #282c34, fg #abb2bf, blue #61afef.
|
|
648
|
+
id: 'one-dark',
|
|
649
|
+
label: 'One Dark',
|
|
650
|
+
description: "Atom's iconic blue-on-graphite editor theme.",
|
|
651
|
+
colorScheme: 'dark',
|
|
652
|
+
tokens: {
|
|
653
|
+
'--dsw-alias-bg-base': { light: '#fafafa', dark: '#282c34' },
|
|
654
|
+
'--dsw-alias-bg-layer-1': { light: '#ffffff', dark: '#21252b' },
|
|
655
|
+
'--dsw-alias-bg-layer-2': { light: '#eaeaeb', dark: '#3e4451' },
|
|
656
|
+
'--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#3e4451' },
|
|
657
|
+
'--dsw-specific-sidebar-fill': { light: '#eaeaeb', dark: '#21252b' },
|
|
658
|
+
'--dsw-alias-border-l1': { light: '#dbdbdc', dark: '#3e4451' },
|
|
659
|
+
'--dsw-alias-border-l2': { light: '#c0c0c2', dark: '#5c6370' },
|
|
660
|
+
'--dsw-alias-label-primary': { light: '#383a42', dark: '#abb2bf' },
|
|
661
|
+
'--dsw-alias-label-secondary': { light: '#696c77', dark: '#8b93a1' },
|
|
662
|
+
'--dsw-alias-brand-primary': { light: '#4078f2', dark: '#61afef' },
|
|
663
|
+
'--dsw-alias-state-error-primary': { light: '#e45649', dark: '#e06c75' },
|
|
664
|
+
'--dsw-alias-state-success-primary': { light: '#50a14f', dark: '#98c379' },
|
|
665
|
+
'--dsw-alias-state-warn-primary': { light: '#986801', dark: '#e5c07b' },
|
|
666
|
+
'--dsw-alias-state-idle-primary': { light: '#a0a1a7', dark: '#5c6370' },
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
// Palette: https://ethanschoonover.com/solarized — base03 #002b36, base0 #839496, blue #268bd2.
|
|
671
|
+
id: 'solarized',
|
|
672
|
+
label: 'Solarized Dark',
|
|
673
|
+
description: 'Precision low-contrast colours from Ethan Schoonover.',
|
|
674
|
+
colorScheme: 'dark',
|
|
675
|
+
tokens: {
|
|
676
|
+
'--dsw-alias-bg-base': { light: '#fdf6e3', dark: '#002b36' },
|
|
677
|
+
'--dsw-alias-bg-layer-1': { light: '#fffbf0', dark: '#073642' },
|
|
678
|
+
'--dsw-alias-bg-layer-2': { light: '#eee8d5', dark: '#0d4756' },
|
|
679
|
+
'--dsw-alias-bg-overlay': { light: '#fffbf0', dark: '#0d4756' },
|
|
680
|
+
'--dsw-specific-sidebar-fill': { light: '#eee8d5', dark: '#00252e' },
|
|
681
|
+
'--dsw-alias-border-l1': { light: '#ddd6c1', dark: '#0d4756' },
|
|
682
|
+
'--dsw-alias-border-l2': { light: '#c9c2ac', dark: '#586e75' },
|
|
683
|
+
'--dsw-alias-label-primary': { light: '#073642', dark: '#93a1a1' },
|
|
684
|
+
'--dsw-alias-label-secondary': { light: '#586e75', dark: '#839496' },
|
|
685
|
+
'--dsw-alias-brand-primary': { light: '#268bd2', dark: '#268bd2' },
|
|
686
|
+
'--dsw-alias-state-error-primary': { light: '#dc322f', dark: '#dc322f' },
|
|
687
|
+
'--dsw-alias-state-success-primary': { light: '#859900', dark: '#859900' },
|
|
688
|
+
'--dsw-alias-state-warn-primary': { light: '#b58900', dark: '#b58900' },
|
|
689
|
+
'--dsw-alias-state-idle-primary': { light: '#93a1a1', dark: '#586e75' },
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
// Palette: https://rosepinetheme.com — base #191724, text #e0def4, rose #ebbcba.
|
|
694
|
+
id: 'rose-pine',
|
|
695
|
+
label: 'Rosé Pine',
|
|
696
|
+
description: 'All natural pine, faux fur and a bit of Soho.',
|
|
697
|
+
colorScheme: 'dark',
|
|
698
|
+
tokens: {
|
|
699
|
+
'--dsw-alias-bg-base': { light: '#faf4ed', dark: '#191724' },
|
|
700
|
+
'--dsw-alias-bg-layer-1': { light: '#fffaf3', dark: '#1f1d2e' },
|
|
701
|
+
'--dsw-alias-bg-layer-2': { light: '#f2e9e1', dark: '#26233a' },
|
|
702
|
+
'--dsw-alias-bg-overlay': { light: '#fffaf3', dark: '#26233a' },
|
|
703
|
+
'--dsw-specific-sidebar-fill': { light: '#f2e9e1', dark: '#1f1d2e' },
|
|
704
|
+
'--dsw-alias-border-l1': { light: '#e5ded5', dark: '#26233a' },
|
|
705
|
+
'--dsw-alias-border-l2': { light: '#d3cbc2', dark: '#403d52' },
|
|
706
|
+
'--dsw-alias-label-primary': { light: '#575279', dark: '#e0def4' },
|
|
707
|
+
'--dsw-alias-label-secondary': { light: '#797593', dark: '#908caa' },
|
|
708
|
+
'--dsw-alias-brand-primary': { light: '#b4637a', dark: '#ebbcba' },
|
|
709
|
+
'--dsw-alias-state-error-primary': { light: '#b4637a', dark: '#eb6f92' },
|
|
710
|
+
'--dsw-alias-state-success-primary': { light: '#286983', dark: '#9ccfd8' },
|
|
711
|
+
'--dsw-alias-state-warn-primary': { light: '#ea9d34', dark: '#f6c177' },
|
|
712
|
+
'--dsw-alias-state-idle-primary': { light: '#9893a5', dark: '#6e6a86' },
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
// Palette: https://github.com/enkia/tokyo-night-vscode-theme — bg #1a1b26, fg #a9b1d6, blue #7aa2f7.
|
|
717
|
+
id: 'tokyo-night',
|
|
718
|
+
label: 'Tokyo Night',
|
|
719
|
+
description: 'A clean dark theme that celebrates downtown Tokyo at night.',
|
|
720
|
+
colorScheme: 'dark',
|
|
721
|
+
tokens: {
|
|
722
|
+
'--dsw-alias-bg-base': { light: '#e9e9ed', dark: '#1a1b26' },
|
|
723
|
+
'--dsw-alias-bg-layer-1': { light: '#f5f5f7', dark: '#16161e' },
|
|
724
|
+
'--dsw-alias-bg-layer-2': { light: '#dcdce3', dark: '#292e42' },
|
|
725
|
+
'--dsw-alias-bg-overlay': { light: '#f5f5f7', dark: '#292e42' },
|
|
726
|
+
'--dsw-specific-sidebar-fill': { light: '#dcdce3', dark: '#16161e' },
|
|
727
|
+
'--dsw-alias-border-l1': { light: '#d0d0d9', dark: '#292e42' },
|
|
728
|
+
'--dsw-alias-border-l2': { light: '#b6b8c4', dark: '#3b4261' },
|
|
729
|
+
'--dsw-alias-label-primary': { light: '#343b58', dark: '#a9b1d6' },
|
|
730
|
+
'--dsw-alias-label-secondary': { light: '#565f89', dark: '#7d86a4' },
|
|
731
|
+
'--dsw-alias-brand-primary': { light: '#34548a', dark: '#7aa2f7' },
|
|
732
|
+
'--dsw-alias-state-error-primary': { light: '#8c4351', dark: '#f7768e' },
|
|
733
|
+
'--dsw-alias-state-success-primary': { light: '#485e30', dark: '#9ece6a' },
|
|
734
|
+
'--dsw-alias-state-warn-primary': { light: '#8f5e15', dark: '#e0af68' },
|
|
735
|
+
'--dsw-alias-state-idle-primary': { light: '#9699a3', dark: '#565f89' },
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
// Palette: https://github.com/ayu-theme — ayu dark bg #0d1017, fg #bfbdb6, orange #ff8f40.
|
|
740
|
+
id: 'ayu-dark',
|
|
741
|
+
label: 'Ayu Dark',
|
|
742
|
+
description: 'Minimal dark theme with a bright amber accent.',
|
|
743
|
+
colorScheme: 'dark',
|
|
744
|
+
tokens: {
|
|
745
|
+
'--dsw-alias-bg-base': { light: '#fafafa', dark: '#0d1017' },
|
|
746
|
+
'--dsw-alias-bg-layer-1': { light: '#ffffff', dark: '#131721' },
|
|
747
|
+
'--dsw-alias-bg-layer-2': { light: '#f0f0ee', dark: '#1f2430' },
|
|
748
|
+
'--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#1f2430' },
|
|
749
|
+
'--dsw-specific-sidebar-fill': { light: '#f0f0ee', dark: '#0b0e14' },
|
|
750
|
+
'--dsw-alias-border-l1': { light: '#e2e2de', dark: '#1f2430' },
|
|
751
|
+
'--dsw-alias-border-l2': { light: '#c9c9c4', dark: '#2d3343' },
|
|
752
|
+
'--dsw-alias-label-primary': { light: '#242936', dark: '#bfbdb6' },
|
|
753
|
+
'--dsw-alias-label-secondary': { light: '#5c6773', dark: '#8a95a1' },
|
|
754
|
+
'--dsw-alias-brand-primary': { light: '#e0882e', dark: '#ff8f40' },
|
|
755
|
+
'--dsw-alias-state-error-primary': { light: '#d95757', dark: '#f26d78' },
|
|
756
|
+
'--dsw-alias-state-success-primary': { light: '#6c9e4a', dark: '#aad94c' },
|
|
757
|
+
'--dsw-alias-state-warn-primary': { light: '#c28c1f', dark: '#e6b450' },
|
|
758
|
+
'--dsw-alias-state-idle-primary': { light: '#8a9199', dark: '#5c6773' },
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
// Palette: https://github.com/microsoft/vscode — GitHub Dark bg #0d1117, fg #c9d1d9, blue #58a6ff.
|
|
763
|
+
id: 'github-dark',
|
|
764
|
+
label: 'GitHub Dark',
|
|
765
|
+
description: 'The familiar GitHub interface palette.',
|
|
766
|
+
colorScheme: 'dark',
|
|
767
|
+
tokens: {
|
|
768
|
+
'--dsw-alias-bg-base': { light: '#ffffff', dark: '#0d1117' },
|
|
769
|
+
'--dsw-alias-bg-layer-1': { light: '#f6f8fa', dark: '#161b22' },
|
|
770
|
+
'--dsw-alias-bg-layer-2': { light: '#eaeef2', dark: '#21262d' },
|
|
771
|
+
'--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#21262d' },
|
|
772
|
+
'--dsw-specific-sidebar-fill': { light: '#f6f8fa', dark: '#010409' },
|
|
773
|
+
'--dsw-alias-border-l1': { light: '#d0d7de', dark: '#30363d' },
|
|
774
|
+
'--dsw-alias-border-l2': { light: '#afb8c1', dark: '#484f58' },
|
|
775
|
+
'--dsw-alias-label-primary': { light: '#1f2328', dark: '#c9d1d9' },
|
|
776
|
+
'--dsw-alias-label-secondary': { light: '#656d76', dark: '#8b949e' },
|
|
777
|
+
'--dsw-alias-brand-primary': { light: '#0969da', dark: '#58a6ff' },
|
|
778
|
+
'--dsw-alias-state-error-primary': { light: '#cf222e', dark: '#f85149' },
|
|
779
|
+
'--dsw-alias-state-success-primary': { light: '#1a7f37', dark: '#3fb950' },
|
|
780
|
+
'--dsw-alias-state-warn-primary': { light: '#9a6700', dark: '#d29922' },
|
|
781
|
+
'--dsw-alias-state-idle-primary': { light: '#6e7781', dark: '#484f58' },
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
// Palette: https://github.com/Monokai/monokai — bg #272822, fg #f8f8f2, pink #f92672.
|
|
786
|
+
id: 'monokai',
|
|
787
|
+
label: 'Monokai',
|
|
788
|
+
description: 'The Sublime Text original, with its hot-pink accent.',
|
|
789
|
+
colorScheme: 'dark',
|
|
790
|
+
tokens: {
|
|
791
|
+
'--dsw-alias-bg-base': { light: '#fafaf5', dark: '#272822' },
|
|
792
|
+
'--dsw-alias-bg-layer-1': { light: '#ffffff', dark: '#1e1f1c' },
|
|
793
|
+
'--dsw-alias-bg-layer-2': { light: '#f0efe5', dark: '#3e3d32' },
|
|
794
|
+
'--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#3e3d32' },
|
|
795
|
+
'--dsw-specific-sidebar-fill': { light: '#f0efe5', dark: '#1e1f1c' },
|
|
796
|
+
'--dsw-alias-border-l1': { light: '#e0dfd1', dark: '#3e3d32' },
|
|
797
|
+
'--dsw-alias-border-l2': { light: '#c6c5b3', dark: '#575744' },
|
|
798
|
+
'--dsw-alias-label-primary': { light: '#272822', dark: '#f8f8f2' },
|
|
799
|
+
'--dsw-alias-label-secondary': { light: '#75715e', dark: '#8a9199' },
|
|
800
|
+
'--dsw-alias-brand-primary': { light: '#c2185b', dark: '#f92672' },
|
|
801
|
+
'--dsw-alias-state-error-primary': { light: '#d13b2f', dark: '#f92672' },
|
|
802
|
+
'--dsw-alias-state-success-primary': { light: '#567f0e', dark: '#a6e22e' },
|
|
803
|
+
'--dsw-alias-state-warn-primary': { light: '#b07d1a', dark: '#e6db74' },
|
|
804
|
+
'--dsw-alias-state-idle-primary': { light: '#8d8b7c', dark: '#75715e' },
|
|
805
|
+
},
|
|
806
|
+
}, {
|
|
807
|
+
id: 'mono',
|
|
808
|
+
label: 'Mono',
|
|
809
|
+
description: 'Grayscale everywhere except state colours.',
|
|
810
|
+
colorScheme: 'dark',
|
|
811
|
+
tokens: {
|
|
812
|
+
'--dsw-alias-bg-base': { light: '#f5f5f5', dark: '#101010' },
|
|
813
|
+
'--dsw-alias-bg-layer-1': { light: '#ffffff', dark: '#1a1a1a' },
|
|
814
|
+
'--dsw-alias-bg-layer-2': { light: '#ececec', dark: '#242424' },
|
|
815
|
+
'--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#282828' },
|
|
816
|
+
'--dsw-specific-sidebar-fill': { light: '#e8e8e8', dark: '#0a0a0a' },
|
|
817
|
+
'--dsw-alias-border-l1': { light: '#d4d4d4', dark: '#2e2e2e' },
|
|
818
|
+
'--dsw-alias-border-l2': { light: '#b0b0b0', dark: '#454545' },
|
|
819
|
+
'--dsw-alias-label-primary': { light: '#0a0a0a', dark: '#ededed' },
|
|
820
|
+
'--dsw-alias-label-secondary': { light: '#525252', dark: '#a3a3a3' },
|
|
821
|
+
'--dsw-alias-brand-primary': { light: '#262626', dark: '#d4d4d4' },
|
|
822
|
+
'--dsw-alias-state-error-primary': { light: '#b42318', dark: '#ff9184' },
|
|
823
|
+
'--dsw-alias-state-success-primary': { light: '#0a6b45', dark: '#6fd9a8' },
|
|
824
|
+
'--dsw-alias-state-warn-primary': { light: '#8a5c00', dark: '#e8bf6a' },
|
|
825
|
+
'--dsw-alias-state-idle-primary': { light: '#6b6b6b', dark: '#8f8f8f' },
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
];
|
|
829
|
+
/** Every preset id, for validation. */
|
|
830
|
+
const PRESET_IDS = PRESETS.map((preset) => preset.id);
|
|
831
|
+
/**
|
|
832
|
+
* Look up one preset by id.
|
|
833
|
+
* @param id - preset id.
|
|
834
|
+
* @returns the preset, or `undefined` for an unknown or `none` id.
|
|
835
|
+
*/
|
|
836
|
+
function findPreset(id) {
|
|
837
|
+
return PRESETS.find((preset) => preset.id === id);
|
|
838
|
+
}
|
|
839
|
+
/** The role names the theme runtime accepts for typography. */
|
|
840
|
+
const FONT_ROLES = ['text', 'code', 'terminal'];
|
|
841
|
+
/** Accepted font-size range per role, mirroring the shipped Appearance rows. */
|
|
842
|
+
const FONT_SIZE_RANGES = {
|
|
843
|
+
text: { min: 10, max: 22, fallback: 14 },
|
|
844
|
+
code: { min: 10, max: 16, fallback: 11 },
|
|
845
|
+
terminal: { min: 10, max: 20, fallback: 13 },
|
|
846
|
+
};
|
|
847
|
+
/** Density presets, expressed as a spacing multiplier applied to derived tokens. */
|
|
848
|
+
const DENSITIES = ['compact', 'comfortable', 'spacious'];
|
|
849
|
+
/** Radius bounds in pixels. */
|
|
850
|
+
const RADIUS_MIN = 0;
|
|
851
|
+
/** Upper radius bound in pixels. */
|
|
852
|
+
const RADIUS_MAX = 24;
|
|
853
|
+
//# sourceMappingURL=spec.js.map
|
|
854
|
+
Object.assign(exports, { tokenSpec: tokenSpec, findPreset: findPreset, TOKEN_GROUPS: TOKEN_GROUPS, TOKEN_SPECS: TOKEN_SPECS, TOKEN_NAMES: TOKEN_NAMES, PRESETS: PRESETS, PRESET_IDS: PRESET_IDS, FONT_ROLES: FONT_ROLES, FONT_SIZE_RANGES: FONT_SIZE_RANGES, DENSITIES: DENSITIES, RADIUS_MIN: RADIUS_MIN, RADIUS_MAX: RADIUS_MAX });
|
|
855
|
+
|
|
856
|
+
},
|
|
857
|
+
"./resolve.js": (module, exports, __require) => {
|
|
858
|
+
/**
|
|
859
|
+
* Resolving a customization document into theme tokens.
|
|
860
|
+
*
|
|
861
|
+
* Three inputs combine, in this order:
|
|
862
|
+
*
|
|
863
|
+
* 1. A **preset**, which is a complete token set.
|
|
864
|
+
* 2. **Per-token overrides**, applied on top of the preset.
|
|
865
|
+
* 3. **Geometry**, expanded into whichever tokens it can express.
|
|
866
|
+
*
|
|
867
|
+
* The result is one `ThemeTokenOverrides` object — token name to `{light, dark}` —
|
|
868
|
+
* which is exactly what `ctx.theme.overrideTokens` accepts. Keeping the merge
|
|
869
|
+
* here, as a pure function, is what lets the panel preview a change and the Host
|
|
870
|
+
* validate one from the same code.
|
|
871
|
+
*
|
|
872
|
+
* @module @d0nj/dsh-ui-custom/resolve
|
|
873
|
+
*/
|
|
874
|
+
const { DENSITIES, FONT_SIZE_RANGES, RADIUS_MAX, RADIUS_MIN, TOKEN_NAMES, findPreset, tokenSpec } = __require('./spec.js');
|
|
875
|
+
/** A colour in one of the forms the theme service and browsers accept. */
|
|
876
|
+
const COLOR_PATTERN = /^(#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})|(?:rgb|hsl|oklch|lab|lch|color)\([^)]*\)|[a-z]+)$/iu;
|
|
877
|
+
/**
|
|
878
|
+
* Whether a value is a plausible CSS colour.
|
|
879
|
+
*
|
|
880
|
+
* Colour tokens are rendered straight into CSS custom properties, so an
|
|
881
|
+
* arbitrary string would not be a security hole but would silently produce an
|
|
882
|
+
* unstyled surface. Rejecting it here turns that into a visible warning.
|
|
883
|
+
* @param value - candidate colour value.
|
|
884
|
+
* @returns true when the value looks like a CSS colour.
|
|
885
|
+
*/
|
|
886
|
+
function isColor(value) {
|
|
887
|
+
return COLOR_PATTERN.test(value.trim());
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Clamp a number into a closed range.
|
|
891
|
+
* @param value - candidate value.
|
|
892
|
+
* @param min - lower bound.
|
|
893
|
+
* @param max - upper bound.
|
|
894
|
+
* @returns the clamped integer.
|
|
895
|
+
*/
|
|
896
|
+
function clamp(value, min, max) {
|
|
897
|
+
if (!Number.isFinite(value))
|
|
898
|
+
return min;
|
|
899
|
+
return Math.min(max, Math.max(min, Math.round(value)));
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Expand geometry into token values.
|
|
903
|
+
*
|
|
904
|
+
* The documented token set has no radius or spacing tokens, so geometry is
|
|
905
|
+
* expressed as a *shadow layer* of `--d0nj-*` variables the panel's own styles
|
|
906
|
+
* consume, plus any `--dsw-*` tokens that exist. Emitting our own names keeps
|
|
907
|
+
* this additive: a token the harness does not define cannot break a surface,
|
|
908
|
+
* and the panel styles read ours.
|
|
909
|
+
*
|
|
910
|
+
* @param geometry - the requested geometry.
|
|
911
|
+
* @returns derived custom properties with identical light and dark values.
|
|
912
|
+
*/
|
|
913
|
+
function geometryTokens(geometry) {
|
|
914
|
+
const radius = clamp(geometry.radius, RADIUS_MIN, RADIUS_MAX);
|
|
915
|
+
const scale = geometry.density === 'compact' ? 0.8 : geometry.density === 'spacious' ? 1.25 : 1;
|
|
916
|
+
const value = (px) => ({ light: px, dark: px });
|
|
917
|
+
return {
|
|
918
|
+
'--d0nj-radius-sm': value(`${String(Math.round(radius * 0.5))}px`),
|
|
919
|
+
'--d0nj-radius-md': value(`${String(radius)}px`),
|
|
920
|
+
'--d0nj-radius-lg': value(`${String(Math.round(radius * 1.5))}px`),
|
|
921
|
+
'--d0nj-space-1': value(`${String(Math.round(4 * scale))}px`),
|
|
922
|
+
'--d0nj-space-2': value(`${String(Math.round(8 * scale))}px`),
|
|
923
|
+
'--d0nj-space-3': value(`${String(Math.round(12 * scale))}px`),
|
|
924
|
+
'--d0nj-space-4': value(`${String(Math.round(16 * scale))}px`),
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Correct one role's font size into its accepted range.
|
|
929
|
+
* @param role - font role.
|
|
930
|
+
* @param px - requested size.
|
|
931
|
+
* @returns the in-range size.
|
|
932
|
+
*/
|
|
933
|
+
function correctFontSize(role, px) {
|
|
934
|
+
const range = FONT_SIZE_RANGES[role];
|
|
935
|
+
return clamp(px, range.min, range.max);
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Whether a string names a known density.
|
|
939
|
+
* @param value - candidate density.
|
|
940
|
+
* @returns true for a known density.
|
|
941
|
+
*/
|
|
942
|
+
function isDensity(value) {
|
|
943
|
+
return DENSITIES.includes(value);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Resolve a customization document into theme tokens and typography.
|
|
947
|
+
*
|
|
948
|
+
* Merge order is preset, then explicit overrides, then geometry, so a user who
|
|
949
|
+
* picks a preset and then tweaks one token keeps the tweak, and geometry always
|
|
950
|
+
* wins over a token of the same name (it is the more specific control).
|
|
951
|
+
*
|
|
952
|
+
* @param document - the customization document.
|
|
953
|
+
* @returns the token overrides, corrected typography, and any warnings.
|
|
954
|
+
*/
|
|
955
|
+
function resolveCustomization(document) {
|
|
956
|
+
const warnings = [];
|
|
957
|
+
const tokens = {};
|
|
958
|
+
if (document.basePreset !== 'none') {
|
|
959
|
+
const preset = findPreset(document.basePreset);
|
|
960
|
+
if (preset === undefined) {
|
|
961
|
+
warnings.push(`unknown preset "${document.basePreset}"; no base tokens applied`);
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
for (const [name, modes] of Object.entries(preset.tokens))
|
|
965
|
+
tokens[name] = modes;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
for (const [name, modes] of Object.entries(document.tokens)) {
|
|
969
|
+
const spec = tokenSpec(name);
|
|
970
|
+
if (spec === undefined) {
|
|
971
|
+
// A hand-edited document can name a token this build does not document.
|
|
972
|
+
// Skipping it with a warning beats applying a property nothing reads.
|
|
973
|
+
warnings.push(`ignoring undocumented token "${name}"`);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const light = modes.light.trim();
|
|
977
|
+
const dark = modes.dark.trim();
|
|
978
|
+
if (light.length === 0 || dark.length === 0) {
|
|
979
|
+
warnings.push(`token "${name}" needs both a light and a dark value`);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (spec.kind === 'color' && (!isColor(light) || !isColor(dark))) {
|
|
983
|
+
warnings.push(`token "${name}" is not a valid CSS colour`);
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
tokens[name] = { light, dark };
|
|
987
|
+
}
|
|
988
|
+
for (const [name, modes] of Object.entries(geometryTokens(document.geometry))) {
|
|
989
|
+
tokens[name] = modes;
|
|
990
|
+
}
|
|
991
|
+
const typography = {
|
|
992
|
+
textFontFamily: document.typography.textFontFamily,
|
|
993
|
+
codeFontFamily: document.typography.codeFontFamily,
|
|
994
|
+
terminalFontFamily: document.typography.terminalFontFamily,
|
|
995
|
+
fontSize: correctFontSize('text', document.typography.fontSize),
|
|
996
|
+
codeFontSize: correctFontSize('code', document.typography.codeFontSize),
|
|
997
|
+
terminalFontSize: correctFontSize('terminal', document.typography.terminalFontSize),
|
|
998
|
+
};
|
|
999
|
+
if (typography.fontSize !== document.typography.fontSize) {
|
|
1000
|
+
warnings.push(`text font size corrected to ${String(typography.fontSize)}px (allowed ${String(FONT_SIZE_RANGES.text.min)}–${String(FONT_SIZE_RANGES.text.max)})`);
|
|
1001
|
+
}
|
|
1002
|
+
return { tokens, typography, warnings };
|
|
1003
|
+
}
|
|
1004
|
+
/** Every documented token name, re-exported for the panel's reset action. */
|
|
1005
|
+
Object.assign(exports, { TOKEN_NAMES: TOKEN_NAMES });
|
|
1006
|
+
//# sourceMappingURL=resolve.js.map
|
|
1007
|
+
Object.assign(exports, { isColor: isColor, clamp: clamp, geometryTokens: geometryTokens, correctFontSize: correctFontSize, isDensity: isDensity, resolveCustomization: resolveCustomization });
|
|
1008
|
+
|
|
1009
|
+
},
|
|
1010
|
+
"./client/panel.js": (module, exports, __require) => {
|
|
1011
|
+
/**
|
|
1012
|
+
* Theme Studio: the settings page.
|
|
1013
|
+
*
|
|
1014
|
+
* Live preview is computed from the same resolver the stored document goes
|
|
1015
|
+
* through, so what the panel shows and what the plugin applies cannot drift.
|
|
1016
|
+
* Writes go through `configForms.get('ui-custom').set/unset`, which means the
|
|
1017
|
+
* panel never touches YAML, never races a revision, and is reversible field by
|
|
1018
|
+
* field.
|
|
1019
|
+
*
|
|
1020
|
+
* Styling follows the plugin rules: theme tokens only (`--dsw-alias-*`, plus the
|
|
1021
|
+
* `--d0nj-*` geometry tokens this plugin derives), no literal colours for
|
|
1022
|
+
* chrome. The swatches themselves are the one place a literal colour is correct,
|
|
1023
|
+
* because they *are* the colour being chosen.
|
|
1024
|
+
*
|
|
1025
|
+
* @module @d0nj/dsh-ui-custom/panel
|
|
1026
|
+
*/
|
|
1027
|
+
/** Shared inline styles, computed from theme tokens. */
|
|
1028
|
+
function styles() {
|
|
1029
|
+
return {
|
|
1030
|
+
root: {
|
|
1031
|
+
display: 'flex',
|
|
1032
|
+
flexDirection: 'column',
|
|
1033
|
+
gap: 24,
|
|
1034
|
+
padding: '4px 0 32px',
|
|
1035
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1036
|
+
fontFamily: 'inherit',
|
|
1037
|
+
},
|
|
1038
|
+
section: { display: 'flex', flexDirection: 'column', gap: 10 },
|
|
1039
|
+
heading: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' },
|
|
1040
|
+
hint: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', lineHeight: 1.5 },
|
|
1041
|
+
grid: {
|
|
1042
|
+
display: 'grid',
|
|
1043
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
|
|
1044
|
+
gap: 10,
|
|
1045
|
+
},
|
|
1046
|
+
card: {
|
|
1047
|
+
display: 'flex',
|
|
1048
|
+
flexDirection: 'column',
|
|
1049
|
+
gap: 8,
|
|
1050
|
+
padding: 10,
|
|
1051
|
+
borderRadius: 'var(--d0nj-radius-md, 8px)',
|
|
1052
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
1053
|
+
// No background and no colour token here: a card represents a theme, so it
|
|
1054
|
+
// paints that theme's own surface (set per card, below). Using a token made
|
|
1055
|
+
// every card show the live theme instead, which read as banding.
|
|
1056
|
+
background: 'transparent',
|
|
1057
|
+
cursor: 'pointer',
|
|
1058
|
+
textAlign: 'left',
|
|
1059
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1060
|
+
font: 'inherit',
|
|
1061
|
+
// The UA focus ring lingers on whichever card was last clicked; a
|
|
1062
|
+
// focus-visible ring keeps keyboard affordance without that trail.
|
|
1063
|
+
outline: 'none',
|
|
1064
|
+
},
|
|
1065
|
+
cardActive: {
|
|
1066
|
+
borderColor: 'var(--dsw-alias-brand-primary)',
|
|
1067
|
+
borderWidth: 2,
|
|
1068
|
+
padding: 9,
|
|
1069
|
+
},
|
|
1070
|
+
swatches: { display: 'flex', gap: 4 },
|
|
1071
|
+
swatch: {
|
|
1072
|
+
width: 22,
|
|
1073
|
+
height: 22,
|
|
1074
|
+
borderRadius: 'var(--d0nj-radius-sm, 4px)',
|
|
1075
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
1076
|
+
},
|
|
1077
|
+
row: {
|
|
1078
|
+
display: 'grid',
|
|
1079
|
+
gridTemplateColumns: 'minmax(120px, 1fr) auto auto',
|
|
1080
|
+
gap: 8,
|
|
1081
|
+
alignItems: 'center',
|
|
1082
|
+
padding: '6px 0',
|
|
1083
|
+
borderBottom: '1px solid var(--dsw-alias-border-l1)',
|
|
1084
|
+
},
|
|
1085
|
+
rowLabel: { display: 'flex', flexDirection: 'column', gap: 2 },
|
|
1086
|
+
input: {
|
|
1087
|
+
width: 92,
|
|
1088
|
+
padding: '4px 6px',
|
|
1089
|
+
borderRadius: 'var(--d0nj-radius-sm, 4px)',
|
|
1090
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
1091
|
+
background: 'var(--dsw-alias-bg-base)',
|
|
1092
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1093
|
+
font: 'inherit',
|
|
1094
|
+
fontSize: 12,
|
|
1095
|
+
},
|
|
1096
|
+
colorInput: {
|
|
1097
|
+
width: 34,
|
|
1098
|
+
height: 26,
|
|
1099
|
+
padding: 0,
|
|
1100
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
1101
|
+
borderRadius: 'var(--d0nj-radius-sm, 4px)',
|
|
1102
|
+
background: 'transparent',
|
|
1103
|
+
cursor: 'pointer',
|
|
1104
|
+
},
|
|
1105
|
+
button: {
|
|
1106
|
+
padding: '5px 10px',
|
|
1107
|
+
borderRadius: 'var(--d0nj-radius-md, 8px)',
|
|
1108
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1109
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1110
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1111
|
+
font: 'inherit',
|
|
1112
|
+
fontSize: 12,
|
|
1113
|
+
cursor: 'pointer',
|
|
1114
|
+
},
|
|
1115
|
+
warning: {
|
|
1116
|
+
padding: '8px 10px',
|
|
1117
|
+
borderRadius: 'var(--d0nj-radius-md, 8px)',
|
|
1118
|
+
border: '1px solid var(--dsw-alias-state-warn-primary)',
|
|
1119
|
+
color: 'var(--dsw-alias-state-warn-primary)',
|
|
1120
|
+
fontSize: 12,
|
|
1121
|
+
},
|
|
1122
|
+
preview: {
|
|
1123
|
+
display: 'flex',
|
|
1124
|
+
flexDirection: 'column',
|
|
1125
|
+
gap: 8,
|
|
1126
|
+
padding: 14,
|
|
1127
|
+
borderRadius: 'var(--d0nj-radius-lg, 12px)',
|
|
1128
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
1129
|
+
background: 'var(--dsw-alias-bg-base)',
|
|
1130
|
+
},
|
|
1131
|
+
row2: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' },
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Render the Theme Studio page.
|
|
1136
|
+
*
|
|
1137
|
+
* Written against a minimal `React.createElement` shim rather than JSX so the
|
|
1138
|
+
* module needs no transform at build time; the client bundle supplies React.
|
|
1139
|
+
*
|
|
1140
|
+
* @param React - the React namespace from the module table.
|
|
1141
|
+
* @param props - current state and the actions to invoke.
|
|
1142
|
+
* @returns the section element.
|
|
1143
|
+
*/
|
|
1144
|
+
function ThemeStudio(React, props) {
|
|
1145
|
+
const h = React.createElement;
|
|
1146
|
+
const s = styles();
|
|
1147
|
+
const { state, actions, presets } = props;
|
|
1148
|
+
// Local mirrors. The panel must react to a click immediately: a settings write
|
|
1149
|
+
// is a round-trip through the Host, and waiting for the echo makes every
|
|
1150
|
+
// control feel dead when the write is slow, refused, or not mirrored at all.
|
|
1151
|
+
// Local state is the source of truth for rendering; the action is the command.
|
|
1152
|
+
const [localPreset, setLocalPreset] = React.useState(state.basePreset);
|
|
1153
|
+
const [localTokens, setLocalTokens] = React.useState({});
|
|
1154
|
+
const [localGeometry, setLocalGeometry] = React.useState(state.geometry);
|
|
1155
|
+
const [localTypography, setLocalTypography] = React.useState(state.typography);
|
|
1156
|
+
/** The preset currently selected, for filling a missing mode from its colours. */
|
|
1157
|
+
const basePreset = presets.find((preset) => preset.id === (localPreset ?? state.basePreset));
|
|
1158
|
+
/**
|
|
1159
|
+
* The effective value of one token: the local edit, else the stored override,
|
|
1160
|
+
* else the base preset's own value for that mode.
|
|
1161
|
+
*/
|
|
1162
|
+
const effective = (name) => {
|
|
1163
|
+
const local = localTokens[name];
|
|
1164
|
+
if (local !== undefined)
|
|
1165
|
+
return local;
|
|
1166
|
+
const stored = state.tokens.find((row) => row.name === name)?.override;
|
|
1167
|
+
if (stored !== undefined)
|
|
1168
|
+
return stored;
|
|
1169
|
+
const presetValue = basePreset === undefined ? undefined : basePreset.tokens[name];
|
|
1170
|
+
return presetValue ?? { light: '', dark: '' };
|
|
1171
|
+
};
|
|
1172
|
+
const presetCards = [
|
|
1173
|
+
{ id: 'none', label: 'No preset', description: 'Start from the built-in palette.', colorScheme: 'dark', tokens: {} },
|
|
1174
|
+
...presets,
|
|
1175
|
+
].map((preset) => {
|
|
1176
|
+
const base = preset.tokens['--dsw-alias-bg-base'];
|
|
1177
|
+
const accent = preset.tokens['--dsw-alias-brand-primary'];
|
|
1178
|
+
const label = preset.tokens['--dsw-alias-label-primary'];
|
|
1179
|
+
const active = (localPreset ?? state.basePreset) === preset.id;
|
|
1180
|
+
return h('button', {
|
|
1181
|
+
key: preset.id,
|
|
1182
|
+
type: 'button',
|
|
1183
|
+
style: {
|
|
1184
|
+
...s.card,
|
|
1185
|
+
// Paint the preset's own surface and text colour, so the grid shows what
|
|
1186
|
+
// each theme looks like rather than the one currently applied.
|
|
1187
|
+
background: base?.light ?? 'transparent',
|
|
1188
|
+
color: label?.light ?? 'var(--dsw-alias-label-primary)',
|
|
1189
|
+
...(active ? s.cardActive : {}),
|
|
1190
|
+
},
|
|
1191
|
+
'aria-pressed': active,
|
|
1192
|
+
onClick: () => {
|
|
1193
|
+
setLocalPreset(preset.id);
|
|
1194
|
+
setLocalTokens({});
|
|
1195
|
+
actions.setPreset(preset.id);
|
|
1196
|
+
},
|
|
1197
|
+
}, h('div', { style: s.swatches }, h('span', { style: { ...s.swatch, background: base?.light ?? 'transparent' } }), h('span', { style: { ...s.swatch, background: base?.dark ?? 'transparent' } }), h('span', { style: { ...s.swatch, background: accent?.dark ?? 'transparent' } }), h('span', { style: { ...s.swatch, background: label?.dark ?? 'transparent' } })), h('span', { style: { fontSize: 13, fontWeight: 600 } }, preset.label), h('span', { style: s.hint }, preset.description));
|
|
1198
|
+
});
|
|
1199
|
+
const tokenRows = state.tokens.map((row) => {
|
|
1200
|
+
const current = effective(row.name);
|
|
1201
|
+
/**
|
|
1202
|
+
* Apply one mode's edit and write both modes.
|
|
1203
|
+
*
|
|
1204
|
+
* The theme runtime needs a value for each scheme, so a half-typed row is
|
|
1205
|
+
* completed from the preset rather than withheld: withholding was why typing
|
|
1206
|
+
* appeared to do nothing until both fields happened to be filled.
|
|
1207
|
+
*/
|
|
1208
|
+
const edit = (mode, value) => {
|
|
1209
|
+
const next = { ...current, [mode]: value };
|
|
1210
|
+
setLocalTokens({ ...localTokens, [row.name]: next });
|
|
1211
|
+
if (next.light.length > 0 && next.dark.length > 0)
|
|
1212
|
+
actions.setToken(row.name, next.light, next.dark);
|
|
1213
|
+
};
|
|
1214
|
+
return h('div', { key: row.name, style: s.row }, h('div', { style: s.rowLabel }, h('span', { style: { fontSize: 12, fontWeight: 600 } }, row.label), h('span', { style: s.hint }, row.description)), h('div', { style: s.row2 }, h('input', {
|
|
1215
|
+
style: s.colorInput,
|
|
1216
|
+
type: 'color',
|
|
1217
|
+
'aria-label': `${row.label} light`,
|
|
1218
|
+
value: /^#[0-9a-f]{6}$/iu.test(current.light) ? current.light : '#000000',
|
|
1219
|
+
onChange: (event) => edit('light', event.target.value),
|
|
1220
|
+
}), h('input', {
|
|
1221
|
+
style: s.input,
|
|
1222
|
+
type: 'text',
|
|
1223
|
+
'aria-label': `${row.label} light value`,
|
|
1224
|
+
placeholder: 'light',
|
|
1225
|
+
value: current.light,
|
|
1226
|
+
onChange: (event) => edit('light', event.target.value),
|
|
1227
|
+
})), h('div', { style: s.row2 }, h('input', {
|
|
1228
|
+
style: s.colorInput,
|
|
1229
|
+
type: 'color',
|
|
1230
|
+
'aria-label': `${row.label} dark`,
|
|
1231
|
+
value: /^#[0-9a-f]{6}$/iu.test(current.dark) ? current.dark : '#000000',
|
|
1232
|
+
onChange: (event) => edit('dark', event.target.value),
|
|
1233
|
+
}), h('input', {
|
|
1234
|
+
style: s.input,
|
|
1235
|
+
type: 'text',
|
|
1236
|
+
'aria-label': `${row.label} dark value`,
|
|
1237
|
+
placeholder: 'dark',
|
|
1238
|
+
value: current.dark,
|
|
1239
|
+
onChange: (event) => edit('dark', event.target.value),
|
|
1240
|
+
}), h('button', {
|
|
1241
|
+
type: 'button',
|
|
1242
|
+
style: s.button,
|
|
1243
|
+
onClick: () => {
|
|
1244
|
+
const next = { ...localTokens };
|
|
1245
|
+
delete next[row.name];
|
|
1246
|
+
setLocalTokens(next);
|
|
1247
|
+
actions.clearToken(row.name);
|
|
1248
|
+
},
|
|
1249
|
+
}, 'Reset')));
|
|
1250
|
+
});
|
|
1251
|
+
const fontRow = (role, label, family, size) => h('div', { style: s.row }, h('div', { style: s.rowLabel }, h('span', { style: { fontSize: 12, fontWeight: 600 } }, label)), h('input', {
|
|
1252
|
+
style: { ...s.input, width: 260 },
|
|
1253
|
+
type: 'text',
|
|
1254
|
+
'aria-label': `${label} font stack`,
|
|
1255
|
+
placeholder: 'Font family, fallback, sans-serif',
|
|
1256
|
+
value: family,
|
|
1257
|
+
onChange: (event) => {
|
|
1258
|
+
const key = role === 'text' ? 'textFontFamily' : role === 'code' ? 'codeFontFamily' : 'terminalFontFamily';
|
|
1259
|
+
setLocalTypography({ ...(localTypography ?? state.typography), [key]: event.target.value });
|
|
1260
|
+
actions.setFont(role, event.target.value);
|
|
1261
|
+
},
|
|
1262
|
+
}), h('div', { style: s.row2 }, h('input', {
|
|
1263
|
+
style: s.input,
|
|
1264
|
+
type: 'number',
|
|
1265
|
+
min: 8,
|
|
1266
|
+
max: 24,
|
|
1267
|
+
'aria-label': `${label} size`,
|
|
1268
|
+
value: size,
|
|
1269
|
+
onChange: (event) => {
|
|
1270
|
+
const px = Number(event.target.value);
|
|
1271
|
+
const key = role === 'text' ? 'fontSize' : role === 'code' ? 'codeFontSize' : 'terminalFontSize';
|
|
1272
|
+
setLocalTypography({ ...(localTypography ?? state.typography), [key]: px });
|
|
1273
|
+
actions.setFontSize(role, px);
|
|
1274
|
+
},
|
|
1275
|
+
}), h('span', { style: s.hint }, 'px')));
|
|
1276
|
+
return h('div', { style: s.root }, state.writable
|
|
1277
|
+
? null
|
|
1278
|
+
: h('div', { style: s.warning }, 'This profile does not accept settings writes; changes will not persist.'), h('section', { style: s.section }, h('h3', { style: s.heading }, 'Base theme'), h('p', { style: s.hint }, 'A preset supplies a complete token set. Tune individual values below it.'), h('div', { style: s.grid }, presetCards)), h('section', { style: s.section }, h('h3', { style: s.heading }, 'Live preview'), h('div', { style: s.preview }, h('span', { style: { fontSize: 15, fontWeight: 600 } }, 'The quick brown fox'), h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Secondary text sits one step down.'), h('div', { style: s.row2 }, h('span', { style: { ...s.button, background: 'var(--dsw-alias-brand-primary)', color: 'var(--dsw-alias-bg-base)' } }, 'Accent'), h('span', { style: { ...s.button, borderColor: 'var(--dsw-alias-state-error-primary)' } }, 'Error'), h('span', { style: { ...s.button, borderColor: 'var(--dsw-alias-state-success-primary)' } }, 'Success'), h('span', { style: { ...s.button, borderColor: 'var(--dsw-alias-state-warn-primary)' } }, 'Warning')))), h('section', { style: s.section }, h('h3', { style: s.heading }, 'Typography'), fontRow('text', 'Interface text', (localTypography ?? state.typography).textFontFamily, (localTypography ?? state.typography).fontSize), fontRow('code', 'Code', (localTypography ?? state.typography).codeFontFamily, (localTypography ?? state.typography).codeFontSize), fontRow('terminal', 'Terminal', (localTypography ?? state.typography).terminalFontFamily, (localTypography ?? state.typography).terminalFontSize)), h('section', { style: s.section }, h('h3', { style: s.heading }, 'Geometry'), h('div', { style: s.row }, h('div', { style: s.rowLabel }, h('span', { style: { fontSize: 12, fontWeight: 600 } }, 'Corner radius')), h('input', {
|
|
1279
|
+
type: 'range',
|
|
1280
|
+
min: 0,
|
|
1281
|
+
max: 24,
|
|
1282
|
+
value: (localGeometry ?? state.geometry).radius,
|
|
1283
|
+
'aria-label': 'Corner radius',
|
|
1284
|
+
onChange: (event) => {
|
|
1285
|
+
const radius = Number(event.target.value);
|
|
1286
|
+
setLocalGeometry({ ...(localGeometry ?? state.geometry), radius });
|
|
1287
|
+
actions.setRadius(radius);
|
|
1288
|
+
},
|
|
1289
|
+
}), h('span', { style: s.hint }, `${String((localGeometry ?? state.geometry).radius)}px`)), h('div', { style: s.row }, h('div', { style: s.rowLabel }, h('span', { style: { fontSize: 12, fontWeight: 600 } }, 'Density')), h('div', { style: s.row2 }, ...['compact', 'comfortable', 'spacious'].map((density) => h('button', {
|
|
1290
|
+
key: density,
|
|
1291
|
+
type: 'button',
|
|
1292
|
+
style: (localGeometry ?? state.geometry).density === density
|
|
1293
|
+
? { ...s.button, borderColor: 'var(--dsw-alias-brand-primary)' }
|
|
1294
|
+
: s.button,
|
|
1295
|
+
'aria-pressed': (localGeometry ?? state.geometry).density === density,
|
|
1296
|
+
onClick: () => {
|
|
1297
|
+
setLocalGeometry({ ...(localGeometry ?? state.geometry), density });
|
|
1298
|
+
actions.setDensity(density);
|
|
1299
|
+
},
|
|
1300
|
+
}, density))), h('span', {}))), h('section', { style: s.section }, h('h3', { style: s.heading }, 'Colour tokens'), h('p', { style: s.hint }, 'Both values are required: one per palette mode, so switching scheme stays legible.'), ...tokenRows), state.warnings.length > 0
|
|
1301
|
+
? h('section', { style: s.section }, ...state.warnings.map((warning, index) => h('div', { key: String(index), style: s.warning }, warning)))
|
|
1302
|
+
: null, h('section', { style: s.section }, h('button', { type: 'button', style: s.button, onClick: () => actions.resetAll() }, 'Reset all customization')));
|
|
1303
|
+
}
|
|
1304
|
+
//# sourceMappingURL=panel.js.map
|
|
1305
|
+
Object.assign(exports, { ThemeStudio: ThemeStudio });
|
|
1306
|
+
|
|
1307
|
+
},
|
|
1308
|
+
}
|
|
1309
|
+
const entry = __require("./client/index.js")
|
|
1310
|
+
if (typeof entry.useReact === 'function') entry.useReact(require('react'))
|
|
1311
|
+
return entry
|
|
1312
|
+
},
|
|
1313
|
+
});
|