);
}
// Horizontal padding around a block (px-4 on each side at sm and up), which the
// image sits inside — so a non-full-bleed image can only ever be this much
// narrower than the page width.
const PAGE_PADDING = 32;
function MaxWidthControl(props: {
entityID: string;
intrinsicWidth: number;
disabled?: boolean;
onPreviewWidth?: (width: number | null) => void;
}) {
let { rep, rootEntity } = useReplicache();
let { data: pubData, normalizedPublication } = useLeafletPublicationData();
// Publication drafts carry page width on the publication record, not on the
// leaflet's theme facts; standalone docs use the leaflet theme.
let entityPageWidth = useEntity(rootEntity, "theme/page-width")?.data.value;
let pageWidth =
(pubData?.publications
? normalizedPublication?.theme?.pageWidth
: entityPageWidth) || 624;
let maxWidth = useEntity(props.entityID, "image/max-width")?.data.value;
const MAX_WIDTH_MIN = 10;
const MAX_WIDTH_MAX = pageWidth - PAGE_PADDING;
const MAX_WIDTH_STEP = 10;
// With no explicit width, the image renders at its intrinsic width capped to
// the page content area — mirror that as the slider's starting value.
let defaultWidth = Math.min(props.intrinsicWidth, MAX_WIDTH_MAX);
// Interim value lets the slider/input update live; committed to the fact on
// release (slider) or blur/Enter (input).
let [interim, setInterim] = useState(maxWidth ?? defaultWidth);
useEffect(
() => setInterim(maxWidth ?? defaultWidth),
[maxWidth, defaultWidth],
);
let sliderValue = Number.isNaN(interim)
? defaultWidth
: Math.max(MAX_WIDTH_MIN, Math.min(MAX_WIDTH_MAX, interim));
// Live-preview the width in the editor as the slider/input changes, before
// the value is committed to the fact on release.
let preview = (value: number) => {
setInterim(value);
if (!Number.isNaN(value))
props.onPreviewWidth?.(
Math.max(MAX_WIDTH_MIN, Math.min(MAX_WIDTH_MAX, value)),
);
};
let commit = (value: number) => {
let clamped = Math.max(
MAX_WIDTH_MIN,
Math.min(MAX_WIDTH_MAX, Number.isNaN(value) ? defaultWidth : value),
);
setInterim(clamped);
rep?.mutate.assertFact({
entity: props.entityID,
attribute: "image/max-width",
data: { type: "number", value: clamped },
});
// Hand rendering back to the committed fact.
props.onPreviewWidth?.(null);
};
return (
);
}
function CoverImageControl(props: { entityID: string }) {
let { rep, rootEntity } = useReplicache();
let entity_set = useEntitySetContext();
let { data: pubData } = useLeafletPublicationData();
let publicationPage = useLeafletPublicationPage();
let image = useEntity(props.entityID, "block/image");
let alt = useEntity(props.entityID, "image/alt")?.data.value;
let coverEntity = useEntity(rootEntity, "root/cover-image")?.data.value;
let coverImage = useEntity(coverEntity ?? null, "block/image");
// Only show in a publication, with write permissions, and an image to copy.
// Publication pages (e.g. an About page) have no cover image.
if (
!pubData?.publications ||
!entity_set.permissions.write ||
publicationPage ||
!image
)
return null;
// The cover is a standalone copy sharing this block's src, so a matching src
// means this block is the current cover.
let isCoverImage = !!coverImage && coverImage.data.src === image.data.src;
return (
<>
{
if (!rep) return;
if (isCoverImage) {
if (coverEntity)
await rep.mutate.deleteEntity({ entity: coverEntity });
} else {
await setCoverImageFromEntity(rep, {
rootEntity,
permission_set: entity_set.set,
image: image.data,
alt,
});
}
}}
>