// components-placeholders.jsx — Image placeholder ("PicSlot") used across the page.
// Renders only when the path is in window.UPLOADED_IMAGES
// (so missing files don't pollute the console). Add a filename to the set
// when the user uploads it.
// ─── Manifest of uploaded images. Populate as the user uploads. ────────
window.UPLOADED_IMAGES = window.UPLOADED_IMAGES || new Set([
"uploads/fenwei1.png",
"uploads/fenwei2.png",
"uploads/hero.png",
"uploads/seal-bi.png",
"uploads/bg-rainy-guest.png",
// Screen placeholders — drop real screenshots in and uncomment.
"uploads/scr-hero.webp",
"uploads/scr-overview.webp",
// "uploads/scr-world.webp",
"uploads/scr-characters.webp",
// "uploads/scr-objects.webp",
// "uploads/scr-events.webp",
// "uploads/scr-writing.webp",
]);
function isUploaded(src) {
return !!src && window.UPLOADED_IMAGES && window.UPLOADED_IMAGES.has(src);
}
function PicSlot({
src,
alt = "",
label, // human label, e.g. "Hero · 主视觉"
hint, // one-line description of what goes here
aspect = "16/10", // CSS aspect-ratio string
variant = "product", // "product" | "mood" | "portrait" | "map" | "desk"
className = "",
style = {},
}) {
const [errored, setErrored] = React.useState(false);
const [loaded, setLoaded] = React.useState(false);
const showImg = isUploaded(src) && !errored;
return (
);
}
function variantLabel(v) {
return {
product: "PRODUCT SHOT",
mood: "MOOD IMAGE",
portrait: "INK PORTRAIT",
map: "MAP / ATLAS",
desk: "FULL DESK SHOT",
}[v] || "IMAGE";
}
/* ────────────────────────────────────────────────────────────
SceneDivider — full-bleed cinematic banner between sections.
Used to break the page rhythm with an atmospheric image
and a short literal excerpt from the demo book.
──────────────────────────────────────────────────────────── */
function SceneDivider({
src,
alt = "",
chapter, // small uppercase label (e.g. "雨中客 · 卷一 · 第 1 章")
quote, // one short line, taken literally from the prose
side = "left", // caption alignment: "left" | "right"
height = "62vh", // banner height; clamped in CSS
}) {
const exists = isUploaded(src);
return (
{exists
?
:
MOOD BANNER
{src}
}
{chapter &&
{chapter}
}
{quote &&
{quote}
}
);
}
/* ────────────────────────────────────────────────────────────
Divider — a small set of decorative dividers, one per variant.
Place between sections, never two in a row.
──────────────────────────────────────────────────────────── */
function Divider({ variant = "brush", label, lang = "zh" }) {
return (