Files
hyperframes/packages/studio/src/contexts/DomEditContext.tsx
T
Miguel Ángel 1eed7a9b1f feat(studio): element groups — studio UI (#1759)
* feat(studio): element groups — source mutations

Wrap/unwrap source mutations (group geometry, the wrap-elements / unwrap-elements
routes) that the studio group feature is built on. Studio UI lands in the next PR.

* fix(studio): hoverable group interior + non-sticky drill-in

Two group selection bugs with animated members:

1) Empty space inside a group's overlay didn't hover/select the group. Members
   animated outside the wrapper's static box (110px box vs 340px member union),
   so elementsFromPoint hit only the full-bleed background there. Add a
   member-union hit-test fallback: a point inside a group's live member bounds
   resolves to that group (innermost wins).

2) After drilling into a group and selecting a child, nothing else was
   selectable — out-of-scope resolved to null. Make drill-in non-sticky:
   interacting outside the drilled group re-resolves normally and exits the
   drill-in, so a later click on the group selects it as a unit again.
2026-06-27 11:27:40 -04:00

367 lines
11 KiB
TypeScript

// fallow-ignore-file code-duplication
import { createContext, useCallback, useContext, useMemo, useRef, type ReactNode } from "react";
import type { useDomEditSession } from "../hooks/useDomEditSession";
type DomEditValue = ReturnType<typeof useDomEditSession>;
export interface DomEditActionsValue extends Pick<
DomEditValue,
| "handleTimelineElementSelect"
| "handlePreviewCanvasMouseDown"
| "handlePreviewCanvasPointerMove"
| "handlePreviewCanvasPointerLeave"
| "applyDomSelection"
| "clearDomSelection"
| "handleDomStyleCommit"
| "handleDomAttributeCommit"
| "handleDomAttributeLiveCommit"
| "handleDomHtmlAttributeCommit"
| "handleDomPathOffsetCommit"
| "handleDomGroupPathOffsetCommit"
| "handleDomZIndexReorderCommit"
| "handleDomBoxSizeCommit"
| "handleDomRotationCommit"
| "handleDomManualEditsReset"
| "handleDomTextCommit"
| "handleDomTextFieldStyleCommit"
| "handleDomAddTextField"
| "handleDomRemoveTextField"
| "handleAskAgent"
| "handleAgentModalSubmit"
| "handleBlockedDomMove"
| "handleDomManualDragStart"
| "handleDomEditElementDelete"
| "handleGroupSelection"
| "handleUngroupSelection"
| "setActiveGroupElement"
| "buildDomSelectionFromTarget"
| "buildDomSelectionForTimelineElement"
| "updateDomEditHoverSelection"
| "resolveImportedFontAsset"
| "setAgentModalOpen"
| "setAgentPromptSelectionContext"
| "setAgentModalAnchorPoint"
| "handleGsapUpdateProperty"
| "handleGsapUpdateMeta"
| "handleGsapDeleteAnimation"
| "handleGsapDeleteAllForElement"
| "handleGsapAddAnimation"
| "handleGsapAddProperty"
| "handleGsapRemoveProperty"
| "handleGsapUpdateFromProperty"
| "handleGsapAddFromProperty"
| "handleGsapRemoveFromProperty"
| "handleGsapAddKeyframe"
| "handleGsapAddKeyframeBatch"
| "handleGsapRemoveKeyframe"
| "handleGsapConvertToKeyframes"
| "handleGsapRemoveAllKeyframes"
| "handleResetSelectedElementKeyframes"
| "commitAnimatedProperty"
| "commitAnimatedProperties"
| "handleSetArcPath"
| "handleUpdateArcSegment"
| "handleUnroll"
| "invalidateGsapCache"
| "previewIframeRef"
| "commitMutation"
| "applyMarqueeSelection"
| "handleUpdateKeyframeEase"
| "handleSetAllKeyframeEases"
> {}
export interface DomEditSelectionValue extends Pick<
DomEditValue,
| "domEditSelection"
| "domEditGroupSelections"
| "domEditHoverSelection"
| "activeGroupElement"
| "domEditSelectionRef"
| "selectedGsapAnimations"
| "gsapMultipleTimelines"
| "gsapUnsupportedTimelinePattern"
| "agentModalOpen"
| "agentModalAnchorPoint"
| "copiedAgentPrompt"
| "agentPromptSelectionContext"
> {}
const DomEditActionsContext = createContext<DomEditActionsValue | null>(null);
const DomEditSelectionContext = createContext<DomEditSelectionValue | null>(null);
export function useDomEditActionsContext(): DomEditActionsValue {
const ctx = useContext(DomEditActionsContext);
if (!ctx) throw new Error("useDomEditActionsContext must be used within DomEditProvider");
return ctx;
}
export function useDomEditSelectionContext(): DomEditSelectionValue {
const ctx = useContext(DomEditSelectionContext);
if (!ctx) throw new Error("useDomEditSelectionContext must be used within DomEditProvider");
return ctx;
}
/** @deprecated Prefer useDomEditActionsContext or useDomEditSelectionContext. */
export function useDomEditContext(): DomEditValue {
return { ...useDomEditActionsContext(), ...useDomEditSelectionContext() };
}
export function DomEditProvider({
value: {
domEditSelection,
domEditGroupSelections,
domEditHoverSelection,
agentModalOpen,
agentModalAnchorPoint,
copiedAgentPrompt,
agentPromptSelectionContext,
domEditSelectionRef,
handleTimelineElementSelect,
handlePreviewCanvasMouseDown,
handlePreviewCanvasPointerMove,
handlePreviewCanvasPointerLeave,
applyDomSelection,
clearDomSelection,
handleDomStyleCommit,
handleDomAttributeCommit,
handleDomAttributeLiveCommit,
handleDomHtmlAttributeCommit,
handleDomPathOffsetCommit,
handleDomGroupPathOffsetCommit,
handleDomZIndexReorderCommit,
handleDomBoxSizeCommit,
handleDomRotationCommit,
handleDomManualEditsReset,
handleDomTextCommit,
handleDomTextFieldStyleCommit,
handleDomAddTextField,
handleDomRemoveTextField,
handleAskAgent,
handleAgentModalSubmit,
handleBlockedDomMove,
handleDomManualDragStart,
handleDomEditElementDelete,
handleGroupSelection,
handleUngroupSelection,
setActiveGroupElement,
activeGroupElement,
buildDomSelectionFromTarget,
buildDomSelectionForTimelineElement,
updateDomEditHoverSelection,
resolveImportedFontAsset,
setAgentModalOpen,
setAgentPromptSelectionContext,
setAgentModalAnchorPoint,
selectedGsapAnimations,
gsapMultipleTimelines,
gsapUnsupportedTimelinePattern,
handleGsapUpdateProperty,
handleGsapUpdateMeta,
handleGsapDeleteAnimation,
handleGsapDeleteAllForElement,
handleGsapAddAnimation,
handleGsapAddProperty,
handleGsapRemoveProperty,
handleGsapUpdateFromProperty,
handleGsapAddFromProperty,
handleGsapRemoveFromProperty,
handleGsapAddKeyframe,
handleGsapAddKeyframeBatch,
handleGsapRemoveKeyframe,
handleGsapConvertToKeyframes,
handleGsapRemoveAllKeyframes,
handleResetSelectedElementKeyframes,
commitAnimatedProperty,
commitAnimatedProperties,
handleSetArcPath,
handleUpdateArcSegment,
handleUnroll,
invalidateGsapCache,
previewIframeRef,
commitMutation,
applyMarqueeSelection,
handleUpdateKeyframeEase,
handleSetAllKeyframeEases,
},
children,
}: {
value: DomEditValue;
children: ReactNode;
}) {
const commitMutationRef = useRef(commitMutation);
commitMutationRef.current = commitMutation;
const stableCommitMutation = useCallback<DomEditActionsValue["commitMutation"]>(
(mutation, options) => commitMutationRef.current(mutation, options),
[],
);
const actions = useMemo<DomEditActionsValue>(
() => ({
handleTimelineElementSelect,
handlePreviewCanvasMouseDown,
handlePreviewCanvasPointerMove,
handlePreviewCanvasPointerLeave,
applyDomSelection,
clearDomSelection,
handleDomStyleCommit,
handleDomAttributeCommit,
handleDomAttributeLiveCommit,
handleDomHtmlAttributeCommit,
handleDomPathOffsetCommit,
handleDomGroupPathOffsetCommit,
handleDomZIndexReorderCommit,
handleDomBoxSizeCommit,
handleDomRotationCommit,
handleDomManualEditsReset,
handleDomTextCommit,
handleDomTextFieldStyleCommit,
handleDomAddTextField,
handleDomRemoveTextField,
handleAskAgent,
handleAgentModalSubmit,
handleBlockedDomMove,
handleDomManualDragStart,
handleDomEditElementDelete,
handleGroupSelection,
handleUngroupSelection,
setActiveGroupElement,
buildDomSelectionFromTarget,
buildDomSelectionForTimelineElement,
updateDomEditHoverSelection,
resolveImportedFontAsset,
setAgentModalOpen,
setAgentPromptSelectionContext,
setAgentModalAnchorPoint,
handleGsapUpdateProperty,
handleGsapUpdateMeta,
handleGsapDeleteAnimation,
handleGsapDeleteAllForElement,
handleGsapAddAnimation,
handleGsapAddProperty,
handleGsapRemoveProperty,
handleGsapUpdateFromProperty,
handleGsapAddFromProperty,
handleGsapRemoveFromProperty,
handleGsapAddKeyframe,
handleGsapAddKeyframeBatch,
handleGsapRemoveKeyframe,
handleGsapConvertToKeyframes,
handleGsapRemoveAllKeyframes,
handleResetSelectedElementKeyframes,
commitAnimatedProperty,
commitAnimatedProperties,
handleSetArcPath,
handleUpdateArcSegment,
handleUnroll,
invalidateGsapCache,
previewIframeRef,
commitMutation: stableCommitMutation,
applyMarqueeSelection,
handleUpdateKeyframeEase,
handleSetAllKeyframeEases,
}),
[
handleTimelineElementSelect,
handlePreviewCanvasMouseDown,
handlePreviewCanvasPointerMove,
handlePreviewCanvasPointerLeave,
applyDomSelection,
clearDomSelection,
handleDomStyleCommit,
handleDomAttributeCommit,
handleDomAttributeLiveCommit,
handleDomHtmlAttributeCommit,
handleDomPathOffsetCommit,
handleDomGroupPathOffsetCommit,
handleDomZIndexReorderCommit,
handleDomBoxSizeCommit,
handleDomRotationCommit,
handleDomManualEditsReset,
handleDomTextCommit,
handleDomTextFieldStyleCommit,
handleDomAddTextField,
handleDomRemoveTextField,
handleAskAgent,
handleAgentModalSubmit,
handleBlockedDomMove,
handleDomManualDragStart,
handleDomEditElementDelete,
handleGroupSelection,
handleUngroupSelection,
setActiveGroupElement,
buildDomSelectionFromTarget,
buildDomSelectionForTimelineElement,
updateDomEditHoverSelection,
resolveImportedFontAsset,
setAgentModalOpen,
setAgentPromptSelectionContext,
setAgentModalAnchorPoint,
handleGsapUpdateProperty,
handleGsapUpdateMeta,
handleGsapDeleteAnimation,
handleGsapDeleteAllForElement,
handleGsapAddAnimation,
handleGsapAddProperty,
handleGsapRemoveProperty,
handleGsapUpdateFromProperty,
handleGsapAddFromProperty,
handleGsapRemoveFromProperty,
handleGsapAddKeyframe,
handleGsapAddKeyframeBatch,
handleGsapRemoveKeyframe,
handleGsapConvertToKeyframes,
handleGsapRemoveAllKeyframes,
handleResetSelectedElementKeyframes,
commitAnimatedProperty,
commitAnimatedProperties,
handleSetArcPath,
handleUpdateArcSegment,
handleUnroll,
invalidateGsapCache,
previewIframeRef,
stableCommitMutation,
applyMarqueeSelection,
handleUpdateKeyframeEase,
handleSetAllKeyframeEases,
],
);
const selection = useMemo<DomEditSelectionValue>(
() => ({
domEditSelection,
domEditGroupSelections,
domEditHoverSelection,
activeGroupElement,
domEditSelectionRef,
selectedGsapAnimations,
gsapMultipleTimelines,
gsapUnsupportedTimelinePattern,
agentModalOpen,
agentModalAnchorPoint,
copiedAgentPrompt,
agentPromptSelectionContext,
}),
[
domEditSelection,
domEditGroupSelections,
domEditHoverSelection,
activeGroupElement,
domEditSelectionRef,
selectedGsapAnimations,
gsapMultipleTimelines,
gsapUnsupportedTimelinePattern,
agentModalOpen,
agentModalAnchorPoint,
copiedAgentPrompt,
agentPromptSelectionContext,
],
);
return (
<DomEditActionsContext value={actions}>
<DomEditSelectionContext value={selection}>{children}</DomEditSelectionContext>
</DomEditActionsContext>
);
}