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,332 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core"
2
+
3
+ /**
4
+ * Toggle Block Extension for TipTap
5
+ *
6
+ * Collapsible/expandable block with a clickable header and nested content.
7
+ * Uses native HTML5 <details> and <summary> elements.
8
+ *
9
+ * Features:
10
+ * - Click arrow or summary to expand/collapse
11
+ * - Editable summary text
12
+ * - Nested block content
13
+ * - Keyboard shortcuts for toggling
14
+ * - Smooth animations
15
+ *
16
+ * @example
17
+ * editor.commands.insertToggle()
18
+ * editor.commands.toggleOpen()
19
+ *
20
+ * @since 0.3.3
21
+ */
22
+
23
+ // Toggle Summary - the clickable header
24
+ export const ToggleSummary = Node.create({
25
+ name: "toggleSummary",
26
+
27
+ content: "inline*",
28
+ defining: true,
29
+ selectable: false,
30
+
31
+ parseHTML() {
32
+ return [{ tag: "summary" }]
33
+ },
34
+
35
+ renderHTML({ HTMLAttributes }) {
36
+ return [
37
+ "summary",
38
+ mergeAttributes(HTMLAttributes, { class: "inkpen-toggle__summary" }),
39
+ 0
40
+ ]
41
+ },
42
+
43
+ addKeyboardShortcuts() {
44
+ return {
45
+ // Enter in summary creates content below, not new summary
46
+ Enter: ({ editor }) => {
47
+ const { state } = editor
48
+ const { $from } = state.selection
49
+
50
+ // Check if we're in a toggle summary
51
+ if ($from.parent.type.name === "toggleSummary") {
52
+ // Find the toggle block
53
+ const togglePos = $from.before($from.depth - 1)
54
+ const toggleNode = state.doc.nodeAt(togglePos)
55
+
56
+ if (toggleNode && toggleNode.type.name === "toggleBlock") {
57
+ // Insert paragraph after summary, inside toggle content
58
+ const summaryEnd = togglePos + 1 + $from.parent.nodeSize
59
+ return editor.chain()
60
+ .insertContentAt(summaryEnd, { type: "paragraph" })
61
+ .focus(summaryEnd + 1)
62
+ .run()
63
+ }
64
+ }
65
+ return false
66
+ },
67
+
68
+ // Backspace at start of empty summary deletes the toggle
69
+ Backspace: ({ editor }) => {
70
+ const { state } = editor
71
+ const { $from, empty } = state.selection
72
+
73
+ if (!empty) return false
74
+
75
+ // Check if at start of toggle summary
76
+ if ($from.parent.type.name === "toggleSummary" && $from.parentOffset === 0) {
77
+ // If summary is empty, delete the whole toggle block
78
+ if ($from.parent.textContent === "") {
79
+ const togglePos = $from.before($from.depth - 1)
80
+ const toggleNode = state.doc.nodeAt(togglePos)
81
+
82
+ if (toggleNode) {
83
+ return editor.chain()
84
+ .deleteRange({ from: togglePos, to: togglePos + toggleNode.nodeSize })
85
+ .run()
86
+ }
87
+ }
88
+ }
89
+ return false
90
+ }
91
+ }
92
+ }
93
+ })
94
+
95
+ // Toggle Block - the container with details/summary structure
96
+ export const ToggleBlock = Node.create({
97
+ name: "toggleBlock",
98
+
99
+ group: "block",
100
+ content: "toggleSummary block*",
101
+ defining: true,
102
+
103
+ addOptions() {
104
+ return {
105
+ defaultOpen: true,
106
+ HTMLAttributes: {
107
+ class: "inkpen-toggle"
108
+ }
109
+ }
110
+ },
111
+
112
+ addAttributes() {
113
+ return {
114
+ open: {
115
+ default: this.options.defaultOpen,
116
+ parseHTML: element => element.hasAttribute("open"),
117
+ renderHTML: attributes => (attributes.open ? { open: "open" } : {})
118
+ }
119
+ }
120
+ },
121
+
122
+ parseHTML() {
123
+ return [
124
+ { tag: "details.inkpen-toggle" },
125
+ { tag: "details" }
126
+ ]
127
+ },
128
+
129
+ renderHTML({ HTMLAttributes }) {
130
+ return [
131
+ "details",
132
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
133
+ 0
134
+ ]
135
+ },
136
+
137
+ addNodeView() {
138
+ return ({ node, getPos, editor }) => {
139
+ // Create details element
140
+ const details = document.createElement("details")
141
+ details.className = "inkpen-toggle"
142
+
143
+ if (node.attrs.open) {
144
+ details.setAttribute("open", "open")
145
+ }
146
+
147
+ // Toggle indicator (arrow)
148
+ const indicator = document.createElement("span")
149
+ indicator.className = "inkpen-toggle__indicator"
150
+ indicator.setAttribute("contenteditable", "false")
151
+ indicator.innerHTML = `<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
152
+ <path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
153
+ </svg>`
154
+
155
+ // Handle toggle via indicator click
156
+ indicator.addEventListener("click", (e) => {
157
+ e.preventDefault()
158
+ e.stopPropagation()
159
+
160
+ if (typeof getPos === "function") {
161
+ const pos = getPos()
162
+ if (pos !== undefined) {
163
+ editor.chain()
164
+ .focus()
165
+ .command(({ tr }) => {
166
+ tr.setNodeMarkup(pos, undefined, {
167
+ ...node.attrs,
168
+ open: !details.hasAttribute("open")
169
+ })
170
+ return true
171
+ })
172
+ .run()
173
+ }
174
+ }
175
+ })
176
+
177
+ // Content wrapper
178
+ const content = document.createElement("div")
179
+ content.className = "inkpen-toggle__content"
180
+
181
+ details.appendChild(indicator)
182
+ details.appendChild(content)
183
+
184
+ // Prevent native toggle behavior (we handle it ourselves)
185
+ details.addEventListener("toggle", (e) => {
186
+ // Sync the attribute with ProseMirror state
187
+ if (typeof getPos === "function") {
188
+ const pos = getPos()
189
+ if (pos !== undefined) {
190
+ const currentNode = editor.state.doc.nodeAt(pos)
191
+ if (currentNode && currentNode.attrs.open !== details.open) {
192
+ editor.chain()
193
+ .command(({ tr }) => {
194
+ tr.setNodeMarkup(pos, undefined, {
195
+ ...currentNode.attrs,
196
+ open: details.open
197
+ })
198
+ return true
199
+ })
200
+ .run()
201
+ }
202
+ }
203
+ }
204
+ })
205
+
206
+ return {
207
+ dom: details,
208
+ contentDOM: content,
209
+ update: (updatedNode) => {
210
+ if (updatedNode.type.name !== "toggleBlock") return false
211
+
212
+ if (updatedNode.attrs.open) {
213
+ details.setAttribute("open", "open")
214
+ } else {
215
+ details.removeAttribute("open")
216
+ }
217
+
218
+ return true
219
+ }
220
+ }
221
+ }
222
+ },
223
+
224
+ addCommands() {
225
+ return {
226
+ insertToggle: (attributes = {}) => ({ commands }) => {
227
+ return commands.insertContent({
228
+ type: this.name,
229
+ attrs: {
230
+ open: attributes.open !== undefined ? attributes.open : this.options.defaultOpen
231
+ },
232
+ content: [
233
+ {
234
+ type: "toggleSummary",
235
+ content: attributes.summary
236
+ ? [{ type: "text", text: attributes.summary }]
237
+ : []
238
+ },
239
+ { type: "paragraph" }
240
+ ]
241
+ })
242
+ },
243
+
244
+ setToggle: () => ({ commands, state }) => {
245
+ const { $from } = state.selection
246
+ const text = $from.parent.textContent
247
+
248
+ return commands.insertContent({
249
+ type: this.name,
250
+ attrs: { open: true },
251
+ content: [
252
+ {
253
+ type: "toggleSummary",
254
+ content: text ? [{ type: "text", text }] : []
255
+ },
256
+ { type: "paragraph" }
257
+ ]
258
+ })
259
+ },
260
+
261
+ toggleOpen: () => ({ state, dispatch }) => {
262
+ const { $from } = state.selection
263
+
264
+ // Find the toggle block
265
+ for (let d = $from.depth; d > 0; d--) {
266
+ const node = $from.node(d)
267
+ if (node.type.name === "toggleBlock") {
268
+ if (dispatch) {
269
+ const pos = $from.before(d)
270
+ const tr = state.tr.setNodeMarkup(pos, undefined, {
271
+ ...node.attrs,
272
+ open: !node.attrs.open
273
+ })
274
+ dispatch(tr)
275
+ }
276
+ return true
277
+ }
278
+ }
279
+ return false
280
+ },
281
+
282
+ expandToggle: () => ({ state, dispatch }) => {
283
+ const { $from } = state.selection
284
+
285
+ for (let d = $from.depth; d > 0; d--) {
286
+ const node = $from.node(d)
287
+ if (node.type.name === "toggleBlock" && !node.attrs.open) {
288
+ if (dispatch) {
289
+ const pos = $from.before(d)
290
+ const tr = state.tr.setNodeMarkup(pos, undefined, {
291
+ ...node.attrs,
292
+ open: true
293
+ })
294
+ dispatch(tr)
295
+ }
296
+ return true
297
+ }
298
+ }
299
+ return false
300
+ },
301
+
302
+ collapseToggle: () => ({ state, dispatch }) => {
303
+ const { $from } = state.selection
304
+
305
+ for (let d = $from.depth; d > 0; d--) {
306
+ const node = $from.node(d)
307
+ if (node.type.name === "toggleBlock" && node.attrs.open) {
308
+ if (dispatch) {
309
+ const pos = $from.before(d)
310
+ const tr = state.tr.setNodeMarkup(pos, undefined, {
311
+ ...node.attrs,
312
+ open: false
313
+ })
314
+ dispatch(tr)
315
+ }
316
+ return true
317
+ }
318
+ }
319
+ return false
320
+ }
321
+ }
322
+ },
323
+
324
+ addKeyboardShortcuts() {
325
+ return {
326
+ "Mod-Shift-t": () => this.editor.commands.insertToggle(),
327
+ "Mod-Enter": () => this.editor.commands.toggleOpen()
328
+ }
329
+ }
330
+ })
331
+
332
+ export default ToggleBlock
@@ -0,0 +1,87 @@
1
+ // Inkpen - TipTap-based rich text editor for Rails
2
+ // Entry point for importmap
3
+
4
+ import { Application } from "@hotwired/stimulus"
5
+ import EditorController from "inkpen/controllers/editor_controller"
6
+ import ToolbarController from "inkpen/controllers/toolbar_controller"
7
+ import StickyToolbarController from "inkpen/controllers/sticky_toolbar_controller"
8
+
9
+ // ============================================
10
+ // IMPORTANT: Register controllers IMMEDIATELY before any async code
11
+ // This ensures controllers are available when Stimulus scans the DOM
12
+ // ============================================
13
+ const application = window.Stimulus || Application.start()
14
+
15
+ application.register("inkpen--editor", EditorController)
16
+ application.register("inkpen--toolbar", ToolbarController)
17
+ application.register("inkpen--sticky-toolbar", StickyToolbarController)
18
+
19
+ // TipTap extensions (static imports)
20
+ import { Section } from "inkpen/extensions/section"
21
+ import { Preformatted } from "inkpen/extensions/preformatted"
22
+ import { SlashCommands } from "inkpen/extensions/slash_commands"
23
+ import { BlockGutter } from "inkpen/extensions/block_gutter"
24
+ import { DragHandle } from "inkpen/extensions/drag_handle"
25
+ import { ToggleBlock, ToggleSummary } from "inkpen/extensions/toggle_block"
26
+ import { Columns, Column } from "inkpen/extensions/columns"
27
+ import { Callout } from "inkpen/extensions/callout"
28
+ import { BlockCommands } from "inkpen/extensions/block_commands"
29
+ import { EnhancedImage } from "inkpen/extensions/enhanced_image"
30
+ import { FileAttachment } from "inkpen/extensions/file_attachment"
31
+ import { Embed } from "inkpen/extensions/embed"
32
+ import { AdvancedTable, AdvancedTableRow, AdvancedTableCell, AdvancedTableHeader } from "inkpen/extensions/advanced_table"
33
+ import { TableOfContents } from "inkpen/extensions/table_of_contents"
34
+ import { Database } from "inkpen/extensions/database"
35
+ import { DocumentSection } from "inkpen/extensions/document_section"
36
+ import { SectionTitle } from "inkpen/extensions/section_title"
37
+
38
+ // InkpenTable is loaded lazily to prevent import failures from breaking the library
39
+ let InkpenTable, InkpenTableRow, InkpenTableCell, InkpenTableHeader
40
+ try {
41
+ const mod = await import("inkpen/extensions/inkpen_table")
42
+ InkpenTable = mod.InkpenTable
43
+ InkpenTableRow = mod.InkpenTableRow
44
+ InkpenTableCell = mod.InkpenTableCell
45
+ InkpenTableHeader = mod.InkpenTableHeader
46
+ } catch (e) {
47
+ console.warn("Inkpen: InkpenTable extension not available:", e.message)
48
+ }
49
+
50
+ // Export controllers
51
+ export { EditorController, ToolbarController, StickyToolbarController }
52
+
53
+ // Export extensions for custom use
54
+ export {
55
+ Section,
56
+ DocumentSection,
57
+ SectionTitle,
58
+ Preformatted,
59
+ SlashCommands,
60
+ BlockGutter,
61
+ DragHandle,
62
+ ToggleBlock,
63
+ ToggleSummary,
64
+ Columns,
65
+ Column,
66
+ Callout,
67
+ BlockCommands,
68
+ EnhancedImage,
69
+ FileAttachment,
70
+ Embed,
71
+ // InkpenTable - Notion-style enhanced tables (recommended)
72
+ InkpenTable,
73
+ InkpenTableRow,
74
+ InkpenTableCell,
75
+ InkpenTableHeader,
76
+ // AdvancedTable - Legacy (use InkpenTable instead)
77
+ AdvancedTable,
78
+ AdvancedTableRow,
79
+ AdvancedTableCell,
80
+ AdvancedTableHeader,
81
+ TableOfContents,
82
+ Database
83
+ }
84
+
85
+ // Export functionality is available separately:
86
+ // import { ExportCommands } from "inkpen/extensions/export_commands"
87
+ // import { exportToMarkdown, ... } from "inkpen/export"