@0m0g1/griot 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,46 +1,48 @@
1
1
  // ─── BlockRenderer.js ─────────────────────────────────────────────────────────
2
- // Renders a single block to a DOM element.
3
- // Used by both Viewer (read-only) and Editor (preview layer).
2
+ // Renders a single block DOM element.
3
+ // Used by Viewer (read-only) and Editor preview layer.
4
4
  //
5
5
  // Options:
6
- // books — array of parsed book objects (for book_citation)
7
- // onEventClick — (eventId) => void
8
- // onCiteClick — (blockId) => void
9
- // editable — if true, skips event listeners (Editor manages them)
6
+ // books — array of book objects
7
+ // onEventClick — (eventId) => void
8
+ // onCiteClick — (blockId) => void
10
9
  // ─────────────────────────────────────────────────────────────────────────────
11
10
 
12
- import { anchorId } from '../core/Block.js';
11
+ import { anchorId } from '../core/Block.js';
13
12
  import { renderInlineToDOM, renderInlineToHTML, escHtml, escAttr } from '../inline/InlineRenderer.js';
14
- import { getBlockDef } from './BlockSchema.js';
13
+ import { getBlockDef } from './BlockSchema.js';
15
14
 
16
- // ─── Public entry point ───────────────────────────────────────────────────────
17
- export function renderBlock(block, { books = [], onEventClick, onCiteClick } = {}) {
18
- const el = _render(block, { books, onEventClick, onCiteClick });
15
+ const LAYOUT_OPTIONS = ['grid','masonry','carousel','strip'];
16
+
17
+ // ─── Public ───────────────────────────────────────────────────────────────────
18
+
19
+ export function renderBlock(block, opts = {}) {
20
+ const el = _render(block, opts);
19
21
  if (el) {
20
- el.id = anchorId(block.id);
21
- el.dataset.blockId = block.id;
22
+ el.id = anchorId(block.id);
23
+ el.dataset.blockId = block.id;
22
24
  el.dataset.blockType = block.type;
23
25
  }
24
26
  return el;
25
27
  }
26
28
 
27
- // ─── Internal ─────────────────────────────────────────────────────────────────
28
- function inlineDOM(text, opts) {
29
- return renderInlineToDOM(text, {
30
- onEventClick: opts.onEventClick,
31
- onCiteClick: opts.onCiteClick,
32
- });
29
+ // ─── Dispatcher ───────────────────────────────────────────────────────────────
30
+
31
+ function il(text, opts) {
32
+ return renderInlineToDOM(text, { onEventClick: opts.onEventClick, onCiteClick: opts.onCiteClick });
33
33
  }
34
34
 
35
35
  function _render(block, opts) {
36
- const { text, meta = {}, type } = block;
36
+ const { text = '', meta = {}, type } = block;
37
37
 
38
38
  switch (type) {
39
39
 
40
+ // ── Text ──────────────────────────────────────────────────────────────────
41
+
40
42
  case 'paragraph': {
41
43
  const el = document.createElement('p');
42
44
  el.className = 'griot-block griot-paragraph';
43
- if (text) el.appendChild(inlineDOM(text, opts));
45
+ if (text) el.appendChild(il(text, opts));
44
46
  return el;
45
47
  }
46
48
 
@@ -55,70 +57,185 @@ function _render(block, opts) {
55
57
  case 'blockquote': {
56
58
  const el = document.createElement('blockquote');
57
59
  el.className = 'griot-block griot-blockquote';
58
- if (text) el.appendChild(inlineDOM(text, opts));
60
+ if (text) el.appendChild(il(text, opts));
59
61
  return el;
60
62
  }
61
63
 
62
- case 'callout': {
63
- const el = document.createElement('div');
64
- const icon = document.createElement('span');
65
- const body = document.createElement('div');
66
- el.className = 'griot-block griot-callout';
64
+ case 'callout':
65
+ case 'callout_warning':
66
+ case 'callout_tip':
67
+ case 'callout_danger': {
68
+ const ICONS = { callout:'💡', callout_warning:'⚠️', callout_tip:'✅', callout_danger:'🚨' };
69
+ const el = document.createElement('div');
70
+ const icon = document.createElement('span');
71
+ const body = document.createElement('div');
72
+ el.className = `griot-block griot-callout griot-callout--${type.replace('callout_','') || 'info'}`;
67
73
  icon.className = 'griot-callout__icon';
68
74
  body.className = 'griot-callout__body';
69
- icon.textContent = meta.icon ?? '💡';
70
- if (text) body.appendChild(inlineDOM(text, opts));
71
- el.appendChild(icon);
72
- el.appendChild(body);
75
+ icon.textContent = meta.icon ?? ICONS[type] ?? '💡';
76
+ if (text) body.appendChild(il(text, opts));
77
+ el.append(icon, body);
73
78
  return el;
74
79
  }
75
80
 
76
81
  case 'code': {
77
82
  const pre = document.createElement('pre');
78
83
  const code = document.createElement('code');
79
- pre.className = 'griot-block griot-code';
80
- if (meta.language) code.className = `language-${meta.language}`;
84
+ pre.className = 'griot-block griot-code';
85
+ if (meta.language) { pre.dataset.language = meta.language; code.className = `language-${meta.language}`; }
81
86
  code.textContent = text ?? '';
82
87
  pre.appendChild(code);
83
88
  return pre;
84
89
  }
85
90
 
86
- case 'divider': {
87
- const el = document.createElement('hr');
88
- el.className = 'griot-block griot-divider';
91
+ case 'list_ul':
92
+ case 'list_ol': {
93
+ const tag = type === 'list_ul' ? 'ul' : 'ol';
94
+ const el = document.createElement(tag);
95
+ el.className = `griot-block griot-list griot-list--${tag}`;
96
+ for (const item of (text ?? '').split('\n').filter(l => l.trim())) {
97
+ const li = document.createElement('li');
98
+ li.appendChild(il(item, opts));
99
+ el.appendChild(li);
100
+ }
89
101
  return el;
90
102
  }
91
103
 
104
+ // ── Media ─────────────────────────────────────────────────────────────────
105
+
92
106
  case 'image': {
93
- const figure = document.createElement('figure');
94
- const img = document.createElement('img');
95
- figure.className = 'griot-block griot-image';
96
- img.src = meta.src ?? '';
97
- img.alt = meta.alt ?? '';
98
- figure.appendChild(img);
99
- if (meta.caption) {
100
- const cap = document.createElement('figcaption');
101
- cap.textContent = meta.caption;
102
- figure.appendChild(cap);
107
+ const fig = document.createElement('figure');
108
+ fig.className = `griot-block griot-image griot-image--${meta.width ?? 'full'}`;
109
+ if (meta.uploading) {
110
+ const sp = document.createElement('div');
111
+ sp.className = 'griot-media-uploading';
112
+ sp.textContent = 'Uploading…';
113
+ fig.appendChild(sp);
114
+ } else if (meta.src) {
115
+ const img = document.createElement('img');
116
+ img.src = meta.src; img.alt = meta.alt ?? '';
117
+ fig.appendChild(img);
118
+ if (meta.caption) {
119
+ const cap = document.createElement('figcaption');
120
+ cap.textContent = meta.caption;
121
+ fig.appendChild(cap);
122
+ }
123
+ }
124
+ return fig;
125
+ }
126
+
127
+ case 'video': {
128
+ const fig = document.createElement('figure');
129
+ fig.className = 'griot-block griot-video';
130
+ const embed = meta.embedUrl ?? _ytEmbed(meta.src) ?? _vimeoEmbed(meta.src);
131
+ if (embed) {
132
+ const iframe = document.createElement('iframe');
133
+ iframe.src = embed; iframe.className = 'griot-video__iframe';
134
+ iframe.frameBorder = '0';
135
+ iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
136
+ iframe.allowFullscreen = true;
137
+ fig.appendChild(iframe);
138
+ } else if (meta.src) {
139
+ const v = document.createElement('video');
140
+ v.src = meta.src; v.controls = true; v.className = 'griot-video__native';
141
+ fig.appendChild(v);
142
+ }
143
+ if (meta.caption) { const c = document.createElement('figcaption'); c.textContent = meta.caption; fig.appendChild(c); }
144
+ return fig;
145
+ }
146
+
147
+ case 'audio': {
148
+ const fig = document.createElement('figure');
149
+ fig.className = 'griot-block griot-audio';
150
+ const embed = meta.embedUrl ?? _spotifyEmbed(meta.src) ?? _scEmbed(meta.src);
151
+ if (embed) {
152
+ const iframe = document.createElement('iframe');
153
+ iframe.src = embed; iframe.className = 'griot-audio__iframe';
154
+ iframe.frameBorder = '0';
155
+ iframe.allow = 'autoplay; clipboard-write; encrypted-media; fullscreen';
156
+ fig.appendChild(iframe);
157
+ } else if (meta.src) {
158
+ const a = document.createElement('audio');
159
+ a.src = meta.src; a.controls = true; a.className = 'griot-audio__native';
160
+ fig.appendChild(a);
103
161
  }
104
- return figure;
162
+ if (meta.caption) { const c = document.createElement('figcaption'); c.textContent = meta.caption; fig.appendChild(c); }
163
+ return fig;
164
+ }
165
+
166
+ case 'gallery': {
167
+ return _renderGallery(meta.items ?? [], meta.layout ?? 'grid', opts);
168
+ }
169
+
170
+ case 'embed': {
171
+ const fig = document.createElement('figure');
172
+ fig.className = 'griot-block griot-embed';
173
+ if (meta.src) {
174
+ const iframe = document.createElement('iframe');
175
+ iframe.src = meta.src;
176
+ iframe.style.height = `${meta.height ?? 400}px`;
177
+ iframe.className = 'griot-embed__iframe';
178
+ iframe.frameBorder = '0';
179
+ iframe.allow = 'autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media';
180
+ iframe.allowFullscreen = true;
181
+ fig.appendChild(iframe);
182
+ }
183
+ if (meta.caption) { const c = document.createElement('figcaption'); c.textContent = meta.caption; fig.appendChild(c); }
184
+ return fig;
185
+ }
186
+
187
+ // ── Structure ─────────────────────────────────────────────────────────────
188
+
189
+ case 'table': {
190
+ const headers = Array.isArray(meta.headers) ? meta.headers : [];
191
+ const rows = Array.isArray(meta.rows) ? meta.rows : [];
192
+ const colCount = Math.max(headers.length, ...rows.map(r => r.length), 1);
193
+ const wrap = document.createElement('div');
194
+ wrap.className = 'griot-block griot-table-wrap';
195
+ const table = document.createElement('table');
196
+ table.className = 'griot-table';
197
+ if (headers.length) {
198
+ const thead = document.createElement('thead');
199
+ const tr = document.createElement('tr');
200
+ for (let ci = 0; ci < colCount; ci++) {
201
+ const th = document.createElement('th');
202
+ th.appendChild(il(headers[ci] ?? '', opts));
203
+ tr.appendChild(th);
204
+ }
205
+ thead.appendChild(tr); table.appendChild(thead);
206
+ }
207
+ const tbody = document.createElement('tbody');
208
+ for (const row of rows) {
209
+ const tr = document.createElement('tr');
210
+ for (let ci = 0; ci < colCount; ci++) {
211
+ const td = document.createElement('td');
212
+ td.appendChild(il(row[ci] ?? '', opts));
213
+ tr.appendChild(td);
214
+ }
215
+ tbody.appendChild(tr);
216
+ }
217
+ table.appendChild(tbody); wrap.appendChild(table);
218
+ return wrap;
219
+ }
220
+
221
+ case 'divider': {
222
+ const el = document.createElement('hr');
223
+ el.className = 'griot-block griot-divider';
224
+ return el;
105
225
  }
106
226
 
107
227
  case 'timeline_ref': {
108
228
  const el = document.createElement('div');
109
229
  el.className = 'griot-block griot-timeline-ref';
110
230
  if (meta.eventId && opts.onEventClick) {
111
- el.setAttribute('role', 'button');
112
- el.tabIndex = 0;
231
+ el.setAttribute('role','button'); el.tabIndex = 0;
113
232
  el.addEventListener('click', () => opts.onEventClick(meta.eventId));
114
- el.addEventListener('keydown', (e) => {
115
- if (e.key === 'Enter' || e.key === ' ') opts.onEventClick(meta.eventId);
116
- });
233
+ el.addEventListener('keydown', e => { if (e.key==='Enter'||e.key===' ') opts.onEventClick(meta.eventId); });
117
234
  }
118
235
  el.innerHTML = `
119
236
  <span class="griot-timeline-ref__icon">⏱</span>
120
237
  <div class="griot-timeline-ref__body">
121
- <div class="griot-timeline-ref__title">${escHtml(meta.eventTitle || 'Timeline Event')}</div>
238
+ <div class="griot-timeline-ref__title">${escHtml(meta.eventTitle||'Timeline Event')}</div>
122
239
  ${meta.note ? `<div class="griot-timeline-ref__note">${escHtml(meta.note)}</div>` : ''}
123
240
  </div>
124
241
  ${meta.eventId ? '<span class="griot-timeline-ref__arrow">→</span>' : ''}
@@ -133,69 +250,99 @@ function _render(block, opts) {
133
250
  default: {
134
251
  const el = document.createElement('p');
135
252
  el.className = 'griot-block griot-paragraph';
136
- el.textContent = text ?? '';
253
+ if (text) el.appendChild(il(text, opts));
137
254
  return el;
138
255
  }
139
256
  }
140
257
  }
141
258
 
259
+ // ─── Gallery renderer ─────────────────────────────────────────────────────────
260
+
261
+ function _renderGallery(items, layout, opts) {
262
+ const wrap = document.createElement('div');
263
+ wrap.className = `griot-block griot-gallery griot-gallery--${LAYOUT_OPTIONS.includes(layout)?layout:'grid'}`;
264
+
265
+ if (!items.length) {
266
+ wrap.innerHTML = `<div class="griot-gallery__empty">No images yet</div>`;
267
+ return wrap;
268
+ }
269
+
270
+ for (const [i, item] of items.entries()) {
271
+ const el = document.createElement('div');
272
+ el.className = 'griot-gallery__item';
273
+ const img = document.createElement('img');
274
+ img.src = item.src ?? item.url ?? '';
275
+ img.alt = item.alt ?? item.alt_text ?? '';
276
+ img.loading = 'lazy';
277
+ el.appendChild(img);
278
+ if (item.caption) {
279
+ const cap = document.createElement('p');
280
+ cap.className = 'griot-gallery__caption';
281
+ cap.textContent = item.caption;
282
+ el.appendChild(cap);
283
+ }
284
+ wrap.appendChild(el);
285
+ }
286
+ return wrap;
287
+ }
288
+
289
+ // ─── Citation renderer ────────────────────────────────────────────────────────
290
+
142
291
  function _renderCitation(block, opts) {
143
292
  const { meta = {} } = block;
144
293
  const wrap = document.createElement('figure');
145
294
  wrap.className = 'griot-block griot-citation';
146
295
 
147
- if (!meta.bookId) {
148
- wrap.innerHTML = `<div class="griot-citation__empty">📖 No source selected yet</div>`;
149
- return wrap;
150
- }
296
+ if (!meta.bookId) { wrap.innerHTML = `<div class="griot-citation__empty">📖 No source selected yet</div>`; return wrap; }
151
297
 
152
298
  const book = (opts.books ?? []).find(b => b.id === meta.bookId);
153
299
  const unit = book?.units?.find(u => u.id === meta.unitId);
154
-
155
- if (!book || !unit) {
156
- wrap.innerHTML = `<div class="griot-citation__missing">📖 Source not found — book may have been removed</div>`;
157
- return wrap;
158
- }
300
+ if (!book || !unit) { wrap.innerHTML = `<div class="griot-citation__missing">📖 Source not found</div>`; return wrap; }
159
301
 
160
302
  const inner = document.createElement('div');
161
303
  inner.className = 'griot-citation__inner';
162
-
163
- // Header
164
- const hdr = document.createElement('div');
165
- hdr.className = 'griot-citation__header';
166
- hdr.innerHTML = `
167
- <span class="griot-citation__book-icon">📖</span>
168
- <span class="griot-citation__book-title">${escHtml(book.title)}</span>
169
- ${book.author ? `<span class="griot-citation__author">${escHtml(book.author)}</span>` : ''}
170
- <span class="griot-citation__unit">${escHtml(unit.label)}</span>
304
+ inner.innerHTML = `
305
+ <div class="griot-citation__header">
306
+ <span class="griot-citation__book-icon">📖</span>
307
+ <span class="griot-citation__book-title">${escHtml(book.title)}</span>
308
+ ${book.author ? `<span class="griot-citation__author">${escHtml(book.author)}</span>` : ''}
309
+ <span class="griot-citation__unit">${escHtml(unit.label)}</span>
310
+ </div>
311
+ ${meta.quote ? `<blockquote class="griot-citation__quote">${escHtml(meta.quote)}</blockquote>` : ''}
171
312
  `;
172
- inner.appendChild(hdr);
173
-
174
- // Quote
175
- if (meta.quote) {
176
- const q = document.createElement('blockquote');
177
- q.className = 'griot-citation__quote';
178
- q.textContent = meta.quote;
179
- inner.appendChild(q);
180
- }
181
-
182
- // Note (supports inline syntax)
183
313
  if (meta.note) {
184
314
  const note = document.createElement('div');
185
315
  note.className = 'griot-citation__note';
186
- note.appendChild(inlineDOM(meta.note, opts));
316
+ note.appendChild(renderInlineToDOM(meta.note, opts));
187
317
  inner.appendChild(note);
188
318
  }
189
-
190
319
  wrap.appendChild(inner);
320
+ return wrap;
321
+ }
191
322
 
192
- // Content preview
193
- if (unit.content) {
194
- const preview = document.createElement('div');
195
- preview.className = 'griot-citation__preview';
196
- preview.textContent = unit.content.slice(0, 180) + (unit.content.length > 180 ? '…' : '');
197
- wrap.appendChild(preview);
198
- }
323
+ // ─── Embed URL helpers ────────────────────────────────────────────────────────
199
324
 
200
- return wrap;
325
+ function _ytEmbed(src) {
326
+ if (!src) return null;
327
+ const m = src.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|shorts\/|embed\/))([a-zA-Z0-9_-]{11})/);
328
+ return m ? `https://www.youtube.com/embed/${m[1]}?rel=0` : null;
329
+ }
330
+ function _vimeoEmbed(src) {
331
+ if (!src) return null;
332
+ const m = src.match(/vimeo\.com\/(\d+)/);
333
+ return m ? `https://player.vimeo.com/video/${m[1]}` : null;
201
334
  }
335
+ function _spotifyEmbed(src) {
336
+ if (!src) return null;
337
+ const m = src.match(/open\.spotify\.com\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/);
338
+ return m ? `https://open.spotify.com/embed/${m[1]}/${m[2]}` : null;
339
+ }
340
+ function _scEmbed(src) {
341
+ if (!src) return null;
342
+ if (src.includes('soundcloud.com/'))
343
+ return `https://w.soundcloud.com/player/?url=${encodeURIComponent(src)}&color=%236366f1&auto_play=false&hide_related=true&show_comments=false`;
344
+ return null;
345
+ }
346
+
347
+ // Export helpers for editor use
348
+ export { _ytEmbed as resolveYouTube, _vimeoEmbed as resolveVimeo, _spotifyEmbed as resolveSpotify, _scEmbed as resolveSoundCloud };
@@ -1,94 +1,37 @@
1
1
  // ─── BlockSchema.js ───────────────────────────────────────────────────────────
2
- // The single source of truth for what block types exist, their labels,
3
- // icons, whether they have text, and any default meta values.
2
+ // Single source of truth for all block types
3
+ // Fields: category, label, icon, slashLabel, hasText, hasInline, defaultMeta, placeholder
4
4
  // ─────────────────────────────────────────────────────────────────────────────
5
5
 
6
6
  const SCHEMA = {
7
- paragraph: {
8
- label: 'Paragraph',
9
- icon: '',
10
- hasText: true,
11
- hasInline: true,
12
- defaultMeta: {},
13
- placeholder: 'Write something… **bold** *italic* `code` [[event:id|label]]',
14
- },
15
- heading: {
16
- label: 'Heading',
17
- icon: 'H',
18
- hasText: true,
19
- hasInline: false, // headings render as plain text, no inline chips
20
- defaultMeta: { level: 2 },
21
- placeholder: 'Heading…',
22
- },
23
- blockquote: {
24
- label: 'Quote',
25
- icon: '❝',
26
- hasText: true,
27
- hasInline: true,
28
- defaultMeta: {},
29
- placeholder: 'Quote…',
30
- },
31
- callout: {
32
- label: 'Callout',
33
- icon: '💡',
34
- hasText: true,
35
- hasInline: true,
36
- defaultMeta: { icon: '💡' },
37
- placeholder: 'Callout text…',
38
- },
39
- code: {
40
- label: 'Code',
41
- icon: '</>',
42
- hasText: true,
43
- hasInline: false, // code blocks are raw, no inline parsing
44
- defaultMeta: { language: '' },
45
- placeholder: '// code…',
46
- },
47
- divider: {
48
- label: 'Divider',
49
- icon: '—',
50
- hasText: false,
51
- hasInline: false,
52
- defaultMeta: {},
53
- placeholder: null,
54
- },
55
- image: {
56
- label: 'Image',
57
- icon: '🖼',
58
- hasText: false,
59
- hasInline: false,
60
- defaultMeta: { src: '', alt: '', caption: '' },
61
- placeholder: null,
62
- },
63
- timeline_ref: {
64
- label: 'Timeline Event',
65
- icon: '⏱',
66
- hasText: false,
67
- hasInline: false,
68
- defaultMeta: { eventId: '', eventTitle: '', note: '' },
69
- placeholder: null,
70
- },
71
- book_citation: {
72
- label: 'Book Citation',
73
- icon: '📖',
74
- hasText: false,
75
- hasInline: false,
76
- defaultMeta: { bookId: '', unitId: '', quote: '', note: '' },
77
- placeholder: null,
78
- },
79
- };
7
+ paragraph: { category:'text', label:'Paragraph', icon:'¶', slashLabel:'Text', hasText:true, hasInline:true, defaultMeta:{}, placeholder:'Write something… **bold** *italic* `code` ==highlight==' },
8
+ heading: { category:'text', label:'Heading', icon:'H', slashLabel:'Heading', hasText:true, hasInline:false, defaultMeta:{ level:2 }, placeholder:'Heading…' },
9
+ blockquote: { category:'text', label:'Quote', icon:'❝', slashLabel:'Quote', hasText:true, hasInline:true, defaultMeta:{}, placeholder:'Quote…' },
10
+ callout: { category:'text', label:'Callout', icon:'💡', slashLabel:'Callout', hasText:true, hasInline:true, defaultMeta:{ icon:'💡' }, placeholder:'Callout text…' },
11
+ callout_warning: { category:'text', label:'Warning', icon:'⚠️', slashLabel:'Warning', hasText:true, hasInline:true, defaultMeta:{ icon:'⚠️' }, placeholder:'Warning message…' },
12
+ callout_tip: { category:'text', label:'Tip', icon:'✅', slashLabel:'Tip', hasText:true, hasInline:true, defaultMeta:{ icon:'✅' }, placeholder:'Tip or note…' },
13
+ callout_danger: { category:'text', label:'Danger', icon:'🚨', slashLabel:'Danger', hasText:true, hasInline:true, defaultMeta:{ icon:'🚨' }, placeholder:'Critical warning…' },
14
+ code: { category:'text', label:'Code', icon:'</>', slashLabel:'Code block', hasText:true, hasInline:false, defaultMeta:{ language:'' }, placeholder:'// code…' },
15
+ list_ul: { category:'text', label:'Bullet List', icon:'•', slashLabel:'Bullet list', hasText:true, hasInline:false, defaultMeta:{}, placeholder:'Item 1\nItem 2\nItem 3' },
16
+ list_ol: { category:'text', label:'Numbered List', icon:'1.', slashLabel:'Numbered list', hasText:true, hasInline:false, defaultMeta:{}, placeholder:'First item\nSecond item' },
17
+
18
+ image: { category:'media', label:'Image', icon:'🖼', slashLabel:'Image', hasText:false, hasInline:false, defaultMeta:{ src:'', alt:'', caption:'', width:'full' }, placeholder:null },
19
+ video: { category:'media', label:'Video', icon:'▶', slashLabel:'Video', hasText:false, hasInline:false, defaultMeta:{ src:'', caption:'', embedUrl:null, platform:null }, placeholder:null },
20
+ audio: { category:'media', label:'Audio', icon:'🎵', slashLabel:'Audio', hasText:false, hasInline:false, defaultMeta:{ src:'', caption:'', embedUrl:null, platform:null }, placeholder:null },
21
+ gallery: { category:'media', label:'Gallery', icon:'▦', slashLabel:'Gallery', hasText:false, hasInline:false, defaultMeta:{ items:[], layout:'grid' }, placeholder:null },
80
22
 
81
- export function getBlockDef(type) {
82
- return SCHEMA[type] ?? SCHEMA.paragraph;
83
- }
23
+ embed: { category:'embed', label:'Embed', icon:'⬡', slashLabel:'Embed / iframe', hasText:false, hasInline:false, defaultMeta:{ src:'', height:400, caption:'' }, placeholder:null },
84
24
 
85
- export function getAllTypes() {
86
- return Object.keys(SCHEMA);
87
- }
25
+ table: { category:'structure', label:'Table', icon:'⊞', slashLabel:'Table', hasText:false, hasInline:false, defaultMeta:{ headers:['Column 1','Column 2'], rows:[['','']] }, placeholder:null },
26
+ divider: { category:'structure', label:'Divider', icon:'—', slashLabel:'Divider', hasText:false, hasInline:false, defaultMeta:{}, placeholder:null },
27
+
28
+ timeline_ref: { category:'structure', label:'Timeline Event', icon:'⏱', slashLabel:'Timeline event', hasText:false, hasInline:false, defaultMeta:{ eventId:'', eventTitle:'', note:'' }, placeholder:null },
29
+ book_citation: { category:'structure', label:'Book Citation', icon:'📖', slashLabel:'Book citation', hasText:false, hasInline:false, defaultMeta:{ bookId:'', unitId:'', quote:'', note:'' }, placeholder:null },
30
+ };
88
31
 
89
- // Returns default meta for a type (shallow copy)
90
- export function defaultMeta(type) {
91
- return { ...(SCHEMA[type]?.defaultMeta ?? {}) };
92
- }
32
+ export function getBlockDef(type) { return SCHEMA[type] ?? SCHEMA.paragraph; }
33
+ export function getAllTypes() { return Object.keys(SCHEMA); }
34
+ export function getTypesByCategory(cat) { return Object.entries(SCHEMA).filter(([,d]) => d.category === cat).map(([t]) => t); }
35
+ export function defaultMeta(type) { return { ...(SCHEMA[type]?.defaultMeta ?? {}) }; }
93
36
 
94
- export default SCHEMA;
37
+ export default SCHEMA;