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,281 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core"
2
+
3
+ /**
4
+ * Section Extension for TipTap
5
+ *
6
+ * Block-level container that controls content width and spacing.
7
+ * Renders with an interactive NodeView for editing controls.
8
+ *
9
+ * Follows Fizzy patterns:
10
+ * - Section comments for organization
11
+ * - Clean command API
12
+ * - Event-driven updates
13
+ *
14
+ * @example
15
+ * editor.commands.insertSection({ width: 'wide', spacing: 'spacious' })
16
+ * editor.commands.setSectionWidth('narrow')
17
+ *
18
+ * @since 0.3.0
19
+ */
20
+ export const Section = Node.create({
21
+ name: "section",
22
+
23
+ group: "block",
24
+ content: "block+",
25
+ defining: true,
26
+ isolating: true,
27
+
28
+ // Options
29
+
30
+ addOptions() {
31
+ return {
32
+ defaultWidth: "default",
33
+ defaultSpacing: "normal",
34
+ showControls: true,
35
+ widthPresets: {
36
+ narrow: { maxWidth: "560px", label: "Narrow" },
37
+ default: { maxWidth: "680px", label: "Default" },
38
+ wide: { maxWidth: "900px", label: "Wide" },
39
+ full: { maxWidth: "100%", label: "Full" }
40
+ },
41
+ spacingPresets: {
42
+ compact: { padding: "1rem 0", label: "Compact" },
43
+ normal: { padding: "2rem 0", label: "Normal" },
44
+ spacious: { padding: "4rem 0", label: "Spacious" }
45
+ },
46
+ HTMLAttributes: {
47
+ class: "inkpen-section"
48
+ }
49
+ }
50
+ },
51
+
52
+ // Attributes
53
+
54
+ addAttributes() {
55
+ return {
56
+ width: {
57
+ default: this.options.defaultWidth,
58
+ parseHTML: element => element.getAttribute("data-width") || this.options.defaultWidth,
59
+ renderHTML: attributes => ({ "data-width": attributes.width })
60
+ },
61
+ spacing: {
62
+ default: this.options.defaultSpacing,
63
+ parseHTML: element => element.getAttribute("data-spacing") || this.options.defaultSpacing,
64
+ renderHTML: attributes => ({ "data-spacing": attributes.spacing })
65
+ }
66
+ }
67
+ },
68
+
69
+ // Parse & Render
70
+
71
+ parseHTML() {
72
+ return [
73
+ { tag: "section[data-type='section']" },
74
+ { tag: "div.inkpen-section" }
75
+ ]
76
+ },
77
+
78
+ renderHTML({ node, HTMLAttributes }) {
79
+ return [
80
+ "section",
81
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
82
+ "data-type": "section"
83
+ }),
84
+ 0 // Content placeholder
85
+ ]
86
+ },
87
+
88
+ // NodeView (interactive controls)
89
+
90
+ addNodeView() {
91
+ return ({ node, getPos, editor }) => {
92
+ const dom = document.createElement("section")
93
+ dom.className = "inkpen-section"
94
+ dom.setAttribute("data-type", "section")
95
+ dom.setAttribute("data-width", node.attrs.width)
96
+ dom.setAttribute("data-spacing", node.attrs.spacing)
97
+
98
+ // Controls container (only in edit mode)
99
+ let controls = null
100
+ if (this.options.showControls && editor.isEditable) {
101
+ controls = this.createControls(node.attrs, (attr, value) => {
102
+ if (typeof getPos === "function") {
103
+ editor.chain()
104
+ .focus()
105
+ .command(({ tr }) => {
106
+ const pos = getPos()
107
+ if (pos !== undefined) {
108
+ tr.setNodeMarkup(pos, undefined, { ...node.attrs, [attr]: value })
109
+ }
110
+ return true
111
+ })
112
+ .run()
113
+ }
114
+ })
115
+ dom.appendChild(controls)
116
+ }
117
+
118
+ // Content container
119
+ const content = document.createElement("div")
120
+ content.className = "inkpen-section__content"
121
+ dom.appendChild(content)
122
+
123
+ return {
124
+ dom,
125
+ contentDOM: content,
126
+ update: (updatedNode) => {
127
+ if (updatedNode.type !== this.type) return false
128
+
129
+ dom.setAttribute("data-width", updatedNode.attrs.width)
130
+ dom.setAttribute("data-spacing", updatedNode.attrs.spacing)
131
+
132
+ // Update control button states
133
+ if (controls) {
134
+ this.updateControlStates(controls, updatedNode.attrs)
135
+ }
136
+
137
+ return true
138
+ },
139
+ destroy: () => {
140
+ // Cleanup if needed
141
+ }
142
+ }
143
+ }
144
+ },
145
+
146
+ // Commands
147
+
148
+ addCommands() {
149
+ return {
150
+ insertSection: (attributes = {}) => ({ commands }) => {
151
+ return commands.insertContent({
152
+ type: this.name,
153
+ attrs: {
154
+ width: attributes.width || this.options.defaultWidth,
155
+ spacing: attributes.spacing || this.options.defaultSpacing
156
+ },
157
+ content: [{ type: "paragraph" }]
158
+ })
159
+ },
160
+
161
+ setSectionWidth: (width) => ({ commands }) => {
162
+ return commands.updateAttributes(this.name, { width })
163
+ },
164
+
165
+ setSectionSpacing: (spacing) => ({ commands }) => {
166
+ return commands.updateAttributes(this.name, { spacing })
167
+ },
168
+
169
+ wrapInSection: (attributes = {}) => ({ commands }) => {
170
+ return commands.wrapIn(this.name, attributes)
171
+ },
172
+
173
+ unwrapSection: () => ({ commands }) => {
174
+ return commands.lift(this.name)
175
+ }
176
+ }
177
+ },
178
+
179
+ // Keyboard Shortcuts
180
+
181
+ addKeyboardShortcuts() {
182
+ return {
183
+ "Mod-Shift-s": () => this.editor.commands.insertSection()
184
+ }
185
+ },
186
+
187
+ // Private Helpers
188
+
189
+ createControls(attrs, onChange) {
190
+ const controls = document.createElement("div")
191
+ controls.className = "inkpen-section-controls"
192
+ controls.contentEditable = "false"
193
+
194
+ // Width buttons
195
+ const widthGroup = document.createElement("div")
196
+ widthGroup.className = "inkpen-section-controls__group"
197
+
198
+ const widthLabel = document.createElement("span")
199
+ widthLabel.className = "inkpen-section-controls__label"
200
+ widthLabel.textContent = "Width"
201
+ widthGroup.appendChild(widthLabel)
202
+
203
+ Object.entries(this.options.widthPresets).forEach(([key, preset]) => {
204
+ const btn = document.createElement("button")
205
+ btn.type = "button"
206
+ btn.className = "inkpen-section-controls__btn"
207
+ btn.dataset.width = key
208
+ btn.textContent = preset.label
209
+ btn.title = `Set width to ${preset.label}`
210
+
211
+ if (attrs.width === key) {
212
+ btn.classList.add("is-active")
213
+ }
214
+
215
+ btn.addEventListener("mousedown", (e) => e.preventDefault())
216
+ btn.addEventListener("click", (e) => {
217
+ e.preventDefault()
218
+ e.stopPropagation()
219
+ onChange("width", key)
220
+ })
221
+
222
+ widthGroup.appendChild(btn)
223
+ })
224
+
225
+ controls.appendChild(widthGroup)
226
+
227
+ // Divider
228
+ const divider = document.createElement("span")
229
+ divider.className = "inkpen-section-controls__divider"
230
+ controls.appendChild(divider)
231
+
232
+ // Spacing buttons
233
+ const spacingGroup = document.createElement("div")
234
+ spacingGroup.className = "inkpen-section-controls__group"
235
+
236
+ const spacingLabel = document.createElement("span")
237
+ spacingLabel.className = "inkpen-section-controls__label"
238
+ spacingLabel.textContent = "Spacing"
239
+ spacingGroup.appendChild(spacingLabel)
240
+
241
+ Object.entries(this.options.spacingPresets).forEach(([key, preset]) => {
242
+ const btn = document.createElement("button")
243
+ btn.type = "button"
244
+ btn.className = "inkpen-section-controls__btn"
245
+ btn.dataset.spacing = key
246
+ btn.textContent = preset.label
247
+ btn.title = `Set spacing to ${preset.label}`
248
+
249
+ if (attrs.spacing === key) {
250
+ btn.classList.add("is-active")
251
+ }
252
+
253
+ btn.addEventListener("mousedown", (e) => e.preventDefault())
254
+ btn.addEventListener("click", (e) => {
255
+ e.preventDefault()
256
+ e.stopPropagation()
257
+ onChange("spacing", key)
258
+ })
259
+
260
+ spacingGroup.appendChild(btn)
261
+ })
262
+
263
+ controls.appendChild(spacingGroup)
264
+
265
+ return controls
266
+ },
267
+
268
+ updateControlStates(controls, attrs) {
269
+ // Update width buttons
270
+ controls.querySelectorAll("[data-width]").forEach(btn => {
271
+ btn.classList.toggle("is-active", btn.dataset.width === attrs.width)
272
+ })
273
+
274
+ // Update spacing buttons
275
+ controls.querySelectorAll("[data-spacing]").forEach(btn => {
276
+ btn.classList.toggle("is-active", btn.dataset.spacing === attrs.spacing)
277
+ })
278
+ }
279
+ })
280
+
281
+ export default Section
@@ -0,0 +1,126 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core"
2
+
3
+ /**
4
+ * Section Title Extension for TipTap
5
+ *
6
+ * Semantic H2 heading for document sections. Integrates with Table of Contents
7
+ * and provides the title/header for DocumentSection nodes.
8
+ *
9
+ * Features:
10
+ * - Renders as semantic H2 element
11
+ * - Auto-generates ID for deep linking
12
+ * - Integrates with TOC extension
13
+ * - Enter key creates paragraph below (not new title)
14
+ * - Backspace at start of empty title deletes parent section
15
+ *
16
+ * @example
17
+ * // Used within DocumentSection, not standalone
18
+ * { type: "documentSection", content: [{ type: "sectionTitle" }, ...blocks] }
19
+ *
20
+ * @since 0.8.0
21
+ */
22
+ export const SectionTitle = Node.create({
23
+ name: "sectionTitle",
24
+
25
+ content: "inline*",
26
+ defining: true,
27
+ selectable: false,
28
+
29
+ // Options
30
+
31
+ addOptions() {
32
+ return {
33
+ HTMLAttributes: {
34
+ class: "inkpen-doc-section__title"
35
+ }
36
+ }
37
+ },
38
+
39
+ // Attributes
40
+
41
+ addAttributes() {
42
+ return {
43
+ id: {
44
+ default: null,
45
+ parseHTML: element => element.getAttribute("id"),
46
+ renderHTML: attributes => (attributes.id ? { id: attributes.id } : {})
47
+ }
48
+ }
49
+ },
50
+
51
+ // Parse & Render
52
+
53
+ parseHTML() {
54
+ return [
55
+ { tag: 'h2[data-type="section-title"]' },
56
+ { tag: "h2.inkpen-doc-section__title" }
57
+ ]
58
+ },
59
+
60
+ renderHTML({ HTMLAttributes }) {
61
+ return [
62
+ "h2",
63
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
64
+ "data-type": "section-title"
65
+ }),
66
+ 0
67
+ ]
68
+ },
69
+
70
+ // Keyboard Shortcuts
71
+
72
+ addKeyboardShortcuts() {
73
+ return {
74
+ // Enter in title creates content below, not new title
75
+ Enter: ({ editor }) => {
76
+ const { state } = editor
77
+ const { $from } = state.selection
78
+
79
+ if ($from.parent.type.name !== "sectionTitle") {
80
+ return false
81
+ }
82
+
83
+ // Find the documentSection parent
84
+ const sectionPos = $from.before($from.depth - 1)
85
+ const sectionNode = state.doc.nodeAt(sectionPos)
86
+
87
+ if (sectionNode && sectionNode.type.name === "documentSection") {
88
+ // Insert paragraph after title, inside section content
89
+ const titleEnd = sectionPos + 1 + $from.parent.nodeSize
90
+ return editor.chain()
91
+ .insertContentAt(titleEnd, { type: "paragraph" })
92
+ .focus(titleEnd + 1)
93
+ .run()
94
+ }
95
+
96
+ return false
97
+ },
98
+
99
+ // Backspace at start of empty title deletes parent section
100
+ Backspace: ({ editor }) => {
101
+ const { state } = editor
102
+ const { $from, empty } = state.selection
103
+
104
+ if (!empty) return false
105
+
106
+ if ($from.parent.type.name === "sectionTitle" && $from.parentOffset === 0) {
107
+ // If title is empty, delete the whole document section
108
+ if ($from.parent.textContent === "") {
109
+ const sectionPos = $from.before($from.depth - 1)
110
+ const sectionNode = state.doc.nodeAt(sectionPos)
111
+
112
+ if (sectionNode && sectionNode.type.name === "documentSection") {
113
+ return editor.chain()
114
+ .deleteRange({ from: sectionPos, to: sectionPos + sectionNode.nodeSize })
115
+ .run()
116
+ }
117
+ }
118
+ }
119
+
120
+ return false
121
+ }
122
+ }
123
+ }
124
+ })
125
+
126
+ export default SectionTitle