// ============================================================
// BLIND QUILL — shared components
// Stateless presentational pieces, exported to window.
// ============================================================
const { useState, useEffect, useRef } = React;
// ---------- Icons (stroke-only, 24x24) ----------
function Icon({ name, size = 18, stroke = 1.7, style }) {
const common = {
width: size, height: size, viewBox: "0 0 24 24", fill: "none",
stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round", style,
};
switch (name) {
case "quill":
return (
);
case "lock":
return ();
case "arrow-right":
return ();
case "arrow-left":
return ();
case "plus":
return ();
case "book":
return ();
case "link":
return ();
case "seal":
return ();
case "check":
return ();
case "scroll":
return ();
case "eye-off":
return ();
default:
return null;
}
}
// ---------- Wordmark ----------
function Wordmark({ onClick }) {
return (
Blind Quill
The Invisible Bindery
);
}
// ---------- Buttons ----------
function Btn({ kind = "ghost-paper", size, icon, iconRight, children, onClick, disabled, type }) {
const cls = ["btn", `btn--${kind}`, size ? `btn--${size}` : ""].join(" ");
return (
);
}
// ---------- Badge + Ledger ----------
function StatusBadge({ status }) {
const sealed = status === "sealed";
return (
{sealed ? "Sealed" : "Open"}
);
}
function Ledger({ count, max, sealed }) {
const pct = Math.round((count / max) * 100);
return (
{count} / {max}
);
}
// ---------- Veiled lines (blinded text) ----------
function Veil({ lines = 4, widths }) {
const ws = widths || Array.from({ length: lines }, (_, i) => 70 + ((i * 37) % 28));
return (
{ws.map((w, i) => )}
);
}
// ---------- Manuscript card (gallery) ----------
function ManuscriptCard({ story, onOpen }) {
const c = story.capsule;
return (
);
}
function NewManuscriptCard({ onClick }) {
return (
);
}
// ---------- Wax seal ----------
function WaxSeal({ label = "Sealed · 30 of 30" }) {
return (
);
}
// ---------- Manuscript reader ----------
function ManuscriptReader({ story, highlightId, highlightIds, newGraft }) {
const [active, setActive] = useState(story.chapters[0] ? 0 : 0);
const refs = useRef([]);
// A graft can land 1–3 paragraphs; accept either a single id or a set.
const grafted = new Set(highlightIds || (highlightId ? [highlightId] : []));
const goChapter = (i) => {
setActive(i);
const el = refs.current[i];
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - 90;
window.scrollTo({ top, behavior: "smooth" });
}
};
return (
{story.capsule.genre} · {story.capsule.tone}
{story.capsule.title}
Story · {story.id}
Grafts · {story.graftCount}/{story.maxGrafts}
{story.status === "sealed" ? "Sealed manuscript" : "Open for grafting"}
{story.chapters.map((ch, i) => (
(refs.current[i] = el)}>
Chapter {ch.no}
{ch.title}
{ch.summary}
{ch.paragraphs.map((p) => {
const isGraft = grafted.has(p.id);
if (isGraft) {
return (
Your graft
{p.text}
);
}
if (p.lead) {
const first = p.text.charAt(0);
const rest = p.text.slice(1);
return (
{first}{rest}
);
}
return
{p.text}
;
})}
))}
);
}
Object.assign(window, {
Icon, Wordmark, Btn, StatusBadge, Ledger, Veil,
ManuscriptCard, NewManuscriptCard, WaxSeal, ManuscriptReader,
});