swal_rails 0.3.1.beta1

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +43 -0
  3. data/CHANGELOG.md +73 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +973 -0
  6. data/Rakefile +12 -0
  7. data/app/assets/javascripts/swal_rails/chain.js +38 -0
  8. data/app/assets/javascripts/swal_rails/confirm.js +93 -0
  9. data/app/assets/javascripts/swal_rails/controllers/swal_controller.js +54 -0
  10. data/app/assets/javascripts/swal_rails/flash.js +24 -0
  11. data/app/assets/javascripts/swal_rails/index.js +62 -0
  12. data/app/assets/stylesheets/swal_rails/index.css +5 -0
  13. data/config/importmap.rb +9 -0
  14. data/config/locales/swal_rails.en.yml +19 -0
  15. data/config/locales/swal_rails.fr.yml +19 -0
  16. data/gemfiles/rails_7_2.gemfile +25 -0
  17. data/gemfiles/rails_8_0.gemfile +25 -0
  18. data/gemfiles/rails_8_1.gemfile +25 -0
  19. data/gemfiles/rails_8_1_sprockets.gemfile +25 -0
  20. data/lib/generators/swal_rails/install/install_generator.rb +138 -0
  21. data/lib/generators/swal_rails/install/templates/initializer.rb +33 -0
  22. data/lib/generators/swal_rails/locales/locales_generator.rb +19 -0
  23. data/lib/swal_rails/configuration.rb +88 -0
  24. data/lib/swal_rails/engine.rb +55 -0
  25. data/lib/swal_rails/helpers.rb +96 -0
  26. data/lib/swal_rails/version.rb +6 -0
  27. data/lib/swal_rails.rb +25 -0
  28. data/vendor/javascript/sweetalert2/LICENSE +22 -0
  29. data/vendor/javascript/sweetalert2/sweetalert2.all.js +4814 -0
  30. data/vendor/javascript/sweetalert2/sweetalert2.all.min.js +6 -0
  31. data/vendor/javascript/sweetalert2/sweetalert2.esm.all.js +4805 -0
  32. data/vendor/javascript/sweetalert2/sweetalert2.esm.all.min.js +6 -0
  33. data/vendor/javascript/sweetalert2/sweetalert2.esm.js +4804 -0
  34. data/vendor/javascript/sweetalert2/sweetalert2.esm.min.js +5 -0
  35. data/vendor/javascript/sweetalert2/sweetalert2.js +4813 -0
  36. data/vendor/javascript/sweetalert2/sweetalert2.min.js +5 -0
  37. data/vendor/stylesheets/sweetalert2/LICENSE +22 -0
  38. data/vendor/stylesheets/sweetalert2/sweetalert2.css +1233 -0
  39. data/vendor/stylesheets/sweetalert2/sweetalert2.min.css +1 -0
  40. data/vendor/stylesheets/sweetalert2/themes/bootstrap-4.css +167 -0
  41. data/vendor/stylesheets/sweetalert2/themes/bootstrap-5.css +173 -0
  42. data/vendor/stylesheets/sweetalert2/themes/borderless.css +46 -0
  43. data/vendor/stylesheets/sweetalert2/themes/bulma.css +94 -0
  44. data/vendor/stylesheets/sweetalert2/themes/material-ui.css +183 -0
  45. data/vendor/stylesheets/sweetalert2/themes/minimal.css +40 -0
  46. metadata +124 -0
data/README.md ADDED
@@ -0,0 +1,973 @@
1
+ <div align="center">
2
+
3
+ # ๐Ÿฌ swal_rails
4
+
5
+ **SweetAlert2 v11 for Rails 7+ โ€” batteries included.**
6
+
7
+ First-class support for **Importmap**, **jsbundling**, and **Sprockets**, with a
8
+ Stimulus controller, auto-wired flash messages, Turbo confirm replacement,
9
+ Ruby view helpers, and full I18n. Everything is configurable.
10
+
11
+ [![CI](https://github.com/Metalzoid/swal_rails/actions/workflows/main.yml/badge.svg)](https://github.com/Metalzoid/swal_rails/actions)
12
+ [![Gem Version](https://badge.fury.io/rb/swal_rails.svg)](https://rubygems.org/gems/swal_rails)
13
+ [![Ruby](https://img.shields.io/gem/ruby-version/swal_rails?label=ruby)](https://www.ruby-lang.org/)
14
+ [![Rails](https://img.shields.io/gem/dv/swal_rails/railties?label=rails)](https://rubyonrails.org/)
15
+ [![SweetAlert2](https://img.shields.io/badge/SweetAlert2-v11.26-3085d6.svg)](https://sweetalert2.github.io/)
16
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE.txt)
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## ๐Ÿ“– Table of contents
23
+
24
+ - [Why swal_rails?](#-why-swal_rails)
25
+ - [Features](#-features)
26
+ - [Compatibility](#-compatibility)
27
+ - [Installation](#-installation)
28
+ - [Quick start](#-quick-start)
29
+ - [Configuration](#%EF%B8%8F-configuration)
30
+ - [Usage](#-usage)
31
+ - [Flash messages](#flash-messages)
32
+ - [Turbo confirmations](#turbo-confirmations)
33
+ - [`data-swal-confirm` attribute](#data-swal-confirm-attribute)
34
+ - [Multi-step confirmations](#multi-step-confirmations)
35
+ - [Stimulus controller](#stimulus-controller)
36
+ - [Ruby view helpers](#ruby-view-helpers)
37
+ - [Programmatic JS](#programmatic-js)
38
+ - [Reference](#-reference)
39
+ - [`SwalRails.configure`](#swalrailsconfigure)
40
+ - [View helpers](#view-helpers)
41
+ - [Data attributes](#data-attributes)
42
+ - [Stimulus controller reference](#stimulus-controller-reference)
43
+ - [JS runtime](#js-runtime)
44
+ - [Generators](#generators)
45
+ - [Flash value shapes](#flash-value-shapes)
46
+ - [Chain semantics](#chain-semantics)
47
+ - [I18n](#-i18n)
48
+ - [Accessibility](#-accessibility)
49
+ - [Security & CSP](#-security--csp)
50
+ - [Themes](#-themes)
51
+ - [Asset pipelines](#-asset-pipelines)
52
+ - [Development](#-development)
53
+ - [Contributing](#-contributing)
54
+ - [Credits & license](#-credits--license)
55
+
56
+ ---
57
+
58
+ ## ๐Ÿค” Why swal_rails?
59
+
60
+ The existing gems haven't shipped a release since 2019 (SA2 was on **v7** back
61
+ then โ€” it's on **v11** now) and were built for the Rails 5 / UJS era.
62
+ `swal_rails` is the modern replacement:
63
+
64
+ | | `sweetalert2-rails` | `sweetify` | **`swal_rails`** |
65
+ | --------------------------------------- | :-----------------: | :------------: | :------------------: |
66
+ | SweetAlert2 v11.x | โŒ | โŒ | โœ… |
67
+ | Rails 7+ / Turbo-native | โŒ | โŒ | โœ… |
68
+ | Importmap | โŒ | โŒ | โœ… |
69
+ | jsbundling (esbuild / vite / rollup) | โŒ | โŒ | โœ… |
70
+ | Sprockets | โœ… | โŒ | โœ… |
71
+ | Stimulus controller | โŒ | โŒ | โœ… |
72
+ | Flash auto-wire, map per key | โŒ | partial | โœ… |
73
+ | Turbo `setConfirmMethod` override | โŒ | โŒ | โœ… |
74
+ | `data-swal-confirm` attribute | โŒ | โŒ | โœ… |
75
+ | Ruby view helpers (`swal_tag`) | โŒ | โŒ | โœ… |
76
+ | I18n Rails (fr/en shipped) | โŒ | โŒ | โœ… |
77
+ | a11y (reduced-motion, ARIA) | โŒ | โŒ | โœ… |
78
+ | Last release | 2019 | 2019 | **maintained** |
79
+
80
+ ---
81
+
82
+ ## โœจ Features
83
+
84
+ - ๐ŸŽจ **SweetAlert2 v11** vendored and pinned โ€” no CDN, no surprise upgrades.
85
+ - โšก **Three asset pipelines**: Importmap (default), jsbundling, Sprockets.
86
+ - ๐Ÿ”” **Auto-wired flash** โ€” `flash[:notice]` โ†’ toast, `flash[:alert]` โ†’ modal, fully mappable per key.
87
+ - ๐Ÿ›ก๏ธ **Turbo confirmations** โ€” replace the native `confirm()` globally **or** opt-in per element.
88
+ - ๐ŸŽฎ **Stimulus controller** (`data-controller="swal"`) for declarative popups.
89
+ - ๐Ÿงฑ **Ruby view helpers** โ€” `swal_tag`, `swal_config_meta_tag`, `swal_flash_meta_tag`.
90
+ - ๐Ÿ”’ **CSP-friendly** โ€” `swal_tag(..., nonce: true)` propagates the per-request nonce.
91
+ - ๐ŸŒ **I18n ready** โ€” `en` / `fr` locales shipped, override freely.
92
+ - โ™ฟ **Accessibility** โ€” honors `prefers-reduced-motion`, preserves ARIA & focus trap.
93
+ - ๐Ÿงฉ **Engine-based** โ€” zero monkey-patching, follows Rails Engine conventions.
94
+
95
+ ---
96
+
97
+ ## ๐Ÿ”ง Compatibility
98
+
99
+ Tested on every combination of Ruby and Rails listed below via the
100
+ [Appraisal](https://github.com/thoughtbot/appraisal) gem:
101
+
102
+ | | Rails 7.2 | Rails 8.0 | Rails 8.1.3 | Rails 8.1.3 + Sprockets |
103
+ | ------- | :-------: | :-------: | :---------: | :---------------------: |
104
+ | Ruby 3.2| โœ… | โœ… | โœ… | โœ… |
105
+ | Ruby 3.3| โœ… | โœ… | โœ… | โœ… |
106
+ | Ruby 3.4| โœ… | โœ… | โœ… | โœ… |
107
+ | Ruby 3.5| โœ… | โœ… | โœ… | โœ… |
108
+ | Ruby 4.0| โ€” | โœ… | โœ… | โœ… |
109
+
110
+ > **Requirements:** Ruby **โ‰ฅ 3.2** (tested up to **4.0**), Rails **โ‰ฅ 7.2** (tested up to **8.1.3**), Turbo recommended.
111
+
112
+ ---
113
+
114
+ ## ๐Ÿ“ฆ Installation
115
+
116
+ Add this line to your application's `Gemfile`:
117
+
118
+ ```ruby
119
+ gem "swal_rails"
120
+ ```
121
+
122
+ > **During the beta**, pin the prerelease explicitly:
123
+ > ```ruby
124
+ > gem "swal_rails", "0.3.1.beta1"
125
+ > ```
126
+ > or install globally with `gem install swal_rails --pre`. Bundler ignores prereleases unless you ask for one.
127
+
128
+ Then install and run the generator:
129
+
130
+ ```bash
131
+ $ bundle install
132
+ $ bin/rails g swal_rails:install
133
+ ```
134
+
135
+ The generator **autodetects** your asset pipeline. You can force it:
136
+
137
+ ```bash
138
+ $ bin/rails g swal_rails:install --mode=importmap # default for new Rails apps
139
+ $ bin/rails g swal_rails:install --mode=jsbundling # esbuild, vite, rollup
140
+ $ bin/rails g swal_rails:install --mode=sprockets # legacy apps
141
+ ```
142
+
143
+ ### What the generator does
144
+
145
+ - ๐Ÿ“„ creates `config/initializers/swal_rails.rb`
146
+ - ๐Ÿ“Œ pins (Importmap) or adds (jsbundling/sprockets) `sweetalert2` and `swal_rails`
147
+ - ๐Ÿ’‰ injects `<%= swal_rails_meta_tags %>` into `app/views/layouts/application.html.erb`
148
+ - ๐ŸŒ loads the `en` / `fr` locales
149
+
150
+ Finally, in your JS entrypoint (e.g. `app/javascript/application.js`):
151
+
152
+ ```js
153
+ import "swal_rails"
154
+ ```
155
+
156
+ ---
157
+
158
+ ## ๐Ÿš€ Quick start
159
+
160
+ Thirty seconds, tops. After running the installer:
161
+
162
+ ```ruby
163
+ # app/controllers/posts_controller.rb
164
+ def create
165
+ @post = Post.create!(post_params)
166
+ redirect_to @post, notice: "Post created!"
167
+ end
168
+ ```
169
+
170
+ ```erb
171
+ <%# app/views/posts/show.html.erb %>
172
+ <%= button_to "Delete", @post, method: :delete,
173
+ data: { turbo_confirm: "Really delete this post?" } %>
174
+ ```
175
+
176
+ That's it. `notice` renders as a **toast**, the delete button opens a
177
+ **SweetAlert2 modal** instead of the browser's ugly native `confirm()`.
178
+
179
+ ---
180
+
181
+ ## โš™๏ธ Configuration
182
+
183
+ Everything lives in `config/initializers/swal_rails.rb`:
184
+
185
+ ```ruby
186
+ SwalRails.configure do |config|
187
+ # โ”€โ”€โ”€ Confirm behavior โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
188
+ # :off โ†’ don't touch Turbo, no data-attribute listener
189
+ # :data_attribute โ†’ only intercept [data-swal-confirm] clicks (default)
190
+ # :turbo_override โ†’ replace Turbo.setConfirmMethod globally
191
+ # :both โ†’ data-attribute + Turbo override
192
+ config.confirm_mode = :data_attribute
193
+
194
+ # โ”€โ”€โ”€ UX โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
195
+ config.respect_reduced_motion = true # disable animations when OS asks
196
+ config.expose_window_swal = true # window.Swal for console hacking
197
+
198
+ # โ”€โ”€โ”€ Defaults passed to every Swal.fire call โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
199
+ config.default_options = {
200
+ buttonsStyling: true,
201
+ reverseButtons: false,
202
+ focusConfirm: true,
203
+ returnFocus: true
204
+ }
205
+
206
+ # โ”€โ”€โ”€ Flash โ†’ Swal mapping (per key) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
207
+ config.flash_map[:notice] = { icon: "success", toast: true, position: "top-end", timer: 3000 }
208
+ config.flash_map[:success] = { icon: "success", toast: true, position: "top-end", timer: 3000 }
209
+ config.flash_map[:alert] = { icon: "error", toast: false }
210
+ config.flash_map[:error] = { icon: "error", toast: false }
211
+ config.flash_map[:warning] = { icon: "warning", toast: true, position: "top-end", timer: 4000 }
212
+ config.flash_map[:info] = { icon: "info", toast: true, position: "top-end", timer: 3000 }
213
+
214
+ # โ”€โ”€โ”€ I18n scope (for button labels) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
215
+ config.i18n_scope = "swal_rails"
216
+ end
217
+ ```
218
+
219
+ > ๐Ÿ’ก **Per-key override tip:** to disable the toast for a single key without
220
+ > rewriting the whole map: `config.flash_map[:alert] = { icon: "error", toast: false }`.
221
+
222
+ ---
223
+
224
+ ## ๐ŸŽฏ Usage
225
+
226
+ ### Flash messages
227
+
228
+ Any flash set from a controller is rendered automatically on page load:
229
+
230
+ ```ruby
231
+ flash[:notice] = "Profile updated" # โ†’ toast top-right
232
+ flash[:alert] = "Could not save" # โ†’ modal
233
+ ```
234
+
235
+ Arrays are expanded into one popup per message โ€” handy for model errors:
236
+
237
+ ```ruby
238
+ flash[:alert] = @post.errors.full_messages # ["Title can't be blank", "Body is too short"]
239
+ # โ†’ two separate Swals, one per message
240
+ ```
241
+
242
+ Need to override the per-key defaults for a single request? Assign a **Hash**
243
+ instead of a String โ€” its keys flow straight into `Swal.fire` and shadow
244
+ `flash_map[key]`:
245
+
246
+ ```ruby
247
+ flash[:notice] = { text: "Deployed!", icon: "rocket", timer: 5000, toast: true }
248
+ # โ†’ ignores flash_map[:notice], fires a 5-second rocket toast
249
+ ```
250
+
251
+ Behind the scenes, the engine serializes the flash into a meta tag
252
+ (`<meta name="swal-flash" content="...">`) and the JS runtime reads it and
253
+ calls `Swal.fire(...)` with your per-key options.
254
+
255
+ ---
256
+
257
+ ### Turbo confirmations
258
+
259
+ Works with standard Rails / Turbo syntax:
260
+
261
+ ```erb
262
+ <%= button_to "Delete", post_path(@post), method: :delete,
263
+ data: { turbo_confirm: "Really delete?" } %>
264
+ ```
265
+
266
+ When `confirm_mode` is `:turbo_override` or `:both`, `swal_rails` replaces
267
+ `Turbo.setConfirmMethod` with a SweetAlert2-backed implementation โ€” the same
268
+ `data-turbo-confirm` attribute now shows a proper modal.
269
+
270
+ Pass a **Hash** instead of a string to carry full SA2 options:
271
+
272
+ ```erb
273
+ <%= button_to "Delete", post_path(@post), method: :delete, data: {
274
+ turbo_confirm: { icon: "error", title: "Really?", confirmButtonText: "Nuke" }
275
+ } %>
276
+ ```
277
+
278
+ Rails JSON-encodes the Hash into the attribute; the runtime parses it back
279
+ and treats it as a full options object (same thing works with
280
+ `data-swal-confirm`).
281
+
282
+ ---
283
+
284
+ ### `data-swal-confirm` attribute
285
+
286
+ If you don't want to override Turbo globally, opt-in per element:
287
+
288
+ ```erb
289
+ <%= link_to "Archive", archive_path,
290
+ data: { swal_confirm: "Archive this item?", swal_icon: "warning" } %>
291
+ ```
292
+
293
+ Supported data attributes:
294
+
295
+ | Attribute | Maps to |
296
+ | ---------------------------------- | ---------------------- |
297
+ | `data-swal-confirm` | `text` / title prompt |
298
+ | `data-swal-title` | `title` |
299
+ | `data-swal-text` | `text` |
300
+ | `data-swal-icon` | `icon` |
301
+ | `data-swal-confirm-text` | `confirmButtonText` |
302
+ | `data-swal-cancel-text` | `cancelButtonText` |
303
+ | `data-swal-options` *(JSON)* | full SA2 options (wins over the above) |
304
+
305
+ Use `data-swal-options` when you need anything beyond the shortcuts โ€” it
306
+ accepts any SweetAlert2 option:
307
+
308
+ ```erb
309
+ <%= button_to "Delete", post_path(@post), method: :delete, data: {
310
+ swal_confirm: "Danger",
311
+ swal_options: { icon: "error", iconColor: "#ff0000", confirmButtonText: "Nuke" }.to_json
312
+ } %>
313
+ ```
314
+
315
+ ---
316
+
317
+ ### Multi-step confirmations
318
+
319
+ For destructive flows (account deletion, legal opt-ins, irreversible
320
+ actions), chain several popups via `data-swal-steps`. Each step only
321
+ fires if the previous one was **confirmed** โ€” any Cancel or Esc aborts
322
+ the whole cascade, and the original click/submit never reaches the
323
+ server:
324
+
325
+ ```erb
326
+ <%= button_to "Delete account", account_path, method: :delete, data: {
327
+ swal_steps: [
328
+ { title: "Delete your account?", icon: "warning" },
329
+ { title: "This cannot be undone", icon: "error" },
330
+ { title: "Type DELETE to confirm", input: "text" }
331
+ ].to_json
332
+ } %>
333
+ ```
334
+
335
+ Every step is a full SweetAlert2 options Hash โ€” override the default
336
+ icon, buttons, timer, `input:` type, anything SA2 accepts. The per-step
337
+ defaults (`showCancelButton: true`, `focusCancel: true`, `icon: "warning"`)
338
+ are merged in first and can be replaced key-by-key.
339
+
340
+ #### Conditional branching (`onConfirmed` / `onDenied`)
341
+
342
+ Add a Deny button (`showDenyButton: true`) to get a three-way choice, and
343
+ attach a nested sub-chain to either outcome:
344
+
345
+ ```erb
346
+ <%= button_to "Delete or disable?", account_path, method: :delete, data: {
347
+ swal_steps: [
348
+ {
349
+ title: "Delete or just disable?",
350
+ icon: "question",
351
+ showDenyButton: true,
352
+ confirmButtonText: "Delete forever",
353
+ denyButtonText: "Disable for 30 days",
354
+ onDenied: [
355
+ { title: "Confirm disable", icon: "info" }
356
+ ]
357
+ }
358
+ ].to_json
359
+ } %>
360
+ ```
361
+
362
+ Semantic rules, per step:
363
+
364
+ | SA2 result | Behavior |
365
+ | ------------- | -------- |
366
+ | `isDismissed` | Abort the entire chain; action does not fire |
367
+ | `isConfirmed` | Run `onConfirmed` sub-chain if present (replaces remainder); else continue linearly |
368
+ | `isDenied` | Run `onDenied` sub-chain if present (its result decides); else abort |
369
+
370
+ Sub-chains are recursive โ€” they're just nested arrays of steps.
371
+
372
+ Under `confirm_mode = :turbo_override` (or `:both`), passing a JSON array
373
+ to `data-turbo-confirm` works the same way:
374
+
375
+ ```erb
376
+ <%= button_to "Delete", account_path, method: :delete, data: {
377
+ turbo_confirm: [
378
+ { title: "Really?" },
379
+ { title: "Really really?" }
380
+ ]
381
+ } %>
382
+ ```
383
+
384
+ From Ruby, the view helper `swal_chain_tag` fires a chain inline on page
385
+ load (same CSP nonce and XSS hardening as `swal_tag`):
386
+
387
+ ```erb
388
+ <%= swal_chain_tag([
389
+ { title: "Welcome back" },
390
+ { title: "Accept updated terms?" }
391
+ ]) %>
392
+ ```
393
+
394
+ ---
395
+
396
+ ### Stimulus controller
397
+
398
+ For fully declarative popups without touching JS:
399
+
400
+ ```erb
401
+ <button data-controller="swal"
402
+ data-action="click->swal#fire"
403
+ data-swal-options-value='{"title":"Hello","icon":"success"}'>
404
+ Ping
405
+ </button>
406
+ ```
407
+
408
+ Available actions: `fire`, `confirm`, `chain`. The `chain` action reads
409
+ `data-swal-steps-value` (same shape as `data-swal-steps`) and submits the
410
+ enclosing form if every step resolves confirmed.
411
+
412
+ ---
413
+
414
+ ### Ruby view helpers
415
+
416
+ Fire a one-shot popup directly from a view:
417
+
418
+ ```erb
419
+ <%= swal_tag(title: "Welcome back!", icon: "info", timer: 2000) %>
420
+ ```
421
+
422
+ Under a strict Content Security Policy, pass `nonce: true` โ€” Rails fills in
423
+ the per-request nonce so the inline `<script>` survives the policy:
424
+
425
+ ```erb
426
+ <%= swal_tag({ title: "Welcome back!" }, nonce: true) %>
427
+ ```
428
+
429
+ > **Heads-up:** the emitted tag is `<script type="module">` with a bare
430
+ > `import Swal from "sweetalert2"`. That resolves via Importmap (or any
431
+ > shim that processes import maps). On a pure esbuild/webpack setup with
432
+ > no importmap tag on the page, prefer the Stimulus controller or call
433
+ > `window.Swal.fire(...)` from your bundle instead.
434
+
435
+ Lower-level helpers (injected by the generator into your layout):
436
+
437
+ ```erb
438
+ <%= swal_rails_meta_tags %>
439
+ <%# expands to: %>
440
+ <%= swal_config_meta_tag %> <%# serializes SwalRails.configuration %>
441
+ <%= swal_flash_meta_tag %> <%# serializes current flash, if any %>
442
+ ```
443
+
444
+ ---
445
+
446
+ ### Programmatic JS
447
+
448
+ `Swal` is re-exported from the gem's JS runtime:
449
+
450
+ ```js
451
+ import Swal from "sweetalert2"
452
+
453
+ Swal.fire({
454
+ title: "Saved!",
455
+ icon: "success",
456
+ toast: true,
457
+ position: "top-end",
458
+ timer: 3000
459
+ })
460
+ ```
461
+
462
+ If `config.expose_window_swal = true`, `window.Swal` is also available for
463
+ quick console debugging.
464
+
465
+ ---
466
+
467
+ ## ๐Ÿ“˜ Reference
468
+
469
+ Complete, at-a-glance specification of every public surface the gem
470
+ exposes. The sections above give narrative walk-throughs โ€” this section
471
+ is the lookup table.
472
+
473
+ ### `SwalRails.configure`
474
+
475
+ ```ruby
476
+ SwalRails.configure { |config| ... } # block form, yields Configuration
477
+ SwalRails.configuration # reader โ€” memoized, safe to mutate
478
+ SwalRails.reset_configuration! # resets to defaults (test fixture helper)
479
+ ```
480
+
481
+ #### Configuration attributes
482
+
483
+ | Attribute | Type | Default | Description |
484
+ | ------------------------ | ------- | ------------------ | ----------- |
485
+ | `confirm_mode` | Symbol | `:data_attribute` | Routing of confirm dialogs โ€” see values below. Validated: assignment with any other value raises `ArgumentError`. Strings are coerced to symbols. |
486
+ | `flash_keys_as_meta` | Boolean | `true` | When `false`, `swal_flash_meta_tag` returns `nil` โ€” useful to opt out without removing the `swal_rails_meta_tags` call from the layout. |
487
+ | `respect_reduced_motion` | Boolean | `true` | When the OS reports `prefers-reduced-motion: reduce`, the gem empties SA2's `showClass` / `hideClass` to suppress animations. |
488
+ | `expose_window_swal` | Boolean | `true` | When `true`, `window.Swal` is set to the mixed-in `Swal` instance after boot (useful for console debugging and inline scripts). |
489
+ | `default_options` | Hash | see below | Merged into **every** `Swal.fire(...)` call via `Swal.mixin(...)`. |
490
+ | `flash_map` | Hash | see below | Flash-key โ†’ SA2 options mapping. Keys normalized to symbols. Non-Hash assignment raises `ArgumentError`. |
491
+ | `i18n_scope` | String | `"swal_rails"` | I18n scope used to look up `confirm_button_text`, `cancel_button_text`, `deny_button_text`, `close_button_aria_label`. Non-string values are coerced. |
492
+
493
+ `confirm_mode` accepted values:
494
+
495
+ | Value | Behavior |
496
+ | ------------------ | -------- |
497
+ | `:off` | No Turbo override, no data-attribute listener. Use `Swal` manually. |
498
+ | `:data_attribute` | **(default)** intercept clicks/submits on `[data-swal-confirm]` and `[data-swal-steps]`. Does not touch Turbo. |
499
+ | `:turbo_override` | Replaces `Turbo.setConfirmMethod` globally. `data-turbo-confirm` attributes open SA2 modals. |
500
+ | `:both` | Enables both mechanisms โ€” useful for mixed codebases migrating over. |
501
+
502
+ `default_options` default:
503
+
504
+ ```ruby
505
+ { buttonsStyling: true, reverseButtons: false, focusConfirm: true, returnFocus: true }
506
+ ```
507
+
508
+ `flash_map` default (per key, all overridable):
509
+
510
+ ```ruby
511
+ {
512
+ notice: { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
513
+ success: { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
514
+ alert: { icon: "error", toast: false, timer: nil },
515
+ error: { icon: "error", toast: false, timer: nil },
516
+ warning: { icon: "warning", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
517
+ info: { icon: "info", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
518
+ }
519
+ ```
520
+
521
+ #### `to_client_payload` (internal, read-only)
522
+
523
+ Serialization contract consumed by the JS runtime via the
524
+ `<meta name="swal-config">` tag. Returns:
525
+
526
+ ```ruby
527
+ {
528
+ confirmMode: Symbol,
529
+ respectReducedMotion: Boolean,
530
+ exposeWindowSwal: Boolean,
531
+ defaultOptions: Hash,
532
+ flashMap: Hash,
533
+ i18n: Hash # only keys whose translation is present
534
+ }
535
+ ```
536
+
537
+ ---
538
+
539
+ ### View helpers
540
+
541
+ All helpers are injected into both `ActionController::Base` and
542
+ `ActionView::Base` by the engine; they are available in every view and
543
+ in every controller-side rendering context.
544
+
545
+ | Helper | Returns | Description |
546
+ | ---------------------------------- | ----------------------------------------- | ----------- |
547
+ | `swal_rails_meta_tags` | `ActiveSupport::SafeBuffer` | Emits `swal_config_meta_tag` + `swal_flash_meta_tag` joined with `"\n"`. Call once, in `<head>`. |
548
+ | `swal_config_meta_tag` | `ActiveSupport::SafeBuffer` | Emits `<meta name="swal-config" content="โ€ฆJSONโ€ฆ">` with the serialized `SwalRails.configuration.to_client_payload`. |
549
+ | `swal_flash_meta_tag` | `ActiveSupport::SafeBuffer` or `nil` | Emits `<meta name="swal-flash" content="โ€ฆJSONโ€ฆ">` if `flash_keys_as_meta` is enabled **and** the current `flash` is non-empty; otherwise returns `nil`. |
550
+ | `swal_tag(options, html_options)` | `ActiveSupport::SafeBuffer` | Inline `<script type="module">` firing `Swal.fire(options)` once. Options are JSON-escaped to neutralize `</script>`, `<!--`, U+2028, U+2029. |
551
+ | `swal_chain_tag(steps, html_options)` | `ActiveSupport::SafeBuffer` | Inline `<script type="module">` firing `chainDialogs(Swal, steps)`. `steps` is an Array of Hashes (single Hash is auto-wrapped). Same escape hardening as `swal_tag`. |
552
+
553
+ #### `html_options` (for `swal_tag` / `swal_chain_tag`)
554
+
555
+ - `nonce: true` โ†’ propagates the per-request CSP nonce (when Rails
556
+ exposes `content_security_policy_nonce`). Silently dropped when no CSP
557
+ helper is configured, so the helper stays safe in apps without CSP.
558
+ - Any other key is forwarded to `javascript_tag` as `<script>`
559
+ attributes.
560
+
561
+ The default `type` is `"module"` โ€” override with `html_options = { type: "text/javascript" }` if you need a classic script.
562
+
563
+ > โš ๏ธ The emitted `<script type="module">` contains bare imports
564
+ > (`import Swal from "sweetalert2"`). These resolve via Importmap; in a
565
+ > pure esbuild/webpack setup with no importmap tag on the page, call
566
+ > `window.Swal.fire(...)` from your bundle instead.
567
+
568
+ ---
569
+
570
+ ### Data attributes
571
+
572
+ All attributes are read from the element's `dataset`. Values carry through `button_to`, `link_to`, `form_with`, and raw HTML equally.
573
+
574
+ #### Single-step confirm (`data-swal-confirm`)
575
+
576
+ | Attribute | Accepts | Maps to (SA2 option) |
577
+ | ------------------------- | ---------------------------- | -------------------- |
578
+ | `data-swal-confirm` | String **or** JSON object *or* JSON array | message (String) โ†’ `text`; Object โ†’ full SA2 options (overrides all shortcuts); Array โ†’ multi-step chain (see below) |
579
+ | `data-swal-title` | String | `title` |
580
+ | `data-swal-text` | String | `text` |
581
+ | `data-swal-icon` | String (`"warning"`, `"error"`, โ€ฆ) | `icon` (default `"warning"`) |
582
+ | `data-swal-confirm-text` | String | `confirmButtonText` |
583
+ | `data-swal-cancel-text` | String | `cancelButtonText` |
584
+ | `data-swal-options` | JSON Object (stringified) | Full SA2 options โ€” **wins over** all shortcuts and over the JSON object form of `data-swal-confirm` |
585
+
586
+ Merge order (later wins):
587
+
588
+ ```
589
+ defaults โ†’ data-swal-* shortcuts โ†’ JSON object in data-swal-confirm โ†’ data-swal-options
590
+ ```
591
+
592
+ #### Multi-step chain (`data-swal-steps`)
593
+
594
+ | Attribute | Accepts | Behavior |
595
+ | ------------------------- | -------------------------------------- | -------- |
596
+ | `data-swal-steps` | JSON Array of step Hashes (stringified) | Runs the chain. Per-step defaults `{ showCancelButton: true, focusCancel: true, icon: "warning" }` are merged first and can be overridden key-by-key. Every step is a full SA2 options Hash plus the optional `onConfirmed` / `onDenied` sub-chain keys. |
597
+
598
+ Both attributes coexist โ€” if `data-swal-steps` is present and non-empty, it takes precedence over `data-swal-confirm`.
599
+
600
+ #### Turbo `data-turbo-confirm`
601
+
602
+ When `confirm_mode` is `:turbo_override` or `:both`, `data-turbo-confirm` accepts:
603
+
604
+ | Form | Behavior |
605
+ | ------- | -------- |
606
+ | String | SA2 popup with the string as `text`. |
607
+ | Hash (JSON-encoded by Rails) | Full SA2 options. |
608
+ | Array (JSON-encoded by Rails) | Multi-step chain โ€” same shape as `data-swal-steps`. |
609
+
610
+ ---
611
+
612
+ ### Stimulus controller reference
613
+
614
+ Registered under the identifier `"swal"`.
615
+
616
+ #### Values
617
+
618
+ | Value | Type | Default | Used by |
619
+ | ------------- | ------ | ------- | ------- |
620
+ | `optionsValue` | Object | `{}` | `fire`, `confirm` |
621
+ | `stepsValue` | Array | `[]` | `chain` |
622
+
623
+ #### Actions
624
+
625
+ | Action | Target behavior |
626
+ | --------- | --------------- |
627
+ | `fire` | Fires `Swal.fire(optionsValue)`. Calls `preventDefault()` on the event if the element is `<a>` or `<button>`. Returns the SA2 promise. |
628
+ | `confirm` | Fires `Swal.fire({ showCancelButton: true, focusCancel: true, ...optionsValue })`. On `isConfirmed`, calls `requestSubmit()` (fallback `submit()`) on the enclosing form. |
629
+ | `chain` | Runs `chainDialogs(Swal, stepsValue)`. On resolved `true`, calls `requestSubmit()` / `submit()` on the enclosing form. Returns the boolean. |
630
+
631
+ Example:
632
+
633
+ ```erb
634
+ <button data-controller="swal"
635
+ data-action="click->swal#chain"
636
+ data-swal-steps-value='[{"title":"Sure?"},{"title":"Really?"}]'>
637
+ Proceed
638
+ </button>
639
+ ```
640
+
641
+ ---
642
+
643
+ ### JS runtime
644
+
645
+ #### Entry point
646
+
647
+ ```js
648
+ import "swal_rails" // installs confirm + flash handlers
649
+ ```
650
+
651
+ Expected in your JS entrypoint (e.g. `app/javascript/application.js`).
652
+
653
+ #### Re-exports
654
+
655
+ ```js
656
+ import Swal from "sweetalert2" // SA2, re-exported as default from swal_rails
657
+ import { Swal } from "swal_rails" // named re-export (same instance)
658
+ import { chainDialogs, CHAIN_DEFAULTS } from "swal_rails/chain"
659
+ import { installConfirm } from "swal_rails/confirm" // for custom boot sequences
660
+ import { installFlash } from "swal_rails/flash" // same
661
+ ```
662
+
663
+ #### `chainDialogs(Swal, steps)`
664
+
665
+ ```ts
666
+ chainDialogs(Swal: typeof import("sweetalert2"), steps: Array<StepOptions>): Promise<boolean>
667
+ ```
668
+
669
+ Returns `true` iff a complete path through the chain was confirmed. `steps` may be empty (resolves `true` immediately). Non-array input resolves `true` as well.
670
+
671
+ `StepOptions` is any valid SA2 options Hash, plus the two chain-only keys:
672
+
673
+ | Key | Type | Effect |
674
+ | -------------- | ------------------- | ------ |
675
+ | `onConfirmed` | `Array<StepOptions>` | On `isConfirmed`, run this sub-chain and adopt its boolean result (replaces the remainder of the current chain). |
676
+ | `onDenied` | `Array<StepOptions>` | On `isDenied` (requires `showDenyButton: true`), run this sub-chain and adopt its boolean result. Without this key, `isDenied` aborts the chain. |
677
+
678
+ #### Events
679
+
680
+ | Event name | Target | `event.detail` | When |
681
+ | ------------------ | ---------- | -------------- | ---- |
682
+ | `swal-rails:ready` | `document` | `{ Swal, config }` | Fired once per page lifetime after the first successful boot (not per Turbo navigation). |
683
+
684
+ #### Meta-tag contract
685
+
686
+ | Meta name | Payload | Read by |
687
+ | ------------- | ------- | ------- |
688
+ | `swal-config` | `to_client_payload` JSON | Runtime boot โ€” mixin, confirm handler, flash handler. Once per page. |
689
+ | `swal-flash` | Array of `{ key, options }` | Flash runtime โ€” re-read on every `turbo:load`. |
690
+
691
+ Flash entries are `{ key: "notice", options: { text: "..." } }` for string values, or `{ key: "notice", options: {...user hash...} }` for Hash values. Arrays in `flash[key]` are expanded into one entry per element.
692
+
693
+ ---
694
+
695
+ ### Generators
696
+
697
+ #### `bin/rails g swal_rails:install`
698
+
699
+ | Flag | Type | Default | Values |
700
+ | ------------------------- | ------- | ----------------- | ------ |
701
+ | `--mode` | String | `auto` | `auto`, `importmap`, `jsbundling`, `sprockets` |
702
+ | `--confirm_mode` | String | `data_attribute` | `off`, `data_attribute`, `turbo_override`, `both` โ€” baked into the generated initializer |
703
+ | `--skip_layout` | Boolean | `false` | When set, does not inject `<%= swal_rails_meta_tags %>` into `app/views/layouts/application.html.erb` |
704
+
705
+ `--mode=auto` detection order:
706
+ 1. `config/importmap.rb` present โ†’ `importmap`
707
+ 2. `package.json` present โ†’ `jsbundling`
708
+ 3. fallback โ†’ `sprockets`
709
+
710
+ Per-mode side effects:
711
+
712
+ - **importmap**: appends `pin "sweetalert2", to: "sweetalert2.esm.all.js"` and `pin "swal_rails", to: "swal_rails/index.js"` to `config/importmap.rb`; appends `import "swal_rails"` to `app/javascript/application.js`.
713
+ - **jsbundling**: runs `yarn add sweetalert2@<pinned>` or `npm install sweetalert2@<pinned>` (based on the lockfile present); appends `import "swal_rails"` to `app/javascript/application.js`.
714
+ - **sprockets**: appends `//= link sweetalert2.js` and `//= link sweetalert2.css` to `app/assets/config/manifest.js`.
715
+
716
+ All append operations are idempotent โ€” running the generator twice is safe.
717
+
718
+ #### `bin/rails g swal_rails:locales`
719
+
720
+ No flags. Copies `config/locales/swal_rails.en.yml` and `swal_rails.fr.yml` from the gem into your app's `config/locales/`.
721
+
722
+ ---
723
+
724
+ ### Flash value shapes
725
+
726
+ | Value type | Rendered as |
727
+ | ---------- | ----------- |
728
+ | `String` | `{ text: value }` โ€” safe by default (SA2 renders via `text:`, no HTML injection). |
729
+ | `Hash` | Full SA2 options, **shadows** `flash_map[key]`. Any SA2 key is accepted (icon, timer, input, html, iconHtml, โ€ฆ). |
730
+ | `Array` | Expanded into one entry per element. Strings become `{ text: elem }`, Hashes pass through verbatim. |
731
+ | `nil` / `""` / `blank?` | Skipped. |
732
+
733
+ Key normalization: Hash keys are `symbolize_keys`-ed before serialization, so `flash[:notice] = { "text" => "..." }` and `flash[:notice] = { text: "..." }` are equivalent.
734
+
735
+ > โš ๏ธ **Hash overrides bypass the `text:` safety net.** Using `html:`, `iconHtml:`, or `footer:` with untrusted input is an XSS โ€” SweetAlert2 renders those as raw HTML by design. Rule of thumb: if the value is user-controlled, keep the String form.
736
+
737
+ ---
738
+
739
+ ### Chain semantics
740
+
741
+ Single source of truth for every chain-aware entry point (`data-swal-steps`, `data-turbo-confirm` with an array, Stimulus `chain` action, `swal_chain_tag`, direct `chainDialogs` call).
742
+
743
+ Per step:
744
+
745
+ | SA2 result | Behavior |
746
+ | ------------- | -------- |
747
+ | `isDismissed` (ร—, Esc, backdrop) | Abort the current chain โ†’ `false`. Outer chain (if this was a sub-chain) also terminates with `false`. |
748
+ | `isConfirmed` | If `onConfirmed: [...]` is defined, run it recursively and **replace** the remainder of the current chain with its result. Else continue linearly. |
749
+ | `isDenied` (requires `showDenyButton: true`) | If `onDenied: [...]` is defined, run it recursively and adopt its result. Else abort โ†’ `false`. |
750
+
751
+ Return rules:
752
+
753
+ - An empty or non-array `steps` input resolves `true` immediately.
754
+ - A chain resolves `true` iff it ran to completion along a path without any dismiss or unbranched deny.
755
+ - Sub-chains inherit the same per-step defaults (`showCancelButton: true`, `focusCancel: true`, `icon: "warning"`).
756
+ - `onConfirmed` / `onDenied` keys are stripped before being passed to `Swal.fire`, so they never leak into the SA2 popup options.
757
+
758
+ Callback contract for Turbo override: the chain's final `Promise<boolean>` is what `Turbo.setConfirmMethod` receives โ€” `false` cancels the navigation / form submit, `true` proceeds.
759
+
760
+ ---
761
+
762
+ ## ๐ŸŒ I18n
763
+
764
+ Locales `en` and `fr` ship with the gem. To copy them into your app for customization:
765
+
766
+ ```bash
767
+ $ bin/rails g swal_rails:locales
768
+ ```
769
+
770
+ Generated keys:
771
+
772
+ ```yaml
773
+ en:
774
+ swal_rails:
775
+ confirm_button_text: "OK"
776
+ cancel_button_text: "Cancel"
777
+ deny_button_text: "No"
778
+ close_button_aria_label: "Close this dialog"
779
+ ```
780
+
781
+ The current `I18n.locale` is read on every request and injected into the
782
+ client payload โ€” change languages, button labels follow.
783
+
784
+ ---
785
+
786
+ ## โ™ฟ Accessibility
787
+
788
+ - **Reduced motion** โ€” when `prefers-reduced-motion: reduce` is set, animations
789
+ are disabled (`showClass`/`hideClass` emptied).
790
+ - **Focus trap & ARIA** โ€” SweetAlert2's built-in focus management and ARIA
791
+ roles are preserved; `returnFocus: true` brings focus back to the trigger.
792
+ - **Translatable labels** โ€” all button / aria labels go through I18n.
793
+
794
+ ---
795
+
796
+ ## ๐Ÿ”’ Security & CSP
797
+
798
+ ### XSS safety
799
+
800
+ All strings flowing through the Ruby helpers are hardened against the usual
801
+ breakout sequences:
802
+
803
+ - **`swal_tag`** runs the serialized options through `ERB::Util.json_escape`,
804
+ which neutralizes `</script>`, `<!--`, U+2028 and U+2029 before they reach
805
+ the inline `<script>` body.
806
+ - **`swal_config_meta_tag`** and **`swal_flash_meta_tag`** emit `<meta>`
807
+ attributes, which Rails HTML-escapes automatically; the JS runtime feeds
808
+ messages to SweetAlert2's `text:` option (not `html:`), so flash payloads
809
+ are rendered as text even if they contain HTML.
810
+
811
+ > โš ๏ธ **Hash-form overrides bypass the `text:` safety net.** When you pass a
812
+ > Hash to `flash[key]`, `data-turbo-confirm`, `data-swal-confirm`, or
813
+ > `data-swal-options`, its keys flow straight into `Swal.fire`. Using
814
+ > `html:`, `iconHtml:`, or `footer:` with untrusted input is an XSS โ€”
815
+ > SweetAlert2 renders those as raw HTML by design. Rule of thumb: if the
816
+ > value is user-controlled, keep the String form (or the `text:` key).
817
+
818
+ ### Content Security Policy
819
+
820
+ Meta tags carry no script and need no nonce. For the inline helper:
821
+
822
+ ```erb
823
+ <%= swal_tag({ title: "Saved" }, nonce: true) %>
824
+ ```
825
+
826
+ When ActionView's CSP helper is available, Rails substitutes the per-request
827
+ nonce; otherwise `nonce: true` is silently dropped so the tag stays valid on
828
+ apps without a configured CSP.
829
+
830
+ > **SweetAlert2 + strict `style-src`** โ€” SA2 injects styles via JavaScript.
831
+ > Under `style-src 'self'` with no `'unsafe-inline'`, the popups won't be
832
+ > styled. Either ship SA2's CSS via your normal stylesheet (the gem vendors
833
+ > `sweetalert2.css`) or allow a style nonce for the inserted tags.
834
+
835
+ ---
836
+
837
+ ## ๐ŸŽญ Themes
838
+
839
+ The six official SweetAlert2 themes are vendored alongside the default
840
+ stylesheet โ€” pick one, load it in your layout, and set the
841
+ `data-swal2-theme` attribute to activate it.
842
+
843
+ | Theme | File |
844
+ | --- | --- |
845
+ | Bootstrap 4 | `sweetalert2/themes/bootstrap-4.css` |
846
+ | Bootstrap 5 | `sweetalert2/themes/bootstrap-5.css` |
847
+ | Borderless | `sweetalert2/themes/borderless.css` |
848
+ | Bulma | `sweetalert2/themes/bulma.css` |
849
+ | Material UI | `sweetalert2/themes/material-ui.css` |
850
+ | Minimal | `sweetalert2/themes/minimal.css` |
851
+
852
+ ### Importmap / jsbundling (apps that load their own CSS)
853
+
854
+ ```erb
855
+ <%# app/views/layouts/application.html.erb %>
856
+ <%= stylesheet_link_tag "sweetalert2/themes/bootstrap-5" %>
857
+ <body data-swal2-theme="bootstrap-5">
858
+ ```
859
+
860
+ ### Sprockets
861
+
862
+ ```css
863
+ /* app/assets/stylesheets/application.css */
864
+ *= require sweetalert2/themes/bootstrap-5
865
+ ```
866
+
867
+ `bootstrap-5` and `material-ui` ship both a light and a dark variant
868
+ baked into the same file โ€” set `data-swal2-theme="bootstrap-5-dark"` or
869
+ `"material-ui-dark"` to opt into dark mode.
870
+
871
+ > **OS-driven dark mode?** Hook the attribute to `prefers-color-scheme`
872
+ > with a one-liner:
873
+ > ```js
874
+ > document.body.dataset.swal2Theme =
875
+ > matchMedia("(prefers-color-scheme: dark)").matches
876
+ > ? "bootstrap-5-dark" : "bootstrap-5"
877
+ > ```
878
+
879
+ ---
880
+
881
+ ## ๐ŸŽจ Asset pipelines
882
+
883
+ `swal_rails` adapts to whichever pipeline you use โ€” the generator picks the
884
+ right template automatically.
885
+
886
+ ```
887
+ โ”Œโ”€ Importmap (Rails 7+ default) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
888
+ โ”‚ config/importmap.rb โ”‚
889
+ โ”‚ pin "sweetalert2", to: "sweetalert2.js" โ”‚
890
+ โ”‚ pin "swal_rails", to: "swal_rails.js" โ”‚
891
+ โ”‚ app/javascript/application.js โ”‚
892
+ โ”‚ import "swal_rails" โ”‚
893
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
894
+
895
+ โ”Œโ”€ jsbundling (esbuild / vite / rollup) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
896
+ โ”‚ package.json โ†’ "sweetalert2": "^11" (your bundler resolves it) โ”‚
897
+ โ”‚ app/javascript/application.js โ”‚
898
+ โ”‚ import "swal_rails" โ”‚
899
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
900
+
901
+ โ”Œโ”€ Sprockets (legacy) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
902
+ โ”‚ app/assets/javascripts/application.js โ”‚
903
+ โ”‚ //= require sweetalert2 โ”‚
904
+ โ”‚ //= require swal_rails โ”‚
905
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
906
+ ```
907
+
908
+ ---
909
+
910
+ ## ๐Ÿงช Development
911
+
912
+ ```bash
913
+ $ git clone https://github.com/Metalzoid/swal_rails.git
914
+ $ cd swal_rails
915
+ $ bundle install
916
+ $ bundle exec rspec # 47 examples, real headless Chromium (Cuprite)
917
+ $ bundle exec rubocop # style
918
+ ```
919
+
920
+ Test against a specific Rails version:
921
+
922
+ ```bash
923
+ $ bundle exec appraisal install
924
+ $ BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rspec
925
+ $ BUNDLE_GEMFILE=gemfiles/rails_8_1_sprockets.gemfile bundle exec rspec
926
+ ```
927
+
928
+ ### Repo layout
929
+
930
+ ```
931
+ swal_rails/
932
+ โ”œโ”€โ”€ app/ # Engine: helpers, views, assets
933
+ โ”œโ”€โ”€ lib/
934
+ โ”‚ โ”œโ”€โ”€ swal_rails.rb # entrypoint + Engine + Railtie
935
+ โ”‚ โ”œโ”€โ”€ swal_rails/configuration.rb
936
+ โ”‚ โ””โ”€โ”€ generators/
937
+ โ”‚ โ”œโ”€โ”€ install/ # bin/rails g swal_rails:install
938
+ โ”‚ โ””โ”€โ”€ locales/ # bin/rails g swal_rails:locales
939
+ โ”œโ”€โ”€ vendor/javascript/sweetalert2/ # pinned SA2 v11.26.24 (MIT)
940
+ โ”œโ”€โ”€ spec/
941
+ โ”‚ โ””โ”€โ”€ dummy/ # minimal Rails app for system tests
942
+ โ”œโ”€โ”€ Appraisals # Rails 7.2 โ†’ 8.1 + sprockets variant
943
+ โ””โ”€โ”€ gemfiles/ # per-version lockfiles (generated)
944
+ ```
945
+
946
+ ---
947
+
948
+ ## ๐Ÿค Contributing
949
+
950
+ Bug reports and pull requests welcome on GitHub at
951
+ <https://github.com/Metalzoid/swal_rails>.
952
+
953
+ 1. Fork it
954
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
955
+ 3. Make sure tests and RuboCop pass (`bundle exec rspec && bundle exec rubocop`)
956
+ 4. Push (`git push origin my-new-feature`)
957
+ 5. Open a Pull Request
958
+
959
+ ---
960
+
961
+ ## ๐Ÿ™ Credits & license
962
+
963
+ This gem bundles [SweetAlert2](https://sweetalert2.github.io/) by Tristan
964
+ Edwards, Limon Monte and contributors, distributed under the MIT License. The
965
+ full license is included at `vendor/javascript/sweetalert2/LICENSE`.
966
+
967
+ `swal_rails` itself is released under the [MIT License](LICENSE.txt).
968
+
969
+ <div align="center">
970
+
971
+ Built with ๐Ÿฌ by [Metalzoid](https://github.com/Metalzoid)
972
+
973
+ </div>