@0m0g1/griot 0.1.4 → 0.1.6

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.
package/README.md CHANGED
@@ -1,15 +1,23 @@
1
1
  # Griot
2
2
 
3
- A lightweight, extensible block editor and viewer for the web. Inspired by Notion, but built with plain JavaScript and zero dependencies. Griot provides a rich editing experience with:
3
+ A lightweight, extensible block editor and viewer for the web. Inspired by Notion, built with plain JavaScript and zero dependencies. Griot ships a complete dark-themed CSS file, an immutable document model, and a fully keyboard-driven editing experience.
4
4
 
5
- - **Block-based editing** – paragraphs, headings, lists, callouts, code blocks, images, video, audio, tables, dividers, timeline references, book citations, and more.
6
- - **Inline formatting** – bold, italic, underline, strikethrough, inline code, highlights, colored text, links, images, and custom event/cite chips.
7
- - **Slash commands** – type `/` to insert any block.
8
- - **Floating format toolbar** – appears when you select text.
9
- - **Undo/redo** with a built‑in history stack.
10
- - **Read‑only viewer** render the same document without editing controls.
11
- - **Immutable document operations** every change produces a new document object.
12
- - **Schema‑driven** all block types are defined in a single schema; easy to extend.
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - **19 block types** across four categories: text, media, embed, and structure
10
+ - **Inline markup** 12 token types parsed by a standalone lexer
11
+ - **Markdown shortcuts** type `# `, `> `, `- `, ` ``` ` etc. to convert a block on the fly
12
+ - **Floating format toolbar** appears on text selection; wraps with bold, italic, underline, strikethrough, inline code, highlight, link, or color
13
+ - **Slash command palette** — type `/` in any empty block; searchable, keyboard-navigable, grouped by category
14
+ - **Undo / redo** — linear history stack, up to 200 snapshots
15
+ - **Live inline preview** — rendered below every text block that supports inline syntax
16
+ - **Read-only viewer** — same document, no editing controls; supports highlight + scroll-to-block
17
+ - **Immutable document operations** — every mutation returns a new document object
18
+ - **Schema-driven** — all block types live in `BlockSchema.js`; easy to extend
19
+ - **Default CSS** — ships with `griot.css`; dark theme with CSS variables scoped to `.griot-editor` / `.griot-viewer`
20
+ - **Zero dependencies** — pure ES modules, no framework, no bundler required
13
21
 
14
22
  ---
15
23
 
@@ -19,84 +27,199 @@ A lightweight, extensible block editor and viewer for the web. Inspired by Notio
19
27
  npm install griot
20
28
  ```
21
29
 
22
- Or include it directly via ES module:
30
+ Or via ES module directly:
23
31
 
24
32
  ```html
25
33
  <script type="module">
26
34
  import { Editor, Viewer } from './path/to/griot.js';
27
- // ...
28
35
  </script>
29
36
  ```
30
37
 
31
38
  ---
32
39
 
40
+ ## Styling
41
+
42
+ Griot ships a complete default stylesheet (`griot.css`). Import it once:
43
+
44
+ ```html
45
+ <link rel="stylesheet" href="node_modules/griot/griot.css">
46
+ ```
47
+
48
+ All styles are scoped to `.griot-editor` and `.griot-viewer`. Every value is a CSS variable — override any of them to theme Griot to your app:
49
+
50
+ ```css
51
+ :root {
52
+ --griot-bg: #060918;
53
+ --griot-surface: rgba(255,255,255,0.03);
54
+ --griot-surface-hover: rgba(255,255,255,0.055);
55
+ --griot-border: rgba(255,255,255,0.07);
56
+ --griot-border-focus: rgba(99,102,241,0.5);
57
+ --griot-accent: #6366f1;
58
+ --griot-accent-soft: rgba(99,102,241,0.12);
59
+ --griot-accent-text: #a5b4fc;
60
+ --griot-text: #e2e8f0;
61
+ --griot-text-muted: #64748b;
62
+ --griot-text-faint: #334155;
63
+ --griot-code-bg: rgba(0,0,0,0.45);
64
+ --griot-code-color: #a5f3fc;
65
+ --griot-font-body: system-ui, -apple-system, sans-serif;
66
+ --griot-font-mono: 'Fira Code', 'Cascadia Code', monospace;
67
+ --griot-font-serif: 'Georgia', 'Times New Roman', serif;
68
+ --griot-radius: 8px;
69
+ }
70
+ ```
71
+
72
+ ---
73
+
33
74
  ## Quick Start
34
75
 
35
76
  ### Editor
36
77
 
37
78
  ```html
38
- <div id="editor-container"></div>
79
+ <link rel="stylesheet" href="griot.css">
80
+ <div id="editor"></div>
81
+
39
82
  <script type="module">
40
83
  import { Editor, createDocument } from 'griot';
41
84
 
42
- const container = document.getElementById('editor-container');
43
- const doc = createDocument([
44
- { id: 'b1', type: 'heading', text: 'Hello World', meta: { level: 1 } },
45
- { id: 'b2', type: 'paragraph', text: 'This is **editable** content.' },
46
- ]);
47
-
48
- const editor = new Editor(container, {
49
- doc,
50
- books: [], // optional, for book citations
51
- onChange: (newDoc) => {
52
- console.log('Document changed:', newDoc);
53
- },
54
- onEventClick: (eventId) => {
55
- console.log('Event clicked:', eventId);
56
- },
57
- onCiteClick: (blockId) => {
58
- console.log('Citation clicked:', blockId);
59
- },
60
- onRequestBookPicker: (blockId, callback) => {
61
- // Open your own book picker UI, then call callback with selection
62
- callback({ bookId: 'book1', unitId: 'unit1', quote: '', note: '' });
63
- }
85
+ const editor = new Editor(document.getElementById('editor'), {
86
+ doc: createDocument([
87
+ { id: 'b1', type: 'heading', text: 'Hello World', meta: { level: 1 } },
88
+ { id: 'b2', type: 'paragraph', text: 'Start writing…' },
89
+ ]),
90
+ books: [],
91
+ onChange: (doc) => console.log('changed', doc),
92
+ onEventClick: (eventId) => console.log('event', eventId),
93
+ onCiteClick: (blockId) => console.log('cite', blockId),
94
+ onRequestBookPicker: (blockId, cb) => cb({ bookId: 'b1', unitId: 'u1', quote: '', note: '' }),
64
95
  });
65
-
66
- // Later, if you need to replace the document:
67
- editor.setDoc(newDoc);
68
96
  </script>
69
97
  ```
70
98
 
71
99
  ### Viewer
72
100
 
73
101
  ```html
74
- <div id="viewer-container"></div>
102
+ <div id="viewer"></div>
103
+
75
104
  <script type="module">
76
105
  import { Viewer } from 'griot';
77
106
 
78
- const container = document.getElementById('viewer-container');
79
- const viewer = new Viewer(container, {
80
- doc: myDocument,
81
- books: myBooks,
82
- onEventClick: (eventId) => { /* ... */ },
83
- onCiteClick: (blockId) => { /* ... */ },
84
- highlightBlockId: 'b2' // optional initial highlight
107
+ const viewer = new Viewer(document.getElementById('viewer'), {
108
+ doc: myDocument,
109
+ books: myBooks,
110
+ onEventClick: (eventId) => { /* … */ },
111
+ onCiteClick: (blockId) => { /* */ },
112
+ highlightBlockId: 'b2',
85
113
  });
86
114
 
87
- // Highlight and scroll to a block
88
- viewer.setHighlight('b1');
115
+ viewer.setHighlight('b1'); // scroll to + 2.2s pulse-highlight
89
116
  </script>
90
117
  ```
91
118
 
92
119
  ---
93
120
 
121
+ ## Block Types
122
+
123
+ All 19 block types are defined in `BlockSchema.js`.
124
+
125
+ ### Text (10 types)
126
+
127
+ | Type | Slash label | Notes |
128
+ |---|---|---|
129
+ | `paragraph` | Text | Supports full inline markup; live preview strip below input |
130
+ | `heading` | Heading | Levels 1–6; level picker in editor toolbar |
131
+ | `blockquote` | Quote | Supports inline markup |
132
+ | `callout` | Callout | 💡 Customisable icon |
133
+ | `callout_warning` | Warning | ⚠️ |
134
+ | `callout_tip` | Tip | ✅ |
135
+ | `callout_danger` | Danger | 🚨 |
136
+ | `code` | Code block | Language input in toolbar; `pre` white-space; monospace |
137
+ | `list_ul` | Bullet list | One item per line; Enter inserts newline |
138
+ | `list_ol` | Numbered list | One item per line; Enter inserts newline |
139
+
140
+ ### Media (4 types)
141
+
142
+ | Type | Slash label | Notes |
143
+ |---|---|---|
144
+ | `image` | Image | `src`, `alt`, `caption`, `width` (`full` etc.) |
145
+ | `video` | Video | Auto-embeds YouTube (incl. Shorts) and Vimeo; falls back to native `<video>` |
146
+ | `audio` | Audio | Auto-embeds Spotify (track/album/playlist/episode) and SoundCloud; falls back to native `<audio>` |
147
+ | `gallery` | Gallery | Multiple items; layout: `grid`, `masonry`, `carousel`, or `strip` |
148
+
149
+ ### Embed (1 type)
150
+
151
+ | Type | Slash label | Notes |
152
+ |---|---|---|
153
+ | `embed` | Embed / iframe | Generic `<iframe>` with configurable `height` and optional `caption` |
154
+
155
+ ### Structure (4 types)
156
+
157
+ | Type | Slash label | Notes |
158
+ |---|---|---|
159
+ | `table` | Table | Full WYSIWYG editor with add/remove rows and columns; inline markup in cells |
160
+ | `divider` | Divider | `<hr>` |
161
+ | `timeline_ref` | Timeline event | `eventId`, `eventTitle`, `note`; clickable in viewer → `onEventClick` |
162
+ | `book_citation` | Book citation | `bookId`, `unitId`, `quote`, `note`; triggers `onRequestBookPicker` |
163
+
164
+ ---
165
+
166
+ ## Inline Markup
167
+
168
+ The inline parser (`InlineLexer.js`) is fully independent and can be used standalone. Twelve token types are supported, evaluated in priority order:
169
+
170
+ | Syntax | Token | Renders as |
171
+ |---|---|---|
172
+ | `**bold**` | `BOLD` | `<strong>` |
173
+ | `*italic*` | `ITALIC` | `<em>` |
174
+ | `__underline__` | `UNDERLINE` | `<u>` |
175
+ | `~~strikethrough~~` | `STRIKE` | `<s>` |
176
+ | `` `code` `` | `CODE` | `<code class="griot-inline-code">` |
177
+ | `==highlight==` | `HIGHLIGHT` | `<mark class="griot-highlight">` |
178
+ | `{#f00:red}` or `{tomato:text}` | `COLOR_MARK` | `<span style="color:…">` |
179
+ | `[label](url)` | `LINK` | `<a class="griot-link" target="_blank">` |
180
+ | `![alt](url)` | `IMAGE` | `<img class="griot-inline-img">` |
181
+ | `[[event:id\|label]]` | `EVENT_REF` | Clickable chip → `onEventClick` |
182
+ | `[[cite:id\|label]]` | `CITE_REF` | Clickable chip → `onCiteClick` |
183
+
184
+ ---
185
+
186
+ ## Markdown Block Shortcuts
187
+
188
+ Typing these at the **start** of a block converts it instantly:
189
+
190
+ | Pattern | Converts to |
191
+ |---|---|
192
+ | `# ` | Heading H1 |
193
+ | `## ` through `###### ` | Heading H2–H6 |
194
+ | `> ` | Blockquote |
195
+ | `- ` or `* ` | Bullet list |
196
+ | `1. ` | Numbered list |
197
+ | `--- ` | Divider (text cleared) |
198
+ | ` ``` ` or ` ``` ` + space | Code block |
199
+
200
+ ---
201
+
202
+ ## Editor Keyboard Shortcuts
203
+
204
+ | Key | Action |
205
+ |---|---|
206
+ | `Enter` | Split block at cursor (newline in list blocks) |
207
+ | `Backspace` at offset 0 | Merge block with previous; cursor placed at merge point |
208
+ | `Delete` at end | Merge next block into current |
209
+ | `↑` on first visual line | Move focus to previous block |
210
+ | `↓` on last visual line | Move focus to next block |
211
+ | `Ctrl/Cmd+Z` | Undo |
212
+ | `Ctrl/Cmd+Y` or `Ctrl/Cmd+Shift+Z` | Redo |
213
+ | `Ctrl/Cmd+B` | Wrap selection in `**…**` |
214
+ | `Ctrl/Cmd+I` | Wrap selection in `*…*` |
215
+ | `Ctrl/Cmd+U` | Wrap selection in `__…__` |
216
+
217
+ ---
218
+
94
219
  ## Concepts
95
220
 
96
221
  ### Document
97
222
 
98
- A Griot document is a plain object with an `id` and an array of blocks:
99
-
100
223
  ```typescript
101
224
  interface Document {
102
225
  id: string;
@@ -106,267 +229,214 @@ interface Document {
106
229
 
107
230
  ### Block
108
231
 
109
- Every block has at least `id`, `type`, and optionally `text` and `meta`. The `text` field is only present for block types that contain editable text (e.g. paragraphs, headings). All other block types store their data in `meta`.
110
-
111
232
  ```typescript
112
233
  interface Block {
113
234
  id: string;
114
235
  type: string;
115
- text?: string | null;
236
+ text: string | null; // only present when hasText: true in schema
116
237
  meta: Record<string, any>;
117
238
  }
118
239
  ```
119
240
 
120
- ### Inline Markup
121
-
122
- Within text blocks, you can use lightweight syntax:
123
-
124
- | Syntax | Result |
125
- |---|---|
126
- | `**bold**` | **bold** |
127
- | `*italic*` | *italic* |
128
- | `__underline__` | underline |
129
- | `~~strikethrough~~` | ~~strikethrough~~ |
130
- | `` `code` `` | `code` |
131
- | `==highlight==` | highlight |
132
- | `{#ff0000:red text}` or `{blue:text}` | colored text |
133
- | `[link text](https://example.com)` | link |
134
- | `![alt text](image.jpg)` | image |
135
- | `[[event:eventId\|label]]` | clickable chip → `onEventClick` |
136
- | `[[cite:blockId\|label]]` | clickable chip → `onCiteClick` |
137
-
138
- The inline parser is fully independent and can be used separately: `tokenizeInline()`, `renderInlineToDOM()`, `renderInlineToHTML()`.
139
-
140
- ### Block Schema
141
-
142
- All block types are defined in `BlockSchema.js`. Each definition includes category, label, icon, slash label, whether it has text, default meta, and placeholder. You can extend the schema by adding new entries.
143
-
144
- ### History
145
-
146
- The `History` class provides a simple linear undo/redo stack. The editor uses it internally; you can also use it standalone.
147
-
148
241
  ---
149
242
 
150
243
  ## API Reference
151
244
 
152
- The public API is exposed through the main entry point (`griot.js`). Below are the most important exports.
153
-
154
245
  ### Core
155
246
 
156
247
  | Export | Description |
157
248
  |---|---|
158
- | `createBlock(type, overrides?)` | Create a new block with a unique id. |
159
- | `cloneBlock(block, newId = true)` | Deep clone a block. |
160
- | `isTextBlock(block)` | Check if a block stores a text string. |
161
- | `isValidBlock(block)` | Minimal structural check. |
162
- | `anchorId(blockId)` | Generate the DOM id used for a block element. |
163
- | `scrollToBlock(blockId, behavior = 'smooth')` | Scroll to a block's element. |
164
- | `TEXT_TYPES` | Set of block types that have a text field. |
165
- | `ALL_TYPES` | Array of all known block type names. |
249
+ | `createBlock(type, overrides?)` | New block with unique id |
250
+ | `cloneBlock(block, newId?)` | Deep clone; `newId` defaults to `true` |
251
+ | `isTextBlock(block)` | `true` if block has a text field |
252
+ | `isValidBlock(block)` | Minimal structural check |
253
+ | `anchorId(blockId)` | DOM `id` string for a block element |
254
+ | `scrollToBlock(blockId, behavior?)` | `scrollIntoView` wrapper |
255
+ | `TEXT_TYPES` | `Set<string>` of types that carry a text field |
256
+ | `ALL_TYPES` | `string[]` of all known types |
166
257
 
167
258
  ### Document Operations
168
259
 
169
- All functions are immutable they return a new document.
260
+ All functions are immutable they return a new document object.
170
261
 
171
262
  | Export | Description |
172
263
  |---|---|
173
- | `createDocument(blocks?)` | Create a new document (with at least one paragraph). |
174
- | `toJSON(doc)` / `fromJSON(json)` | Serialize / deserialize. |
175
- | `getBlock(doc, id)` | Find a block by id. |
176
- | `getBlockIndex(doc, id)` | Get index of a block. |
177
- | `getBlockBefore(doc, id)` / `getBlockAfter(doc, id)` | Adjacent blocks. |
178
- | `updateBlock(doc, id, patch)` | Update text and/or meta. |
179
- | `insertBlockAfter(doc, afterId, newBlock)` | Insert block. |
180
- | `insertBlockBefore(doc, beforeId, newBlock)` | Insert block. |
181
- | `removeBlock(doc, id)` | Delete a block. |
182
- | `moveBlock(doc, fromIdx, toIdx)` | Reorder blocks. |
183
- | `splitBlock(doc, blockId, offset)` | Split a text block at offset. |
184
- | `mergeBlockWithPrev(doc, blockId)` | Merge block into previous one. |
264
+ | `createDocument(blocks?)` | New document; falls back to a single empty paragraph |
265
+ | `toJSON(doc)` / `fromJSON(json)` | Serialize / deserialize |
266
+ | `getBlock(doc, id)` | Find a block by id |
267
+ | `getBlockIndex(doc, id)` | Index of a block |
268
+ | `getBlockBefore(doc, id)` / `getBlockAfter(doc, id)` | Adjacent blocks |
269
+ | `updateBlock(doc, id, patch)` | Patch `text`, `type`, and/or `meta` (meta is shallow-merged) |
270
+ | `insertBlockAfter(doc, afterId, block)` | Insert a block |
271
+ | `insertBlockBefore(doc, beforeId, block)` | Insert a block |
272
+ | `removeBlock(doc, id)` | Delete a block |
273
+ | `moveBlock(doc, fromIdx, toIdx)` | Reorder blocks |
274
+ | `splitBlock(doc, blockId, offset)` | Split at cursor offset; headings become paragraphs. Returns `[newDoc, newBlockId]` |
275
+ | `mergeBlockWithPrev(doc, blockId)` | Concatenate text with previous block. Returns `[newDoc, prevId, mergeOffset]` |
185
276
 
186
277
  ### Inline Parsing & Rendering
187
278
 
188
279
  | Export | Description |
189
280
  |---|---|
190
- | `tokenizeInline(text)` | Return an array of token objects. |
191
- | `renderInlineToDOM(text, callbacks?)` | Render tokens into a DocumentFragment. |
192
- | `renderInlineToHTML(text)` | Render tokens into an HTML string. |
193
- | `escHtml(str)` / `escAttr(str)` | Escape helpers. |
194
- | `TOKEN` | Enum of token types. |
281
+ | `tokenizeInline(text)` | Returns `Token[]` |
282
+ | `renderInlineToDOM(text, callbacks?)` | Returns a `DocumentFragment` |
283
+ | `renderInlineToHTML(text)` | Returns an HTML string |
284
+ | `escHtml(str)` / `escAttr(str)` | Escape helpers |
285
+ | `TOKEN` | Frozen enum of all token type strings |
195
286
 
196
- ### Block Rendering (for Viewer or custom use)
287
+ ### Block Rendering
197
288
 
198
289
  | Export | Description |
199
290
  |---|---|
200
- | `renderBlock(block, options)` | Render a single block to a DOM element. Used by Viewer. |
201
- | `getBlockDef(type)` | Get the schema definition for a block type. |
202
- | `getAllTypes()` | All registered block type names. |
203
- | `getTypesByCategory(category)` | Filter types by category. |
204
- | `defaultMeta(type)` | Get default meta for a type. |
205
- | `resolveYouTube(src)` / `resolveVimeo` / `resolveSpotify` / `resolveSoundCloud` | Extract embed URLs from various sources. |
206
-
207
- ### Editor Classes
291
+ | `renderBlock(block, options)` | Renders a single block to a DOM element |
292
+ | `getBlockDef(type)` | Schema definition for a type |
293
+ | `getAllTypes()` | All registered type names |
294
+ | `getTypesByCategory(category)` | Types filtered by `'text'`, `'media'`, `'embed'`, or `'structure'` |
295
+ | `defaultMeta(type)` | Default meta object for a type |
296
+ | `resolveYouTube(src)` | Extracts YouTube embed URL |
297
+ | `resolveVimeo(src)` | Extracts Vimeo embed URL |
298
+ | `resolveSpotify(src)` | Extracts Spotify embed URL |
299
+ | `resolveSoundCloud(src)` | Builds SoundCloud player URL |
208
300
 
209
- | Export | Description |
210
- |---|---|
211
- | `Editor` | Main editor class. See constructor options below. |
212
- | `FormatToolbar` | Floating formatting toolbar (used internally, but can be used standalone). |
213
- | `SlashMenu` | Slash command menu (used internally). |
214
- | `DropHandler` | Handles drag & drop of files/images (not yet shown, but exported). |
215
-
216
- **Editor constructor options:**
301
+ ### Editor
217
302
 
218
303
  ```typescript
219
- {
220
- doc: Document; // initial document
221
- books?: Book[]; // array of book objects for citations
222
- onChange?: (doc: Document) => void; // called after every change (debounced)
304
+ new Editor(container: HTMLElement, options: {
305
+ doc: Document;
306
+ books?: Book[];
307
+ onChange?: (doc: Document) => void; // debounced 400 ms while typing
223
308
  onEventClick?: (eventId: string) => void;
224
309
  onCiteClick?: (blockId: string) => void;
225
- onRequestBookPicker?: (blockId: string, callback: (selection) => void) => void;
226
- }
310
+ onRequestBookPicker?: (
311
+ blockId: string,
312
+ callback: (selection: { bookId: string; unitId: string; quote: string; note: string }) => void
313
+ ) => void;
314
+ })
227
315
  ```
228
316
 
229
- **Editor methods:**
317
+ **Methods:** `setDoc(doc)`, `setBooks(books)`, `focus(blockId)`, `destroy()`
230
318
 
231
- - `setDoc(doc)`replace the document.
232
- - `setBooks(books)` – update the book list.
233
- - `focus(blockId)` – focus a specific block.
234
- - `destroy()` – clean up.
319
+ **Per-block toolbar:** type switcher (all 19 types), heading level selector (H1H6), code language input, move up/down, add below, delete.
235
320
 
236
- ### Viewer
237
-
238
- | Export | Description |
239
- |---|---|
240
- | `Viewer` | Read‑only renderer. |
321
+ **`onChange` debouncing:** while the user types, intermediate state is captured via `history.replace()`. A new undo point is committed 400 ms after the last keystroke.
241
322
 
242
- **Viewer constructor options:**
323
+ ### Viewer
243
324
 
244
325
  ```typescript
245
- {
326
+ new Viewer(container: HTMLElement, options: {
246
327
  doc?: Document;
247
328
  books?: Book[];
248
329
  onEventClick?: (eventId: string) => void;
249
330
  onCiteClick?: (blockId: string) => void;
250
- highlightBlockId?: string; // initial highlight
251
- }
331
+ highlightBlockId?: string;
332
+ })
252
333
  ```
253
334
 
254
- **Viewer methods:**
335
+ **Methods:** `setDoc(doc)`, `setBooks(books)`, `setHighlight(blockId, options?)`, `destroy()`
255
336
 
256
- - `setDoc(doc)`
257
- - `setBooks(books)`
258
- - `setHighlight(blockId, options?)` – scroll to and briefly highlight a block.
259
- - `destroy()`
260
-
261
- ### Keyboard Helpers
262
-
263
- | Export | Description |
264
- |---|---|
265
- | `attachKeyboardHandler(el, blockId, callbacks)` | Attach editor keyboard shortcuts to a contenteditable element. |
266
- | `getCursorOffset(el)` / `setCursorOffset(el, offset)` | Get/set caret position by character offset. |
267
- | `getSelectionOffsets(el)` | Get start/end offsets of current selection. |
268
- | `focusAtEnd(el)` / `focusAtStart(el)` | Move caret to end/start. |
337
+ `setHighlight` scrolls to the block and applies a 2.2-second CSS pulse animation, then removes the highlight class automatically.
269
338
 
270
339
  ### History
271
340
 
272
341
  ```javascript
273
342
  import { History } from 'griot';
274
343
 
275
- const history = new History(initialDoc);
276
- history.push(newDoc);
277
- history.undo(); // returns previous document
278
- history.redo();
279
- history.current; // current document
344
+ const history = new History(initialDoc); // max 200 snapshots
345
+ history.push(doc); // new undo point (discards redo future)
346
+ history.replace(doc); // overwrite current snapshot without a new undo point
347
+ history.undo(); // returns previous document
348
+ history.redo(); // returns next document
349
+ history.current; // current document
350
+ history.canUndo(); // boolean
351
+ history.canRedo(); // boolean
280
352
  ```
281
353
 
282
- ---
283
-
284
- ## Styling
285
-
286
- Griot includes no CSS by design – you can style it to match your application. All elements have semantic class names prefixed with `griot-`. For a quick start, you can copy the example styles from the test page or browse the class names used in the source.
287
-
288
- **Minimal recommended styles:**
354
+ ### Keyboard Helpers
289
355
 
290
- - Make `.griot-editor-block__input[contenteditable]` look like a normal block.
291
- - Add basic spacing and borders.
292
- - Style the floating toolbar and slash menu as floating cards.
356
+ | Export | Description |
357
+ |---|---|
358
+ | `attachKeyboardHandler(el, blockId, callbacks)` | Attach all editor key bindings to a `contenteditable` |
359
+ | `getCursorOffset(el)` | Character offset of caret |
360
+ | `setCursorOffset(el, offset)` | Move caret to character offset |
361
+ | `getSelectionOffsets(el)` | `{ start, end }` of current selection |
362
+ | `focusAtEnd(el)` / `focusAtStart(el)` | Move caret to end / start |
293
363
 
294
364
  ---
295
365
 
296
- ## Examples
297
-
298
- ### Basic Editor with Slash Menu and Toolbar
366
+ ## Multimedia
299
367
 
300
- The editor includes the slash menu and format toolbar automatically. Just instantiate it.
301
-
302
- ### Using the Viewer with Highlight
368
+ ### Gallery layouts
303
369
 
304
370
  ```javascript
305
- const viewer = new Viewer(container, { doc });
306
- viewer.setHighlight('some-block-id');
371
+ { type: 'gallery', meta: { items: [{ src, alt, caption }, …], layout: 'grid' } }
372
+ // layout: 'grid' | 'masonry' | 'carousel' | 'strip'
307
373
  ```
308
374
 
309
- ### Custom Book Picker
310
-
311
- When a `book_citation` block is added, the editor calls `onRequestBookPicker`. Implement your own modal or dropdown:
312
-
313
- ```javascript
314
- onRequestBookPicker: (blockId, callback) => {
315
- const book = prompt('Enter book ID:');
316
- const unit = prompt('Enter unit ID:');
317
- callback({ bookId: book, unitId: unit, quote: '', note: '' });
318
- }
319
- ```
375
+ ### Auto-embed detection
320
376
 
321
- ### Using Inline Renderer Standalone
377
+ Setting `meta.src` on a `video` or `audio` block automatically produces the right embed:
322
378
 
323
- ```javascript
324
- import { renderInlineToDOM } from 'griot';
325
-
326
- const text = 'This is **bold** and [a link](https://example.com).';
327
- const fragment = renderInlineToDOM(text, {
328
- onEventClick: (id) => console.log(id),
329
- onCiteClick: (id) => console.log(id)
330
- });
331
- document.getElementById('output').appendChild(fragment);
332
- ```
379
+ | URL pattern | Result |
380
+ |---|---|
381
+ | `youtube.com/watch?v=…`, `youtu.be/…`, `/shorts/…` | YouTube iframe |
382
+ | `vimeo.com/…` | Vimeo iframe |
383
+ | `open.spotify.com/track/…` (album/playlist/episode too) | Spotify iframe |
384
+ | `soundcloud.com/…` | SoundCloud widget |
385
+ | Anything else | Native `<video>` / `<audio>` |
333
386
 
334
387
  ---
335
388
 
336
389
  ## Extending
337
390
 
338
- ### Adding a New Block Type
391
+ ### Adding a Block Type
339
392
 
340
- 1. Add an entry in `BlockSchema.js` (or patch the schema at runtime).
341
- 2. Add a rendering case in `BlockRenderer.js`.
342
- 3. If the block has a special editor UI, add a case in `Editor._buildSpecialBlockUI()`.
343
- 4. (Optional) Add support in the slash menu – it reads from the schema automatically.
393
+ 1. Add an entry to `BlockSchema.js` (or patch the schema at runtime).
394
+ 2. Add a rendering case to `BlockRenderer.js`.
395
+ 3. If it needs an editor UI, add a case to `Editor._buildSpecialBlockUI()`.
396
+ 4. The slash menu reads from the schema — no changes needed there.
344
397
 
345
398
  ### Custom Inline Syntax
346
399
 
347
- Modify `InlineLexer.js` by adding a new rule. Then update `InlineRenderer.js` to render the new token type.
400
+ Add a rule to `InlineLexer.js`, then handle the new token type in both `InlineRenderer._toNode()` (DOM) and `InlineRenderer._toHTML()` (HTML string).
401
+
402
+ ---
403
+
404
+ ## Project Structure
405
+
406
+ ```
407
+ src/
408
+ core/
409
+ Block.js — block primitives, TEXT_TYPES, uid, anchorId
410
+ Document.js — immutable document operations
411
+ History.js — undo/redo stack (max 200)
412
+ blocks/
413
+ BlockSchema.js — single source of truth for all 19 block types
414
+ BlockRenderer.js — block → DOM element (used by Viewer)
415
+ inline/
416
+ InlineLexer.js — tokenizer (12 token types)
417
+ InlineRenderer.js — tokens → DOM fragment or HTML string
418
+ editor/
419
+ Editor.js — full editing lifecycle
420
+ FormatToolbar.js — floating selection toolbar
421
+ SlashMenu.js — slash command palette
422
+ Keyboard.js — key bindings and cursor helpers
423
+ viewer/
424
+ Viewer.js — read-only renderer
425
+ griot.js — public entry point
426
+ griot.css — default dark theme (CSS variables)
427
+ ```
348
428
 
349
429
  ---
350
430
 
351
431
  ## Development
352
432
 
353
433
  ```bash
354
- git clone https://github.com/yourname/griot.git
434
+ git clone https://github.com/yourname/griot.git
355
435
  cd griot
356
436
  npm install
357
- npm run dev # serves test page at localhost:5000 (or similar)
437
+ npm run dev # dev server at localhost:5000
358
438
  ```
359
439
 
360
- The source is organised as:
361
-
362
- - `src/core/` – block primitives, document operations, history.
363
- - `src/blocks/` – block schema and renderer.
364
- - `src/inline/` – inline lexer and renderer.
365
- - `src/editor/` – editor UI, keyboard handling, slash menu, toolbar.
366
- - `src/viewer/` – read-only renderer.
367
-
368
- All public exports are aggregated in `src/griot.js`.
369
-
370
440
  ---
371
441
 
372
442
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0m0g1/griot",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A self-contained block-based rich text editor and renderer built for historical document authoring.",
5
5
  "type": "module",
6
6
  "main": "./src/Griot.js",