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,30 +2,28 @@ import React, { CSSProperties } from 'react';
|
||||
import { useEditor } from '@craftjs/core';
|
||||
import {
|
||||
IMAGE_RADIUS_PRESETS,
|
||||
OBJECT_FIT,
|
||||
} from '../../../constants/presets';
|
||||
import {
|
||||
StylePanelProps,
|
||||
SectionLabel,
|
||||
PresetButtonGrid,
|
||||
TextInputField,
|
||||
SizeControl,
|
||||
AspectRatioControl,
|
||||
FocalPointGrid,
|
||||
useNodeProp,
|
||||
} from './shared';
|
||||
import { AssetPicker } from '../../../ui/AssetPicker';
|
||||
import { PLACEHOLDER_SRC } from '../../../components/media/ImageBlock';
|
||||
import { BoxModelSection, BorderEffectsSection, AnimVisSection } from './mediaBoxModel';
|
||||
|
||||
/* ---------- IMAGE (with upload/browse/drop) ---------- */
|
||||
export const ImageStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
|
||||
const { actions } = useEditor();
|
||||
const style: CSSProperties = nodeProps.style || {};
|
||||
|
||||
const { setPropStyle } = useNodeProp(selectedId);
|
||||
|
||||
const maxWidthPresets = [
|
||||
{ label: '25%', value: '25%' },
|
||||
{ label: '50%', value: '50%' },
|
||||
{ label: '75%', value: '75%' },
|
||||
{ label: '100%', value: '100%' },
|
||||
];
|
||||
const { setProp, setPropStyle } = useNodeProp(selectedId);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -49,6 +47,39 @@ export const ImageStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodePro
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Size -- Width absorbs the historical maxWidth presets (25/50/75/100%
|
||||
are a subset of SizeControl's default preset row), Height is new. */}
|
||||
<SizeControl
|
||||
label="Width"
|
||||
value={(style.maxWidth as string) || ''}
|
||||
onChange={(v) => setPropStyle('maxWidth', v)}
|
||||
/>
|
||||
<SizeControl
|
||||
label="Height"
|
||||
value={(style.height as string) || ''}
|
||||
onChange={(v) => setPropStyle('height', v)}
|
||||
/>
|
||||
|
||||
{/* Crop & Framing -- aspect-ratio + object-fit + object-position on the
|
||||
<img> itself is a CSS framing crop (no server-side image processing
|
||||
needed). */}
|
||||
<AspectRatioControl
|
||||
value={(style.aspectRatio as string) || ''}
|
||||
onChange={(v) => setPropStyle('aspectRatio', v)}
|
||||
/>
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Object Fit</SectionLabel>
|
||||
<PresetButtonGrid
|
||||
presets={OBJECT_FIT}
|
||||
activeValue={(style.objectFit as string) || ''}
|
||||
onSelect={(v) => setPropStyle('objectFit', v)}
|
||||
/>
|
||||
</div>
|
||||
<FocalPointGrid
|
||||
value={(style.objectPosition as string) || ''}
|
||||
onChange={(v) => setPropStyle('objectPosition', v)}
|
||||
/>
|
||||
|
||||
{/* Border Radius */}
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Border Radius</SectionLabel>
|
||||
@@ -59,15 +90,10 @@ export const ImageStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodePro
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Max Width */}
|
||||
<div className="guided-section">
|
||||
<SectionLabel>Max Width</SectionLabel>
|
||||
<PresetButtonGrid
|
||||
presets={maxWidthPresets}
|
||||
activeValue={style.maxWidth as string}
|
||||
onSelect={(v) => setPropStyle('maxWidth', v)}
|
||||
/>
|
||||
</div>
|
||||
{/* Box model + animation/visibility rollout */}
|
||||
<BoxModelSection style={style} setPropStyle={setPropStyle} />
|
||||
<BorderEffectsSection style={style} setPropStyle={setPropStyle} />
|
||||
<AnimVisSection nodeProps={nodeProps} setProp={setProp} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user