import { useEffect, useState } from "react"; import { Eye, Layers, Palette, Settings, Square, Zap } from "../../icons/SystemIcons"; import { buildDefaultGradientModel, serializeGradient } from "./gradientValue"; import { isTextEditableSelection, type DomEditSelection } from "./domEditing"; import { buildBoxShadowPresetValue, buildClipPathValue, buildInsetClipPathValue, buildStrokeStyleUpdates, buildStrokeWidthStyleUpdates, extractBackgroundImageUrl, formatNumericValue, formatPxMetricValue, getCssFilterFunctionPx, getClipPathInsetPx, inferBoxShadowPreset, inferClipPathPreset, LABEL, normalizePanelPxValue, parseNumericValue, parsePxMetricValue, RESPONSIVE_GRID, setCssFilterFunctionPx, type BoxShadowPreset, } from "./propertyPanelHelpers"; import { DetailField, MetricField, Section, SegmentedControl, SelectField, SliderControl, } from "./propertyPanelPrimitives"; import { ColorField } from "./propertyPanelColor"; import { GradientField, ImageFillField } from "./propertyPanelFill"; import { BorderRadiusEditor } from "./BorderRadiusEditor"; export function StyleSections({ projectId, element, styles, assets, onSetStyle, onImportAssets, gsapBorderRadius, }: { projectId: string; element: DomEditSelection; styles: Record; 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 = 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"] ?? "") ?? parsePxMetricValue(styles["border-top-width"] ?? "") ?? 0; const hasVisualBackground = (styles.background != null && styles.background !== "none" && styles.background !== "") || (styles["background-color"] != null && styles["background-color"] !== "transparent" && styles["background-color"] !== "rgba(0, 0, 0, 0)" && styles["background-color"] !== "") || (styles["background-image"] != null && styles["background-image"] !== "none" && styles["background-image"] !== "") || borderWidthValue > 0; const borderStyleValue = styles["border-style"] || styles["border-top-style"] || "none"; const borderColorValue = styles["border-color"] || styles["border-top-color"] || "rgba(255, 255, 255, 0.18)"; const boxShadowPreset = inferBoxShadowPreset(styles["box-shadow"]); const filterBlurValue = getCssFilterFunctionPx(styles.filter, "blur"); const backdropBlurValue = getCssFilterFunctionPx(styles["backdrop-filter"], "blur"); const clipPathValue = styles["clip-path"] || "none"; const clipPathPreset = inferClipPathPreset(clipPathValue); const clipInsetValue = getClipPathInsetPx(clipPathValue); const backgroundImage = styles["background-image"] ?? "none"; const hasTextControls = isTextEditableSelection(element); const fillMode = backgroundImage && backgroundImage !== "none" ? backgroundImage.includes("gradient") ? "Gradient" : "Image" : "Solid"; const [preferredFillMode, setPreferredFillMode] = useState(fillMode); const imageUrl = extractBackgroundImageUrl(backgroundImage); useEffect(() => { setPreferredFillMode(fillMode); }, [fillMode, element.id, element.selector, backgroundImage]); const handleFillModeChange = (nextMode: string) => { setPreferredFillMode(nextMode); if (nextMode === "Solid") { onSetStyle("background-image", "none"); return; } if (nextMode === "Gradient" && !backgroundImage.includes("gradient")) { onSetStyle( "background-image", serializeGradient(buildDefaultGradientModel(styles["background-color"])), ); } }; return ( <> {isFlex && (
} defaultCollapsed>
onSetStyle("flex-direction", next)} options={[ { label: "→ Row", value: "row" }, { label: "↓ Column", value: "column" }, ]} />
onSetStyle("justify-content", next)} options={[ "flex-start", "center", "space-between", "space-around", "space-evenly", "flex-end", ]} /> onSetStyle("align-items", next)} options={["stretch", "flex-start", "center", "flex-end", "baseline"]} />
onSetStyle("gap", next.endsWith("px") ? next : `${next}px`)} />
)} {hasVisualBackground && (
} defaultCollapsed> { 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); } }} />
)}
} defaultCollapsed>
{ const normalized = normalizePanelPxValue(next, { min: 0, max: 200, fallback: borderWidthValue, }); if (!normalized) return; for (const [property, value] of buildStrokeWidthStyleUpdates( normalized, borderStyleValue, )) { await onSetStyle(property, value); } }} /> { for (const [property, value] of buildStrokeStyleUpdates( next, formatPxMetricValue(borderWidthValue), )) { await onSetStyle(property, value); } }} options={[ "none", "solid", "dashed", "dotted", "double", "hidden", "groove", "ridge", "inset", "outset", ]} />
onSetStyle("border-color", next)} />
} defaultCollapsed>
{ if (next === "custom") return; onSetStyle( "box-shadow", buildBoxShadowPresetValue(next as BoxShadowPreset, styles["box-shadow"]), ); }} options={["custom", "none", "soft", "lift", "glow"]} />
Layer blur `${formatNumericValue(next)}px`} onCommit={(next) => onSetStyle("filter", setCssFilterFunctionPx(styles.filter, "blur", next)) } />
Backdrop `${formatNumericValue(next)}px`} onCommit={(next) => onSetStyle( "backdrop-filter", setCssFilterFunctionPx(styles["backdrop-filter"], "blur", next), ) } />
} defaultCollapsed>
onSetStyle("overflow", next)} options={["visible", "hidden", "clip", "auto", "scroll"]} /> { if (next === "custom") return; onSetStyle( "clip-path", buildClipPathValue( next as "none" | "inset" | "circle", radiusValue, clipPathValue, ), ); }} options={["custom", "none", "inset", "circle"]} />
Mask inset `${formatNumericValue(next)}px`} onCommit={(next) => onSetStyle("clip-path", buildInsetClipPathValue(next, radiusValue)) } />
} defaultCollapsed>
`${Math.round(next)}%`} onCommit={(next) => onSetStyle("opacity", formatNumericValue(next / 100))} /> onSetStyle("mix-blend-mode", next)} options={["normal", "multiply", "screen", "overlay", "darken", "lighten"]} />
}>
{preferredFillMode === "Solid" ? ( onSetStyle("background-color", next)} /> ) : preferredFillMode === "Gradient" ? ( onSetStyle("background-image", next)} /> ) : ( onSetStyle("background-image", next)} onImportAssets={onImportAssets} /> )} {!hasTextControls && ( onSetStyle("color", next)} /> )}
); }