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,756 @@
1
+ # INKPEN Code Samples - CORRECTED (Stimulus Only, No Vue)
2
+
3
+ ## Complete Code Examples - All Stimulus + Vanilla JavaScript
4
+
5
+ ---
6
+
7
+ ## D. Custom Blocks (CORRECTED - No Vue)
8
+
9
+ ### D1: Custom Block - Hero (Stimulus Implementation)
10
+
11
+ ```ruby
12
+ # lib/inkpen/extensions/hero.rb
13
+ # frozen_string_literal: true
14
+
15
+ module Inkpen
16
+ module Extensions
17
+ class Hero < Base
18
+ def name
19
+ :hero
20
+ end
21
+
22
+ def background_image_url
23
+ options[:background_image_url]
24
+ end
25
+
26
+ def headline
27
+ options[:headline]
28
+ end
29
+
30
+ def subheadline
31
+ options[:subheadline]
32
+ end
33
+
34
+ def cta_text
35
+ options[:cta_text]
36
+ end
37
+
38
+ def cta_url
39
+ options[:cta_url]
40
+ end
41
+
42
+ def text_align
43
+ options.fetch(:text_align, "center")
44
+ end
45
+
46
+ def dark_overlay?
47
+ options.fetch(:dark_overlay, false)
48
+ end
49
+
50
+ def to_config
51
+ {
52
+ backgroundImageUrl: background_image_url,
53
+ headline: headline,
54
+ subheadline: subheadline,
55
+ ctaText: cta_text,
56
+ ctaUrl: cta_url,
57
+ textAlign: text_align,
58
+ darkOverlay: dark_overlay?
59
+ }.compact
60
+ end
61
+ end
62
+ end
63
+ end
64
+ ```
65
+
66
+ JavaScript (Pure Stimulus - No Vue):
67
+
68
+ ```javascript
69
+ // app/javascript/extensions/hero.js
70
+ // Pure Stimulus implementation for Hero block
71
+
72
+ import { Node } from "@tiptap/core"
73
+
74
+ export const HeroBlock = Node.create({
75
+ name: "hero",
76
+ group: "block",
77
+ atom: true,
78
+ draggable: true,
79
+ selectable: true,
80
+
81
+ addAttributes() {
82
+ return {
83
+ backgroundImageUrl: { default: null },
84
+ headline: { default: "Your Headline Here" },
85
+ subheadline: { default: "Add a subheadline" },
86
+ ctaText: { default: "Learn More" },
87
+ ctaUrl: { default: "#" },
88
+ textAlign: { default: "center" },
89
+ darkOverlay: { default: false }
90
+ }
91
+ },
92
+
93
+ parseHTML() {
94
+ return [{ tag: "hero-block" }]
95
+ },
96
+
97
+ renderHTML({ HTMLAttributes }) {
98
+ return [
99
+ "div",
100
+ { class: "hero-block", ...HTMLAttributes },
101
+ [
102
+ "style",
103
+ `
104
+ background-image: url('${HTMLAttributes.backgroundImageUrl || ""}');
105
+ text-align: ${HTMLAttributes.textAlign};
106
+ `
107
+ ],
108
+ ["div", { class: "hero-overlay" }],
109
+ [
110
+ "div",
111
+ { class: "hero-content" },
112
+ ["h1", { class: "hero-headline" }, HTMLAttributes.headline],
113
+ ["p", { class: "hero-subheadline" }, HTMLAttributes.subheadline],
114
+ [
115
+ "a",
116
+ { href: HTMLAttributes.ctaUrl, class: "hero-cta" },
117
+ HTMLAttributes.ctaText
118
+ ]
119
+ ]
120
+ ]
121
+ },
122
+
123
+ addNodeView() {
124
+ return {
125
+ dom: this.createDOM(),
126
+ contentDOM: null,
127
+ update: (node, decorations, innerDecorations) => {
128
+ this.updateDOM(node)
129
+ return true
130
+ }
131
+ }
132
+ },
133
+
134
+ createDOM() {
135
+ const dom = document.createElement("div")
136
+ dom.className = "hero-block"
137
+ dom.setAttribute("data-type", "hero")
138
+ return dom
139
+ },
140
+
141
+ updateDOM(node) {
142
+ // Update hero block with new attributes
143
+ const { backgroundImageUrl, headline, subheadline, ctaText, ctaUrl, textAlign, darkOverlay } = node.attrs
144
+
145
+ const heroContent = document.querySelector(".hero-content")
146
+ if (heroContent) {
147
+ heroContent.innerHTML = `
148
+ <h1 class="hero-headline">${headline}</h1>
149
+ <p class="hero-subheadline">${subheadline}</p>
150
+ <a href="${ctaUrl}" class="hero-cta">${ctaText}</a>
151
+ `
152
+ }
153
+ }
154
+ })
155
+ ```
156
+
157
+ ---
158
+
159
+ ## C. Integration Pattern (CORRECTED - Stimulus Only)
160
+
161
+ ### C3: Stimulus Controller (Pure Stimulus - No Vue Dependencies)
162
+
163
+ ```javascript
164
+ // app/javascript/controllers/inkpen/editor_controller.js
165
+ // Pure Stimulus - no Vue.js, no frameworks
166
+
167
+ import { Controller } from "@hotwired/stimulus"
168
+ import { Editor } from "@tiptap/core"
169
+ import StarterKit from "@tiptap/starter-kit"
170
+ import ExtensionsLoader from "../utils/extensions_loader"
171
+
172
+ export default class extends Controller {
173
+ static targets = ["editor", "content", "title", "autosaveIndicator"]
174
+ static values = { features: String, autosave: Boolean }
175
+
176
+ connect() {
177
+ console.log("Inkpen Editor Controller Connected")
178
+ this.initEditor()
179
+ }
180
+
181
+ async initEditor() {
182
+ try {
183
+ const config = await this.fetchExtensionsConfig()
184
+ const extensions = await ExtensionsLoader.load(config)
185
+
186
+ this.editor = new Editor({
187
+ element: this.editorTarget,
188
+ extensions: [
189
+ StarterKit.configure({
190
+ heading: { levels: [1, 2, 3, 4, 5, 6] }
191
+ }),
192
+ ...extensions
193
+ ],
194
+ content: this.contentTarget.value || "",
195
+ onUpdate: ({ editor }) => {
196
+ this.contentTarget.value = editor.getHTML()
197
+
198
+ if (this.autosaveValue) {
199
+ this.debounce(() => this.autosave(), 1500)
200
+ }
201
+ },
202
+ onSelectionUpdate: ({ editor }) => {
203
+ this.updateToolbarState(editor)
204
+ }
205
+ })
206
+
207
+ console.log("Editor initialized successfully")
208
+ this.attachEditorListeners()
209
+
210
+ } catch (error) {
211
+ console.error("Failed to initialize editor:", error)
212
+ this.showError("Failed to load editor: " + error.message)
213
+ }
214
+ }
215
+
216
+ async fetchExtensionsConfig() {
217
+ const response = await fetch(`/inkpen/extensions/${this.featuresValue}.json`)
218
+ if (!response.ok) {
219
+ throw new Error(`Failed to fetch extensions: ${response.status}`)
220
+ }
221
+ return await response.json()
222
+ }
223
+
224
+ updateToolbarState(editor) {
225
+ // Update toolbar button states based on editor state
226
+ document.querySelectorAll("[data-command]").forEach(btn => {
227
+ const command = btn.dataset.command
228
+ const isActive = this.isCommandActive(editor, command)
229
+
230
+ if (isActive) {
231
+ btn.classList.add("is-active")
232
+ } else {
233
+ btn.classList.remove("is-active")
234
+ }
235
+ })
236
+ }
237
+
238
+ isCommandActive(editor, command) {
239
+ const [name, attr] = command.split(":")
240
+
241
+ if (name === "bold") return editor.isActive("bold")
242
+ if (name === "italic") return editor.isActive("italic")
243
+ if (name === "underline") return editor.isActive("underline")
244
+ if (name === "strike") return editor.isActive("strike")
245
+ if (name === "heading") {
246
+ const level = parseInt(attr)
247
+ return editor.isActive("heading", { level })
248
+ }
249
+ if (name === "code") return editor.isActive("code")
250
+ if (name === "codeBlock") return editor.isActive("codeBlock")
251
+ if (name === "blockquote") return editor.isActive("blockquote")
252
+ if (name === "bulletList") return editor.isActive("bulletList")
253
+ if (name === "orderedList") return editor.isActive("orderedList")
254
+ if (name === "taskList") return editor.isActive("taskList")
255
+
256
+ return false
257
+ }
258
+
259
+ autosave() {
260
+ if (!this.form) return
261
+
262
+ const formData = new FormData(this.form)
263
+
264
+ fetch(this.form.action, {
265
+ method: "PATCH",
266
+ body: formData,
267
+ headers: {
268
+ "X-CSRF-Token": this.csrfToken,
269
+ "Accept": "application/json"
270
+ }
271
+ })
272
+ .then(r => {
273
+ if (r.ok) {
274
+ this.showAutosaveIndicator()
275
+ } else {
276
+ console.warn("Autosave returned status:", r.status)
277
+ }
278
+ })
279
+ .catch(e => console.error("Autosave failed:", e))
280
+ }
281
+
282
+ showAutosaveIndicator() {
283
+ const indicator = this.autosaveIndicatorTarget
284
+ if (!indicator) return
285
+
286
+ indicator.style.display = "block"
287
+ indicator.textContent = "✓ Saved"
288
+
289
+ setTimeout(() => {
290
+ indicator.style.display = "none"
291
+ }, 2000)
292
+ }
293
+
294
+ showError(message) {
295
+ const errorDiv = document.createElement("div")
296
+ errorDiv.className = "inkpen-error"
297
+ errorDiv.textContent = message
298
+ this.editorTarget.parentNode.insertBefore(errorDiv, this.editorTarget)
299
+
300
+ setTimeout(() => errorDiv.remove(), 5000)
301
+ }
302
+
303
+ attachEditorListeners() {
304
+ // Attach any custom listeners here
305
+ this.editor.on("update", () => {
306
+ console.log("Editor content updated")
307
+ })
308
+ }
309
+
310
+ disconnect() {
311
+ if (this.editor) {
312
+ this.editor.destroy()
313
+ }
314
+ clearTimeout(this.debounceTimer)
315
+ }
316
+
317
+ debounce(fn, delay) {
318
+ clearTimeout(this.debounceTimer)
319
+ this.debounceTimer = setTimeout(fn, delay)
320
+ }
321
+
322
+ get form() {
323
+ return this.element.closest("form")
324
+ }
325
+
326
+ get csrfToken() {
327
+ return document.querySelector('meta[name="csrf-token"]')?.content || ""
328
+ }
329
+ }
330
+ ```
331
+
332
+ ### C4: Extensions Loader (Pure Vanilla JavaScript - No Vue)
333
+
334
+ ```javascript
335
+ // app/javascript/utils/extensions_loader.js
336
+ // Pure vanilla JavaScript - no framework dependencies
337
+
338
+ import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight"
339
+ import { lowlight } from "lowlight"
340
+ import { TaskList } from "@tiptap/extension-task-list"
341
+ import { TaskItem } from "@tiptap/extension-task-item"
342
+ import { Table } from "@tiptap/extension-table"
343
+ import { TableRow } from "@tiptap/extension-table-row"
344
+ import { TableHeader } from "@tiptap/extension-table-header"
345
+ import { TableCell } from "@tiptap/extension-table-cell"
346
+ import { Mention } from "@tiptap/extension-mention"
347
+ import { SlashCommand } from "@tiptap/extension-slash-command"
348
+ import tippy from "tippy.js"
349
+
350
+ export default class ExtensionsLoader {
351
+ static async load(config) {
352
+ const extensions = []
353
+
354
+ for (const extConfig of config.extensions) {
355
+ const ext = await this.loadExtension(extConfig)
356
+ if (ext) {
357
+ if (Array.isArray(ext)) {
358
+ extensions.push(...ext)
359
+ } else {
360
+ extensions.push(ext)
361
+ }
362
+ }
363
+ }
364
+
365
+ return extensions
366
+ }
367
+
368
+ static async loadExtension(config) {
369
+ switch (config.name) {
370
+ case "forced_document":
371
+ return this.configureForcedDocument(config)
372
+
373
+ case "code_block_syntax":
374
+ return this.configureCodeBlock(config)
375
+
376
+ case "task_list":
377
+ return this.configureTaskList(config)
378
+
379
+ case "table":
380
+ return this.configureTable(config)
381
+
382
+ case "mention":
383
+ return this.configureMention(config)
384
+
385
+ case "slash_commands":
386
+ return this.configureSlashCommands(config)
387
+
388
+ default:
389
+ console.warn(`Unknown extension: ${config.name}`)
390
+ return null
391
+ }
392
+ }
393
+
394
+ static configureForcedDocument(config) {
395
+ const ForcedDocument = require("@tiptap/extension-forced-document").default
396
+
397
+ return ForcedDocument.configure({
398
+ titleLevel: config.config.titleLevel,
399
+ titlePlaceholder: config.config.titlePlaceholder,
400
+ subtitle: config.config.subtitle,
401
+ subtitleLevel: config.config.subtitleLevel,
402
+ subtitlePlaceholder: config.config.subtitlePlaceholder,
403
+ allowDeletion: config.config.allowDeletion
404
+ })
405
+ }
406
+
407
+ static configureCodeBlock(config) {
408
+ const languages = {}
409
+
410
+ // Dynamically load language modules
411
+ for (const lang of config.config.languages) {
412
+ try {
413
+ languages[lang] = require(`highlight.js/lib/languages/${lang}`).default
414
+ } catch (e) {
415
+ console.warn(`Language not found: ${lang}`)
416
+ }
417
+ }
418
+
419
+ return CodeBlockLowlight.configure({
420
+ lowlight,
421
+ languages,
422
+ defaultLanguage: config.config.defaultLanguage || "javascript"
423
+ })
424
+ }
425
+
426
+ static configureTaskList(config) {
427
+ return [
428
+ TaskList.configure({
429
+ nested: config.config.nested,
430
+ textToggle: config.config.textToggle,
431
+ keyboardShortcut: config.config.keyboardShortcut
432
+ }),
433
+ TaskItem.configure({
434
+ nested: config.config.nested
435
+ })
436
+ ]
437
+ }
438
+
439
+ static configureTable(config) {
440
+ return [
441
+ Table.configure({
442
+ resizable: config.config.resizable,
443
+ handleWidth: 4,
444
+ cellMinWidth: config.config.cellMinWidth,
445
+ lastColumnResizable: true,
446
+ allowTableNodeSelection: false
447
+ }),
448
+ TableRow,
449
+ TableHeader,
450
+ TableCell
451
+ ]
452
+ }
453
+
454
+ static configureMention(config) {
455
+ return Mention.configure({
456
+ HTMLAttributes: { class: "mention" },
457
+
458
+ suggestion: {
459
+ items: async ({ query }) => {
460
+ if (!query) return []
461
+
462
+ try {
463
+ const response = await fetch(
464
+ `${config.config.searchUrl}?query=${encodeURIComponent(query)}`
465
+ )
466
+ if (!response.ok) return []
467
+ return await response.json()
468
+ } catch (e) {
469
+ console.error("Mention search failed:", e)
470
+ return []
471
+ }
472
+ },
473
+
474
+ render: () => {
475
+ let mentionList = null
476
+ let popup = null
477
+
478
+ return {
479
+ onStart: props => {
480
+ // Create mention list using vanilla JavaScript
481
+ mentionList = this.createMentionList(props)
482
+
483
+ popup = tippy("body", {
484
+ getReferenceClientRect: props.clientRect,
485
+ appendTo: () => document.body,
486
+ content: mentionList.element,
487
+ showOnCreate: true,
488
+ interactive: true,
489
+ trigger: "manual",
490
+ placement: "bottom-start"
491
+ })[0]
492
+ },
493
+
494
+ onUpdate(props) {
495
+ if (mentionList) {
496
+ mentionList.update(props)
497
+ }
498
+ },
499
+
500
+ onKeyDown(props) {
501
+ if (mentionList) {
502
+ return mentionList.onKeyDown(props)
503
+ }
504
+ return false
505
+ },
506
+
507
+ onExit() {
508
+ if (popup) popup.destroy()
509
+ if (mentionList) mentionList.destroy()
510
+ }
511
+ }
512
+ }
513
+ }
514
+ })
515
+ }
516
+
517
+ static createMentionList(props) {
518
+ const element = document.createElement("div")
519
+ element.className = "mention-list"
520
+
521
+ let selectedIndex = 0
522
+
523
+ const update = (newProps) => {
524
+ selectedIndex = 0
525
+
526
+ element.innerHTML = ""
527
+
528
+ if (newProps.items.length === 0) {
529
+ const emptyItem = document.createElement("div")
530
+ emptyItem.className = "mention-item empty"
531
+ emptyItem.textContent = "No users found"
532
+ element.appendChild(emptyItem)
533
+ return
534
+ }
535
+
536
+ newProps.items.forEach((item, index) => {
537
+ const itemEl = document.createElement("div")
538
+ itemEl.className = "mention-item"
539
+ if (index === selectedIndex) itemEl.classList.add("is-selected")
540
+
541
+ itemEl.innerHTML = `
542
+ <div class="mention-item-avatar">${item.avatar ? `<img src="${item.avatar}" />` : "?"}</div>
543
+ <div class="mention-item-name">${item.label}</div>
544
+ `
545
+
546
+ itemEl.addEventListener("click", () => {
547
+ newProps.command({ id: item.id, label: item.label })
548
+ })
549
+
550
+ element.appendChild(itemEl)
551
+ })
552
+ }
553
+
554
+ const onKeyDown = (event) => {
555
+ if (event.key === "ArrowUp") {
556
+ selectedIndex = (selectedIndex - 1 + element.children.length) % element.children.length
557
+ updateSelection()
558
+ return true
559
+ }
560
+
561
+ if (event.key === "ArrowDown") {
562
+ selectedIndex = (selectedIndex + 1) % element.children.length
563
+ updateSelection()
564
+ return true
565
+ }
566
+
567
+ if (event.key === "Enter") {
568
+ const selectedItem = element.children[selectedIndex]
569
+ if (selectedItem) {
570
+ selectedItem.click()
571
+ }
572
+ return true
573
+ }
574
+
575
+ return false
576
+ }
577
+
578
+ const updateSelection = () => {
579
+ element.querySelectorAll(".mention-item").forEach((item, index) => {
580
+ if (index === selectedIndex) {
581
+ item.classList.add("is-selected")
582
+ item.scrollIntoView({ block: "nearest" })
583
+ } else {
584
+ item.classList.remove("is-selected")
585
+ }
586
+ })
587
+ }
588
+
589
+ const destroy = () => {
590
+ element.remove()
591
+ }
592
+
593
+ update(props)
594
+
595
+ return { element, update, onKeyDown, destroy }
596
+ }
597
+
598
+ static configureSlashCommands(config) {
599
+ const commands = config.config.commands
600
+
601
+ return SlashCommand.configure({
602
+ suggestion: {
603
+ items: ({ query }) => {
604
+ return commands
605
+ .filter(cmd =>
606
+ cmd.label.toLowerCase().startsWith(query.toLowerCase()) ||
607
+ cmd.description.toLowerCase().includes(query.toLowerCase())
608
+ )
609
+ .slice(0, config.config.maxSuggestions)
610
+ },
611
+
612
+ render: () => {
613
+ let commandList = null
614
+ let popup = null
615
+
616
+ return {
617
+ onStart: props => {
618
+ // Create command palette using vanilla JavaScript
619
+ commandList = this.createCommandPalette(props)
620
+
621
+ popup = tippy("body", {
622
+ getReferenceClientRect: props.clientRect,
623
+ appendTo: () => document.body,
624
+ content: commandList.element,
625
+ showOnCreate: true,
626
+ interactive: true,
627
+ trigger: "manual",
628
+ placement: "bottom-start"
629
+ })[0]
630
+ },
631
+
632
+ onUpdate(props) {
633
+ if (commandList) {
634
+ commandList.update(props)
635
+ }
636
+ },
637
+
638
+ onKeyDown(props) {
639
+ if (commandList) {
640
+ return commandList.onKeyDown(props)
641
+ }
642
+ return false
643
+ },
644
+
645
+ onExit() {
646
+ if (popup) popup.destroy()
647
+ if (commandList) commandList.destroy()
648
+ }
649
+ }
650
+ }
651
+ }
652
+ })
653
+ }
654
+
655
+ static createCommandPalette(props) {
656
+ const element = document.createElement("div")
657
+ element.className = "command-palette"
658
+
659
+ let selectedIndex = 0
660
+ let items = []
661
+
662
+ const update = (newProps) => {
663
+ selectedIndex = 0
664
+ items = newProps.items
665
+
666
+ element.innerHTML = ""
667
+
668
+ if (items.length === 0) {
669
+ const emptyItem = document.createElement("div")
670
+ emptyItem.className = "command-item empty"
671
+ emptyItem.textContent = "No commands found"
672
+ element.appendChild(emptyItem)
673
+ return
674
+ }
675
+
676
+ items.forEach((cmd, index) => {
677
+ const itemEl = document.createElement("div")
678
+ itemEl.className = "command-item"
679
+ if (index === selectedIndex) itemEl.classList.add("is-selected")
680
+
681
+ itemEl.innerHTML = `
682
+ <div class="command-icon">${cmd.icon ? `📝` : "•"}</div>
683
+ <div class="command-info">
684
+ <div class="command-name">${cmd.label}</div>
685
+ <div class="command-desc">${cmd.description}</div>
686
+ </div>
687
+ ${cmd.shortcut ? `<div class="command-shortcut">${cmd.shortcut}</div>` : ""}
688
+ `
689
+
690
+ itemEl.addEventListener("click", () => {
691
+ newProps.command({ name: cmd.name })
692
+ })
693
+
694
+ element.appendChild(itemEl)
695
+ })
696
+ }
697
+
698
+ const onKeyDown = (event) => {
699
+ if (event.key === "ArrowUp") {
700
+ selectedIndex = (selectedIndex - 1 + element.children.length) % element.children.length
701
+ updateSelection()
702
+ return true
703
+ }
704
+
705
+ if (event.key === "ArrowDown") {
706
+ selectedIndex = (selectedIndex + 1) % element.children.length
707
+ updateSelection()
708
+ return true
709
+ }
710
+
711
+ if (event.key === "Enter") {
712
+ const selectedItem = element.children[selectedIndex]
713
+ if (selectedItem && !selectedItem.classList.contains("empty")) {
714
+ selectedItem.click()
715
+ }
716
+ return true
717
+ }
718
+
719
+ return false
720
+ }
721
+
722
+ const updateSelection = () => {
723
+ element.querySelectorAll(".command-item").forEach((item, index) => {
724
+ if (index === selectedIndex) {
725
+ item.classList.add("is-selected")
726
+ item.scrollIntoView({ block: "nearest" })
727
+ } else {
728
+ item.classList.remove("is-selected")
729
+ }
730
+ })
731
+ }
732
+
733
+ const destroy = () => {
734
+ element.remove()
735
+ }
736
+
737
+ update(props)
738
+
739
+ return { element, update, onKeyDown, destroy }
740
+ }
741
+ }
742
+ ```
743
+
744
+ ---
745
+
746
+ ## Key Changes Made:
747
+
748
+ ✅ **Removed all Vue.js references**
749
+ ✅ **Removed VueNodeViewRenderer**
750
+ ✅ **Removed Vue component files**
751
+ ✅ **Using pure vanilla JavaScript for dropdowns**
752
+ ✅ **Using Stimulus for state management**
753
+ ✅ **Custom list components using DOM API**
754
+ ✅ **All interactive elements built with vanilla JS**
755
+
756
+ You were 100% correct - this is **pure Stimulus + vanilla JavaScript throughout**. No Vue, no component frameworks. Everything uses vanilla DOM manipulation.