// page-shell.jsx // Shared building blocks for every Allo Education book page (8.5×11). // Header / Footer / Page wrapper / Ketty slot / decorative doodles / QR placeholder. const { useState, useMemo, useEffect, useRef } = React; /* ---------- Logo (vector — "Allo Education / Éditions") ---------- */ const AlloLogoMark = ({ size = "0.42in" }) => ( {/* Mortar board */} ); const AlloHeaderLogo = () => (
Allo Education Éditions
); /* ---------- Header doodles (subtle, white-on-blue) ---------- */ const HeaderDoodles = () => ( <> ); /* ---------- Header ---------- */ function PageHeader({ matiere = "Mode d'emploi", niveau = "Primaire", page = 3, showDoodles = true, variant = "default", splitOf }) { return (
{showDoodles && }
{matiere} {niveau} {splitOf && {splitOf.n} / {splitOf.total}}
{typeof page === "number" ? `Page ${page}` : page}
); } /* ---------- QR placeholder (visually realistic — V1.1) ---------- */ function QRPlaceholder({ size = "0.9in", seed = 42, fg = "#0F1A3D", bg = "white", quiet = true }) { // 33×33 grid (Version 4 QR). Three finder patterns, one alignment pattern, // timing rows, and a deterministic dense data field. Looks real, isn't scannable. const grid = 33; const cells = React.useMemo(() => { let s = seed * 1103515245 + 12345; const rand = () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return (s >>> 0) / 0x7fffffff; }; const inFinder = (x, y) => (x < 7 && y < 7) || (x > grid - 8 && y < 7) || (x < 7 && y > grid - 8); const finderRing = (x, y) => { const fx = x > grid - 8 ? grid - 1 - x : x; const fy = y > grid - 8 ? grid - 1 - y : y; if (fx === 0 || fx === 6 || fy === 0 || fy === 6) return 1; if (fx >= 2 && fx <= 4 && fy >= 2 && fy <= 4) return 1; return 0; }; // Alignment pattern (5×5) at bottom-right interior const ax = grid - 9, ay = grid - 9; const inAlign = (x, y) => x >= ax && x < ax + 5 && y >= ay && y < ay + 5; const alignCell = (x, y) => { const dx = x - ax, dy = y - ay; if (dx === 0 || dx === 4 || dy === 0 || dy === 4) return 1; if (dx === 2 && dy === 2) return 1; return 0; }; const out = []; for (let y = 0; y < grid; y++) { for (let x = 0; x < grid; x++) { let v; if (inFinder(x, y)) v = finderRing(x, y); else if (inAlign(x, y)) v = alignCell(x, y); else if (x === 6) v = y % 2 === 0 ? 1 : 0; // timing column else if (y === 6) v = x % 2 === 0 ? 1 : 0; // timing row else v = rand() > 0.48 ? 1 : 0; // data field if (v) out.push([x, y]); } } return out; }, [seed]); // Quiet zone is part of the visual block (1 module padding embedded in viewBox) const q = quiet ? 1 : 0; return ( {cells.map(([x, y], i) => )} ); } /* ---------- Footer (V1.2 — square white-bg QR for KDP print fidelity) ---------- */ function PageFooter({ score = "/ 10", scoreVisible = true, qrVisible = true, qrSlug, // production: builds the URL via factoryUrl qrUrl = "alloeducation.fr", qrLabel = "Corriger avec Ketty", qrSubLabel, // optional second line shown above the URL page = 3, }) { // qrSlug overrides qrUrl when present (production wiring) const url = qrSlug ? `alloeducation.fr/qr/${qrSlug}` : qrUrl; return ( ); } /* ---------- Ketty slot (placeholder for 3D PNG) ---------- */ const KETTY_SVG = `data:image/svg+xml;utf8,${encodeURIComponent(` `)}`; function KettySlot({ pose = "hello", // alias accepted: kettyPose -> pose kettyPose, kettyImageSrc, // production: real PNG path (e.g. "img/ketty/pointing.png") kettyAlt, // production: alt text for accessibility className = "", style = {}, }) { const finalPose = kettyPose || pose; if (kettyImageSrc) { return (
{kettyAlt
); } return (
Ketty PNG transparent
pose : {finalPose}
); } /* ---------- Full page wrapper ---------- */ // V1.2.2 — density is now LOCKED per template (not driven by a global tweak). // The optional `splitOf` prop renders a "1 / 2" badge in the header and is // the only safe way to declare a content-too-long page split. function AePage({ matiere = "Mode d'emploi", niveau = "Primaire", page = 3, scale = 0.7, density = "std", // LOCKED per template — air | std | dense qrVisible = true, scoreVisible = true, showGrid = false, showMargins = false, // V1.2 official QR props qrSlug, qrUrl, qrLabel = "Corriger avec Ketty", qrSubLabel, // V1.2.2 split-page support splitOf, // e.g. {n: 1, total: 2} // V1.2.2 auditor mode — when true (set by sidebar toggle), flags violations audit = false, children, }) { const contentRef = React.useRef(null); const [overflow, setOverflow] = React.useState(false); // Overflow detection — runs after layout in auditor mode React.useEffect(() => { if (!audit || !contentRef.current) return; const el = contentRef.current; setOverflow(el.scrollHeight - el.clientHeight > 2); }, [audit, children]); const cls = [ "ae-page", "scaled", `density-${density}`, showGrid && "show-grid", showMargins && "show-margins", ].filter(Boolean).join(" "); return (
{children}
); } /* ---------- Page-frame wrapper for previews in mini-site ---------- */ function PageFrame({ label = "8.5 × 11 in", scale = 0.62, children }) { // children is an AePage; we render it scaled and reserve actual scaled space. // Width/height of scaled content = page * scale. const w = `calc(${8.5} * ${scale} * 96px)`; const h = `calc(${11} * ${scale} * 96px)`; return (
{label} 300 dpi · KDP ready
{React.cloneElement(children, { scale })}
); } /* Export */ Object.assign(window, { AlloHeaderLogo, AlloLogoMark, PageHeader, PageFooter, AePage, PageFrame, KettySlot, QRPlaceholder, HeaderDoodles, });