@theredhead/lucid-theme-studio 0.1.1

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,414 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, signal, computed, Injectable, input, output, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import * as i1 from '@angular/forms';
4
+ import { FormsModule } from '@angular/forms';
5
+ import { DOCUMENT, NgTemplateOutlet, KeyValuePipe } from '@angular/common';
6
+ import { LoggerFactory } from '@theredhead/lucid-foundation';
7
+ import { UIIcons, UISplitContainer, UISplitPanel, UIButton, UIIcon, UIBadge, UICard, UICardBody, UICardFooter, UICardHeader, UIChip, UICheckbox, UIDropdownList, UIInput, UIProgress, UIToggle } from '@theredhead/lucid-kit';
8
+
9
+ /**
10
+ * State management service for the Theme Studio drawer.
11
+ *
12
+ * Loads the CSS token manifest, reads live computed values from the document
13
+ * root, tracks per-token overrides, and applies them in real time via
14
+ * `document.documentElement.style.setProperty`.
15
+ *
16
+ * Inject this wherever you need to open/close the studio or query its state.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * export class DevToolbar {
21
+ * private readonly studio = inject(ThemeStudioService);
22
+ *
23
+ * protected open(): void { this.studio.open(); }
24
+ * }
25
+ * ```
26
+ */
27
+ class ThemeStudioService {
28
+ document = inject(DOCUMENT);
29
+ log = inject(LoggerFactory).createLogger("ThemeStudioService");
30
+ // ── Drawer visibility ──────────────────────────────────────────────
31
+ /** Whether the studio drawer is currently visible. */
32
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
33
+ /** Open the studio drawer, loading the manifest if needed. */
34
+ open(manifestUrl = "assets/css-token-manifest.json") {
35
+ if (!this.isOpen()) {
36
+ this.loadManifest(manifestUrl);
37
+ this.isOpen.set(true);
38
+ }
39
+ }
40
+ /** Close the studio drawer. */
41
+ close() {
42
+ this.isOpen.set(false);
43
+ }
44
+ /** Toggle the studio drawer. */
45
+ toggle(manifestUrl = "assets/css-token-manifest.json") {
46
+ if (this.isOpen()) {
47
+ this.close();
48
+ }
49
+ else {
50
+ this.open(manifestUrl);
51
+ }
52
+ }
53
+ // ── Manifest + token state ─────────────────────────────────────────
54
+ /** Loading state for the manifest fetch. */
55
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
56
+ /** Error message from the most recent manifest load attempt, or null. */
57
+ loadError = signal(null, ...(ngDevMode ? [{ debugName: "loadError" }] : []));
58
+ /** Raw manifest loaded from JSON. */
59
+ manifest = signal(null, ...(ngDevMode ? [{ debugName: "manifest" }] : []));
60
+ /** All tokens with their live computed values and any studio overrides. */
61
+ tokens = signal([], ...(ngDevMode ? [{ debugName: "tokens" }] : []));
62
+ // ── Filter ─────────────────────────────────────────────────────────
63
+ /** Active filter applied to the token list. */
64
+ filter = signal({
65
+ query: "",
66
+ type: "",
67
+ scope: "",
68
+ namespace: "",
69
+ modifiedOnly: false,
70
+ }, ...(ngDevMode ? [{ debugName: "filter" }] : []));
71
+ /** Tokens after the active filter is applied. */
72
+ filteredTokens = computed(() => {
73
+ const { query, type, scope, namespace, modifiedOnly } = this.filter();
74
+ const q = query.toLowerCase();
75
+ return this.tokens().filter((t) => {
76
+ if (modifiedOnly && t.override === null)
77
+ return false;
78
+ if (type && t.type !== type)
79
+ return false;
80
+ if (scope && t.scope !== scope)
81
+ return false;
82
+ if (namespace && t.namespace !== namespace)
83
+ return false;
84
+ if (q &&
85
+ !t.name.toLowerCase().includes(q) &&
86
+ !t.description.toLowerCase().includes(q)) {
87
+ return false;
88
+ }
89
+ return true;
90
+ });
91
+ }, ...(ngDevMode ? [{ debugName: "filteredTokens" }] : []));
92
+ /** Tokens that have been modified by the studio. */
93
+ modifiedTokens = computed(() => this.tokens().filter((t) => t.override !== null), ...(ngDevMode ? [{ debugName: "modifiedTokens" }] : []));
94
+ // ── Override API ───────────────────────────────────────────────────
95
+ /**
96
+ * Apply an override value to a token and update the live document.
97
+ * Pass `null` to reset the token to its original computed value.
98
+ */
99
+ setOverride(tokenName, value) {
100
+ const root = this.document.documentElement;
101
+ if (value === null) {
102
+ root.style.removeProperty(tokenName);
103
+ }
104
+ else {
105
+ root.style.setProperty(tokenName, value);
106
+ }
107
+ this.tokens.update((list) => list.map((t) => (t.name === tokenName ? { ...t, override: value } : t)));
108
+ }
109
+ /** Reset all overrides and re-read computed values from the document. */
110
+ resetAll() {
111
+ this.tokens().forEach((t) => {
112
+ if (t.override !== null) {
113
+ this.document.documentElement.style.removeProperty(t.name);
114
+ }
115
+ });
116
+ this.tokens.update((list) => list.map((t) => ({
117
+ ...t,
118
+ override: null,
119
+ computedValue: this.readComputedValue(t.name) ||
120
+ t.values.light ||
121
+ t.values.dark ||
122
+ "",
123
+ })));
124
+ }
125
+ /**
126
+ * Export the current overrides as a CSS snippet that can be pasted into
127
+ * a stylesheet.
128
+ */
129
+ exportCss() {
130
+ const lines = this.modifiedTokens()
131
+ .map((t) => ` ${t.name}: ${t.override};`)
132
+ .join("\n");
133
+ return lines ? `:root {\n${lines}\n}` : "";
134
+ }
135
+ /**
136
+ * Export the current overrides as a JSON object
137
+ * (`{ "--ui-accent": "#ff0000", ... }`).
138
+ */
139
+ exportJson() {
140
+ return Object.fromEntries(this.modifiedTokens()
141
+ .filter((t) => t.override !== null)
142
+ .map((t) => [t.name, t.override]));
143
+ }
144
+ /**
145
+ * Import a set of overrides from a plain object.
146
+ * Only tokens present in the manifest are applied.
147
+ */
148
+ importOverrides(overrides) {
149
+ const known = new Set(this.tokens().map((t) => t.name));
150
+ Object.entries(overrides).forEach(([name, value]) => {
151
+ if (known.has(name))
152
+ this.setOverride(name, value);
153
+ });
154
+ }
155
+ // ── Private helpers ────────────────────────────────────────────────
156
+ loaded = false;
157
+ loadManifest(url) {
158
+ if (this.loaded)
159
+ return;
160
+ this.loading.set(true);
161
+ this.loadError.set(null);
162
+ fetch(url)
163
+ .then((res) => {
164
+ if (!res.ok)
165
+ throw new Error(`${res.status} ${url}`);
166
+ return res.json();
167
+ })
168
+ .then((manifest) => {
169
+ this.manifest.set(manifest);
170
+ this.tokens.set(manifest.tokens.map((t) => this.hydrateToken(t)));
171
+ this.loaded = true;
172
+ this.loading.set(false);
173
+ this.log.info("Loaded token manifest", [manifest.tokenCount, "tokens"]);
174
+ })
175
+ .catch((err) => {
176
+ const msg = err instanceof Error ? err.message : String(err);
177
+ this.loadError.set(msg);
178
+ this.loading.set(false);
179
+ this.log.error("Failed to load token manifest", [msg]);
180
+ });
181
+ }
182
+ hydrateToken(token) {
183
+ const computedValue = this.readComputedValue(token.name) ||
184
+ token.values.light ||
185
+ token.values.dark ||
186
+ "";
187
+ return {
188
+ ...token,
189
+ computedValue,
190
+ override: null,
191
+ };
192
+ }
193
+ readComputedValue(name) {
194
+ return getComputedStyle(this.document.documentElement)
195
+ .getPropertyValue(name)
196
+ .trim();
197
+ }
198
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ThemeStudioService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
199
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ThemeStudioService, providedIn: "root" });
200
+ }
201
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ThemeStudioService, decorators: [{
202
+ type: Injectable,
203
+ args: [{ providedIn: "root" }]
204
+ }] });
205
+
206
+ /**
207
+ * A single row in the theme-studio token list.
208
+ *
209
+ * Displays the token name, description, type/scope chips, and an editor
210
+ * appropriate to the token type (colour swatch + text, or plain text).
211
+ */
212
+ class UIThemeTokenRow {
213
+ // ── Inputs / outputs ───────────────────────────────────────────────
214
+ /** The token to display and edit. */
215
+ token = input.required(...(ngDevMode ? [{ debugName: "token" }] : []));
216
+ /** Emitted when the user changes the token value. */
217
+ valueChange = output();
218
+ /** Emitted when the user clicks Reset. */
219
+ reset = output();
220
+ // ── Private ────────────────────────────────────────────────────────
221
+ studio = inject(ThemeStudioService);
222
+ // ── Protected helpers ──────────────────────────────────────────────
223
+ /** The effective displayed value: override if set, else computed. */
224
+ get displayValue() {
225
+ const t = this.token();
226
+ return t.override !== null ? t.override : t.computedValue;
227
+ }
228
+ /** `true` when the token type is `"color"`. */
229
+ get isColor() {
230
+ return this.token().type === "color";
231
+ }
232
+ /** Forward text-input changes through the output. */
233
+ onTextInput(event) {
234
+ const value = event.target.value;
235
+ this.valueChange.emit(value);
236
+ }
237
+ /** Forward colour-picker changes through the output. */
238
+ onColorInput(event) {
239
+ const value = event.target.value;
240
+ this.valueChange.emit(value);
241
+ }
242
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UIThemeTokenRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
243
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: UIThemeTokenRow, isStandalone: true, selector: "ui-theme-token-row", inputs: { token: { classPropertyName: "token", publicName: "token", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { valueChange: "valueChange", reset: "reset" }, host: { properties: { "class.modified": "token().override !== null" }, classAttribute: "ui-theme-token-row" }, ngImport: i0, template: "<div class=\"info\">\n <span class=\"name\">{{ token().name }}</span>\n <span class=\"description\">{{ token().description }}</span>\n <span class=\"chips\">\n <span class=\"chip type\">{{ token().type }}</span>\n <span class=\"chip scope\">{{ token().scope }}</span>\n <span class=\"chip ns\">{{ token().namespace }}</span>\n </span>\n</div>\n\n<div class=\"editor\">\n @if (isColor) {\n <input\n type=\"color\"\n class=\"color-swatch\"\n [value]=\"displayValue\"\n [attr.aria-label]=\"'Color picker for ' + token().name\"\n (input)=\"onColorInput($event)\"\n />\n }\n <input\n type=\"text\"\n class=\"value-input\"\n [value]=\"displayValue\"\n [attr.aria-label]=\"'Value for ' + token().name\"\n (input)=\"onTextInput($event)\"\n />\n</div>\n\n@if (token().override !== null) {\n <button\n type=\"button\"\n class=\"reset-btn\"\n aria-label=\"Reset token to default\"\n (click)=\"reset.emit()\"\n >\n Reset\n </button>\n}\n", styles: [":host{display:grid;grid-template-columns:1fr auto auto;align-items:start;gap:.5rem .75rem;padding:.6rem .75rem;border-bottom:1px solid var(--ui-border, #d7dce2);color:var(--ui-text, #1d232b);background:var(--ui-surface, #fff);transition:background .12s}:host.modified{background:color-mix(in srgb,var(--ui-accent, #3584e4) 10%,var(--ui-surface, #fff))}.info{display:flex;flex-direction:column;gap:.2rem;min-width:0}.name{font-family:monospace;font-size:.8rem;color:var(--ui-text, #1d232b);word-break:break-all}.description{font-size:.75rem;color:var(--ui-text-secondary, #6b7280)}.chips{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.15rem}.chip{display:inline-block;padding:.1rem .4rem;border-radius:10px;font-size:.65rem;font-weight:600;color:var(--ui-text, #1d232b);background:var(--ui-border, #d7dce2)}.chip.type{background:#dbeafe;color:#1e3a8a}.chip.scope{background:#dcfce7;color:#14532d}.chip.ns{background:#fef9c3;color:#713f12}.editor{display:flex;align-items:center;gap:.4rem}.color-swatch{width:2rem;height:2rem;border:1px solid var(--ui-border, #d7dce2);border-radius:4px;padding:0;cursor:pointer;background:none}.value-input{width:9rem;padding:.3rem .5rem;font-size:.8rem;font-family:monospace;border:1px solid var(--ui-border, #d7dce2);border-radius:4px;color:var(--ui-text, #1d232b);background:var(--ui-surface, #fff);outline:none}.value-input:focus{border-color:var(--ui-accent, #3584e4)}.reset-btn{padding:.25rem .6rem;font-size:.75rem;border:1px solid var(--ui-border, #d7dce2);border-radius:4px;cursor:pointer;color:var(--ui-text, #1d232b);background:var(--ui-surface, #fff);white-space:nowrap}.reset-btn:hover{background:var(--ui-border, #d7dce2)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
244
+ }
245
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UIThemeTokenRow, decorators: [{
246
+ type: Component,
247
+ args: [{ selector: "ui-theme-token-row", standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
248
+ class: "ui-theme-token-row",
249
+ "[class.modified]": "token().override !== null",
250
+ }, template: "<div class=\"info\">\n <span class=\"name\">{{ token().name }}</span>\n <span class=\"description\">{{ token().description }}</span>\n <span class=\"chips\">\n <span class=\"chip type\">{{ token().type }}</span>\n <span class=\"chip scope\">{{ token().scope }}</span>\n <span class=\"chip ns\">{{ token().namespace }}</span>\n </span>\n</div>\n\n<div class=\"editor\">\n @if (isColor) {\n <input\n type=\"color\"\n class=\"color-swatch\"\n [value]=\"displayValue\"\n [attr.aria-label]=\"'Color picker for ' + token().name\"\n (input)=\"onColorInput($event)\"\n />\n }\n <input\n type=\"text\"\n class=\"value-input\"\n [value]=\"displayValue\"\n [attr.aria-label]=\"'Value for ' + token().name\"\n (input)=\"onTextInput($event)\"\n />\n</div>\n\n@if (token().override !== null) {\n <button\n type=\"button\"\n class=\"reset-btn\"\n aria-label=\"Reset token to default\"\n (click)=\"reset.emit()\"\n >\n Reset\n </button>\n}\n", styles: [":host{display:grid;grid-template-columns:1fr auto auto;align-items:start;gap:.5rem .75rem;padding:.6rem .75rem;border-bottom:1px solid var(--ui-border, #d7dce2);color:var(--ui-text, #1d232b);background:var(--ui-surface, #fff);transition:background .12s}:host.modified{background:color-mix(in srgb,var(--ui-accent, #3584e4) 10%,var(--ui-surface, #fff))}.info{display:flex;flex-direction:column;gap:.2rem;min-width:0}.name{font-family:monospace;font-size:.8rem;color:var(--ui-text, #1d232b);word-break:break-all}.description{font-size:.75rem;color:var(--ui-text-secondary, #6b7280)}.chips{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.15rem}.chip{display:inline-block;padding:.1rem .4rem;border-radius:10px;font-size:.65rem;font-weight:600;color:var(--ui-text, #1d232b);background:var(--ui-border, #d7dce2)}.chip.type{background:#dbeafe;color:#1e3a8a}.chip.scope{background:#dcfce7;color:#14532d}.chip.ns{background:#fef9c3;color:#713f12}.editor{display:flex;align-items:center;gap:.4rem}.color-swatch{width:2rem;height:2rem;border:1px solid var(--ui-border, #d7dce2);border-radius:4px;padding:0;cursor:pointer;background:none}.value-input{width:9rem;padding:.3rem .5rem;font-size:.8rem;font-family:monospace;border:1px solid var(--ui-border, #d7dce2);border-radius:4px;color:var(--ui-text, #1d232b);background:var(--ui-surface, #fff);outline:none}.value-input:focus{border-color:var(--ui-accent, #3584e4)}.reset-btn{padding:.25rem .6rem;font-size:.75rem;border:1px solid var(--ui-border, #d7dce2);border-radius:4px;cursor:pointer;color:var(--ui-text, #1d232b);background:var(--ui-surface, #fff);white-space:nowrap}.reset-btn:hover{background:var(--ui-border, #d7dce2)}\n"] }]
251
+ }], propDecorators: { token: [{ type: i0.Input, args: [{ isSignal: true, alias: "token", required: true }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], reset: [{ type: i0.Output, args: ["reset"] }] } });
252
+
253
+ /**
254
+ * Full-height split-pane theme editor.
255
+ *
256
+ * Drop it into any layout that gives it height. The left panel contains the
257
+ * token editor; the right panel renders a live component canvas that reflects
258
+ * every token change in real time. A toolbar lets you switch between
259
+ * horizontal and vertical orientations and swap the two panels.
260
+ *
261
+ * ### Usage
262
+ * ```html
263
+ * <ui-theme-studio style="height: 100%" />
264
+ * ```
265
+ */
266
+ class UIThemeStudio {
267
+ // ── Inputs ─────────────────────────────────────────────────────────
268
+ /**
269
+ * URL of the token manifest JSON.
270
+ * Defaults to `"assets/css-token-manifest.json"`.
271
+ */
272
+ manifestUrl = input("assets/css-token-manifest.json", ...(ngDevMode ? [{ debugName: "manifestUrl" }] : []));
273
+ /**
274
+ * Accessible label for the component region.
275
+ */
276
+ ariaLabel = input("Theme Studio", ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
277
+ // ── Layout state ───────────────────────────────────────────────────
278
+ /**
279
+ * Orientation of the split divider.
280
+ * `"horizontal"` = side by side, `"vertical"` = stacked.
281
+ */
282
+ splitOrientation = signal("horizontal", ...(ngDevMode ? [{ debugName: "splitOrientation" }] : []));
283
+ /**
284
+ * When `true`, the canvas is on the left / top and the token editor is on
285
+ * the right / bottom.
286
+ */
287
+ panelsSwapped = signal(false, ...(ngDevMode ? [{ debugName: "panelsSwapped" }] : []));
288
+ // ── Services ───────────────────────────────────────────────────────
289
+ /** @internal */
290
+ studio = inject(ThemeStudioService);
291
+ log = inject(LoggerFactory).createLogger("UIThemeStudio");
292
+ // ── Canvas data ────────────────────────────────────────────────────
293
+ /** @internal */
294
+ icons = {
295
+ horizontal: UIIcons.Lucide.Layout.LayoutPanelLeft,
296
+ vertical: UIIcons.Lucide.Layout.LayoutPanelTop,
297
+ swap: UIIcons.Lucide.Design.FlipHorizontal2,
298
+ };
299
+ /** @internal */
300
+ sampleDropdownOptions = [
301
+ { label: "Option A", value: "a" },
302
+ { label: "Option B", value: "b" },
303
+ { label: "Option C", value: "c" },
304
+ ];
305
+ // ── Lifecycle ──────────────────────────────────────────────────────
306
+ /** @inheritdoc */
307
+ ngOnInit() {
308
+ this.studio.open(this.manifestUrl());
309
+ }
310
+ // ── Toolbar actions ────────────────────────────────────────────────
311
+ /**
312
+ * Switch the split container orientation.
313
+ */
314
+ setOrientation(o) {
315
+ this.splitOrientation.set(o);
316
+ }
317
+ /**
318
+ * Toggle which panel appears first (left/top).
319
+ */
320
+ toggleSwap() {
321
+ this.panelsSwapped.update((s) => !s);
322
+ }
323
+ // ── Token editor handlers ──────────────────────────────────────────
324
+ /** @internal */
325
+ setFilterField(key, value) {
326
+ this.studio.filter.update((f) => ({ ...f, [key]: value }));
327
+ }
328
+ /** @internal */
329
+ onQueryChange(event) {
330
+ this.setFilterField("query", event.target.value);
331
+ }
332
+ /** @internal */
333
+ onTypeChange(event) {
334
+ this.setFilterField("type", event.target.value);
335
+ }
336
+ /** @internal */
337
+ onScopeChange(event) {
338
+ this.setFilterField("scope", event.target.value);
339
+ }
340
+ /** @internal */
341
+ onNamespaceChange(event) {
342
+ this.setFilterField("namespace", event.target.value);
343
+ }
344
+ /** @internal */
345
+ onModifiedOnlyChange(event) {
346
+ this.setFilterField("modifiedOnly", event.target.checked);
347
+ }
348
+ /** @internal */
349
+ onTokenValueChange(tokenName, value) {
350
+ this.studio.setOverride(tokenName, value);
351
+ }
352
+ /** @internal */
353
+ onTokenReset(tokenName) {
354
+ this.studio.setOverride(tokenName, null);
355
+ }
356
+ /** @internal */
357
+ copyToClipboard(text) {
358
+ navigator.clipboard.writeText(text).catch((err) => {
359
+ this.log.warn("Clipboard write failed", [err]);
360
+ });
361
+ }
362
+ /** @internal */
363
+ exportCss() {
364
+ const css = this.studio.exportCss();
365
+ if (css)
366
+ this.copyToClipboard(css);
367
+ }
368
+ /** @internal */
369
+ exportJson() {
370
+ const json = JSON.stringify(this.studio.exportJson(), null, 2);
371
+ if (json !== "{}")
372
+ this.copyToClipboard(json);
373
+ }
374
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UIThemeStudio, deps: [], target: i0.ɵɵFactoryTarget.Component });
375
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: UIThemeStudio, isStandalone: true, selector: "ui-theme-studio", inputs: { manifestUrl: { classPropertyName: "manifestUrl", publicName: "manifestUrl", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-label": "ariaLabel()" }, classAttribute: "ui-theme-studio" }, ngImport: i0, template: "<!-- \u2500\u2500 Top toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<div class=\"toolbar\">\n <span class=\"title\">Theme Studio</span>\n\n <div class=\"layout-controls\">\n <button\n type=\"button\"\n class=\"icon-btn\"\n [class.active]=\"splitOrientation() === 'horizontal'\"\n title=\"Horizontal split\"\n aria-label=\"Horizontal split\"\n (click)=\"setOrientation('horizontal')\"\n >\n <ui-icon [svg]=\"icons.horizontal\" [size]=\"16\" />\n </button>\n <button\n type=\"button\"\n class=\"icon-btn\"\n [class.active]=\"splitOrientation() === 'vertical'\"\n title=\"Vertical split\"\n aria-label=\"Vertical split\"\n (click)=\"setOrientation('vertical')\"\n >\n <ui-icon [svg]=\"icons.vertical\" [size]=\"16\" />\n </button>\n <button\n type=\"button\"\n class=\"icon-btn\"\n [class.active]=\"panelsSwapped()\"\n title=\"Swap panels\"\n aria-label=\"Swap panels\"\n (click)=\"toggleSwap()\"\n >\n <ui-icon [svg]=\"icons.swap\" [size]=\"16\" />\n </button>\n </div>\n\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"action-btn\"\n [disabled]=\"studio.modifiedTokens().length === 0\"\n (click)=\"exportCss()\"\n title=\"Copy CSS overrides to clipboard\"\n aria-label=\"Copy CSS overrides\"\n >\n Copy CSS\n </button>\n <button\n type=\"button\"\n class=\"action-btn\"\n [disabled]=\"studio.modifiedTokens().length === 0\"\n (click)=\"exportJson()\"\n title=\"Copy JSON overrides to clipboard\"\n aria-label=\"Copy JSON overrides\"\n >\n Copy JSON\n </button>\n <button\n type=\"button\"\n class=\"action-btn danger\"\n [disabled]=\"studio.modifiedTokens().length === 0\"\n (click)=\"studio.resetAll()\"\n aria-label=\"Reset all token overrides\"\n >\n Reset all\n </button>\n </div>\n</div>\n\n<!-- \u2500\u2500 Split container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (panelsSwapped()) {\n <ui-split-container\n class=\"split\"\n [orientation]=\"splitOrientation()\"\n name=\"theme-studio\"\n >\n <ui-split-panel [min]=\"300\">\n <ng-container [ngTemplateOutlet]=\"canvasTpl\" />\n </ui-split-panel>\n <ui-split-panel [min]=\"280\">\n <ng-container [ngTemplateOutlet]=\"tokenEditorTpl\" />\n </ui-split-panel>\n </ui-split-container>\n} @else {\n <ui-split-container\n class=\"split\"\n [orientation]=\"splitOrientation()\"\n name=\"theme-studio\"\n >\n <ui-split-panel [min]=\"280\">\n <ng-container [ngTemplateOutlet]=\"tokenEditorTpl\" />\n </ui-split-panel>\n <ui-split-panel [min]=\"300\">\n <ng-container [ngTemplateOutlet]=\"canvasTpl\" />\n </ui-split-panel>\n </ui-split-container>\n}\n\n<!-- \u2500\u2500 Token editor template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #tokenEditorTpl>\n <div class=\"editor-panel\">\n\n <!-- Filter toolbar -->\n <div class=\"filter-toolbar\">\n <input\n type=\"search\"\n class=\"search-input\"\n placeholder=\"Search tokens\u2026\"\n [value]=\"studio.filter().query\"\n (input)=\"onQueryChange($event)\"\n aria-label=\"Search tokens\"\n />\n <select\n class=\"filter-select\"\n [value]=\"studio.filter().type\"\n (change)=\"onTypeChange($event)\"\n aria-label=\"Filter by type\"\n >\n <option value=\"\">All types</option>\n <option value=\"color\">color</option>\n <option value=\"length\">length</option>\n <option value=\"font\">font</option>\n <option value=\"shadow\">shadow</option>\n <option value=\"number\">number</option>\n <option value=\"keyword\">keyword</option>\n </select>\n <select\n class=\"filter-select\"\n [value]=\"studio.filter().scope\"\n (change)=\"onScopeChange($event)\"\n aria-label=\"Filter by scope\"\n >\n <option value=\"\">All scopes</option>\n <option value=\"global\">global</option>\n <option value=\"component\">component</option>\n </select>\n\n @if (studio.manifest(); as m) {\n <select\n class=\"filter-select\"\n [value]=\"studio.filter().namespace\"\n (change)=\"onNamespaceChange($event)\"\n aria-label=\"Filter by namespace\"\n >\n <option value=\"\">All namespaces</option>\n @for (entry of m.namespaces | keyvalue; track entry.key) {\n <option [value]=\"entry.key\">{{ entry.key }}</option>\n }\n </select>\n }\n\n <label class=\"modified-toggle\">\n <input\n type=\"checkbox\"\n [checked]=\"studio.filter().modifiedOnly\"\n (change)=\"onModifiedOnlyChange($event)\"\n />\n Modified only\n </label>\n </div>\n\n <!-- Stats bar -->\n <div class=\"stats\">\n {{ studio.filteredTokens().length }} of {{ studio.tokens().length }} tokens\n @if (studio.modifiedTokens().length > 0) {\n \u00B7 <strong>{{ studio.modifiedTokens().length }} modified</strong>\n }\n </div>\n\n <!-- Token list -->\n <div class=\"token-list\" role=\"list\">\n @if (studio.loading()) {\n <div class=\"state-message\">Loading tokens\u2026</div>\n } @else if (studio.loadError()) {\n <div class=\"state-message error\">\n Failed to load manifest: {{ studio.loadError() }}\n </div>\n } @else if (studio.filteredTokens().length === 0) {\n <div class=\"state-message\">No tokens match the current filter.</div>\n } @else {\n @for (token of studio.filteredTokens(); track token.name) {\n <ui-theme-token-row\n [token]=\"token\"\n (valueChange)=\"onTokenValueChange(token.name, $event)\"\n (reset)=\"onTokenReset(token.name)\"\n role=\"listitem\"\n />\n }\n }\n </div>\n </div>\n</ng-template>\n\n<!-- \u2500\u2500 Canvas template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #canvasTpl>\n <div class=\"canvas\">\n\n <!-- Swatches -->\n <section class=\"section\">\n <h4 class=\"section-title\">Color tokens</h4>\n <div class=\"swatch-row\">\n <span class=\"swatch accent\">\n <span class=\"swatch-label\">accent</span>\n </span>\n <span class=\"swatch surface\">\n <span class=\"swatch-label\">surface</span>\n </span>\n <span class=\"swatch text-on-surface\">\n <span class=\"swatch-label\">text</span>\n </span>\n <span class=\"swatch border\">\n <span class=\"swatch-label\">border</span>\n </span>\n <span class=\"swatch error\">\n <span class=\"swatch-label\">error</span>\n </span>\n <span class=\"swatch success\">\n <span class=\"swatch-label\">success</span>\n </span>\n </div>\n </section>\n\n <!-- Buttons -->\n <section class=\"section\">\n <h4 class=\"section-title\">Buttons</h4>\n <div class=\"row wrap\">\n <ui-button variant=\"filled\" ariaLabel=\"Filled button\">Filled</ui-button>\n <ui-button variant=\"outlined\" ariaLabel=\"Outlined button\">Outlined</ui-button>\n <ui-button variant=\"ghost\" ariaLabel=\"Ghost button\">Ghost</ui-button>\n <ui-button variant=\"filled\" [disabled]=\"true\" ariaLabel=\"Disabled button\">Disabled</ui-button>\n </div>\n </section>\n\n <!-- Inputs -->\n <section class=\"section\">\n <h4 class=\"section-title\">Inputs</h4>\n <div class=\"col\">\n <ui-input placeholder=\"Text input\" ariaLabel=\"Sample text input\" />\n <ui-dropdown-list\n [options]=\"sampleDropdownOptions\"\n ariaLabel=\"Sample dropdown\"\n />\n </div>\n </section>\n\n <!-- Card -->\n <section class=\"section\">\n <h4 class=\"section-title\">Card</h4>\n <ui-card variant=\"outlined\">\n <ui-card-header>Card header</ui-card-header>\n <ui-card-body>\n <p class=\"body-text\">\n Token changes to <code>--ui-surface</code>,\n <code>--ui-border</code>, and <code>--ui-text</code> are reflected\n here immediately.\n </p>\n </ui-card-body>\n <ui-card-footer>\n <ui-button variant=\"filled\" ariaLabel=\"Card action\">Action</ui-button>\n <ui-button variant=\"ghost\" ariaLabel=\"Cancel\">Cancel</ui-button>\n </ui-card-footer>\n </ui-card>\n </section>\n\n <!-- Badges & chips -->\n <section class=\"section\">\n <h4 class=\"section-title\">Badges &amp; chips</h4>\n <div class=\"row wrap\">\n <ui-badge color=\"primary\">Primary</ui-badge>\n <ui-badge color=\"success\">Success</ui-badge>\n <ui-badge color=\"warning\">Warning</ui-badge>\n <ui-badge color=\"danger\">Danger</ui-badge>\n <ui-badge color=\"neutral\">Neutral</ui-badge>\n </div>\n <div class=\"row wrap\">\n <ui-chip>Default</ui-chip>\n <ui-chip>Active</ui-chip>\n <ui-chip>Archived</ui-chip>\n </div>\n </section>\n\n <!-- Progress -->\n <section class=\"section\">\n <h4 class=\"section-title\">Progress</h4>\n <ui-progress [value]=\"65\" ariaLabel=\"Sample progress, 65%\" />\n </section>\n\n <!-- Toggles & checkboxes -->\n <section class=\"section\">\n <h4 class=\"section-title\">Toggles &amp; checkboxes</h4>\n <div class=\"row wrap\">\n <ui-toggle [value]=\"true\" ariaLabel=\"Toggle on\" />\n <ui-toggle [value]=\"false\" ariaLabel=\"Toggle off\" />\n <ui-checkbox [checked]=\"true\" ariaLabel=\"Checked checkbox\">\n Checked\n </ui-checkbox>\n <ui-checkbox [checked]=\"false\" ariaLabel=\"Unchecked checkbox\">\n Unchecked\n </ui-checkbox>\n </div>\n </section>\n\n </div>\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;color:var(--ui-text, #1d232b);background:var(--ui-surface, #f7f8fa)}.toolbar{display:flex;align-items:center;gap:.5rem;padding:.55rem .75rem;border-bottom:1px solid var(--ui-border, #d7dce2);color:var(--ui-text, #1d232b);background:var(--ui-surface, #f7f8fa);flex-shrink:0}.title{font-weight:700;font-size:.9rem;color:var(--ui-text, #1d232b);white-space:nowrap}.layout-controls{display:flex;align-items:center;gap:.2rem;margin-left:.5rem}.icon-btn{display:flex;align-items:center;justify-content:center;width:1.8rem;height:1.8rem;border:1px solid transparent;border-radius:4px;cursor:pointer;color:var(--ui-text, #1d232b);background:transparent;opacity:.55}.icon-btn:hover{opacity:1;background:var(--ui-border, #d7dce2);border-color:var(--ui-border, #d7dce2)}.icon-btn.active{opacity:1;color:var(--ui-surface, #fff);background:var(--ui-accent, #1f6feb);border-color:var(--ui-accent, #1f6feb)}.header-actions{display:flex;align-items:center;gap:.35rem;margin-left:auto}.action-btn{padding:.25rem .65rem;font-size:.75rem;border:1px solid var(--ui-border, #d7dce2);border-radius:4px;cursor:pointer;color:var(--ui-text, #1d232b);background:var(--ui-surface, #fff);white-space:nowrap}.action-btn:hover:not(:disabled){background:var(--ui-border, #d7dce2)}.action-btn:disabled{opacity:.4;cursor:not-allowed}.action-btn.danger:not(:disabled){color:#dc2626;border-color:#fca5a5}.action-btn.danger:not(:disabled):hover{background:#fef2f2}.split{flex:1 1 0;min-height:0}.editor-panel{display:flex;flex-direction:column;height:100%;min-height:0;color:var(--ui-text, #1d232b);background:var(--ui-surface, #f7f8fa)}.filter-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding:.6rem .75rem;border-bottom:1px solid var(--ui-border, #d7dce2);flex-shrink:0}.search-input,.filter-select{padding:.3rem .5rem;font-size:.8rem;border:1px solid var(--ui-border, #d7dce2);border-radius:4px;color:var(--ui-text, #1d232b);background:var(--ui-surface, #fff);outline:none}.search-input:focus,.filter-select:focus{border-color:var(--ui-accent, #3584e4)}.search-input{flex:1 1 10rem;min-width:0}.filter-select{flex:0 1 auto}.modified-toggle{display:flex;align-items:center;gap:.3rem;font-size:.8rem;color:var(--ui-text, #1d232b);cursor:pointer;white-space:nowrap}.stats{padding:.35rem .75rem;font-size:.75rem;color:var(--ui-text, #1d232b);background:var(--ui-surface, #f7f8fa);border-bottom:1px solid var(--ui-border, #d7dce2);flex-shrink:0;opacity:.7}.token-list{flex:1 1 0;overflow-y:auto;overscroll-behavior:contain}.state-message{padding:2rem 1rem;text-align:center;font-size:.875rem;color:var(--ui-text, #1d232b);opacity:.6}.state-message.error{color:var(--ui-error, #dc2626);background:transparent;opacity:1}.canvas{display:flex;flex-direction:column;gap:0;height:100%;overflow-y:auto;overscroll-behavior:contain;padding:1rem;color:var(--ui-text, #1d232b);background:var(--ui-surface, #f7f8fa)}.section{display:flex;flex-direction:column;gap:.65rem;padding:1rem 0;border-bottom:1px solid var(--ui-border, #d7dce2)}.section:last-child{border-bottom:none}.section-title{margin:0;font-size:.7rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ui-text, #1d232b);opacity:.55}.row{display:flex;align-items:center;gap:.5rem}.row.wrap{flex-wrap:wrap}.col{display:flex;flex-direction:column;gap:.5rem}.body-text{margin:0;font-size:.875rem;line-height:1.55;color:var(--ui-text, #1d232b)}.swatch-row{display:flex;flex-wrap:wrap;gap:.5rem}.swatch{display:flex;align-items:flex-end;padding:.4rem .6rem;border-radius:6px;min-width:4.5rem;min-height:3rem;border:1px solid rgba(0,0,0,.08)}.swatch-label{font-size:.7rem;font-weight:600}.swatch.accent{background:var(--ui-accent, #1f6feb);color:#fff}.swatch.surface{background:var(--ui-surface, #f7f8fa);color:var(--ui-text, #1d232b);border-color:var(--ui-border, #d7dce2)}.swatch.text-on-surface{background:var(--ui-text, #1d232b);color:var(--ui-surface, #f7f8fa)}.swatch.border{background:var(--ui-border, #d7dce2);color:var(--ui-text, #1d232b)}.swatch.error{background:var(--ui-error, #dc2626);color:#fff}.swatch.success{background:var(--ui-success, #16a34a);color:#fff}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UIThemeTokenRow, selector: "ui-theme-token-row", inputs: ["token"], outputs: ["valueChange", "reset"] }, { kind: "component", type: UISplitContainer, selector: "ui-split-container", inputs: ["disabled", "orientation", "initialSizes", "name", "dividerWidth", "ariaLabel"], outputs: ["resized", "resizing"] }, { kind: "component", type: UISplitPanel, selector: "ui-split-panel", inputs: ["min", "max"] }, { kind: "component", type: UIButton, selector: "ui-button", inputs: ["type", "variant", "color", "size", "pill", "disabled", "ariaLabel"] }, { kind: "component", type: UIIcon, selector: "ui-icon", inputs: ["svg", "size", "ariaLabel"] }, { kind: "component", type: UIBadge, selector: "ui-badge", inputs: ["variant", "color", "count", "maxCount", "ariaLabel"] }, { kind: "component", type: UICard, selector: "ui-card", inputs: ["variant", "interactive", "ariaLabel"] }, { kind: "component", type: UICardBody, selector: "ui-card-body" }, { kind: "component", type: UICardFooter, selector: "ui-card-footer" }, { kind: "component", type: UICardHeader, selector: "ui-card-header", inputs: ["icon", "iconAriaLabel", "iconSize", "avatarSrc", "avatarEmail", "avatarName", "avatarAriaLabel", "avatarSize", "subtitle"] }, { kind: "component", type: UIChip, selector: "ui-chip", inputs: ["color", "removable", "disabled", "ariaLabel"], outputs: ["removed"] }, { kind: "component", type: UICheckbox, selector: "ui-checkbox", inputs: ["variant", "checked", "disabled", "indeterminate", "ariaLabel"], outputs: ["checkedChange"] }, { kind: "component", type: UIDropdownList, selector: "ui-dropdown-list", inputs: ["options", "placeholder", "disabled", "ariaLabel", "value"], outputs: ["valueChange"] }, { kind: "component", type: UIInput, selector: "ui-input", inputs: ["type", "text", "value", "adapter", "placeholder", "disabled", "multiline", "rows", "heightAdjustable", "ariaLabel"], outputs: ["textChange", "valueChange"] }, { kind: "component", type: UIProgress, selector: "ui-progress", inputs: ["variant", "mode", "value", "ariaLabel"] }, { kind: "component", type: UIToggle, selector: "ui-toggle", inputs: ["value", "onLabel", "offLabel", "disabled", "size", "ariaLabel"], outputs: ["valueChange"] }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
376
+ }
377
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UIThemeStudio, decorators: [{
378
+ type: Component,
379
+ args: [{ selector: "ui-theme-studio", standalone: true, imports: [
380
+ FormsModule,
381
+ KeyValuePipe,
382
+ NgTemplateOutlet,
383
+ UIThemeTokenRow,
384
+ UISplitContainer,
385
+ UISplitPanel,
386
+ UIButton,
387
+ UIIcon,
388
+ UIBadge,
389
+ UICard,
390
+ UICardBody,
391
+ UICardFooter,
392
+ UICardHeader,
393
+ UIChip,
394
+ UICheckbox,
395
+ UIDropdownList,
396
+ UIInput,
397
+ UIProgress,
398
+ UIToggle,
399
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
400
+ class: "ui-theme-studio",
401
+ "[attr.aria-label]": "ariaLabel()",
402
+ }, template: "<!-- \u2500\u2500 Top toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<div class=\"toolbar\">\n <span class=\"title\">Theme Studio</span>\n\n <div class=\"layout-controls\">\n <button\n type=\"button\"\n class=\"icon-btn\"\n [class.active]=\"splitOrientation() === 'horizontal'\"\n title=\"Horizontal split\"\n aria-label=\"Horizontal split\"\n (click)=\"setOrientation('horizontal')\"\n >\n <ui-icon [svg]=\"icons.horizontal\" [size]=\"16\" />\n </button>\n <button\n type=\"button\"\n class=\"icon-btn\"\n [class.active]=\"splitOrientation() === 'vertical'\"\n title=\"Vertical split\"\n aria-label=\"Vertical split\"\n (click)=\"setOrientation('vertical')\"\n >\n <ui-icon [svg]=\"icons.vertical\" [size]=\"16\" />\n </button>\n <button\n type=\"button\"\n class=\"icon-btn\"\n [class.active]=\"panelsSwapped()\"\n title=\"Swap panels\"\n aria-label=\"Swap panels\"\n (click)=\"toggleSwap()\"\n >\n <ui-icon [svg]=\"icons.swap\" [size]=\"16\" />\n </button>\n </div>\n\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"action-btn\"\n [disabled]=\"studio.modifiedTokens().length === 0\"\n (click)=\"exportCss()\"\n title=\"Copy CSS overrides to clipboard\"\n aria-label=\"Copy CSS overrides\"\n >\n Copy CSS\n </button>\n <button\n type=\"button\"\n class=\"action-btn\"\n [disabled]=\"studio.modifiedTokens().length === 0\"\n (click)=\"exportJson()\"\n title=\"Copy JSON overrides to clipboard\"\n aria-label=\"Copy JSON overrides\"\n >\n Copy JSON\n </button>\n <button\n type=\"button\"\n class=\"action-btn danger\"\n [disabled]=\"studio.modifiedTokens().length === 0\"\n (click)=\"studio.resetAll()\"\n aria-label=\"Reset all token overrides\"\n >\n Reset all\n </button>\n </div>\n</div>\n\n<!-- \u2500\u2500 Split container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n@if (panelsSwapped()) {\n <ui-split-container\n class=\"split\"\n [orientation]=\"splitOrientation()\"\n name=\"theme-studio\"\n >\n <ui-split-panel [min]=\"300\">\n <ng-container [ngTemplateOutlet]=\"canvasTpl\" />\n </ui-split-panel>\n <ui-split-panel [min]=\"280\">\n <ng-container [ngTemplateOutlet]=\"tokenEditorTpl\" />\n </ui-split-panel>\n </ui-split-container>\n} @else {\n <ui-split-container\n class=\"split\"\n [orientation]=\"splitOrientation()\"\n name=\"theme-studio\"\n >\n <ui-split-panel [min]=\"280\">\n <ng-container [ngTemplateOutlet]=\"tokenEditorTpl\" />\n </ui-split-panel>\n <ui-split-panel [min]=\"300\">\n <ng-container [ngTemplateOutlet]=\"canvasTpl\" />\n </ui-split-panel>\n </ui-split-container>\n}\n\n<!-- \u2500\u2500 Token editor template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #tokenEditorTpl>\n <div class=\"editor-panel\">\n\n <!-- Filter toolbar -->\n <div class=\"filter-toolbar\">\n <input\n type=\"search\"\n class=\"search-input\"\n placeholder=\"Search tokens\u2026\"\n [value]=\"studio.filter().query\"\n (input)=\"onQueryChange($event)\"\n aria-label=\"Search tokens\"\n />\n <select\n class=\"filter-select\"\n [value]=\"studio.filter().type\"\n (change)=\"onTypeChange($event)\"\n aria-label=\"Filter by type\"\n >\n <option value=\"\">All types</option>\n <option value=\"color\">color</option>\n <option value=\"length\">length</option>\n <option value=\"font\">font</option>\n <option value=\"shadow\">shadow</option>\n <option value=\"number\">number</option>\n <option value=\"keyword\">keyword</option>\n </select>\n <select\n class=\"filter-select\"\n [value]=\"studio.filter().scope\"\n (change)=\"onScopeChange($event)\"\n aria-label=\"Filter by scope\"\n >\n <option value=\"\">All scopes</option>\n <option value=\"global\">global</option>\n <option value=\"component\">component</option>\n </select>\n\n @if (studio.manifest(); as m) {\n <select\n class=\"filter-select\"\n [value]=\"studio.filter().namespace\"\n (change)=\"onNamespaceChange($event)\"\n aria-label=\"Filter by namespace\"\n >\n <option value=\"\">All namespaces</option>\n @for (entry of m.namespaces | keyvalue; track entry.key) {\n <option [value]=\"entry.key\">{{ entry.key }}</option>\n }\n </select>\n }\n\n <label class=\"modified-toggle\">\n <input\n type=\"checkbox\"\n [checked]=\"studio.filter().modifiedOnly\"\n (change)=\"onModifiedOnlyChange($event)\"\n />\n Modified only\n </label>\n </div>\n\n <!-- Stats bar -->\n <div class=\"stats\">\n {{ studio.filteredTokens().length }} of {{ studio.tokens().length }} tokens\n @if (studio.modifiedTokens().length > 0) {\n \u00B7 <strong>{{ studio.modifiedTokens().length }} modified</strong>\n }\n </div>\n\n <!-- Token list -->\n <div class=\"token-list\" role=\"list\">\n @if (studio.loading()) {\n <div class=\"state-message\">Loading tokens\u2026</div>\n } @else if (studio.loadError()) {\n <div class=\"state-message error\">\n Failed to load manifest: {{ studio.loadError() }}\n </div>\n } @else if (studio.filteredTokens().length === 0) {\n <div class=\"state-message\">No tokens match the current filter.</div>\n } @else {\n @for (token of studio.filteredTokens(); track token.name) {\n <ui-theme-token-row\n [token]=\"token\"\n (valueChange)=\"onTokenValueChange(token.name, $event)\"\n (reset)=\"onTokenReset(token.name)\"\n role=\"listitem\"\n />\n }\n }\n </div>\n </div>\n</ng-template>\n\n<!-- \u2500\u2500 Canvas template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n<ng-template #canvasTpl>\n <div class=\"canvas\">\n\n <!-- Swatches -->\n <section class=\"section\">\n <h4 class=\"section-title\">Color tokens</h4>\n <div class=\"swatch-row\">\n <span class=\"swatch accent\">\n <span class=\"swatch-label\">accent</span>\n </span>\n <span class=\"swatch surface\">\n <span class=\"swatch-label\">surface</span>\n </span>\n <span class=\"swatch text-on-surface\">\n <span class=\"swatch-label\">text</span>\n </span>\n <span class=\"swatch border\">\n <span class=\"swatch-label\">border</span>\n </span>\n <span class=\"swatch error\">\n <span class=\"swatch-label\">error</span>\n </span>\n <span class=\"swatch success\">\n <span class=\"swatch-label\">success</span>\n </span>\n </div>\n </section>\n\n <!-- Buttons -->\n <section class=\"section\">\n <h4 class=\"section-title\">Buttons</h4>\n <div class=\"row wrap\">\n <ui-button variant=\"filled\" ariaLabel=\"Filled button\">Filled</ui-button>\n <ui-button variant=\"outlined\" ariaLabel=\"Outlined button\">Outlined</ui-button>\n <ui-button variant=\"ghost\" ariaLabel=\"Ghost button\">Ghost</ui-button>\n <ui-button variant=\"filled\" [disabled]=\"true\" ariaLabel=\"Disabled button\">Disabled</ui-button>\n </div>\n </section>\n\n <!-- Inputs -->\n <section class=\"section\">\n <h4 class=\"section-title\">Inputs</h4>\n <div class=\"col\">\n <ui-input placeholder=\"Text input\" ariaLabel=\"Sample text input\" />\n <ui-dropdown-list\n [options]=\"sampleDropdownOptions\"\n ariaLabel=\"Sample dropdown\"\n />\n </div>\n </section>\n\n <!-- Card -->\n <section class=\"section\">\n <h4 class=\"section-title\">Card</h4>\n <ui-card variant=\"outlined\">\n <ui-card-header>Card header</ui-card-header>\n <ui-card-body>\n <p class=\"body-text\">\n Token changes to <code>--ui-surface</code>,\n <code>--ui-border</code>, and <code>--ui-text</code> are reflected\n here immediately.\n </p>\n </ui-card-body>\n <ui-card-footer>\n <ui-button variant=\"filled\" ariaLabel=\"Card action\">Action</ui-button>\n <ui-button variant=\"ghost\" ariaLabel=\"Cancel\">Cancel</ui-button>\n </ui-card-footer>\n </ui-card>\n </section>\n\n <!-- Badges & chips -->\n <section class=\"section\">\n <h4 class=\"section-title\">Badges &amp; chips</h4>\n <div class=\"row wrap\">\n <ui-badge color=\"primary\">Primary</ui-badge>\n <ui-badge color=\"success\">Success</ui-badge>\n <ui-badge color=\"warning\">Warning</ui-badge>\n <ui-badge color=\"danger\">Danger</ui-badge>\n <ui-badge color=\"neutral\">Neutral</ui-badge>\n </div>\n <div class=\"row wrap\">\n <ui-chip>Default</ui-chip>\n <ui-chip>Active</ui-chip>\n <ui-chip>Archived</ui-chip>\n </div>\n </section>\n\n <!-- Progress -->\n <section class=\"section\">\n <h4 class=\"section-title\">Progress</h4>\n <ui-progress [value]=\"65\" ariaLabel=\"Sample progress, 65%\" />\n </section>\n\n <!-- Toggles & checkboxes -->\n <section class=\"section\">\n <h4 class=\"section-title\">Toggles &amp; checkboxes</h4>\n <div class=\"row wrap\">\n <ui-toggle [value]=\"true\" ariaLabel=\"Toggle on\" />\n <ui-toggle [value]=\"false\" ariaLabel=\"Toggle off\" />\n <ui-checkbox [checked]=\"true\" ariaLabel=\"Checked checkbox\">\n Checked\n </ui-checkbox>\n <ui-checkbox [checked]=\"false\" ariaLabel=\"Unchecked checkbox\">\n Unchecked\n </ui-checkbox>\n </div>\n </section>\n\n </div>\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;color:var(--ui-text, #1d232b);background:var(--ui-surface, #f7f8fa)}.toolbar{display:flex;align-items:center;gap:.5rem;padding:.55rem .75rem;border-bottom:1px solid var(--ui-border, #d7dce2);color:var(--ui-text, #1d232b);background:var(--ui-surface, #f7f8fa);flex-shrink:0}.title{font-weight:700;font-size:.9rem;color:var(--ui-text, #1d232b);white-space:nowrap}.layout-controls{display:flex;align-items:center;gap:.2rem;margin-left:.5rem}.icon-btn{display:flex;align-items:center;justify-content:center;width:1.8rem;height:1.8rem;border:1px solid transparent;border-radius:4px;cursor:pointer;color:var(--ui-text, #1d232b);background:transparent;opacity:.55}.icon-btn:hover{opacity:1;background:var(--ui-border, #d7dce2);border-color:var(--ui-border, #d7dce2)}.icon-btn.active{opacity:1;color:var(--ui-surface, #fff);background:var(--ui-accent, #1f6feb);border-color:var(--ui-accent, #1f6feb)}.header-actions{display:flex;align-items:center;gap:.35rem;margin-left:auto}.action-btn{padding:.25rem .65rem;font-size:.75rem;border:1px solid var(--ui-border, #d7dce2);border-radius:4px;cursor:pointer;color:var(--ui-text, #1d232b);background:var(--ui-surface, #fff);white-space:nowrap}.action-btn:hover:not(:disabled){background:var(--ui-border, #d7dce2)}.action-btn:disabled{opacity:.4;cursor:not-allowed}.action-btn.danger:not(:disabled){color:#dc2626;border-color:#fca5a5}.action-btn.danger:not(:disabled):hover{background:#fef2f2}.split{flex:1 1 0;min-height:0}.editor-panel{display:flex;flex-direction:column;height:100%;min-height:0;color:var(--ui-text, #1d232b);background:var(--ui-surface, #f7f8fa)}.filter-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding:.6rem .75rem;border-bottom:1px solid var(--ui-border, #d7dce2);flex-shrink:0}.search-input,.filter-select{padding:.3rem .5rem;font-size:.8rem;border:1px solid var(--ui-border, #d7dce2);border-radius:4px;color:var(--ui-text, #1d232b);background:var(--ui-surface, #fff);outline:none}.search-input:focus,.filter-select:focus{border-color:var(--ui-accent, #3584e4)}.search-input{flex:1 1 10rem;min-width:0}.filter-select{flex:0 1 auto}.modified-toggle{display:flex;align-items:center;gap:.3rem;font-size:.8rem;color:var(--ui-text, #1d232b);cursor:pointer;white-space:nowrap}.stats{padding:.35rem .75rem;font-size:.75rem;color:var(--ui-text, #1d232b);background:var(--ui-surface, #f7f8fa);border-bottom:1px solid var(--ui-border, #d7dce2);flex-shrink:0;opacity:.7}.token-list{flex:1 1 0;overflow-y:auto;overscroll-behavior:contain}.state-message{padding:2rem 1rem;text-align:center;font-size:.875rem;color:var(--ui-text, #1d232b);opacity:.6}.state-message.error{color:var(--ui-error, #dc2626);background:transparent;opacity:1}.canvas{display:flex;flex-direction:column;gap:0;height:100%;overflow-y:auto;overscroll-behavior:contain;padding:1rem;color:var(--ui-text, #1d232b);background:var(--ui-surface, #f7f8fa)}.section{display:flex;flex-direction:column;gap:.65rem;padding:1rem 0;border-bottom:1px solid var(--ui-border, #d7dce2)}.section:last-child{border-bottom:none}.section-title{margin:0;font-size:.7rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ui-text, #1d232b);opacity:.55}.row{display:flex;align-items:center;gap:.5rem}.row.wrap{flex-wrap:wrap}.col{display:flex;flex-direction:column;gap:.5rem}.body-text{margin:0;font-size:.875rem;line-height:1.55;color:var(--ui-text, #1d232b)}.swatch-row{display:flex;flex-wrap:wrap;gap:.5rem}.swatch{display:flex;align-items:flex-end;padding:.4rem .6rem;border-radius:6px;min-width:4.5rem;min-height:3rem;border:1px solid rgba(0,0,0,.08)}.swatch-label{font-size:.7rem;font-weight:600}.swatch.accent{background:var(--ui-accent, #1f6feb);color:#fff}.swatch.surface{background:var(--ui-surface, #f7f8fa);color:var(--ui-text, #1d232b);border-color:var(--ui-border, #d7dce2)}.swatch.text-on-surface{background:var(--ui-text, #1d232b);color:var(--ui-surface, #f7f8fa)}.swatch.border{background:var(--ui-border, #d7dce2);color:var(--ui-text, #1d232b)}.swatch.error{background:var(--ui-error, #dc2626);color:#fff}.swatch.success{background:var(--ui-success, #16a34a);color:#fff}\n"] }]
403
+ }], propDecorators: { manifestUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "manifestUrl", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
404
+
405
+ /*
406
+ * Public API Surface of @theredhead/lucid-theme-studio
407
+ */
408
+
409
+ /**
410
+ * Generated bundle index. Do not edit.
411
+ */
412
+
413
+ export { ThemeStudioService, UIThemeStudio, UIThemeTokenRow };
414
+ //# sourceMappingURL=theredhead-lucid-theme-studio.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theredhead-lucid-theme-studio.mjs","sources":["../../../packages/ui-theme-studio/src/lib/theme-studio.service.ts","../../../packages/ui-theme-studio/src/lib/token-row/theme-token-row.component.ts","../../../packages/ui-theme-studio/src/lib/token-row/theme-token-row.component.html","../../../packages/ui-theme-studio/src/lib/theme-studio.component.ts","../../../packages/ui-theme-studio/src/lib/theme-studio.component.html","../../../packages/ui-theme-studio/src/public-api.ts","../../../packages/ui-theme-studio/src/theredhead-lucid-theme-studio.ts"],"sourcesContent":["import { DOCUMENT } from \"@angular/common\";\nimport { computed, inject, Injectable, signal } from \"@angular/core\";\n\nimport { LoggerFactory } from \"@theredhead/lucid-foundation\";\n\nimport type {\n ThemeToken,\n ThemeTokenFilter,\n ThemeTokenManifest,\n ThemeTokenState,\n} from \"./theme-studio.types\";\n\n/**\n * State management service for the Theme Studio drawer.\n *\n * Loads the CSS token manifest, reads live computed values from the document\n * root, tracks per-token overrides, and applies them in real time via\n * `document.documentElement.style.setProperty`.\n *\n * Inject this wherever you need to open/close the studio or query its state.\n *\n * @example\n * ```ts\n * export class DevToolbar {\n * private readonly studio = inject(ThemeStudioService);\n *\n * protected open(): void { this.studio.open(); }\n * }\n * ```\n */\n@Injectable({ providedIn: \"root\" })\nexport class ThemeStudioService {\n private readonly document = inject(DOCUMENT);\n\n private readonly log =\n inject(LoggerFactory).createLogger(\"ThemeStudioService\");\n\n // ── Drawer visibility ──────────────────────────────────────────────\n\n /** Whether the studio drawer is currently visible. */\n public readonly isOpen = signal(false);\n\n /** Open the studio drawer, loading the manifest if needed. */\n public open(manifestUrl = \"assets/css-token-manifest.json\"): void {\n if (!this.isOpen()) {\n this.loadManifest(manifestUrl);\n this.isOpen.set(true);\n }\n }\n\n /** Close the studio drawer. */\n public close(): void {\n this.isOpen.set(false);\n }\n\n /** Toggle the studio drawer. */\n public toggle(manifestUrl = \"assets/css-token-manifest.json\"): void {\n if (this.isOpen()) {\n this.close();\n } else {\n this.open(manifestUrl);\n }\n }\n\n // ── Manifest + token state ─────────────────────────────────────────\n\n /** Loading state for the manifest fetch. */\n public readonly loading = signal(false);\n\n /** Error message from the most recent manifest load attempt, or null. */\n public readonly loadError = signal<string | null>(null);\n\n /** Raw manifest loaded from JSON. */\n public readonly manifest = signal<ThemeTokenManifest | null>(null);\n\n /** All tokens with their live computed values and any studio overrides. */\n public readonly tokens = signal<ThemeTokenState[]>([]);\n\n // ── Filter ─────────────────────────────────────────────────────────\n\n /** Active filter applied to the token list. */\n public readonly filter = signal<ThemeTokenFilter>({\n query: \"\",\n type: \"\",\n scope: \"\",\n namespace: \"\",\n modifiedOnly: false,\n });\n\n /** Tokens after the active filter is applied. */\n public readonly filteredTokens = computed(() => {\n const { query, type, scope, namespace, modifiedOnly } = this.filter();\n const q = query.toLowerCase();\n\n return this.tokens().filter((t) => {\n if (modifiedOnly && t.override === null) return false;\n if (type && t.type !== type) return false;\n if (scope && t.scope !== scope) return false;\n if (namespace && t.namespace !== namespace) return false;\n if (\n q &&\n !t.name.toLowerCase().includes(q) &&\n !t.description.toLowerCase().includes(q)\n ) {\n return false;\n }\n return true;\n });\n });\n\n /** Tokens that have been modified by the studio. */\n public readonly modifiedTokens = computed(() =>\n this.tokens().filter((t) => t.override !== null),\n );\n\n // ── Override API ───────────────────────────────────────────────────\n\n /**\n * Apply an override value to a token and update the live document.\n * Pass `null` to reset the token to its original computed value.\n */\n public setOverride(tokenName: string, value: string | null): void {\n const root = this.document.documentElement;\n\n if (value === null) {\n root.style.removeProperty(tokenName);\n } else {\n root.style.setProperty(tokenName, value);\n }\n\n this.tokens.update((list) =>\n list.map((t) => (t.name === tokenName ? { ...t, override: value } : t)),\n );\n }\n\n /** Reset all overrides and re-read computed values from the document. */\n public resetAll(): void {\n this.tokens().forEach((t) => {\n if (t.override !== null) {\n this.document.documentElement.style.removeProperty(t.name);\n }\n });\n this.tokens.update((list) =>\n list.map((t) => ({\n ...t,\n override: null,\n computedValue:\n this.readComputedValue(t.name) ||\n t.values.light ||\n t.values.dark ||\n \"\",\n })),\n );\n }\n\n /**\n * Export the current overrides as a CSS snippet that can be pasted into\n * a stylesheet.\n */\n public exportCss(): string {\n const lines = this.modifiedTokens()\n .map((t) => ` ${t.name}: ${t.override};`)\n .join(\"\\n\");\n return lines ? `:root {\\n${lines}\\n}` : \"\";\n }\n\n /**\n * Export the current overrides as a JSON object\n * (`{ \"--ui-accent\": \"#ff0000\", ... }`).\n */\n public exportJson(): Record<string, string> {\n return Object.fromEntries(\n this.modifiedTokens()\n .filter(\n (t): t is ThemeTokenState & { override: string } =>\n t.override !== null,\n )\n .map((t) => [t.name, t.override]),\n );\n }\n\n /**\n * Import a set of overrides from a plain object.\n * Only tokens present in the manifest are applied.\n */\n public importOverrides(overrides: Record<string, string>): void {\n const known = new Set(this.tokens().map((t) => t.name));\n Object.entries(overrides).forEach(([name, value]) => {\n if (known.has(name)) this.setOverride(name, value);\n });\n }\n\n // ── Private helpers ────────────────────────────────────────────────\n\n private loaded = false;\n\n private loadManifest(url: string): void {\n if (this.loaded) return;\n this.loading.set(true);\n this.loadError.set(null);\n\n fetch(url)\n .then((res) => {\n if (!res.ok) throw new Error(`${res.status} ${url}`);\n return res.json() as Promise<ThemeTokenManifest>;\n })\n .then((manifest) => {\n this.manifest.set(manifest);\n this.tokens.set(manifest.tokens.map((t) => this.hydrateToken(t)));\n this.loaded = true;\n this.loading.set(false);\n this.log.info(\"Loaded token manifest\", [manifest.tokenCount, \"tokens\"]);\n })\n .catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : String(err);\n this.loadError.set(msg);\n this.loading.set(false);\n this.log.error(\"Failed to load token manifest\", [msg]);\n });\n }\n\n private hydrateToken(token: ThemeToken): ThemeTokenState {\n const computedValue =\n this.readComputedValue(token.name) ||\n token.values.light ||\n token.values.dark ||\n \"\";\n return {\n ...token,\n computedValue,\n override: null,\n };\n }\n\n private readComputedValue(name: string): string {\n return getComputedStyle(this.document.documentElement)\n .getPropertyValue(name)\n .trim();\n }\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n inject,\n input,\n output,\n} from \"@angular/core\";\n\nimport type { ThemeTokenState } from \"../theme-studio.types\";\nimport { ThemeStudioService } from \"../theme-studio.service\";\n\n/**\n * A single row in the theme-studio token list.\n *\n * Displays the token name, description, type/scope chips, and an editor\n * appropriate to the token type (colour swatch + text, or plain text).\n */\n@Component({\n selector: \"ui-theme-token-row\",\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: \"./theme-token-row.component.html\",\n styleUrl: \"./theme-token-row.component.scss\",\n host: {\n class: \"ui-theme-token-row\",\n \"[class.modified]\": \"token().override !== null\",\n },\n})\nexport class UIThemeTokenRow {\n // ── Inputs / outputs ───────────────────────────────────────────────\n\n /** The token to display and edit. */\n public readonly token = input.required<ThemeTokenState>();\n\n /** Emitted when the user changes the token value. */\n public readonly valueChange = output<string>();\n\n /** Emitted when the user clicks Reset. */\n public readonly reset = output<void>();\n\n // ── Private ────────────────────────────────────────────────────────\n\n private readonly studio = inject(ThemeStudioService);\n\n // ── Protected helpers ──────────────────────────────────────────────\n\n /** The effective displayed value: override if set, else computed. */\n protected get displayValue(): string {\n const t = this.token();\n return t.override !== null ? t.override : t.computedValue;\n }\n\n /** `true` when the token type is `\"color\"`. */\n protected get isColor(): boolean {\n return this.token().type === \"color\";\n }\n\n /** Forward text-input changes through the output. */\n protected onTextInput(event: Event): void {\n const value = (event.target as HTMLInputElement).value;\n this.valueChange.emit(value);\n }\n\n /** Forward colour-picker changes through the output. */\n protected onColorInput(event: Event): void {\n const value = (event.target as HTMLInputElement).value;\n this.valueChange.emit(value);\n }\n}\n","<div class=\"info\">\n <span class=\"name\">{{ token().name }}</span>\n <span class=\"description\">{{ token().description }}</span>\n <span class=\"chips\">\n <span class=\"chip type\">{{ token().type }}</span>\n <span class=\"chip scope\">{{ token().scope }}</span>\n <span class=\"chip ns\">{{ token().namespace }}</span>\n </span>\n</div>\n\n<div class=\"editor\">\n @if (isColor) {\n <input\n type=\"color\"\n class=\"color-swatch\"\n [value]=\"displayValue\"\n [attr.aria-label]=\"'Color picker for ' + token().name\"\n (input)=\"onColorInput($event)\"\n />\n }\n <input\n type=\"text\"\n class=\"value-input\"\n [value]=\"displayValue\"\n [attr.aria-label]=\"'Value for ' + token().name\"\n (input)=\"onTextInput($event)\"\n />\n</div>\n\n@if (token().override !== null) {\n <button\n type=\"button\"\n class=\"reset-btn\"\n aria-label=\"Reset token to default\"\n (click)=\"reset.emit()\"\n >\n Reset\n </button>\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n inject,\n input,\n OnInit,\n signal,\n} from \"@angular/core\";\nimport { FormsModule } from \"@angular/forms\";\nimport { NgTemplateOutlet, KeyValuePipe } from \"@angular/common\";\n\nimport { LoggerFactory } from \"@theredhead/lucid-foundation\";\nimport {\n UIBadge,\n UIButton,\n UICard,\n UICardBody,\n UICardFooter,\n UICardHeader,\n UICheckbox,\n UIChip,\n UIDropdownList,\n UIIcon,\n UIIcons,\n UIInput,\n UIProgress,\n UISplitContainer,\n UISplitPanel,\n UIToggle,\n type SelectOption,\n type SplitOrientation,\n} from \"@theredhead/lucid-kit\";\n\nimport { ThemeStudioService } from \"./theme-studio.service\";\nimport { UIThemeTokenRow } from \"./token-row/theme-token-row.component\";\n\n/**\n * Full-height split-pane theme editor.\n *\n * Drop it into any layout that gives it height. The left panel contains the\n * token editor; the right panel renders a live component canvas that reflects\n * every token change in real time. A toolbar lets you switch between\n * horizontal and vertical orientations and swap the two panels.\n *\n * ### Usage\n * ```html\n * <ui-theme-studio style=\"height: 100%\" />\n * ```\n */\n@Component({\n selector: \"ui-theme-studio\",\n standalone: true,\n imports: [\n FormsModule,\n KeyValuePipe,\n NgTemplateOutlet,\n UIThemeTokenRow,\n UISplitContainer,\n UISplitPanel,\n UIButton,\n UIIcon,\n UIBadge,\n UICard,\n UICardBody,\n UICardFooter,\n UICardHeader,\n UIChip,\n UICheckbox,\n UIDropdownList,\n UIInput,\n UIProgress,\n UIToggle,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: \"./theme-studio.component.html\",\n styleUrl: \"./theme-studio.component.scss\",\n host: {\n class: \"ui-theme-studio\",\n \"[attr.aria-label]\": \"ariaLabel()\",\n },\n})\nexport class UIThemeStudio implements OnInit {\n // ── Inputs ─────────────────────────────────────────────────────────\n\n /**\n * URL of the token manifest JSON.\n * Defaults to `\"assets/css-token-manifest.json\"`.\n */\n public readonly manifestUrl = input(\"assets/css-token-manifest.json\");\n\n /**\n * Accessible label for the component region.\n */\n public readonly ariaLabel = input(\"Theme Studio\");\n\n // ── Layout state ───────────────────────────────────────────────────\n\n /**\n * Orientation of the split divider.\n * `\"horizontal\"` = side by side, `\"vertical\"` = stacked.\n */\n protected readonly splitOrientation = signal<SplitOrientation>(\"horizontal\");\n\n /**\n * When `true`, the canvas is on the left / top and the token editor is on\n * the right / bottom.\n */\n protected readonly panelsSwapped = signal(false);\n\n // ── Services ───────────────────────────────────────────────────────\n\n /** @internal */\n protected readonly studio = inject(ThemeStudioService);\n\n private readonly log = inject(LoggerFactory).createLogger(\"UIThemeStudio\");\n\n // ── Canvas data ────────────────────────────────────────────────────\n\n /** @internal */\n protected readonly icons = {\n horizontal: UIIcons.Lucide.Layout.LayoutPanelLeft,\n vertical: UIIcons.Lucide.Layout.LayoutPanelTop,\n swap: UIIcons.Lucide.Design.FlipHorizontal2,\n } as const;\n\n /** @internal */\n protected readonly sampleDropdownOptions: readonly SelectOption[] = [\n { label: \"Option A\", value: \"a\" },\n { label: \"Option B\", value: \"b\" },\n { label: \"Option C\", value: \"c\" },\n ];\n\n // ── Lifecycle ──────────────────────────────────────────────────────\n\n /** @inheritdoc */\n public ngOnInit(): void {\n this.studio.open(this.manifestUrl());\n }\n\n // ── Toolbar actions ────────────────────────────────────────────────\n\n /**\n * Switch the split container orientation.\n */\n protected setOrientation(o: SplitOrientation): void {\n this.splitOrientation.set(o);\n }\n\n /**\n * Toggle which panel appears first (left/top).\n */\n protected toggleSwap(): void {\n this.panelsSwapped.update((s) => !s);\n }\n\n // ── Token editor handlers ──────────────────────────────────────────\n\n /** @internal */\n protected setFilterField<\n K extends keyof ReturnType<typeof this.studio.filter>,\n >(key: K, value: ReturnType<typeof this.studio.filter>[K]): void {\n this.studio.filter.update((f) => ({ ...f, [key]: value }));\n }\n\n /** @internal */\n protected onQueryChange(event: Event): void {\n this.setFilterField(\"query\", (event.target as HTMLInputElement).value);\n }\n\n /** @internal */\n protected onTypeChange(event: Event): void {\n this.setFilterField(\"type\", (event.target as HTMLSelectElement).value);\n }\n\n /** @internal */\n protected onScopeChange(event: Event): void {\n this.setFilterField(\"scope\", (event.target as HTMLSelectElement).value);\n }\n\n /** @internal */\n protected onNamespaceChange(event: Event): void {\n this.setFilterField(\"namespace\", (event.target as HTMLSelectElement).value);\n }\n\n /** @internal */\n protected onModifiedOnlyChange(event: Event): void {\n this.setFilterField(\n \"modifiedOnly\",\n (event.target as HTMLInputElement).checked,\n );\n }\n\n /** @internal */\n protected onTokenValueChange(tokenName: string, value: string): void {\n this.studio.setOverride(tokenName, value);\n }\n\n /** @internal */\n protected onTokenReset(tokenName: string): void {\n this.studio.setOverride(tokenName, null);\n }\n\n /** @internal */\n protected copyToClipboard(text: string): void {\n navigator.clipboard.writeText(text).catch((err: unknown) => {\n this.log.warn(\"Clipboard write failed\", [err]);\n });\n }\n\n /** @internal */\n protected exportCss(): void {\n const css = this.studio.exportCss();\n if (css) this.copyToClipboard(css);\n }\n\n /** @internal */\n protected exportJson(): void {\n const json = JSON.stringify(this.studio.exportJson(), null, 2);\n if (json !== \"{}\") this.copyToClipboard(json);\n }\n}\n","<!-- ── Top toolbar ──────────────────────────────────────────────────── -->\n<div class=\"toolbar\">\n <span class=\"title\">Theme Studio</span>\n\n <div class=\"layout-controls\">\n <button\n type=\"button\"\n class=\"icon-btn\"\n [class.active]=\"splitOrientation() === 'horizontal'\"\n title=\"Horizontal split\"\n aria-label=\"Horizontal split\"\n (click)=\"setOrientation('horizontal')\"\n >\n <ui-icon [svg]=\"icons.horizontal\" [size]=\"16\" />\n </button>\n <button\n type=\"button\"\n class=\"icon-btn\"\n [class.active]=\"splitOrientation() === 'vertical'\"\n title=\"Vertical split\"\n aria-label=\"Vertical split\"\n (click)=\"setOrientation('vertical')\"\n >\n <ui-icon [svg]=\"icons.vertical\" [size]=\"16\" />\n </button>\n <button\n type=\"button\"\n class=\"icon-btn\"\n [class.active]=\"panelsSwapped()\"\n title=\"Swap panels\"\n aria-label=\"Swap panels\"\n (click)=\"toggleSwap()\"\n >\n <ui-icon [svg]=\"icons.swap\" [size]=\"16\" />\n </button>\n </div>\n\n <div class=\"header-actions\">\n <button\n type=\"button\"\n class=\"action-btn\"\n [disabled]=\"studio.modifiedTokens().length === 0\"\n (click)=\"exportCss()\"\n title=\"Copy CSS overrides to clipboard\"\n aria-label=\"Copy CSS overrides\"\n >\n Copy CSS\n </button>\n <button\n type=\"button\"\n class=\"action-btn\"\n [disabled]=\"studio.modifiedTokens().length === 0\"\n (click)=\"exportJson()\"\n title=\"Copy JSON overrides to clipboard\"\n aria-label=\"Copy JSON overrides\"\n >\n Copy JSON\n </button>\n <button\n type=\"button\"\n class=\"action-btn danger\"\n [disabled]=\"studio.modifiedTokens().length === 0\"\n (click)=\"studio.resetAll()\"\n aria-label=\"Reset all token overrides\"\n >\n Reset all\n </button>\n </div>\n</div>\n\n<!-- ── Split container ─────────────────────────────────────────────── -->\n@if (panelsSwapped()) {\n <ui-split-container\n class=\"split\"\n [orientation]=\"splitOrientation()\"\n name=\"theme-studio\"\n >\n <ui-split-panel [min]=\"300\">\n <ng-container [ngTemplateOutlet]=\"canvasTpl\" />\n </ui-split-panel>\n <ui-split-panel [min]=\"280\">\n <ng-container [ngTemplateOutlet]=\"tokenEditorTpl\" />\n </ui-split-panel>\n </ui-split-container>\n} @else {\n <ui-split-container\n class=\"split\"\n [orientation]=\"splitOrientation()\"\n name=\"theme-studio\"\n >\n <ui-split-panel [min]=\"280\">\n <ng-container [ngTemplateOutlet]=\"tokenEditorTpl\" />\n </ui-split-panel>\n <ui-split-panel [min]=\"300\">\n <ng-container [ngTemplateOutlet]=\"canvasTpl\" />\n </ui-split-panel>\n </ui-split-container>\n}\n\n<!-- ── Token editor template ──────────────────────────────────────── -->\n<ng-template #tokenEditorTpl>\n <div class=\"editor-panel\">\n\n <!-- Filter toolbar -->\n <div class=\"filter-toolbar\">\n <input\n type=\"search\"\n class=\"search-input\"\n placeholder=\"Search tokens…\"\n [value]=\"studio.filter().query\"\n (input)=\"onQueryChange($event)\"\n aria-label=\"Search tokens\"\n />\n <select\n class=\"filter-select\"\n [value]=\"studio.filter().type\"\n (change)=\"onTypeChange($event)\"\n aria-label=\"Filter by type\"\n >\n <option value=\"\">All types</option>\n <option value=\"color\">color</option>\n <option value=\"length\">length</option>\n <option value=\"font\">font</option>\n <option value=\"shadow\">shadow</option>\n <option value=\"number\">number</option>\n <option value=\"keyword\">keyword</option>\n </select>\n <select\n class=\"filter-select\"\n [value]=\"studio.filter().scope\"\n (change)=\"onScopeChange($event)\"\n aria-label=\"Filter by scope\"\n >\n <option value=\"\">All scopes</option>\n <option value=\"global\">global</option>\n <option value=\"component\">component</option>\n </select>\n\n @if (studio.manifest(); as m) {\n <select\n class=\"filter-select\"\n [value]=\"studio.filter().namespace\"\n (change)=\"onNamespaceChange($event)\"\n aria-label=\"Filter by namespace\"\n >\n <option value=\"\">All namespaces</option>\n @for (entry of m.namespaces | keyvalue; track entry.key) {\n <option [value]=\"entry.key\">{{ entry.key }}</option>\n }\n </select>\n }\n\n <label class=\"modified-toggle\">\n <input\n type=\"checkbox\"\n [checked]=\"studio.filter().modifiedOnly\"\n (change)=\"onModifiedOnlyChange($event)\"\n />\n Modified only\n </label>\n </div>\n\n <!-- Stats bar -->\n <div class=\"stats\">\n {{ studio.filteredTokens().length }} of {{ studio.tokens().length }} tokens\n @if (studio.modifiedTokens().length > 0) {\n · <strong>{{ studio.modifiedTokens().length }} modified</strong>\n }\n </div>\n\n <!-- Token list -->\n <div class=\"token-list\" role=\"list\">\n @if (studio.loading()) {\n <div class=\"state-message\">Loading tokens…</div>\n } @else if (studio.loadError()) {\n <div class=\"state-message error\">\n Failed to load manifest: {{ studio.loadError() }}\n </div>\n } @else if (studio.filteredTokens().length === 0) {\n <div class=\"state-message\">No tokens match the current filter.</div>\n } @else {\n @for (token of studio.filteredTokens(); track token.name) {\n <ui-theme-token-row\n [token]=\"token\"\n (valueChange)=\"onTokenValueChange(token.name, $event)\"\n (reset)=\"onTokenReset(token.name)\"\n role=\"listitem\"\n />\n }\n }\n </div>\n </div>\n</ng-template>\n\n<!-- ── Canvas template ────────────────────────────────────────────── -->\n<ng-template #canvasTpl>\n <div class=\"canvas\">\n\n <!-- Swatches -->\n <section class=\"section\">\n <h4 class=\"section-title\">Color tokens</h4>\n <div class=\"swatch-row\">\n <span class=\"swatch accent\">\n <span class=\"swatch-label\">accent</span>\n </span>\n <span class=\"swatch surface\">\n <span class=\"swatch-label\">surface</span>\n </span>\n <span class=\"swatch text-on-surface\">\n <span class=\"swatch-label\">text</span>\n </span>\n <span class=\"swatch border\">\n <span class=\"swatch-label\">border</span>\n </span>\n <span class=\"swatch error\">\n <span class=\"swatch-label\">error</span>\n </span>\n <span class=\"swatch success\">\n <span class=\"swatch-label\">success</span>\n </span>\n </div>\n </section>\n\n <!-- Buttons -->\n <section class=\"section\">\n <h4 class=\"section-title\">Buttons</h4>\n <div class=\"row wrap\">\n <ui-button variant=\"filled\" ariaLabel=\"Filled button\">Filled</ui-button>\n <ui-button variant=\"outlined\" ariaLabel=\"Outlined button\">Outlined</ui-button>\n <ui-button variant=\"ghost\" ariaLabel=\"Ghost button\">Ghost</ui-button>\n <ui-button variant=\"filled\" [disabled]=\"true\" ariaLabel=\"Disabled button\">Disabled</ui-button>\n </div>\n </section>\n\n <!-- Inputs -->\n <section class=\"section\">\n <h4 class=\"section-title\">Inputs</h4>\n <div class=\"col\">\n <ui-input placeholder=\"Text input\" ariaLabel=\"Sample text input\" />\n <ui-dropdown-list\n [options]=\"sampleDropdownOptions\"\n ariaLabel=\"Sample dropdown\"\n />\n </div>\n </section>\n\n <!-- Card -->\n <section class=\"section\">\n <h4 class=\"section-title\">Card</h4>\n <ui-card variant=\"outlined\">\n <ui-card-header>Card header</ui-card-header>\n <ui-card-body>\n <p class=\"body-text\">\n Token changes to <code>--ui-surface</code>,\n <code>--ui-border</code>, and <code>--ui-text</code> are reflected\n here immediately.\n </p>\n </ui-card-body>\n <ui-card-footer>\n <ui-button variant=\"filled\" ariaLabel=\"Card action\">Action</ui-button>\n <ui-button variant=\"ghost\" ariaLabel=\"Cancel\">Cancel</ui-button>\n </ui-card-footer>\n </ui-card>\n </section>\n\n <!-- Badges & chips -->\n <section class=\"section\">\n <h4 class=\"section-title\">Badges &amp; chips</h4>\n <div class=\"row wrap\">\n <ui-badge color=\"primary\">Primary</ui-badge>\n <ui-badge color=\"success\">Success</ui-badge>\n <ui-badge color=\"warning\">Warning</ui-badge>\n <ui-badge color=\"danger\">Danger</ui-badge>\n <ui-badge color=\"neutral\">Neutral</ui-badge>\n </div>\n <div class=\"row wrap\">\n <ui-chip>Default</ui-chip>\n <ui-chip>Active</ui-chip>\n <ui-chip>Archived</ui-chip>\n </div>\n </section>\n\n <!-- Progress -->\n <section class=\"section\">\n <h4 class=\"section-title\">Progress</h4>\n <ui-progress [value]=\"65\" ariaLabel=\"Sample progress, 65%\" />\n </section>\n\n <!-- Toggles & checkboxes -->\n <section class=\"section\">\n <h4 class=\"section-title\">Toggles &amp; checkboxes</h4>\n <div class=\"row wrap\">\n <ui-toggle [value]=\"true\" ariaLabel=\"Toggle on\" />\n <ui-toggle [value]=\"false\" ariaLabel=\"Toggle off\" />\n <ui-checkbox [checked]=\"true\" ariaLabel=\"Checked checkbox\">\n Checked\n </ui-checkbox>\n <ui-checkbox [checked]=\"false\" ariaLabel=\"Unchecked checkbox\">\n Unchecked\n </ui-checkbox>\n </div>\n </section>\n\n </div>\n</ng-template>\n","/*\n * Public API Surface of @theredhead/lucid-theme-studio\n */\nexport { UIThemeStudio } from \"./lib/theme-studio.component\";\nexport { UIThemeTokenRow } from \"./lib/token-row/theme-token-row.component\";\nexport { ThemeStudioService } from \"./lib/theme-studio.service\";\nexport type {\n ThemeToken,\n ThemeTokenManifest,\n ThemeTokenState,\n ThemeTokenFilter,\n} from \"./lib/theme-studio.types\";\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@theredhead/lucid-theme-studio",
3
+ "version": "0.1.1",
4
+ "description": "Live CSS token editor for LucidKit Angular apps — inspects and edits every --ui-* design token in a slide-in drawer",
5
+ "license": "MIT",
6
+ "author": "theredhead",
7
+ "keywords": [
8
+ "angular",
9
+ "ui",
10
+ "theme",
11
+ "devtools",
12
+ "design-tokens",
13
+ "lucidkit"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/theredhead/lucidkit.git",
18
+ "directory": "packages/ui-theme-studio"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "private": false,
24
+ "peerDependencies": {
25
+ "@angular/common": "^21.0.0",
26
+ "@angular/core": "^21.0.0",
27
+ "@theredhead/lucid-foundation": "*",
28
+ "@theredhead/lucid-kit": "*",
29
+ "@theredhead/lucid-theme": "*"
30
+ },
31
+ "module": "fesm2022/theredhead-lucid-theme-studio.mjs",
32
+ "typings": "types/theredhead-lucid-theme-studio.d.ts",
33
+ "exports": {
34
+ "./package.json": {
35
+ "default": "./package.json"
36
+ },
37
+ ".": {
38
+ "types": "./types/theredhead-lucid-theme-studio.d.ts",
39
+ "default": "./fesm2022/theredhead-lucid-theme-studio.mjs"
40
+ }
41
+ },
42
+ "sideEffects": false,
43
+ "dependencies": {
44
+ "tslib": "^2.3.0"
45
+ }
46
+ }
@@ -0,0 +1,243 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { OnInit } from '@angular/core';
3
+ import { SplitOrientation, SelectOption } from '@theredhead/lucid-kit';
4
+
5
+ /**
6
+ * Token entry from `css-token-manifest.json`.
7
+ */
8
+ interface ThemeToken {
9
+ /** CSS custom property name, e.g. `--ui-accent` */
10
+ readonly name: string;
11
+ /** Human-readable purpose description */
12
+ readonly description: string;
13
+ /** Value type: color | length | font | shadow | number | string | reference | keyword */
14
+ readonly type: string;
15
+ /** `"global"` or `"component"` */
16
+ readonly scope: string;
17
+ /** Namespace prefix, e.g. `"ui"`, `"cv"`, `"kb"` */
18
+ readonly namespace: string;
19
+ /** Default values per colour scheme */
20
+ readonly values: {
21
+ readonly light?: string;
22
+ readonly dark?: string;
23
+ };
24
+ /** Definition sites (optional) */
25
+ readonly definitions?: readonly {
26
+ readonly file: string;
27
+ readonly line: number;
28
+ readonly owner: string;
29
+ readonly package: string;
30
+ readonly mode: string;
31
+ }[];
32
+ }
33
+ /**
34
+ * Shape of the token manifest JSON file.
35
+ */
36
+ interface ThemeTokenManifest {
37
+ readonly tokenCount: number;
38
+ readonly namespaces: Record<string, string>;
39
+ readonly tokens: readonly ThemeToken[];
40
+ }
41
+ /**
42
+ * A token paired with its current live value and any override applied by
43
+ * the studio.
44
+ */
45
+ interface ThemeTokenState extends ThemeToken {
46
+ /** The current computed value from `document.documentElement` */
47
+ readonly computedValue: string;
48
+ /** Override applied by the studio, or `null` if unmodified */
49
+ override: string | null;
50
+ }
51
+ /**
52
+ * Active filter state for the token list.
53
+ */
54
+ interface ThemeTokenFilter {
55
+ readonly query: string;
56
+ readonly type: string;
57
+ readonly scope: string;
58
+ readonly namespace: string;
59
+ readonly modifiedOnly: boolean;
60
+ }
61
+
62
+ /**
63
+ * State management service for the Theme Studio drawer.
64
+ *
65
+ * Loads the CSS token manifest, reads live computed values from the document
66
+ * root, tracks per-token overrides, and applies them in real time via
67
+ * `document.documentElement.style.setProperty`.
68
+ *
69
+ * Inject this wherever you need to open/close the studio or query its state.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * export class DevToolbar {
74
+ * private readonly studio = inject(ThemeStudioService);
75
+ *
76
+ * protected open(): void { this.studio.open(); }
77
+ * }
78
+ * ```
79
+ */
80
+ declare class ThemeStudioService {
81
+ private readonly document;
82
+ private readonly log;
83
+ /** Whether the studio drawer is currently visible. */
84
+ readonly isOpen: _angular_core.WritableSignal<boolean>;
85
+ /** Open the studio drawer, loading the manifest if needed. */
86
+ open(manifestUrl?: string): void;
87
+ /** Close the studio drawer. */
88
+ close(): void;
89
+ /** Toggle the studio drawer. */
90
+ toggle(manifestUrl?: string): void;
91
+ /** Loading state for the manifest fetch. */
92
+ readonly loading: _angular_core.WritableSignal<boolean>;
93
+ /** Error message from the most recent manifest load attempt, or null. */
94
+ readonly loadError: _angular_core.WritableSignal<string | null>;
95
+ /** Raw manifest loaded from JSON. */
96
+ readonly manifest: _angular_core.WritableSignal<ThemeTokenManifest | null>;
97
+ /** All tokens with their live computed values and any studio overrides. */
98
+ readonly tokens: _angular_core.WritableSignal<ThemeTokenState[]>;
99
+ /** Active filter applied to the token list. */
100
+ readonly filter: _angular_core.WritableSignal<ThemeTokenFilter>;
101
+ /** Tokens after the active filter is applied. */
102
+ readonly filteredTokens: _angular_core.Signal<ThemeTokenState[]>;
103
+ /** Tokens that have been modified by the studio. */
104
+ readonly modifiedTokens: _angular_core.Signal<ThemeTokenState[]>;
105
+ /**
106
+ * Apply an override value to a token and update the live document.
107
+ * Pass `null` to reset the token to its original computed value.
108
+ */
109
+ setOverride(tokenName: string, value: string | null): void;
110
+ /** Reset all overrides and re-read computed values from the document. */
111
+ resetAll(): void;
112
+ /**
113
+ * Export the current overrides as a CSS snippet that can be pasted into
114
+ * a stylesheet.
115
+ */
116
+ exportCss(): string;
117
+ /**
118
+ * Export the current overrides as a JSON object
119
+ * (`{ "--ui-accent": "#ff0000", ... }`).
120
+ */
121
+ exportJson(): Record<string, string>;
122
+ /**
123
+ * Import a set of overrides from a plain object.
124
+ * Only tokens present in the manifest are applied.
125
+ */
126
+ importOverrides(overrides: Record<string, string>): void;
127
+ private loaded;
128
+ private loadManifest;
129
+ private hydrateToken;
130
+ private readComputedValue;
131
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ThemeStudioService, never>;
132
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<ThemeStudioService>;
133
+ }
134
+
135
+ /**
136
+ * Full-height split-pane theme editor.
137
+ *
138
+ * Drop it into any layout that gives it height. The left panel contains the
139
+ * token editor; the right panel renders a live component canvas that reflects
140
+ * every token change in real time. A toolbar lets you switch between
141
+ * horizontal and vertical orientations and swap the two panels.
142
+ *
143
+ * ### Usage
144
+ * ```html
145
+ * <ui-theme-studio style="height: 100%" />
146
+ * ```
147
+ */
148
+ declare class UIThemeStudio implements OnInit {
149
+ /**
150
+ * URL of the token manifest JSON.
151
+ * Defaults to `"assets/css-token-manifest.json"`.
152
+ */
153
+ readonly manifestUrl: _angular_core.InputSignal<string>;
154
+ /**
155
+ * Accessible label for the component region.
156
+ */
157
+ readonly ariaLabel: _angular_core.InputSignal<string>;
158
+ /**
159
+ * Orientation of the split divider.
160
+ * `"horizontal"` = side by side, `"vertical"` = stacked.
161
+ */
162
+ protected readonly splitOrientation: _angular_core.WritableSignal<SplitOrientation>;
163
+ /**
164
+ * When `true`, the canvas is on the left / top and the token editor is on
165
+ * the right / bottom.
166
+ */
167
+ protected readonly panelsSwapped: _angular_core.WritableSignal<boolean>;
168
+ /** @internal */
169
+ protected readonly studio: ThemeStudioService;
170
+ private readonly log;
171
+ /** @internal */
172
+ protected readonly icons: {
173
+ readonly horizontal: "<rect width=\"7\" height=\"18\" x=\"3\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"7\" x=\"14\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\" />";
174
+ readonly vertical: "<rect width=\"18\" height=\"7\" x=\"3\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"7\" x=\"3\" y=\"14\" rx=\"1\" /><rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\" />";
175
+ readonly swap: "<path d=\"m3 7 5 5-5 5V7\" /><path d=\"m21 7-5 5 5 5V7\" /><path d=\"M12 20v2\" /><path d=\"M12 14v2\" /><path d=\"M12 8v2\" /><path d=\"M12 2v2\" />";
176
+ };
177
+ /** @internal */
178
+ protected readonly sampleDropdownOptions: readonly SelectOption[];
179
+ /** @inheritdoc */
180
+ ngOnInit(): void;
181
+ /**
182
+ * Switch the split container orientation.
183
+ */
184
+ protected setOrientation(o: SplitOrientation): void;
185
+ /**
186
+ * Toggle which panel appears first (left/top).
187
+ */
188
+ protected toggleSwap(): void;
189
+ /** @internal */
190
+ protected setFilterField<K extends keyof ReturnType<typeof this$1.studio.filter>>(key: K, value: ReturnType<typeof this$1.studio.filter>[K]): void;
191
+ /** @internal */
192
+ protected onQueryChange(event: Event): void;
193
+ /** @internal */
194
+ protected onTypeChange(event: Event): void;
195
+ /** @internal */
196
+ protected onScopeChange(event: Event): void;
197
+ /** @internal */
198
+ protected onNamespaceChange(event: Event): void;
199
+ /** @internal */
200
+ protected onModifiedOnlyChange(event: Event): void;
201
+ /** @internal */
202
+ protected onTokenValueChange(tokenName: string, value: string): void;
203
+ /** @internal */
204
+ protected onTokenReset(tokenName: string): void;
205
+ /** @internal */
206
+ protected copyToClipboard(text: string): void;
207
+ /** @internal */
208
+ protected exportCss(): void;
209
+ /** @internal */
210
+ protected exportJson(): void;
211
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<UIThemeStudio, never>;
212
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<UIThemeStudio, "ui-theme-studio", never, { "manifestUrl": { "alias": "manifestUrl"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
213
+ }
214
+
215
+ /**
216
+ * A single row in the theme-studio token list.
217
+ *
218
+ * Displays the token name, description, type/scope chips, and an editor
219
+ * appropriate to the token type (colour swatch + text, or plain text).
220
+ */
221
+ declare class UIThemeTokenRow {
222
+ /** The token to display and edit. */
223
+ readonly token: _angular_core.InputSignal<ThemeTokenState>;
224
+ /** Emitted when the user changes the token value. */
225
+ readonly valueChange: _angular_core.OutputEmitterRef<string>;
226
+ /** Emitted when the user clicks Reset. */
227
+ readonly reset: _angular_core.OutputEmitterRef<void>;
228
+ private readonly studio;
229
+ /** The effective displayed value: override if set, else computed. */
230
+ protected get displayValue(): string;
231
+ /** `true` when the token type is `"color"`. */
232
+ protected get isColor(): boolean;
233
+ /** Forward text-input changes through the output. */
234
+ protected onTextInput(event: Event): void;
235
+ /** Forward colour-picker changes through the output. */
236
+ protected onColorInput(event: Event): void;
237
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<UIThemeTokenRow, never>;
238
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<UIThemeTokenRow, "ui-theme-token-row", never, { "token": { "alias": "token"; "required": true; "isSignal": true; }; }, { "valueChange": "valueChange"; "reset": "reset"; }, never, never, true, never>;
239
+ }
240
+
241
+ export { ThemeStudioService, UIThemeStudio, UIThemeTokenRow };
242
+ export type { ThemeToken, ThemeTokenFilter, ThemeTokenManifest, ThemeTokenState };
243
+ //# sourceMappingURL=theredhead-lucid-theme-studio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theredhead-lucid-theme-studio.d.ts","sources":["../../../packages/ui-theme-studio/src/lib/theme-studio.types.ts","../../../packages/ui-theme-studio/src/lib/theme-studio.service.ts","../../../packages/ui-theme-studio/src/lib/theme-studio.component.ts","../../../packages/ui-theme-studio/src/lib/token-row/theme-token-row.component.ts"],"sourcesContent":[null,null,null,null],"names":["this"],"mappings":";;;;AAAA;;AAEG;;;AAGD;;AAGA;;AAGA;;AAGA;;AAGA;;;AAGmB;AAAyB;;;AAG5C;AACE;AACA;AACA;AACA;AACA;AACD;AACF;AAED;;AAEG;;AAED;;AAEA,8BAAA,UAAA;AACD;AAED;;;AAGG;AACG,UAAA,eAAA,SAAA,UAAA;;AAEJ;;AAGA;AACD;AAED;;AAEG;;AAED;AACA;AACA;AACA;AACA;AACD;;AClDD;;;;;;;;;;;;;;;;;AAiBG;AACH,cAAA,kBAAA;AAEE;AAEA;;AAMA,qBAAsB,aAAA,CAAA,cAAA;;AAGf;;AAQA;;AAKA;;AAWP,sBAAuB,aAAA,CAAA,cAAA;;AAGvB,wBAAyB,aAAA,CAAA,cAAA;;AAGzB,uBAAwB,aAAA,CAAA,cAAA,CAAA,kBAAA;;AAGxB,qBAAsB,aAAA,CAAA,cAAA,CAAA,eAAA;;AAKtB,qBAAsB,aAAA,CAAA,cAAA,CAAA,gBAAA;;AAStB,6BAA8B,aAAA,CAAA,MAAA,CAAA,eAAA;;AAqB9B,6BAA8B,aAAA,CAAA,MAAA,CAAA,eAAA;AAM9B;;;AAGG;;;AAgBI;AAmBP;;;AAGG;AACI;AAOP;;;AAGG;AACI,kBAAA,MAAA;AAWP;;;AAGG;;;AAYH;AAyBA;AAaA;;;AAKD;;AC3MD;;;;;;;;;;;;AAYG;AACH,cAAA,aAAA,YAAA,MAAA;AAmCE;;;AAGG;AACH,0BAA2B,aAAA,CAAA,WAAA;AAE3B;;AAEG;AACH,wBAAyB,aAAA,CAAA,WAAA;AAIzB;;;AAGG;AACH,yCAAmC,aAAA,CAAA,cAAA,CAAA,gBAAA;AAEnC;;;AAGG;AACH,sCAAgC,aAAA,CAAA,cAAA;;AAKhC,+BAAA,kBAAA;AAEA;;;;;;AASW;;;;AAYJ;AAMP;;AAEG;AACH,gCAAA,gBAAA;AAIA;;AAEG;;;AAQH,6CAAA,UAAA,QAAAA,MAAA,CAAA,MAAA,CAAA,MAAA,kBAAA,UAAA,QAAAA,MAAA,CAAA,MAAA,CAAA,MAAA;;AAOA,mCAAA,KAAA;;AAKA,kCAAA,KAAA;;AAKA,mCAAA,KAAA;;AAKA,uCAAA,KAAA;;AAKA,0CAAA,KAAA;;;;AAaA;;AAKA;;;;;;;AAiBD;;ACjND;;;;;AAKG;AACH,cAAA,eAAA;;AAeE,oBAAqB,aAAA,CAAA,WAAA,CAAA,eAAA;;AAGrB,0BAA2B,aAAA,CAAA,gBAAA;;AAG3B,oBAAqB,aAAA,CAAA,gBAAA;AAIrB;;AAKA;;AAMA;;AAKA,iCAAA,KAAA;;AAMA,kCAAA,KAAA;;;AAID;;;;"}