mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 07:09:59 +00:00
feat(studio): promote to variable from the design panel
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Wraps a Design-panel property control with the promote-to-variable gesture.
|
||||
* When a control can be promoted it shows a visible "◇ var" button; clicking it
|
||||
* declares a variable (default = current value, so the render is unchanged) and
|
||||
* binds this property to it. Once bound, the button is replaced by a "◆ {id}"
|
||||
* chip and edits route to the variable's default (edit-in-place). Controls that
|
||||
* aren't eligible (or render outside a promote context, or are disabled by the
|
||||
* caller) pass through untouched. Uses a render-prop so each control keeps its
|
||||
* own value/onCommit shape.
|
||||
*/
|
||||
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
useVariablePromoteChannel,
|
||||
type PromoteChannel,
|
||||
} from "../../contexts/VariablePromoteContext";
|
||||
|
||||
interface RenderArgs {
|
||||
/** When bound, the variable's default to display; otherwise undefined. */
|
||||
value?: string;
|
||||
/** When bound, routes commits to the variable default; otherwise undefined. */
|
||||
onCommit?: (value: string) => void;
|
||||
bound: boolean;
|
||||
}
|
||||
|
||||
export function PromotableControl({
|
||||
channel,
|
||||
enabled = true,
|
||||
children,
|
||||
}: {
|
||||
channel: PromoteChannel;
|
||||
/**
|
||||
* Caller-side gate. Text-section controls only promote when the edited field
|
||||
* is the selected element's OWN text (source "self"); binding a child/text-
|
||||
* node field would target a different element than the control edits.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
children: (args: RenderArgs) => React.ReactNode;
|
||||
}) {
|
||||
const promote = useVariablePromoteChannel(channel);
|
||||
|
||||
// A binding attribute (`data-var-*` / `var(--id)`) pointing at a declaration
|
||||
// that no longer exists renders as a plain unbound control — a silent
|
||||
// fallback that leaves a dev wondering why "their binding isn't showing".
|
||||
// Surface it in the console so the dangling reference is discoverable.
|
||||
const danglingId =
|
||||
enabled && promote && promote.boundId != null && promote.declaration == null
|
||||
? promote.boundId
|
||||
: null;
|
||||
useEffect(() => {
|
||||
if (danglingId != null) {
|
||||
console.warn(
|
||||
`[hyperframes] Control is bound to variable "${danglingId}", but no such declaration exists. The element still carries the binding on disk — re-declare the variable or unbind the element.`,
|
||||
);
|
||||
}
|
||||
}, [danglingId]);
|
||||
|
||||
if (!promote || !enabled) return <>{children({ bound: false })}</>;
|
||||
|
||||
// A binding whose declaration was removed elsewhere is dangling: don't show
|
||||
// it as an editable bound control (setDefault would silently no-op) — let it
|
||||
// fall back to a plain, re-promotable control.
|
||||
const bound = promote.boundId != null && promote.declaration != null;
|
||||
const canPromote = promote.action != null && !bound;
|
||||
const defaultValue = promote.declaration?.default;
|
||||
|
||||
const rendered = children(
|
||||
bound
|
||||
? {
|
||||
// Only string defaults render inline; a FontValue/ImageValue object
|
||||
// falls back to the element's real value instead of "[object Object]".
|
||||
value: typeof defaultValue === "string" ? defaultValue : undefined,
|
||||
onCommit: promote.setDefault,
|
||||
bound: true,
|
||||
}
|
||||
: { bound: false },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`relative ${bound ? "rounded-lg ring-1 ring-studio-accent/40" : ""}`}>
|
||||
{rendered}
|
||||
{bound && (
|
||||
<span
|
||||
className="pointer-events-none absolute right-1.5 top-0 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent"
|
||||
title={`Bound to variable "${promote.boundId}"`}
|
||||
>
|
||||
◆ {promote.boundId}
|
||||
</span>
|
||||
)}
|
||||
{canPromote && (
|
||||
<button
|
||||
type="button"
|
||||
title="Make this a variable"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
promote.promote();
|
||||
}}
|
||||
className="absolute right-1.5 top-0 z-10 inline-flex items-center gap-1 rounded bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100"
|
||||
>
|
||||
◇ var
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user