This commit is contained in:
@@ -1556,13 +1556,27 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
background: rgba(251, 241, 220, 0.06);
|
||||
}
|
||||
.arc-card.active, .chapter-card.active { border-color: var(--blue); background: rgba(117, 169, 189, 0.16); }
|
||||
.moment-card.active { border-color: var(--gold); background: rgba(211, 166, 77, 0.12); }
|
||||
.moment-card {
|
||||
border: 1px solid rgba(232, 202, 139, 0.14);
|
||||
color: #efd9b2;
|
||||
font-size: 13px;
|
||||
}
|
||||
.moment-card strong, .arc-card strong, .chapter-card strong { overflow-wrap: anywhere; }
|
||||
.arc-toolbar { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px; }
|
||||
.arc-toolbar { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
|
||||
.workspace-switcher { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin: 14px 0; }
|
||||
.workspace-switcher button.active { border-color: var(--gold); background: rgba(211, 166, 77, 0.22); }
|
||||
body:not(.arc-workspace) #arcStudioPanel { display: none; }
|
||||
body.arc-workspace .story-workspace-only,
|
||||
body.arc-workspace .drawer > :not(#arcStudioPanel) { display: none !important; }
|
||||
body.arc-workspace #arcStudioPanel { display: block; margin-top: 0; }
|
||||
.arc-node, .chapter-node, .moment-node { cursor: pointer; }
|
||||
.arc-node circle, .chapter-node circle, .moment-node circle { transition: stroke-width 160ms ease, opacity 160ms ease; }
|
||||
.arc-node.selected circle, .chapter-node.selected circle, .moment-node.selected circle { stroke: #fff3c7; stroke-width: 4; }
|
||||
.arc-branch { fill: none; stroke: rgba(117, 169, 189, 0.34); stroke-width: 2; }
|
||||
.arc-moment-path { fill: none; stroke: rgba(232, 202, 139, 0.26); stroke-width: 1.4; }
|
||||
.arc-map-label { fill: #f7ead3; font-size: 20px; font-weight: 800; paint-order: stroke; stroke: rgba(14, 11, 8, 0.9); stroke-width: 4px; }
|
||||
.arc-map-meta { fill: rgba(248, 234, 209, 0.68); font-size: 13px; paint-order: stroke; stroke: rgba(14, 11, 8, 0.9); stroke-width: 3px; }
|
||||
.story-cover {
|
||||
border: 1px solid rgba(232, 202, 139, 0.18);
|
||||
border-radius: 8px;
|
||||
@@ -1820,40 +1834,44 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<div class="observatory">
|
||||
<aside class="left">
|
||||
<h1>Memory Observatory</h1>
|
||||
<div class="subtle">A constellation map for the hidden soul of the website.</div>
|
||||
<div id="status" class="status">aphy is dimming the room lights.</div>
|
||||
<div class="modebar" id="modebar">
|
||||
<div class="subtle">A constellation map for the hidden soul of the website.</div>
|
||||
<div id="status" class="status">aphy is dimming the room lights.</div>
|
||||
<div class="workspace-switcher" id="workspaceSwitcher">
|
||||
<button type="button" data-workspace="stories" class="active">Stories & memories</button>
|
||||
<button type="button" data-workspace="arcs">Life arcs</button>
|
||||
</div>
|
||||
<div class="modebar story-workspace-only" id="modebar">
|
||||
<button data-mode="graph" class="active">Story constellations</button>
|
||||
<button data-mode="hidden">Hidden routes</button>
|
||||
<button data-mode="character">Character stories</button>
|
||||
</div>
|
||||
<div class="filters">
|
||||
<div class="filters story-workspace-only">
|
||||
<input id="search" type="search" placeholder="Search memories and stories" />
|
||||
<select id="layerFilter"></select>
|
||||
<select id="characterFilter"></select>
|
||||
<select id="toneFilter"></select>
|
||||
<select id="rarityFilter" class="hidden"></select>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="actions story-workspace-only">
|
||||
<button id="newBtn" type="button">New memory</button>
|
||||
<button id="newStoryBtn" type="button">New story</button>
|
||||
<button id="saveBtn" class="primary" type="button">Save</button>
|
||||
</div>
|
||||
<div class="actions" style="margin-top:8px">
|
||||
<div class="actions story-workspace-only" style="margin-top:8px">
|
||||
<button id="softResetBtn" type="button">Clear Focus</button>
|
||||
<button id="fullResetBtn" type="button">Return to the Whole Sky</button>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<div class="guide-card story-workspace-only">
|
||||
<h2>How to Read the Observatory</h2>
|
||||
<p class="subtle">This is a story constellation library. Stories are emotional routes, and memories are lights arranged along those routes.</p>
|
||||
<p class="subtle"><strong>aphy:</strong> zoomed out, I show stories. zoom in, I show the memories inside them. the sky stays quieter this way.</p>
|
||||
<p class="subtle"><strong>sensei chi:</strong> depth is not importance. Depth is how quietly a thing asks to be approached.</p>
|
||||
</div>
|
||||
<div id="dragGuide" class="guide-card hidden">
|
||||
<div id="dragGuide" class="guide-card story-workspace-only hidden">
|
||||
<h2>Building Stories</h2>
|
||||
<p class="subtle"><strong>aphy:</strong> drag a memory into a story or reorder the route in the builder. no more tangled relationship engineering.</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<div class="guide-card story-workspace-only">
|
||||
<h2>Guided Exploration</h2>
|
||||
<div class="tour-list">
|
||||
<button data-tour="lima" type="button">show hidden lima memories</button>
|
||||
@@ -1862,19 +1880,19 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<button data-tour="hidden">show hidden routes</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<div class="guide-card story-workspace-only">
|
||||
<h2>Story Covers</h2>
|
||||
<div id="storyAtlas" class="story-list"></div>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<div class="guide-card story-workspace-only">
|
||||
<h2>Character Territories</h2>
|
||||
<div id="characterAtlas" class="stack"></div>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<div class="guide-card story-workspace-only">
|
||||
<h2>Integrity</h2>
|
||||
<div id="integrityStatus" class="subtle">checking character territories.</div>
|
||||
</div>
|
||||
<div class="layer-key" id="layerKey"></div>
|
||||
<div class="layer-key story-workspace-only" id="layerKey"></div>
|
||||
</aside>
|
||||
|
||||
<main class="map">
|
||||
@@ -1889,12 +1907,12 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<button id="backBtn" type="button">Back</button>
|
||||
<button id="homeBtn" type="button">Whole map</button>
|
||||
<button id="exploreUndoBtn" type="button">Undo exploration</button>
|
||||
<button id="focusBtn" type="button">Focus story</button>
|
||||
<button id="readStoryBtn" type="button">Read story</button>
|
||||
<button id="focusBtn" class="story-workspace-only" type="button">Focus story</button>
|
||||
<button id="readStoryBtn" class="story-workspace-only" type="button">Read story</button>
|
||||
<button id="undoBtn" type="button">Undo</button>
|
||||
<button id="redoBtn" type="button">Redo</button>
|
||||
<button id="duplicateBtn" type="button">Duplicate</button>
|
||||
<button id="deleteBtn" class="danger" type="button">Delete</button>
|
||||
<button id="duplicateBtn" class="story-workspace-only" type="button">Duplicate</button>
|
||||
<button id="deleteBtn" class="danger story-workspace-only" type="button">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="breadcrumb" class="breadcrumb"></div>
|
||||
@@ -1957,6 +1975,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<div class="arc-toolbar" style="margin-top:10px">
|
||||
<button id="newArcBtn" type="button">New arc</button>
|
||||
<button id="newChapterBtn" type="button">New chapter</button>
|
||||
<button id="arcSaveBtn" class="primary" type="button">Save arcs</button>
|
||||
<button id="deleteArcBtn" class="danger" type="button">Delete arc</button>
|
||||
</div>
|
||||
<div class="editor-grid" style="margin-top:10px">
|
||||
@@ -1993,7 +2012,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
<label class="full">Link memory<select id="momentEntryLink"></select></label>
|
||||
</div>
|
||||
<div class="actions" style="margin-top:10px">
|
||||
<button id="saveChapterBtn" class="primary" type="button">Save chapter</button>
|
||||
<button id="addMomentBtn" type="button">Add moment</button>
|
||||
<button id="cancelMomentBtn" class="hidden" type="button">Cancel edit</button>
|
||||
<button id="deleteChapterBtn" class="danger" type="button">Delete chapter</button>
|
||||
</div>
|
||||
<div id="momentList" class="moment-list" style="margin-top:10px"></div>
|
||||
@@ -2083,7 +2104,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
const draftKey = "hiddenNarrativeObservatoryDraft:v1";
|
||||
const WORLD = { width: 4600, height: 3300, cx: 2300, cy: 1650 };
|
||||
const SCREEN = { width: 1000, height: 760 };
|
||||
const state = { entries: [], stories: [], lifeArcs: [], meta: {}, mode: "graph", editorMode: "memory", selectedId: "", selectedStoryId: "", selectedArcId: "", selectedChapterId: "", editingSectionId: "", readerIndex: 0, undo: [], redo: [], draggingId: "", positions: new Map(), clusterPositions: new Map(), zoom: 0, focusCluster: "", focusKind: "", focusIds: null, focusLabel: "", history: [], explorationUndo: [], explorationRedo: [], camera: { x: 0, y: 0, k: 0.22 }, pan: null, drag: null, pendingDrag: null, renderTimer: 0 };
|
||||
const state = { entries: [], stories: [], lifeArcs: [], meta: {}, workspace: "stories", mode: "graph", editorMode: "memory", selectedId: "", selectedStoryId: "", selectedArcId: "", selectedChapterId: "", selectedMomentId: "", editingSectionId: "", readerIndex: 0, undo: [], redo: [], draggingId: "", positions: new Map(), clusterPositions: new Map(), zoom: 0, focusCluster: "", focusKind: "", focusIds: null, focusLabel: "", history: [], explorationUndo: [], explorationRedo: [], camera: { x: 0, y: 0, k: 0.22 }, pan: null, drag: null, pendingDrag: null, renderTimer: 0 };
|
||||
const fields = ["title","type","contentClass","familyLayer","characters","tone","rarity","discoveryDifficulty","mysteryLevel","resonanceScore","pageLocation","triggerConditions","content","surfaces","observatoryRole","canonicalUse","symbols","narrativeArcs","tags","emotionalRole","continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries","enabled","cssClassHooks","audioSettings","animationTrigger","notes"];
|
||||
const storyFields = ["storyTitle","storyDescription","storyTone","storyDiscovery","storyCharacters","storySymbols","storyMarkers","storyLayers","storyUnlock","storyHidden"];
|
||||
const arcFields = ["arcTitle","arcDomain","arcVisibility","arcTone","arcThemes","arcSummary","arcOrgWeb"];
|
||||
@@ -2251,7 +2272,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
$("status").textContent = message;
|
||||
$("autosave").textContent = message;
|
||||
}
|
||||
function setEditorMode(mode, detail = "") {
|
||||
function setEditorMode(mode, detail = "") {
|
||||
state.editorMode = mode === "story" ? "story" : "memory";
|
||||
$("editorModebar").querySelectorAll("[data-editor-mode]").forEach((button) => button.classList.toggle("active", button.dataset.editorMode === state.editorMode));
|
||||
$("storyEditorPanel").classList.toggle("active-editor", state.editorMode === "story");
|
||||
@@ -2262,8 +2283,23 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
const entry = currentEntry();
|
||||
$("editingBanner").innerHTML = state.editorMode === "story"
|
||||
? `<strong>Editing Story:</strong> ${html(story?.title || "No story selected")}<br><span class="subtle">${html(detail || "Shape the route, pacing, sections, and emotional sequence.")}</span>`
|
||||
: `<strong>Editing Memory:</strong> ${html(entry?.title || "No memory selected")}<br><span class="subtle">${html(detail || "Craft the reusable fragment and choose where it can recur.")}</span>`;
|
||||
}
|
||||
: `<strong>Editing Memory:</strong> ${html(entry?.title || "No memory selected")}<br><span class="subtle">${html(detail || "Craft the reusable fragment and choose where it can recur.")}</span>`;
|
||||
}
|
||||
function setWorkspace(workspace) {
|
||||
state.workspace = workspace === "arcs" ? "arcs" : "stories";
|
||||
document.body.classList.toggle("arc-workspace", state.workspace === "arcs");
|
||||
$("workspaceSwitcher").querySelectorAll("[data-workspace]").forEach((button) => button.classList.toggle("active", button.dataset.workspace === state.workspace));
|
||||
if (state.workspace === "arcs") {
|
||||
if (!state.selectedArcId) state.selectedArcId = state.lifeArcs[0]?.id || "";
|
||||
if (!state.selectedChapterId) state.selectedChapterId = arcById()?.chapters?.[0]?.id || "";
|
||||
fillArcForm(arcById());
|
||||
fitCameraTo({ minX: 520, minY: 420, maxX: WORLD.width - 520, maxY: WORLD.height - 420 }, true);
|
||||
} else {
|
||||
setEditorMode(state.editorMode);
|
||||
fitCameraTo({ minX: 250, minY: 220, maxX: WORLD.width - 250, maxY: WORLD.height - 220 }, true);
|
||||
}
|
||||
render();
|
||||
}
|
||||
function cameraTransform() {
|
||||
return `translate(${state.camera.x} ${state.camera.y}) scale(${state.camera.k})`;
|
||||
}
|
||||
@@ -2546,6 +2582,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
state.selectedArcId = state.lifeArcs[index].id;
|
||||
rememberDraft();
|
||||
renderArcStudio();
|
||||
if (state.workspace === "arcs") renderArcConstellation();
|
||||
}
|
||||
function applyChapterFormToState() {
|
||||
const arc = arcById();
|
||||
@@ -2556,6 +2593,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
state.selectedChapterId = arc.chapters[index].id;
|
||||
rememberDraft();
|
||||
renderArcStudio();
|
||||
if (state.workspace === "arcs") renderArcConstellation();
|
||||
}
|
||||
function fillArcForm(arc) {
|
||||
if (!arc) {
|
||||
@@ -2611,8 +2649,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
state.lifeArcs.unshift(arc);
|
||||
state.selectedArcId = arc.id;
|
||||
state.selectedChapterId = "";
|
||||
clearMomentForm();
|
||||
fillArcForm(arc);
|
||||
rememberDraft();
|
||||
render();
|
||||
}
|
||||
function deleteArc() {
|
||||
const arc = arcById();
|
||||
@@ -2621,10 +2661,12 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
state.lifeArcs = state.lifeArcs.filter((item) => item.id !== arc.id);
|
||||
state.selectedArcId = state.lifeArcs[0]?.id || "";
|
||||
state.selectedChapterId = arcById()?.chapters?.[0]?.id || "";
|
||||
clearMomentForm();
|
||||
fillArcForm(arcById());
|
||||
rememberDraft();
|
||||
render();
|
||||
}
|
||||
function newChapter() {
|
||||
async function newChapter() {
|
||||
if (!arcById()) newArc();
|
||||
const arc = arcById();
|
||||
if (!arc) return;
|
||||
@@ -2641,8 +2683,11 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
};
|
||||
arc.chapters = [chapter, ...(arc.chapters || [])];
|
||||
state.selectedChapterId = chapter.id;
|
||||
clearMomentForm();
|
||||
fillChapterForm(chapter);
|
||||
rememberDraft();
|
||||
render();
|
||||
setStatus("new chapter created. Give it a title, then save the chapter.");
|
||||
}
|
||||
function deleteChapter() {
|
||||
const arc = arcById();
|
||||
@@ -2651,12 +2696,19 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
snapshot();
|
||||
arc.chapters = (arc.chapters || []).filter((item) => item.id !== chapter.id);
|
||||
state.selectedChapterId = arc.chapters[0]?.id || "";
|
||||
clearMomentForm();
|
||||
fillChapterForm(chapterById());
|
||||
rememberDraft();
|
||||
render();
|
||||
}
|
||||
function addMoment() {
|
||||
async function saveChapter() {
|
||||
if (!chapterById()) return setStatus("create or select a chapter first.");
|
||||
applyChapterFormToState();
|
||||
await save("chapter saved.");
|
||||
}
|
||||
async function addMoment() {
|
||||
const chapter = chapterById();
|
||||
if (!chapter) return;
|
||||
if (!chapter) return setStatus("create or select a chapter before adding a moment.");
|
||||
const text = $("momentText").value.trim();
|
||||
const lesson = $("momentLesson").value.trim();
|
||||
if (!text && !lesson) {
|
||||
@@ -2664,8 +2716,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
return;
|
||||
}
|
||||
snapshot();
|
||||
chapter.moments = [{
|
||||
id: `moment-${Date.now()}`,
|
||||
const updatingMoment = Boolean(state.selectedMomentId);
|
||||
const moment = {
|
||||
id: state.selectedMomentId || `moment-${Date.now()}`,
|
||||
date: $("momentDate").value,
|
||||
kind: $("momentKind").value || "moment",
|
||||
text,
|
||||
@@ -2676,17 +2729,54 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
visibility: $("momentVisibility").value || "private",
|
||||
linkedStoryId: $("momentStoryLink").value,
|
||||
linkedEntryId: $("momentEntryLink").value,
|
||||
}, ...(chapter.moments || [])];
|
||||
["momentText","momentFeeling","momentMetric","momentLesson","momentSource"].forEach((id) => $(id).value = "");
|
||||
};
|
||||
if (state.selectedMomentId) {
|
||||
const index = (chapter.moments || []).findIndex((item) => item.id === state.selectedMomentId);
|
||||
if (index >= 0) chapter.moments[index] = moment;
|
||||
} else {
|
||||
chapter.moments = [moment, ...(chapter.moments || [])];
|
||||
}
|
||||
clearMomentForm();
|
||||
renderArcStudio();
|
||||
if (state.workspace === "arcs") renderArcConstellation();
|
||||
rememberDraft();
|
||||
await save(updatingMoment ? "moment updated." : "moment added.");
|
||||
}
|
||||
function fillMomentForm(moment) {
|
||||
if (!moment) return clearMomentForm();
|
||||
state.selectedMomentId = moment.id;
|
||||
$("momentDate").value = moment.date || "";
|
||||
$("momentKind").value = moment.kind || "moment";
|
||||
$("momentText").value = moment.text || "";
|
||||
$("momentFeeling").value = moment.feeling || "";
|
||||
$("momentMetric").value = moment.metric || "";
|
||||
$("momentLesson").value = moment.lesson || "";
|
||||
$("momentSource").value = moment.sourcePointer || "";
|
||||
$("momentVisibility").value = moment.visibility || "private";
|
||||
$("momentStoryLink").value = moment.linkedStoryId || "";
|
||||
$("momentEntryLink").value = moment.linkedEntryId || "";
|
||||
$("addMomentBtn").textContent = "Update moment";
|
||||
$("cancelMomentBtn").classList.remove("hidden");
|
||||
renderArcStudio();
|
||||
}
|
||||
function clearMomentForm() {
|
||||
state.selectedMomentId = "";
|
||||
["momentDate","momentText","momentFeeling","momentMetric","momentLesson","momentSource"].forEach((id) => $(id).value = "");
|
||||
$("momentKind").value = "moment";
|
||||
$("momentVisibility").value = "private";
|
||||
$("momentStoryLink").value = "";
|
||||
$("momentEntryLink").value = "";
|
||||
$("addMomentBtn").textContent = "Add moment";
|
||||
$("cancelMomentBtn").classList.add("hidden");
|
||||
}
|
||||
function deleteMoment(momentId) {
|
||||
const chapter = chapterById();
|
||||
if (!chapter) return;
|
||||
snapshot();
|
||||
chapter.moments = (chapter.moments || []).filter((moment) => moment.id !== momentId);
|
||||
if (state.selectedMomentId === momentId) clearMomentForm();
|
||||
renderArcStudio();
|
||||
if (state.workspace === "arcs") renderArcConstellation();
|
||||
rememberDraft();
|
||||
}
|
||||
function renderArcStudio() {
|
||||
@@ -2698,18 +2788,23 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
$("arcList").querySelectorAll("[data-arc]").forEach((button) => button.addEventListener("click", () => {
|
||||
state.selectedArcId = button.dataset.arc;
|
||||
state.selectedChapterId = arcById()?.chapters?.[0]?.id || "";
|
||||
clearMomentForm();
|
||||
fillArcForm(arcById());
|
||||
if (state.workspace === "arcs") renderArcConstellation();
|
||||
}));
|
||||
$("chapterList").innerHTML = arc?.chapters?.length
|
||||
? arc.chapters.map((item) => `<button class="chapter-card ${item.id === state.selectedChapterId ? "active" : ""}" type="button" data-chapter="${html(item.id)}"><strong>${html(item.title)}</strong><span class="subtle">${html(item.timeframe || "open timeframe")} / ${(item.moments || []).length} moments</span></button>`).join("")
|
||||
: "<span class='subtle'>No chapters in this arc yet.</span>";
|
||||
$("chapterList").querySelectorAll("[data-chapter]").forEach((button) => button.addEventListener("click", () => {
|
||||
state.selectedChapterId = button.dataset.chapter;
|
||||
clearMomentForm();
|
||||
fillChapterForm(chapterById());
|
||||
if (state.workspace === "arcs") renderArcConstellation();
|
||||
}));
|
||||
$("momentList").innerHTML = chapter?.moments?.length
|
||||
? chapter.moments.map((moment) => `<div class="moment-card"><strong>${html(moment.date || moment.kind || "moment")}</strong><span>${html(moment.text || moment.lesson)}</span><span class="subtle">${[moment.feeling, moment.metric, moment.sourcePointer, moment.visibility].filter(Boolean).map(html).join(" / ")}</span><button type="button" data-delete-moment="${html(moment.id)}">Delete</button></div>`).join("")
|
||||
? chapter.moments.map((moment) => `<div class="moment-card ${moment.id === state.selectedMomentId ? "active" : ""}"><strong>${html(moment.date || moment.kind || "moment")}</strong><span>${html(moment.text || moment.lesson)}</span><span class="subtle">${[moment.feeling, moment.metric, moment.sourcePointer, moment.visibility].filter(Boolean).map(html).join(" / ")}</span><div class="actions"><button type="button" data-edit-moment="${html(moment.id)}">Edit</button><button type="button" data-delete-moment="${html(moment.id)}">Delete</button></div></div>`).join("")
|
||||
: "<span class='subtle'>Log compact moments here, then turn selected material into the chapter story draft.</span>";
|
||||
$("momentList").querySelectorAll("[data-edit-moment]").forEach((button) => button.addEventListener("click", () => fillMomentForm((chapter.moments || []).find((moment) => moment.id === button.dataset.editMoment))));
|
||||
$("momentList").querySelectorAll("[data-delete-moment]").forEach((button) => button.addEventListener("click", () => deleteMoment(button.dataset.deleteMoment)));
|
||||
}
|
||||
function fillStoryForm(story) {
|
||||
@@ -3098,13 +3193,103 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
function characterColor(character) {
|
||||
return state.meta.characterRegistry?.[character]?.territoryColor || "";
|
||||
}
|
||||
function hash(value) {
|
||||
function hash(value) {
|
||||
let h = 0;
|
||||
for (let i = 0; i < value.length; i += 1) h = (h * 31 + value.charCodeAt(i)) % 9999;
|
||||
return h;
|
||||
}
|
||||
function render() {
|
||||
updateSemanticZoom();
|
||||
return h;
|
||||
}
|
||||
function renderArcConstellation() {
|
||||
const graph = $("graph");
|
||||
const arcs = state.lifeArcs;
|
||||
const selectedArc = arcById();
|
||||
const selectedChapter = chapterById();
|
||||
const arcPositions = new Map();
|
||||
const radius = arcs.length > 1 ? Math.min(1180, 460 + arcs.length * 95) : 0;
|
||||
arcs.forEach((arc, index) => {
|
||||
const angle = -Math.PI / 2 + (Math.PI * 2 * index) / Math.max(1, arcs.length);
|
||||
arcPositions.set(arc.id, { x: WORLD.cx + Math.cos(angle) * radius, y: WORLD.cy + Math.sin(angle) * radius * 0.62 });
|
||||
});
|
||||
const selectedPos = selectedArc ? arcPositions.get(selectedArc.id) : null;
|
||||
const chapterPositions = new Map();
|
||||
(selectedArc?.chapters || []).forEach((chapter, index, chapters) => {
|
||||
const angle = -Math.PI / 2 + (Math.PI * 2 * index) / Math.max(1, chapters.length);
|
||||
chapterPositions.set(chapter.id, { x: selectedPos.x + Math.cos(angle) * 390, y: selectedPos.y + Math.sin(angle) * 270 });
|
||||
});
|
||||
const arcBranches = (selectedArc?.chapters || []).map((chapter) => {
|
||||
const pos = chapterPositions.get(chapter.id);
|
||||
return `<path class="arc-branch" d="M ${selectedPos.x} ${selectedPos.y} Q ${(selectedPos.x + pos.x) / 2 + 35} ${(selectedPos.y + pos.y) / 2 - 25} ${pos.x} ${pos.y}"></path>`;
|
||||
}).join("");
|
||||
const arcNodes = arcs.map((arc) => {
|
||||
const pos = arcPositions.get(arc.id);
|
||||
const selected = arc.id === state.selectedArcId;
|
||||
const color = toneColors[arc.tone] || "#d3a64d";
|
||||
const size = selected ? 76 : 54;
|
||||
return `<g class="arc-node${selected ? " selected" : ""}" data-arc-node="${html(arc.id)}" transform="translate(${pos.x} ${pos.y})">
|
||||
<circle class="hit-target" r="${size + 20}"></circle><circle r="${size}" fill="${html(color)}" opacity="${selected ? 0.82 : 0.56}" stroke="rgba(255,246,223,.8)" stroke-width="2"></circle>
|
||||
<text class="arc-map-label" x="0" y="${size + 32}" text-anchor="middle">${html(truncate(arc.title, 34))}</text>
|
||||
<text class="arc-map-meta" x="0" y="${size + 54}" text-anchor="middle">${(arc.chapters || []).length} chapters / ${arcMomentCount(arc)} moments</text>
|
||||
</g>`;
|
||||
}).join("");
|
||||
const chapterNodes = (selectedArc?.chapters || []).map((chapter) => {
|
||||
const pos = chapterPositions.get(chapter.id);
|
||||
const selected = chapter.id === state.selectedChapterId;
|
||||
const color = toneColors[chapter.tone] || toneColors[selectedArc.tone] || "#75a9bd";
|
||||
return `<g class="chapter-node${selected ? " selected" : ""}" data-chapter-node="${html(chapter.id)}" transform="translate(${pos.x} ${pos.y})">
|
||||
<circle class="hit-target" r="58"></circle><circle r="${selected ? 39 : 31}" fill="${html(color)}" opacity=".72" stroke="rgba(255,246,223,.76)" stroke-width="2"></circle>
|
||||
<text class="arc-map-label" x="0" y="64" text-anchor="middle">${html(truncate(chapter.title, 30))}</text>
|
||||
<text class="arc-map-meta" x="0" y="84" text-anchor="middle">${html(chapter.timeframe || "open timeframe")}</text>
|
||||
</g>`;
|
||||
}).join("");
|
||||
const momentCenter = selectedChapter ? chapterPositions.get(selectedChapter.id) : null;
|
||||
const moments = selectedChapter?.moments || [];
|
||||
const momentPositions = moments.map((moment, index) => {
|
||||
const angle = -Math.PI / 2 + (Math.PI * 2 * index) / Math.max(1, moments.length);
|
||||
const band = 145 + (index % 3) * 42;
|
||||
return { moment, x: momentCenter.x + Math.cos(angle) * band, y: momentCenter.y + Math.sin(angle) * band * 0.72 };
|
||||
});
|
||||
const momentPaths = momentCenter ? momentPositions.map((item) => `<line class="arc-moment-path" x1="${momentCenter.x}" y1="${momentCenter.y}" x2="${item.x}" y2="${item.y}"></line>`).join("") : "";
|
||||
const momentNodes = momentPositions.map(({ moment, x, y }) => {
|
||||
const selected = moment.id === state.selectedMomentId;
|
||||
return `<g class="moment-node${selected ? " selected" : ""}" data-moment-node="${html(moment.id)}" transform="translate(${x} ${y})">
|
||||
<circle class="hit-target" r="25"></circle><circle r="${selected ? 13 : 9}" fill="#f2d28b" opacity=".88" stroke="rgba(255,246,223,.8)" stroke-width="1.5"></circle>
|
||||
<text class="arc-map-meta" x="18" y="5">${html(truncate(moment.date || moment.kind || "moment", 20))}</text>
|
||||
</g>`;
|
||||
}).join("");
|
||||
graph.innerHTML = `<g id="world" transform="${cameraTransform()}">${arcBranches}${momentPaths}${arcNodes}${chapterNodes}${momentNodes}</g>`;
|
||||
graph.querySelectorAll("[data-arc-node]").forEach((node) => node.addEventListener("click", () => {
|
||||
state.selectedArcId = node.dataset.arcNode;
|
||||
state.selectedChapterId = arcById()?.chapters?.[0]?.id || "";
|
||||
clearMomentForm();
|
||||
fillArcForm(arcById());
|
||||
renderArcConstellation();
|
||||
}));
|
||||
graph.querySelectorAll("[data-chapter-node]").forEach((node) => node.addEventListener("click", () => {
|
||||
state.selectedChapterId = node.dataset.chapterNode;
|
||||
clearMomentForm();
|
||||
fillChapterForm(chapterById());
|
||||
renderArcConstellation();
|
||||
}));
|
||||
graph.querySelectorAll("[data-moment-node]").forEach((node) => node.addEventListener("click", () => {
|
||||
fillMomentForm((chapterById()?.moments || []).find((moment) => moment.id === node.dataset.momentNode));
|
||||
renderArcConstellation();
|
||||
}));
|
||||
$("mapTitle").textContent = selectedArc?.title || "Life Arc Constellation";
|
||||
$("mapSub").textContent = selectedChapter ? `${selectedChapter.title} / ${moments.length} moments` : "Choose an arc, then a chapter, to reveal its moments.";
|
||||
$("mapNote").textContent = "Arcs hold chapters; chapters hold the moments that changed the story. Select any light to edit it.";
|
||||
$("breadcrumb").innerHTML = `<button type="button">Life arcs</button>${selectedArc ? `<button type="button">${html(selectedArc.title)}</button>` : ""}${selectedChapter ? `<button type="button">${html(selectedChapter.title)}</button>` : ""}`;
|
||||
$("activeState").innerHTML = selectedChapter ? `<span class="filter-pill">Editing chapter: ${html(selectedChapter.title)}</span>` : `<span class="subtle">Choose or create a life arc.</span>`;
|
||||
$("emptyRecovery").classList.toggle("hidden", Boolean(arcs.length));
|
||||
if (!arcs.length) $("emptyRecovery").innerHTML = `<h2>No life arcs yet</h2><p class="subtle">Create the first arc from the editor on the right.</p><button type="button" data-new-arc>New arc</button>`;
|
||||
$("emptyRecovery").querySelector("[data-new-arc]")?.addEventListener("click", newArc);
|
||||
$("minimap").innerHTML = arcs.map((arc) => { const pos = arcPositions.get(arc.id); return `<circle cx="${Math.max(5, Math.min(95, pos.x / WORLD.width * 100))}" cy="${Math.max(5, Math.min(65, pos.y / WORLD.height * 70))}" r="${arc.id === state.selectedArcId ? 5 : 3}" fill="${toneColors[arc.tone] || "#d3a64d"}"></circle>`; }).join("");
|
||||
}
|
||||
function render() {
|
||||
if (state.workspace === "arcs") {
|
||||
renderArcStudio();
|
||||
renderArcConstellation();
|
||||
return;
|
||||
}
|
||||
updateSemanticZoom();
|
||||
const scope = visibleScope();
|
||||
const entries = scope.entries;
|
||||
const clusters = scope.clusters;
|
||||
@@ -4225,7 +4410,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
$("momentStoryLink").innerHTML = opt("", "No story link") + state.stories.map((story) => opt(story.id, story.title)).join("");
|
||||
$("momentEntryLink").innerHTML = opt("", "No memory link") + state.entries.map((entry) => opt(entry.id, entry.title)).join("");
|
||||
}
|
||||
async function save() {
|
||||
async function save(successMessage = "") {
|
||||
applyFormToState();
|
||||
applyStoryFormToState();
|
||||
applyArcFormToState();
|
||||
@@ -4238,7 +4423,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
state.lifeArcs = data.lifeArcs || [];
|
||||
state.meta = data;
|
||||
localStorage.removeItem(draftKey);
|
||||
setStatus(data.queuedBuild ? "constellation stored safely. asset republish queued." : (data.message || "constellation stored safely."));
|
||||
setStatus(successMessage || (data.queuedBuild ? "constellation stored safely. asset republish queued." : (data.message || "constellation stored safely.")));
|
||||
renderValidation();
|
||||
refreshArcLinkOptions();
|
||||
renderArcStudio();
|
||||
@@ -4290,7 +4475,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
zoomAt(x, y, event.deltaY < 0 ? 1.12 : 1 / 1.12);
|
||||
}, { passive: false });
|
||||
graph.addEventListener("pointerdown", (event) => {
|
||||
if (event.button !== 0 || event.target.closest(".memory-node, .cluster-node")) return;
|
||||
if (event.button !== 0 || event.target.closest(".memory-node, .cluster-node, .arc-node, .chapter-node, .moment-node")) return;
|
||||
graph.setPointerCapture(event.pointerId);
|
||||
graph.classList.add("is-panning");
|
||||
state.pan = { id: event.pointerId, x: event.clientX, y: event.clientY, vx: 0, vy: 0, last: performance.now() };
|
||||
@@ -4338,7 +4523,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
finishPan(event.pointerId);
|
||||
});
|
||||
graph.addEventListener("dblclick", (event) => {
|
||||
if (event.target.closest(".memory-node, .cluster-node")) return;
|
||||
if (event.target.closest(".memory-node, .cluster-node, .arc-node, .chapter-node, .moment-node")) return;
|
||||
const rect = graph.getBoundingClientRect();
|
||||
const x = ((event.clientX - rect.left) / rect.width) * SCREEN.width;
|
||||
const y = ((event.clientY - rect.top) / rect.height) * SCREEN.height;
|
||||
@@ -4412,12 +4597,15 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
$("deleteArcBtn").onclick = deleteArc;
|
||||
$("deleteChapterBtn").onclick = deleteChapter;
|
||||
$("addMomentBtn").onclick = addMoment;
|
||||
$("saveChapterBtn").onclick = saveChapter;
|
||||
$("cancelMomentBtn").onclick = clearMomentForm;
|
||||
$("arcSaveBtn").onclick = () => save();
|
||||
$("storyFromSelectionBtn").onclick = () => newStory(state.selectedId);
|
||||
$("addSectionBtn").onclick = addStorySection;
|
||||
$("saveSectionBtn").onclick = saveStorySection;
|
||||
$("closeSectionBtn").onclick = closeSectionEditor;
|
||||
$("deleteStoryBtn").onclick = deleteStory;
|
||||
$("saveBtn").onclick = save;
|
||||
$("saveBtn").onclick = () => save();
|
||||
$("zoomOutBtn").onclick = () => zoomBy(-1);
|
||||
$("zoomInBtn").onclick = () => zoomBy(1);
|
||||
$("backBtn").onclick = goBack;
|
||||
@@ -4434,7 +4622,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
$("deleteBtn").onclick = deleteEntry;
|
||||
$("undoBtn").onclick = () => { if (!state.undo.length) return; state.redo.push(JSON.stringify({ entries: state.entries, stories: state.stories, lifeArcs: state.lifeArcs, selectedStoryId: state.selectedStoryId, selectedArcId: state.selectedArcId, selectedChapterId: state.selectedChapterId })); restore(state.undo.pop()); };
|
||||
$("redoBtn").onclick = () => { if (!state.redo.length) return; state.undo.push(JSON.stringify({ entries: state.entries, stories: state.stories, lifeArcs: state.lifeArcs, selectedStoryId: state.selectedStoryId, selectedArcId: state.selectedArcId, selectedChapterId: state.selectedChapterId })); restore(state.redo.pop()); };
|
||||
$("modebar").querySelectorAll("button").forEach((button) => button.addEventListener("click", () => {
|
||||
$("modebar").querySelectorAll("button").forEach((button) => button.addEventListener("click", () => {
|
||||
snapshotExploration(`switch to ${button.dataset.mode}`);
|
||||
state.mode = button.dataset.mode;
|
||||
state.focusCluster = "";
|
||||
@@ -4442,8 +4630,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
||||
state.focusLabel = "";
|
||||
state.zoom = 0;
|
||||
$("modebar").querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button));
|
||||
render();
|
||||
}));
|
||||
render();
|
||||
}));
|
||||
$("workspaceSwitcher").querySelectorAll("[data-workspace]").forEach((button) => button.addEventListener("click", () => setWorkspace(button.dataset.workspace)));
|
||||
$("editorModebar").querySelectorAll("[data-editor-mode]").forEach((button) => button.addEventListener("click", () => {
|
||||
if (button.dataset.editorMode === "story") {
|
||||
setEditorMode("story");
|
||||
|
||||
Reference in New Issue
Block a user