feat(builder): media package -- image crop/perf, video size+picker, gallery cols/lightbox, box-model+anim/vis rollout
- ImageBlock/ImageStylePanel: SizeControl width(absorbs old maxWidth presets)/height, AspectRatioControl + OBJECT_FIT grid + FocalPointGrid for CSS framing crop (aspect-ratio/object-fit/object-position on the <img>). Exported <img> always gets loading="lazy" decoding="async", plus width/ height attrs when the style has a plain px length (pxAttr helper). - VideoBlock/MediaStylePanel: Video URL text input replaced with AssetPicker(mediaType="video") writing videoUrl (paste-URL still handles YouTube/Vimeo, upload/browse handle files). Added SizeControl(width) + AspectRatioControl so the video frame honors real size/aspect instead of a hardcoded 16:9 (padding-bottom hack replaced with CSS aspect-ratio). New optional `poster` prop (image AssetPicker) + preload="metadata" on file-type <video>. - Gallery: surfaced the existing-but-unexposed `columns` and `lightbox` props with panel controls. - All 5 owned components (ImageBlock, VideoBlock, Gallery, ContentSlider, MapEmbed): added margin/padding (per-side)/border/box-shadow/opacity style defaults + AnimationControl/VisibilityControl-backed animation/ animationDelay/hideOnDesktop/hideOnTablet/hideOnMobile props. New shared mediaBoxModel.tsx (package-local, not shared.tsx) DRYs the box-model + border/effects + animation/visibility panel sections across ImageStylePanel and MediaStylePanel. - Tests: extended *.toHtml.test.ts for all 5 components (crop/perf attrs, video size/aspect/poster/preload, gallery columns/lightbox, box-model style emission, craft.props presence) + new MediaStylePanel.video.test.tsx verifying the AssetPicker wiring writes videoUrl/poster and the video-only size controls are gated on videoUrl. 694 tests green, tsc + vite build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import { useEditor } from '@craftjs/core';
|
||||
import {
|
||||
BG_COLORS,
|
||||
SPACING_PRESETS,
|
||||
RADIUS_PRESETS,
|
||||
} from '../../../constants/presets';
|
||||
import {
|
||||
@@ -13,6 +12,8 @@ import {
|
||||
CollapsibleSection,
|
||||
ColorPickerField,
|
||||
ArrayPropEditor,
|
||||
SizeControl,
|
||||
AspectRatioControl,
|
||||
labelStyle,
|
||||
inputStyle,
|
||||
smallInputStyle,
|
||||
@@ -20,6 +21,14 @@ import {
|
||||
useNodeProp,
|
||||
} from './shared';
|
||||
import { AssetPicker } from '../../../ui/AssetPicker';
|
||||
import { BoxModelSection, BorderEffectsSection, AnimVisSection } from './mediaBoxModel';
|
||||
|
||||
const GALLERY_COLUMN_PRESETS = [
|
||||
{ label: '2', value: '2' },
|
||||
{ label: '3', value: '3' },
|
||||
{ label: '4', value: '4' },
|
||||
{ label: '5', value: '5' },
|
||||
];
|
||||
|
||||
/* ---------- MEDIA (Video / Gallery / Map / Slider) ---------- */
|
||||
export const MediaStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
|
||||
@@ -30,11 +39,48 @@ export const MediaStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodePro
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Video URL */}
|
||||
{/* Video source -- upload/browse/paste-URL (paste-URL still handles
|
||||
YouTube/Vimeo; upload/browse handle files via detectVideoType's
|
||||
serve_asset resolution). */}
|
||||
{nodeProps.videoUrl !== undefined && (
|
||||
<div style={sectionGap}>
|
||||
<label style={labelStyle}>Video URL</label>
|
||||
<input type="text" value={nodeProps.videoUrl || ''} onChange={(e) => setProp('videoUrl', e.target.value)} placeholder="YouTube, Vimeo, or .mp4 URL" style={inputStyle} />
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Video Source</SectionLabel>
|
||||
<AssetPicker
|
||||
mediaType="video"
|
||||
value={nodeProps.videoUrl || ''}
|
||||
onChange={(url) => setProp('videoUrl', url)}
|
||||
variant="full"
|
||||
placeholder="Or paste a YouTube/Vimeo/.mp4 URL..."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Video size -- width + aspect ratio for the video frame. */}
|
||||
{nodeProps.videoUrl !== undefined && (
|
||||
<>
|
||||
<SizeControl
|
||||
label="Width"
|
||||
value={(style.width as string) || ''}
|
||||
onChange={(v) => setPropStyle('width', v)}
|
||||
/>
|
||||
<AspectRatioControl
|
||||
value={(style.aspectRatio as string) || ''}
|
||||
onChange={(v) => setPropStyle('aspectRatio', v)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Poster image (optional, file-type videos only in practice but
|
||||
harmless to offer whenever the component has a poster prop). */}
|
||||
{nodeProps.poster !== undefined && (
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Poster Image (optional)</SectionLabel>
|
||||
<AssetPicker
|
||||
mediaType="image"
|
||||
value={nodeProps.poster || ''}
|
||||
onChange={(url) => setProp('poster', url)}
|
||||
variant="full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -80,6 +126,26 @@ export const MediaStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodePro
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gallery layout -- columns + lightbox (existing-but-previously-unexposed props). */}
|
||||
{nodeProps.images !== undefined && Array.isArray(nodeProps.images) && (
|
||||
<>
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Columns</SectionLabel>
|
||||
<PresetButtonGrid
|
||||
presets={GALLERY_COLUMN_PRESETS}
|
||||
activeValue={String(nodeProps.columns ?? 3)}
|
||||
onSelect={(v) => setProp('columns', Number(v))}
|
||||
/>
|
||||
</div>
|
||||
<div style={sectionGap}>
|
||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={!!nodeProps.lightbox} onChange={(e) => setProp('lightbox', e.target.checked)} />
|
||||
Enable Lightbox
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Gallery items */}
|
||||
{nodeProps.images !== undefined && Array.isArray(nodeProps.images) && (
|
||||
<CollapsibleSection title="Images">
|
||||
@@ -178,21 +244,23 @@ export const MediaStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodePro
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Background & padding */}
|
||||
{/* Background & radius */}
|
||||
<CollapsibleSection title="Style" defaultOpen={false}>
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Background</SectionLabel>
|
||||
<ColorSwatchGrid colors={BG_COLORS} activeValue={style.backgroundColor} onSelect={(v: string) => setPropStyle('backgroundColor', v)} />
|
||||
</div>
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Padding</SectionLabel>
|
||||
<PresetButtonGrid presets={SPACING_PRESETS} activeValue={style.padding as string} onSelect={(v) => setPropStyle('padding', v)} />
|
||||
</div>
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Border Radius</SectionLabel>
|
||||
<PresetButtonGrid presets={RADIUS_PRESETS} activeValue={style.borderRadius as string} onSelect={(v) => setPropStyle('borderRadius', v)} />
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Box model + animation/visibility rollout (Video/Gallery/Map/Slider
|
||||
all carry these props now, so it's safe to render unconditionally). */}
|
||||
<BoxModelSection style={style} setPropStyle={setPropStyle} />
|
||||
<BorderEffectsSection style={style} setPropStyle={setPropStyle} />
|
||||
<AnimVisSection nodeProps={nodeProps} setProp={setProp} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user