Add a Description field to the Guide information dialog
Reuses the guide's existing descriptionHtml/descriptionText, which already renders on the PDF cover and at the top of every other export format, so it surfaces alongside author/co-authors/organization with no exporter changes needed. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
+11
-2
@@ -640,14 +640,19 @@ function showPlaceholdersDialog({ title = 'Placeholders', hint = '', values = {}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional document metadata (author, co-authors, organization) shown at the
|
* Optional document metadata (author, co-authors, organization, description)
|
||||||
* top of the guide, below the title, and surfaced on the PDF cover page.
|
* shown at the top of the guide, below the title, and surfaced on the PDF
|
||||||
|
* cover page and the top of other export formats.
|
||||||
*/
|
*/
|
||||||
function showGuideInfoDialog({ values = {}, onSave } = {}) {
|
function showGuideInfoDialog({ values = {}, onSave } = {}) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const authorInput = makeInput(values.author || '', 'text', { placeholder: 'e.g. Jane Doe' });
|
const authorInput = makeInput(values.author || '', 'text', { placeholder: 'e.g. Jane Doe' });
|
||||||
const coAuthorsInput = makeInput(values.coAuthors || '', 'text', { placeholder: 'e.g. Alex Lee, Sam Patel' });
|
const coAuthorsInput = makeInput(values.coAuthors || '', 'text', { placeholder: 'e.g. Alex Lee, Sam Patel' });
|
||||||
const organizationInput = makeInput(values.organization || '', 'text', { placeholder: 'e.g. Acme Corp' });
|
const organizationInput = makeInput(values.organization || '', 'text', { placeholder: 'e.g. Acme Corp' });
|
||||||
|
const descriptionInput = el('textarea', {
|
||||||
|
rows: 4,
|
||||||
|
placeholder: 'A short summary of this guide.',
|
||||||
|
}, values.description || '');
|
||||||
|
|
||||||
const { close } = openModal({
|
const { close } = openModal({
|
||||||
title: 'Guide information',
|
title: 'Guide information',
|
||||||
@@ -657,6 +662,9 @@ function showGuideInfoDialog({ values = {}, onSave } = {}) {
|
|||||||
labeledRow('Author', authorInput),
|
labeledRow('Author', authorInput),
|
||||||
labeledRow('Co-authors', coAuthorsInput),
|
labeledRow('Co-authors', coAuthorsInput),
|
||||||
labeledRow('Organization', organizationInput),
|
labeledRow('Organization', organizationInput),
|
||||||
|
labeledRow('Description', descriptionInput, { stacked: true }),
|
||||||
|
el('div.muted', { style: { marginTop: '-4px' } },
|
||||||
|
'Shown on the first page of the PDF and at the top of other export formats.'),
|
||||||
),
|
),
|
||||||
footer: [
|
footer: [
|
||||||
el('button', { onClick: () => { close(); resolve(false); } }, 'Cancel'),
|
el('button', { onClick: () => { close(); resolve(false); } }, 'Cancel'),
|
||||||
@@ -666,6 +674,7 @@ function showGuideInfoDialog({ values = {}, onSave } = {}) {
|
|||||||
author: authorInput.value.trim(),
|
author: authorInput.value.trim(),
|
||||||
coAuthors: coAuthorsInput.value.trim(),
|
coAuthors: coAuthorsInput.value.trim(),
|
||||||
organization: organizationInput.value.trim(),
|
organization: organizationInput.value.trim(),
|
||||||
|
description: descriptionInput.value.trim(),
|
||||||
});
|
});
|
||||||
close();
|
close();
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
|||||||
@@ -1574,9 +1574,13 @@ class GuideEditor {
|
|||||||
async openGuideInfo() {
|
async openGuideInfo() {
|
||||||
if (!this.guide) return;
|
if (!this.guide) return;
|
||||||
await dialogs.showGuideInfoDialog({
|
await dialogs.showGuideInfoDialog({
|
||||||
values: this.guide.metadata || {},
|
values: {
|
||||||
onSave: async (metadata) => {
|
...this.guide.metadata,
|
||||||
|
description: htmlToPlainText(this.guide.descriptionHtml || ''),
|
||||||
|
},
|
||||||
|
onSave: async ({ description, ...metadata }) => {
|
||||||
this.guide.metadata = metadata;
|
this.guide.metadata = metadata;
|
||||||
|
this.guide.descriptionHtml = textToHtml(description);
|
||||||
await api.guide.save({ guide: this.guide });
|
await api.guide.save({ guide: this.guide });
|
||||||
this.onToast('Guide information saved.');
|
this.onToast('Guide information saved.');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -187,3 +187,21 @@ function fmtDate(iso) {
|
|||||||
|
|
||||||
const escapeHtml = (s) => String(s ?? '')
|
const escapeHtml = (s) => String(s ?? '')
|
||||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
|
||||||
|
/** Inverse of textToHtml, for loading sanitized description HTML into a plain textarea. */
|
||||||
|
function htmlToPlainText(html) {
|
||||||
|
return String(html || '')
|
||||||
|
.replace(/<(br|\/p|\/div|\/li|\/h[1-6])\s*\/?>/gi, '\n')
|
||||||
|
.replace(/<[^>]*>/g, '')
|
||||||
|
.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '\'').replace(/&/g, '&')
|
||||||
|
.replace(/[ \t]+/g, ' ').replace(/\s*\n\s*/g, '\n').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Plain textarea text -> sanitizer-allowed paragraph HTML (blank line = new paragraph). */
|
||||||
|
function textToHtml(text) {
|
||||||
|
const trimmed = String(text || '').trim();
|
||||||
|
if (!trimmed) return '';
|
||||||
|
return trimmed.split(/\n{2,}/)
|
||||||
|
.map((para) => `<p>${escapeHtml(para).replace(/\n/g, '<br>')}</p>`)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user