diff --git a/packages/studio/src/components/editor/PropertyPanel.tsx b/packages/studio/src/components/editor/PropertyPanel.tsx index f882e39bd4..1cf6688798 100644 --- a/packages/studio/src/components/editor/PropertyPanel.tsx +++ b/packages/studio/src/components/editor/PropertyPanel.tsx @@ -565,6 +565,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro styles={styles} assets={assets} onSetStyle={onSetStyle} + onSetHtmlAttribute={props.onSetHtmlAttribute} onImportAssets={onImportAssets} gsapBorderRadius={gsapBorderRadius} /> diff --git a/packages/studio/src/components/editor/PropertyPanelFlat.tsx b/packages/studio/src/components/editor/PropertyPanelFlat.tsx index d254216719..f2778b4e9f 100644 --- a/packages/studio/src/components/editor/PropertyPanelFlat.tsx +++ b/packages/studio/src/components/editor/PropertyPanelFlat.tsx @@ -351,6 +351,7 @@ export function PropertyPanelFlat({ styles={styles} assets={assets} onSetStyle={onSetStyle} + onSetHtmlAttribute={forwardedProps.onSetHtmlAttribute} onPreviewStyle={onPreviewStyle} onImportAssets={onImportAssets} gsapBorderRadius={gsapBorderRadius} diff --git a/packages/studio/src/components/editor/propertyPanelFill.tsx b/packages/studio/src/components/editor/propertyPanelFill.tsx index 8bb9e998c3..248e01eb09 100644 --- a/packages/studio/src/components/editor/propertyPanelFill.tsx +++ b/packages/studio/src/components/editor/propertyPanelFill.tsx @@ -10,6 +10,7 @@ import { } from "./gradientValue"; import { ReverseGradientIcon } from "../icons/ReverseGradientIcon"; import { IMAGE_EXT } from "../../utils/mediaTypes"; +import type { DomEditSelection } from "./domEditing"; import { FIELD, LABEL, RESPONSIVE_GRID } from "./propertyPanelHelpers"; import { DetailField, @@ -27,9 +28,13 @@ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; function normalizeProjectPath(value: string): string { const trimmed = value.trim(); const maybeUrl = /^[a-z]+:\/\//i.test(trimmed) ? new URL(trimmed).pathname : trimmed; - return decodeURIComponent(maybeUrl) - .replace(/\\/g, "/") - .replace(/^\.?\//, ""); + let decodedPath = maybeUrl; + try { + decodedPath = decodeURIComponent(maybeUrl); + } catch (error) { + if (!(error instanceof URIError)) throw error; + } + return decodedPath.replace(/\\/g, "/").replace(/^\.?\//, ""); } function toRelativeProjectAssetPath(sourceFile: string, assetPath: string): string { @@ -75,7 +80,8 @@ function resolveSelectedAsset( export function ImageFillField({ projectId, - sourceFile, + element, + onSetHtmlAttribute, value, assets, disabled, @@ -83,7 +89,8 @@ export function ImageFillField({ onImportAssets, }: { projectId: string; - sourceFile: string; + element: DomEditSelection; + onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise; value: string; assets: string[]; disabled?: boolean; @@ -91,15 +98,29 @@ export function ImageFillField({ onImportAssets?: (files: FileList) => Promise; }) { const track = useTrackDesignInput(); + const sourceFile = element.sourceFile; + const isImage = element.tagName === "img"; + const imageUrl = isImage ? (element.element.getAttribute("src") ?? "") : value; + const commitImage = (next: string, projectAsset = false) => { + if (isImage) { + if (next) + void onSetHtmlAttribute( + "src", + projectAsset ? toRelativeProjectAssetPath(sourceFile, next) : next, + ); + return; + } + onCommit(next ? `url("${next}")` : "none"); + }; const fileInputRef = useRef(null); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const imageAssets = useMemo(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]); const selectedAsset = useMemo( - () => resolveSelectedAsset(value, sourceFile, imageAssets), - [imageAssets, sourceFile, value], + () => resolveSelectedAsset(imageUrl, sourceFile, imageAssets), + [imageAssets, sourceFile, imageUrl], ); - const externalUrlValue = selectedAsset ? "" : value; + const externalUrlValue = selectedAsset ? "" : imageUrl; const handleUpload = async (files: FileList | null) => { if (!files?.length || !onImportAssets) return; @@ -110,7 +131,7 @@ export function ImageFillField({ const nextImage = uploaded.find((a) => IMAGE_EXT.test(a)); if (nextImage) { track("button", "Upload image"); - onCommit(`url("${toProjectRootAssetPath(nextImage)}")`); + commitImage(toProjectRootAssetPath(nextImage), true); } } catch { setUploadError("Upload failed — check the file and try again."); @@ -174,14 +195,16 @@ export function ImageFillField({ const next = e.target.value; track("select", "Project asset"); if (!next) { - onCommit("none"); + commitImage(""); return; } - onCommit(`url("${toProjectRootAssetPath(next)}")`); + commitImage(toProjectRootAssetPath(next), true); }} className="min-w-0 w-full appearance-none bg-transparent text-[11px] font-medium text-neutral-100 outline-hidden disabled:cursor-not-allowed disabled:text-neutral-600" > - + {imageAssets.map((asset) => (