@lessonkit/themes 1.2.0 → 1.3.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.
- package/dist/index.cjs +50 -9
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +49 -9
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
lightTheme: () => lightTheme,
|
|
37
37
|
mergeThemes: () => mergeThemes,
|
|
38
38
|
radiusVarName: () => radiusVarName,
|
|
39
|
+
sanitizeCssCustomPropertyValue: () => sanitizeCssCustomPropertyValue,
|
|
39
40
|
shadowVarName: () => shadowVarName,
|
|
40
41
|
spacingVarName: () => spacingVarName,
|
|
41
42
|
themeToCssDeclarationBlock: () => themeToCssDeclarationBlock,
|
|
@@ -68,9 +69,20 @@ var TYPOGRAPHY_KEYS = [
|
|
|
68
69
|
];
|
|
69
70
|
var RADIUS_KEYS = ["sm", "md", "lg"];
|
|
70
71
|
var SHADOW_KEYS = ["sm", "md", "lg"];
|
|
72
|
+
var EXTRA_COLOR_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
71
73
|
function isNonEmptyString(v) {
|
|
72
74
|
return typeof v === "string" && v.trim().length > 0;
|
|
73
75
|
}
|
|
76
|
+
function isSafeCssCustomPropertyValue(value) {
|
|
77
|
+
return !/[;}\r\n\\<>]/.test(value) && !value.includes("/*");
|
|
78
|
+
}
|
|
79
|
+
function validateCssTokenValue(path, value, issues) {
|
|
80
|
+
if (!isSafeCssCustomPropertyValue(value)) {
|
|
81
|
+
issues.push({ path, message: "must not contain CSS-breaking characters (;, }, newlines, or comments)" });
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
74
86
|
function validateRequiredGroup(group, value, keys, issues) {
|
|
75
87
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
76
88
|
issues.push({ path: group, message: "must be an object" });
|
|
@@ -82,7 +94,7 @@ function validateRequiredGroup(group, value, keys, issues) {
|
|
|
82
94
|
const v = obj[key];
|
|
83
95
|
if (!isNonEmptyString(v)) {
|
|
84
96
|
issues.push({ path: `${group}.${key}`, message: "required non-empty string" });
|
|
85
|
-
} else {
|
|
97
|
+
} else if (validateCssTokenValue(`${group}.${key}`, v, issues)) {
|
|
86
98
|
out[key] = v;
|
|
87
99
|
}
|
|
88
100
|
}
|
|
@@ -96,9 +108,16 @@ function validateColorsExtra(value, issues) {
|
|
|
96
108
|
}
|
|
97
109
|
const extra = {};
|
|
98
110
|
for (const [k, v] of Object.entries(value)) {
|
|
111
|
+
if (!EXTRA_COLOR_KEY_PATTERN.test(k)) {
|
|
112
|
+
issues.push({
|
|
113
|
+
path: `colors.extra.${k}`,
|
|
114
|
+
message: "key must match [a-zA-Z][a-zA-Z0-9_-]*"
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
99
118
|
if (!isNonEmptyString(v)) {
|
|
100
119
|
issues.push({ path: `colors.extra.${k}`, message: "must be a non-empty string" });
|
|
101
|
-
} else {
|
|
120
|
+
} else if (validateCssTokenValue(`colors.extra.${k}`, v, issues)) {
|
|
102
121
|
extra[k] = v;
|
|
103
122
|
}
|
|
104
123
|
}
|
|
@@ -204,6 +223,21 @@ function mergeThemes(base, ...overrides) {
|
|
|
204
223
|
}
|
|
205
224
|
|
|
206
225
|
// src/cssVariables.ts
|
|
226
|
+
function sanitizeCssCustomPropertyValue(value) {
|
|
227
|
+
if (/[;}\r\n\\<>]/.test(value) || value.includes("/*")) return null;
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
function isSafeCssSelector(selector) {
|
|
231
|
+
if (selector === ":root") return true;
|
|
232
|
+
if (/^\.[a-zA-Z_][\w-]*$/.test(selector)) return true;
|
|
233
|
+
if (/^#[a-zA-Z_][\w-]*$/.test(selector)) return true;
|
|
234
|
+
if (/^\[data-lk-theme=(["'])[^"']+\1\]$/.test(selector)) return true;
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
function assignCssVar(vars, key, value) {
|
|
238
|
+
const safe = sanitizeCssCustomPropertyValue(value);
|
|
239
|
+
if (safe !== null) vars[key] = safe;
|
|
240
|
+
}
|
|
207
241
|
function tokenKeyToKebab(key) {
|
|
208
242
|
return key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
209
243
|
}
|
|
@@ -230,23 +264,23 @@ function themeToCssVariables(theme) {
|
|
|
230
264
|
for (const [key, value] of Object.entries(theme.colors)) {
|
|
231
265
|
if (key === "extra" && value && typeof value === "object") {
|
|
232
266
|
for (const [ek, ev] of Object.entries(value)) {
|
|
233
|
-
vars
|
|
267
|
+
assignCssVar(vars, colorExtraVarName(ek), ev);
|
|
234
268
|
}
|
|
235
269
|
} else if (key !== "extra") {
|
|
236
|
-
vars
|
|
270
|
+
assignCssVar(vars, colorVarName(key), value);
|
|
237
271
|
}
|
|
238
272
|
}
|
|
239
273
|
for (const [key, value] of Object.entries(theme.spacing)) {
|
|
240
|
-
vars
|
|
274
|
+
assignCssVar(vars, spacingVarName(key), value);
|
|
241
275
|
}
|
|
242
276
|
for (const [key, value] of Object.entries(theme.typography)) {
|
|
243
|
-
vars
|
|
277
|
+
assignCssVar(vars, typographyVarName(key), value);
|
|
244
278
|
}
|
|
245
279
|
for (const [key, value] of Object.entries(theme.radius)) {
|
|
246
|
-
vars
|
|
280
|
+
assignCssVar(vars, radiusVarName(key), value);
|
|
247
281
|
}
|
|
248
282
|
for (const [key, value] of Object.entries(theme.shadows)) {
|
|
249
|
-
vars
|
|
283
|
+
assignCssVar(vars, shadowVarName(key), value);
|
|
250
284
|
}
|
|
251
285
|
const sorted = {};
|
|
252
286
|
for (const key of Object.keys(vars).sort()) {
|
|
@@ -256,6 +290,9 @@ function themeToCssVariables(theme) {
|
|
|
256
290
|
}
|
|
257
291
|
function themeToCssDeclarationBlock(theme, opts) {
|
|
258
292
|
const selector = opts?.selector ?? ":root";
|
|
293
|
+
if (!isSafeCssSelector(selector)) {
|
|
294
|
+
throw new Error(`[lessonkit] unsafe CSS selector for theme block: ${selector}`);
|
|
295
|
+
}
|
|
259
296
|
const vars = themeToCssVariables(theme);
|
|
260
297
|
const body = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`).join("\n");
|
|
261
298
|
return `${selector} {
|
|
@@ -373,8 +410,11 @@ var PRESETS = {
|
|
|
373
410
|
dark: darkTheme,
|
|
374
411
|
brand: brandTheme
|
|
375
412
|
};
|
|
413
|
+
function cloneTheme(theme) {
|
|
414
|
+
return JSON.parse(JSON.stringify(theme));
|
|
415
|
+
}
|
|
376
416
|
function getPresetTheme(preset) {
|
|
377
|
-
return PRESETS[preset];
|
|
417
|
+
return cloneTheme(PRESETS[preset]);
|
|
378
418
|
}
|
|
379
419
|
|
|
380
420
|
// src/catalog.ts
|
|
@@ -492,6 +532,7 @@ function buildThemeCatalog() {
|
|
|
492
532
|
lightTheme,
|
|
493
533
|
mergeThemes,
|
|
494
534
|
radiusVarName,
|
|
535
|
+
sanitizeCssCustomPropertyValue,
|
|
495
536
|
shadowVarName,
|
|
496
537
|
spacingVarName,
|
|
497
538
|
themeToCssDeclarationBlock,
|
package/dist/index.d.cts
CHANGED
|
@@ -53,6 +53,8 @@ declare const REQUIRED_SHADOW_KEYS: ThemeShadowKey[];
|
|
|
53
53
|
*/
|
|
54
54
|
declare function mergeThemes(base: LessonkitThemeV1, ...overrides: (PartialLessonkitThemeV1 | undefined)[]): LessonkitThemeV1;
|
|
55
55
|
|
|
56
|
+
/** Reject values that could break out of a CSS declaration block. */
|
|
57
|
+
declare function sanitizeCssCustomPropertyValue(value: string): string | null;
|
|
56
58
|
/** Map a token path segment to kebab-case for CSS custom property names. */
|
|
57
59
|
declare function tokenKeyToKebab(key: string): string;
|
|
58
60
|
/** CSS custom property name for a color token (required keys). */
|
|
@@ -93,4 +95,4 @@ type ThemeCatalogEntry = {
|
|
|
93
95
|
/** Enumerable catalog of themeable tokens (v1). */
|
|
94
96
|
declare function buildThemeCatalog(): ThemeCatalogEntry[];
|
|
95
97
|
|
|
96
|
-
export { type LessonkitTheme, type LessonkitThemeColors, type LessonkitThemeRadius, type LessonkitThemeShadows, type LessonkitThemeSpacing, type LessonkitThemeTypography, type LessonkitThemeV1, type PartialLessonkitThemeV1, REQUIRED_COLOR_KEYS, REQUIRED_RADIUS_KEYS, REQUIRED_SHADOW_KEYS, REQUIRED_SPACING_KEYS, REQUIRED_TYPOGRAPHY_KEYS, type ThemeCatalogEntry, type ThemeColorKey, type ThemePresetName, type ThemeRadiusKey, type ThemeShadowKey, type ThemeSpacingKey, type ThemeTypographyKey, type ThemeValidationIssue, type ThemeValidationResult, brandTheme, brandThemeOverrides, buildThemeCatalog, colorExtraVarName, colorVarName, darkTheme, defaultTheme, getPresetTheme, lightTheme, mergeThemes, radiusVarName, shadowVarName, spacingVarName, themeToCssDeclarationBlock, themeToCssVariables, tokenKeyToKebab, typographyVarName, validateTheme };
|
|
98
|
+
export { type LessonkitTheme, type LessonkitThemeColors, type LessonkitThemeRadius, type LessonkitThemeShadows, type LessonkitThemeSpacing, type LessonkitThemeTypography, type LessonkitThemeV1, type PartialLessonkitThemeV1, REQUIRED_COLOR_KEYS, REQUIRED_RADIUS_KEYS, REQUIRED_SHADOW_KEYS, REQUIRED_SPACING_KEYS, REQUIRED_TYPOGRAPHY_KEYS, type ThemeCatalogEntry, type ThemeColorKey, type ThemePresetName, type ThemeRadiusKey, type ThemeShadowKey, type ThemeSpacingKey, type ThemeTypographyKey, type ThemeValidationIssue, type ThemeValidationResult, brandTheme, brandThemeOverrides, buildThemeCatalog, colorExtraVarName, colorVarName, darkTheme, defaultTheme, getPresetTheme, lightTheme, mergeThemes, radiusVarName, sanitizeCssCustomPropertyValue, shadowVarName, spacingVarName, themeToCssDeclarationBlock, themeToCssVariables, tokenKeyToKebab, typographyVarName, validateTheme };
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ declare const REQUIRED_SHADOW_KEYS: ThemeShadowKey[];
|
|
|
53
53
|
*/
|
|
54
54
|
declare function mergeThemes(base: LessonkitThemeV1, ...overrides: (PartialLessonkitThemeV1 | undefined)[]): LessonkitThemeV1;
|
|
55
55
|
|
|
56
|
+
/** Reject values that could break out of a CSS declaration block. */
|
|
57
|
+
declare function sanitizeCssCustomPropertyValue(value: string): string | null;
|
|
56
58
|
/** Map a token path segment to kebab-case for CSS custom property names. */
|
|
57
59
|
declare function tokenKeyToKebab(key: string): string;
|
|
58
60
|
/** CSS custom property name for a color token (required keys). */
|
|
@@ -93,4 +95,4 @@ type ThemeCatalogEntry = {
|
|
|
93
95
|
/** Enumerable catalog of themeable tokens (v1). */
|
|
94
96
|
declare function buildThemeCatalog(): ThemeCatalogEntry[];
|
|
95
97
|
|
|
96
|
-
export { type LessonkitTheme, type LessonkitThemeColors, type LessonkitThemeRadius, type LessonkitThemeShadows, type LessonkitThemeSpacing, type LessonkitThemeTypography, type LessonkitThemeV1, type PartialLessonkitThemeV1, REQUIRED_COLOR_KEYS, REQUIRED_RADIUS_KEYS, REQUIRED_SHADOW_KEYS, REQUIRED_SPACING_KEYS, REQUIRED_TYPOGRAPHY_KEYS, type ThemeCatalogEntry, type ThemeColorKey, type ThemePresetName, type ThemeRadiusKey, type ThemeShadowKey, type ThemeSpacingKey, type ThemeTypographyKey, type ThemeValidationIssue, type ThemeValidationResult, brandTheme, brandThemeOverrides, buildThemeCatalog, colorExtraVarName, colorVarName, darkTheme, defaultTheme, getPresetTheme, lightTheme, mergeThemes, radiusVarName, shadowVarName, spacingVarName, themeToCssDeclarationBlock, themeToCssVariables, tokenKeyToKebab, typographyVarName, validateTheme };
|
|
98
|
+
export { type LessonkitTheme, type LessonkitThemeColors, type LessonkitThemeRadius, type LessonkitThemeShadows, type LessonkitThemeSpacing, type LessonkitThemeTypography, type LessonkitThemeV1, type PartialLessonkitThemeV1, REQUIRED_COLOR_KEYS, REQUIRED_RADIUS_KEYS, REQUIRED_SHADOW_KEYS, REQUIRED_SPACING_KEYS, REQUIRED_TYPOGRAPHY_KEYS, type ThemeCatalogEntry, type ThemeColorKey, type ThemePresetName, type ThemeRadiusKey, type ThemeShadowKey, type ThemeSpacingKey, type ThemeTypographyKey, type ThemeValidationIssue, type ThemeValidationResult, brandTheme, brandThemeOverrides, buildThemeCatalog, colorExtraVarName, colorVarName, darkTheme, defaultTheme, getPresetTheme, lightTheme, mergeThemes, radiusVarName, sanitizeCssCustomPropertyValue, shadowVarName, spacingVarName, themeToCssDeclarationBlock, themeToCssVariables, tokenKeyToKebab, typographyVarName, validateTheme };
|
package/dist/index.js
CHANGED
|
@@ -20,9 +20,20 @@ var TYPOGRAPHY_KEYS = [
|
|
|
20
20
|
];
|
|
21
21
|
var RADIUS_KEYS = ["sm", "md", "lg"];
|
|
22
22
|
var SHADOW_KEYS = ["sm", "md", "lg"];
|
|
23
|
+
var EXTRA_COLOR_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
23
24
|
function isNonEmptyString(v) {
|
|
24
25
|
return typeof v === "string" && v.trim().length > 0;
|
|
25
26
|
}
|
|
27
|
+
function isSafeCssCustomPropertyValue(value) {
|
|
28
|
+
return !/[;}\r\n\\<>]/.test(value) && !value.includes("/*");
|
|
29
|
+
}
|
|
30
|
+
function validateCssTokenValue(path, value, issues) {
|
|
31
|
+
if (!isSafeCssCustomPropertyValue(value)) {
|
|
32
|
+
issues.push({ path, message: "must not contain CSS-breaking characters (;, }, newlines, or comments)" });
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
26
37
|
function validateRequiredGroup(group, value, keys, issues) {
|
|
27
38
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
28
39
|
issues.push({ path: group, message: "must be an object" });
|
|
@@ -34,7 +45,7 @@ function validateRequiredGroup(group, value, keys, issues) {
|
|
|
34
45
|
const v = obj[key];
|
|
35
46
|
if (!isNonEmptyString(v)) {
|
|
36
47
|
issues.push({ path: `${group}.${key}`, message: "required non-empty string" });
|
|
37
|
-
} else {
|
|
48
|
+
} else if (validateCssTokenValue(`${group}.${key}`, v, issues)) {
|
|
38
49
|
out[key] = v;
|
|
39
50
|
}
|
|
40
51
|
}
|
|
@@ -48,9 +59,16 @@ function validateColorsExtra(value, issues) {
|
|
|
48
59
|
}
|
|
49
60
|
const extra = {};
|
|
50
61
|
for (const [k, v] of Object.entries(value)) {
|
|
62
|
+
if (!EXTRA_COLOR_KEY_PATTERN.test(k)) {
|
|
63
|
+
issues.push({
|
|
64
|
+
path: `colors.extra.${k}`,
|
|
65
|
+
message: "key must match [a-zA-Z][a-zA-Z0-9_-]*"
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
51
69
|
if (!isNonEmptyString(v)) {
|
|
52
70
|
issues.push({ path: `colors.extra.${k}`, message: "must be a non-empty string" });
|
|
53
|
-
} else {
|
|
71
|
+
} else if (validateCssTokenValue(`colors.extra.${k}`, v, issues)) {
|
|
54
72
|
extra[k] = v;
|
|
55
73
|
}
|
|
56
74
|
}
|
|
@@ -156,6 +174,21 @@ function mergeThemes(base, ...overrides) {
|
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
// src/cssVariables.ts
|
|
177
|
+
function sanitizeCssCustomPropertyValue(value) {
|
|
178
|
+
if (/[;}\r\n\\<>]/.test(value) || value.includes("/*")) return null;
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
function isSafeCssSelector(selector) {
|
|
182
|
+
if (selector === ":root") return true;
|
|
183
|
+
if (/^\.[a-zA-Z_][\w-]*$/.test(selector)) return true;
|
|
184
|
+
if (/^#[a-zA-Z_][\w-]*$/.test(selector)) return true;
|
|
185
|
+
if (/^\[data-lk-theme=(["'])[^"']+\1\]$/.test(selector)) return true;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
function assignCssVar(vars, key, value) {
|
|
189
|
+
const safe = sanitizeCssCustomPropertyValue(value);
|
|
190
|
+
if (safe !== null) vars[key] = safe;
|
|
191
|
+
}
|
|
159
192
|
function tokenKeyToKebab(key) {
|
|
160
193
|
return key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
161
194
|
}
|
|
@@ -182,23 +215,23 @@ function themeToCssVariables(theme) {
|
|
|
182
215
|
for (const [key, value] of Object.entries(theme.colors)) {
|
|
183
216
|
if (key === "extra" && value && typeof value === "object") {
|
|
184
217
|
for (const [ek, ev] of Object.entries(value)) {
|
|
185
|
-
vars
|
|
218
|
+
assignCssVar(vars, colorExtraVarName(ek), ev);
|
|
186
219
|
}
|
|
187
220
|
} else if (key !== "extra") {
|
|
188
|
-
vars
|
|
221
|
+
assignCssVar(vars, colorVarName(key), value);
|
|
189
222
|
}
|
|
190
223
|
}
|
|
191
224
|
for (const [key, value] of Object.entries(theme.spacing)) {
|
|
192
|
-
vars
|
|
225
|
+
assignCssVar(vars, spacingVarName(key), value);
|
|
193
226
|
}
|
|
194
227
|
for (const [key, value] of Object.entries(theme.typography)) {
|
|
195
|
-
vars
|
|
228
|
+
assignCssVar(vars, typographyVarName(key), value);
|
|
196
229
|
}
|
|
197
230
|
for (const [key, value] of Object.entries(theme.radius)) {
|
|
198
|
-
vars
|
|
231
|
+
assignCssVar(vars, radiusVarName(key), value);
|
|
199
232
|
}
|
|
200
233
|
for (const [key, value] of Object.entries(theme.shadows)) {
|
|
201
|
-
vars
|
|
234
|
+
assignCssVar(vars, shadowVarName(key), value);
|
|
202
235
|
}
|
|
203
236
|
const sorted = {};
|
|
204
237
|
for (const key of Object.keys(vars).sort()) {
|
|
@@ -208,6 +241,9 @@ function themeToCssVariables(theme) {
|
|
|
208
241
|
}
|
|
209
242
|
function themeToCssDeclarationBlock(theme, opts) {
|
|
210
243
|
const selector = opts?.selector ?? ":root";
|
|
244
|
+
if (!isSafeCssSelector(selector)) {
|
|
245
|
+
throw new Error(`[lessonkit] unsafe CSS selector for theme block: ${selector}`);
|
|
246
|
+
}
|
|
211
247
|
const vars = themeToCssVariables(theme);
|
|
212
248
|
const body = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`).join("\n");
|
|
213
249
|
return `${selector} {
|
|
@@ -325,8 +361,11 @@ var PRESETS = {
|
|
|
325
361
|
dark: darkTheme,
|
|
326
362
|
brand: brandTheme
|
|
327
363
|
};
|
|
364
|
+
function cloneTheme(theme) {
|
|
365
|
+
return JSON.parse(JSON.stringify(theme));
|
|
366
|
+
}
|
|
328
367
|
function getPresetTheme(preset) {
|
|
329
|
-
return PRESETS[preset];
|
|
368
|
+
return cloneTheme(PRESETS[preset]);
|
|
330
369
|
}
|
|
331
370
|
|
|
332
371
|
// src/catalog.ts
|
|
@@ -443,6 +482,7 @@ export {
|
|
|
443
482
|
lightTheme,
|
|
444
483
|
mergeThemes,
|
|
445
484
|
radiusVarName,
|
|
485
|
+
sanitizeCssCustomPropertyValue,
|
|
446
486
|
shadowVarName,
|
|
447
487
|
spacingVarName,
|
|
448
488
|
themeToCssDeclarationBlock,
|