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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user