From f40e9456a88a07ea10459f05b968cc637bd429fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 5 Jun 2026 13:33:06 -0400 Subject: [PATCH 1/6] fix(studio): sync code editor after GSAP mutations + clear keyframe cache on deletion Two stability fixes for the keyframe editing flow: 1. Code editor refreshes after GSAP script mutations via onFileContentChanged callback that syncs result.after to editingFile state. 2. Keyframe cache clears on deletion: commitMutation removes cache entries for elements without keyframes, usePopulateKeyframeCacheForFile clears stale entries before repopulating. Also fixes pre-existing typecheck: handleGsapMaterializeKeyframes optional in TimelineToolbar DomEditSessionSlice. --- packages/studio/src/App.tsx | 1 + .../src/contexts/FileManagerContext.tsx | 3 +++ .../studio/src/hooks/useDomEditSession.ts | 3 +++ packages/studio/src/hooks/useFileManager.ts | 7 ++++++ .../studio/src/hooks/useGsapScriptCommits.ts | 23 ++++++++++++++++--- .../studio/src/hooks/useGsapTweenCache.ts | 9 +++++++- 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index 76f4048c1..5bbaaf29e 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -334,6 +334,7 @@ export function StudioApp() { queueDomEditSave: previewPersistence.queueDomEditSave, readProjectFile: fileManager.readProjectFile, writeProjectFile: fileManager.writeProjectFile, + updateEditingFileContent: fileManager.updateEditingFileContent, domEditSaveTimestampRef, editHistory: { recordEdit: editHistory.recordEdit }, fileTree: fileManager.fileTree, diff --git a/packages/studio/src/contexts/FileManagerContext.tsx b/packages/studio/src/contexts/FileManagerContext.tsx index f634eb8a1..51fe9308b 100644 --- a/packages/studio/src/contexts/FileManagerContext.tsx +++ b/packages/studio/src/contexts/FileManagerContext.tsx @@ -26,6 +26,7 @@ export function FileManagerProvider({ readProjectFile, writeProjectFile, readOptionalProjectFile, + updateEditingFileContent, revealSourceOffset, openSourceForSelection, handleFileSelect, @@ -64,6 +65,7 @@ export function FileManagerProvider({ readProjectFile, writeProjectFile, readOptionalProjectFile, + updateEditingFileContent, revealSourceOffset, openSourceForSelection, handleFileSelect, @@ -96,6 +98,7 @@ export function FileManagerProvider({ readProjectFile, writeProjectFile, readOptionalProjectFile, + updateEditingFileContent, revealSourceOffset, openSourceForSelection, handleFileSelect, diff --git a/packages/studio/src/hooks/useDomEditSession.ts b/packages/studio/src/hooks/useDomEditSession.ts index 1ee449de8..81fe6904d 100644 --- a/packages/studio/src/hooks/useDomEditSession.ts +++ b/packages/studio/src/hooks/useDomEditSession.ts @@ -59,6 +59,7 @@ export interface UseDomEditSessionParams { queueDomEditSave: (save: () => Promise) => Promise; readProjectFile: (path: string) => Promise; writeProjectFile: (path: string, content: string) => Promise; + updateEditingFileContent: (path: string, content: string) => void; domEditSaveTimestampRef: React.MutableRefObject; editHistory: { recordEdit: (entry: RecordEditInput) => Promise }; fileTree: string[]; @@ -100,6 +101,7 @@ export function useDomEditSession({ queueDomEditSave, readProjectFile: _readProjectFile, writeProjectFile, + updateEditingFileContent, domEditSaveTimestampRef, editHistory, fileTree, @@ -268,6 +270,7 @@ export function useDomEditSession({ domEditSaveTimestampRef, reloadPreview, onCacheInvalidate: bumpGsapCache, + onFileContentChanged: updateEditingFileContent, }); // ── Commit handlers (delegated to useDomEditCommits) ── diff --git a/packages/studio/src/hooks/useFileManager.ts b/packages/studio/src/hooks/useFileManager.ts index caef9eeb9..f977d8bd9 100644 --- a/packages/studio/src/hooks/useFileManager.ts +++ b/packages/studio/src/hooks/useFileManager.ts @@ -108,6 +108,12 @@ export function useFileManager({ } }, []); + const updateEditingFileContent = useCallback((path: string, content: string) => { + if (editingPathRef.current === path) { + setEditingFile({ path, content }); + } + }, []); + const readOptionalProjectFile = useCallback(async (path: string): Promise => { const pid = projectIdRef.current; if (!pid) throw new Error("No active project"); @@ -460,6 +466,7 @@ export function useFileManager({ readProjectFile, writeProjectFile, readOptionalProjectFile, + updateEditingFileContent, // Click-to-source revealSourceOffset, diff --git a/packages/studio/src/hooks/useGsapScriptCommits.ts b/packages/studio/src/hooks/useGsapScriptCommits.ts index 4c5908f4a..fbc8acd86 100644 --- a/packages/studio/src/hooks/useGsapScriptCommits.ts +++ b/packages/studio/src/hooks/useGsapScriptCommits.ts @@ -108,6 +108,7 @@ interface GsapScriptCommitsParams { domEditSaveTimestampRef: React.MutableRefObject; reloadPreview: () => void; onCacheInvalidate: () => void; + onFileContentChanged?: (path: string, content: string) => void; } const DEBOUNCE_MS = 150; @@ -121,6 +122,7 @@ export function useGsapScriptCommits({ domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, + onFileContentChanged, }: GsapScriptCommitsParams) { const pendingPropertyEditRef = useRef<{ selection: DomEditSelection; @@ -164,14 +166,28 @@ export function useGsapScriptCommits({ onCacheInvalidate(); + if (result.after != null) { + onFileContentChanged?.(targetPath, result.after); + } + if (result.parsed?.animations) { const { setKeyframeCache } = usePlayerStore.getState(); + const idsWithKeyframes = new Set(); for (const anim of result.parsed.animations) { - if (!anim.keyframes) continue; const id = anim.targetSelector.match(/^#([\w-]+)/)?.[1]; if (!id) continue; - setKeyframeCache(`${targetPath}#${id}`, anim.keyframes); - if (targetPath !== "index.html") setKeyframeCache(`index.html#${id}`, anim.keyframes); + if (anim.keyframes) { + idsWithKeyframes.add(id); + setKeyframeCache(`${targetPath}#${id}`, anim.keyframes); + if (targetPath !== "index.html") setKeyframeCache(`index.html#${id}`, anim.keyframes); + } + } + const targetId = + (mutation as { targetSelector?: string }).targetSelector?.match(/^#([\w-]+)/)?.[1] ?? + selection.id; + if (targetId && !idsWithKeyframes.has(targetId)) { + setKeyframeCache(`${targetPath}#${targetId}`, undefined); + if (targetPath !== "index.html") setKeyframeCache(`index.html#${targetId}`, undefined); } } @@ -195,6 +211,7 @@ export function useGsapScriptCommits({ domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, + onFileContentChanged, ], ); diff --git a/packages/studio/src/hooks/useGsapTweenCache.ts b/packages/studio/src/hooks/useGsapTweenCache.ts index b2c2d5280..f47b806cf 100644 --- a/packages/studio/src/hooks/useGsapTweenCache.ts +++ b/packages/studio/src/hooks/useGsapTweenCache.ts @@ -224,7 +224,14 @@ export function usePopulateKeyframeCacheForFile( const sf = sourceFile; fetchParsedAnimations(projectId, sf).then((parsed) => { if (!parsed) return; - const { setKeyframeCache } = usePlayerStore.getState(); + const { setKeyframeCache, keyframeCache } = usePlayerStore.getState(); + const sfPrefix = `${sf}#`; + const fallbackPrefix = "index.html#"; + for (const key of keyframeCache.keys()) { + if (key.startsWith(sfPrefix) || (sf !== "index.html" && key.startsWith(fallbackPrefix))) { + setKeyframeCache(key, undefined); + } + } for (const anim of parsed.animations) { const id = extractIdFromSelector(anim.targetSelector); if (!id || !anim.keyframes) continue; From 677c1b2c40c824b37c64775869e1454162e97076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 5 Jun 2026 13:35:17 -0400 Subject: [PATCH 2/6] feat(studio): per-corner border-radius display in design panel Add border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius to CURATED_STYLE_PROPERTIES. The Radius section auto-detects uniform vs per-corner mode: single slider when all corners match, 4 MetricFields (TL/TR/BR/BL) when they differ. --- .../src/components/editor/domEditingTypes.ts | 4 + .../editor/propertyPanelStyleSections.tsx | 78 ++++++++++++++++--- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/packages/studio/src/components/editor/domEditingTypes.ts b/packages/studio/src/components/editor/domEditingTypes.ts index 96f69ff8c..b26a3f2d1 100644 --- a/packages/studio/src/components/editor/domEditingTypes.ts +++ b/packages/studio/src/components/editor/domEditingTypes.ts @@ -29,6 +29,10 @@ export const CURATED_STYLE_PROPERTIES = [ "opacity", "mix-blend-mode", "border-radius", + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-right-radius", + "border-bottom-left-radius", "border-width", "border-style", "border-color", diff --git a/packages/studio/src/components/editor/propertyPanelStyleSections.tsx b/packages/studio/src/components/editor/propertyPanelStyleSections.tsx index 5702fe764..ed5fe3410 100644 --- a/packages/studio/src/components/editor/propertyPanelStyleSections.tsx +++ b/packages/studio/src/components/editor/propertyPanelStyleSections.tsx @@ -52,6 +52,11 @@ export function StyleSections({ const styleEditingDisabled = !element.capabilities.canEditStyles; const isFlex = styles.display === "flex" || styles.display === "inline-flex"; const radiusValue = parseNumericValue(styles["border-radius"]) ?? 0; + const radiusTL = parseNumericValue(styles["border-top-left-radius"]) ?? radiusValue; + const radiusTR = parseNumericValue(styles["border-top-right-radius"]) ?? radiusValue; + const radiusBR = parseNumericValue(styles["border-bottom-right-radius"]) ?? radiusValue; + const radiusBL = parseNumericValue(styles["border-bottom-left-radius"]) ?? radiusValue; + const radiusUniform = radiusTL === radiusTR && radiusTR === radiusBR && radiusBR === radiusBL; const opacityValue = Math.round((parseNumericValue(styles.opacity) ?? 1) * 100); const borderWidthValue = parsePxMetricValue(styles["border-width"] ?? "") ?? @@ -155,16 +160,69 @@ export function StyleSections({ {hasVisualBackground && (
} defaultCollapsed> - `${formatNumericValue(next)}px`} - onCommit={(next) => onSetStyle("border-radius", `${formatNumericValue(next)}px`)} - /> + {radiusUniform ? ( + `${formatNumericValue(next)}px`} + onCommit={(next) => onSetStyle("border-radius", `${formatNumericValue(next)}px`)} + /> + ) : ( +
+ + onSetStyle( + "border-top-left-radius", + `${formatNumericValue(parseNumericValue(next) ?? 0)}px`, + ) + } + /> + + onSetStyle( + "border-top-right-radius", + `${formatNumericValue(parseNumericValue(next) ?? 0)}px`, + ) + } + /> + + onSetStyle( + "border-bottom-right-radius", + `${formatNumericValue(parseNumericValue(next) ?? 0)}px`, + ) + } + /> + + onSetStyle( + "border-bottom-left-radius", + `${formatNumericValue(parseNumericValue(next) ?? 0)}px`, + ) + } + /> +
+ )}
)} From 9935b08fa74eecdc4fc41c4df41bafb4cd9fbdd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 5 Jun 2026 13:40:50 -0400 Subject: [PATCH 3/6] feat(studio): visual border-radius editor with corner picker Figma-style SVG corner picker showing a rounded rectangle preview with 4 corner handles. Link/unlink toggle switches between uniform mode (single field) and per-corner mode (4 fields TL/TR/BL/BR). --- .../components/editor/BorderRadiusEditor.tsx | 209 ++++++++++++++++++ .../editor/propertyPanelStyleSections.tsx | 86 ++----- 2 files changed, 231 insertions(+), 64 deletions(-) create mode 100644 packages/studio/src/components/editor/BorderRadiusEditor.tsx diff --git a/packages/studio/src/components/editor/BorderRadiusEditor.tsx b/packages/studio/src/components/editor/BorderRadiusEditor.tsx new file mode 100644 index 000000000..c4f697a5f --- /dev/null +++ b/packages/studio/src/components/editor/BorderRadiusEditor.tsx @@ -0,0 +1,209 @@ +import { useCallback, useState } from "react"; +import { MetricField } from "./propertyPanelPrimitives"; +import { formatNumericValue, parseNumericValue, RESPONSIVE_GRID } from "./propertyPanelHelpers"; + +type Corner = "tl" | "tr" | "br" | "bl"; + +interface BorderRadiusEditorProps { + tl: number; + tr: number; + br: number; + bl: number; + disabled?: boolean; + onCommit: (corner: Corner | "all", value: number) => void; +} + +const PREVIEW_W = 72; +const PREVIEW_H = 52; +const MAX_RADIUS = 26; + +function clampRadius(v: number): number { + return Math.max(0, Math.min(MAX_RADIUS, v)); +} + +function scaleRadius(v: number, maxPx: number): number { + if (maxPx <= 0) return 0; + return clampRadius(Math.round((v / Math.max(maxPx, 1)) * MAX_RADIUS)); +} + +export function BorderRadiusEditor({ + tl, + tr, + br, + bl, + disabled, + onCommit, +}: BorderRadiusEditorProps) { + const uniform = tl === tr && tr === br && br === bl; + const [linked, setLinked] = useState(uniform); + + const maxVal = Math.max(tl, tr, br, bl, 1); + const sTL = scaleRadius(tl, maxVal); + const sTR = scaleRadius(tr, maxVal); + const sBR = scaleRadius(br, maxVal); + const sBL = scaleRadius(bl, maxVal); + + const handleCornerCommit = useCallback( + (corner: Corner, raw: string) => { + const v = parseNumericValue(raw) ?? 0; + if (linked) { + onCommit("all", v); + } else { + onCommit(corner, v); + } + }, + [linked, onCommit], + ); + + const handleToggleLinked = useCallback(() => { + if (!linked && !uniform) { + onCommit("all", tl); + } + setLinked((l) => !l); + }, [linked, uniform, tl, onCommit]); + + const path = buildRoundedRectPath(PREVIEW_W, PREVIEW_H, sTL, sTR, sBR, sBL); + + return ( +
+
+ + + + + + + + + +
+ + {linked ? ( + handleCornerCommit("tl", next)} + /> + ) : ( +
+ handleCornerCommit("tl", next)} + /> + handleCornerCommit("tr", next)} + /> + handleCornerCommit("bl", next)} + /> + handleCornerCommit("br", next)} + /> +
+ )} +
+ ); +} + +function buildRoundedRectPath( + w: number, + h: number, + tl: number, + tr: number, + br: number, + bl: number, +): string { + return [ + `M ${tl} 0`, + `L ${w - tr} 0`, + `Q ${w} 0 ${w} ${tr}`, + `L ${w} ${h - br}`, + `Q ${w} ${h} ${w - br} ${h}`, + `L ${bl} ${h}`, + `Q 0 ${h} 0 ${h - bl}`, + `L 0 ${tl}`, + `Q 0 0 ${tl} 0`, + "Z", + ].join(" "); +} diff --git a/packages/studio/src/components/editor/propertyPanelStyleSections.tsx b/packages/studio/src/components/editor/propertyPanelStyleSections.tsx index ed5fe3410..b51be69e1 100644 --- a/packages/studio/src/components/editor/propertyPanelStyleSections.tsx +++ b/packages/studio/src/components/editor/propertyPanelStyleSections.tsx @@ -33,6 +33,7 @@ import { } from "./propertyPanelPrimitives"; import { ColorField } from "./propertyPanelColor"; import { GradientField, ImageFillField } from "./propertyPanelFill"; +import { BorderRadiusEditor } from "./BorderRadiusEditor"; export function StyleSections({ projectId, @@ -56,7 +57,6 @@ export function StyleSections({ const radiusTR = parseNumericValue(styles["border-top-right-radius"]) ?? radiusValue; const radiusBR = parseNumericValue(styles["border-bottom-right-radius"]) ?? radiusValue; const radiusBL = parseNumericValue(styles["border-bottom-left-radius"]) ?? radiusValue; - const radiusUniform = radiusTL === radiusTR && radiusTR === radiusBR && radiusBR === radiusBL; const opacityValue = Math.round((parseNumericValue(styles.opacity) ?? 1) * 100); const borderWidthValue = parsePxMetricValue(styles["border-width"] ?? "") ?? @@ -160,69 +160,27 @@ export function StyleSections({ {hasVisualBackground && (
} defaultCollapsed> - {radiusUniform ? ( - `${formatNumericValue(next)}px`} - onCommit={(next) => onSetStyle("border-radius", `${formatNumericValue(next)}px`)} - /> - ) : ( -
- - onSetStyle( - "border-top-left-radius", - `${formatNumericValue(parseNumericValue(next) ?? 0)}px`, - ) - } - /> - - onSetStyle( - "border-top-right-radius", - `${formatNumericValue(parseNumericValue(next) ?? 0)}px`, - ) - } - /> - - onSetStyle( - "border-bottom-right-radius", - `${formatNumericValue(parseNumericValue(next) ?? 0)}px`, - ) - } - /> - - onSetStyle( - "border-bottom-left-radius", - `${formatNumericValue(parseNumericValue(next) ?? 0)}px`, - ) - } - /> -
- )} + { + const px = `${formatNumericValue(value)}px`; + if (corner === "all") { + onSetStyle("border-radius", px); + } else { + const prop = { + tl: "border-top-left-radius", + tr: "border-top-right-radius", + br: "border-bottom-right-radius", + bl: "border-bottom-left-radius", + }[corner]; + onSetStyle(prop, px); + } + }} + />
)} From a15f1cd80cdb821f0d6c56019243065b149758e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 5 Jun 2026 13:42:51 -0400 Subject: [PATCH 4/6] feat(studio): wire GSAP runtime border-radius values to corner picker When GSAP animates borderRadius, read the per-corner computed styles from the iframe element and pass them to the BorderRadiusEditor. The design panel now shows interpolated border-radius values as the playhead moves through GSAP keyframes. --- .../src/components/editor/PropertyPanel.tsx | 29 +++++++++++++++++++ .../editor/propertyPanelStyleSections.tsx | 14 ++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/studio/src/components/editor/PropertyPanel.tsx b/packages/studio/src/components/editor/PropertyPanel.tsx index aafee19da..551d734be 100644 --- a/packages/studio/src/components/editor/PropertyPanel.tsx +++ b/packages/studio/src/components/editor/PropertyPanel.tsx @@ -217,6 +217,34 @@ export const PropertyPanel = memo(function PropertyPanel({ } })(); + const gsapBorderRadius: { tl: number; tr: number; br: number; bl: number } | null = (() => { + if (!gsapRuntimeValues || !("borderRadius" in gsapRuntimeValues)) { + const hasBRProp = gsapAnimations.some( + (a) => + "borderRadius" in a.properties || + a.keyframes?.keyframes.some((kf) => "borderRadius" in kf.properties), + ); + if (!hasBRProp) return null; + } + const iframe = previewIframeRef?.current; + const selector = element.id ? `#${element.id}` : element.selector; + if (!iframe?.contentDocument || !selector) return null; + try { + const el = iframe.contentDocument.querySelector(selector); + if (!el) return null; + const cs = iframe.contentWindow!.getComputedStyle(el); + const parse = (v: string) => Number.parseFloat(v) || 0; + return { + tl: parse(cs.borderTopLeftRadius), + tr: parse(cs.borderTopRightRadius), + br: parse(cs.borderBottomRightRadius), + bl: parse(cs.borderBottomLeftRadius), + }; + } catch { + return null; + } + })(); + const displayX = gsapRuntimeValues?.x ?? manualOffset.x; const displayY = gsapRuntimeValues?.y ?? manualOffset.y; const displayW = gsapRuntimeValues?.width ?? resolvedWidth; @@ -544,6 +572,7 @@ export const PropertyPanel = memo(function PropertyPanel({ assets={assets} onSetStyle={onSetStyle} onImportAssets={onImportAssets} + gsapBorderRadius={gsapBorderRadius} /> )} diff --git a/packages/studio/src/components/editor/propertyPanelStyleSections.tsx b/packages/studio/src/components/editor/propertyPanelStyleSections.tsx index b51be69e1..a755824d5 100644 --- a/packages/studio/src/components/editor/propertyPanelStyleSections.tsx +++ b/packages/studio/src/components/editor/propertyPanelStyleSections.tsx @@ -42,6 +42,7 @@ export function StyleSections({ assets, onSetStyle, onImportAssets, + gsapBorderRadius, }: { projectId: string; element: DomEditSelection; @@ -49,14 +50,19 @@ export function StyleSections({ assets: string[]; onSetStyle: (prop: string, value: string) => void | Promise; onImportAssets?: (files: FileList) => Promise; + gsapBorderRadius?: { tl: number; tr: number; br: number; bl: number } | null; }) { const styleEditingDisabled = !element.capabilities.canEditStyles; const isFlex = styles.display === "flex" || styles.display === "inline-flex"; const radiusValue = parseNumericValue(styles["border-radius"]) ?? 0; - const radiusTL = parseNumericValue(styles["border-top-left-radius"]) ?? radiusValue; - const radiusTR = parseNumericValue(styles["border-top-right-radius"]) ?? radiusValue; - const radiusBR = parseNumericValue(styles["border-bottom-right-radius"]) ?? radiusValue; - const radiusBL = parseNumericValue(styles["border-bottom-left-radius"]) ?? radiusValue; + const radiusTL = + gsapBorderRadius?.tl ?? parseNumericValue(styles["border-top-left-radius"]) ?? radiusValue; + const radiusTR = + gsapBorderRadius?.tr ?? parseNumericValue(styles["border-top-right-radius"]) ?? radiusValue; + const radiusBR = + gsapBorderRadius?.br ?? parseNumericValue(styles["border-bottom-right-radius"]) ?? radiusValue; + const radiusBL = + gsapBorderRadius?.bl ?? parseNumericValue(styles["border-bottom-left-radius"]) ?? radiusValue; const opacityValue = Math.round((parseNumericValue(styles.opacity) ?? 1) * 100); const borderWidthValue = parsePxMetricValue(styles["border-width"] ?? "") ?? From b235c42b0ff607a2e72589829a7a6c91a350e7a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 5 Jun 2026 13:59:08 -0400 Subject: [PATCH 5/6] fix(studio): bump GSAP cache on code-tab edits When the code editor saves changes (via refreshKey), the GSAP keyframe cache was not invalidated because code-tab edits bypass commitMutation. Watch refreshKey changes and bump gsapCacheVersion so timeline diamonds clear when keyframes are manually removed in the code editor. --- packages/studio/src/hooks/useDomEditSession.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/studio/src/hooks/useDomEditSession.ts b/packages/studio/src/hooks/useDomEditSession.ts index 81fe6904d..cf788ec4f 100644 --- a/packages/studio/src/hooks/useDomEditSession.ts +++ b/packages/studio/src/hooks/useDomEditSession.ts @@ -226,6 +226,18 @@ export function useDomEditSession({ const { version: gsapCacheVersion, bump: bumpGsapCache } = useGsapCacheVersion(); + // Bump GSAP cache when refreshKey changes (code-tab edits trigger iframe + // reload via refreshKey but don't go through commitMutation, so the cache + // would otherwise retain stale keyframe entries). + const prevRefreshKeyRef = useRef(refreshKey); + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + if (refreshKey !== prevRefreshKeyRef.current) { + prevRefreshKeyRef.current = refreshKey; + bumpGsapCache(); + } + }, [refreshKey, bumpGsapCache]); + const gsapSourceFile = domEditSelection?.sourceFile || activeCompPath || "index.html"; usePopulateKeyframeCacheForFile( From 40109f6c7ce59007b74abbb8929daa41e2057461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 5 Jun 2026 22:14:23 -0400 Subject: [PATCH 6/6] =?UTF-8?q?refactor(studio):=20flat=20undo/redo/captur?= =?UTF-8?q?e=20buttons=20=E2=80=94=20no=20borders,=20subtle=20hover?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/studio/src/components/StudioHeader.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/studio/src/components/StudioHeader.tsx b/packages/studio/src/components/StudioHeader.tsx index 1a7cf8c28..9384950ab 100644 --- a/packages/studio/src/components/StudioHeader.tsx +++ b/packages/studio/src/components/StudioHeader.tsx @@ -171,10 +171,10 @@ export function StudioHeader({ void handleUndo(); }} disabled={!editHistory.canUndo} - className={`h-7 w-7 flex items-center justify-center rounded-md border transition-colors ${ + className={`h-7 w-7 flex items-center justify-center rounded-md transition-colors ${ editHistory.canUndo - ? "border-neutral-700 text-neutral-300 hover:border-neutral-500 hover:bg-neutral-800" - : "border-neutral-900 text-neutral-700" + ? "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800" + : "text-neutral-700 cursor-default" }`} title={ editHistory.undoLabel @@ -192,10 +192,10 @@ export function StudioHeader({ void handleRedo(); }} disabled={!editHistory.canRedo} - className={`h-7 w-7 flex items-center justify-center rounded-md border transition-colors ${ + className={`h-7 w-7 flex items-center justify-center rounded-md transition-colors ${ editHistory.canRedo - ? "border-neutral-700 text-neutral-300 hover:border-neutral-500 hover:bg-neutral-800" - : "border-neutral-900 text-neutral-700" + ? "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800" + : "text-neutral-700 cursor-default" }`} title={ editHistory.redoLabel @@ -215,7 +215,7 @@ export function StudioHeader({ }} onFocus={refreshCaptureFrameTime} onPointerDown={refreshCaptureFrameTime} - className="h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border border-neutral-700 text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800" + className="h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium text-neutral-400 transition-colors hover:text-neutral-200 hover:bg-neutral-800" title="Capture current frame" aria-label="Capture current frame" >