<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Art Book Builder Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
--bg: #f5f2ec;
--text: #1a1a1a;
--accent: #a38b4d;
--accent-dark: #4a2f1b;
--border: #c7c2ba;
--muted: #8a8478;
--danger: #b14a3c;
--radius: 6px;
}
* {
box-sizing: border-box;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
}
header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background: #fdfbf8;
position: sticky;
top: 0;
z-index: 10;
}
header h1 {
margin: 0;
font-size: 20px;
letter-spacing: 0.04em;
}
header p {
margin: 4px 0 0;
font-size: 12px;
color: var(--muted);
}
.app {
display: flex;
flex-direction: row;
min-height: calc(100vh - 60px);
}
@media (max-width: 800px) {
.app {
flex-direction: column;
}
}
.sidebar {
width: 260px;
padding: 12px;
border-right: 1px solid var(--border);
background: #faf7f2;
}
@media (max-width: 800px) {
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--border);
}
}
.main {
flex: 1;
padding: 12px;
}
h2 {
margin: 8px 0;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--accent-dark);
}
.small-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--muted);
margin-bottom: 4px;
}
button, select, input, textarea {
font-size: 13px;
}
button {
border-radius: var(--radius);
border: 1px solid var(--accent);
background: var(--accent);
color: #fdfbf8;
padding: 6px 10px;
cursor: pointer;
}
button.secondary {
background: transparent;
color: var(--accent-dark);
border-color: var(--border);
}
button.danger {
border-color: var(--danger);
background: transparent;
color: var(--danger);
}
button:disabled {
opacity: 0.6;
cursor: default;
}
input[type="text"], input[type="number"], textarea, select {
width: 100%;
border-radius: var(--radius);
border: 1px solid var(--border);
padding: 5px 8px;
background: #fdfbf8;
color: var(--text);
}
textarea {
resize: vertical;
min-height: 60px;
}
.section-list {
max-height: 350px;
overflow-y: auto;
padding-right: 4px;
}
.section-item {
border-radius: var(--radius);
border: 1px solid var(--border);
padding: 6px 8px;
margin-bottom: 6px;
background: white;
cursor: pointer;
}
.section-item.active {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(163, 139, 77, 0.2);
}
.section-title {
font-size: 13px;
font-weight: 600;
}
.section-meta {
font-size: 11px;
color: var(--muted);
margin-top: 2px;
}
.pill {
display: inline-block;
padding: 1px 6px;
border-radius: 999px;
font-size: 10px;
border: 1px solid var(--border);
margin-right: 4px;
}
.pill.status-done {
border-color: #4c8c52;
color: #2d6a34;
}
.pill.status-inprogress {
border-color: #d08b2e;
color: #a46a1b;
}
.pill.status-notstarted {
border-color: var(--border);
color: var(--muted);
}
.toolbar {
display: flex;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
align-items: center;
}
.toolbar span.hint {
font-size: 11px;
color: var(--muted);
}
.card {
border-radius: var(--radius);
border: 1px solid var(--border);
padding: 8px;
background: #fdfbf8;
margin-bottom: 10px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
}
.card-title {
font-size: 13px;
font-weight: 600;
}
.field {
margin-bottom: 6px;
}
.field label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
display: block;
margin-bottom: 3px;
}
.columns {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.col-2 {
flex: 1 1 200px;
}
.badge {
font-size: 10px;
padding: 1px 6px;
border-radius: 999px;
background: #eee4d7;
color: var(--accent-dark);
}
.page-list {
margin-top: 8px;
}
.page-item {
border-radius: var(--radius);
border: 1px solid var(--border);
padding: 6px;
background: white;
margin-bottom: 6px;
}
.page-top {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}
.page-meta {
font-size: 11px;
color: var(--muted);
margin-top: 2px;
}
.status-select {
width: auto;
min-width: 110px;
}
.tag {
display: inline-block;
font-size: 10px;
color: var(--muted);
border-radius: 999px;
border: 1px solid var(--border);
padding: 1px 6px;
margin-right: 4px;
margin-top: 2px;
}
.summary {
font-size: 11px;
color: var(--muted);
margin-top: 4px;
}
.divider {
border-top: 1px dashed var(--border);
margin: 8px 0;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.topbar-left {
display: flex;
flex-direction: column;
gap: 2px;
}
.topbar-left span {
font-size: 11px;
color: var(--muted);
}
.chip-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.chip {
border-radius: 999px;
border: 1px solid var(--border);
padding: 2px 8px;
font-size: 11px;
background: white;
cursor: pointer;
}
.chip.active {
border-color: var(--accent);
background: #f3ece0;
}
</style>
</head>
<body>
<header>
<h1>Art Book Builder</h1>
<p>Plan sections, assign pages, and track your mythic art book from concept to print.</p>
</header>
<div class="app">
<aside class="sidebar">
<div class="toolbar">
<button id="addSectionBtn">+ New Section</button>
<button class="secondary" id="resetDataBtn">Reset All</button>
</div>
<div class="small-label">Sections</div>
<div id="sectionList" class="section-list"></div>
</aside>
<main class="main">
<div class="topbar">
<div class="topbar-left">
<strong style="font-size:13px;">Section Detail & Page Planner</strong>
<span id="sectionHint">Select a section on the left or create a new one.</span>
</div>
<div>
<button class="secondary" id="exportJsonBtn">Export JSON</button>
</div>
</div>
<div id="sectionDetail"></div>
</main>
</div>
<script>
// ---- Data Model ----
const STORAGE_KEY = "artbook_dashboard_data_v1";
let state = {
sections: [],
selectedSectionId: null
};
function loadState() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
state = JSON.parse(saved);
}
} catch (e) {
console.warn("Could not load saved state", e);
}
render();
}
function saveState() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function createSection() {
const id = "sec_" + Date.now();
const section = {
id,
title: "New Section",
subtitle: "",
type: "content", // content / frontmatter / backmatter
order: state.sections.length + 1,
notes: "",
status: "notstarted", // notstarted / inprogress / done
pages: []
};
state.sections.push(section);
state.selectedSectionId = id;
saveState();
render();
}
function deleteSection(id) {
if (!confirm("Delete this section and all its pages?")) return;
state.sections = state.sections.filter(s => s.id !== id);
if (state.selectedSectionId === id) {
state.selectedSectionId = state.sections[0]?.id || null;
}
saveState();
render();
}
function updateSection(id, updates) {
const idx = state.sections.findIndex(s => s.id === id);
if (idx === -1) return;
state.sections[idx] = { ...state.sections[idx], ...updates };
saveState();
render();
}
function addPageToSection(sectionId, presetType) {
const section = state.sections.find(s => s.id === sectionId);
if (!section) return;
const page = {
id: "pg_" + Date.now() + "_" + Math.floor(Math.random() * 1000),
pageNumber: "", // you can assign later
layoutType: presetType, // "full-art", "diptych", etc.
title: "",
contentRef: "",
notes: "",
status: "notstarted"
};
section.pages.push(page);
saveState();
render();
}
function updatePage(sectionId, pageId, updates) {
const section = state.sections.find(s => s.id === sectionId);
if (!section) return;
const idx = section.pages.findIndex(p => p.id === pageId);
if (idx === -1) return;
section.pages[idx] = { ...section.pages[idx], ...updates };
saveState();
render();
}
function deletePage(sectionId, pageId) {
const section = state.sections.find(s => s.id === sectionId);
if (!section) return;
section.pages = section.pages.filter(p => p.id !== pageId);
saveState();
render();
}
function resetAll() {
if (!confirm("This will erase all sections and pages. Continue?")) return;
state = { sections: [], selectedSectionId: null };
saveState();
render();
}
function exportJson() {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(state, null, 2));
const a = document.createElement("a");
a.setAttribute("href", dataStr);
a.setAttribute("download", "artbook-dashboard-export.json");
document.body.appendChild(a);
a.click();
a.remove();
}
// ---- Rendering ----
function render() {
renderSectionList();
renderSectionDetail();
}
function renderSectionList() {
const container = document.getElementById("sectionList");
container.innerHTML = "";
const sections = [...state.sections].sort((a, b) => a.order - b.order);
if (!sections.length) {
container.innerHTML = `<div style="font-size:12px;color:var(--muted);margin-top:6px;">
No sections yet. Click <strong>+ New Section</strong> to start planning your book.
</div>`;
return;
}
sections.forEach(sec => {
const div = document.createElement("div");
div.className = "section-item" + (sec.id === state.selectedSectionId ? " active" : "");
div.onclick = () => {
state.selectedSectionId = sec.id;
saveState();
render();
};
const pagesDone = sec.pages.filter(p => p.status === "done").length;
const pagesTotal = sec.pages.length;
const statusPillClass =
sec.status === "done" ? "status-done" :
sec.status === "inprogress" ? "status-inprogress" :
"status-notstarted";
div.innerHTML = `
<div class="section-title">${sec.title || "Untitled Section"}</div>
<div class="section-meta">
<span class="pill ${statusPillClass}">${sec.status === "done" ? "Done" : sec.status === "inprogress" ? "In Progress" : "Planning"}</span>
<span>${pagesTotal} page${pagesTotal === 1 ? "" : "s"}</span>
${pagesTotal ? ` • ${pagesDone}/${pagesTotal} ready` : ""}
</div>
`;
container.appendChild(div);
});
}
function renderSectionDetail() {
const container = document.getElementById("sectionDetail");
const hint = document.getElementById("sectionHint");
if (!state.selectedSectionId || !state.sections.length) {
container.innerHTML = "";
hint.textContent = "Select a section on the left or create a new one.";
return;
}
const section = state.sections.find(s => s.id === state.selectedSectionId);
if (!section) {
container.innerHTML = "";
hint.textContent = "Select a section on the left or create a new one.";
return;
}
hint.textContent = `You’re editing: ${section.title || "Untitled Section"}`;
const pages = section.pages;
const pagesDone = pages.filter(p => p.status === "done").length;
const pagesInProgress = pages.filter(p => p.status === "inprogress").length;
container.innerHTML = `
<div class="card">
<div class="card-header">
<div class="card-title">Section Info</div>
<button class="danger" id="deleteSectionBtn">Delete Section</button>
</div>
<div class="columns">
<div class="col-2">
<div class="field">
<label>Section Title</label>
<input type="text" id="secTitleInput" value="${section.title || ""}" placeholder="e.g., Thresholds, Portraits of Becoming" />
</div>
<div class="field">
<label>Subtitle / Invocation Phrase</label>
<input type="text" id="secSubtitleInput" value="${section.subtitle || ""}" placeholder="Short phrase that sets the tone" />
</div>
</div>
<div class="col-2">
<div class="field">
<label>Section Type</label>
<select id="secTypeSelect">
<option value="frontmatter" ${section.type === "frontmatter" ? "selected" : ""}>Front Matter</option>
<option value="content" ${section.type === "content" ? "selected" : ""}>Content</option>
<option value="backmatter" ${section.type === "backmatter" ? "selected" : ""}>Back Matter</option>
</select>
</div>
<div class="field">
<label>Status</label>
<select id="secStatusSelect">
<option value="notstarted" ${section.status === "notstarted" ? "selected" : ""}>Planning</option>
<option value="inprogress" ${section.status === "inprogress" ? "selected" : ""}>In Progress</option>
<option value="done" ${section.status === "done" ? "selected" : ""}>Complete</option>
</select>
</div>
<div class="field">
<label>Internal Notes</label>
<textarea id="secNotesInput" placeholder="Concept, mood, ritual frame for this section...">${section.notes || ""}</textarea>
</div>
</div>
</div>
<div class="summary">
Pages: ${pages.length} • Ready: ${pagesDone} • In progress: ${pagesInProgress}
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title">Add Pages to this Section</div>
</div>
<div class="chip-group" id="presetChipGroup">
<div class="chip" data-layout="full-art">+ Full-page artwork</div>
<div class="chip" data-layout="single-image">+ Single image ritual page</div>
<div class="chip" data-layout="diptych">+ Diptych spread</div>
<div class="chip" data-layout="triptych">+ Triptych grid</div>
<div class="chip" data-layout="text-only">+ Text / Written echoes</div>
<div class="chip" data-layout="process">+ Process / Ritual notes</div>
<div class="chip" data-layout="custom">+ Custom layout</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title">Pages in this Section</div>
</div>
<div class="page-list" id="pageList"></div>
</div>
`;
// Attach handlers for section info
document.getElementById("deleteSectionBtn").onclick = () => deleteSection(section.id);
document.getElementById("secTitleInput").onchange = (e) => updateSection(section.id, { title: e.target.value });
document.getElementById("secSubtitleInput").onchange = (e) => updateSection(section.id, { subtitle: e.target.value });
document.getElementById("secTypeSelect").onchange = (e) => updateSection(section.id, { type: e.target.value });
document.getElementById("secStatusSelect").onchange = (e) => updateSection(section.id, { status: e.target.value });
document.getElementById("secNotesInput").onchange = (e) => updateSection(section.id, { notes: e.target.value });
// Chip handlers
const chipGroup = document.getElementById("presetChipGroup");
chipGroup.querySelectorAll(".chip").forEach(chip => {
chip.onclick = () => {
const layout = chip.getAttribute("data-layout");
addPageToSection(section.id, layout);
};
});
// Render pages
const pageList = document.getElementById("pageList");
if (!pages.length) {
pageList.innerHTML = `<div style="font-size:12px;color:var(--muted);">
No pages yet. Use the layout chips above to add your first page.
</div>`;
} else {
pages.forEach(page => {
const div = document.createElement("div");
div.className = "page-item";
const layoutLabel = layoutTypeToLabel(page.layoutType);
const statusLabel =
page.status === "done" ? "Ready" :
page.status === "inprogress" ? "In progress" :
"Planning";
div.innerHTML = `
<div class="page-top">
<div>
<div style="font-size:13px;font-weight:600;">
${page.title || "(Untitled)"}
${page.pageNumber ? `<span class="badge">p. ${page.pageNumber}</span>` : ""}
</div>
<div class="page-meta">
<span class="tag">${layoutLabel}</span>
<span class="tag">${statusLabel}</span>
${page.contentRef ? `<span class="tag">Ref: ${escapeHtml(page.contentRef)}</span>` : ""}
</div>
</div>
<div>
<select class="status-select" data-pageid="${page.id}">
<option value="notstarted" ${page.status === "notstarted" ? "selected" : ""}>Planning</option>
<option value="inprogress" ${page.status === "inprogress" ? "selected" : ""}>In Progress</option>
<option value="done" ${page.status === "done" ? "selected" : ""}>Ready</option>
</select>
<button class="danger" data-delpageid="${page.id}" style="margin-left:4px;">×</button>
</div>
</div>
<div class="divider"></div>
<div class="columns">
<div class="col-2">
<div class="field">
<label>Page Number (in book)</label>
<input type="number" min="1" value="${page.pageNumber || ""}" data-field="pageNumber" data-pageid="${page.id}" />
</div>
<div class="field">
<label>Page Title / Label</label>
<input type="text" value="${page.title || ""}" data-field="title" data-pageid="${page.id}" placeholder="e.g., Threshold Figure I" />
</div>
<div class="field">
<label>Content Reference</label>
<input type="text" value="${page.contentRef || ""}" data-field="contentRef" data-pageid="${page.id}" placeholder="e.g., Artwork file name, text piece ID" />
</div>
</div>
<div class="col-2">
<div class="field">
<label>Notes / Concept</label>
<textarea data-field="notes" data-pageid="${page.id}" placeholder="What happens on this page? Ritual, symbolism, layout notes...">${page.notes || ""}</textarea>
</div>
</div>
</div>
`;
pageList.appendChild(div);
});
// Attach listeners for page inputs
pageList.querySelectorAll("input[data-pageid], textarea[data-pageid]").forEach(el => {
el.onchange = (e) => {
const pageId = el.getAttribute("data-pageid");
const field = el.getAttribute("data-field");
let value = el.value;
if (field === "pageNumber" && value !== "") {
value = parseInt(value, 10);
}
updatePage(section.id, pageId, { [field]: value });
};
});
// Status dropdowns
pageList.querySelectorAll("select.status-select").forEach(sel => {
sel.onchange = (e) => {
const pageId = sel.getAttribute("data-pageid");
updatePage(section.id, pageId, { status: sel.value });
};
});
// Delete buttons
pageList.querySelectorAll("button[data-delpageid]").forEach(btn => {
btn.onclick = () => {
const pageId = btn.getAttribute("data-delpageid");
deletePage(section.id, pageId);
};
});
}
}
function layoutTypeToLabel(type) {
switch (type) {
case "full-art": return "Full-page artwork";
case "single-image": return "Single image ritual page";
case "diptych": return "Diptych spread";
case "triptych": return "Triptych grid";
case "text-only": return "Text / Written echoes";
case "process": return "Process / Ritual notes";
case "custom": return "Custom layout";
default: return "Layout";
}
}
function escapeHtml(str) {
if (!str) return "";
return str.replace(/[&<>"']/g, function(m) {
return {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
}[m];
});
}
// ---- Event bindings ----
document.getElementById("addSectionBtn").onclick = createSection;
document.getElementById("resetDataBtn").onclick = resetAll;
document.getElementById("exportJsonBtn").onclick = exportJson;
loadState();
</script>
</body>
</html>