active_canvas 0.0.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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +318 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/active_canvas/editor/ai_panel.js +1607 -0
  6. data/app/assets/javascripts/active_canvas/editor/asset_manager.js +498 -0
  7. data/app/assets/javascripts/active_canvas/editor/blocks.js +1083 -0
  8. data/app/assets/javascripts/active_canvas/editor/code_panel.js +572 -0
  9. data/app/assets/javascripts/active_canvas/editor/component_toolbar.js +394 -0
  10. data/app/assets/javascripts/active_canvas/editor/panels.js +460 -0
  11. data/app/assets/javascripts/active_canvas/editor/utils.js +56 -0
  12. data/app/assets/javascripts/active_canvas/editor.js +295 -0
  13. data/app/assets/stylesheets/active_canvas/application.css +15 -0
  14. data/app/assets/stylesheets/active_canvas/editor.css +2929 -0
  15. data/app/controllers/active_canvas/admin/ai_controller.rb +181 -0
  16. data/app/controllers/active_canvas/admin/application_controller.rb +56 -0
  17. data/app/controllers/active_canvas/admin/media_controller.rb +61 -0
  18. data/app/controllers/active_canvas/admin/page_types_controller.rb +57 -0
  19. data/app/controllers/active_canvas/admin/page_versions_controller.rb +23 -0
  20. data/app/controllers/active_canvas/admin/pages_controller.rb +133 -0
  21. data/app/controllers/active_canvas/admin/partials_controller.rb +88 -0
  22. data/app/controllers/active_canvas/admin/settings_controller.rb +256 -0
  23. data/app/controllers/active_canvas/application_controller.rb +20 -0
  24. data/app/controllers/active_canvas/pages_controller.rb +18 -0
  25. data/app/controllers/concerns/active_canvas/current_user.rb +12 -0
  26. data/app/controllers/concerns/active_canvas/rate_limitable.rb +75 -0
  27. data/app/controllers/concerns/active_canvas/tailwind_compilation.rb +39 -0
  28. data/app/helpers/active_canvas/application_helper.rb +4 -0
  29. data/app/jobs/active_canvas/application_job.rb +4 -0
  30. data/app/jobs/active_canvas/compile_tailwind_job.rb +64 -0
  31. data/app/mailers/active_canvas/application_mailer.rb +6 -0
  32. data/app/models/active_canvas/ai_model.rb +136 -0
  33. data/app/models/active_canvas/application_record.rb +5 -0
  34. data/app/models/active_canvas/media.rb +141 -0
  35. data/app/models/active_canvas/page.rb +85 -0
  36. data/app/models/active_canvas/page_type.rb +22 -0
  37. data/app/models/active_canvas/page_version.rb +80 -0
  38. data/app/models/active_canvas/partial.rb +73 -0
  39. data/app/models/active_canvas/setting.rb +292 -0
  40. data/app/services/active_canvas/ai_configuration.rb +40 -0
  41. data/app/services/active_canvas/ai_models.rb +128 -0
  42. data/app/services/active_canvas/ai_service.rb +289 -0
  43. data/app/services/active_canvas/content_sanitizer.rb +112 -0
  44. data/app/services/active_canvas/tailwind_compiler.rb +156 -0
  45. data/app/views/active_canvas/admin/media/index.html.erb +401 -0
  46. data/app/views/active_canvas/admin/media/show.html.erb +297 -0
  47. data/app/views/active_canvas/admin/page_types/_form.html.erb +25 -0
  48. data/app/views/active_canvas/admin/page_types/edit.html.erb +13 -0
  49. data/app/views/active_canvas/admin/page_types/index.html.erb +29 -0
  50. data/app/views/active_canvas/admin/page_types/new.html.erb +9 -0
  51. data/app/views/active_canvas/admin/page_types/show.html.erb +18 -0
  52. data/app/views/active_canvas/admin/page_versions/show.html.erb +469 -0
  53. data/app/views/active_canvas/admin/pages/_form.html.erb +62 -0
  54. data/app/views/active_canvas/admin/pages/content.html.erb +139 -0
  55. data/app/views/active_canvas/admin/pages/edit.html.erb +335 -0
  56. data/app/views/active_canvas/admin/pages/editor.html.erb +710 -0
  57. data/app/views/active_canvas/admin/pages/index.html.erb +149 -0
  58. data/app/views/active_canvas/admin/pages/new.html.erb +19 -0
  59. data/app/views/active_canvas/admin/pages/show.html.erb +258 -0
  60. data/app/views/active_canvas/admin/pages/versions.html.erb +333 -0
  61. data/app/views/active_canvas/admin/partials/edit.html.erb +182 -0
  62. data/app/views/active_canvas/admin/partials/editor.html.erb +703 -0
  63. data/app/views/active_canvas/admin/partials/index.html.erb +131 -0
  64. data/app/views/active_canvas/admin/settings/show.html.erb +1864 -0
  65. data/app/views/active_canvas/pages/no_homepage.html.erb +45 -0
  66. data/app/views/active_canvas/pages/show.html.erb +113 -0
  67. data/app/views/layouts/active_canvas/admin/application.html.erb +960 -0
  68. data/app/views/layouts/active_canvas/admin/editor.html.erb +826 -0
  69. data/app/views/layouts/active_canvas/application.html.erb +55 -0
  70. data/config/routes.rb +48 -0
  71. data/db/migrate/20260202000001_create_active_canvas_tables.rb +113 -0
  72. data/db/migrate/20260202000002_create_active_canvas_ai_models.rb +26 -0
  73. data/lib/active_canvas/configuration.rb +232 -0
  74. data/lib/active_canvas/engine.rb +44 -0
  75. data/lib/active_canvas/version.rb +3 -0
  76. data/lib/active_canvas.rb +26 -0
  77. data/lib/generators/active_canvas/install/install_generator.rb +263 -0
  78. data/lib/generators/active_canvas/install/templates/initializer.rb.tt +163 -0
  79. data/lib/tasks/active_canvas_tasks.rake +69 -0
  80. metadata +150 -0
@@ -0,0 +1,394 @@
1
+ /**
2
+ * ActiveCanvas Editor - Component Toolbar and Context Menu
3
+ */
4
+
5
+ (function() {
6
+ 'use strict';
7
+
8
+ window.ActiveCanvasEditor = window.ActiveCanvasEditor || {};
9
+
10
+ /**
11
+ * Setup component code editing and toolbar
12
+ * @param {Object} editor - GrapeJS editor instance
13
+ */
14
+ function setupComponentToolbar(editor) {
15
+ const { showToast } = window.ActiveCanvasEditor;
16
+
17
+ // Define default toolbar for all components
18
+ const defaultToolbar = [
19
+ {
20
+ id: 'edit-code',
21
+ label: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
22
+ attributes: { class: 'toolbar-icon-btn', title: 'Edit Code' },
23
+ command: 'edit-component-code'
24
+ },
25
+ {
26
+ id: 'edit-ai',
27
+ label: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>',
28
+ attributes: { class: 'toolbar-icon-btn toolbar-ai-btn', title: 'Edit with AI' },
29
+ command: 'edit-component-ai'
30
+ },
31
+ {
32
+ id: 'component-menu',
33
+ label: '⋮',
34
+ attributes: { class: 'toolbar-menu-btn', title: 'More Actions' },
35
+ command: 'ac-show-menu'
36
+ }
37
+ ];
38
+
39
+ // Add toolbar buttons on component selection
40
+ editor.on('component:selected', (component) => {
41
+ if (!component) return;
42
+
43
+ let toolbar = component.get('toolbar') || [];
44
+
45
+ // Ensure toolbar is an array and make a copy
46
+ if (!Array.isArray(toolbar)) {
47
+ toolbar = [];
48
+ } else {
49
+ toolbar = [...toolbar];
50
+ }
51
+
52
+ // Check if our buttons already exist
53
+ const hasCodeBtn = toolbar.some(item => item.id === 'edit-code' || item.command === 'edit-component-code');
54
+ const hasAiBtn = toolbar.some(item => item.id === 'edit-ai' || item.command === 'edit-component-ai');
55
+ const hasMenuBtn = toolbar.some(item => item.id === 'component-menu' || item.command === 'ac-show-menu');
56
+
57
+ if (!hasCodeBtn) {
58
+ toolbar.unshift(defaultToolbar[0]);
59
+ }
60
+
61
+ if (!hasAiBtn) {
62
+ toolbar.splice(1, 0, defaultToolbar[1]);
63
+ }
64
+
65
+ if (!hasMenuBtn) {
66
+ toolbar.push(defaultToolbar[2]);
67
+ }
68
+
69
+ component.set('toolbar', toolbar);
70
+ });
71
+
72
+ // Add command for editing component code
73
+ editor.Commands.add('edit-component-code', {
74
+ run(editor) {
75
+ const selected = editor.getSelected();
76
+ if (!selected) return;
77
+
78
+ window.ActiveCanvasEditor.openComponentCodeEditor(selected);
79
+ }
80
+ });
81
+
82
+ // Add command for editing with AI
83
+ editor.Commands.add('edit-component-ai', {
84
+ run(editor) {
85
+ const selected = editor.getSelected();
86
+ if (!selected) return;
87
+
88
+ // Open AI panel
89
+ const panelAi = document.getElementById('panel-ai');
90
+ const btnToggleAi = document.getElementById('btn-toggle-ai');
91
+ if (panelAi && panelAi.classList.contains('collapsed')) {
92
+ panelAi.classList.remove('collapsed');
93
+ if (btnToggleAi) btnToggleAi.classList.add('active');
94
+ }
95
+
96
+ // Switch to Element mode
97
+ const elementModeBtn = document.querySelector('.ai-mode-btn[data-mode="element"]');
98
+ if (elementModeBtn && !elementModeBtn.classList.contains('active')) {
99
+ elementModeBtn.click();
100
+ }
101
+
102
+ // Focus the prompt input
103
+ setTimeout(() => {
104
+ const promptInput = document.getElementById('ai-text-prompt');
105
+ if (promptInput) promptInput.focus();
106
+ }, 100);
107
+ }
108
+ });
109
+
110
+ // Helper to cleanup menu
111
+ const cleanupComponentMenu = () => {
112
+ const existingMenu = document.querySelector('.component-context-menu');
113
+ if (existingMenu) {
114
+ existingMenu.remove();
115
+ }
116
+ };
117
+
118
+ // Close menu when selection changes
119
+ editor.on('component:selected', cleanupComponentMenu);
120
+ editor.on('component:deselected', cleanupComponentMenu);
121
+
122
+ // Function to show the component menu
123
+ const showComponentMenu = () => {
124
+ const selected = editor.getSelected();
125
+ if (!selected) return;
126
+
127
+ // Always cleanup first
128
+ cleanupComponentMenu();
129
+
130
+ // Get position - use the toolbar
131
+ let btnRect = null;
132
+ const toolbarEl = document.querySelector('.gjs-toolbar');
133
+
134
+ if (toolbarEl) {
135
+ const allItems = toolbarEl.querySelectorAll('.gjs-toolbar-item');
136
+ const menuBtn = allItems[allItems.length - 1];
137
+ btnRect = menuBtn ? menuBtn.getBoundingClientRect() : toolbarEl.getBoundingClientRect();
138
+ }
139
+
140
+ // Final fallback: center of screen
141
+ if (!btnRect) {
142
+ btnRect = {
143
+ top: 100,
144
+ bottom: 120,
145
+ left: window.innerWidth / 2 - 80,
146
+ right: window.innerWidth / 2 + 80
147
+ };
148
+ }
149
+
150
+ // Create dropdown menu
151
+ const menu = document.createElement('div');
152
+ menu.className = 'component-context-menu';
153
+ menu.innerHTML = `
154
+ <div class="context-menu-item" data-action="edit-ai">
155
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
156
+ <path d="M12 8V4H8"/>
157
+ <rect width="16" height="12" x="4" y="8" rx="2"/>
158
+ <path d="M2 14h2"/>
159
+ <path d="M20 14h2"/>
160
+ <path d="M15 13v2"/>
161
+ <path d="M9 13v2"/>
162
+ </svg>
163
+ <span>Edit with AI</span>
164
+ </div>
165
+ <div class="context-menu-item" data-action="open-styles">
166
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
167
+ <path d="M12 19l7-7 3 3-7 7-3-3z"></path>
168
+ <path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path>
169
+ <path d="M2 2l7.586 7.586"></path>
170
+ <circle cx="11" cy="11" r="2"></circle>
171
+ </svg>
172
+ <span>Edit Styles</span>
173
+ </div>
174
+ <div class="context-menu-divider"></div>
175
+ <div class="context-menu-item" data-action="duplicate">
176
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
177
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
178
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
179
+ </svg>
180
+ <span>Duplicate</span>
181
+ </div>
182
+ <div class="context-menu-item" data-action="add-div-above">
183
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
184
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
185
+ <line x1="12" y1="8" x2="12" y2="16"></line>
186
+ <line x1="8" y1="12" x2="16" y2="12"></line>
187
+ </svg>
188
+ <span>Add Div Above</span>
189
+ </div>
190
+ <div class="context-menu-item" data-action="add-div-below">
191
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
192
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
193
+ <line x1="12" y1="8" x2="12" y2="16"></line>
194
+ <line x1="8" y1="12" x2="16" y2="12"></line>
195
+ </svg>
196
+ <span>Add Div Below</span>
197
+ </div>
198
+ <div class="context-menu-divider"></div>
199
+ <div class="context-menu-item" data-action="move-up">
200
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
201
+ <polyline points="18 15 12 9 6 15"></polyline>
202
+ </svg>
203
+ <span>Move Up</span>
204
+ </div>
205
+ <div class="context-menu-item" data-action="move-down">
206
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
207
+ <polyline points="6 9 12 15 18 9"></polyline>
208
+ </svg>
209
+ <span>Move Down</span>
210
+ </div>
211
+ <div class="context-menu-divider"></div>
212
+ <div class="context-menu-item context-menu-item-danger" data-action="delete">
213
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
214
+ <polyline points="3 6 5 6 21 6"></polyline>
215
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
216
+ </svg>
217
+ <span>Delete</span>
218
+ </div>
219
+ `;
220
+
221
+ // Position the menu
222
+ menu.style.position = 'fixed';
223
+ menu.style.zIndex = '10000';
224
+
225
+ document.body.appendChild(menu);
226
+
227
+ // Get menu dimensions after adding to DOM
228
+ const menuRect = menu.getBoundingClientRect();
229
+
230
+ // Position below the button, aligned to the right edge
231
+ let top = btnRect.bottom + 5;
232
+ let left = btnRect.right - menuRect.width;
233
+
234
+ // Keep menu on screen
235
+ if (left < 10) left = 10;
236
+ if (top + menuRect.height > window.innerHeight - 10) {
237
+ top = btnRect.top - menuRect.height - 5;
238
+ }
239
+
240
+ menu.style.top = top + 'px';
241
+ menu.style.left = left + 'px';
242
+
243
+ // Handle menu item clicks
244
+ menu.querySelectorAll('.context-menu-item').forEach(item => {
245
+ item.addEventListener('click', (e) => {
246
+ e.preventDefault();
247
+ e.stopPropagation();
248
+ const action = item.dataset.action;
249
+ cleanupComponentMenu();
250
+ handleComponentAction(editor, selected, action);
251
+ });
252
+ });
253
+
254
+ // Close menu on click outside (with delay)
255
+ setTimeout(() => {
256
+ const closeHandler = (e) => {
257
+ if (menu.contains(e.target)) return;
258
+ if (e.target.closest('.toolbar-menu-btn') || e.target.textContent === '⋮') return;
259
+ cleanupComponentMenu();
260
+ document.removeEventListener('mousedown', closeHandler);
261
+ };
262
+ document.addEventListener('mousedown', closeHandler);
263
+ }, 50);
264
+ };
265
+
266
+ // Add command that just calls our function
267
+ editor.Commands.add('ac-show-menu', {
268
+ run() {
269
+ showComponentMenu();
270
+ }
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Handle component context menu actions
276
+ */
277
+ function handleComponentAction(editor, component, action) {
278
+ const { showToast } = window.ActiveCanvasEditor;
279
+
280
+ if (!component) return;
281
+
282
+ const parent = component.parent();
283
+
284
+ switch (action) {
285
+ case 'edit-ai':
286
+ // Open AI panel in element mode
287
+ editor.runCommand('edit-component-ai');
288
+ break;
289
+
290
+ case 'open-styles':
291
+ // Ensure the component is selected
292
+ editor.select(component);
293
+
294
+ // Open the right panel if collapsed
295
+ const panelRight = document.getElementById('panel-right');
296
+ const btnToggleRight = document.getElementById('btn-toggle-right');
297
+ if (panelRight && panelRight.classList.contains('collapsed')) {
298
+ panelRight.classList.remove('collapsed');
299
+ if (btnToggleRight) btnToggleRight.classList.add('active');
300
+ }
301
+
302
+ // Switch to the Styles tab
303
+ const stylesTab = document.querySelector('.editor-panel-right .panel-tab[data-panel="styles"]');
304
+ if (stylesTab) {
305
+ stylesTab.click();
306
+ }
307
+
308
+ // Scroll the style manager into view if needed
309
+ const stylesContainer = document.getElementById('styles-container');
310
+ if (stylesContainer) {
311
+ stylesContainer.style.display = 'block';
312
+ // Hide traits container
313
+ const traitsContainer = document.getElementById('traits-container');
314
+ if (traitsContainer) traitsContainer.style.display = 'none';
315
+ }
316
+
317
+ // Refresh the editor to update the style manager
318
+ setTimeout(() => editor.refresh(), 100);
319
+
320
+ showToast('Style panel opened', 'success');
321
+ break;
322
+
323
+ case 'duplicate':
324
+ const cloned = component.clone();
325
+ if (parent) {
326
+ const index = parent.components().indexOf(component);
327
+ parent.components().add(cloned, { at: index + 1 });
328
+ editor.select(cloned);
329
+ showToast('Component duplicated', 'success');
330
+ }
331
+ break;
332
+
333
+ case 'add-div-above':
334
+ if (parent) {
335
+ const index = parent.components().indexOf(component);
336
+ const newDiv = parent.components().add({
337
+ tagName: 'div',
338
+ style: { padding: '1rem', 'min-height': '50px' },
339
+ content: ''
340
+ }, { at: index });
341
+ editor.select(newDiv);
342
+ showToast('Div added above', 'success');
343
+ }
344
+ break;
345
+
346
+ case 'add-div-below':
347
+ if (parent) {
348
+ const index = parent.components().indexOf(component);
349
+ const newDiv = parent.components().add({
350
+ tagName: 'div',
351
+ style: { padding: '1rem', 'min-height': '50px' },
352
+ content: ''
353
+ }, { at: index + 1 });
354
+ editor.select(newDiv);
355
+ showToast('Div added below', 'success');
356
+ }
357
+ break;
358
+
359
+ case 'move-up':
360
+ if (parent) {
361
+ const index = parent.components().indexOf(component);
362
+ if (index > 0) {
363
+ parent.components().remove(component);
364
+ parent.components().add(component, { at: index - 1 });
365
+ editor.select(component);
366
+ showToast('Moved up', 'success');
367
+ }
368
+ }
369
+ break;
370
+
371
+ case 'move-down':
372
+ if (parent) {
373
+ const components = parent.components();
374
+ const index = components.indexOf(component);
375
+ if (index < components.length - 1) {
376
+ components.remove(component);
377
+ components.add(component, { at: index + 1 });
378
+ editor.select(component);
379
+ showToast('Moved down', 'success');
380
+ }
381
+ }
382
+ break;
383
+
384
+ case 'delete':
385
+ component.remove();
386
+ showToast('Component deleted', 'success');
387
+ break;
388
+ }
389
+ }
390
+
391
+ // Expose function
392
+ window.ActiveCanvasEditor.setupComponentToolbar = setupComponentToolbar;
393
+
394
+ })();