@deneb-ui/cli 2.0.33 → 2.0.35
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/package.json +2 -2
- package/src/common/fivora-site-data.ts +339 -0
- package/src/common/media.constants.ts +92 -0
- package/src/common/template-editor-schema.ts +2280 -0
- package/src/common/template-package-archive.ts +176 -0
- package/src/common/template-package-manifest.ts +272 -0
- package/src/common/template-visual-edit-contract.ts +2277 -0
- package/src/common/visual-customization.ts +679 -0
- package/src/sites/universal-page-selection.ts +520 -0
- package/src/tools/deneb-template-validator.cjs +23 -17
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
export const VISUAL_CUSTOMIZATION_VERSION = 1 as const;
|
|
2
|
+
export const PREMIUM_TEMPLATE_TIER = 'PREMIUM';
|
|
3
|
+
export const CUSTOM_TEMPLATE_TIER = 'CUSTOM';
|
|
4
|
+
|
|
5
|
+
export type VisualCustomizationColors = {
|
|
6
|
+
primary?: string;
|
|
7
|
+
secondary?: string;
|
|
8
|
+
accent?: string;
|
|
9
|
+
background?: string;
|
|
10
|
+
text?: string;
|
|
11
|
+
surface?: string;
|
|
12
|
+
surfaceAlt?: string;
|
|
13
|
+
heading?: string;
|
|
14
|
+
mutedText?: string;
|
|
15
|
+
border?: string;
|
|
16
|
+
headerBackground?: string;
|
|
17
|
+
footerBackground?: string;
|
|
18
|
+
cardBackground?: string;
|
|
19
|
+
buttonBackground?: string;
|
|
20
|
+
buttonText?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type VisualCustomizationTypography = {
|
|
24
|
+
headingFont?: string;
|
|
25
|
+
bodyFont?: string;
|
|
26
|
+
baseSize?: string;
|
|
27
|
+
headingScale?: string;
|
|
28
|
+
bodyLineHeight?: string;
|
|
29
|
+
headingLineHeight?: string;
|
|
30
|
+
headingWeight?: string;
|
|
31
|
+
bodyWeight?: string;
|
|
32
|
+
letterSpacing?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type VisualCustomizationSpacing = {
|
|
36
|
+
heroMinHeight?: string;
|
|
37
|
+
sectionPadding?: string;
|
|
38
|
+
contentMaxWidth?: string;
|
|
39
|
+
containerPadding?: string;
|
|
40
|
+
sectionGap?: string;
|
|
41
|
+
elementGap?: string;
|
|
42
|
+
gridGap?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type VisualCustomizationLayout = {
|
|
46
|
+
textAlign?: string;
|
|
47
|
+
contentAlign?: string;
|
|
48
|
+
heroTextAlign?: string;
|
|
49
|
+
cardTextAlign?: string;
|
|
50
|
+
gridColumns?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type VisualCustomizationComponents = {
|
|
54
|
+
cardWidth?: string;
|
|
55
|
+
cardMinHeight?: string;
|
|
56
|
+
cardPadding?: string;
|
|
57
|
+
cardRadius?: string;
|
|
58
|
+
cardBorderWidth?: string;
|
|
59
|
+
cardShadow?: string;
|
|
60
|
+
buttonPadding?: string;
|
|
61
|
+
buttonRadius?: string;
|
|
62
|
+
buttonShadow?: string;
|
|
63
|
+
imageRadius?: string;
|
|
64
|
+
headerHeight?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type VisualCustomizationSectionOverride = {
|
|
68
|
+
backgroundColor?: string;
|
|
69
|
+
textColor?: string;
|
|
70
|
+
headingColor?: string;
|
|
71
|
+
minHeight?: string;
|
|
72
|
+
padding?: string;
|
|
73
|
+
contentMaxWidth?: string;
|
|
74
|
+
gap?: string;
|
|
75
|
+
textAlign?: string;
|
|
76
|
+
contentAlign?: string;
|
|
77
|
+
cardBackgroundColor?: string;
|
|
78
|
+
cardWidth?: string;
|
|
79
|
+
cardMinHeight?: string;
|
|
80
|
+
cardRadius?: string;
|
|
81
|
+
gridColumns?: string;
|
|
82
|
+
visible?: boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type VisualCustomizationElementStyle = {
|
|
86
|
+
fontFamily?: string;
|
|
87
|
+
fontSize?: string;
|
|
88
|
+
lineHeight?: string;
|
|
89
|
+
fontWeight?: string;
|
|
90
|
+
letterSpacing?: string;
|
|
91
|
+
color?: string;
|
|
92
|
+
backgroundColor?: string;
|
|
93
|
+
textAlign?: string;
|
|
94
|
+
width?: string;
|
|
95
|
+
height?: string;
|
|
96
|
+
minWidth?: string;
|
|
97
|
+
minHeight?: string;
|
|
98
|
+
maxWidth?: string;
|
|
99
|
+
maxHeight?: string;
|
|
100
|
+
marginTop?: string;
|
|
101
|
+
marginRight?: string;
|
|
102
|
+
marginBottom?: string;
|
|
103
|
+
marginLeft?: string;
|
|
104
|
+
paddingTop?: string;
|
|
105
|
+
paddingRight?: string;
|
|
106
|
+
paddingBottom?: string;
|
|
107
|
+
paddingLeft?: string;
|
|
108
|
+
borderRadius?: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type VisualCustomization = {
|
|
112
|
+
version: typeof VISUAL_CUSTOMIZATION_VERSION;
|
|
113
|
+
colors?: VisualCustomizationColors;
|
|
114
|
+
colorReplacements?: Record<string, string>;
|
|
115
|
+
typography?: VisualCustomizationTypography;
|
|
116
|
+
spacing?: VisualCustomizationSpacing;
|
|
117
|
+
layout?: VisualCustomizationLayout;
|
|
118
|
+
components?: VisualCustomizationComponents;
|
|
119
|
+
sections?: Record<string, VisualCustomizationSectionOverride>;
|
|
120
|
+
elementStyles?: Record<string, VisualCustomizationElementStyle>;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type ThemeSchemaDefaults = {
|
|
124
|
+
colors?: VisualCustomizationColors;
|
|
125
|
+
typography?: VisualCustomizationTypography;
|
|
126
|
+
spacing?: VisualCustomizationSpacing;
|
|
127
|
+
layout?: VisualCustomizationLayout;
|
|
128
|
+
components?: VisualCustomizationComponents;
|
|
129
|
+
sections?: Record<string, VisualCustomizationSectionOverride>;
|
|
130
|
+
elementStyles?: Record<string, VisualCustomizationElementStyle>;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type ThemeSchema = {
|
|
134
|
+
version: number;
|
|
135
|
+
tokens?: string[];
|
|
136
|
+
defaults?: ThemeSchemaDefaults;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type TemplateThemeShape = {
|
|
140
|
+
primaryColor?: string;
|
|
141
|
+
secondaryColor?: string;
|
|
142
|
+
accentColor?: string;
|
|
143
|
+
backgroundColor?: string;
|
|
144
|
+
textColor?: string;
|
|
145
|
+
surfaceColor?: string;
|
|
146
|
+
surfaceAltColor?: string;
|
|
147
|
+
headingColor?: string;
|
|
148
|
+
mutedTextColor?: string;
|
|
149
|
+
borderColor?: string;
|
|
150
|
+
headerBackgroundColor?: string;
|
|
151
|
+
footerBackgroundColor?: string;
|
|
152
|
+
cardBackgroundColor?: string;
|
|
153
|
+
buttonBackgroundColor?: string;
|
|
154
|
+
buttonTextColor?: string;
|
|
155
|
+
headingFont?: string;
|
|
156
|
+
bodyFont?: string;
|
|
157
|
+
baseSize?: string;
|
|
158
|
+
headingScale?: string;
|
|
159
|
+
bodyLineHeight?: string;
|
|
160
|
+
headingLineHeight?: string;
|
|
161
|
+
headingWeight?: string;
|
|
162
|
+
bodyWeight?: string;
|
|
163
|
+
letterSpacing?: string;
|
|
164
|
+
heroMinHeight?: string;
|
|
165
|
+
sectionPadding?: string;
|
|
166
|
+
contentMaxWidth?: string;
|
|
167
|
+
containerPadding?: string;
|
|
168
|
+
sectionGap?: string;
|
|
169
|
+
elementGap?: string;
|
|
170
|
+
gridGap?: string;
|
|
171
|
+
textAlign?: string;
|
|
172
|
+
contentAlign?: string;
|
|
173
|
+
heroTextAlign?: string;
|
|
174
|
+
cardTextAlign?: string;
|
|
175
|
+
gridColumns?: string;
|
|
176
|
+
cardWidth?: string;
|
|
177
|
+
cardMinHeight?: string;
|
|
178
|
+
cardPadding?: string;
|
|
179
|
+
cardRadius?: string;
|
|
180
|
+
cardBorderWidth?: string;
|
|
181
|
+
cardShadow?: string;
|
|
182
|
+
buttonPadding?: string;
|
|
183
|
+
buttonRadius?: string;
|
|
184
|
+
buttonShadow?: string;
|
|
185
|
+
imageRadius?: string;
|
|
186
|
+
headerHeight?: string;
|
|
187
|
+
style?: string;
|
|
188
|
+
designCustomizationVersion?: number;
|
|
189
|
+
colorReplacements?: Record<string, string>;
|
|
190
|
+
sections?: Record<string, VisualCustomizationSectionOverride>;
|
|
191
|
+
elementStyles?: Record<string, VisualCustomizationElementStyle>;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const COLOR_KEYS = [
|
|
195
|
+
'primary',
|
|
196
|
+
'secondary',
|
|
197
|
+
'accent',
|
|
198
|
+
'background',
|
|
199
|
+
'text',
|
|
200
|
+
'surface',
|
|
201
|
+
'surfaceAlt',
|
|
202
|
+
'heading',
|
|
203
|
+
'mutedText',
|
|
204
|
+
'border',
|
|
205
|
+
'headerBackground',
|
|
206
|
+
'footerBackground',
|
|
207
|
+
'cardBackground',
|
|
208
|
+
'buttonBackground',
|
|
209
|
+
'buttonText',
|
|
210
|
+
] as const;
|
|
211
|
+
|
|
212
|
+
const TYPOGRAPHY_KEYS = [
|
|
213
|
+
'headingFont',
|
|
214
|
+
'bodyFont',
|
|
215
|
+
'baseSize',
|
|
216
|
+
'headingScale',
|
|
217
|
+
'bodyLineHeight',
|
|
218
|
+
'headingLineHeight',
|
|
219
|
+
'headingWeight',
|
|
220
|
+
'bodyWeight',
|
|
221
|
+
'letterSpacing',
|
|
222
|
+
] as const;
|
|
223
|
+
|
|
224
|
+
const SPACING_KEYS = [
|
|
225
|
+
'heroMinHeight',
|
|
226
|
+
'sectionPadding',
|
|
227
|
+
'contentMaxWidth',
|
|
228
|
+
'containerPadding',
|
|
229
|
+
'sectionGap',
|
|
230
|
+
'elementGap',
|
|
231
|
+
'gridGap',
|
|
232
|
+
] as const;
|
|
233
|
+
|
|
234
|
+
const LAYOUT_KEYS = [
|
|
235
|
+
'textAlign',
|
|
236
|
+
'contentAlign',
|
|
237
|
+
'heroTextAlign',
|
|
238
|
+
'cardTextAlign',
|
|
239
|
+
'gridColumns',
|
|
240
|
+
] as const;
|
|
241
|
+
|
|
242
|
+
const COMPONENT_KEYS = [
|
|
243
|
+
'cardWidth',
|
|
244
|
+
'cardMinHeight',
|
|
245
|
+
'cardPadding',
|
|
246
|
+
'cardRadius',
|
|
247
|
+
'cardBorderWidth',
|
|
248
|
+
'cardShadow',
|
|
249
|
+
'buttonPadding',
|
|
250
|
+
'buttonRadius',
|
|
251
|
+
'buttonShadow',
|
|
252
|
+
'imageRadius',
|
|
253
|
+
'headerHeight',
|
|
254
|
+
] as const;
|
|
255
|
+
|
|
256
|
+
const SECTION_STRING_KEYS = [
|
|
257
|
+
'backgroundColor',
|
|
258
|
+
'textColor',
|
|
259
|
+
'headingColor',
|
|
260
|
+
'minHeight',
|
|
261
|
+
'padding',
|
|
262
|
+
'contentMaxWidth',
|
|
263
|
+
'gap',
|
|
264
|
+
'textAlign',
|
|
265
|
+
'contentAlign',
|
|
266
|
+
'cardBackgroundColor',
|
|
267
|
+
'cardWidth',
|
|
268
|
+
'cardMinHeight',
|
|
269
|
+
'cardRadius',
|
|
270
|
+
'gridColumns',
|
|
271
|
+
] as const;
|
|
272
|
+
|
|
273
|
+
export const ELEMENT_STYLE_KEYS = [
|
|
274
|
+
'fontFamily',
|
|
275
|
+
'fontSize',
|
|
276
|
+
'lineHeight',
|
|
277
|
+
'fontWeight',
|
|
278
|
+
'letterSpacing',
|
|
279
|
+
'color',
|
|
280
|
+
'backgroundColor',
|
|
281
|
+
'textAlign',
|
|
282
|
+
'width',
|
|
283
|
+
'height',
|
|
284
|
+
'minWidth',
|
|
285
|
+
'minHeight',
|
|
286
|
+
'maxWidth',
|
|
287
|
+
'maxHeight',
|
|
288
|
+
'marginTop',
|
|
289
|
+
'marginRight',
|
|
290
|
+
'marginBottom',
|
|
291
|
+
'marginLeft',
|
|
292
|
+
'paddingTop',
|
|
293
|
+
'paddingRight',
|
|
294
|
+
'paddingBottom',
|
|
295
|
+
'paddingLeft',
|
|
296
|
+
'borderRadius',
|
|
297
|
+
] as const;
|
|
298
|
+
|
|
299
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
300
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function safeDesignValue(value: string) {
|
|
304
|
+
const trimmed = value.trim();
|
|
305
|
+
if (!trimmed) return '';
|
|
306
|
+
if (trimmed.length > 160) return undefined;
|
|
307
|
+
if (/[;{}<>\r\n]/.test(trimmed)) return undefined;
|
|
308
|
+
if (/(?:url\s*\(|expression\s*\(|@import|javascript:)/i.test(trimmed)) {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
return trimmed;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalizeHexColor(value: string) {
|
|
315
|
+
const normalized = value.trim().toLowerCase();
|
|
316
|
+
const short = normalized.match(/^#([0-9a-f]{3})$/);
|
|
317
|
+
if (short) {
|
|
318
|
+
return `#${[...short[1]].map((digit) => digit.repeat(2)).join('')}`;
|
|
319
|
+
}
|
|
320
|
+
return /^#[0-9a-f]{6}$/.test(normalized) ? normalized : null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseColorReplacements(value: unknown) {
|
|
324
|
+
if (!isPlainRecord(value)) return undefined;
|
|
325
|
+
const replacements: Record<string, string> = {};
|
|
326
|
+
for (const [source, replacement] of Object.entries(value).slice(0, 64)) {
|
|
327
|
+
if (typeof replacement !== 'string') continue;
|
|
328
|
+
const normalizedSource = normalizeHexColor(source);
|
|
329
|
+
const normalizedReplacement = normalizeHexColor(replacement);
|
|
330
|
+
if (!normalizedSource || !normalizedReplacement) continue;
|
|
331
|
+
replacements[normalizedSource] = normalizedReplacement;
|
|
332
|
+
}
|
|
333
|
+
return Object.keys(replacements).length > 0 ? replacements : undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function pickSafeStrings<const Key extends string>(
|
|
337
|
+
value: unknown,
|
|
338
|
+
keys: readonly Key[],
|
|
339
|
+
): Partial<Record<Key, string>> | undefined {
|
|
340
|
+
if (!isPlainRecord(value)) return undefined;
|
|
341
|
+
const result: Partial<Record<Key, string>> = {};
|
|
342
|
+
for (const key of keys) {
|
|
343
|
+
if (typeof value[key] !== 'string') continue;
|
|
344
|
+
const safe = safeDesignValue(value[key]);
|
|
345
|
+
if (safe !== undefined) result[key] = safe;
|
|
346
|
+
}
|
|
347
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function isSafeSectionKey(key: string) {
|
|
351
|
+
return /^[a-zA-Z0-9_-]{1,64}$/.test(key);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function isSafeElementPath(path: string) {
|
|
355
|
+
return /^[a-zA-Z0-9_.:\[\]-]{1,180}$/.test(path);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function normalizeTemplateTier(tier: string | null | undefined) {
|
|
359
|
+
return tier?.trim().toUpperCase() ?? 'STANDARD';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function isPremiumTemplateTier(tier: string | null | undefined) {
|
|
363
|
+
return normalizeTemplateTier(tier) === PREMIUM_TEMPLATE_TIER;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function isFullVisualCustomizationTier(tier: string | null | undefined) {
|
|
367
|
+
const normalizedTier = normalizeTemplateTier(tier);
|
|
368
|
+
return (
|
|
369
|
+
normalizedTier === PREMIUM_TEMPLATE_TIER ||
|
|
370
|
+
normalizedTier === CUSTOM_TEMPLATE_TIER
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function emptyVisualCustomization(): VisualCustomization {
|
|
375
|
+
return { version: VISUAL_CUSTOMIZATION_VERSION };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function parseVisualCustomization(value: unknown): VisualCustomization {
|
|
379
|
+
if (!isPlainRecord(value)) {
|
|
380
|
+
return emptyVisualCustomization();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const colors = pickSafeStrings(value.colors, COLOR_KEYS);
|
|
384
|
+
const colorReplacements = parseColorReplacements(value.colorReplacements);
|
|
385
|
+
const typography = pickSafeStrings(value.typography, TYPOGRAPHY_KEYS);
|
|
386
|
+
const spacing = pickSafeStrings(value.spacing, SPACING_KEYS);
|
|
387
|
+
const layout = pickSafeStrings(value.layout, LAYOUT_KEYS);
|
|
388
|
+
const components = pickSafeStrings(value.components, COMPONENT_KEYS);
|
|
389
|
+
|
|
390
|
+
const sections: Record<string, VisualCustomizationSectionOverride> = {};
|
|
391
|
+
if (isPlainRecord(value.sections)) {
|
|
392
|
+
for (const [key, sectionValue] of Object.entries(value.sections)) {
|
|
393
|
+
if (!isSafeSectionKey(key) || !isPlainRecord(sectionValue)) continue;
|
|
394
|
+
const strings = pickSafeStrings(sectionValue, SECTION_STRING_KEYS) ?? {};
|
|
395
|
+
const section: VisualCustomizationSectionOverride = { ...strings };
|
|
396
|
+
if (typeof sectionValue.visible === 'boolean') {
|
|
397
|
+
section.visible = sectionValue.visible;
|
|
398
|
+
}
|
|
399
|
+
if (Object.keys(section).length > 0) sections[key] = section;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const elementStyles: Record<string, VisualCustomizationElementStyle> = {};
|
|
404
|
+
if (isPlainRecord(value.elementStyles)) {
|
|
405
|
+
for (const [path, styleValue] of Object.entries(value.elementStyles).slice(
|
|
406
|
+
0,
|
|
407
|
+
256,
|
|
408
|
+
)) {
|
|
409
|
+
if (!isSafeElementPath(path) || !isPlainRecord(styleValue)) continue;
|
|
410
|
+
const style = pickSafeStrings(styleValue, ELEMENT_STYLE_KEYS);
|
|
411
|
+
if (style && Object.keys(style).length > 0) {
|
|
412
|
+
elementStyles[path] = style;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
version: VISUAL_CUSTOMIZATION_VERSION,
|
|
419
|
+
...(colors ? { colors } : {}),
|
|
420
|
+
...(colorReplacements ? { colorReplacements } : {}),
|
|
421
|
+
...(typography ? { typography } : {}),
|
|
422
|
+
...(spacing ? { spacing } : {}),
|
|
423
|
+
...(layout ? { layout } : {}),
|
|
424
|
+
...(components ? { components } : {}),
|
|
425
|
+
...(Object.keys(sections).length > 0 ? { sections } : {}),
|
|
426
|
+
...(Object.keys(elementStyles).length > 0 ? { elementStyles } : {}),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function limitVisualCustomizationForTier(
|
|
431
|
+
value: unknown,
|
|
432
|
+
tier: string | null | undefined,
|
|
433
|
+
): VisualCustomization {
|
|
434
|
+
const parsed = parseVisualCustomization(value);
|
|
435
|
+
if (isFullVisualCustomizationTier(tier)) return parsed;
|
|
436
|
+
|
|
437
|
+
// Standard can save color tokens only. The client groups tokens
|
|
438
|
+
// that share the same loaded value and submits each grouped token together.
|
|
439
|
+
return parseVisualCustomization({
|
|
440
|
+
version: VISUAL_CUSTOMIZATION_VERSION,
|
|
441
|
+
colors: parsed.colors,
|
|
442
|
+
colorReplacements: parsed.colorReplacements,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function parseThemeSchema(manifest: unknown): ThemeSchema | null {
|
|
447
|
+
if (!isPlainRecord(manifest)) return null;
|
|
448
|
+
const themeSchema = manifest.themeSchema;
|
|
449
|
+
if (!isPlainRecord(themeSchema)) return null;
|
|
450
|
+
|
|
451
|
+
const parsedDefaults = parseVisualCustomization(
|
|
452
|
+
isPlainRecord(themeSchema.defaults)
|
|
453
|
+
? { version: VISUAL_CUSTOMIZATION_VERSION, ...themeSchema.defaults }
|
|
454
|
+
: null,
|
|
455
|
+
);
|
|
456
|
+
const defaults: ThemeSchemaDefaults = {
|
|
457
|
+
...(parsedDefaults.colors ? { colors: parsedDefaults.colors } : {}),
|
|
458
|
+
...(parsedDefaults.typography
|
|
459
|
+
? { typography: parsedDefaults.typography }
|
|
460
|
+
: {}),
|
|
461
|
+
...(parsedDefaults.spacing ? { spacing: parsedDefaults.spacing } : {}),
|
|
462
|
+
...(parsedDefaults.layout ? { layout: parsedDefaults.layout } : {}),
|
|
463
|
+
...(parsedDefaults.components
|
|
464
|
+
? { components: parsedDefaults.components }
|
|
465
|
+
: {}),
|
|
466
|
+
...(parsedDefaults.sections ? { sections: parsedDefaults.sections } : {}),
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
version: typeof themeSchema.version === 'number' ? themeSchema.version : 1,
|
|
471
|
+
tokens: Array.isArray(themeSchema.tokens)
|
|
472
|
+
? themeSchema.tokens.filter(
|
|
473
|
+
(token): token is string => typeof token === 'string',
|
|
474
|
+
)
|
|
475
|
+
: undefined,
|
|
476
|
+
...(Object.keys(defaults).length > 0 ? { defaults } : {}),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function getDefaultThemeTokens(
|
|
481
|
+
themeSchema: ThemeSchema | null,
|
|
482
|
+
baseTheme: TemplateThemeShape | null | undefined,
|
|
483
|
+
): ThemeSchemaDefaults {
|
|
484
|
+
const defaults: ThemeSchemaDefaults = {
|
|
485
|
+
colors: {
|
|
486
|
+
primary: baseTheme?.primaryColor ?? '#088395',
|
|
487
|
+
secondary: baseTheme?.secondaryColor,
|
|
488
|
+
accent: baseTheme?.accentColor,
|
|
489
|
+
background: baseTheme?.backgroundColor,
|
|
490
|
+
text: baseTheme?.textColor,
|
|
491
|
+
surface: baseTheme?.surfaceColor,
|
|
492
|
+
surfaceAlt: baseTheme?.surfaceAltColor,
|
|
493
|
+
heading: baseTheme?.headingColor,
|
|
494
|
+
mutedText: baseTheme?.mutedTextColor,
|
|
495
|
+
border: baseTheme?.borderColor,
|
|
496
|
+
headerBackground: baseTheme?.headerBackgroundColor,
|
|
497
|
+
footerBackground: baseTheme?.footerBackgroundColor,
|
|
498
|
+
cardBackground: baseTheme?.cardBackgroundColor,
|
|
499
|
+
buttonBackground: baseTheme?.buttonBackgroundColor,
|
|
500
|
+
buttonText: baseTheme?.buttonTextColor,
|
|
501
|
+
},
|
|
502
|
+
typography: {
|
|
503
|
+
headingFont: baseTheme?.headingFont,
|
|
504
|
+
bodyFont: baseTheme?.bodyFont,
|
|
505
|
+
baseSize: baseTheme?.baseSize,
|
|
506
|
+
headingScale: baseTheme?.headingScale,
|
|
507
|
+
bodyLineHeight: baseTheme?.bodyLineHeight,
|
|
508
|
+
headingLineHeight: baseTheme?.headingLineHeight,
|
|
509
|
+
headingWeight: baseTheme?.headingWeight,
|
|
510
|
+
bodyWeight: baseTheme?.bodyWeight,
|
|
511
|
+
letterSpacing: baseTheme?.letterSpacing,
|
|
512
|
+
},
|
|
513
|
+
spacing: {
|
|
514
|
+
heroMinHeight: baseTheme?.heroMinHeight ?? '80vh',
|
|
515
|
+
sectionPadding: baseTheme?.sectionPadding ?? '4rem',
|
|
516
|
+
contentMaxWidth: baseTheme?.contentMaxWidth,
|
|
517
|
+
containerPadding: baseTheme?.containerPadding,
|
|
518
|
+
sectionGap: baseTheme?.sectionGap,
|
|
519
|
+
elementGap: baseTheme?.elementGap,
|
|
520
|
+
gridGap: baseTheme?.gridGap,
|
|
521
|
+
},
|
|
522
|
+
layout: {
|
|
523
|
+
textAlign: baseTheme?.textAlign,
|
|
524
|
+
contentAlign: baseTheme?.contentAlign,
|
|
525
|
+
heroTextAlign: baseTheme?.heroTextAlign,
|
|
526
|
+
cardTextAlign: baseTheme?.cardTextAlign,
|
|
527
|
+
gridColumns: baseTheme?.gridColumns,
|
|
528
|
+
},
|
|
529
|
+
components: {
|
|
530
|
+
cardWidth: baseTheme?.cardWidth,
|
|
531
|
+
cardMinHeight: baseTheme?.cardMinHeight,
|
|
532
|
+
cardPadding: baseTheme?.cardPadding,
|
|
533
|
+
cardRadius: baseTheme?.cardRadius,
|
|
534
|
+
cardBorderWidth: baseTheme?.cardBorderWidth,
|
|
535
|
+
cardShadow: baseTheme?.cardShadow,
|
|
536
|
+
buttonPadding: baseTheme?.buttonPadding,
|
|
537
|
+
buttonRadius: baseTheme?.buttonRadius,
|
|
538
|
+
buttonShadow: baseTheme?.buttonShadow,
|
|
539
|
+
imageRadius: baseTheme?.imageRadius,
|
|
540
|
+
headerHeight: baseTheme?.headerHeight,
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
if (!themeSchema?.defaults) return defaults;
|
|
545
|
+
return {
|
|
546
|
+
colors: { ...defaults.colors, ...themeSchema.defaults.colors },
|
|
547
|
+
typography: {
|
|
548
|
+
...defaults.typography,
|
|
549
|
+
...themeSchema.defaults.typography,
|
|
550
|
+
},
|
|
551
|
+
spacing: { ...defaults.spacing, ...themeSchema.defaults.spacing },
|
|
552
|
+
layout: { ...defaults.layout, ...themeSchema.defaults.layout },
|
|
553
|
+
components: {
|
|
554
|
+
...defaults.components,
|
|
555
|
+
...themeSchema.defaults.components,
|
|
556
|
+
},
|
|
557
|
+
sections: themeSchema.defaults.sections,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function customizationToTheme(
|
|
562
|
+
customization: VisualCustomization,
|
|
563
|
+
): TemplateThemeShape {
|
|
564
|
+
const colors = customization.colors ?? {};
|
|
565
|
+
const typography = customization.typography ?? {};
|
|
566
|
+
const spacing = customization.spacing ?? {};
|
|
567
|
+
const layout = customization.layout ?? {};
|
|
568
|
+
const components = customization.components ?? {};
|
|
569
|
+
return {
|
|
570
|
+
designCustomizationVersion: VISUAL_CUSTOMIZATION_VERSION,
|
|
571
|
+
colorReplacements: customization.colorReplacements,
|
|
572
|
+
primaryColor: colors.primary,
|
|
573
|
+
secondaryColor: colors.secondary,
|
|
574
|
+
accentColor: colors.accent,
|
|
575
|
+
backgroundColor: colors.background,
|
|
576
|
+
textColor: colors.text,
|
|
577
|
+
surfaceColor: colors.surface,
|
|
578
|
+
surfaceAltColor: colors.surfaceAlt,
|
|
579
|
+
headingColor: colors.heading,
|
|
580
|
+
mutedTextColor: colors.mutedText,
|
|
581
|
+
borderColor: colors.border,
|
|
582
|
+
headerBackgroundColor: colors.headerBackground,
|
|
583
|
+
footerBackgroundColor: colors.footerBackground,
|
|
584
|
+
cardBackgroundColor: colors.cardBackground,
|
|
585
|
+
buttonBackgroundColor: colors.buttonBackground,
|
|
586
|
+
buttonTextColor: colors.buttonText,
|
|
587
|
+
...typography,
|
|
588
|
+
...spacing,
|
|
589
|
+
...layout,
|
|
590
|
+
...components,
|
|
591
|
+
sections: customization.sections,
|
|
592
|
+
elementStyles: customization.elementStyles,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function mergeVisualCustomizationIntoTheme(
|
|
597
|
+
baseTheme: TemplateThemeShape | null | undefined,
|
|
598
|
+
customization: unknown,
|
|
599
|
+
templateTier: string | null | undefined,
|
|
600
|
+
): TemplateThemeShape | null {
|
|
601
|
+
const parsed = limitVisualCustomizationForTier(customization, templateTier);
|
|
602
|
+
const hasOverrides =
|
|
603
|
+
[
|
|
604
|
+
'colors',
|
|
605
|
+
'colorReplacements',
|
|
606
|
+
'typography',
|
|
607
|
+
'spacing',
|
|
608
|
+
'layout',
|
|
609
|
+
'components',
|
|
610
|
+
].some((key) => {
|
|
611
|
+
const group = parsed[key as keyof VisualCustomization];
|
|
612
|
+
return Boolean(
|
|
613
|
+
group &&
|
|
614
|
+
typeof group === 'object' &&
|
|
615
|
+
Object.values(group).some(
|
|
616
|
+
(value) => value !== undefined && value !== '',
|
|
617
|
+
),
|
|
618
|
+
);
|
|
619
|
+
}) ||
|
|
620
|
+
Boolean(parsed.sections && Object.keys(parsed.sections).length > 0) ||
|
|
621
|
+
Boolean(
|
|
622
|
+
parsed.elementStyles && Object.keys(parsed.elementStyles).length > 0,
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
if (!hasOverrides) return baseTheme ?? null;
|
|
626
|
+
|
|
627
|
+
const overrideTheme = customizationToTheme(parsed);
|
|
628
|
+
return Object.fromEntries(
|
|
629
|
+
Object.entries({ ...(baseTheme ?? {}), ...overrideTheme }).filter(
|
|
630
|
+
([, value]) => value !== undefined,
|
|
631
|
+
),
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function mergeCustomizationRecords(
|
|
636
|
+
base: VisualCustomization,
|
|
637
|
+
patch: Partial<VisualCustomization>,
|
|
638
|
+
): VisualCustomization {
|
|
639
|
+
return {
|
|
640
|
+
version: VISUAL_CUSTOMIZATION_VERSION,
|
|
641
|
+
colors: { ...base.colors, ...patch.colors },
|
|
642
|
+
colorReplacements: {
|
|
643
|
+
...base.colorReplacements,
|
|
644
|
+
...patch.colorReplacements,
|
|
645
|
+
},
|
|
646
|
+
typography: { ...base.typography, ...patch.typography },
|
|
647
|
+
spacing: { ...base.spacing, ...patch.spacing },
|
|
648
|
+
layout: { ...base.layout, ...patch.layout },
|
|
649
|
+
components: { ...base.components, ...patch.components },
|
|
650
|
+
sections: { ...base.sections, ...patch.sections },
|
|
651
|
+
elementStyles: Object.fromEntries(
|
|
652
|
+
Array.from(
|
|
653
|
+
new Set([
|
|
654
|
+
...Object.keys(base.elementStyles ?? {}),
|
|
655
|
+
...Object.keys(patch.elementStyles ?? {}),
|
|
656
|
+
]),
|
|
657
|
+
).map((path) => [
|
|
658
|
+
path,
|
|
659
|
+
{
|
|
660
|
+
...base.elementStyles?.[path],
|
|
661
|
+
...patch.elementStyles?.[path],
|
|
662
|
+
},
|
|
663
|
+
]),
|
|
664
|
+
),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export function seedVisualCustomizationFromRequirements(input: {
|
|
669
|
+
preferredColorTheme?: string | null;
|
|
670
|
+
preferredStyle?: string | null;
|
|
671
|
+
}): VisualCustomization {
|
|
672
|
+
const customization = emptyVisualCustomization();
|
|
673
|
+
const preferredColor = input.preferredColorTheme?.trim();
|
|
674
|
+
const preferredStyle = input.preferredStyle?.trim();
|
|
675
|
+
|
|
676
|
+
if (preferredColor) customization.colors = { primary: preferredColor };
|
|
677
|
+
if (preferredStyle) customization.typography = { bodyFont: preferredStyle };
|
|
678
|
+
return parseVisualCustomization(customization);
|
|
679
|
+
}
|