An ungrouped tween (mixed property groups classify to propertyGroup
undefined) fed keyframeCache but was skipped by every gsapAnimations
writer, so the collapsed row drew diamonds the expanded lanes had no
source animation to render. Drop the property-group gate at all three
writers; lane consumers already filter by group.
Also route the same-percentage merge in updateKeyframeCacheFromParsed
through deduplicateKeyframes so the easeAmbiguous rule has one owner.
The keyframe cache writes three key variants per element: the source-prefixed
key (sourceFile#id), the index.html fallback (index.html#id), and the bare
element id (id). The clear paths only dropped the prefixed variants, leaving
the bare entry behind.
PropertyPanel reads the bare key and gives it precedence over live data
(cacheEntry?.keyframes ?? gsapKeyframes), so after an element's keyframes are
removed the inspector kept rendering the deleted keyframes. Consumers that fall
back to the bare id (timeline diamonds, preview overlay) saw the same stale
entry.
Add clearKeyframeCacheForElement and clearKeyframeCacheForFile and route the
three clear sites through them so the bare key is dropped alongside the prefixed
ones. Each delete is guarded by has to avoid reallocating the cache map for an
absent key.
Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
* fix(studio): guard Zustand no-op setters and fix useConsoleErrorCapture memory leak
- Guard setIsPlaying to skip set() when value unchanged (eliminates 60
notifications/sec during reverse playback)
- Guard caption store selectGroup to bail before set() when group missing
(prevents empty Zustand notifications)
- Guard clearSelection to skip when already empty
- Fix useConsoleErrorCapture: restore original console.error, remove error
event listener, and delete __hfErrorCapture flag on cleanup
* fix(studio): delete dead files and unused exports
Remove 7 dead files (audioBeatDetection, keyframeSnapping,
timelineInspector, DopesheetStrip, StaggerControls,
TimelineLayerPanel, TimelineEditorNotice) and their test companions.
Delete unused computeFitToChildrenSize export from propertyPanelHelpers.
Fix re-export indirection: useDomEditCommits and studioMotionOps.test
now import patch builders directly from manualEditsDomPatches instead
of the re-export passthrough in manualEditsDom.
* fix(studio): eliminate effect-chain state mirroring for lint findings, hover, and GSAP fetch
Move lint findingsByElement sync from App.tsx into useLintModal where
the value is produced, removing the mirroring useEffect. Consolidate
4 hover-clearing effects in useDomSelection into 2 (one unconditional
on context change, one conditional combining caption mode, selection
match, and disconnected element checks). Fold the GSAP retry effect
into the fetch effect in useGsapTweenCache, scheduling a single retry
via setTimeout when the initial fetch returns 0 animations.
Eliminates 3 unnecessary render cycles from effect chains.
* fix(studio): memoize renderQueue, toolbar, and canvas rect to prevent re-render cascade
- Wrap renderQueue object in useMemo so StudioContext consumers don't
re-render on every App render
- Memoize timelineToolbar JSX so NLELayout memo isn't defeated
- Move canvasRect getBoundingClientRect() from render-time IIFE to a
useLayoutEffect-backed ref, eliminating layout thrashing
- Track and clear setTimeout handles in refreshPreviewDocumentVersion
to prevent stale timer accumulation on rapid calls and unmount
* refactor(studio): consolidate GSAP shared primitives — defaults, iframe access, keyframe parsing
Extract duplicated PROPERTY_DEFAULTS, IframeGsap interface, iframe
accessors (getIframeGsap, queryIframeElement), percentage keyframe
parsing, and toAbsoluteTime into a single gsapShared.ts module.
Removes ~120 lines of copy-pasted logic across 8 hook files, reducing
drift risk between the duplicate implementations.
* fix(studio): remove dead store fields, dead file, duplicate helper, and unsafe assertions
* refactor(studio): deduplicate selector helpers, rounding utils, percentage computation, and iframe access
* fix(studio): split StudioContext into Shell + Playback to prevent cascade re-renders
* refactor(studio): decompose useGsapScriptCommits into focused mutation hooks
* refactor(studio): decompose useFileManager into focused file operation hooks
Extract useFileTree (tree loading, refresh, derived assets/compositions)
and useEditorSave (debounced save with history tracking) from the 508-LOC
useFileManager. The parent hook composes both and retains file I/O,
click-to-source, upload/import, and CRUD — preserving the same public
interface so no consumers change.
* refactor(studio): decompose useDomEditCommits into focused commit hooks
Extract geometry (path offset, box size, rotation) and element lifecycle
(delete, z-index reorder) into useDomGeometryCommits and
useElementLifecycleOps. Parent keeps persistDomEditOperations as core
and composes all sub-hooks — public interface unchanged.
* refactor(studio): simplify useAppHotkeys with declarative command table
* refactor(studio): simplify useAppHotkeys with declarative command table
Replace 15 individual useRef callback refs with a single cbRef object.
Extract keydown dispatch into pure dispatchModifierKey/dispatchPlainKey
functions. Merge duplicate undo/redo logic into shared applyHistory.
Extract cross-origin listener boilerplate into safeAddListener/safeRemoveListener.
Hook body: 204 LOC (down from 445). Public API unchanged.
* fix(studio): remove unused getDomEditTargetKey import
* refactor(studio): decompose useDomEditSession into focused editing hooks
Extract GSAP-aware geometry intercepts (move/resize/rotation) and
animated property commit into useGsapAwareEditing, and selection
wiring, GSAP cache management, preview sync, and selection handlers
into useDomEditWiring. The parent remains a pure composition shell.
* style(studio): fix formatting in 5 files
* fix(studio): trim App.tsx to 598 lines (under 600 limit)
---------
Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
* fix(core): per-property-group keyframe foundations
Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.
* fix(core): add split-into-property-groups and replace-with-keyframes mutations
Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
* fix(studio): per-property-group intercept routing + drag/resize fixes
Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.
* fix(studio): keyframe cache propertyGroup tagging + timeline UI fixes
Tag cached keyframes with propertyGroup for group-aware operations.
Add tweenPercentage for accurate keyframe matching, activeKeyframePct
for diamond-click targeting, context menu offset, selected diamond z-index,
clearProps after kill in soft reload.