@fragments-sdk/cli 0.3.2 → 0.4.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/dist/bin.js +18 -13
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-MUZ6CM66.js → chunk-5JNME72P.js} +3 -3
- package/dist/{chunk-MUZ6CM66.js.map → chunk-5JNME72P.js.map} +1 -1
- package/dist/{chunk-XHNKNI6J.js → chunk-AW7MWOUH.js} +9 -1
- package/dist/chunk-AW7MWOUH.js.map +1 -0
- package/dist/{chunk-LY2CFFPY.js → chunk-FYIYMXGA.js} +2 -2
- package/dist/{chunk-3OTEW66K.js → chunk-LDKNZ55O.js} +4 -4
- package/dist/{chunk-BSCG3IP7.js → chunk-NOTYONHY.js} +2 -2
- package/dist/{chunk-ACIDZOYW.js → chunk-ODXQAQQX.js} +21 -8
- package/dist/chunk-ODXQAQQX.js.map +1 -0
- package/dist/{chunk-PMGI7ATF.js → chunk-OZQ7Z6C3.js} +31 -2
- package/dist/chunk-OZQ7Z6C3.js.map +1 -0
- package/dist/{core-DWKLGY4N.js → core-F3VT277E.js} +5 -3
- package/dist/{generate-3LBZANQ3.js → generate-PNIUR75D.js} +4 -4
- package/dist/index.d.ts +18 -0
- package/dist/index.js +6 -6
- package/dist/{init-NKIUCYTG.js → init-ON6WYG66.js} +4 -4
- package/dist/mcp-bin.js +8 -3
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-E6U644RS.js +12 -0
- package/dist/{service-QSZMZJBJ.js → service-U7AR2PC2.js} +4 -4
- package/dist/{static-viewer-MIPGZ4Z7.js → static-viewer-QL2SCWYB.js} +4 -4
- package/dist/{test-ZCTR4LBB.js → test-PBPKJ4WJ.js} +3 -3
- package/dist/{tokens-5JQ5IOR2.js → tokens-4J4PRIGT.js} +5 -5
- package/dist/{viewer-D7QC4GM2.js → viewer-6VCZMA3T.js} +13 -13
- package/package.json +1 -1
- package/src/bin.ts +7 -1
- package/src/build.ts +16 -0
- package/src/core/index.ts +4 -0
- package/src/core/parser.ts +54 -1
- package/src/core/schema.ts +11 -0
- package/src/core/types.ts +27 -0
- package/src/mcp/server.ts +11 -1
- package/src/migrate/bin.ts +7 -1
- package/src/migrate/report.ts +1 -1
- package/src/service/report.ts +1 -1
- package/src/theme/__tests__/generator.test.ts +412 -0
- package/src/theme/__tests__/presets.test.ts +169 -0
- package/src/theme/__tests__/schema.test.ts +463 -0
- package/src/theme/__tests__/serializer.test.ts +326 -0
- package/src/theme/generator.ts +355 -0
- package/src/theme/index.ts +61 -0
- package/src/theme/presets.ts +189 -0
- package/src/theme/schema.ts +193 -0
- package/src/theme/serializer.ts +123 -0
- package/src/theme/types.ts +210 -0
- package/src/viewer/styles/globals.css +1 -1
- package/dist/chunk-ACIDZOYW.js.map +0 -1
- package/dist/chunk-PMGI7ATF.js.map +0 -1
- package/dist/chunk-XHNKNI6J.js.map +0 -1
- package/dist/scan-3ZAOVO4U.js +0 -12
- /package/dist/{chunk-LY2CFFPY.js.map → chunk-FYIYMXGA.js.map} +0 -0
- /package/dist/{chunk-3OTEW66K.js.map → chunk-LDKNZ55O.js.map} +0 -0
- /package/dist/{chunk-BSCG3IP7.js.map → chunk-NOTYONHY.js.map} +0 -0
- /package/dist/{core-DWKLGY4N.js.map → core-F3VT277E.js.map} +0 -0
- /package/dist/{generate-3LBZANQ3.js.map → generate-PNIUR75D.js.map} +0 -0
- /package/dist/{init-NKIUCYTG.js.map → init-ON6WYG66.js.map} +0 -0
- /package/dist/{scan-3ZAOVO4U.js.map → scan-E6U644RS.js.map} +0 -0
- /package/dist/{service-QSZMZJBJ.js.map → service-U7AR2PC2.js.map} +0 -0
- /package/dist/{static-viewer-MIPGZ4Z7.js.map → static-viewer-QL2SCWYB.js.map} +0 -0
- /package/dist/{test-ZCTR4LBB.js.map → test-PBPKJ4WJ.js.map} +0 -0
- /package/dist/{tokens-5JQ5IOR2.js.map → tokens-4J4PRIGT.js.map} +0 -0
- /package/dist/{viewer-D7QC4GM2.js.map → viewer-6VCZMA3T.js.map} +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Customization Module
|
|
3
|
+
*
|
|
4
|
+
* Provides theme configuration, validation, serialization, and generation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type {
|
|
9
|
+
ThemeConfig,
|
|
10
|
+
ThemeColors,
|
|
11
|
+
ThemeSurfaces,
|
|
12
|
+
ThemeText,
|
|
13
|
+
ThemeBorders,
|
|
14
|
+
ThemeTypography,
|
|
15
|
+
ThemeRadius,
|
|
16
|
+
ThemeShadows,
|
|
17
|
+
ThemeDarkMode,
|
|
18
|
+
TokenOutputFormat,
|
|
19
|
+
TokenGeneratorOptions,
|
|
20
|
+
TokenGeneratorResult,
|
|
21
|
+
PresetName,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
|
|
24
|
+
// Schema validation
|
|
25
|
+
export {
|
|
26
|
+
themeConfigSchema,
|
|
27
|
+
themeColorsSchema,
|
|
28
|
+
themeSurfacesSchema,
|
|
29
|
+
themeTextSchema,
|
|
30
|
+
themeBordersSchema,
|
|
31
|
+
themeTypographySchema,
|
|
32
|
+
themeRadiusSchema,
|
|
33
|
+
themeShadowsSchema,
|
|
34
|
+
themeDarkModeSchema,
|
|
35
|
+
validateThemeConfig,
|
|
36
|
+
} from "./schema.js";
|
|
37
|
+
export type { ThemeValidationResult } from "./schema.js";
|
|
38
|
+
|
|
39
|
+
// Serialization
|
|
40
|
+
export {
|
|
41
|
+
encodeThemeToUrl,
|
|
42
|
+
decodeThemeFromUrl,
|
|
43
|
+
compressTheme,
|
|
44
|
+
decompressTheme,
|
|
45
|
+
} from "./serializer.js";
|
|
46
|
+
|
|
47
|
+
// Generation
|
|
48
|
+
export {
|
|
49
|
+
generateScssTokens,
|
|
50
|
+
generateCssTokens,
|
|
51
|
+
generateTokenFiles,
|
|
52
|
+
} from "./generator.js";
|
|
53
|
+
|
|
54
|
+
// Presets
|
|
55
|
+
export {
|
|
56
|
+
DEFAULT_PRESET,
|
|
57
|
+
PRESETS,
|
|
58
|
+
getPreset,
|
|
59
|
+
listPresets,
|
|
60
|
+
isValidPresetName,
|
|
61
|
+
} from "./presets.js";
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Presets
|
|
3
|
+
*
|
|
4
|
+
* Built-in theme presets including the default theme
|
|
5
|
+
* Values extracted from libs/ui/src/tokens/_variables.scss
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ThemeConfig, PresetName } from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default preset - matches libs/ui/src/tokens/_variables.scss exactly
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_PRESET: ThemeConfig = {
|
|
14
|
+
name: "Default",
|
|
15
|
+
colors: {
|
|
16
|
+
accent: "#6366f1",
|
|
17
|
+
accentHover: "#4f46e5",
|
|
18
|
+
accentActive: "#4338ca",
|
|
19
|
+
danger: "#ef4444",
|
|
20
|
+
dangerHover: "#dc2626",
|
|
21
|
+
success: "#22c55e",
|
|
22
|
+
warning: "#f59e0b",
|
|
23
|
+
info: "#3b82f6",
|
|
24
|
+
dangerBg: "rgba(239, 68, 68, 0.1)",
|
|
25
|
+
successBg: "rgba(34, 197, 94, 0.1)",
|
|
26
|
+
warningBg: "rgba(245, 158, 11, 0.1)",
|
|
27
|
+
infoBg: "rgba(59, 130, 246, 0.1)",
|
|
28
|
+
},
|
|
29
|
+
surfaces: {
|
|
30
|
+
bgPrimary: "#ffffff",
|
|
31
|
+
bgSecondary: "#f8fafc",
|
|
32
|
+
bgTertiary: "#f1f5f9",
|
|
33
|
+
bgElevated: "#ffffff",
|
|
34
|
+
bgHover: "rgba(0, 0, 0, 0.04)",
|
|
35
|
+
bgActive: "rgba(0, 0, 0, 0.06)",
|
|
36
|
+
},
|
|
37
|
+
text: {
|
|
38
|
+
primary: "#0f172a",
|
|
39
|
+
secondary: "#64748b",
|
|
40
|
+
tertiary: "#94a3b8",
|
|
41
|
+
inverse: "#ffffff",
|
|
42
|
+
},
|
|
43
|
+
borders: {
|
|
44
|
+
default: "#e2e8f0",
|
|
45
|
+
strong: "#cbd5e1",
|
|
46
|
+
},
|
|
47
|
+
typography: {
|
|
48
|
+
fontSans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
49
|
+
fontMono: "'SF Mono', SFMono-Regular, ui-monospace, Menlo, monospace",
|
|
50
|
+
fontWeightNormal: 400,
|
|
51
|
+
fontWeightMedium: 500,
|
|
52
|
+
fontWeightSemibold: 600,
|
|
53
|
+
},
|
|
54
|
+
radius: {
|
|
55
|
+
sm: "0.25rem",
|
|
56
|
+
md: "0.375rem",
|
|
57
|
+
lg: "0.5rem",
|
|
58
|
+
full: "9999px",
|
|
59
|
+
},
|
|
60
|
+
shadows: {
|
|
61
|
+
sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
|
62
|
+
md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
|
|
63
|
+
},
|
|
64
|
+
dark: {
|
|
65
|
+
surfaces: {
|
|
66
|
+
bgPrimary: "#0f172a",
|
|
67
|
+
bgSecondary: "#1e293b",
|
|
68
|
+
bgTertiary: "#334155",
|
|
69
|
+
bgElevated: "#1e293b",
|
|
70
|
+
bgHover: "rgba(255, 255, 255, 0.06)",
|
|
71
|
+
bgActive: "rgba(255, 255, 255, 0.08)",
|
|
72
|
+
},
|
|
73
|
+
text: {
|
|
74
|
+
primary: "#f8fafc",
|
|
75
|
+
secondary: "#94a3b8",
|
|
76
|
+
tertiary: "#64748b",
|
|
77
|
+
inverse: "#0f172a",
|
|
78
|
+
},
|
|
79
|
+
borders: {
|
|
80
|
+
default: "#334155",
|
|
81
|
+
strong: "#475569",
|
|
82
|
+
},
|
|
83
|
+
shadows: {
|
|
84
|
+
sm: "0 1px 2px 0 rgba(0, 0, 0, 0.3)",
|
|
85
|
+
md: "0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3)",
|
|
86
|
+
},
|
|
87
|
+
dangerBg: "rgba(239, 68, 68, 0.15)",
|
|
88
|
+
successBg: "rgba(34, 197, 94, 0.15)",
|
|
89
|
+
warningBg: "rgba(245, 158, 11, 0.15)",
|
|
90
|
+
infoBg: "rgba(59, 130, 246, 0.15)",
|
|
91
|
+
backdrop: "rgba(0, 0, 0, 0.7)",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Neutral preset - gray-based accent colors (Zinc)
|
|
97
|
+
*/
|
|
98
|
+
const NEUTRAL_PRESET: ThemeConfig = {
|
|
99
|
+
name: "Neutral",
|
|
100
|
+
extends: "default",
|
|
101
|
+
colors: {
|
|
102
|
+
accent: "#71717a", // zinc-500
|
|
103
|
+
accentHover: "#52525b", // zinc-600
|
|
104
|
+
accentActive: "#3f3f46", // zinc-700
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Slate preset - blue-gray based colors
|
|
110
|
+
*/
|
|
111
|
+
const SLATE_PRESET: ThemeConfig = {
|
|
112
|
+
name: "Slate",
|
|
113
|
+
extends: "default",
|
|
114
|
+
colors: {
|
|
115
|
+
accent: "#64748b", // slate-500
|
|
116
|
+
accentHover: "#475569", // slate-600
|
|
117
|
+
accentActive: "#334155", // slate-700
|
|
118
|
+
},
|
|
119
|
+
surfaces: {
|
|
120
|
+
bgSecondary: "#f1f5f9", // slate-100
|
|
121
|
+
bgTertiary: "#e2e8f0", // slate-200
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Emerald preset - green accent colors
|
|
127
|
+
*/
|
|
128
|
+
const EMERALD_PRESET: ThemeConfig = {
|
|
129
|
+
name: "Emerald",
|
|
130
|
+
extends: "default",
|
|
131
|
+
colors: {
|
|
132
|
+
accent: "#10b981", // emerald-500
|
|
133
|
+
accentHover: "#059669", // emerald-600
|
|
134
|
+
accentActive: "#047857", // emerald-700
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Rose preset - pink accent colors
|
|
140
|
+
*/
|
|
141
|
+
const ROSE_PRESET: ThemeConfig = {
|
|
142
|
+
name: "Rose",
|
|
143
|
+
extends: "default",
|
|
144
|
+
colors: {
|
|
145
|
+
accent: "#f43f5e", // rose-500
|
|
146
|
+
accentHover: "#e11d48", // rose-600
|
|
147
|
+
accentActive: "#be123c", // rose-700
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* All built-in presets
|
|
153
|
+
*/
|
|
154
|
+
export const PRESETS: Record<string, ThemeConfig> = {
|
|
155
|
+
default: DEFAULT_PRESET,
|
|
156
|
+
neutral: NEUTRAL_PRESET,
|
|
157
|
+
slate: SLATE_PRESET,
|
|
158
|
+
emerald: EMERALD_PRESET,
|
|
159
|
+
rose: ROSE_PRESET,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get a preset by name
|
|
164
|
+
*
|
|
165
|
+
* @param name - Preset name
|
|
166
|
+
* @returns Theme configuration or null if not found
|
|
167
|
+
*/
|
|
168
|
+
export function getPreset(name: string): ThemeConfig | null {
|
|
169
|
+
return PRESETS[name] ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* List all available preset names
|
|
174
|
+
*
|
|
175
|
+
* @returns Array of preset names
|
|
176
|
+
*/
|
|
177
|
+
export function listPresets(): string[] {
|
|
178
|
+
return Object.keys(PRESETS);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if a string is a valid preset name
|
|
183
|
+
*
|
|
184
|
+
* @param name - Name to check
|
|
185
|
+
* @returns true if valid preset name
|
|
186
|
+
*/
|
|
187
|
+
export function isValidPresetName(name: string): name is PresetName {
|
|
188
|
+
return name in PRESETS;
|
|
189
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Configuration Zod Schemas
|
|
3
|
+
*
|
|
4
|
+
* Provides validation for theme configuration objects
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { ThemeConfig } from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Regex patterns for color validation
|
|
12
|
+
*/
|
|
13
|
+
const HEX_COLOR_REGEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
14
|
+
const RGB_COLOR_REGEX = /^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(,\s*(0|1|0?\.\d+))?\s*\)$/;
|
|
15
|
+
const HSL_COLOR_REGEX = /^hsla?\(\s*\d{1,3}\s*,\s*\d{1,3}%\s*,\s*\d{1,3}%\s*(,\s*(0|1|0?\.\d+))?\s*\)$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* CSS color string validator
|
|
19
|
+
*/
|
|
20
|
+
const colorSchema = z.string().refine(
|
|
21
|
+
(val) => {
|
|
22
|
+
return (
|
|
23
|
+
HEX_COLOR_REGEX.test(val) ||
|
|
24
|
+
RGB_COLOR_REGEX.test(val) ||
|
|
25
|
+
HSL_COLOR_REGEX.test(val)
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
{ message: "Invalid color format. Use hex (#rrggbb), rgb(), rgba(), hsl(), or hsla()" }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* CSS size value validator (px, rem, em, etc.)
|
|
33
|
+
*/
|
|
34
|
+
const sizeSchema = z.string().refine(
|
|
35
|
+
(val) => {
|
|
36
|
+
// Match number followed by unit, or just a number (for unitless values like 9999px)
|
|
37
|
+
return /^-?\d*\.?\d+(px|rem|em|%|vw|vh)?$/.test(val);
|
|
38
|
+
},
|
|
39
|
+
{ message: "Invalid size format. Use px, rem, em, %, vw, or vh" }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Shadow value validator (more permissive for complex shadow syntax)
|
|
44
|
+
*/
|
|
45
|
+
const shadowSchema = z.string();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Font stack validator
|
|
49
|
+
*/
|
|
50
|
+
const fontStackSchema = z.string();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Font weight validator (100-900 in increments of 100)
|
|
54
|
+
*/
|
|
55
|
+
const fontWeightSchema = z.number().int().min(100).max(900);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Theme colors schema
|
|
59
|
+
*/
|
|
60
|
+
export const themeColorsSchema = z.object({
|
|
61
|
+
accent: colorSchema.optional(),
|
|
62
|
+
accentHover: colorSchema.optional(),
|
|
63
|
+
accentActive: colorSchema.optional(),
|
|
64
|
+
danger: colorSchema.optional(),
|
|
65
|
+
dangerHover: colorSchema.optional(),
|
|
66
|
+
success: colorSchema.optional(),
|
|
67
|
+
warning: colorSchema.optional(),
|
|
68
|
+
info: colorSchema.optional(),
|
|
69
|
+
dangerBg: colorSchema.optional(),
|
|
70
|
+
successBg: colorSchema.optional(),
|
|
71
|
+
warningBg: colorSchema.optional(),
|
|
72
|
+
infoBg: colorSchema.optional(),
|
|
73
|
+
}).strict();
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Theme surfaces schema
|
|
77
|
+
*/
|
|
78
|
+
export const themeSurfacesSchema = z.object({
|
|
79
|
+
bgPrimary: colorSchema.optional(),
|
|
80
|
+
bgSecondary: colorSchema.optional(),
|
|
81
|
+
bgTertiary: colorSchema.optional(),
|
|
82
|
+
bgElevated: colorSchema.optional(),
|
|
83
|
+
bgHover: colorSchema.optional(),
|
|
84
|
+
bgActive: colorSchema.optional(),
|
|
85
|
+
}).strict();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Theme text schema
|
|
89
|
+
*/
|
|
90
|
+
export const themeTextSchema = z.object({
|
|
91
|
+
primary: colorSchema.optional(),
|
|
92
|
+
secondary: colorSchema.optional(),
|
|
93
|
+
tertiary: colorSchema.optional(),
|
|
94
|
+
inverse: colorSchema.optional(),
|
|
95
|
+
}).strict();
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Theme borders schema
|
|
99
|
+
*/
|
|
100
|
+
export const themeBordersSchema = z.object({
|
|
101
|
+
default: colorSchema.optional(),
|
|
102
|
+
strong: colorSchema.optional(),
|
|
103
|
+
}).strict();
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Theme typography schema
|
|
107
|
+
*/
|
|
108
|
+
export const themeTypographySchema = z.object({
|
|
109
|
+
fontSans: fontStackSchema.optional(),
|
|
110
|
+
fontMono: fontStackSchema.optional(),
|
|
111
|
+
fontWeightNormal: fontWeightSchema.optional(),
|
|
112
|
+
fontWeightMedium: fontWeightSchema.optional(),
|
|
113
|
+
fontWeightSemibold: fontWeightSchema.optional(),
|
|
114
|
+
}).strict();
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Theme radius schema
|
|
118
|
+
*/
|
|
119
|
+
export const themeRadiusSchema = z.object({
|
|
120
|
+
sm: sizeSchema.optional(),
|
|
121
|
+
md: sizeSchema.optional(),
|
|
122
|
+
lg: sizeSchema.optional(),
|
|
123
|
+
full: sizeSchema.optional(),
|
|
124
|
+
}).strict();
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Theme shadows schema
|
|
128
|
+
*/
|
|
129
|
+
export const themeShadowsSchema = z.object({
|
|
130
|
+
sm: shadowSchema.optional(),
|
|
131
|
+
md: shadowSchema.optional(),
|
|
132
|
+
}).strict();
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Theme dark mode schema
|
|
136
|
+
*/
|
|
137
|
+
export const themeDarkModeSchema = z.object({
|
|
138
|
+
surfaces: themeSurfacesSchema.optional(),
|
|
139
|
+
text: themeTextSchema.optional(),
|
|
140
|
+
borders: themeBordersSchema.optional(),
|
|
141
|
+
shadows: themeShadowsSchema.optional(),
|
|
142
|
+
dangerBg: colorSchema.optional(),
|
|
143
|
+
successBg: colorSchema.optional(),
|
|
144
|
+
warningBg: colorSchema.optional(),
|
|
145
|
+
infoBg: colorSchema.optional(),
|
|
146
|
+
backdrop: colorSchema.optional(),
|
|
147
|
+
}).strict();
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Complete theme configuration schema
|
|
151
|
+
*/
|
|
152
|
+
export const themeConfigSchema = z.object({
|
|
153
|
+
name: z.string().min(1, "Theme name is required"),
|
|
154
|
+
version: z.string().optional(),
|
|
155
|
+
extends: z.string().optional(),
|
|
156
|
+
colors: themeColorsSchema.optional(),
|
|
157
|
+
surfaces: themeSurfacesSchema.optional(),
|
|
158
|
+
text: themeTextSchema.optional(),
|
|
159
|
+
borders: themeBordersSchema.optional(),
|
|
160
|
+
typography: themeTypographySchema.optional(),
|
|
161
|
+
radius: themeRadiusSchema.optional(),
|
|
162
|
+
shadows: themeShadowsSchema.optional(),
|
|
163
|
+
dark: themeDarkModeSchema.optional(),
|
|
164
|
+
}).strict();
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Type for the inferred schema
|
|
168
|
+
*/
|
|
169
|
+
export type ThemeConfigInput = z.input<typeof themeConfigSchema>;
|
|
170
|
+
export type ThemeConfigOutput = z.output<typeof themeConfigSchema>;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Validation result type
|
|
174
|
+
*/
|
|
175
|
+
export type ThemeValidationResult =
|
|
176
|
+
| { success: true; data: ThemeConfig }
|
|
177
|
+
| { success: false; error: z.ZodError };
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Validate a theme configuration object
|
|
181
|
+
*
|
|
182
|
+
* @param config - The theme configuration to validate
|
|
183
|
+
* @returns Validation result with either the parsed data or error details
|
|
184
|
+
*/
|
|
185
|
+
export function validateThemeConfig(config: unknown): ThemeValidationResult {
|
|
186
|
+
const result = themeConfigSchema.safeParse(config);
|
|
187
|
+
|
|
188
|
+
if (result.success) {
|
|
189
|
+
return { success: true, data: result.data as ThemeConfig };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { success: false, error: result.error };
|
|
193
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Serialization
|
|
3
|
+
*
|
|
4
|
+
* Encodes/decodes theme configurations to/from URL-safe strings
|
|
5
|
+
* using zlib compression and base64url encoding.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { deflateSync, inflateSync } from "node:zlib";
|
|
9
|
+
import { validateThemeConfig } from "./schema.js";
|
|
10
|
+
import type { ThemeConfig } from "./types.js";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_BASE_URL = "https://fragments.dev/init";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a Buffer to base64url encoding (URL-safe base64)
|
|
16
|
+
*/
|
|
17
|
+
function toBase64Url(buffer: Buffer): string {
|
|
18
|
+
return buffer
|
|
19
|
+
.toString("base64")
|
|
20
|
+
.replace(/\+/g, "-")
|
|
21
|
+
.replace(/\//g, "_")
|
|
22
|
+
.replace(/=/g, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert a base64url string to Buffer
|
|
27
|
+
*/
|
|
28
|
+
function fromBase64Url(str: string): Buffer {
|
|
29
|
+
// Restore standard base64 characters
|
|
30
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
31
|
+
|
|
32
|
+
// Add padding if needed
|
|
33
|
+
const pad = base64.length % 4;
|
|
34
|
+
if (pad) {
|
|
35
|
+
base64 += "=".repeat(4 - pad);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return Buffer.from(base64, "base64");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compress a theme configuration to a URL-safe string
|
|
43
|
+
*
|
|
44
|
+
* Process: JSON → deflate → base64url
|
|
45
|
+
*
|
|
46
|
+
* @param config - Theme configuration to compress
|
|
47
|
+
* @returns URL-safe encoded string
|
|
48
|
+
*/
|
|
49
|
+
export function compressTheme(config: ThemeConfig): string {
|
|
50
|
+
const json = JSON.stringify(config);
|
|
51
|
+
const compressed = deflateSync(Buffer.from(json, "utf-8"), { level: 9 });
|
|
52
|
+
return toBase64Url(compressed);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Decompress a URL-safe string back to theme configuration
|
|
57
|
+
*
|
|
58
|
+
* Process: base64url → inflate → JSON
|
|
59
|
+
*
|
|
60
|
+
* @param encoded - URL-safe encoded string
|
|
61
|
+
* @returns Theme configuration or null if invalid
|
|
62
|
+
*/
|
|
63
|
+
export function decompressTheme(encoded: string): ThemeConfig | null {
|
|
64
|
+
try {
|
|
65
|
+
const buffer = fromBase64Url(encoded);
|
|
66
|
+
const decompressed = inflateSync(buffer);
|
|
67
|
+
const json = decompressed.toString("utf-8");
|
|
68
|
+
const parsed = JSON.parse(json);
|
|
69
|
+
|
|
70
|
+
// Validate the parsed object
|
|
71
|
+
const result = validateThemeConfig(parsed);
|
|
72
|
+
if (result.success) {
|
|
73
|
+
return result.data;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Encode a theme configuration to a full URL with ?preset= parameter
|
|
84
|
+
*
|
|
85
|
+
* @param config - Theme configuration to encode
|
|
86
|
+
* @param baseUrl - Base URL (default: https://fragments.dev/init)
|
|
87
|
+
* @returns Full URL with encoded theme as preset parameter
|
|
88
|
+
*/
|
|
89
|
+
export function encodeThemeToUrl(
|
|
90
|
+
config: ThemeConfig,
|
|
91
|
+
baseUrl: string = DEFAULT_BASE_URL
|
|
92
|
+
): string {
|
|
93
|
+
const encoded = compressTheme(config);
|
|
94
|
+
const url = new URL(baseUrl);
|
|
95
|
+
url.searchParams.set("preset", encoded);
|
|
96
|
+
return url.toString();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Decode a theme from a URL or raw encoded string
|
|
101
|
+
*
|
|
102
|
+
* Handles:
|
|
103
|
+
* - Full URLs with ?preset= parameter
|
|
104
|
+
* - Raw encoded strings (compressed theme data)
|
|
105
|
+
*
|
|
106
|
+
* @param input - URL or encoded string
|
|
107
|
+
* @returns Theme configuration or null if invalid
|
|
108
|
+
*/
|
|
109
|
+
export function decodeThemeFromUrl(input: string): ThemeConfig | null {
|
|
110
|
+
// Try to parse as URL first
|
|
111
|
+
try {
|
|
112
|
+
const url = new URL(input);
|
|
113
|
+
const preset = url.searchParams.get("preset");
|
|
114
|
+
if (preset) {
|
|
115
|
+
return decompressTheme(preset);
|
|
116
|
+
}
|
|
117
|
+
// URL parsed but no preset param
|
|
118
|
+
return null;
|
|
119
|
+
} catch {
|
|
120
|
+
// Not a valid URL, try as raw encoded string
|
|
121
|
+
return decompressTheme(input);
|
|
122
|
+
}
|
|
123
|
+
}
|