Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
54dc061
feat(studio): add flat-inspector tokens and STUDIO_FLAT_INSPECTOR_ENA…
vanceingalls Jul 9, 2026
c2ba521
feat(studio): add 3-tier value/label color resolver for the flat insp…
vanceingalls Jul 9, 2026
361db4b
feat(studio): add FlatRow and FlatSegmentedRow flat-inspector primitives
vanceingalls Jul 9, 2026
d8f9b35
feat(studio): add FlatGroup accordion and PinnedZoneDivider primitives
vanceingalls Jul 9, 2026
3c9500e
feat(studio): add flat identity header for the inspector
vanceingalls Jul 9, 2026
031184a
feat(studio): add flat footer (ask agent + record) for the inspector
vanceingalls Jul 9, 2026
baccd51
feat(studio): add flat trigger variants for ColorField, FontFamilyFie…
vanceingalls Jul 9, 2026
1e77d6b
feat(studio): add FlatTextSection, the flat inspector's canonical ref…
vanceingalls Jul 9, 2026
8e2902b
feat(studio): add flat empty and multi-select states (#6b)
vanceingalls Jul 9, 2026
fb51286
feat(studio): wire group-selection and hide-all into the property panel
vanceingalls Jul 9, 2026
4521061
feat(studio): render the flat Ledger inspector shell behind STUDIO_FL…
vanceingalls Jul 9, 2026
535a072
fix(studio): gate flat Text group to text-editable elements, dedupe h…
vanceingalls Jul 9, 2026
692e1f6
feat(studio): add FlatSlider primitive for the flat inspector
vanceingalls Jul 9, 2026
47bb563
feat(studio): add FlatSelectRow primitive for the flat inspector
vanceingalls Jul 9, 2026
59a252a
feat(studio): add stroke-summary format/parse helpers for the flat St…
vanceingalls Jul 9, 2026
6c0a780
feat(studio): add FlatStyleSection with the flat Fill sub-block
vanceingalls Jul 9, 2026
bd9027c
feat(studio): add flat Stroke and Radius rows to the Style group
vanceingalls Jul 9, 2026
5b68078
feat(studio): add flat Shadow and Blend rows to the Style group
vanceingalls Jul 9, 2026
c0040d0
feat(studio): add flat Layer blur and Backdrop sliders to the Style g…
vanceingalls Jul 9, 2026
6cf5d35
feat(studio): add flat Overflow and Mask rows to the Style group
vanceingalls Jul 9, 2026
4e57175
feat(studio): add flat Opacity slider, completing the Style group
vanceingalls Jul 9, 2026
cf1442f
feat(studio): wire the flat Style group into the one-open accordion
vanceingalls Jul 9, 2026
1eff57c
feat(studio): add flat Layout geometry rows (X/Y/W/H/Angle) with keyf…
vanceingalls Jul 9, 2026
52a099f
feat(studio): add flat Layout Z-index row and Flex sub-block
vanceingalls Jul 9, 2026
ac7c016
feat(studio): nest the existing 3D transform sub-view in a flat wrapper
vanceingalls Jul 9, 2026
db6cb2a
feat(studio): compose FlatLayoutSection from geometry, z-index, flex,…
vanceingalls Jul 9, 2026
6c06d18
feat(studio): wire the flat Layout group into the one-open accordion
vanceingalls Jul 9, 2026
d94b906
feat(studio): add FlatTimingRow for the flat Motion group
vanceingalls Jul 9, 2026
99de825
feat(studio): add flat collapsed-header variant to AnimationCard
vanceingalls Jul 9, 2026
67534b1
feat(studio): compose FlatMotionSection from Timing and the flat effe…
vanceingalls Jul 9, 2026
1944144
feat(studio): wire the flat Motion group into the one-open accordion
vanceingalls Jul 9, 2026
2240322
fix(studio): reconcile keyframe-seek timing basis between flat Layout…
vanceingalls Jul 9, 2026
9c48c00
fix(studio): recompute keyframe currentPct from the corrected timing …
vanceingalls Jul 9, 2026
e3ac18d
feat(studio): add FlatToggle primitive
vanceingalls Jul 9, 2026
b218319
feat(studio): add FlatMediaSection scaffold + source/copy row
vanceingalls Jul 9, 2026
0858610
feat(studio): add cutout sub-block to FlatMediaSection
vanceingalls Jul 9, 2026
ba9ad19
feat(studio): add volume/rate/media-start sliders to FlatMediaSection
vanceingalls Jul 9, 2026
9637a1e
feat(studio): add loop/muted/has-audio-track toggles to FlatMediaSection
vanceingalls Jul 9, 2026
7942f9d
feat(studio): add fit/position rows to FlatMediaSection
vanceingalls Jul 9, 2026
69c2dca
feat(studio): wire the flat Media group into the one-open accordion
vanceingalls Jul 9, 2026
2a6cd33
refactor(studio): extract useColorGradingController from ColorGrading…
vanceingalls Jul 9, 2026
8cc6bdb
feat(studio): add centerTick and reset-slot to FlatSlider for bipolar…
vanceingalls Jul 9, 2026
96fd16a
feat(studio): widen FlatSelectRow options to support label!=value ent…
vanceingalls Jul 9, 2026
492cd89
feat(studio): add FlatColorGradingAccessory (compare, status dot, reset)
vanceingalls Jul 9, 2026
d9a12f0
feat(studio): add FlatColorGradingSection Preset and Custom LUT rows
vanceingalls Jul 9, 2026
6550f65
feat(studio): add the 10 Adjust sliders to FlatColorGradingSection
vanceingalls Jul 9, 2026
09ce465
refactor(studio): collapse dead-branch scale ternary in Adjust sliders
vanceingalls Jul 9, 2026
a2e2ddb
feat(studio): add Vignette and Grain rows with tuned-settings sub-panel
vanceingalls Jul 9, 2026
d66ba59
feat(studio): add Blur and Pixelate effect sliders
vanceingalls Jul 9, 2026
a15a8d6
feat(studio): add HDR warning banner and Apply-to-scope row, completi…
vanceingalls Jul 9, 2026
e2b46b3
feat(studio): wire the flat Grade group into the one-open accordion
vanceingalls Jul 9, 2026
42539ac
feat(studio): persist per-element-type pinned groups in studioUiPrefe…
vanceingalls Jul 9, 2026
fe33895
feat(studio): add usePersistedPinnedGroups hook
vanceingalls Jul 9, 2026
53c7a4f
feat(studio): add PinnedGroupRow always-open pinned-group primitive
vanceingalls Jul 10, 2026
ae92364
feat(studio): render pinned groups first with a persisted, per-elemen…
vanceingalls Jul 10, 2026
f8460f7
test(studio): rename unpin test to match what it actually verifies
vanceingalls Jul 10, 2026
6c89167
feat(studio): add FlatTextLayerList for multi-field text
vanceingalls Jul 10, 2026
28b5da9
test(studio): assert stopPropagation prevents onSelect on remove click
vanceingalls Jul 10, 2026
ef3e937
feat(studio): flatten multi-field Text, retiring TextSection fallback
vanceingalls Jul 10, 2026
cbbe2ae
fix(studio): retire legacy Style and Grade sections from flat inspector
vanceingalls Jul 10, 2026
dfbf05b
fix(studio): restore stroke color, radius unlink, and mask inset in f…
vanceingalls Jul 10, 2026
628108c
docs(studio): fix stale radius-row comment after unlink fix
vanceingalls Jul 10, 2026
6156621
fix(studio): restore zero-strength revive, keyboard hold, and HDR det…
vanceingalls Jul 10, 2026
c8d1d59
fix(studio): restore capitalize, end-align, live-commit size, and aut…
vanceingalls Jul 10, 2026
e850ab2
refactor(studio): scroll only the open group's body, keep headers fixed
vanceingalls Jul 10, 2026
b337c33
refactor(studio): remove section pinning from the flat inspector
vanceingalls Jul 10, 2026
459a4fe
feat(studio): animate flat inspector group expand/collapse
vanceingalls Jul 10, 2026
023b326
fix(studio): animate the implicitly-closed group too, add regression …
vanceingalls Jul 10, 2026
1c3ebfe
fix(studio): widen FlatSlider's click/drag hit area vertically
vanceingalls Jul 10, 2026
8b2b91c
fix(studio): support drag on FlatSlider, not just click-to-set
vanceingalls Jul 10, 2026
5219ad1
fix(studio): debounce flat slider drag commits to prevent rapid-fire …
vanceingalls Jul 11, 2026
6d0f755
fix(studio): throttle FlatSlider commits instead of debouncing them
vanceingalls Jul 11, 2026
65ad878
fix(studio): resolve flat-inspector review defects
vanceingalls Jul 11, 2026
353310a
style(studio): distinguish open-section body from header background
vanceingalls Jul 13, 2026
c8e293a
feat(studio): gate flat inspector redesign
vanceingalls Jul 14, 2026
85409ce
fix(studio): keep dev server alive without Chrome
vanceingalls Jul 14, 2026
5c66813
fix(studio): address flat inspector review feedback
vanceingalls Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,28 @@
"file": "packages/core/src/figma/manifest.ts",
"exports": ["mediaDir", "typeDirPath", "isFigmaManifestRecord"],
},
// STUDIO_FLAT_INSPECTOR_ENABLED: exported for use by downstream studio
// inspector redesign tasks; consumed by components in later PRs.
{
"file": "packages/studio/src/components/editor/manualEditingAvailability.ts",
"exports": ["STUDIO_FLAT_INSPECTOR_ENABLED"],
},
// TextAreaField: newly exported for FlatTextSection (flat inspector
// redesign, Task 8), which lands in a later commit on this branch.
{
"file": "packages/studio/src/components/editor/propertyPanelSections.tsx",
"exports": ["TextAreaField"],
},
// Link: its only consumer was FlatRadiusRow's uniform-only fallback row in
// propertyPanelFlatStyleSections.tsx, deleted by the Style parity fix
// (p8-task-style-parity) — that row was unreachable from a uniform radius
// and is now replaced by BorderRadiusEditor's own unlink toggle. Kept in
// the icon set for future reuse rather than deleted from a file outside
// this fix's scope.
{
"file": "packages/studio/src/icons/SystemIcons.tsx",
"exports": ["Link"],
},
],
"ignoreDependencies": [
// Runtime/dynamic deps not visible to static analysis: tsup `external`,
Expand Down Expand Up @@ -483,6 +505,18 @@
// scopeRootSelectors handling shifts lines and re-flags both.
"packages/core/src/compiler/inlineSubCompositions.ts",
"packages/core/src/compiler/compositionScoping.test.ts",
// Studio flat-inspector redesign (Plans 2-4): each Flat*Section test file
// repeats the same renderInto/pointerdown-drag/reset-click scaffold as its
// sibling group's tests, added task-by-task across separate PRs on this
// branch. Pre-existing relative to Grade group (Plan 5) work; consistent
// with the norm above of leaving parallel arrange/act/assert test cases
// unabstracted where each case verifies a distinct control's behavior.
"packages/studio/src/components/editor/propertyPanelFlatMediaSection.test.tsx",
"packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx",
"packages/studio/src/components/editor/propertyPanelMediaSection.tsx",
"packages/studio/src/components/editor/PropertyPanel.test.tsx",
"packages/studio/src/components/editor/propertyPanelFlatStyleSections.test.tsx",
"packages/studio/src/components/editor/propertyPanelFlatMotionSection.test.tsx",
],
},
"health": {
Expand Down Expand Up @@ -660,6 +694,10 @@
"packages/core/src/compiler/inlineSubCompositions.ts",
"packages/core/src/compiler/htmlBundler.ts",
"packages/core/src/runtime/compositionLoader.ts",
// TextFieldEditor: pre-existing complexity from earlier Text-inspector
// work on this same branch (commits 444639d75, b57b31beb, 6f2e9848c,
// eba8a0fa2), unrelated to the Grade group (Plan 5) currently landing.
"packages/studio/src/components/editor/propertyPanelSections.tsx",
],
},
}
20 changes: 11 additions & 9 deletions packages/studio/src/components/StudioRightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
type ColorGradingScope,
} from "./studioColorGradingScope";
import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes";
import { timelineKeysForSelections, type ToggleHiddenHandler } from "../utils/studioHelpers";

const MIN_INSPECTOR_SPLIT_PERCENT = 20;
const MAX_INSPECTOR_SPLIT_PERCENT = 75;
Expand All @@ -63,7 +64,7 @@ export interface StudioRightPanelProps {
kind: EditHistoryKind;
files: Record<string, { before: string; after: string }>;
}) => Promise<void>;
onToggleElementHidden?: (elementKey: string, hidden: boolean) => Promise<void> | void;
onToggleElementHidden?: ToggleHiddenHandler;
}

// fallow-ignore-next-line complexity
Expand Down Expand Up @@ -109,6 +110,7 @@ export function StudioRightPanel({
copiedAgentPrompt,
clearDomSelection,
handleUngroupSelection,
handleGroupSelection,
handleDomStyleCommit,
handleDomAttributeCommit,
handleDomAttributeLiveCommit,
Expand Down Expand Up @@ -342,6 +344,11 @@ export function StudioRightPanel({
[projectId, refreshFileTree, showToast],
);

const handleHideAllSelected = () => {
const { elements } = usePlayerStore.getState();
const keys = timelineKeysForSelections(domEditGroupSelections, elements, activeCompPath);
if (keys.length > 0) void onToggleElementHidden?.(keys, true);
};
const propertyPanel = (
<DesignPanelPromoteProvider
selection={domEditGroupSelections.length > 1 ? null : domEditSelection}
Expand All @@ -359,6 +366,9 @@ export function StudioRightPanel({
assets={assets}
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
multiSelectCount={domEditGroupSelections.length}
multiSelectedElements={domEditGroupSelections}
onGroupSelection={handleGroupSelection}
onHideAllSelected={handleHideAllSelected}
copiedAgentPrompt={copiedAgentPrompt}
onClearSelection={clearDomSelection}
onToggleElementHidden={onToggleElementHidden}
Expand Down Expand Up @@ -446,8 +456,6 @@ export function StudioRightPanel({

return (
<>
{/* Vertical resize divider: 3px visible seam, 8px pointer-capture zone via
the absolutely-positioned inner hit area. */}
<div
role="separator"
aria-label="Resize inspector panel"
Expand All @@ -467,9 +475,7 @@ export function StudioRightPanel({
setRightWidth(Math.max(160, Math.min(600, rightWidth + delta)));
}}
>
{/* Expanded hit zone: 8px wide, centered on the 3px seam */}
<div className="absolute inset-y-0 -left-[2.5px] w-2" />
{/* Visible hairline */}
<div className="absolute top-1/2 left-0 h-[52px] w-[3px] -translate-y-1/2 bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
</div>
<div
Expand Down Expand Up @@ -566,10 +572,6 @@ export function StudioRightPanel({
) : designPaneOpen ? (
propertyPanel
) : inspectorTabActive ? (
// Inspector tab selected but no pane can render (panes toggled
// off, or inspector inactive during playback/recording): show an
// explanation instead of silently rendering the render queue
// under a highlighted inspector tab.
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
<p className="text-xs text-neutral-500">
Inspector is unavailable right now — select the Design or Layers pane above, or
Expand Down
134 changes: 134 additions & 0 deletions packages/studio/src/components/editor/AnimationCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// @vitest-environment happy-dom

import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AnimationCard } from "./AnimationCard";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";

(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

afterEach(() => {
document.body.innerHTML = "";
});

function baseAnimation(overrides: Partial<GsapAnimation> = {}): GsapAnimation {
return {
id: "anim-1",
method: "to",
position: 0.8,
duration: 1.2,
ease: "power2.out",
properties: { opacity: 1 },
...overrides,
} as GsapAnimation;
}

const noop = () => {};

describe("AnimationCard flat branch", () => {
it("renders a mint border-left and panel-token colors when flat", () => {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(
<AnimationCard
animation={baseAnimation()}
defaultExpanded={false}
flat
onUpdateProperty={noop}
onUpdateMeta={noop}
onDeleteAnimation={noop}
onAddProperty={noop}
onRemoveProperty={noop}
/>,
);
});
const card = host.querySelector('[data-flat-effect-card="true"]');
expect(card).not.toBeNull();
expect(card?.className).toContain("border-panel-accent");
act(() => root.unmount());
});

it("still renders the legacy (non-flat) appearance when flat is omitted", () => {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(
<AnimationCard
animation={baseAnimation()}
defaultExpanded={false}
onUpdateProperty={noop}
onUpdateMeta={noop}
onDeleteAnimation={noop}
onAddProperty={noop}
onRemoveProperty={noop}
/>,
);
});
expect(host.querySelector('[data-flat-effect-card="true"]')).toBeNull();
expect(host.textContent).toContain("power2.out");
act(() => root.unmount());
});

it("toggles expanded state when the collapsed header button is clicked, in both modes", () => {
for (const flat of [false, true]) {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(
<AnimationCard
animation={baseAnimation()}
defaultExpanded={false}
flat={flat || undefined}
onUpdateProperty={noop}
onUpdateMeta={noop}
onDeleteAnimation={noop}
onAddProperty={noop}
onRemoveProperty={noop}
/>,
);
});
expect(host.textContent).not.toContain("Remove");
const button = host.querySelector("button");
expect(button).not.toBeNull();
act(() => {
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(host.textContent).toContain("Remove");
act(() => root.unmount());
}
});

it("invokes onDeleteAnimation with the animation id when Remove is clicked, in flat mode", () => {
const onDeleteAnimation = vi.fn();
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(
<AnimationCard
animation={baseAnimation()}
defaultExpanded={true}
flat
onUpdateProperty={noop}
onUpdateMeta={noop}
onDeleteAnimation={onDeleteAnimation}
onAddProperty={noop}
onRemoveProperty={noop}
/>,
);
});
const buttons = Array.from(host.querySelectorAll("button"));
const removeButton = buttons.find((b) => b.textContent === "Remove");
expect(removeButton).not.toBeUndefined();
act(() => {
removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onDeleteAnimation).toHaveBeenCalledWith("anim-1");
act(() => root.unmount());
});
});
23 changes: 19 additions & 4 deletions packages/studio/src/components/editor/AnimationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import {
interface AnimationCardProps extends GsapAnimationEditCallbacks {
animation: GsapAnimation;
defaultExpanded: boolean;
flat?: boolean;
}

// fallow-ignore-next-line complexity
export const AnimationCard = memo(function AnimationCard({
animation,
defaultExpanded,
flat,
onUpdateProperty,
onUpdateMeta,
onDeleteAnimation,
Expand Down Expand Up @@ -150,7 +152,14 @@ export const AnimationCard = memo(function AnimationCard({
);

return (
<div className="border-b border-neutral-800 pb-3">
<div
data-flat-effect-card={flat ? "true" : undefined}
className={
flat
? "border-b border-l-2 border-panel-accent/40 border-b-panel-hairline pb-3 pl-2"
: "border-b border-neutral-800 pb-3"
}
>
<button
type="button"
onClick={() => setExpanded((v) => !v)}
Expand All @@ -162,21 +171,27 @@ export const AnimationCard = memo(function AnimationCard({
>
{methodLabel}
</span>
<span className="text-[11px] font-medium text-neutral-400" title="When this effect plays">
<span
className={`text-[11px] font-medium ${flat ? "text-panel-text-3" : "text-neutral-400"}`}
title="When this effect plays"
>
{typeof animation.position === "number"
? `${parseFloat(animation.position.toFixed(3))}s`
: animation.position}{" "}
– {typeof endTime === "number" ? `${parseFloat(endTime.toFixed(3))}s` : endTime}
</span>
<span className="ml-auto text-[10px] text-neutral-500" title={easeName}>
<span
className={`ml-auto text-[10px] ${flat ? "text-panel-text-3" : "text-neutral-500"}`}
title={easeName}
>
{easeLabel}
</span>
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="currentColor"
className={`flex-shrink-0 text-neutral-500 transition-transform ${expanded ? "" : "-rotate-90"}`}
className={`flex-shrink-0 transition-transform ${flat ? "text-panel-text-5" : "text-neutral-500"} ${expanded ? "" : "-rotate-90"}`}
>
<path d="M2 3l3 4 3-4z" />
</svg>
Expand Down
Loading
Loading