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,352 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core"
2
+ import { SectionTitle } from "inkpen/extensions/section_title"
3
+
4
+ /**
5
+ * Document Section Extension for TipTap
6
+ *
7
+ * True content-grouping container with semantic H2 title and collapsible
8
+ * content. Unlike the layout Section extension (width/spacing), DocumentSection
9
+ * groups related blocks under a heading for document structure.
10
+ *
11
+ * Features:
12
+ * - Semantic H2 title (integrates with Table of Contents)
13
+ * - Collapsible content with left-gutter toggle
14
+ * - Nesting support (sections within sections, up to 3 levels)
15
+ * - Draggable as a group
16
+ * - Auto-generated IDs for deep linking
17
+ *
18
+ * @example
19
+ * editor.commands.insertDocumentSection()
20
+ * editor.commands.insertDocumentSection({ title: "My Section" })
21
+ * editor.commands.toggleSectionCollapsed()
22
+ *
23
+ * @since 0.8.0
24
+ */
25
+
26
+ // SVG icons for collapse toggle
27
+ const ICON_EXPANDED = `<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
28
+ <path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
29
+ </svg>`
30
+
31
+ const ICON_COLLAPSED = `<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
32
+ <path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
33
+ </svg>`
34
+
35
+ export const DocumentSection = Node.create({
36
+ name: "documentSection",
37
+
38
+ group: "block",
39
+ content: "sectionTitle block*",
40
+ defining: true,
41
+ isolating: true,
42
+ draggable: true,
43
+
44
+ // Options
45
+
46
+ addOptions() {
47
+ return {
48
+ maxDepth: 3,
49
+ showControls: true,
50
+ defaultCollapsed: false,
51
+ allowNesting: true,
52
+ HTMLAttributes: {
53
+ class: "inkpen-doc-section"
54
+ }
55
+ }
56
+ },
57
+
58
+ // Attributes
59
+
60
+ addAttributes() {
61
+ return {
62
+ collapsed: {
63
+ default: this.options.defaultCollapsed,
64
+ parseHTML: element => element.getAttribute("data-collapsed") === "true",
65
+ renderHTML: attributes => ({
66
+ "data-collapsed": attributes.collapsed ? "true" : "false"
67
+ })
68
+ },
69
+ id: {
70
+ default: null,
71
+ parseHTML: element => element.getAttribute("id"),
72
+ renderHTML: attributes => {
73
+ if (!attributes.id) return {}
74
+ return { id: attributes.id }
75
+ }
76
+ },
77
+ depth: {
78
+ default: 1,
79
+ parseHTML: element => parseInt(element.getAttribute("data-depth") || "1", 10),
80
+ renderHTML: attributes => ({ "data-depth": attributes.depth })
81
+ }
82
+ }
83
+ },
84
+
85
+ // Parse & Render
86
+
87
+ parseHTML() {
88
+ return [
89
+ { tag: 'div[data-type="document-section"]' },
90
+ { tag: "div.inkpen-doc-section" }
91
+ ]
92
+ },
93
+
94
+ renderHTML({ HTMLAttributes }) {
95
+ return [
96
+ "div",
97
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
98
+ "data-type": "document-section"
99
+ }),
100
+ 0
101
+ ]
102
+ },
103
+
104
+ // NodeView (interactive controls)
105
+
106
+ addNodeView() {
107
+ return ({ node, getPos, editor }) => {
108
+ // Create container
109
+ const dom = document.createElement("div")
110
+ dom.className = "inkpen-doc-section"
111
+ dom.setAttribute("data-type", "document-section")
112
+ dom.setAttribute("data-collapsed", node.attrs.collapsed ? "true" : "false")
113
+ dom.setAttribute("data-depth", node.attrs.depth)
114
+
115
+ if (node.attrs.id) {
116
+ dom.setAttribute("id", node.attrs.id)
117
+ }
118
+
119
+ // Left gutter with collapse toggle (only in edit mode with controls)
120
+ let gutter = null
121
+ if (this.options.showControls && editor.isEditable) {
122
+ gutter = document.createElement("div")
123
+ gutter.className = "inkpen-doc-section__gutter"
124
+ gutter.contentEditable = "false"
125
+
126
+ const toggle = document.createElement("button")
127
+ toggle.type = "button"
128
+ toggle.className = "inkpen-doc-section__toggle"
129
+ toggle.title = node.attrs.collapsed ? "Expand section" : "Collapse section"
130
+ toggle.innerHTML = node.attrs.collapsed ? ICON_COLLAPSED : ICON_EXPANDED
131
+
132
+ toggle.addEventListener("click", (e) => {
133
+ e.preventDefault()
134
+ e.stopPropagation()
135
+
136
+ if (typeof getPos === "function") {
137
+ const pos = getPos()
138
+ if (pos !== undefined) {
139
+ editor.chain()
140
+ .focus()
141
+ .command(({ tr }) => {
142
+ tr.setNodeMarkup(pos, undefined, {
143
+ ...node.attrs,
144
+ collapsed: !node.attrs.collapsed
145
+ })
146
+ return true
147
+ })
148
+ .run()
149
+ }
150
+ }
151
+ })
152
+
153
+ gutter.appendChild(toggle)
154
+ dom.appendChild(gutter)
155
+ }
156
+
157
+ // Content container
158
+ const content = document.createElement("div")
159
+ content.className = "inkpen-doc-section__content"
160
+ dom.appendChild(content)
161
+
162
+ return {
163
+ dom,
164
+ contentDOM: content,
165
+ update: (updatedNode) => {
166
+ if (updatedNode.type.name !== "documentSection") return false
167
+
168
+ // Update collapsed state
169
+ dom.setAttribute("data-collapsed", updatedNode.attrs.collapsed ? "true" : "false")
170
+ dom.setAttribute("data-depth", updatedNode.attrs.depth)
171
+
172
+ if (updatedNode.attrs.id) {
173
+ dom.setAttribute("id", updatedNode.attrs.id)
174
+ } else {
175
+ dom.removeAttribute("id")
176
+ }
177
+
178
+ // Update toggle button
179
+ if (gutter) {
180
+ const toggle = gutter.querySelector(".inkpen-doc-section__toggle")
181
+ if (toggle) {
182
+ toggle.innerHTML = updatedNode.attrs.collapsed ? ICON_COLLAPSED : ICON_EXPANDED
183
+ toggle.title = updatedNode.attrs.collapsed ? "Expand section" : "Collapse section"
184
+ }
185
+ }
186
+
187
+ return true
188
+ },
189
+ destroy: () => {
190
+ // Cleanup if needed
191
+ }
192
+ }
193
+ }
194
+ },
195
+
196
+ // Commands
197
+
198
+ addCommands() {
199
+ return {
200
+ insertDocumentSection: (attributes = {}) => ({ commands, state }) => {
201
+ // Generate unique ID for deep linking
202
+ const id = attributes.id || `section-${Date.now()}`
203
+
204
+ // Calculate depth based on cursor position
205
+ let depth = 1
206
+ if (this.options.allowNesting) {
207
+ const { $from } = state.selection
208
+ for (let d = $from.depth; d > 0; d--) {
209
+ if ($from.node(d).type.name === "documentSection") {
210
+ const parentDepth = $from.node(d).attrs.depth || 1
211
+ depth = Math.min(parentDepth + 1, this.options.maxDepth)
212
+ break
213
+ }
214
+ }
215
+ }
216
+
217
+ return commands.insertContent({
218
+ type: this.name,
219
+ attrs: {
220
+ collapsed: attributes.collapsed ?? this.options.defaultCollapsed,
221
+ id,
222
+ depth
223
+ },
224
+ content: [
225
+ {
226
+ type: "sectionTitle",
227
+ attrs: { id: `${id}-title` },
228
+ content: attributes.title
229
+ ? [{ type: "text", text: attributes.title }]
230
+ : []
231
+ },
232
+ { type: "paragraph" }
233
+ ]
234
+ })
235
+ },
236
+
237
+ setDocumentSection: () => ({ commands, state }) => {
238
+ const { $from } = state.selection
239
+ const text = $from.parent.textContent
240
+ const id = `section-${Date.now()}`
241
+
242
+ return commands.insertContent({
243
+ type: this.name,
244
+ attrs: {
245
+ collapsed: false,
246
+ id,
247
+ depth: 1
248
+ },
249
+ content: [
250
+ {
251
+ type: "sectionTitle",
252
+ attrs: { id: `${id}-title` },
253
+ content: text ? [{ type: "text", text }] : []
254
+ },
255
+ { type: "paragraph" }
256
+ ]
257
+ })
258
+ },
259
+
260
+ toggleSectionCollapsed: () => ({ state, dispatch }) => {
261
+ const { $from } = state.selection
262
+
263
+ // Find the document section
264
+ for (let d = $from.depth; d > 0; d--) {
265
+ const node = $from.node(d)
266
+ if (node.type.name === "documentSection") {
267
+ if (dispatch) {
268
+ const pos = $from.before(d)
269
+ const tr = state.tr.setNodeMarkup(pos, undefined, {
270
+ ...node.attrs,
271
+ collapsed: !node.attrs.collapsed
272
+ })
273
+ dispatch(tr)
274
+ }
275
+ return true
276
+ }
277
+ }
278
+ return false
279
+ },
280
+
281
+ expandSection: () => ({ state, dispatch }) => {
282
+ const { $from } = state.selection
283
+
284
+ for (let d = $from.depth; d > 0; d--) {
285
+ const node = $from.node(d)
286
+ if (node.type.name === "documentSection" && node.attrs.collapsed) {
287
+ if (dispatch) {
288
+ const pos = $from.before(d)
289
+ const tr = state.tr.setNodeMarkup(pos, undefined, {
290
+ ...node.attrs,
291
+ collapsed: false
292
+ })
293
+ dispatch(tr)
294
+ }
295
+ return true
296
+ }
297
+ }
298
+ return false
299
+ },
300
+
301
+ collapseSection: () => ({ state, dispatch }) => {
302
+ const { $from } = state.selection
303
+
304
+ for (let d = $from.depth; d > 0; d--) {
305
+ const node = $from.node(d)
306
+ if (node.type.name === "documentSection" && !node.attrs.collapsed) {
307
+ if (dispatch) {
308
+ const pos = $from.before(d)
309
+ const tr = state.tr.setNodeMarkup(pos, undefined, {
310
+ ...node.attrs,
311
+ collapsed: true
312
+ })
313
+ dispatch(tr)
314
+ }
315
+ return true
316
+ }
317
+ }
318
+ return false
319
+ },
320
+
321
+ wrapInDocumentSection: (attributes = {}) => ({ commands }) => {
322
+ const id = attributes.id || `section-${Date.now()}`
323
+ return commands.wrapIn(this.name, {
324
+ ...attributes,
325
+ id,
326
+ depth: 1
327
+ })
328
+ },
329
+
330
+ unwrapDocumentSection: () => ({ commands }) => {
331
+ return commands.lift(this.name)
332
+ }
333
+ }
334
+ },
335
+
336
+ // Keyboard Shortcuts
337
+
338
+ addKeyboardShortcuts() {
339
+ return {
340
+ "Mod-Shift-Enter": () => this.editor.commands.insertDocumentSection(),
341
+ "Mod-.": () => this.editor.commands.toggleSectionCollapsed()
342
+ }
343
+ },
344
+
345
+ // Extensions to include
346
+
347
+ addExtensions() {
348
+ return [SectionTitle]
349
+ }
350
+ })
351
+
352
+ export default DocumentSection