panda-cms 0.10.0 → 0.10.2

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -11
  3. data/app/components/panda/cms/code_component.rb +45 -8
  4. data/app/components/panda/cms/menu_component.rb +9 -3
  5. data/app/components/panda/cms/page_menu_component.rb +9 -1
  6. data/app/components/panda/cms/rich_text_component.rb +49 -17
  7. data/app/components/panda/cms/text_component.rb +46 -14
  8. data/app/controllers/panda/cms/admin/menus_controller.rb +2 -2
  9. data/app/controllers/panda/cms/admin/pages_controller.rb +6 -2
  10. data/app/controllers/panda/cms/admin/posts_controller.rb +3 -1
  11. data/app/controllers/panda/cms/form_submissions_controller.rb +134 -11
  12. data/app/controllers/panda/cms/pages_controller.rb +7 -2
  13. data/app/controllers/panda/cms/posts_controller.rb +16 -0
  14. data/app/helpers/panda/cms/application_helper.rb +2 -3
  15. data/app/helpers/panda/cms/asset_helper.rb +14 -72
  16. data/app/helpers/panda/cms/forms_helper.rb +60 -0
  17. data/app/helpers/panda/cms/seo_helper.rb +85 -0
  18. data/app/javascript/panda/cms/{application_panda_cms.js → application.js} +4 -0
  19. data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +31 -4
  20. data/app/javascript/panda/cms/controllers/file_upload_controller.js +165 -0
  21. data/app/javascript/panda/cms/controllers/index.js +6 -0
  22. data/app/javascript/panda/cms/controllers/menu_form_controller.js +14 -1
  23. data/app/javascript/panda/cms/controllers/page_form_controller.js +454 -0
  24. data/app/javascript/panda/cms/stimulus-loading.js +2 -1
  25. data/app/models/panda/cms/menu.rb +12 -0
  26. data/app/models/panda/cms/page.rb +106 -0
  27. data/app/models/panda/cms/post.rb +97 -0
  28. data/app/views/layouts/homepage.html.erb +1 -4
  29. data/app/views/layouts/page.html.erb +1 -4
  30. data/app/views/panda/cms/admin/dashboard/show.html.erb +1 -1
  31. data/app/views/panda/cms/admin/files/index.html.erb +1 -1
  32. data/app/views/panda/cms/admin/forms/show.html.erb +3 -3
  33. data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +3 -3
  34. data/app/views/panda/cms/admin/menus/edit.html.erb +12 -14
  35. data/app/views/panda/cms/admin/menus/index.html.erb +1 -1
  36. data/app/views/panda/cms/admin/menus/new.html.erb +5 -7
  37. data/app/views/panda/cms/admin/pages/edit.html.erb +139 -20
  38. data/app/views/panda/cms/admin/pages/index.html.erb +6 -6
  39. data/app/views/panda/cms/admin/posts/_form.html.erb +41 -2
  40. data/app/views/panda/cms/admin/posts/edit.html.erb +1 -1
  41. data/app/views/panda/cms/admin/posts/index.html.erb +4 -4
  42. data/app/views/shared/_header.html.erb +1 -4
  43. data/config/brakeman.ignore +38 -0
  44. data/config/importmap.rb +8 -6
  45. data/config/locales/en.yml +41 -0
  46. data/config/routes.rb +1 -1
  47. data/db/migrate/20251109131150_add_seo_fields_to_pages.rb +32 -0
  48. data/db/migrate/20251109131205_add_seo_fields_to_posts.rb +27 -0
  49. data/db/migrate/20251110114258_add_spam_tracking_to_form_submissions.rb +7 -0
  50. data/db/migrate/20251110122812_add_performance_indexes_to_pages_and_redirects.rb +13 -0
  51. data/lib/panda/cms/asset_loader.rb +27 -77
  52. data/lib/panda/cms/bulk_editor.rb +288 -12
  53. data/lib/panda/cms/engine/asset_config.rb +49 -0
  54. data/lib/panda/cms/engine/autoload_config.rb +19 -0
  55. data/lib/panda/cms/engine/backtrace_config.rb +42 -0
  56. data/lib/panda/cms/engine/core_config.rb +106 -0
  57. data/lib/panda/cms/engine/helper_config.rb +20 -0
  58. data/lib/panda/cms/engine/route_config.rb +34 -0
  59. data/lib/panda/cms/engine/view_component_config.rb +31 -0
  60. data/lib/panda/cms/engine.rb +44 -221
  61. data/lib/panda/cms.rb +10 -0
  62. data/lib/panda-cms/version.rb +1 -1
  63. data/lib/panda-cms.rb +16 -2
  64. metadata +20 -22
  65. data/app/javascript/panda_cms/stimulus-loading.js +0 -39
  66. data/app/views/panda/cms/shared/_importmap.html.erb +0 -34
  67. data/config/initializers/inflections.rb +0 -5
  68. data/lib/tasks/assets.rake +0 -540
@@ -1,540 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- namespace :panda do
4
- namespace :cms do
5
- namespace :assets do
6
- desc "Compile Panda CMS assets for GitHub release distribution"
7
- task :compile do
8
- puts "🐼 Compiling Panda CMS assets..."
9
- puts "Rails.root: #{Rails.root}"
10
- puts "Working directory: #{Dir.pwd}"
11
-
12
- # Create output directory
13
- output_dir = Rails.root.join("tmp", "panda_cms_assets")
14
- FileUtils.mkdir_p(output_dir)
15
-
16
- version = Panda::CMS::VERSION
17
- puts "Version: #{version}"
18
- puts "Output directory: #{output_dir}"
19
-
20
- # Compile JavaScript bundle
21
- js_bundle = compile_javascript_bundle(version)
22
- js_file = output_dir.join("panda-cms-#{version}.js")
23
- File.write(js_file, js_bundle)
24
- puts "✅ JavaScript compiled: #{js_file} (#{File.size(js_file)} bytes)"
25
-
26
- # CSS is now provided by Panda Core
27
- puts "ℹ️ CSS is provided by Panda Core at /panda-core-assets/panda-core.css"
28
-
29
- # Create manifest file
30
- manifest = create_asset_manifest(version)
31
- manifest_file = output_dir.join("manifest.json")
32
- File.write(manifest_file, JSON.pretty_generate(manifest))
33
- puts "✅ Manifest created: #{manifest_file}"
34
-
35
- # Copy assets to test environment location for consistent testing
36
- # Rails.root is the dummy app, so we need to go to its public directory
37
- test_asset_dir = Rails.root.join("public", "panda-cms-assets")
38
- FileUtils.mkdir_p(test_asset_dir)
39
-
40
- js_file_name = "panda-cms-#{version}.js"
41
-
42
- # Copy JavaScript file
43
- if File.exist?(output_dir.join(js_file_name))
44
- FileUtils.cp(output_dir.join(js_file_name), test_asset_dir.join(js_file_name))
45
- puts "✅ Copied JavaScript to test location: #{test_asset_dir.join(js_file_name)}"
46
- end
47
-
48
- # Copy manifest
49
- if File.exist?(output_dir.join("manifest.json"))
50
- FileUtils.cp(output_dir.join("manifest.json"), test_asset_dir.join("manifest.json"))
51
- puts "✅ Copied manifest to test location: #{test_asset_dir.join("manifest.json")}"
52
- end
53
-
54
- puts "🎉 Asset compilation complete!"
55
- puts "📁 Output directory: #{output_dir}"
56
- puts "📁 Test assets directory: #{test_asset_dir}"
57
- end
58
-
59
- desc "Upload compiled assets to GitHub release"
60
- task upload: :compile do
61
- version = Panda::CMS::VERSION
62
- output_dir = Rails.root.join("tmp", "panda_cms_assets")
63
-
64
- puts "📤 Uploading assets to GitHub release v#{version}..."
65
-
66
- # Check if gh CLI is available
67
- unless system("gh --version > /dev/null 2>&1")
68
- puts "❌ GitHub CLI (gh) not found. Please install: https://cli.github.com/"
69
- exit 1
70
- end
71
-
72
- # Check if release exists
73
- unless system("gh release view v#{version} > /dev/null 2>&1")
74
- puts "❌ Release v#{version} not found. Create it first with: gh release create v#{version}"
75
- exit 1
76
- end
77
-
78
- # Upload each asset file
79
- Dir.glob(output_dir.join("*")).each do |file|
80
- filename = File.basename(file)
81
- puts "Uploading #{filename}..."
82
-
83
- if system("gh release upload v#{version} #{file} --clobber")
84
- puts "✅ Uploaded: #{filename}"
85
- else
86
- puts "❌ Failed to upload: #{filename}"
87
- exit 1
88
- end
89
- end
90
-
91
- puts "🎉 All assets uploaded successfully!"
92
- end
93
-
94
- desc "Download assets from GitHub release for local development"
95
- task :download do
96
- version = Panda::CMS::VERSION
97
- output_dir = Rails.root.join("public", "panda-cms-assets", version)
98
- FileUtils.mkdir_p(output_dir)
99
-
100
- puts "📥 Downloading assets from GitHub release v#{version}..."
101
-
102
- # Download manifest first to know what files to get
103
- manifest_url = "https://github.com/pandacms/panda-cms/releases/download/v#{version}/manifest.json"
104
-
105
- begin
106
- require "net/http"
107
- require "uri"
108
-
109
- uri = URI(manifest_url)
110
- response = Net::HTTP.get_response(uri)
111
-
112
- if response.code == "200"
113
- manifest = JSON.parse(response.body)
114
- puts "✅ Downloaded manifest"
115
-
116
- # Download each file listed in manifest
117
- manifest["files"].each do |file_info|
118
- filename = file_info["filename"]
119
- file_url = "https://github.com/pandacms/panda-cms/releases/download/v#{version}/#{filename}"
120
-
121
- puts "Downloading #{filename}..."
122
- file_uri = URI(file_url)
123
- file_response = Net::HTTP.get_response(file_uri)
124
-
125
- if file_response.code == "200"
126
- File.write(output_dir.join(filename), file_response.body)
127
- puts "✅ Downloaded: #{filename}"
128
- else
129
- puts "❌ Failed to download: #{filename}"
130
- end
131
- end
132
-
133
- puts "🎉 Assets downloaded to: #{output_dir}"
134
- else
135
- puts "❌ Failed to download manifest from: #{manifest_url}"
136
- puts "Response: #{response.code} #{response.message}"
137
- end
138
- rescue => e
139
- puts "❌ Error downloading assets: #{e.message}"
140
- puts "Falling back to local development mode..."
141
- end
142
- end
143
- end
144
- end
145
- end
146
-
147
- private
148
-
149
- def compile_javascript_bundle(version)
150
- puts "Creating full JavaScript bundle from importmap modules..."
151
-
152
- bundle = []
153
- bundle << "// Panda CMS JavaScript Bundle v#{version}"
154
- bundle << "// Compiled: #{Time.now.utc.iso8601}"
155
- bundle << "// Full bundle with all Stimulus controllers and functionality"
156
- bundle << ""
157
-
158
- # Add Stimulus polyfill/setup
159
- bundle << create_stimulus_setup
160
-
161
- # Add TailwindCSS Stimulus components
162
- bundle << create_tailwind_components
163
-
164
- # Add all Panda CMS controllers
165
- bundle << compile_all_controllers
166
-
167
- # Add editor components
168
- bundle << compile_editor_components
169
-
170
- # Add main application initialization
171
- bundle << create_application_init(version)
172
-
173
- puts "✅ Created full JavaScript bundle (#{bundle.join("\n").length} chars)"
174
- bundle.join("\n")
175
- end
176
-
177
- # CSS compilation removed - all CSS is now provided by Panda Core
178
- # The panda-core.css file includes all admin interface styling including
179
- # EditorJS styles, theme variables, and component styles
180
-
181
- def create_asset_manifest(version)
182
- output_dir = Rails.root.join("tmp", "panda_cms_assets")
183
-
184
- files = Dir.glob(output_dir.join("*")).reject { |f| File.basename(f) == "manifest.json" }.map do |file|
185
- {
186
- filename: File.basename(file),
187
- size: File.size(file),
188
- sha256: Digest::SHA256.file(file).hexdigest
189
- }
190
- end
191
-
192
- {
193
- version: version,
194
- compiled_at: Time.now.utc.iso8601,
195
- files: files,
196
- cdn_base_url: "https://github.com/tastybamboo/panda-cms/releases/download/v#{version}/",
197
- integrity: {
198
- algorithm: "sha256"
199
- }
200
- }
201
- end
202
-
203
- def create_stimulus_setup
204
- [
205
- "// Stimulus setup and polyfill",
206
- "window.Stimulus = window.Stimulus || {",
207
- " controllers: new Map(),",
208
- " register: function(name, controller) {",
209
- " this.controllers.set(name, controller);",
210
- " console.log('[Panda CMS] Registered controller:', name);",
211
- " // Simple controller connection simulation",
212
- " document.addEventListener('DOMContentLoaded', () => {",
213
- " const elements = document.querySelectorAll(`[data-controller*='${name}']`);",
214
- " elements.forEach(element => {",
215
- " if (controller.connect) {",
216
- " const instance = Object.create(controller);",
217
- " instance.element = element;",
218
- " instance.connect();",
219
- " }",
220
- " });",
221
- " });",
222
- " }",
223
- "};",
224
- ""
225
- ].join("\n")
226
- end
227
-
228
- def create_tailwind_components
229
- [
230
- "// TailwindCSS Stimulus Components (simplified)",
231
- "const Alert = {",
232
- " static: {",
233
- " values: { dismissAfter: Number }",
234
- " },",
235
- " connect() {",
236
- " console.log('[Panda CMS] Alert controller connected');",
237
- " // Get dismiss time from data attribute or default to 5 seconds for tests",
238
- " const dismissAfter = this.dismissAfterValue || 5000;",
239
- " setTimeout(() => {",
240
- " if (this.element && this.element.remove) {",
241
- " this.element.remove();",
242
- " }",
243
- " }, dismissAfter);",
244
- " },",
245
- " close() {",
246
- " console.log('[Panda CMS] Alert closed manually');",
247
- " if (this.element && this.element.remove) {",
248
- " this.element.remove();",
249
- " }",
250
- " }",
251
- "};",
252
- "",
253
- "const Dropdown = {",
254
- " connect() {",
255
- " console.log('[Panda CMS] Dropdown controller connected');",
256
- " }",
257
- "};",
258
- "",
259
- "const Modal = {",
260
- " connect() {",
261
- " console.log('[Panda CMS] Modal controller connected');",
262
- " }",
263
- "};",
264
- "",
265
- "const Toggle = {",
266
- " static: {",
267
- " values: { open: { type: Boolean, default: false } },",
268
- " targets: ['toggleable']",
269
- " },",
270
- " connect() {",
271
- " console.log('[Panda CMS] Toggle controller connected');",
272
- " this.openValue = false;",
273
- " // Find toggleable elements",
274
- " this.toggleableTargets = Array.from(this.element.querySelectorAll('[data-toggle-target=\"toggleable\"]'));",
275
- " if (this.toggleableTargets.length === 0) {",
276
- " // For slideover, the toggleable element might be a sibling",
277
- " const slideover = document.querySelector('#slideover');",
278
- " if (slideover) {",
279
- " this.toggleableTargets = [slideover];",
280
- " }",
281
- " }",
282
- " },",
283
- " toggle(event) {",
284
- " event.preventDefault();",
285
- " console.log('[Panda CMS] Toggle action triggered');",
286
- " this.openValue = !this.openValue;",
287
- " this.animate();",
288
- " },",
289
- " animate() {",
290
- " this.toggleableTargets.forEach(element => {",
291
- " if (this.openValue) {",
292
- " element.classList.remove('hidden');",
293
- " element.style.display = 'block';",
294
- " } else {",
295
- " element.classList.add('hidden');",
296
- " element.style.display = 'none';",
297
- " }",
298
- " });",
299
- " }",
300
- "};",
301
- "",
302
- "// Register TailwindCSS components",
303
- "Stimulus.register('alert', Alert);",
304
- "Stimulus.register('dropdown', Dropdown);",
305
- "Stimulus.register('modal', Modal);",
306
- "Stimulus.register('toggle', Toggle);",
307
- ""
308
- ].join("\n")
309
- end
310
-
311
- def compile_all_controllers
312
- engine_root = Panda::CMS::Engine.root
313
- puts "Engine root: #{engine_root}"
314
- controller_files = Dir.glob(engine_root.join("app/javascript/panda/cms/controllers/*.js"))
315
- puts "Found controller files: #{controller_files}"
316
- controllers = []
317
-
318
- controller_files.each do |file|
319
- next if File.basename(file) == "index.js"
320
-
321
- controller_name = File.basename(file, ".js")
322
- puts "Compiling controller: #{controller_name}"
323
-
324
- # Read and process the controller file
325
- content = File.read(file)
326
-
327
- # Convert ES6 controller to simple object
328
- controllers << convert_es6_controller_to_simple(controller_name, content)
329
- end
330
-
331
- controllers.join("\n\n")
332
- end
333
-
334
- def convert_es6_controller_to_simple(name, content)
335
- # For now, create a simpler working controller that focuses on form validation
336
- controller_name = name.tr("_", "-")
337
-
338
- case name
339
- when "theme_form_controller"
340
- [
341
- "// Theme Form Controller",
342
- "const ThemeFormController = {",
343
- " connect() {",
344
- " console.log('[Panda CMS] Theme form controller connected');",
345
- " // Ensure submit button is enabled",
346
- " const submitButton = this.element.querySelector('input[type=\"submit\"], button[type=\"submit\"]');",
347
- " if (submitButton) submitButton.disabled = false;",
348
- " },",
349
- " updateTheme(event) {",
350
- " const newTheme = event.target.value;",
351
- " document.documentElement.dataset.theme = newTheme;",
352
- " }",
353
- "};",
354
- "",
355
- "Stimulus.register('theme-form', ThemeFormController);"
356
- ].join("\n")
357
- when "slug_controller"
358
- [
359
- "// Slug Controller",
360
- "const SlugController = {",
361
- " static: {",
362
- " targets: ['titleField', 'pathField'],",
363
- " values: { basePath: String }",
364
- " },",
365
- " connect() {",
366
- " console.log('[Panda CMS] Slug controller connected');",
367
- " this.titleFieldTarget = this.element.querySelector('[data-slug-target=\"titleField\"]') ||",
368
- " this.element.querySelector('#page_title, #post_title, input[name*=\"title\"]');",
369
- " this.pathFieldTarget = this.element.querySelector('[data-slug-target=\"pathField\"]') ||",
370
- " this.element.querySelector('#page_path, #post_path, input[name*=\"path\"], input[name*=\"slug\"]');",
371
- " ",
372
- " if (this.titleFieldTarget) {",
373
- " this.titleFieldTarget.addEventListener('input', this.generatePath.bind(this));",
374
- " this.titleFieldTarget.addEventListener('blur', this.generatePath.bind(this));",
375
- " }",
376
- " },",
377
- " generatePath(event) {",
378
- " console.log('[Panda CMS] Generating path...');",
379
- " if (!this.titleFieldTarget || !this.pathFieldTarget) return;",
380
- " ",
381
- " const title = this.titleFieldTarget.value;",
382
- " if (!title) return;",
383
- " ",
384
- " // Simple slug generation",
385
- " let slug = title.toLowerCase()",
386
- " .replace(/[^a-z0-9\\s-]/g, '')",
387
- " .replace(/\\s+/g, '-')",
388
- " .replace(/-+/g, '-')",
389
- " .replace(/^-|-$/g, '');",
390
- " ",
391
- " // Add base path if needed",
392
- " const basePath = this.basePathValue || '';",
393
- " if (basePath && !basePath.endsWith('/')) {",
394
- " slug = basePath + '/' + slug;",
395
- " } else if (basePath) {",
396
- " slug = basePath + slug;",
397
- " }",
398
- " ",
399
- " this.pathFieldTarget.value = slug;",
400
- " ",
401
- " // Trigger change event",
402
- " this.pathFieldTarget.dispatchEvent(new Event('change', { bubbles: true }));",
403
- " }",
404
- "};",
405
- "",
406
- "Stimulus.register('slug', SlugController);"
407
- ].join("\n")
408
- when "editor_form_controller"
409
- [
410
- "// Editor Form Controller",
411
- "const EditorFormController = {",
412
- " static: {",
413
- " targets: ['editorContainer', 'hiddenField'],",
414
- " values: { editorId: String }",
415
- " },",
416
- " connect() {",
417
- " console.log('[Panda CMS] Editor form controller connected');",
418
- " this.editorContainerTarget = this.element.querySelector('[data-editor-form-target=\"editorContainer\"]');",
419
- " this.hiddenFieldTarget = this.element.querySelector('[data-editor-form-target=\"hiddenField\"]') ||",
420
- " this.element.querySelector('input[type=\"hidden\"]');",
421
- " ",
422
- " // Mark editor as ready for tests",
423
- " window.pandaCmsEditorReady = true;",
424
- " },",
425
- " submit(event) {",
426
- " console.log('[Panda CMS] Form submission triggered');",
427
- " // Allow form submission to proceed",
428
- " return true;",
429
- " }",
430
- "};",
431
- "",
432
- "Stimulus.register('editor-form', EditorFormController);"
433
- ].join("\n")
434
- else
435
- [
436
- "// #{name.tr("_", " ").titleize} Controller",
437
- "const #{name.camelize}Controller = {",
438
- " connect() {",
439
- " console.log('[Panda CMS] #{name.tr("_", " ").titleize} controller connected');",
440
- " }",
441
- "};",
442
- "",
443
- "Stimulus.register('#{controller_name}', #{name.camelize}Controller);"
444
- ].join("\n")
445
- end
446
- end
447
-
448
- def process_controller_methods(class_body)
449
- # Simple method extraction - just copy methods as-is but clean up syntax
450
- methods = []
451
-
452
- # Split by methods (looking for function patterns)
453
- class_body.scan(/(static\s+\w+\s*=.*?;|connect\(\)\s*\{.*?\}|\w+\([^)]*\)\s*\{.*?\})/m) do |match|
454
- method = match[0].strip
455
-
456
- # Skip static properties for now, focus on methods
457
- next if method.start_with?("static")
458
-
459
- # Clean up the method syntax for object format
460
- if method.match?(/(\w+)\(\s*\)\s*\{/)
461
- # No-argument methods
462
- method = method.gsub(/(\w+)\(\s*\)\s*\{/, '\1() {')
463
- elsif method.match?(/(\w+)\([^)]+\)\s*\{/)
464
- # Methods with arguments
465
- method = method.gsub(/(\w+)\(([^)]+)\)\s*\{/, '\1(\2) {')
466
- end
467
-
468
- methods << " #{method}"
469
- end
470
-
471
- methods.join(",\n\n")
472
- end
473
-
474
- def compile_editor_components
475
- [
476
- "// Editor components placeholder",
477
- "// EditorJS resources will be loaded dynamically as needed",
478
- "window.pandaCmsEditorReady = true;",
479
- ""
480
- ].join("\n")
481
- end
482
-
483
- def create_application_init(version)
484
- [
485
- "// Application initialization",
486
- "// Immediate execution marker for CI debugging",
487
- "window.pandaCmsScriptExecuted = true;",
488
- "console.log('[Panda CMS] Script execution started');",
489
- "",
490
- "(function() {",
491
- " 'use strict';",
492
- " ",
493
- " try {",
494
- " console.log('[Panda CMS] Full JavaScript bundle v#{version} loaded');",
495
- " ",
496
- " // Mark as loaded immediately to help with CI timing issues",
497
- " window.pandaCmsVersion = '#{version}';",
498
- " window.pandaCmsLoaded = true;",
499
- " window.pandaCmsFullBundle = true;",
500
- " window.pandaCmsStimulus = window.Stimulus;",
501
- " ",
502
- " // Also set on document for iframe context issues",
503
- " if (window.document) {",
504
- " window.document.pandaCmsLoaded = true;",
505
- " }",
506
- " ",
507
- " // Initialize on DOM ready",
508
- " if (document.readyState === 'loading') {",
509
- " document.addEventListener('DOMContentLoaded', initializePandaCMS);",
510
- " } else {",
511
- " initializePandaCMS();",
512
- " }",
513
- " ",
514
- " function initializePandaCMS() {",
515
- " console.log('[Panda CMS] Initializing controllers...');",
516
- " ",
517
- " // Trigger controller connections for existing elements",
518
- " Stimulus.controllers.forEach((controller, name) => {",
519
- " const elements = document.querySelectorAll(`[data-controller*='${name}']`);",
520
- " elements.forEach(element => {",
521
- " if (controller.connect) {",
522
- " const instance = Object.create(controller);",
523
- " instance.element = element;",
524
- " // Add target helpers",
525
- " instance.targets = instance.targets || {};",
526
- " controller.connect.call(instance);",
527
- " }",
528
- " });",
529
- " });",
530
- " }",
531
- " } catch (error) {",
532
- " console.error('[Panda CMS] Error during initialization:', error);",
533
- " // Still mark as loaded to prevent test failures",
534
- " window.pandaCmsLoaded = true;",
535
- " window.pandaCmsError = error.message;",
536
- " }",
537
- "})();",
538
- ""
539
- ].join("\n")
540
- end