@fluid-app/portal-sdk 0.1.199 → 0.1.201
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/dist/{ContactsScreen-DvohvoOE.cjs → ContactsScreen-BQ6pvYOa.cjs} +291 -281
- package/dist/ContactsScreen-BQ6pvYOa.cjs.map +1 -0
- package/dist/{ContactsScreen-Cheiwaxn.cjs → ContactsScreen-Bw2GcYtk.cjs} +2 -2
- package/dist/{ContactsScreen-BtBNMZzG.mjs → ContactsScreen-BzRFTCBS.mjs} +17 -7
- package/dist/ContactsScreen-BzRFTCBS.mjs.map +1 -0
- package/dist/{FluidProvider-D177ez3m.cjs → FluidProvider-BRkRo8Wl.cjs} +12 -992
- package/dist/FluidProvider-BRkRo8Wl.cjs.map +1 -0
- package/dist/{FluidProvider-DiJy4Zve.mjs → FluidProvider-BTZAiT69.mjs} +9 -820
- package/dist/FluidProvider-BTZAiT69.mjs.map +1 -0
- package/dist/{MessagingScreen-Cu3tcpPs.mjs → MessagingScreen-Bk3Eh1dN.mjs} +2 -2
- package/dist/{MessagingScreen-Cu3tcpPs.mjs.map → MessagingScreen-Bk3Eh1dN.mjs.map} +1 -1
- package/dist/{MessagingScreen-Ysi48svi.cjs → MessagingScreen-DN2eQRxF.cjs} +6 -6
- package/dist/{MessagingScreen-gKidMcrr.cjs → MessagingScreen-DtDbS3VZ.cjs} +2 -2
- package/dist/{MessagingScreen-gKidMcrr.cjs.map → MessagingScreen-DtDbS3VZ.cjs.map} +1 -1
- package/dist/{PortalContentApiProvider-CPnqELEX.cjs → PortalContentApiProvider-BDbrZCyI.cjs} +115 -89
- package/dist/PortalContentApiProvider-BDbrZCyI.cjs.map +1 -0
- package/dist/{PortalContentApiProvider-CWRAw9kL.mjs → PortalContentApiProvider-CzLqEN5C.mjs} +116 -90
- package/dist/PortalContentApiProvider-CzLqEN5C.mjs.map +1 -0
- package/dist/{ProductsScreen-CQ5-A8AL.cjs → ProductsScreen-68jB202M.cjs} +2 -2
- package/dist/{ProductsScreen-CQ5-A8AL.cjs.map → ProductsScreen-68jB202M.cjs.map} +1 -1
- package/dist/{ProductsScreen-CexZ0gx9.mjs → ProductsScreen-BeNUsjh1.mjs} +2 -2
- package/dist/{ProductsScreen-Al6H4ujs.mjs → ProductsScreen-BidL3ZF5.mjs} +2 -2
- package/dist/{ProductsScreen-Al6H4ujs.mjs.map → ProductsScreen-BidL3ZF5.mjs.map} +1 -1
- package/dist/{ProductsScreen-BnwMOZ4-.cjs → ProductsScreen-Bq0f4pQL.cjs} +2 -2
- package/dist/{ProfileScreen-CGS7YkcT.cjs → ProfileScreen-BMNq0NEB.cjs} +6 -6
- package/dist/{ProfileScreen-hE1S_99P.mjs → ProfileScreen-D-pTegtY.mjs} +3 -3
- package/dist/{ProfileScreen-hE1S_99P.mjs.map → ProfileScreen-D-pTegtY.mjs.map} +1 -1
- package/dist/{ProfileScreen-DJZoMzE6.cjs → ProfileScreen-D5OxmzhM.cjs} +101 -101
- package/dist/{ProfileScreen-DJZoMzE6.cjs.map → ProfileScreen-D5OxmzhM.cjs.map} +1 -1
- package/dist/{QuickShareWidget-0GD4KWAr.cjs → QuickShareWidget-C_p3tPs5.cjs} +2 -2
- package/dist/QuickShareWidget-C_p3tPs5.cjs.map +1 -0
- package/dist/{QuickShareWidget-DZzrQjOx.mjs → QuickShareWidget-xKcV3ZQ5.mjs} +2 -2
- package/dist/QuickShareWidget-xKcV3ZQ5.mjs.map +1 -0
- package/dist/{ShareablesScreen-CzTU7e0l.mjs → ShareablesScreen-BRfgOnpL.mjs} +2 -2
- package/dist/{ShareablesScreen-CzTU7e0l.mjs.map → ShareablesScreen-BRfgOnpL.mjs.map} +1 -1
- package/dist/{ShareablesScreen-DujtMoAi.cjs → ShareablesScreen-BYP65ZnU.cjs} +2 -2
- package/dist/{ShareablesScreen-CmZ5CX99.cjs → ShareablesScreen-CCqADUXE.cjs} +2 -2
- package/dist/{ShareablesScreen-CmZ5CX99.cjs.map → ShareablesScreen-CCqADUXE.cjs.map} +1 -1
- package/dist/{ShareablesScreen-yscAsNpq.mjs → ShareablesScreen-YnNF0dD6.mjs} +2 -2
- package/dist/{ShopScreen-Bdo59te-.mjs → ShopScreen-BOJGcSyG.mjs} +3 -3
- package/dist/{ShopScreen-Bdo59te-.mjs.map → ShopScreen-BOJGcSyG.mjs.map} +1 -1
- package/dist/{ShopScreen-R9zk7d5d.cjs → ShopScreen-BzyBZ24D.cjs} +6 -6
- package/dist/{ShopScreen-DKlDKNom.cjs → ShopScreen-DeLp93hN.cjs} +3 -3
- package/dist/{ShopScreen-DKlDKNom.cjs.map → ShopScreen-DeLp93hN.cjs.map} +1 -1
- package/dist/{SpacerWidget-Da_sNa_X.mjs → SpacerWidget-BJFO-Xyh.mjs} +2 -2
- package/dist/SpacerWidget-BJFO-Xyh.mjs.map +1 -0
- package/dist/{SpacerWidget-CLFbkgoz.cjs → SpacerWidget-D9lOLPr5.cjs} +2 -2
- package/dist/SpacerWidget-D9lOLPr5.cjs.map +1 -0
- package/dist/{TableWidget-lKjTu7Go.cjs → TableWidget-C7qiWZc3.cjs} +1 -1
- package/dist/{TableWidget-B65hwjKS.mjs → TableWidget-DRByd9ig.mjs} +9 -9
- package/dist/TableWidget-DRByd9ig.mjs.map +1 -0
- package/dist/{TableWidget-FDbnEYZb.cjs → TableWidget-DUnz9hrD.cjs} +9 -9
- package/dist/TableWidget-DUnz9hrD.cjs.map +1 -0
- package/dist/index.cjs +68 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +126 -133
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +126 -133
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +25 -25
- package/dist/src-BNcNh8fM.cjs +963 -0
- package/dist/src-BNcNh8fM.cjs.map +1 -0
- package/dist/src-BjCPR0aG.mjs +788 -0
- package/dist/src-BjCPR0aG.mjs.map +1 -0
- package/package.json +16 -16
- package/dist/ContactsScreen-BtBNMZzG.mjs.map +0 -1
- package/dist/ContactsScreen-DvohvoOE.cjs.map +0 -1
- package/dist/FluidProvider-D177ez3m.cjs.map +0 -1
- package/dist/FluidProvider-DiJy4Zve.mjs.map +0 -1
- package/dist/PortalContentApiProvider-CPnqELEX.cjs.map +0 -1
- package/dist/PortalContentApiProvider-CWRAw9kL.mjs.map +0 -1
- package/dist/QuickShareWidget-0GD4KWAr.cjs.map +0 -1
- package/dist/QuickShareWidget-DZzrQjOx.mjs.map +0 -1
- package/dist/SpacerWidget-CLFbkgoz.cjs.map +0 -1
- package/dist/SpacerWidget-Da_sNa_X.mjs.map +0 -1
- package/dist/TableWidget-B65hwjKS.mjs.map +0 -1
- package/dist/TableWidget-FDbnEYZb.cjs.map +0 -1
- package/dist/countries-api-context-Dob_AzPO.mjs +0 -13
- package/dist/countries-api-context-Dob_AzPO.mjs.map +0 -1
- package/dist/countries-api-context-G-NW4BoH.cjs +0 -25
- package/dist/countries-api-context-G-NW4BoH.cjs.map +0 -1
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import Color from "colorjs.io";
|
|
3
|
+
//#region ../../store/core/src/countries-api-context.ts
|
|
4
|
+
const CountriesApiContext = createContext(null);
|
|
5
|
+
const CountriesApiProvider = CountriesApiContext.Provider;
|
|
6
|
+
function useCountriesApi() {
|
|
7
|
+
const api = useContext(CountriesApiContext);
|
|
8
|
+
if (!api) throw new Error("useCountriesApi must be used within a CountriesApiProvider");
|
|
9
|
+
return api;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region ../../platform/theme-engine/src/types.ts
|
|
13
|
+
const SEMANTIC_COLOR_NAMES = [
|
|
14
|
+
"background",
|
|
15
|
+
"foreground",
|
|
16
|
+
"primary",
|
|
17
|
+
"secondary",
|
|
18
|
+
"accent",
|
|
19
|
+
"muted",
|
|
20
|
+
"destructive"
|
|
21
|
+
];
|
|
22
|
+
const FONT_SIZE_KEYS = [
|
|
23
|
+
"extraSmall",
|
|
24
|
+
"small",
|
|
25
|
+
"regular",
|
|
26
|
+
"large",
|
|
27
|
+
"extraLarge",
|
|
28
|
+
"giant"
|
|
29
|
+
];
|
|
30
|
+
const FONT_FAMILY_KEYS = ["header", "body"];
|
|
31
|
+
const RADIUS_KEYS = [
|
|
32
|
+
"small",
|
|
33
|
+
"medium",
|
|
34
|
+
"large",
|
|
35
|
+
"extraLarge"
|
|
36
|
+
];
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region ../../platform/theme-engine/src/color-engine.ts
|
|
39
|
+
const BARE_HEX_RE = /^[0-9a-fA-F]{6}$/;
|
|
40
|
+
/**
|
|
41
|
+
* Attempt to convert any string into a Color using colorjs.io.
|
|
42
|
+
* If the string is exactly 6 hex digits it is assumed to be a bare hex value
|
|
43
|
+
* (e.g. "3b82f6") and a "#" prefix is added before parsing. Six-letter
|
|
44
|
+
* named colours like "orange" or "maroon" are left untouched.
|
|
45
|
+
*
|
|
46
|
+
* @returns the parsed Color, or a neutral gray (`oklch(0.5 0 0)`) on failure
|
|
47
|
+
*/
|
|
48
|
+
function parseColor(value) {
|
|
49
|
+
if (BARE_HEX_RE.test(value)) value = `#${value}`;
|
|
50
|
+
try {
|
|
51
|
+
return new Color(value);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn("[theme] Failed to parse color:", value, error);
|
|
54
|
+
return new Color("oklch", [
|
|
55
|
+
.5,
|
|
56
|
+
0,
|
|
57
|
+
0
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns either the original foreground or a corrected lightness variant,
|
|
63
|
+
* whichever provides better contrast against `color`.
|
|
64
|
+
* Inversion triggers when the |APCA contrast| is below 50 — APCA is signed
|
|
65
|
+
* (negative for dark-on-light, positive for light-on-dark), so comparing the
|
|
66
|
+
* absolute value avoids flipping dark text that already contrasts well on a
|
|
67
|
+
* medium background.
|
|
68
|
+
*/
|
|
69
|
+
function getForegroundColor(foreground, color) {
|
|
70
|
+
if (foreground.oklch.l == null || color.oklch.l == null) return foreground;
|
|
71
|
+
const contrast = color.contrastAPCA(foreground);
|
|
72
|
+
if (Math.abs(contrast) < 50) return new Color("oklch", [
|
|
73
|
+
color.oklch.l < .7 ? .95 : .15,
|
|
74
|
+
foreground.oklch.c || 0,
|
|
75
|
+
foreground.oklch.h || 0
|
|
76
|
+
]);
|
|
77
|
+
return foreground;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Convenience helper: given a background color string, return a CSS color
|
|
81
|
+
* string for text overlaid on it. Uses APCA contrast to pick between
|
|
82
|
+
* near-black and near-white. Returns `null` when the background cannot be
|
|
83
|
+
* parsed (e.g. a CSS custom property reference like `var(--color-muted)` or
|
|
84
|
+
* a malformed value), so callers can fall back to their theme foreground.
|
|
85
|
+
*/
|
|
86
|
+
function getContrastingTextColor(background) {
|
|
87
|
+
const normalised = BARE_HEX_RE.test(background) ? `#${background}` : background;
|
|
88
|
+
let bg;
|
|
89
|
+
try {
|
|
90
|
+
bg = new Color(normalised);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return getForegroundColor(new Color("oklch", [
|
|
95
|
+
.15,
|
|
96
|
+
0,
|
|
97
|
+
0
|
|
98
|
+
]), bg).toString({ format: "oklch" });
|
|
99
|
+
}
|
|
100
|
+
const DARK_DERIVATION_CONFIG = {
|
|
101
|
+
background: {
|
|
102
|
+
baseLightness: .15,
|
|
103
|
+
fgLightness: .93
|
|
104
|
+
},
|
|
105
|
+
foreground: {
|
|
106
|
+
baseLightness: .93,
|
|
107
|
+
fgLightness: .15
|
|
108
|
+
},
|
|
109
|
+
muted: {
|
|
110
|
+
baseLightness: .22,
|
|
111
|
+
fgLightness: .75
|
|
112
|
+
},
|
|
113
|
+
primary: {
|
|
114
|
+
baseLightness: "invert",
|
|
115
|
+
fgLightness: .95,
|
|
116
|
+
chromaScale: .9
|
|
117
|
+
},
|
|
118
|
+
secondary: {
|
|
119
|
+
baseLightness: "invert",
|
|
120
|
+
fgLightness: .93,
|
|
121
|
+
chromaScale: .85
|
|
122
|
+
},
|
|
123
|
+
accent: {
|
|
124
|
+
baseLightness: "invert",
|
|
125
|
+
fgLightness: .95,
|
|
126
|
+
chromaScale: .9
|
|
127
|
+
},
|
|
128
|
+
destructive: {
|
|
129
|
+
baseLightness: "invert",
|
|
130
|
+
fgLightness: .95,
|
|
131
|
+
chromaScale: .95
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
/** Invert OKLCH lightness (1 - l), clamped to [0.35, 0.75] to avoid extremes. */
|
|
135
|
+
function invertLightness(l) {
|
|
136
|
+
const inverted = 1 - l;
|
|
137
|
+
return Math.max(.35, Math.min(.75, inverted));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Derive a dark-mode ThemeColorInput from its light-mode counterpart.
|
|
141
|
+
*/
|
|
142
|
+
function deriveDarkVariant(name, light) {
|
|
143
|
+
const config = DARK_DERIVATION_CONFIG[name];
|
|
144
|
+
const chromaScale = config.chromaScale ?? 1;
|
|
145
|
+
const baseLightness = config.baseLightness === "invert" ? invertLightness(light.base.oklch.l ?? 0) : config.baseLightness;
|
|
146
|
+
const fgLightness = config.fgLightness === "invert" ? invertLightness(light.foreground.oklch.l ?? 0) : config.fgLightness;
|
|
147
|
+
return {
|
|
148
|
+
base: new Color("oklch", [
|
|
149
|
+
baseLightness,
|
|
150
|
+
(light.base.oklch.c || 0) * chromaScale,
|
|
151
|
+
light.base.oklch.h || 0
|
|
152
|
+
]),
|
|
153
|
+
foreground: new Color("oklch", [
|
|
154
|
+
fgLightness,
|
|
155
|
+
(light.foreground.oklch.c || 0) * chromaScale,
|
|
156
|
+
light.foreground.oklch.h || 0
|
|
157
|
+
])
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Merge auto-derived dark colors with any user-specified overrides.
|
|
162
|
+
* For each semantic color, if the user has fully overridden both base and
|
|
163
|
+
* foreground those are used; otherwise the missing channels are derived.
|
|
164
|
+
*/
|
|
165
|
+
function mergeDarkOverrides(def) {
|
|
166
|
+
const darkColors = {};
|
|
167
|
+
for (const name of SEMANTIC_COLOR_NAMES) {
|
|
168
|
+
const lightInput = def.light[name];
|
|
169
|
+
const darkOverride = def.dark[name];
|
|
170
|
+
if (darkOverride?.base && darkOverride?.foreground) darkColors[name] = darkOverride;
|
|
171
|
+
else if (darkOverride) {
|
|
172
|
+
const base = darkOverride.base ?? deriveDarkVariant(name, lightInput).base;
|
|
173
|
+
darkColors[name] = {
|
|
174
|
+
base,
|
|
175
|
+
foreground: darkOverride.foreground ?? getForegroundColor(def.light.foreground.base, base)
|
|
176
|
+
};
|
|
177
|
+
} else darkColors[name] = deriveDarkVariant(name, lightInput);
|
|
178
|
+
}
|
|
179
|
+
return darkColors;
|
|
180
|
+
}
|
|
181
|
+
function resolveColorSet(colors) {
|
|
182
|
+
const resolved = {};
|
|
183
|
+
for (const name of SEMANTIC_COLOR_NAMES) {
|
|
184
|
+
const input = colors[name];
|
|
185
|
+
resolved[name] = {
|
|
186
|
+
base: input.base.clone(),
|
|
187
|
+
foreground: input.foreground.clone()
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return resolved;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Resolve a ThemeDefinition into a complete ResolvedTheme.
|
|
194
|
+
* Dark mode colors are derived from light where not overridden.
|
|
195
|
+
*/
|
|
196
|
+
function resolveTheme(def) {
|
|
197
|
+
return {
|
|
198
|
+
id: def.id,
|
|
199
|
+
name: def.name,
|
|
200
|
+
light: resolveColorSet(def.light),
|
|
201
|
+
dark: resolveColorSet(mergeDarkOverrides(def)),
|
|
202
|
+
fontSizes: { ...def.fontSizes },
|
|
203
|
+
fontFamilies: { ...def.fontFamilies },
|
|
204
|
+
spacing: def.spacing,
|
|
205
|
+
radii: { ...def.radii }
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region ../../platform/theme-engine/src/tailwind-overrides.ts
|
|
210
|
+
const OVERRIDES = {
|
|
211
|
+
"--color-gray-50": "var(--color-muted)",
|
|
212
|
+
"--color-gray-100": "color-mix(in oklch, var(--color-muted), var(--color-foreground) 15%)",
|
|
213
|
+
"--color-gray-200": "var(--color-border)"
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Map Tailwind built-in color names to semantic theme colors using color-mix
|
|
217
|
+
* for shade interpolation. Each shade maps to a percentage of the semantic
|
|
218
|
+
* color mixed with transparent, so shades naturally adapt to both light and
|
|
219
|
+
* dark modes without inversion logic.
|
|
220
|
+
*/
|
|
221
|
+
function emitTailwindOverrides() {
|
|
222
|
+
const TAILWIND_COLOR_MAP = {
|
|
223
|
+
gray: "foreground",
|
|
224
|
+
red: "destructive",
|
|
225
|
+
blue: "primary",
|
|
226
|
+
green: "accent"
|
|
227
|
+
};
|
|
228
|
+
const TAILWIND_SHADES = [
|
|
229
|
+
50,
|
|
230
|
+
100,
|
|
231
|
+
200,
|
|
232
|
+
300,
|
|
233
|
+
400,
|
|
234
|
+
500,
|
|
235
|
+
600,
|
|
236
|
+
700,
|
|
237
|
+
800,
|
|
238
|
+
900,
|
|
239
|
+
950
|
|
240
|
+
];
|
|
241
|
+
const lines = [];
|
|
242
|
+
for (const [twName, semantic] of Object.entries(TAILWIND_COLOR_MAP)) for (const shade of TAILWIND_SHADES) {
|
|
243
|
+
const override = OVERRIDES[`--color-${twName}-${shade}`];
|
|
244
|
+
if (override) lines.push(`--color-${twName}-${shade}: ${override};`);
|
|
245
|
+
else {
|
|
246
|
+
const percent = Math.max(10, Math.min(Math.round(shade / 10), 100));
|
|
247
|
+
lines.push(`--color-${twName}-${shade}: color-mix(in oklch, var(--color-${semantic}) ${percent}%, transparent);`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
lines.push("--color-white: var(--color-background);");
|
|
251
|
+
lines.push("--color-black: var(--color-foreground);");
|
|
252
|
+
return lines;
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region ../../platform/theme-engine/src/css-generator.ts
|
|
256
|
+
function colorToCSS(color) {
|
|
257
|
+
const result = color.toString({ format: "oklch" });
|
|
258
|
+
if (result.includes("NaN")) {
|
|
259
|
+
console.warn("[theme] colorToCSS produced NaN, using neutral fallback:", result);
|
|
260
|
+
return "oklch(0.5 0 0)";
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
function camelToKebab(str) {
|
|
265
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Emit --color-{name} and --color-{name}-foreground vars.
|
|
269
|
+
* Uses --color- prefix to match portal-widgets/tailwind.config.ts.
|
|
270
|
+
*/
|
|
271
|
+
function emitColorVars(colors) {
|
|
272
|
+
const lines = [];
|
|
273
|
+
for (const name of SEMANTIC_COLOR_NAMES) {
|
|
274
|
+
const color = colors[name];
|
|
275
|
+
lines.push(`--color-${name}: ${colorToCSS(color.base)};`);
|
|
276
|
+
lines.push(`--color-${name}-foreground: ${colorToCSS(color.foreground)};`);
|
|
277
|
+
}
|
|
278
|
+
return lines;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Format a font family value for CSS output.
|
|
282
|
+
* - If the value starts with "var(" (legacy), pass through as-is
|
|
283
|
+
* - If the value already contains a comma (has fallback), pass through as-is
|
|
284
|
+
* - Otherwise, wrap in quotes and append a generic sans-serif fallback
|
|
285
|
+
*/
|
|
286
|
+
function formatFontFamily(value) {
|
|
287
|
+
if (value.startsWith("var(")) return value;
|
|
288
|
+
if (value.includes(",")) return value;
|
|
289
|
+
return `'${value}', sans-serif`;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Emit non-color CSS variables (font sizes, families, spacing, radii).
|
|
293
|
+
*/
|
|
294
|
+
function emitNonColorVars(theme) {
|
|
295
|
+
const lines = [];
|
|
296
|
+
for (const key of FONT_SIZE_KEYS) lines.push(`--font-size-${camelToKebab(key)}: ${theme.fontSizes[key]};`);
|
|
297
|
+
for (const key of FONT_FAMILY_KEYS) lines.push(`--font-${key}: ${formatFontFamily(theme.fontFamilies[key])};`);
|
|
298
|
+
lines.push(`--spacing: ${theme.spacing};`);
|
|
299
|
+
for (const key of RADIUS_KEYS) lines.push(`--radius-${camelToKebab(key)}: ${theme.radii[key]};`);
|
|
300
|
+
return lines;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Static CSS alias variables that bridge theme var names to Tailwind/component conventions.
|
|
304
|
+
* These are always emitted and not mode-dependent.
|
|
305
|
+
*/
|
|
306
|
+
const globalCSSOverride = [
|
|
307
|
+
"--color-background-foreground: var(--color-foreground);",
|
|
308
|
+
"--color-foreground-foreground: var(--color-background);",
|
|
309
|
+
"--color-contrast: var(--color-foreground);",
|
|
310
|
+
...SEMANTIC_COLOR_NAMES.map((value) => `--${value}: var(--color-${value});`),
|
|
311
|
+
...SEMANTIC_COLOR_NAMES.map((value) => `--${value}-foreground: var(--color-${value}-foreground);`),
|
|
312
|
+
"--sidebar-ring: var(--color-primary);",
|
|
313
|
+
"--sidebar-border: var(--color-border);",
|
|
314
|
+
"--sidebar-accent-foreground: var(--color-accent-foreground);",
|
|
315
|
+
"--sidebar-accent: var(--color-accent);",
|
|
316
|
+
"--sidebar-primary-foreground: var(--color-primary-foreground);",
|
|
317
|
+
"--sidebar-primary: var(--color-primary);",
|
|
318
|
+
"--sidebar-foreground: var(--color-muted-foreground);",
|
|
319
|
+
"--sidebar: var(--color-muted);",
|
|
320
|
+
"--border: color-mix(in oklch, var(--color-background), var(--color-foreground) 15%);",
|
|
321
|
+
"--ring: var(--color-primary);",
|
|
322
|
+
"--popover: var(--color-background);",
|
|
323
|
+
"--popover-foreground: var(--color-foreground);",
|
|
324
|
+
"--card: var(--color-muted);",
|
|
325
|
+
"--card-foreground: var(--color-muted-foreground);",
|
|
326
|
+
"--radius-sm: var(--radius-small);",
|
|
327
|
+
"--radius-md: var(--radius-medium);",
|
|
328
|
+
"--radius-lg: var(--radius-large);",
|
|
329
|
+
"--radius-xl: var(--radius-extra-large);",
|
|
330
|
+
"--text-xs: var(--font-size-extra-small);",
|
|
331
|
+
"--text-sm: var(--font-size-small);",
|
|
332
|
+
"--text-base: var(--font-size-regular);",
|
|
333
|
+
"--text-lg: var(--font-size-large);",
|
|
334
|
+
"--text-xl: var(--font-size-extra-large);",
|
|
335
|
+
"--text-2xl: var(--font-size-giant);"
|
|
336
|
+
];
|
|
337
|
+
/**
|
|
338
|
+
* Overrides for global tailwindcss for specifically dark mode.
|
|
339
|
+
*/
|
|
340
|
+
const globalDarkCSSOverride = ["--border: color-mix(in oklch, var(--color-background), var(--color-foreground) 15%);"];
|
|
341
|
+
/**
|
|
342
|
+
* Generate a complete CSS string for a resolved theme.
|
|
343
|
+
* Outputs 2–3 blocks: light default, dark explicit via `[data-theme-mode="dark"]`,
|
|
344
|
+
* and (unless `disableAutoTheme`) a `prefers-color-scheme: dark` media query block.
|
|
345
|
+
*/
|
|
346
|
+
function generateThemeCSS(theme, options = {}) {
|
|
347
|
+
const sel = `[data-theme="${theme.id}"]`;
|
|
348
|
+
const tw = options.mapTailwindColors ?? true;
|
|
349
|
+
const blocks = [];
|
|
350
|
+
blocks.push(`${sel} {`);
|
|
351
|
+
blocks.push(...globalCSSOverride);
|
|
352
|
+
blocks.push(...emitNonColorVars(theme));
|
|
353
|
+
blocks.push(...emitColorVars(theme.light));
|
|
354
|
+
if (tw) blocks.push(...emitTailwindOverrides());
|
|
355
|
+
blocks.push(`}`);
|
|
356
|
+
blocks.push(`${sel}[data-theme-mode="dark"] {`);
|
|
357
|
+
blocks.push(...globalDarkCSSOverride);
|
|
358
|
+
blocks.push(...emitColorVars(theme.dark));
|
|
359
|
+
if (tw) blocks.push(...emitTailwindOverrides());
|
|
360
|
+
blocks.push(`}`);
|
|
361
|
+
if (!options.disableAutoTheme) {
|
|
362
|
+
blocks.push(`@media (prefers-color-scheme: dark) {`);
|
|
363
|
+
blocks.push(`${sel}:not([data-theme-mode]) {`);
|
|
364
|
+
blocks.push(...globalDarkCSSOverride);
|
|
365
|
+
blocks.push(...emitColorVars(theme.dark).map((l) => `${l}`));
|
|
366
|
+
if (tw) blocks.push(...emitTailwindOverrides().map((l) => `${l}`));
|
|
367
|
+
blocks.push(`}`);
|
|
368
|
+
blocks.push(`}`);
|
|
369
|
+
}
|
|
370
|
+
return blocks.join("\n");
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region ../../platform/theme-engine/src/defaults.ts
|
|
374
|
+
const DEFAULT_FONT_SIZES = {
|
|
375
|
+
extraSmall: "0.75rem",
|
|
376
|
+
small: "0.875rem",
|
|
377
|
+
regular: "1rem",
|
|
378
|
+
large: "1.125rem",
|
|
379
|
+
extraLarge: "1.25rem",
|
|
380
|
+
giant: "1.5rem"
|
|
381
|
+
};
|
|
382
|
+
const DEFAULT_FONT_FAMILIES = {
|
|
383
|
+
header: "Inter",
|
|
384
|
+
body: "Inter"
|
|
385
|
+
};
|
|
386
|
+
const DEFAULT_SPACING = "0.25rem";
|
|
387
|
+
const DEFAULT_RADII = {
|
|
388
|
+
small: "0.25rem",
|
|
389
|
+
medium: "0.5rem",
|
|
390
|
+
large: "0.75rem",
|
|
391
|
+
extraLarge: "1rem"
|
|
392
|
+
};
|
|
393
|
+
const DEFAULT_COLORS = {
|
|
394
|
+
background: "#ffffff",
|
|
395
|
+
foreground: "#1a1a1a",
|
|
396
|
+
primary: "#3b82f6",
|
|
397
|
+
secondary: "#6b7280",
|
|
398
|
+
accent: "#10b981",
|
|
399
|
+
muted: "#f3f4f6",
|
|
400
|
+
destructive: "#ef4444",
|
|
401
|
+
mutedForeground: "#6b7280"
|
|
402
|
+
};
|
|
403
|
+
const DEFAULT_THEME_ID = "default";
|
|
404
|
+
const DEFAULT_THEME_NAME = "Default Theme";
|
|
405
|
+
/**
|
|
406
|
+
* Build a fresh ThemeDefinition populated with all defaults.
|
|
407
|
+
* Returns a new object each call because Color instances are mutable — do not cache the result.
|
|
408
|
+
*/
|
|
409
|
+
function getDefaultThemeDefinition() {
|
|
410
|
+
const bg = new Color(DEFAULT_COLORS.background);
|
|
411
|
+
const fg = new Color(DEFAULT_COLORS.foreground);
|
|
412
|
+
const primary = new Color(DEFAULT_COLORS.primary);
|
|
413
|
+
const secondary = new Color(DEFAULT_COLORS.secondary);
|
|
414
|
+
const accent = new Color(DEFAULT_COLORS.accent);
|
|
415
|
+
const muted = new Color(DEFAULT_COLORS.muted);
|
|
416
|
+
const destructive = new Color(DEFAULT_COLORS.destructive);
|
|
417
|
+
const mutedFg = new Color(DEFAULT_COLORS.mutedForeground);
|
|
418
|
+
const darkBg = new Color("#0a0a0a");
|
|
419
|
+
const darkFg = new Color("#fafafa");
|
|
420
|
+
const darkMuted = new Color("#171717");
|
|
421
|
+
const darkMutedForeground = new Color("#dddddd");
|
|
422
|
+
return {
|
|
423
|
+
id: DEFAULT_THEME_ID,
|
|
424
|
+
name: DEFAULT_THEME_NAME,
|
|
425
|
+
light: {
|
|
426
|
+
background: {
|
|
427
|
+
base: bg,
|
|
428
|
+
foreground: fg
|
|
429
|
+
},
|
|
430
|
+
foreground: {
|
|
431
|
+
base: fg,
|
|
432
|
+
foreground: bg
|
|
433
|
+
},
|
|
434
|
+
primary: {
|
|
435
|
+
base: primary,
|
|
436
|
+
foreground: getForegroundColor(fg, primary)
|
|
437
|
+
},
|
|
438
|
+
secondary: {
|
|
439
|
+
base: secondary,
|
|
440
|
+
foreground: getForegroundColor(fg, secondary)
|
|
441
|
+
},
|
|
442
|
+
accent: {
|
|
443
|
+
base: accent,
|
|
444
|
+
foreground: getForegroundColor(fg, accent)
|
|
445
|
+
},
|
|
446
|
+
muted: {
|
|
447
|
+
base: muted,
|
|
448
|
+
foreground: mutedFg
|
|
449
|
+
},
|
|
450
|
+
destructive: {
|
|
451
|
+
base: destructive,
|
|
452
|
+
foreground: getForegroundColor(fg, destructive)
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
dark: {
|
|
456
|
+
background: {
|
|
457
|
+
base: darkBg,
|
|
458
|
+
foreground: darkFg
|
|
459
|
+
},
|
|
460
|
+
foreground: {
|
|
461
|
+
base: darkFg,
|
|
462
|
+
foreground: darkBg
|
|
463
|
+
},
|
|
464
|
+
muted: {
|
|
465
|
+
base: darkMuted,
|
|
466
|
+
foreground: darkMutedForeground
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
fontSizes: { ...DEFAULT_FONT_SIZES },
|
|
470
|
+
fontFamilies: { ...DEFAULT_FONT_FAMILIES },
|
|
471
|
+
spacing: DEFAULT_SPACING,
|
|
472
|
+
radii: { ...DEFAULT_RADII }
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region ../../platform/theme-engine/src/serialisation.ts
|
|
477
|
+
function colorToPlain(color) {
|
|
478
|
+
return {
|
|
479
|
+
l: color.oklch.l ?? 0,
|
|
480
|
+
c: color.oklch.c ?? 0,
|
|
481
|
+
h: color.oklch.h ?? 0
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function plainToColor(plain) {
|
|
485
|
+
return new Color("oklch", [
|
|
486
|
+
plain.l,
|
|
487
|
+
plain.c,
|
|
488
|
+
plain.h
|
|
489
|
+
]);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Serialise a ThemeDefinition (with Color objects) to a plain JSON payload
|
|
493
|
+
* suitable for backend storage.
|
|
494
|
+
*/
|
|
495
|
+
function serialiseTheme(def) {
|
|
496
|
+
const light = {};
|
|
497
|
+
for (const name of SEMANTIC_COLOR_NAMES) light[name] = {
|
|
498
|
+
base: colorToPlain(def.light[name].base),
|
|
499
|
+
foreground: colorToPlain(def.light[name].foreground)
|
|
500
|
+
};
|
|
501
|
+
const dark = {};
|
|
502
|
+
for (const [name, value] of Object.entries(def.dark)) {
|
|
503
|
+
if (!value) continue;
|
|
504
|
+
dark[name] = {
|
|
505
|
+
...value.base ? { base: colorToPlain(value.base) } : {},
|
|
506
|
+
...value.foreground ? { foreground: colorToPlain(value.foreground) } : {}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
id: def.id,
|
|
511
|
+
name: def.name,
|
|
512
|
+
light,
|
|
513
|
+
dark,
|
|
514
|
+
fontSizes: { ...def.fontSizes },
|
|
515
|
+
fontFamilies: { ...def.fontFamilies },
|
|
516
|
+
spacing: def.spacing,
|
|
517
|
+
radii: { ...def.radii },
|
|
518
|
+
...def.syncWithBrandColors ? { syncWithBrandColors: true } : {}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Deserialise a backend payload into a ThemeDefinition with Color objects.
|
|
523
|
+
* Accepts `Record<string, unknown>` because API data is untyped at the boundary.
|
|
524
|
+
* Falls back to default colors for any missing light-mode entries.
|
|
525
|
+
*/
|
|
526
|
+
function deserialiseTheme(payload) {
|
|
527
|
+
const lightRaw = payload.light ?? {};
|
|
528
|
+
const darkRaw = payload.dark ?? {};
|
|
529
|
+
const defaults = getDefaultThemeDefinition();
|
|
530
|
+
const light = {};
|
|
531
|
+
for (const name of SEMANTIC_COLOR_NAMES) {
|
|
532
|
+
const entry = lightRaw[name];
|
|
533
|
+
if (entry) light[name] = {
|
|
534
|
+
base: plainToColor(entry.base),
|
|
535
|
+
foreground: plainToColor(entry.foreground)
|
|
536
|
+
};
|
|
537
|
+
else {
|
|
538
|
+
console.warn(`[theme] deserialiseTheme: missing light color "${name}", using default`);
|
|
539
|
+
light[name] = defaults.light[name];
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const dark = {};
|
|
543
|
+
for (const [name, value] of Object.entries(darkRaw)) {
|
|
544
|
+
if (!value) continue;
|
|
545
|
+
dark[name] = {
|
|
546
|
+
...value.base ? { base: plainToColor(value.base) } : {},
|
|
547
|
+
...value.foreground ? { foreground: plainToColor(value.foreground) } : {}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
id: payload.id,
|
|
552
|
+
name: payload.name,
|
|
553
|
+
light,
|
|
554
|
+
dark,
|
|
555
|
+
fontSizes: payload.fontSizes ?? DEFAULT_FONT_SIZES,
|
|
556
|
+
fontFamilies: payload.fontFamilies ?? DEFAULT_FONT_FAMILIES,
|
|
557
|
+
spacing: payload.spacing ?? "0.25rem",
|
|
558
|
+
radii: payload.radii ?? DEFAULT_RADII,
|
|
559
|
+
...payload.syncWithBrandColors === true ? { syncWithBrandColors: true } : {}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region ../../platform/theme-engine/src/transforms.ts
|
|
564
|
+
/**
|
|
565
|
+
* Check if a theme config uses the new structured format (has a `light` key
|
|
566
|
+
* that is an object) vs the legacy flat format.
|
|
567
|
+
*/
|
|
568
|
+
function isNewThemeFormat(config) {
|
|
569
|
+
return config.light != null && typeof config.light === "object";
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Convert a legacy flat config to a ThemeDefinition.
|
|
573
|
+
* Legacy format: { base: "#fff", text: "#000", primary: "oklch(0.6 0.2 250)", ... }
|
|
574
|
+
*/
|
|
575
|
+
function legacyConfigToDefinition(id, name, config) {
|
|
576
|
+
const bg = parseColor(config.base ?? config.background ?? DEFAULT_COLORS.background);
|
|
577
|
+
const fg = parseColor(config.text ?? config.foreground ?? DEFAULT_COLORS.foreground);
|
|
578
|
+
const primary = parseColor(config.primary ?? DEFAULT_COLORS.primary);
|
|
579
|
+
const secondary = parseColor(config.secondary ?? DEFAULT_COLORS.secondary);
|
|
580
|
+
const accent = parseColor(config.accent ?? DEFAULT_COLORS.accent);
|
|
581
|
+
const muted = parseColor(config.muted ?? DEFAULT_COLORS.muted);
|
|
582
|
+
const destructive = parseColor(config.destructive ?? DEFAULT_COLORS.destructive);
|
|
583
|
+
const mutedFg = parseColor(config.mutedForeground ?? DEFAULT_COLORS.mutedForeground);
|
|
584
|
+
return {
|
|
585
|
+
id: String(id),
|
|
586
|
+
name,
|
|
587
|
+
light: {
|
|
588
|
+
background: {
|
|
589
|
+
base: bg,
|
|
590
|
+
foreground: fg
|
|
591
|
+
},
|
|
592
|
+
foreground: {
|
|
593
|
+
base: fg,
|
|
594
|
+
foreground: bg
|
|
595
|
+
},
|
|
596
|
+
primary: {
|
|
597
|
+
base: primary,
|
|
598
|
+
foreground: getForegroundColor(fg, primary)
|
|
599
|
+
},
|
|
600
|
+
secondary: {
|
|
601
|
+
base: secondary,
|
|
602
|
+
foreground: getForegroundColor(fg, secondary)
|
|
603
|
+
},
|
|
604
|
+
accent: {
|
|
605
|
+
base: accent,
|
|
606
|
+
foreground: getForegroundColor(fg, accent)
|
|
607
|
+
},
|
|
608
|
+
muted: {
|
|
609
|
+
base: muted,
|
|
610
|
+
foreground: mutedFg
|
|
611
|
+
},
|
|
612
|
+
destructive: {
|
|
613
|
+
base: destructive,
|
|
614
|
+
foreground: getForegroundColor(fg, destructive)
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
dark: {},
|
|
618
|
+
fontSizes: {
|
|
619
|
+
extraSmall: config.extraSmall ?? DEFAULT_FONT_SIZES.extraSmall,
|
|
620
|
+
small: config.small ?? DEFAULT_FONT_SIZES.small,
|
|
621
|
+
regular: config.regular ?? DEFAULT_FONT_SIZES.regular,
|
|
622
|
+
large: config.large ?? DEFAULT_FONT_SIZES.large,
|
|
623
|
+
extraLarge: config.extraLarge ?? DEFAULT_FONT_SIZES.extraLarge,
|
|
624
|
+
giant: config.giant ?? DEFAULT_FONT_SIZES.giant
|
|
625
|
+
},
|
|
626
|
+
fontFamilies: {
|
|
627
|
+
header: config.headerFont ?? DEFAULT_FONT_FAMILIES.header,
|
|
628
|
+
body: config.bodyFont ?? DEFAULT_FONT_FAMILIES.body
|
|
629
|
+
},
|
|
630
|
+
spacing: config.globalSpacing ?? "0.25rem",
|
|
631
|
+
radii: {
|
|
632
|
+
small: config.radiusSmall ?? DEFAULT_RADII.small,
|
|
633
|
+
medium: config.radiusMedium ?? DEFAULT_RADII.medium,
|
|
634
|
+
large: config.radiusLarge ?? DEFAULT_RADII.large,
|
|
635
|
+
extraLarge: config.radiusExtraLarge ?? DEFAULT_RADII.extraLarge
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Build a ThemeDefinition from a single API theme object.
|
|
641
|
+
* Handles both new structured format and legacy flat format.
|
|
642
|
+
*/
|
|
643
|
+
function buildThemeDefinition(theme) {
|
|
644
|
+
const config = theme.config ?? {};
|
|
645
|
+
if (isNewThemeFormat(config)) return deserialiseTheme({
|
|
646
|
+
...config,
|
|
647
|
+
id: String(theme.id),
|
|
648
|
+
name: theme.name ?? "Untitled Theme"
|
|
649
|
+
});
|
|
650
|
+
return legacyConfigToDefinition(theme.id, theme.name ?? "Untitled Theme", config);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Transform raw API themes to ThemeDefinition[].
|
|
654
|
+
* Catches and logs errors per theme (graceful degradation).
|
|
655
|
+
*/
|
|
656
|
+
function transformThemes(themes) {
|
|
657
|
+
return themes.flatMap((theme) => {
|
|
658
|
+
try {
|
|
659
|
+
return [buildThemeDefinition(theme)];
|
|
660
|
+
} catch (error) {
|
|
661
|
+
console.error(`[theme] Failed to build theme id=${theme.id}:`, error);
|
|
662
|
+
return [];
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Get the active theme ID from a list of raw API themes.
|
|
668
|
+
* Falls back to the first theme if none is marked active.
|
|
669
|
+
*/
|
|
670
|
+
function getActiveThemeId(themes) {
|
|
671
|
+
const active = themes.find((t) => t.active) ?? themes[0];
|
|
672
|
+
return active ? String(active.id) : void 0;
|
|
673
|
+
}
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region ../../platform/theme-engine/src/theme-applicator.ts
|
|
676
|
+
const STYLE_PREFIX = "theme-style-";
|
|
677
|
+
const FONT_LINK_PREFIX = "theme-font-";
|
|
678
|
+
const SYSTEM_FONTS = new Set([
|
|
679
|
+
"sans-serif",
|
|
680
|
+
"serif",
|
|
681
|
+
"monospace",
|
|
682
|
+
"cursive",
|
|
683
|
+
"fantasy",
|
|
684
|
+
"system-ui",
|
|
685
|
+
"ui-sans-serif",
|
|
686
|
+
"ui-serif",
|
|
687
|
+
"ui-monospace"
|
|
688
|
+
]);
|
|
689
|
+
/** Build a Google Fonts CSS2 URL for a given font family with all weights. */
|
|
690
|
+
function buildGoogleFontUrl(family) {
|
|
691
|
+
return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family).replace(/%20/g, "+")}:wght@100;200;300;400;500;600;700;800;900&display=swap`;
|
|
692
|
+
}
|
|
693
|
+
/** Check if a font family value needs to be loaded (i.e. is not a CSS var or system font). */
|
|
694
|
+
function isLoadableFont(value) {
|
|
695
|
+
if (!value) return false;
|
|
696
|
+
if (value.startsWith("var(")) return false;
|
|
697
|
+
return !SYSTEM_FONTS.has(value.toLowerCase());
|
|
698
|
+
}
|
|
699
|
+
/** Deterministic link element ID for a font family. */
|
|
700
|
+
function fontLinkId(family) {
|
|
701
|
+
return `${FONT_LINK_PREFIX}${family.replace(/\s+/g, "-").toLowerCase()}`;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Inject or update `<link>` elements for Google Fonts used by the theme.
|
|
705
|
+
* Removes links for fonts that are no longer referenced.
|
|
706
|
+
*/
|
|
707
|
+
function loadThemeFonts(theme) {
|
|
708
|
+
if (typeof document === "undefined") return;
|
|
709
|
+
const fontsToLoad = /* @__PURE__ */ new Set();
|
|
710
|
+
for (const key of FONT_FAMILY_KEYS) {
|
|
711
|
+
const value = theme.fontFamilies[key];
|
|
712
|
+
if (isLoadableFont(value)) fontsToLoad.add(value);
|
|
713
|
+
}
|
|
714
|
+
document.querySelectorAll(`link[id^="${FONT_LINK_PREFIX}"]`).forEach((link) => {
|
|
715
|
+
const owners = link.getAttribute("data-font-theme-ids")?.split(",") ?? [];
|
|
716
|
+
if (!owners.includes(theme.id)) return;
|
|
717
|
+
const fontName = link.getAttribute("data-font-family");
|
|
718
|
+
if (fontName && !fontsToLoad.has(fontName)) {
|
|
719
|
+
const remaining = owners.filter((id) => id !== theme.id);
|
|
720
|
+
if (remaining.length === 0) link.remove();
|
|
721
|
+
else link.setAttribute("data-font-theme-ids", remaining.join(","));
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
for (const family of fontsToLoad) {
|
|
725
|
+
const id = fontLinkId(family);
|
|
726
|
+
const existing = document.getElementById(id);
|
|
727
|
+
if (existing) {
|
|
728
|
+
const owners = existing.getAttribute("data-font-theme-ids")?.split(",") ?? [];
|
|
729
|
+
if (!owners.includes(theme.id)) existing.setAttribute("data-font-theme-ids", [...owners, theme.id].join(","));
|
|
730
|
+
} else {
|
|
731
|
+
const link = document.createElement("link");
|
|
732
|
+
link.id = id;
|
|
733
|
+
link.rel = "stylesheet";
|
|
734
|
+
link.href = buildGoogleFontUrl(family);
|
|
735
|
+
link.setAttribute("data-font-family", family);
|
|
736
|
+
link.setAttribute("data-font-theme-ids", theme.id);
|
|
737
|
+
document.head.appendChild(link);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/** Remove all font `<link>` elements injected by the theme system. */
|
|
742
|
+
function removeAllFontLinks() {
|
|
743
|
+
if (typeof document === "undefined") return;
|
|
744
|
+
document.querySelectorAll(`link[id^="${FONT_LINK_PREFIX}"]`).forEach((el) => el.remove());
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Inject or update a `<style>` element in `<head>` for the given theme.
|
|
748
|
+
* The element ID is deterministic (`theme-style-{themeId}`) so repeated calls
|
|
749
|
+
* for the same theme are idempotent — the existing element is updated in place.
|
|
750
|
+
* Also loads Google Fonts referenced by the theme's font families.
|
|
751
|
+
* No-op when `document` is unavailable (SSR).
|
|
752
|
+
*/
|
|
753
|
+
function applyTheme(theme, options) {
|
|
754
|
+
if (typeof document === "undefined") return;
|
|
755
|
+
try {
|
|
756
|
+
loadThemeFonts(theme);
|
|
757
|
+
const styleId = `${STYLE_PREFIX}${theme.id}`;
|
|
758
|
+
let el = document.getElementById(styleId);
|
|
759
|
+
if (!el) {
|
|
760
|
+
el = document.createElement("style");
|
|
761
|
+
el.id = styleId;
|
|
762
|
+
document.head.appendChild(el);
|
|
763
|
+
}
|
|
764
|
+
el.textContent = generateThemeCSS(theme, options);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
console.error(`[theme] applyTheme failed for "${theme.id}":`, error);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/** Remove an injected theme stylesheet and clean up font link ownership. No-op during SSR. */
|
|
770
|
+
function removeTheme(themeId) {
|
|
771
|
+
if (typeof document === "undefined") return;
|
|
772
|
+
document.getElementById(`${STYLE_PREFIX}${themeId}`)?.remove();
|
|
773
|
+
document.querySelectorAll(`link[id^="${FONT_LINK_PREFIX}"]`).forEach((link) => {
|
|
774
|
+
const remaining = (link.getAttribute("data-font-theme-ids")?.split(",") ?? []).filter((id) => id !== themeId);
|
|
775
|
+
if (remaining.length === 0) link.remove();
|
|
776
|
+
else link.setAttribute("data-font-theme-ids", remaining.join(","));
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
/** Remove all injected theme stylesheets and font links. No-op during SSR. */
|
|
780
|
+
function removeAllThemes() {
|
|
781
|
+
if (typeof document === "undefined") return;
|
|
782
|
+
document.querySelectorAll(`style[id^="${STYLE_PREFIX}"]`).forEach((el) => el.remove());
|
|
783
|
+
removeAllFontLinks();
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
786
|
+
export { resolveTheme as C, SEMANTIC_COLOR_NAMES as D, RADIUS_KEYS as E, CountriesApiProvider as O, parseColor as S, FONT_SIZE_KEYS as T, generateThemeCSS as _, getActiveThemeId as a, getForegroundColor as b, serialiseTheme as c, DEFAULT_FONT_SIZES as d, DEFAULT_RADII as f, getDefaultThemeDefinition as g, DEFAULT_THEME_NAME as h, buildThemeDefinition as i, useCountriesApi as k, DEFAULT_COLORS as l, DEFAULT_THEME_ID as m, removeAllThemes as n, transformThemes as o, DEFAULT_SPACING as p, removeTheme as r, deserialiseTheme as s, applyTheme as t, DEFAULT_FONT_FAMILIES as u, deriveDarkVariant as v, FONT_FAMILY_KEYS as w, mergeDarkOverrides as x, getContrastingTextColor as y };
|
|
787
|
+
|
|
788
|
+
//# sourceMappingURL=src-BjCPR0aG.mjs.map
|