* fix(player): drive composition ticks from widget-frame rAF via postMessage
Chromium throttles requestAnimationFrame in deeply nested cross-origin
iframes. In Claude desktop (Electron), the composition iframe's own rAF
loop stalls, so GSAP is never seeked and animation freezes even when
TransportClock.isPlaying() is true.
The correct fix is to drive ticks from the widget-frame rAF, which lives
one level up and is not subject to the same throttling. When play() takes
the runtime bridge path (no direct timeline adapter), the player now starts
a parent-frame rAF loop that sends "tick" postMessages to the composition
iframe on every frame. The runtime's control bridge handles "tick" by calling
seekTimelineAndAdapters(clock.now()) if the clock is playing — identical to
what transportTick does on each rAF, just driven from outside.
The composition iframe's own rAF loop is unchanged and keeps running
normally in standard browsers. Seeking GSAP twice per frame is idempotent,
so there is no regression on claude.ai or any other non-throttled environment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(player): address review issues in parent tick clock
- _paused must be false before _startParentTickClock runs; otherwise
the first RAF callback sees _paused=true and self-terminates immediately
- Guard _startParentTickClock behind this._ready && !this._directTimelineAdapter
so tick messages aren't sent into an uninitialized iframe when play() is
called before the composition probe has resolved
- Add clock.reachedEnd() check to onTick so end-of-composition handling
(pause, seek-to-end, postState) runs even when the composition iframe
RAF is fully throttled
- Stop the parent tick clock in seek() alongside _stopDirectTimelineClock
to avoid burning CPU frames while paused after a scrub
- Add onTick to bridge.test.ts createMockDeps() and add a dispatch test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: bump versions to 0.6.5-alpha.0 for testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(studio): decompose App.tsx from 4297 to 567 lines
Break the monolithic StudioApp component into focused modules:
Hooks (12 new):
- usePanelLayout: resizable/collapsible panel state
- useFileManager: file tree, CRUD, uploads, derived lists
- useManifestPersistence: manual edit + motion manifest save queue
- useTimelineEditing: clip move/resize/delete/drop handlers
- useDomEditSession: DOM selection, style/text commits, preview interaction
- useAppHotkeys: keyboard shortcuts, undo/redo, iframe hotkey sync
- useCaptionDetection: auto-detect caption compositions
- useRenderClipContent: timeline clip thumbnail rendering
- useConsoleErrorCapture: preview iframe console error capture
- useFrameCapture: frame capture download flow
- useLintModal: lint execution and modal state
- useCompositionDimensions: stage-size message listener
Components (6 new):
- AskAgentModal: agent prompt modal
- StudioHeader: toolbar with undo/redo, capture, inspector toggle
- StudioLeftSidebar: file tree + code editor (handles collapsed state)
- StudioPreviewArea: NLELayout + overlays + caption timeline
- StudioRightPanel: Design/Motion/Renders tab panel
- TimelineToolbar: zoom controls + timeline toggle
Utilities (4 new):
- studioHelpers: types, path helpers, DOM utilities
- studioPreviewHelpers: preview pointer/player interaction
- domEditHelpers: selection group algebra
- studioFontHelpers: font injection + @font-face management
Also removes dead timeline layer inspector code (eye icon, thumbnail
toggle, layer panel) that was disabled behind a feature flag.
* feat(studio): add Layer (z-index) field to design panel
Adds a scrub-enabled "Layer" field below the W/H inputs in the Layout
section. Available for all elements regardless of style editing
capability since z-index is fundamental to composition stacking order.
* docs: architecture spec for studio domain contexts, hook split, and file-size lint
* docs: implementation plan for studio contexts, hook split, and file-size lint
* refactor(studio): consolidate duplicate helpers in useDomEditSession
Remove ~370 lines of helper functions that were copied into the hook
instead of imported. All removed functions already exist in the
canonical utility files (studioHelpers, studioFontHelpers,
studioPreviewHelpers, domEditHelpers). Also removes the duplicate
local type definitions for RightPanelTab, AgentModalAnchorPoint, and
PreviewLocalPointer, and drops now-unused imports (googleFontStylesheetUrl,
importedFontFaceCss, resolveVisualDomEditSelectionTarget, DomEditViewport).
Temporarily excludes useDomEditSession.ts from the 500 LOC file-size
check until Tasks 3-5 split it into focused hooks.
* refactor(studio): extract useDomSelection from useDomEditSession
* refactor(studio): extract useAskAgentModal from useDomEditSession
* refactor(studio): extract usePreviewInteraction from useDomEditSession
* refactor(studio): extract useDomEditCommits, useDomEditSession now thin orchestrator
Split the 897-line useDomEditSession into focused hooks:
- useDomEditCommits (439 LOC): manifest commits (path offset, box size,
rotation, manual edits reset, motion), persist operations, element delete,
font asset resolution
- useDomEditTextCommits (329 LOC): style/text/text-field commits
- useDomEditSession (339 LOC): thin orchestrator wiring selection, agent
modal, preview interaction, and commit hooks
All files now under 500 LOC limit. Removed the temporary lefthook
filesize exclusion for useDomEditSession.
* feat(studio): add 4 domain contexts (PanelLayout, FileManager, DomEdit, Studio)
Create context providers that wrap hook return values for prop-drilling
elimination. Each context destructures and reconstructs the value inside
useMemo so exhaustive-deps is satisfied and re-renders are minimized.
Not yet wired into App.tsx — that comes in a follow-up.
* refactor(studio): wire domain contexts, eliminate prop drilling in 4 components
Wire StudioProvider, PanelLayoutProvider, FileManagerProvider, and
DomEditProvider in App.tsx. Migrate StudioHeader, StudioLeftSidebar,
StudioPreviewArea, and StudioRightPanel to consume contexts instead
of props.
Prop counts reduced:
- StudioHeader: 13 -> 6
- StudioLeftSidebar: 19 -> 4
- StudioPreviewArea: 37 -> 11
- StudioRightPanel: 39 -> 3
Net: -118 lines, 108 props removed from call sites.
* chore: upgrade to React 19
Upgrade react and react-dom from 18.3 to 19.2.6 across the workspace.
Add resolutions/overrides in root package.json to prevent peer
dependency pins (e.g. @phosphor-icons/react) from pulling React 18.
Regenerate bun.lock.
This enables the React 19 context syntax (<Context value={...}>)
used by the new domain contexts.
* fix(studio): refresh preview after z-index change so stacking updates visually
* fix(studio): remove duplicate duration override causing oscillation
The timeline message handler set the duration twice: once via
processTimelineMessage and once via a raw durationInFrames override.
When drilled into a sub-composition, these could disagree, causing
the duration to oscillate after element deletion.
* fix(studio): use in-place iframe reload after clip delete, remove confirm dialogs
Two changes to fix duration oscillation after deleting a timeline clip:
1. Replace setRefreshKey (full Player remount) with in-place
iframe.contentWindow.location.reload() after deleting a clip.
The full remount triggered a chaotic re-probing cycle with multiple
duration sources (adapter, manifest, postMessage) fighting each
other, causing the timeline to oscillate between durations.
In-place reload preserves the Player web component and its state.
2. Remove window.confirm dialogs from both timeline clip delete and
DOM element delete. Undo is available so the confirmation adds
friction without value.
* chore: gitignore docs/superpowers
* feat(studio): add favicon
* perf(studio): skip no-op state updates in timeline sync
syncTimelineElements was called 60+ times per page load, each time
triggering setElements/setDuration/setTimelineReady even when nothing
changed. This caused massive re-render churn and memory usage.
Add early-return guards to skip updates when values haven't changed.
Also fixes the duration oscillation after element delete.
* refactor(studio): split PropertyPanel.tsx (3126 LOC) into 8 focused modules
The monolithic PropertyPanel.tsx exceeded the 500 LOC filesize limit.
Split into cohesive modules by responsibility:
- propertyPanelHelpers.ts (401) — pure utility functions, shared types/constants
- propertyPanelPrimitives.tsx (357) — CommitField, MetricField, DetailField,
SliderControl, SegmentedControl, SelectField, Section
- propertyPanelColor.tsx (371) — ColorField, ColorSlider
- propertyPanelFill.tsx (421) — ImageFillField, GradientField, asset path helpers
- propertyPanelFont.tsx (455) — FontFamilyField + font catalog helpers
- propertyPanelSections.tsx (453) — TextSection, TextFieldEditor, text controls
- propertyPanelStyleSections.tsx (411) — StyleSections (stroke, effects, clip, fill)
- PropertyPanel.tsx (347) — main component, LayerTree, re-exports for consumers
All re-exports from PropertyPanel.tsx preserved for backwards compatibility.
No behavioral changes — pure structural split.
* fix(studio): use in-place iframe reload for all timeline operations
Replace setRefreshKey with in-place iframe reload for move, resize,
and asset drop — matching delete which was already fixed. Prevents
the Player remount probe cycle that causes duration oscillation.
* perf(studio): replace 5s polling loop with event-driven adapter init
The Player's onIframeLoad used a setInterval polling loop (25 attempts
× 200ms = 5 seconds) to detect when the runtime's __player/__timeline
globals appeared. Each poll that missed triggered wasted work, and
multiple duration sources fighting during the probe cycle caused
oscillation bugs.
Replace with event-driven initialization:
1. Fast path: try initializeAdapter() immediately (works for in-place
reloads where the adapter is already present)
2. If not ready, listen for the runtime's "state"/"timeline" postMessage
signals and initialize on the first one
3. Single 5s timeout as safety net (replaces 25 interval ticks)
This eliminates the polling overhead, reduces setDuration/setElements
calls to exactly 1 per load, and makes the Player responsive within
one frame of the runtime being ready instead of up to 200ms later.
* fix(studio): prevent duration oscillation after element delete
Two fixes for the duration display oscillating between sub-composition
and master durations after deleting an element in the preview:
1. Clear store elements before iframe reload in handleDomEditElementDelete.
Without this, stale pre-delete elements remain in the store and cause
mergeTimelineElementsPreservingDowngrades to alternate between REPLACE
and PRESERVE modes as the element count fluctuates.
2. Add 500ms cooldown on enrichMissingCompositions after timeline messages.
The "state" handler was calling enrichMissingCompositions every ~80ms,
which added extra elements from GSAP timelines. These fought with the
authoritative element list from "timeline" messages (~333ms), creating
a feedback loop where element count oscillated and triggered alternating
merge strategies with different durations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(studio): single reloadPreview as source of truth for preview refresh
Create reloadPreview() in App.tsx that encapsulates the correct
behavior (in-place iframe reload with setRefreshKey fallback). Pass it
as the sole refresh mechanism to hooks, removing direct setRefreshKey
access from useTimelineEditing and useDomEditCommits.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add rotation (R) field to geometry row (X, Y, W, H, R) in property panel.
Goes through manifest via handleDomRotationCommit, resettable with Reset Edits.
- Auto-promote display:inline elements to inline-block when dragged so
translate works on inline spans.
- Fix regression from polling fix: iframe load now passes readFromDiskFirst
to load manifest from disk, so Reset Edits finds existing entries.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Line height and letter-spacing: convert from free-text to select with presets
- Font style: remove oblique (browser falls back to italic), keep normal/italic
- Font weight: detect available weights via document.fonts.check(), add labels
- Font source: local fonts matching Google catalog tagged as Google
- Font list: balanced per-source caps prevent any source from being cut off
- Sort order: Google fonts rank before Local so curated fonts appear first
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the eye icon (inspector) and image icon (thumbnail toggle) from timeline
clips. The timeline layer inspector feature and all supporting code is removed.
Enable manual dragging in the preview by default. Add scrub-to-drag on X/Y/W/H
fields in the design panel. Hide the Radius section when the element has no
visible background. Fix pre-existing ResolutionPreset type for square presets.
Inspector picks invisible elements when an ancestor has GSAP-set opacity: 0
because CSS opacity is not inherited — getComputedStyle on the child still
returns 1. Walk the ancestor chain in the picker, domEditing, and overlay
visibility checks to catch this.
Also:
- Containers with all-invisible children are no longer selectable
- Selection/hover overlay hides during playback and while loading
- Undo/redo no longer double-refreshes (echo suppression for all file writes)
- Undo/redo reloads iframe in-place instead of recreating the Player,
preserving shader transition cache
- Preview routes return ETag + Cache-Control headers; composition HTML uses
project signature for conditional 304, binary assets use mtime+size
- Loading overlay deferred 400ms so cached loads never flash it
Three bugs that compound in Studio preview:
1. **Double audio on pause/resume**: syncRuntimeMedia played audio through
the HTML <audio> element while WebAudioTransport simultaneously played
the same source through AudioBufferSourceNode. Fixed by passing
webAudio.isActive() as outputMuted so HTML elements stay muted when
Web Audio owns playback. Also removed the priorMuted restore in
stopAll() which raced with the next play cycle.
2. **Manifest polling loop**: applyStudioManualEditsToPreview and
applyStudioMotionToPreview unconditionally fetched from disk on every
call, even without forceFromDisk. The runtime posts state messages
every frame via postMessage, triggering React re-renders that re-invoked
these functions ~60x/second. Fixed by returning early when no disk read
is requested, and using refs instead of callbacks in useEffect deps.
3. **Parent proxy double-play**: the player web component created parent-frame
audio proxies even when the runtime bridge was available, causing two
audio sources on autoplay-blocked promotion. Fixed by skipping proxy
creation when _hasRuntimeBridge returns true, and synchronously muting
iframe media on promotion to close the async race window.
Also fixes pre-existing ResolutionPreset type missing square variants.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
React registers onWheel passively, so preventDefault had no effect
on the parent scroll container. Replace with a native wheel listener
(passive: false) that blocks both default scroll and propagation.
- Move Text section to the top of the panel (before Layout)
- Remove Selection Colors section
- Rename "Blending" to "Transparency"
- Fix stroke Width/Style height mismatch by making SelectField
use inline label layout matching MetricField
Only show the composition loading overlay on the first iframe load.
Hot-reloads (source editor save, timeline edits, element delete)
no longer flash the full-screen loading state.
The four existing presets only cover 16:9 (landscape) and 9:16 (portrait)
aspect ratios. A 1080×1080 square comp had nowhere to land at any scale:
"Auto" rendered at the comp's authored 1080×1080, and picking 1080p or 4K
mapped to a landscape/portrait preset whose aspect ratio mismatched, which
the producer's resolveDeviceScaleFactor validator rejects with
"does not match the aspect ratio of the composition".
Add `square` (1080×1080) and `square-4k` (2160×2160) to CANVAS_DIMENSIONS
in core. The existing `keyof typeof CANVAS_DIMENSIONS` derivation
extends the `CanvasResolution` union and `VALID_CANVAS_RESOLUTIONS` array
automatically, so the producer's validator, the render API route, and
the CLI `--resolution` flag pick the new presets up without further
changes.
- core: extend CANVAS_DIMENSIONS, RESOLUTION_ALIASES, and the
htmlParser to recognize `data-resolution="square|square-4k"` and to
infer square from equal width/height (vs. the prior "square defaults
to portrait" tie-breaker).
- studio: extend the local ResolutionPreset / CANVAS_DIMENSIONS mirrors;
collapse isPortraitComp into a 3-way `compAspect` helper so
resolveResolution returns the square preset for square comps.
- cli: update --resolution help text on `init` and `render` to mention
the new presets.
- tests: add square cases to renderOrchestrator's resolveDeviceScaleFactor
suite (returns 1 for square→square, 2 for square→square-4k, rejects
landscape preset on square comp), update the htmlParser test that
previously pinned the "square→portrait" tiebreaker.
The consolidated handleAppKeyDown was only added to the parent
window. When focus was inside the preview iframe (after clicking
an element), keydown events didn't reach the parent, so Delete
and other shortcuts didn't fire.
Replace the per-function iframe forwarding (handleTimelineToggleHotkey
only) with the full app-level handler via a ref-stable wrapper.
All app shortcuts (Delete, Undo/Redo, Shift+T, Cmd+1/2) now work
from within the preview iframe.
Leftover from moving Delete handling to the consolidated
keyboard handler in App.tsx. Also suppress pre-existing
exhaustive-deps warning on the intentional every-render
selection-change watcher.
Cleanup from the /simplify pass on PR #715.
- App.tsx: subscribe to the runtime's `stage-size` message (which
carries authoritative width/height post-applyCompositionSizing)
instead of re-parsing data-width/data-height from the iframe DOM.
Drops the cross-origin try/catch, querySelector, and parseInt logic,
and fires once per comp load instead of on every state/timeline tick.
- App.tsx: import CompositionDimensions from RenderQueue instead of
inlining the shape.
- RenderQueue.tsx: replace scaleLabel() with a SCALE_LABEL record,
inline the one-call formatDims helper, and trim the type comment to
the WHY.
Two bugs in getSharedBrowser() could take down the entire Vite dev
server:
1. Unhandled rejection from puppeteer.launch() — the timeout error
surfaces through puppeteer's internal RxJS chain, and any uncaught
path crashes the Node process. The thumbnail route's try/catch
doesn't always intercept it.
2. _browserLaunchPromise was never reset on failure, so subsequent
thumbnail requests reused a stale rejected promise instead of
retrying.
Wrap the IIFE in try/catch, return null on any failure (the thumbnail
route already handles a null adapter result with a 500), and reset
_browserLaunchPromise in a finally block so a transient launch failure
doesn't poison the singleton. Also drop the launch timeout from
puppeteer's 30s default to 10s so a wedged handshake fails fast instead
of stalling every pending thumbnail.
Verified locally: the dev server now logs
"[Studio] puppeteer launch failed — thumbnails disabled: ..." and
keeps serving the studio UI after a thumbnail request fails.
The consolidated keyboard handler only checked selectedElementId
(timeline clips). When a user selected a child element in the
preview via the inspector, selectedElementId was null because
the element didn't correspond to a top-level timeline clip, so
Delete/Backspace did nothing.
Add handleDomEditElementDelete that removes the element referenced
by the current domEditSelection via the remove-element mutation
API. The Delete key handler now falls through from timeline
selection to DOM edit selection.
Orientation is a property of the composition, not a user choice — the
backend's portrait/landscape presets are tied to the comp's authored
aspect ratio. Letting users pick "1080p portrait" for a landscape
composition just produces a wrong-aspect render.
The dropdown now exposes three scale choices (Auto / 1080p / 4K) and
maps to the correct portrait/landscape preset based on the active
composition's data-width / data-height. Native <select title> tooltips
are unreliable across browsers, so the resolved dimensions render
inline in each option label (e.g. "1080p · 1920×1080") — always
visible, no hover needed.
App.tsx tracks the active comp's dimensions by listening for the
existing hf-preview state/timeline postMessages (same source the
caption-detection logic uses) and passes them to RenderQueue. The
useRenderQueue / backend contract is unchanged: RenderQueue still emits
"landscape" | "portrait" | "landscape-4k" | "portrait-4k" | "auto".
1. Sidebar tabs: use equal 1fr columns, shorter "Comps" label, truncate
on overflow, tighter padding. Fixes tabs clipping outside the rounded
pill at narrow sidebar widths.
2. Hot reload: set domEditSaveTimestampRef before every save-then-refresh
path (source editor, timeline move/resize/delete, asset drop). The
file-change watcher already checks this timestamp and suppresses
echoed events — but source editor saves and timeline operations
weren't setting it, causing a double refreshKey increment that could
leave the player in a non-playable state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move all window-level keyboard shortcuts from 4 separate files into
one `handleAppKeyDown` listener in App.tsx:
- Shift+T: toggle timeline (was App.tsx, separate useMountEffect)
- Cmd/Ctrl+Z: undo (was App.tsx, separate useEffect)
- Cmd/Ctrl+Shift+Z: redo (was App.tsx, separate useEffect)
- Cmd/Ctrl+1: sidebar Compositions tab (was LeftSidebar.tsx)
- Cmd/Ctrl+2: sidebar Assets tab (was LeftSidebar.tsx)
- Delete/Backspace: remove selected element (was Timeline.tsx)
LeftSidebar exposes a ref handle for tab switching. Timeline watches
selectedElement becoming null to clean up popover/range UI state.
History hotkey kept as named function for iframe forwarding.
Playback shortcuts (Space, J/K/L, arrows) and caption nudge remain
in their component hooks — tightly coupled to component state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Seeking a playing video resets the browser's decoder pipeline, causing
a ~150ms freeze while it re-buffers. During that freeze the monotonic
clock advances, drift grows, and strict sync fires another seek —
creating a perpetual stutter loop (176 seek events / 8s observed on
the apple-presentation composition).
Skip strict and force drift corrections for playing video elements;
only hard sync (>0.5s catastrophic drift) warrants the decoder-reset
cost. Audio elements are unaffected and retain the full correction
tiers.
Also propagate the asset-loading overlay state to the timeline so
controls are disabled during "Preparing preview assets", matching the
existing behavior for the initial composition loading overlay.
Restores handleDomAddTextField and handleDomRemoveTextField that were
dropped when resolving App.tsx conflicts during the main→next rebase.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The seekRuntimeTimeline helper added timeline.pause() before every
totalTime() seek. During transport-driven playback, this runs 60 times
per second, causing GSAP to cascade pause events to media elements on
every frame. The result: audio plays/stops/plays/stops in a stutter
pattern.
The captured root timeline is already paused once in player.play() —
the TransportClock drives it via totalTime(t) which keeps it paused.
The extra per-tick pause() was redundant for the root timeline but
actively harmful for media sync.
Fix: restore the original inline seek for the captured timeline
(totalTime without pause), keep seekRuntimeTimeline with pause() only
for standalone child timelines where explicit pause control is needed.
Also fixes rebase artifact: missing PropertyPanel props in App.tsx.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevent users from selecting elements in the preview while the
composition is still loading (showing "Loading composition" overlay).
Selection and hover highlighting are suppressed until the player fires
the ready event.
Also reverts motion panel and manual drag editing defaults to false —
these were accidentally set to true during the PR #693 merge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Power-user audit fixes for the alpha studio:
- vite.config.ts: wrap thumbnail generation in try/catch so Puppeteer
TimeoutError doesn't crash the entire vite dev server as an uncaught
rejection. Close the page on error to prevent browser session leaks.
- manualEditingAvailability.ts: enable motion panel and manual canvas
drag editing by default (were both false, undiscoverable without
knowing the env vars).
- PropertyPanel.tsx: show "N elements selected" feedback when multiple
elements are selected instead of the generic "Select an element"
empty state.
- RenderQueue.tsx + App.tsx: add FPS selector (24/30/60) to the render
export bar instead of hardcoding 30fps. Pass the user's choice
through to startRender.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The seek/play/applyAfter wrapper functions in manualEdits.ts crashed
with "Cannot set property X which has only a getter" when the player
or timeline objects define seek/play as getter-only properties. This
prevented ALL manual edits (position, rotation, size) from persisting
to disk — the error thrown during applyCurrentStudioManualEditsToPreview
aborted the save queue.
Wrapped all three property assignments in try/catch so wrapping
gracefully degrades when the target object is non-configurable.
Verified: position edit (X=42px) now persists to
.hyperframes/studio-manual-edits.json and survives page refresh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Motion panel stays opt-in via env var per product direction. Only
the Design panel is enabled by default.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The shared Puppeteer browser pool in getSharedBrowser() could throw a
30s TimeoutError during launch. This error propagated as an uncaught
rejection and killed the vite process, even though generateThumbnail
had its own try/catch — the browser launch promise rejected outside
that scope. Now getSharedBrowser itself catches launch failures and
returns null, so thumbnails degrade gracefully instead of crashing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes and improvements based on power-user testing feedback:
1. Fix stale selection after style edits — handleDomStyleCommit now
calls refreshDomEditSelectionFromPreview after persisting, matching
every other commit handler. Without this, the PropertyPanel showed
frozen computedStyles after color/radius/shadow edits, making it
look like editing "didn't work." Also adds error handling around
the persist call.
2. Add rotation field to the Design panel Layout section — reads the
current rotation angle from the manual edit manifest and commits
via the existing handleDomRotationCommit handler.
3. Enable motion panel by default — STUDIO_MOTION_PANEL_ENABLED now
defaults to true so the Motion tab is discoverable without env vars.
4. Color controls only when element has color — fill color section now
only shows when the element has an explicit non-transparent
background-color. Text color shows only when the element has a
color style. Prevents showing color pickers on elements where
color edits have no visible effect.
5. Exclude canvas from selection — added "canvas" to
DOM_LAYER_IGNORED_TAGS so canvas elements are not selectable in the
preview or listed in the layer panel.
6. Multi-selection feedback — shows "N elements selected" with
guidance instead of the generic empty state when multiple elements
are selected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>