@0m0g1/griot 0.1.4 → 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 +292 -222
- package/package.json +1 -1
- package/src/Griot.js +3 -0
- package/src/viewer/GalleryRenderer.js +417 -0
- package/src/viewer/Lightbox.js +320 -0
- package/src/viewer/Viewer.js +47 -8
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,
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
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
|
|
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
|
-
<
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
102
|
+
<div id="viewer"></div>
|
|
103
|
+
|
|
75
104
|
<script type="module">
|
|
76
105
|
import { Viewer } from 'griot';
|
|
77
106
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
//
|
|
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
|
+
| `` | `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
|
|
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
|
-
| `` | 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?)` |
|
|
159
|
-
| `cloneBlock(block, newId
|
|
160
|
-
| `isTextBlock(block)` |
|
|
161
|
-
| `isValidBlock(block)` | Minimal structural check
|
|
162
|
-
| `anchorId(blockId)` |
|
|
163
|
-
| `scrollToBlock(blockId, behavior
|
|
164
|
-
| `TEXT_TYPES` | Set of
|
|
165
|
-
| `ALL_TYPES` |
|
|
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
|
|
260
|
+
All functions are immutable — they return a new document object.
|
|
170
261
|
|
|
171
262
|
| Export | Description |
|
|
172
263
|
|---|---|
|
|
173
|
-
| `createDocument(blocks?)` |
|
|
174
|
-
| `toJSON(doc)` / `fromJSON(json)` | Serialize / deserialize
|
|
175
|
-
| `getBlock(doc, id)` | Find a block by id
|
|
176
|
-
| `getBlockIndex(doc, id)` |
|
|
177
|
-
| `getBlockBefore(doc, id)` / `getBlockAfter(doc, id)` | Adjacent blocks
|
|
178
|
-
| `updateBlock(doc, id, patch)` |
|
|
179
|
-
| `insertBlockAfter(doc, afterId,
|
|
180
|
-
| `insertBlockBefore(doc, beforeId,
|
|
181
|
-
| `removeBlock(doc, id)` | Delete a block
|
|
182
|
-
| `moveBlock(doc, fromIdx, toIdx)` | Reorder blocks
|
|
183
|
-
| `splitBlock(doc, blockId, offset)` | Split
|
|
184
|
-
| `mergeBlockWithPrev(doc, blockId)` |
|
|
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)` |
|
|
191
|
-
| `renderInlineToDOM(text, callbacks?)` |
|
|
192
|
-
| `renderInlineToHTML(text)` |
|
|
193
|
-
| `escHtml(str)` / `escAttr(str)` | Escape helpers
|
|
194
|
-
| `TOKEN` |
|
|
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
|
|
287
|
+
### Block Rendering
|
|
197
288
|
|
|
198
289
|
| Export | Description |
|
|
199
290
|
|---|---|
|
|
200
|
-
| `renderBlock(block, options)` |
|
|
201
|
-
| `getBlockDef(type)` |
|
|
202
|
-
| `getAllTypes()` | All registered
|
|
203
|
-
| `getTypesByCategory(category)` |
|
|
204
|
-
| `defaultMeta(type)` |
|
|
205
|
-
| `resolveYouTube(src)`
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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;
|
|
221
|
-
books?: Book[];
|
|
222
|
-
onChange?: (doc: Document) => void;
|
|
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?: (
|
|
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
|
-
**
|
|
317
|
+
**Methods:** `setDoc(doc)`, `setBooks(books)`, `focus(blockId)`, `destroy()`
|
|
230
318
|
|
|
231
|
-
-
|
|
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 (H1–H6), code language input, move up/down, add below, delete.
|
|
235
320
|
|
|
236
|
-
|
|
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
|
-
|
|
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;
|
|
251
|
-
}
|
|
331
|
+
highlightBlockId?: string;
|
|
332
|
+
})
|
|
252
333
|
```
|
|
253
334
|
|
|
254
|
-
**
|
|
335
|
+
**Methods:** `setDoc(doc)`, `setBooks(books)`, `setHighlight(blockId, options?)`, `destroy()`
|
|
255
336
|
|
|
256
|
-
-
|
|
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(
|
|
277
|
-
history.
|
|
278
|
-
history.
|
|
279
|
-
history.
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
##
|
|
297
|
-
|
|
298
|
-
### Basic Editor with Slash Menu and Toolbar
|
|
366
|
+
## Multimedia
|
|
299
367
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
### Using the Viewer with Highlight
|
|
368
|
+
### Gallery layouts
|
|
303
369
|
|
|
304
370
|
```javascript
|
|
305
|
-
|
|
306
|
-
|
|
371
|
+
{ type: 'gallery', meta: { items: [{ src, alt, caption }, …], layout: 'grid' } }
|
|
372
|
+
// layout: 'grid' | 'masonry' | 'carousel' | 'strip'
|
|
307
373
|
```
|
|
308
374
|
|
|
309
|
-
###
|
|
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
|
-
|
|
377
|
+
Setting `meta.src` on a `video` or `audio` block automatically produces the right embed:
|
|
322
378
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
391
|
+
### Adding a Block Type
|
|
339
392
|
|
|
340
|
-
1. Add an entry
|
|
341
|
-
2. Add a rendering case
|
|
342
|
-
3. If
|
|
343
|
-
4.
|
|
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
|
-
|
|
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 #
|
|
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