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,640 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core"
2
+ import Table from "@tiptap/extension-table"
3
+ import TableRow from "@tiptap/extension-table-row"
4
+ import TableCell from "@tiptap/extension-table-cell"
5
+ import TableHeader from "@tiptap/extension-table-header"
6
+
7
+ /**
8
+ * Advanced Table Extension for TipTap
9
+ *
10
+ * Extends TipTap's table with professional features:
11
+ * - Column alignment (left, center, right)
12
+ * - Table caption/title
13
+ * - Striped rows option
14
+ * - Border style variants (default, striped, borderless, minimal)
15
+ * - Cell background colors
16
+ * - Table toolbar on selection
17
+ * - Sticky header option
18
+ *
19
+ * @example
20
+ * editor.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
21
+ * editor.commands.setTableVariant('striped')
22
+ * editor.commands.setCellAlignment('center')
23
+ *
24
+ * @since 0.6.0
25
+ */
26
+
27
+ // Cell background color options
28
+ const CELL_BACKGROUNDS = {
29
+ gray: { label: "Gray", color: "var(--inkpen-table-bg-gray)" },
30
+ red: { label: "Red", color: "var(--inkpen-table-bg-red)" },
31
+ orange: { label: "Orange", color: "var(--inkpen-table-bg-orange)" },
32
+ yellow: { label: "Yellow", color: "var(--inkpen-table-bg-yellow)" },
33
+ green: { label: "Green", color: "var(--inkpen-table-bg-green)" },
34
+ blue: { label: "Blue", color: "var(--inkpen-table-bg-blue)" },
35
+ purple: { label: "Purple", color: "var(--inkpen-table-bg-purple)" }
36
+ }
37
+
38
+ // Table variant styles
39
+ const TABLE_VARIANTS = {
40
+ default: { label: "Default", description: "Standard bordered table" },
41
+ striped: { label: "Striped", description: "Alternating row colors" },
42
+ borderless: { label: "Borderless", description: "No vertical borders" },
43
+ minimal: { label: "Minimal", description: "Clean, no borders" }
44
+ }
45
+
46
+ // Alignment options
47
+ const ALIGNMENTS = {
48
+ left: { label: "Left", icon: "⬅" },
49
+ center: { label: "Center", icon: "⬌" },
50
+ right: { label: "Right", icon: "➡" }
51
+ }
52
+
53
+ /**
54
+ * Extended TableCell with alignment and background attributes
55
+ */
56
+ export const AdvancedTableCell = TableCell.extend({
57
+ addAttributes() {
58
+ return {
59
+ ...this.parent?.(),
60
+ align: {
61
+ default: null,
62
+ parseHTML: element => element.getAttribute("data-align") || element.style.textAlign || null,
63
+ renderHTML: attributes => {
64
+ if (!attributes.align) return {}
65
+ return {
66
+ "data-align": attributes.align,
67
+ style: `text-align: ${attributes.align}`
68
+ }
69
+ }
70
+ },
71
+ background: {
72
+ default: null,
73
+ parseHTML: element => element.getAttribute("data-background"),
74
+ renderHTML: attributes => {
75
+ if (!attributes.background) return {}
76
+ return { "data-background": attributes.background }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ })
82
+
83
+ /**
84
+ * Extended TableHeader with alignment and background attributes
85
+ */
86
+ export const AdvancedTableHeader = TableHeader.extend({
87
+ addAttributes() {
88
+ return {
89
+ ...this.parent?.(),
90
+ align: {
91
+ default: null,
92
+ parseHTML: element => element.getAttribute("data-align") || element.style.textAlign || null,
93
+ renderHTML: attributes => {
94
+ if (!attributes.align) return {}
95
+ return {
96
+ "data-align": attributes.align,
97
+ style: `text-align: ${attributes.align}`
98
+ }
99
+ }
100
+ },
101
+ background: {
102
+ default: null,
103
+ parseHTML: element => element.getAttribute("data-background"),
104
+ renderHTML: attributes => {
105
+ if (!attributes.background) return {}
106
+ return { "data-background": attributes.background }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ })
112
+
113
+ /**
114
+ * Extended TableRow (unchanged, included for completeness)
115
+ */
116
+ export const AdvancedTableRow = TableRow.extend({
117
+ // No changes needed, but exported for consistency
118
+ })
119
+
120
+ /**
121
+ * Advanced Table Extension
122
+ */
123
+ export const AdvancedTable = Table.extend({
124
+ addOptions() {
125
+ return {
126
+ ...this.parent?.(),
127
+ resizable: true,
128
+ showControls: true,
129
+ defaultVariant: "default",
130
+ variants: TABLE_VARIANTS,
131
+ cellBackgrounds: CELL_BACKGROUNDS,
132
+ alignments: ALIGNMENTS,
133
+ HTMLAttributes: {
134
+ class: "inkpen-table"
135
+ }
136
+ }
137
+ },
138
+
139
+ addAttributes() {
140
+ return {
141
+ ...this.parent?.(),
142
+ caption: {
143
+ default: null,
144
+ parseHTML: element => {
145
+ const caption = element.querySelector("caption")
146
+ return caption ? caption.textContent : null
147
+ },
148
+ renderHTML: () => ({}) // Caption rendered separately in NodeView
149
+ },
150
+ variant: {
151
+ default: "default",
152
+ parseHTML: element => element.getAttribute("data-variant") || "default",
153
+ renderHTML: attributes => ({ "data-variant": attributes.variant })
154
+ },
155
+ stickyHeader: {
156
+ default: false,
157
+ parseHTML: element => element.hasAttribute("data-sticky-header"),
158
+ renderHTML: attributes => attributes.stickyHeader ? { "data-sticky-header": "" } : {}
159
+ }
160
+ }
161
+ },
162
+
163
+ addNodeView() {
164
+ return ({ node, editor, getPos }) => {
165
+ const { caption, variant, stickyHeader } = node.attrs
166
+
167
+ // Wrapper container
168
+ const wrapper = document.createElement("div")
169
+ wrapper.className = "inkpen-table-wrapper"
170
+ if (stickyHeader) wrapper.classList.add("inkpen-table-wrapper--sticky-header")
171
+
172
+ // Caption (if present)
173
+ let captionEl = null
174
+ if (caption || editor.isEditable) {
175
+ captionEl = document.createElement("div")
176
+ captionEl.className = "inkpen-table__caption"
177
+ captionEl.contentEditable = editor.isEditable ? "true" : "false"
178
+ captionEl.textContent = caption || ""
179
+ captionEl.setAttribute("placeholder", "Add table caption...")
180
+
181
+ if (editor.isEditable) {
182
+ captionEl.addEventListener("input", () => {
183
+ if (typeof getPos === "function") {
184
+ const pos = getPos()
185
+ if (pos !== undefined) {
186
+ editor.chain().command(({ tr }) => {
187
+ tr.setNodeMarkup(pos, undefined, {
188
+ ...node.attrs,
189
+ caption: captionEl.textContent || null
190
+ })
191
+ return true
192
+ }).run()
193
+ }
194
+ }
195
+ })
196
+
197
+ captionEl.addEventListener("keydown", (e) => {
198
+ if (e.key === "Enter") {
199
+ e.preventDefault()
200
+ captionEl.blur()
201
+ }
202
+ })
203
+ }
204
+
205
+ wrapper.appendChild(captionEl)
206
+ }
207
+
208
+ // Table controls toolbar (shown on hover/focus)
209
+ if (this.options.showControls && editor.isEditable) {
210
+ const controls = this.createTableControls(node, editor, getPos)
211
+ wrapper.appendChild(controls)
212
+ }
213
+
214
+ // Table container (contentDOM for ProseMirror)
215
+ const tableContainer = document.createElement("div")
216
+ tableContainer.className = `inkpen-table__container inkpen-table--${variant}`
217
+ wrapper.appendChild(tableContainer)
218
+
219
+ return {
220
+ dom: wrapper,
221
+ contentDOM: tableContainer,
222
+ update: (updatedNode) => {
223
+ if (updatedNode.type.name !== "table") return false
224
+
225
+ const newVariant = updatedNode.attrs.variant || "default"
226
+ const newCaption = updatedNode.attrs.caption
227
+ const newSticky = updatedNode.attrs.stickyHeader
228
+
229
+ // Update variant class
230
+ tableContainer.className = `inkpen-table__container inkpen-table--${newVariant}`
231
+
232
+ // Update sticky class
233
+ wrapper.classList.toggle("inkpen-table-wrapper--sticky-header", newSticky)
234
+
235
+ // Update caption
236
+ if (captionEl && captionEl.textContent !== newCaption) {
237
+ captionEl.textContent = newCaption || ""
238
+ }
239
+
240
+ return true
241
+ }
242
+ }
243
+ }
244
+ },
245
+
246
+ addCommands() {
247
+ return {
248
+ ...this.parent?.(),
249
+
250
+ setTableCaption: (caption) => ({ tr, state, dispatch }) => {
251
+ const { selection } = state
252
+ const tableNode = findParentTable(selection)
253
+
254
+ if (!tableNode) return false
255
+
256
+ if (dispatch) {
257
+ tr.setNodeMarkup(tableNode.pos, undefined, {
258
+ ...tableNode.node.attrs,
259
+ caption
260
+ })
261
+ }
262
+
263
+ return true
264
+ },
265
+
266
+ setTableVariant: (variant) => ({ tr, state, dispatch }) => {
267
+ const { selection } = state
268
+ const tableNode = findParentTable(selection)
269
+
270
+ if (!tableNode) return false
271
+
272
+ if (dispatch) {
273
+ tr.setNodeMarkup(tableNode.pos, undefined, {
274
+ ...tableNode.node.attrs,
275
+ variant
276
+ })
277
+ }
278
+
279
+ return true
280
+ },
281
+
282
+ toggleStickyHeader: () => ({ tr, state, dispatch }) => {
283
+ const { selection } = state
284
+ const tableNode = findParentTable(selection)
285
+
286
+ if (!tableNode) return false
287
+
288
+ if (dispatch) {
289
+ tr.setNodeMarkup(tableNode.pos, undefined, {
290
+ ...tableNode.node.attrs,
291
+ stickyHeader: !tableNode.node.attrs.stickyHeader
292
+ })
293
+ }
294
+
295
+ return true
296
+ },
297
+
298
+ setCellAlignment: (align) => ({ tr, state, dispatch }) => {
299
+ const { selection } = state
300
+ const cellPositions = getSelectedCellPositions(state)
301
+
302
+ if (cellPositions.length === 0) return false
303
+
304
+ if (dispatch) {
305
+ cellPositions.forEach(({ pos, node }) => {
306
+ tr.setNodeMarkup(pos, undefined, {
307
+ ...node.attrs,
308
+ align
309
+ })
310
+ })
311
+ }
312
+
313
+ return true
314
+ },
315
+
316
+ setCellBackground: (background) => ({ tr, state, dispatch }) => {
317
+ const { selection } = state
318
+ const cellPositions = getSelectedCellPositions(state)
319
+
320
+ if (cellPositions.length === 0) return false
321
+
322
+ if (dispatch) {
323
+ cellPositions.forEach(({ pos, node }) => {
324
+ tr.setNodeMarkup(pos, undefined, {
325
+ ...node.attrs,
326
+ background
327
+ })
328
+ })
329
+ }
330
+
331
+ return true
332
+ },
333
+
334
+ clearCellBackground: () => ({ commands }) => {
335
+ return commands.setCellBackground(null)
336
+ }
337
+ }
338
+ },
339
+
340
+ addKeyboardShortcuts() {
341
+ return {
342
+ ...this.parent?.(),
343
+ "Mod-Shift-l": () => this.editor.commands.setCellAlignment("left"),
344
+ "Mod-Shift-e": () => this.editor.commands.setCellAlignment("center"),
345
+ "Mod-Shift-r": () => this.editor.commands.setCellAlignment("right")
346
+ }
347
+ },
348
+
349
+ // Private: Create table controls toolbar
350
+
351
+ createTableControls(node, editor, getPos) {
352
+ const controls = document.createElement("div")
353
+ controls.className = "inkpen-table__controls"
354
+ controls.contentEditable = "false"
355
+
356
+ // Alignment buttons
357
+ const alignGroup = document.createElement("div")
358
+ alignGroup.className = "inkpen-table__control-group"
359
+
360
+ Object.entries(ALIGNMENTS).forEach(([align, { label, icon }]) => {
361
+ const btn = document.createElement("button")
362
+ btn.type = "button"
363
+ btn.className = "inkpen-table__control-btn"
364
+ btn.title = `Align ${label}`
365
+ btn.textContent = icon
366
+
367
+ btn.addEventListener("mousedown", (e) => e.preventDefault())
368
+ btn.addEventListener("click", () => {
369
+ editor.chain().focus().setCellAlignment(align).run()
370
+ })
371
+
372
+ alignGroup.appendChild(btn)
373
+ })
374
+
375
+ controls.appendChild(alignGroup)
376
+
377
+ // Divider
378
+ const divider1 = document.createElement("span")
379
+ divider1.className = "inkpen-table__control-divider"
380
+ controls.appendChild(divider1)
381
+
382
+ // Variant dropdown
383
+ const variantBtn = document.createElement("button")
384
+ variantBtn.type = "button"
385
+ variantBtn.className = "inkpen-table__control-btn"
386
+ variantBtn.title = "Table Style"
387
+ variantBtn.textContent = "≡"
388
+
389
+ variantBtn.addEventListener("mousedown", (e) => e.preventDefault())
390
+ variantBtn.addEventListener("click", () => {
391
+ this.showVariantDropdown(variantBtn, node, editor, getPos)
392
+ })
393
+
394
+ controls.appendChild(variantBtn)
395
+
396
+ // Cell color button
397
+ const colorBtn = document.createElement("button")
398
+ colorBtn.type = "button"
399
+ colorBtn.className = "inkpen-table__control-btn"
400
+ colorBtn.title = "Cell Color"
401
+ colorBtn.textContent = "🎨"
402
+
403
+ colorBtn.addEventListener("mousedown", (e) => e.preventDefault())
404
+ colorBtn.addEventListener("click", () => {
405
+ this.showColorDropdown(colorBtn, editor)
406
+ })
407
+
408
+ controls.appendChild(colorBtn)
409
+
410
+ // Divider
411
+ const divider2 = document.createElement("span")
412
+ divider2.className = "inkpen-table__control-divider"
413
+ controls.appendChild(divider2)
414
+
415
+ // Add row button
416
+ const addRowBtn = document.createElement("button")
417
+ addRowBtn.type = "button"
418
+ addRowBtn.className = "inkpen-table__control-btn"
419
+ addRowBtn.title = "Add Row Below"
420
+ addRowBtn.textContent = "+↓"
421
+
422
+ addRowBtn.addEventListener("mousedown", (e) => e.preventDefault())
423
+ addRowBtn.addEventListener("click", () => {
424
+ editor.chain().focus().addRowAfter().run()
425
+ })
426
+
427
+ controls.appendChild(addRowBtn)
428
+
429
+ // Add column button
430
+ const addColBtn = document.createElement("button")
431
+ addColBtn.type = "button"
432
+ addColBtn.className = "inkpen-table__control-btn"
433
+ addColBtn.title = "Add Column Right"
434
+ addColBtn.textContent = "+→"
435
+
436
+ addColBtn.addEventListener("mousedown", (e) => e.preventDefault())
437
+ addColBtn.addEventListener("click", () => {
438
+ editor.chain().focus().addColumnAfter().run()
439
+ })
440
+
441
+ controls.appendChild(addColBtn)
442
+
443
+ return controls
444
+ },
445
+
446
+ // Private: Show variant selection dropdown
447
+
448
+ showVariantDropdown(anchor, node, editor, getPos) {
449
+ removeExistingDropdown()
450
+
451
+ const dropdown = document.createElement("div")
452
+ dropdown.className = "inkpen-table__dropdown"
453
+
454
+ Object.entries(TABLE_VARIANTS).forEach(([variant, { label, description }]) => {
455
+ const item = document.createElement("button")
456
+ item.type = "button"
457
+ item.className = "inkpen-table__dropdown-item"
458
+ if (node.attrs.variant === variant) {
459
+ item.classList.add("is-active")
460
+ }
461
+
462
+ item.innerHTML = `
463
+ <span class="inkpen-table__dropdown-label">${label}</span>
464
+ <span class="inkpen-table__dropdown-desc">${description}</span>
465
+ `
466
+
467
+ item.addEventListener("mousedown", (e) => e.preventDefault())
468
+ item.addEventListener("click", () => {
469
+ editor.chain().focus().setTableVariant(variant).run()
470
+ dropdown.remove()
471
+ })
472
+
473
+ dropdown.appendChild(item)
474
+ })
475
+
476
+ // Sticky header toggle
477
+ const stickyItem = document.createElement("button")
478
+ stickyItem.type = "button"
479
+ stickyItem.className = "inkpen-table__dropdown-item"
480
+ if (node.attrs.stickyHeader) {
481
+ stickyItem.classList.add("is-active")
482
+ }
483
+
484
+ stickyItem.innerHTML = `
485
+ <span class="inkpen-table__dropdown-label">Sticky Header</span>
486
+ <span class="inkpen-table__dropdown-desc">Header stays visible on scroll</span>
487
+ `
488
+
489
+ stickyItem.addEventListener("mousedown", (e) => e.preventDefault())
490
+ stickyItem.addEventListener("click", () => {
491
+ editor.chain().focus().toggleStickyHeader().run()
492
+ dropdown.remove()
493
+ })
494
+
495
+ dropdown.appendChild(stickyItem)
496
+
497
+ positionDropdown(dropdown, anchor)
498
+ setupDropdownClose(dropdown, anchor)
499
+ },
500
+
501
+ // Private: Show color selection dropdown
502
+
503
+ showColorDropdown(anchor, editor) {
504
+ removeExistingDropdown()
505
+
506
+ const dropdown = document.createElement("div")
507
+ dropdown.className = "inkpen-table__dropdown inkpen-table__dropdown--colors"
508
+
509
+ // No color option
510
+ const clearItem = document.createElement("button")
511
+ clearItem.type = "button"
512
+ clearItem.className = "inkpen-table__color-btn inkpen-table__color-btn--clear"
513
+ clearItem.title = "No Color"
514
+ clearItem.innerHTML = "∅"
515
+
516
+ clearItem.addEventListener("mousedown", (e) => e.preventDefault())
517
+ clearItem.addEventListener("click", () => {
518
+ editor.chain().focus().clearCellBackground().run()
519
+ dropdown.remove()
520
+ })
521
+
522
+ dropdown.appendChild(clearItem)
523
+
524
+ // Color options
525
+ Object.entries(CELL_BACKGROUNDS).forEach(([key, { label }]) => {
526
+ const colorBtn = document.createElement("button")
527
+ colorBtn.type = "button"
528
+ colorBtn.className = `inkpen-table__color-btn inkpen-table__color-btn--${key}`
529
+ colorBtn.title = label
530
+ colorBtn.setAttribute("data-color", key)
531
+
532
+ colorBtn.addEventListener("mousedown", (e) => e.preventDefault())
533
+ colorBtn.addEventListener("click", () => {
534
+ editor.chain().focus().setCellBackground(key).run()
535
+ dropdown.remove()
536
+ })
537
+
538
+ dropdown.appendChild(colorBtn)
539
+ })
540
+
541
+ positionDropdown(dropdown, anchor)
542
+ setupDropdownClose(dropdown, anchor)
543
+ }
544
+ })
545
+
546
+ // Helper: Find parent table node
547
+
548
+ function findParentTable(selection) {
549
+ const { $from } = selection
550
+ for (let d = $from.depth; d > 0; d--) {
551
+ const node = $from.node(d)
552
+ if (node.type.name === "table") {
553
+ return { node, pos: $from.before(d) }
554
+ }
555
+ }
556
+ return null
557
+ }
558
+
559
+ // Helper: Get all selected cell positions
560
+
561
+ function getSelectedCellPositions(state) {
562
+ const cells = []
563
+ const { selection, doc } = state
564
+
565
+ // Check for CellSelection (multiple cells selected)
566
+ if (selection.$anchorCell && selection.$headCell) {
567
+ const cellSelection = selection
568
+ const table = cellSelection.$anchorCell.node(-1)
569
+ const map = {}
570
+
571
+ doc.nodesBetween(
572
+ cellSelection.$anchorCell.start(-1),
573
+ cellSelection.$headCell.start(-1) + cellSelection.$headCell.parent.nodeSize,
574
+ (node, pos) => {
575
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
576
+ if (!map[pos]) {
577
+ map[pos] = true
578
+ cells.push({ node, pos })
579
+ }
580
+ }
581
+ }
582
+ )
583
+ } else {
584
+ // Single cell - find the cell we're in
585
+ const { $from } = selection
586
+ for (let d = $from.depth; d > 0; d--) {
587
+ const node = $from.node(d)
588
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
589
+ cells.push({ node, pos: $from.before(d) })
590
+ break
591
+ }
592
+ }
593
+ }
594
+
595
+ return cells
596
+ }
597
+
598
+ // Helper: Remove existing dropdown
599
+
600
+ function removeExistingDropdown() {
601
+ const existing = document.querySelector(".inkpen-table__dropdown")
602
+ if (existing) existing.remove()
603
+ }
604
+
605
+ // Helper: Position dropdown below anchor
606
+
607
+ function positionDropdown(dropdown, anchor) {
608
+ const rect = anchor.getBoundingClientRect()
609
+ dropdown.style.position = "fixed"
610
+ dropdown.style.left = `${rect.left}px`
611
+ dropdown.style.top = `${rect.bottom + 4}px`
612
+ dropdown.style.zIndex = "10000"
613
+ document.body.appendChild(dropdown)
614
+ }
615
+
616
+ // Helper: Setup dropdown close handlers
617
+
618
+ function setupDropdownClose(dropdown, anchor) {
619
+ const closeHandler = (e) => {
620
+ if (!dropdown.contains(e.target) && !anchor.contains(e.target)) {
621
+ dropdown.remove()
622
+ document.removeEventListener("mousedown", closeHandler)
623
+ }
624
+ }
625
+
626
+ setTimeout(() => {
627
+ document.addEventListener("mousedown", closeHandler)
628
+ }, 0)
629
+
630
+ const escHandler = (e) => {
631
+ if (e.key === "Escape") {
632
+ dropdown.remove()
633
+ document.removeEventListener("keydown", escHandler)
634
+ }
635
+ }
636
+ document.addEventListener("keydown", escHandler)
637
+ }
638
+
639
+ export { CELL_BACKGROUNDS, TABLE_VARIANTS, ALIGNMENTS }
640
+ export default AdvancedTable