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,498 @@
1
+ /**
2
+ * ActiveCanvas Editor - Custom Asset Manager Modal
3
+ */
4
+
5
+ (function() {
6
+ 'use strict';
7
+
8
+ window.ActiveCanvasEditor = window.ActiveCanvasEditor || {};
9
+
10
+ /**
11
+ * Setup the custom asset manager modal
12
+ * @param {Object} editor - GrapeJS editor instance
13
+ * @param {Object} config - Editor configuration
14
+ * @param {string} csrfToken - CSRF token for requests
15
+ */
16
+ function setupCustomAssetManager(editor, config, csrfToken) {
17
+ const { showToast } = window.ActiveCanvasEditor;
18
+ let currentPage = 1;
19
+ let totalPages = 1;
20
+ let currentTarget = null;
21
+
22
+ // Override the default asset manager open behavior
23
+ editor.on('run:open-assets', () => {
24
+ const am = editor.AssetManager;
25
+ currentTarget = am.getConfig().target;
26
+ openCustomAssetModal();
27
+ return false;
28
+ });
29
+
30
+ // Also listen for asset manager open command
31
+ const originalOpen = editor.AssetManager.open;
32
+ editor.AssetManager.open = function(options) {
33
+ currentTarget = options?.target;
34
+ openCustomAssetModal();
35
+ };
36
+
37
+ function openCustomAssetModal() {
38
+ // Remove existing modal if any
39
+ const existing = document.querySelector('.ac-asset-modal');
40
+ if (existing) existing.remove();
41
+
42
+ // Create modal
43
+ const modal = document.createElement('div');
44
+ modal.className = 'ac-asset-modal';
45
+ modal.innerHTML = `
46
+ <div class="ac-asset-modal-overlay"></div>
47
+ <div class="ac-asset-modal-dialog">
48
+ <div class="ac-asset-modal-header">
49
+ <h3>Select Image</h3>
50
+ <button class="ac-asset-modal-close" title="Close">
51
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
52
+ <line x1="18" y1="6" x2="6" y2="18"></line>
53
+ <line x1="6" y1="6" x2="18" y2="18"></line>
54
+ </svg>
55
+ </button>
56
+ </div>
57
+ <div class="ac-asset-modal-tabs">
58
+ <button class="ac-asset-tab active" data-tab="library">Media Library</button>
59
+ <button class="ac-asset-tab" data-tab="upload">Upload New</button>
60
+ </div>
61
+ <div class="ac-asset-modal-body">
62
+ <div class="ac-asset-tab-content active" data-tab="library">
63
+ <div class="ac-asset-grid" id="ac-asset-grid">
64
+ <div class="ac-asset-loading">Loading media...</div>
65
+ </div>
66
+ <div class="ac-asset-pagination" id="ac-asset-pagination"></div>
67
+ </div>
68
+ <div class="ac-asset-tab-content" data-tab="upload">
69
+ <div class="ac-asset-upload-zone" id="ac-upload-zone">
70
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
71
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
72
+ <polyline points="17 8 12 3 7 8"/>
73
+ <line x1="12" y1="3" x2="12" y2="15"/>
74
+ </svg>
75
+ <p>Drag & drop images here or click to browse</p>
76
+ <p class="ac-upload-hint">Supports: JPEG, PNG, GIF, WebP, SVG</p>
77
+ <input type="file" id="ac-upload-input" accept="image/*" multiple style="display:none">
78
+ </div>
79
+ <div class="ac-upload-progress" id="ac-upload-progress" style="display:none">
80
+ <div class="ac-upload-progress-bar"></div>
81
+ <span class="ac-upload-progress-text">Uploading...</span>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ `;
87
+
88
+ document.body.appendChild(modal);
89
+ setupModalHandlers(modal);
90
+ loadMediaPage(1);
91
+ }
92
+
93
+ function setupModalHandlers(modal) {
94
+ // Close button
95
+ modal.querySelector('.ac-asset-modal-close').addEventListener('click', () => {
96
+ modal.remove();
97
+ });
98
+
99
+ // Overlay click to close
100
+ modal.querySelector('.ac-asset-modal-overlay').addEventListener('click', () => {
101
+ modal.remove();
102
+ });
103
+
104
+ // Tab switching
105
+ modal.querySelectorAll('.ac-asset-tab').forEach(tab => {
106
+ tab.addEventListener('click', () => {
107
+ modal.querySelectorAll('.ac-asset-tab').forEach(t => t.classList.remove('active'));
108
+ modal.querySelectorAll('.ac-asset-tab-content').forEach(c => c.classList.remove('active'));
109
+ tab.classList.add('active');
110
+ modal.querySelector(`.ac-asset-tab-content[data-tab="${tab.dataset.tab}"]`).classList.add('active');
111
+ });
112
+ });
113
+
114
+ // Upload zone
115
+ const uploadZone = modal.querySelector('#ac-upload-zone');
116
+ const uploadInput = modal.querySelector('#ac-upload-input');
117
+
118
+ uploadZone.addEventListener('click', () => uploadInput.click());
119
+
120
+ uploadZone.addEventListener('dragover', (e) => {
121
+ e.preventDefault();
122
+ uploadZone.classList.add('dragover');
123
+ });
124
+
125
+ uploadZone.addEventListener('dragleave', () => {
126
+ uploadZone.classList.remove('dragover');
127
+ });
128
+
129
+ uploadZone.addEventListener('drop', (e) => {
130
+ e.preventDefault();
131
+ uploadZone.classList.remove('dragover');
132
+ handleFileUpload(e.dataTransfer.files, modal);
133
+ });
134
+
135
+ uploadInput.addEventListener('change', (e) => {
136
+ handleFileUpload(e.target.files, modal);
137
+ });
138
+ }
139
+
140
+ async function loadMediaPage(page) {
141
+ currentPage = page;
142
+ const grid = document.getElementById('ac-asset-grid');
143
+ const pagination = document.getElementById('ac-asset-pagination');
144
+
145
+ if (!grid) return;
146
+
147
+ grid.innerHTML = '<div class="ac-asset-loading">Loading media...</div>';
148
+
149
+ try {
150
+ const response = await fetch(`${config.mediaUrl}?page=${page}&per_page=20`, {
151
+ headers: { 'Accept': 'application/json' }
152
+ });
153
+ const result = await response.json();
154
+
155
+ if (result.data && result.data.length > 0) {
156
+ totalPages = result.meta?.total_pages || 1;
157
+ renderMediaGrid(result.data, grid);
158
+ renderPagination(pagination, result.meta);
159
+ } else {
160
+ grid.innerHTML = '<div class="ac-asset-empty">No media found. Upload some images to get started.</div>';
161
+ pagination.innerHTML = '';
162
+ }
163
+ } catch (error) {
164
+ console.error('Failed to load media:', error);
165
+ grid.innerHTML = '<div class="ac-asset-empty">Failed to load media.</div>';
166
+ }
167
+ }
168
+
169
+ function renderMediaGrid(media, container) {
170
+ container.innerHTML = media.map(item => `
171
+ <div class="ac-asset-item" data-src="${item.src}" data-name="${item.name || ''}">
172
+ <div class="ac-asset-thumb">
173
+ <img src="${item.src}" alt="${item.name || 'Image'}" loading="lazy">
174
+ </div>
175
+ <div class="ac-asset-name">${item.name || 'Untitled'}</div>
176
+ </div>
177
+ `).join('');
178
+
179
+ // Add click handlers
180
+ container.querySelectorAll('.ac-asset-item').forEach(item => {
181
+ item.addEventListener('click', () => {
182
+ selectAsset(item.dataset.src, item.dataset.name);
183
+ });
184
+ });
185
+ }
186
+
187
+ function renderPagination(container, meta) {
188
+ if (!meta || meta.total_pages <= 1) {
189
+ container.innerHTML = '';
190
+ return;
191
+ }
192
+
193
+ let html = '<div class="ac-pagination-info">';
194
+ html += `Page ${meta.current_page} of ${meta.total_pages} (${meta.total_count} items)`;
195
+ html += '</div><div class="ac-pagination-buttons">';
196
+
197
+ if (meta.current_page > 1) {
198
+ html += `<button class="ac-pagination-btn" data-page="${meta.current_page - 1}">Previous</button>`;
199
+ }
200
+
201
+ // Page numbers
202
+ const startPage = Math.max(1, meta.current_page - 2);
203
+ const endPage = Math.min(meta.total_pages, meta.current_page + 2);
204
+
205
+ for (let i = startPage; i <= endPage; i++) {
206
+ html += `<button class="ac-pagination-btn ${i === meta.current_page ? 'active' : ''}" data-page="${i}">${i}</button>`;
207
+ }
208
+
209
+ if (meta.current_page < meta.total_pages) {
210
+ html += `<button class="ac-pagination-btn" data-page="${meta.current_page + 1}">Next</button>`;
211
+ }
212
+
213
+ html += '</div>';
214
+ container.innerHTML = html;
215
+
216
+ // Add click handlers
217
+ container.querySelectorAll('.ac-pagination-btn').forEach(btn => {
218
+ btn.addEventListener('click', () => {
219
+ loadMediaPage(parseInt(btn.dataset.page));
220
+ });
221
+ });
222
+ }
223
+
224
+ function selectAsset(src, name) {
225
+ // Add to GrapeJS asset manager
226
+ editor.AssetManager.add({ src, name, type: 'image' });
227
+
228
+ // If there's a target component, set the image
229
+ if (currentTarget) {
230
+ currentTarget.set('src', src);
231
+ }
232
+
233
+ // Close modal
234
+ const modal = document.querySelector('.ac-asset-modal');
235
+ if (modal) modal.remove();
236
+
237
+ showToast('Image selected', 'success');
238
+ }
239
+
240
+ async function handleFileUpload(files, modal) {
241
+ const progressContainer = modal.querySelector('#ac-upload-progress');
242
+ const progressBar = modal.querySelector('.ac-upload-progress-bar');
243
+ const progressText = modal.querySelector('.ac-upload-progress-text');
244
+
245
+ progressContainer.style.display = 'block';
246
+
247
+ for (let i = 0; i < files.length; i++) {
248
+ const file = files[i];
249
+ progressText.textContent = `Uploading ${file.name}... (${i + 1}/${files.length})`;
250
+ progressBar.style.width = `${((i + 1) / files.length) * 100}%`;
251
+
252
+ const formData = new FormData();
253
+ formData.append('media[file]', file);
254
+ formData.append('media[filename]', file.name);
255
+
256
+ try {
257
+ const response = await fetch(config.uploadUrl, {
258
+ method: 'POST',
259
+ headers: { 'X-CSRF-Token': csrfToken },
260
+ body: formData
261
+ });
262
+
263
+ const result = await response.json();
264
+
265
+ if (response.ok && result.src) {
266
+ editor.AssetManager.add(result);
267
+ showToast(`Uploaded ${file.name}`, 'success');
268
+ } else if (result.errors) {
269
+ showToast(`Failed: ${result.errors.join(', ')}`, 'error');
270
+ }
271
+ } catch (error) {
272
+ showToast(`Error uploading ${file.name}`, 'error');
273
+ }
274
+ }
275
+
276
+ progressContainer.style.display = 'none';
277
+ progressBar.style.width = '0';
278
+
279
+ // Reload media library and switch to it
280
+ loadMediaPage(1);
281
+ modal.querySelector('.ac-asset-tab[data-tab="library"]').click();
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Load assets into the editor and sidebar panel
287
+ * @param {Object} editor - GrapeJS editor instance
288
+ * @param {string} mediaUrl - URL to fetch media from
289
+ */
290
+ function loadAssets(editor, mediaUrl) {
291
+ const { showToast } = window.ActiveCanvasEditor;
292
+
293
+ fetch(mediaUrl, {
294
+ headers: { 'Accept': 'application/json' }
295
+ })
296
+ .then(response => response.json())
297
+ .then(result => {
298
+ if (result.data) {
299
+ editor.AssetManager.add(result.data);
300
+ renderAssetsPanel(result.data, editor);
301
+ }
302
+ })
303
+ .catch(error => {
304
+ console.error('Failed to load assets:', error);
305
+ const grid = document.getElementById('assets-grid');
306
+ if (grid) {
307
+ grid.innerHTML = '<div class="assets-empty">Failed to load assets</div>';
308
+ }
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Render the assets panel in the sidebar
314
+ */
315
+ function renderAssetsPanel(assets, editor) {
316
+ const { showToast } = window.ActiveCanvasEditor;
317
+ const grid = document.getElementById('assets-grid');
318
+ if (!grid) return;
319
+
320
+ if (!assets || assets.length === 0) {
321
+ grid.innerHTML = `
322
+ <div class="assets-empty">
323
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
324
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
325
+ <circle cx="8.5" cy="8.5" r="1.5"/>
326
+ <polyline points="21 15 16 10 5 21"/>
327
+ </svg>
328
+ <p>No assets yet</p>
329
+ <span>Upload images to use them here</span>
330
+ </div>
331
+ `;
332
+ return;
333
+ }
334
+
335
+ grid.innerHTML = assets.map(asset => `
336
+ <div class="asset-item" draggable="true" data-src="${asset.src}" data-name="${asset.name || 'Image'}" title="${asset.name || 'Image'}">
337
+ <img src="${asset.src}" alt="${asset.name || 'Image'}" loading="lazy">
338
+ <div class="asset-item-overlay">
339
+ <button class="asset-insert-btn" data-src="${asset.src}" title="Insert image">
340
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
341
+ <line x1="12" y1="5" x2="12" y2="19"/>
342
+ <line x1="5" y1="12" x2="19" y2="12"/>
343
+ </svg>
344
+ </button>
345
+ </div>
346
+ </div>
347
+ `).join('');
348
+
349
+ // Add drag and insert functionality
350
+ grid.querySelectorAll('.asset-item').forEach(item => {
351
+ // Drag to canvas
352
+ item.addEventListener('dragstart', (e) => {
353
+ const src = item.dataset.src;
354
+ e.dataTransfer.setData('text/html', `<img src="${src}" alt="Image" style="max-width: 100%;">`);
355
+ e.dataTransfer.effectAllowed = 'copy';
356
+ });
357
+
358
+ // Click to insert
359
+ const insertBtn = item.querySelector('.asset-insert-btn');
360
+ if (insertBtn) {
361
+ insertBtn.addEventListener('click', (e) => {
362
+ e.stopPropagation();
363
+ const src = insertBtn.dataset.src;
364
+ insertImageToCanvas(editor, src);
365
+ });
366
+ }
367
+
368
+ // Double-click to insert
369
+ item.addEventListener('dblclick', () => {
370
+ const src = item.dataset.src;
371
+ insertImageToCanvas(editor, src);
372
+ });
373
+ });
374
+ }
375
+
376
+ /**
377
+ * Insert an image into the canvas
378
+ */
379
+ function insertImageToCanvas(editor, src) {
380
+ const { showToast } = window.ActiveCanvasEditor;
381
+ const selected = editor.getSelected();
382
+ const wrapper = editor.getWrapper();
383
+
384
+ const imageComponent = {
385
+ type: 'image',
386
+ attributes: { src: src, alt: 'Image' },
387
+ style: { 'max-width': '100%' }
388
+ };
389
+
390
+ if (selected) {
391
+ const parent = selected.parent();
392
+ if (parent) {
393
+ const index = parent.components().indexOf(selected);
394
+ parent.components().add(imageComponent, { at: index + 1 });
395
+ } else {
396
+ wrapper.append(imageComponent);
397
+ }
398
+ } else {
399
+ wrapper.append(imageComponent);
400
+ }
401
+
402
+ showToast('Image inserted', 'success');
403
+ }
404
+
405
+ /**
406
+ * Setup the assets panel controls (upload, refresh)
407
+ */
408
+ function setupAssetsPanel(editor, config, csrfToken) {
409
+ const { showToast } = window.ActiveCanvasEditor;
410
+ const uploadBtn = document.getElementById('btn-upload-asset');
411
+ const refreshBtn = document.getElementById('btn-refresh-assets');
412
+ const uploadInput = document.getElementById('asset-upload-input');
413
+
414
+ if (!uploadBtn || !refreshBtn || !uploadInput) return;
415
+
416
+ // Upload button click
417
+ uploadBtn.addEventListener('click', () => {
418
+ uploadInput.click();
419
+ });
420
+
421
+ // Refresh button click
422
+ refreshBtn.addEventListener('click', () => {
423
+ const grid = document.getElementById('assets-grid');
424
+ if (grid) {
425
+ grid.innerHTML = '<div class="assets-loading">Loading assets...</div>';
426
+ }
427
+ loadAssets(editor, config.mediaUrl);
428
+ });
429
+
430
+ // Handle file selection
431
+ uploadInput.addEventListener('change', async (e) => {
432
+ const files = e.target.files;
433
+ if (!files || files.length === 0) return;
434
+
435
+ uploadBtn.disabled = true;
436
+ uploadBtn.innerHTML = `
437
+ <svg class="spin" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
438
+ <line x1="12" y1="2" x2="12" y2="6"/>
439
+ <line x1="12" y1="18" x2="12" y2="22"/>
440
+ <line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/>
441
+ <line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/>
442
+ <line x1="2" y1="12" x2="6" y2="12"/>
443
+ <line x1="18" y1="12" x2="22" y2="12"/>
444
+ <line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/>
445
+ <line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/>
446
+ </svg>
447
+ Uploading...
448
+ `;
449
+
450
+ for (const file of files) {
451
+ const formData = new FormData();
452
+ formData.append('media[file]', file);
453
+ formData.append('media[filename]', file.name);
454
+
455
+ try {
456
+ const response = await fetch(config.uploadUrl, {
457
+ method: 'POST',
458
+ headers: { 'X-CSRF-Token': csrfToken },
459
+ body: formData
460
+ });
461
+
462
+ const result = await response.json();
463
+
464
+ if (response.ok && result.src) {
465
+ editor.AssetManager.add(result);
466
+ showToast(`Uploaded ${file.name}`, 'success');
467
+ } else if (result.errors) {
468
+ showToast(`Failed: ${result.errors.join(', ')}`, 'error');
469
+ }
470
+ } catch (error) {
471
+ showToast(`Error uploading ${file.name}`, 'error');
472
+ console.error('Upload error:', error);
473
+ }
474
+ }
475
+
476
+ // Reset and refresh
477
+ uploadInput.value = '';
478
+ uploadBtn.disabled = false;
479
+ uploadBtn.innerHTML = `
480
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
481
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
482
+ <polyline points="17 8 12 3 7 8"/>
483
+ <line x1="12" y1="3" x2="12" y2="15"/>
484
+ </svg>
485
+ Upload
486
+ `;
487
+
488
+ // Refresh assets panel
489
+ loadAssets(editor, config.mediaUrl);
490
+ });
491
+ }
492
+
493
+ // Expose functions
494
+ window.ActiveCanvasEditor.setupCustomAssetManager = setupCustomAssetManager;
495
+ window.ActiveCanvasEditor.loadAssets = loadAssets;
496
+ window.ActiveCanvasEditor.setupAssetsPanel = setupAssetsPanel;
497
+
498
+ })();