inkpen 0.7.1

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.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.rubocop.yml +8 -0
  4. data/.yardopts +11 -0
  5. data/CLAUDE.md +141 -0
  6. data/README.md +409 -0
  7. data/Rakefile +19 -0
  8. data/app/assets/javascripts/inkpen/controllers/editor_controller.js +2050 -0
  9. data/app/assets/javascripts/inkpen/controllers/sticky_toolbar_controller.js +667 -0
  10. data/app/assets/javascripts/inkpen/controllers/toolbar_controller.js +693 -0
  11. data/app/assets/javascripts/inkpen/export/html.js +637 -0
  12. data/app/assets/javascripts/inkpen/export/index.js +30 -0
  13. data/app/assets/javascripts/inkpen/export/markdown.js +697 -0
  14. data/app/assets/javascripts/inkpen/export/pdf.js +372 -0
  15. data/app/assets/javascripts/inkpen/extensions/advanced_table.js +640 -0
  16. data/app/assets/javascripts/inkpen/extensions/block_commands.js +300 -0
  17. data/app/assets/javascripts/inkpen/extensions/block_gutter.js +338 -0
  18. data/app/assets/javascripts/inkpen/extensions/callout.js +303 -0
  19. data/app/assets/javascripts/inkpen/extensions/columns.js +403 -0
  20. data/app/assets/javascripts/inkpen/extensions/database.js +990 -0
  21. data/app/assets/javascripts/inkpen/extensions/document_section.js +352 -0
  22. data/app/assets/javascripts/inkpen/extensions/drag_handle.js +407 -0
  23. data/app/assets/javascripts/inkpen/extensions/embed.js +629 -0
  24. data/app/assets/javascripts/inkpen/extensions/enhanced_image.js +566 -0
  25. data/app/assets/javascripts/inkpen/extensions/export_commands.js +271 -0
  26. data/app/assets/javascripts/inkpen/extensions/file_attachment.js +593 -0
  27. data/app/assets/javascripts/inkpen/extensions/inkpen_table/index.js +58 -0
  28. data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table.js +638 -0
  29. data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_cell.js +100 -0
  30. data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_header.js +100 -0
  31. data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_constants.js +152 -0
  32. data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_helpers.js +254 -0
  33. data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_menu.js +282 -0
  34. data/app/assets/javascripts/inkpen/extensions/preformatted.js +239 -0
  35. data/app/assets/javascripts/inkpen/extensions/section.js +281 -0
  36. data/app/assets/javascripts/inkpen/extensions/section_title.js +126 -0
  37. data/app/assets/javascripts/inkpen/extensions/slash_commands.js +439 -0
  38. data/app/assets/javascripts/inkpen/extensions/table_of_contents.js +474 -0
  39. data/app/assets/javascripts/inkpen/extensions/toggle_block.js +332 -0
  40. data/app/assets/javascripts/inkpen/index.js +87 -0
  41. data/app/assets/stylesheets/inkpen/advanced_table.css +514 -0
  42. data/app/assets/stylesheets/inkpen/animations.css +626 -0
  43. data/app/assets/stylesheets/inkpen/block_gutter.css +265 -0
  44. data/app/assets/stylesheets/inkpen/callout.css +359 -0
  45. data/app/assets/stylesheets/inkpen/columns.css +314 -0
  46. data/app/assets/stylesheets/inkpen/database.css +658 -0
  47. data/app/assets/stylesheets/inkpen/document_section.css +305 -0
  48. data/app/assets/stylesheets/inkpen/drag_drop.css +220 -0
  49. data/app/assets/stylesheets/inkpen/editor.css +652 -0
  50. data/app/assets/stylesheets/inkpen/embed.css +468 -0
  51. data/app/assets/stylesheets/inkpen/enhanced_image.css +453 -0
  52. data/app/assets/stylesheets/inkpen/export.css +499 -0
  53. data/app/assets/stylesheets/inkpen/file_attachment.css +347 -0
  54. data/app/assets/stylesheets/inkpen/footnotes.css +136 -0
  55. data/app/assets/stylesheets/inkpen/inkpen_table.css +608 -0
  56. data/app/assets/stylesheets/inkpen/preformatted.css +215 -0
  57. data/app/assets/stylesheets/inkpen/search_replace.css +58 -0
  58. data/app/assets/stylesheets/inkpen/section.css +236 -0
  59. data/app/assets/stylesheets/inkpen/slash_menu.css +252 -0
  60. data/app/assets/stylesheets/inkpen/sticky_toolbar.css +314 -0
  61. data/app/assets/stylesheets/inkpen/toc.css +386 -0
  62. data/app/assets/stylesheets/inkpen/toggle.css +260 -0
  63. data/app/helpers/inkpen/editor_helper.rb +114 -0
  64. data/app/views/inkpen/_editor.html.erb +139 -0
  65. data/config/importmap.rb +170 -0
  66. data/docs/.DS_Store +0 -0
  67. data/docs/CHANGELOG.md +571 -0
  68. data/docs/FEATURES.md +436 -0
  69. data/docs/ROADMAP.md +3029 -0
  70. data/docs/VISION.md +235 -0
  71. data/docs/extensions/INKPEN_TABLE.md +482 -0
  72. data/docs/thinking/CORRECTED_NO_VUE.md +756 -0
  73. data/docs/thinking/EXECUTIVE_SUMMARY.md +403 -0
  74. data/docs/thinking/INKPEN_CODE_SAMPLES.md +1479 -0
  75. data/docs/thinking/INKPEN_MASTER_GUIDE.md +891 -0
  76. data/docs/thinking/README_START_HERE.md +341 -0
  77. data/lib/inkpen/configuration.rb +175 -0
  78. data/lib/inkpen/editor.rb +204 -0
  79. data/lib/inkpen/engine.rb +32 -0
  80. data/lib/inkpen/extensions/base.rb +109 -0
  81. data/lib/inkpen/extensions/code_block_syntax.rb +177 -0
  82. data/lib/inkpen/extensions/document_section.rb +111 -0
  83. data/lib/inkpen/extensions/forced_document.rb +183 -0
  84. data/lib/inkpen/extensions/mention.rb +155 -0
  85. data/lib/inkpen/extensions/preformatted.rb +111 -0
  86. data/lib/inkpen/extensions/section.rb +139 -0
  87. data/lib/inkpen/extensions/slash_commands.rb +100 -0
  88. data/lib/inkpen/extensions/table.rb +182 -0
  89. data/lib/inkpen/extensions/task_list.rb +145 -0
  90. data/lib/inkpen/sticky_toolbar.rb +157 -0
  91. data/lib/inkpen/toolbar.rb +145 -0
  92. data/lib/inkpen/version.rb +5 -0
  93. data/lib/inkpen.rb +101 -0
  94. data/sig/inkpen.rbs +4 -0
  95. metadata +165 -0
data/docs/ROADMAP.md ADDED
@@ -0,0 +1,3029 @@
1
+ # Inkpen: Notion-like Block Editor Implementation Plan
2
+
3
+ > **Vision**: Transform Inkpen into a world-class Notion/Editor.js style block editor while maintaining Rails-native simplicity.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Architecture Overview](#architecture-overview)
10
+ 2. [Completed Extensions](#completed-extensions)
11
+ 3. [Phase 1: Drag & Drop](#phase-1-drag--drop-v032)
12
+ 4. [Phase 2: Enhanced Blocks](#phase-2-enhanced-blocks-v033)
13
+ 5. [Phase 3: BlockNote-Style Polish](#phase-3-blocknote-style-polish-v040)
14
+ 6. [Phase 4: Markdown Mode](#phase-4-markdown-mode-v050)
15
+ 7. [Technical References](#technical-references)
16
+
17
+ ---
18
+
19
+ ## Upcoming: Markdown Mode (v0.5.0)
20
+
21
+ Toggle between WYSIWYG, raw markdown, and split view editing.
22
+
23
+ **See full plan:** [docs/plans/MARKDOWN_MODE.md](plans/MARKDOWN_MODE.md)
24
+
25
+ **Key features:**
26
+ - Mode toggle: WYSIWYG / Markdown / Split
27
+ - Live sync between modes (debounced)
28
+ - Ruby PORO configuration
29
+ - Keyboard shortcuts (`Cmd+Shift+M`)
30
+
31
+ **Status:** 📋 Planned
32
+
33
+ ---
34
+
35
+ ## Architecture Overview
36
+
37
+ ### Current Stack
38
+ ```
39
+ ┌─────────────────────────────────────────────────────┐
40
+ │ Rails Application (mademysite.com) │
41
+ ├─────────────────────────────────────────────────────┤
42
+ │ Inkpen Gem (Rails Engine) │
43
+ │ ├── Ruby POROs (Editor, Toolbar, Extensions) │
44
+ │ ├── Stimulus Controllers (editor, toolbar, sticky) │
45
+ │ └── TipTap/ProseMirror Core │
46
+ └─────────────────────────────────────────────────────┘
47
+ ```
48
+
49
+ ### Target Architecture
50
+ ```
51
+ ┌─────────────────────────────────────────────────────┐
52
+ │ Rails Application │
53
+ ├─────────────────────────────────────────────────────┤
54
+ │ Inkpen Gem v0.4.0+ │
55
+ │ ├── Ruby POROs │
56
+ │ ├── Stimulus Controllers │
57
+ │ │ ├── editor_controller.js │
58
+ │ │ ├── toolbar_controller.js │
59
+ │ │ ├── sticky_toolbar_controller.js │
60
+ │ │ ├── slash_menu_controller.js ← NEW │
61
+ │ │ ├── block_gutter_controller.js ← NEW │
62
+ │ │ └── drag_handle_controller.js ← NEW │
63
+ │ ├── TipTap Extensions │
64
+ │ │ ├── SlashCommands ← ENHANCED │
65
+ │ │ ├── BlockGutter ← NEW │
66
+ │ │ ├── DragHandle ← NEW │
67
+ │ │ └── UniqueID ← NEW │
68
+ │ └── Helpers │
69
+ │ ├── position_helpers.js ← NEW │
70
+ │ ├── block_helpers.js ← NEW │
71
+ │ └── drag_helpers.js ← NEW │
72
+ └─────────────────────────────────────────────────────┘
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Completed Extensions
78
+
79
+ ### Section Extension (v0.2.2) ✅
80
+
81
+ Page-builder style layout blocks with configurable width and spacing.
82
+
83
+ **Features:**
84
+ - Width presets: narrow (560px), default (680px), wide (900px), full (100%)
85
+ - Spacing presets: none, small (1rem), medium (2rem), large (4rem)
86
+ - Interactive NodeView with hover controls
87
+ - Keyboard shortcut: `Cmd+Shift+S`
88
+
89
+ **Files:**
90
+ ```
91
+ lib/inkpen/extensions/section.rb
92
+ app/assets/javascripts/inkpen/extensions/section.js
93
+ app/assets/stylesheets/inkpen/section.css
94
+ ```
95
+
96
+ **Commands:**
97
+ - `insertSection()` - Insert new section block
98
+ - `setSectionWidth(width)` - Change width preset
99
+ - `setSectionSpacing(spacing)` - Change spacing preset
100
+ - `wrapInSection()` - Wrap selection in section
101
+
102
+ ---
103
+
104
+ ### Preformatted Text Extension (v0.3.0) ✅
105
+
106
+ Plain text block for ASCII art, tables, and diagrams with strict whitespace preservation.
107
+
108
+ **Features:**
109
+ - Strict monospace font (no ligatures)
110
+ - Whitespace preservation (`white-space: pre`)
111
+ - Tab key inserts actual tabs
112
+ - No formatting marks allowed (bold, italic disabled)
113
+ - "Plain Text" label badge
114
+ - Paste handling preserves whitespace
115
+
116
+ **Files:**
117
+ ```
118
+ lib/inkpen/extensions/preformatted.rb
119
+ app/assets/javascripts/inkpen/extensions/preformatted.js
120
+ app/assets/stylesheets/inkpen/preformatted.css
121
+ ```
122
+
123
+ **Commands:**
124
+ - `setPreformatted()` - Convert to preformatted
125
+ - `togglePreformatted()` - Toggle preformatted/paragraph
126
+ - `insertPreformatted(content)` - Insert with content
127
+
128
+ **Keyboard Shortcuts:**
129
+ | Shortcut | Action |
130
+ |----------|--------|
131
+ | `Cmd+Shift+P` | Toggle preformatted |
132
+ | `Tab` | Insert tab character |
133
+ | `Shift+Tab` | Remove leading tab |
134
+ | `Enter` | Insert newline (not new block) |
135
+ | `Backspace` (empty) | Exit to paragraph |
136
+
137
+ ---
138
+
139
+ ### Slash Commands Extension (v0.3.0) ✅
140
+
141
+ Notion-style "/" command palette for rapid block insertion.
142
+
143
+ **Features:**
144
+ - Type "/" to open menu, then type to filter
145
+ - Keyboard navigation (arrows, Enter, Escape)
146
+ - Grouped commands: Basic, Lists, Blocks, Media, Advanced
147
+ - Fuzzy search across title, keywords, description
148
+ - Customizable command list
149
+
150
+ **Files:**
151
+ ```
152
+ lib/inkpen/extensions/slash_commands.rb
153
+ app/assets/javascripts/inkpen/extensions/slash_commands.js
154
+ app/assets/stylesheets/inkpen/slash_menu.css
155
+ ```
156
+
157
+ **Default Commands:**
158
+ - Basic: paragraph, heading1, heading2, heading3
159
+ - Lists: bulletList, orderedList, taskList
160
+ - Blocks: blockquote, codeBlock, preformatted, divider
161
+ - Media: image, youtube, table
162
+ - Advanced: section
163
+
164
+ ---
165
+
166
+ ### Block Gutter Extension (v0.3.1) ✅
167
+
168
+ Left-side gutter with drag handles and plus buttons for each block.
169
+
170
+ **Features:**
171
+ - Drag handle (⋮⋮) for block reordering
172
+ - Plus button (+) to insert new block below
173
+ - Integrates with slash commands
174
+ - Shows on hover, hides when not focused
175
+ - Skips blocks inside tables
176
+ - Mobile-optimized
177
+
178
+ **Files:**
179
+ ```
180
+ app/assets/javascripts/inkpen/extensions/block_gutter.js
181
+ app/assets/stylesheets/inkpen/block_gutter.css
182
+ ```
183
+
184
+ **Visual Design:**
185
+ ```
186
+ ┌────────────────────────────────────────┐
187
+ │ │
188
+ ⋮⋮ + │ Heading 1 │
189
+ │ │
190
+ ├────────────────────────────────────────┤
191
+ │ │
192
+ ⋮⋮ + │ Paragraph text goes here... │
193
+ │ │
194
+ ├────────────────────────────────────────┤
195
+ │ │
196
+ ⋮⋮ + │ • List item 1 │
197
+ │ • List item 2 │
198
+ │ │
199
+ └────────────────────────────────────────┘
200
+
201
+ Legend:
202
+ ⋮⋮ = Drag handle (appears on hover)
203
+ + = Plus button (opens slash menu or quick insert)
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Phase 1: Drag & Drop (v0.3.2) ✅
209
+
210
+ ### Goal
211
+ Enable visual block reordering via drag and drop.
212
+
213
+ **Status:** Complete
214
+
215
+ ### Components
216
+
217
+ #### 3.1 TipTap Extension: DragHandle
218
+ ```javascript
219
+ // app/assets/javascripts/inkpen/extensions/drag_handle.js
220
+
221
+ import { Extension } from '@tiptap/core'
222
+ import { Plugin, PluginKey } from '@tiptap/pm/state'
223
+
224
+ export const DragHandle = Extension.create({
225
+ name: 'dragHandle',
226
+
227
+ addOptions() {
228
+ return {
229
+ scrollThreshold: 100,
230
+ scrollSpeed: 10
231
+ }
232
+ },
233
+
234
+ addProseMirrorPlugins() {
235
+ const extension = this
236
+ let draggedPos = null
237
+ let dropIndicator = null
238
+
239
+ return [
240
+ new Plugin({
241
+ key: new PluginKey('dragHandle'),
242
+ props: {
243
+ handleDOMEvents: {
244
+ dragstart(view, event) {
245
+ const target = event.target.closest('.inkpen-block-gutter__drag')
246
+ if (!target) return false
247
+
248
+ const pos = parseInt(target.closest('.inkpen-block-gutter').dataset.pos)
249
+ draggedPos = pos
250
+
251
+ // Set drag data
252
+ event.dataTransfer.effectAllowed = 'move'
253
+ event.dataTransfer.setData('text/plain', pos.toString())
254
+
255
+ // Add dragging class
256
+ view.dom.classList.add('is-dragging')
257
+
258
+ return true
259
+ },
260
+
261
+ dragover(view, event) {
262
+ if (draggedPos === null) return false
263
+
264
+ event.preventDefault()
265
+ event.dataTransfer.dropEffect = 'move'
266
+
267
+ // Calculate drop position
268
+ const coords = { left: event.clientX, top: event.clientY }
269
+ const dropPos = view.posAtCoords(coords)
270
+
271
+ if (dropPos) {
272
+ extension.showDropIndicator(view, dropPos.pos)
273
+ }
274
+
275
+ return true
276
+ },
277
+
278
+ drop(view, event) {
279
+ if (draggedPos === null) return false
280
+
281
+ event.preventDefault()
282
+
283
+ const coords = { left: event.clientX, top: event.clientY }
284
+ const dropPos = view.posAtCoords(coords)
285
+
286
+ if (dropPos) {
287
+ extension.moveBlock(view, draggedPos, dropPos.pos)
288
+ }
289
+
290
+ extension.cleanup(view)
291
+ return true
292
+ },
293
+
294
+ dragend(view) {
295
+ extension.cleanup(view)
296
+ return true
297
+ }
298
+ }
299
+ }
300
+ })
301
+ ]
302
+ },
303
+
304
+ showDropIndicator(view, pos) {
305
+ // Remove existing indicator
306
+ this.hideDropIndicator()
307
+
308
+ // Create new indicator
309
+ const indicator = document.createElement('div')
310
+ indicator.className = 'inkpen-drop-indicator'
311
+
312
+ const coords = view.coordsAtPos(pos)
313
+ indicator.style.top = `${coords.top}px`
314
+ indicator.style.left = `${view.dom.getBoundingClientRect().left}px`
315
+ indicator.style.width = `${view.dom.clientWidth}px`
316
+
317
+ view.dom.parentNode.appendChild(indicator)
318
+ this.dropIndicator = indicator
319
+ },
320
+
321
+ hideDropIndicator() {
322
+ if (this.dropIndicator) {
323
+ this.dropIndicator.remove()
324
+ this.dropIndicator = null
325
+ }
326
+ },
327
+
328
+ moveBlock(view, from, to) {
329
+ const { state, dispatch } = view
330
+ const node = state.doc.nodeAt(from)
331
+
332
+ if (!node) return
333
+
334
+ const tr = state.tr
335
+ const nodeSize = node.nodeSize
336
+
337
+ // Delete from original position
338
+ tr.delete(from, from + nodeSize)
339
+
340
+ // Adjust target position if needed
341
+ const adjustedTo = to > from ? to - nodeSize : to
342
+
343
+ // Insert at new position
344
+ tr.insert(adjustedTo, node)
345
+
346
+ dispatch(tr)
347
+ },
348
+
349
+ cleanup(view) {
350
+ this.hideDropIndicator()
351
+ view.dom.classList.remove('is-dragging')
352
+ this.draggedPos = null
353
+ }
354
+ })
355
+ ```
356
+
357
+ #### 3.2 CSS for Drag & Drop
358
+ ```css
359
+ /* app/assets/stylesheets/inkpen/drag_drop.css */
360
+
361
+ .inkpen-editor.is-dragging {
362
+ cursor: grabbing;
363
+ }
364
+
365
+ .inkpen-editor.is-dragging .ProseMirror > * {
366
+ transition: transform 150ms;
367
+ }
368
+
369
+ .inkpen-drop-indicator {
370
+ position: absolute;
371
+ height: 3px;
372
+ background: var(--inkpen-color-primary);
373
+ border-radius: 1.5px;
374
+ pointer-events: none;
375
+ z-index: 100;
376
+ }
377
+
378
+ .inkpen-drop-indicator::before,
379
+ .inkpen-drop-indicator::after {
380
+ content: '';
381
+ position: absolute;
382
+ top: 50%;
383
+ width: 8px;
384
+ height: 8px;
385
+ background: var(--inkpen-color-primary);
386
+ border-radius: 50%;
387
+ transform: translateY(-50%);
388
+ }
389
+
390
+ .inkpen-drop-indicator::before {
391
+ left: -4px;
392
+ }
393
+
394
+ .inkpen-drop-indicator::after {
395
+ right: -4px;
396
+ }
397
+
398
+ /* Ghost element during drag */
399
+ .inkpen-drag-ghost {
400
+ position: fixed;
401
+ padding: 0.5rem 1rem;
402
+ background: var(--inkpen-toolbar-bg);
403
+ border: 1px solid var(--inkpen-color-border);
404
+ border-radius: var(--inkpen-radius);
405
+ box-shadow: var(--inkpen-shadow);
406
+ opacity: 0.9;
407
+ pointer-events: none;
408
+ z-index: 9999;
409
+ max-width: 300px;
410
+ overflow: hidden;
411
+ text-overflow: ellipsis;
412
+ white-space: nowrap;
413
+ }
414
+ ```
415
+
416
+ ### References
417
+ - [Plate DnD Examples](https://platejs.org/docs/examples/dnd)
418
+ - [React DnD](https://react-dnd.github.io/react-dnd/about)
419
+ - [Editor.js Drag Plugin](https://github.com/kommitters/editorjs-drag-drop)
420
+
421
+ ---
422
+
423
+ ## Phase 2: Enhanced Blocks (v0.3.3)
424
+
425
+ ### Goal
426
+ Add Notion-style blocks: toggles, columns, callouts with variants.
427
+
428
+ ### 4.1 Toggle/Collapsible Block ✅
429
+
430
+ **Status:** Complete
431
+
432
+ **Features:**
433
+ - Collapsible/expandable blocks with clickable header
434
+ - Native HTML5 `<details>` and `<summary>` elements
435
+ - Editable summary text
436
+ - Nested block content support
437
+ - Smooth expand/collapse animations
438
+ - Keyboard shortcuts: `Cmd+Shift+T`, `Cmd+Enter`
439
+ - Commands: `insertToggle`, `toggleOpen`, `expandToggle`, `collapseToggle`
440
+
441
+ **Files:**
442
+ ```
443
+ app/assets/javascripts/inkpen/extensions/toggle_block.js
444
+ app/assets/stylesheets/inkpen/toggle.css
445
+ ```
446
+
447
+ ### 4.1 Toggle/Collapsible Block
448
+ ```javascript
449
+ // app/assets/javascripts/inkpen/extensions/toggle_block.js
450
+
451
+ import { Node, mergeAttributes } from '@tiptap/core'
452
+
453
+ export const ToggleBlock = Node.create({
454
+ name: 'toggleBlock',
455
+ group: 'block',
456
+ content: 'block+',
457
+
458
+ addAttributes() {
459
+ return {
460
+ open: { default: true }
461
+ }
462
+ },
463
+
464
+ parseHTML() {
465
+ return [{ tag: 'details' }]
466
+ },
467
+
468
+ renderHTML({ HTMLAttributes }) {
469
+ return ['details', mergeAttributes(HTMLAttributes, {
470
+ class: 'inkpen-toggle',
471
+ open: HTMLAttributes.open ? '' : null
472
+ }), 0]
473
+ },
474
+
475
+ addCommands() {
476
+ return {
477
+ setToggleBlock: () => ({ commands }) => {
478
+ return commands.wrapIn(this.name)
479
+ },
480
+ toggleOpen: () => ({ tr, state }) => {
481
+ // Toggle open attribute
482
+ }
483
+ }
484
+ }
485
+ })
486
+ ```
487
+
488
+ ### 4.2 Column Layout ✅
489
+
490
+ **Status:** Complete
491
+
492
+ **Features:**
493
+ - Multi-column layouts (2, 3, or 4 columns)
494
+ - Layout presets: equal, 1:2, 2:1, 1:3, 3:1, 1:2:1, etc.
495
+ - Interactive controls to change layout and add/remove columns
496
+ - Responsive stacking on mobile
497
+ - Keyboard shortcut: `Cmd+Shift+C`
498
+ - Commands: `insertColumns`, `setColumnLayout`, `addColumn`, `removeColumn`
499
+
500
+ **Files:**
501
+ ```
502
+ app/assets/javascripts/inkpen/extensions/columns.js
503
+ app/assets/stylesheets/inkpen/columns.css
504
+ ```
505
+
506
+ ### 4.3 Callout ✅
507
+
508
+ **Status:** Complete
509
+
510
+ **Features:**
511
+ - Highlighted blocks for tips, warnings, notes, and other callouts
512
+ - Six types: info, warning, tip, note, success, error
513
+ - Default emoji icons per type (customizable)
514
+ - Click emoji to change callout type via dropdown
515
+ - Colored backgrounds and left borders per type
516
+ - Keyboard shortcut: `Cmd+Shift+O`
517
+ - Commands: `insertCallout`, `setCalloutType`, `setCalloutEmoji`, `toggleCallout`
518
+
519
+ **Files:**
520
+ ```
521
+ app/assets/javascripts/inkpen/extensions/callout.js
522
+ app/assets/stylesheets/inkpen/callout.css
523
+ ```
524
+
525
+ ### CSS for Enhanced Blocks
526
+ ```css
527
+ /* Toggle */
528
+ .inkpen-toggle {
529
+ border: 1px solid var(--inkpen-color-border);
530
+ border-radius: var(--inkpen-radius);
531
+ margin: 1rem 0;
532
+ }
533
+
534
+ .inkpen-toggle > summary {
535
+ padding: 0.75rem 1rem;
536
+ cursor: pointer;
537
+ font-weight: 500;
538
+ list-style: none;
539
+ }
540
+
541
+ .inkpen-toggle > summary::before {
542
+ content: '▶';
543
+ display: inline-block;
544
+ margin-right: 0.5rem;
545
+ transition: transform 150ms;
546
+ }
547
+
548
+ .inkpen-toggle[open] > summary::before {
549
+ transform: rotate(90deg);
550
+ }
551
+
552
+ .inkpen-toggle > *:not(summary) {
553
+ padding: 0 1rem 1rem;
554
+ }
555
+
556
+ /* Columns */
557
+ .inkpen-columns {
558
+ display: grid;
559
+ grid-template-columns: repeat(var(--column-count, 2), 1fr);
560
+ gap: 1.5rem;
561
+ margin: 1rem 0;
562
+ }
563
+
564
+ .inkpen-column {
565
+ min-width: 0;
566
+ }
567
+
568
+ /* Callout variants */
569
+ .inkpen-callout--success {
570
+ background: rgba(16, 185, 129, 0.1);
571
+ border-left-color: #10b981;
572
+ }
573
+
574
+ .inkpen-callout--error {
575
+ background: rgba(239, 68, 68, 0.1);
576
+ border-left-color: #ef4444;
577
+ }
578
+ ```
579
+
580
+ ---
581
+
582
+ ## Phase 3: BlockNote-Style Polish (v0.4.0) ✅
583
+
584
+ ### Goal
585
+ Add the finishing touches that make the editor feel polished and professional.
586
+
587
+ **Status:** Complete
588
+
589
+ ### 5.1 Block Commands Extension ✅
590
+
591
+ **Features:**
592
+ - Block selection via gutter click
593
+ - Multi-block selection with Shift+Click
594
+ - Duplicate block (`Cmd+D`)
595
+ - Delete empty block (`Backspace`)
596
+ - Select all content in block (`Cmd+A` when block selected)
597
+ - Commands: `duplicateBlock`, `deleteBlock`, `selectBlock`, `selectBlockAt`
598
+
599
+ **Files:**
600
+ ```
601
+ app/assets/javascripts/inkpen/extensions/block_commands.js
602
+ ```
603
+
604
+ ### 5.2 Smooth Animations ✅
605
+
606
+ **Features:**
607
+ - Block entry animations (fade + slide)
608
+ - Block focus ring
609
+ - Block selection highlighting
610
+ - Menu entrance animations (slash menu, bubble menu, dropdowns)
611
+ - Gutter fade in/out
612
+ - Toggle block expand/collapse
613
+ - Cursor and placeholder animations
614
+ - Table cell selection
615
+ - Image selection
616
+ - Scrollbar styling
617
+
618
+ **Files:**
619
+ ```
620
+ app/assets/stylesheets/inkpen/animations.css
621
+ ```
622
+
623
+ ### 5.3 Keyboard Shortcuts
624
+ | Shortcut | Action |
625
+ |----------|--------|
626
+ | `/` | Open slash menu |
627
+ | `Cmd+Shift+↑` | Move block up |
628
+ | `Cmd+Shift+↓` | Move block down |
629
+ | `Cmd+D` | Duplicate block |
630
+ | `Backspace` (empty block) | Delete block |
631
+ | `Enter` (end of block) | Create new paragraph |
632
+ | `Tab` | Indent list item |
633
+ | `Shift+Tab` | Outdent list item |
634
+
635
+ ### 5.4 Mobile Touch Optimizations ✅
636
+
637
+ **Features:**
638
+ - Larger touch targets (32px)
639
+ - Touch-friendly tap feedback
640
+ - Always-visible gutter on mobile
641
+ - Faster animations for snappy mobile feel
642
+ - Touch-friendly block selection with box-shadow
643
+ - Prevent text selection during drag
644
+ - Smooth scrolling (`-webkit-overflow-scrolling: touch`)
645
+ - Reduced motion support (`prefers-reduced-motion`)
646
+ - Print styles (animations disabled)
647
+
648
+ ---
649
+
650
+ ## Phase 4: Media & Embeds (v0.5.0) ✅
651
+
652
+ ### Goal
653
+ Transform Inkpen into a rich media editor with enhanced image handling, file attachments, and social embeds.
654
+
655
+ **Status:** Complete
656
+
657
+ ### 4.1 Enhanced Image Extension (v0.5.0-alpha) ✅
658
+
659
+ **Features:**
660
+ - Resizable images with drag handles
661
+ - Alignment options: left, center, right, full-width
662
+ - Image captions (editable text below image)
663
+ - Lightbox preview on click
664
+ - Lazy loading with blur placeholder
665
+ - Alt text editing
666
+ - Link wrapping (make image clickable)
667
+
668
+ **Implementation:**
669
+ ```javascript
670
+ // app/assets/javascripts/inkpen/extensions/enhanced_image.js
671
+
672
+ import { Node, mergeAttributes } from '@tiptap/core'
673
+ import { Plugin } from '@tiptap/pm/state'
674
+
675
+ export const EnhancedImage = Node.create({
676
+ name: 'enhancedImage',
677
+ group: 'block',
678
+
679
+ addAttributes() {
680
+ return {
681
+ src: { default: null },
682
+ alt: { default: null },
683
+ title: { default: null },
684
+ width: { default: null },
685
+ alignment: { default: 'center' }, // left, center, right, full
686
+ caption: { default: null },
687
+ link: { default: null }
688
+ }
689
+ },
690
+
691
+ addNodeView() {
692
+ // Interactive NodeView with:
693
+ // - Resize handles (corners)
694
+ // - Alignment toolbar on selection
695
+ // - Caption input below image
696
+ // - Lightbox trigger
697
+ },
698
+
699
+ addCommands() {
700
+ return {
701
+ setImageAlignment: (alignment) => ({ commands }) => {...},
702
+ setImageWidth: (width) => ({ commands }) => {...},
703
+ setImageCaption: (caption) => ({ commands }) => {...},
704
+ setImageLink: (url) => ({ commands }) => {...}
705
+ }
706
+ }
707
+ })
708
+ ```
709
+
710
+ **CSS:**
711
+ ```css
712
+ /* app/assets/stylesheets/inkpen/enhanced_image.css */
713
+
714
+ .inkpen-image {
715
+ position: relative;
716
+ display: inline-block;
717
+ max-width: 100%;
718
+ }
719
+
720
+ .inkpen-image--left { margin-right: auto; }
721
+ .inkpen-image--center { margin: 0 auto; }
722
+ .inkpen-image--right { margin-left: auto; }
723
+ .inkpen-image--full { width: 100%; }
724
+
725
+ .inkpen-image__resize-handle {
726
+ position: absolute;
727
+ width: 12px;
728
+ height: 12px;
729
+ background: var(--inkpen-color-primary);
730
+ border: 2px solid white;
731
+ border-radius: 50%;
732
+ cursor: nwse-resize;
733
+ }
734
+
735
+ .inkpen-image__caption {
736
+ text-align: center;
737
+ font-size: 0.875rem;
738
+ color: var(--inkpen-color-text-muted);
739
+ margin-top: 0.5rem;
740
+ }
741
+ ```
742
+
743
+ **Keyboard Shortcuts:**
744
+ | Shortcut | Action |
745
+ |----------|--------|
746
+ | `Enter` on image | Edit caption |
747
+ | `Delete` on image | Remove image |
748
+ | `Cmd+Shift+L` | Align left |
749
+ | `Cmd+Shift+E` | Align center |
750
+ | `Cmd+Shift+R` | Align right |
751
+
752
+ ---
753
+
754
+ ### 4.2 File Attachment Extension (v0.5.0-beta)
755
+
756
+ **Features:**
757
+ - Upload any file type via drag & drop or button
758
+ - File type icons (PDF, Word, Excel, ZIP, etc.)
759
+ - File size display
760
+ - Download button
761
+ - Inline PDF preview (optional)
762
+ - Progress indicator during upload
763
+ - Configurable upload endpoint
764
+
765
+ **Implementation:**
766
+ ```javascript
767
+ // app/assets/javascripts/inkpen/extensions/file_attachment.js
768
+
769
+ export const FileAttachment = Node.create({
770
+ name: 'fileAttachment',
771
+ group: 'block',
772
+
773
+ addAttributes() {
774
+ return {
775
+ url: { default: null },
776
+ filename: { default: null },
777
+ filesize: { default: null },
778
+ filetype: { default: null },
779
+ uploadProgress: { default: null }
780
+ }
781
+ },
782
+
783
+ addOptions() {
784
+ return {
785
+ uploadUrl: '/uploads',
786
+ allowedTypes: '*',
787
+ maxSize: 10 * 1024 * 1024, // 10MB
788
+ onUpload: null, // Custom upload handler
789
+ onError: null
790
+ }
791
+ },
792
+
793
+ addNodeView() {
794
+ // File card with:
795
+ // - Icon based on file type
796
+ // - Filename + size
797
+ // - Download button
798
+ // - Upload progress bar
799
+ },
800
+
801
+ addCommands() {
802
+ return {
803
+ insertFile: (file) => ({ commands }) => {...},
804
+ uploadFile: (file) => ({ commands }) => {...}
805
+ }
806
+ },
807
+
808
+ addProseMirrorPlugins() {
809
+ return [
810
+ // Drop handler for file uploads
811
+ new Plugin({
812
+ props: {
813
+ handleDrop(view, event) {
814
+ const files = event.dataTransfer?.files
815
+ if (files?.length) {
816
+ // Handle file upload
817
+ }
818
+ }
819
+ }
820
+ })
821
+ ]
822
+ }
823
+ })
824
+ ```
825
+
826
+ **File Type Icons:**
827
+ ```
828
+ 📄 PDF, DOC, DOCX, TXT
829
+ 📊 XLS, XLSX, CSV
830
+ 📁 ZIP, RAR, 7Z
831
+ 🎵 MP3, WAV, OGG
832
+ 🎬 MP4, MOV, AVI
833
+ 🖼️ Image files (fallback)
834
+ 📎 Other files
835
+ ```
836
+
837
+ **CSS:**
838
+ ```css
839
+ .inkpen-file {
840
+ display: flex;
841
+ align-items: center;
842
+ gap: 0.75rem;
843
+ padding: 0.75rem 1rem;
844
+ background: var(--inkpen-color-border);
845
+ border-radius: var(--inkpen-radius);
846
+ margin: 1rem 0;
847
+ }
848
+
849
+ .inkpen-file__icon {
850
+ font-size: 1.5rem;
851
+ }
852
+
853
+ .inkpen-file__info {
854
+ flex: 1;
855
+ }
856
+
857
+ .inkpen-file__name {
858
+ font-weight: 500;
859
+ }
860
+
861
+ .inkpen-file__size {
862
+ font-size: 0.75rem;
863
+ color: var(--inkpen-color-text-muted);
864
+ }
865
+
866
+ .inkpen-file__download {
867
+ padding: 0.5rem;
868
+ border-radius: var(--inkpen-radius);
869
+ background: var(--inkpen-color-primary);
870
+ color: white;
871
+ }
872
+
873
+ .inkpen-file__progress {
874
+ height: 4px;
875
+ background: var(--inkpen-color-border);
876
+ border-radius: 2px;
877
+ overflow: hidden;
878
+ }
879
+
880
+ .inkpen-file__progress-bar {
881
+ height: 100%;
882
+ background: var(--inkpen-color-primary);
883
+ transition: width 150ms;
884
+ }
885
+ ```
886
+
887
+ ---
888
+
889
+ ### 4.3 Social Embeds Extension (v0.5.0-rc)
890
+
891
+ **Features:**
892
+ - Paste URL to auto-embed
893
+ - Supported platforms:
894
+ - Twitter/X posts
895
+ - Instagram posts
896
+ - TikTok videos
897
+ - Figma designs
898
+ - Loom videos
899
+ - CodePen pens
900
+ - GitHub Gists
901
+ - Spotify tracks/playlists
902
+ - Responsive embeds
903
+ - Fallback link card for unsupported URLs
904
+ - Privacy-aware (no tracking until clicked)
905
+
906
+ **Implementation:**
907
+ ```javascript
908
+ // app/assets/javascripts/inkpen/extensions/embed.js
909
+
910
+ const EMBED_PROVIDERS = {
911
+ twitter: {
912
+ regex: /https?:\/\/(twitter|x)\.com\/\w+\/status\/(\d+)/,
913
+ template: (id) => `<blockquote class="twitter-tweet" data-id="${id}"></blockquote>`,
914
+ script: 'https://platform.twitter.com/widgets.js'
915
+ },
916
+ instagram: {
917
+ regex: /https?:\/\/www\.instagram\.com\/(p|reel)\/([A-Za-z0-9_-]+)/,
918
+ template: (id) => `<blockquote class="instagram-media" data-instgrm-permalink="https://www.instagram.com/p/${id}/"></blockquote>`,
919
+ script: 'https://www.instagram.com/embed.js'
920
+ },
921
+ figma: {
922
+ regex: /https?:\/\/www\.figma\.com\/(file|proto)\/([A-Za-z0-9]+)/,
923
+ template: (url) => `<iframe src="https://www.figma.com/embed?embed_host=inkpen&url=${encodeURIComponent(url)}"></iframe>`
924
+ },
925
+ loom: {
926
+ regex: /https?:\/\/www\.loom\.com\/share\/([a-zA-Z0-9]+)/,
927
+ template: (id) => `<iframe src="https://www.loom.com/embed/${id}"></iframe>`
928
+ },
929
+ codepen: {
930
+ regex: /https?:\/\/codepen\.io\/([^\/]+)\/pen\/([A-Za-z0-9]+)/,
931
+ template: (user, id) => `<iframe src="https://codepen.io/${user}/embed/${id}?default-tab=result"></iframe>`
932
+ },
933
+ gist: {
934
+ regex: /https?:\/\/gist\.github\.com\/([^\/]+)\/([a-f0-9]+)/,
935
+ template: (user, id) => `<script src="https://gist.github.com/${user}/${id}.js"></script>`
936
+ },
937
+ spotify: {
938
+ regex: /https?:\/\/open\.spotify\.com\/(track|album|playlist)\/([A-Za-z0-9]+)/,
939
+ template: (type, id) => `<iframe src="https://open.spotify.com/embed/${type}/${id}"></iframe>`
940
+ }
941
+ }
942
+
943
+ export const Embed = Node.create({
944
+ name: 'embed',
945
+ group: 'block',
946
+
947
+ addAttributes() {
948
+ return {
949
+ url: { default: null },
950
+ provider: { default: null },
951
+ embedId: { default: null },
952
+ loaded: { default: false }
953
+ }
954
+ },
955
+
956
+ addOptions() {
957
+ return {
958
+ providers: EMBED_PROVIDERS,
959
+ allowedProviders: null, // null = all, or ['twitter', 'youtube']
960
+ privacyMode: true // Show placeholder until clicked
961
+ }
962
+ },
963
+
964
+ addNodeView() {
965
+ // Privacy-first embed:
966
+ // 1. Show preview card with provider logo
967
+ // 2. "Click to load" button
968
+ // 3. Load actual embed on click
969
+ },
970
+
971
+ addPasteRules() {
972
+ // Auto-detect and embed URLs on paste
973
+ },
974
+
975
+ addCommands() {
976
+ return {
977
+ insertEmbed: (url) => ({ commands }) => {...},
978
+ loadEmbed: () => ({ commands }) => {...}
979
+ }
980
+ }
981
+ })
982
+ ```
983
+
984
+ **Link Card Fallback:**
985
+ ```css
986
+ .inkpen-link-card {
987
+ display: flex;
988
+ border: 1px solid var(--inkpen-color-border);
989
+ border-radius: var(--inkpen-radius);
990
+ overflow: hidden;
991
+ text-decoration: none;
992
+ color: inherit;
993
+ }
994
+
995
+ .inkpen-link-card__image {
996
+ width: 120px;
997
+ height: 80px;
998
+ object-fit: cover;
999
+ flex-shrink: 0;
1000
+ }
1001
+
1002
+ .inkpen-link-card__content {
1003
+ padding: 0.75rem;
1004
+ flex: 1;
1005
+ }
1006
+
1007
+ .inkpen-link-card__title {
1008
+ font-weight: 500;
1009
+ margin-bottom: 0.25rem;
1010
+ }
1011
+
1012
+ .inkpen-link-card__description {
1013
+ font-size: 0.875rem;
1014
+ color: var(--inkpen-color-text-muted);
1015
+ display: -webkit-box;
1016
+ -webkit-line-clamp: 2;
1017
+ -webkit-box-orient: vertical;
1018
+ overflow: hidden;
1019
+ }
1020
+
1021
+ .inkpen-link-card__domain {
1022
+ font-size: 0.75rem;
1023
+ color: var(--inkpen-color-text-muted);
1024
+ margin-top: 0.5rem;
1025
+ }
1026
+ ```
1027
+
1028
+ ---
1029
+
1030
+ ### 4.4 Slash Commands Updates
1031
+
1032
+ Add new commands to slash menu:
1033
+ - `/image` - Insert image (upload or URL)
1034
+ - `/file` - Upload file attachment
1035
+ - `/embed` - Paste URL to embed
1036
+ - `/twitter` - Embed tweet
1037
+ - `/figma` - Embed Figma design
1038
+ - `/loom` - Embed Loom video
1039
+ - `/codepen` - Embed CodePen
1040
+
1041
+ ---
1042
+
1043
+ ### Implementation Priority
1044
+
1045
+ | Feature | Priority | Complexity | Files |
1046
+ |---------|----------|------------|-------|
1047
+ | Enhanced Image | High | Medium | enhanced_image.js, enhanced_image.css |
1048
+ | File Attachment | High | High | file_attachment.js, file_attachment.css |
1049
+ | Social Embeds | Medium | Medium | embed.js, embed.css |
1050
+ | Link Cards | Medium | Low | link_card.js, link_card.css |
1051
+ | Slash Menu Updates | Low | Low | slash_commands.js |
1052
+
1053
+ ---
1054
+
1055
+ ### Files to Create (v0.5.0)
1056
+
1057
+ ```
1058
+ app/assets/javascripts/inkpen/extensions/
1059
+ ├── enhanced_image.js ← v0.5.0-alpha
1060
+ ├── file_attachment.js ← v0.5.0-beta
1061
+ ├── embed.js ← v0.5.0-rc
1062
+ └── link_card.js ← v0.5.0-rc
1063
+
1064
+ app/assets/stylesheets/inkpen/
1065
+ ├── enhanced_image.css ← v0.5.0-alpha
1066
+ ├── file_attachment.css ← v0.5.0-beta
1067
+ ├── embed.css ← v0.5.0-rc
1068
+ └── link_card.css ← v0.5.0-rc
1069
+
1070
+ lib/inkpen/extensions/
1071
+ ├── enhanced_image.rb ← v0.5.0-alpha
1072
+ ├── file_attachment.rb ← v0.5.0-beta
1073
+ └── embed.rb ← v0.5.0-rc
1074
+ ```
1075
+
1076
+ ---
1077
+
1078
+ ## Phase 5: Tables & Data (v0.6.0) ✅
1079
+
1080
+ ### Goal
1081
+ Transform Inkpen into a data-rich editor with advanced table features, Notion-style database blocks, and automatic table of contents generation.
1082
+
1083
+ **Status:** Complete
1084
+
1085
+ ---
1086
+
1087
+ ### 5.1 Advanced Tables Extension (v0.6.0-alpha) ✅
1088
+
1089
+ Enhance the existing TipTap table with professional features.
1090
+
1091
+ **Features:**
1092
+ - Column resizing with visual handles (existing)
1093
+ - Cell merging and splitting (existing)
1094
+ - Header rows with sticky behavior
1095
+ - **NEW:** Column alignment (left, center, right)
1096
+ - **NEW:** Table caption/title
1097
+ - **NEW:** Striped rows option
1098
+ - **NEW:** Border style variants (default, borderless, minimal)
1099
+ - **NEW:** Table toolbar on cell selection
1100
+ - **NEW:** Cell background colors
1101
+ - **NEW:** Row/column drag reordering
1102
+
1103
+ **Implementation:**
1104
+ ```javascript
1105
+ // app/assets/javascripts/inkpen/extensions/advanced_table.js
1106
+
1107
+ import { Table } from '@tiptap/extension-table'
1108
+ import { TableRow } from '@tiptap/extension-table-row'
1109
+ import { TableCell } from '@tiptap/extension-table-cell'
1110
+ import { TableHeader } from '@tiptap/extension-table-header'
1111
+ import { Plugin, PluginKey } from '@tiptap/pm/state'
1112
+
1113
+ // Extended TableCell with new attributes
1114
+ export const AdvancedTableCell = TableCell.extend({
1115
+ addAttributes() {
1116
+ return {
1117
+ ...this.parent?.(),
1118
+ align: {
1119
+ default: 'left',
1120
+ parseHTML: element => element.style.textAlign || 'left',
1121
+ renderHTML: attributes => ({
1122
+ style: `text-align: ${attributes.align}`
1123
+ })
1124
+ },
1125
+ background: {
1126
+ default: null,
1127
+ parseHTML: element => element.getAttribute('data-background'),
1128
+ renderHTML: attributes => attributes.background
1129
+ ? { 'data-background': attributes.background, style: `background: var(--inkpen-table-bg-${attributes.background})` }
1130
+ : {}
1131
+ }
1132
+ }
1133
+ }
1134
+ })
1135
+
1136
+ // Extended Table with caption and style variants
1137
+ export const AdvancedTable = Table.extend({
1138
+ addAttributes() {
1139
+ return {
1140
+ ...this.parent?.(),
1141
+ caption: { default: null },
1142
+ variant: { default: 'default' }, // default, striped, borderless, minimal
1143
+ stickyHeader: { default: false }
1144
+ }
1145
+ },
1146
+
1147
+ addNodeView() {
1148
+ return ({ node, editor, getPos }) => {
1149
+ const wrapper = document.createElement('div')
1150
+ wrapper.className = 'inkpen-table-wrapper'
1151
+
1152
+ // Caption above table
1153
+ if (node.attrs.caption) {
1154
+ const caption = document.createElement('div')
1155
+ caption.className = 'inkpen-table__caption'
1156
+ caption.textContent = node.attrs.caption
1157
+ caption.contentEditable = 'true'
1158
+ wrapper.appendChild(caption)
1159
+ }
1160
+
1161
+ // Table controls toolbar
1162
+ if (editor.isEditable) {
1163
+ const controls = document.createElement('div')
1164
+ controls.className = 'inkpen-table__controls'
1165
+ controls.innerHTML = `
1166
+ <button data-action="align-left" title="Align Left">⬅</button>
1167
+ <button data-action="align-center" title="Align Center">⬌</button>
1168
+ <button data-action="align-right" title="Align Right">➡</button>
1169
+ <span class="divider"></span>
1170
+ <button data-action="toggle-striped" title="Striped Rows">≡</button>
1171
+ <button data-action="cell-color" title="Cell Color">🎨</button>
1172
+ <span class="divider"></span>
1173
+ <button data-action="add-row" title="Add Row">+↓</button>
1174
+ <button data-action="add-col" title="Add Column">+→</button>
1175
+ `
1176
+ wrapper.appendChild(controls)
1177
+ }
1178
+
1179
+ // Table element
1180
+ const tableContainer = document.createElement('div')
1181
+ tableContainer.className = 'inkpen-table__container'
1182
+ wrapper.appendChild(tableContainer)
1183
+
1184
+ return {
1185
+ dom: wrapper,
1186
+ contentDOM: tableContainer
1187
+ }
1188
+ }
1189
+ },
1190
+
1191
+ addCommands() {
1192
+ return {
1193
+ ...this.parent?.(),
1194
+ setTableCaption: (caption) => ({ tr, state, dispatch }) => {
1195
+ // Set caption on table node
1196
+ },
1197
+ setTableVariant: (variant) => ({ tr, state, dispatch }) => {
1198
+ // Set variant (default, striped, borderless, minimal)
1199
+ },
1200
+ setCellAlignment: (align) => ({ tr, state, dispatch }) => {
1201
+ // Set alignment for selected cells
1202
+ },
1203
+ setCellBackground: (color) => ({ tr, state, dispatch }) => {
1204
+ // Set background for selected cells
1205
+ },
1206
+ toggleStickyHeader: () => ({ tr, state, dispatch }) => {
1207
+ // Toggle sticky header behavior
1208
+ }
1209
+ }
1210
+ },
1211
+
1212
+ addKeyboardShortcuts() {
1213
+ return {
1214
+ ...this.parent?.(),
1215
+ 'Tab': () => this.editor.commands.goToNextCell(),
1216
+ 'Shift-Tab': () => this.editor.commands.goToPreviousCell()
1217
+ }
1218
+ }
1219
+ })
1220
+ ```
1221
+
1222
+ **CSS:**
1223
+ ```css
1224
+ /* app/assets/stylesheets/inkpen/advanced_table.css */
1225
+
1226
+ .inkpen-table-wrapper {
1227
+ margin: 1.5rem 0;
1228
+ position: relative;
1229
+ }
1230
+
1231
+ .inkpen-table__caption {
1232
+ font-size: 0.875rem;
1233
+ color: var(--inkpen-color-text-muted);
1234
+ margin-bottom: 0.5rem;
1235
+ font-style: italic;
1236
+ }
1237
+
1238
+ .inkpen-table__controls {
1239
+ position: absolute;
1240
+ top: -36px;
1241
+ left: 50%;
1242
+ transform: translateX(-50%);
1243
+ display: flex;
1244
+ gap: 0.25rem;
1245
+ padding: 0.25rem;
1246
+ background: var(--inkpen-toolbar-bg);
1247
+ border: 1px solid var(--inkpen-color-border);
1248
+ border-radius: var(--inkpen-radius);
1249
+ box-shadow: var(--inkpen-shadow);
1250
+ opacity: 0;
1251
+ transition: opacity 150ms;
1252
+ }
1253
+
1254
+ .inkpen-table-wrapper:focus-within .inkpen-table__controls,
1255
+ .inkpen-table-wrapper:hover .inkpen-table__controls {
1256
+ opacity: 1;
1257
+ }
1258
+
1259
+ /* Table Variants */
1260
+ .inkpen-table {
1261
+ width: 100%;
1262
+ border-collapse: collapse;
1263
+ }
1264
+
1265
+ .inkpen-table--default td,
1266
+ .inkpen-table--default th {
1267
+ border: 1px solid var(--inkpen-color-border);
1268
+ padding: 0.5rem 0.75rem;
1269
+ }
1270
+
1271
+ .inkpen-table--striped tr:nth-child(even) {
1272
+ background: var(--inkpen-color-bg-subtle);
1273
+ }
1274
+
1275
+ .inkpen-table--borderless td,
1276
+ .inkpen-table--borderless th {
1277
+ border: none;
1278
+ border-bottom: 1px solid var(--inkpen-color-border);
1279
+ }
1280
+
1281
+ .inkpen-table--minimal td,
1282
+ .inkpen-table--minimal th {
1283
+ border: none;
1284
+ padding: 0.5rem 1rem;
1285
+ }
1286
+
1287
+ /* Sticky Header */
1288
+ .inkpen-table--sticky-header thead {
1289
+ position: sticky;
1290
+ top: 0;
1291
+ background: var(--inkpen-toolbar-bg);
1292
+ z-index: 10;
1293
+ }
1294
+
1295
+ /* Cell Colors */
1296
+ .inkpen-table [data-background="gray"] { background: var(--inkpen-color-bg-subtle); }
1297
+ .inkpen-table [data-background="red"] { background: rgba(239, 68, 68, 0.15); }
1298
+ .inkpen-table [data-background="orange"] { background: rgba(249, 115, 22, 0.15); }
1299
+ .inkpen-table [data-background="yellow"] { background: rgba(234, 179, 8, 0.15); }
1300
+ .inkpen-table [data-background="green"] { background: rgba(34, 197, 94, 0.15); }
1301
+ .inkpen-table [data-background="blue"] { background: rgba(59, 130, 246, 0.15); }
1302
+ .inkpen-table [data-background="purple"] { background: rgba(168, 85, 247, 0.15); }
1303
+
1304
+ /* Resize Handles */
1305
+ .inkpen-table .column-resize-handle {
1306
+ position: absolute;
1307
+ right: -2px;
1308
+ top: 0;
1309
+ bottom: 0;
1310
+ width: 4px;
1311
+ background: var(--inkpen-color-primary);
1312
+ cursor: col-resize;
1313
+ opacity: 0;
1314
+ transition: opacity 150ms;
1315
+ }
1316
+
1317
+ .inkpen-table td:hover .column-resize-handle,
1318
+ .inkpen-table th:hover .column-resize-handle {
1319
+ opacity: 1;
1320
+ }
1321
+ ```
1322
+
1323
+ ---
1324
+
1325
+ ### 5.2 Database Block Extension (v0.6.0-beta)
1326
+
1327
+ Notion-style inline databases with multiple views.
1328
+
1329
+ **Features:**
1330
+ - Property types: Text, Number, Select, Multi-select, Date, Checkbox, URL, Email, Person
1331
+ - Views: Table, List, Gallery, Board (Kanban)
1332
+ - Filters with AND/OR logic
1333
+ - Sorting (single and multi-column)
1334
+ - Inline database (embedded in document)
1335
+ - Full-page database option
1336
+ - Template rows
1337
+ - Formulas (basic: SUM, COUNT, AVG)
1338
+ - Linked databases (reference same data)
1339
+
1340
+ **Implementation:**
1341
+ ```javascript
1342
+ // app/assets/javascripts/inkpen/extensions/database.js
1343
+
1344
+ import { Node, mergeAttributes } from '@tiptap/core'
1345
+
1346
+ const PROPERTY_TYPES = {
1347
+ text: { icon: 'Aa', default: '' },
1348
+ number: { icon: '#', default: 0 },
1349
+ select: { icon: '▼', default: null, options: [] },
1350
+ multiSelect: { icon: '☰', default: [], options: [] },
1351
+ date: { icon: '📅', default: null },
1352
+ checkbox: { icon: '☑', default: false },
1353
+ url: { icon: '🔗', default: '' },
1354
+ email: { icon: '✉', default: '' },
1355
+ person: { icon: '👤', default: null },
1356
+ formula: { icon: 'ƒ', default: null, formula: '' }
1357
+ }
1358
+
1359
+ const VIEWS = {
1360
+ table: { icon: '⊞', name: 'Table' },
1361
+ list: { icon: '☰', name: 'List' },
1362
+ gallery: { icon: '⊟', name: 'Gallery' },
1363
+ board: { icon: '▣', name: 'Board' }
1364
+ }
1365
+
1366
+ export const Database = Node.create({
1367
+ name: 'database',
1368
+ group: 'block',
1369
+ atom: true,
1370
+ draggable: true,
1371
+
1372
+ addAttributes() {
1373
+ return {
1374
+ title: { default: 'Untitled Database' },
1375
+ properties: {
1376
+ default: [
1377
+ { id: 'name', name: 'Name', type: 'text' },
1378
+ { id: 'status', name: 'Status', type: 'select', options: ['To Do', 'In Progress', 'Done'] }
1379
+ ]
1380
+ },
1381
+ rows: {
1382
+ default: []
1383
+ },
1384
+ views: {
1385
+ default: [
1386
+ { id: 'default', type: 'table', name: 'Table View', filters: [], sorts: [] }
1387
+ ]
1388
+ },
1389
+ activeView: { default: 'default' },
1390
+ isInline: { default: true },
1391
+ linkedFrom: { default: null } // ID of source database if linked
1392
+ }
1393
+ },
1394
+
1395
+ addNodeView() {
1396
+ return ({ node, editor, getPos, HTMLAttributes }) => {
1397
+ const { title, properties, rows, views, activeView, isInline } = node.attrs
1398
+ const currentView = views.find(v => v.id === activeView) || views[0]
1399
+
1400
+ const wrapper = document.createElement('div')
1401
+ wrapper.className = `inkpen-database inkpen-database--${currentView.type}`
1402
+ if (isInline) wrapper.classList.add('inkpen-database--inline')
1403
+
1404
+ // Header with title and view tabs
1405
+ const header = document.createElement('div')
1406
+ header.className = 'inkpen-database__header'
1407
+ header.innerHTML = `
1408
+ <input type="text" class="inkpen-database__title" value="${title}" placeholder="Untitled" />
1409
+ <div class="inkpen-database__views">
1410
+ ${views.map(v => `
1411
+ <button class="inkpen-database__view-tab ${v.id === activeView ? 'is-active' : ''}"
1412
+ data-view-id="${v.id}">
1413
+ ${VIEWS[v.type].icon} ${v.name}
1414
+ </button>
1415
+ `).join('')}
1416
+ <button class="inkpen-database__add-view">+ Add View</button>
1417
+ </div>
1418
+ <div class="inkpen-database__actions">
1419
+ <button class="inkpen-database__filter">Filter</button>
1420
+ <button class="inkpen-database__sort">Sort</button>
1421
+ <button class="inkpen-database__new-row">+ New</button>
1422
+ </div>
1423
+ `
1424
+ wrapper.appendChild(header)
1425
+
1426
+ // Render view based on type
1427
+ const content = document.createElement('div')
1428
+ content.className = 'inkpen-database__content'
1429
+
1430
+ switch (currentView.type) {
1431
+ case 'table':
1432
+ content.innerHTML = this.renderTableView(properties, rows, currentView)
1433
+ break
1434
+ case 'list':
1435
+ content.innerHTML = this.renderListView(properties, rows, currentView)
1436
+ break
1437
+ case 'gallery':
1438
+ content.innerHTML = this.renderGalleryView(properties, rows, currentView)
1439
+ break
1440
+ case 'board':
1441
+ content.innerHTML = this.renderBoardView(properties, rows, currentView)
1442
+ break
1443
+ }
1444
+ wrapper.appendChild(content)
1445
+
1446
+ return { dom: wrapper }
1447
+ }
1448
+ },
1449
+
1450
+ renderTableView(properties, rows, view) {
1451
+ return `
1452
+ <table class="inkpen-database__table">
1453
+ <thead>
1454
+ <tr>
1455
+ ${properties.map(p => `
1456
+ <th data-prop-id="${p.id}">
1457
+ ${PROPERTY_TYPES[p.type].icon} ${p.name}
1458
+ </th>
1459
+ `).join('')}
1460
+ <th class="inkpen-database__add-prop">+</th>
1461
+ </tr>
1462
+ </thead>
1463
+ <tbody>
1464
+ ${rows.map(row => `
1465
+ <tr data-row-id="${row.id}">
1466
+ ${properties.map(p => `
1467
+ <td data-prop-id="${p.id}" data-type="${p.type}">
1468
+ ${this.renderCell(p, row[p.id])}
1469
+ </td>
1470
+ `).join('')}
1471
+ </tr>
1472
+ `).join('')}
1473
+ <tr class="inkpen-database__new-row-placeholder">
1474
+ <td colspan="${properties.length + 1}">+ New row</td>
1475
+ </tr>
1476
+ </tbody>
1477
+ </table>
1478
+ `
1479
+ },
1480
+
1481
+ renderBoardView(properties, rows, view) {
1482
+ const groupBy = view.groupBy || properties.find(p => p.type === 'select')?.id
1483
+ const groupProp = properties.find(p => p.id === groupBy)
1484
+ const groups = groupProp?.options || ['No Status']
1485
+
1486
+ return `
1487
+ <div class="inkpen-database__board">
1488
+ ${groups.map(group => `
1489
+ <div class="inkpen-database__column" data-group="${group}">
1490
+ <div class="inkpen-database__column-header">
1491
+ <span>${group}</span>
1492
+ <span class="inkpen-database__column-count">
1493
+ ${rows.filter(r => r[groupBy] === group).length}
1494
+ </span>
1495
+ </div>
1496
+ <div class="inkpen-database__column-items">
1497
+ ${rows.filter(r => r[groupBy] === group).map(row => `
1498
+ <div class="inkpen-database__card" data-row-id="${row.id}">
1499
+ ${this.renderCard(properties, row)}
1500
+ </div>
1501
+ `).join('')}
1502
+ <button class="inkpen-database__add-card">+ Add</button>
1503
+ </div>
1504
+ </div>
1505
+ `).join('')}
1506
+ </div>
1507
+ `
1508
+ },
1509
+
1510
+ addCommands() {
1511
+ return {
1512
+ insertDatabase: (options = {}) => ({ commands }) => {
1513
+ return commands.insertContent({
1514
+ type: this.name,
1515
+ attrs: options
1516
+ })
1517
+ },
1518
+ addDatabaseProperty: (propertyDef) => ({ tr, state }) => {
1519
+ // Add new property column
1520
+ },
1521
+ addDatabaseRow: (rowData) => ({ tr, state }) => {
1522
+ // Add new row
1523
+ },
1524
+ setDatabaseView: (viewId) => ({ tr, state }) => {
1525
+ // Switch active view
1526
+ },
1527
+ addDatabaseView: (viewDef) => ({ tr, state }) => {
1528
+ // Create new view
1529
+ },
1530
+ setDatabaseFilter: (filters) => ({ tr, state }) => {
1531
+ // Update view filters
1532
+ },
1533
+ setDatabaseSort: (sorts) => ({ tr, state }) => {
1534
+ // Update view sorts
1535
+ }
1536
+ }
1537
+ }
1538
+ })
1539
+ ```
1540
+
1541
+ **CSS:**
1542
+ ```css
1543
+ /* app/assets/stylesheets/inkpen/database.css */
1544
+
1545
+ .inkpen-database {
1546
+ margin: 1.5rem 0;
1547
+ border: 1px solid var(--inkpen-color-border);
1548
+ border-radius: var(--inkpen-radius);
1549
+ overflow: hidden;
1550
+ }
1551
+
1552
+ .inkpen-database--inline {
1553
+ max-height: 400px;
1554
+ overflow-y: auto;
1555
+ }
1556
+
1557
+ /* Header */
1558
+ .inkpen-database__header {
1559
+ display: flex;
1560
+ align-items: center;
1561
+ gap: 1rem;
1562
+ padding: 0.75rem 1rem;
1563
+ border-bottom: 1px solid var(--inkpen-color-border);
1564
+ background: var(--inkpen-color-bg-subtle);
1565
+ }
1566
+
1567
+ .inkpen-database__title {
1568
+ font-size: 1rem;
1569
+ font-weight: 600;
1570
+ border: none;
1571
+ background: transparent;
1572
+ flex: 1;
1573
+ }
1574
+
1575
+ .inkpen-database__views {
1576
+ display: flex;
1577
+ gap: 0.25rem;
1578
+ }
1579
+
1580
+ .inkpen-database__view-tab {
1581
+ padding: 0.375rem 0.75rem;
1582
+ border-radius: var(--inkpen-radius);
1583
+ border: none;
1584
+ background: transparent;
1585
+ cursor: pointer;
1586
+ }
1587
+
1588
+ .inkpen-database__view-tab.is-active {
1589
+ background: var(--inkpen-toolbar-bg);
1590
+ }
1591
+
1592
+ /* Table View */
1593
+ .inkpen-database__table {
1594
+ width: 100%;
1595
+ border-collapse: collapse;
1596
+ }
1597
+
1598
+ .inkpen-database__table th {
1599
+ text-align: left;
1600
+ padding: 0.5rem 0.75rem;
1601
+ border-bottom: 1px solid var(--inkpen-color-border);
1602
+ font-weight: 500;
1603
+ font-size: 0.875rem;
1604
+ color: var(--inkpen-color-text-muted);
1605
+ white-space: nowrap;
1606
+ }
1607
+
1608
+ .inkpen-database__table td {
1609
+ padding: 0.5rem 0.75rem;
1610
+ border-bottom: 1px solid var(--inkpen-color-border);
1611
+ }
1612
+
1613
+ .inkpen-database__table td:hover {
1614
+ background: var(--inkpen-color-bg-subtle);
1615
+ }
1616
+
1617
+ /* Board View (Kanban) */
1618
+ .inkpen-database__board {
1619
+ display: flex;
1620
+ gap: 1rem;
1621
+ padding: 1rem;
1622
+ overflow-x: auto;
1623
+ }
1624
+
1625
+ .inkpen-database__column {
1626
+ flex: 0 0 280px;
1627
+ background: var(--inkpen-color-bg-subtle);
1628
+ border-radius: var(--inkpen-radius);
1629
+ }
1630
+
1631
+ .inkpen-database__column-header {
1632
+ display: flex;
1633
+ justify-content: space-between;
1634
+ padding: 0.75rem;
1635
+ font-weight: 500;
1636
+ }
1637
+
1638
+ .inkpen-database__column-items {
1639
+ padding: 0.5rem;
1640
+ display: flex;
1641
+ flex-direction: column;
1642
+ gap: 0.5rem;
1643
+ }
1644
+
1645
+ .inkpen-database__card {
1646
+ padding: 0.75rem;
1647
+ background: var(--inkpen-toolbar-bg);
1648
+ border-radius: var(--inkpen-radius);
1649
+ border: 1px solid var(--inkpen-color-border);
1650
+ cursor: pointer;
1651
+ }
1652
+
1653
+ .inkpen-database__card:hover {
1654
+ box-shadow: var(--inkpen-shadow);
1655
+ }
1656
+
1657
+ /* Gallery View */
1658
+ .inkpen-database__gallery {
1659
+ display: grid;
1660
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1661
+ gap: 1rem;
1662
+ padding: 1rem;
1663
+ }
1664
+
1665
+ /* Property Cells */
1666
+ .inkpen-database [data-type="checkbox"] {
1667
+ text-align: center;
1668
+ }
1669
+
1670
+ .inkpen-database [data-type="select"] .inkpen-tag {
1671
+ display: inline-block;
1672
+ padding: 0.125rem 0.5rem;
1673
+ border-radius: 1rem;
1674
+ font-size: 0.75rem;
1675
+ font-weight: 500;
1676
+ }
1677
+
1678
+ .inkpen-database [data-type="date"] {
1679
+ font-family: var(--inkpen-font-mono);
1680
+ font-size: 0.875rem;
1681
+ }
1682
+ ```
1683
+
1684
+ ---
1685
+
1686
+ ### 5.3 Table of Contents Extension (v0.6.0-rc)
1687
+
1688
+ Auto-generated navigation from document headings.
1689
+
1690
+ **Features:**
1691
+ - Auto-detect headings (H1-H6)
1692
+ - Clickable links with smooth scroll
1693
+ - Configurable max depth (e.g., H1-H3 only)
1694
+ - Numbered or bulleted style
1695
+ - Collapsible sections
1696
+ - Sticky positioning option
1697
+ - Real-time updates as document changes
1698
+ - Click heading to scroll into view
1699
+
1700
+ **Implementation:**
1701
+ ```javascript
1702
+ // app/assets/javascripts/inkpen/extensions/table_of_contents.js
1703
+
1704
+ import { Node } from '@tiptap/core'
1705
+ import { Plugin, PluginKey } from '@tiptap/pm/state'
1706
+
1707
+ export const TableOfContents = Node.create({
1708
+ name: 'tableOfContents',
1709
+ group: 'block',
1710
+ atom: true,
1711
+ draggable: true,
1712
+
1713
+ addAttributes() {
1714
+ return {
1715
+ maxDepth: { default: 3 }, // Max heading level (1-6)
1716
+ style: { default: 'numbered' }, // numbered, bulleted, plain
1717
+ title: { default: 'Table of Contents' },
1718
+ collapsible: { default: false },
1719
+ sticky: { default: false }
1720
+ }
1721
+ },
1722
+
1723
+ addNodeView() {
1724
+ return ({ node, editor }) => {
1725
+ const wrapper = document.createElement('div')
1726
+ wrapper.className = 'inkpen-toc'
1727
+ if (node.attrs.sticky) wrapper.classList.add('inkpen-toc--sticky')
1728
+
1729
+ const render = () => {
1730
+ const headings = this.getHeadings(editor, node.attrs.maxDepth)
1731
+ wrapper.innerHTML = this.renderTOC(headings, node.attrs)
1732
+ this.attachClickHandlers(wrapper, editor)
1733
+ }
1734
+
1735
+ render()
1736
+
1737
+ // Update on document change
1738
+ const updateHandler = () => render()
1739
+ editor.on('update', updateHandler)
1740
+
1741
+ return {
1742
+ dom: wrapper,
1743
+ destroy: () => editor.off('update', updateHandler)
1744
+ }
1745
+ }
1746
+ },
1747
+
1748
+ getHeadings(editor, maxDepth) {
1749
+ const headings = []
1750
+ let index = 0
1751
+
1752
+ editor.state.doc.descendants((node, pos) => {
1753
+ if (node.type.name === 'heading' && node.attrs.level <= maxDepth) {
1754
+ headings.push({
1755
+ id: `heading-${index++}`,
1756
+ level: node.attrs.level,
1757
+ text: node.textContent,
1758
+ pos
1759
+ })
1760
+ }
1761
+ })
1762
+
1763
+ return headings
1764
+ },
1765
+
1766
+ renderTOC(headings, attrs) {
1767
+ const { title, style, collapsible } = attrs
1768
+
1769
+ if (headings.length === 0) {
1770
+ return `
1771
+ <div class="inkpen-toc__empty">
1772
+ No headings found. Add headings to generate a table of contents.
1773
+ </div>
1774
+ `
1775
+ }
1776
+
1777
+ const listTag = style === 'numbered' ? 'ol' : 'ul'
1778
+
1779
+ return `
1780
+ <div class="inkpen-toc__header">
1781
+ <span class="inkpen-toc__title">${title}</span>
1782
+ ${collapsible ? '<button class="inkpen-toc__toggle">▼</button>' : ''}
1783
+ </div>
1784
+ <nav class="inkpen-toc__nav">
1785
+ <${listTag} class="inkpen-toc__list inkpen-toc__list--${style}">
1786
+ ${headings.map(h => `
1787
+ <li class="inkpen-toc__item inkpen-toc__item--level-${h.level}"
1788
+ style="--toc-indent: ${(h.level - 1) * 1}rem">
1789
+ <a href="#${h.id}" data-pos="${h.pos}">${h.text}</a>
1790
+ </li>
1791
+ `).join('')}
1792
+ </${listTag}>
1793
+ </nav>
1794
+ `
1795
+ },
1796
+
1797
+ attachClickHandlers(wrapper, editor) {
1798
+ wrapper.querySelectorAll('.inkpen-toc__item a').forEach(link => {
1799
+ link.addEventListener('click', (e) => {
1800
+ e.preventDefault()
1801
+ const pos = parseInt(link.dataset.pos)
1802
+
1803
+ // Scroll to heading
1804
+ const coords = editor.view.coordsAtPos(pos)
1805
+ window.scrollTo({
1806
+ top: coords.top - 100, // Offset for sticky headers
1807
+ behavior: 'smooth'
1808
+ })
1809
+
1810
+ // Focus editor at heading
1811
+ editor.commands.setTextSelection(pos)
1812
+ })
1813
+ })
1814
+ },
1815
+
1816
+ addCommands() {
1817
+ return {
1818
+ insertTableOfContents: (options = {}) => ({ commands }) => {
1819
+ return commands.insertContent({
1820
+ type: this.name,
1821
+ attrs: options
1822
+ })
1823
+ },
1824
+ setTocMaxDepth: (depth) => ({ tr, state }) => {
1825
+ // Update max depth
1826
+ },
1827
+ setTocStyle: (style) => ({ tr, state }) => {
1828
+ // Update style (numbered, bulleted, plain)
1829
+ }
1830
+ }
1831
+ }
1832
+ })
1833
+ ```
1834
+
1835
+ **CSS:**
1836
+ ```css
1837
+ /* app/assets/stylesheets/inkpen/toc.css */
1838
+
1839
+ .inkpen-toc {
1840
+ margin: 1.5rem 0;
1841
+ padding: 1rem 1.5rem;
1842
+ background: var(--inkpen-color-bg-subtle);
1843
+ border-radius: var(--inkpen-radius);
1844
+ border: 1px solid var(--inkpen-color-border);
1845
+ }
1846
+
1847
+ .inkpen-toc--sticky {
1848
+ position: sticky;
1849
+ top: 1rem;
1850
+ max-height: calc(100vh - 2rem);
1851
+ overflow-y: auto;
1852
+ }
1853
+
1854
+ .inkpen-toc__header {
1855
+ display: flex;
1856
+ justify-content: space-between;
1857
+ align-items: center;
1858
+ margin-bottom: 0.75rem;
1859
+ }
1860
+
1861
+ .inkpen-toc__title {
1862
+ font-weight: 600;
1863
+ font-size: 0.875rem;
1864
+ text-transform: uppercase;
1865
+ letter-spacing: 0.05em;
1866
+ color: var(--inkpen-color-text-muted);
1867
+ }
1868
+
1869
+ .inkpen-toc__toggle {
1870
+ border: none;
1871
+ background: none;
1872
+ cursor: pointer;
1873
+ padding: 0.25rem;
1874
+ }
1875
+
1876
+ .inkpen-toc__list {
1877
+ margin: 0;
1878
+ padding: 0;
1879
+ list-style: none;
1880
+ }
1881
+
1882
+ .inkpen-toc__list--numbered {
1883
+ counter-reset: toc;
1884
+ }
1885
+
1886
+ .inkpen-toc__list--numbered .inkpen-toc__item::before {
1887
+ counter-increment: toc;
1888
+ content: counters(toc, ".") ". ";
1889
+ color: var(--inkpen-color-text-muted);
1890
+ margin-right: 0.5rem;
1891
+ }
1892
+
1893
+ .inkpen-toc__list--bulleted .inkpen-toc__item::before {
1894
+ content: "•";
1895
+ color: var(--inkpen-color-text-muted);
1896
+ margin-right: 0.5rem;
1897
+ }
1898
+
1899
+ .inkpen-toc__item {
1900
+ padding: 0.375rem 0;
1901
+ padding-left: var(--toc-indent, 0);
1902
+ }
1903
+
1904
+ .inkpen-toc__item a {
1905
+ color: var(--inkpen-color-text);
1906
+ text-decoration: none;
1907
+ transition: color 150ms;
1908
+ }
1909
+
1910
+ .inkpen-toc__item a:hover {
1911
+ color: var(--inkpen-color-primary);
1912
+ }
1913
+
1914
+ /* Level styling */
1915
+ .inkpen-toc__item--level-1 { font-weight: 600; }
1916
+ .inkpen-toc__item--level-2 { font-weight: 500; }
1917
+ .inkpen-toc__item--level-3,
1918
+ .inkpen-toc__item--level-4,
1919
+ .inkpen-toc__item--level-5,
1920
+ .inkpen-toc__item--level-6 {
1921
+ font-size: 0.875rem;
1922
+ color: var(--inkpen-color-text-muted);
1923
+ }
1924
+
1925
+ .inkpen-toc__empty {
1926
+ font-size: 0.875rem;
1927
+ color: var(--inkpen-color-text-muted);
1928
+ font-style: italic;
1929
+ }
1930
+
1931
+ /* Collapsible */
1932
+ .inkpen-toc.is-collapsed .inkpen-toc__nav {
1933
+ display: none;
1934
+ }
1935
+
1936
+ .inkpen-toc.is-collapsed .inkpen-toc__toggle {
1937
+ transform: rotate(-90deg);
1938
+ }
1939
+
1940
+ /* Dark mode */
1941
+ @media (prefers-color-scheme: dark) {
1942
+ .inkpen-toc {
1943
+ background: var(--inkpen-color-bg-subtle);
1944
+ }
1945
+ }
1946
+ ```
1947
+
1948
+ ---
1949
+
1950
+ ### 5.4 Slash Commands Updates
1951
+
1952
+ Add new commands to slash menu:
1953
+
1954
+ ```javascript
1955
+ // Added to DEFAULT_COMMANDS in slash_commands.js
1956
+
1957
+ // Data group
1958
+ { id: "table", title: "Table", description: "Insert a table", icon: "⊞", group: "Data" },
1959
+ { id: "database", title: "Database", description: "Create an inline database", icon: "🗃️", group: "Data", keywords: ["notion", "airtable", "spreadsheet"] },
1960
+ { id: "databaseTable", title: "Database - Table View", description: "Database with table view", icon: "⊞", group: "Data" },
1961
+ { id: "databaseBoard", title: "Database - Board View", description: "Kanban-style board", icon: "▣", group: "Data", keywords: ["kanban", "trello"] },
1962
+ { id: "databaseGallery", title: "Database - Gallery", description: "Card gallery view", icon: "⊟", group: "Data" },
1963
+
1964
+ // Navigation group
1965
+ { id: "toc", title: "Table of Contents", description: "Auto-generated navigation", icon: "📑", group: "Navigation", keywords: ["contents", "navigation", "index"] },
1966
+ ```
1967
+
1968
+ ---
1969
+
1970
+ ### Implementation Priority
1971
+
1972
+ | Feature | Priority | Complexity | Files |
1973
+ |---------|----------|------------|-------|
1974
+ | Advanced Tables | High | Medium | advanced_table.js, advanced_table.css |
1975
+ | Table of Contents | High | Low | table_of_contents.js, toc.css |
1976
+ | Database Blocks | Medium | High | database.js, database.css |
1977
+ | Slash Menu Updates | Low | Low | slash_commands.js |
1978
+
1979
+ ---
1980
+
1981
+ ### Files to Create (v0.6.0)
1982
+
1983
+ ```
1984
+ app/assets/javascripts/inkpen/extensions/
1985
+ ├── advanced_table.js ← v0.6.0-alpha
1986
+ ├── database.js ← v0.6.0-beta
1987
+ └── table_of_contents.js ← v0.6.0-rc
1988
+
1989
+ app/assets/stylesheets/inkpen/
1990
+ ├── advanced_table.css ← v0.6.0-alpha
1991
+ ├── database.css ← v0.6.0-beta
1992
+ └── toc.css ← v0.6.0-rc
1993
+ ```
1994
+
1995
+ ---
1996
+
1997
+ ## Phase 6: Export & Import (v0.7.0) ✅
1998
+
1999
+ ### Goal
2000
+ Enable seamless content portability with Markdown import/export, clean HTML export, and PDF generation.
2001
+
2002
+ **Status:** Complete
2003
+
2004
+ ---
2005
+
2006
+ ### 6.1 Markdown Export/Import (v0.7.0-alpha) ✅
2007
+
2008
+ Convert editor content to/from Markdown with full fidelity.
2009
+
2010
+ **Features:**
2011
+ - Export to GitHub-Flavored Markdown (GFM)
2012
+ - Import from Markdown files
2013
+ - Frontmatter support (YAML metadata)
2014
+ - Table conversion (GFM tables)
2015
+ - Code block language preservation
2016
+ - Image handling (inline or reference style)
2017
+ - Task list conversion
2018
+ - Callout to blockquote mapping
2019
+ - Custom block fallbacks (HTML comments)
2020
+
2021
+ **Implementation:**
2022
+ ```javascript
2023
+ // app/assets/javascripts/inkpen/export/markdown.js
2024
+
2025
+ /**
2026
+ * Markdown Exporter
2027
+ *
2028
+ * Converts TipTap/ProseMirror document to Markdown.
2029
+ * Uses custom serializers for Inkpen-specific nodes.
2030
+ */
2031
+
2032
+ const NODE_SERIALIZERS = {
2033
+ paragraph: (node) => node.textContent + '\n\n',
2034
+ heading: (node) => '#'.repeat(node.attrs.level) + ' ' + node.textContent + '\n\n',
2035
+ bulletList: (node, serialize) => serializeList(node, serialize, '-'),
2036
+ orderedList: (node, serialize) => serializeList(node, serialize, '1.'),
2037
+ taskList: (node, serialize) => serializeTaskList(node, serialize),
2038
+ blockquote: (node, serialize) => node.content.map(n => '> ' + serialize(n)).join(''),
2039
+ codeBlock: (node) => '```' + (node.attrs.language || '') + '\n' + node.textContent + '\n```\n\n',
2040
+ horizontalRule: () => '---\n\n',
2041
+ image: (node) => `![${node.attrs.alt || ''}](${node.attrs.src})\n\n`,
2042
+ table: (node, serialize) => serializeTable(node, serialize),
2043
+ callout: (node, serialize) => serializeCallout(node, serialize),
2044
+ toggleBlock: (node, serialize) => serializeToggle(node, serialize),
2045
+ }
2046
+
2047
+ const MARK_SERIALIZERS = {
2048
+ bold: (text) => `**${text}**`,
2049
+ italic: (text) => `_${text}_`,
2050
+ strike: (text) => `~~${text}~~`,
2051
+ code: (text) => `\`${text}\``,
2052
+ link: (text, mark) => `[${text}](${mark.attrs.href})`,
2053
+ }
2054
+
2055
+ export function exportToMarkdown(doc, options = {}) {
2056
+ const { includeFrontmatter = true, imageStyle = 'inline' } = options
2057
+ let markdown = ''
2058
+
2059
+ if (includeFrontmatter && options.frontmatter) {
2060
+ markdown += '---\n'
2061
+ markdown += Object.entries(options.frontmatter)
2062
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
2063
+ .join('\n')
2064
+ markdown += '\n---\n\n'
2065
+ }
2066
+
2067
+ markdown += serializeNode(doc)
2068
+ return markdown
2069
+ }
2070
+
2071
+ export function importFromMarkdown(markdown, options = {}) {
2072
+ // Parse frontmatter
2073
+ const { content, frontmatter } = parseFrontmatter(markdown)
2074
+
2075
+ // Convert Markdown to ProseMirror document
2076
+ const doc = parseMarkdown(content)
2077
+
2078
+ return { doc, frontmatter }
2079
+ }
2080
+
2081
+ function serializeTable(node, serialize) {
2082
+ const rows = []
2083
+ let headerRow = null
2084
+
2085
+ node.content.forEach((row, index) => {
2086
+ const cells = row.content.map(cell => serialize(cell).trim())
2087
+ if (index === 0) {
2088
+ headerRow = '| ' + cells.join(' | ') + ' |'
2089
+ rows.push(headerRow)
2090
+ rows.push('| ' + cells.map(() => '---').join(' | ') + ' |')
2091
+ } else {
2092
+ rows.push('| ' + cells.join(' | ') + ' |')
2093
+ }
2094
+ })
2095
+
2096
+ return rows.join('\n') + '\n\n'
2097
+ }
2098
+
2099
+ function serializeCallout(node, serialize) {
2100
+ const type = node.attrs.type || 'info'
2101
+ const emoji = node.attrs.emoji || ''
2102
+ const content = serialize(node.content)
2103
+
2104
+ // Convert to blockquote with type indicator
2105
+ return `> [!${type.toUpperCase()}] ${emoji}\n> ${content.replace(/\n/g, '\n> ')}\n\n`
2106
+ }
2107
+ ```
2108
+
2109
+ **Commands:**
2110
+ ```javascript
2111
+ // Added to editor_controller.js
2112
+
2113
+ exportMarkdown(options = {}) {
2114
+ const markdown = exportToMarkdown(this.editor.state.doc, options)
2115
+ return markdown
2116
+ }
2117
+
2118
+ importMarkdown(markdown, options = {}) {
2119
+ const { doc, frontmatter } = importFromMarkdown(markdown, options)
2120
+ this.editor.commands.setContent(doc)
2121
+ return frontmatter
2122
+ }
2123
+
2124
+ downloadMarkdown(filename = 'document.md') {
2125
+ const markdown = this.exportMarkdown()
2126
+ downloadFile(markdown, filename, 'text/markdown')
2127
+ }
2128
+ ```
2129
+
2130
+ ---
2131
+
2132
+ ### 6.2 HTML Export (v0.7.0-beta)
2133
+
2134
+ Export clean, semantic HTML with optional styling.
2135
+
2136
+ **Features:**
2137
+ - Clean semantic HTML5 output
2138
+ - Optional inline CSS styling
2139
+ - Optional external stylesheet link
2140
+ - Configurable class prefixes
2141
+ - Image embedding (base64) or external URLs
2142
+ - Table accessibility attributes
2143
+ - Print-optimized output
2144
+ - Dark mode CSS variant
2145
+
2146
+ **Implementation:**
2147
+ ```javascript
2148
+ // app/assets/javascripts/inkpen/export/html.js
2149
+
2150
+ /**
2151
+ * HTML Exporter
2152
+ *
2153
+ * Generates clean, semantic HTML from editor content.
2154
+ */
2155
+
2156
+ export function exportToHTML(doc, options = {}) {
2157
+ const {
2158
+ includeStyles = true,
2159
+ inlineStyles = false,
2160
+ classPrefix = 'inkpen-',
2161
+ embedImages = false,
2162
+ includeWrapper = true,
2163
+ title = 'Document'
2164
+ } = options
2165
+
2166
+ let html = ''
2167
+
2168
+ // Document wrapper
2169
+ if (includeWrapper) {
2170
+ html += `<!DOCTYPE html>
2171
+ <html lang="en">
2172
+ <head>
2173
+ <meta charset="UTF-8">
2174
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2175
+ <title>${escapeHtml(title)}</title>
2176
+ ${includeStyles ? getStyleTag(inlineStyles, classPrefix) : ''}
2177
+ </head>
2178
+ <body>
2179
+ <article class="${classPrefix}document">
2180
+ `
2181
+ }
2182
+
2183
+ // Serialize content
2184
+ html += serializeToHTML(doc, { classPrefix, embedImages })
2185
+
2186
+ if (includeWrapper) {
2187
+ html += ` </article>
2188
+ </body>
2189
+ </html>`
2190
+ }
2191
+
2192
+ return html
2193
+ }
2194
+
2195
+ function getStyleTag(inline, prefix) {
2196
+ if (inline) {
2197
+ return `<style>${getExportStyles(prefix)}</style>`
2198
+ }
2199
+ return `<link rel="stylesheet" href="inkpen-export.css">`
2200
+ }
2201
+
2202
+ function getExportStyles(prefix) {
2203
+ return `
2204
+ .${prefix}document {
2205
+ max-width: 680px;
2206
+ margin: 0 auto;
2207
+ padding: 2rem;
2208
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2209
+ line-height: 1.6;
2210
+ color: #1a1a1a;
2211
+ }
2212
+ .${prefix}document h1 { font-size: 2rem; margin: 2rem 0 1rem; }
2213
+ .${prefix}document h2 { font-size: 1.5rem; margin: 1.5rem 0 0.75rem; }
2214
+ .${prefix}document h3 { font-size: 1.25rem; margin: 1.25rem 0 0.5rem; }
2215
+ .${prefix}document p { margin: 0 0 1rem; }
2216
+ .${prefix}document blockquote {
2217
+ margin: 1rem 0;
2218
+ padding-left: 1rem;
2219
+ border-left: 3px solid #e0e0e0;
2220
+ color: #666;
2221
+ }
2222
+ .${prefix}document pre {
2223
+ background: #f5f5f5;
2224
+ padding: 1rem;
2225
+ border-radius: 4px;
2226
+ overflow-x: auto;
2227
+ }
2228
+ .${prefix}document code {
2229
+ background: #f0f0f0;
2230
+ padding: 0.125rem 0.25rem;
2231
+ border-radius: 2px;
2232
+ font-size: 0.875em;
2233
+ }
2234
+ .${prefix}document table {
2235
+ width: 100%;
2236
+ border-collapse: collapse;
2237
+ margin: 1rem 0;
2238
+ }
2239
+ .${prefix}document th, .${prefix}document td {
2240
+ border: 1px solid #e0e0e0;
2241
+ padding: 0.5rem;
2242
+ text-align: left;
2243
+ }
2244
+ .${prefix}document img {
2245
+ max-width: 100%;
2246
+ height: auto;
2247
+ }
2248
+ .${prefix}callout {
2249
+ padding: 1rem;
2250
+ margin: 1rem 0;
2251
+ border-radius: 4px;
2252
+ border-left: 4px solid;
2253
+ }
2254
+ .${prefix}callout--info { background: #e3f2fd; border-color: #2196f3; }
2255
+ .${prefix}callout--warning { background: #fff3e0; border-color: #ff9800; }
2256
+ .${prefix}callout--tip { background: #e8f5e9; border-color: #4caf50; }
2257
+ @media print {
2258
+ .${prefix}document { max-width: none; padding: 0; }
2259
+ }
2260
+ `
2261
+ }
2262
+ ```
2263
+
2264
+ **Commands:**
2265
+ ```javascript
2266
+ exportHTML(options = {}) {
2267
+ return exportToHTML(this.editor.state.doc, options)
2268
+ }
2269
+
2270
+ downloadHTML(filename = 'document.html', options = {}) {
2271
+ const html = this.exportHTML(options)
2272
+ downloadFile(html, filename, 'text/html')
2273
+ }
2274
+
2275
+ copyHTML() {
2276
+ const html = this.exportHTML({ includeWrapper: false, includeStyles: false })
2277
+ navigator.clipboard.writeText(html)
2278
+ }
2279
+ ```
2280
+
2281
+ ---
2282
+
2283
+ ### 6.3 PDF Export (v0.7.0-rc)
2284
+
2285
+ Generate PDF documents from editor content.
2286
+
2287
+ **Features:**
2288
+ - Client-side PDF generation (no server required)
2289
+ - Page size options (A4, Letter, Legal)
2290
+ - Margins and orientation
2291
+ - Header/footer with page numbers
2292
+ - Table of contents generation
2293
+ - Cover page option
2294
+ - Custom fonts
2295
+ - Image quality settings
2296
+ - Watermark support
2297
+
2298
+ **Implementation:**
2299
+ ```javascript
2300
+ // app/assets/javascripts/inkpen/export/pdf.js
2301
+
2302
+ /**
2303
+ * PDF Exporter
2304
+ *
2305
+ * Generates PDF using html2pdf.js or jsPDF.
2306
+ * Falls back to print dialog if libraries unavailable.
2307
+ */
2308
+
2309
+ import { exportToHTML } from './html'
2310
+
2311
+ export async function exportToPDF(doc, options = {}) {
2312
+ const {
2313
+ filename = 'document.pdf',
2314
+ pageSize = 'a4',
2315
+ orientation = 'portrait',
2316
+ margins = { top: 20, right: 20, bottom: 20, left: 20 },
2317
+ includeHeader = false,
2318
+ includeFooter = true,
2319
+ includeTOC = false,
2320
+ coverPage = null,
2321
+ watermark = null,
2322
+ quality = 2
2323
+ } = options
2324
+
2325
+ // Generate HTML first
2326
+ const html = exportToHTML(doc, {
2327
+ includeStyles: true,
2328
+ inlineStyles: true,
2329
+ includeWrapper: false
2330
+ })
2331
+
2332
+ // Check for html2pdf library
2333
+ if (typeof html2pdf !== 'undefined') {
2334
+ return generateWithHtml2Pdf(html, {
2335
+ filename,
2336
+ pageSize,
2337
+ orientation,
2338
+ margins,
2339
+ quality
2340
+ })
2341
+ }
2342
+
2343
+ // Fallback to print dialog
2344
+ return printToPDF(html, options)
2345
+ }
2346
+
2347
+ async function generateWithHtml2Pdf(html, options) {
2348
+ const { filename, pageSize, orientation, margins, quality } = options
2349
+
2350
+ const element = document.createElement('div')
2351
+ element.innerHTML = html
2352
+ element.style.width = '210mm' // A4 width
2353
+ document.body.appendChild(element)
2354
+
2355
+ const opt = {
2356
+ margin: [margins.top, margins.right, margins.bottom, margins.left],
2357
+ filename,
2358
+ image: { type: 'jpeg', quality: 0.98 },
2359
+ html2canvas: { scale: quality, useCORS: true },
2360
+ jsPDF: { unit: 'mm', format: pageSize, orientation }
2361
+ }
2362
+
2363
+ try {
2364
+ await html2pdf().set(opt).from(element).save()
2365
+ } finally {
2366
+ document.body.removeChild(element)
2367
+ }
2368
+ }
2369
+
2370
+ function printToPDF(html, options) {
2371
+ // Open print dialog as fallback
2372
+ const printWindow = window.open('', '_blank')
2373
+ printWindow.document.write(`
2374
+ <!DOCTYPE html>
2375
+ <html>
2376
+ <head>
2377
+ <title>${options.filename || 'Document'}</title>
2378
+ <style>
2379
+ @page {
2380
+ size: ${options.pageSize || 'A4'} ${options.orientation || 'portrait'};
2381
+ margin: ${options.margins?.top || 20}mm ${options.margins?.right || 20}mm
2382
+ ${options.margins?.bottom || 20}mm ${options.margins?.left || 20}mm;
2383
+ }
2384
+ body {
2385
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2386
+ line-height: 1.6;
2387
+ }
2388
+ @media print {
2389
+ .no-print { display: none; }
2390
+ }
2391
+ </style>
2392
+ </head>
2393
+ <body>
2394
+ ${html}
2395
+ <script>window.onload = function() { window.print(); window.close(); }</script>
2396
+ </body>
2397
+ </html>
2398
+ `)
2399
+ printWindow.document.close()
2400
+ }
2401
+ ```
2402
+
2403
+ **Commands:**
2404
+ ```javascript
2405
+ async exportPDF(options = {}) {
2406
+ await exportToPDF(this.editor.state.doc, options)
2407
+ }
2408
+
2409
+ async downloadPDF(filename = 'document.pdf', options = {}) {
2410
+ await this.exportPDF({ ...options, filename })
2411
+ }
2412
+ ```
2413
+
2414
+ ---
2415
+
2416
+ ### 6.4 Export Toolbar/Menu
2417
+
2418
+ Add export options to the UI.
2419
+
2420
+ **Features:**
2421
+ - Export dropdown menu in toolbar
2422
+ - Keyboard shortcuts for quick export
2423
+ - Recent exports history
2424
+ - Export presets (save favorite settings)
2425
+
2426
+ **Implementation:**
2427
+ ```javascript
2428
+ // Added to sticky_toolbar_controller.js or separate export_controller.js
2429
+
2430
+ const EXPORT_MENU = [
2431
+ { id: 'markdown', label: 'Markdown (.md)', icon: 'M↓', shortcut: 'Cmd+Shift+M' },
2432
+ { id: 'html', label: 'HTML (.html)', icon: '<>', shortcut: 'Cmd+Shift+H' },
2433
+ { id: 'pdf', label: 'PDF (.pdf)', icon: '📄', shortcut: 'Cmd+Shift+P' },
2434
+ { divider: true },
2435
+ { id: 'copy-html', label: 'Copy as HTML', icon: '📋' },
2436
+ { id: 'copy-markdown', label: 'Copy as Markdown', icon: '📋' }
2437
+ ]
2438
+ ```
2439
+
2440
+ **CSS:**
2441
+ ```css
2442
+ /* app/assets/stylesheets/inkpen/export.css */
2443
+
2444
+ .inkpen-export-menu {
2445
+ position: absolute;
2446
+ top: 100%;
2447
+ right: 0;
2448
+ min-width: 200px;
2449
+ padding: 0.5rem;
2450
+ background: var(--inkpen-toolbar-bg);
2451
+ border: 1px solid var(--inkpen-color-border);
2452
+ border-radius: var(--inkpen-radius);
2453
+ box-shadow: var(--inkpen-shadow-lg);
2454
+ z-index: 100;
2455
+ }
2456
+
2457
+ .inkpen-export-menu__item {
2458
+ display: flex;
2459
+ align-items: center;
2460
+ gap: 0.75rem;
2461
+ width: 100%;
2462
+ padding: 0.5rem 0.75rem;
2463
+ border: none;
2464
+ border-radius: var(--inkpen-radius-sm);
2465
+ background: transparent;
2466
+ color: var(--inkpen-color-text);
2467
+ font-size: 0.875rem;
2468
+ text-align: left;
2469
+ cursor: pointer;
2470
+ }
2471
+
2472
+ .inkpen-export-menu__item:hover {
2473
+ background: var(--inkpen-color-bg-subtle);
2474
+ }
2475
+
2476
+ .inkpen-export-menu__shortcut {
2477
+ margin-left: auto;
2478
+ font-size: 0.75rem;
2479
+ color: var(--inkpen-color-text-muted);
2480
+ }
2481
+
2482
+ .inkpen-export-menu__divider {
2483
+ height: 1px;
2484
+ margin: 0.5rem 0;
2485
+ background: var(--inkpen-color-border);
2486
+ }
2487
+ ```
2488
+
2489
+ ---
2490
+
2491
+ ### Implementation Priority
2492
+
2493
+ | Feature | Priority | Complexity | Files |
2494
+ |---------|----------|------------|-------|
2495
+ | Markdown Export | High | Medium | markdown.js |
2496
+ | Markdown Import | High | High | markdown.js |
2497
+ | HTML Export | High | Low | html.js |
2498
+ | PDF Export | Medium | Medium | pdf.js |
2499
+ | Export Menu | Medium | Low | export_menu.js, export.css |
2500
+
2501
+ ---
2502
+
2503
+ ### Files to Create (v0.7.0)
2504
+
2505
+ ```
2506
+ app/assets/javascripts/inkpen/export/
2507
+ ├── markdown.js ← v0.7.0-alpha
2508
+ ├── html.js ← v0.7.0-beta
2509
+ ├── pdf.js ← v0.7.0-rc
2510
+ └── index.js ← exports all
2511
+
2512
+ app/assets/stylesheets/inkpen/
2513
+ └── export.css ← v0.7.0
2514
+
2515
+ lib/inkpen/
2516
+ └── export.rb ← Ruby helpers for server-side export (optional)
2517
+ ```
2518
+
2519
+ ---
2520
+
2521
+ ## Technical References
2522
+
2523
+ ### TipTap/ProseMirror
2524
+ - [TipTap Documentation](https://tiptap.dev/docs)
2525
+ - [TipTap Notion Template](https://tiptap.dev/docs/ui-components/templates/notion-like-editor)
2526
+ - [TipTap Suggestion Plugin](https://tiptap.dev/docs/editor/api/utilities/suggestion)
2527
+ - [ProseMirror Guide](https://prosemirror.net/docs/guide/)
2528
+ - [ProseMirror Decorations](https://prosemirror.net/docs/ref/#view.Decorations)
2529
+
2530
+ ### Block Editors
2531
+ - [Plate Editor](https://platejs.org/) - React block editor on ProseMirror
2532
+ - [BlockNote](https://www.blocknotejs.org/) - Notion-style TipTap wrapper
2533
+ - [Editor.js](https://editorjs.io/) - Block editor with JSON output
2534
+ - [Notitap](https://github.com/sereneinserenade/notitap) - Notion clone on TipTap
2535
+
2536
+ ### Drag & Drop
2537
+ - [Plate DnD](https://platejs.org/docs/dnd)
2538
+ - [dnd-kit](https://dndkit.com/) - Modern React DnD
2539
+ - [Editor.js Drag Plugin](https://github.com/kommitters/editorjs-drag-drop)
2540
+
2541
+ ### Design Patterns
2542
+ - [Notion Block Model](https://www.notion.so/help/what-is-a-block)
2543
+ - [Craft.do Editor](https://www.craft.do/)
2544
+ - [Coda Editor](https://coda.io/)
2545
+
2546
+ ---
2547
+
2548
+ ## Implementation Priority
2549
+
2550
+ | Phase | Feature | Priority | Complexity | Impact |
2551
+ |-------|---------|----------|------------|--------|
2552
+ | 1 | Slash Commands | High | Medium | High |
2553
+ | 2 | Block Gutter | High | Medium | High |
2554
+ | 3 | Drag & Drop | Medium | High | Medium |
2555
+ | 4.1 | Toggle Blocks | Medium | Low | Medium |
2556
+ | 4.2 | Columns | Low | Medium | Low |
2557
+ | 4.3 | Enhanced Callouts | Medium | Low | Medium |
2558
+ | 5 | Polish & Animations | Low | Medium | High |
2559
+
2560
+ ---
2561
+
2562
+ ## File Structure After Implementation
2563
+
2564
+ ```
2565
+ app/assets/javascripts/inkpen/
2566
+ ├── controllers/
2567
+ │ ├── editor_controller.js
2568
+ │ ├── toolbar_controller.js
2569
+ │ ├── sticky_toolbar_controller.js
2570
+ │ └── block_menu_controller.js ← v0.3.1
2571
+ ├── extensions/
2572
+ │ ├── section.js ✅ DONE
2573
+ │ ├── preformatted.js ✅ DONE
2574
+ │ ├── slash_commands.js ✅ DONE
2575
+ │ ├── block_gutter.js ✅ DONE
2576
+ │ ├── drag_handle.js ✅ DONE
2577
+ │ ├── toggle_block.js ✅ DONE
2578
+ │ ├── columns.js ✅ DONE
2579
+ │ ├── callout.js ✅ DONE
2580
+ │ └── block_commands.js ✅ DONE
2581
+ ├── helpers/
2582
+ │ └── block_helpers.js ← future
2583
+ └── index.js
2584
+
2585
+ app/assets/stylesheets/inkpen/
2586
+ ├── editor.css
2587
+ ├── toolbar.css
2588
+ ├── sticky_toolbar.css
2589
+ ├── section.css ✅ DONE
2590
+ ├── preformatted.css ✅ DONE
2591
+ ├── slash_menu.css ✅ DONE
2592
+ ├── block_gutter.css ✅ DONE
2593
+ ├── drag_drop.css ✅ DONE
2594
+ ├── toggle.css ✅ DONE
2595
+ ├── columns.css ✅ DONE
2596
+ ├── callout.css ✅ DONE
2597
+ └── animations.css ✅ DONE
2598
+
2599
+ lib/inkpen/extensions/
2600
+ ├── base.rb
2601
+ ├── section.rb ✅ DONE
2602
+ ├── preformatted.rb ✅ DONE
2603
+ ├── slash_commands.rb ✅ DONE
2604
+ ├── mention.rb
2605
+ ├── table.rb
2606
+ ├── task_list.rb
2607
+ ├── code_block_syntax.rb
2608
+ └── forced_document.rb
2609
+ ```
2610
+
2611
+ ---
2612
+
2613
+ ## Phase 7: Document Sections (v0.8.0) — PLANNED
2614
+
2615
+ ### Goal
2616
+ Add true "content grouping" sections that group blocks under a heading, enabling Notion-style document structure, collapsible sections, and outline navigation.
2617
+
2618
+ **Status:** Planned
2619
+
2620
+ > **Note**: This is different from our existing `Section` extension, which controls layout (width/spacing). Document sections are **container nodes** that group related content.
2621
+
2622
+ ---
2623
+
2624
+ ### 7.1 Understanding the Difference
2625
+
2626
+ | Feature | Layout Section (v0.2.2) | Document Section (v0.8.0) |
2627
+ |---------|------------------------|---------------------------|
2628
+ | Purpose | Visual presentation | Content organization |
2629
+ | Contains | Any blocks | Title + blocks |
2630
+ | Schema | `content: 'block+'` | `content: 'sectionTitle block*'` |
2631
+ | Use case | Page builder widths | Document outline |
2632
+ | Collapsible | No | Yes |
2633
+ | Drag behavior | Single block | Group of blocks |
2634
+
2635
+ ---
2636
+
2637
+ ### 7.2 Document Section Extension
2638
+
2639
+ A container node with explicit title + content structure.
2640
+
2641
+ **Features:**
2642
+ - Section title (sectionTitle node, renders as H2)
2643
+ - Collapsible content (toggle visibility)
2644
+ - Drag entire section as a group
2645
+ - Outline navigation integration
2646
+ - Nesting support (sections within sections)
2647
+ - Keyboard shortcuts for navigation
2648
+
2649
+ **Schema:**
2650
+ ```javascript
2651
+ // Document Section container
2652
+ const DocumentSection = Node.create({
2653
+ name: 'documentSection',
2654
+ group: 'block',
2655
+ content: 'sectionTitle block*', // Title + any blocks
2656
+ defining: true,
2657
+ isolating: true,
2658
+ draggable: true,
2659
+
2660
+ addAttributes() {
2661
+ return {
2662
+ collapsed: { default: false },
2663
+ id: { default: null } // For deep linking
2664
+ }
2665
+ }
2666
+ })
2667
+
2668
+ // Section Title (always first child)
2669
+ const SectionTitle = Node.create({
2670
+ name: 'sectionTitle',
2671
+ content: 'inline*',
2672
+ defining: true,
2673
+
2674
+ parseHTML() {
2675
+ return [{ tag: 'h2[data-section-title]' }]
2676
+ },
2677
+
2678
+ renderHTML({ HTMLAttributes }) {
2679
+ return ['h2', { ...HTMLAttributes, 'data-section-title': 'true' }, 0]
2680
+ }
2681
+ })
2682
+ ```
2683
+
2684
+ **NodeView:**
2685
+ ```javascript
2686
+ // Section wrapper with collapse UI
2687
+ const DocumentSectionView = ({ node, editor, getPos }) => {
2688
+ const dom = document.createElement('div')
2689
+ dom.className = 'inkpen-doc-section'
2690
+
2691
+ // Collapse toggle
2692
+ const toggle = document.createElement('button')
2693
+ toggle.className = 'inkpen-doc-section__toggle'
2694
+ toggle.innerHTML = node.attrs.collapsed ? '▶' : '▼'
2695
+ toggle.onclick = () => toggleCollapsed(editor, getPos())
2696
+
2697
+ // Section chrome
2698
+ const header = document.createElement('div')
2699
+ header.className = 'inkpen-doc-section__header'
2700
+ header.appendChild(toggle)
2701
+
2702
+ // Content area (ProseMirror renders children here)
2703
+ const contentDOM = document.createElement('div')
2704
+ contentDOM.className = 'inkpen-doc-section__content'
2705
+ if (node.attrs.collapsed) contentDOM.style.display = 'none'
2706
+
2707
+ dom.appendChild(header)
2708
+ dom.appendChild(contentDOM)
2709
+
2710
+ return { dom, contentDOM }
2711
+ }
2712
+ ```
2713
+
2714
+ **Commands:**
2715
+ - `insertDocumentSection()` - Insert new section with title
2716
+ - `toggleSectionCollapsed()` - Expand/collapse section
2717
+ - `wrapInDocumentSection()` - Wrap selected blocks in section
2718
+ - `unwrapDocumentSection()` - Remove section wrapper, keep content
2719
+ - `moveSectionUp()` / `moveSectionDown()` - Reorder sections
2720
+ - `goToNextSection()` / `goToPreviousSection()` - Navigation
2721
+
2722
+ **Keyboard Shortcuts:**
2723
+ | Shortcut | Action |
2724
+ |----------|--------|
2725
+ | `Cmd+Shift+Enter` | Insert document section |
2726
+ | `Cmd+.` | Toggle section collapsed |
2727
+ | `Cmd+Alt+↑` | Go to previous section |
2728
+ | `Cmd+Alt+↓` | Go to next section |
2729
+
2730
+ ---
2731
+
2732
+ ### 7.3 Section Outline Panel
2733
+
2734
+ A sidebar/panel showing document structure.
2735
+
2736
+ **Features:**
2737
+ - Tree view of all document sections
2738
+ - Click to navigate to section
2739
+ - Drag to reorder sections
2740
+ - Collapse/expand from outline
2741
+ - Search/filter sections
2742
+ - Section word count
2743
+
2744
+ **Implementation:**
2745
+ ```javascript
2746
+ // Get all sections as tree
2747
+ function getSectionOutline(editor) {
2748
+ const sections = []
2749
+ let index = 0
2750
+
2751
+ editor.state.doc.descendants((node, pos) => {
2752
+ if (node.type.name === 'documentSection') {
2753
+ const title = node.firstChild?.textContent || 'Untitled'
2754
+ sections.push({
2755
+ id: node.attrs.id || `section-${index++}`,
2756
+ title,
2757
+ pos,
2758
+ collapsed: node.attrs.collapsed,
2759
+ depth: getDepth(node, editor.state.doc)
2760
+ })
2761
+ }
2762
+ })
2763
+
2764
+ return sections
2765
+ }
2766
+ ```
2767
+
2768
+ ---
2769
+
2770
+ ### 7.4 Ruby Layer
2771
+
2772
+ **Block Registry (optional, for validation):**
2773
+ ```ruby
2774
+ # lib/inkpen/block_registry.rb
2775
+ module Inkpen
2776
+ class BlockRegistry
2777
+ ALLOWED_NODES = %w[
2778
+ doc paragraph text heading bulletList orderedList listItem
2779
+ blockquote codeBlock horizontalRule hardBreak
2780
+ documentSection sectionTitle
2781
+ callout toggleBlock columns section
2782
+ enhancedImage fileAttachment embed
2783
+ database tableOfContents
2784
+ ].freeze
2785
+
2786
+ def self.validate!(doc_json)
2787
+ walk_nodes(doc_json) do |node|
2788
+ type = node['type']
2789
+ raise InvalidNodeError, "Unknown node: #{type}" unless valid_type?(type)
2790
+ end
2791
+ end
2792
+
2793
+ def self.valid_type?(type)
2794
+ ALLOWED_NODES.include?(type) || core_type?(type)
2795
+ end
2796
+ end
2797
+ end
2798
+ ```
2799
+
2800
+ **Document Section Extension PORO:**
2801
+ ```ruby
2802
+ # lib/inkpen/extensions/document_section.rb
2803
+ module Inkpen
2804
+ module Extensions
2805
+ class DocumentSection < Base
2806
+ def name
2807
+ :document_section
2808
+ end
2809
+
2810
+ def default_collapsed
2811
+ options.fetch(:default_collapsed, false)
2812
+ end
2813
+
2814
+ def nesting_enabled?
2815
+ options.fetch(:nesting, true)
2816
+ end
2817
+
2818
+ def max_depth
2819
+ options.fetch(:max_depth, 3)
2820
+ end
2821
+
2822
+ def to_config
2823
+ {
2824
+ defaultCollapsed: default_collapsed,
2825
+ nestingEnabled: nesting_enabled?,
2826
+ maxDepth: max_depth
2827
+ }
2828
+ end
2829
+ end
2830
+ end
2831
+ end
2832
+ ```
2833
+
2834
+ ---
2835
+
2836
+ ### 7.5 CSS
2837
+
2838
+ ```css
2839
+ /* app/assets/stylesheets/inkpen/document_section.css */
2840
+
2841
+ .inkpen-doc-section {
2842
+ position: relative;
2843
+ margin: 1.5rem 0;
2844
+ padding-left: 1.5rem;
2845
+ border-left: 2px solid var(--inkpen-color-border);
2846
+ }
2847
+
2848
+ .inkpen-doc-section__header {
2849
+ position: absolute;
2850
+ left: -0.75rem;
2851
+ top: 0;
2852
+ }
2853
+
2854
+ .inkpen-doc-section__toggle {
2855
+ width: 1.5rem;
2856
+ height: 1.5rem;
2857
+ display: flex;
2858
+ align-items: center;
2859
+ justify-content: center;
2860
+ border: none;
2861
+ background: var(--inkpen-toolbar-bg);
2862
+ border-radius: var(--inkpen-radius-sm);
2863
+ cursor: pointer;
2864
+ font-size: 0.75rem;
2865
+ color: var(--inkpen-color-text-muted);
2866
+ }
2867
+
2868
+ .inkpen-doc-section__toggle:hover {
2869
+ background: var(--inkpen-color-bg-subtle);
2870
+ color: var(--inkpen-color-text);
2871
+ }
2872
+
2873
+ .inkpen-doc-section__content {
2874
+ /* Content area */
2875
+ }
2876
+
2877
+ .inkpen-doc-section.is-collapsed .inkpen-doc-section__content {
2878
+ display: none;
2879
+ }
2880
+
2881
+ /* Nested sections */
2882
+ .inkpen-doc-section .inkpen-doc-section {
2883
+ margin-left: 1rem;
2884
+ border-left-color: var(--inkpen-color-border-light);
2885
+ }
2886
+
2887
+ /* Drag state */
2888
+ .inkpen-doc-section.is-dragging {
2889
+ opacity: 0.5;
2890
+ }
2891
+
2892
+ /* Selected section */
2893
+ .inkpen-doc-section.is-selected {
2894
+ border-left-color: var(--inkpen-color-primary);
2895
+ background: rgba(var(--inkpen-color-primary-rgb), 0.05);
2896
+ }
2897
+ ```
2898
+
2899
+ ---
2900
+
2901
+ ### 7.6 Integration Points
2902
+
2903
+ **Table of Contents:**
2904
+ - TOC extension should recognize documentSection nodes
2905
+ - Show section titles in outline
2906
+ - Navigate to section on click
2907
+
2908
+ **Export:**
2909
+ - Markdown: Use heading + content pattern
2910
+ - HTML: Use `<section>` + `<h2>` structure
2911
+ - PDF: Respect collapsed state (expand for export)
2912
+
2913
+ **Slash Commands:**
2914
+ ```javascript
2915
+ {
2916
+ id: 'documentSection',
2917
+ title: 'Section',
2918
+ description: 'Create a collapsible section with title',
2919
+ icon: '📑',
2920
+ group: 'Structure',
2921
+ keywords: ['section', 'group', 'collapse', 'outline']
2922
+ }
2923
+ ```
2924
+
2925
+ ---
2926
+
2927
+ ### Implementation Priority
2928
+
2929
+ | Feature | Priority | Complexity | Notes |
2930
+ |---------|----------|------------|-------|
2931
+ | DocumentSection node | High | Medium | Core functionality |
2932
+ | SectionTitle node | High | Low | Simple inline node |
2933
+ | Collapse/expand | High | Low | Toggle attribute |
2934
+ | NodeView with chrome | Medium | Medium | UI wrapper |
2935
+ | Outline panel | Medium | High | Separate component |
2936
+ | Nesting support | Low | Medium | Recursive schema |
2937
+ | Block registry | Low | Low | Optional validation |
2938
+
2939
+ ---
2940
+
2941
+ ### Files to Create (v0.8.0)
2942
+
2943
+ ```
2944
+ lib/inkpen/extensions/
2945
+ └── document_section.rb
2946
+
2947
+ app/assets/javascripts/inkpen/extensions/
2948
+ ├── document_section.js
2949
+ └── section_title.js
2950
+
2951
+ app/assets/stylesheets/inkpen/
2952
+ └── document_section.css
2953
+ ```
2954
+
2955
+ ---
2956
+
2957
+ ## Phase 8: Collaboration (v0.9.0) — FUTURE
2958
+
2959
+ ### Goal
2960
+ Real-time collaborative editing with presence awareness.
2961
+
2962
+ **Potential Features:**
2963
+ - Y.js or Hocuspocus integration
2964
+ - Cursor presence (see where others are typing)
2965
+ - User avatars and names
2966
+ - Conflict resolution
2967
+ - Offline support with sync
2968
+
2969
+ ---
2970
+
2971
+ ## Phase 9: AI Integration (v1.0.0) — FUTURE
2972
+
2973
+ ### Goal
2974
+ AI-assisted writing and editing capabilities.
2975
+
2976
+ **Potential Features:**
2977
+ - AI writing suggestions
2978
+ - Grammar and style checking
2979
+ - Content summarization
2980
+ - Translation
2981
+ - Image generation prompts
2982
+
2983
+ ---
2984
+
2985
+ ## Architecture Notes
2986
+
2987
+ ### Block Types Taxonomy
2988
+
2989
+ ```
2990
+ Inkpen Node Types
2991
+ ├── Core (from TipTap)
2992
+ │ ├── doc
2993
+ │ ├── paragraph
2994
+ │ ├── text
2995
+ │ ├── heading
2996
+ │ ├── bulletList / orderedList / listItem
2997
+ │ ├── blockquote
2998
+ │ ├── codeBlock
2999
+ │ ├── horizontalRule
3000
+ │ └── hardBreak
3001
+ ├── Layout Blocks
3002
+ │ ├── section (width/spacing)
3003
+ │ ├── columns / column
3004
+ │ └── documentSection (content grouping) ← v0.8.0
3005
+ ├── Content Blocks
3006
+ │ ├── callout
3007
+ │ ├── toggleBlock
3008
+ │ ├── preformatted
3009
+ │ └── database
3010
+ ├── Media Blocks
3011
+ │ ├── enhancedImage
3012
+ │ ├── fileAttachment
3013
+ │ ├── embed
3014
+ │ └── youtube
3015
+ └── Navigation Blocks
3016
+ └── tableOfContents
3017
+ ```
3018
+
3019
+ ### When to Use What
3020
+
3021
+ | Need | Use |
3022
+ |------|-----|
3023
+ | Different content widths | `section` (layout) |
3024
+ | Collapsible single block | `toggleBlock` |
3025
+ | Collapsible group of blocks | `documentSection` |
3026
+ | Side-by-side content | `columns` |
3027
+ | Highlighted message | `callout` |
3028
+ | Structured data | `database` |
3029
+ | Document navigation | `tableOfContents` |