Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/studio/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 7 additions & 7 deletions packages/studio/src/components/StudioHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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"
>
Expand Down
209 changes: 209 additions & 0 deletions packages/studio/src/components/editor/BorderRadiusEditor.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="space-y-3">
<div className="flex items-center gap-3">
<svg
width={PREVIEW_W}
height={PREVIEW_H}
viewBox={`0 0 ${PREVIEW_W} ${PREVIEW_H}`}
className="flex-shrink-0"
>
<path
d={path}
fill="rgba(255,255,255,0.06)"
stroke="rgba(255,255,255,0.24)"
strokeWidth={1.5}
/>
<circle
cx={sTL}
cy={sTL}
r={3}
fill={linked ? "#3b82f6" : "#a78bfa"}
className="cursor-pointer"
/>
<circle
cx={PREVIEW_W - sTR}
cy={sTR}
r={3}
fill={linked ? "#3b82f6" : "#a78bfa"}
className="cursor-pointer"
/>
<circle
cx={PREVIEW_W - sBR}
cy={PREVIEW_H - sBR}
r={3}
fill={linked ? "#3b82f6" : "#a78bfa"}
className="cursor-pointer"
/>
<circle
cx={sBL}
cy={PREVIEW_H - sBL}
r={3}
fill={linked ? "#3b82f6" : "#a78bfa"}
className="cursor-pointer"
/>
</svg>

<button
type="button"
className="flex h-7 w-7 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
onClick={handleToggleLinked}
disabled={disabled}
title={linked ? "Unlink corners" : "Link all corners"}
>
{linked ? (
<svg
width={14}
height={14}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
>
<path d="M6 12H4a4 4 0 010-8h2M10 4h2a4 4 0 010 8h-2M5 8h6" />
</svg>
) : (
<svg
width={14}
height={14}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
>
<path d="M6 12H4a4 4 0 010-8h2M10 4h2a4 4 0 010 8h-2" />
</svg>
)}
</button>
</div>

{linked ? (
<MetricField
label="All"
value={formatNumericValue(tl)}
disabled={disabled}
liveCommit
onCommit={(next) => handleCornerCommit("tl", next)}
/>
) : (
<div className={RESPONSIVE_GRID}>
<MetricField
label="TL"
value={formatNumericValue(tl)}
disabled={disabled}
liveCommit
onCommit={(next) => handleCornerCommit("tl", next)}
/>
<MetricField
label="TR"
value={formatNumericValue(tr)}
disabled={disabled}
liveCommit
onCommit={(next) => handleCornerCommit("tr", next)}
/>
<MetricField
label="BL"
value={formatNumericValue(bl)}
disabled={disabled}
liveCommit
onCommit={(next) => handleCornerCommit("bl", next)}
/>
<MetricField
label="BR"
value={formatNumericValue(br)}
disabled={disabled}
liveCommit
onCommit={(next) => handleCornerCommit("br", next)}
/>
</div>
)}
</div>
);
}

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(" ");
}
29 changes: 29 additions & 0 deletions packages/studio/src/components/editor/PropertyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -544,6 +572,7 @@ export const PropertyPanel = memo(function PropertyPanel({
assets={assets}
onSetStyle={onSetStyle}
onImportAssets={onImportAssets}
gsapBorderRadius={gsapBorderRadius}
/>
)}
</div>
Expand Down
4 changes: 4 additions & 0 deletions packages/studio/src/components/editor/domEditingTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from "./propertyPanelPrimitives";
import { ColorField } from "./propertyPanelColor";
import { GradientField, ImageFillField } from "./propertyPanelFill";
import { BorderRadiusEditor } from "./BorderRadiusEditor";

export function StyleSections({
projectId,
Expand All @@ -41,17 +42,27 @@ export function StyleSections({
assets,
onSetStyle,
onImportAssets,
gsapBorderRadius,
}: {
projectId: string;
element: DomEditSelection;
styles: Record<string, string>;
assets: string[];
onSetStyle: (prop: string, value: string) => void | Promise<void>;
onImportAssets?: (files: FileList) => Promise<string[]>;
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 =
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"] ?? "") ??
Expand Down Expand Up @@ -155,15 +166,26 @@ export function StyleSections({

{hasVisualBackground && (
<Section title="Radius" icon={<Settings size={15} />} defaultCollapsed>
<SliderControl
value={radiusValue}
min={0}
max={Math.max(240, Math.ceil(radiusValue))}
step={1}
<BorderRadiusEditor
tl={radiusTL}
tr={radiusTR}
br={radiusBR}
bl={radiusBL}
disabled={styleEditingDisabled}
displayValue={`${formatNumericValue(radiusValue)}px`}
formatDisplayValue={(next) => `${formatNumericValue(next)}px`}
onCommit={(next) => onSetStyle("border-radius", `${formatNumericValue(next)}px`)}
onCommit={(corner, value) => {
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);
}
}}
/>
</Section>
)}
Expand Down
Loading
Loading