@shoppexio/builder-contracts 0.1.0 → 0.1.2

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 (92) hide show
  1. package/dist/builder-settings.d.ts +11 -666
  2. package/dist/builder-settings.d.ts.map +1 -1
  3. package/dist/builder-settings.js +2 -1
  4. package/dist/canonical-settings.d.ts +18 -0
  5. package/dist/canonical-settings.d.ts.map +1 -0
  6. package/dist/canonical-settings.js +106 -0
  7. package/dist/custom-pages.d.ts +15 -0
  8. package/dist/custom-pages.d.ts.map +1 -0
  9. package/dist/custom-pages.js +40 -0
  10. package/dist/dedicated-pages.d.ts +15 -0
  11. package/dist/dedicated-pages.d.ts.map +1 -0
  12. package/dist/dedicated-pages.js +142 -0
  13. package/dist/events.d.ts +35 -240
  14. package/dist/events.d.ts.map +1 -1
  15. package/dist/events.js +7 -0
  16. package/dist/fields.d.ts +229 -10
  17. package/dist/fields.d.ts.map +1 -1
  18. package/dist/fields.js +27 -0
  19. package/dist/index.d.ts +10 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -0
  22. package/dist/legacy-manifest.d.ts +18 -0
  23. package/dist/legacy-manifest.d.ts.map +1 -1
  24. package/dist/legacy-manifest.js +137 -22
  25. package/dist/migrations.d.ts.map +1 -1
  26. package/dist/migrations.js +55 -6
  27. package/dist/persistence.d.ts +7 -0
  28. package/dist/persistence.d.ts.map +1 -0
  29. package/dist/persistence.js +58 -0
  30. package/dist/preview-boot.d.ts +68 -0
  31. package/dist/preview-boot.d.ts.map +1 -0
  32. package/dist/preview-boot.js +38 -0
  33. package/dist/preview-protocol.d.ts +227 -459
  34. package/dist/preview-protocol.d.ts.map +1 -1
  35. package/dist/preview-protocol.js +112 -0
  36. package/dist/preview-session-resolve.d.ts +115 -0
  37. package/dist/preview-session-resolve.d.ts.map +1 -0
  38. package/dist/preview-session-resolve.js +25 -0
  39. package/dist/preview-trusted-origins.d.ts +4 -0
  40. package/dist/preview-trusted-origins.d.ts.map +1 -0
  41. package/dist/preview-trusted-origins.js +28 -0
  42. package/dist/storefront-initial-data-html.d.ts +17 -0
  43. package/dist/storefront-initial-data-html.d.ts.map +1 -0
  44. package/dist/storefront-initial-data-html.js +83 -0
  45. package/dist/storefront-typography-fonts.d.ts +18 -0
  46. package/dist/storefront-typography-fonts.d.ts.map +1 -0
  47. package/dist/storefront-typography-fonts.js +89 -0
  48. package/dist/style-slots.d.ts +50 -152
  49. package/dist/style-slots.d.ts.map +1 -1
  50. package/dist/style-slots.js +80 -32
  51. package/dist/theme-manifest.d.ts +287 -456
  52. package/dist/theme-manifest.d.ts.map +1 -1
  53. package/dist/theme-manifest.js +92 -0
  54. package/dist/theme-schemes.d.ts +10 -0
  55. package/dist/theme-schemes.d.ts.map +1 -0
  56. package/dist/theme-schemes.js +25 -0
  57. package/dist/validation.d.ts +1 -1
  58. package/dist/validation.d.ts.map +1 -1
  59. package/dist/validation.js +23 -12
  60. package/package.json +43 -1
  61. package/src/builder-contracts.test.ts +416 -3
  62. package/src/builder-settings.ts +4 -1
  63. package/src/canonical-settings.ts +156 -0
  64. package/src/custom-pages.test.ts +74 -0
  65. package/src/custom-pages.ts +70 -0
  66. package/src/dedicated-pages.test.ts +88 -0
  67. package/src/dedicated-pages.ts +173 -0
  68. package/src/events.ts +8 -0
  69. package/src/fields.ts +30 -0
  70. package/src/index.ts +10 -0
  71. package/src/legacy-manifest.ts +147 -23
  72. package/src/migrations.ts +70 -6
  73. package/src/persistence.ts +77 -0
  74. package/src/preview-boot.test.ts +72 -0
  75. package/src/preview-boot.ts +49 -0
  76. package/src/preview-protocol.test.ts +132 -0
  77. package/src/preview-protocol.ts +122 -0
  78. package/src/preview-session-resolve.test.ts +37 -0
  79. package/src/preview-session-resolve.ts +34 -0
  80. package/src/preview-trusted-origins.test.ts +24 -0
  81. package/src/preview-trusted-origins.ts +35 -0
  82. package/src/storefront-initial-data-html.test.ts +73 -0
  83. package/src/storefront-initial-data-html.ts +112 -0
  84. package/src/storefront-typography-fonts.test.ts +48 -0
  85. package/src/storefront-typography-fonts.ts +108 -0
  86. package/src/style-slots.ts +102 -34
  87. package/src/theme-manifest.ts +118 -1
  88. package/src/theme-schemes.ts +34 -0
  89. package/src/validation.ts +32 -13
  90. package/dist/builder-contracts.test.d.ts +0 -2
  91. package/dist/builder-contracts.test.d.ts.map +0 -1
  92. package/dist/builder-contracts.test.js +0 -361
@@ -0,0 +1,108 @@
1
+ export type StorefrontCuratedFont = {
2
+ value: string;
3
+ label: string;
4
+ stack: string;
5
+ };
6
+
7
+ export const STOREFRONT_SYSTEM_FONTS = new Set(['Arial', 'Georgia', 'System UI']);
8
+
9
+ export const STOREFRONT_CURATED_FONTS: StorefrontCuratedFont[] = [
10
+ { value: 'Inter', label: 'Inter', stack: 'Inter, system-ui, sans-serif' },
11
+ { value: 'Geist', label: 'Geist', stack: 'Geist, system-ui, sans-serif' },
12
+ { value: 'Manrope', label: 'Manrope', stack: 'Manrope, system-ui, sans-serif' },
13
+ {
14
+ value: 'Plus Jakarta Sans',
15
+ label: 'Plus Jakarta Sans',
16
+ stack: '"Plus Jakarta Sans", system-ui, sans-serif',
17
+ },
18
+ { value: 'DM Sans', label: 'DM Sans', stack: '"DM Sans", system-ui, sans-serif' },
19
+ {
20
+ value: 'Space Grotesk',
21
+ label: 'Space Grotesk',
22
+ stack: '"Space Grotesk", system-ui, sans-serif',
23
+ },
24
+ { value: 'Sora', label: 'Sora', stack: 'Sora, system-ui, sans-serif' },
25
+ {
26
+ value: 'IBM Plex Sans',
27
+ label: 'IBM Plex Sans',
28
+ stack: '"IBM Plex Sans", system-ui, sans-serif',
29
+ },
30
+ { value: 'Arial', label: 'Arial', stack: 'Arial, sans-serif' },
31
+ { value: 'Georgia', label: 'Georgia', stack: 'Georgia, serif' },
32
+ { value: 'System UI', label: 'System UI', stack: 'system-ui, sans-serif' },
33
+ ];
34
+
35
+ const GOOGLE_FONT_FAMILY_PATTERN = /^[A-Za-z0-9 +\-_]+$/;
36
+
37
+ export function resolvePrimaryFontFamilyName(raw: unknown): string | null {
38
+ if (typeof raw !== 'string') return null;
39
+ const trimmed = raw.trim();
40
+ if (!trimmed) return null;
41
+
42
+ const exact = STOREFRONT_CURATED_FONTS.find(
43
+ (font) => font.value === trimmed || font.stack === trimmed,
44
+ );
45
+ if (exact) return exact.value;
46
+
47
+ const prefix = STOREFRONT_CURATED_FONTS.find((font) => trimmed.startsWith(`${font.value},`));
48
+ if (prefix) return prefix.value;
49
+
50
+ const quoted = trimmed.match(/^["'](.+?)["']/);
51
+ if (quoted?.[1]) {
52
+ return quoted[1].trim() || null;
53
+ }
54
+
55
+ const primary = trimmed.split(',')[0]?.trim();
56
+ return primary && primary.length > 0 ? primary : null;
57
+ }
58
+
59
+ export function resolveCuratedFontValue(raw: unknown, fallback: string): string {
60
+ return resolvePrimaryFontFamilyName(raw) ?? fallback;
61
+ }
62
+
63
+ export function curatedFontOptionsForValue(value: string): StorefrontCuratedFont[] {
64
+ if (STOREFRONT_CURATED_FONTS.some((font) => font.value === value)) {
65
+ return STOREFRONT_CURATED_FONTS;
66
+ }
67
+
68
+ return [
69
+ ...STOREFRONT_CURATED_FONTS,
70
+ {
71
+ value,
72
+ label: value,
73
+ stack: value.includes(',') ? value : `${value}, system-ui, sans-serif`,
74
+ },
75
+ ];
76
+ }
77
+
78
+ export function findStorefrontCuratedFont(value: string): StorefrontCuratedFont | undefined {
79
+ return curatedFontOptionsForValue(value).find((font) => font.value === value);
80
+ }
81
+
82
+ export function isValidGoogleFontFamily(value: unknown): value is string {
83
+ if (typeof value !== 'string') return false;
84
+ const trimmed = value.trim();
85
+ return trimmed.length > 0 && trimmed.length <= 60 && GOOGLE_FONT_FAMILY_PATTERN.test(trimmed);
86
+ }
87
+
88
+ export function buildStorefrontGoogleFontHref(family: string): string {
89
+ const familyParam = family.trim().replace(/\s+/g, '+');
90
+ return `https://fonts.googleapis.com/css2?family=${familyParam}:wght@400;500;600;700&display=swap`;
91
+ }
92
+
93
+ export function getStorefrontGoogleFontHrefs(input: {
94
+ bodyFont?: unknown;
95
+ headingFont?: unknown;
96
+ }): string[] {
97
+ const families = new Set<string>();
98
+
99
+ for (const raw of [input.bodyFont, input.headingFont]) {
100
+ const family = resolvePrimaryFontFamilyName(raw);
101
+ if (!family) continue;
102
+ if (STOREFRONT_SYSTEM_FONTS.has(family)) continue;
103
+ if (!isValidGoogleFontFamily(family)) continue;
104
+ families.add(family);
105
+ }
106
+
107
+ return [...families].map((family) => buildStorefrontGoogleFontHref(family));
108
+ }
@@ -25,9 +25,12 @@ export const ResponsiveStringSchema = z
25
25
  .strict();
26
26
  export type ResponsiveString = z.infer<typeof ResponsiveStringSchema>;
27
27
 
28
- export const ColorSchema = z
29
- .string()
30
- .regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, 'Expected a hex color');
28
+ const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
29
+
30
+ export const ColorSchema = z.union([
31
+ z.literal('transparent'),
32
+ z.string().regex(HEX_COLOR_PATTERN, 'Expected a hex color'),
33
+ ]);
31
34
  export type Color = z.infer<typeof ColorSchema>;
32
35
 
33
36
  export const FontWeightSchema = z.union([
@@ -43,7 +46,7 @@ export const FontWeightSchema = z.union([
43
46
  ]);
44
47
  export type FontWeight = z.infer<typeof FontWeightSchema>;
45
48
 
46
- export const StyleSlotIdSchema = z.enum([
49
+ export const CoreStyleSlotIdSchema = z.enum([
47
50
  'button.radius',
48
51
  'button.background',
49
52
  'button.foreground',
@@ -69,39 +72,104 @@ export const StyleSlotIdSchema = z.enum([
69
72
  'typography.heading.weight',
70
73
  'typography.body.size',
71
74
  ]);
72
- export type StyleSlotId = z.infer<typeof StyleSlotIdSchema>;
75
+ export type CoreStyleSlotId = z.infer<typeof CoreStyleSlotIdSchema>;
76
+
77
+ export const ThemeStyleSlotIdSchema = z
78
+ .string()
79
+ .regex(/^theme\.[a-z0-9][a-z0-9._-]*$/, 'Expected a theme-scoped style slot id');
80
+ export type ThemeStyleSlotId = `theme.${string}`;
81
+
82
+ export const StyleSlotIdSchema = z.union([CoreStyleSlotIdSchema, ThemeStyleSlotIdSchema]);
83
+ export type StyleSlotId = CoreStyleSlotId | ThemeStyleSlotId;
84
+
85
+ export const CORE_STYLE_SLOT_IDS = CoreStyleSlotIdSchema.options;
86
+
87
+ export const ThemeStyleSlotValueSchema = z.union([
88
+ ColorSchema,
89
+ ResponsiveNumberSchema,
90
+ ResponsiveStringSchema,
91
+ FontWeightSchema,
92
+ z.string().min(1),
93
+ z.number().finite(),
94
+ z.boolean(),
95
+ ]);
96
+ export type ThemeStyleSlotValue = z.infer<typeof ThemeStyleSlotValueSchema>;
97
+
98
+ export type StyleSlotValue =
99
+ | Color
100
+ | ResponsiveNumber
101
+ | ResponsiveString
102
+ | FontWeight
103
+ | string
104
+ | number
105
+ | boolean;
73
106
 
74
- export const CORE_STYLE_SLOT_IDS = StyleSlotIdSchema.options;
107
+ const CORE_STYLE_SLOT_SCHEMAS: Record<CoreStyleSlotId, z.ZodType<unknown>> = {
108
+ 'button.radius': ResponsiveNumberSchema,
109
+ 'button.background': ColorSchema,
110
+ 'button.foreground': ColorSchema,
111
+ 'button.border': ColorSchema,
112
+ 'button.font.weight': FontWeightSchema,
113
+ 'input.radius': ResponsiveNumberSchema,
114
+ 'input.height': ResponsiveNumberSchema,
115
+ 'input.border': ColorSchema,
116
+ 'input.background': ColorSchema,
117
+ 'input.foreground': ColorSchema,
118
+ 'card.radius': ResponsiveNumberSchema,
119
+ 'card.background': ColorSchema,
120
+ 'card.border': ColorSchema,
121
+ 'section.padding.y': ResponsiveNumberSchema,
122
+ 'section.padding.x': ResponsiveNumberSchema,
123
+ 'container.width': ResponsiveNumberSchema,
124
+ 'color.primary': ColorSchema,
125
+ 'color.accent': ColorSchema,
126
+ 'color.background': ColorSchema,
127
+ 'color.foreground': ColorSchema,
128
+ 'color.muted': ColorSchema,
129
+ 'link.color': ColorSchema,
130
+ 'typography.heading.weight': FontWeightSchema,
131
+ 'typography.body.size': ResponsiveNumberSchema,
132
+ };
133
+
134
+ export type StyleSlots = Partial<Record<StyleSlotId, StyleSlotValue>>;
75
135
 
76
136
  export const StyleSlotsSchema = z
77
- .object({
78
- 'button.radius': ResponsiveNumberSchema.optional(),
79
- 'button.background': ColorSchema.optional(),
80
- 'button.foreground': ColorSchema.optional(),
81
- 'button.border': ColorSchema.optional(),
82
- 'button.font.weight': FontWeightSchema.optional(),
83
- 'input.radius': ResponsiveNumberSchema.optional(),
84
- 'input.height': ResponsiveNumberSchema.optional(),
85
- 'input.border': ColorSchema.optional(),
86
- 'input.background': ColorSchema.optional(),
87
- 'input.foreground': ColorSchema.optional(),
88
- 'card.radius': ResponsiveNumberSchema.optional(),
89
- 'card.background': ColorSchema.optional(),
90
- 'card.border': ColorSchema.optional(),
91
- 'section.padding.y': ResponsiveNumberSchema.optional(),
92
- 'section.padding.x': ResponsiveNumberSchema.optional(),
93
- 'container.width': ResponsiveNumberSchema.optional(),
94
- 'color.primary': ColorSchema.optional(),
95
- 'color.accent': ColorSchema.optional(),
96
- 'color.background': ColorSchema.optional(),
97
- 'color.foreground': ColorSchema.optional(),
98
- 'color.muted': ColorSchema.optional(),
99
- 'link.color': ColorSchema.optional(),
100
- 'typography.heading.weight': FontWeightSchema.optional(),
101
- 'typography.body.size': ResponsiveNumberSchema.optional(),
102
- })
103
- .strict();
104
- export type StyleSlots = z.infer<typeof StyleSlotsSchema>;
137
+ .record(z.string().min(1), z.unknown())
138
+ .superRefine((slots, ctx) => {
139
+ for (const [slotId, value] of Object.entries(slots)) {
140
+ const coreSlotId = CoreStyleSlotIdSchema.safeParse(slotId);
141
+ if (coreSlotId.success) {
142
+ const parsedValue = CORE_STYLE_SLOT_SCHEMAS[coreSlotId.data].safeParse(value);
143
+ if (!parsedValue.success) {
144
+ ctx.addIssue({
145
+ code: 'custom',
146
+ path: [slotId],
147
+ message: `Invalid value for core style slot "${slotId}"`,
148
+ });
149
+ }
150
+ continue;
151
+ }
152
+
153
+ const themeSlotId = ThemeStyleSlotIdSchema.safeParse(slotId);
154
+ if (!themeSlotId.success) {
155
+ ctx.addIssue({
156
+ code: 'custom',
157
+ path: [slotId],
158
+ message: `Unknown style slot "${slotId}"`,
159
+ });
160
+ continue;
161
+ }
162
+
163
+ const parsedThemeValue = ThemeStyleSlotValueSchema.safeParse(value);
164
+ if (!parsedThemeValue.success) {
165
+ ctx.addIssue({
166
+ code: 'custom',
167
+ path: [slotId],
168
+ message: `Invalid value for theme style slot "${slotId}"`,
169
+ });
170
+ }
171
+ }
172
+ }) as z.ZodType<StyleSlots>;
105
173
 
106
174
  export const StyleSlotDefaultsSchema = StyleSlotsSchema;
107
175
  export type StyleSlotDefaults = StyleSlots;
@@ -1,6 +1,6 @@
1
1
  import * as z from 'zod/v4';
2
2
  import { BlockSettingsSchema } from './fields.ts';
3
- import { StyleSlotDefaultsSchema, StyleSlotIdSchema } from './style-slots.ts';
3
+ import { StyleSlotDefaultsSchema, StyleSlotIdSchema, type StyleSlotDefaults } from './style-slots.ts';
4
4
 
5
5
  export const ThemeIdSchema = z.string().min(1).regex(/^[a-z0-9][a-z0-9-_.]*$/);
6
6
  export type ThemeId = z.infer<typeof ThemeIdSchema>;
@@ -62,6 +62,7 @@ export const ThemePresetSchema = z
62
62
  .object({
63
63
  label: z.string().min(1),
64
64
  description: z.string().min(1).optional(),
65
+ preview: z.string().min(1).optional(),
65
66
  content: z.record(z.string().min(1), z.unknown()).default({}),
66
67
  layout: z.record(z.string().min(1), z.unknown()).default({}),
67
68
  style_slots: StyleSlotDefaultsSchema.default({}),
@@ -69,15 +70,63 @@ export const ThemePresetSchema = z
69
70
  .strict();
70
71
  export type ThemePreset = z.infer<typeof ThemePresetSchema>;
71
72
 
73
+ export const ThemeManifestMetadataSchema = z
74
+ .object({
75
+ author: z.string().min(1).optional(),
76
+ description: z.string().min(1).optional(),
77
+ preview: z.string().min(1).optional(),
78
+ features: z.array(z.string().min(1)).optional(),
79
+ techStack: z.array(z.string().min(1)).optional(),
80
+ templates: z.array(z.string().min(1)).optional(),
81
+ createdAt: z.string().min(1).optional(),
82
+ demoUrl: z.string().min(1).optional(),
83
+ audience: z.string().min(1).optional(),
84
+ tags: z.array(z.string().min(1)).optional(),
85
+ hotfixPaths: z.array(z.string().min(1)).optional(),
86
+ })
87
+ .strict();
88
+ export type ThemeManifestMetadata = z.infer<typeof ThemeManifestMetadataSchema>;
89
+
90
+ export const ThemeManifestLinkGroupSchema = z
91
+ .object({
92
+ path: z.string().min(1),
93
+ label: z.string().min(1),
94
+ description: z.string().min(1).optional(),
95
+ pageIds: z.array(z.string().min(1)).optional(),
96
+ })
97
+ .strict();
98
+ export type ThemeManifestLinkGroup = z.infer<typeof ThemeManifestLinkGroupSchema>;
99
+
100
+ export const ThemeManifestLinkItemSchema = z
101
+ .object({
102
+ label: z.string().min(1),
103
+ href: z.string().min(1),
104
+ })
105
+ .strict();
106
+ export type ThemeManifestLinkItem = z.infer<typeof ThemeManifestLinkItemSchema>;
107
+
72
108
  export const ThemeManifestSchema = z
73
109
  .object({
74
110
  id: ThemeIdSchema,
75
111
  name: z.string().min(1),
76
112
  version: z.string().min(1),
113
+ author: ThemeManifestMetadataSchema.shape.author,
114
+ description: ThemeManifestMetadataSchema.shape.description,
115
+ preview: ThemeManifestMetadataSchema.shape.preview,
116
+ features: ThemeManifestMetadataSchema.shape.features,
117
+ techStack: ThemeManifestMetadataSchema.shape.techStack,
118
+ templates: ThemeManifestMetadataSchema.shape.templates,
119
+ createdAt: ThemeManifestMetadataSchema.shape.createdAt,
120
+ demoUrl: ThemeManifestMetadataSchema.shape.demoUrl,
121
+ audience: ThemeManifestMetadataSchema.shape.audience,
122
+ tags: ThemeManifestMetadataSchema.shape.tags,
123
+ hotfixPaths: ThemeManifestMetadataSchema.shape.hotfixPaths,
77
124
  pages: z.record(z.string().min(1), ManifestPageSchema),
78
125
  blocks: z.record(z.string().min(1), ManifestBlockSchema),
79
126
  styleSlots: StyleSlotDefaultsSchema.default({}),
80
127
  presets: z.record(z.string().min(1), ThemePresetSchema).default({}),
128
+ linkGroups: z.array(ThemeManifestLinkGroupSchema).optional(),
129
+ defaultLinkItems: z.record(z.string().min(1), z.array(ThemeManifestLinkItemSchema)).optional(),
81
130
  })
82
131
  .strict()
83
132
  .superRefine((manifest, ctx) => {
@@ -138,3 +187,71 @@ export type ThemeManifest = z.infer<typeof ThemeManifestSchema>;
138
187
  export function parseThemeManifest(input: unknown): ThemeManifest {
139
188
  return ThemeManifestSchema.parse(input);
140
189
  }
190
+
191
+ export type ThemeManifestPresetListItem = {
192
+ id: string;
193
+ name: string;
194
+ description?: string;
195
+ preview?: string;
196
+ overrides: {
197
+ content?: Record<string, unknown>;
198
+ layout?: Record<string, unknown>;
199
+ tokens?: Record<string, unknown>;
200
+ style_slots?: StyleSlotDefaults;
201
+ };
202
+ };
203
+
204
+ export function listThemeManifestPresets(input: unknown): ThemeManifestPresetListItem[] {
205
+ if (!isRecord(input) || !isRecord(input.presets)) {
206
+ if (isRecord(input) && Array.isArray(input.presets)) {
207
+ return input.presets.flatMap((preset) => normalizeLegacyPresetListItem(preset));
208
+ }
209
+ return [];
210
+ }
211
+
212
+ const manifest = ThemeManifestSchema.safeParse(input);
213
+ if (!manifest.success) {
214
+ return [];
215
+ }
216
+
217
+ return Object.entries(manifest.data.presets).map(([id, preset]) => ({
218
+ id,
219
+ name: preset.label,
220
+ ...(preset.description ? { description: preset.description } : {}),
221
+ ...(preset.preview ? { preview: preset.preview } : {}),
222
+ overrides: {
223
+ ...(Object.keys(preset.content).length > 0 ? { content: preset.content } : {}),
224
+ ...(Object.keys(preset.layout).length > 0 ? { layout: preset.layout } : {}),
225
+ ...(Object.keys(preset.style_slots).length > 0 ? { style_slots: preset.style_slots } : {}),
226
+ },
227
+ }));
228
+ }
229
+
230
+ function normalizeLegacyPresetListItem(input: unknown): ThemeManifestPresetListItem[] {
231
+ if (!isRecord(input) || typeof input.id !== 'string') {
232
+ return [];
233
+ }
234
+
235
+ const overrides = isRecord(input.overrides) ? input.overrides : {};
236
+ const styleSlots = StyleSlotDefaultsSchema.safeParse(overrides.style_slots ?? overrides.styleSlots);
237
+ return [{
238
+ id: input.id,
239
+ name: typeof input.name === 'string'
240
+ ? input.name
241
+ : typeof input.label === 'string'
242
+ ? input.label
243
+ : input.id,
244
+ ...(typeof input.description === 'string' ? { description: input.description } : {}),
245
+ ...(typeof input.preview === 'string' ? { preview: input.preview } : {}),
246
+ overrides: {
247
+ ...(isRecord(overrides.content) ? { content: overrides.content } : {}),
248
+ ...(isRecord(overrides.layout) ? { layout: overrides.layout } : {}),
249
+ ...(isRecord(overrides.tokens) ? { tokens: overrides.tokens } : {}),
250
+ ...(styleSlots.success && Object.keys(styleSlots.data).length > 0 ? { style_slots: styleSlots.data } : {}),
251
+ },
252
+ }];
253
+ }
254
+
255
+ function isRecord(value: unknown): value is Record<string, unknown> {
256
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
257
+ }
@@ -0,0 +1,34 @@
1
+ export const PUBLIC_BUILDER_THEME_SCHEMES = [
2
+ 'default',
3
+ 'classic',
4
+ 'nebula',
5
+ 'pulse',
6
+ 'phantom',
7
+ 'starlight',
8
+ 'apex',
9
+ 'vault',
10
+ 'clean-minimal',
11
+ ] as const;
12
+
13
+ export const STARTER_BUILDER_THEME_SCHEMES = ['blank'] as const;
14
+
15
+ export const BUILDER_READY_THEME_SCHEMES = [
16
+ ...STARTER_BUILDER_THEME_SCHEMES,
17
+ ...PUBLIC_BUILDER_THEME_SCHEMES,
18
+ ] as const;
19
+
20
+ export type PublicBuilderThemeScheme = (typeof PUBLIC_BUILDER_THEME_SCHEMES)[number];
21
+ export type StarterBuilderThemeScheme = (typeof STARTER_BUILDER_THEME_SCHEMES)[number];
22
+ export type BuilderReadyThemeScheme = (typeof BUILDER_READY_THEME_SCHEMES)[number];
23
+
24
+ export function isPublicBuilderThemeScheme(value: string): value is PublicBuilderThemeScheme {
25
+ return (PUBLIC_BUILDER_THEME_SCHEMES as readonly string[]).includes(value);
26
+ }
27
+
28
+ export function isStarterBuilderThemeScheme(value: string): value is StarterBuilderThemeScheme {
29
+ return (STARTER_BUILDER_THEME_SCHEMES as readonly string[]).includes(value);
30
+ }
31
+
32
+ export function isBuilderReadyThemeScheme(value: string): value is BuilderReadyThemeScheme {
33
+ return (BUILDER_READY_THEME_SCHEMES as readonly string[]).includes(value);
34
+ }
package/src/validation.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { BlockInstance, BuilderSettings } from './builder-settings.ts';
2
+ import { mergeCustomPagesIntoManifest } from './custom-pages.ts';
3
+ import { ThemeStyleSlotIdSchema } from './style-slots.ts';
2
4
  import type { ThemeManifest } from './theme-manifest.ts';
3
5
 
4
6
  export type BuilderManifestValidationIssueCode =
@@ -9,6 +11,7 @@ export type BuilderManifestValidationIssueCode =
9
11
  | 'too_many_block_instances'
10
12
  | 'unknown_block_variant'
11
13
  | 'unknown_block_setting'
14
+ | 'unknown_style_slot'
12
15
  | 'unexposed_style_slot';
13
16
 
14
17
  export type BuilderManifestValidationIssue = {
@@ -31,10 +34,13 @@ export function validateBuilderSettingsAgainstManifest(
31
34
  settings: BuilderSettings,
32
35
  manifest: ThemeManifest,
33
36
  ): BuilderManifestValidationIssue[] {
37
+ const effectiveManifest = mergeCustomPagesIntoManifest(manifest, settings);
34
38
  const issues: BuilderManifestValidationIssue[] = [];
35
39
 
40
+ validateGlobalStyleSlots(settings, effectiveManifest, issues);
41
+
36
42
  for (const [pageId, layout] of Object.entries(settings.theme.layout)) {
37
- const page = manifest.pages[pageId];
43
+ const page = effectiveManifest.pages[pageId];
38
44
  if (!page) {
39
45
  issues.push({
40
46
  code: 'unknown_page',
@@ -59,7 +65,7 @@ export function validateBuilderSettingsAgainstManifest(
59
65
  }
60
66
  blockIds.add(block.id);
61
67
 
62
- const blockDefinition = manifest.blocks[block.type];
68
+ const blockDefinition = effectiveManifest.blocks[block.type];
63
69
  if (!blockDefinition) {
64
70
  issues.push({
65
71
  code: 'unknown_block_type',
@@ -84,7 +90,7 @@ export function validateBuilderSettingsAgainstManifest(
84
90
  }
85
91
 
86
92
  for (const [blockType, count] of blockTypeCounts.entries()) {
87
- const maxInstances = manifest.blocks[blockType]?.maxInstances;
93
+ const maxInstances = effectiveManifest.blocks[blockType]?.maxInstances;
88
94
  if (maxInstances !== undefined && count > maxInstances) {
89
95
  issues.push({
90
96
  code: 'too_many_block_instances',
@@ -98,6 +104,28 @@ export function validateBuilderSettingsAgainstManifest(
98
104
  return issues;
99
105
  }
100
106
 
107
+ function validateGlobalStyleSlots(
108
+ settings: BuilderSettings,
109
+ manifest: ThemeManifest,
110
+ issues: BuilderManifestValidationIssue[],
111
+ ): void {
112
+ const declaredThemeSlots = new Set(Object.keys(manifest.styleSlots));
113
+
114
+ for (const slotId of Object.keys(settings.theme.style_slots)) {
115
+ if (!ThemeStyleSlotIdSchema.safeParse(slotId).success) {
116
+ continue;
117
+ }
118
+
119
+ if (!declaredThemeSlots.has(slotId)) {
120
+ issues.push({
121
+ code: 'unknown_style_slot',
122
+ path: `theme.style_slots.${slotId}`,
123
+ message: `Theme style slot "${slotId}" is not declared by the manifest`,
124
+ });
125
+ }
126
+ }
127
+ }
128
+
101
129
  export function assertBuilderSettingsMatchManifest(
102
130
  settings: BuilderSettings,
103
131
  manifest: ThemeManifest,
@@ -130,11 +158,7 @@ function validateBlockSettings(
130
158
  return;
131
159
  }
132
160
 
133
- const allowedSettings = new Set<string>();
134
- for (const path of settingPaths) {
135
- allowedSettings.add(path);
136
- allowedSettings.add(getShortSettingKey(path));
137
- }
161
+ const allowedSettings = new Set(settingPaths);
138
162
 
139
163
  for (const key of Object.keys(block.settings)) {
140
164
  if (allowedSettings.has(key)) {
@@ -168,11 +192,6 @@ function validateBlockVariant(
168
192
  }
169
193
  }
170
194
 
171
- function getShortSettingKey(path: string): string {
172
- const parts = path.split('.').filter(Boolean);
173
- return parts.at(-1) ?? path;
174
- }
175
-
176
195
  function validateBlockStyleOverrides(
177
196
  block: BlockInstance,
178
197
  exposedStyleSlots: string[],
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=builder-contracts.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"builder-contracts.test.d.ts","sourceRoot":"","sources":["../src/builder-contracts.test.ts"],"names":[],"mappings":""}