@d0nj/dsh-ui-custom 0.0.0-stage → 0.2.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.
@@ -0,0 +1,1626 @@
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
+ /** CSS generic families, which always resolve and so never need a local match. */
878
+ const GENERIC_FAMILIES = new Set([
879
+ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
880
+ 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'emoji', 'math',
881
+ 'fangsong', '-apple-system', 'blinkmacsystemfont',
882
+ 'inherit', 'initial', 'unset',
883
+ ]);
884
+ /**
885
+ * Whether a value is a plausible CSS colour.
886
+ *
887
+ * Colour tokens are rendered straight into CSS custom properties, so an
888
+ * arbitrary string would not be a security hole but would silently produce an
889
+ * unstyled surface. Rejecting it here turns that into a visible warning.
890
+ * @param value - candidate colour value.
891
+ * @returns true when the value looks like a CSS colour.
892
+ */
893
+ function isColor(value) {
894
+ return COLOR_PATTERN.test(value.trim());
895
+ }
896
+ /**
897
+ * Clamp a number into a closed range.
898
+ * @param value - candidate value.
899
+ * @param min - lower bound.
900
+ * @param max - upper bound.
901
+ * @returns the clamped integer.
902
+ */
903
+ function clamp(value, min, max) {
904
+ if (!Number.isFinite(value))
905
+ return min;
906
+ return Math.min(max, Math.max(min, Math.round(value)));
907
+ }
908
+ /**
909
+ * Expand geometry into token values.
910
+ *
911
+ * The documented token set has no radius or spacing tokens, so geometry is
912
+ * expressed as a *shadow layer* of `--d0nj-*` variables the panel's own styles
913
+ * consume, plus any `--dsw-*` tokens that exist. Emitting our own names keeps
914
+ * this additive: a token the harness does not define cannot break a surface,
915
+ * and the panel styles read ours.
916
+ *
917
+ * @param geometry - the requested geometry.
918
+ * @returns derived custom properties with identical light and dark values.
919
+ */
920
+ function geometryTokens(geometry) {
921
+ const radius = clamp(geometry.radius, RADIUS_MIN, RADIUS_MAX);
922
+ const scale = geometry.density === 'compact' ? 0.8 : geometry.density === 'spacious' ? 1.25 : 1;
923
+ const value = (px) => ({ light: px, dark: px });
924
+ return {
925
+ '--d0nj-radius-sm': value(`${String(Math.round(radius * 0.5))}px`),
926
+ '--d0nj-radius-md': value(`${String(radius)}px`),
927
+ '--d0nj-radius-lg': value(`${String(Math.round(radius * 1.5))}px`),
928
+ '--d0nj-space-1': value(`${String(Math.round(4 * scale))}px`),
929
+ '--d0nj-space-2': value(`${String(Math.round(8 * scale))}px`),
930
+ '--d0nj-space-3': value(`${String(Math.round(12 * scale))}px`),
931
+ '--d0nj-space-4': value(`${String(Math.round(16 * scale))}px`),
932
+ };
933
+ }
934
+ /**
935
+ * Correct one role's font size into its accepted range.
936
+ * @param role - font role.
937
+ * @param px - requested size.
938
+ * @returns the in-range size.
939
+ */
940
+ function correctFontSize(role, px) {
941
+ const range = FONT_SIZE_RANGES[role];
942
+ return clamp(px, range.min, range.max);
943
+ }
944
+ /**
945
+ * Whether a string names a known density.
946
+ * @param value - candidate density.
947
+ * @returns true for a known density.
948
+ */
949
+ function isDensity(value) {
950
+ return DENSITIES.includes(value);
951
+ }
952
+ /**
953
+ * @param stack - comma-separated font stack.
954
+ * @returns the concrete family names, in order, without generics.
955
+ */
956
+ function familyNames(stack) {
957
+ return stack
958
+ .split(',')
959
+ .map((part) => part.trim().replace(/^["']|["']$/gu, '').trim())
960
+ .filter((name) => name !== '' && !GENERIC_FAMILIES.has(name.toLowerCase()));
961
+ }
962
+ /**
963
+ * Resolve a customization document into theme tokens and typography.
964
+ *
965
+ * Merge order is preset, then explicit overrides, then geometry, so a user who
966
+ * picks a preset and then tweaks one token keeps the tweak, and geometry always
967
+ * wins over a token of the same name (it is the more specific control).
968
+ *
969
+ * @param document - the customization document.
970
+ * @returns the token overrides, corrected typography, and any warnings.
971
+ */
972
+ function resolveCustomization(document) {
973
+ const warnings = [];
974
+ const tokens = {};
975
+ if (document.basePreset !== 'none') {
976
+ const preset = findPreset(document.basePreset);
977
+ if (preset === undefined) {
978
+ warnings.push(`unknown preset "${document.basePreset}"; no base tokens applied`);
979
+ }
980
+ else {
981
+ for (const [name, modes] of Object.entries(preset.tokens))
982
+ tokens[name] = modes;
983
+ }
984
+ }
985
+ for (const [name, modes] of Object.entries(document.tokens)) {
986
+ const spec = tokenSpec(name);
987
+ if (spec === undefined) {
988
+ // A hand-edited document can name a token this build does not document.
989
+ // Skipping it with a warning beats applying a property nothing reads.
990
+ warnings.push(`ignoring undocumented token "${name}"`);
991
+ continue;
992
+ }
993
+ const light = modes.light.trim();
994
+ const dark = modes.dark.trim();
995
+ if (light.length === 0 || dark.length === 0) {
996
+ warnings.push(`token "${name}" needs both a light and a dark value`);
997
+ continue;
998
+ }
999
+ if (spec.kind === 'color' && (!isColor(light) || !isColor(dark))) {
1000
+ warnings.push(`token "${name}" is not a valid CSS colour`);
1001
+ continue;
1002
+ }
1003
+ tokens[name] = { light, dark };
1004
+ }
1005
+ for (const [name, modes] of Object.entries(geometryTokens(document.geometry))) {
1006
+ tokens[name] = modes;
1007
+ }
1008
+ const typography = {
1009
+ textFontFamily: document.typography.textFontFamily,
1010
+ codeFontFamily: document.typography.codeFontFamily,
1011
+ terminalFontFamily: document.typography.terminalFontFamily,
1012
+ fontSize: correctFontSize('text', document.typography.fontSize),
1013
+ codeFontSize: correctFontSize('code', document.typography.codeFontSize),
1014
+ terminalFontSize: correctFontSize('terminal', document.typography.terminalFontSize),
1015
+ };
1016
+ if (typography.fontSize !== document.typography.fontSize) {
1017
+ warnings.push(`text font size corrected to ${String(typography.fontSize)}px (allowed ${String(FONT_SIZE_RANGES.text.min)}–${String(FONT_SIZE_RANGES.text.max)})`);
1018
+ }
1019
+ return { tokens, typography, warnings };
1020
+ }
1021
+ /** Every documented token name, re-exported for the panel's reset action. */
1022
+ Object.assign(exports, { TOKEN_NAMES: TOKEN_NAMES });
1023
+ //# sourceMappingURL=resolve.js.map
1024
+ Object.assign(exports, { isColor: isColor, clamp: clamp, geometryTokens: geometryTokens, correctFontSize: correctFontSize, isDensity: isDensity, familyNames: familyNames, resolveCustomization: resolveCustomization });
1025
+
1026
+ },
1027
+ "./client/panel.js": (module, exports, __require) => {
1028
+ /**
1029
+ * Theme Studio: the settings page.
1030
+ *
1031
+ * Live preview is computed from the same resolver the stored document goes
1032
+ * through, so what the panel shows and what the plugin applies cannot drift.
1033
+ * Writes go through `configForms.get('ui-custom').set/unset`, which means the
1034
+ * panel never touches YAML, never races a revision, and is reversible field by
1035
+ * field.
1036
+ *
1037
+ * Styling follows the plugin rules: theme tokens only (`--dsw-alias-*`, plus the
1038
+ * `--d0nj-*` geometry tokens this plugin derives), no literal colours for
1039
+ * chrome. The swatches themselves are the one place a literal colour is correct,
1040
+ * because they *are* the colour being chosen.
1041
+ *
1042
+ * @module @d0nj/dsh-ui-custom/panel
1043
+ */
1044
+ const { findHostedFont, hostedFamilies, loadHostedFont } = __require('./client/hosted-fonts.js');
1045
+ /** Shared inline styles, computed from theme tokens. */
1046
+ function styles() {
1047
+ return {
1048
+ root: {
1049
+ display: 'flex',
1050
+ flexDirection: 'column',
1051
+ gap: 24,
1052
+ padding: '4px 0 32px',
1053
+ color: 'var(--dsw-alias-label-primary)',
1054
+ fontFamily: 'inherit',
1055
+ },
1056
+ section: { display: 'flex', flexDirection: 'column', gap: 10 },
1057
+ heading: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' },
1058
+ hint: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', lineHeight: 1.5 },
1059
+ grid: {
1060
+ display: 'grid',
1061
+ gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
1062
+ gap: 10,
1063
+ },
1064
+ card: {
1065
+ display: 'flex',
1066
+ flexDirection: 'column',
1067
+ gap: 8,
1068
+ padding: 10,
1069
+ borderRadius: 'var(--d0nj-radius-md, 8px)',
1070
+ border: '1px solid var(--dsw-alias-border-l1)',
1071
+ // No background and no colour token here: a card represents a theme, so it
1072
+ // paints that theme's own surface (set per card, below). Using a token made
1073
+ // every card show the live theme instead, which read as banding.
1074
+ background: 'transparent',
1075
+ cursor: 'pointer',
1076
+ textAlign: 'left',
1077
+ color: 'var(--dsw-alias-label-primary)',
1078
+ font: 'inherit',
1079
+ // The UA focus ring lingers on whichever card was last clicked; a
1080
+ // focus-visible ring keeps keyboard affordance without that trail.
1081
+ outline: 'none',
1082
+ },
1083
+ cardActive: {
1084
+ borderColor: 'var(--dsw-alias-brand-primary)',
1085
+ borderWidth: 2,
1086
+ padding: 9,
1087
+ },
1088
+ swatches: { display: 'flex', gap: 4 },
1089
+ swatch: {
1090
+ width: 22,
1091
+ height: 22,
1092
+ borderRadius: 'var(--d0nj-radius-sm, 4px)',
1093
+ border: '1px solid var(--dsw-alias-border-l1)',
1094
+ },
1095
+ row: {
1096
+ display: 'grid',
1097
+ gridTemplateColumns: 'minmax(120px, 1fr) auto auto',
1098
+ gap: 8,
1099
+ alignItems: 'center',
1100
+ padding: '6px 0',
1101
+ borderBottom: '1px solid var(--dsw-alias-border-l1)',
1102
+ },
1103
+ rowLabel: { display: 'flex', flexDirection: 'column', gap: 2 },
1104
+ input: {
1105
+ width: 92,
1106
+ padding: '4px 6px',
1107
+ borderRadius: 'var(--d0nj-radius-sm, 4px)',
1108
+ border: '1px solid var(--dsw-alias-border-l1)',
1109
+ background: 'var(--dsw-alias-bg-base)',
1110
+ color: 'var(--dsw-alias-label-primary)',
1111
+ font: 'inherit',
1112
+ fontSize: 12,
1113
+ },
1114
+ select: {
1115
+ width: 260,
1116
+ padding: '4px 6px',
1117
+ borderRadius: 'var(--d0nj-radius-sm, 4px)',
1118
+ border: '1px solid var(--dsw-alias-border-l1)',
1119
+ background: 'var(--dsw-alias-bg-base)',
1120
+ color: 'var(--dsw-alias-label-primary)',
1121
+ font: 'inherit',
1122
+ fontSize: 12,
1123
+ },
1124
+ fontStack: { display: 'flex', flexDirection: 'column', gap: 4, width: 260 },
1125
+ fontRow: { gap: 18 },
1126
+ combo: { position: 'relative', width: 260 },
1127
+ listbox: {
1128
+ position: 'absolute',
1129
+ zIndex: 10,
1130
+ top: '100%',
1131
+ left: 0,
1132
+ right: 0,
1133
+ marginTop: 4,
1134
+ maxHeight: 240,
1135
+ overflowY: 'auto',
1136
+ padding: 4,
1137
+ borderRadius: 'var(--d0nj-radius-sm, 4px)',
1138
+ border: '1px solid var(--dsw-alias-border-l2)',
1139
+ // A popup over arbitrary content cannot inherit the surface behind it, so
1140
+ // it paints the module fill; only the element's own list uses this literal.
1141
+ background: 'var(--dsw-alias-bg-module-platform, var(--dsw-alias-bg-layer-1))',
1142
+ boxShadow: 'var(--dsw-elevation-lv2, 0 4px 12px rgba(0, 0, 0, 0.2))',
1143
+ },
1144
+ option: {
1145
+ padding: '4px 6px',
1146
+ borderRadius: 'var(--d0nj-radius-sm, 4px)',
1147
+ fontSize: 12,
1148
+ color: 'var(--dsw-alias-label-primary)',
1149
+ cursor: 'pointer',
1150
+ whiteSpace: 'nowrap',
1151
+ overflow: 'hidden',
1152
+ textOverflow: 'ellipsis',
1153
+ },
1154
+ optionActive: { background: 'var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2))' },
1155
+ colorInput: {
1156
+ width: 34,
1157
+ height: 26,
1158
+ padding: 0,
1159
+ border: '1px solid var(--dsw-alias-border-l1)',
1160
+ borderRadius: 'var(--d0nj-radius-sm, 4px)',
1161
+ background: 'transparent',
1162
+ cursor: 'pointer',
1163
+ },
1164
+ button: {
1165
+ padding: '5px 10px',
1166
+ borderRadius: 'var(--d0nj-radius-md, 8px)',
1167
+ border: '1px solid var(--dsw-alias-border-l2)',
1168
+ background: 'var(--dsw-alias-bg-layer-2)',
1169
+ color: 'var(--dsw-alias-label-primary)',
1170
+ font: 'inherit',
1171
+ fontSize: 12,
1172
+ cursor: 'pointer',
1173
+ },
1174
+ warning: {
1175
+ padding: '8px 10px',
1176
+ borderRadius: 'var(--d0nj-radius-md, 8px)',
1177
+ border: '1px solid var(--dsw-alias-state-warn-primary)',
1178
+ color: 'var(--dsw-alias-state-warn-primary)',
1179
+ fontSize: 12,
1180
+ },
1181
+ preview: {
1182
+ display: 'flex',
1183
+ flexDirection: 'column',
1184
+ gap: 8,
1185
+ padding: 14,
1186
+ borderRadius: 'var(--d0nj-radius-lg, 12px)',
1187
+ border: '1px solid var(--dsw-alias-border-l1)',
1188
+ background: 'var(--dsw-alias-bg-base)',
1189
+ },
1190
+ row2: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' },
1191
+ };
1192
+ }
1193
+ /**
1194
+ * Combobox state per role.
1195
+ *
1196
+ * Kept at module scope because the panel re-renders on every settings echo, and
1197
+ * state created inside the render would reset to closed each pass.
1198
+ */
1199
+ const combos = new Map();
1200
+ /** Exported so tests do not inherit an open list between cases. */
1201
+ function resetCombos() {
1202
+ combos.clear();
1203
+ }
1204
+ /**
1205
+ * @param key - the role's accessible label.
1206
+ * @returns the state for that role, created on first use.
1207
+ */
1208
+ function comboState(key) {
1209
+ const existing = combos.get(key);
1210
+ if (existing !== undefined)
1211
+ return existing;
1212
+ const created = { open: false, query: '', active: 0 };
1213
+ combos.set(key, created);
1214
+ return created;
1215
+ }
1216
+ /**
1217
+ * Fonts offered by the picker.
1218
+ *
1219
+ * Hosted rather than local: a browser cannot enumerate installed fonts without a
1220
+ * permission-gated API most lack, and a local family looks different elsewhere.
1221
+ */
1222
+ let fontChoiceCache;
1223
+ /** @returns the families the picker offers. */
1224
+ function fontChoices() {
1225
+ fontChoiceCache ??= hostedFamilies();
1226
+ return fontChoiceCache;
1227
+ }
1228
+ /**
1229
+ * Render the Theme Studio page.
1230
+ *
1231
+ * Written against a minimal `React.createElement` shim rather than JSX so the
1232
+ * module needs no transform at build time; the client bundle supplies React.
1233
+ *
1234
+ * @param React - the React namespace from the module table.
1235
+ * @param props - current state and the actions to invoke.
1236
+ * @returns the section element.
1237
+ */
1238
+ function ThemeStudio(React, props) {
1239
+ const h = React.createElement;
1240
+ const s = styles();
1241
+ const { state, actions, presets } = props;
1242
+ // Local mirrors. The panel must react to a click immediately: a settings write
1243
+ // is a round-trip through the Host, and waiting for the echo makes every
1244
+ // control feel dead when the write is slow, refused, or not mirrored at all.
1245
+ // Local state is the source of truth for rendering; the action is the command.
1246
+ const [localPreset, setLocalPreset] = React.useState(state.basePreset);
1247
+ const [localTokens, setLocalTokens] = React.useState({});
1248
+ const [localGeometry, setLocalGeometry] = React.useState(state.geometry);
1249
+ const [localTypography, setLocalTypography] = React.useState(state.typography);
1250
+ // The counter must keep changing: writing the same number makes React bail out.
1251
+ const [renderTick, setRender] = React.useState(0);
1252
+ const choices = fontChoices();
1253
+ const rerender = () => setRender(renderTick + 1);
1254
+ /**
1255
+ * @param role - font role being changed.
1256
+ * @param value - CSS stack to apply.
1257
+ */
1258
+ const applyFont = (role, value) => {
1259
+ const hosted = findHostedFont(value);
1260
+ if (hosted === undefined) {
1261
+ actions.setFont(role, value);
1262
+ return;
1263
+ }
1264
+ void loadHostedFont(hosted).then(() => actions.setFont(role, value));
1265
+ };
1266
+ /**
1267
+ * @param ariaLabel - accessible name for the input.
1268
+ * @param family - current stack.
1269
+ * @param onChange - writes the chosen stack.
1270
+ * @returns the combobox element.
1271
+ */
1272
+ const fontCombo = (ariaLabel, family, onChange) => {
1273
+ const combo = comboState(ariaLabel);
1274
+ // Not the word "Default", which could collide with a family of that name.
1275
+ const DEFAULT_CHOICE = '\u0000default';
1276
+ const isDefault = (choice) => choice === DEFAULT_CHOICE;
1277
+ const matches = () => {
1278
+ const query = combo.query.trim().toLowerCase();
1279
+ if (query === '')
1280
+ return [DEFAULT_CHOICE, ...choices.slice(0, 39)];
1281
+ const filtered = choices.filter((choice) => choice.toLowerCase().includes(query));
1282
+ const keepDefault = 'default (built-in)'.includes(query) || 'built-in'.includes(query);
1283
+ return (keepDefault ? [DEFAULT_CHOICE, ...filtered] : filtered).slice(0, 40);
1284
+ };
1285
+ const commit = (value) => {
1286
+ combo.open = false;
1287
+ combo.query = '';
1288
+ combo.active = 0;
1289
+ const next = isDefault(value) ? '' : value;
1290
+ if (next !== combo.written)
1291
+ onChange(next);
1292
+ combo.written = undefined;
1293
+ rerender();
1294
+ };
1295
+ const visible = combo.open ? matches() : [];
1296
+ return h('div', { style: s.combo }, h('input', {
1297
+ type: 'text',
1298
+ role: 'combobox',
1299
+ style: s.select,
1300
+ 'aria-label': ariaLabel,
1301
+ 'aria-expanded': combo.open,
1302
+ 'aria-autocomplete': 'list',
1303
+ placeholder: 'Default (built-in stack)',
1304
+ value: family,
1305
+ onFocus: (event) => {
1306
+ combo.open = true;
1307
+ combo.query = '';
1308
+ combo.active = 0;
1309
+ event.target.select?.();
1310
+ rerender();
1311
+ },
1312
+ onInput: (event) => {
1313
+ combo.open = true;
1314
+ combo.query = event.target.value;
1315
+ combo.active = 0;
1316
+ combo.written = event.target.value;
1317
+ onChange(event.target.value);
1318
+ rerender();
1319
+ },
1320
+ onKeyDown: (event) => {
1321
+ const list = visible;
1322
+ if (event.key === 'Escape') {
1323
+ combo.open = false;
1324
+ combo.query = '';
1325
+ rerender();
1326
+ return;
1327
+ }
1328
+ if (event.key === 'ArrowDown' && list.length > 0) {
1329
+ event.preventDefault();
1330
+ combo.active = (combo.active + 1) % list.length;
1331
+ rerender();
1332
+ return;
1333
+ }
1334
+ if (event.key === 'ArrowUp' && list.length > 0) {
1335
+ event.preventDefault();
1336
+ combo.active = (combo.active - 1 + list.length) % list.length;
1337
+ rerender();
1338
+ return;
1339
+ }
1340
+ if (event.key === 'Enter') {
1341
+ const picked = list[combo.active];
1342
+ if (combo.open && picked !== undefined)
1343
+ commit(picked);
1344
+ else if (combo.open && combo.query.trim() !== '')
1345
+ commit(combo.query.trim());
1346
+ }
1347
+ },
1348
+ onBlur: () => {
1349
+ combo.open = false;
1350
+ combo.query = '';
1351
+ rerender();
1352
+ },
1353
+ }), combo.open && visible.length > 0
1354
+ ? h('div', { style: s.listbox, role: 'listbox' }, ...visible.map((choice, index) => h('div', {
1355
+ key: choice,
1356
+ role: 'option',
1357
+ 'aria-selected': index === combo.active,
1358
+ style: {
1359
+ ...s.option,
1360
+ ...(isDefault(choice) ? {} : { fontFamily: `"${choice}"` }),
1361
+ ...(index === combo.active ? s.optionActive : {}),
1362
+ },
1363
+ // Fires before the input's blur, which would close the list first.
1364
+ onMouseDown: (event) => {
1365
+ event.preventDefault();
1366
+ commit(choice);
1367
+ },
1368
+ }, isDefault(choice) ? 'Default (built-in stack)' : choice)))
1369
+ : null);
1370
+ };
1371
+ /** The preset currently selected, for filling a missing mode from its colours. */
1372
+ const basePreset = presets.find((preset) => preset.id === (localPreset ?? state.basePreset));
1373
+ /**
1374
+ * The effective value of one token: the local edit, else the stored override,
1375
+ * else the base preset's own value for that mode.
1376
+ */
1377
+ const effective = (name) => {
1378
+ const local = localTokens[name];
1379
+ if (local !== undefined)
1380
+ return local;
1381
+ const stored = state.tokens.find((row) => row.name === name)?.override;
1382
+ if (stored !== undefined)
1383
+ return stored;
1384
+ const presetValue = basePreset === undefined ? undefined : basePreset.tokens[name];
1385
+ return presetValue ?? { light: '', dark: '' };
1386
+ };
1387
+ const presetCards = [
1388
+ { id: 'none', label: 'No preset', description: 'Start from the built-in palette.', colorScheme: 'dark', tokens: {} },
1389
+ ...presets,
1390
+ ].map((preset) => {
1391
+ const base = preset.tokens['--dsw-alias-bg-base'];
1392
+ const accent = preset.tokens['--dsw-alias-brand-primary'];
1393
+ const label = preset.tokens['--dsw-alias-label-primary'];
1394
+ const active = (localPreset ?? state.basePreset) === preset.id;
1395
+ return h('button', {
1396
+ key: preset.id,
1397
+ type: 'button',
1398
+ style: {
1399
+ ...s.card,
1400
+ // Paint the preset's own surface and text colour, so the grid shows what
1401
+ // each theme looks like rather than the one currently applied.
1402
+ background: base?.light ?? 'transparent',
1403
+ color: label?.light ?? 'var(--dsw-alias-label-primary)',
1404
+ ...(active ? s.cardActive : {}),
1405
+ },
1406
+ 'aria-pressed': active,
1407
+ onClick: () => {
1408
+ setLocalPreset(preset.id);
1409
+ setLocalTokens({});
1410
+ actions.setPreset(preset.id);
1411
+ },
1412
+ }, 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));
1413
+ });
1414
+ const tokenRows = state.tokens.map((row) => {
1415
+ const current = effective(row.name);
1416
+ /**
1417
+ * Apply one mode's edit and write both modes.
1418
+ *
1419
+ * The theme runtime needs a value for each scheme, so a half-typed row is
1420
+ * completed from the preset rather than withheld: withholding was why typing
1421
+ * appeared to do nothing until both fields happened to be filled.
1422
+ */
1423
+ const edit = (mode, value) => {
1424
+ const next = { ...current, [mode]: value };
1425
+ setLocalTokens({ ...localTokens, [row.name]: next });
1426
+ if (next.light.length > 0 && next.dark.length > 0)
1427
+ actions.setToken(row.name, next.light, next.dark);
1428
+ };
1429
+ 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', {
1430
+ style: s.colorInput,
1431
+ type: 'color',
1432
+ 'aria-label': `${row.label} light`,
1433
+ value: /^#[0-9a-f]{6}$/iu.test(current.light) ? current.light : '#000000',
1434
+ onChange: (event) => edit('light', event.target.value),
1435
+ }), h('input', {
1436
+ style: s.input,
1437
+ type: 'text',
1438
+ 'aria-label': `${row.label} light value`,
1439
+ placeholder: 'light',
1440
+ value: current.light,
1441
+ onChange: (event) => edit('light', event.target.value),
1442
+ })), h('div', { style: s.row2 }, h('input', {
1443
+ style: s.colorInput,
1444
+ type: 'color',
1445
+ 'aria-label': `${row.label} dark`,
1446
+ value: /^#[0-9a-f]{6}$/iu.test(current.dark) ? current.dark : '#000000',
1447
+ onChange: (event) => edit('dark', event.target.value),
1448
+ }), h('input', {
1449
+ style: s.input,
1450
+ type: 'text',
1451
+ 'aria-label': `${row.label} dark value`,
1452
+ placeholder: 'dark',
1453
+ value: current.dark,
1454
+ onChange: (event) => edit('dark', event.target.value),
1455
+ }), h('button', {
1456
+ type: 'button',
1457
+ style: s.button,
1458
+ onClick: () => {
1459
+ const next = { ...localTokens };
1460
+ delete next[row.name];
1461
+ setLocalTokens(next);
1462
+ actions.clearToken(row.name);
1463
+ },
1464
+ }, 'Reset')));
1465
+ });
1466
+ const fontRow = (role, label, family, size) => {
1467
+ const edit = (value) => {
1468
+ const key = role === 'text' ? 'textFontFamily' : role === 'code' ? 'codeFontFamily' : 'terminalFontFamily';
1469
+ setLocalTypography({ ...(localTypography ?? state.typography), [key]: value });
1470
+ applyFont(role, value);
1471
+ };
1472
+ return h('div', { style: { ...s.row, ...s.fontRow } }, h('div', { style: s.rowLabel }, h('span', { style: { fontSize: 12, fontWeight: 600 } }, label)), fontCombo(`${label} font family`, family, edit), h('div', { style: s.row2 }, h('input', {
1473
+ style: s.input,
1474
+ type: 'number',
1475
+ min: 8,
1476
+ max: 24,
1477
+ 'aria-label': `${label} size`,
1478
+ value: size,
1479
+ onChange: (event) => {
1480
+ const px = Number(event.target.value);
1481
+ const key = role === 'text' ? 'fontSize' : role === 'code' ? 'codeFontSize' : 'terminalFontSize';
1482
+ setLocalTypography({ ...(localTypography ?? state.typography), [key]: px });
1483
+ actions.setFontSize(role, px);
1484
+ },
1485
+ }), h('span', { style: s.hint }, 'px')));
1486
+ };
1487
+ return h('div', { style: s.root }, state.writable
1488
+ ? null
1489
+ : 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', {
1490
+ type: 'range',
1491
+ min: 0,
1492
+ max: 24,
1493
+ value: (localGeometry ?? state.geometry).radius,
1494
+ 'aria-label': 'Corner radius',
1495
+ onChange: (event) => {
1496
+ const radius = Number(event.target.value);
1497
+ setLocalGeometry({ ...(localGeometry ?? state.geometry), radius });
1498
+ actions.setRadius(radius);
1499
+ },
1500
+ }), 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', {
1501
+ key: density,
1502
+ type: 'button',
1503
+ style: (localGeometry ?? state.geometry).density === density
1504
+ ? { ...s.button, borderColor: 'var(--dsw-alias-brand-primary)' }
1505
+ : s.button,
1506
+ 'aria-pressed': (localGeometry ?? state.geometry).density === density,
1507
+ onClick: () => {
1508
+ setLocalGeometry({ ...(localGeometry ?? state.geometry), density });
1509
+ actions.setDensity(density);
1510
+ },
1511
+ }, 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
1512
+ ? h('section', { style: s.section }, ...state.warnings.map((warning, index) => h('div', { key: String(index), style: s.warning }, warning)))
1513
+ : null, h('section', { style: s.section }, h('button', { type: 'button', style: s.button, onClick: () => actions.resetAll() }, 'Reset all customization')));
1514
+ }
1515
+ //# sourceMappingURL=panel.js.map
1516
+ Object.assign(exports, { resetCombos: resetCombos, ThemeStudio: ThemeStudio });
1517
+
1518
+ },
1519
+ "./client/hosted-fonts.js": (module, exports, __require) => {
1520
+ /**
1521
+ * Families fetched from a font CDN on demand.
1522
+ *
1523
+ * @module @d0nj/dsh-ui-custom/hosted-fonts
1524
+ */
1525
+ const HOSTED_FONTS = [
1526
+ { family: 'Inter', category: 'sans' },
1527
+ { family: 'IBM Plex Sans', category: 'sans' },
1528
+ { family: 'Source Sans 3', category: 'sans' },
1529
+ { family: 'Public Sans', category: 'sans' },
1530
+ { family: 'Work Sans', category: 'sans' },
1531
+ { family: 'DM Sans', category: 'sans' },
1532
+ { family: 'Manrope', category: 'sans' },
1533
+ { family: 'Figtree', category: 'sans' },
1534
+ { family: 'Nunito Sans', category: 'sans' },
1535
+ { family: 'Open Sans', category: 'sans' },
1536
+ { family: 'Roboto', category: 'sans' },
1537
+ { family: 'Lato', category: 'sans' },
1538
+ { family: 'Montserrat', category: 'sans' },
1539
+ { family: 'Poppins', category: 'sans' },
1540
+ { family: 'Lexend', category: 'sans' },
1541
+ { family: 'Outfit', category: 'sans' },
1542
+ { family: 'Sora', category: 'sans' },
1543
+ { family: 'Space Grotesk', category: 'sans' },
1544
+ { family: 'JetBrains Mono', category: 'mono' },
1545
+ { family: 'Fira Code', category: 'mono' },
1546
+ { family: 'Source Code Pro', category: 'mono' },
1547
+ { family: 'IBM Plex Mono', category: 'mono' },
1548
+ { family: 'Roboto Mono', category: 'mono' },
1549
+ { family: 'Space Mono', category: 'mono' },
1550
+ { family: 'Inconsolata', category: 'mono' },
1551
+ { family: 'Ubuntu Mono', category: 'mono' },
1552
+ { family: 'Anonymous Pro', category: 'mono' },
1553
+ { family: 'Source Serif 4', category: 'serif' },
1554
+ { family: 'IBM Plex Serif', category: 'serif' },
1555
+ { family: 'Lora', category: 'serif' },
1556
+ { family: 'Merriweather', category: 'serif' },
1557
+ { family: 'Newsreader', category: 'serif' },
1558
+ { family: 'Crimson Pro', category: 'serif' },
1559
+ { family: 'Bitter', category: 'serif' },
1560
+ { family: 'Bricolage Grotesque', category: 'display' },
1561
+ { family: 'Instrument Serif', category: 'display' },
1562
+ ];
1563
+ const CDN_BASE = 'https://fonts.googleapis.com/css2';
1564
+ /** Stylesheets outlive any one render, and a repeat request is a wasted round-trip. */
1565
+ const requested = new Set();
1566
+ /**
1567
+ * @param font - the family to request.
1568
+ * @returns an absolute stylesheet URL.
1569
+ */
1570
+ function hostedFontUrl(font) {
1571
+ const name = font.family.trim().replace(/\s+/gu, '+');
1572
+ const weights = (font.weights ?? [400]).join(';');
1573
+ return `${CDN_BASE}?family=${name}:wght@${weights}&display=swap`;
1574
+ }
1575
+ /**
1576
+ * Load one family into the page; resolves on failure too, because a font that
1577
+ * cannot be fetched must not break the panel.
1578
+ *
1579
+ * @param font - the family to load.
1580
+ * @param doc - document to load into; defaults to the global one.
1581
+ */
1582
+ async function loadHostedFont(font, doc = typeof document === 'undefined' ? undefined : document) {
1583
+ if (doc === undefined)
1584
+ return;
1585
+ const key = font.family.toLowerCase();
1586
+ if (requested.has(key))
1587
+ return;
1588
+ requested.add(key);
1589
+ const stylesheet = doc.createElement('link');
1590
+ stylesheet.rel = 'stylesheet';
1591
+ stylesheet.href = hostedFontUrl(font);
1592
+ stylesheet.dataset.d0njHostedFont = font.family;
1593
+ await new Promise((resolve) => {
1594
+ stylesheet.addEventListener('load', () => resolve(), { once: true });
1595
+ stylesheet.addEventListener('error', () => resolve(), { once: true });
1596
+ doc.head.append(stylesheet);
1597
+ });
1598
+ // Without this the first render after a pick can still measure the fallback.
1599
+ await doc.fonts?.ready.catch(() => { });
1600
+ }
1601
+ /**
1602
+ * @param fonts - catalog to read.
1603
+ * @returns family names, sorted.
1604
+ */
1605
+ function hostedFamilies(fonts = HOSTED_FONTS) {
1606
+ return fonts.map((font) => font.family).sort((left, right) => left.localeCompare(right));
1607
+ }
1608
+ /**
1609
+ * @param family - family name to find.
1610
+ * @param fonts - catalog to search.
1611
+ * @returns the entry, or `undefined` when the name is not hosted here.
1612
+ */
1613
+ function findHostedFont(family, fonts = HOSTED_FONTS) {
1614
+ const key = family.trim().replace(/^["']|["']$/gu, '').toLowerCase();
1615
+ return fonts.find((font) => font.family.toLowerCase() === key);
1616
+ }
1617
+ //# sourceMappingURL=hosted-fonts.js.map
1618
+ Object.assign(exports, { hostedFontUrl: hostedFontUrl, loadHostedFont: loadHostedFont, hostedFamilies: hostedFamilies, findHostedFont: findHostedFont, HOSTED_FONTS: HOSTED_FONTS });
1619
+
1620
+ },
1621
+ }
1622
+ const entry = __require("./client/index.js")
1623
+ if (typeof entry.useReact === 'function') entry.useReact(require('react'))
1624
+ return entry
1625
+ },
1626
+ });