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,100 @@
1
+ /**
2
+ * InkpenTableCell Extension
3
+ *
4
+ * Enhanced table cell with alignment, background color, and text color support.
5
+ *
6
+ * @since 0.8.0
7
+ */
8
+
9
+ import TableCell from "@tiptap/extension-table-cell"
10
+
11
+ export const InkpenTableCell = TableCell.extend({
12
+ name: "tableCell",
13
+
14
+ addAttributes() {
15
+ return {
16
+ ...this.parent?.(),
17
+
18
+ // Text alignment
19
+ align: {
20
+ default: null,
21
+ parseHTML: element => {
22
+ return element.getAttribute("data-align") ||
23
+ element.style.textAlign ||
24
+ null
25
+ },
26
+ renderHTML: attributes => {
27
+ if (!attributes.align) return {}
28
+ return {
29
+ "data-align": attributes.align,
30
+ style: `text-align: ${attributes.align}`
31
+ }
32
+ }
33
+ },
34
+
35
+ // Background color
36
+ background: {
37
+ default: null,
38
+ parseHTML: element => element.getAttribute("data-background"),
39
+ renderHTML: attributes => {
40
+ if (!attributes.background) return {}
41
+ return { "data-background": attributes.background }
42
+ }
43
+ },
44
+
45
+ // Text color (NEW)
46
+ textColor: {
47
+ default: null,
48
+ parseHTML: element => element.getAttribute("data-text-color"),
49
+ renderHTML: attributes => {
50
+ if (!attributes.textColor) return {}
51
+ return { "data-text-color": attributes.textColor }
52
+ }
53
+ },
54
+
55
+ // Colspan
56
+ colspan: {
57
+ default: 1,
58
+ parseHTML: element => {
59
+ const colspan = element.getAttribute("colspan")
60
+ return colspan ? parseInt(colspan, 10) : 1
61
+ },
62
+ renderHTML: attributes => {
63
+ if (attributes.colspan === 1) return {}
64
+ return { colspan: attributes.colspan }
65
+ }
66
+ },
67
+
68
+ // Rowspan
69
+ rowspan: {
70
+ default: 1,
71
+ parseHTML: element => {
72
+ const rowspan = element.getAttribute("rowspan")
73
+ return rowspan ? parseInt(rowspan, 10) : 1
74
+ },
75
+ renderHTML: attributes => {
76
+ if (attributes.rowspan === 1) return {}
77
+ return { rowspan: attributes.rowspan }
78
+ }
79
+ },
80
+
81
+ // Column width for resizing
82
+ colwidth: {
83
+ default: null,
84
+ parseHTML: element => {
85
+ const colwidth = element.getAttribute("colwidth")
86
+ return colwidth ? colwidth.split(",").map(Number) : null
87
+ },
88
+ renderHTML: attributes => {
89
+ if (!attributes.colwidth) return {}
90
+ return {
91
+ colwidth: attributes.colwidth.join(","),
92
+ style: `width: ${attributes.colwidth[0]}px`
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ })
99
+
100
+ export default InkpenTableCell
@@ -0,0 +1,100 @@
1
+ /**
2
+ * InkpenTableHeader Extension
3
+ *
4
+ * Enhanced table header cell with alignment, background color, and text color support.
5
+ *
6
+ * @since 0.8.0
7
+ */
8
+
9
+ import TableHeader from "@tiptap/extension-table-header"
10
+
11
+ export const InkpenTableHeader = TableHeader.extend({
12
+ name: "tableHeader",
13
+
14
+ addAttributes() {
15
+ return {
16
+ ...this.parent?.(),
17
+
18
+ // Text alignment
19
+ align: {
20
+ default: null,
21
+ parseHTML: element => {
22
+ return element.getAttribute("data-align") ||
23
+ element.style.textAlign ||
24
+ null
25
+ },
26
+ renderHTML: attributes => {
27
+ if (!attributes.align) return {}
28
+ return {
29
+ "data-align": attributes.align,
30
+ style: `text-align: ${attributes.align}`
31
+ }
32
+ }
33
+ },
34
+
35
+ // Background color
36
+ background: {
37
+ default: null,
38
+ parseHTML: element => element.getAttribute("data-background"),
39
+ renderHTML: attributes => {
40
+ if (!attributes.background) return {}
41
+ return { "data-background": attributes.background }
42
+ }
43
+ },
44
+
45
+ // Text color (NEW)
46
+ textColor: {
47
+ default: null,
48
+ parseHTML: element => element.getAttribute("data-text-color"),
49
+ renderHTML: attributes => {
50
+ if (!attributes.textColor) return {}
51
+ return { "data-text-color": attributes.textColor }
52
+ }
53
+ },
54
+
55
+ // Colspan
56
+ colspan: {
57
+ default: 1,
58
+ parseHTML: element => {
59
+ const colspan = element.getAttribute("colspan")
60
+ return colspan ? parseInt(colspan, 10) : 1
61
+ },
62
+ renderHTML: attributes => {
63
+ if (attributes.colspan === 1) return {}
64
+ return { colspan: attributes.colspan }
65
+ }
66
+ },
67
+
68
+ // Rowspan
69
+ rowspan: {
70
+ default: 1,
71
+ parseHTML: element => {
72
+ const rowspan = element.getAttribute("rowspan")
73
+ return rowspan ? parseInt(rowspan, 10) : 1
74
+ },
75
+ renderHTML: attributes => {
76
+ if (attributes.rowspan === 1) return {}
77
+ return { rowspan: attributes.rowspan }
78
+ }
79
+ },
80
+
81
+ // Column width for resizing
82
+ colwidth: {
83
+ default: null,
84
+ parseHTML: element => {
85
+ const colwidth = element.getAttribute("colwidth")
86
+ return colwidth ? colwidth.split(",").map(Number) : null
87
+ },
88
+ renderHTML: attributes => {
89
+ if (!attributes.colwidth) return {}
90
+ return {
91
+ colwidth: attributes.colwidth.join(","),
92
+ style: `width: ${attributes.colwidth[0]}px`
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ })
99
+
100
+ export default InkpenTableHeader
@@ -0,0 +1,152 @@
1
+ /**
2
+ * InkpenTable Constants
3
+ *
4
+ * Colors, menu items, and configuration for the enhanced table extension.
5
+ *
6
+ * @since 0.8.0
7
+ */
8
+
9
+ // =============================================================================
10
+ // Text Colors
11
+ // =============================================================================
12
+
13
+ export const TEXT_COLORS = [
14
+ { name: "default", label: "Default", value: null },
15
+ { name: "gray", label: "Gray", value: "#787774" },
16
+ { name: "red", label: "Red", value: "#d44c47" },
17
+ { name: "orange", label: "Orange", value: "#d9730d" },
18
+ { name: "yellow", label: "Yellow", value: "#cb912f" },
19
+ { name: "green", label: "Green", value: "#448361" },
20
+ { name: "blue", label: "Blue", value: "#337ea9" },
21
+ { name: "purple", label: "Purple", value: "#9065b0" },
22
+ { name: "pink", label: "Pink", value: "#c14c8a" }
23
+ ]
24
+
25
+ // =============================================================================
26
+ // Background Colors
27
+ // =============================================================================
28
+
29
+ export const BACKGROUND_COLORS = [
30
+ { name: "default", label: "Default", value: null },
31
+ { name: "gray", label: "Gray", value: "rgba(120, 120, 120, 0.12)" },
32
+ { name: "red", label: "Red", value: "rgba(239, 68, 68, 0.15)" },
33
+ { name: "orange", label: "Orange", value: "rgba(249, 115, 22, 0.15)" },
34
+ { name: "yellow", label: "Yellow", value: "rgba(234, 179, 8, 0.15)" },
35
+ { name: "green", label: "Green", value: "rgba(34, 197, 94, 0.15)" },
36
+ { name: "blue", label: "Blue", value: "rgba(59, 130, 246, 0.15)" },
37
+ { name: "purple", label: "Purple", value: "rgba(168, 85, 247, 0.15)" },
38
+ { name: "pink", label: "Pink", value: "rgba(236, 72, 153, 0.15)" }
39
+ ]
40
+
41
+ // =============================================================================
42
+ // Table Variants
43
+ // =============================================================================
44
+
45
+ export const TABLE_VARIANTS = [
46
+ { name: "default", label: "Default" },
47
+ { name: "striped", label: "Striped" },
48
+ { name: "borderless", label: "Borderless" },
49
+ { name: "minimal", label: "Minimal" }
50
+ ]
51
+
52
+ // =============================================================================
53
+ // Row Menu Items
54
+ // =============================================================================
55
+
56
+ export const ROW_MENU_ITEMS = [
57
+ { type: "action", id: "addRowAbove", label: "Add row above", icon: "↑" },
58
+ { type: "action", id: "addRowBelow", label: "Add row below", icon: "↓" },
59
+ { type: "separator" },
60
+ { type: "action", id: "duplicateRow", label: "Duplicate row", icon: "⧉" },
61
+ { type: "action", id: "deleteRow", label: "Delete row", icon: "✕", danger: true },
62
+ { type: "separator" },
63
+ { type: "action", id: "moveRowUp", label: "Move up", icon: "↑" },
64
+ { type: "action", id: "moveRowDown", label: "Move down", icon: "↓" },
65
+ { type: "separator" },
66
+ { type: "action", id: "toggleHeaderRow", label: "Toggle header", icon: "H" },
67
+ { type: "separator" },
68
+ { type: "submenu", id: "textColor", label: "Text color", icon: "A" },
69
+ { type: "submenu", id: "backgroundColor", label: "Background", icon: "◼" }
70
+ ]
71
+
72
+ // =============================================================================
73
+ // Column Menu Items
74
+ // =============================================================================
75
+
76
+ export const COLUMN_MENU_ITEMS = [
77
+ { type: "action", id: "addColumnLeft", label: "Add column left", icon: "←" },
78
+ { type: "action", id: "addColumnRight", label: "Add column right", icon: "→" },
79
+ { type: "separator" },
80
+ { type: "action", id: "duplicateColumn", label: "Duplicate column", icon: "⧉" },
81
+ { type: "action", id: "deleteColumn", label: "Delete column", icon: "✕", danger: true },
82
+ { type: "separator" },
83
+ { type: "action", id: "moveColumnLeft", label: "Move left", icon: "←" },
84
+ { type: "action", id: "moveColumnRight", label: "Move right", icon: "→" },
85
+ { type: "separator" },
86
+ { type: "submenu", id: "alignment", label: "Alignment", icon: "≡" },
87
+ { type: "separator" },
88
+ { type: "submenu", id: "textColor", label: "Text color", icon: "A" },
89
+ { type: "submenu", id: "backgroundColor", label: "Background", icon: "◼" }
90
+ ]
91
+
92
+ // =============================================================================
93
+ // Alignment Options
94
+ // =============================================================================
95
+
96
+ export const ALIGNMENT_OPTIONS = [
97
+ { name: "left", label: "Left", icon: "←" },
98
+ { name: "center", label: "Center", icon: "↔" },
99
+ { name: "right", label: "Right", icon: "→" }
100
+ ]
101
+
102
+ // =============================================================================
103
+ // Default Configuration
104
+ // =============================================================================
105
+
106
+ export const DEFAULT_CONFIG = {
107
+ resizable: true,
108
+ defaultVariant: "default",
109
+ showHandles: true,
110
+ showAddButtons: true,
111
+ showCaption: true,
112
+ stickyHeader: false,
113
+ cellBackgrounds: BACKGROUND_COLORS,
114
+ textColors: TEXT_COLORS
115
+ }
116
+
117
+ // =============================================================================
118
+ // CSS Classes
119
+ // =============================================================================
120
+
121
+ export const CSS_CLASSES = {
122
+ wrapper: "inkpen-table-wrapper",
123
+ table: "inkpen-table",
124
+ colHandles: "inkpen-table__col-handles",
125
+ rowHandles: "inkpen-table__row-handles",
126
+ handle: "inkpen-table__handle",
127
+ handleActive: "inkpen-table__handle--active",
128
+ addRow: "inkpen-table__add-row",
129
+ addCol: "inkpen-table__add-col",
130
+ body: "inkpen-table__body",
131
+ content: "inkpen-table__content",
132
+ caption: "inkpen-table__caption",
133
+ menu: "inkpen-table-menu",
134
+ menuItem: "inkpen-table-menu__item",
135
+ menuSeparator: "inkpen-table-menu__separator",
136
+ menuSubmenu: "inkpen-table-menu__submenu",
137
+ colorSwatch: "inkpen-table-menu__color-swatch",
138
+ selected: "inkpen-table--selected",
139
+ rowSelected: "inkpen-table__row--selected",
140
+ colSelected: "inkpen-table__col--selected"
141
+ }
142
+
143
+ // =============================================================================
144
+ // Keyboard Shortcuts
145
+ // =============================================================================
146
+
147
+ export const KEYBOARD_SHORTCUTS = {
148
+ moveRowUp: { key: "ArrowUp", modifiers: ["Mod", "Shift"] },
149
+ moveRowDown: { key: "ArrowDown", modifiers: ["Mod", "Shift"] },
150
+ moveColumnLeft: { key: "ArrowLeft", modifiers: ["Mod", "Shift"] },
151
+ moveColumnRight: { key: "ArrowRight", modifiers: ["Mod", "Shift"] }
152
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * InkpenTable Helpers
3
+ *
4
+ * DOM utilities and positioning functions for the enhanced table extension.
5
+ * Follows Fizzy patterns: named function exports, no object exports.
6
+ *
7
+ * @since 0.8.0
8
+ */
9
+
10
+ // =============================================================================
11
+ // DOM Utilities
12
+ // =============================================================================
13
+
14
+ /**
15
+ * Create an element with optional attributes and children
16
+ */
17
+ export function createElement(tag, attrs = {}, children = []) {
18
+ const el = document.createElement(tag)
19
+
20
+ for (const [key, value] of Object.entries(attrs)) {
21
+ if (key === "className") {
22
+ el.className = value
23
+ } else if (key === "dataset") {
24
+ Object.assign(el.dataset, value)
25
+ } else if (key.startsWith("on") && typeof value === "function") {
26
+ el.addEventListener(key.slice(2).toLowerCase(), value)
27
+ } else {
28
+ el.setAttribute(key, value)
29
+ }
30
+ }
31
+
32
+ for (const child of children) {
33
+ if (typeof child === "string") {
34
+ el.appendChild(document.createTextNode(child))
35
+ } else if (child) {
36
+ el.appendChild(child)
37
+ }
38
+ }
39
+
40
+ return el
41
+ }
42
+
43
+ /**
44
+ * Wait for next animation frame
45
+ */
46
+ export function nextFrame() {
47
+ return new Promise(resolve => requestAnimationFrame(resolve))
48
+ }
49
+
50
+ /**
51
+ * Wait for multiple animation frames
52
+ */
53
+ export function waitFrames(count = 2) {
54
+ return new Promise(resolve => {
55
+ let remaining = count
56
+ const tick = () => {
57
+ remaining--
58
+ if (remaining <= 0) {
59
+ resolve()
60
+ } else {
61
+ requestAnimationFrame(tick)
62
+ }
63
+ }
64
+ requestAnimationFrame(tick)
65
+ })
66
+ }
67
+
68
+ // =============================================================================
69
+ // Positioning
70
+ // =============================================================================
71
+
72
+ /**
73
+ * Position an element below a reference element
74
+ */
75
+ export function positionBelow(element, reference, options = {}) {
76
+ const { offsetX = 0, offsetY = 4 } = options
77
+ const refRect = reference.getBoundingClientRect()
78
+ const elRect = element.getBoundingClientRect()
79
+
80
+ let left = refRect.left + offsetX
81
+ let top = refRect.bottom + offsetY
82
+
83
+ // Keep within viewport horizontally
84
+ const viewportWidth = window.innerWidth
85
+ if (left + elRect.width > viewportWidth - 8) {
86
+ left = viewportWidth - elRect.width - 8
87
+ }
88
+ if (left < 8) {
89
+ left = 8
90
+ }
91
+
92
+ // Flip above if not enough space below
93
+ const viewportHeight = window.innerHeight
94
+ if (top + elRect.height > viewportHeight - 8) {
95
+ top = refRect.top - elRect.height - offsetY
96
+ }
97
+
98
+ element.style.position = "fixed"
99
+ element.style.left = `${left}px`
100
+ element.style.top = `${top}px`
101
+ }
102
+
103
+ /**
104
+ * Position an element to the right of a reference element (for submenus)
105
+ */
106
+ export function positionRight(element, reference, options = {}) {
107
+ const { offsetX = 0, offsetY = 0 } = options
108
+ const refRect = reference.getBoundingClientRect()
109
+ const elRect = element.getBoundingClientRect()
110
+
111
+ let left = refRect.right + offsetX
112
+ let top = refRect.top + offsetY
113
+
114
+ // Flip to left if not enough space on right
115
+ const viewportWidth = window.innerWidth
116
+ if (left + elRect.width > viewportWidth - 8) {
117
+ left = refRect.left - elRect.width - offsetX
118
+ }
119
+
120
+ // Keep within viewport vertically
121
+ const viewportHeight = window.innerHeight
122
+ if (top + elRect.height > viewportHeight - 8) {
123
+ top = viewportHeight - elRect.height - 8
124
+ }
125
+ if (top < 8) {
126
+ top = 8
127
+ }
128
+
129
+ element.style.position = "fixed"
130
+ element.style.left = `${left}px`
131
+ element.style.top = `${top}px`
132
+ }
133
+
134
+ // =============================================================================
135
+ // Table Utilities
136
+ // =============================================================================
137
+
138
+ /**
139
+ * Get table dimensions from a table element
140
+ */
141
+ export function getTableDimensions(tableElement) {
142
+ const rows = tableElement.querySelectorAll("tr")
143
+ const rowCount = rows.length
144
+ const colCount = rows[0]?.querySelectorAll("td, th").length || 0
145
+
146
+ return { rowCount, colCount }
147
+ }
148
+
149
+ /**
150
+ * Get row index from a cell element
151
+ */
152
+ export function getRowIndex(cellElement) {
153
+ const row = cellElement.closest("tr")
154
+ if (!row) return -1
155
+
156
+ const tbody = row.parentElement
157
+ return Array.from(tbody.children).indexOf(row)
158
+ }
159
+
160
+ /**
161
+ * Get column index from a cell element
162
+ */
163
+ export function getColumnIndex(cellElement) {
164
+ const row = cellElement.closest("tr")
165
+ if (!row) return -1
166
+
167
+ return Array.from(row.children).indexOf(cellElement)
168
+ }
169
+
170
+ /**
171
+ * Find the table wrapper from any child element
172
+ */
173
+ export function findTableWrapper(element) {
174
+ return element.closest(".inkpen-table-wrapper")
175
+ }
176
+
177
+ /**
178
+ * Find the actual table element from a wrapper
179
+ */
180
+ export function findTableElement(wrapper) {
181
+ return wrapper?.querySelector("table")
182
+ }
183
+
184
+ // =============================================================================
185
+ // Selection Utilities
186
+ // =============================================================================
187
+
188
+ /**
189
+ * Check if a ProseMirror selection is a cell selection
190
+ */
191
+ export function isCellSelection(selection) {
192
+ return selection && selection.$anchorCell !== undefined
193
+ }
194
+
195
+ /**
196
+ * Get selected cell positions from a CellSelection
197
+ */
198
+ export function getSelectedCells(selection) {
199
+ if (!isCellSelection(selection)) return []
200
+
201
+ const cells = []
202
+ selection.forEachCell((node, pos) => {
203
+ cells.push({ node, pos })
204
+ })
205
+ return cells
206
+ }
207
+
208
+ // =============================================================================
209
+ // Event Utilities
210
+ // =============================================================================
211
+
212
+ /**
213
+ * Stop event propagation and prevent default
214
+ */
215
+ export function stopEvent(event) {
216
+ event.preventDefault()
217
+ event.stopPropagation()
218
+ }
219
+
220
+ /**
221
+ * Create a click-outside handler
222
+ */
223
+ export function onClickOutside(element, callback) {
224
+ const handler = (event) => {
225
+ if (!element.contains(event.target)) {
226
+ callback(event)
227
+ }
228
+ }
229
+
230
+ document.addEventListener("mousedown", handler)
231
+ document.addEventListener("touchstart", handler)
232
+
233
+ return () => {
234
+ document.removeEventListener("mousedown", handler)
235
+ document.removeEventListener("touchstart", handler)
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Create an escape key handler
241
+ */
242
+ export function onEscapeKey(callback) {
243
+ const handler = (event) => {
244
+ if (event.key === "Escape") {
245
+ callback(event)
246
+ }
247
+ }
248
+
249
+ document.addEventListener("keydown", handler)
250
+
251
+ return () => {
252
+ document.removeEventListener("keydown", handler)
253
+ }
254
+ }