@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.
Files changed (64) hide show
  1. package/dist/bin.js +18 -13
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-MUZ6CM66.js → chunk-5JNME72P.js} +3 -3
  4. package/dist/{chunk-MUZ6CM66.js.map → chunk-5JNME72P.js.map} +1 -1
  5. package/dist/{chunk-XHNKNI6J.js → chunk-AW7MWOUH.js} +9 -1
  6. package/dist/chunk-AW7MWOUH.js.map +1 -0
  7. package/dist/{chunk-LY2CFFPY.js → chunk-FYIYMXGA.js} +2 -2
  8. package/dist/{chunk-3OTEW66K.js → chunk-LDKNZ55O.js} +4 -4
  9. package/dist/{chunk-BSCG3IP7.js → chunk-NOTYONHY.js} +2 -2
  10. package/dist/{chunk-ACIDZOYW.js → chunk-ODXQAQQX.js} +21 -8
  11. package/dist/chunk-ODXQAQQX.js.map +1 -0
  12. package/dist/{chunk-PMGI7ATF.js → chunk-OZQ7Z6C3.js} +31 -2
  13. package/dist/chunk-OZQ7Z6C3.js.map +1 -0
  14. package/dist/{core-DWKLGY4N.js → core-F3VT277E.js} +5 -3
  15. package/dist/{generate-3LBZANQ3.js → generate-PNIUR75D.js} +4 -4
  16. package/dist/index.d.ts +18 -0
  17. package/dist/index.js +6 -6
  18. package/dist/{init-NKIUCYTG.js → init-ON6WYG66.js} +4 -4
  19. package/dist/mcp-bin.js +8 -3
  20. package/dist/mcp-bin.js.map +1 -1
  21. package/dist/scan-E6U644RS.js +12 -0
  22. package/dist/{service-QSZMZJBJ.js → service-U7AR2PC2.js} +4 -4
  23. package/dist/{static-viewer-MIPGZ4Z7.js → static-viewer-QL2SCWYB.js} +4 -4
  24. package/dist/{test-ZCTR4LBB.js → test-PBPKJ4WJ.js} +3 -3
  25. package/dist/{tokens-5JQ5IOR2.js → tokens-4J4PRIGT.js} +5 -5
  26. package/dist/{viewer-D7QC4GM2.js → viewer-6VCZMA3T.js} +13 -13
  27. package/package.json +1 -1
  28. package/src/bin.ts +7 -1
  29. package/src/build.ts +16 -0
  30. package/src/core/index.ts +4 -0
  31. package/src/core/parser.ts +54 -1
  32. package/src/core/schema.ts +11 -0
  33. package/src/core/types.ts +27 -0
  34. package/src/mcp/server.ts +11 -1
  35. package/src/migrate/bin.ts +7 -1
  36. package/src/migrate/report.ts +1 -1
  37. package/src/service/report.ts +1 -1
  38. package/src/theme/__tests__/generator.test.ts +412 -0
  39. package/src/theme/__tests__/presets.test.ts +169 -0
  40. package/src/theme/__tests__/schema.test.ts +463 -0
  41. package/src/theme/__tests__/serializer.test.ts +326 -0
  42. package/src/theme/generator.ts +355 -0
  43. package/src/theme/index.ts +61 -0
  44. package/src/theme/presets.ts +189 -0
  45. package/src/theme/schema.ts +193 -0
  46. package/src/theme/serializer.ts +123 -0
  47. package/src/theme/types.ts +210 -0
  48. package/src/viewer/styles/globals.css +1 -1
  49. package/dist/chunk-ACIDZOYW.js.map +0 -1
  50. package/dist/chunk-PMGI7ATF.js.map +0 -1
  51. package/dist/chunk-XHNKNI6J.js.map +0 -1
  52. package/dist/scan-3ZAOVO4U.js +0 -12
  53. /package/dist/{chunk-LY2CFFPY.js.map → chunk-FYIYMXGA.js.map} +0 -0
  54. /package/dist/{chunk-3OTEW66K.js.map → chunk-LDKNZ55O.js.map} +0 -0
  55. /package/dist/{chunk-BSCG3IP7.js.map → chunk-NOTYONHY.js.map} +0 -0
  56. /package/dist/{core-DWKLGY4N.js.map → core-F3VT277E.js.map} +0 -0
  57. /package/dist/{generate-3LBZANQ3.js.map → generate-PNIUR75D.js.map} +0 -0
  58. /package/dist/{init-NKIUCYTG.js.map → init-ON6WYG66.js.map} +0 -0
  59. /package/dist/{scan-3ZAOVO4U.js.map → scan-E6U644RS.js.map} +0 -0
  60. /package/dist/{service-QSZMZJBJ.js.map → service-U7AR2PC2.js.map} +0 -0
  61. /package/dist/{static-viewer-MIPGZ4Z7.js.map → static-viewer-QL2SCWYB.js.map} +0 -0
  62. /package/dist/{test-ZCTR4LBB.js.map → test-PBPKJ4WJ.js.map} +0 -0
  63. /package/dist/{tokens-5JQ5IOR2.js.map → tokens-4J4PRIGT.js.map} +0 -0
  64. /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
+ }