diff --git a/src/index.html b/src/index.html
index 3d9001a..9dc05eb 100644
--- a/src/index.html
+++ b/src/index.html
@@ -5221,6 +5221,15 @@
+
+ Typography (chapter text styles)
+ Overrides the v1 markdown stylesheet for reflow chapters. Empty fields fall back to defaults.
+
+
+
+
+
+
@@ -8360,6 +8369,7 @@
if (numDetails) numDetails.style.display = spec.pageNumbering?.enabled ? '' : 'none';
_syncBookSettingsSpineNote();
_syncBookSettingsPresetLock();
+ renderBookTypographyEditor(spec.textStyles);
modal.style.display = 'flex';
modal.setAttribute('aria-hidden', 'false');
if (typeof bringModalToFront === 'function') bringModalToFront(modal);
@@ -8398,6 +8408,121 @@
trimH.disabled = false;
}
}
+ // Schema for the Typography editor. Each group lists the fields
+ // we surface in the UI; defaults come from defaultBookTextStyles().
+ // Add a field here and it auto-renders + persists with the spec.
+ const BOOK_TEXT_STYLE_GROUPS = [
+ { key: 'body', label: 'Body' },
+ { key: 'h1', label: 'Heading 1' },
+ { key: 'h2', label: 'Heading 2' },
+ { key: 'h3', label: 'Heading 3' },
+ { key: 'h4', label: 'Heading 4' },
+ { key: 'h5', label: 'Heading 5' },
+ { key: 'h6', label: 'Heading 6' },
+ { key: 'blockquote', label: 'Blockquote' },
+ { key: 'codeBlock', label: 'Code block' },
+ { key: 'inlineCode', label: 'Inline code' },
+ { key: 'link', label: 'Link' },
+ { key: 'list', label: 'List' },
+ { key: 'hr', label: 'Horizontal rule' },
+ ];
+ const BOOK_TEXT_STYLE_FIELDS = {
+ body: [ ['fontFamily','Font','text'], ['fontSize','Size (pt)','number',0.5,4,72], ['lineHeight','Line height','number',0.05,0.5,3], ['color','Color','color'], ['alignment','Align','select',['left','right','center','justify']], ['marginAfter','Margin after (in)','number',0.01,0,2] ],
+ h1: [ ['fontFamily','Font','text'], ['fontSize','Size (pt)','number',0.5,4,96], ['fontWeight','Weight','select',[400,500,600,700,800]], ['alignment','Align','select',['left','right','center','justify']], ['marginBefore','Margin before (in)','number',0.01,0,2], ['marginAfter','Margin after (in)','number',0.01,0,2] ],
+ h2: [ ['fontFamily','Font','text'], ['fontSize','Size (pt)','number',0.5,4,72], ['fontWeight','Weight','select',[400,500,600,700,800]], ['marginBefore','Margin before (in)','number',0.01,0,2], ['marginAfter','Margin after (in)','number',0.01,0,2] ],
+ h3: [ ['fontFamily','Font','text'], ['fontSize','Size (pt)','number',0.5,4,72], ['fontWeight','Weight','select',[400,500,600,700,800]], ['marginBefore','Margin before (in)','number',0.01,0,2], ['marginAfter','Margin after (in)','number',0.01,0,2] ],
+ h4: [ ['fontFamily','Font','text'], ['fontSize','Size (pt)','number',0.5,4,72], ['fontWeight','Weight','select',[400,500,600,700,800]] ],
+ h5: [ ['fontFamily','Font','text'], ['fontSize','Size (pt)','number',0.5,4,72], ['fontWeight','Weight','select',[400,500,600,700,800]] ],
+ h6: [ ['fontFamily','Font','text'], ['fontSize','Size (pt)','number',0.5,4,72], ['fontWeight','Weight','select',[400,500,600,700,800]], ['fontStyle','Style','select',['normal','italic']] ],
+ blockquote: [ ['fontStyle','Style','select',['normal','italic']], ['marginLeft','Indent left (in)','number',0.01,0,2], ['marginRight','Indent right (in)','number',0.01,0,2] ],
+ codeBlock: [ ['fontFamily','Font','text'], ['fontSize','Size (pt)','number',0.5,4,72], ['background','Background','color'], ['padding','Padding (in)','number',0.01,0,1] ],
+ inlineCode: [ ['fontFamily','Font','text'], ['fontSize','Size (pt)','number',0.5,4,72], ['background','Background','color'] ],
+ link: [ ['color','Color','color'], ['underline','Underline','bool'] ],
+ list: [ ['indent','Indent (in)','number',0.01,0,1], ['marginAfter','Margin after (in)','number',0.01,0,2] ],
+ hr: [ ['color','Color','color'], ['thickness','Thickness (pt)','number',0.1,0,8], ['marginBefore','Margin before (in)','number',0.01,0,2], ['marginAfter','Margin after (in)','number',0.01,0,2] ],
+ };
+
+ // Render the typography editor body into bookSettingsTypographyBody
+ // from a textStyles object (defaults to live spec when null).
+ function renderBookTypographyEditor(textStyles) {
+ const host = document.getElementById('bookSettingsTypographyBody');
+ if (!host) return;
+ // Show the effective styles by merging defaults under user
+ // overrides — otherwise selects would render wrong (e.g. a
+ // fontWeight of 700 from defaults would show as 400 if the
+ // user hasn't customized it yet).
+ const defaults = defaultBookTextStyles();
+ const base = textStyles || state.book?.spec?.textStyles || {};
+ const styles = {};
+ const allKeys = new Set([...Object.keys(defaults), ...Object.keys(base)]);
+ for (const k of allKeys) styles[k] = { ...(defaults[k] || {}), ...(base[k] || {}) };
+ const html = BOOK_TEXT_STYLE_GROUPS.map(group => {
+ const fields = BOOK_TEXT_STYLE_FIELDS[group.key] || [];
+ const style = styles[group.key] || {};
+ const inputs = fields.map(([name, label, type, step, min, max]) => {
+ const id = `bookSettingsTypo-${group.key}-${name}`;
+ const v = style[name];
+ if (type === 'select') {
+ const options = step; // 4th slot for select holds the options array
+ const opts = options.map(o => `
`).join('');
+ return `
`;
+ }
+ if (type === 'bool') {
+ return `
`;
+ }
+ if (type === 'color') {
+ const val = (typeof v === 'string' && /^#[0-9a-f]{3,8}$/i.test(v)) ? v : '#000000';
+ return `
`;
+ }
+ if (type === 'number') {
+ const stepAttr = step != null ? ` step="${step}"` : '';
+ const minAttr = min != null ? ` min="${min}"` : '';
+ const maxAttr = max != null ? ` max="${max}"` : '';
+ const val = (typeof v === 'number' && Number.isFinite(v)) ? v : '';
+ return `
`;
+ }
+ // default text
+ const val = (typeof v === 'string') ? v : '';
+ return `
`;
+ }).join('');
+ return `
`;
+ }).join('');
+ host.innerHTML = html;
+ }
+ // Read the editor inputs back into a textStyles object. Empty
+ // inputs leave the field unset, so the engine's defaults apply.
+ function readBookTypographyEditor() {
+ const out = {};
+ for (const group of BOOK_TEXT_STYLE_GROUPS) {
+ const fields = BOOK_TEXT_STYLE_FIELDS[group.key] || [];
+ const obj = {};
+ for (const [name, , type, , , , ] of fields) {
+ const id = `bookSettingsTypo-${group.key}-${name}`;
+ const el = document.getElementById(id);
+ if (!el) continue;
+ if (type === 'select') {
+ const raw = el.value;
+ if (raw === '') continue;
+ const asNum = Number(raw);
+ obj[name] = (raw !== '' && !isNaN(asNum) && String(asNum) === raw) ? asNum : raw;
+ } else if (type === 'bool') {
+ obj[name] = !!el.checked;
+ } else if (type === 'color') {
+ obj[name] = el.value;
+ } else if (type === 'number') {
+ if (el.value === '') continue;
+ const v = Number(el.value);
+ if (Number.isFinite(v)) obj[name] = v;
+ } else {
+ const v = (el.value || '').trim();
+ if (v) obj[name] = v;
+ }
+ }
+ if (Object.keys(obj).length) out[group.key] = obj;
+ }
+ return out;
+ }
+
function applyBookDocumentSettings() {
const book = ensureBookState();
pushHistory('book: document settings');
@@ -8423,9 +8548,33 @@
firstNumberedPage: Math.max(1, Math.round(num('bookSettingsNumberingFirst', 1))),
format: ($('bookSettingsNumberingFormat')?.value || '{n}'),
};
+ // Merge the typography editor onto the live textStyles so any
+ // un-edited groups (or fields outside the schema) survive.
+ try {
+ const edited = readBookTypographyEditor();
+ const defaults = defaultBookTextStyles();
+ const existing = book.spec.textStyles || {};
+ const merged = {};
+ const keys = new Set([
+ ...Object.keys(defaults),
+ ...Object.keys(existing),
+ ...Object.keys(edited),
+ ]);
+ for (const k of keys) {
+ merged[k] = { ...(defaults[k] || {}), ...(existing[k] || {}), ...(edited[k] || {}) };
+ }
+ book.spec.textStyles = merged;
+ } catch (err) {
+ console.warn('typography apply failed', err);
+ }
closeBookDocumentSettings();
syncInputs();
render();
+ // Repaint chapter-editor readouts if a chapter is open — the
+ // typography change can move the page break.
+ if (state.book?.editingChapterId && typeof syncBookChapterEditor === 'function') {
+ try { syncBookChapterEditor(); } catch {}
+ }
}
// Resolve a page's effective master — its own `masterId`
@@ -32794,6 +32943,7 @@
on('bookDocumentSettingsCloseBtn', 'click', closeBookDocumentSettings);
on('bookSettingsCancelBtn', 'click', closeBookDocumentSettings);
on('bookSettingsApplyBtn', 'click', applyBookDocumentSettings);
+ on('bookSettingsTypographyResetBtn', 'click', () => renderBookTypographyEditor(defaultBookTextStyles()));
const _bookDocModal = $('bookDocumentSettingsModal');
if (_bookDocModal) _bookDocModal.addEventListener('click', (e) => { if (e.target === _bookDocModal) closeBookDocumentSettings(); });
on('bookSettingsPreset', 'change', _syncBookSettingsPresetLock);