Preset buttons in BoxModelSection / BorderEffectsSection now write through the discrete setters (setPropStyles / setPropStyle), so a keyboard-activated preset (no pointerup or focusout to close the burst) never merges into the previous edit of the same key. SpacingControl gains an optional onPreset for this; margin/padding presets are also one undo step for all four sides in the media panels. Refs #7 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
4.9 KiB
TypeScript
127 lines
4.9 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,
|
|
continuousSetProp,
|
|
} from './shared';
|
|
import { useResolvedStyle } from './responsive-panel';
|
|
import { OverrideDot } from './OverrideDot';
|
|
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 = useResolvedStyle(nodeProps);
|
|
const dot = (key: string | readonly string[]) => <OverrideDot nodeProps={nodeProps} selectedId={selectedId} styleKey={key} />;
|
|
|
|
const { setProp, setPropStyle, setProps, setPropStyleContinuous, setPropStyles } = useNodeProp(selectedId);
|
|
|
|
// Applying an aspect-ratio crop should FILL the frame by default (object-fit:
|
|
// cover) rather than leave letterboxed empty bands, and width + ratio + cover
|
|
// fully determine the box -- so a stale `height` can't linger and conflict
|
|
// (kills the %-height no-op that made resize look like it wasn't working).
|
|
// Clearing the ratio (back to 'Original') leaves objectFit as the user left it.
|
|
const applyAspectRatio = (v: string) => {
|
|
// One setProp call so the whole ratio change is a single undo step.
|
|
setPropStyles({
|
|
aspectRatio: v,
|
|
...(v ? { ...(!style.objectFit && { objectFit: 'cover' }), height: '' } : {}),
|
|
});
|
|
};
|
|
|
|
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"
|
|
// #55: a stock photo carries its own alt/credit; only fill an EMPTY alt.
|
|
onPick={(a) => {
|
|
if (!nodeProps.alt && a.alt) actions.setProp(selectedId, (props: any) => { props.alt = a.alt; });
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Alt Text */}
|
|
<TextInputField
|
|
label="Alt Text"
|
|
value={nodeProps.alt || ''}
|
|
placeholder="Describe the image..."
|
|
onChange={(v) => {
|
|
continuousSetProp(actions, selectedId, 'alt', (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{dot('maxWidth')}</>}
|
|
value={(style.maxWidth as string) || ''}
|
|
onChange={(v) => setPropStyleContinuous('maxWidth', v)}
|
|
/>
|
|
{/* Height is only meaningful when there's no aspect-ratio crop -- once a
|
|
ratio is set, Width + ratio + cover fully determine the box, so a
|
|
separate Height control would only conflict/mislead (see
|
|
applyAspectRatio, which clears any stale height at that moment). */}
|
|
{!style.aspectRatio && (
|
|
<SizeControl
|
|
label={<>Height{dot('height')}</>}
|
|
value={(style.height as string) || ''}
|
|
onChange={(v) => setPropStyleContinuous('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={applyAspectRatio}
|
|
/>
|
|
<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={setPropStyleContinuous} labelAddon={dot} setPropStylesDiscrete={setPropStyles} />
|
|
<BorderEffectsSection style={style} setPropStyle={setPropStyleContinuous} setPropStyleDiscrete={setPropStyle} />
|
|
<AnimVisSection nodeProps={nodeProps} setProps={setProps} />
|
|
</>
|
|
);
|
|
};
|