@0m0g1/griot 0.1.3 → 0.1.5

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,179 +1,444 @@
1
1
  # Griot
2
2
 
3
- A self-contained block-based rich text editor and renderer.
4
- Built for structured historical document authoring — works standalone or embedded inside a larger app.
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.
5
4
 
6
5
  ---
7
6
 
8
- ## Install
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
21
+
22
+ ---
23
+
24
+ ## Installation
9
25
 
10
26
  ```bash
11
- # Copy src/ into your project, or:
12
- npm install griot # (once published)
27
+ npm install griot
28
+ ```
29
+
30
+ Or via ES module directly:
31
+
32
+ ```html
33
+ <script type="module">
34
+ import { Editor, Viewer } from './path/to/griot.js';
35
+ </script>
36
+ ```
37
+
38
+ ---
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">
13
46
  ```
14
47
 
15
- ```js
16
- import '@0m0g1/griot/css'; // styles
17
- import { Editor, Viewer, createDocument } from 'griot';
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
+ }
18
70
  ```
19
71
 
20
72
  ---
21
73
 
22
- ## Quick start
74
+ ## Quick Start
23
75
 
24
76
  ### Editor
25
77
 
26
- ```js
27
- import { Editor, createDocument } from 'griot';
28
- import '@0m0g1/griot/css';
29
-
30
- const doc = createDocument('My Article');
31
-
32
- const editor = new Editor(document.querySelector('#editor'), {
33
- doc,
34
- books: [], // optional: parsed books for citations
35
- onChange(updatedDoc) {
36
- localStorage.setItem('draft', JSON.stringify(updatedDoc));
37
- },
38
- onEventClick(eventId) {
39
- // e.g. AppShell.handleSelectItemById(eventId)
40
- console.log('Open timeline event:', eventId);
41
- },
42
- onCiteClick(blockId) {
43
- // scroll viewer to that block
44
- viewer.setHighlight(blockId);
45
- },
46
- onRequestBookPicker(blockId, callback) {
47
- // Open your SourcePicker UI, then call:
48
- // callback({ bookId, unitId, quote, note })
49
- },
50
- });
78
+ ```html
79
+ <link rel="stylesheet" href="griot.css">
80
+ <div id="editor"></div>
81
+
82
+ <script type="module">
83
+ import { Editor, createDocument } from 'griot';
84
+
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: '' }),
95
+ });
96
+ </script>
51
97
  ```
52
98
 
53
99
  ### Viewer
54
100
 
55
- ```js
56
- import { Viewer } from 'griot';
101
+ ```html
102
+ <div id="viewer"></div>
103
+
104
+ <script type="module">
105
+ import { Viewer } from 'griot';
57
106
 
58
- const viewer = new Viewer(document.querySelector('#viewer'), {
59
- doc,
60
- books: [],
61
- onEventClick(eventId) {
62
- console.log('Open event:', eventId);
63
- },
64
- });
107
+ const viewer = new Viewer(document.getElementById('viewer'), {
108
+ doc: myDocument,
109
+ books: myBooks,
110
+ onEventClick: (eventId) => { /* … */ },
111
+ onCiteClick: (blockId) => { /* … */ },
112
+ highlightBlockId: 'b2',
113
+ });
65
114
 
66
- // Jump to a block (e.g. from a timeline citation)
67
- viewer.setHighlight('b_abc123');
115
+ viewer.setHighlight('b1'); // scroll to + 2.2s pulse-highlight
116
+ </script>
68
117
  ```
69
118
 
70
119
  ---
71
120
 
72
- ## Block types
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)
73
141
 
74
- | Type | Icon | Text field | Notes |
75
- |---|---|---|---|
76
- | `paragraph` | | | Inline syntax supported |
77
- | `heading` | H | | `meta.level` 1–6 |
78
- | `blockquote` | | | Inline syntax supported |
79
- | `callout` | 💡 | | `meta.icon` for the emoji |
80
- | `code` | </> | ✓ | No inline parsing. `meta.language` for highlight |
81
- | `divider` | — | — | Horizontal rule |
82
- | `image` | 🖼 | — | `meta.src`, `meta.alt`, `meta.caption` |
83
- | `timeline_ref` | | | `meta.eventId`, `meta.eventTitle`, `meta.note` |
84
- | `book_citation` | 📖 | — | `meta.bookId`, `meta.unitId`, `meta.quote`, `meta.note` |
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` |
85
163
 
86
164
  ---
87
165
 
88
- ## Inline syntax
166
+ ## Inline Markup
89
167
 
90
- Works inside any block with `hasInline: true` (paragraph, blockquote, callout, note fields):
168
+ The inline parser (`InlineLexer.js`) is fully independent and can be used standalone. Twelve token types are supported, evaluated in priority order:
91
169
 
92
- ```
93
- **bold**
94
- *italic*
95
- `inline code`
96
- [link text](https://example.com)
97
- [[event:rome_founding|The Founding of Rome]] → timeline event chip
98
- [[cite:b_abc123|See Chapter 2]] → citation cross-reference
99
- ```
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` |
100
183
 
101
184
  ---
102
185
 
103
- ## Document format (`.griot.json`)
104
-
105
- ```json
106
- {
107
- "version": 1,
108
- "id": "doc_abc",
109
- "title": "The Fall of Rome",
110
- "createdAt": "2025-01-01T00:00:00.000Z",
111
- "updatedAt": "2025-01-01T00:00:00.000Z",
112
- "blocks": [
113
- { "id": "b_1", "type": "heading", "text": "The Fall of Rome", "meta": { "level": 1 } },
114
- { "id": "b_2", "type": "paragraph", "text": "In **476 CE** the last emperor [[event:fall_of_rome|was deposed]].", "meta": {} },
115
- { "id": "b_3", "type": "book_citation", "text": null, "meta": {
116
- "bookId": "book_xyz", "unitId": "unit_abc",
117
- "quote": "The barbarians had long served in Roman armies.",
118
- "note": "Essential context for understanding the transition."
119
- }}
120
- ]
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
+
219
+ ## Concepts
220
+
221
+ ### Document
222
+
223
+ ```typescript
224
+ interface Document {
225
+ id: string;
226
+ blocks: Block[];
227
+ }
228
+ ```
229
+
230
+ ### Block
231
+
232
+ ```typescript
233
+ interface Block {
234
+ id: string;
235
+ type: string;
236
+ text: string | null; // only present when hasText: true in schema
237
+ meta: Record<string, any>;
121
238
  }
122
239
  ```
123
240
 
124
241
  ---
125
242
 
126
- ## Deep-link anchors
243
+ ## API Reference
244
+
245
+ ### Core
246
+
247
+ | Export | Description |
248
+ |---|---|
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 |
257
+
258
+ ### Document Operations
259
+
260
+ All functions are immutable — they return a new document object.
261
+
262
+ | Export | Description |
263
+ |---|---|
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]` |
276
+
277
+ ### Inline Parsing & Rendering
278
+
279
+ | Export | Description |
280
+ |---|---|
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 |
127
286
 
128
- Every rendered block gets `id="griot-{blockId}"` in the DOM.
287
+ ### Block Rendering
129
288
 
130
- ```js
131
- import { anchorId, scrollToBlock } from 'griot';
289
+ | Export | Description |
290
+ |---|---|
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 |
132
300
 
133
- // Get the DOM id for a block
134
- anchorId('b_abc123') // → "griot-b_abc123"
301
+ ### Editor
135
302
 
136
- // Scroll to a block (viewer or editor)
137
- scrollToBlock('b_abc123');
138
- scrollToBlock('b_abc123', 'instant');
303
+ ```typescript
304
+ new Editor(container: HTMLElement, options: {
305
+ doc: Document;
306
+ books?: Book[];
307
+ onChange?: (doc: Document) => void; // debounced 400 ms while typing
308
+ onEventClick?: (eventId: string) => void;
309
+ onCiteClick?: (blockId: string) => void;
310
+ onRequestBookPicker?: (
311
+ blockId: string,
312
+ callback: (selection: { bookId: string; unitId: string; quote: string; note: string }) => void
313
+ ) => void;
314
+ })
139
315
  ```
140
316
 
141
- This is the contract for timeline → article navigation:
142
- store `{ docId, blockId }` on a citation, then call `scrollToBlock(blockId)` when the timeline jumps to it.
317
+ **Methods:** `setDoc(doc)`, `setBooks(books)`, `focus(blockId)`, `destroy()`
143
318
 
144
- ---
319
+ **Per-block toolbar:** type switcher (all 19 types), heading level selector (H1–H6), code language input, move up/down, add below, delete.
320
+
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.
322
+
323
+ ### Viewer
324
+
325
+ ```typescript
326
+ new Viewer(container: HTMLElement, options: {
327
+ doc?: Document;
328
+ books?: Book[];
329
+ onEventClick?: (eventId: string) => void;
330
+ onCiteClick?: (blockId: string) => void;
331
+ highlightBlockId?: string;
332
+ })
333
+ ```
334
+
335
+ **Methods:** `setDoc(doc)`, `setBooks(books)`, `setHighlight(blockId, options?)`, `destroy()`
336
+
337
+ `setHighlight` scrolls to the block and applies a 2.2-second CSS pulse animation, then removes the highlight class automatically.
145
338
 
146
- ## API reference
339
+ ### History
147
340
 
148
- ### `Editor`
149
- | Method | Description |
341
+ ```javascript
342
+ import { History } from 'griot';
343
+
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
352
+ ```
353
+
354
+ ### Keyboard Helpers
355
+
356
+ | Export | Description |
150
357
  |---|---|
151
- | `new Editor(el, options)` | Mount editor into `el` |
152
- | `editor.doc` | Current document (read-only) |
153
- | `editor.setDoc(doc)` | Replace document |
154
- | `editor.setBooks(books)` | Update available books |
155
- | `editor.focus(blockId)` | Focus a specific block |
156
- | `editor.destroy()` | Unmount and clean up |
157
-
158
- ### `Viewer`
159
- | Method | Description |
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 |
363
+
364
+ ---
365
+
366
+ ## Multimedia
367
+
368
+ ### Gallery layouts
369
+
370
+ ```javascript
371
+ { type: 'gallery', meta: { items: [{ src, alt, caption }, …], layout: 'grid' } }
372
+ // layout: 'grid' | 'masonry' | 'carousel' | 'strip'
373
+ ```
374
+
375
+ ### Auto-embed detection
376
+
377
+ Setting `meta.src` on a `video` or `audio` block automatically produces the right embed:
378
+
379
+ | URL pattern | Result |
160
380
  |---|---|
161
- | `new Viewer(el, options)` | Mount viewer into `el` |
162
- | `viewer.setDoc(doc)` | Replace document |
163
- | `viewer.setBooks(books)` | Update available books |
164
- | `viewer.setHighlight(blockId)` | Scroll to + briefly highlight a block |
165
- | `viewer.destroy()` | Unmount and clean up |
166
-
167
- ### Document helpers
168
- ```js
169
- createDocument(title)
170
- createBlock(type, overrides)
171
- updateBlock(doc, blockId, patch)
172
- insertBlockAfter(doc, blockId, newBlock)
173
- removeBlock(doc, blockId)
174
- splitBlock(doc, blockId, offset) // returns [newDoc, newBlockId]
175
- mergeBlockWithPrev(doc, blockId) // returns [newDoc, prevId, offset]
176
- moveBlock(doc, fromIndex, toIndex)
177
- toJSON(doc)
178
- fromJSON(jsonStringOrObject)
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>` |
386
+
387
+ ---
388
+
389
+ ## Extending
390
+
391
+ ### Adding a Block Type
392
+
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.
397
+
398
+ ### Custom Inline Syntax
399
+
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
+
179
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
+ ```
428
+
429
+ ---
430
+
431
+ ## Development
432
+
433
+ ```bash
434
+ git clone https://github.com/yourname/griot.git
435
+ cd griot
436
+ npm install
437
+ npm run dev # dev server at localhost:5000
438
+ ```
439
+
440
+ ---
441
+
442
+ ## License
443
+
444
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0m0g1/griot",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",