railsui 3.2.7 → 3.3.0

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +8 -1
  3. data/README.md +196 -42
  4. data/app/assets/javascripts/railsui-controllers.js +12 -0
  5. data/app/controllers/railsui/configurations_controller.rb +11 -2
  6. data/app/helpers/railsui/application_helper.rb +12 -0
  7. data/app/javascript/controllers/index.js +3 -31
  8. data/app/javascript/controllers/railsui_anchor_controller.js +4 -3
  9. data/app/javascript/controllers/railsui_auto_expand_text_area_controller.js +1 -1
  10. data/app/javascript/controllers/railsui_canvas_controller.js +1 -1
  11. data/app/javascript/controllers/railsui_code_controller.js +3 -28
  12. data/app/javascript/controllers/railsui_color_controller.js +1 -1
  13. data/app/javascript/controllers/railsui_configuration_controller.js +1 -1
  14. data/app/javascript/controllers/railsui_dialog_controller.js +1 -1
  15. data/app/javascript/controllers/railsui_flash_controller.js +1 -1
  16. data/app/javascript/controllers/railsui_helper_controller.js +1 -1
  17. data/app/javascript/controllers/railsui_loading_controller.js +1 -1
  18. data/app/javascript/controllers/railsui_modal_controller.js +4 -3
  19. data/app/javascript/controllers/railsui_nav_controller.js +4 -3
  20. data/app/javascript/controllers/railsui_pages_controller.js +1 -1
  21. data/app/javascript/controllers/railsui_prevent_controller.js +1 -1
  22. data/app/javascript/controllers/railsui_scroll_controller.js +1 -1
  23. data/app/javascript/controllers/railsui_scroll_spy_controller.js +1 -1
  24. data/app/javascript/controllers/railsui_search_controller.js +1 -1
  25. data/app/javascript/controllers/railsui_smooth_controller.js +1 -1
  26. data/app/javascript/controllers/railsui_snippet_controller.js +1 -1
  27. data/app/views/layouts/railsui/application.html.erb +7 -5
  28. data/app/views/layouts/railsui/fullwidth.html.erb +4 -4
  29. data/app/views/layouts/railsui/landing.html.erb +3 -4
  30. data/app/views/layouts/railsui/routes.html.erb +4 -3
  31. data/app/views/railsui/admin/_form.html.erb +18 -1
  32. data/app/views/railsui/admin/fields/_theme.html.erb +0 -1
  33. data/app/views/railsui/shared/_cdn_dependencies.html.erb +121 -0
  34. data/app/views/railsui/shared/_inline_controllers.html.erb +498 -0
  35. data/app/views/railsui/shared/_snippet.html.erb +23 -1
  36. data/app/views/railsui/themes/hound/forms/_input_group.html.erb +3 -1
  37. data/app/views/railsui/themes/shepherd/authentication/devise/_overview.html.erb +30 -28
  38. data/app/views/railsui/themes/shepherd/authentication/static/_overview.html.erb +8 -8
  39. data/app/views/railsui/themes/shepherd/content/typography/_headings.html.erb +23 -21
  40. data/app/views/railsui/themes/shepherd/forms/_input.html.erb +1 -1
  41. data/guides/CONFIGURATION.md +199 -0
  42. data/guides/MIGRATION_GUIDE.md +220 -0
  43. data/lib/generators/railsui/install/install_generator.rb +124 -38
  44. data/lib/generators/railsui/install/templates/Procfile.dev.build +1 -0
  45. data/lib/generators/railsui/install/templates/Procfile.dev.nobuild +2 -0
  46. data/lib/generators/railsui/install/templates/bin/dev +21 -0
  47. data/lib/generators/railsui/install/templates/themes/corgie/stylesheets/railsui/actiontext.css +0 -1
  48. data/lib/generators/railsui/install/templates/themes/corgie/views/layouts/rui/railsui.html.erb +7 -2
  49. data/lib/generators/railsui/install/templates/themes/corgie/views/layouts/rui/railsui_admin.html.erb +7 -2
  50. data/lib/generators/railsui/install/templates/themes/corgie/views/layouts/rui/railsui_auth.html.erb +6 -2
  51. data/lib/generators/railsui/install/templates/themes/corgie/views/rui/pages/{privacy.html.erb → privacy_policy.html.erb} +1 -1
  52. data/lib/generators/railsui/install/templates/themes/corgie/views/rui/pages/terms.html.erb +2 -2
  53. data/lib/generators/railsui/install/templates/themes/corgie/views/rui/shared/sidebar/_link.html.erb +4 -4
  54. data/lib/generators/railsui/install/templates/themes/hound/stylesheets/railsui/actiontext.css +0 -1
  55. data/lib/generators/railsui/install/templates/themes/hound/views/layouts/rui/railsui.html.erb +6 -2
  56. data/lib/generators/railsui/install/templates/themes/hound/views/layouts/rui/railsui_admin.html.erb +6 -2
  57. data/lib/generators/railsui/install/templates/themes/shepherd/stylesheets/railsui/actiontext.css +0 -1
  58. data/lib/generators/railsui/install/templates/themes/shepherd/views/layouts/rui/railsui.html.erb +6 -2
  59. data/lib/generators/railsui/install/templates/themes/shepherd/views/layouts/rui/railsui_admin.html.erb +6 -2
  60. data/lib/generators/railsui/update/update_generator.rb +40 -4
  61. data/lib/railsui/configuration.rb +116 -15
  62. data/lib/railsui/engine.rb +15 -0
  63. data/lib/railsui/theme_setup.rb +598 -38
  64. data/lib/railsui/version.rb +1 -1
  65. data/lib/railsui.rb +10 -7
  66. data/lib/tasks/install.rake +9 -3
  67. data/lib/tasks/migrate.rake +219 -0
  68. metadata +26 -4
  69. data/.claude/settings.local.json +0 -10
@@ -7,12 +7,39 @@ module Railsui
7
7
  # gems
8
8
  def install_gems
9
9
  rails_command "generate railsui_icon:install"
10
- rails_command "action_text:install"
10
+
11
+ # Only install Action Text if not already installed
12
+ unless action_text_installed?
13
+ rails_command "action_text:install"
14
+ else
15
+ say "✓ Action Text already installed", :green
16
+ end
17
+ end
18
+
19
+ def action_text_installed?
20
+ # Check if Action Text migration exists
21
+ migration_files = Dir.glob(Rails.root.join("db/migrate/*_create_action_text_tables*.rb"))
22
+ return true if migration_files.any?
23
+
24
+ # Check if table exists in database
25
+ ActiveRecord::Base.connection.table_exists?('action_text_rich_texts')
26
+ rescue
27
+ false
11
28
  end
12
29
 
13
30
  # Assets
14
31
  def copy_theme_javascript(theme)
15
- say("Adding theme-specific stimulus.js controllers", :yellow)
32
+ config = Railsui::Configuration.load!
33
+
34
+ if config.nobuild?
35
+ copy_theme_javascript_nobuild(theme)
36
+ else
37
+ copy_theme_javascript_build(theme)
38
+ end
39
+ end
40
+
41
+ def copy_theme_javascript_build(theme)
42
+ say("Adding theme-specific stimulus.js controllers (build mode)", :yellow)
16
43
 
17
44
  # Define paths
18
45
  source_path = "themes/#{theme}/javascript/controllers/railsui"
@@ -36,8 +63,7 @@ module Railsui
36
63
  controller_name = File.basename(file, ".js").sub("_controller", "")
37
64
  import_name = controller_name.camelize
38
65
  registration_name = controller_name.dasherize
39
- "import #{import_name}Controller from \"./#{File.basename(file,
40
- ".js")}\";\napplication.register(\"#{registration_name}\", #{import_name}Controller);"
66
+ "import #{import_name}Controller from \"./#{File.basename(file, ".js")}\";\napplication.register(\"#{registration_name}\", #{import_name}Controller);"
41
67
  end.join("\n")
42
68
 
43
69
  js_content = <<-JAVASCRIPT.strip_heredoc
@@ -76,9 +102,92 @@ module Railsui
76
102
 
77
103
  # Write the updated content back to main index.js
78
104
  create_file index_js_path, new_index_js_content, force: true
79
- say(
80
- "Updated app/javascript/controllers/index.js and created app/javascript/controllers/railsui/index.js successfully.", :green
81
- )
105
+ say("Updated app/javascript/controllers/index.js and created app/javascript/controllers/railsui/index.js successfully.", :green)
106
+ end
107
+
108
+ def copy_theme_javascript_nobuild(theme)
109
+ say("Adding theme-specific stimulus.js controllers (importmap mode)", :yellow)
110
+
111
+ # Define paths
112
+ source_path = "themes/#{theme}/javascript/controllers/railsui"
113
+ destination_path = "app/javascript/controllers/railsui"
114
+ railsui_index_js_path = Rails.root.join("app/javascript/controllers/railsui/index.js")
115
+
116
+ # Empty the railsui folder
117
+ remove_dir destination_path
118
+ empty_directory destination_path
119
+
120
+ # Copy the directory
121
+ directory source_path, destination_path, force: true
122
+
123
+ # Get the list of controller files
124
+ controller_files = Dir.children(Rails.root.join(destination_path)).select { |f| f.end_with?("_controller.js") }
125
+ say("Controller files: 🗄️ #{controller_files}", :cyan)
126
+
127
+ # Generate import and register statements for theme-specific controllers
128
+ theme_controllers = controller_files.map do |file|
129
+ controller_name = File.basename(file, ".js").sub("_controller", "")
130
+ import_name = controller_name.camelize
131
+ registration_name = controller_name.dasherize
132
+ "import #{import_name}Controller from \"controllers/railsui/#{File.basename(file, ".js")}\"\napplication.register(\"#{registration_name}\", #{import_name}Controller)"
133
+ end.join("\n")
134
+
135
+ # Import railsui-stimulus components and register them
136
+ js_content = <<-JAVASCRIPT.strip_heredoc
137
+ import { application } from "controllers/application"
138
+ import {
139
+ RailsuiClipboard,
140
+ RailsuiCountUp,
141
+ RailsuiCombobox,
142
+ RailsuiDateRangePicker,
143
+ RailsuiDropdown,
144
+ RailsuiModal,
145
+ RailsuiRange,
146
+ RailsuiReadMore,
147
+ RailsuiSelectAll,
148
+ RailsuiTabs,
149
+ RailsuiToast,
150
+ RailsuiToggle,
151
+ RailsuiTooltip
152
+ } from "railsui-stimulus"
153
+
154
+ // Register railsui-stimulus components
155
+ application.register("railsui-clipboard", RailsuiClipboard)
156
+ application.register("railsui-count-up", RailsuiCountUp)
157
+ application.register("railsui-combobox", RailsuiCombobox)
158
+ application.register("railsui-date-range-picker", RailsuiDateRangePicker)
159
+ application.register("railsui-dropdown", RailsuiDropdown)
160
+ application.register("railsui-modal", RailsuiModal)
161
+ application.register("railsui-range", RailsuiRange)
162
+ application.register("railsui-read-more", RailsuiReadMore)
163
+ application.register("railsui-select-all", RailsuiSelectAll)
164
+ application.register("railsui-tabs", RailsuiTabs)
165
+ application.register("railsui-toast", RailsuiToast)
166
+ application.register("railsui-toggle", RailsuiToggle)
167
+ application.register("railsui-tooltip", RailsuiTooltip)
168
+
169
+ // Register theme-specific controllers
170
+ #{theme_controllers}
171
+ JAVASCRIPT
172
+
173
+ # Write the railsui/index.js file
174
+ create_file railsui_index_js_path, js_content, force: true
175
+
176
+ # Update the main index.js to import railsui controllers
177
+ index_js_path = Rails.root.join("app/javascript/controllers/index.js")
178
+ if File.exist?(index_js_path)
179
+ index_js_content = File.read(index_js_path)
180
+ # Use absolute importmap path (importmap maps index.js to controllers/railsui without /index)
181
+ unless index_js_content.include?('import "controllers/railsui"')
182
+ # Remove old relative import if it exists
183
+ index_js_content.gsub!(/import\s+["']\.\/railsui["']\s*\n?/, '')
184
+ File.write(index_js_path, index_js_content)
185
+ File.open(index_js_path, 'a') { |f| f.write("\nimport \"controllers/railsui\"\n") }
186
+ say("✓ Added railsui import to controllers/index.js", :green)
187
+ end
188
+ end
189
+
190
+ say("✅ Controllers configured for importmap mode", :green)
82
191
  end
83
192
 
84
193
  def copy_theme_stylesheets(theme)
@@ -87,7 +196,18 @@ module Railsui
87
196
  # Define paths
88
197
  source_path = "themes/#{theme}/stylesheets/railsui"
89
198
  destination_path = "app/assets/stylesheets/railsui"
90
- application_css_path = Rails.root.join("app/assets/stylesheets/application.tailwind.css")
199
+
200
+ # Use tailwindcss-rails v4 default path
201
+ application_css_path = Rails.root.join("app/assets/tailwind/application.css")
202
+
203
+ # Ensure the directory exists
204
+ FileUtils.mkdir_p(application_css_path.dirname)
205
+
206
+ # Create the file if it doesn't exist (in case tailwindcss:install failed)
207
+ unless File.exist?(application_css_path)
208
+ File.write(application_css_path, '@import "tailwindcss";')
209
+ say "✓ Created #{application_css_path.relative_path_from(Rails.root)}", :green
210
+ end
91
211
 
92
212
  # Empty the destination directory before copying
93
213
  FileUtils.rm_rf(Dir.glob("#{destination_path}/*"))
@@ -100,80 +220,306 @@ module Railsui
100
220
  puts "Stylesheet files: 🗄️ #{stylesheet_files}"
101
221
 
102
222
  # Generate import statements for stylesheets
223
+ # Path is relative from app/assets/tailwind/ to app/assets/stylesheets/railsui/
103
224
  import_statements = stylesheet_files.map do |file|
104
- "@import \"./railsui/#{File.basename(file, ".css")}\";"
225
+ "@import \"../stylesheets/railsui/#{File.basename(file, ".css")}\";"
105
226
  end.join("\n")
106
227
 
107
228
  # Read the existing application.tailwind.css content
108
229
  application_css_content = File.exist?(application_css_path) ? File.read(application_css_path) : ""
109
230
 
110
- # Remove old @tailwind directives and import statements for tailwindcss and railsui stylesheets
231
+ # Remove old import statements for tailwindcss and railsui stylesheets
232
+ # BUT preserve actiontext import (core Rails ActionText CSS)
111
233
  cleaned_css_content = application_css_content
112
234
  cleaned_css_content = cleaned_css_content.gsub(/@import "tailwindcss";\n*/, "")
113
235
  cleaned_css_content = cleaned_css_content.gsub(%r{@import "\.\./stylesheets/railsui/.*";\n*}, "")
114
236
  cleaned_css_content = cleaned_css_content.gsub(%r{@import "railsui/.*";\n*}, "")
115
237
  cleaned_css_content = cleaned_css_content.gsub(%r{@import "\./railsui/.*";\n*}, "")
116
238
 
239
+ # Extract and preserve actiontext import if it exists
240
+ actiontext_import_match = cleaned_css_content.match(%r{@import ['"]\.\./stylesheets/actiontext['"];?\n*})
241
+ cleaned_css_content = cleaned_css_content.gsub(%r{@import ['"]\.\./stylesheets/actiontext['"];?\n*}, "") if actiontext_import_match
242
+
243
+ # Check if actiontext.css exists (from action_text:install)
244
+ actiontext_css_path = Rails.root.join("app/assets/stylesheets/actiontext.css")
245
+ actiontext_import = File.exist?(actiontext_css_path) ? '@import "../stylesheets/actiontext";' : nil
246
+
117
247
  # Add the new import statements in the correct order
118
248
  new_application_css_content = [
119
249
  '@import "tailwindcss";',
250
+ actiontext_import,
120
251
  cleaned_css_content.strip, # Preserving existing content
121
252
  import_statements
122
- ].join("\n")
253
+ ].compact.join("\n")
123
254
 
124
255
  # Write the updated content back to application.tailwind.css
125
256
  File.write(application_css_path, new_application_css_content)
126
- say("Updated app/assets/stylesheets/application.tailwind.css successfully.", :green)
257
+ say("Updated #{application_css_path.relative_path_from(Rails.root)} successfully.", :green)
258
+ end
259
+
260
+ # CSS Dependencies (unified for both build and nobuild modes)
261
+ def install_css_dependencies
262
+ say("Installing CSS dependencies (tailwindcss-rails)", :yellow)
263
+
264
+ unless gem_installed?('tailwindcss-rails')
265
+ say "tailwindcss-rails not found. Installing...", :green
266
+ begin
267
+ run "bundle add tailwindcss-rails"
268
+ rescue => e
269
+ say "❌ Failed to install tailwindcss-rails gem", :red
270
+ say "Error: #{e.message}", :red
271
+ say "Please install manually: bundle add tailwindcss-rails", :yellow
272
+ raise
273
+ end
274
+ end
275
+
276
+ # Create builds directory for tailwindcss-rails
277
+ unless File.exist?(Rails.root.join("app/assets/builds/.keep"))
278
+ say "Setting up Tailwind CSS...", :yellow
279
+ FileUtils.mkdir_p(Rails.root.join("app/assets/builds"))
280
+ FileUtils.touch(Rails.root.join("app/assets/builds/.keep"))
281
+ say "✓ Tailwind CSS configured", :green
282
+ end
283
+ end
284
+
285
+ # JS Dependencies for build mode (existing behavior)
286
+ def install_js_dependencies_build(theme)
287
+ say("Installing JS dependencies via package manager", :yellow)
288
+
289
+ # Fix application.js imports for bundler (convert importmap style to bundler style)
290
+ fix_application_js_for_bundler
291
+
292
+ # Install theme-specific packages
293
+ add_yarn_packages(js_theme_dependencies(theme))
294
+ end
295
+
296
+ def fix_application_js_for_bundler
297
+ app_js = Rails.root.join("app/javascript/application.js")
298
+ return unless File.exist?(app_js)
299
+
300
+ content = File.read(app_js)
301
+ original = content.dup
302
+
303
+ # Fix relative imports
304
+ content.gsub!('import "controllers"', 'import "./controllers"')
305
+
306
+ File.write(app_js, content) if content != original
307
+ end
308
+
309
+ # JS Dependencies for nobuild mode (importmap)
310
+ def install_js_dependencies_nobuild(theme)
311
+ say("Installing JS dependencies via importmap", :yellow)
312
+
313
+ # Check if importmap-rails gem is installed
314
+ unless gem_installed?('importmap-rails')
315
+ say "📦 importmap-rails not found. Installing...", :green
316
+ begin
317
+ run "bundle add importmap-rails"
318
+ rescue => e
319
+ say "❌ Failed to install importmap-rails gem", :red
320
+ say "Error: #{e.message}", :red
321
+ say "Please install manually: bundle add importmap-rails", :yellow
322
+ raise
323
+ end
324
+ end
325
+
326
+ # Only run importmap:install if importmap.rb doesn't exist
327
+ # (Rails 8 apps already have it configured)
328
+ unless File.exist?(Rails.root.join("config/importmap.rb"))
329
+ begin
330
+ rails_command "importmap:install"
331
+ rescue => e
332
+ say "❌ Failed to run importmap:install", :red
333
+ say "Error: #{e.message}", :red
334
+ say "Please run manually: rails importmap:install", :yellow
335
+ raise
336
+ end
337
+ end
338
+
339
+ pin_importmap_dependencies(theme)
340
+ add_importmap_css_dependencies(theme)
127
341
  end
128
342
 
343
+ # Legacy method for backward compatibility
129
344
  def install_theme_dependencies(theme)
130
- say("Installing dependencies", :yellow)
131
- add_yarn_packages(theme_dependencies(theme))
345
+ install_js_dependencies_build(theme)
132
346
  end
133
347
 
134
348
  def remove_action_text_defaults
135
- say "Remove default ActionText CSS"
136
- # remove import from application.tailwind.css if present as we add it to another imported css file.
137
-
138
- gsub_file "app/assets/stylesheets/application.tailwind.css", /@import 'actiontext.css';/, ""
349
+ # This method is kept for backwards compatibility but no longer needed
350
+ # ActionText CSS import is now handled in copy_theme_stylesheets
139
351
  end
140
352
 
141
353
  def humanize_theme(theme)
142
354
  theme.humanize
143
355
  end
144
356
 
145
- def theme_dependencies(theme)
357
+ # JS-only dependencies (Tailwind v4 plugins handled by standalone binary via @plugin directive)
358
+ def js_theme_dependencies(theme)
146
359
  case theme
147
360
  when "hound"
148
- ["@tailwindcss/typography", "apexcharts", "railsui-stimulus", "stimulus-use", "tailwindcss@latest", "@tailwindcss/cli@latest", "tippy.js"]
361
+ ["apexcharts", "railsui-stimulus", "stimulus-use", "tippy.js"]
149
362
  when "shepherd"
150
- ["@tailwindcss/typography", "apexcharts", "flatpickr", "hotkeys-js", "photoswipe", "railsui-stimulus",
151
- "stimulus-use", "tippy.js", "tailwindcss@latest", "@tailwindcss/cli@latest"]
363
+ ["apexcharts", "flatpickr", "hotkeys-js", "photoswipe", "railsui-stimulus", "stimulus-use", "tippy.js"]
152
364
  when "corgie"
153
- ["@tailwindcss/typography", "railsui-stimulus", "stimulus-use", "tailwindcss@latest", "@tailwindcss/cli@latest", "tippy.js", "marked", "highlight.js", "sanitize-html"]
365
+ ["railsui-stimulus", "stimulus-use", "tippy.js", "marked", "highlight.js", "sanitize-html"]
154
366
  else
155
- ["@tailwindcss/typography", "railsui-stimulus", "stimulus-use", "tailwindcss@latest",
156
- "@tailwindcss/cli@latest", "tippy.js"]
367
+ ["railsui-stimulus", "stimulus-use", "tippy.js"]
368
+ end
369
+ end
370
+
371
+ # Legacy method for backward compatibility
372
+ def theme_dependencies(theme)
373
+ js_theme_dependencies(theme)
374
+ end
375
+
376
+ # Importmap pins for nobuild mode
377
+ def importmap_theme_dependencies(theme)
378
+ # Core railsui-stimulus dependencies (required for all themes)
379
+ # Note: @hotwired/stimulus is already provided by Rails, so we don't pin it
380
+ # Using esm.sh CDN which provides optimized ESM builds without process.env references
381
+ base_pins = {
382
+ "railsui-stimulus" => "https://unpkg.com/railsui-stimulus@1.1.2/dist/importmap/index.js",
383
+ "tippy.js" => "https://esm.sh/tippy.js@6.3.7",
384
+ "@popperjs/core" => "https://esm.sh/@popperjs/core@2.11.8",
385
+ "stimulus-use" => "https://esm.sh/stimulus-use@0.52.2",
386
+ "flatpickr" => "https://esm.sh/flatpickr@4.6.13"
387
+ }
388
+
389
+ # Theme-specific dependencies
390
+ theme_specific = case theme
391
+ when "hound"
392
+ {
393
+ "apexcharts" => "https://esm.sh/apexcharts@3.45.2"
394
+ }
395
+ when "shepherd"
396
+ {
397
+ "apexcharts" => "https://esm.sh/apexcharts@3.45.2",
398
+ "hotkeys-js" => "https://esm.sh/hotkeys-js@3.13.15",
399
+ "photoswipe" => "https://esm.sh/photoswipe@5.4.3",
400
+ "photoswipe/lightbox" => "https://esm.sh/photoswipe@5.4.3/lightbox"
401
+ }
402
+ when "corgie"
403
+ {
404
+ "marked" => "https://esm.sh/marked@11.1.1",
405
+ "highlight.js" => "https://esm.sh/highlight.js@11.9.0",
406
+ "sanitize-html" => "https://esm.sh/sanitize-html@2.11.0"
407
+ }
408
+ else
409
+ {}
410
+ end
411
+
412
+ base_pins.merge(theme_specific)
413
+ end
414
+
415
+ def pin_importmap_dependencies(theme)
416
+ importmap_file = Rails.root.join("config/importmap.rb")
417
+ return unless File.exist?(importmap_file)
418
+
419
+ importmap_content = File.read(importmap_file)
420
+
421
+ say "Pinning Rails UI dependencies to importmap...", :yellow
422
+
423
+ # Pin external theme dependencies (railsui-stimulus, tippy.js, etc.)
424
+ importmap_theme_dependencies(theme).each do |package, url|
425
+ pin_statement = "pin \"#{package}\", to: \"#{url}\"\n"
426
+ unless importmap_content.include?("pin \"#{package}\"")
427
+ File.open(importmap_file, 'a') { |f| f.write(pin_statement) }
428
+ say "✓ Pinned #{package}", :green
429
+ else
430
+ say "- #{package} already pinned", :cyan
431
+ end
432
+ end
433
+
434
+ # Note: Gem's internal controllers are NOT pinned to importmap because:
435
+ # - They're only used on /railsui/* routes (gem's internal pages)
436
+ # - Those pages use CDN dependencies loaded inline via _cdn_dependencies.html.erb
437
+ # - User-installed theme pages (/rui/*) use theme-specific controllers from app/javascript/controllers/railsui/
438
+
439
+ say "✅ All importmap dependencies pinned successfully!", :green
440
+ end
441
+
442
+ def gem_controllers_dependencies
443
+ # Map import names to propshaft-served paths for gem controllers
444
+ {
445
+ "controllers/railsui/index" => "controllers/railsui/index.js",
446
+ "controllers/railsui/application" => "controllers/application.js",
447
+ "controllers/railsui_anchor_controller" => "controllers/railsui_anchor_controller.js",
448
+ "controllers/railsui_configuration_controller" => "controllers/railsui_configuration_controller.js",
449
+ "controllers/railsui_code_controller" => "controllers/railsui_code_controller.js",
450
+ "controllers/railsui_canvas_controller" => "controllers/railsui_canvas_controller.js",
451
+ "controllers/railsui_dialog_controller" => "controllers/railsui_dialog_controller.js",
452
+ "controllers/railsui_flash_controller" => "controllers/railsui_flash_controller.js",
453
+ "controllers/railsui_helper_controller" => "controllers/railsui_helper_controller.js",
454
+ "controllers/railsui_nav_controller" => "controllers/railsui_nav_controller.js",
455
+ "controllers/railsui_prevent_controller" => "controllers/railsui_prevent_controller.js",
456
+ "controllers/railsui_scroll_controller" => "controllers/railsui_scroll_controller.js",
457
+ "controllers/railsui_scroll_spy_controller" => "controllers/railsui_scroll_spy_controller.js",
458
+ "controllers/railsui_search_controller" => "controllers/railsui_search_controller.js",
459
+ "controllers/railsui_smooth_controller" => "controllers/railsui_smooth_controller.js",
460
+ "controllers/railsui_snippet_controller" => "controllers/railsui_snippet_controller.js",
461
+ "controllers/railsui_pages_controller" => "controllers/railsui_pages_controller.js",
462
+ "controllers/railsui_loading_controller" => "controllers/railsui_loading_controller.js"
463
+ }
464
+ end
465
+
466
+ def add_importmap_css_dependencies(theme)
467
+ application_css_path = Rails.root.join("app/assets/tailwind/application.css")
468
+ return unless File.exist?(application_css_path)
469
+
470
+ css_content = File.read(application_css_path)
471
+
472
+ # Required CSS dependencies for railsui-stimulus components
473
+ css_imports = []
474
+ css_imports << '@import "https://unpkg.com/tippy.js@6.3.7/dist/tippy.css";'
475
+
476
+ # Add flatpickr CSS for themes that use date picker
477
+ if ["shepherd"].include?(theme)
478
+ css_imports << '@import "https://unpkg.com/flatpickr@4.6.13/dist/flatpickr.min.css";'
479
+ end
480
+
481
+ css_imports.each do |import_statement|
482
+ unless css_content.include?(import_statement)
483
+ # Add after @import "tailwindcss" if it exists, otherwise at the top
484
+ if css_content.include?('@import "tailwindcss"')
485
+ css_content.sub!('@import "tailwindcss";', "@import \"tailwindcss\";\n#{import_statement}")
486
+ else
487
+ css_content = "#{import_statement}\n#{css_content}"
488
+ end
489
+ say "✓ Added CSS import: #{import_statement}", :green
490
+ end
157
491
  end
492
+
493
+ File.write(application_css_path, css_content)
494
+ say "✅ CSS dependencies added for importmap mode", :green
158
495
  end
159
496
 
160
497
  def add_yarn_packages(packages)
161
498
  package_manager = detect_package_manager
162
499
  say "Using #{package_manager} to install packages...", :green
163
500
 
164
- case package_manager
165
- when "yarn"
166
- run "yarn add #{packages.join(" ")}"
167
- when "npm"
168
- run "npm install #{packages.join(" ")}"
169
- when "pnpm"
170
- run "pnpm add #{packages.join(" ")}"
171
- when "bun"
172
- run "bun add #{packages.join(" ")}"
173
- else
174
- # Fallback to yarn if no package manager detected
175
- say "No package manager detected, falling back to yarn", :yellow
176
- run "yarn add #{packages.join(" ")}"
501
+ begin
502
+ case package_manager
503
+ when "yarn"
504
+ run "yarn add #{packages.join(" ")}"
505
+ when "npm"
506
+ run "npm install #{packages.join(" ")}"
507
+ when "pnpm"
508
+ run "pnpm add #{packages.join(" ")}"
509
+ when "bun"
510
+ run "bun add #{packages.join(" ")}"
511
+ else
512
+ # Fallback to yarn if no package manager detected
513
+ say "⚠️ No package manager detected, falling back to yarn", :yellow
514
+ run "yarn add #{packages.join(" ")}"
515
+ end
516
+ say "✅ Installed packages: #{packages.join(', ')}", :green
517
+ rescue => e
518
+ say "❌ Failed to install JavaScript packages", :red
519
+ say "Error: #{e.message}", :red
520
+ say "Please install manually:", :yellow
521
+ say " #{package_manager} add #{packages.join(' ')}", :yellow
522
+ raise
177
523
  end
178
524
  end
179
525
 
@@ -195,6 +541,153 @@ module Railsui
195
541
  end
196
542
  end
197
543
 
544
+ def detect_js_bundler
545
+ # Check for bundler configs first (since Rails 8 apps have importmap.rb by default)
546
+ # Check package.json for build script as primary indicator
547
+ package_json = Rails.root.join("package.json")
548
+ if File.exist?(package_json)
549
+ content = File.read(package_json)
550
+ data = JSON.parse(content) rescue {}
551
+ build_script = data.dig("scripts", "build").to_s
552
+
553
+ return "esbuild" if build_script.include?("esbuild")
554
+ return "webpack" if build_script.include?("webpack")
555
+ return "rollup" if build_script.include?("rollup")
556
+ return "bun" if build_script.include?("bun")
557
+ end
558
+
559
+ # Fall back to config file detection
560
+ if File.exist?(Rails.root.join("esbuild.config.js")) || File.exist?(Rails.root.join("esbuild.config.mjs"))
561
+ "esbuild"
562
+ elsif File.exist?(Rails.root.join("webpack.config.js"))
563
+ "webpack"
564
+ elsif File.exist?(Rails.root.join("rollup.config.js"))
565
+ "rollup"
566
+ elsif File.exist?(Rails.root.join("bun.lockb"))
567
+ "bun"
568
+ elsif File.exist?(Rails.root.join("config/importmap.rb"))
569
+ "importmap"
570
+ else
571
+ "unknown"
572
+ end
573
+ end
574
+
575
+ def gem_installed?(gem_name)
576
+ Gem::Specification.find_by_name(gem_name)
577
+ true
578
+ rescue Gem::LoadError
579
+ false
580
+ end
581
+
582
+ def detect_and_warn_about_setup
583
+ warnings = []
584
+ recommendations = []
585
+
586
+ # Check for existing setup
587
+ has_cssbundling = gem_installed?('cssbundling-rails')
588
+ has_jsbundling = gem_installed?('jsbundling-rails')
589
+ has_importmap = File.exist?(Rails.root.join("config/importmap.rb"))
590
+
591
+ # Check for Tailwind in package.json
592
+ has_tailwind_npm = false
593
+ package_json_path = Rails.root.join("package.json")
594
+ if File.exist?(package_json_path)
595
+ package_json = JSON.parse(File.read(package_json_path))
596
+ has_tailwind_npm = package_json.dig('dependencies', 'tailwindcss') ||
597
+ package_json.dig('devDependencies', 'tailwindcss')
598
+ end
599
+
600
+ # Detect legacy build mode setup (cssbundling + jsbundling)
601
+ legacy_build_setup = (has_cssbundling || has_tailwind_npm) && has_jsbundling
602
+
603
+ if legacy_build_setup && !options[:build]
604
+ say ""
605
+ say "=" * 70, :red
606
+ say "❌ Build mode setup detected but --build flag not provided", :red
607
+ say "=" * 70, :red
608
+ say ""
609
+ say "Your app uses build mode (cssbundling + jsbundling).", :yellow
610
+ say "You must choose a mode before installing:", :yellow
611
+ say ""
612
+ say "Keep build mode (recommended):", :cyan
613
+ say " rails railsui:install --build", :cyan
614
+ say " rails railsui:migrate_to_tailwindcss_rails", :cyan
615
+ say ""
616
+ say "Or switch to no-build (requires manual JS refactoring):", :cyan
617
+ say " bundle add importmap-rails && rails importmap:install", :cyan
618
+ say " rails railsui:install", :cyan
619
+ say ""
620
+ exit(1)
621
+ elsif has_cssbundling && !has_jsbundling
622
+ warnings << "⚠️ Detected cssbundling-rails gem"
623
+ recommendations << "After install, migrate to tailwindcss-rails:"
624
+ recommendations << " rails railsui:migrate_to_tailwindcss_rails"
625
+ elsif has_jsbundling && !has_importmap && !options[:build]
626
+ say ""
627
+ say "=" * 70, :red
628
+ say "❌ jsbundling-rails detected but --build flag not provided", :red
629
+ say "=" * 70, :red
630
+ say ""
631
+ say "Your app has jsbundling-rails installed.", :yellow
632
+ say "You must choose a mode before installing:", :yellow
633
+ say ""
634
+ say "Use JS bundler (recommended): rails railsui:install --build", :cyan
635
+ say ""
636
+ say "Or switch to importmap (requires manual JS refactoring):", :cyan
637
+ say " bundle add importmap-rails && rails importmap:install", :cyan
638
+ say " rails railsui:install", :cyan
639
+ say ""
640
+ exit(1)
641
+ end
642
+
643
+ # Inform about coexisting importmap when using build mode
644
+ if options[:build] && has_importmap && bundler != "importmap"
645
+ warnings << "ℹ️ Both importmap.rb and JS bundler detected"
646
+ recommendations << "Rails UI will install packages for the bundler and migrate imports"
647
+ recommendations << "It's recommended to use the bundler format rather than mixing both"
648
+ recommendations << "You can remove config/importmap.rb if you don't need it"
649
+ recommendations << ""
650
+ recommendations << "Note: You may see build errors during installation - these will be"
651
+ recommendations << "resolved as Rails UI installs the required packages"
652
+ end
653
+
654
+ if !has_jsbundling && !has_importmap
655
+ warnings << "ℹ️ No JS bundler or importmap detected"
656
+ if options[:build]
657
+ recommendations << "Please install a JS bundler first:"
658
+ recommendations << " bundle add jsbundling-rails"
659
+ recommendations << " rails javascript:install:[bun|esbuild|rollup|webpack]"
660
+ recommendations << "Then run: rails railsui:install --build"
661
+ else
662
+ recommendations << "Will install importmap-rails for you (Rails 8 default)"
663
+ end
664
+ end
665
+
666
+ # Display warnings
667
+ if warnings.any?
668
+ say ""
669
+ say "=" * 60, :yellow
670
+ warnings.each { |w| say w, :yellow }
671
+ say "=" * 60, :yellow
672
+ say ""
673
+ if recommendations.any?
674
+ say "Recommendations:", :cyan
675
+ recommendations.each { |r| say " #{r}", :cyan }
676
+ say ""
677
+ end
678
+
679
+ unless ENV['RAILSUI_SKIP_WARNINGS']
680
+ print "Continue with installation? (y/n): "
681
+ response = STDIN.gets.chomp.downcase
682
+ unless response == 'y'
683
+ say "Installation cancelled", :red
684
+ exit
685
+ end
686
+ say ""
687
+ end
688
+ end
689
+ end
690
+
198
691
  # Mailers
199
692
  def generate_sample_mailers(theme)
200
693
  say "Adding Rails UI mailers", :yellow
@@ -346,6 +839,73 @@ module Railsui
346
839
  directory theme_images_dir, target_images_dir, force: true
347
840
  end
348
841
 
842
+ def copy_procfile
843
+ config = Railsui::Configuration.load!
844
+ procfile_path = Rails.root.join("Procfile.dev")
845
+
846
+ if config.build?
847
+ # In build mode, jsbundling-rails already created the Procfile
848
+ # We just need to add the CSS line if it's not already there
849
+ if File.exist?(procfile_path)
850
+ content = File.read(procfile_path)
851
+ unless content.include?("tailwindcss:watch")
852
+ File.open(procfile_path, "a") do |f|
853
+ f.puts "css: bin/rails tailwindcss:watch"
854
+ end
855
+ say("Added CSS watch to existing Procfile.dev", :green)
856
+ else
857
+ say("✓ Procfile.dev already has CSS watch (skipping)", :yellow)
858
+ end
859
+ else
860
+ # Fallback if Procfile doesn't exist (shouldn't happen with jsbundling-rails)
861
+ copy_file "Procfile.dev.build", procfile_path
862
+ say("Created Procfile.dev for build mode", :green)
863
+ end
864
+ else
865
+ # In nobuild mode, ensure required processes exist without being destructive
866
+ if File.exist?(procfile_path)
867
+ content = File.read(procfile_path)
868
+ lines_to_add = []
869
+
870
+ # Check for web process
871
+ unless content.match?(/^web:/)
872
+ lines_to_add << "web: bin/rails server -p 3000"
873
+ end
874
+
875
+ # Check for CSS process
876
+ unless content.include?("tailwindcss:watch")
877
+ lines_to_add << "css: bin/rails tailwindcss:watch"
878
+ end
879
+
880
+ if lines_to_add.any?
881
+ File.open(procfile_path, "a") do |f|
882
+ lines_to_add.each { |line| f.puts line }
883
+ end
884
+ say("Added missing processes to Procfile.dev: #{lines_to_add.join(', ')}", :green)
885
+ else
886
+ say("✓ Procfile.dev already has required processes (skipping)", :yellow)
887
+ end
888
+ else
889
+ # No Procfile exists, create it
890
+ copy_file "Procfile.dev.nobuild", procfile_path
891
+ say("Created Procfile.dev for no-build mode", :green)
892
+ end
893
+ end
894
+ end
895
+
896
+ def copy_bin_dev
897
+ bin_dev_path = Rails.root.join("bin/dev")
898
+
899
+ # Only copy if it doesn't exist or if it's the simple Rails server version
900
+ if !File.exist?(bin_dev_path) || File.read(bin_dev_path).include?('exec "./bin/rails", "server"')
901
+ copy_file "bin/dev", bin_dev_path, force: true
902
+ chmod bin_dev_path, 0755
903
+ say("Created bin/dev script", :green)
904
+ else
905
+ say("✓ bin/dev already exists (skipping)", :yellow)
906
+ end
907
+ end
908
+
349
909
  private
350
910
 
351
911
  def remove_directory(directory_path, thing)