@12nil/theme-registry-package 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/dist/app_theme-K3SQNO7I.js +7 -0
- package/dist/app_theme-K3SQNO7I.js.map +1 -0
- package/dist/chunk-GUPUN2GN.js +732 -0
- package/dist/chunk-GUPUN2GN.js.map +1 -0
- package/dist/index.cjs +954 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +291 -0
- package/dist/index.d.ts +291 -0
- package/dist/index.js +172 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
// theme-registry.ts
|
|
2
|
+
var RUNTIME_THEME_REGISTRY_VERSION = "0.2.0";
|
|
3
|
+
var LEGACY_REQUIRED_KEYS = ["primary", "secondary", "background", "text", "accent", "muted", "error", "warning", "success", "info"];
|
|
4
|
+
var LEGACY_ALLOWED_KEYS = [...LEGACY_REQUIRED_KEYS, "tertiary"];
|
|
5
|
+
var SEMANTIC_SCHEMA = {
|
|
6
|
+
colors: ["primary", "secondary", "destructive", "success", "warning", "info"],
|
|
7
|
+
surface: ["page", "card", "sidebar", "modal", "popover"],
|
|
8
|
+
text: ["primary", "secondary", "tertiary", "disabled"],
|
|
9
|
+
border: ["default", "subtle", "strong"]
|
|
10
|
+
};
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function isLikelyColor(value) {
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(trimmed) || /^rgba?\(.+\)$/.test(trimmed) || /^hsla?\(.+\)$/.test(trimmed) || /^var\(--.+\)$/.test(trimmed) || /^[a-zA-Z]+$/.test(trimmed);
|
|
17
|
+
}
|
|
18
|
+
function parseSemver(version) {
|
|
19
|
+
const normalized = version.split("-")[0];
|
|
20
|
+
const parts = normalized.split(".");
|
|
21
|
+
if (parts.length < 3) return null;
|
|
22
|
+
const major = Number(parts[0]);
|
|
23
|
+
const minor = Number(parts[1]);
|
|
24
|
+
const patch = Number(parts[2]);
|
|
25
|
+
if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return [major, minor, patch];
|
|
29
|
+
}
|
|
30
|
+
function compareSemver(a, b) {
|
|
31
|
+
const av = parseSemver(a);
|
|
32
|
+
const bv = parseSemver(b);
|
|
33
|
+
if (!av || !bv) return 0;
|
|
34
|
+
for (let i = 0; i < 3; i += 1) {
|
|
35
|
+
if (av[i] > bv[i]) return 1;
|
|
36
|
+
if (av[i] < bv[i]) return -1;
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
function assertCompatibility(minVersion, label) {
|
|
41
|
+
if (!minVersion) return;
|
|
42
|
+
if (compareSemver(RUNTIME_THEME_REGISTRY_VERSION, minVersion) < 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`${label} requires Runtime Theme Registry >= ${minVersion}, current version is ${RUNTIME_THEME_REGISTRY_VERSION}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function validateMetadata(metadata) {
|
|
49
|
+
const issues = [];
|
|
50
|
+
if (!metadata) return issues;
|
|
51
|
+
if (metadata.version && !parseSemver(metadata.version)) {
|
|
52
|
+
issues.push({ path: "theme.metadata.version", message: "Expected semver format like 1.0.0" });
|
|
53
|
+
}
|
|
54
|
+
if (metadata.compatibility?.minRegistryVersion && !parseSemver(metadata.compatibility.minRegistryVersion)) {
|
|
55
|
+
issues.push({ path: "theme.metadata.compatibility.minRegistryVersion", message: "Expected semver format like 1.0.0" });
|
|
56
|
+
}
|
|
57
|
+
return issues;
|
|
58
|
+
}
|
|
59
|
+
function isThemeTokensV2(tokens) {
|
|
60
|
+
return isRecord(tokens) && "colors" in tokens && "surface" in tokens && "text" in tokens && "border" in tokens;
|
|
61
|
+
}
|
|
62
|
+
function validateLegacyTokens(tokens, modePath) {
|
|
63
|
+
const issues = [];
|
|
64
|
+
for (const key of LEGACY_REQUIRED_KEYS) {
|
|
65
|
+
if (!(key in tokens)) {
|
|
66
|
+
issues.push({ path: `${modePath}.${key}`, message: "Missing required token" });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
70
|
+
if (!LEGACY_ALLOWED_KEYS.includes(key)) {
|
|
71
|
+
issues.push({ path: `${modePath}.${key}`, message: "Unknown token property" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (typeof value !== "string" || !isLikelyColor(value)) {
|
|
75
|
+
issues.push({ path: `${modePath}.${key}`, message: "Invalid color value" });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return issues;
|
|
79
|
+
}
|
|
80
|
+
function validateSemanticTokens(tokens, modePath) {
|
|
81
|
+
const issues = [];
|
|
82
|
+
for (const category of Object.keys(SEMANTIC_SCHEMA)) {
|
|
83
|
+
if (!(category in tokens)) {
|
|
84
|
+
issues.push({ path: `${modePath}.${category}`, message: "Missing required category" });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const categoryValue = tokens[category];
|
|
88
|
+
if (!isRecord(categoryValue)) {
|
|
89
|
+
issues.push({ path: `${modePath}.${category}`, message: "Category must be an object" });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const requiredKeys = SEMANTIC_SCHEMA[category];
|
|
93
|
+
for (const key of requiredKeys) {
|
|
94
|
+
if (!(key in categoryValue)) {
|
|
95
|
+
issues.push({ path: `${modePath}.${category}.${key}`, message: "Missing required token" });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const [key, value] of Object.entries(categoryValue)) {
|
|
99
|
+
if (!requiredKeys.includes(key)) {
|
|
100
|
+
issues.push({ path: `${modePath}.${category}.${key}`, message: "Unknown token property" });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (typeof value !== "string" || !isLikelyColor(value)) {
|
|
104
|
+
issues.push({ path: `${modePath}.${category}.${key}`, message: "Invalid color value" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const key of Object.keys(tokens)) {
|
|
109
|
+
if (!(key in SEMANTIC_SCHEMA)) {
|
|
110
|
+
issues.push({ path: `${modePath}.${key}`, message: "Unknown category" });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return issues;
|
|
114
|
+
}
|
|
115
|
+
function validateTheme(theme) {
|
|
116
|
+
const issues = [];
|
|
117
|
+
if (!theme.name || !theme.name.trim()) {
|
|
118
|
+
issues.push({ path: "theme.name", message: "Theme name is required" });
|
|
119
|
+
}
|
|
120
|
+
issues.push(...validateMetadata(theme.metadata));
|
|
121
|
+
if (!isRecord(theme.modes) || Object.keys(theme.modes).length === 0) {
|
|
122
|
+
issues.push({ path: "theme.modes", message: "At least one mode is required" });
|
|
123
|
+
return { valid: issues.length === 0, issues };
|
|
124
|
+
}
|
|
125
|
+
for (const [modeName, tokens] of Object.entries(theme.modes)) {
|
|
126
|
+
const modePath = `theme.modes.${modeName}`;
|
|
127
|
+
if (!isRecord(tokens)) {
|
|
128
|
+
issues.push({ path: modePath, message: "Mode tokens must be an object" });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (isThemeTokensV2(tokens)) {
|
|
132
|
+
issues.push(...validateSemanticTokens(tokens, modePath));
|
|
133
|
+
} else {
|
|
134
|
+
issues.push(...validateLegacyTokens(tokens, modePath));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
valid: issues.length === 0,
|
|
139
|
+
issues
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function mergeTokenSets(existing, incoming) {
|
|
143
|
+
if (isThemeTokensV2(existing) && isThemeTokensV2(incoming)) {
|
|
144
|
+
return {
|
|
145
|
+
colors: { ...existing.colors, ...incoming.colors },
|
|
146
|
+
surface: { ...existing.surface, ...incoming.surface },
|
|
147
|
+
text: { ...existing.text, ...incoming.text },
|
|
148
|
+
border: { ...existing.border, ...incoming.border }
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return { ...existing, ...incoming };
|
|
152
|
+
}
|
|
153
|
+
function mergePlugins(existing, incoming) {
|
|
154
|
+
return {
|
|
155
|
+
...existing,
|
|
156
|
+
...incoming,
|
|
157
|
+
icons: { ...existing.icons ?? {}, ...incoming.icons ?? {} },
|
|
158
|
+
fonts: { ...existing.fonts ?? {}, ...incoming.fonts ?? {} },
|
|
159
|
+
spacing: { ...existing.spacing ?? {}, ...incoming.spacing ?? {} },
|
|
160
|
+
typography: { ...existing.typography ?? {}, ...incoming.typography ?? {} },
|
|
161
|
+
metadata: { ...existing.metadata ?? {}, ...incoming.metadata ?? {} }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function toPluginContributions(plugin) {
|
|
165
|
+
return {
|
|
166
|
+
icons: { ...plugin.icons ?? {} },
|
|
167
|
+
fonts: { ...plugin.fonts ?? {} },
|
|
168
|
+
spacing: { ...plugin.spacing ?? {} },
|
|
169
|
+
typography: { ...plugin.typography ?? {} },
|
|
170
|
+
metadata: { ...plugin.metadata ?? {} }
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function mergePluginContributions(target, source) {
|
|
174
|
+
return {
|
|
175
|
+
icons: { ...target.icons, ...source.icons },
|
|
176
|
+
fonts: { ...target.fonts, ...source.fonts },
|
|
177
|
+
spacing: { ...target.spacing, ...source.spacing },
|
|
178
|
+
typography: { ...target.typography, ...source.typography },
|
|
179
|
+
metadata: { ...target.metadata, ...source.metadata }
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function createEmptyContributions() {
|
|
183
|
+
return {
|
|
184
|
+
icons: {},
|
|
185
|
+
fonts: {},
|
|
186
|
+
spacing: {},
|
|
187
|
+
typography: {},
|
|
188
|
+
metadata: {}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function createCompositionLayerMaps() {
|
|
192
|
+
return {
|
|
193
|
+
brand: /* @__PURE__ */ new Map(),
|
|
194
|
+
appearance: /* @__PURE__ */ new Map(),
|
|
195
|
+
accessibility: /* @__PURE__ */ new Map()
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function normalizeLoadedThemes(payload) {
|
|
199
|
+
return Array.isArray(payload) ? payload : [payload];
|
|
200
|
+
}
|
|
201
|
+
function tokenSetToCssVariables(tokens) {
|
|
202
|
+
const variables = {};
|
|
203
|
+
if (isThemeTokensV2(tokens)) {
|
|
204
|
+
const appendCategory = (category, categoryValues) => {
|
|
205
|
+
;
|
|
206
|
+
Object.entries(categoryValues).forEach(([key, value]) => {
|
|
207
|
+
const keyName = String(key);
|
|
208
|
+
variables[`--theme-${category}-${keyName}`] = value;
|
|
209
|
+
if (category === "colors") {
|
|
210
|
+
variables[`--color-theme-${keyName}`] = value;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
appendCategory("colors", tokens.colors);
|
|
215
|
+
appendCategory("surface", tokens.surface);
|
|
216
|
+
appendCategory("text", tokens.text);
|
|
217
|
+
appendCategory("border", tokens.border);
|
|
218
|
+
return variables;
|
|
219
|
+
}
|
|
220
|
+
Object.entries(tokens).forEach(([key, value]) => {
|
|
221
|
+
if (key === "tertiary" && !value) return;
|
|
222
|
+
variables[`--color-theme-${key}`] = value;
|
|
223
|
+
});
|
|
224
|
+
return variables;
|
|
225
|
+
}
|
|
226
|
+
function isCacheEntryFresh(entry, maxAgeMs) {
|
|
227
|
+
if (typeof maxAgeMs !== "number") return true;
|
|
228
|
+
return Date.now() - entry.loadedAt <= maxAgeMs;
|
|
229
|
+
}
|
|
230
|
+
var ThemeRegistry = class {
|
|
231
|
+
constructor() {
|
|
232
|
+
this.themes = /* @__PURE__ */ new Map();
|
|
233
|
+
this.plugins = /* @__PURE__ */ new Map();
|
|
234
|
+
this.compositionLayers = createCompositionLayerMaps();
|
|
235
|
+
this.loadedThemeCache = /* @__PURE__ */ new Map();
|
|
236
|
+
this.currentTheme = null;
|
|
237
|
+
this.currentMode = null;
|
|
238
|
+
this.fallbackConfig = {};
|
|
239
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
240
|
+
}
|
|
241
|
+
getVersion() {
|
|
242
|
+
return RUNTIME_THEME_REGISTRY_VERSION;
|
|
243
|
+
}
|
|
244
|
+
on(event, handler) {
|
|
245
|
+
const existing = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
246
|
+
existing.add(handler);
|
|
247
|
+
this.listeners.set(event, existing);
|
|
248
|
+
return () => {
|
|
249
|
+
existing.delete(handler);
|
|
250
|
+
if (existing.size === 0) {
|
|
251
|
+
this.listeners.delete(event);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
onRegistered(handler) {
|
|
256
|
+
return this.on("registered", handler);
|
|
257
|
+
}
|
|
258
|
+
onThemeChanged(handler) {
|
|
259
|
+
return this.on("themeChanged", handler);
|
|
260
|
+
}
|
|
261
|
+
onVariantAdded(handler) {
|
|
262
|
+
return this.on("variantAdded", handler);
|
|
263
|
+
}
|
|
264
|
+
onDestroyed(handler) {
|
|
265
|
+
return this.on("destroyed", handler);
|
|
266
|
+
}
|
|
267
|
+
onLoaded(handler) {
|
|
268
|
+
return this.on("loaded", handler);
|
|
269
|
+
}
|
|
270
|
+
onComposed(handler) {
|
|
271
|
+
return this.on("composed", handler);
|
|
272
|
+
}
|
|
273
|
+
emit(event, payload) {
|
|
274
|
+
const handlers = this.listeners.get(event);
|
|
275
|
+
if (!handlers) return;
|
|
276
|
+
handlers.forEach((handler) => {
|
|
277
|
+
;
|
|
278
|
+
handler(payload);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
register(theme, options = {}) {
|
|
282
|
+
const ifExists = options.ifExists ?? "throw";
|
|
283
|
+
const shouldValidate = options.validate ?? true;
|
|
284
|
+
assertCompatibility(theme.metadata?.compatibility?.minRegistryVersion, `Theme "${theme.name}"`);
|
|
285
|
+
if (shouldValidate) {
|
|
286
|
+
const validation = validateTheme(theme);
|
|
287
|
+
if (!validation.valid) {
|
|
288
|
+
const details = validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n");
|
|
289
|
+
throw new Error(`Theme "${theme.name}" failed validation:
|
|
290
|
+
${details}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const exists = this.themes.has(theme.name);
|
|
294
|
+
if (!exists) {
|
|
295
|
+
this.themes.set(theme.name, theme);
|
|
296
|
+
this.emit("registered", { themeName: theme.name });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (ifExists === "throw") {
|
|
300
|
+
throw new Error(`Theme "${theme.name}" already exists`);
|
|
301
|
+
}
|
|
302
|
+
if (ifExists === "replace") {
|
|
303
|
+
this.themes.set(theme.name, theme);
|
|
304
|
+
this.emit("replaced", { themeName: theme.name });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const existing = this.themes.get(theme.name);
|
|
308
|
+
if (!existing) {
|
|
309
|
+
this.themes.set(theme.name, theme);
|
|
310
|
+
this.emit("registered", { themeName: theme.name });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const mergedModes = { ...existing.modes };
|
|
314
|
+
for (const [modeName, tokens] of Object.entries(theme.modes)) {
|
|
315
|
+
if (mergedModes[modeName]) {
|
|
316
|
+
mergedModes[modeName] = mergeTokenSets(mergedModes[modeName], tokens);
|
|
317
|
+
} else {
|
|
318
|
+
mergedModes[modeName] = tokens;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.themes.set(theme.name, {
|
|
322
|
+
...existing,
|
|
323
|
+
...theme,
|
|
324
|
+
metadata: { ...existing.metadata ?? {}, ...theme.metadata ?? {} },
|
|
325
|
+
modes: mergedModes
|
|
326
|
+
});
|
|
327
|
+
this.emit("merged", { themeName: theme.name });
|
|
328
|
+
}
|
|
329
|
+
replace(themeName, theme) {
|
|
330
|
+
if (themeName !== theme.name) {
|
|
331
|
+
throw new Error("replace() requires matching themeName and theme.name");
|
|
332
|
+
}
|
|
333
|
+
this.register(theme, { ifExists: "replace" });
|
|
334
|
+
}
|
|
335
|
+
merge(themeName, theme) {
|
|
336
|
+
if (themeName !== theme.name) {
|
|
337
|
+
throw new Error("merge() requires matching themeName and theme.name");
|
|
338
|
+
}
|
|
339
|
+
this.register(theme, { ifExists: "merge" });
|
|
340
|
+
}
|
|
341
|
+
unregister(themeName) {
|
|
342
|
+
const removed = this.themes.delete(themeName);
|
|
343
|
+
if (removed) {
|
|
344
|
+
this.emit("unregistered", { themeName });
|
|
345
|
+
}
|
|
346
|
+
if (removed && this.currentTheme === themeName) {
|
|
347
|
+
this.currentTheme = null;
|
|
348
|
+
this.currentMode = null;
|
|
349
|
+
}
|
|
350
|
+
return removed;
|
|
351
|
+
}
|
|
352
|
+
setFallback(config) {
|
|
353
|
+
this.fallbackConfig = { ...this.fallbackConfig, ...config };
|
|
354
|
+
}
|
|
355
|
+
registerPlugin(plugin, options = {}) {
|
|
356
|
+
const ifExists = options.ifExists ?? "throw";
|
|
357
|
+
const themeIfExists = options.themeIfExists ?? "merge";
|
|
358
|
+
const validateThemes = options.validateThemes ?? true;
|
|
359
|
+
assertCompatibility(plugin.minRegistryVersion, `Plugin "${plugin.name}"`);
|
|
360
|
+
const exists = this.plugins.has(plugin.name);
|
|
361
|
+
if (exists && ifExists === "throw") {
|
|
362
|
+
throw new Error(`Plugin "${plugin.name}" already exists`);
|
|
363
|
+
}
|
|
364
|
+
let pluginToStore = plugin;
|
|
365
|
+
if (exists && ifExists === "merge") {
|
|
366
|
+
const existing = this.plugins.get(plugin.name);
|
|
367
|
+
if (existing) {
|
|
368
|
+
pluginToStore = mergePlugins(existing, plugin);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
this.plugins.set(plugin.name, pluginToStore);
|
|
372
|
+
const pluginThemes = [
|
|
373
|
+
...pluginToStore.theme ? [pluginToStore.theme] : [],
|
|
374
|
+
...pluginToStore.themes ?? []
|
|
375
|
+
];
|
|
376
|
+
for (const theme of pluginThemes) {
|
|
377
|
+
this.register(theme, { ifExists: themeIfExists, validate: validateThemes });
|
|
378
|
+
}
|
|
379
|
+
this.emit("pluginRegistered", { pluginName: plugin.name });
|
|
380
|
+
}
|
|
381
|
+
registerCompositionLayer(layer, options = {}) {
|
|
382
|
+
const targetMap = this.compositionLayers[layer.type];
|
|
383
|
+
const ifExists = options.ifExists ?? "throw";
|
|
384
|
+
const exists = targetMap.has(layer.name);
|
|
385
|
+
if (exists && ifExists === "throw") {
|
|
386
|
+
throw new Error(`Composition layer "${layer.type}:${layer.name}" already exists`);
|
|
387
|
+
}
|
|
388
|
+
if (exists && ifExists === "merge") {
|
|
389
|
+
const existing = targetMap.get(layer.name);
|
|
390
|
+
if (existing) {
|
|
391
|
+
targetMap.set(layer.name, {
|
|
392
|
+
...existing,
|
|
393
|
+
...layer,
|
|
394
|
+
metadata: { ...existing.metadata ?? {}, ...layer.metadata ?? {} },
|
|
395
|
+
tokens: mergeTokenSets(existing.tokens, layer.tokens)
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
targetMap.set(layer.name, layer);
|
|
401
|
+
}
|
|
402
|
+
getCompositionLayer(type, name) {
|
|
403
|
+
return this.compositionLayers[type].get(name);
|
|
404
|
+
}
|
|
405
|
+
getCompositionLayers(type) {
|
|
406
|
+
if (type) {
|
|
407
|
+
return Array.from(this.compositionLayers[type].values());
|
|
408
|
+
}
|
|
409
|
+
return [
|
|
410
|
+
...Array.from(this.compositionLayers.brand.values()),
|
|
411
|
+
...Array.from(this.compositionLayers.appearance.values()),
|
|
412
|
+
...Array.from(this.compositionLayers.accessibility.values())
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
compose(selection) {
|
|
416
|
+
const appliedLayers = [];
|
|
417
|
+
let composedTokens = null;
|
|
418
|
+
const resolutionOrder = [
|
|
419
|
+
["brand", selection.brand],
|
|
420
|
+
["appearance", selection.appearance],
|
|
421
|
+
["accessibility", selection.accessibility]
|
|
422
|
+
];
|
|
423
|
+
resolutionOrder.forEach(([type, layerName]) => {
|
|
424
|
+
if (!layerName) return;
|
|
425
|
+
const layer = this.compositionLayers[type].get(layerName);
|
|
426
|
+
if (!layer) return;
|
|
427
|
+
appliedLayers.push(layer);
|
|
428
|
+
composedTokens = composedTokens ? mergeTokenSets(composedTokens, layer.tokens) : layer.tokens;
|
|
429
|
+
});
|
|
430
|
+
this.emit("composed", {
|
|
431
|
+
appliedLayers: appliedLayers.map((layer) => `${layer.type}:${layer.name}`)
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
selection,
|
|
435
|
+
tokens: composedTokens,
|
|
436
|
+
appliedLayers
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
getInitialThemeAttributes(themeName, modeName) {
|
|
440
|
+
const resolved = this.resolveSelection(themeName, modeName);
|
|
441
|
+
if (!resolved.themeName || !resolved.modeName) return null;
|
|
442
|
+
return {
|
|
443
|
+
"data-theme": resolved.themeName,
|
|
444
|
+
"data-mode": resolved.modeName
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
hydrateThemeOnDocument(themeName, modeName) {
|
|
448
|
+
const resolved = this.resolveSelection(themeName, modeName);
|
|
449
|
+
if (!resolved.themeName || !resolved.modeName || !resolved.tokens) {
|
|
450
|
+
return resolved;
|
|
451
|
+
}
|
|
452
|
+
if (typeof document !== "undefined") {
|
|
453
|
+
document.documentElement.setAttribute("data-theme", resolved.themeName);
|
|
454
|
+
document.documentElement.setAttribute("data-mode", resolved.modeName);
|
|
455
|
+
const cssVariables = tokenSetToCssVariables(resolved.tokens);
|
|
456
|
+
Object.entries(cssVariables).forEach(([name, value]) => {
|
|
457
|
+
document.documentElement.style.setProperty(name, value);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
this.currentTheme = resolved.themeName;
|
|
461
|
+
this.currentMode = resolved.modeName;
|
|
462
|
+
this.emit("themeChanged", { themeName: resolved.themeName, modeName: resolved.modeName });
|
|
463
|
+
return resolved;
|
|
464
|
+
}
|
|
465
|
+
async load(source, options = {}) {
|
|
466
|
+
const ifExists = options.ifExists ?? "merge";
|
|
467
|
+
const validate = options.validate ?? true;
|
|
468
|
+
const cacheKey = options.cacheKey ?? (typeof source === "string" ? source : void 0);
|
|
469
|
+
const forceRefresh = options.forceRefresh ?? false;
|
|
470
|
+
const maxAgeMs = options.maxAgeMs;
|
|
471
|
+
if (cacheKey && !forceRefresh && this.loadedThemeCache.has(cacheKey)) {
|
|
472
|
+
const entry = this.loadedThemeCache.get(cacheKey);
|
|
473
|
+
if (entry && isCacheEntryFresh(entry, maxAgeMs)) {
|
|
474
|
+
const names2 = entry.themes.map((theme) => theme.name);
|
|
475
|
+
this.emit("loaded", {
|
|
476
|
+
source: entry.source,
|
|
477
|
+
count: names2.length,
|
|
478
|
+
fromCache: true
|
|
479
|
+
});
|
|
480
|
+
return {
|
|
481
|
+
loaded: names2.length,
|
|
482
|
+
themes: names2,
|
|
483
|
+
source: entry.source,
|
|
484
|
+
fromCache: true
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
let loadedThemes = [];
|
|
489
|
+
if (typeof source === "string") {
|
|
490
|
+
const response = await fetch(source);
|
|
491
|
+
if (!response.ok) {
|
|
492
|
+
throw new Error(`Failed to load themes from ${source}: ${response.status} ${response.statusText}`);
|
|
493
|
+
}
|
|
494
|
+
const payload = await response.json();
|
|
495
|
+
loadedThemes = normalizeLoadedThemes(payload);
|
|
496
|
+
} else {
|
|
497
|
+
const payload = await source();
|
|
498
|
+
loadedThemes = normalizeLoadedThemes(payload);
|
|
499
|
+
}
|
|
500
|
+
const names = [];
|
|
501
|
+
loadedThemes.forEach((theme) => {
|
|
502
|
+
this.register(theme, { ifExists, validate });
|
|
503
|
+
names.push(theme.name);
|
|
504
|
+
});
|
|
505
|
+
const sourceName = typeof source === "string" ? source : "loader";
|
|
506
|
+
if (cacheKey) {
|
|
507
|
+
this.loadedThemeCache.set(cacheKey, {
|
|
508
|
+
themes: loadedThemes,
|
|
509
|
+
loadedAt: Date.now(),
|
|
510
|
+
source: sourceName
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
this.emit("loaded", {
|
|
514
|
+
source: sourceName,
|
|
515
|
+
count: names.length,
|
|
516
|
+
fromCache: false
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
loaded: names.length,
|
|
520
|
+
themes: names,
|
|
521
|
+
source: sourceName,
|
|
522
|
+
fromCache: false
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
clearLoadCache(cacheKey) {
|
|
526
|
+
if (cacheKey) {
|
|
527
|
+
this.loadedThemeCache.delete(cacheKey);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
this.loadedThemeCache.clear();
|
|
531
|
+
}
|
|
532
|
+
unregisterPlugin(pluginName) {
|
|
533
|
+
const removed = this.plugins.delete(pluginName);
|
|
534
|
+
if (removed) {
|
|
535
|
+
this.emit("pluginUnregistered", { pluginName });
|
|
536
|
+
}
|
|
537
|
+
return removed;
|
|
538
|
+
}
|
|
539
|
+
getPlugin(name) {
|
|
540
|
+
return this.plugins.get(name);
|
|
541
|
+
}
|
|
542
|
+
getPlugins() {
|
|
543
|
+
return Array.from(this.plugins.values());
|
|
544
|
+
}
|
|
545
|
+
getPluginContributions(pluginName) {
|
|
546
|
+
const plugin = this.plugins.get(pluginName);
|
|
547
|
+
if (!plugin) return void 0;
|
|
548
|
+
return toPluginContributions(plugin);
|
|
549
|
+
}
|
|
550
|
+
getMergedPluginContributions() {
|
|
551
|
+
let merged = createEmptyContributions();
|
|
552
|
+
this.plugins.forEach((plugin) => {
|
|
553
|
+
merged = mergePluginContributions(merged, toPluginContributions(plugin));
|
|
554
|
+
});
|
|
555
|
+
return merged;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Dynamically register a theme mode - useful for packages to add their own modes
|
|
559
|
+
* @param themeName - The name of the existing theme to add the mode to
|
|
560
|
+
* @param modeName - The name of the mode (e.g., 'ocean', 'forest', 'high-contrast')
|
|
561
|
+
* @param tokens - The tokens for this mode
|
|
562
|
+
*/
|
|
563
|
+
registerMode(themeName, modeName, tokens) {
|
|
564
|
+
const theme = this.themes.get(themeName);
|
|
565
|
+
if (!theme) {
|
|
566
|
+
throw new Error(`Theme "${themeName}" not found. Register the theme first with themeRegistry.register(theme)`);
|
|
567
|
+
}
|
|
568
|
+
theme.modes[modeName] = tokens;
|
|
569
|
+
const validation = validateTheme(theme);
|
|
570
|
+
if (!validation.valid) {
|
|
571
|
+
delete theme.modes[modeName];
|
|
572
|
+
const details = validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n");
|
|
573
|
+
throw new Error(`Invalid mode "${modeName}" for theme "${themeName}":
|
|
574
|
+
${details}`);
|
|
575
|
+
}
|
|
576
|
+
this.emit("variantAdded", { themeName, variantName: modeName });
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Register multiple modes at once for a theme
|
|
580
|
+
*/
|
|
581
|
+
registerModes(themeName, modes) {
|
|
582
|
+
const theme = this.themes.get(themeName);
|
|
583
|
+
if (!theme) {
|
|
584
|
+
throw new Error(`Theme "${themeName}" not found. Register the theme first with themeRegistry.register(theme)`);
|
|
585
|
+
}
|
|
586
|
+
const originalModes = { ...theme.modes };
|
|
587
|
+
Object.assign(theme.modes, modes);
|
|
588
|
+
const validation = validateTheme(theme);
|
|
589
|
+
if (!validation.valid) {
|
|
590
|
+
theme.modes = originalModes;
|
|
591
|
+
const details = validation.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n");
|
|
592
|
+
throw new Error(`Invalid modes for theme "${themeName}":
|
|
593
|
+
${details}`);
|
|
594
|
+
}
|
|
595
|
+
Object.keys(modes).forEach((modeName) => {
|
|
596
|
+
this.emit("variantAdded", { themeName, variantName: modeName });
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Create and register a new theme with specific modes
|
|
601
|
+
* Useful for packages that want to add their own themes dynamically
|
|
602
|
+
*/
|
|
603
|
+
createTheme(name, modes) {
|
|
604
|
+
const theme = { name, modes };
|
|
605
|
+
this.register(theme);
|
|
606
|
+
}
|
|
607
|
+
getFallbackThemeName() {
|
|
608
|
+
if (this.fallbackConfig.themeName && this.themes.has(this.fallbackConfig.themeName)) {
|
|
609
|
+
return this.fallbackConfig.themeName;
|
|
610
|
+
}
|
|
611
|
+
const firstTheme = this.themes.keys().next().value;
|
|
612
|
+
return firstTheme ?? null;
|
|
613
|
+
}
|
|
614
|
+
resolveModeName(theme, requestedMode) {
|
|
615
|
+
if (requestedMode && theme.modes[requestedMode]) {
|
|
616
|
+
return requestedMode;
|
|
617
|
+
}
|
|
618
|
+
if (this.fallbackConfig.modeName && theme.modes[this.fallbackConfig.modeName]) {
|
|
619
|
+
return this.fallbackConfig.modeName;
|
|
620
|
+
}
|
|
621
|
+
const firstMode = Object.keys(theme.modes)[0];
|
|
622
|
+
return firstMode ?? null;
|
|
623
|
+
}
|
|
624
|
+
resolveSelection(themeName, modeName) {
|
|
625
|
+
const effectiveThemeName = themeName && this.themes.has(themeName) ? themeName : this.getFallbackThemeName();
|
|
626
|
+
if (!effectiveThemeName) {
|
|
627
|
+
return { themeName: null, modeName: null, tokens: null };
|
|
628
|
+
}
|
|
629
|
+
const theme = this.themes.get(effectiveThemeName);
|
|
630
|
+
if (!theme) {
|
|
631
|
+
return { themeName: null, modeName: null, tokens: null };
|
|
632
|
+
}
|
|
633
|
+
const effectiveModeName = this.resolveModeName(theme, modeName);
|
|
634
|
+
if (!effectiveModeName) {
|
|
635
|
+
return { themeName: theme.name, modeName: null, tokens: null };
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
themeName: theme.name,
|
|
639
|
+
modeName: effectiveModeName,
|
|
640
|
+
tokens: theme.modes[effectiveModeName] ?? null
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
get(name) {
|
|
644
|
+
return this.themes.get(name);
|
|
645
|
+
}
|
|
646
|
+
getAll() {
|
|
647
|
+
return Array.from(this.themes.values());
|
|
648
|
+
}
|
|
649
|
+
getTokens(themeName, modeName) {
|
|
650
|
+
const theme = this.themes.get(themeName);
|
|
651
|
+
return theme?.modes[modeName];
|
|
652
|
+
}
|
|
653
|
+
setCurrent(themeName, modeName) {
|
|
654
|
+
const resolved = this.resolveSelection(themeName, modeName);
|
|
655
|
+
if (!resolved.themeName || !resolved.modeName) {
|
|
656
|
+
throw new Error(`No valid theme selection for "${themeName}" and mode "${modeName}"`);
|
|
657
|
+
}
|
|
658
|
+
this.currentTheme = resolved.themeName;
|
|
659
|
+
this.currentMode = resolved.modeName;
|
|
660
|
+
this.emit("themeChanged", { themeName: resolved.themeName, modeName: resolved.modeName });
|
|
661
|
+
}
|
|
662
|
+
getCurrent() {
|
|
663
|
+
return {
|
|
664
|
+
theme: this.currentTheme,
|
|
665
|
+
mode: this.currentMode
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
has(themeName) {
|
|
669
|
+
return this.themes.has(themeName);
|
|
670
|
+
}
|
|
671
|
+
getModes(themeName) {
|
|
672
|
+
const theme = this.themes.get(themeName);
|
|
673
|
+
return theme ? Object.keys(theme.modes) : [];
|
|
674
|
+
}
|
|
675
|
+
getVariants(themeName) {
|
|
676
|
+
return this.getModes(themeName);
|
|
677
|
+
}
|
|
678
|
+
destroy() {
|
|
679
|
+
this.themes.clear();
|
|
680
|
+
this.plugins.clear();
|
|
681
|
+
this.compositionLayers = createCompositionLayerMaps();
|
|
682
|
+
this.loadedThemeCache.clear();
|
|
683
|
+
this.currentTheme = null;
|
|
684
|
+
this.currentMode = null;
|
|
685
|
+
this.fallbackConfig = {};
|
|
686
|
+
this.emit("destroyed", { timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
687
|
+
this.listeners.clear();
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
var themeRegistry = new ThemeRegistry();
|
|
691
|
+
|
|
692
|
+
// app_theme.ts
|
|
693
|
+
var fnpTheme = {
|
|
694
|
+
name: "fnp",
|
|
695
|
+
modes: {
|
|
696
|
+
light: {
|
|
697
|
+
primary: "#031011",
|
|
698
|
+
secondary: "#153e46",
|
|
699
|
+
tertiary: "#378d93",
|
|
700
|
+
background: "#ffffff",
|
|
701
|
+
text: "#031011",
|
|
702
|
+
accent: "#378d93",
|
|
703
|
+
muted: "#667085",
|
|
704
|
+
error: "#DF1C41",
|
|
705
|
+
warning: "#F4C790",
|
|
706
|
+
success: "#27AE60",
|
|
707
|
+
info: "#378d93"
|
|
708
|
+
},
|
|
709
|
+
dark: {
|
|
710
|
+
primary: "#c4e8ee",
|
|
711
|
+
secondary: "#378d93",
|
|
712
|
+
tertiary: "#153e46",
|
|
713
|
+
background: "#031011",
|
|
714
|
+
text: "#c4e8ee",
|
|
715
|
+
accent: "#378d93",
|
|
716
|
+
muted: "#667085",
|
|
717
|
+
error: "#DF1C41",
|
|
718
|
+
warning: "#F4C790",
|
|
719
|
+
success: "#27AE60",
|
|
720
|
+
info: "#378d93"
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
themeRegistry.register(fnpTheme);
|
|
725
|
+
|
|
726
|
+
export {
|
|
727
|
+
RUNTIME_THEME_REGISTRY_VERSION,
|
|
728
|
+
isThemeTokensV2,
|
|
729
|
+
validateTheme,
|
|
730
|
+
themeRegistry
|
|
731
|
+
};
|
|
732
|
+
//# sourceMappingURL=chunk-GUPUN2GN.js.map
|