{/* Action footer — like / comment / reshare on the left, add to
collection on the right. */}
{onUpdated ? (
setBgModal(false)}
post={post}
isMobile={isMobile}
onUpdated={onUpdated}
/>
) : null}
);
}
// ─── Pieces ──────────────────────────────────────────────────────────────
function ImageCarousel({ images }: { images: PostImage[] }) {
const [active, setActive] = useState(0);
function onScroll(e: React.UIEvent) {
const el = e.currentTarget;
const w = el.offsetWidth;
if (w === 0) return;
setActive(Math.round(el.scrollLeft / w));
}
if (images.length === 1) {
return (
);
}
return (
{images.map((img, i) => (
))}
{images.map((_, i) => (
))}
);
}
function QuotePanel({ post }: { post: Post }) {
// No-image post: the entire square is a flat palette-aware surface
// (pure white on light themes, palette-specific dark on dark themes)
// with the quote in serif inside. Text uses --foreground so it inverts
// automatically with the theme.
return (
“{post.caption}”
);
}
function Caption({
text,
expanded,
onToggle,
}: {
text: string;
expanded: boolean;
onToggle: () => void;
}) {
// We clamp to 3 lines collapsed and expose a "More" toggle only when
// the clamp is actually hiding something. A char-length heuristic gets
// this wrong: a 200-char caption can fit cleanly in 3 lines on a wide
// card (no clipping → "More" would be a false promise). Instead measure
// scrollHeight vs clientHeight on the clamped
. Once we've observed
// overflow we latch it on so "Show less" stays visible after expansion
// (where scrollHeight would equal clientHeight and we'd otherwise lose
// the way back).
const pRef = useRef(null);
const [overflowed, setOverflowed] = useState(false);
useEffect(() => {
if (expanded) return;
const el = pRef.current;
if (!el) return;
const measure = () => {
// Sub-pixel rounding can leave a 1px gap even when content fits;
// require a real difference before flagging overflow.
setOverflowed(el.scrollHeight - el.clientHeight > 1);
};
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, [text, expanded]);
const showToggle = overflowed || expanded;
return (