From d24c55ded74c4e1f3b318daa26c5e2b2647e0a3b Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 3 Jun 2026 08:33:10 -0400 Subject: [PATCH] book(C.5.5): textStyles editor in Document settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface book.spec.textStyles as an editable Typography section inside the Document settings modal so authors can override the v1 markdown stylesheet without dropping into JSON. - Per-group schema lists the fields to surface (font/size/weight/color/ alignment/margins/etc). Adding a row in BOOK_TEXT_STYLE_FIELDS makes the field render + persist automatically. - renderBookTypographyEditor merges defaults under the live spec when populating, so selects show the effective value rather than the empty default — e.g. a default fontWeight of 700 doesn't render as 400 just because the user hasn't customized it yet. - Apply merges editor output over existing spec + defaults so partial customizations don't wipe untouched fields. - Reset button restores defaults in the editor without applying. - After Apply, render() repaints reflow tiles and any open chapter editor's readout refreshes — typography changes can move page breaks. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 150 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) 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 `
${escapeHtml(group.label)}
${inputs}
`; + }).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);