feat(builder): unify StylePanel image fields on AssetPicker

Replace the ad-hoc upload/browse/URL blocks in ImageStylePanel, HeroStylePanel
(bgImage + bgVideo), BackgroundSectionStylePanel, NavStylePanel (standalone
Logo imageSrc + Navbar logoImage), MediaStylePanel (Gallery/ContentSlider
array items), and FeaturesEditor (per-feature image) with the shared
AssetPicker component, dropping each panel's duplicated local
showBrowser/browserAssets/handleUpload state and inline asset-browser JSX.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 13:00:10 -07:00
parent c50b385661
commit 4674a99ec9
6 changed files with 56 additions and 347 deletions
@@ -1,22 +1,7 @@
import React, { useRef } from 'react';
import React from 'react';
import { useEditor } from '@craftjs/core';
import { labelStyle, inputStyle, sectionGap } from './shared';
/* Upload helper (same contract as ImageBlock/Navbar): uploads to WHP if a
WHP_CONFIG is present, otherwise falls back to a local blob URL. */
async function uploadToWhp(file: File): Promise<string | null> {
const cfg = (window as any).WHP_CONFIG;
if (!cfg) return URL.createObjectURL(file);
const fd = new FormData();
fd.append('file', file);
try {
const resp = await fetch(`${cfg.apiUrl}?action=upload_asset&site_id=${cfg.siteId}`, {
method: 'POST', headers: { 'X-CSRF-Token': cfg.csrfToken }, body: fd,
});
const data = await resp.json();
return data.success && data.url ? data.url : null;
} catch { return null; }
}
import { AssetPicker } from '../../../ui/AssetPicker';
interface Feature {
title?: string; description?: string; icon?: string;
@@ -33,7 +18,6 @@ interface Feature {
export const FeaturesEditor: React.FC<{ selectedId: string; features: unknown }> = ({ selectedId, features }) => {
const { actions } = useEditor();
const list: Feature[] = Array.isArray(features) ? (features as Feature[]) : [];
const fileRefs = useRef<Record<number, HTMLInputElement | null>>({});
const mutate = (fn: (arr: Feature[]) => Feature[]) =>
actions.setProp(selectedId, (p: any) => { p.features = fn([...(p.features || [])]); });
@@ -42,9 +26,6 @@ export const FeaturesEditor: React.FC<{ selectedId: string; features: unknown }>
const add = () =>
mutate((arr) => [...arr, { title: 'New Feature', description: 'Describe this feature.', icon: '🔧', image: '', imageAlt: '', buttonText: '', buttonUrl: '' }]);
const remove = (i: number) => mutate((arr) => { arr.splice(i, 1); return arr; });
const onUpload = async (i: number, file: File) => { const url = await uploadToWhp(file); if (url) update(i, 'image', url); };
const smallBtn: React.CSSProperties = { padding: '4px 8px', fontSize: 11, borderRadius: 4, cursor: 'pointer', border: '1px solid #3f3f46', background: '#27272a', color: '#e4e4e7' };
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
@@ -57,21 +38,12 @@ export const FeaturesEditor: React.FC<{ selectedId: string; features: unknown }>
<textarea value={feat.description || ''} onChange={(e) => update(i, 'description', e.target.value)} placeholder="Description" rows={2} style={{ ...inputStyle, resize: 'vertical' }} />
{/* Media: image (upload/URL) takes precedence over the icon when set */}
{feat.image ? (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<img src={feat.image} alt="" style={{ width: 28, height: 28, objectFit: 'cover', borderRadius: 4, border: '1px solid #3f3f46' }} />
<input type="text" value={feat.image} onChange={(e) => update(i, 'image', e.target.value)} placeholder="Image URL" style={{ ...inputStyle, flex: 1 }} />
<button onClick={() => update(i, 'image', '')} title="Use icon instead" style={smallBtn}>Icon</button>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={feat.icon || ''} onChange={(e) => update(i, 'icon', e.target.value)} placeholder="Icon (emoji)" style={{ ...inputStyle, width: 60, flex: 'none', textAlign: 'center' }} />
<div style={{ flex: 1 }}>
<AssetPicker variant="compact" value={feat.image || ''} onChange={(url) => update(i, 'image', url)} placeholder="or image URL" />
</div>
) : (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={feat.icon || ''} onChange={(e) => update(i, 'icon', e.target.value)} placeholder="Icon (emoji)" style={{ ...inputStyle, width: 60, flex: 'none', textAlign: 'center' }} />
<button onClick={() => fileRefs.current[i]?.click()} style={{ ...smallBtn, flex: 1 }}> Upload image</button>
<input type="text" value={feat.image || ''} onChange={(e) => update(i, 'image', e.target.value)} placeholder="or image URL" style={{ ...inputStyle, flex: 1 }} />
</div>
)}
<input ref={(el) => { fileRefs.current[i] = el; }} type="file" accept="image/*" style={{ display: 'none' }}
onChange={(e) => { const f = e.target.files?.[0]; if (f) onUpload(i, f); e.target.value = ''; }} />
</div>
{/* Optional button */}
<div style={{ display: 'flex', gap: 4 }}>