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
@@ -0,0 +1,407 @@
1
+ import { Extension } from "@tiptap/core"
2
+ import { Plugin, PluginKey } from "@tiptap/pm/state"
3
+
4
+ /**
5
+ * Drag Handle Extension for TipTap
6
+ *
7
+ * Enables drag and drop reordering of blocks. Works in conjunction with
8
+ * the BlockGutter extension which provides the visual drag handles.
9
+ *
10
+ * Features:
11
+ * - Visual drop indicator showing insertion point
12
+ * - Smooth block movement with ProseMirror transactions
13
+ * - Edge scrolling when dragging near viewport edges
14
+ * - Keyboard shortcut for moving blocks (Cmd+Shift+Arrow)
15
+ *
16
+ * @since 0.3.2
17
+ */
18
+
19
+ const DRAG_HANDLE_KEY = new PluginKey("dragHandle")
20
+
21
+ export const DragHandle = Extension.create({
22
+ name: "dragHandle",
23
+
24
+ addOptions() {
25
+ return {
26
+ // Scroll speed when dragging near edges
27
+ scrollSpeed: 10,
28
+ // Distance from edge to trigger scrolling
29
+ scrollThreshold: 80,
30
+ // Class for drop indicator
31
+ dropIndicatorClass: "inkpen-drop-indicator",
32
+ // Callback when block is moved
33
+ onBlockMoved: null
34
+ }
35
+ },
36
+
37
+ addKeyboardShortcuts() {
38
+ return {
39
+ "Mod-Shift-ArrowUp": () => this.editor.commands.moveBlockUp(),
40
+ "Mod-Shift-ArrowDown": () => this.editor.commands.moveBlockDown()
41
+ }
42
+ },
43
+
44
+ addCommands() {
45
+ return {
46
+ moveBlockUp: () => ({ state, dispatch }) => {
47
+ return this.moveBlock(state, dispatch, -1)
48
+ },
49
+
50
+ moveBlockDown: () => ({ state, dispatch }) => {
51
+ return this.moveBlock(state, dispatch, 1)
52
+ },
53
+
54
+ moveBlockToPosition: (fromPos, toPos) => ({ state, dispatch }) => {
55
+ return this.moveBlockTo(state, dispatch, fromPos, toPos)
56
+ }
57
+ }
58
+ },
59
+
60
+ moveBlock(state, dispatch, direction) {
61
+ const { selection, doc } = state
62
+ const $from = selection.$from
63
+
64
+ // Find the current block
65
+ const depth = $from.depth
66
+ if (depth === 0) return false
67
+
68
+ const blockStart = $from.before(depth)
69
+ const blockEnd = $from.after(depth)
70
+ const node = doc.nodeAt(blockStart)
71
+
72
+ if (!node) return false
73
+
74
+ // Find sibling block
75
+ let targetPos
76
+ if (direction < 0) {
77
+ // Moving up - find previous sibling
78
+ const $blockStart = doc.resolve(blockStart)
79
+ if ($blockStart.nodeBefore) {
80
+ targetPos = blockStart - $blockStart.nodeBefore.nodeSize
81
+ } else {
82
+ return false // Already at top
83
+ }
84
+ } else {
85
+ // Moving down - find next sibling
86
+ const $blockEnd = doc.resolve(blockEnd)
87
+ if ($blockEnd.nodeAfter) {
88
+ targetPos = blockEnd + $blockEnd.nodeAfter.nodeSize
89
+ } else {
90
+ return false // Already at bottom
91
+ }
92
+ }
93
+
94
+ if (dispatch) {
95
+ const tr = state.tr
96
+
97
+ // Delete from original position
98
+ tr.delete(blockStart, blockEnd)
99
+
100
+ // Adjust target position after deletion
101
+ const adjustedPos = targetPos > blockStart ? targetPos - node.nodeSize : targetPos
102
+
103
+ // Insert at new position
104
+ tr.insert(adjustedPos, node)
105
+
106
+ // Set selection to moved block
107
+ const newPos = adjustedPos + 1
108
+ tr.setSelection(state.selection.constructor.near(tr.doc.resolve(newPos)))
109
+
110
+ dispatch(tr.scrollIntoView())
111
+ }
112
+
113
+ return true
114
+ },
115
+
116
+ moveBlockTo(state, dispatch, fromPos, toPos) {
117
+ const { doc } = state
118
+ const node = doc.nodeAt(fromPos)
119
+
120
+ if (!node) return false
121
+
122
+ // Don't move if positions are the same
123
+ if (fromPos === toPos) return false
124
+
125
+ const nodeSize = node.nodeSize
126
+
127
+ if (dispatch) {
128
+ const tr = state.tr
129
+
130
+ // Delete from original position
131
+ tr.delete(fromPos, fromPos + nodeSize)
132
+
133
+ // Adjust target position after deletion
134
+ const adjustedPos = toPos > fromPos ? toPos - nodeSize : toPos
135
+
136
+ // Insert at new position
137
+ tr.insert(adjustedPos, node)
138
+
139
+ dispatch(tr.scrollIntoView())
140
+ }
141
+
142
+ return true
143
+ },
144
+
145
+ addProseMirrorPlugins() {
146
+ const extension = this
147
+ const options = this.options
148
+
149
+ return [
150
+ new Plugin({
151
+ key: DRAG_HANDLE_KEY,
152
+
153
+ state: {
154
+ init() {
155
+ return {
156
+ isDragging: false,
157
+ draggedPos: null,
158
+ dropPos: null
159
+ }
160
+ },
161
+ apply(tr, value) {
162
+ const meta = tr.getMeta(DRAG_HANDLE_KEY)
163
+ if (meta) {
164
+ return { ...value, ...meta }
165
+ }
166
+ return value
167
+ }
168
+ },
169
+
170
+ props: {
171
+ handleDOMEvents: {
172
+ dragover(view, event) {
173
+ // Check if this is a block drag (from our gutter)
174
+ const blockData = event.dataTransfer.types.includes("application/inkpen-block")
175
+ if (!blockData) return false
176
+
177
+ event.preventDefault()
178
+ event.dataTransfer.dropEffect = "move"
179
+
180
+ // Find drop position
181
+ const coords = { left: event.clientX, top: event.clientY }
182
+ const dropInfo = extension.findDropPosition(view, coords)
183
+
184
+ if (dropInfo) {
185
+ extension.showDropIndicator(view, dropInfo)
186
+
187
+ // Edge scrolling
188
+ extension.handleEdgeScroll(view, event.clientY)
189
+ }
190
+
191
+ return true
192
+ },
193
+
194
+ dragleave(view, event) {
195
+ // Only hide if leaving the editor entirely
196
+ const relatedTarget = event.relatedTarget
197
+ if (!view.dom.contains(relatedTarget)) {
198
+ extension.hideDropIndicator(view)
199
+ }
200
+ return false
201
+ },
202
+
203
+ drop(view, event) {
204
+ const blockDataStr = event.dataTransfer.getData("application/inkpen-block")
205
+ if (!blockDataStr) return false
206
+
207
+ event.preventDefault()
208
+
209
+ try {
210
+ const blockData = JSON.parse(blockDataStr)
211
+ const fromPos = blockData.pos
212
+
213
+ // Find drop position
214
+ const coords = { left: event.clientX, top: event.clientY }
215
+ const dropInfo = extension.findDropPosition(view, coords)
216
+
217
+ if (dropInfo && dropInfo.pos !== fromPos) {
218
+ // Execute the move
219
+ const node = view.state.doc.nodeAt(fromPos)
220
+ if (node) {
221
+ extension.moveBlockTo(
222
+ view.state,
223
+ view.dispatch.bind(view),
224
+ fromPos,
225
+ dropInfo.insertPos
226
+ )
227
+
228
+ // Callback
229
+ options.onBlockMoved?.(fromPos, dropInfo.insertPos, node)
230
+ }
231
+ }
232
+ } catch (e) {
233
+ console.error("Drop failed:", e)
234
+ }
235
+
236
+ extension.cleanup(view)
237
+ return true
238
+ },
239
+
240
+ dragend(view) {
241
+ extension.cleanup(view)
242
+ return false
243
+ }
244
+ }
245
+ },
246
+
247
+ view() {
248
+ return {
249
+ destroy() {
250
+ // Clean up any lingering indicators
251
+ document.querySelectorAll(`.${options.dropIndicatorClass}`).forEach(el => el.remove())
252
+ }
253
+ }
254
+ }
255
+ })
256
+ ]
257
+ },
258
+
259
+ findDropPosition(view, coords) {
260
+ const { state } = view
261
+ const { doc } = state
262
+
263
+ // Get position from coordinates
264
+ const posInfo = view.posAtCoords(coords)
265
+ if (!posInfo) return null
266
+
267
+ // Resolve to find the block boundary
268
+ const $pos = doc.resolve(posInfo.pos)
269
+
270
+ // Find the top-level block
271
+ let blockPos = null
272
+ let insertBefore = true
273
+
274
+ // Walk up to find the top-level block
275
+ for (let d = $pos.depth; d > 0; d--) {
276
+ const node = $pos.node(d)
277
+ const parent = $pos.node(d - 1)
278
+
279
+ // Check if this is a direct child of the document
280
+ if (parent.type.name === "doc") {
281
+ blockPos = $pos.before(d)
282
+ break
283
+ }
284
+ }
285
+
286
+ // If no block found, use document structure
287
+ if (blockPos === null) {
288
+ // Find the nearest block by iterating through document
289
+ let closestPos = 0
290
+ let closestDistance = Infinity
291
+
292
+ doc.forEach((node, pos) => {
293
+ const nodeCoords = view.coordsAtPos(pos)
294
+ const distance = Math.abs(nodeCoords.top - coords.top)
295
+
296
+ if (distance < closestDistance) {
297
+ closestDistance = distance
298
+ closestPos = pos
299
+ }
300
+ })
301
+
302
+ blockPos = closestPos
303
+ }
304
+
305
+ // Determine if we should insert before or after this block
306
+ const blockCoords = view.coordsAtPos(blockPos)
307
+ const node = doc.nodeAt(blockPos)
308
+ const nodeHeight = node ? view.coordsAtPos(blockPos + node.nodeSize).top - blockCoords.top : 20
309
+ const midpoint = blockCoords.top + nodeHeight / 2
310
+
311
+ insertBefore = coords.top < midpoint
312
+
313
+ // Calculate the actual insertion position
314
+ let insertPos
315
+ if (insertBefore) {
316
+ insertPos = blockPos
317
+ } else {
318
+ insertPos = blockPos + (node?.nodeSize || 0)
319
+ }
320
+
321
+ return {
322
+ pos: blockPos,
323
+ insertPos,
324
+ insertBefore,
325
+ coords: insertBefore ? blockCoords : { ...blockCoords, top: blockCoords.top + nodeHeight }
326
+ }
327
+ },
328
+
329
+ showDropIndicator(view, dropInfo) {
330
+ const { dropIndicatorClass } = this.options
331
+
332
+ // Remove existing indicator
333
+ this.hideDropIndicator(view)
334
+
335
+ // Create indicator element
336
+ const indicator = document.createElement("div")
337
+ indicator.className = dropIndicatorClass
338
+
339
+ // Position the indicator
340
+ const editorRect = view.dom.getBoundingClientRect()
341
+ const wrapperRect = view.dom.parentElement?.getBoundingClientRect() || editorRect
342
+
343
+ indicator.style.position = "absolute"
344
+ indicator.style.left = "0"
345
+ indicator.style.right = "0"
346
+ indicator.style.top = `${dropInfo.coords.top - wrapperRect.top}px`
347
+
348
+ // Append to editor wrapper
349
+ const wrapper = view.dom.parentElement
350
+ if (wrapper) {
351
+ wrapper.appendChild(indicator)
352
+ }
353
+
354
+ // Store reference for cleanup
355
+ this.dropIndicator = indicator
356
+ },
357
+
358
+ hideDropIndicator() {
359
+ if (this.dropIndicator) {
360
+ this.dropIndicator.remove()
361
+ this.dropIndicator = null
362
+ }
363
+ },
364
+
365
+ handleEdgeScroll(view, clientY) {
366
+ const { scrollThreshold, scrollSpeed } = this.options
367
+ const viewportHeight = window.innerHeight
368
+
369
+ // Cancel any existing scroll animation
370
+ if (this.scrollAnimationId) {
371
+ cancelAnimationFrame(this.scrollAnimationId)
372
+ }
373
+
374
+ // Check if near top or bottom edge
375
+ if (clientY < scrollThreshold) {
376
+ // Scroll up
377
+ const intensity = 1 - clientY / scrollThreshold
378
+ this.animateScroll(-scrollSpeed * intensity)
379
+ } else if (clientY > viewportHeight - scrollThreshold) {
380
+ // Scroll down
381
+ const intensity = 1 - (viewportHeight - clientY) / scrollThreshold
382
+ this.animateScroll(scrollSpeed * intensity)
383
+ }
384
+ },
385
+
386
+ animateScroll(delta) {
387
+ window.scrollBy(0, delta)
388
+
389
+ // Continue scrolling while dragging
390
+ this.scrollAnimationId = requestAnimationFrame(() => {
391
+ // This will be cancelled when drag ends or mouse moves away from edge
392
+ })
393
+ },
394
+
395
+ cleanup(view) {
396
+ this.hideDropIndicator(view)
397
+
398
+ if (this.scrollAnimationId) {
399
+ cancelAnimationFrame(this.scrollAnimationId)
400
+ this.scrollAnimationId = null
401
+ }
402
+
403
+ view.dom.classList.remove("is-block-dragging")
404
+ }
405
+ })
406
+
407
+ export default DragHandle