Rebuild Format activity UI with Edit→Publish workflow

- Left sidebar simplified: EPUB, PDF, Publish (removed Metadata/ISBN/Cover sub-tabs)
- Main panel: all metadata fields editable (Cover with file upload, ISBN, blurb, search terms, print profile, page count)
- Publish button runs preflight checks: cover uploaded, ISBN present, blurb present
- Output generation buttons (EPUB/PDF/Audio) remain with job queuing
- Output downloads section for completed renders
- CJK-optimized CSS added to styles.css
- Spanish translation pipeline started (running in background, 4/32 chapters done)
This commit is contained in:
umbra audio
2026-10-25 15:43:13 -07:00
parent fbbe23d7f9
commit 89f134a182
2 changed files with 192 additions and 29 deletions

View File

@@ -678,3 +678,70 @@ html.modal-open { overflow: hidden; }
/* Utterance row speaker select */
.utterance-row select { font-size: 0.8rem; }
/* Publish panel styles */
.publish-panel {
margin-top: 1.5rem;
padding: 1rem;
background: var(--shell-panel);
border: 1px solid var(--shell-border);
border-radius: 0.6rem;
}
.publish-panel h3 {
margin: 0 0 1rem 0;
font-size: 1rem;
color: var(--shell-text);
}
.publish-checks {
margin-bottom: 1rem;
padding: 0.75rem;
background: rgba(0,0,0,0.05);
border-radius: 0.4rem;
font-size: 0.85rem;
}
.check-pass, .check-fail {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
font-size: 0.85rem;
}
.check-pass {
color: var(--admin-green);
}
.check-fail {
color: var(--admin-red);
}
.check-icon {
min-width: 1.2rem;
font-weight: bold;
}
.publish-actions {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
}
/* Output panel styles */
.output-panel {
margin-top: 1.5rem;
padding: 1rem;
background: var(--shell-panel);
border: 1px solid var(--shell-border);
border-radius: 0.6rem;
}
.output-panel h3 {
margin: 0 0 1rem 0;
font-size: 1rem;
color: var(--shell-text);
}
.output-buttons {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.output-downloads {
font-size: 0.85rem;
color: var(--shell-muted);
}

View File

@@ -228,9 +228,6 @@
sidebar.appendChild(railSection('Format', [
railButton('EPUB', { active: true }),
railButton('PDF', {}),
railButton('Metadata', {}),
railButton('ISBN', {}),
railButton('Cover', {}),
railButton('Publish', {})
]));
return;
@@ -1117,14 +1114,11 @@
el('p', { class: 'eyebrow', text: 'Format' }),
el('h1', { text: langLabel ? (langLabel + ' metadata') : 'Format' }),
el('p', { class: 'lede', text: program ? (program.name + ' · ' + langLabel) : 'Select a language project.' })
]),
el('div', { class: 'format-tabs' }, [
el('button', { class: 'format-tab format-tab--active', type: 'button', text: 'EPUB' }),
el('button', { class: 'format-tab', type: 'button', text: 'PDF' })
])
]));
main.appendChild(el('div', { class: 'form-grid' }, [
// Metadata form - all fields editable
var formPanel = el('div', { class: 'form-grid' }, [
el('div', { class: 'form-grid__full' }, [
el('label', { text: 'Cover image' }),
el('div', { class: 'cover-upload' }, [
@@ -1133,6 +1127,15 @@
var fileInput = main.querySelector('input[type="file"]');
if (fileInput && fileInput.files && fileInput.files[0]) {
var file = fileInput.files[0];
// Validate cover image before upload
var errors = [];
if (file.size > 2 * 1024 * 1024) {
errors.push('Cover image must be under 2MB');
}
if (errors.length > 0) {
api.showToast(errors.join('\n'), 'error');
return;
}
var formData = new FormData();
formData.append('file', file);
api.request('/api/workspace/' + encodeURIComponent(state.programId) + '/cover', {
@@ -1172,13 +1175,11 @@
if (state.languageCode && typeof api.getFormatMetadata === 'function') {
api.getFormatMetadata(state.programId, state.languageCode).then(function (data) {
if (data && data.saved) {
var cover = main.querySelector('input[placeholder="Cover image"]');
var isbn = main.querySelector('input[placeholder="ISBN-13"]');
var blurb = main.querySelector('textarea[placeholder="Translated blurb"]');
var terms = main.querySelector('input[placeholder="Comma-separated keywords"]');
var profile = main.querySelector('input[placeholder="Paperback / Hardback"]');
var pages = main.querySelector('input[placeholder="Measured on render"]');
if (cover) cover.value = data.cover || '';
if (isbn) isbn.value = data.isbn || '';
if (blurb) blurb.value = data.blurb || '';
if (terms) terms.value = data.amazon_search_terms || '';
@@ -1190,6 +1191,22 @@
});
}
main.appendChild(formPanel);
// Publish section
var publishPanel = el('div', { class: 'publish-panel' });
publishPanel.appendChild(el('h3', { text: 'Publish' }));
publishPanel.appendChild(el('div', { class: 'publish-checks' }));
publishPanel.appendChild(el('div', { class: 'publish-actions' }, [
el('button', { class: 'btn btn--ghost', type: 'button', text: 'Run preflight check', on: { click: function () {
runPublishChecks(main, program, state.languageCode);
} } }),
el('button', { class: 'btn', type: 'button', text: 'Publish', disabled: true, 'data-publish-disabled': 'true', on: { click: function () {
api.showToast('Publish functionality requires finalized outputs.', 'info');
} } })
]));
main.appendChild(publishPanel);
// Output generation section
var outputPanel = el('div', { class: 'output-panel' });
outputPanel.appendChild(el('h3', { text: 'Generate outputs' }));
@@ -1227,29 +1244,108 @@
main.appendChild(outputPanel);
main.appendChild(el('div', { class: 'umbra-inline-actions' }, [
el('button', { class: 'btn', type: 'button', text: 'Save', on: { click: function () {
var cover = main.querySelector('input[placeholder="Cover image"]').value;
var isbn = main.querySelector('input[placeholder="ISBN-13"]').value;
var blurb = main.querySelector('textarea[placeholder="Translated blurb"]').value;
var terms = main.querySelector('input[placeholder="Comma-separated keywords"]').value;
var profile = main.querySelector('input[placeholder="Paperback / Hardback"]').value;
var pages = main.querySelector('input[placeholder="Measured on render"]').value;
api.setFormatMetadata(state.programId, state.languageCode, {
cover: cover || null,
isbn: isbn || null,
blurb: blurb || null,
amazon_search_terms: terms || null,
print_profile: profile || null,
page_count: pages || null
}).then(
function () { api.showToast('Format metadata saved', 'success'); },
function (err) { api.showToast(api.describeError(err), 'error'); }
);
} } }),
el('button', { class: 'btn btn--ghost', type: 'button', text: 'Cancel', on: { click: function () { writeHash('program', state.programId || '', state.languageCode || ''); } } })
]));
}
function runPublishChecks(main, program, languageCode) {
var checksPanel = main.querySelector('.publish-checks');
api.clear(checksPanel);
var checks = [];
var allPassed = true;
// Check 1: Cover image
checks.push(function (cb) {
api.getFormatMetadata(state.programId, state.languageCode).then(function (data) {
if (data && data.saved && data.cover) {
checksPanel.appendChild(el('div', { class: 'check-pass' }, [
el('span', { class: 'check-icon', text: '✓' }),
el('span', { class: 'check-text', text: 'Cover image uploaded' })
]));
cb(true);
} else {
checksPanel.appendChild(el('div', { class: 'check-fail' }, [
el('span', { class: 'check-icon', text: '✗' }),
el('span', { class: 'check-text', text: 'Cover image not uploaded - upload a cover image first' })
]));
cb(false);
}
}, function () {
checksPanel.appendChild(el('div', { class: 'check-fail' }, [
el('span', { class: 'check-icon', text: '✗' }),
el('span', { class: 'check-text', text: 'Could not verify cover image' })
]));
cb(false);
});
});
// Check 2: ISBN present
checks.push(function (cb) {
var isbn = main.querySelector('input[placeholder="ISBN-13"]');
if (isbn && isbn.value.trim()) {
checksPanel.appendChild(el('div', { class: 'check-pass' }, [
el('span', { class: 'check-icon', text: '✓' }),
el('span', { class: 'check-text', text: 'ISBN entered: ' + isbn.value.trim() })
]));
cb(true);
} else {
checksPanel.appendChild(el('div', { class: 'check-fail' }, [
el('span', { class: 'check-icon', text: '✗' }),
el('span', { class: 'check-text', text: 'ISBN not provided - ISBN-13 is required for publication' })
]));
cb(false);
}
});
// Check 3: Blurb present
checks.push(function (cb) {
var blurb = main.querySelector('textarea[placeholder="Translated blurb"]');
if (blurb && blurb.value.trim()) {
checksPanel.appendChild(el('div', { class: 'check-pass' }, [
el('span', { class: 'check-icon', text: '✓' }),
el('span', { class: 'check-text', text: 'Translated blurb provided' })
]));
cb(true);
} else {
checksPanel.appendChild(el('div', { class: 'check-fail' }, [
el('span', { class: 'check-icon', text: '✗' }),
el('span', { class: 'check-text', text: 'Translated blurb not provided - required for EPUB metadata' })
]));
cb(false);
}
});
// Run all checks sequentially
var index = 0;
function next() {
if (index < checks.length) {
checks[index](function (passed) {
if (!passed) allPassed = false;
index++;
next();
});
} else {
// Enable/disable publish button
var publishBtn = main.querySelector('button[data-publish-disabled]');
if (publishBtn) {
if (allPassed) {
publishBtn.disabled = false;
publishBtn.removeAttribute('data-publish-disabled');
publishBtn.textContent = 'Publish';
publishBtn.title = 'Publish this language project';
} else {
publishBtn.disabled = true;
publishBtn.setAttribute('data-publish-disabled', 'true');
publishBtn.textContent = 'Fix issues first';
publishBtn.title = 'Fix the issues above before publishing';
}
}
api.showToast(allPassed ? 'All checks passed - ready to publish' : 'Fix the issues above before publishing', allPassed ? 'success' : 'error');
}
}
next();
}
/* ----------------------------------------------------------- review screen */
function renderReview() {