Updated layout, added ability to make new sections, added new block types, increased layout size by 30%

This commit is contained in:
nathan
2026-05-01 14:37:36 -04:00
parent a472255a94
commit d0200f3975
7 changed files with 841 additions and 35 deletions

View File

@@ -5,6 +5,8 @@
*/
class BlockRenderer {
static galleryUid = 0;
static render(blocks) {
if (!Array.isArray(blocks)) return '';
return blocks.map(function(block) { return BlockRenderer.renderBlock(block); }).join('\n');
@@ -17,6 +19,8 @@ class BlockRenderer {
case 'paragraph': return BlockRenderer.renderParagraph(block);
case 'image': return BlockRenderer.renderImage(block);
case 'quote': return BlockRenderer.renderQuote(block);
case 'fact-panel': return BlockRenderer.renderFactPanel(block);
case 'timeline-card': return BlockRenderer.renderTimelineCard(block);
case 'gallery': return BlockRenderer.renderGallery(block);
case 'grid4': return BlockRenderer.renderGrid4(block);
case 'statistics': return BlockRenderer.renderStatistics(block);
@@ -48,15 +52,170 @@ class BlockRenderer {
+ '</blockquote>';
}
static renderGallery(block) {
var images = block.images || [];
var items = images.map(function(img) {
return '<div class="gallery-item">'
+ '<img src="' + (img.src || '') + '" alt="' + (img.alt || '') + '" loading="lazy">'
+ (img.caption ? '<div class="gallery-item-caption">' + img.caption + '</div>' : '')
+ '</div>';
static renderFactPanel(block) {
var title = block.title || 'Fact Briefing';
var factItems = block.facts || block.items || [];
var facts = factItems.map(function(item) {
if (typeof item === 'string') return item;
return item && item.text ? item.text : '';
}).filter(function(text) {
return String(text).trim() !== '';
});
if (facts.length === 0) {
facts = ['Add fact entries in the builder to populate this panel.'];
}
var listHtml = facts.map(function(text) {
return '<li>' + text + '</li>';
}).join('');
return '<div class="block block-gallery"><div class="gallery-scroll">' + items + '</div></div>';
return '<section class="block block-fact-panel fact-box">'
+ '<h3>' + title + '</h3>'
+ '<ul class="fact-panel-list">' + listHtml + '</ul>'
+ '</section>';
}
static renderTimelineCard(block) {
return '<article class="block block-timeline-card">'
+ '<header class="block-timeline-header">'
+ '<span class="block-timeline-date">' + (block.date || '') + '</span>'
+ '<h4 class="block-timeline-title">' + (block.title || '') + '</h4>'
+ '</header>'
+ '<p class="block-timeline-text">' + (block.text || '') + '</p>'
+ '</article>';
}
static renderGallery(block) {
var images = Array.isArray(block.images) ? block.images.filter(function(img) {
return img && img.src;
}) : [];
if (images.length === 0) {
return '<div class="block block-gallery"><div class="gallery-empty">No gallery images configured.</div></div>';
}
// Backward compatible: explicit strip mode keeps the original horizontal gallery.
var mode = (block.mode || 'carousel').toLowerCase();
if (mode === 'strip') {
var stripItems = images.map(function(img) {
return '<div class="gallery-item">'
+ '<img src="' + (img.src || '') + '" alt="' + (img.alt || '') + '" loading="lazy">'
+ (img.caption ? '<div class="gallery-item-caption">' + img.caption + '</div>' : '')
+ '</div>';
}).join('');
return '<div class="block block-gallery"><div class="gallery-scroll">' + stripItems + '</div></div>';
}
var id = 'gallery-' + (++BlockRenderer.galleryUid);
var autoplay = block.autoplay === false ? '0' : '1';
var intervalSec = parseInt(block.intervalSec, 10);
if (isNaN(intervalSec)) intervalSec = 5;
intervalSec = Math.max(2, Math.min(intervalSec, 15));
var showDots = block.showDots === false ? '0' : '1';
var showArrows = block.showArrows === false ? '0' : '1';
var slides = images.map(function(img, index) {
return '<figure class="gallery-slide' + (index === 0 ? ' is-active' : '') + '" data-slide-index="' + index + '">'
+ '<img src="' + (img.src || '') + '" alt="' + (img.alt || '') + '" loading="lazy">'
+ (img.caption ? '<figcaption class="gallery-slide-caption">' + img.caption + '</figcaption>' : '')
+ '</figure>';
}).join('');
var dots = images.map(function(_, index) {
return '<button type="button" class="gallery-dot' + (index === 0 ? ' is-active' : '') + '" data-gallery-dot="' + index + '" aria-label="Go to image ' + (index + 1) + '"></button>';
}).join('');
return '<div class="block block-gallery gallery-mode-carousel" id="' + id + '" data-gallery="1" data-autoplay="' + autoplay + '" data-interval-ms="' + (intervalSec * 1000) + '">'
+ '<div class="gallery-carousel">'
+ '<div class="gallery-slides">' + slides + '</div>'
+ (showArrows === '1' && images.length > 1
? '<button type="button" class="gallery-nav gallery-nav-prev" data-gallery-prev aria-label="Previous image">&#x2039;</button>'
+ '<button type="button" class="gallery-nav gallery-nav-next" data-gallery-next aria-label="Next image">&#x203A;</button>'
: '')
+ '</div>'
+ (showDots === '1' && images.length > 1 ? '<div class="gallery-dots">' + dots + '</div>' : '')
+ '</div>';
}
static initGalleries(root) {
var scope = root || document;
if (!scope || !scope.querySelectorAll) return;
scope.querySelectorAll('[data-gallery="1"]').forEach(function(gallery) {
if (gallery.dataset.galleryInit === '1') return;
gallery.dataset.galleryInit = '1';
var slides = Array.from(gallery.querySelectorAll('.gallery-slide'));
if (slides.length <= 1) return;
var dots = Array.from(gallery.querySelectorAll('[data-gallery-dot]'));
var nextBtn = gallery.querySelector('[data-gallery-next]');
var prevBtn = gallery.querySelector('[data-gallery-prev]');
var index = 0;
var timerId = null;
var autoplay = gallery.dataset.autoplay !== '0';
var intervalMs = parseInt(gallery.dataset.intervalMs, 10);
if (isNaN(intervalMs)) intervalMs = 5000;
intervalMs = Math.max(2000, Math.min(intervalMs, 15000));
function showAt(newIndex) {
index = (newIndex + slides.length) % slides.length;
slides.forEach(function(slide, i) {
slide.classList.toggle('is-active', i === index);
});
dots.forEach(function(dot, i) {
dot.classList.toggle('is-active', i === index);
});
}
function stopTimer() {
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}
function scheduleNext() {
stopTimer();
if (!autoplay) return;
timerId = setTimeout(function tick() {
if (!document.body.contains(gallery)) {
stopTimer();
return;
}
showAt(index + 1);
timerId = setTimeout(tick, intervalMs);
}, intervalMs);
}
if (nextBtn) {
nextBtn.addEventListener('click', function() {
showAt(index + 1);
scheduleNext();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', function() {
showAt(index - 1);
scheduleNext();
});
}
dots.forEach(function(dot) {
dot.addEventListener('click', function() {
showAt(parseInt(dot.dataset.galleryDot, 10) || 0);
scheduleNext();
});
});
gallery.addEventListener('pointerenter', stopTimer);
gallery.addEventListener('pointerleave', scheduleNext);
showAt(0);
scheduleNext();
});
}
static renderGrid4(block) {

View File

@@ -6,29 +6,75 @@
class SectionBuilder {
constructor() {
this.sections = {
context: { title: 'Historical Context', icon: '🌍', color: '#d4a574', blocks: [] },
rcaf: { title: 'RCAF Desert Cats', icon: '✈️', color: '#3498db', blocks: [] },
rcn: { title: 'RCN Task Group', icon: '⚓', color: '#2c3e50', blocks: [] },
army: { title: 'Canadian Army', icon: '🏥', color: '#27ae60', blocks: [] },
legacy: { title: 'After the War', icon: '🍁', color: '#c0392b', blocks: [] }
};
this.sections = this.buildDefaultSections();
this.sectionsStorageKey = 'pf-builder-sections';
this.currentSection = 'context';
this.editingIndex = null;
this.defaultContent = null;
this.init();
}
buildDefaultSections() {
return {
context: {
title: 'Historical Context',
subtitle: 'Coalition Building & Diplomacy',
icon: '🌍',
color: '#d4a574',
description: 'Understand how Iraq\'s invasion of Kuwait triggered the largest military coalition since WWII and Canada\'s diplomatic role.',
stats: { articles: 7, storyMaps: 3, images: 45 },
blocks: []
},
rcaf: {
title: 'RCAF Desert Cats',
subtitle: '409 Squadron Air Operations',
icon: '✈️',
color: '#3498db',
description: 'Follow the Desert Cats from defensive patrols to strike missions, including the TNC-45 engagement and bombing runs.',
stats: { articles: 8, storyMaps: 4, images: 60 },
blocks: []
},
rcn: {
title: 'RCN Task Group',
subtitle: 'Naval Blockade Operations',
icon: '⚓',
color: '#2c3e50',
description: 'The transformation of HMCS Terra Nova into a guided-missile destroyer and enforcement of UN sanctions.',
stats: { articles: 7, storyMaps: 2, images: 35 },
blocks: []
},
army: {
title: 'Canadian Army',
subtitle: 'Medical & Security Forces',
icon: '🏥',
color: '#27ae60',
description: '1 Canadian Field Hospital\'s life-saving work and security operations protecting coalition installations.',
stats: { articles: 7, storyMaps: 3, images: 40 },
blocks: []
},
legacy: {
title: 'After the War',
subtitle: 'Veterans & Long-term Impact',
icon: '🍁',
color: '#c0392b',
description: 'Gulf War Syndrome, environmental cleanup, peacekeeping missions, and ongoing support for veterans.',
stats: { articles: 7, storyMaps: 2, images: 30 },
blocks: []
}
};
}
async init() {
try {
var res = await fetch('data/sections-content.json');
if (res.ok) this.defaultContent = await res.json();
} catch (e) { console.warn('Could not load default content'); }
this.loadSectionsManifest();
this.loadAllFromStorage();
this.renderSectionTabs();
this.bindGlobalEvents();
this.loadSection('context');
this.loadSection(this.sections.context ? 'context' : Object.keys(this.sections)[0]);
}
// ── Storage ────────────────────────────────────────────────────────────
@@ -45,8 +91,72 @@ class SectionBuilder {
});
}
loadSectionsManifest() {
var saved = localStorage.getItem(this.sectionsStorageKey);
if (!saved) return;
try {
var parsed = JSON.parse(saved);
var existing = this.sections;
var merged = {};
var order = Array.isArray(parsed.order) ? parsed.order : Object.keys(parsed.sections || {});
var sectionMeta = parsed.sections || {};
order.forEach(function(id) {
if (!sectionMeta[id]) return;
merged[id] = {
title: sectionMeta[id].title || id,
subtitle: sectionMeta[id].subtitle || '',
description: sectionMeta[id].description || '',
icon: sectionMeta[id].icon || '📚',
color: sectionMeta[id].color || '#7f8c8d',
stats: {
articles: parseInt(sectionMeta[id].stats && sectionMeta[id].stats.articles, 10) || 0,
storyMaps: parseInt(sectionMeta[id].stats && sectionMeta[id].stats.storyMaps, 10) || 0,
images: parseInt(sectionMeta[id].stats && sectionMeta[id].stats.images, 10) || 0
},
blocks: (existing[id] && existing[id].blocks) || []
};
});
Object.keys(existing).forEach(function(id) {
if (!merged[id]) merged[id] = existing[id];
});
this.sections = merged;
} catch (e) {
console.warn('Could not parse saved section metadata');
}
}
saveSectionsManifest() {
var payload = { order: Object.keys(this.sections), sections: {} };
var self = this;
payload.order.forEach(function(id) {
payload.sections[id] = self.sectionMeta(id);
});
localStorage.setItem(this.sectionsStorageKey, JSON.stringify(payload));
}
sectionMeta(id) {
var section = this.sections[id] || {};
return {
title: section.title || id,
subtitle: section.subtitle || '',
description: section.description || '',
icon: section.icon || '📚',
color: section.color || '#7f8c8d',
stats: {
articles: parseInt(section.stats && section.stats.articles, 10) || 0,
storyMaps: parseInt(section.stats && section.stats.storyMaps, 10) || 0,
images: parseInt(section.stats && section.stats.images, 10) || 0
}
};
}
saveSection(id) {
localStorage.setItem('pf-section-' + id, JSON.stringify(this.sections[id].blocks));
this.saveSectionsManifest();
this.showToast(this.sections[id].title + ' saved!');
}
@@ -92,6 +202,9 @@ class SectionBuilder {
+ '</div>';
canvas.innerHTML = html;
if (typeof BlockRenderer !== 'undefined' && typeof BlockRenderer.initGalleries === 'function') {
BlockRenderer.initGalleries(canvas);
}
this.bindCanvasEvents();
}
@@ -173,7 +286,33 @@ class SectionBuilder {
case 'paragraph': return { id: id, type: type, text: 'Enter paragraph text here.' };
case 'image': return { id: id, type: type, src: '', alt: '', caption: '' };
case 'quote': return { id: id, type: type, text: 'Quote text here.', author: 'Source' };
case 'gallery': return { id: id, type: type, images: [{ src: '', alt: '', caption: '' }] };
case 'fact-panel': return {
id: id,
type: type,
title: 'Fact Briefing',
facts: [
{ text: 'Day 1: 4,000 U.S. troops' },
{ text: 'Day 30: 72,000 coalition troops' },
{ text: 'Day 60: 185,000 coalition troops' }
]
};
case 'timeline-card': return {
id: id,
type: type,
date: 'Sept 24',
title: 'Canadian Naval Task Group Arrives',
text: 'HMCS Terra Nova, Athabaskan, and Protecteur begin patrol operations in the central Persian Gulf.'
};
case 'gallery': return {
id: id,
type: type,
mode: 'carousel',
autoplay: true,
intervalSec: 5,
showDots: true,
showArrows: true,
images: [{ src: '', alt: '', caption: '' }]
};
case 'grid4': return { id: id, type: type, images: [
{ src: '', alt: '', caption: '' }, { src: '', alt: '', caption: '' },
{ src: '', alt: '', caption: '' }, { src: '', alt: '', caption: '' }
@@ -237,8 +376,46 @@ class SectionBuilder {
return this.formGroup('Quote Text', '<textarea name="text" rows="4">' + this.esc(block.text || '') + '</textarea>')
+ this.formGroup('Attribution (optional)', '<input type="text" name="author" value="' + this.esc(block.author || '') + '" placeholder="Name, Title, Date">');
case 'fact-panel':
return this.formGroup('Panel Title', '<input type="text" name="title" value="' + this.esc(block.title || '') + '" placeholder="e.g. Desert Shield Force Buildup">')
+ this.formGroup('Facts',
'<div id="factItems">'
+ ((block.facts || block.items || []).map(function(fact, i) {
var text = (typeof fact === 'string') ? fact : (fact && fact.text) || '';
return SectionBuilder.factRow(i, text);
}).join('') || SectionBuilder.factRow(0, ''))
+ '</div>'
+ '<button type="button" class="add-item-btn btn-add-fact">+ Add Fact</button>');
case 'timeline-card':
return this.formGroup('Date Label', '<input type="text" name="date" value="' + this.esc(block.date || '') + '" placeholder="e.g. Sept 24">')
+ this.formGroup('Heading', '<input type="text" name="title" value="' + this.esc(block.title || '') + '" placeholder="Timeline event title">')
+ this.formGroup('Details', '<textarea name="text" rows="4">' + this.esc(block.text || '') + '</textarea>');
case 'gallery':
return this.formGroup('Gallery Images',
return this.formGroup('Display Mode',
'<select name="mode">'
+ '<option value="carousel"' + ((block.mode || 'carousel') === 'carousel' ? ' selected' : '') + '>Carousel (single image, rotates)</option>'
+ '<option value="strip"' + ((block.mode || 'carousel') === 'strip' ? ' selected' : '') + '>Strip (horizontal scroll)</option>'
+ '</select>')
+ this.formGroup('Auto-rotate',
'<select name="autoplay">'
+ '<option value="true"' + (block.autoplay === false ? '' : ' selected') + '>On</option>'
+ '<option value="false"' + (block.autoplay === false ? ' selected' : '') + '>Off</option>'
+ '</select>')
+ this.formGroup('Rotate Every (seconds)',
'<input type="number" name="intervalSec" min="2" max="15" value="' + (parseInt(block.intervalSec, 10) || 5) + '">')
+ this.formGroup('Show Dots',
'<select name="showDots">'
+ '<option value="true"' + (block.showDots === false ? '' : ' selected') + '>On</option>'
+ '<option value="false"' + (block.showDots === false ? ' selected' : '') + '>Off</option>'
+ '</select>')
+ this.formGroup('Show Arrows',
'<select name="showArrows">'
+ '<option value="true"' + (block.showArrows === false ? '' : ' selected') + '>On</option>'
+ '<option value="false"' + (block.showArrows === false ? ' selected' : '') + '>Off</option>'
+ '</select>')
+ this.formGroup('Gallery Images',
'<div id="galleryItems">'
+ (block.images || []).map(function(img, i) { return SectionBuilder.imageRow(i, img, true); }).join('')
+ '</div>'
@@ -289,6 +466,13 @@ class SectionBuilder {
+ '</div>';
}
static factRow(index, text) {
return '<div class="item-row" data-index="' + index + '">'
+ '<input type="text" data-field="text" data-index="' + index + '" value="' + (text || '') + '" placeholder="Fact line (e.g. Day 90: 320,000 coalition troops)">'
+ '<button type="button" class="btn-remove-item">&#x2715;</button>'
+ '</div>';
}
bindDynamicFormEvents(container) {
var self = this;
@@ -312,6 +496,16 @@ class SectionBuilder {
});
}
var addFactBtn = container.querySelector('.btn-add-fact');
if (addFactBtn) {
addFactBtn.addEventListener('click', function() {
var items = container.querySelector('#factItems');
var newIdx = items.querySelectorAll('.item-row').length;
items.insertAdjacentHTML('beforeend', SectionBuilder.factRow(newIdx, ''));
self.bindRemoveBtns(container);
});
}
this.bindRemoveBtns(container);
}
@@ -348,7 +542,23 @@ class SectionBuilder {
block.text = body.querySelector('[name="text"]').value;
block.author = body.querySelector('[name="author"]').value;
break;
case 'fact-panel':
block.title = body.querySelector('[name="title"]').value;
block.facts = this.readItemRows(body.querySelector('#factItems'), ['text'])
.filter(function(f) { return (f.text || '').trim() !== ''; });
break;
case 'timeline-card':
block.date = body.querySelector('[name="date"]').value;
block.title = body.querySelector('[name="title"]').value;
block.text = body.querySelector('[name="text"]').value;
break;
case 'gallery':
block.mode = body.querySelector('[name="mode"]').value || 'carousel';
block.autoplay = body.querySelector('[name="autoplay"]').value === 'true';
block.intervalSec = parseInt(body.querySelector('[name="intervalSec"]').value, 10) || 5;
block.intervalSec = Math.max(2, Math.min(block.intervalSec, 15));
block.showDots = body.querySelector('[name="showDots"]').value === 'true';
block.showArrows = body.querySelector('[name="showArrows"]').value === 'true';
block.images = this.readItemRows(body.querySelector('#galleryItems'), ['src','alt','caption']);
break;
case 'grid4':
@@ -437,12 +647,46 @@ class SectionBuilder {
reader.onload = function(evt) {
try {
var data = JSON.parse(evt.target.result);
Object.keys(self.sections).forEach(function(id) {
if (data[id] && data[id].blocks) {
self.sections[id].blocks = data[id].blocks;
localStorage.setItem('pf-section-' + id, JSON.stringify(data[id].blocks));
Object.keys(data).forEach(function(id) {
var entry = data[id] || {};
var blocks = Array.isArray(entry.blocks) ? entry.blocks : (Array.isArray(entry) ? entry : []);
if (!self.sections[id]) {
var meta = entry.meta || { title: id, icon: '📚', color: '#7f8c8d' };
self.sections[id] = {
title: meta.title || id,
subtitle: meta.subtitle || '',
description: meta.description || '',
icon: meta.icon || '📚',
color: meta.color || '#7f8c8d',
stats: {
articles: parseInt(meta.stats && meta.stats.articles, 10) || 0,
storyMaps: parseInt(meta.stats && meta.stats.storyMaps, 10) || 0,
images: parseInt(meta.stats && meta.stats.images, 10) || 0
},
blocks: []
};
} else if (entry.meta) {
self.sections[id] = Object.assign({}, self.sections[id], {
title: entry.meta.title || self.sections[id].title,
subtitle: entry.meta.subtitle || self.sections[id].subtitle,
description: entry.meta.description || self.sections[id].description,
icon: entry.meta.icon || self.sections[id].icon,
color: entry.meta.color || self.sections[id].color,
stats: {
articles: parseInt(entry.meta.stats && entry.meta.stats.articles, 10) || 0,
storyMaps: parseInt(entry.meta.stats && entry.meta.stats.storyMaps, 10) || 0,
images: parseInt(entry.meta.stats && entry.meta.stats.images, 10) || 0
}
});
}
self.sections[id].blocks = blocks;
localStorage.setItem('pf-section-' + id, JSON.stringify(blocks));
});
self.saveSectionsManifest();
self.renderSectionTabs();
if (!self.sections[self.currentSection]) self.currentSection = Object.keys(self.sections)[0];
self.loadSection(self.currentSection);
self.showToast('Content imported!');
} catch (err) {
@@ -460,13 +704,25 @@ class SectionBuilder {
});
});
document.getElementById('newSectionBtn').addEventListener('click', function() {
self.openSectionWizard();
});
document.getElementById('modalSave').addEventListener('click', function() { self.saveModalBlock(); });
document.getElementById('modalCancel').addEventListener('click', function() { self.closeModal(); });
document.getElementById('modalClose').addEventListener('click', function() { self.closeModal(); });
document.querySelector('.modal-overlay').addEventListener('click', function() { self.closeModal(); });
document.querySelector('#builderModal .modal-overlay').addEventListener('click', function() { self.closeModal(); });
document.getElementById('sectionWizardSave').addEventListener('click', function() { self.createSectionFromWizard(); });
document.getElementById('sectionWizardCancel').addEventListener('click', function() { self.closeSectionWizard(); });
document.getElementById('sectionWizardClose').addEventListener('click', function() { self.closeSectionWizard(); });
document.querySelector('#sectionWizardModal .modal-overlay').addEventListener('click', function() { self.closeSectionWizard(); });
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') self.closeModal();
if (e.key === 'Escape') {
self.closeModal();
self.closeSectionWizard();
}
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
if (self.currentSection) self.saveSection(self.currentSection);
@@ -474,10 +730,73 @@ class SectionBuilder {
});
}
openSectionWizard() {
document.getElementById('wizardTitle').value = '';
document.getElementById('wizardSubtitle').value = '';
document.getElementById('wizardDescription').value = '';
document.getElementById('wizardIcon').value = '📚';
document.getElementById('wizardColor').value = '#7f8c8d';
document.getElementById('wizardArticles').value = '0';
document.getElementById('wizardStoryMaps').value = '0';
document.getElementById('wizardImages').value = '0';
document.getElementById('sectionWizardModal').classList.remove('hidden');
document.getElementById('wizardTitle').focus();
}
closeSectionWizard() {
document.getElementById('sectionWizardModal').classList.add('hidden');
}
createSectionFromWizard() {
var title = (document.getElementById('wizardTitle').value || '').trim();
if (!title) {
alert('Please enter a title for the new section.');
return;
}
var idBase = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
if (!idBase) idBase = 'section';
var id = idBase;
var i = 2;
while (this.sections[id]) {
id = idBase + '-' + i;
i++;
}
var color = (document.getElementById('wizardColor').value || '').trim() || '#7f8c8d';
if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) {
alert('Please enter a valid hex color (for example: #3498db).');
return;
}
this.sections[id] = {
title: title,
subtitle: (document.getElementById('wizardSubtitle').value || '').trim(),
description: (document.getElementById('wizardDescription').value || '').trim(),
icon: (document.getElementById('wizardIcon').value || '').trim() || '📚',
color: color,
stats: {
articles: parseInt(document.getElementById('wizardArticles').value, 10) || 0,
storyMaps: parseInt(document.getElementById('wizardStoryMaps').value, 10) || 0,
images: parseInt(document.getElementById('wizardImages').value, 10) || 0
},
blocks: []
};
this.saveSectionsManifest();
this.renderSectionTabs();
this.closeSectionWizard();
this.loadSection(id);
this.showToast('Section created. Add blocks and save when ready.');
}
exportJSON() {
var output = {};
Object.keys(this.sections).forEach(function(id) {
output[id] = { blocks: this.sections[id].blocks };
output[id] = {
meta: this.sectionMeta(id),
blocks: this.sections[id].blocks
};
}, this);
var blob = new Blob([JSON.stringify(output, null, 2)], { type: 'application/json' });
var url = URL.createObjectURL(blob);
@@ -503,7 +822,7 @@ class SectionBuilder {
blockTypeLabel(type) {
var labels = {
heading: 'H Heading', paragraph: '¶ Paragraph', image: '🖼 Image',
quote: '" Quote', gallery: '⊞ Gallery', grid4: '▦ Grid 4',
quote: '" Quote', 'fact-panel': '▸ Fact Briefing Panel', 'timeline-card': '🗓 Timeline Card', gallery: '⊞ Gallery', grid4: '▦ Grid 4',
statistics: '# Statistics', 'map-button': '📍 Map Button'
};
return labels[type] || type;