ultimate_turbo_modal 2.1.2 → 2.2.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/.tool-versions +1 -1
  3. data/CHANGELOG.md +17 -0
  4. data/CLAUDE.md +188 -2
  5. data/Gemfile.lock +1 -1
  6. data/README.md +22 -0
  7. data/VERSION +1 -1
  8. data/javascript/index.js +12 -3
  9. data/javascript/modal_controller.js +16 -4
  10. data/javascript/package.json +2 -1
  11. data/javascript/styles/vanilla.css +86 -0
  12. data/lib/generators/ultimate_turbo_modal/base.rb +117 -0
  13. data/lib/generators/ultimate_turbo_modal/install_generator.rb +46 -95
  14. data/lib/generators/ultimate_turbo_modal/templates/flavors/custom.rb +29 -2
  15. data/lib/generators/ultimate_turbo_modal/templates/flavors/tailwind.rb +30 -3
  16. data/lib/generators/ultimate_turbo_modal/templates/flavors/tailwind3.rb +29 -2
  17. data/lib/generators/ultimate_turbo_modal/templates/flavors/vanilla.rb +31 -3
  18. data/lib/generators/ultimate_turbo_modal/update_generator.rb +109 -0
  19. data/lib/ultimate_turbo_modal/base.rb +35 -14
  20. data/lib/ultimate_turbo_modal.rb +2 -0
  21. data/script/build_and_release.sh +20 -7
  22. metadata +8 -99
  23. data/demo-app/.gitattributes +0 -7
  24. data/demo-app/.gitignore +0 -34
  25. data/demo-app/.ruby-version +0 -1
  26. data/demo-app/.tool-versions +0 -2
  27. data/demo-app/Gemfile +0 -41
  28. data/demo-app/Gemfile.lock +0 -248
  29. data/demo-app/Procfile.dev +0 -3
  30. data/demo-app/README.md +0 -15
  31. data/demo-app/Rakefile +0 -6
  32. data/demo-app/app/assets/builds/.keep +0 -0
  33. data/demo-app/app/assets/images/.keep +0 -0
  34. data/demo-app/app/assets/stylesheets/application.css +0 -42
  35. data/demo-app/app/controllers/application_controller.rb +0 -10
  36. data/demo-app/app/controllers/concerns/.keep +0 -0
  37. data/demo-app/app/controllers/concerns/set_flavor.rb +0 -10
  38. data/demo-app/app/controllers/hide_from_backends_controller.rb +0 -34
  39. data/demo-app/app/controllers/modal_controller.rb +0 -11
  40. data/demo-app/app/controllers/posts_controller.rb +0 -69
  41. data/demo-app/app/controllers/welcome_controller.rb +0 -2
  42. data/demo-app/app/helpers/application_helper.rb +0 -9
  43. data/demo-app/app/javascript/application.js +0 -4
  44. data/demo-app/app/javascript/controllers/application.js +0 -13
  45. data/demo-app/app/javascript/controllers/dark_mode_controller.js +0 -28
  46. data/demo-app/app/javascript/controllers/flash_controller.js +0 -9
  47. data/demo-app/app/javascript/controllers/hello_controller.js +0 -7
  48. data/demo-app/app/javascript/controllers/index.js +0 -15
  49. data/demo-app/app/models/application_record.rb +0 -3
  50. data/demo-app/app/models/concerns/.keep +0 -0
  51. data/demo-app/app/models/post.rb +0 -4
  52. data/demo-app/app/views/hide_from_backends/_notice.html.erb +0 -10
  53. data/demo-app/app/views/hide_from_backends/create.turbo_stream.erb +0 -2
  54. data/demo-app/app/views/hide_from_backends/new.html.erb +0 -30
  55. data/demo-app/app/views/layouts/application.html.erb +0 -40
  56. data/demo-app/app/views/modal/index.html.erb +0 -45
  57. data/demo-app/app/views/modal/show.html.erb +0 -39
  58. data/demo-app/app/views/posts/_form.html.erb +0 -36
  59. data/demo-app/app/views/posts/_post.html.erb +0 -9
  60. data/demo-app/app/views/posts/edit.html.erb +0 -9
  61. data/demo-app/app/views/posts/index.html.erb +0 -14
  62. data/demo-app/app/views/posts/new.html.erb +0 -3
  63. data/demo-app/app/views/posts/show.html.erb +0 -21
  64. data/demo-app/app/views/shared/_flash.html.erb +0 -13
  65. data/demo-app/app/views/welcome/index.html.erb +0 -19
  66. data/demo-app/bin/bundle +0 -109
  67. data/demo-app/bin/dev +0 -18
  68. data/demo-app/bin/rails +0 -4
  69. data/demo-app/bin/rake +0 -4
  70. data/demo-app/bin/setup +0 -34
  71. data/demo-app/config/application.rb +0 -43
  72. data/demo-app/config/boot.rb +0 -3
  73. data/demo-app/config/credentials.yml.enc +0 -1
  74. data/demo-app/config/database.yml +0 -25
  75. data/demo-app/config/environment.rb +0 -14
  76. data/demo-app/config/environments/development.rb +0 -51
  77. data/demo-app/config/environments/production.rb +0 -67
  78. data/demo-app/config/environments/test.rb +0 -42
  79. data/demo-app/config/initializers/assets.rb +0 -7
  80. data/demo-app/config/initializers/content_security_policy.rb +0 -25
  81. data/demo-app/config/initializers/filter_parameter_logging.rb +0 -8
  82. data/demo-app/config/initializers/inflections.rb +0 -14
  83. data/demo-app/config/initializers/new_framework_defaults_8_0.rb +0 -30
  84. data/demo-app/config/initializers/permissions_policy.rb +0 -11
  85. data/demo-app/config/initializers/ultimate_turbo_modal.rb +0 -12
  86. data/demo-app/config/initializers/ultimate_turbo_modal_tailwind.rb +0 -21
  87. data/demo-app/config/initializers/ultimate_turbo_modal_vanilla.rb +0 -21
  88. data/demo-app/config/locales/en.yml +0 -33
  89. data/demo-app/config/puma.rb +0 -41
  90. data/demo-app/config/routes.rb +0 -9
  91. data/demo-app/config.ru +0 -6
  92. data/demo-app/db/migrate/20230331002502_create_posts.rb +0 -10
  93. data/demo-app/db/migrate/20231031012703_add_post.rb +0 -9
  94. data/demo-app/db/migrate/20231128141054_add_publish_on_to_posts.rb +0 -5
  95. data/demo-app/db/schema.rb +0 -22
  96. data/demo-app/lib/assets/.keep +0 -0
  97. data/demo-app/lib/tasks/.keep +0 -0
  98. data/demo-app/log/.keep +0 -0
  99. data/demo-app/package.json +0 -28
  100. data/demo-app/postcss.config.js +0 -7
  101. data/demo-app/public/400.html +0 -114
  102. data/demo-app/public/404.html +0 -114
  103. data/demo-app/public/406-unsupported-browser.html +0 -114
  104. data/demo-app/public/422.html +0 -114
  105. data/demo-app/public/500.html +0 -114
  106. data/demo-app/public/apple-touch-icon-precomposed.png +0 -0
  107. data/demo-app/public/apple-touch-icon.png +0 -0
  108. data/demo-app/public/favicon.ico +0 -0
  109. data/demo-app/public/icon.png +0 -0
  110. data/demo-app/public/icon.svg +0 -3
  111. data/demo-app/public/img/bootstrap-logo-shadow.png +0 -0
  112. data/demo-app/public/img/unicat.jpg +0 -0
  113. data/demo-app/public/img/vanilla.png +0 -0
  114. data/demo-app/public/robots.txt +0 -1
  115. data/demo-app/tmp/.keep +0 -0
  116. data/demo-app/tmp/pids/.keep +0 -0
  117. data/demo-app/vendor/.keep +0 -0
  118. data/demo-app/yarn.lock +0 -1035
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17d0eab498c9c828467bea1e33c1250763a8361595847a29a421bedc84947f06
4
- data.tar.gz: 41ab99f7579857ccd65d7c549d8626d2566fb952cabb5c97b7f8193e7930fa44
3
+ metadata.gz: 8076c3d9802abd28ef2ff1858b7268f730455c69a214dfb2f8e04d3695c8f68f
4
+ data.tar.gz: fa211158221a41661229457a63874690eac7e07fb19159045362eb0341d4ccee
5
5
  SHA512:
6
- metadata.gz: e4925980e51ac9f8fc7e08245e8d79e3a73faca356e44ae45bedfd089e3ec3d2de17e2a5ab264f57dcae0c79b91a53e37ee3661dcb57541b422b563f3e2a8ff1
7
- data.tar.gz: 93f0bb944adfcc54e844ce3d2d3438bdfdaabb3cd648bb22b87937a6b8267a46e842db622ffc7c04c7b9a2a578da3ea1c4c411742f44821368c4fd299a1c411f
6
+ metadata.gz: 9414a16b2a0de555727b258403230d5a65564e1158b95c61904cca0fd0b9280fe5433eaeb156962b1dbb1bbcdfe25a86d1ec7b1a65c03209211e5b97abb2fde7
7
+ data.tar.gz: 783c9c2af43c3b31e8e974642545ac094e2c5faf1925c84947186e215ba809daac4a6236afa7fc1e24768379f1a12376515982a48cd08350274a9a8a2230fc7b
data/.tool-versions CHANGED
@@ -1,2 +1,2 @@
1
- ruby 3.4.4
1
+ ruby 3.3.9
2
2
  nodejs 22.14.0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## [2.2.2] - 2026-03-12
2
+
3
+ - Added `close` function on Stimulus Controller. Thanks @bendangelo
4
+ - Focus bug fix. Thanks @pasl
5
+ - Refactor generator and fix [bug #33](https://github.com/cmer/ultimate_turbo_modal/issues/33).
6
+
7
+ ## [2.2.1] - 2025-08-08
8
+
9
+ - Added `rails generate ultimate_turbo_modal:update` for easy updates
10
+ - Exclude demo-app directory from gem package
11
+
12
+ ## [2.2.0] - 2025-08-07
13
+
14
+ - BREAKING: Make sure to re-run the generator `rails generate ultimate_turbo_modal:install` after install.
15
+ - Fixed transistions that were sometimes not showing
16
+ - Improved demo app to make it easier to use for development
17
+
1
18
  ## [2.1.2] - 2025-08-06
2
19
 
3
20
  - Fixed scroll lock
data/CLAUDE.md CHANGED
@@ -4,7 +4,187 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Overview
6
6
 
7
- Ultimate Turbo Modal (UTMR) is a full-featured modal implementation for Rails applications using Turbo, Stimulus, and Hotwire. It consists of both a Ruby gem and an npm package that work together.
7
+ Ultimate Turbo Modal (UTMR) is a full-featured modal implementation for Rails applications using Turbo, Stimulus, and Hotwire. It consists of both a Ruby gem and an npm package that work together to provide seamless modal functionality with proper focus management, history manipulation, and customizable styling.
8
+
9
+ ## Architecture
10
+
11
+ ### High-Level Design
12
+
13
+ The system follows a separation of concerns between server-side rendering (Ruby/Rails) and client-side behavior (JavaScript/Stimulus):
14
+
15
+ 1. **Server-Side (Ruby Gem)**: Handles HTML generation, configuration management, and Rails integration
16
+ 2. **Client-Side (JavaScript Package)**: Manages modal behavior, focus trapping, scroll locking, and Turbo interactions
17
+ 3. **Communication Layer**: Uses Turbo Frames, Turbo Streams, and data attributes to coordinate between server and client
18
+
19
+ ### Core Components
20
+
21
+ #### Ruby Gem Architecture
22
+
23
+ - **Module Structure**: `UltimateTurboModal` is the main module that delegates to configuration and instantiates modal classes
24
+ - **Base Class**: `UltimateTurboModal::Base` extends `Phlex::HTML` for component-based HTML generation
25
+ - **Configuration System**: Centralized configuration with validation and type checking
26
+ - **Flavor System**: CSS framework-specific implementations (Tailwind, Vanilla, Custom) that define styling classes
27
+ - **Rails Integration**: Via Railtie that injects helpers into ActionController and ActionView
28
+
29
+ #### JavaScript Architecture
30
+
31
+ - **Stimulus Controller**: `modal_controller.js` handles all modal interactions
32
+ - **Dependencies**:
33
+ - `el-transition`: For smooth enter/leave animations
34
+ - `focus-trap`: For accessibility-compliant focus management
35
+ - `idiomorph`: For intelligent DOM morphing to prevent flicker
36
+ - **Global Registration**: Modal instance exposed as `window.modal` for programmatic access
37
+ - **Turbo Integration**: Custom stream actions and frame handling
38
+
39
+ ## Detailed Implementation
40
+
41
+ ### Ruby Components
42
+
43
+ #### `UltimateTurboModal` Module (`lib/ultimate_turbo_modal.rb`)
44
+ - Entry point for the gem
45
+ - Factory method `new` creates modal instances
46
+ - `modal_class` method dynamically loads flavor classes based on configuration
47
+ - Extends self for module-level methods
48
+
49
+ #### `Base` Class (`lib/ultimate_turbo_modal/base.rb`)
50
+ - **Inheritance**: `Phlex::HTML` for HTML generation with Ruby DSL
51
+ - **Mixins**:
52
+ - `Phlex::DeferredRenderWithMainContent` for content block handling
53
+ - Dynamic inclusion of Turbo helpers (FramesHelper, StreamsHelper)
54
+ - **Key Methods**:
55
+ - `initialize`: Accepts configuration options with defaults from global config
56
+ - `view_template`: Main rendering method that wraps content in appropriate Turbo tags
57
+ - `modal`: Orchestrates HTML structure generation
58
+ - `div_*` methods: Generate specific HTML elements with proper classes and attributes
59
+ - **Data Attributes**: Passes configuration to JavaScript via data attributes on the container div
60
+
61
+ #### `Configuration` Class (`lib/ultimate_turbo_modal/configuration.rb`)
62
+ - **Options with Validation**:
63
+ - `flavor`: Symbol/String for CSS framework (default: `:tailwind`)
64
+ - `close_button`: Boolean for showing close button
65
+ - `advance`: Boolean for browser history manipulation
66
+ - `padding`: Boolean or String for content padding
67
+ - `header`, `header_divider`, `footer_divider`: Boolean display options
68
+ - `allowed_click_outside_selector`: Array of CSS selectors that won't dismiss modal
69
+ - **Type Safety**: Each setter validates input types and raises `ArgumentError` on invalid values
70
+
71
+ #### Rails Helpers
72
+
73
+ ##### `ViewHelper` (`helpers/view_helper.rb`)
74
+ - `modal` method: Renders modal component with current request context
75
+ - Instantiates `UltimateTurboModal` with passed options
76
+
77
+ ##### `ControllerHelper` (`helpers/controller_helper.rb`)
78
+ - `inside_modal?` method: Detects if request is within modal context
79
+ - Uses `Turbo-Frame` header to determine modal context
80
+ - Exposed as helper method to views
81
+
82
+ ##### `StreamHelper` (`helpers/stream_helper.rb`)
83
+ - `modal` method: Generates Turbo Stream actions for modal control
84
+ - Supports `:close` and `:hide` messages
85
+ - Creates custom `modal` stream action with message attribute
86
+
87
+ #### Flavor System
88
+ - Located in generator templates (`lib/generators/ultimate_turbo_modal/templates/flavors/`)
89
+ - Each flavor defines CSS class constants for modal elements:
90
+ - `DIV_DIALOG_CLASSES`, `DIV_OVERLAY_CLASSES`, `DIV_OUTER_CLASSES`, etc.
91
+ - Flavors inherit from `Base` and override class constants
92
+ - Supports Tailwind (v3 and v4), Vanilla CSS, and Custom implementations
93
+
94
+ ### JavaScript Components
95
+
96
+ #### Modal Controller (`javascript/modal_controller.js`)
97
+
98
+ ##### Stimulus Configuration
99
+ - **Targets**: `container`, `content`
100
+ - **Values**: `advanceUrl`, `allowedClickOutsideSelector`
101
+ - **Actions**: Responds to keyboard, click, and Turbo events
102
+
103
+ ##### Lifecycle Methods
104
+ - **`connect()`**:
105
+ - Initializes focus trap and scroll lock variables
106
+ - Shows modal immediately
107
+ - Sets up popstate listener for browser back button
108
+ - Exposes controller as `window.modal`
109
+ - **`disconnect()`**: Cleans up focus trap and global reference
110
+
111
+ ##### Core Functionality
112
+
113
+ ###### Modal Display
114
+ - **`showModal()`**:
115
+ - Locks body scroll
116
+ - Triggers enter transition
117
+ - Activates focus trap after transition
118
+ - Pushes history state if `advance` is enabled
119
+ - **`hideModal()`**:
120
+ - Prevents double-hiding with `hidingModal` flag
121
+ - Dispatches cancelable `modal:closing` event
122
+ - Deactivates focus trap
123
+ - Triggers leave transition
124
+ - Cleans up DOM and history
125
+ - Dispatches `modal:closed` event
126
+
127
+ ###### Focus Management (`#activateFocusTrap()`, `#deactivateFocusTrap()`)
128
+ - Creates focus trap with sensible defaults
129
+ - Finds first focusable element or focuses modal itself
130
+ - Handles errors gracefully without breaking modal
131
+ - Respects modal's own keyboard/click handlers
132
+
133
+ ###### Scroll Locking (`#lockBodyScroll()`, `#unlockBodyScroll()`)
134
+ - Stores current scroll position
135
+ - Sets body to `position: fixed` to prevent scroll
136
+ - Restores original overflow and scroll position on unlock
137
+ - Prevents layout shift during modal display
138
+
139
+ ###### History Management
140
+ - Uses data attribute on body to track history state
141
+ - `#hasHistoryAdvanced()`, `#setHistoryAdvanced()`, `#resetHistoryAdvanced()`
142
+ - Coordinates with browser back button via popstate listener
143
+
144
+ ###### Event Handlers
145
+ - **`submitEnd()`**: Closes modal on successful form submission
146
+ - **`closeWithKeyboard()`**: ESC key handler
147
+ - **`outsideModalClicked()`**: Dismisses modal on outside clicks unless allowed selector matches
148
+
149
+ ###### Version Checking
150
+ - `#checkVersions()`: Warns about gem/npm version mismatches in development
151
+ - Helps developers keep packages in sync
152
+
153
+ #### Main Package Entry (`javascript/index.js`)
154
+
155
+ ##### Turbo Stream Actions
156
+ - Registers custom `modal` stream action
157
+ - Handles `hide` and `close` messages via `window.modal` reference
158
+
159
+ ##### Turbo Frame Integration
160
+ - **`handleTurboFrameMissing`**: Escapes modal on redirects
161
+ - **`handleTurboBeforeFrameRender`**: Uses Idiomorph for intelligent morphing
162
+ - Prevents flicker and unwanted animations
163
+ - Morphs only innerHTML to preserve modal container
164
+
165
+ ### Modal Lifecycle Flow
166
+
167
+ 1. **Trigger**: Link/form targets `data-turbo-frame="modal"`
168
+ 2. **Request**: Rails controller renders modal content
169
+ 3. **Response**:
170
+ - If Turbo Frame request: Wrapped in `<turbo-frame id="modal">`
171
+ - If Turbo Stream: Wrapped in stream action targeting modal
172
+ 4. **Client Processing**:
173
+ - Turbo updates modal frame content
174
+ - Stimulus controller connects and shows modal
175
+ - Focus trap activates, scroll locks
176
+ - History state pushed (if enabled)
177
+ 5. **Interaction**:
178
+ - User interacts with modal content
179
+ - Form submissions handled via Turbo
180
+ - ESC key, close button, or outside clicks trigger hiding
181
+ 6. **Dismissal**:
182
+ - `modal:closing` event fired (cancelable)
183
+ - Focus trap deactivates
184
+ - Leave transition plays
185
+ - DOM cleaned up
186
+ - History restored
187
+ - `modal:closed` event fired
8
188
 
9
189
  ## Project Structure
10
190
 
@@ -12,13 +192,19 @@ Ultimate Turbo Modal (UTMR) is a full-featured modal implementation for Rails ap
12
192
  - `base.rb`: Core modal component (Phlex-based)
13
193
  - `configuration.rb`: Global configuration management
14
194
  - `helpers/`: Rails helpers for views and controllers
195
+ - `railtie.rb`: Rails integration setup
15
196
  - Generators in `/lib/generators/` for installation
16
197
 
17
198
  - **JavaScript Package**: Located in `/javascript/`
18
199
  - `modal_controller.js`: Stimulus controller for modal behavior
19
- - `index.js`: Main entry point
200
+ - `index.js`: Main entry point with Turbo integration
201
+ - `styles/`: CSS files for vanilla styling
20
202
  - Distributed files built to `/javascript/dist/`
21
203
 
204
+ - **Demo Application**: Located in `/demo-app/`
205
+ - `Procfile.dev`: Development process file for overmind/foreman
206
+ - `bin/dev`: Development script for starting the demo app
207
+
22
208
  ## Common Development Commands
23
209
 
24
210
  ### JavaScript Development (run from `/javascript/` directory)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ultimate_turbo_modal (2.1.2)
4
+ ultimate_turbo_modal (2.2.2)
5
5
  actionpack
6
6
  activesupport
7
7
  phlex-rails
data/README.md CHANGED
@@ -157,6 +157,28 @@ The demo app provides examples of:
157
157
  - Various trigger methods
158
158
  - Advanced features like scrollable content and custom footers
159
159
 
160
+ ## Updating between minor versions
161
+
162
+ To upgrade within the same major version (for example 2.1 → 2.2):
163
+
164
+ 1. Change the UTMR gem version in your `Gemfile`:
165
+
166
+ ```ruby
167
+ gem "ultimate_turbo_modal", "~> 2.2"
168
+ ```
169
+
170
+ 2. Install updated dependencies:
171
+
172
+ ```sh
173
+ bundle install
174
+ ```
175
+
176
+ 3. Run the update generator:
177
+
178
+ ```sh
179
+ bundle exec rails g ultimate_turbo_modal:update
180
+ ```
181
+
160
182
  ## Upgrading from 1.x
161
183
 
162
184
  Please see the [Upgrading Guide](UPGRADING.md) for detailed instructions on how to upgrade from version 1.x.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.2
1
+ 2.2.2
data/javascript/index.js CHANGED
@@ -8,10 +8,19 @@ Turbo.StreamActions.modal = function () {
8
8
  if (message == "close") window.modal?.hide();
9
9
  };
10
10
 
11
+ // Check if the event target is one of our modal Turbo Frames
12
+ const isModalFrameTarget = (event) => {
13
+ const target = event?.target;
14
+ return (
15
+ target instanceof Element &&
16
+ target.tagName.toLowerCase() === 'turbo-frame' &&
17
+ (target.id === 'modal' || target.id === 'modal-inner')
18
+ );
19
+ };
20
+
11
21
  // Escape modal from the backend on redirects
12
22
  const handleTurboFrameMissing = (event) => {
13
- if (event.detail.response.redirected &&
14
- event.target == document.querySelector("turbo-frame#modal")) {
23
+ if (event.detail.response.redirected && isModalFrameTarget(event)) {
15
24
  event.preventDefault()
16
25
  event.detail.visit(event.detail.response)
17
26
  }
@@ -19,7 +28,7 @@ const handleTurboFrameMissing = (event) => {
19
28
 
20
29
  // Morph the innerHTML of the modal to prevent flickering and transition animations
21
30
  const handleTurboBeforeFrameRender = (event) => {
22
- if (event.target.id === "modal") {
31
+ if (isModalFrameTarget(event)) {
23
32
  event.detail.render = (currentElement, newElement) => {
24
33
  Idiomorph.morph(currentElement, newElement, {
25
34
  morphstyle: 'innerHTML'
@@ -6,7 +6,7 @@ import { createFocusTrap } from 'focus-trap';
6
6
  const PACKAGE_VERSION = '__PACKAGE_VERSION__';
7
7
 
8
8
  export default class extends Controller {
9
- static targets = ["container", "content"]
9
+ static targets = ["container", "content", "overlay", "outer"]
10
10
  static values = {
11
11
  advanceUrl: String,
12
12
  allowedClickOutsideSelector: String
@@ -48,7 +48,11 @@ export default class extends Controller {
48
48
  // Lock body scroll
49
49
  this.#lockBodyScroll();
50
50
 
51
- enter(this.containerTarget).then(() => {
51
+ // Apply transitions to both overlay and outer elements
52
+ Promise.all([
53
+ enter(this.overlayTarget),
54
+ enter(this.outerTarget)
55
+ ]).then(() => {
52
56
  // Activate focus trap after the modal transition is complete
53
57
  this.#activateFocusTrap();
54
58
  });
@@ -92,6 +96,10 @@ export default class extends Controller {
92
96
  this.hideModal();
93
97
  }
94
98
 
99
+ close() {
100
+ this.hideModal();
101
+ }
102
+
95
103
  refreshPage() {
96
104
  window.Turbo.visit(window.location.href, { action: "replace" });
97
105
  }
@@ -122,7 +130,11 @@ export default class extends Controller {
122
130
  // Unlock body scroll
123
131
  this.#unlockBodyScroll();
124
132
 
125
- leave(this.containerTarget).then(() => {
133
+ // Apply leave transitions to both overlay and outer elements
134
+ Promise.all([
135
+ leave(this.overlayTarget),
136
+ leave(this.outerTarget)
137
+ ]).then(() => {
126
138
  this.turboFrame.removeAttribute("src");
127
139
  this.containerTarget.remove();
128
140
  this.#resetHistoryAdvanced();
@@ -170,7 +182,7 @@ export default class extends Controller {
170
182
  initialFocus: () => {
171
183
  // Try to focus the first focusable element, or the modal itself
172
184
  const firstFocusable = this.contentTarget.querySelector(
173
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
185
+ 'button:not([tabindex="-1"]), [href]:not([tabindex="-1"]), input:not([tabindex="-1"]), select:not([tabindex="-1"]), textarea:not([tabindex="-1"]), [tabindex]:not([tabindex="-1"])'
174
186
  );
175
187
  return firstFocusable || this.contentTarget;
176
188
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate_turbo_modal",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "description": "The ultimate Turbo / Stimulus / Hotwire modal window for Rails",
5
5
  "main": "dist/ultimate_turbo_modal.min.js",
6
6
  "module": "dist/ultimate_turbo_modal.min.js",
@@ -10,6 +10,7 @@
10
10
  "scripts": {
11
11
  "update-version": "node scripts/update-version.js",
12
12
  "build": "yarn install && rollup -c",
13
+ "build:watch": "rollup -c -w",
13
14
  "release": "bash scripts/release-npm.sh"
14
15
  },
15
16
  "repository": {
@@ -1,3 +1,7 @@
1
+ .hidden {
2
+ display: none
3
+ }
4
+
1
5
  .dark {
2
6
  .modal-overlay {
3
7
  background-color: rgba(17, 24, 39, 0.8)
@@ -177,3 +181,85 @@
177
181
  width: 1.25rem;
178
182
  height: 1.25rem;
179
183
  }
184
+
185
+ /*
186
+ Transition utilities for Vanilla flavor
187
+ These replicate the Tailwind transitions defined in tailwind.rb
188
+ using plain CSS classes referenced in vanilla.rb
189
+ */
190
+
191
+ /* Overlay: Enter */
192
+ .modal-transition-overlay-enter-animation {
193
+ transition-property: opacity;
194
+ transition-duration: 300ms; /* duration-300 */
195
+ transition-timing-function: ease-out; /* ease-out */
196
+ }
197
+
198
+ .modal-transition-overlay-enter-start {
199
+ opacity: 0; /* opacity-0 */
200
+ }
201
+
202
+ .modal-transition-overlay-enter-end {
203
+ opacity: 1; /* opacity-100 */
204
+ }
205
+
206
+ /* Overlay: Leave */
207
+ .modal-transition-overlay-leave-animation {
208
+ transition-property: opacity;
209
+ transition-duration: 200ms; /* duration-200 */
210
+ transition-timing-function: ease-in; /* ease-in */
211
+ }
212
+
213
+ .modal-transition-overlay-leave-start {
214
+ opacity: 1; /* opacity-100 */
215
+ }
216
+
217
+ .modal-transition-overlay-leave-end {
218
+ opacity: 0; /* opacity-0 */
219
+ }
220
+
221
+ /* Dialog: Enter */
222
+ .modal-transition-dialog-enter-animation {
223
+ transition-property: opacity, transform;
224
+ transition-duration: 300ms; /* duration-300 */
225
+ transition-timing-function: ease-out; /* ease-out */
226
+ }
227
+
228
+ .modal-transition-dialog-enter-start {
229
+ opacity: 0; /* opacity-0 */
230
+ transform: translateY(1rem) scale(1); /* translate-y-4 */
231
+ }
232
+
233
+ @media (min-width: 640px) {
234
+ .modal-transition-dialog-enter-start {
235
+ transform: translateY(0) scale(0.95); /* sm:translate-y-0 sm:scale-95 */
236
+ }
237
+ }
238
+
239
+ .modal-transition-dialog-enter-end {
240
+ opacity: 1; /* opacity-100 */
241
+ transform: translateY(0) scale(1); /* translate-y-0 scale-100 */
242
+ }
243
+
244
+ /* Dialog: Leave */
245
+ .modal-transition-dialog-leave-animation {
246
+ transition-property: opacity, transform;
247
+ transition-duration: 200ms; /* duration-200 */
248
+ transition-timing-function: ease-in; /* ease-in */
249
+ }
250
+
251
+ .modal-transition-dialog-leave-start {
252
+ opacity: 1; /* opacity-100 */
253
+ transform: translateY(0) scale(1); /* translate-y-0 scale-100 */
254
+ }
255
+
256
+ .modal-transition-dialog-leave-end {
257
+ opacity: 0; /* opacity-0 */
258
+ transform: translateY(1rem) scale(1); /* translate-y-4 */
259
+ }
260
+
261
+ @media (min-width: 640px) {
262
+ .modal-transition-dialog-leave-end {
263
+ transform: translateY(0) scale(0.95); /* sm:translate-y-0 sm:scale-95 */
264
+ }
265
+ }
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "pathname"
5
+
6
+ module UltimateTurboModal
7
+ module Generators
8
+ class Base < Rails::Generators::Base
9
+ protected
10
+
11
+ def package_name
12
+ "ultimate_turbo_modal"
13
+ end
14
+
15
+ # Add JS dependency (for install flow)
16
+ def add_js_dependency
17
+ say "Attempting to set up JavaScript dependencies...", :yellow
18
+
19
+ version_spec = "#{package_name}@#{UltimateTurboModal::VERSION}"
20
+
21
+ if uses_importmaps?
22
+ say "Detected Importmaps. Pinning #{version_spec}...", :green
23
+ run "bin/importmap pin #{version_spec}"
24
+ say "✅ Pinned '#{package_name}' via importmap.", :green
25
+ return
26
+ end
27
+
28
+ if uses_javascript_bundler?
29
+ say "Detected jsbundling-rails (Yarn/npm/Bun). Adding #{package_name} package...", :green
30
+ if uses_yarn?
31
+ run "yarn add #{version_spec}"
32
+ say "✅ Added '#{package_name}' using Yarn.", :green
33
+ elsif uses_npm?
34
+ run "npm install --save #{version_spec}"
35
+ say "✅ Added '#{package_name}' using npm.", :green
36
+ elsif uses_bun?
37
+ run "bun add #{version_spec}"
38
+ say "✅ Added '#{package_name}' using Bun.", :green
39
+ else
40
+ say "Attempting to add with Yarn. If you use npm or Bun, please add manually.", :yellow
41
+ run "yarn add #{version_spec}"
42
+ say "If this failed or you use npm/bun, please run:", :yellow
43
+ say "npm install --save #{version_spec}", :cyan
44
+ say "# or", :cyan
45
+ say "bun add #{version_spec}", :cyan
46
+ end
47
+ else
48
+ say "Could not automatically detect Importmaps or jsbundling-rails.", :yellow
49
+ say "Please manually add the '#{package_name}' JavaScript package.", :yellow
50
+ say "If using Importmaps: bin/importmap pin #{version_spec}", :cyan
51
+ say "If using Yarn: yarn add #{version_spec}", :cyan
52
+ say "If using npm: npm install --save #{version_spec}", :cyan
53
+ say "If using Bun: bun add #{version_spec}", :cyan
54
+ say "Then, import it in your app/javascript/application.js:", :yellow
55
+ say "import '#{package_name}'", :cyan
56
+ end
57
+ end
58
+
59
+ # Install all JS dependencies (for update flow)
60
+ def install_all_js_dependencies
61
+ if uses_importmaps?
62
+ version_spec = "#{package_name}@#{UltimateTurboModal::VERSION}"
63
+ say "Detected Importmaps. Ensuring pin for #{version_spec}...", :green
64
+ run "bin/importmap pin #{version_spec}"
65
+ say "✅ Pinned '#{package_name}' via importmap.", :green
66
+ return
67
+ end
68
+
69
+ unless uses_javascript_bundler?
70
+ say "Could not detect Importmaps or jsbundling-rails. Skipping JS install step.", :yellow
71
+ return
72
+ end
73
+
74
+ say "Installing JavaScript dependencies...", :yellow
75
+ if uses_yarn?
76
+ run "yarn install"
77
+ say "✅ Installed dependencies with Yarn.", :green
78
+ elsif uses_npm?
79
+ run "npm install"
80
+ say "✅ Installed dependencies with npm.", :green
81
+ elsif uses_bun?
82
+ run "bun install"
83
+ say "✅ Installed dependencies with Bun.", :green
84
+ else
85
+ say "Attempting to install with Yarn. If you use npm or Bun, please run the appropriate command.", :yellow
86
+ run "yarn install"
87
+ end
88
+ end
89
+
90
+ def uses_importmaps?
91
+ File.exist?(rails_root_join("config", "importmap.rb"))
92
+ end
93
+
94
+ def uses_javascript_bundler?
95
+ File.exist?(rails_root_join("package.json"))
96
+ end
97
+
98
+ def uses_yarn?
99
+ File.exist?(rails_root_join("yarn.lock"))
100
+ end
101
+
102
+ def uses_npm?
103
+ File.exist?(rails_root_join("package-lock.json")) && !uses_yarn? && !uses_bun?
104
+ end
105
+
106
+ def uses_bun?
107
+ File.exist?(rails_root_join("bun.lockb"))
108
+ end
109
+
110
+ def rails_root_join(*args)
111
+ Pathname.new(destination_root).join(*args)
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+