mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 20:38:21 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10b10e7063 | ||
|
|
a9e320a009 |
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user