mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
1eed7a9b1f8bf7b93ad8cc22bcc7125c644c43d9
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
1ea0ed55e3 |
fix(studio): stabilize manual drag targets (#1393)
## Problem Studio manual drag had two bad target paths in `hf-keyframes-test`: - Drag could start from a cached hover selection instead of the selected overlay, so moving `#cta` could write an offset to the wrong element while the overlay appeared correct. - `#stage` is the visual 1920x1080 canvas, but the composition metadata lives on `<html data-composition-id ...>`. Studio treated `#stage` as a normal movable layer, so dragging it moved the coordinate basis the overlay depends on and left Undo with no reliable saved edit to reverse. ## Fix - Manual drag now starts only from the selected overlay bounds; canvas pointer-down no longer starts movement from `hoverSelectionRef`. - Added a structural root-layer heuristic: a direct body child matching the composition root's declared `data-width` / `data-height` is selectable and style-editable, but cannot receive manual offset/size/rotation edits. - Manual geometry commits now return/rethrow the save promise so failed writes roll back optimistic DOM changes instead of silently sticking. - GSAP-targeted CSS fallback edits are blocked before applying manual geometry when the GSAP drag intercept is unavailable. ## Tests - New regression coverage for stale-hover drag start and full-canvas root-stage capability resolution. - Focused DOM-edit suite: 119/119 pass. - Full Studio suite: 800 pass, 18 todo. - Studio typecheck clean; oxlint/oxfmt clean; lefthook pre-commit clean. - Studio build passes with the existing Vite chunk-size warning. - Browser verified with `agent-browser`: attempted `#stage` drag writes no offset and keeps Undo disabled; `#cta` drag writes exactly one offset to `#cta`; Undo returns the fixture to zero offsets. Local browser artifacts: `artifacts/studio-cta-drag/stage-cta-drag-fix.webm`, `artifacts/studio-cta-drag/stage-cta-drag-fix-final.png`. |
||
|
|
0923bc0787 |
feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b) (#1299)
* feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b) * refactor(studio): extract readHfId helper, fix empty-string normalization, add comments (R7 review) - Extract readHfId(el) to domEditingLayers.ts — centralizes `?.trim() || undefined` normalisation; guards against empty-string data-hf-id reaching findTagByTarget - Wire readHfId into domEditingLayers.ts and useDomEditCommits.ts (the one site that still used `?? undefined` instead of `|| undefined`) - Re-export readHfId through domEditing.ts public API - Add readHfId unit tests: present, absent, empty-string, whitespace-only - Add comment on PatchTarget: runtime validation lives in findTagByTarget, type is docs-only - Suppress pre-existing unused re-exports in timelineDOM.ts (backward-compat re-exports brought into fallow scope by the T5b hfId changes) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(core): clear data-hf-id on split clone to prevent dual-match (R7 review) cloneNode(true) copies all attributes including data-hf-id. Without clearing it, both halves of a split share the same hf-id; the server's findByHfId picks the first match and silently patches the wrong clip. Remove the attribute from the clone so write-back re-mints a fresh id on the next preview load. Adds a test: splitElementInHtml — hfId clone isolation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(studio): add hfId to DomEditLayerItem + getDomLayerPatchTarget return type (R7 review) - Add hfId to DomEditLayerItem interface (domEditingTypes.ts) so layer item construction in collectDomEditLayerItems compiles - Widen getDomLayerPatchTarget return type to include hfId + populate it from data-hf-id attribute (domEditingElement.ts) - Widen findDomEditSelectionTarget to check hfId-first when no id/selector - Widen Pick types in domEditOverlayGeometry.ts and useGsapScriptCommits.ts - Add hfId to buildMissingCompositionElements element construction - Add hfId-targeted test coverage in domEditing.test.ts, domEditOverlayGeometry.test.ts, timelineIframeHelpers.test.ts - Update hfIds.test.ts KNOWN LIMITATION labels — write-back landed in R7 T1-2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
82c754f27a | feat(studio): wire hfId through DOM-edit patch targets, activate hfId lookup path (R7, T5a) (#1297) | ||
|
|
2071c85c9a |
feat(studio,core): populate hfId in DomEditSelection + widen MutationTarget (R7, T5a) (#1296)
## Summary - Adds `hfId` field to `resolveDomEditSelection` — reads `data-hf-id` off the live element and stores it in `DomEditSelection.hfId` - `DomEditSelection extends PatchTarget` which already declares `hfId?: string`, so this is a single new line at the return site - Widens `MutationTarget` in `files.ts` to include `hfId?: string` (type hygiene — the value already survives through `parseMutationBody`'s by-reference pass, so this is documentation not a behaviour change) ## Why R7 / Task 5a. The full hf-id write-back and patch-engine infrastructure (R1 + R7 Tasks 0–4, PRs #1269–#1292) is server-complete. The only missing piece was: the Studio client never read `data-hf-id` off a hit-tested element, so `target.hfId` was always `undefined` and the `hfId`-first lookup branches in both patch engines were unreachable in production. This PR fixes the selection side — the commit wire (#1297) completes the path. ## Test plan - [ ] `packages/studio/src/components/editor/domEditingLayers.test.ts` — two new tests with jsdom environment: - `resolveDomEditSelection` on an element with `data-hf-id` → `selection.hfId` is populated - element without `data-hf-id` → `selection.hfId` is `undefined` - [ ] All 65 studio test files pass, all 72 core test files pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
f19d6fd471 |
feat: CLI observability + fix studio save failures on JS-created elements (#1091)
* feat(core): add probeElementInSource for source-existence checks
* feat(core): add probe-element endpoint for source-existence checks
* feat(studio): gate editing capabilities on source existence
* fix(studio): enrich save_failure telemetry with target details
* feat(studio): async selection resolution with source probe
Make `resolveDomEditSelection` async and wire a `probeSourceElement` call
into the selection path so elements generated by scripts (not present in the
source HTML) are detected early and have all edit capabilities disabled with
a clear reason message ("This element is generated by a script and cannot be
edited visually.").
Part A – core probe logic:
- `domEditingLayers.ts`: `resolveDomEditSelection` is now async; calls
`probeSourceElement` (POST /api/projects/:id/file-mutations/probe-element/:file)
when `projectId` is supplied and the element has a stable id/selector.
`existsInSource: false` flows into `resolveDomEditCapabilities`, which
disables all write capabilities with the appropriate reason.
- `domEditingLayers.ts`: `refreshDomEditSelection` promoted to async.
- `files.ts`: new `probe-element` route; extracted `resolveProjectPath`,
`resolveFileMutationContext`, `writeIfChanged`, and `parseMutationBody`
helpers to eliminate repeated boilerplate across remove/patch/probe handlers.
Part B – caller propagation (all eight consumer sites):
- `useDomSelection.ts`: `buildDomSelectionFromTarget`,
`resolveDomSelectionFromPreviewPoint`,
`buildDomSelectionForTimelineElement`, `handleTimelineElementSelect`,
`refreshDomEditSelectionFromPreview`, and
`refreshDomEditGroupSelectionsFromPreview` all made async; `projectId`
forwarded into `resolveDomEditSelection`.
- `useDomEditCommits.ts`, `useDomEditTextCommits.ts`: updated
`buildDomSelectionFromTarget` parameter type; added `await` at call sites.
- `useDomEditSession.ts`: inner `syncSelectionFromDocument` made async; fire
with `void` to satisfy the surrounding effect.
- `usePreviewInteraction.ts`: `handlePreviewCanvasMouseDown` and
`handlePreviewCanvasPointerMove` made async (React ignores handler return
values, so this is safe).
- `useStudioUrlState.ts`: deferred `buildDomSelectionFromTarget` call
converted to `.then()` chain with `void` prefix so the effect stays sync.
- `LayersPanel.tsx`: `seekToLayer`, `handleSelectLayer`, and
`handleLayerHover` made async.
- `DomEditOverlay.tsx` / `useDomEditOverlayGestures.ts`: `onCanvasPointerMove`
return type widened to `Promise<DomEditSelection | null>`; pointer-down
handler falls back to `hoverSelectionRef.current` (always populated by a
prior hover) instead of awaiting the async move callback inline.
Part C – test and tooling fixes:
- `lefthook.yml`: filesize hook shell loop explicitly skips `*.test.ts/tsx`
files as a guard against a lefthook v2.1.6 bug where `exclude` patterns are
not applied to `{staged_files}` in shell scripts.
- `domEditing.test.ts`: all `it()` blocks calling `resolveDomEditSelection`
made async with `await`.
- `DomEditOverlay.test.ts`: mock updated to return `Promise.resolve(selection)`
and `hoverSelection` pre-seeded so pointer-down test works with the new
hover-first path.
- `studioUrlState.test.ts`: `buildDomSelectionFromTarget` mocks wrapped in
`Promise.resolve()`; seek/selection hydration test made async with
`await act(async () => { await Promise.resolve(); })` to flush microtasks.
* feat(cli): add global error handlers for crash telemetry
Register process-level uncaughtException and unhandledRejection handlers
that fire trackCliError so unhandled crashes are captured in telemetry.
Add the trackCliError function to events.ts and re-export it from the
telemetry barrel.
* feat(cli): track per-command success/failure and duration
* test(core): add integration test for JS-created element probe scenario
* fix: address PR review feedback
- uncaughtException handler now calls process.exit(1) after flushing
- cli_command_result uses real exit code from process "exit" event
- drop stack_trace from cli_error (contains filesystem paths)
- skip source probe during hover — only probe on click/selection
- format .fallowrc.jsonc
* fix(cli): restore stack_trace in cli_error telemetry
* fix(cli): use captured module refs in exit handlers instead of dead import()
|
||
|
|
576598f5ad |
feat(studio): disable move/resize/style editing for locked composition children
Elements inside a data-timeline-locked composition now have all canvas interactions disabled (move, resize, manual offset/size/rotation, style editing). Prevents "Unable to patch" errors when trying to move JS-generated caption elements that can't be patched back to source. |
||
|
|
8371f42aed |
feat(studio): hide text panel for elements inside locked compositions
Elements inside a data-timeline-locked sub-composition now have the TEXT property panel hidden. These elements are JS-generated — editing them in the panel won't persist since the script rebuilds the DOM on load. Uses findClosestByAttribute to detect the locked ancestor. |
||
|
|
cd0e6b02a0 |
feat(studio): Timing inspector + fix mixed-content text editing (#896)
* feat(studio): add clipboard payload types and ID deduplication * feat(studio): add Ctrl+C/V/X copy/paste for timeline clips and DOM elements * fix(studio): use duck-typing for cross-frame element access in clipboard Elements from the preview iframe are from a different window context, so `el instanceof HTMLElement` always returns false. Use `"outerHTML" in el` instead to correctly detect elements across frame boundaries. * fix(studio): preserve playhead position after paste reloadPreview() used location.reload() which bypassed the NLELayout saveSeekPosition effect, causing the playhead to reset to 0:00 after paste. Switch to setRefreshKey which triggers the effect and restores the seek position after the iframe reloads. * fix(studio): paste DOM elements as siblings, not at composition root DOM element paste was inserting at the composition root, losing the parent context that provides CSS styles and positioning. Now stores the origin selector on copy and inserts the paste as a sibling immediately after the original element, preserving style inheritance. Falls back to root insertion if the selector can't be matched. * fix(studio): address review — deduplicateIds, native copy, altKey guard - deduplicateIds regex used \b which matched data-composition-id, data-clip-id, etc. Switch to lookbehind (?<=\s) so only standalone id="..." attributes are rewritten. Add test pinning this. - Ctrl+C no longer calls preventDefault() before confirming there's a selected element. Native browser copy (text selections outside inputs) is preserved when nothing is selected in the Studio. - Add !event.altKey guard on C/V/X to avoid intercepting Cmd+Alt+V (paste-as-plain-text) and similar OS gestures. - Remove no-op .replace(/"/g, '"') flagged by CodeQL. * fix(studio): address review round 2 — Cmd+X guard, data-start scope, revert drive-by - Cmd+X now pre-checks selection state before preventDefault, mirroring the Cmd+C fix. Native cut preserved when nothing is selected. - handleCut returns Promise<boolean> so the caller can gate on it. - data-start rewrite scoped to the outermost opening tag only, so nested clip timing is preserved on paste. - Removed system clipboard write (cross-tab paste unsupported, in-memory ref is the only read path). - Reverted the reloadPreview drive-by (setRefreshKey→location.reload); the perf branch (#895) handles this properly via refreshPlayer(). * perf(studio): use lightweight iframe.src reload instead of Player teardown Content refreshes (paste, move, resize, delete, asset drop) previously triggered setRefreshKey which changed the Player's React key, causing full web-component destruction + iframe teardown + crossfade animation + re-initialization of all event listeners and asset polling. Now NLELayout intercepts refreshKey changes and calls refreshPlayer() which just appends a cache-busting _t param to the iframe src. The Player web component stays alive, event listeners persist, and the reload is ~10x faster with no "waiting for media" flash. Key-based teardown is preserved for actual structural changes (project switch, composition drill-down via directUrl change). * perf(studio): skip asset-loading overlay on content refreshes The asset-loading overlay ("Preparing preview assets") polled for video/audio readyState on every iframe load, including content refreshes from paste/move/resize. On reloads the browser serves assets from cache so they resolve near-instantly — the overlay just created a disruptive flash. Now skips the polling on subsequent loads (loadCountRef > 1), only showing it on the initial cold load. * feat(studio): add Timing section to inspector Design panel Adds Start, End, and Duration fields to the Design panel when the selected element has data-start/data-duration attributes. Editing any field commits via the attribute patch pipeline (same as timeline edits) and refreshes the preview. End is computed from start+duration and writing End adjusts duration accordingly. * fix(studio): preserve bare text nodes in mixed-content elements collectDomEditTextFields only captured child HTML elements, ignoring bare text nodes. For elements like: <div class="headline">If you're <span>turning 65</span> soon...</div> only the <span> was collected as a text field. When commitDomTextFields serialized back, "If you're " and " soon..." were lost. Now walks childNodes and creates text-node fields for bare text nodes alongside child element fields. serializeDomEditTextFields emits bare text for text-node fields, preserving the complete mixed content. * fix(studio): address #896 review — remove scrub from timing, add mixed-content test - Remove scrub from Timing fields: 1px = 1 second is too coarse. Scroll-wheel and direct typing still work with sub-second precision. - Add mixed-content text-node serialization test in a separate file (domEditingTextFields.test.ts) to avoid bloating the existing domEditing.test.ts past the filesize limit. |
||
|
|
91bdffffe6 |
fix(ci): scope LOC check to studio, split useTimelinePlayer + hyperframes-player under 500 LOC (#750)
* refactor: split useTimelinePlayer.ts and hyperframes-player.ts into focused modules (<500 LOC each) * fix(ci): scope 500 LOC check to packages/studio, add allowlist for grandfathered files * feat(cli): Linux ARM64 support — auto-install Chromium on DGX Spark / GB10 / Jetson Chrome Headless Shell has no Linux ARM64 binary. On arm64 Linux: - Detects the platform automatically - Tries to auto-install system Chromium via apt-get (works on Ubuntu/Debian ARM) - Falls back to clear manual instructions with exact commands - 'hyperframes browser ensure' guides through the setup interactively - After setup, all render commands work without any flags * fix(ci): disable Windows Defender real-time monitoring to prevent EPERM builds Path exclusions are insufficient — Defender re-scans new files created during bun install before the exclusion takes effect. Disable real-time monitoring for the entire job duration instead (standard CI practice). * refactor(studio): split all files >500 LOC + extract useToast, delete allowlist All 11 large files split into focused modules under 500 LOC. App.tsx extracted toast logic into useToast hook (493 LOC now). .filesize-allowlist deleted — no longer needed. * fix: remove unused imports from split files, extract useToast from App.tsx App.tsx: 504 → 493 lines (toast logic extracted to useToast hook) timelineDOM.ts: remove unused imports from re-export pattern MotionPanel.tsx: remove unused clampStudioCustomEasePoints import studioMotionOps.ts: remove unused StudioGsapMotionDirection import * fix(ci): use Set-MpPreference to fully disable Windows Defender (both jobs) * fix(producer): use node --experimental-strip-types instead of tsx for build:fonts Eliminates the tsx binary dependency that Windows Defender locks during bun install, causing EPERM errors. Node 22.6+ strips TypeScript types natively with no external binary. * chore: remove .filesize-allowlist — App.tsx is now 493 lines (<500) * fix(ci): disable Windows Defender before checkout to prevent all EPERM races * fix(producer): skip build:fonts if fontData.generated.ts already exists The generated file is tracked in git, so CI doesn't need to regenerate it. This avoids @fontsource/inter node_modules access on Windows which triggers EPERM from Defender scanning during bun install. |