Files
site-builder/craft/src/panels/right/styles/ImageStylePanel.tsx
T
shadowdaoandClaude Opus 4.8 0419291259 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>
2026-07-14 06:30:50 -07:00

100 lines
3.2 KiB
TypeScript

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 { setProp, setPropStyle } = useNodeProp(selectedId);
return (
<>
{/* Image source */}
<div className="guided-section">
<SectionLabel>Image Source</SectionLabel>
<AssetPicker
value={nodeProps.src || ''}
onChange={(url) => actions.setProp(selectedId, (props: any) => { props.src = url || PLACEHOLDER_SRC; })}
variant="full"
/>
</div>
{/* Alt Text */}
<TextInputField
label="Alt Text"
value={nodeProps.alt || ''}
placeholder="Describe the image..."
onChange={(v) => {
actions.setProp(selectedId, (props: any) => { props.alt = v; });
}}
/>
{/* 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>
<PresetButtonGrid
presets={IMAGE_RADIUS_PRESETS}
activeValue={style.borderRadius as string}
onSelect={(v) => setPropStyle('borderRadius', v)}
/>
</div>
{/* Box model + animation/visibility rollout */}
<BoxModelSection style={style} setPropStyle={setPropStyle} />
<BorderEffectsSection style={style} setPropStyle={setPropStyle} />
<AnimVisSection nodeProps={nodeProps} setProp={setProp} />
</>
);
};