visualfries 0.1.0 → 0.1.3

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.
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { isValidColor } from './utils.js';
3
3
  import { EffectBaseShape, EffectShape as BaseEffectShape, BlurEffectShape, ShadowShape as StructuredShadowShape, OutlineShape as StructuredOutlineShape, ColorTypeShape, GradientDefinitionShape } from './properties.js';
4
- import { AnimationPresetShape, AnimationReferenceShape } from './animations.js';
4
+ import { AnimationReferenceShape } from './animations.js';
5
+ import { coerceValidNumber, coerceNumber, coercePositiveNumber, coerceNormalizedNumber, coerceNonNegativeNumber, coerceInteger } from './utils.js';
5
6
  // Utility functions
6
7
  const toFixed3 = (val) => parseFloat(val.toFixed(3));
7
8
  const toFixed3Optional = (val) => {
@@ -19,14 +20,14 @@ export const StructuredFontSizeShape = z.union([
19
20
  .transform((val) => ({ value: val, unit: 'px' })),
20
21
  z.object({
21
22
  value: z.number().positive(),
22
- unit: z.enum(['px', 'em', 'rem', '%']).default('px')
23
+ unit: z.enum(['px', 'em', 'rem', '%']).prefault('px')
23
24
  })
24
25
  ]);
25
26
  export const StructuredEmSizeShape = z.union([
26
27
  z.number().transform((val) => ({ value: val, unit: 'em' })),
27
28
  z.object({
28
29
  value: z.number(),
29
- unit: z.enum(['px', 'em', 'rem', '%']).default('em')
30
+ unit: z.enum(['px', 'em', 'rem', '%']).prefault('em')
30
31
  })
31
32
  ]);
32
33
  const FontWeightShape = z.enum([
@@ -74,10 +75,10 @@ export const TextAppearanceShape = z.object({
74
75
  color: ColorTypeShape.nullable().optional(),
75
76
  backgroundColor: ColorTypeShape.nullable().optional(),
76
77
  fontWeight: FontWeightShape.optional(),
77
- scale: z.number().positive().optional(),
78
- backgroundPaddingX: z.number().min(0).optional(),
79
- backgroundPaddingY: z.number().min(0).optional(),
80
- backgroundBorderRadius: z.number().min(0).optional()
78
+ scale: coercePositiveNumber().optional(),
79
+ backgroundPaddingX: coerceNonNegativeNumber().optional(),
80
+ backgroundPaddingY: coerceNonNegativeNumber().optional(),
81
+ backgroundBorderRadius: coerceNonNegativeNumber().optional()
81
82
  })
82
83
  .nullable()
83
84
  .optional(),
@@ -87,20 +88,20 @@ export const TextAppearanceShape = z.object({
87
88
  color: ColorTypeShape.nullable().optional(),
88
89
  backgroundColor: ColorTypeShape.nullable().optional(),
89
90
  fontWeight: FontWeightShape.optional(),
90
- scale: z.number().positive().optional(),
91
- backgroundPaddingX: z.number().min(0).optional(),
92
- backgroundPaddingY: z.number().min(0).optional(),
93
- backgroundBorderRadius: z.number().min(0).optional()
91
+ scale: coercePositiveNumber().optional(),
92
+ backgroundPaddingX: coerceNonNegativeNumber().optional(),
93
+ backgroundPaddingY: coerceNonNegativeNumber().optional(),
94
+ backgroundBorderRadius: coerceNonNegativeNumber().optional()
94
95
  })
95
96
  .nullable()
96
97
  .optional(),
97
98
  highlightColors: z.array(ColorTypeShape).nullable().optional()
98
99
  });
99
100
  const BgShape = z.object({
100
- enabled: z.boolean().default(false),
101
+ enabled: z.boolean().prefault(false),
101
102
  color: ColorTypeShape,
102
- target: z.enum(['wrapper', 'element']).default('wrapper').optional(),
103
- radius: z.number().min(0).default(0).optional()
103
+ target: z.enum(['wrapper', 'element']).prefault('wrapper').optional(),
104
+ radius: z.number().min(0).prefault(0).optional()
104
105
  });
105
106
  export const BackgroundShape = z.union([BgShape, ColorTypeShape]).transform((value) => {
106
107
  if (typeof value === 'object' && value !== null && 'enabled' in value) {
@@ -117,31 +118,96 @@ export const BackgroundShape = z.union([BgShape, ColorTypeShape]).transform((val
117
118
  * General appearance schema for all components
118
119
  */
119
120
  export const AppearanceShape = z.object({
120
- x: z.number(),
121
- y: z.number(),
122
- width: z.number().positive(),
123
- height: z.number().positive(),
124
- offsetX: z.number().optional(), // 0 = left, 132 = 132px from left
125
- offsetY: z.number().optional(), // 0 = top, 132 = 132px from top
126
- opacity: z.number().min(0).max(1).default(1).optional(),
127
- rotation: z.number().default(0).optional(),
128
- scaleX: z.number().default(1).optional(),
129
- scaleY: z.number().default(1).optional(),
130
- background: BackgroundShape.nullable().default(null).optional(), // ColorTypeShape
121
+ x: coerceValidNumber(),
122
+ y: coerceValidNumber(),
123
+ width: coercePositiveNumber(),
124
+ height: coercePositiveNumber(),
125
+ offsetX: coerceValidNumber().optional(), // 0 = left, 132 = 132px from left
126
+ offsetY: coerceValidNumber().optional(), // 0 = top, 132 = 132px from top
127
+ opacity: coerceNormalizedNumber().prefault(1).optional(),
128
+ rotation: coerceValidNumber().prefault(0).optional(),
129
+ scaleX: coerceValidNumber().prefault(1).optional(),
130
+ scaleY: coerceValidNumber().prefault(1).optional(),
131
+ background: BackgroundShape.nullable().prefault(null).optional(), // ColorTypeShape
131
132
  // Text-specific appearance properties
132
133
  text: TextAppearanceShape.optional(),
133
134
  // Optional alignment properties
134
135
  verticalAlign: z.enum(['top', 'center', 'bottom']).optional(),
135
136
  horizontalAlign: z.enum(['left', 'center', 'right']).optional(),
136
137
  // not optimal but to prevent linter errors. This is for subtitles only
137
- backgroundAlwaysVisible: z.boolean().default(false).optional()
138
+ backgroundAlwaysVisible: z.boolean().prefault(false).optional()
139
+ });
140
+ /**
141
+ * Specialized appearance shapes for different component types
142
+ * These are NOT exported to reduce .d.ts file size - they're only used internally
143
+ */
144
+ /**
145
+ * Text-focused appearance shape for TEXT components
146
+ */
147
+ const TextFocusedAppearanceShape = AppearanceShape.extend({
148
+ text: TextAppearanceShape,
149
+ verticalAlign: z.enum(['top', 'center', 'bottom']).optional(),
150
+ horizontalAlign: z.enum(['left', 'center', 'right']).optional()
151
+ });
152
+ /**
153
+ * Shape appearance for SHAPE components
154
+ */
155
+ const ShapeAppearanceShape = AppearanceShape.extend({
156
+ color: ColorTypeShape.optional() // fill color
157
+ });
158
+ /**
159
+ * Color appearance for COLOR components
160
+ */
161
+ const ColorAppearanceShape = AppearanceShape.extend({
162
+ background: z.string().refine(isValidColor, {
163
+ error: 'Invalid color format for ColorComponent background'
164
+ })
165
+ });
166
+ /**
167
+ * Gradient appearance for GRADIENT components
168
+ */
169
+ const GradientAppearanceShape = AppearanceShape.extend({
170
+ background: GradientDefinitionShape // Requires a gradient type in background
171
+ });
172
+ /**
173
+ * AI Emoji shape for subtitle components
174
+ */
175
+ export const AIEmojiShape = z.object({
176
+ text: z.string(),
177
+ emoji: z.string(),
178
+ startAt: coerceValidNumber(),
179
+ endAt: coerceValidNumber(),
180
+ componentId: z.string().optional()
181
+ }).refine((data) => data.startAt <= data.endAt, {
182
+ error: 'endAt must be greater than or equal to startAt',
183
+ path: ['endAt']
184
+ });
185
+ /**
186
+ * Subtitle appearance for SUBTITLES components
187
+ */
188
+ const SubtitleAppearanceShape = AppearanceShape.extend({
189
+ text: TextAppearanceShape,
190
+ verticalAlign: z.enum(['top', 'center', 'bottom']).optional(),
191
+ horizontalAlign: z.enum(['left', 'center', 'right']).optional(),
192
+ hasAIEmojis: z.boolean().prefault(false).optional(),
193
+ aiEmojisPlacement: z.enum(['top', 'bottom']).prefault('top').optional(),
194
+ aiEmojisPlacementOffset: coerceValidNumber().prefault(30).optional(),
195
+ aiEmojis: z.array(AIEmojiShape).optional(),
196
+ highlighterColor1: ColorTypeShape.optional(),
197
+ highlighterColor2: ColorTypeShape.optional(),
198
+ highlighterColor3: ColorTypeShape.optional()
138
199
  });
139
200
  /**
140
201
  * Timeline structure for components
141
202
  */
142
- export const ComponentTimelineShape = z.object({
143
- startAt: z.number().min(0).transform(toFixed3),
144
- endAt: z.number().min(0).transform(toFixed3)
203
+ export const ComponentTimelineShape = z
204
+ .object({
205
+ startAt: coerceNonNegativeNumber().transform(toFixed3),
206
+ endAt: coerceNonNegativeNumber().transform(toFixed3)
207
+ })
208
+ .refine((t) => t.startAt <= t.endAt, {
209
+ message: 'timeline endAt must be ≥ startAt',
210
+ path: ['endAt']
145
211
  });
146
212
  /**
147
213
  * Animation structure
@@ -149,35 +215,50 @@ export const ComponentTimelineShape = z.object({
149
215
  export const AnimationShape = z.object({
150
216
  id: z.string(),
151
217
  name: z.string(),
152
- startAt: z.number().min(0).optional(),
218
+ startAt: coerceNonNegativeNumber().optional(),
153
219
  animation: AnimationReferenceShape,
154
- enabled: z.boolean().default(true).optional()
220
+ enabled: z.boolean().prefault(true).optional()
155
221
  });
156
222
  /**
157
223
  * Source metadata for media components
158
224
  */
159
225
  export const SourceMetadataShape = z.object({
160
- width: z.number().positive().optional(),
161
- height: z.number().positive().optional(),
162
- duration: z.number().min(0).optional(),
226
+ width: coercePositiveNumber().optional(),
227
+ height: coercePositiveNumber().optional(),
228
+ duration: coerceNonNegativeNumber().optional(),
163
229
  format: z.string().optional(),
164
230
  codec: z.string().optional(),
165
- bitrate: z.number().positive().optional(),
166
- fps: z.number().positive().optional(),
231
+ bitrate: coercePositiveNumber().optional(),
232
+ fps: coercePositiveNumber().optional(),
167
233
  hasAudio: z.boolean().optional()
168
234
  });
169
235
  /**
170
236
  * Component source definition
171
237
  */
172
- export const ComponentSourceShape = z.object({
173
- url: z.string().url().optional(), // might have assetId. However should be required for video and other components
174
- streamUrl: z.string().url().optional(),
238
+ export const ComponentSourceShape = z
239
+ .object({
240
+ url: z.url().optional(), // might have assetId. However should be required for video and other components
241
+ streamUrl: z.url().optional(),
175
242
  assetId: z.string().optional(),
176
243
  languageCode: z.string().optional(),
177
- startAt: z.number().min(0).optional().transform(toFixed3Optional),
178
- endAt: z.number().min(0).optional().transform(toFixed3Optional),
244
+ startAt: coerceNonNegativeNumber().transform(toFixed3Optional).optional(),
245
+ endAt: coerceNonNegativeNumber().transform(toFixed3Optional).optional(),
179
246
  metadata: SourceMetadataShape.optional(),
180
247
  transcriptFormat: z.string().optional()
248
+ })
249
+ .refine((data) => {
250
+ // Only validate if both startAt and endAt are present and not null
251
+ if (data.startAt !== undefined &&
252
+ data.startAt !== null &&
253
+ data.endAt !== undefined &&
254
+ data.endAt !== null) {
255
+ return data.startAt <= data.endAt;
256
+ }
257
+ // If either is undefined or null, validation passes
258
+ return true;
259
+ }, {
260
+ message: 'endAt must be greater than or equal to startAt',
261
+ path: ['endAt'] // Error targets the endAt field
181
262
  });
182
263
  /**
183
264
  * Timing anchor for components that need synchronization (like subtitles)
@@ -187,32 +268,32 @@ export const TimingAnchorShape = z.object({
187
268
  assetId: z.string().optional(),
188
269
  layerId: z.string().optional(),
189
270
  componentId: z.string().optional(),
190
- offset: z.number().default(0)
271
+ offset: coerceValidNumber().prefault(0)
191
272
  });
192
273
  /**
193
274
  * Layout split effect schema
194
275
  */
195
276
  export const LayoutSplitEffectShape = EffectBaseShape.extend({
196
277
  type: z.literal('layoutSplit'),
197
- pieces: z.number().int().positive().optional(),
198
- sceneWidth: z.number().positive().optional(),
199
- sceneHeight: z.number().positive().optional(),
200
- chunks: z.array(z.record(z.any())).optional()
278
+ pieces: coerceInteger(1).optional(),
279
+ sceneWidth: coercePositiveNumber().optional(),
280
+ sceneHeight: coercePositiveNumber().optional(),
281
+ chunks: z.array(z.record(z.string(), z.any())).optional()
201
282
  });
202
283
  /**
203
284
  * Rotation randomizer effect schema
204
285
  */
205
286
  export const RotationRandomizerEffectShape = EffectBaseShape.extend({
206
287
  type: z.literal('rotationRandomizer'),
207
- maxRotation: z.number().default(2),
208
- animate: z.boolean().default(true),
209
- seed: z.number().int().optional()
288
+ maxRotation: coerceValidNumber().prefault(2),
289
+ animate: z.boolean().prefault(true),
290
+ seed: coerceInteger().optional()
210
291
  });
211
292
  export const FillBackgroundBlurEffectShape = z.object({
212
293
  type: z.literal('fillBackgroundBlur'),
213
- enabled: z.boolean().default(true),
294
+ enabled: z.boolean().prefault(true),
214
295
  // Using the hardcoded value from PixiSplitScreenDisplayObjectHook.ts as default
215
- blurAmount: z.number().min(0).default(50)
296
+ blurAmount: coerceNonNegativeNumber().prefault(50)
216
297
  // Note: intensity and blendMode might not be applicable here, keeping it simple.
217
298
  });
218
299
  /**
@@ -244,7 +325,7 @@ export const ComponentEffectShape = z.union([
244
325
  TextOutlineEffectShape
245
326
  ]);
246
327
  const ComponentEffectsShape = z.object({
247
- enabled: z.boolean().optional().default(true), // Globally enable/disable all effects?
328
+ enabled: z.boolean().optional().prefault(true), // Globally enable/disable all effects?
248
329
  map: z
249
330
  .union([
250
331
  z.record(z.string().min(1), // Key: Effect name/ID (e.g., "mainBlur", "rotator")
@@ -260,17 +341,18 @@ const ComponentEffectsShape = z.object({
260
341
  // If it's already an object, return as is
261
342
  return val;
262
343
  })
263
- .default({}) // Default to an empty object if no effects
344
+ .prefault({}) // Default to an empty object if no effects
264
345
  });
265
346
  const ComponentAnimationsShape = z.object({
266
- enabled: z.boolean().optional().default(true), // Globally enable/disable all animations?
267
- list: z.array(AnimationShape).default([]),
268
- subtitlesSeed: z.number().int().optional()
347
+ enabled: z.boolean().optional().prefault(true), // Globally enable/disable all animations?
348
+ list: z.array(AnimationShape).prefault([]),
349
+ subtitlesSeed: z.int().optional()
269
350
  });
270
351
  /**
271
- * Base component schema that all component types will extend
352
+ * Base component schema without appearance (to reduce type duplication)
353
+ * Each component type will add its own specialized appearance
272
354
  */
273
- export const ComponentBaseShape = z.object({
355
+ const ComponentBaseWithoutAppearanceShape = z.object({
274
356
  id: z.string(),
275
357
  name: z.string().optional(),
276
358
  type: z.enum([
@@ -285,77 +367,96 @@ export const ComponentBaseShape = z.object({
285
367
  'SUBTITLES'
286
368
  ]),
287
369
  timeline: ComponentTimelineShape,
288
- appearance: AppearanceShape,
289
- animations: ComponentAnimationsShape.default({}),
290
- effects: ComponentEffectsShape.default({}),
291
- visible: z.boolean().default(true),
292
- order: z.number().default(0),
370
+ animations: ComponentAnimationsShape.prefault({}),
371
+ effects: ComponentEffectsShape.prefault({}),
372
+ visible: z.boolean().prefault(true),
373
+ order: coerceValidNumber().prefault(0),
293
374
  checksum: z.string().optional()
294
375
  });
376
+ /**
377
+ * Base component schema that all component types will extend
378
+ * @deprecated Use ComponentBaseWithoutAppearanceShape and add appearance per component
379
+ */
380
+ export const ComponentBaseShape = ComponentBaseWithoutAppearanceShape.extend({
381
+ appearance: AppearanceShape
382
+ });
295
383
  /**
296
384
  * Text component schema
297
385
  */
298
- export const TextComponentShape = ComponentBaseShape.extend({
386
+ export const TextComponentShape = ComponentBaseWithoutAppearanceShape.extend({
299
387
  type: z.literal('TEXT'),
300
388
  text: z.string(),
301
- isAIEmoji: z.boolean().default(false).optional(),
302
- appearance: AppearanceShape.extend({
303
- text: TextAppearanceShape,
304
- verticalAlign: z.enum(['top', 'center', 'bottom']).optional(),
305
- horizontalAlign: z.enum(['left', 'center', 'right']).optional()
306
- })
389
+ isAIEmoji: z.boolean().prefault(false).optional(),
390
+ appearance: TextFocusedAppearanceShape
307
391
  }).strict();
308
392
  /**
309
393
  * Image component schema
310
394
  */
311
- export const ImageComponentShape = ComponentBaseShape.extend({
395
+ export const ImageComponentShape = ComponentBaseWithoutAppearanceShape.extend({
312
396
  type: z.literal('IMAGE'),
313
397
  source: ComponentSourceShape,
398
+ appearance: AppearanceShape,
314
399
  crop: z
315
400
  .object({
316
- xPercent: z.number().min(0).max(1).default(0),
317
- yPercent: z.number().min(0).max(1).default(0),
318
- widthPercent: z.number().min(0).max(1).default(1),
319
- heightPercent: z.number().min(0).max(1).default(1)
401
+ xPercent: coerceNormalizedNumber().prefault(0),
402
+ yPercent: coerceNormalizedNumber().prefault(0),
403
+ widthPercent: coerceNormalizedNumber().prefault(1),
404
+ heightPercent: coerceNormalizedNumber().prefault(1)
320
405
  })
321
406
  .optional()
322
407
  }).strict();
323
408
  /**
324
409
  * GIF component schema
325
410
  */
326
- export const GifComponentShape = ComponentBaseShape.extend({
411
+ export const GifComponentShape = ComponentBaseWithoutAppearanceShape.extend({
327
412
  type: z.literal('GIF'),
328
413
  source: ComponentSourceShape,
414
+ appearance: AppearanceShape,
329
415
  playback: z
330
416
  .object({
331
- loop: z.boolean().default(true),
332
- speed: z.number().positive().default(1)
417
+ loop: z.boolean().prefault(true),
418
+ speed: coercePositiveNumber().prefault(1)
333
419
  })
334
420
  .optional()
335
421
  }).strict();
336
422
  /**
337
423
  * Video component schema
338
424
  */
339
- export const VideoComponentShape = ComponentBaseShape.extend({
425
+ export const VideoComponentShape = ComponentBaseWithoutAppearanceShape.extend({
340
426
  type: z.literal('VIDEO'),
341
427
  source: ComponentSourceShape,
342
- volume: z.number().min(0).max(1).default(1),
343
- muted: z.boolean().default(false),
428
+ appearance: AppearanceShape,
429
+ volume: coerceNormalizedNumber().prefault(1),
430
+ muted: z.boolean().prefault(false),
344
431
  playback: z
345
432
  .object({
346
- autoplay: z.boolean().default(true),
347
- loop: z.boolean().default(false),
348
- playbackRate: z.number().positive().default(1),
349
- startAt: z.number().min(0).default(0),
350
- endAt: z.number().optional()
433
+ autoplay: z.boolean().prefault(true),
434
+ loop: z.boolean().prefault(false),
435
+ playbackRate: coercePositiveNumber().prefault(1),
436
+ startAt: coerceNonNegativeNumber().prefault(0),
437
+ endAt: coerceValidNumber().optional()
438
+ })
439
+ .refine((data) => {
440
+ // Only validate if both startAt and endAt are present and not null
441
+ if (data.startAt !== undefined &&
442
+ data.startAt !== null &&
443
+ data.endAt !== undefined &&
444
+ data.endAt !== null) {
445
+ return data.startAt <= data.endAt;
446
+ }
447
+ // If either is undefined or null, validation passes
448
+ return true;
449
+ }, {
450
+ error: 'endAt must be greater than or equal to startAt',
451
+ path: ['endAt']
351
452
  })
352
453
  .optional(),
353
454
  crop: z
354
455
  .object({
355
- x: z.number().default(0),
356
- y: z.number().default(0),
357
- width: z.number().min(0).max(1).default(1),
358
- height: z.number().min(0).max(1).default(1)
456
+ x: coerceValidNumber().prefault(0),
457
+ y: coerceValidNumber().prefault(0),
458
+ width: coerceNormalizedNumber().prefault(1),
459
+ height: coerceNormalizedNumber().prefault(1)
359
460
  })
360
461
  .optional()
361
462
  }).strict();
@@ -364,23 +465,25 @@ export const VideoComponentShape = ComponentBaseShape.extend({
364
465
  */
365
466
  export const LinearProgressConfigShape = z.object({
366
467
  type: z.literal('linear'),
367
- direction: z.enum(['horizontal', 'vertical']).default('horizontal'),
368
- reverse: z.boolean().default(false).optional(),
369
- anchor: z.enum(['start', 'center', 'end']).default('start').optional()
468
+ direction: z.enum(['horizontal', 'vertical']).prefault('horizontal'),
469
+ reverse: z.boolean().prefault(false).optional(),
470
+ anchor: z.enum(['start', 'center', 'end']).prefault('start').optional()
370
471
  });
371
472
  export const PerimeterProgressConfigShape = z.object({
372
473
  type: z.literal('perimeter'),
373
- startCorner: z.enum(['top-left', 'top-right', 'bottom-right', 'bottom-left']).default('top-left'),
374
- clockwise: z.boolean().default(true).optional(),
375
- strokeWidth: z.number().positive().default(4).optional()
474
+ startCorner: z
475
+ .enum(['top-left', 'top-right', 'bottom-right', 'bottom-left'])
476
+ .prefault('top-left'),
477
+ clockwise: z.boolean().prefault(true).optional(),
478
+ strokeWidth: coercePositiveNumber().prefault(4).optional()
376
479
  });
377
480
  export const RadialProgressConfigShape = z.object({
378
481
  type: z.literal('radial'),
379
- startAngle: z.number().default(-90).optional(), // -90 = top (12 o'clock), 0 = right (3 o'clock)
380
- clockwise: z.boolean().default(true).optional(),
381
- innerRadius: z.number().min(0).max(1).default(0).optional(), // 0 = filled circle, >0 = ring/donut
382
- strokeWidth: z.number().positive().optional(), // For ring style
383
- capStyle: z.enum(['butt', 'round', 'square']).default('round').optional()
482
+ startAngle: coerceValidNumber().prefault(-90).optional(), // -90 = top (12 o'clock), 0 = right (3 o'clock)
483
+ clockwise: z.boolean().prefault(true).optional(),
484
+ innerRadius: coerceNormalizedNumber().prefault(0).optional(), // 0 = filled circle, >0 = ring/donut
485
+ strokeWidth: coercePositiveNumber().optional(), // For ring style
486
+ capStyle: z.enum(['butt', 'round', 'square']).prefault('round').optional()
384
487
  });
385
488
  export const DoubleProgressConfigShape = z.object({
386
489
  type: z.literal('double'),
@@ -388,8 +491,8 @@ export const DoubleProgressConfigShape = z.object({
388
491
  .array(z.object({
389
492
  direction: z.enum(['horizontal', 'vertical']),
390
493
  position: z.enum(['top', 'bottom', 'left', 'right']),
391
- reverse: z.boolean().default(false).optional(),
392
- offset: z.number().default(0).optional() // Offset from edge in pixels
494
+ reverse: z.boolean().prefault(false).optional(),
495
+ offset: coerceValidNumber().prefault(0).optional() // Offset from edge in pixels
393
496
  }))
394
497
  .min(2)
395
498
  .max(4) // At least 2 paths, max 4 for performance
@@ -397,8 +500,8 @@ export const DoubleProgressConfigShape = z.object({
397
500
  export const CustomProgressConfigShape = z.object({
398
501
  type: z.literal('custom'),
399
502
  pathData: z.string(), // SVG path data for custom progress shapes
400
- strokeWidth: z.number().positive().default(4).optional(),
401
- capStyle: z.enum(['butt', 'round', 'square']).default('round').optional()
503
+ strokeWidth: coercePositiveNumber().prefault(4).optional(),
504
+ capStyle: z.enum(['butt', 'round', 'square']).prefault('round').optional()
402
505
  });
403
506
  /**
404
507
  * Union of all progress configuration types
@@ -413,13 +516,13 @@ export const ProgressConfigShape = z.discriminatedUnion('type', [
413
516
  /**
414
517
  * Shape component schema for basic geometric shapes
415
518
  */
416
- export const ShapeComponentShape = ComponentBaseShape.extend({
519
+ export const ShapeComponentShape = ComponentBaseWithoutAppearanceShape.extend({
417
520
  type: z.literal('SHAPE'),
418
521
  shape: z.union([
419
522
  // Progress shape with specialized configuration
420
523
  z.object({
421
524
  type: z.literal('progress'),
422
- progressConfig: ProgressConfigShape.optional().default({
525
+ progressConfig: ProgressConfigShape.optional().prefault({
423
526
  type: 'linear',
424
527
  direction: 'horizontal',
425
528
  reverse: false,
@@ -435,69 +538,44 @@ export const ShapeComponentShape = ComponentBaseShape.extend({
435
538
  cornerRadius: z.number().min(0).optional() // For rectangle
436
539
  })
437
540
  ]),
438
- appearance: AppearanceShape.extend({
439
- color: ColorTypeShape.optional() // fill color
440
- })
541
+ appearance: ShapeAppearanceShape
441
542
  }).strict();
442
543
  /**
443
544
  * Audio component schema
444
545
  */
445
- export const AudioComponentShape = ComponentBaseShape.extend({
546
+ export const AudioComponentShape = ComponentBaseWithoutAppearanceShape.extend({
446
547
  type: z.literal('AUDIO'),
447
548
  source: ComponentSourceShape,
448
- volume: z.number().min(0).max(1).default(1),
449
- muted: z.boolean().default(false)
549
+ appearance: AppearanceShape,
550
+ volume: coerceNormalizedNumber().prefault(1),
551
+ muted: z.boolean().prefault(false)
450
552
  }).strict();
451
553
  /**
452
554
  * Color component schema
453
555
  */
454
- export const ColorComponentShape = ComponentBaseShape.extend({
556
+ export const ColorComponentShape = ComponentBaseWithoutAppearanceShape.extend({
455
557
  type: z.literal('COLOR'),
456
- appearance: AppearanceShape.extend({
457
- background: z
458
- .string()
459
- .refine(isValidColor, { message: 'Invalid color format for ColorComponent background' })
460
- })
558
+ appearance: ColorAppearanceShape
461
559
  }).strict();
462
560
  /**
463
561
  * Gradient component schema
464
562
  */
465
- export const GradientComponentShape = ComponentBaseShape.extend({
563
+ export const GradientComponentShape = ComponentBaseWithoutAppearanceShape.extend({
466
564
  type: z.literal('GRADIENT'),
467
- appearance: AppearanceShape.extend({
468
- background: GradientDefinitionShape // Requires a gradient type in background
469
- })
565
+ appearance: GradientAppearanceShape
470
566
  }).strict();
471
567
  /**
472
568
  * Subtitles component schema
473
569
  */
474
- export const AIEmojiShape = z.object({
475
- text: z.string(),
476
- emoji: z.string(),
477
- startAt: z.number(),
478
- endAt: z.number(),
479
- componentId: z.string().optional()
480
- });
481
- export const SubtitleComponentShape = ComponentBaseShape.extend({
570
+ export const SubtitleComponentShape = ComponentBaseWithoutAppearanceShape.extend({
482
571
  type: z.literal('SUBTITLES'),
483
- source: ComponentSourceShape.extend({
484
- url: z.string().url().optional()
572
+ source: ComponentSourceShape.safeExtend({
573
+ url: z.url().optional()
485
574
  // Subtitles might need specific source fields, e.g., format
486
575
  }).optional(),
487
576
  timingAnchor: TimingAnchorShape,
488
577
  text: z.string().optional(), // Optional: if text is directly embedded
489
- appearance: AppearanceShape.extend({
490
- text: TextAppearanceShape,
491
- verticalAlign: z.enum(['top', 'center', 'bottom']).optional(),
492
- horizontalAlign: z.enum(['left', 'center', 'right']).optional(),
493
- hasAIEmojis: z.boolean().default(false).optional(),
494
- aiEmojisPlacement: z.enum(['top', 'bottom']).default('top').optional(),
495
- aiEmojisPlacementOffset: z.number().default(30).optional(),
496
- aiEmojis: z.array(AIEmojiShape).optional(),
497
- highlighterColor1: ColorTypeShape.optional(),
498
- highlighterColor2: ColorTypeShape.optional(),
499
- highlighterColor3: ColorTypeShape.optional()
500
- })
578
+ appearance: SubtitleAppearanceShape
501
579
  }).strict();
502
580
  /**
503
581
  * Union of all component types for polymorphic handling