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,282 @@
1
+ /**
2
+ * InkpenTable Menu
3
+ *
4
+ * Context menu component for row/column operations.
5
+ * Follows Fizzy patterns: private fields with #, section comments.
6
+ *
7
+ * @since 0.8.0
8
+ */
9
+
10
+ import {
11
+ createElement,
12
+ positionBelow,
13
+ positionRight,
14
+ onClickOutside,
15
+ onEscapeKey,
16
+ stopEvent
17
+ } from "inkpen/extensions/inkpen_table/table_helpers"
18
+
19
+ import {
20
+ CSS_CLASSES,
21
+ ROW_MENU_ITEMS,
22
+ COLUMN_MENU_ITEMS,
23
+ TEXT_COLORS,
24
+ BACKGROUND_COLORS,
25
+ ALIGNMENT_OPTIONS
26
+ } from "inkpen/extensions/inkpen_table/table_constants"
27
+
28
+ // =============================================================================
29
+ // TableMenu Class
30
+ // =============================================================================
31
+
32
+ export class TableMenu {
33
+ #element = null
34
+ #submenu = null
35
+ #reference = null
36
+ #onAction = null
37
+ #cleanupClickOutside = null
38
+ #cleanupEscapeKey = null
39
+ #menuType = null
40
+
41
+ // Lifecycle
42
+
43
+ constructor(options = {}) {
44
+ this.#onAction = options.onAction || (() => {})
45
+ }
46
+
47
+ destroy() {
48
+ this.hide()
49
+ }
50
+
51
+ // Public Methods
52
+
53
+ showRowMenu(reference, options = {}) {
54
+ this.#menuType = "row"
55
+ this.#show(reference, ROW_MENU_ITEMS, options)
56
+ }
57
+
58
+ showColumnMenu(reference, options = {}) {
59
+ this.#menuType = "column"
60
+ this.#show(reference, COLUMN_MENU_ITEMS, options)
61
+ }
62
+
63
+ hide() {
64
+ this.#hideSubmenu()
65
+ this.#cleanup()
66
+ }
67
+
68
+ isVisible() {
69
+ return this.#element !== null
70
+ }
71
+
72
+ // Private
73
+
74
+ #show(reference, items, options = {}) {
75
+ this.hide()
76
+
77
+ this.#reference = reference
78
+ this.#element = this.#createMenu(items, options)
79
+
80
+ document.body.appendChild(this.#element)
81
+ positionBelow(this.#element, reference)
82
+
83
+ this.#setupEventHandlers()
84
+ }
85
+
86
+ #createMenu(items, options = {}) {
87
+ const menu = createElement("div", {
88
+ className: CSS_CLASSES.menu,
89
+ role: "menu"
90
+ })
91
+
92
+ for (const item of items) {
93
+ const element = this.#createMenuItem(item, options)
94
+ if (element) {
95
+ menu.appendChild(element)
96
+ }
97
+ }
98
+
99
+ return menu
100
+ }
101
+
102
+ #createMenuItem(item, options = {}) {
103
+ if (item.type === "separator") {
104
+ return createElement("div", {
105
+ className: CSS_CLASSES.menuSeparator,
106
+ role: "separator"
107
+ })
108
+ }
109
+
110
+ if (item.type === "submenu") {
111
+ return this.#createSubmenuTrigger(item, options)
112
+ }
113
+
114
+ const button = createElement("button", {
115
+ className: `${CSS_CLASSES.menuItem}${item.danger ? " " + CSS_CLASSES.menuItem + "--danger" : ""}`,
116
+ role: "menuitem",
117
+ dataset: { action: item.id }
118
+ }, [
119
+ createElement("span", { className: `${CSS_CLASSES.menuItem}__icon` }, [item.icon]),
120
+ createElement("span", { className: `${CSS_CLASSES.menuItem}__label` }, [item.label])
121
+ ])
122
+
123
+ button.addEventListener("click", (e) => {
124
+ stopEvent(e)
125
+ this.#handleAction(item.id)
126
+ })
127
+
128
+ return button
129
+ }
130
+
131
+ #createSubmenuTrigger(item, options = {}) {
132
+ const trigger = createElement("button", {
133
+ className: `${CSS_CLASSES.menuItem} ${CSS_CLASSES.menuItem}--has-submenu`,
134
+ role: "menuitem",
135
+ "aria-haspopup": "true",
136
+ dataset: { submenu: item.id }
137
+ }, [
138
+ createElement("span", { className: `${CSS_CLASSES.menuItem}__icon` }, [item.icon]),
139
+ createElement("span", { className: `${CSS_CLASSES.menuItem}__label` }, [item.label]),
140
+ createElement("span", { className: `${CSS_CLASSES.menuItem}__arrow` }, ["›"])
141
+ ])
142
+
143
+ trigger.addEventListener("mouseenter", () => {
144
+ this.#showSubmenu(item.id, trigger, options)
145
+ })
146
+
147
+ trigger.addEventListener("click", (e) => {
148
+ stopEvent(e)
149
+ this.#showSubmenu(item.id, trigger, options)
150
+ })
151
+
152
+ return trigger
153
+ }
154
+
155
+ #showSubmenu(submenuId, trigger, options = {}) {
156
+ this.#hideSubmenu()
157
+
158
+ let items = []
159
+ let isColorPicker = false
160
+
161
+ switch (submenuId) {
162
+ case "textColor":
163
+ items = TEXT_COLORS
164
+ isColorPicker = true
165
+ break
166
+ case "backgroundColor":
167
+ items = BACKGROUND_COLORS
168
+ isColorPicker = true
169
+ break
170
+ case "alignment":
171
+ items = ALIGNMENT_OPTIONS
172
+ break
173
+ default:
174
+ return
175
+ }
176
+
177
+ this.#submenu = createElement("div", {
178
+ className: `${CSS_CLASSES.menu} ${CSS_CLASSES.menuSubmenu}`,
179
+ role: "menu"
180
+ })
181
+
182
+ if (isColorPicker) {
183
+ this.#submenu.classList.add(`${CSS_CLASSES.menu}--colors`)
184
+ const grid = createElement("div", { className: `${CSS_CLASSES.menu}__color-grid` })
185
+
186
+ for (const color of items) {
187
+ const swatch = createElement("button", {
188
+ className: CSS_CLASSES.colorSwatch,
189
+ role: "menuitem",
190
+ title: color.label,
191
+ dataset: { color: color.name }
192
+ })
193
+
194
+ if (color.value) {
195
+ swatch.style.backgroundColor = color.value
196
+ } else {
197
+ swatch.classList.add(`${CSS_CLASSES.colorSwatch}--none`)
198
+ swatch.textContent = "∅"
199
+ }
200
+
201
+ swatch.addEventListener("click", (e) => {
202
+ stopEvent(e)
203
+ this.#handleAction(submenuId, { color: color.name, value: color.value })
204
+ })
205
+
206
+ grid.appendChild(swatch)
207
+ }
208
+
209
+ this.#submenu.appendChild(grid)
210
+ } else {
211
+ for (const item of items) {
212
+ const button = createElement("button", {
213
+ className: CSS_CLASSES.menuItem,
214
+ role: "menuitem",
215
+ dataset: { value: item.name }
216
+ }, [
217
+ createElement("span", { className: `${CSS_CLASSES.menuItem}__icon` }, [item.icon]),
218
+ createElement("span", { className: `${CSS_CLASSES.menuItem}__label` }, [item.label])
219
+ ])
220
+
221
+ button.addEventListener("click", (e) => {
222
+ stopEvent(e)
223
+ this.#handleAction(submenuId, { value: item.name })
224
+ })
225
+
226
+ this.#submenu.appendChild(button)
227
+ }
228
+ }
229
+
230
+ document.body.appendChild(this.#submenu)
231
+ positionRight(this.#submenu, trigger)
232
+ }
233
+
234
+ #hideSubmenu() {
235
+ if (this.#submenu) {
236
+ this.#submenu.remove()
237
+ this.#submenu = null
238
+ }
239
+ }
240
+
241
+ #handleAction(actionId, data = {}) {
242
+ this.#onAction({
243
+ action: actionId,
244
+ menuType: this.#menuType,
245
+ ...data
246
+ })
247
+ this.hide()
248
+ }
249
+
250
+ #setupEventHandlers() {
251
+ this.#cleanupClickOutside = onClickOutside(this.#element, (e) => {
252
+ if (this.#submenu && this.#submenu.contains(e.target)) {
253
+ return
254
+ }
255
+ this.hide()
256
+ })
257
+
258
+ this.#cleanupEscapeKey = onEscapeKey(() => {
259
+ this.hide()
260
+ })
261
+ }
262
+
263
+ #cleanup() {
264
+ if (this.#cleanupClickOutside) {
265
+ this.#cleanupClickOutside()
266
+ this.#cleanupClickOutside = null
267
+ }
268
+
269
+ if (this.#cleanupEscapeKey) {
270
+ this.#cleanupEscapeKey()
271
+ this.#cleanupEscapeKey = null
272
+ }
273
+
274
+ if (this.#element) {
275
+ this.#element.remove()
276
+ this.#element = null
277
+ }
278
+
279
+ this.#reference = null
280
+ this.#menuType = null
281
+ }
282
+ }
@@ -0,0 +1,239 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core"
2
+ import { Plugin, PluginKey } from "@tiptap/pm/state"
3
+
4
+ /**
5
+ * Preformatted Text Extension for TipTap
6
+ *
7
+ * Plain text block that preserves whitespace exactly as typed.
8
+ * No syntax highlighting - perfect for ASCII art, tables, and diagrams.
9
+ *
10
+ * Features:
11
+ * - Strict monospace font
12
+ * - Preserves all whitespace (spaces, tabs, newlines)
13
+ * - No text wrapping by default
14
+ * - Optional line numbers
15
+ * - Tab key inserts actual tab character
16
+ *
17
+ * @example
18
+ * editor.commands.insertPreformatted()
19
+ * editor.commands.togglePreformatted()
20
+ *
21
+ * @since 0.3.0
22
+ */
23
+ export const Preformatted = Node.create({
24
+ name: "preformatted",
25
+
26
+ group: "block",
27
+ content: "text*",
28
+ marks: "", // No marks allowed (bold, italic, etc.)
29
+ code: true, // Tells ProseMirror to preserve whitespace
30
+ defining: true,
31
+
32
+ // Options
33
+
34
+ addOptions() {
35
+ return {
36
+ showLineNumbers: false,
37
+ wrapLines: false,
38
+ tabSize: 4,
39
+ showLabel: true,
40
+ labelText: "Plain Text",
41
+ HTMLAttributes: {
42
+ class: "inkpen-preformatted"
43
+ }
44
+ }
45
+ },
46
+
47
+ // Attributes
48
+
49
+ addAttributes() {
50
+ return {
51
+ label: {
52
+ default: this.options.labelText,
53
+ parseHTML: element => element.getAttribute("data-label") || this.options.labelText,
54
+ renderHTML: attributes => ({ "data-label": attributes.label })
55
+ }
56
+ }
57
+ },
58
+
59
+ // Parse & Render
60
+
61
+ parseHTML() {
62
+ return [
63
+ { tag: "pre[data-type='preformatted']" },
64
+ { tag: "pre.inkpen-preformatted" },
65
+ // Also capture plain <pre> without code highlighting
66
+ {
67
+ tag: "pre",
68
+ priority: 40, // Lower priority than CodeBlock
69
+ getAttrs: node => {
70
+ // Only match if it doesn't have a language class (not a code block)
71
+ const hasLanguage = node.querySelector("code[class*='language-']") ||
72
+ node.classList.contains("hljs") ||
73
+ node.hasAttribute("data-language")
74
+ return hasLanguage ? false : null
75
+ }
76
+ }
77
+ ]
78
+ },
79
+
80
+ renderHTML({ node, HTMLAttributes }) {
81
+ const attrs = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
82
+ "data-type": "preformatted",
83
+ "data-wrap": this.options.wrapLines ? "true" : "false",
84
+ style: `tab-size: ${this.options.tabSize}; -moz-tab-size: ${this.options.tabSize};`
85
+ })
86
+
87
+ return ["pre", attrs, 0]
88
+ },
89
+
90
+ // NodeView for label display
91
+
92
+ addNodeView() {
93
+ return ({ node, HTMLAttributes, getPos, editor }) => {
94
+ const dom = document.createElement("div")
95
+ dom.className = "inkpen-preformatted-wrapper"
96
+
97
+ // Label badge
98
+ if (this.options.showLabel) {
99
+ const label = document.createElement("span")
100
+ label.className = "inkpen-preformatted__label"
101
+ label.textContent = node.attrs.label || this.options.labelText
102
+ label.contentEditable = "false"
103
+ dom.appendChild(label)
104
+ }
105
+
106
+ // The actual pre element
107
+ const pre = document.createElement("pre")
108
+ pre.className = "inkpen-preformatted"
109
+ pre.setAttribute("data-type", "preformatted")
110
+ pre.setAttribute("data-wrap", this.options.wrapLines ? "true" : "false")
111
+ pre.style.tabSize = this.options.tabSize
112
+ pre.style.MozTabSize = this.options.tabSize
113
+ dom.appendChild(pre)
114
+
115
+ return {
116
+ dom,
117
+ contentDOM: pre,
118
+ update: (updatedNode) => {
119
+ if (updatedNode.type !== this.type) return false
120
+ return true
121
+ }
122
+ }
123
+ }
124
+ },
125
+
126
+ // Commands
127
+
128
+ addCommands() {
129
+ return {
130
+ setPreformatted: (attributes = {}) => ({ commands }) => {
131
+ return commands.setNode(this.name, attributes)
132
+ },
133
+
134
+ togglePreformatted: (attributes = {}) => ({ commands }) => {
135
+ return commands.toggleNode(this.name, "paragraph", attributes)
136
+ },
137
+
138
+ insertPreformatted: (content = "") => ({ commands }) => {
139
+ return commands.insertContent({
140
+ type: this.name,
141
+ content: content ? [{ type: "text", text: content }] : []
142
+ })
143
+ }
144
+ }
145
+ },
146
+
147
+ // Keyboard Shortcuts
148
+
149
+ addKeyboardShortcuts() {
150
+ return {
151
+ // Cmd+Shift+P to toggle preformatted
152
+ "Mod-Shift-p": () => this.editor.commands.togglePreformatted(),
153
+
154
+ // Tab inserts a real tab character (not focus change)
155
+ Tab: ({ editor }) => {
156
+ if (!editor.isActive(this.name)) return false
157
+
158
+ return editor.commands.insertContent("\t")
159
+ },
160
+
161
+ // Shift+Tab removes a tab if at start of line
162
+ "Shift-Tab": ({ editor }) => {
163
+ if (!editor.isActive(this.name)) return false
164
+
165
+ const { state } = editor
166
+ const { selection } = state
167
+ const { $from } = selection
168
+
169
+ // Get text before cursor on current line
170
+ const lineStart = $from.start()
171
+ const textBefore = state.doc.textBetween(lineStart, $from.pos)
172
+
173
+ // If line starts with tab, remove it
174
+ if (textBefore.startsWith("\t")) {
175
+ return editor.commands.command(({ tr }) => {
176
+ tr.delete(lineStart, lineStart + 1)
177
+ return true
178
+ })
179
+ }
180
+
181
+ return false
182
+ },
183
+
184
+ // Enter creates a new line (not a new block)
185
+ Enter: ({ editor }) => {
186
+ if (!editor.isActive(this.name)) return false
187
+
188
+ return editor.commands.insertContent("\n")
189
+ },
190
+
191
+ // Backspace at start exits the block
192
+ Backspace: ({ editor }) => {
193
+ if (!editor.isActive(this.name)) return false
194
+
195
+ const { state } = editor
196
+ const { selection } = state
197
+ const { $from } = selection
198
+
199
+ // If at the very start of the preformatted block and it's empty
200
+ if ($from.parentOffset === 0 && $from.parent.textContent === "") {
201
+ return editor.commands.togglePreformatted()
202
+ }
203
+
204
+ return false
205
+ }
206
+ }
207
+ },
208
+
209
+ // Input Rules (convert ``` to preformatted if no language specified)
210
+
211
+ addInputRules() {
212
+ return [] // Let CodeBlock handle ``` - we use Cmd+Shift+P
213
+ },
214
+
215
+ // Plugins
216
+
217
+ addProseMirrorPlugins() {
218
+ return [
219
+ // Plugin to handle paste - preserve whitespace
220
+ new Plugin({
221
+ key: new PluginKey("preformattedPaste"),
222
+ props: {
223
+ handlePaste: (view, event) => {
224
+ if (!this.editor.isActive(this.name)) return false
225
+
226
+ const text = event.clipboardData?.getData("text/plain")
227
+ if (!text) return false
228
+
229
+ // Insert plain text, preserving whitespace
230
+ this.editor.commands.insertContent(text)
231
+ return true
232
+ }
233
+ }
234
+ })
235
+ ]
236
+ }
237
+ })
238
+
239
+ export default Preformatted