Add continuity dashboard and scene authoring workflow

This commit is contained in:
Joey Grasty
2026-08-27 17:31:01 -05:00
parent 4499d4bd8f
commit 50b11264a9
247 changed files with 24024 additions and 9464 deletions

View File

@@ -7,6 +7,7 @@ let acceptedSceneUuid = '';
let summarySceneUuid = '';
let contextSceneUuid = '';
let updatesSceneUuid = '';
let hasLoadedProject = false;
const byId = (id) => document.getElementById(id);
const selected = (container) => [...container.querySelectorAll('input:checked')].map((input) => input.value);
@@ -40,18 +41,68 @@ function paragraphTail(text, targetWords) {
return tail.join('\n\n');
}
function threadChainFrom(sceneUuid) {
const chain = [];
const seen = new Set();
let cursor = sceneByUuid(sceneUuid);
while (cursor && !seen.has(cursor.uuid)) {
chain.push(cursor);
seen.add(cursor.uuid);
cursor = cursor.predecessor_uuid ? sceneByUuid(cursor.predecessor_uuid) : null;
}
return chain;
}
function updateThreadStatus() {
const startsNew = byId('new-thread').checked;
byId('thread-predecessor').disabled = startsNew;
const source = sceneByUuid(byId('thread-predecessor').value);
if (startsNew) {
byId('thread-status').textContent = 'This scene will start a new narrative thread when it is accepted into the book.';
return;
}
if (!source) {
byId('thread-status').textContent = 'Choose a canonical scene to continue, or explicitly start a new thread.';
return;
}
byId('thread-status').textContent = `When accepted into the book, this scene will continue from ${source.title}.`;
}
function loadStory() {
const selectedSummaries = [...byId('story-summaries').querySelectorAll('input:checked')]
.map((input) => sceneByUuid(input.value)).filter(Boolean);
byId('story').value = selectedSummaries.map((scene) => scene.summary).join('\n\n');
byId('new-thread').checked = selectedSummaries.length === 0;
}
function loadPrevious() {
const source = sceneByUuid(byId('previous-source').value);
if (!source) { byId('previous').value = ''; return; }
byId('previous').value = paragraphTail(source.text, Number(byId('previous-words').value) || 400);
byId('new-thread').checked = false;
}
function setLexiconSelection(value) {
const selectedText = value || '';
byId('lexicon-entries').querySelectorAll('input').forEach((input) => {
const entry = project.lexicon_entries[Number(input.value)];
input.checked = Boolean(entry && selectedText.includes(entry.text));
});
}
function loadLexicon() {
const entries = [...byId('lexicon-entries').querySelectorAll('input:checked')]
.map((input) => project.lexicon_entries[Number(input.value)]).filter(Boolean);
byId('lexicon').value = entries.map((entry) => entry.text).join('\n\n');
byId('lexicon-status').textContent = entries.length ? `${entries.length} selected lexicon entr${entries.length === 1 ? 'y' : 'ies'} loaded. You may still edit the package text.` : 'No lexicon entries selected; LEXICON will be empty.';
}
function clearLexicon() {
byId('lexicon-entries').querySelectorAll('input').forEach((input) => { input.checked = false; });
byId('lexicon').value = '';
byId('lexicon-status').textContent = 'Lexicon selection cleared for this package.';
}
function renderLexiconEntries(entries) {
byId('lexicon-entries').innerHTML = entries.map((entry, index) => `<label title="${escapeHtml(entry.text)}"><input type="checkbox" value="${index}"> ${escapeHtml(entry.title)}</label>`).join('') || '<p class="muted">No Term — definition entries are available in Master Lexicon.</p>';
}
function priorPackageContext() {
@@ -95,17 +146,61 @@ function populateContinuity(defaults = false) {
const withSummaries = earlier.filter((candidate) => candidate.summary);
const latest = earlier.at(-1);
byId('story-summaries').innerHTML = withSummaries.map((candidate) => `<label><input type="checkbox" value="${candidate.uuid}"> ${escapeHtml(candidate.title)}${escapeHtml(candidate.summary)}</label>`).join('') || '<p class="muted">No earlier approved scene summaries are available.</p>';
setOptions('previous-source', earlier, 'None — new narrative thread');
setOptions('previous-source', earlier, 'None — no verbatim previous text');
setOptions('thread-predecessor', earlier, 'Choose a canonical scene');
setOptions('recall-source', project.scenes.filter((candidate) => candidate.state === 'canonical' && candidate.uuid !== scene.uuid), 'Choose a canonical scene to recall');
if (latest) byId('previous-source').value = latest.uuid;
if (latest && latest.summary) {
if (latest) {
byId('previous-source').value = latest.uuid;
byId('thread-predecessor').value = latest.uuid;
const check = byId('story-summaries').querySelector(`input[value="${latest.uuid}"]`);
if (check) check.checked = true;
}
if (defaults) {
loadStory();
loadPrevious();
if (latest && latest.package_context.character_uuids.length) carryCharacters();
const context = scene.package_context;
const hasSavedPackage = Boolean(context && (context.directive || context.story || context.previous || context.recall || context.lexicon || context.notes || context.character_uuids.length));
if (hasSavedPackage) {
const characterUuids = new Set(context.character_uuids);
const placeUuids = new Set(context.place_uuids || []);
byId('characters').querySelectorAll('input').forEach((input) => { input.checked = characterUuids.has(input.value); });
byId('places').querySelectorAll('input').forEach((input) => { input.checked = placeUuids.has(input.value); });
byId('lexicon').value = context.lexicon === '(none)' ? '' : context.lexicon;
byId('story').value = context.story === '(none)' || context.story === '(none - new storyline)' ? '' : context.story;
byId('recall').value = context.recall === '(none)' ? '' : context.recall;
byId('previous').value = context.previous === '(none)' ? '' : context.previous;
byId('notes').value = context.notes === '(none)' ? '' : context.notes;
byId('directive').value = context.directive === '(none)' ? '' : context.directive;
byId('package').textContent = context.exact_package || 'No archived package is available for this scene.';
setLexiconSelection(byId('lexicon').value);
byId('story-summaries').querySelectorAll('input').forEach((input) => {
const candidate = sceneByUuid(input.value);
input.checked = Boolean(candidate?.summary && context.story.includes(candidate.summary));
});
const previousSource = earlier.find((candidate) => context.previous && candidate.text.trim().endsWith(context.previous.trim()));
if (previousSource) byId('previous-source').value = previousSource.uuid;
} else {
byId('package').textContent = 'No archived package is available for this scene.';
const carriedLexicon = latest?.package_context.lexicon;
if (carriedLexicon && carriedLexicon !== '(none)') {
byId('lexicon').value = carriedLexicon;
setLexiconSelection(carriedLexicon);
byId('lexicon-status').textContent = 'Lexicon selection carried forward from the latest canonical scene; adjust it for this package.';
} else {
byId('lexicon').value = '';
setLexiconSelection('');
}
loadStory();
loadPrevious();
if (latest && latest.package_context.character_uuids.length) carryCharacters();
}
if (scene.state === 'canonical' && scene.thread_state === 'new-thread') {
byId('new-thread').checked = true;
} else if (scene.state === 'canonical' && scene.predecessor_uuid) {
byId('new-thread').checked = false;
byId('thread-predecessor').value = scene.predecessor_uuid;
} else {
byId('new-thread').checked = !latest;
}
updateThreadStatus();
}
}
@@ -113,6 +208,39 @@ function escapeHtml(value) {
return value.replace(/[&<>'"]/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[character]));
}
function renderDashboard() {
const canonical = project.scenes.filter((scene) => scene.state === 'canonical');
const byUuid = new Map(canonical.map((scene) => [scene.uuid, scene]));
const children = new Map();
canonical.forEach((scene) => {
if (scene.predecessor_uuid && byUuid.has(scene.predecessor_uuid)) {
children.set(scene.predecessor_uuid, [...(children.get(scene.predecessor_uuid) || []), scene]);
}
});
const roots = canonical.filter((scene) => !scene.predecessor_uuid || !byUuid.has(scene.predecessor_uuid));
function treeNode(scene, ancestry = new Set()) {
const isCycle = ancestry.has(scene.uuid);
const state = scene.thread_state === 'new-thread' ? 'thread start' : 'continues';
const childNodes = isCycle ? [] : (children.get(scene.uuid) || []);
const nextAncestry = new Set(ancestry); nextAncestry.add(scene.uuid);
return `<li><button class="scene-jump" data-scene="${scene.uuid}">${escapeHtml(scene.title)}</button><span class="tree-state">${state}</span>${childNodes.length ? `<ul>${childNodes.map((child) => treeNode(child, nextAncestry)).join('')}</ul>` : ''}</li>`;
}
byId('thread-chains').innerHTML = roots.length ? `<div class="thread-tree"><p class="muted">Narrative threads</p><ul>${roots.map((root) => treeNode(root)).join('')}</ul></div>` : '<p class="muted">No canonical scenes yet.</p>';
byId('continuity-dashboard').innerHTML = `<table><thead><tr><th>Scene</th><th>Chapter</th><th>Thread link</th><th>Approved summary</th></tr></thead><tbody>${canonical.map((scene) => {
const predecessor = byUuid.get(scene.predecessor_uuid);
const link = scene.thread_state === 'new-thread' ? 'starts a thread' : predecessor ? `continues from ${predecessor.title}` : 'not assigned';
const summary = scene.summary ? scene.summary.slice(0, 180) + (scene.summary.length > 180 ? '…' : '') : '—';
return `<tr><td><button class="scene-jump" data-scene="${scene.uuid}">${escapeHtml(scene.title)}</button></td><td>${escapeHtml(scene.chapter)}</td><td>${escapeHtml(link)}</td><td>${escapeHtml(summary)}</td></tr>`;
}).join('')}</tbody></table>`;
}
function selectScene(uuid) {
if ([...byId('scene').options].some((option) => option.value === uuid)) {
byId('scene').value = uuid;
changeScene();
}
}
function updateSceneState() {
const scene = project.scenes.find((candidate) => candidate.uuid === byId('scene').value);
const binding = scene?.binding_state === 'stable' ? 'stable UUID binding' : scene?.binding_state === 'legacy-title-match' ? 'legacy title match' : 'BRIEF missing';
@@ -121,16 +249,43 @@ function updateSceneState() {
byId('brief').disabled = project.locked;
byId('save-brief').disabled = project.locked || !scene;
byId('brief-status').textContent = project.locked ? 'Close Scrivener before saving a BRIEF.' : scene?.brief_uuid ? 'Editing the paired BRIEF.' : 'Saving will create and stably bind a new BRIEF for this scene.';
byId('save-thread-link').disabled = project.locked || !(scene && scene.state === 'canonical');
byId('propose-summary').disabled = !(scene && scene.state === 'canonical');
byId('propose-updates').disabled = !(scene && scene.state === 'canonical');
}
function changeScene() {
const sceneChanged = contextSceneUuid !== byId('scene').value;
const keepsActiveWorkingDraft = workingUuid && acceptedSceneUuid === byId('scene').value;
if (sceneChanged && !keepsActiveWorkingDraft) resetDraftState('Select a scene to begin drafting, or review its stored authoring context below.');
updateSceneState();
if (contextSceneUuid !== byId('scene').value) {
if (sceneChanged) {
contextSceneUuid = byId('scene').value;
populateContinuity(true);
}
const scene = sceneByUuid(byId('scene').value);
if (scene?.state === 'canonical') {
byId('working-draft').value = scene.text;
byId('working-draft').disabled = true;
byId('archive-status').textContent = 'Canonical Manuscript text is shown read-only. Edit the scene in Scrivener; this page restores its saved drafting context.';
summarySceneUuid = scene.uuid;
byId('scene-summary').value = scene.summary;
byId('scene-summary').disabled = false;
byId('save-summary').disabled = !scene.summary;
byId('summary-status').textContent = scene.summary ? 'Approved summary loaded; revise it here and confirm before saving.' : 'No approved summary yet.';
} else if (scene?.working_copy) {
workingUuid = scene.working_copy.uuid;
acceptedSceneUuid = scene.uuid;
byId('working-draft').value = scene.working_copy.text;
byId('working-draft').disabled = false;
byId('accept-confirmation').checked = false;
byId('final-confirmation').checked = false;
byId('accept-editing').disabled = true;
byId('reject-draft').disabled = true;
byId('save-working').disabled = false;
byId('accept-book').disabled = false;
byId('archive-status').textContent = 'Saved working copy restored from Accepted for Editing. You may continue editing or accept it into Manuscript.';
}
if (summarySceneUuid && summarySceneUuid !== byId('scene').value) {
summarySceneUuid = '';
byId('scene-summary').value = '';
@@ -143,23 +298,39 @@ function changeScene() {
function render(data) {
project = data;
const priorSceneUuid = acceptedSceneUuid || byId('scene').value;
const canonicalScenes = data.scenes.filter((scene) => scene.state === 'canonical');
const priorSceneUuid = acceptedSceneUuid || (hasLoadedProject ? byId('scene').value : canonicalScenes.at(-1)?.uuid || data.scenes.at(-1)?.uuid);
byId('status').textContent = data.locked ? 'Scrivener lock detected — archive and manuscript writes are disabled' : 'Project closed — accepted-draft writes are available';
byId('status').className = `status ${data.locked ? 'warning' : 'ok'}`;
const plannedScenes = data.scenes.filter((scene) => scene.state === 'planned');
const approvedSummaries = canonicalScenes.filter((scene) => scene.summary.trim()).length;
const threadStarts = canonicalScenes.filter((scene) => scene.thread_state === 'new-thread').length;
const threadContinuations = canonicalScenes.filter((scene) => scene.thread_state === 'continues').length;
byId('project-check').innerHTML = [
['Scenes', data.scenes.length], ['Briefs without scene', data.unmatched_briefs.length],
['Scenes without brief', data.unmatched_scenes.length], ['Stable scene bindings', data.binding_counts.stable], ['Legacy title matches', data.binding_counts.legacy], ['Characters', data.characters.length], ['Places', data.places.length],
['Canonical scenes', canonicalScenes.length], ['Planned scenes', plannedScenes.length],
['Approved dense summaries', `${approvedSummaries} of ${canonicalScenes.length}`],
['Narrative thread starts', threadStarts], ['Thread continuations', threadContinuations],
['Characters', data.characters.length], ['Places', data.places.length],
['Scenes without BRIEF', data.unmatched_scenes.length], ['Briefs without scene', data.unmatched_briefs.length],
['Stable scene bindings', data.binding_counts.stable], ['Legacy title matches', data.binding_counts.legacy],
].map(([label, value]) => `<dt>${label}</dt><dd>${value}</dd>`).join('');
byId('reconcile-bindings').disabled = data.locked;
byId('binding-status').textContent = data.locked ? 'Close Scrivener before creating or updating bindings.' : data.binding_counts.legacy ? 'Create stable bindings before renaming or moving scenes.' : 'Scene/BRIEF bindings are stored by UUID.';
byId('scene').innerHTML = data.scenes.map((scene) => `<option value="${scene.uuid}">${escapeHtml(scene.title)}${escapeHtml(scene.chapter)}</option>`).join('');
if ([...byId('scene').options].some((option) => option.value === priorSceneUuid)) byId('scene').value = priorSceneUuid;
byId('new-scene-chapter').innerHTML = data.chapters.map((chapter) => `<option value="${chapter.uuid}">${escapeHtml(chapter.title)}</option>`).join('');
byId('new-scene-chapter').disabled = data.locked || data.chapters.length === 0;
byId('new-scene-title').disabled = data.locked;
byId('create-scene').disabled = data.locked || data.chapters.length === 0;
byId('create-scene-status').textContent = data.locked ? 'Close Scrivener before creating a scene.' : 'Creates a blank Manuscript scene and a blank paired BRIEF.';
checks(byId('characters'), data.characters);
checks(byId('places'), data.places);
renderLexiconEntries(data.lexicon_entries);
byId('world-notes').textContent = data.world_notes || 'No project-level World Notes recorded.';
updateSceneState();
contextSceneUuid = byId('scene').value;
populateContinuity(true);
renderDashboard();
contextSceneUuid = '';
changeScene();
hasLoadedProject = true;
}
async function load() {
@@ -197,6 +368,34 @@ async function saveBrief() {
} catch (error) { byId('error').textContent = error.message; }
}
async function createScene() {
byId('error').textContent = '';
try {
const data = await request('/api/create-scene', {
chapter_uuid: byId('new-scene-chapter').value,
title: byId('new-scene-title').value,
});
byId('new-scene-title').value = '';
await load();
selectScene(data.scene_uuid);
byId('create-scene-status').textContent = `${data.title} and its paired blank BRIEF were created.`;
} catch (error) { byId('error').textContent = error.message; }
}
async function saveThreadLink() {
byId('error').textContent = '';
const predecessorUuid = byId('new-thread').checked ? null : byId('thread-predecessor').value || null;
if (!byId('new-thread').checked && !predecessorUuid) {
byId('error').textContent = 'Choose a canonical scene to continue, or explicitly start a new thread.';
return;
}
try {
await request('/api/save-thread-link', { scene_uuid: byId('scene').value, predecessor_uuid: predecessorUuid });
byId('thread-status').textContent = predecessorUuid ? 'Narrative-thread link saved.' : 'This scene is recorded as the start of a narrative thread.';
await load();
} catch (error) { byId('error').textContent = error.message; }
}
async function build() {
byId('error').textContent = ''; byId('warnings').textContent = '';
try {
@@ -255,8 +454,10 @@ async function saveWorkingCopy() {
async function acceptIntoBook() {
byId('error').textContent = '';
try {
const predecessorUuid = byId('new-thread').checked ? null : byId('thread-predecessor').value || null;
if (!byId('new-thread').checked && !predecessorUuid) throw new Error('Choose a PREVIOUS source scene or explicitly start a new narrative thread.');
await request('/api/save-working-copy', { working_uuid: workingUuid, text: byId('working-draft').value });
await request('/api/accept-into-book', { scene_uuid: acceptedSceneUuid, text: byId('working-draft').value, final_confirmation: byId('final-confirmation').checked });
await request('/api/accept-into-book', { scene_uuid: acceptedSceneUuid, text: byId('working-draft').value, predecessor_uuid: predecessorUuid, final_confirmation: byId('final-confirmation').checked });
byId('archive-status').textContent = 'Working copy saved and canonical prose written into Manuscript.';
await load();
byId('propose-summary').disabled = false;
@@ -315,6 +516,8 @@ function renderNewCharacterUpdates(characters) {
async function proposeUpdates() {
byId('error').textContent = '';
const scene = sceneByUuid(byId('scene').value);
byId('updates-status').textContent = 'Requesting continuity proposals from OpenRouter…';
byId('propose-updates').disabled = true;
try {
const data = await request('/api/propose-continuity-updates', { scene_uuid: scene.uuid, text: scene.text, send_confirmation: byId('updates-send-confirmation').checked });
updatesSceneUuid = scene.uuid;
@@ -326,7 +529,12 @@ async function proposeUpdates() {
byId('world-note-additions').disabled = false;
byId('save-updates').disabled = false;
byId('updates-status').textContent = 'Each selected Character field is a complete replacement record: trim or revise it before saving. Uncheck to exclude it.';
} catch (error) { byId('error').textContent = error.message; }
} catch (error) {
byId('error').textContent = error.message;
byId('updates-status').textContent = `Continuity proposal failed: ${error.message}`;
} finally {
byId('propose-updates').disabled = !(sceneByUuid(byId('scene').value)?.state === 'canonical');
}
}
async function saveUpdates() {
@@ -344,8 +552,13 @@ async function saveUpdates() {
byId('scene').addEventListener('change', changeScene);
byId('reconcile-bindings').addEventListener('click', reconcileBindings);
byId('save-brief').addEventListener('click', saveBrief);
byId('create-scene').addEventListener('click', createScene);
byId('save-thread-link').addEventListener('click', saveThreadLink);
byId('load-story').addEventListener('click', loadStory);
byId('load-previous').addEventListener('click', loadPrevious);
byId('load-lexicon').addEventListener('click', loadLexicon);
byId('clear-lexicon').addEventListener('click', clearLexicon);
byId('thread-predecessor').addEventListener('change', updateThreadStatus);
byId('add-recall').addEventListener('click', addRecall);
byId('carry-characters').addEventListener('click', carryCharacters);
byId('insert-prior-notes').addEventListener('click', () => appendNotes(priorPackageContext()?.notes || '', 'prior package NOTES'));
@@ -359,10 +572,13 @@ byId('story-summaries').addEventListener('change', (event) => {
});
byId('new-thread').addEventListener('change', () => {
if (byId('new-thread').checked) {
byId('story').value = '';
byId('previous').value = '';
byId('story-summaries').querySelectorAll('input:checked').forEach((input) => { input.checked = false; });
byId('previous-source').value = '';
updateThreadStatus();
} else if (!byId('thread-predecessor').value) {
const latest = earlierCanonicalScenes(byId('scene').value).at(-1);
if (latest) byId('thread-predecessor').value = latest.uuid;
updateThreadStatus();
} else {
updateThreadStatus();
}
});
byId('build').addEventListener('click', build);
@@ -375,4 +591,10 @@ byId('propose-summary').addEventListener('click', proposeSummary);
byId('save-summary').addEventListener('click', saveSummary);
byId('propose-updates').addEventListener('click', proposeUpdates);
byId('save-updates').addEventListener('click', saveUpdates);
function selectDashboardScene(event) {
const button = event.target.closest('.scene-jump');
if (button) selectScene(button.dataset.scene);
}
byId('thread-chains').addEventListener('click', selectDashboardScene);
byId('continuity-dashboard').addEventListener('click', selectDashboardScene);
load().catch((error) => { byId('status').textContent = error.message; byId('status').className = 'status warning'; });

View File

@@ -24,12 +24,27 @@
<label for="scene">Paired Manuscript scene</label>
<select id="scene"></select>
<p id="scene-state" class="muted"></p>
<section class="create-scene">
<h2>Create scene</h2>
<label for="new-scene-chapter">Manuscript chapter</label>
<select id="new-scene-chapter"></select>
<label for="new-scene-title">Scene label <span class="read-only">optional</span></label>
<input id="new-scene-title" type="text" placeholder="Defaults to next Scene X">
<button id="create-scene" type="button">Create blank scene and BRIEF</button>
<p id="create-scene-status" class="muted"></p>
</section>
<h2>Characters</h2>
<div id="characters" class="checks"></div>
<h2>Places for NOTES</h2>
<div id="places" class="checks"></div>
</aside>
<section class="workspace">
<section class="dashboard-panel">
<h2>Continuity dashboard</h2>
<p class="muted">Canonical scenes and their author-directed narrative-thread links. Select a row to work with that scene.</p>
<div id="thread-chains"></div>
<div id="continuity-dashboard"></div>
</section>
<section class="brief-panel">
<h2>Author BRIEF <span class="read-only">editable</span></h2>
<textarea id="brief" rows="10" placeholder="Write the BRIEF that drives this scene."></textarea>
@@ -42,7 +57,14 @@
</section>
<section class="continuity-panel">
<h2>Continuity context</h2>
<p class="muted">The default follows the latest earlier canonical scene. Change the selections freely for a different narrative thread.</p>
<p class="muted">Thread membership and package context are independent author choices. The controls suggest the latest earlier canonical scene as a starting point.</p>
<div class="thread-controls">
<label class="checkbox"><input id="new-thread" type="checkbox" checked> Start a new narrative thread</label>
<label for="thread-predecessor">Or continue an existing thread from</label>
<select id="thread-predecessor"></select>
<button id="save-thread-link" type="button">Save thread link for this canonical scene</button>
<p id="thread-status" class="muted"></p>
</div>
<div class="continuity-grid">
<div>
<label>STORY summaries for this thread</label>
@@ -70,6 +92,13 @@
<button id="insert-world-notes" type="button">Insert project World Notes</button>
<p id="carry-status" class="muted"></p>
</div>
<div class="lexicon-selector">
<label>LEXICON entries <span class="read-only">select for this package</span></label>
<div id="lexicon-entries" class="context-list"></div>
<button id="load-lexicon" type="button">Load selected entries into LEXICON</button>
<button id="clear-lexicon" type="button">Clear selection</button>
<p id="lexicon-status" class="muted"></p>
</div>
</section>
<div class="grid">
<label>LEXICON (manual subset)
@@ -91,7 +120,6 @@
<textarea id="directive" rows="4" placeholder="Expand to a maximum of 1500 words. Bring the scene to a close…"></textarea>
</label>
</div>
<label class="checkbox"><input id="new-thread" type="checkbox" checked> Treat empty STORY/PREVIOUS as a new narrative thread</label>
<button id="build">Build package preview</button>
<label class="checkbox"><input id="send-confirmation" type="checkbox"> I understand this sends the package to OpenRouter.</label>
<button id="draft">Generate temporary draft — no Scrivener write</button>
@@ -141,6 +169,6 @@
</section>
</section>
</main>
<script src="/static/app.js"></script>
<script src="/static/app.js?v=continuity-request-status-1"></script>
</body>
</html>

View File

@@ -5,11 +5,15 @@ header { display: flex; justify-content: space-between; align-items: center; pad
h1 { margin: 0; font-size: 1.8rem; } h2 { margin: 1.5rem 0 .6rem; font-size: 1rem; } .eyebrow { margin: 0 0 .25rem; font-size: .7rem; letter-spacing: .12em; color: #c7ded6; }
main { display: grid; grid-template-columns: 300px minmax(0, 1fr); min-height: calc(100vh - 84px); }
aside { padding: 1.2rem 1.5rem; border-right: 1px solid #d8d2c5; background: #fcfaf5; } .workspace { padding: 1.5rem 2rem; }
label { display: block; font-size: .85rem; font-weight: 650; } select, textarea, input[type="number"] { width: 100%; margin-top: .35rem; border: 1px solid #bab2a5; border-radius: 4px; background: #fffefb; padding: .55rem; font: .9rem ui-monospace, SFMono-Regular, Menlo, monospace; }
label { display: block; font-size: .85rem; font-weight: 650; } select, textarea, input[type="number"], input[type="text"] { width: 100%; margin-top: .35rem; border: 1px solid #bab2a5; border-radius: 4px; background: #fffefb; padding: .55rem; font: .9rem ui-monospace, SFMono-Regular, Menlo, monospace; }
select { font-family: inherit; } .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } .checks label { padding: .2rem 0; font-weight: 400; } .checks input { margin-right: .35rem; }
button { margin: 1rem 0; border: 0; border-radius: 4px; padding: .65rem 1rem; background: #b54c2d; color: white; font-weight: 700; cursor: pointer; } button:hover { background: #923b22; }
button:disabled { cursor: not-allowed; opacity: .5; } .draft-actions, .final-actions { margin-top: .5rem; padding: .75rem 1rem; border: 1px solid #d8d2c5; background: #fcfaf5; } .draft-actions button, .final-actions button { margin: .6rem .5rem 0 0; }
.continuity-panel { margin-bottom: 1.25rem; padding: 0 1rem 1rem; border: 1px solid #d8d2c5; background: #eef4f0; } .continuity-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; } .continuity-panel button { margin: .6rem 0 0; } .carry-actions { margin-top: 1rem; padding-top: .25rem; border-top: 1px solid #cbd8d0; } .carry-actions button { margin-right: .5rem; } .context-list { min-height: 6rem; max-height: 12rem; overflow: auto; margin-top: .35rem; padding: .45rem; border: 1px solid #bab2a5; background: #fffefb; } .context-list label { padding: .22rem 0; font-weight: 400; }
.lexicon-selector { margin-top: 1rem; padding-top: .25rem; border-top: 1px solid #cbd8d0; } .lexicon-selector button { margin-right: .5rem; }
.create-scene { margin: 1.5rem -1.5rem 0; padding: .9rem 1.5rem 0; border-top: 1px solid #d8d2c5; } .create-scene h2 { margin-top: 0; } .create-scene button { width: 100%; margin: .7rem 0 .25rem; }
.dashboard-panel { margin-bottom: 1.25rem; padding: 0 1rem 1rem; border: 1px solid #d8d2c5; background: #fcfaf5; } .dashboard-panel h2 { margin-top: 1rem; } .thread-tree { margin: .75rem 0; padding: .65rem .8rem; border-left: 3px solid #4e7b6b; background: #eef4f0; } .thread-tree > .muted { margin: 0 0 .3rem; } .thread-tree ul { list-style: none; margin: 0; padding-left: 1.35rem; } .thread-tree > ul { padding-left: 0; } .thread-tree li { position: relative; padding: .25rem 0; line-height: 1.4; } .thread-tree li li::before { content: ""; position: absolute; top: 0; bottom: 0; left: -.82rem; border-left: 1px solid #91aa9d; } .thread-tree li li::after { content: ""; position: absolute; top: .9rem; left: -.82rem; width: .55rem; border-top: 1px solid #91aa9d; } .tree-state { margin-left: .4rem; color: #49675c; font-size: .75rem; font-weight: 650; } #continuity-dashboard { max-height: 19rem; overflow: auto; } table { width: 100%; border-collapse: collapse; font-size: .82rem; } th, td { padding: .45rem .5rem; border-bottom: 1px solid #e3ddd1; text-align: left; vertical-align: top; } th { position: sticky; top: 0; background: #fcfaf5; } .scene-jump { margin: 0; padding: .15rem .35rem; background: #3d6e61; font-size: .78rem; } .scene-jump:hover { background: #2c5148; }
.thread-controls { margin: .8rem 0 1rem; padding: .75rem; border: 1px solid #cbd8d0; background: #f8fbf8; } .thread-controls .checkbox { margin-top: 0; } .thread-controls button { margin-bottom: 0; }
#working-draft { min-height: 28rem; max-height: 65vh; resize: vertical; background: #fffefb; color: #17212b; font: .9rem/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
.summary-panel, .updates-panel { margin-top: 1.25rem; padding: 0 1rem 1rem; border: 1px solid #d8d2c5; background: #fcfaf5; } .proposal-list { margin: .75rem 0; } .proposal { margin: .6rem 0; padding: .6rem; border: 1px solid #d8d2c5; background: #fffefb; font-weight: 650; } .proposal textarea, .proposal input:not([type]) { margin-top: .55rem; font-weight: 400; }
pre { min-height: 28rem; max-height: 65vh; overflow: auto; margin: 0; padding: 1rem; border: 1px solid #d8d2c5; background: #202525; color: #e5f3ed; white-space: pre-wrap; font: .82rem/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }