Commit Graph
4 Commits
Author SHA1 Message Date
Miguel Ángel 225010800a feat(studio): persist element positions in HTML, fix resize overlay drift and GSAP double-translation (#829)
* feat(studio): add pasteboard background to preview viewport

Adds bg-neutral-800 to the preview viewport so the area outside the
canvas is visually distinct from the composition content — consistent
with professional video editors (Premiere, DaVinci, Figma).

* feat(studio): pasteboard background and canvas outline around preview

- NLEPreview: viewport gets bg-neutral-700 (#404040) as the pasteboard
  color surrounding the canvas — distinct from the app chrome (#0a0a0a)
- Player wrapper: drop bg-black so the pasteboard shows around the canvas
  (loading overlays still cover the area with bg-black during load)
- Player: set host background to transparent via inline style (overrides
  :host { background: #000 } in shadow DOM), and inject a style rule into
  the open shadow root so .hfp-container has overflow:visible and the
  canvas iframe gets a thin white ring + soft drop-shadow — making the
  canvas boundary legible against the pasteboard

* feat(studio): disable manual positioning JSON by default, add toggle

Manual edits were always stored in `.hyperframes/studio-manual-edits.json`,
making it hard to share source without the sidecar file and easy to
accidentally reposition elements via drag.

Changes:
- `enabled` field added to `StudioManualEditManifest` (defaults to `false`
  when absent — existing projects are unaffected until they opt in)
- Drag handles, resize, and rotation handles are hidden when disabled
- Layout X/Y/W/H/R fields in the Design panel are read-only when disabled
- "Manual positioning" toggle added at the bottom of the Design panel,
  visible whether or not an element is selected
- Toggle state is persisted to `.hyperframes/studio-manual-edits.json`
  so each project can opt in independently
- `STUDIO_PREVIEW_MANUAL_EDITING_ENABLED` env flag still acts as a hard
  cap (env off → feature off regardless of project setting)

* feat(studio): enable manual positioning by default (opt-out)

* feat(studio): allow absolute elements to drag without toggle; gate JSON-backed drag behind toggle

* feat(studio): persist positions directly to HTML; remove JSON sidecar and manual positioning toggle

Replace the `.hyperframes/studio-manual-edits.json` sidecar with inline-style
persistence baked directly into the HTML source. Drag/resize/rotation values
are written as CSS custom properties (`--hf-studio-offset-x/y`, `--hf-studio-width/height`,
`--hf-studio-rotation`) plus `translate`/`width`/`height`/`rotate` inline styles via
`persistDomEditOperations` — no re-apply step needed on load.

Key changes:
- `sourcePatcher`: add `value: string | null` to `PatchOperation` — null removes the
  property/attribute from the HTML tag instead of setting it
- `manualEditsDom`: add `build*Patches` / `buildClear*Patches` helpers that capture live
  element state into `PatchOperation[]` for HTML source writes; add
  `reapplyPositionEditsAfterSeek` (DOM-query-based seek hook, queries data-attribute markers)
- `manualEdits.ts`: remove `applyStudioManualEditManifest` and all manifest target
  resolution; export `reapplyPositionEditsAfterSeek`; keep seek/play wrap infrastructure
- `useManifestPersistence`: remove all JSON I/O — no disk read on load, no manifest
  state, no toggle state; `applyCurrentStudioManualEditsToPreview` now only installs
  seek hooks via `reapplyPositionEditsAfterSeek`
- `useDomEditCommits`: replace `commitStudioManualEditManifestOptimistically` calls with
  direct DOM apply + `commitPositionPatchToHtml` (queued HTML patch write, skipRefresh)
- `DomEditOverlay`: remove `manualEditsEnabled` prop; revert all `canMove || manualEditsEnabled`
  gates to just `canApplyManualOffset` — every draggable element is always draggable
- `PropertyPanel`: remove `ManualPositioningToggle` component and all toggle props
- `manualEditsParsing/manualEditsTypes`: remove manifest types, upsert functions, and
  `STUDIO_MANUAL_EDITS_PATH`; keep `finiteNumber`, `readStudioFileChangePath`,
  `roundRotationAngle`, and snapshot/CSS-property types

* fix(studio): sync keyboard shortcut handler with main; fix keepPlaying seek assertions in test

* fix(studio): strip GSAP-cached translate from transform on path offset apply

* fix(studio): remove Reset edits button from design panel

* feat(studio): wire reloadPreview into manifest persistence; drop stale group-selection refresh

- Pass `reloadPreview` into `useManifestPersistence` so undo/redo reloads
  via the refresh-key path instead of directly touching the iframe.
- Remove `refreshDomEditGroupSelectionsFromPreview` from commit handlers;
  HTML is now the source of truth so no stale-ref refresh is needed.
- Add `manualEditsRenderScript` helper; export via studio-api and apply
  it in `htmlCompiler` during HTML compilation.

* fix(studio): prevent root composition from being selected; correct overlay drift on resize

- Guard `getDomLayerPatchTarget` against elements with `data-composition-id`
  so the root composition div is never returned as a visual selection target.
- Apply the same guard to the raw `elementFromPoint` fallback in
  `getPreviewTargetFromPointer`, which was the actual escape path.
- Thread `iframeRef` into gesture handler opts; after applying draft
  dimensions during resize, re-read the element BCR via `toOverlayRect`
  and update the overlay box position to compensate for visual drift on
  elements with centered transform-origin (e.g. GSAP scale tweens).

* fix(studio): correct resize overlay for scaled elements; block invisible element selection

- Resize: use BCR from `toOverlayRect` for both position and size after
  applying draft dimensions — GSAP scale makes visual size diverge from
  raw CSS size, BCR is the only accurate source during a gesture.
- Click selection: add `isElementComputedVisible` guard to the
  `elementFromPoint` fallback so opacity-0 / autoAlpha-hidden elements
  cannot be selected even though the browser hit-test returns them.

* fix(studio): reload preview on external file changes via SSE/HMR

Share the app-level domEditSaveTimestampRef with useManifestPersistence
so the SSE/HMR handler can suppress echoes from all studio saves (code
tab, timeline, DOM edits), then call reloadPreview() for non-motion
external changes that aren't echoes of our own saves.

* fix(studio): suppress post-resize click to keep selection on resized element

* fix(studio): serve registry blocks without index.html in preview

Blocks ship as {id}.html + assets/ with no index.html. The preview
route hard-coded index.html so these projects returned 404 and their
assets (e.g. korea-map.png, map-nyc-paris.png) were never served.

Add resolveProjectMainHtml() that falls back to {id}.html, thread the
resolved compositionPath through transformPreviewHtml and
injectStudioPreviewAugmentations, and update listProjects() in the
vite adapter to surface block directories in the project list.

* fix(render): preserve studio drag/resize/rotation offsets in rendered video

Three issues caused studio-edited positions to be lost during rendering:

1. The seek-reapply script used setInterval to wrap window.__hf.seek, but
   Puppeteer's page.evaluate() calls don't yield the event loop for
   macrotasks — the interval never fired, so reapplyAll() never ran after
   GSAP seeks. Fix: use Object.defineProperty to trap writes to the seek
   property, wrapping it synchronously the instant the bridge assigns it.

2. MEDIA_VISUAL_STYLE_PROPERTIES (copied from <video> to proxy <img>
   during render) included "transform" but not "translate", "rotate", or
   "scale" — the CSS Transforms Level 2 individual properties used by
   studio drag/resize/rotation. The proxy was positioned at offsetLeft/
   offsetTop without the translate offset.

3. getViewportMatrix (HDR compositor) only read cs.transform, missing
   individual transform properties entirely. Added composeIndividualTransforms
   to build the translate × rotate × scale matrix and compose it before
   the legacy transform matrix.

* fix(studio): select elements with pointer-events: none in preview

Compositions often set pointer-events: none on scenes, avatar wrappers,
and decorative layers. elementsFromPoint() skips these elements entirely,
making them unselectable in the Studio. Fix: temporarily inject a
* { pointer-events: auto !important } stylesheet during hit-testing, then
remove it immediately after.

Also adds a pointer_events_none lint rule (info severity, visible with
--verbose) so authors know which selectors may affect Studio selection.
2026-05-15 06:43:00 +02:00
Miguel Ángel 1caeb28658 feat(studio): header logo, playbar cleanup, and I/O work-area markers (#811)
* feat(studio): header logo, playbar cleanup, and I/O work-area markers

- Add Hyperframes icon mark to the studio header (left of project name)
- Remove m:ss toggle button — click the timecode directly to switch modes
- Remove frame jump input from controls bar — moved into ⌨ shortcuts panel
- Replace Loop text button with a repeat icon
- Collapse J/K/L shortcut badges into a single ⌨ icon that opens a panel
- Shortcuts panel: Jump to frame, Work area I/O display, shortcuts reference
- Implement I/O work-area markers (closes #807):
  - I / Shift+I: set / clear in-point at playhead
  - O / Shift+O: set / clear out-point at playhead
  - A: jump to in-point (or start); E: jump to out-point (or end)
  - Loop respects in/out boundaries for both forward and backward playback
  - Teal work-area band + tick markers rendered on the seek bar

* fix(studio): guard against inverted in/out work-area points in loop ticks

If the user sets out-point before in-point (outPoint < inPoint), rawLoopStart
>= rawLoopEnd caused the loop guard to fire immediately on every tick, creating
a tight infinite seek loop. Both the forward RAF tick and the reverse RAF tick
now fall back to the full composition range when the work area is invalid.

* fix(studio): address work-area edge cases from review

- setInPoint/setOutPoint now cross-clear the opposite marker when setting one
  would produce an inverted range (in >= out), preventing the invalid state
  rather than correcting it at tick time
- Forward tick no longer gates on !adapter.isPlaying() — outPoint crossing
  fires even while the adapter is running; explicitly pauses on the non-loop
  path so playback stops at out-point rather than sailing to dur
- play() end-of-stream reset seeks to inPoint (if set) instead of hardcoded 0

* feat(studio): use full Hyperframes wordmark logo in header

Replace the standalone icon mark with the complete logo from logo-dark.svg
(icon mark + Hyperframes wordmark), with all black text fills inverted to
white for the dark header background. Project name is shown next to the logo
separated by a middot.

* Revert "feat(studio): use full Hyperframes wordmark logo in header"

This reverts commit a2815fc7d0.

* feat(studio): show full HeyGen/Hyperframes logo in header

Replace the standalone chevron icon with the complete logo from logo-dark.svg:
heygen label + gradient mark + hyperframes wordmark, all white fills on dark
background. Project name follows after a middot separator.

* fix(studio): use | instead of · as logo/project separator
2026-05-14 01:06:55 +02:00
Miguel Ángel c3f70c91db fix(studio): restore saved positions on page refresh (#801)
* fix(studio): restore saved positions on page refresh

studio-manual-edits.json was correctly persisted to disk but never read back
into memory on bootstrap. On every page refresh, studioManualEditManifestRef
started empty, so handleLoad applied an empty manifest and all saved
positions/sizes/rotations were silently discarded.

applyStudioManualEditsToPreview now reads from disk whenever the in-memory
manifest is empty. The existing readRevision guard prevents overwriting an
in-flight optimistic edit if a position change races with the disk read.

* fix(studio): close delete-all race and apply same bootstrap to motion manifest

Two follow-up fixes from review:

1. Replace edits.length === 0 with an explicit manifestBootstrappedRef boolean.
   The old condition was true in two distinct states: never-bootstrapped AND
   user-deleted-all-edits. Because the delete-all disk write is async-queued,
   there was a window where applyStudioManualEditsToPreview could read stale
   disk content and resurrect just-deleted positions. The boolean flag is set
   on the first apply and reset on project switch, cleanly separating the two
   states.

2. applyStudioMotionToPreview had the identical bug: GSAP motion edits were
   also lost on page refresh. Applied the same motionBootstrappedRef pattern.
2026-05-13 22:39:30 +02:00
Miguel ÁngelandClaude Opus 4.6 6fe970651d feat(studio): add Layers panel as new inspector tab (#784)
* feat(studio): add Layers panel as new inspector tab

Adds a dedicated Layers tab alongside Design and Renders in the right
panel inspector. The panel shows the full composition element tree with
collapsible hierarchy — clicking a layer selects it without navigating
away from the tree view.

Closes #783

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(studio): add visual element previews to Layers panel

Each layer row now shows a small color/content preview thumbnail:
- Text elements show a snippet of their content in the actual font color
- Container elements show their background color as a colored swatch
- Image elements show a tiny thumbnail of the image
- Media elements show an icon indicator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(studio): add hover-to-highlight and auto-seek to Layers panel

Replace tiny preview thumbnails with hover highlighting — hovering a
layer row highlights the element in the preview canvas. Clicking a layer
auto-seeks the playhead to that element's start time in the timeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): layers panel tab order, autoseek, and renders toolbar overflow

- Reorder inspector tabs to Design → Layers → Renders
- Fix autoseek: walk DOM ancestors when selected element has no direct
  timeline match, so clicking a child like S2 Heading correctly seeks
  to the start of its parent scene
- Fix Renders toolbar overflow: add flex-wrap to the export controls
  header so selects and the Export button wrap instead of clipping

* fix(studio): seek to midpoint of element duration in layers panel autoseek

* fix(studio): layers panel seek now drives adapter.seek via requestSeek signal

setCurrentTime() only updated the store — adapter.seek() and liveTime.notify()
were never called so the iframe never moved. Add requestedSeekTime to the player
store; useTimelinePlayer subscribes and calls the real seek() path when it fires.

* feat(studio): hover over a layer auto-seeks to element midpoint (300ms debounce)

* feat(studio): add collapsible sections to Design panel

Section component now supports collapse/expand with a chevron toggle.
Text, Layout, and Fill sections stay expanded by default. Less-used
sections (Flex, Radius, Stroke, Effects, Clip, Transparency) start
collapsed to reduce scrolling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): remove stale selectedTimelineElement usages dropped in rebase

* fix(studio): stabilize resetErrors in useConsoleErrorCapture to break render loop

resetErrors was a new function object on every render. handlePreviewIframeRef
had it as a dep, so it also changed every render. NLELayout's useEffect watching
onIframeRef would re-fire, calling setPreviewIframe again, which re-ran
useConsoleErrorCapture with the new iframe — infinite loop.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 08:22:26 +02:00