Compare commits

...
Author SHA1 Message Date
Chris Tate 10b10e7063 more resilient 2026-02-09 07:02:41 -06:00
Chris Tate a9e320a009 more resilient 2026-02-09 07:00:27 -06:00
4 changed files with 175 additions and 15 deletions
+25 -8
View File
@@ -823,7 +823,12 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
const [localValue, setLocalValue] = useState<string>(""); const [localValue, setLocalValue] = useState<string>("");
const value = props.statePath ? (boundValue ?? "") : localValue; const value = props.statePath ? (boundValue ?? "") : localValue;
const setValue = props.statePath ? setBoundValue! : setLocalValue; const setValue = props.statePath ? setBoundValue! : setLocalValue;
const options = props.options ?? []; const rawOptions = props.options ?? [];
// Coerce options to strings – AI may produce objects/numbers instead of
// plain strings which would cause duplicate `[object Object]` keys.
const options = rawOptions.map((opt) =>
typeof opt === "string" ? opt : String(opt ?? ""),
);
return ( return (
<div className="space-y-2"> <div className="space-y-2">
@@ -839,8 +844,11 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
<SelectValue placeholder={props.placeholder ?? "Select..."} /> <SelectValue placeholder={props.placeholder ?? "Select..."} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{options.map((opt) => ( {options.map((opt, idx) => (
<SelectItem key={opt} value={opt}> <SelectItem
key={`${idx}-${opt}`}
value={opt || `option-${idx}`}
>
{opt} {opt}
</SelectItem> </SelectItem>
))} ))}
@@ -876,7 +884,10 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
}, },
Radio: ({ props, emit }) => { Radio: ({ props, emit }) => {
const options = props.options ?? []; const rawOptions = props.options ?? [];
const options = rawOptions.map((opt) =>
typeof opt === "string" ? opt : String(opt ?? ""),
);
const [boundValue, setBoundValue] = props.statePath const [boundValue, setBoundValue] = props.statePath
? useStateBinding<string>(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks ? useStateBinding<string>(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
: [undefined, undefined]; : [undefined, undefined];
@@ -894,11 +905,17 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
emit?.("change"); emit?.("change");
}} }}
> >
{options.map((opt) => ( {options.map((opt, idx) => (
<div key={opt} className="flex items-center space-x-2"> <div
<RadioGroupItem value={opt} id={`${props.name}-${opt}`} /> key={`${idx}-${opt}`}
className="flex items-center space-x-2"
>
<RadioGroupItem
value={opt || `option-${idx}`}
id={`${props.name}-${idx}-${opt}`}
/>
<Label <Label
htmlFor={`${props.name}-${opt}`} htmlFor={`${props.name}-${idx}-${opt}`}
className="cursor-pointer" className="cursor-pointer"
> >
{opt} {opt}
+51 -3
View File
@@ -1,5 +1,6 @@
import React, { import React, {
type ComponentType, type ComponentType,
type ErrorInfo,
type ReactNode, type ReactNode,
useCallback, useCallback,
useMemo, useMemo,
@@ -81,6 +82,51 @@ export interface RendererProps {
fallback?: ComponentRenderer; fallback?: ComponentRenderer;
} }
// ---------------------------------------------------------------------------
// ElementErrorBoundary – catches rendering errors in individual elements so
// a single bad component never crashes the whole page.
// ---------------------------------------------------------------------------
interface ElementErrorBoundaryProps {
elementType: string;
children: ReactNode;
}
interface ElementErrorBoundaryState {
hasError: boolean;
}
class ElementErrorBoundary extends React.Component<
ElementErrorBoundaryProps,
ElementErrorBoundaryState
> {
constructor(props: ElementErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): ElementErrorBoundaryState {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error(
`[json-render] Rendering error in <${this.props.elementType}>:`,
error,
info.componentStack,
);
}
render() {
if (this.state.hasError) {
// Render nothing – the element silently disappears rather than
// crashing the entire application.
return null;
}
return this.props.children;
}
}
/** /**
* Element renderer component * Element renderer component
*/ */
@@ -214,9 +260,11 @@ function ElementRenderer({
); );
return ( return (
<Component element={resolvedElement} emit={emit} loading={loading}> <ElementErrorBoundary elementType={resolvedElement.type}>
{children} <Component element={resolvedElement} emit={emit} loading={loading}>
</Component> {children}
</Component>
</ElementErrorBoundary>
); );
} }
+51 -3
View File
@@ -2,6 +2,7 @@
import React, { import React, {
type ComponentType, type ComponentType,
type ErrorInfo,
type ReactNode, type ReactNode,
useCallback, useCallback,
useMemo, useMemo,
@@ -77,6 +78,51 @@ export interface RendererProps {
fallback?: ComponentRenderer; fallback?: ComponentRenderer;
} }
// ---------------------------------------------------------------------------
// ElementErrorBoundary – catches rendering errors in individual elements so
// a single bad component never crashes the whole page.
// ---------------------------------------------------------------------------
interface ElementErrorBoundaryProps {
elementType: string;
children: ReactNode;
}
interface ElementErrorBoundaryState {
hasError: boolean;
}
class ElementErrorBoundary extends React.Component<
ElementErrorBoundaryProps,
ElementErrorBoundaryState
> {
constructor(props: ElementErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): ElementErrorBoundaryState {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error(
`[json-render] Rendering error in <${this.props.elementType}>:`,
error,
info.componentStack,
);
}
render() {
if (this.state.hasError) {
// Render nothing – the element silently disappears rather than
// crashing the entire application.
return null;
}
return this.props.children;
}
}
/** /**
* Element renderer component * Element renderer component
*/ */
@@ -210,9 +256,11 @@ function ElementRenderer({
); );
return ( return (
<Component element={resolvedElement} emit={emit} loading={loading}> <ElementErrorBoundary elementType={resolvedElement.type}>
{children} <Component element={resolvedElement} emit={emit} loading={loading}>
</Component> {children}
</Component>
</ElementErrorBoundary>
); );
} }
+48 -1
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import React, { type ErrorInfo, type ReactNode } from "react";
import { AbsoluteFill, Sequence } from "remotion"; import { AbsoluteFill, Sequence } from "remotion";
import type { TimelineSpec, ComponentRegistry, Clip } from "./types"; import type { TimelineSpec, ComponentRegistry, Clip } from "./types";
@@ -33,6 +34,50 @@ export const standardComponents: ComponentRegistry = {
VideoClip, VideoClip,
}; };
// ---------------------------------------------------------------------------
// ClipErrorBoundary – catches rendering errors in individual clips so
// a single bad clip never crashes the entire composition.
// ---------------------------------------------------------------------------
interface ClipErrorBoundaryProps {
clipId: string;
component: string;
children: ReactNode;
}
interface ClipErrorBoundaryState {
hasError: boolean;
}
class ClipErrorBoundary extends React.Component<
ClipErrorBoundaryProps,
ClipErrorBoundaryState
> {
constructor(props: ClipErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): ClipErrorBoundaryState {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error(
`[json-render/remotion] Rendering error in clip "${this.props.clipId}" (<${this.props.component}>):`,
error,
info.componentStack,
);
}
render() {
if (this.state.hasError) {
return null;
}
return this.props.children;
}
}
interface RendererProps { interface RendererProps {
/** The timeline spec to render */ /** The timeline spec to render */
spec: TimelineSpec; spec: TimelineSpec;
@@ -102,7 +147,9 @@ export function Renderer({
from={clip.from} from={clip.from}
durationInFrames={clip.durationInFrames} durationInFrames={clip.durationInFrames}
> >
<Component clip={clip} /> <ClipErrorBoundary clipId={clip.id} component={clip.component}>
<Component clip={clip} />
</ClipErrorBoundary>
</Sequence> </Sequence>
); );
}; };