visualfries 0.1.102 → 0.1.103

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.
@@ -276,6 +276,25 @@ export class SceneBuilder {
276
276
  },
277
277
  checksum: 'new-' + uuidv4()
278
278
  };
279
+ // For VIDEO components, adjust source.startAt for the second part
280
+ // source.startAt determines at what point in the video file playback starts
281
+ // when the timeline reaches timeline.startAt
282
+ if (cloneData.type === 'VIDEO') {
283
+ const videoClone = cloneData;
284
+ // Calculate the time offset from original component start to split point
285
+ const timeOffsetFromStart = currentTime - compStart;
286
+ // Get current source.startAt (or 0 if not set/undefined)
287
+ const currentSourceStartAt = videoClone.source?.startAt ?? 0;
288
+ // The new source.startAt is the original offset plus the split offset
289
+ const newSourceStartAt = currentSourceStartAt + timeOffsetFromStart;
290
+ // Ensure source object exists and update startAt
291
+ if (!videoClone.source) {
292
+ videoClone.source = { startAt: newSourceStartAt };
293
+ }
294
+ else {
295
+ videoClone.source.startAt = newSourceStartAt;
296
+ }
297
+ }
279
298
  // Update original component's end time
280
299
  component.props.setEnd(this.currentTime);
281
300
  // Add the cloned component to the same layer
@@ -179,8 +179,10 @@ export class ComponentState {
179
179
  // }
180
180
  }
181
181
  async updateAppearance(appearance) {
182
- const mergedAppearance = merge({}, this.#data.appearance, appearance);
183
- this.#data = { ...this.#data, appearance: mergedAppearance };
182
+ // Use $state.snapshot() to properly extract all properties from the reactive proxy
183
+ const currentData = $state.snapshot(this.#data);
184
+ const mergedAppearance = merge({}, currentData.appearance, appearance);
185
+ this.#data = { ...currentData, appearance: mergedAppearance };
184
186
  this.#emitChange();
185
187
  await this.maybeAutoRefresh();
186
188
  }
@@ -23,6 +23,21 @@ export class SeekCommand {
23
23
  this.timeline.seek(time);
24
24
  // Ensure a deterministic render on server after seek to advance media frames
25
25
  if (this.state.environment === 'server') {
26
+ // Wait for fonts to be ready before rendering
27
+ // This is critical for subtitle animations that use SplitText -
28
+ // if fonts aren't loaded, text measurements will be wrong and
29
+ // animations may fail silently or not appear on first subtitles
30
+ if (typeof document !== 'undefined' && document.fonts?.ready) {
31
+ try {
32
+ await Promise.race([
33
+ document.fonts.ready,
34
+ new Promise((resolve) => setTimeout(resolve, 2000)) // 2s timeout
35
+ ]);
36
+ }
37
+ catch {
38
+ // Ignore font loading errors, continue with rendering
39
+ }
40
+ }
26
41
  // Try multiple render passes until loading state clears or attempts exhausted
27
42
  const maxAttempts = 10;
28
43
  for (let i = 0; i < maxAttempts; i += 1) {
@@ -34,6 +49,11 @@ export class SeekCommand {
34
49
  if (this.state.state === 'loading') {
35
50
  console.warn('SeekCommand: Max render attempts exhausted while still loading');
36
51
  }
52
+ // Re-seek to apply correct animation state to any animations
53
+ // that were added during the render passes above.
54
+ // This fixes the race condition where subtitle animations are added
55
+ // AFTER the initial seek, causing them to miss their initial state.
56
+ this.timeline.seek(time);
37
57
  }
38
58
  }
39
59
  }
@@ -98,7 +98,22 @@ export class ComponentContext {
98
98
  const sortedHooks = [...hooks].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
99
99
  for (let i = 0; i < sortedHooks.length; i += 1) {
100
100
  const handler = sortedHooks[i];
101
- await handler.handle(type, this);
101
+ try {
102
+ await handler.handle(type, this);
103
+ }
104
+ catch (error) {
105
+ // Log the error but continue to next hook
106
+ const hookName = handler.constructor?.name ?? `Hook[${i}]`;
107
+ console.warn(`[ComponentContext] Hook "${hookName}" failed during "${type}" for component "${this.id}":`, error instanceof Error ? error.message : String(error));
108
+ // Emit error event for debugging/monitoring
109
+ this.eventManager.emit('hookerror', {
110
+ hookName,
111
+ hookType: type,
112
+ error: error instanceof Error ? error : new Error(String(error)),
113
+ componentId: this.id,
114
+ timestamp: Date.now()
115
+ });
116
+ }
102
117
  }
103
118
  }
104
119
  destroy() { }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Component Context Helpers
3
+ *
4
+ * Type guards and helper functions for type-safe access to component-specific data
5
+ * in hook contexts. Eliminates the need for `as any` casts throughout the codebase.
6
+ */
7
+ import type { Component, VideoComponent, AudioComponent, ImageComponent, GifComponent, TextComponent, SubtitleComponent, ShapeComponent, ColorComponent, GradientComponent, ComponentSource, IComponentContext } from '..';
8
+ export declare const MEDIA_COMPONENT_TYPES: readonly ["VIDEO", "AUDIO"];
9
+ export declare const SOURCE_COMPONENT_TYPES: readonly ["VIDEO", "AUDIO", "IMAGE", "GIF"];
10
+ export declare const VISUAL_COMPONENT_TYPES: readonly ["VIDEO", "IMAGE", "GIF", "TEXT", "SHAPE", "SUBTITLES", "COLOR", "GRADIENT"];
11
+ export type MediaComponentType = (typeof MEDIA_COMPONENT_TYPES)[number];
12
+ export type SourceComponentType = (typeof SOURCE_COMPONENT_TYPES)[number];
13
+ /**
14
+ * Type guard for VIDEO component
15
+ */
16
+ export declare function isVideoComponent(data: Component | undefined): data is VideoComponent;
17
+ /**
18
+ * Type guard for AUDIO component
19
+ */
20
+ export declare function isAudioComponent(data: Component | undefined): data is AudioComponent;
21
+ /**
22
+ * Type guard for IMAGE component
23
+ */
24
+ export declare function isImageComponent(data: Component | undefined): data is ImageComponent;
25
+ /**
26
+ * Type guard for GIF component
27
+ */
28
+ export declare function isGifComponent(data: Component | undefined): data is GifComponent;
29
+ /**
30
+ * Type guard for TEXT component
31
+ */
32
+ export declare function isTextComponent(data: Component | undefined): data is TextComponent;
33
+ /**
34
+ * Type guard for SUBTITLES component
35
+ */
36
+ export declare function isSubtitleComponent(data: Component | undefined): data is SubtitleComponent;
37
+ /**
38
+ * Type guard for SHAPE component
39
+ */
40
+ export declare function isShapeComponent(data: Component | undefined): data is ShapeComponent;
41
+ /**
42
+ * Type guard for COLOR component
43
+ */
44
+ export declare function isColorComponent(data: Component | undefined): data is ColorComponent;
45
+ /**
46
+ * Type guard for GRADIENT component
47
+ */
48
+ export declare function isGradientComponent(data: Component | undefined): data is GradientComponent;
49
+ /**
50
+ * Type guard for components with a media source (VIDEO, AUDIO, IMAGE, GIF)
51
+ */
52
+ export declare function hasSource(data: Component | undefined): data is VideoComponent | AudioComponent | ImageComponent | GifComponent;
53
+ /**
54
+ * Type guard for media components (VIDEO, AUDIO)
55
+ */
56
+ export declare function isMediaComponent(data: Component | undefined): data is VideoComponent | AudioComponent;
57
+ /**
58
+ * Safely get source from component data
59
+ */
60
+ export declare function getSource(data: Component | undefined): ComponentSource | undefined;
61
+ /**
62
+ * Safely get source.url from component data
63
+ */
64
+ export declare function getSourceUrl(data: Component | undefined): string | undefined;
65
+ /**
66
+ * Safely get source.startAt from component data (for VIDEO/AUDIO)
67
+ */
68
+ export declare function getSourceStartAt(data: Component | undefined): number | undefined;
69
+ /**
70
+ * Safely get muted state from media component
71
+ */
72
+ export declare function getMuted(data: Component | undefined): boolean;
73
+ /**
74
+ * Safely get volume from media component
75
+ */
76
+ export declare function getVolume(data: Component | undefined): number;
77
+ /**
78
+ * Safely get text content from TEXT component
79
+ */
80
+ export declare function getTextContent(data: Component | undefined): string | undefined;
81
+ /**
82
+ * Get typed component data from context
83
+ */
84
+ export declare function getContextData<T extends Component>(context: IComponentContext, typeGuard: (data: Component | undefined) => data is T): T | undefined;
85
+ /**
86
+ * Get video component data from context
87
+ */
88
+ export declare function getVideoData(context: IComponentContext): VideoComponent | undefined;
89
+ /**
90
+ * Get audio component data from context
91
+ */
92
+ export declare function getAudioData(context: IComponentContext): AudioComponent | undefined;
93
+ /**
94
+ * Get image component data from context
95
+ */
96
+ export declare function getImageData(context: IComponentContext): ImageComponent | undefined;
97
+ /**
98
+ * Get gif component data from context
99
+ */
100
+ export declare function getGifData(context: IComponentContext): GifComponent | undefined;
101
+ /**
102
+ * Get text component data from context
103
+ */
104
+ export declare function getTextData(context: IComponentContext): TextComponent | undefined;
105
+ /**
106
+ * Get source from context's component data
107
+ */
108
+ export declare function getContextSource(context: IComponentContext): ComponentSource | undefined;
109
+ /**
110
+ * Get media properties (muted, volume) from context
111
+ */
112
+ export declare function getMediaProps(context: IComponentContext): {
113
+ muted: boolean;
114
+ volume: number;
115
+ };
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Component Context Helpers
3
+ *
4
+ * Type guards and helper functions for type-safe access to component-specific data
5
+ * in hook contexts. Eliminates the need for `as any` casts throughout the codebase.
6
+ */
7
+ // ============================================================================
8
+ // Component Type Constants
9
+ // ============================================================================
10
+ export const MEDIA_COMPONENT_TYPES = ['VIDEO', 'AUDIO'];
11
+ export const SOURCE_COMPONENT_TYPES = ['VIDEO', 'AUDIO', 'IMAGE', 'GIF'];
12
+ export const VISUAL_COMPONENT_TYPES = ['VIDEO', 'IMAGE', 'GIF', 'TEXT', 'SHAPE', 'SUBTITLES', 'COLOR', 'GRADIENT'];
13
+ // ============================================================================
14
+ // Type Guards for Component Data
15
+ // ============================================================================
16
+ /**
17
+ * Type guard for VIDEO component
18
+ */
19
+ export function isVideoComponent(data) {
20
+ return data?.type === 'VIDEO';
21
+ }
22
+ /**
23
+ * Type guard for AUDIO component
24
+ */
25
+ export function isAudioComponent(data) {
26
+ return data?.type === 'AUDIO';
27
+ }
28
+ /**
29
+ * Type guard for IMAGE component
30
+ */
31
+ export function isImageComponent(data) {
32
+ return data?.type === 'IMAGE';
33
+ }
34
+ /**
35
+ * Type guard for GIF component
36
+ */
37
+ export function isGifComponent(data) {
38
+ return data?.type === 'GIF';
39
+ }
40
+ /**
41
+ * Type guard for TEXT component
42
+ */
43
+ export function isTextComponent(data) {
44
+ return data?.type === 'TEXT';
45
+ }
46
+ /**
47
+ * Type guard for SUBTITLES component
48
+ */
49
+ export function isSubtitleComponent(data) {
50
+ return data?.type === 'SUBTITLES';
51
+ }
52
+ /**
53
+ * Type guard for SHAPE component
54
+ */
55
+ export function isShapeComponent(data) {
56
+ return data?.type === 'SHAPE';
57
+ }
58
+ /**
59
+ * Type guard for COLOR component
60
+ */
61
+ export function isColorComponent(data) {
62
+ return data?.type === 'COLOR';
63
+ }
64
+ /**
65
+ * Type guard for GRADIENT component
66
+ */
67
+ export function isGradientComponent(data) {
68
+ return data?.type === 'GRADIENT';
69
+ }
70
+ /**
71
+ * Type guard for components with a media source (VIDEO, AUDIO, IMAGE, GIF)
72
+ */
73
+ export function hasSource(data) {
74
+ return (data !== undefined &&
75
+ SOURCE_COMPONENT_TYPES.includes(data.type));
76
+ }
77
+ /**
78
+ * Type guard for media components (VIDEO, AUDIO)
79
+ */
80
+ export function isMediaComponent(data) {
81
+ return (data !== undefined &&
82
+ MEDIA_COMPONENT_TYPES.includes(data.type));
83
+ }
84
+ // ============================================================================
85
+ // Safe Accessor Helpers
86
+ // ============================================================================
87
+ /**
88
+ * Safely get source from component data
89
+ */
90
+ export function getSource(data) {
91
+ if (hasSource(data)) {
92
+ return data.source;
93
+ }
94
+ return undefined;
95
+ }
96
+ /**
97
+ * Safely get source.url from component data
98
+ */
99
+ export function getSourceUrl(data) {
100
+ return getSource(data)?.url;
101
+ }
102
+ /**
103
+ * Safely get source.startAt from component data (for VIDEO/AUDIO)
104
+ */
105
+ export function getSourceStartAt(data) {
106
+ const startAt = getSource(data)?.startAt;
107
+ // Convert null to undefined for consistency
108
+ return startAt ?? undefined;
109
+ }
110
+ /**
111
+ * Safely get muted state from media component
112
+ */
113
+ export function getMuted(data) {
114
+ if (isVideoComponent(data) || isAudioComponent(data)) {
115
+ return data.muted ?? false;
116
+ }
117
+ return false;
118
+ }
119
+ /**
120
+ * Safely get volume from media component
121
+ */
122
+ export function getVolume(data) {
123
+ if (isVideoComponent(data) || isAudioComponent(data)) {
124
+ return data.volume ?? 1;
125
+ }
126
+ return 1;
127
+ }
128
+ /**
129
+ * Safely get text content from TEXT component
130
+ */
131
+ export function getTextContent(data) {
132
+ if (isTextComponent(data)) {
133
+ return data.text;
134
+ }
135
+ return undefined;
136
+ }
137
+ // ============================================================================
138
+ // Context Helper Functions
139
+ // ============================================================================
140
+ /**
141
+ * Get typed component data from context
142
+ */
143
+ export function getContextData(context, typeGuard) {
144
+ const data = context.contextData;
145
+ if (typeGuard(data)) {
146
+ return data;
147
+ }
148
+ return undefined;
149
+ }
150
+ /**
151
+ * Get video component data from context
152
+ */
153
+ export function getVideoData(context) {
154
+ return getContextData(context, isVideoComponent);
155
+ }
156
+ /**
157
+ * Get audio component data from context
158
+ */
159
+ export function getAudioData(context) {
160
+ return getContextData(context, isAudioComponent);
161
+ }
162
+ /**
163
+ * Get image component data from context
164
+ */
165
+ export function getImageData(context) {
166
+ return getContextData(context, isImageComponent);
167
+ }
168
+ /**
169
+ * Get gif component data from context
170
+ */
171
+ export function getGifData(context) {
172
+ return getContextData(context, isGifComponent);
173
+ }
174
+ /**
175
+ * Get text component data from context
176
+ */
177
+ export function getTextData(context) {
178
+ return getContextData(context, isTextComponent);
179
+ }
180
+ /**
181
+ * Get source from context's component data
182
+ */
183
+ export function getContextSource(context) {
184
+ const data = context.contextData;
185
+ return getSource(data);
186
+ }
187
+ /**
188
+ * Get media properties (muted, volume) from context
189
+ */
190
+ export function getMediaProps(context) {
191
+ const data = context.contextData;
192
+ return {
193
+ muted: getMuted(data),
194
+ volume: getVolume(data)
195
+ };
196
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Safe Hook Runner
3
+ *
4
+ * Utility for safely executing hooks with error boundaries.
5
+ * Prevents one failing hook from crashing the entire component lifecycle.
6
+ */
7
+ import type { EventManager } from '../managers/EventManager.js';
8
+ export interface HookError {
9
+ hookName: string;
10
+ hookType: string;
11
+ error: Error;
12
+ componentId: string;
13
+ timestamp: number;
14
+ }
15
+ export interface SafeHookRunnerOptions {
16
+ /**
17
+ * Whether to continue executing remaining hooks after one fails
18
+ * @default true
19
+ */
20
+ continueOnError?: boolean;
21
+ /**
22
+ * Whether to log errors to console
23
+ * @default true
24
+ */
25
+ logErrors?: boolean;
26
+ /**
27
+ * Event manager to emit error events
28
+ */
29
+ eventManager?: EventManager;
30
+ }
31
+ /**
32
+ * Safely execute a single hook handler with error boundary
33
+ */
34
+ export declare function safeExecuteHook<T>(hookName: string, hookType: string, componentId: string, handler: () => Promise<T> | T, options?: SafeHookRunnerOptions): Promise<{
35
+ success: boolean;
36
+ result?: T;
37
+ error?: HookError;
38
+ }>;
39
+ /**
40
+ * Execute multiple hooks in sequence with error boundaries
41
+ */
42
+ export declare function safeExecuteHooks(hooks: Array<{
43
+ name: string;
44
+ handler: () => Promise<void> | void;
45
+ }>, hookType: string, componentId: string, options?: SafeHookRunnerOptions): Promise<{
46
+ allSucceeded: boolean;
47
+ errors: HookError[];
48
+ }>;
49
+ /**
50
+ * Create a wrapped version of a hook handler that catches errors
51
+ */
52
+ export declare function createSafeHandler<T extends (...args: any[]) => Promise<any> | any>(hookName: string, hookType: string, componentId: string, handler: T, options?: SafeHookRunnerOptions): T;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Safe Hook Runner
3
+ *
4
+ * Utility for safely executing hooks with error boundaries.
5
+ * Prevents one failing hook from crashing the entire component lifecycle.
6
+ */
7
+ const defaultOptions = {
8
+ continueOnError: true,
9
+ logErrors: true
10
+ };
11
+ /**
12
+ * Safely execute a single hook handler with error boundary
13
+ */
14
+ export async function safeExecuteHook(hookName, hookType, componentId, handler, options = {}) {
15
+ const opts = { ...defaultOptions, ...options };
16
+ try {
17
+ const result = await handler();
18
+ return { success: true, result };
19
+ }
20
+ catch (err) {
21
+ const error = err instanceof Error ? err : new Error(String(err));
22
+ const hookError = {
23
+ hookName,
24
+ hookType,
25
+ error,
26
+ componentId,
27
+ timestamp: Date.now()
28
+ };
29
+ if (opts.logErrors) {
30
+ console.warn(`[SafeHookRunner] Hook "${hookName}" failed during "${hookType}" for component "${componentId}":`, error.message);
31
+ }
32
+ if (opts.eventManager) {
33
+ opts.eventManager.emit('hookerror', hookError);
34
+ }
35
+ return { success: false, error: hookError };
36
+ }
37
+ }
38
+ /**
39
+ * Execute multiple hooks in sequence with error boundaries
40
+ */
41
+ export async function safeExecuteHooks(hooks, hookType, componentId, options = {}) {
42
+ const opts = { ...defaultOptions, ...options };
43
+ const errors = [];
44
+ for (const hook of hooks) {
45
+ const result = await safeExecuteHook(hook.name, hookType, componentId, hook.handler, options);
46
+ if (!result.success && result.error) {
47
+ errors.push(result.error);
48
+ if (!opts.continueOnError) {
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ return {
54
+ allSucceeded: errors.length === 0,
55
+ errors
56
+ };
57
+ }
58
+ /**
59
+ * Create a wrapped version of a hook handler that catches errors
60
+ */
61
+ export function createSafeHandler(hookName, hookType, componentId, handler, options = {}) {
62
+ const wrapped = async (...args) => {
63
+ const result = await safeExecuteHook(hookName, hookType, componentId, () => handler(...args), options);
64
+ return result.result;
65
+ };
66
+ return wrapped;
67
+ }
@@ -37,6 +37,13 @@ export interface LayerEvents {
37
37
  export interface ComponentEvents {
38
38
  componentschange: void;
39
39
  componentchange: ComponentData;
40
+ hookerror: {
41
+ hookName: string;
42
+ hookType: string;
43
+ error: Error;
44
+ componentId: string;
45
+ timestamp: number;
46
+ };
40
47
  }
41
48
  export interface SubtitlesEvents {
42
49
  subtitleschange: void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visualfries",
3
- "version": "0.1.102",
3
+ "version": "0.1.103",
4
4
  "license": "MIT",
5
5
  "author": "ContentFries",
6
6
  "repository": {