const { useState: uS, useEffect: uE, useMemo: uM, useRef: uR } = React;
const LS_KEY = "ctm_review_state_v1:" + CTM_DATA.mock.code;
const SUBMISSION_KEY = LS_KEY + ":last_submission";
const WORK_KEY = LS_KEY + ":active_work_packet";
function loadState() { try { return JSON.parse(localStorage.getItem(LS_KEY)) || {}; } catch (e) { return {}; } }
function loadSubmission() { try { return JSON.parse(localStorage.getItem(SUBMISSION_KEY)) || null; } catch (e) { return null; } }
function loadWorkPacket() { try { return JSON.parse(localStorage.getItem(WORK_KEY)) || null; } catch (e) { return null; } }
function useStore() {
const saved = uM(loadState, []);
const [statuses, setStatuses] = uS(() => {
const base = {}; CTM_DATA.assets.forEach(a => base[a.id] = a.status);
return { ...base, ...(saved.statuses || {}) };
});
const [notes, setNotes] = uS(() => {
const base = { ...CTM_DATA.seedNotes };
Object.entries(saved.notes || {}).forEach(([k, v]) => { base[k] = [...(CTM_DATA.seedNotes[k] || []), ...v]; });
return base;
});
const [userNotes, setUserNotes] = uS(saved.notes || {});
const [replacements, setReplacements] = uS({});
const [workPacket, setWorkPacket] = uS(loadWorkPacket);
uE(() => { localStorage.setItem(LS_KEY, JSON.stringify({ statuses, notes: userNotes })); }, [statuses, userNotes]);
const applySubmission = (submission) => {
if (!submission || submission.schema !== "af1.review_submission.v1") throw new Error("Not an AF1 review submission packet.");
if (!submission.mock || submission.mock.code !== CTM_DATA.mock.code) throw new Error(`Packet is for ${submission.mock?.code || "another mock"}, not ${CTM_DATA.mock.code}.`);
const valid = new Set(CTM_DATA.assets.map(a => a.id));
const nextStatuses = {};
const nextUserNotes = {};
(submission.assets || []).forEach(asset => {
if (!valid.has(asset.id)) return;
if (asset.status) nextStatuses[asset.id] = asset.status;
const importedNotes = (asset.notes || []).filter(n => (n.by || "Reviewer") !== "Codex").map(n => ({ ...n, refUrl: null }));
if (importedNotes.length) nextUserNotes[asset.id] = importedNotes;
});
setStatuses(p => ({ ...p, ...nextStatuses }));
setUserNotes(nextUserNotes);
const nextNotes = { ...CTM_DATA.seedNotes };
Object.entries(nextUserNotes).forEach(([k, v]) => { nextNotes[k] = [...(CTM_DATA.seedNotes[k] || []), ...v]; });
setNotes(nextNotes);
localStorage.setItem(LS_KEY, JSON.stringify({ statuses: { ...statuses, ...nextStatuses }, notes: nextUserNotes }));
return { statusCount: Object.keys(nextStatuses).length, noteCount: Object.values(nextUserNotes).reduce((sum, rows) => sum + rows.length, 0) };
};
const submitAssetWork = (id) => {
const a = CTM_DATA.assets.find(asset => asset.id === id);
if (!a) throw new Error("Asset not found.");
const packet = buildAf1AssetWorkPacket(a, { statuses, notes, replacements });
localStorage.setItem(WORK_KEY, JSON.stringify(packet));
setWorkPacket(packet);
window.AF1_ACTIVE_WORK_PACKET = packet;
return packet;
};
return {
statuses, notes, replacements, workPacket,
setStatus: (id, s) => setStatuses(p => ({ ...p, [id]: s })),
addNote: (id, n) => {
const full = { ...n, id: "u" + Date.now(), ts: new Date().toISOString(), by: "You" };
setNotes(p => ({ ...p, [id]: [...(p[id] || []), full] }));
setUserNotes(p => ({ ...p, [id]: [...(p[id] || []), { ...full, refUrl: null }] }));
},
stageReplacement: (id, url, name) => { setReplacements(p => ({ ...p, [id]: { url, name } })); setStatuses(p => ({ ...p, [id]: "pending" })); },
applySubmission,
submitAssetWork
};
}
const FILTERS = [["all", "All"], ["pending", "Pending"], ["approved", "Hot"], ["rejected", "Not it"], ["revision", "Needs work"], ["superseded", "Superseded"], ["active", "Product-active"]];
function buildForgeBrief(a, store) {
const group = CTM_DATA.groups.find(g => g.id === a.g);
const notes = store.notes[a.id] || [];
return [
`AF1 BUILD BRIEF`,
`Asset: ${a.name} (${a.id.toUpperCase()})`,
`Group: ${group?.name || a.g}`,
`Current status: ${STATUS[store.statuses[a.id]].label}`,
`Ownership: ${OWN[a.own].label}`,
`Purpose: ${a.purpose}`,
`Reference crop: ${a.ref || "none"}`,
`Current candidate: ${store.replacements[a.id]?.name || a.cand || "none"}`,
`Instruction: Build this as a separate reusable asset, not a flattened page crop.`,
`Transfer rule: The asset must work as a template piece beyond this Shootout screen where sensible.`,
`Approval rule: Do not implement into product until marked Hot in AF1.`,
`Reviewer notes: ${notes.map(n => n.text).join(" | ") || "none yet"}`
].join("\n");
}
function buildAf1Export(store) {
const exportedAt = new Date().toISOString();
return {
schema: "af1.asset_forge_export.v1",
exportedAt,
mock: CTM_DATA.mock,
summary: CTM_DATA.assets.reduce((acc, a) => {
const s = store.statuses[a.id];
acc[s] = (acc[s] || 0) + 1;
return acc;
}, {}),
assets: CTM_DATA.assets.map(a => {
const notes = store.notes[a.id] || [];
return {
id: a.id,
name: a.name,
group: a.g,
groupName: CTM_DATA.groups.find(g => g.id === a.g)?.name || a.g,
status: store.statuses[a.id],
statusLabel: STATUS[store.statuses[a.id]].label,
ownership: a.own,
ownershipLabel: OWN[a.own].label,
confidence: a.conf,
purpose: a.purpose,
reference: a.ref || null,
candidate: store.replacements[a.id]?.name || a.cand || null,
replacementStaged: store.replacements[a.id] || null,
notes,
annotations: notes.filter(n => n.annotation).map(n => ({ noteId: n.id, text: n.text, targets: n.targets || [], ...n.annotation })),
forgeBrief: buildForgeBrief(a, store),
implementationReady: ["approved", "active"].includes(store.statuses[a.id])
};
})
};
}
function buildAf1Submission(packet) {
const submittedAt = new Date().toISOString();
const safeTime = submittedAt.replace(/[-:.TZ]/g, "").slice(0, 14);
const approvedAssets = packet.assets.filter(a => a.status === "approved" || a.status === "active");
return {
schema: "af1.review_submission.v1",
submissionId: `${packet.mock.code.toLowerCase()}-${safeTime}`,
submittedAt,
source: "AF1 browser submit",
instructions: "Use this frozen packet as the review truth. Browser-local Hot/Not state is not enough without this submission or explicit chat approval.",
mock: packet.mock,
summary: packet.summary,
approvedAssetIds: approvedAssets.map(a => a.id),
implementationReadyAssetIds: packet.assets.filter(a => a.implementationReady).map(a => a.id),
assets: packet.assets
};
}
function buildAf1AssetWorkPacket(a, store) {
const queuedAt = new Date().toISOString();
const notes = store.notes[a.id] || [];
const annotations = notes.filter(n => n.annotation).map(n => ({ noteId: n.id, text: n.text, targets: n.targets || [], ...n.annotation }));
return {
schema: "af1.asset_work_packet.v1",
workId: `${CTM_DATA.mock.code.toLowerCase()}-${a.id}-${queuedAt.replace(/[-:.TZ]/g, "").slice(0, 14)}`,
queuedAt,
source: "AF1 Send notes to Codex",
instructions: "Use this focused packet as the active asset job. Build or apply only this asset unless the user explicitly asks for a wider batch.",
mock: CTM_DATA.mock,
asset: {
id: a.id,
name: a.name,
group: a.g,
groupName: CTM_DATA.groups.find(g => g.id === a.g)?.name || a.g,
status: store.statuses[a.id],
statusLabel: STATUS[store.statuses[a.id]].label,
ownership: a.own,
ownershipLabel: OWN[a.own].label,
confidence: a.conf,
purpose: a.purpose,
reference: a.ref || null,
candidate: store.replacements[a.id]?.name || a.cand || null,
replacementStaged: store.replacements[a.id] || null,
notes,
annotations,
forgeBrief: buildForgeBrief(a, store)
},
uiContract: {
leftPane: "The open modal left pane is the visible build feed for this asset.",
domHook: "window.AF1_ACTIVE_WORK_PACKET and [data-af1-active-work-json]",
scope: "one asset inside the current page/component lab"
}
};
}
function af1Markdown(packet) {
return [
`# AF1 Export — ${packet.mock.title}`,
``,
`Exported: ${packet.exportedAt}`,
`Code: ${packet.mock.code}`,
``,
`## Summary`,
...Object.entries(packet.summary).map(([k, v]) => `- ${k}: ${v}`),
``,
`## Assets`,
...packet.assets.flatMap(a => [
``,
`### ${a.id.toUpperCase()} — ${a.name}`,
`- Status: ${a.statusLabel}`,
`- Group: ${a.groupName}`,
`- Ownership: ${a.ownershipLabel}`,
`- Reference: ${a.reference || "none"}`,
`- Candidate: ${a.candidate || "none"}`,
`- Implementation-ready: ${a.implementationReady ? "yes" : "no"}`,
`- Purpose: ${a.purpose}`,
``,
`#### Notes`,
...(a.notes.length ? a.notes.map(n => `- ${n.text}${n.annotation ? ` [${n.annotation.shape} ${n.annotation.view} ${Math.round(n.annotation.box.x)},${Math.round(n.annotation.box.y)},${Math.round(n.annotation.box.w)},${Math.round(n.annotation.box.h)}]` : ""}`) : [`- none`]),
``,
`#### Forge Brief`,
`\`\`\``,
a.forgeBrief,
`\`\`\``
])
].join("\n");
}
function downloadText(name, text, type) {
const blob = new Blob([text], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = name; a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function ExportControls({ store }) {
const [copied, setCopied] = uS(null);
const [submission, setSubmission] = uS(loadSubmission);
const [resumeMsg, setResumeMsg] = uS("");
const importRef = uR();
const packet = buildAf1Export(store);
const json = JSON.stringify(packet, null, 2);
const md = af1Markdown(packet);
const submissionJson = submission ? JSON.stringify(submission, null, 2) : "";
window.AF1_EXPORT = packet;
window.AF1_EXPORT_MARKDOWN = md;
window.AF1_LAST_SUBMISSION = submission;
const copy = async (kind, text) => {
try { await navigator.clipboard.writeText(text); setCopied(kind); } catch (e) { setCopied("blocked"); }
};
const submit = async () => {
const frozen = buildAf1Submission(packet);
const frozenJson = JSON.stringify(frozen, null, 2);
localStorage.setItem(SUBMISSION_KEY, frozenJson);
setSubmission(frozen);
window.AF1_LAST_SUBMISSION = frozen;
try { await navigator.clipboard.writeText(frozenJson); setCopied("submitted"); } catch (e) { setCopied("submit-blocked"); }
downloadText(`${frozen.submissionId}.json`, frozenJson, "application/json");
};
const resumeFromFile = (file) => {
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const imported = JSON.parse(String(reader.result || ""));
const result = store.applySubmission(imported);
localStorage.setItem(SUBMISSION_KEY, JSON.stringify(imported));
setSubmission(imported);
window.AF1_LAST_SUBMISSION = imported;
setResumeMsg(`Resumed ${result.statusCount} statuses · ${result.noteCount} notes`);
setCopied("resumed");
} catch (e) {
setResumeMsg(e.message || "Resume failed");
setCopied("resume-failed");
}
};
reader.readAsText(file);
};
return (
<div className="export-card">
<div className="mono-lab">EXPORT HANDOFF</div>
<textarea data-af1-export-json="" readOnly value={json} style={{ display: "none" }} />
<textarea data-af1-export-markdown="" readOnly value={md} style={{ display: "none" }} />
<textarea data-af1-submission-json="" readOnly value={submissionJson} style={{ display: "none" }} />
<div className="export-actions">
<button className="blue-btn submit-btn" onClick={submit}>{copied === "submitted" ? "Submitted + copied" : "Submit for Codex"}</button>
<input ref={importRef} type="file" accept="application/json,.json" style={{ display: "none" }} onChange={(e) => { resumeFromFile(e.target.files[0]); e.target.value = ""; }} />
<button className="ghost-btn" onClick={() => importRef.current.click()}>{copied === "resumed" ? "Resumed" : "Resume from JSON"}</button>
<button className="ghost-btn" onClick={() => copy("json", json)}>{copied === "json" ? "Copied JSON" : "Copy JSON"}</button>
<button className="ghost-btn" onClick={() => copy("md", md)}>{copied === "md" ? "Copied MD" : "Copy MD"}</button>
<button className="blue-btn" onClick={() => downloadText(`${CTM_DATA.mock.code.toLowerCase()}-af1-export.json`, json, "application/json")}>Download JSON</button>
<button className="blue-btn" onClick={() => downloadText(`${CTM_DATA.mock.code.toLowerCase()}-af1-brief.md`, md, "text/markdown")}>Download MD</button>
</div>
{submission ? (
<p className="submit-proof">Last submission: <code>{submission.submissionId}</code> · {(submission.approvedAssetIds || []).length} Hot assets. {resumeMsg && <span>{resumeMsg}. </span>}Codex can read <code>window.AF1_LAST_SUBMISSION</code> or <code>[data-af1-submission-json]</code>.</p>
) : (
<p>Submit freezes the current Hot/Not state into a packet. Resume restores a previous packet into this browser for the same page/component batch.</p>
)}
</div>
);
}
function AssetCard({ a, store, onOpen, onForge }) {
const notes = store.notes[a.id] || [];
const st = store.statuses[a.id];
const ready = st === "approved" || st === "active";
return (
<div className="card" onClick={onOpen}>
<div className="card-pair"><Art a={a} kind="o" /><Art a={a} kind="c" repl={store.replacements[a.id]} /></div>
<div className="card-body">
<div className="card-title"><h3>{a.name}</h3><span className="mono card-id">{a.id.toUpperCase()}</span></div>
<p className="card-purpose">{a.purpose}</p>
<div className="card-chips"><StatusChip s={st} /><OwnChip o={a.own} /></div>
<div className="card-meta">
<span className="mono-lab">◆ {notes.length} NOTE{notes.length === 1 ? "" : "S"}</span>
<ConfBar v={a.conf} />
</div>
<div className="card-foot">
<span className={"ready-flag" + (ready ? " ok" : "")}>{ready ? "Implementation-ready" : "⌀ Not build-cleared"}</span>
<span className="card-actions">
<button className="review-cta" onClick={(e) => { e.stopPropagation(); onOpen(); }}>Review →</button>
<button className="forge-cta" onClick={(e) => { e.stopPropagation(); onForge(); }}>Forge candidate</button>
</span>
</div>
</div>
</div>
);
}
function GroupSection({ g, assets, store, onOpen, onForge }) {
const done = assets.filter(a => ["approved", "active"].includes(store.statuses[a.id])).length;
return (
<section className="group">
<div className="group-head">
<div><h2>{g.name}</h2><span className="group-desc">{g.desc}</span></div>
<div className="group-count mono-lab">{assets.length ? `${done} / ${assets.length} CLEARED` : "NO CHOPS"}</div>
</div>
{assets.length === 0 ? (
<div className="group-empty">
<span className="mono-lab">AWAITING EXTRACTION</span>
<p>No candidate assets chopped from this layer yet. Once the mock crop is dissected, pairs land here for review.</p>
</div>
) : (
<div className="card-grid">{assets.map(a => <AssetCard key={a.id} a={a} store={store} onOpen={() => onOpen(a.id)} onForge={() => onForge(a.id)} />)}</div>
)}
</section>
);
}
function Sitrep({ store, total, onPresent }) {
const counts = { approved: 0, active: 0, pending: 0, rejected: 0, revision: 0, superseded: 0 };
CTM_DATA.assets.forEach(a => counts[store.statuses[a.id]]++);
const ready = counts.approved + counts.active;
return (
<div className="sitrep">
<div className="sitrep-l">
<div className="mono-lab">AF1 / ASSET FORGE 1 · CHOP THE MOCK · {CTM_DATA.mock.code}</div>
<h1>{CTM_DATA.mock.title}</h1>
<p>{CTM_DATA.mock.note}</p>
<div className="mission-card">
<div className="mono-lab">END GOAL</div>
<ol>
<li>Turn each chopped mock piece into a separate reusable source asset.</li>
<li>Mark exact issues with circle/box notes before any rebuild.</li>
<li>Forge replacement candidates one asset at a time.</li>
<li>Only implement assets marked Hot into the Shootout landing page.</li>
</ol>
</div>
</div>
<div className="sitrep-r">
<div className="gauge">
<div className="gauge-num">{ready}<span>/{total}</span></div>
<div className="mono-lab">IMPLEMENTATION-READY</div>
<div className="gauge-bar"><span style={{ width: (ready / total * 100) + "%" }}></span></div>
</div>
<div className="sitrep-pills">
{Object.entries(counts).filter(([, v]) => v > 0).map(([k, v]) => <span key={k} className="pill"><i style={{ background: STATUS[k].c }}></i>{v} {STATUS[k].label.toLowerCase()}</span>)}
</div>
<button className="blue-btn present-btn" onClick={onPresent}>▸ Hot or Not review</button>
<ExportControls store={store} />
</div>
</div>
);
}
function App() {
const store = useStore();
const [filter, setFilter] = uS("all");
const [openId, setOpenId] = uS(null);
const [forgeId, setForgeId] = uS(null);
const [mode, setMode] = uS("modal");
const list = uM(() => filter === "all" ? CTM_DATA.assets : CTM_DATA.assets.filter(a => store.statuses[a.id] === filter), [filter, store.statuses]);
const idx = list.findIndex(a => a.id === openId);
const setIdx = (i) => setOpenId(list[i].id);
const open = openId != null && idx >= 0;
return (
<div className="shell">
<header className="topbar">
<div className="topbar-l"><span className="wordmark">AF1</span><span className="mono-lab crumb">ASSET FORGE 1 / DESIGN + ENG INSTINCT</span></div>
<div className="mono-lab clock">{CTM_DATA.assets.length} PAIRS · 12 LAYERS</div>
</header>
<main className="page">
<Sitrep store={store} total={CTM_DATA.assets.length} onPresent={() => { setOpenId((list[0] || CTM_DATA.assets[0]).id); setMode("present"); }} />
<div className="filter-row">
<span className="mono-lab">STATUS</span>
{FILTERS.map(([k, l]) => <button key={k} className={"tg" + (filter === k ? " on" : "")} onClick={() => setFilter(k)}>{l}</button>)}
</div>
{filter !== "all" && list.length === 0 && (
<div className="group-empty page-empty"><span className="mono-lab">NOTHING HERE</span><p>No assets currently marked “{STATUS[filter].label}”. Clear the filter to see the full board.</p></div>
)}
{CTM_DATA.groups.map(g => {
const ga = list.filter(a => a.g === g.id);
if (filter !== "all" && ga.length === 0) return null;
return <GroupSection key={g.id} g={g} assets={ga} store={store} onOpen={(id) => { setOpenId(id); setMode("modal"); }} onForge={(id) => setForgeId(id)} />;
})}
<footer className="foot mono-lab">AF1 DOCTRINE — NO ASSET IS IMPLEMENTATION-READY UNTIL MARKED HOT. CANDIDATES STAY SEPARATE AND REVIEWABLE; NOTHING IS FLATTENED INTO THE MOCK.</footer>
</main>
{open && mode === "modal" && <ReviewModal list={list} idx={idx} setIdx={setIdx} close={() => setOpenId(null)} store={store} present={() => setMode("present")} onForge={(id) => setForgeId(id)} />}
{open && mode === "present" && <PresentMode list={list} idx={idx} setIdx={setIdx} close={() => { setMode("modal"); setOpenId(null); }} store={store} />}
{forgeId && <ForgeBriefSheet a={CTM_DATA.assets.find(a => a.id === forgeId)} store={store} close={() => setForgeId(null)} />}
</div>
);
}

function ForgeBriefSheet({ a, store, close }) {
const [copied, setCopied] = uS(false);
const [queued, setQueued] = uS(false);
if (!a) return null;
const brief = buildForgeBrief(a, store);
const copyBrief = async () => {
try { await navigator.clipboard.writeText(brief); setCopied(true); } catch (e) { setCopied(false); }
};
const queue = () => {
store.addNote(a.id, { text: "AF1 forge brief queued. Build as an individual reusable candidate, not a flattened mock crop.", targets: ["Shape", "Scale", "Vibe"] });
store.setStatus(a.id, "revision");
setQueued(true);
};
return (
<div className="modal-veil forge-veil">
<div className="forge-sheet">
<div className="modal-head">
<div>
<div className="mono-lab">AF1 FORGE ACTION · {a.id.toUpperCase()}</div>
<h2>{a.name}</h2>
</div>
<button className="icon-btn" onClick={close}>✕</button>
</div>
<div className="forge-body">
<div className="forge-preview"><Art a={a} kind="o" /><Art a={a} kind="c" repl={store.replacements[a.id]} /></div>
<div className="forge-panel">
<div className="mono-lab">WHAT HAPPENS NEXT</div>
<p>This creates a focused asset job. The candidate stays separate, reviewable, and transferable. Nothing goes into the product until you mark it Hot.</p>
<pre>{brief}</pre>
<div className="forge-actions">
<button className="blue-btn" onClick={copyBrief}>{copied ? "Copied brief" : "Copy build brief"}</button>
<button className="ghost-btn" onClick={queue}>{queued ? "Queued in notes" : "Queue forge note"}</button>
<UploadReplace a={a} store={store} />
</div>
</div>
</div>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
