diff --git a/apps/daemon/src/inline-assets.ts b/apps/daemon/src/inline-assets.ts index bd1cccc38..cd4f63bec 100644 --- a/apps/daemon/src/inline-assets.ts +++ b/apps/daemon/src/inline-assets.ts @@ -410,4 +410,3 @@ export function escapeHtmlAttr(value: string): string { .replace(//g, '>'); } - diff --git a/apps/web/src/components/FileViewer.tsx b/apps/web/src/components/FileViewer.tsx index 7c1bce248..da0cdcfab 100644 --- a/apps/web/src/components/FileViewer.tsx +++ b/apps/web/src/components/FileViewer.tsx @@ -78,18 +78,25 @@ import type { import { ManualEditPanel, emptyManualEditDraft, type ManualEditDraft } from './ManualEditPanel'; import { applyManualEditPatch, + isManualEditFullHtmlDocument, readManualEditAttributes, readManualEditFields, readManualEditOuterHtml, readManualEditStyles, } from '../edit-mode/source-patches'; -import type { ManualEditBridgeMessage, ManualEditHistoryEntry, ManualEditPatch, ManualEditStyles, ManualEditTarget } from '../edit-mode/types'; +import { MANUAL_EDIT_STYLE_PROPS, type ManualEditBridgeMessage, type ManualEditHistoryEntry, type ManualEditPatch, type ManualEditStyles, type ManualEditTarget } from '../edit-mode/types'; import { isRenderableSketchJson, SketchPreview } from './SketchPreview'; type TranslateFn = (key: keyof Dict, vars?: Record) => string; type SlideState = { active: number; count: number }; type BoardTool = 'inspect' | 'pod'; type StrokePoint = { x: number; y: number }; +export type ManualEditPendingStyleSave = { + id: string; + styles: Partial; + label: string; + version: number; +}; type PreviewViewportId = 'desktop' | 'tablet' | 'mobile'; type PreviewCanvasSize = { width: number; height: number }; type PreviewViewportPreset = { @@ -215,6 +222,57 @@ const DEPLOY_PROVIDER_OPTIONS: DeployProviderOption[] = [ }, ]; +function mergeManualEditInspectorStyles( + sourceStyles: ManualEditStyles, + previewStyles: ManualEditStyles, +): ManualEditStyles { + return MANUAL_EDIT_STYLE_PROPS.reduce((acc, key) => { + const sourceValue = sourceStyles[key]?.trim(); + const previewValue = previewStyles[key]?.trim(); + const value = sourceValue || previewValue || ''; + acc[key] = manualEditInspectorStyleValue(key, value); + return acc; + }, {} as ManualEditStyles); +} + +function manualEditInspectorStyleValue(key: keyof ManualEditStyles, value: string): string { + if (!value) return ''; + if (key === 'color' || key === 'backgroundColor' || key === 'borderColor') { + return normalizeManualEditInspectorColor(value); + } + return value; +} + +function normalizeManualEditInspectorColor(value: string): string { + const trimmed = value.trim(); + if (/^#[0-9a-f]{6}$/i.test(trimmed)) return trimmed.toLowerCase(); + if (/^#[0-9a-f]{3}$/i.test(trimmed)) { + const r = trimmed[1]!, g = trimmed[2]!, b = trimmed[3]!; + return `#${r}${r}${g}${g}${b}${b}`.toLowerCase(); + } + const rgba = trimmed.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i); + if (!rgba) return trimmed; + if (rgba[4] !== undefined && Number(rgba[4]) === 0) return ''; + const toHex = (raw: string) => Math.max(0, Math.min(255, Math.round(Number(raw)))) + .toString(16) + .padStart(2, '0'); + return `#${toHex(rgba[1]!)}${toHex(rgba[2]!)}${toHex(rgba[3]!)}`; +} + +function manualEditPersistedValueMatchesSavedSnapshot( + key: keyof ManualEditStyles, + persistedValue: string, + savedValue: string, +): boolean { + return canonicalManualEditStyleValue(key, persistedValue) === canonicalManualEditStyleValue(key, savedValue); +} + +function canonicalManualEditStyleValue(key: keyof ManualEditStyles, value: string): string { + const normalized = manualEditInspectorStyleValue(key, value).trim(); + if (!normalized) return ''; + return normalized.toLowerCase(); +} + function getDeployProviderOption(providerId: WebDeployProviderId): DeployProviderOption { return DEPLOY_PROVIDER_OPTIONS.find((option) => option.id === providerId) ?? DEPLOY_PROVIDER_OPTIONS[0]!; } @@ -379,6 +437,34 @@ function previewScaleShellStyle( }; } +function manualEditPreviewShellStyle( + viewport: PreviewViewportId, + previewScale: number, + frozenWidth: number | null, +): CSSProperties & Record { + if (viewport === 'desktop' && frozenWidth) { + return { + width: `${frozenWidth / previewScale}px`, + height: `${100 / previewScale}%`, + transform: `scale(${previewScale})`, + transformOrigin: '0 0', + }; + } + return previewScaleShellStyle(viewport, previewScale); +} + +export function cancelManualEditPendingStyleSnapshot( + pending: ManualEditPendingStyleSave | null, + id: string, + keys: Array, +): ManualEditPendingStyleSave | null { + if (!pending || pending.id !== id || keys.length === 0) return pending; + const nextStyles = { ...pending.styles }; + for (const key of keys) delete nextStyles[key]; + if (Object.keys(nextStyles).length === 0) return null; + return { ...pending, styles: nextStyles }; +} + function usePreviewCanvasSize() { const ref = useRef(null); const [size, setSize] = useState(undefined); @@ -3353,7 +3439,7 @@ function HtmlViewer({ const [inTabPresent, setInTabPresent] = useState(false); const [reloadKey, setReloadKey] = useState(0); const [boardMode, setBoardMode] = useState(false); - const boardTool: BoardTool = 'inspect'; + const [boardTool, setBoardTool] = useState('inspect'); const [inspectMode, setInspectMode] = useState(false); const [palettePopoverOpen, setPalettePopoverOpen] = useState(false); const [selectedPalette, setSelectedPalette] = useState(null); @@ -3364,21 +3450,31 @@ function HtmlViewer({ const [openHintBox, setOpenHintBox] = useState(true); const [manualEditMode, setManualEditModeRaw] = useState(false); const [manualEditFrozenSource, setManualEditFrozenSource] = useState(null); + const [manualEditViewportWidth, setManualEditViewportWidth] = useState(null); const setManualEditMode = useCallback((next: boolean | ((prev: boolean) => boolean)) => { setManualEditModeRaw((prev) => { const value = typeof next === 'function' ? (next as (p: boolean) => boolean)(prev) : next; - if (value !== prev && !value) setManualEditFrozenSource(null); + if (value !== prev && !value) { + setManualEditFrozenSource(null); + setManualEditViewportWidth(null); + } return value; }); }, []); const [manualEditTargets, setManualEditTargets] = useState([]); const [selectedManualEditTarget, setSelectedManualEditTarget] = useState(null); + const selectedManualEditTargetIdRef = useRef(null); const [manualEditDraft, setManualEditDraft] = useState(() => emptyManualEditDraft()); const [manualEditHistory, setManualEditHistory] = useState([]); const [manualEditUndone, setManualEditUndone] = useState([]); const [manualEditError, setManualEditError] = useState(null); const [manualEditSaving, setManualEditSaving] = useState(false); const manualEditSavingRef = useRef(false); + const manualEditPendingStyleRef = useRef(null); + const manualEditStyleTimerRef = useRef | null>(null); + const manualEditPreviewVersionRef = useRef(0); + const sourceRef = useRef(source); + const sourceFileKeyRef = useRef(null); const templateNameId = useId(); const templateDescriptionId = useId(); // Opt back into the legacy inline-asset srcDoc path via `?forceInline=1` @@ -3559,14 +3655,25 @@ function HtmlViewer({ }, [liveCommentTargets]); useEffect(() => { + const sourceFileKey = `${projectId}\0${file.name}\0${liveHtml === undefined ? 'raw' : 'live'}`; if (liveHtml !== undefined) { + sourceFileKeyRef.current = sourceFileKey; setSource(liveHtml); + sourceRef.current = liveHtml; return; } - setSource(null); + const fileChanged = sourceFileKeyRef.current !== sourceFileKey; + sourceFileKeyRef.current = sourceFileKey; + if (fileChanged) { + setSource(null); + sourceRef.current = null; + } let cancelled = false; void fetchProjectFileText(projectId, file.name).then((text) => { - if (!cancelled) setSource(text); + if (!cancelled) { + setSource(text); + sourceRef.current = text; + } }); return () => { cancelled = true; @@ -3614,6 +3721,7 @@ function HtmlViewer({ const previewSource = (manualEditMode && manualEditFrozenSource !== null) ? manualEditFrozenSource : livePreviewSource; + const manualEditPageStylesEnabled = typeof source === 'string' && isManualEditFullHtmlDocument(source); const drawClickSelectionMode = drawOverlayOpen && drawOverlayMode === 'click' && !manualEditMode; // When we URL-load the iframe directly, skip every in-host inlining / // srcDoc-rebuilding step. The browser does the asset resolution itself, @@ -3694,13 +3802,21 @@ function HtmlViewer({ const win = iframeRef.current?.contentWindow; if (!win) return; win.postMessage({ type: 'od-edit-mode', enabled: manualEditMode }, '*'); - }, [manualEditMode, srcDoc]); + postSelectedManualEditTargetToIframe(manualEditMode ? selectedManualEditTarget?.id ?? null : null); + }, [manualEditMode, selectedManualEditTarget?.id, srcDoc]); - const previewStyleToIframe = useCallback((id: string, styles: Partial) => { + const previewStyleToIframe = useCallback((id: string, styles: Partial, version: number) => { + const win = iframeRef.current?.contentWindow; + if (!win) return false; + win.postMessage({ type: 'od-edit-preview-style', id, styles, version }, '*'); + return true; + }, []); + + function postSelectedManualEditTargetToIframe(id: string | null) { const win = iframeRef.current?.contentWindow; if (!win) return; - win.postMessage({ type: 'od-edit-preview-style', id, styles }, '*'); - }, []); + win.postMessage({ type: 'od-edit-selected-target', id }, '*'); + } function syncBridgeModes() { const win = iframeRef.current?.contentWindow; @@ -3711,6 +3827,7 @@ function HtmlViewer({ mode: drawClickSelectionMode ? 'picker' : boardTool, }, '*'); win.postMessage({ type: 'od-edit-mode', enabled: manualEditMode }, '*'); + postSelectedManualEditTargetToIframe(manualEditMode ? selectedManualEditTarget?.id ?? null : null); } useEffect(() => { @@ -3789,12 +3906,17 @@ function HtmlViewer({ setInspectError(null); setQueuedBoardNotes([]); setStrokePoints([]); + setManualEditFrozenSource(null); + setManualEditViewportWidth(null); setManualEditTargets([]); setSelectedManualEditTarget(null); + selectedManualEditTargetIdRef.current = null; setManualEditDraft(emptyManualEditDraft()); setManualEditHistory([]); setManualEditUndone([]); setManualEditError(null); + manualEditPendingStyleRef.current = null; + clearManualEditStyleTimer(); }, [file.name]); // Selecting a new file or turning inspect off resets the panel target. @@ -3828,12 +3950,17 @@ function HtmlViewer({ } useEffect(() => { + sourceRef.current = source; if (source == null) return; setManualEditDraft((current) => ( current.fullSource === source ? current : { ...current, fullSource: source } )); }, [source]); + useEffect(() => { + selectedManualEditTargetIdRef.current = selectedManualEditTarget?.id ?? null; + }, [selectedManualEditTarget?.id]); + useEffect(() => { const selectionMode = boardMode || drawClickSelectionMode; if (!selectionMode) { @@ -3960,6 +4087,11 @@ function HtmlViewer({ setManualEditTargets([]); setSelectedManualEditTarget(null); setManualEditError(null); + manualEditPendingStyleRef.current = null; + if (manualEditStyleTimerRef.current) { + clearTimeout(manualEditStyleTimerRef.current); + manualEditStyleTimerRef.current = null; + } return; } function onMessage(ev: MessageEvent) { @@ -3968,21 +4100,126 @@ function HtmlViewer({ if (!data?.type) return; if (data.type === 'od-edit-targets' && Array.isArray(data.targets)) { setManualEditTargets(data.targets); + // Target broadcasts can be briefly empty while the iframe/save path is + // settling; keep the user's inspector selection unless a fresh copy is + // available to update its metadata. setSelectedManualEditTarget((current) => - current ? data.targets.find((target) => target.id === current.id) ?? null : current, + current ? data.targets.find((target) => target.id === current.id) ?? current : current, ); + const selectedId = selectedManualEditTargetIdRef.current; + if (selectedId) setTimeout(() => postSelectedManualEditTargetToIframe(selectedId), 0); return; } if (data.type === 'od-edit-select') { - selectManualEditTarget(data.target); + void selectManualEditTarget(data.target); + return; } } window.addEventListener('message', onMessage); return () => window.removeEventListener('message', onMessage); }, [manualEditMode, source]); - function selectManualEditTarget(target: ManualEditTarget) { - const base = source ?? ''; + function nextManualEditPreviewVersion(): number { + manualEditPreviewVersionRef.current += 1; + return manualEditPreviewVersionRef.current; + } + + function inspectorManualEditStyles(target: ManualEditTarget, baseSource: string): ManualEditStyles { + const inlineStyles = readManualEditStyles(baseSource, target.id); + return mergeManualEditInspectorStyles(inlineStyles, target.styles); + } + + function reconcileManualEditStyleSave( + id: string, + savedStyles: Partial, + savedSource: string, + ) { + if (id !== '__body__' && !readManualEditOuterHtml(savedSource, id)) { + setManualEditError('The selected target no longer exists in the saved source. Refreshing the preview.'); + setSelectedManualEditTarget(null); + setManualEditFrozenSource(null); + setReloadKey((key) => key + 1); + return; + } + const sourceStyles = readManualEditStyles(savedSource, id); + const supersededStyles = manualEditPendingStyleRef.current?.id === id + ? manualEditPendingStyleRef.current.styles + : {}; + const repairStyles: Partial = {}; + for (const key of Object.keys(savedStyles) as Array) { + if (Object.prototype.hasOwnProperty.call(supersededStyles, key)) continue; + const sourceValue = manualEditInspectorStyleValue(key, sourceStyles[key] ?? ''); + const savedValue = savedStyles[key] ?? ''; + if (manualEditPersistedValueMatchesSavedSnapshot(key, sourceValue, savedValue)) continue; + repairStyles[key] = sourceValue; + } + if (Object.keys(repairStyles).length === 0) return; + previewStyleToIframe(id, repairStyles, nextManualEditPreviewVersion()); + setManualEditDraft((current) => ({ + ...current, + styles: { ...current.styles, ...repairStyles }, + })); + setManualEditError('Saved styles differed from the active preview. Reconciled the selected target from source.'); + } + + function scheduleManualEditStyleSave() { + if (manualEditStyleTimerRef.current) clearTimeout(manualEditStyleTimerRef.current); + manualEditStyleTimerRef.current = setTimeout(() => { + manualEditStyleTimerRef.current = null; + void flushManualEditStyleSave(); + }, 1000); + } + + function clearManualEditStyleTimer() { + if (!manualEditStyleTimerRef.current) return; + clearTimeout(manualEditStyleTimerRef.current); + manualEditStyleTimerRef.current = null; + } + + function cancelManualEditPendingStyles(id: string, keys: Array) { + const nextPending = cancelManualEditPendingStyleSnapshot(manualEditPendingStyleRef.current, id, keys); + if (!nextPending) { + manualEditPendingStyleRef.current = null; + clearManualEditStyleTimer(); + return; + } + manualEditPendingStyleRef.current = nextPending; + } + + async function handleManualEditStyleChange(id: string, styles: Partial, label: string) { + const version = nextManualEditPreviewVersion(); + const currentPending = manualEditPendingStyleRef.current; + const pendingStyles = currentPending?.id === id + ? { ...currentPending.styles, ...styles } + : styles; + const pending: ManualEditPendingStyleSave = { id, styles: pendingStyles, label, version }; + manualEditPendingStyleRef.current = pending; + setManualEditError(null); + previewStyleToIframe(id, styles, version); + scheduleManualEditStyleSave(); + } + + async function flushManualEditStyleSave(): Promise { + const pending = manualEditPendingStyleRef.current; + if (!pending) return true; + if (manualEditSavingRef.current) { + scheduleManualEditStyleSave(); + return false; + } + manualEditPendingStyleRef.current = null; + return applyManualEdit({ id: pending.id, kind: 'set-style', styles: pending.styles }, pending.label); + } + + async function exitManualEditModeAfterFlush(): Promise { + const ok = await flushManualEditStyleSave(); + if (!ok) return false; + setManualEditMode(false); + return true; + } + + async function selectManualEditTarget(target: ManualEditTarget) { + if (!(await flushManualEditStyleSave())) return; + const base = sourceRef.current ?? ''; const fields = readManualEditFields(base, target.id); setSelectedManualEditTarget(target); setManualEditDraft({ @@ -3990,7 +4227,7 @@ function HtmlViewer({ href: fields.href ?? target.fields.href ?? '', src: fields.src ?? target.fields.src ?? '', alt: fields.alt ?? target.fields.alt ?? '', - styles: readManualEditStyles(base, target.id), + styles: inspectorManualEditStyles(target, base), attributesText: JSON.stringify(readManualEditAttributes(base, target.id), null, 2), outerHtml: readManualEditOuterHtml(base, target.id) || target.outerHtml, fullSource: base, @@ -3998,29 +4235,36 @@ function HtmlViewer({ setManualEditError(null); } - async function applyManualEdit(patch: ManualEditPatch, label: string) { - if (manualEditSavingRef.current) return; - if (source == null) return; + async function clearManualEditTargetSelection() { + if (!(await flushManualEditStyleSave())) return; + setSelectedManualEditTarget(null); + setManualEditDraft(emptyManualEditDraft(sourceRef.current ?? '')); + setManualEditError(null); + } + + async function applyManualEdit(patch: ManualEditPatch, label: string): Promise { + if (manualEditSavingRef.current) return false; + if (sourceRef.current == null) return false; manualEditSavingRef.current = true; setManualEditSaving(true); setManualEditError(null); try { - const baseSource = source; + const baseSource = sourceRef.current; const result = applyManualEditPatch(baseSource, patch); if (!result.ok) { setManualEditError(result.error ?? 'Could not apply edit.'); - return; + return false; } if (!(await confirmManualEditHistorySource( baseSource, 'The file changed outside manual edit mode. Refreshing before applying manual edits.', - ))) return; + ))) return false; const saved = await writeProjectTextFile(projectId, file.name, result.source, { artifactManifest: file.artifactManifest, }); if (!saved) { setManualEditError('Could not save the edited file.'); - return; + return false; } const entry: ManualEditHistoryEntry = { id: `${Date.now()}-${manualEditHistory.length}`, @@ -4031,14 +4275,20 @@ function HtmlViewer({ createdAt: Date.now(), }; setSource(result.source); + sourceRef.current = result.source; setInlinedSource(null); setManualEditHistory((current) => [entry, ...current]); setManualEditUndone([]); setManualEditDraft((current) => ({ ...current, fullSource: result.source })); + if (patch.kind === 'set-style') { + reconcileManualEditStyleSave(patch.id, patch.styles, result.source); + } await onFileSaved?.(); + return true; } finally { manualEditSavingRef.current = false; setManualEditSaving(false); + if (manualEditPendingStyleRef.current) scheduleManualEditStyleSave(); } } @@ -4049,9 +4299,11 @@ function HtmlViewer({ }); if (persisted == null || persisted === expectedSource) return true; setSource(persisted); + sourceRef.current = persisted; setInlinedSource(null); setManualEditHistory([]); setManualEditUndone([]); + manualEditPendingStyleRef.current = null; setManualEditDraft((current) => ({ ...current, fullSource: persisted })); setManualEditError(message); return false; @@ -4076,6 +4328,7 @@ function HtmlViewer({ return; } setSource(latest.beforeSource); + sourceRef.current = latest.beforeSource; setInlinedSource(null); setManualEditHistory(rest); setManualEditUndone((current) => [latest, ...current]); @@ -4106,6 +4359,7 @@ function HtmlViewer({ return; } setSource(latest.afterSource); + sourceRef.current = latest.afterSource; setInlinedSource(null); setManualEditUndone(rest); setManualEditHistory((current) => [latest, ...current]); @@ -4565,6 +4819,12 @@ function HtmlViewer({ setStrokePoints([]); } + function activateBoard(tool: BoardTool) { + setBoardTool(tool); + setDrawOverlayOpen(false); + setBoardMode(true); + } + function queueCurrentDraft() { const note = commentDraft.trim(); if (!note) return; @@ -4704,6 +4964,7 @@ function HtmlViewer({ if (state === 'failed') return t('fileViewer.deployLinkFailed'); return t('fileViewer.deployLinkPreparingLabel'); }; + const boardAvailable = mode === 'preview' && source !== null; return (
@@ -4821,8 +5082,11 @@ function HtmlViewer({ setInspectMode(false); setDrawOverlayOpen(false); setMode('preview'); + setManualEditViewportWidth(previewBodyRef.current?.clientWidth ?? null); + setManualEditMode(true); + return; } - setManualEditMode((value) => !value); + void exitManualEditModeAfterFlush(); }} > @@ -4836,15 +5100,25 @@ function HtmlViewer({ aria-pressed={drawOverlayOpen} onClick={() => { const next = !drawOverlayOpen; - if (next) { - setManualEditMode(false); + if (!next) { + setDrawOverlayOpen(false); + return; + } + const activateDraw = () => { setBoardMode(false); clearBoardComposer(); setInspectMode(false); setDrawOverlayMode('draw'); setMode('preview'); + setDrawOverlayOpen(true); + }; + if (manualEditMode) { + void exitManualEditModeAfterFlush().then((ok) => { + if (ok) activateDraw(); + }); + return; } - setDrawOverlayOpen(next); + activateDraw(); }} > @@ -5123,13 +5397,20 @@ function HtmlViewer({ canUndo={manualEditHistory.length > 0} canRedo={manualEditUndone.length > 0} busy={manualEditSaving} + pageStylesEnabled={manualEditPageStylesEnabled} onSelectTarget={selectManualEditTarget} onDraftChange={setManualEditDraft} - onPreviewStyle={previewStyleToIframe} + onStyleChange={(id, styles, label) => { + void handleManualEditStyleChange(id, styles, label); + }} + onInvalidStyle={cancelManualEditPendingStyles} onApplyPatch={(patch, label) => { void applyManualEdit(patch, label); }} onError={setManualEditError} + onClearSelection={() => { + void clearManualEditTargetSelection(); + }} onCancelDraft={() => { if (selectedManualEditTarget) selectManualEditTarget(selectedManualEditTarget); }} @@ -5143,7 +5424,11 @@ function HtmlViewer({ ) : null}
void; onDraftChange: (draft: ManualEditDraft) => void; - onPreviewStyle?: (id: string, styles: Partial) => void; + onStyleChange?: (id: string, styles: Partial, label: string) => void; + onInvalidStyle?: (id: string, keys: Array) => void; onApplyPatch: (patch: ManualEditPatch, label: string) => void; onError: (message: string) => void; + onClearSelection: () => void; onCancelDraft: () => void; onUndo: () => void; onRedo: () => void; }) { - const t = useT(); - const [tab, setTab] = useState('style'); - const [advancedOpen, setAdvancedOpen] = useState(false); - - useEffect(() => { setTab('style'); }, [selectedTarget?.id]); - - // Live preview: every style change goes to the iframe via postMessage so the - // canvas updates instantly without an iframe reload. The 1.5s debounced - // source-rewrite still happens via onApplyPatch for persistence. - const lastPreviewedRef = useRef(''); - const lastAppliedRef = useRef(''); - const debounceRef = useRef | null>(null); - useEffect(() => { - if (!selectedTarget || tab !== 'style') return; - const key = JSON.stringify(draft.styles); - if (key !== lastPreviewedRef.current) { - lastPreviewedRef.current = key; - onPreviewStyle?.(selectedTarget.id, draft.styles); - } - if (key === lastAppliedRef.current) return; - if (debounceRef.current) clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(() => { - lastAppliedRef.current = key; - onApplyPatch({ id: selectedTarget.id, kind: 'set-style', styles: draft.styles }, `Style: ${selectedTarget.label}`); - }, 1500); - return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; - }, [draft.styles, selectedTarget?.id, tab, onPreviewStyle, onApplyPatch]); - - useEffect(() => { - lastAppliedRef.current = JSON.stringify(draft.styles); - lastPreviewedRef.current = lastAppliedRef.current; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedTarget?.id]); - const targetForInspector = selectedTarget; + const changeTargetStyle = (key: keyof ManualEditStyles, value: string) => { + const nextStyles = { ...draft.styles, [key]: value }; + onDraftChange({ ...draft, styles: nextStyles }); + if (!targetForInspector) return; + const normalized = normalizeManualEditStyles({ [key]: value }, { + layoutEnabled: targetForInspector.isLayoutContainer, + }); + if (!normalized.ok) { + onError(normalized.error); + onInvalidStyle?.(targetForInspector.id, [key]); + return; + } + onError(''); + onStyleChange?.(targetForInspector.id, normalized.styles, `Style: ${targetForInspector.label}`); + }; return (