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:
@@ -9,6 +9,23 @@ interface ImageBlockProps {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
style?: CSSProperties;
|
||||
animation?: string;
|
||||
animationDelay?: string;
|
||||
hideOnDesktop?: boolean;
|
||||
hideOnTablet?: boolean;
|
||||
hideOnMobile?: boolean;
|
||||
}
|
||||
|
||||
/** Extracts the numeric portion of a plain "<n>px" CSS length string, for
|
||||
* emitting real `width`/`height` HTML attributes on the exported `<img>`
|
||||
* (helps the browser reserve layout space before the image loads --
|
||||
* avoiding CLS -- something a CSS-only width/height can't do on its own).
|
||||
* Returns undefined for any other unit ('%', 'auto', '', etc.) so the
|
||||
* attribute is simply omitted when the pixel size isn't known. */
|
||||
export function pxAttr(v: unknown): string | undefined {
|
||||
if (typeof v !== 'string') return undefined;
|
||||
const m = v.trim().match(/^(\d+(?:\.\d+)?)px$/);
|
||||
return m ? m[1] : undefined;
|
||||
}
|
||||
|
||||
// Helper: upload a file to the WHP API and return the proxy URL
|
||||
@@ -83,7 +100,27 @@ export const ImageBlock: UserComponent<ImageBlockProps> = ({
|
||||
|
||||
ImageBlock.craft = {
|
||||
displayName: 'Image',
|
||||
props: { src: PLACEHOLDER_SRC, alt: '', style: { width: '100%', height: 'auto' } },
|
||||
props: {
|
||||
src: PLACEHOLDER_SRC,
|
||||
alt: '',
|
||||
style: {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
aspectRatio: '',
|
||||
objectFit: '' as CSSProperties['objectFit'],
|
||||
objectPosition: '',
|
||||
marginTop: '', marginRight: '', marginBottom: '', marginLeft: '',
|
||||
paddingTop: '', paddingRight: '', paddingBottom: '', paddingLeft: '',
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
opacity: '1',
|
||||
},
|
||||
animation: '',
|
||||
animationDelay: '0',
|
||||
hideOnDesktop: false,
|
||||
hideOnTablet: false,
|
||||
hideOnMobile: false,
|
||||
},
|
||||
rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true },
|
||||
};
|
||||
|
||||
@@ -95,5 +132,8 @@ ImageBlock.craft = {
|
||||
}
|
||||
const s = cssPropsToString({ display: 'block', maxWidth: '100%', ...props.style });
|
||||
const alt = props.alt ? ` alt="${escapeAttr(props.alt)}"` : ' alt=""';
|
||||
return { html: `<img src="${escapeAttr(safeImageUrl(src))}"${alt}${s ? ` style="${s}"` : ''} />` };
|
||||
const widthAttr = pxAttr((props.style as any)?.width);
|
||||
const heightAttr = pxAttr((props.style as any)?.height);
|
||||
const dims = `${widthAttr ? ` width="${widthAttr}"` : ''}${heightAttr ? ` height="${heightAttr}"` : ''}`;
|
||||
return { html: `<img src="${escapeAttr(safeImageUrl(src))}"${alt}${dims} loading="lazy" decoding="async"${s ? ` style="${s}"` : ''} />` };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user