refactor(builder): remove dead component settings UI

Each component defined a .craft.related.settings panel that was never
rendered -- the right panel renders only GuidedStyles (per-type
*StylePanel components), never .related.settings. Removed all dead
settings components across every component, their settings-only helpers
(including the dead uploadToWhp/showBrowser/handleBrowse asset-browse
blocks in Logo/Navbar/VideoBlock/FeaturesGrid, and CtasEditor in
_cta-helpers), and dropped the now-empty related keys. Render output,
.craft props/rules, and toHtml statics are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 13:16:53 -07:00
parent 4674a99ec9
commit 65a10a1ef9
40 changed files with 10 additions and 6718 deletions
+1 -383
View File
@@ -1,4 +1,4 @@
import React, { CSSProperties, useCallback, useRef, useState } from 'react';
import React, { CSSProperties, useCallback, useRef } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { escapeAttr, safeUrl } from '../../utils/escape';
@@ -81,392 +81,10 @@ export const ImageBlock: UserComponent<ImageBlockProps> = ({
);
};
/* ---------- Helpers for parsing CSS unit values ---------- */
type SizeUnit = 'px' | '%' | 'auto';
function parseSizeValue(value: string | number | undefined): { num: string; unit: SizeUnit } {
if (!value || value === 'auto') return { num: '', unit: 'auto' };
const str = String(value);
if (str === 'auto') return { num: '', unit: 'auto' };
const match = str.match(/^(\d+(?:\.\d+)?)\s*(px|%)$/);
if (match) return { num: match[1], unit: match[2] as SizeUnit };
// Pure number = px
if (/^\d+(?:\.\d+)?$/.test(str)) return { num: str, unit: 'px' };
return { num: '', unit: 'px' };
}
function buildSizeString(num: string, unit: SizeUnit): string | undefined {
if (unit === 'auto') return 'auto';
if (!num) return undefined;
return `${num}${unit}`;
}
type Alignment = 'left' | 'center' | 'right';
function detectAlignment(style: CSSProperties | undefined): Alignment {
if (!style) return 'left';
const ml = style.marginLeft;
const mr = style.marginRight;
if (ml === 'auto' && mr === 'auto') return 'center';
if (ml === 'auto' && mr !== 'auto') return 'right';
return 'left';
}
/* ---------- Settings panel ---------- */
const ImageBlockSettings: React.FC = () => {
const { actions: { setProp }, props } = useNode((node) => ({
props: node.data.props as ImageBlockProps,
}));
const isPlaceholder = !props.src || props.src === PLACEHOLDER_SRC || props.src?.startsWith('data:image/svg');
const fileInputRef = useRef<HTMLInputElement>(null);
const [showBrowser, setShowBrowser] = useState(false);
const [browserAssets, setBrowserAssets] = useState<any[]>([]);
const [browserLoading, setBrowserLoading] = useState(false);
// Sizing unit state
const widthParsed = parseSizeValue(props.style?.width);
const [widthUnit, setWidthUnit] = useState<SizeUnit>(widthParsed.unit === 'auto' ? 'px' : widthParsed.unit);
const heightParsed = parseSizeValue(props.style?.height);
const [heightUnit, setHeightUnit] = useState<SizeUnit>(heightParsed.unit === 'auto' ? 'px' : heightParsed.unit);
const maxWidthParsed = parseSizeValue(props.style?.maxWidth);
const [maxWidthUnit, setMaxWidthUnit] = useState<SizeUnit>(maxWidthParsed.unit === 'auto' ? '%' : maxWidthParsed.unit);
const alignment = detectAlignment(props.style);
const handleUpload = useCallback(async (file: File) => {
const url = await uploadToWhp(file);
if (url) setProp((p: ImageBlockProps) => { p.src = url; });
}, [setProp]);
const handleBrowse = useCallback(async () => {
if (showBrowser) { setShowBrowser(false); return; }
const cfg = (window as any).WHP_CONFIG;
if (!cfg) return;
setBrowserLoading(true);
try {
const resp = await fetch(`${cfg.apiUrl}?action=list_assets&site_id=${cfg.siteId}`);
const data = await resp.json();
if (data.success && Array.isArray(data.assets)) {
const images = data.assets.filter((a: any) => (a.type || '').startsWith('image'));
setBrowserAssets(images);
setShowBrowser(true);
}
} catch (e) {
console.error('Browse failed:', e);
} finally {
setBrowserLoading(false);
}
}, [showBrowser]);
const radiusPresets = ['0', '8px', '16px', '32px', '50%'];
const setPropStyle = useCallback((key: string, value: string | undefined) => {
setProp((p: ImageBlockProps) => {
p.style = { ...p.style, [key]: value };
});
}, [setProp]);
const setAlignment = useCallback((align: Alignment) => {
setProp((p: ImageBlockProps) => {
const s = { ...p.style };
if (align === 'center') {
s.marginLeft = 'auto';
s.marginRight = 'auto';
s.display = 'block';
} else if (align === 'right') {
s.marginLeft = 'auto';
s.marginRight = undefined;
s.display = 'block';
} else {
s.marginLeft = undefined;
s.marginRight = undefined;
s.display = 'block';
}
p.style = s;
});
}, [setProp]);
// Extract friendly filename from URL
const getFriendlyName = (src: string) => {
const match = src.match(/filename=([^&]+)/);
if (match) return decodeURIComponent(match[1]).replace(/^\d+_[a-f0-9]+_/, '');
return src.split('/').pop() || 'image';
};
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 4 };
const inputStyle: CSSProperties = { flex: 1, minWidth: 0, padding: '4px 6px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12 };
const selectStyle: CSSProperties = { padding: '4px 2px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11, cursor: 'pointer' };
const btnStyle = (active: boolean): CSSProperties => ({
flex: 1, padding: '4px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
border: '1px solid #3f3f46',
background: active ? '#3b82f6' : '#27272a',
color: active ? '#fff' : '#a1a1aa',
});
return (
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* Image preview */}
<div>
<label style={labelStyle}>Image Source</label>
{!isPlaceholder ? (
<>
{/* Current image thumbnail + filename + remove */}
<div style={{ marginBottom: 8, borderRadius: 6, overflow: 'hidden', border: '1px solid #3f3f46', position: 'relative' }}>
<img src={props.src} alt="" style={{ width: '100%', height: 'auto', display: 'block', maxHeight: 150, objectFit: 'cover' }} />
<button onClick={() => setProp((p: ImageBlockProps) => { p.src = PLACEHOLDER_SRC; })}
style={{ position: 'absolute', top: 4, right: 4, width: 24, height: 24, borderRadius: '50%', background: 'rgba(0,0,0,0.7)', border: 'none', color: '#fff', cursor: 'pointer', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
title="Remove image">
<i className="fa fa-times" />
</button>
</div>
<div style={{ fontSize: 11, color: '#a1a1aa', marginBottom: 8, display: 'flex', alignItems: 'center', gap: 4 }}>
<i className="fa fa-check-circle" style={{ color: '#10b981' }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{getFriendlyName(props.src || '')}</span>
</div>
</>
) : (
/* Drop zone when no image set */
<div
style={{ padding: '20px 12px', border: '2px dashed #3f3f46', borderRadius: 6, textAlign: 'center', color: '#71717a', fontSize: 12, cursor: 'pointer', marginBottom: 8, transition: 'border-color 0.15s' }}
onDragOver={(e) => { e.preventDefault(); e.currentTarget.style.borderColor = '#3b82f6'; }}
onDragLeave={(e) => { e.currentTarget.style.borderColor = '#3f3f46'; }}
onDrop={async (e) => {
e.preventDefault();
e.currentTarget.style.borderColor = '#3f3f46';
const file = e.dataTransfer.files?.[0];
if (file && file.type.startsWith('image/')) await handleUpload(file);
}}
onClick={() => fileInputRef.current?.click()}
>
<i className="fa fa-cloud-upload" style={{ fontSize: 24, display: 'block', marginBottom: 6, color: '#3b82f6' }} />
Drop image here or click to upload
</div>
)}
{/* Action buttons: Upload + Browse */}
<div style={{ display: 'flex', gap: 4 }}>
<button
onClick={() => fileInputRef.current?.click()}
style={{ flex: 1, padding: '8px 10px', fontSize: 12, borderRadius: 4, cursor: 'pointer', border: '1px solid #3f3f46', background: '#3b82f6', color: '#fff', fontWeight: 500 }}
>
<i className="fa fa-upload" style={{ marginRight: 4 }} /> Upload
</button>
<button
onClick={handleBrowse}
style={{ flex: 1, padding: '8px 10px', fontSize: 12, borderRadius: 4, cursor: 'pointer', border: '1px solid #3f3f46', background: showBrowser ? '#3b82f6' : '#27272a', color: showBrowser ? '#fff' : '#e4e4e7' }}
>
<i className={`fa ${browserLoading ? 'fa-spinner fa-spin' : 'fa-folder-open'}`} style={{ marginRight: 4 }} /> Browse
</button>
</div>
{/* Inline asset browser grid */}
{showBrowser && (
<div style={{ maxHeight: 200, overflowY: 'auto', display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4, marginTop: 8, background: '#18181b', borderRadius: 6, padding: 4 }}>
{browserAssets.map(asset => (
<div
key={asset.name}
onClick={() => { setProp((p: ImageBlockProps) => { p.src = asset.url; }); setShowBrowser(false); }}
style={{ cursor: 'pointer', borderRadius: 4, overflow: 'hidden', border: '2px solid transparent', aspectRatio: '1', transition: 'border-color 0.15s' }}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = '#3b82f6'; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'transparent'; }}
>
<img src={asset.url} alt={asset.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
</div>
))}
{browserAssets.length === 0 && (
<p style={{ gridColumn: '1 / -1', textAlign: 'center', color: '#71717a', fontSize: 11, padding: '12px 0', margin: 0 }}>No images uploaded yet. Use Upload above.</p>
)}
</div>
)}
<input ref={fileInputRef} type="file" accept="image/*" style={{ display: 'none' }}
onChange={(e) => { const file = e.target.files?.[0]; if (file) handleUpload(file); e.target.value = ''; }} />
{/* URL input (collapsed, for advanced users) */}
<div style={{ marginTop: 6 }}>
<input type="text"
value={isPlaceholder ? '' : (props.src || '')}
onChange={(e) => setProp((p: ImageBlockProps) => { p.src = e.target.value || PLACEHOLDER_SRC; })}
placeholder="Or paste image URL..."
style={{ width: '100%', padding: '4px 8px', background: '#1e1e2a', color: '#71717a', border: '1px solid #27272a', borderRadius: 4, fontSize: 10 }}
/>
</div>
</div>
{/* Alt Text */}
<div>
<label style={labelStyle}>Alt Text</label>
<input
type="text"
value={props.alt || ''}
onChange={(e) => setProp((p: ImageBlockProps) => { p.alt = e.target.value; })}
placeholder="Describe the image..."
style={{ width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12 }}
/>
</div>
{/* Width */}
<div>
<label style={labelStyle}>Width</label>
<div style={{ display: 'flex', gap: 4 }}>
<input
type="number"
min={0}
value={widthParsed.num}
disabled={props.style?.width === 'auto'}
onChange={(e) => {
const val = buildSizeString(e.target.value, widthUnit);
setPropStyle('width', val || 'auto');
}}
placeholder="auto"
style={inputStyle}
/>
<select
value={props.style?.width === 'auto' ? 'auto' : widthUnit}
onChange={(e) => {
const unit = e.target.value as SizeUnit;
if (unit === 'auto') {
setPropStyle('width', 'auto');
} else {
setWidthUnit(unit);
const num = widthParsed.num || '100';
setPropStyle('width', `${num}${unit}`);
}
}}
style={selectStyle}
>
<option value="px">px</option>
<option value="%">%</option>
<option value="auto">auto</option>
</select>
</div>
</div>
{/* Max Width */}
<div>
<label style={labelStyle}>Max Width</label>
<div style={{ display: 'flex', gap: 4 }}>
<input
type="number"
min={0}
value={maxWidthParsed.num}
onChange={(e) => {
const val = buildSizeString(e.target.value, maxWidthUnit);
setPropStyle('maxWidth', val || '100%');
}}
placeholder="100%"
style={inputStyle}
/>
<select
value={maxWidthUnit}
onChange={(e) => {
const unit = e.target.value as SizeUnit;
setMaxWidthUnit(unit);
const num = maxWidthParsed.num || '100';
setPropStyle('maxWidth', `${num}${unit}`);
}}
style={selectStyle}
>
<option value="px">px</option>
<option value="%">%</option>
</select>
</div>
</div>
{/* Height */}
<div>
<label style={labelStyle}>Height</label>
<div style={{ display: 'flex', gap: 4 }}>
<input
type="number"
min={0}
value={heightParsed.num}
disabled={props.style?.height === 'auto'}
onChange={(e) => {
const val = buildSizeString(e.target.value, heightUnit);
setPropStyle('height', val || 'auto');
}}
placeholder="auto"
style={inputStyle}
/>
<select
value={props.style?.height === 'auto' ? 'auto' : heightUnit}
onChange={(e) => {
const unit = e.target.value as SizeUnit;
if (unit === 'auto') {
setPropStyle('height', 'auto');
} else {
setHeightUnit(unit);
const num = heightParsed.num || '300';
setPropStyle('height', `${num}${unit}`);
}
}}
style={selectStyle}
>
<option value="px">px</option>
<option value="auto">auto</option>
</select>
</div>
</div>
{/* Object Fit (visible when both width and height are explicit values) */}
{props.style?.width && props.style.width !== 'auto' && props.style?.height && props.style.height !== 'auto' && (
<div>
<label style={labelStyle}>Object Fit</label>
<div style={{ display: 'flex', gap: 4 }}>
{(['cover', 'contain', 'fill', 'none'] as const).map((fit) => (
<button
key={fit}
onClick={() => setPropStyle('objectFit', fit)}
style={btnStyle(props.style?.objectFit === fit)}
>
{fit}
</button>
))}
</div>
</div>
)}
{/* Alignment */}
<div>
<label style={labelStyle}>Alignment</label>
<div style={{ display: 'flex', gap: 4 }}>
<button onClick={() => setAlignment('left')} style={btnStyle(alignment === 'left')}>
<i className="fa fa-align-left" style={{ marginRight: 3 }} />Left
</button>
<button onClick={() => setAlignment('center')} style={btnStyle(alignment === 'center')}>
<i className="fa fa-align-center" style={{ marginRight: 3 }} />Center
</button>
<button onClick={() => setAlignment('right')} style={btnStyle(alignment === 'right')}>
<i className="fa fa-align-right" style={{ marginRight: 3 }} />Right
</button>
</div>
</div>
{/* Border Radius */}
<div>
<label style={labelStyle}>Border Radius</label>
<div style={{ display: 'flex', gap: 4 }}>
{radiusPresets.map((r) => (
<button key={r} onClick={() => setPropStyle('borderRadius', r)}
style={btnStyle(props.style?.borderRadius === r)}
>{r}</button>
))}
</div>
</div>
</div>
);
};
ImageBlock.craft = {
displayName: 'Image',
props: { src: PLACEHOLDER_SRC, alt: '', style: { width: '100%', height: 'auto' } },
rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true },
related: { settings: ImageBlockSettings },
};
(ImageBlock as any).toHtml = (props: ImageBlockProps, _c: string) => {
-76
View File
@@ -54,79 +54,6 @@ export const MapEmbed: UserComponent<MapEmbedProps> = ({
);
};
/* ---------- Settings panel ---------- */
const MapEmbedSettings: React.FC = () => {
const { actions: { setProp }, props } = useNode((node) => ({
props: node.data.props as MapEmbedProps,
}));
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
const inputStyle: CSSProperties = {
width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7',
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12,
};
const heightPresets = ['300px', '400px', '500px', '600px'];
return (
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
{/* Address */}
<div>
<label style={labelStyle}>Address</label>
<input
type="text"
value={props.address || ''}
onChange={(e) => setProp((p: MapEmbedProps) => { p.address = e.target.value; })}
placeholder="Enter an address or location..."
style={inputStyle}
/>
</div>
{/* Zoom */}
<div>
<label style={labelStyle}>Zoom: {props.zoom || 14}</label>
<input
type="range"
min={1}
max={20}
value={props.zoom || 14}
onChange={(e) => setProp((p: MapEmbedProps) => { p.zoom = parseInt(e.target.value, 10); })}
style={{ width: '100%' }}
/>
</div>
{/* Height */}
<div>
<label style={labelStyle}>Height</label>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap', marginBottom: 6 }}>
{heightPresets.map((h) => (
<button
key={h}
onClick={() => setProp((p: MapEmbedProps) => { p.height = h; })}
style={{
padding: '4px 10px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
border: '1px solid #3f3f46',
background: props.height === h ? '#3b82f6' : '#27272a',
color: '#e4e4e7',
}}
>
{h}
</button>
))}
</div>
<input
type="text"
value={props.height || ''}
onChange={(e) => setProp((p: MapEmbedProps) => { p.height = e.target.value; })}
placeholder="e.g. 400px"
style={inputStyle}
/>
</div>
</div>
);
};
/* ---------- Craft config ---------- */
MapEmbed.craft = {
@@ -142,9 +69,6 @@ MapEmbed.craft = {
canMoveIn: () => false,
canMoveOut: () => true,
},
related: {
settings: MapEmbedSettings,
},
};
/* ---------- HTML export ---------- */
+1 -362
View File
@@ -1,4 +1,4 @@
import React, { CSSProperties, useCallback, useRef, useState } from 'react';
import React, { CSSProperties } from 'react';
import { useNode, Element, UserComponent } from '@craftjs/core';
import { Container } from '../layout/Container';
import { cssPropsToString } from '../../utils/style-helpers';
@@ -61,28 +61,6 @@ function buildEmbedParams(
return url.toString();
}
/* ---------- Upload helper ---------- */
async function uploadToWhp(file: File): Promise<string | null> {
const cfg = (window as any).WHP_CONFIG;
if (!cfg) return URL.createObjectURL(file);
const formData = new FormData();
formData.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: formData,
});
const data = await resp.json();
if (data.success && data.url) return data.url;
return null;
} catch {
return null;
}
}
/* ---------- Placeholder ---------- */
const VIDEO_PLACEHOLDER = (
@@ -297,342 +275,6 @@ export const VideoBlock: UserComponent<VideoBlockProps> = ({
);
};
/* ========================================================================
Settings Panel
======================================================================== */
const VideoBlockSettings: React.FC = () => {
const {
actions: { setProp },
props,
} = useNode((node) => ({
props: node.data.props as VideoBlockProps,
}));
const [urlInput, setUrlInput] = useState(props.videoUrl || '');
const fileInputRef = useRef<HTMLInputElement>(null);
const detected = props.videoUrl ? detectVideoType(props.videoUrl) : { type: 'none' as VideoType, embedUrl: '' };
const applyUrl = useCallback(
(url: string) => {
const info = detectVideoType(url);
setProp((p: VideoBlockProps) => {
p.videoUrl = url;
p.videoType = info.type;
p.embedUrl = info.embedUrl;
});
},
[setProp]
);
const handleUpload = useCallback(
async (file: File) => {
const url = await uploadToWhp(file);
if (url) {
setUrlInput(url);
applyUrl(url);
}
},
[applyUrl]
);
const handleBrowse = useCallback(async () => {
const cfg = (window as any).WHP_CONFIG;
if (!cfg) return;
try {
const resp = await fetch(`${cfg.apiUrl}?action=list_assets&site_id=${cfg.siteId}`);
const data = await resp.json();
if (data.success && Array.isArray(data.assets)) {
const videos = data.assets.filter(
(a: any) => (a.type || '').startsWith('video') || (a.name || '').match(/\.(mp4|webm|ogg|mov)$/i)
);
if (videos.length === 0) {
alert('No video assets uploaded yet. Use the Upload button to add one.');
return;
}
const names = videos.map((a: any, i: number) => `${i + 1}. ${a.name}`).join('\n');
const choice = prompt(`Select a video (enter number):\n\n${names}`);
if (choice) {
const idx = parseInt(choice, 10) - 1;
if (videos[idx]) {
setUrlInput(videos[idx].url);
applyUrl(videos[idx].url);
}
}
}
} catch (e) {
console.error('Browse failed:', e);
}
}, [applyUrl]);
const typeBadge = (label: string, color: string) => (
<span
style={{
display: 'inline-block',
padding: '2px 8px',
borderRadius: 4,
background: color,
color: '#fff',
fontSize: 10,
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
{label}
</span>
);
const overlayPresets = ['#000000', '#1e293b', '#0f172a', '#312e81', '#064e3b', '#7f1d1d'];
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 4 };
const inputStyle: CSSProperties = {
width: '100%',
padding: '4px 8px',
background: '#27272a',
color: '#e4e4e7',
border: '1px solid #3f3f46',
borderRadius: 4,
fontSize: 12,
};
const checkboxRowStyle: CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: 6,
fontSize: 12,
color: '#e4e4e7',
cursor: 'pointer',
};
return (
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 14 }}>
{/* Video URL */}
<div>
<label style={labelStyle}>Video URL</label>
<div style={{ display: 'flex', gap: 4 }}>
<input
type="text"
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') applyUrl(urlInput);
}}
placeholder="YouTube, Vimeo, or direct video URL..."
style={{ ...inputStyle, flex: 1 }}
/>
<button
onClick={() => applyUrl(urlInput)}
style={{
padding: '4px 12px',
fontSize: 11,
borderRadius: 4,
cursor: 'pointer',
border: '1px solid #3f3f46',
background: '#3b82f6',
color: '#fff',
fontWeight: 600,
whiteSpace: 'nowrap',
}}
>
Apply
</button>
</div>
{/* Detected type badge */}
{detected.type !== 'none' && (
<div style={{ marginTop: 6 }}>
{detected.type === 'youtube' && typeBadge('YouTube', '#dc2626')}
{detected.type === 'vimeo' && typeBadge('Vimeo', '#1ab7ea')}
{detected.type === 'file' && typeBadge('Video File', '#16a34a')}
</div>
)}
</div>
{/* Upload / Browse */}
<div>
<div style={{ display: 'flex', gap: 4 }}>
<button
onClick={() => fileInputRef.current?.click()}
style={{
flex: 1,
padding: '8px 10px',
fontSize: 12,
borderRadius: 4,
cursor: 'pointer',
border: '1px solid #3f3f46',
background: '#3b82f6',
color: '#fff',
fontWeight: 500,
}}
>
<i className="fa fa-upload" style={{ marginRight: 4 }} /> Upload
</button>
<button
onClick={handleBrowse}
style={{
flex: 1,
padding: '8px 10px',
fontSize: 12,
borderRadius: 4,
cursor: 'pointer',
border: '1px solid #3f3f46',
background: '#27272a',
color: '#e4e4e7',
}}
>
<i className="fa fa-folder-open" style={{ marginRight: 4 }} /> Browse
</button>
</div>
<input
ref={fileInputRef}
type="file"
accept="video/*"
style={{ display: 'none' }}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleUpload(file);
e.target.value = '';
}}
/>
</div>
{/* Playback options */}
<div>
<label style={labelStyle}>Playback</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<label style={checkboxRowStyle}>
<input
type="checkbox"
checked={props.autoplay ?? false}
onChange={(e) => setProp((p: VideoBlockProps) => { p.autoplay = e.target.checked; })}
/>
Autoplay
</label>
<label style={checkboxRowStyle}>
<input
type="checkbox"
checked={props.muted ?? true}
onChange={(e) => setProp((p: VideoBlockProps) => { p.muted = e.target.checked; })}
/>
Muted
</label>
<label style={checkboxRowStyle}>
<input
type="checkbox"
checked={props.loop ?? false}
onChange={(e) => setProp((p: VideoBlockProps) => { p.loop = e.target.checked; })}
/>
Loop
</label>
<label style={checkboxRowStyle}>
<input
type="checkbox"
checked={props.controls ?? true}
onChange={(e) => setProp((p: VideoBlockProps) => { p.controls = e.target.checked; })}
/>
Show Controls
</label>
</div>
</div>
{/* Mode toggle */}
<div>
<label style={labelStyle}>Display Mode</label>
<div style={{ display: 'flex', gap: 4 }}>
<button
onClick={() => setProp((p: VideoBlockProps) => { p.isBackground = false; })}
style={{
flex: 1,
padding: '6px 10px',
fontSize: 11,
borderRadius: 4,
cursor: 'pointer',
border: '1px solid #3f3f46',
background: !props.isBackground ? '#3b82f6' : '#27272a',
color: !props.isBackground ? '#fff' : '#a1a1aa',
fontWeight: 500,
}}
>
Normal
</button>
<button
onClick={() => setProp((p: VideoBlockProps) => { p.isBackground = true; })}
style={{
flex: 1,
padding: '6px 10px',
fontSize: 11,
borderRadius: 4,
cursor: 'pointer',
border: '1px solid #3f3f46',
background: props.isBackground ? '#3b82f6' : '#27272a',
color: props.isBackground ? '#fff' : '#a1a1aa',
fontWeight: 500,
}}
>
Background
</button>
</div>
</div>
{/* Background mode options */}
{props.isBackground && (
<>
<div>
<label style={labelStyle}>Overlay Color</label>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{overlayPresets.map((c) => (
<button
key={c}
onClick={() => setProp((p: VideoBlockProps) => { p.overlayColor = c; })}
style={{
width: 24,
height: 24,
borderRadius: 4,
border: '1px solid #3f3f46',
backgroundColor: c,
cursor: 'pointer',
outline: props.overlayColor === c ? '2px solid #3b82f6' : 'none',
outlineOffset: 1,
}}
/>
))}
</div>
</div>
<div>
<label style={labelStyle}>
Overlay Opacity: {props.overlayOpacity ?? 50}%
</label>
<input
type="range"
min={0}
max={100}
value={props.overlayOpacity ?? 50}
onChange={(e) =>
setProp((p: VideoBlockProps) => {
p.overlayOpacity = parseInt(e.target.value, 10);
})
}
style={{ width: '100%' }}
/>
</div>
<div>
<label style={labelStyle}>Inner Max Width</label>
<input
type="text"
value={props.innerMaxWidth || '1200px'}
onChange={(e) => setProp((p: VideoBlockProps) => { p.innerMaxWidth = e.target.value; })}
style={inputStyle}
/>
</div>
</>
)}
</div>
);
};
/* ========================================================================
Craft Config
======================================================================== */
@@ -658,9 +300,6 @@ VideoBlock.craft = {
canMoveIn: () => true,
canMoveOut: () => true,
},
related: {
settings: VideoBlockSettings,
},
};
/* ========================================================================