Commit Graph

2939 Commits

Author SHA1 Message Date
Miguel Angel Simon Sierra f24e7f3393 feat(studio): collapsed-row ease editor bulk-edits all colliding animations
When the focused segment spans multiple source animations (collidingAnimationIds),
the ease editor commits through onUpdateSegmentEase (one-undo batch fan-out to all
of them) instead of the single-animation path; a single-property segment keeps the
single path. Show an 'Applies to N properties' note for multi-id segments; the
starting curve is the primary animation's ease (mixed eases collapse to primary).
The curve drag has no live-preview callbacks wired, so no preview fan-out is added.
Completes the bulk collapsed-row ease editor; supersedes the hide-on-ambiguity
behavior.
2026-07-16 11:27:20 -04:00
Miguel Angel Simon Sierra dc2d897bcb feat(studio): bulk ease commit fanning out to all colliding animations (one undo)
Add handleUpdateSegmentEase(animationIds, tweenPct, ease): a single id commits
through the existing single-mutation path; multiple ids dispatch one ease-update
mutation per id through the commit layer's batch path (gsap-mutations-batch) so
the whole fan-out is ONE undo entry, in the given deterministic order.
handleUpdateKeyframeEase is preserved as a thin single-id wrapper (no behavior
change). Exposed as onUpdateSegmentEase through the panel callback bag.
2026-07-16 11:27:20 -04:00
Miguel Angel Simon Sierra 3b77b01b96 feat(studio): always show the inline ease button on merged segments
Remove the collision-based suppression: the collapsed-row inline ease button now
shows on every segment (bulk edit handles the shared-keyframe case in U4/U5).
Only a keyframe with no source animation id (runtime-scanned) still hides it.
2026-07-16 11:25:49 -04:00
Miguel Angel Simon Sierra 6e63eb5f2f feat(studio): thread colliding animation ids into focusedEaseSegment
onSelectSegment now carries the merged keyframe's collidingAnimationIds through
the target and into focusedEaseSegment (alongside the primary animationId,
tweenPercentage, and elementId scoping). Single-animation segments leave the set
undefined. Sets up the bulk ease commit + editor surface (U4/U5).
2026-07-16 11:25:48 -04:00
Miguel Angel Simon Sierra 9056edad0d feat(studio): record colliding animation ids on merged keyframes
Foundation for bulk collapsed-row ease editing: each merged keyframe now carries
collidingAnimationIds (the source animation ids sharing that clip %, first-seen
order, deduped) via a shared accumulateCollidingAnimationIds helper used by both
merge sites (deduplicateKeyframes and updateKeyframeCacheFromParsed), replacing the
boolean easeAmbiguous. The inline ease button gate temporarily derives ambiguity
from the set length; U3 removes that suppression entirely.
2026-07-16 11:25:48 -04:00
Miguel Angel Simon Sierra ecbe482feb fix(studio): scope ease ambiguity + focus by identity, not raw ease/selection
- Flag a collapsed-row segment ambiguous on ANY cross-animation collision at the
  same %, not only when raw eases differ: the single inline button can target
  only one arbitrary animation, and each may inherit a different easeEach/animation
  ease that raw comparison misses.
- Scope the flat inspector's ease-focus to the element the panel actually renders
  (its own sourceFile#id) instead of the store's selectedElementId, which flips
  synchronously during async selection resolution and let a stale panel consume a
  focus meant for its successor when both share a class-selector animation id.
- Rename the stale byte-identical cache test to describe the shape+identity it now
  asserts.
2026-07-16 11:25:48 -04:00
Miguel Angel Simon Sierra 861ab9e765 fix(studio): commit non-selected keyframe deletion through the clicked element
The earlier removal fix resolved the animation from the clicked element but
still called removeKeyframeTarget without a selection override, so the mutation
persisted through the current domEditSelection — wrong when the clicked element
lives in a different source file. Build the clicked element's own DomEditSelection
and pass it as the override. Tests assert the override fires for a non-selected
element's flat and keyframed endpoints.
2026-07-16 11:25:47 -04:00
Miguel Angel Simon Sierra bc2d918707 fix(studio): harden diamond retime (post-move identity, boundary clamp, cleanup)
Re-review of the earlier retime fixes found edge-case gaps:
- Rapid second retime now submits the pending (already-moved) position as the
  mutation's FROM identity, not the stale rendered one, so the serialized
  mutation locates the keyframe the first move relocated.
- Clamp the reselected tween % to the mapped range: clipToTweenPercentage
  extrapolates, so a boundary drag past the range no longer reselects an
  out-of-range % (e.g. 150%) while the mutation clamps the endpoint to 100%.
- Clear pending retime entries by tolerance, not exact-% equality (cache writers
  round), so entries no longer leak after a successful retime.
- Hide the inline ease button on a segment whose keyframe has no source
  animation id (runtime-scanned), avoiding a dead button with no target.
2026-07-16 11:25:47 -04:00
Miguel Angel Simon Sierra 9027a579ad fix(studio): suppress inline ease button on ambiguous collapsed-row segments
The collapsed clip row merges a clip's source animations and dedups keyframes
by percentage (last ease wins), so an inline ease segment where two animations
collide at the same clip % with different eases targeted an arbitrary tween.
Flag such merged keyframes (easeAmbiguous) at both merge sites (deduplicateKeyframes
and updateKeyframeCacheFromParsed), computed before the ease overwrite, and hide
the inline ease button on those segments; unambiguous segments are unchanged and
the user edits colliding ones by expanding the clip's per-lane view.
2026-07-16 11:25:47 -04:00
Miguel Angel Simon Sierra 629ea7e82f fix(studio): compose a rapid second retime from the pending position
A second diamond retime issued before the first commit's async cache rebuild
resolved its delta from the original pointer-down clip %, so it landed on a
stale/no-op target and was silently discarded. Track the last-resolved clip %
per keyframe key and use it as the next drag origin; clear it once the cache
reflects that percentage. Preserves U1's post-move reselect; no optimistic hold.
2026-07-16 11:25:46 -04:00
Miguel Angel Simon Sierra 4e1a537a89 fix(studio): reselect a retimed keyframe at its post-move tween percentage
The move branch reselected with {...target, percentage: toClipPct}, keeping the
stale pre-move tweenPercentage, so activeKeyframePct and a later canvas edit
targeted the old keyframe. Derive the post-move tween % from the new clip %
via clipToTweenPercentage (scoped to the target animation's keyframes; cache
rebuild is async so we can't read it back synchronously).
2026-07-16 11:25:46 -04:00
Miguel Angel Simon Sierra 3824459e82 fix(studio): resolve keyframe removal from the clicked element, not the selection
onDeleteKeyframe resolved the animation from selectedGsapAnimations, so removing
a flat-tween lane endpoint on a non-selected element found nothing and no-opped.
Resolve the clicked element's animations from the store (source-prefixed /
index.html / bare-id keys) and thread them through resolveKeyframeTarget and
removeKeyframeTarget; the drag-commit path keeps using the selection. Covers
non-selected flat + keyframed deletion and the selected-element regression.
2026-07-16 11:25:46 -04:00
Miguel Angel Simon Sierra 1c67e2de8f test(studio): enable thumbnails in the empty-label test (fixes pre-existing failure)
The thumbnails-off-by-default toggle (6b5ea8257) left this test asserting
thumbnail content without enabling thumbnails, so it produced no thumbnail
content and failed. Enable thumbnailsEnabled for the case and reset it in
afterEach; the label ownership assertion is unchanged (TimelineClip owns names).
2026-07-16 11:25:45 -04:00
Miguel Angel Simon Sierra 33eb91abc5 test(studio): assert elementId on focusedEaseSegment in the segment-focus test 2026-07-16 11:25:44 -04:00
Miguel Angel Simon Sierra 05cf38095f fix(studio): scope inline ease focus to its element; clear drag preview on cancel
- focusedEaseSegment now carries elementId (set from the clicked timeline
  element). PropertyPanelFlat and FlatMotionSection only open/consume the ease
  editor when it matches the selected element, so a shared class-selector
  animation id can no longer open the wrong element's editor. Verified the
  common single-element inline-ease path still opens correctly.
- TimelineClipDiamonds: clear the armed drag and preview on pointercancel so an
  interrupted diamond drag no longer leaves a stuck ghost.
2026-07-16 11:25:43 -04:00
Miguel Angel Simon Sierra 17e1435ffd fix(studio): reserve label column for flat-tween lanes; clamp bezier control X
- Timeline: hasKeyframedClips now uses animationContributesLane (the same
  predicate the layout/auto-expand use) instead of checking animation.keyframes
  directly, so a composition whose only animated clips are synthesized flat
  tweens still reserves its label column and gutter.
- EaseParamFields: clamp cubic-bezier control-point X to [0,1] on commit so an
  out-of-range entry can't persist as a non-monotonic (broken) custom ease; Y
  stays free for overshoot curves.
2026-07-16 11:25:42 -04:00
Miguel Angel Simon Sierra ffa71e6b35 fix(studio): address code-review findings (cache identity, spring-ease compat, tests)
- useGsapTweenCache: the file-wide cache producer now stamps animationId on
  every keyframe, matching the selected-element and post-mutation writers, so
  the inline clip-row ease button works on a collapsed clip that was never the
  selected element (it silently no-opped before).
- springEase: restore the deprecated re-export of the parsers spring-ease
  symbols on core's still-published ./spring-ease subpath (dropping them was a
  breaking change for external importers).
- gsapKeyframeCacheHelpers.test: update two stale exact-shape assertions that
  omitted animationId and were failing against the producer (pre-commit runs no
  vitest, so they slipped in).
- EaseCurveSection: replace an em dash in the preset hint with a colon.
2026-07-16 11:25:42 -04:00
Miguel Angel Simon Sierra f9cdc600be fix(studio): inline ease button opens the ease editor in the flat inspector
The flat inspector never consumed focusedEaseSegment: FlatMotionSection
rendered AnimationCard without focusedSegment/onFocusSegmentConsumed, and
the accordion left the Motion group collapsed, so clicking the inline
clip-row ease button only selected the element. Mirror the legacy
GsapAnimationSection: pass the focused segment to the matching card and
open the Motion group when a segment on this element is focused.
2026-07-16 11:25:42 -04:00
Miguel Angel Simon Sierra c0f7834b01 fix(studio): show the segment ease button on inline clip-row keyframes
The mid-point ease button only rendered in the expanded per-property lanes: it
was gated on `groupAware`, and the inline clip-row diamonds are rendered
group-unaware without an onSelectSegment handler. The ease is per-keyframe
(each carries its own animationId/tweenPercentage), so the button resolves
unambiguously on the merged inline row too. Thread animationId into the
keyframe cache, gate the button on onSelectSegment (with an always group-aware
target), and forward onSelectSegment through the inline TimelineClipDiamonds.
2026-07-16 11:24:53 -04:00
Miguel Angel Simon Sierra 0b264c1f15 fix(studio): keep ease-curve draft until the commit round-trips (no flicker)
Releasing a bezier control point committed the new ease AND cleared the local
draft synchronously. The committed ease prop only returns a few frames later
(write -> reparse -> re-render), so in the gap activeTuple fell back to curve
derived from the STALE prop -> the curve snapped to the old value for a frame
then jumped to the new one. Keep the draft on screen and clear it via an effect
when the ease prop actually changes, so curve already equals the draft when the
handoff happens.
2026-07-16 11:24:53 -04:00
Miguel Angel Simon Sierra e850f3891f fix(studio): address element targets by a valid selector for non-identifier ids
Element ids that aren't valid CSS identifiers — most commonly digit-leading ones
like 01-hook-hero-word — make #id an invalid selector: querySelector and GSAP's
querySelectorAll throw a SyntaxError, surfacing as a masked cross-origin Script
error that crashes the preview the instant such a target is committed (dragging
the element writes gsap.set(#01-...)). Route the studio's commit/selection
selector building through a shared idSelector() that falls back to an
[id=...] attribute selector for those ids.
2026-07-16 11:24:53 -04:00
Miguel Angel Simon Sierra 5730959f75 fix(studio): resolve Chrome for dev thumbnails via env + puppeteer cache
Dev-server thumbnail generation only probed three hardcoded system Chrome paths,
so on a machine with no system Chrome install (but a Puppeteer-cached
Chrome-for-Testing) it found no browser and every thumbnail request 500'd.
Resolve the executable from PUPPETEER_EXECUTABLE_PATH / CHROME_PATH / CHROME_BIN,
then a broader set of system browsers, then the Puppeteer cache (highest
version).
2026-07-16 11:24:53 -04:00
Miguel Angel Simon Sierra a46cea5a6f fix(studio): repair keyframes inner-ease baked before custom-ease registration
A GSAP keyframes tween resolves its inner timeline's ease ONCE, at build time,
via the internal ease map. The composition inline script builds these tweens
before the runtime registers the custom eases (hold/spring/wiggle/custom), so a
custom container ease bakes the inner _ease to undefined; GSAP then throws
_ease is not a function on the first render (masked as a cross-origin Script
error) on composition load and every manual edit. Re-resolve each keyframes
tween's inner ease during rebind, once the eases are registered.
2026-07-16 11:24:53 -04:00
Miguel Angel Simon Sierra 784cc29686 fix(studio): re-assert custom-ease registration at the timeline render site
The one-shot ensureStudioCustomEase() in init runs before GSAP may be ready and
never retries, so on a lost load-order race the custom eases are unregistered
when bindRootTimelineIfAvailable fires its prime render (progress/totalTime) —
and a keyframe segment using hold/spring/wiggle/custom throws _ease is not a
function (masked as Script error on composition load). Call the idempotent
ensure at the top of the rebind so registration is guaranteed before any render.
2026-07-16 11:24:53 -04:00
Miguel Angel Simon Sierra 434ee9b962 fix(studio): register custom eases in GSAP internal map so keyframe segments resolve
Custom eases (hold/spring/wiggle/custom) only overrode the public gsap.parseEase.
GSAP resolves a keyframe segment's ease through its INTERNAL _parseEase/_easeMap,
which never saw the override, so a custom ease inside keyframes:{...} resolved to
undefined and threw _ease is not a function on the first render (surfacing as a
masked cross-origin Script error on every keyframes composition and every manual
edit's soft-reload rebind). Register the eases in the internal map too; a
configurable ease's .config rejoins GSAP's comma-split params to losslessly
reconstruct the original string, including custom() bezier paths.
2026-07-16 11:24:52 -04:00
Miguel Angel Simon Sierra 43dd2567ec fix(parsers): exclude per-keyframe duration from object-keyframe properties
A %-keyed object keyframe step's duration (GSAP array-keyframe segment
timing) was collected as an animatable property, so it surfaced as a bogus
duration keyframe lane and got round-tripped as a property — corrupting the
tween on the next manual edit in Studio. Both the live acorn read parser and
the retired recast parser had the leak; the array-form parser already
excludes it. Skip duration in the %-keyed extraction to match.
2026-07-16 11:24:52 -04:00
Miguel Angel Simon Sierra 9f96c219a2 refactor(core): wire runtime control bridge after all transport helpers
Follow-up to the load-time TDZ fix: the real invariant is that the runtime's
external control surface must not be callable before the helpers its handlers
dispatch to are declared. Moves installRuntimeControlBridge to run after every
transport helper (seekTimelineAndAdapters, applyWebAudioRate, ...), so a
load-time seek / set-playback-rate can never reach an uninitialized helper.
One owner of the 'runtime is controllable' boundary -> the TDZ class can't
recur via a future helper added below the wiring.
2026-07-16 11:24:52 -04:00
Miguel Angel Simon Sierra e8be020b5d feat(studio): timeline clip thumbnails off by default with a toolbar toggle
Background thumbnail generation (per-clip composition/video frame capture)
lags timeline scrubbing on long videos (#2428). Adds a persisted
thumbnailsEnabled setting, default OFF, so the timeline is snappy out of
the box; a toggle next to Fit in the zoom toolbar turns thumbnails on.
When off, clips render as plain bars (audio still shows its waveform).
2026-07-16 11:24:52 -04:00
Miguel Angel Simon Sierra 7e41582a20 fix(core): hoist runtime transport helpers to avoid load-time TDZ
seekTimelineAndAdapters and applyWebAudioRate were const arrows declared
~370-680 lines after the control bridge + transport that call them. When
the studio issues a seek / set-playback-rate during composition load
(before those consts initialize) the runtime threw ReferenceError: Cannot
access '...' before initialization, cascading to 'l._ease is not a
function' in GSAP. Hoisting both to function declarations makes them
callable regardless of init order.
2026-07-16 11:24:52 -04:00
Miguel Angel Simon Sierra a749295804 feat(studio): keyframe timeline (lanes, ease editor) reconciled onto main
Squash-merge of the keyframe-timeline feature onto the current main, which
had refactored the timeline in parallel. Preserves main's work (z-order
mirror, track gap menu, gap highlights, hook extraction) and layers the
keyframe feature on top: expandable property lanes, per-segment ease editor
(curve/spring/wiggle), flat-tween lanes, prev/next keyframe nav.

Unified the horizontal origin on the threaded contentOrigin parameter
(GUTTER collapsed, LABEL_COL_W in keyframe mode); main's TRACKS_LEFT_PAD
horizontal pad is superseded by the label-column model.
2026-07-16 11:24:51 -04:00
Miguel Ángel b8a44e49c9 fix(lint): ignore !important in font families (#2538) 2026-07-16 10:43:17 -04:00
WaterrrForever f8c33cab72 feat(skills): act on stale CLI pin during project resume (#2540)
* feat(skills): probe and bump stale CLI pins during project resume

The entry skill now keeps a resumed project's pinned CLI current instead of
leaving that to a notice nobody acts on. On resuming a project with pinned
scripts, run the read-only probe 'npx hyperframes@latest upgrade --project
. --check'; when it (or the stale-pin stderr notice, or _meta.updateAvailable
from a pinned run) reports the project behind, apply the bump and verify
with 'hyperframes check'. A failed check reverts the bump and keeps the
project on its pinned version, preserving the reproducibility contract the
pin exists for.

The probe matters because the stale-pin notice only exists in >= 0.7.59:
a pinned run of an older CLI prints no warning at all, so a notice-only
trigger never fires for exactly the projects most behind. The probe runs
unpinned, so its behavior never depends on the project's CLI age.

Telemetry: the fleet converges to new releases within about a week via the
background auto-updater and ephemeral npx, but pinned projects form a
persistent stale tail (~10% of weekly actives, e.g. 6.3k users still on
0.6.x three weeks after 0.7.0).

Both skill surfaces now pass an explicit dir ('--project .') because a bare
'--project' followed by another flag consumes that flag as its directory
value and no-ops; the parsing fix is a separate CLI change.

* fix(cli): stop bare --project from eating the next flag as its directory

citty parses --project as a string option, so 'upgrade --project --check'
arrived with project="--check": the dir resolved to a nonexistent path and
the command no-opd with 'No package.json found' while --check was lost.
The documented default-cwd behavior only worked when --project was the
final token — and the trap-prone form is exactly what the scaffolded
template CLAUDE.md instructs.

A leading dash can never be a real directory argument, so resolveProjectArgs
now reclaims the eaten token as the flag the user wrote (--check / --json),
falls back to the current directory, and drops unrelated eaten flags rather
than treating them as paths. Templates and skill references switch to the
explicit-dir form ('--project .'), which behaves correctly on every release
including ones that predate this fix.

* feat(skills): report a successful pin bump in the run summary

Review follow-up on the stale-pin rule: 'hyperframes check' validates
composition structure, not render-output equivalence, so a check-passing
bump can still shift a project's rendered output. The bump stays the right
default for stale projects, but it must not be silent — the summary now
names the old and new version so the user knows the reproducibility
trade was made.
2026-07-16 22:28:11 +08:00
James Russo 1b4cf12cd3 refactor(cli): add stderr diagnostics logger + oxlint guard (#2551) 2026-07-16 09:35:27 -04:00
James Russo d92d1d4f51 fix(cli): keep diagnostics on stderr for --json commands (#2522) 2026-07-16 09:14:39 -04:00
Somansh Reddy 5e78a29af3 docs(guides): add Claude Design → Send to HyperFrames single-file import guide (#2131)
* docs(guides): add Claude Design → Send to HyperFrames single-file import guide

Self-contained authoring guide for the 'Send to HyperFrames' MCP import path
(distinct from the download-ZIP flow): single self-contained HTML wire format,
the composition contract the importer validates, inline-brand-assets fidelity
rules (base64 data: URIs), no bundled/splash artifacts, keep-the-original-as-
reference, a complete worked skeleton, the animation-pattern catalog, and
determinism rules. Reviewed for technical accuracy against a known-good
imported+rendered composition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(send-to guide): assets must be resolvable (inline OR fetchable URL), not inline-only

Large media can exceed the import size cap, so "inline everything" is wrong for it,
and the import tool description now states "resolvable refs." Align the guide: inline
data: URIs preferred (fonts/images/logos — the only fully self-contained form), a
publicly-fetchable absolute URL allowed for large media, and explicitly forbid relative
paths / local-file variables / expiring-or-private hosts (the media-expiry trap).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 04:43:57 -07:00
Vance Ingalls dbb684fa6c Merge pull request #2535 from heygen-com/fix/runtime-partial-timeline-pause-guard
fix(runtime): tolerate registry timelines without pause() in interactive transport
2026-07-16 03:03:22 -07:00
Vance Ingalls 74f52fcd96 Merge pull request #2534 from heygen-com/fix/panel-sections-audio-layout-style-gate
fix(studio): gate Layout and Style panel sections on the element having a rendered box
2026-07-16 02:43:57 -07:00
Vance Ingalls 7e0bd401c8 chore(core): regenerate position-edits-render artifact with current esbuild
The committed artifact was generated with an older esbuild than the
current lockfile resolves; CI's check:position-edits-render regen now
produces different (equivalent) minifier variable naming and fails the
diff gate. Regenerate to match — no source change to positionEdits.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 02:17:36 -07:00
Vance Ingalls eb967f1542 Merge pull request #2533 from heygen-com/fix/studio-promotable-badge-overlap
fix(studio): reserve a gutter for the promote-to-variable badge
2026-07-16 02:06:28 -07:00
Vance Ingalls 1a2e7524e2 Merge pull request #2532 from heygen-com/fix/studio-flat-stroke-select
fix(studio): make flat inspector's Stroke width free of style-name typing
2026-07-16 02:06:07 -07:00
Vance Ingalls f96b78165e Merge pull request #2528 from heygen-com/fix/studio-flat-row-values-right-aligned
fix(studio): right-align FlatRow's value input in the flat inspector
2026-07-16 02:05:48 -07:00
James Russo 6a1585df27 refactor(render): centralize execution ownership (#2158) 2026-07-16 05:00:40 -04:00
Vance Ingalls 8826d2d71d fix(runtime): tolerate registry timelines without pause() in interactive transport
A window.__timelines entry is authored content and may be a partial
RuntimeTimelineLike (duration/seek only, no pause). Timeline resolution
is deliberately permissive — duration-based — and such compositions
render fine, because the render path only seeks. But every interactive
transport path (play/pause/seek, bind, rebind-tick, boot) called
capturedTimeline.pause() unguarded, crashing studio playback with
'tl.pause is not a function' — the top recurring studio:unhandled_error
in telemetry across versions 0.6.121 through 0.7.59 (~150-175/day).

Guard all pause sites through one helper (typeof check + swallow, plus
a once-per-page timeline_missing_pause analytics event so composition
authors can find the partial timeline), matching the safeVoid pattern
player.ts already uses. In the rebind restore path, pause is guarded
separately so a missing pause() no longer aborts the seek/play restore
behind it in the same try/catch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 01:58:39 -07:00
Vance Ingalls ff26e5f2c8 Merge pull request #2529 from heygen-com/via/resolution-portrait-fix
fix(cli): accept portrait aspects for --resolution alias flag
2026-07-16 01:54:55 -07:00
Vance Ingalls 8a793ba2b2 fix(studio): gate Layout and Style panel sections on the element having a rendered box
Both panels showed Layout (X/Y/W/H/Rotation/Z-index) unconditionally —
no gate existed for it at all — and Style was gated only on
canEditStyles (a permission check), never on the element's tag. Neither
gate accounted for `<audio>`, which never paints a visual frame, so a
music track's inspector showed a full set of position/size/fill/shadow
controls with zero visual effect.

Add `layout`/`style` applicability to resolveEditingSections (core),
keyed on tag !== "audio", and gate both panels' Layout section and the
existing Style gate on it. Media/Motion/Grade/Text were already
correctly gated (verified via a research pass across both panels) and
are untouched.
2026-07-16 01:47:46 -07:00
Vance Ingalls 14dbfca5a7 fix(studio): give flat Text section rows more vertical breathing room
6px between rows left little clearance above a row's value for the
promote-to-variable badge (now positioned above the row). Widen the
row gap to 10px.
2026-07-16 01:17:04 -07:00
Vance Ingalls 5f045b6bdf fix(studio): move the promote-to-variable badge above the row instead of a padding gutter
Shrinking the wrapped control's width to make room for the badge (previous
commit) fixed the overlap but pushed the value left unnecessarily. Move the
badge to sit above the row instead, clearing the value without touching its
layout.
2026-07-16 01:11:30 -07:00
Vance Ingalls 9fa1b26570 fix(studio): reserve a gutter for the promote-to-variable badge
PromotableControl absolutely-positions its "◇ var" / "◆ {id}" badge over
the wrapped control without reserving any space, so on rows where the
value renders flush to the right edge (flat Font/Color rows) the badge
sat directly on top of the value text instead of beside it. Add a
right-padding gutter on the wrapper sized to each badge state, and cap
the bound chip to a fixed max-width so it always fits inside its gutter.
2026-07-16 01:06:11 -07:00
Via 2d398ed274 fix(cli): wire aspect-agnostic resolution through cloudrun/lambda/batch + preflight recompute
Addresses R2 CHANGES_REQUESTED from Miga + Rames on PR #2529:

1. Sibling-surface gap (blocker): `hyperframes cloudrun render{,-batch}`,
   `hyperframes lambda render{,-batch}` all advertised the same tier-only
   aliases (`1080p` / `hd` / `4k` / `uhd`) but normalized them to `landscape`
   and never set `outputResolutionAspectAgnostic`. The distributed plumbing
   PR #2529 added received `undefined` from those callers, so portrait `1080p`
   still hit the original aspect-mismatch on Cloud Run / Lambda.

   Fix: introduce `resolveResolutionFlagPair` in `@hyperframes/parsers` (the
   single source of truth for the two-step normalize + aspect-agnostic
   detect) and route every distributed entrypoint through a shared
   `parseOutputResolutionFlag` CLI util so the alias signal now reaches
   `SerializableDistributedRenderConfig`. Studio Server keeps its
   canonical-only HTTP contract; that intent is now pinned in tests.

2. Preflight recompute (hardening): the earlier "downgrade aspect-mismatch"
   preflight cleared un-remapped mismatches, so IG 4:5 (non-preset aspect,
   no sibling) and portrait-4K comp + `--resolution 1080p` (remap +
   downsample) both slipped through to fail late in `resolveDeviceScaleFactor`.
   Now `checkRenderResolutionPreflight` computes the effective preset via
   `suggestMatchingPreset` (mirroring the compile stage's
   `adaptAspectAgnosticResolution`) and re-checks against that — only
   genuinely-fixable mismatches clear early. New tests pin both regressed
   input classes.

3. Docker forwarding boundary test (Miga's important #2): pinned
   `1080p` survives verbatim as `--resolution 1080p` in the Docker args
   so the in-container CLI can re-run `isAspectAgnosticResolutionAlias`.

4. Doc-nit (Miga): parsers/src/types.ts no longer references the
   nonexistent `resolveResolutionForComposition` — points at the actual
   remap helpers.

Fallow: cloudrun.ts / lambda.ts share 390 lines of pre-existing structural
symmetry (parallel AWS + GCP dispatchers), and lambda/render.ts +
render-batch.ts declare parallel RenderArgs interfaces. Both re-flagged
after threading the aspect-agnostic field through each surface; ignored
with justification in .fallowrc.jsonc. lambda.ts's `run` and
lambda/render.ts's `waitForCompletion` are pre-existing CRAP-score
hotspots untouched by this PR — added under health.ignore.

Co-Authored-By: Claude <noreply@anthropic.com>

— Via
2026-07-16 08:01:56 +00:00
Vance Ingalls d5a259a3d2 fix(studio): make flat inspector's Stroke width free of style-name typing
Stroke width committed border-width and border-style together from one
free-text field, so setting a style meant typing an exact CSS keyword
(e.g. "dashed") with no indication of which ones were valid — the row
also duplicated the discoverable Stroke style select directly below it.
Stroke width now only commits border-width; style changes go exclusively
through the existing select.
2026-07-16 00:48:53 -07:00