// fallow-ignore-file code-duplication import { useEffect, useState } from "react"; import { isTextEditableSelection, type DomEditSelection } from "./domEditing"; import { buildDefaultGradientModel, serializeGradient } from "./gradientValue"; import { BorderRadiusEditor } from "./BorderRadiusEditor"; import { STROKE_STYLE_OPTIONS } from "./propertyPanelFlatStyleHelpers"; import { buildBoxShadowPresetValue, buildClipPathValue, buildStrokeStyleUpdates, buildStrokeWidthStyleUpdates, extractBackgroundImageUrl, formatNumericValue, formatPxMetricValue, getCssFilterFunctionPx, inferBoxShadowPreset, inferClipPathPreset, normalizePanelPxValue, parseNumericValue, parsePxMetricValue, setCssFilterFunctionPx, type BoxShadowPreset, } from "./propertyPanelHelpers"; import { FlatRow, FlatSegmentedRow, FlatSelectRow, FlatSlider, } from "./propertyPanelFlatPrimitives"; import { FlatMaskInsetRows } from "./propertyPanelFlatMaskInsetRows"; import { resolveValueTier } from "./propertyPanelValueTier"; import { ColorField } from "./propertyPanelColor"; import { GradientField, ImageFillField } from "./propertyPanelFill"; /* ------------------------------------------------------------------ */ /* Flat Fill sub-block (design_handoff_studio_inspector, #11a) */ /* ------------------------------------------------------------------ */ // fallow-ignore-next-line complexity function FlatFillFields({ projectId, element, styles, assets, onSetStyle, onPreviewStyle, onImportAssets, }: { projectId: string; element: DomEditSelection; styles: Record; assets: string[]; onSetStyle: (prop: string, value: string) => void | Promise; onPreviewStyle?: (prop: string, value: string) => void; onImportAssets?: (files: FileList) => Promise; }) { const styleEditingDisabled = !element.capabilities.canEditStyles; 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 ( <> {preferredFillMode === "Solid" ? ( onPreviewStyle?.("background-color", next)} onCommit={(next) => onSetStyle("background-color", next)} /> ) : preferredFillMode === "Gradient" ? ( onSetStyle("background-image", next)} /> ) : ( onSetStyle("background-image", next)} onImportAssets={onImportAssets} /> )} {!hasTextControls && ( onSetStyle("color", next)} /> )} ); } /* ------------------------------------------------------------------ */ /* Flat Stroke row — width (numeric), style (select), color */ /* ------------------------------------------------------------------ */ function FlatStrokeRow({ styles, disabled, onSetStyle, }: { styles: Record; disabled: boolean; onSetStyle: (prop: string, value: string) => void | Promise; }) { const borderWidthValue = parsePxMetricValue(styles["border-width"] ?? "") ?? parsePxMetricValue(styles["border-top-width"] ?? "") ?? 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 widthDisplay = formatPxMetricValue(borderWidthValue); return ( <> { const normalizedWidth = normalizePanelPxValue(next, { min: 0, max: 200, fallback: borderWidthValue, }); if (!normalizedWidth) return; // buildStrokeWidthStyleUpdates already covers "a width typed in // from 0 needs a style to actually render a visible border" — // it only defaults to solid when the current style is none/hidden, // never clobbering an already-chosen style (dashed, dotted, …). for (const [property, value] of buildStrokeWidthStyleUpdates( normalizedWidth, borderStyleValue, )) { await onSetStyle(property, value); } }} /> { for (const [property, value] of buildStrokeStyleUpdates( next, formatPxMetricValue(borderWidthValue), )) { await onSetStyle(property, value); } }} /> onSetStyle("border-color", next)} /> ); } /* ------------------------------------------------------------------ */ /* Flat Radius row — always delegates to BorderRadiusEditor */ /* ------------------------------------------------------------------ */ // fallow-ignore-next-line complexity function FlatRadiusRow({ styles, gsapBorderRadius, disabled, onSetStyle, }: { styles: Record; gsapBorderRadius?: { tl: number; tr: number; br: number; bl: number } | null; disabled: boolean; onSetStyle: (prop: string, value: string) => void | Promise; }) { 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 commit = (corner: "all" | "tl" | "tr" | "br" | "bl", value: number) => { const px = `${formatNumericValue(value)}px`; if (corner === "all") { void onSetStyle("border-radius", px); return; } const prop = { tl: "border-top-left-radius", tr: "border-top-right-radius", br: "border-bottom-right-radius", bl: "border-bottom-left-radius", }[corner]; void onSetStyle(prop, px); }; return ( ); } /* ------------------------------------------------------------------ */ /* Flat Shadow + Blend rows */ /* ------------------------------------------------------------------ */ function FlatShadowBlendRows({ styles, disabled, onSetStyle, }: { styles: Record; disabled: boolean; onSetStyle: (prop: string, value: string) => void | Promise; }) { const boxShadowPreset = inferBoxShadowPreset(styles["box-shadow"]); const blendValue = styles["mix-blend-mode"] || "normal"; return ( <> { if (next === "custom") return; void onSetStyle( "box-shadow", buildBoxShadowPresetValue(next as BoxShadowPreset, styles["box-shadow"]), ); }} onReset={() => void onSetStyle("box-shadow", "none")} /> void onSetStyle("mix-blend-mode", next)} onReset={() => void onSetStyle("mix-blend-mode", "normal")} /> ); } /* ------------------------------------------------------------------ */ /* Flat Layer blur + Backdrop sliders */ /* ------------------------------------------------------------------ */ function FlatBlurSliders({ styles, disabled, onSetStyle, }: { styles: Record; disabled: boolean; onSetStyle: (prop: string, value: string) => void | Promise; }) { const filterBlurValue = getCssFilterFunctionPx(styles.filter, "blur"); const backdropBlurValue = getCssFilterFunctionPx(styles["backdrop-filter"], "blur"); return ( <> 0 ? "explicitCustom" : "default"} displayValue={`${formatNumericValue(filterBlurValue)}px`} disabled={disabled} onCommit={(next) => void onSetStyle("filter", setCssFilterFunctionPx(styles.filter, "blur", next)) } /> 0 ? "explicitCustom" : "default"} displayValue={`${formatNumericValue(backdropBlurValue)}px`} disabled={disabled} onCommit={(next) => void onSetStyle( "backdrop-filter", setCssFilterFunctionPx(styles["backdrop-filter"], "blur", next), ) } /> ); } // Flat Overflow + Mask rows (+ inset sides). function FlatOverflowMaskRows({ styles, disabled, onSetStyle, }: { styles: Record; disabled: boolean; onSetStyle: (prop: string, value: string) => void | Promise; }) { const radiusValue = parseNumericValue(styles["border-radius"]) ?? 0; const clipPathValue = styles["clip-path"] || "none"; const clipPathPreset = inferClipPathPreset(clipPathValue); return ( <> void onSetStyle("overflow", next)} onReset={() => void onSetStyle("overflow", "visible")} /> { if (next === "custom") return; void onSetStyle( "clip-path", buildClipPathValue(next as "none" | "inset" | "circle", radiusValue, clipPathValue), ); }} onReset={() => void onSetStyle("clip-path", "none")} /> ); } /* ------------------------------------------------------------------ */ /* Flat Opacity slider */ /* ------------------------------------------------------------------ */ function FlatOpacitySlider({ styles, disabled, onSetStyle, }: { styles: Record; disabled: boolean; onSetStyle: (prop: string, value: string) => void | Promise; }) { const opacityValue = Math.round((parseNumericValue(styles.opacity) ?? 1) * 100); return ( void onSetStyle("opacity", formatNumericValue(next / 100))} /> ); } export function FlatStyleSection({ projectId, element, styles, assets, onSetStyle, onPreviewStyle, onImportAssets, gsapBorderRadius, }: { projectId: string; element: DomEditSelection; styles: Record; assets: string[]; onSetStyle: (prop: string, value: string) => void | Promise; onPreviewStyle?: (prop: string, value: string) => void; onImportAssets?: (files: FileList) => Promise; gsapBorderRadius?: { tl: number; tr: number; br: number; bl: number } | null; }) { const styleEditingDisabled = !element.capabilities.canEditStyles; return (
); }