turbo_overlay 0.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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +436 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +330 -0
  5. data/Rakefile +35 -0
  6. data/app/assets/stylesheets/turbo_overlay.css +234 -0
  7. data/app/javascript/turbo_overlay/dialog_utils.js +46 -0
  8. data/app/javascript/turbo_overlay/hint.js +670 -0
  9. data/app/javascript/turbo_overlay/history.js +184 -0
  10. data/app/javascript/turbo_overlay/index.js +53 -0
  11. data/app/javascript/turbo_overlay/options.js +152 -0
  12. data/app/javascript/turbo_overlay/overlay_controller.js +882 -0
  13. data/app/javascript/turbo_overlay/popover_position.js +64 -0
  14. data/app/javascript/turbo_overlay/setup.js +885 -0
  15. data/app/javascript/turbo_overlay/stack_controller.js +131 -0
  16. data/app/javascript/turbo_overlay/submit_close.js +49 -0
  17. data/app/javascript/turbo_overlay/visit.js +52 -0
  18. data/app/views/layouts/turbo_overlay/drawer.html.erb +5 -0
  19. data/app/views/layouts/turbo_overlay/hint.html.erb +10 -0
  20. data/app/views/layouts/turbo_overlay/modal.html.erb +5 -0
  21. data/app/views/layouts/turbo_overlay/popover.html.erb +5 -0
  22. data/app/views/turbo_overlay/_drawer.html.erb +49 -0
  23. data/app/views/turbo_overlay/_hint.html.erb +6 -0
  24. data/app/views/turbo_overlay/_loading.html.erb +12 -0
  25. data/app/views/turbo_overlay/_modal.html.erb +46 -0
  26. data/app/views/turbo_overlay/_popover.html.erb +54 -0
  27. data/config/importmap.rb +11 -0
  28. data/lib/generators/turbo_overlay/eject_generator.rb +115 -0
  29. data/lib/generators/turbo_overlay/install_generator.rb +443 -0
  30. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_confirm.html.erb +13 -0
  31. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_drawer.html.erb +50 -0
  32. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_hint.html.erb +9 -0
  33. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_loading.html.erb +9 -0
  34. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_modal.html.erb +49 -0
  35. data/lib/generators/turbo_overlay/templates/chrome/bootstrap3/_popover.html.erb +54 -0
  36. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_confirm.html.erb +13 -0
  37. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_drawer.html.erb +55 -0
  38. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_hint.html.erb +9 -0
  39. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_loading.html.erb +9 -0
  40. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_modal.html.erb +58 -0
  41. data/lib/generators/turbo_overlay/templates/chrome/bootstrap5/_popover.html.erb +53 -0
  42. data/lib/generators/turbo_overlay/templates/chrome/plain/_confirm.html.erb +14 -0
  43. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_confirm.html.erb +17 -0
  44. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_drawer.html.erb +55 -0
  45. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_hint.html.erb +6 -0
  46. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_loading.html.erb +9 -0
  47. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_modal.html.erb +46 -0
  48. data/lib/generators/turbo_overlay/templates/chrome/tailwind/_popover.html.erb +54 -0
  49. data/lib/generators/turbo_overlay/templates/initializer.rb.tt +67 -0
  50. data/lib/turbo_overlay/configuration.rb +226 -0
  51. data/lib/turbo_overlay/controller.rb +405 -0
  52. data/lib/turbo_overlay/engine.rb +52 -0
  53. data/lib/turbo_overlay/helpers/stream_helper.rb +77 -0
  54. data/lib/turbo_overlay/helpers/view_helper.rb +651 -0
  55. data/lib/turbo_overlay/version.rb +3 -0
  56. data/lib/turbo_overlay.rb +20 -0
  57. metadata +161 -0
@@ -0,0 +1,443 @@
1
+ require "rails/generators/base"
2
+
3
+ module TurboOverlay
4
+ module Generators
5
+ # Wires the host app to use turbo_overlay. Detects the host's JS
6
+ # and CSS toolchain, then either injects the required one-liners
7
+ # or prints the snippets the user needs to paste. Also copies the
8
+ # chosen chrome partials into `app/views/turbo_overlay/` so the
9
+ # app owns its modal/drawer appearance from day one (and so
10
+ # Tailwind / similar scanners can see the markup).
11
+ #
12
+ # bin/rails g turbo_overlay:install
13
+ # bin/rails g turbo_overlay:install --theme tailwind
14
+ #
15
+ # Re-running is idempotent: existing files and already-injected
16
+ # wiring are detected and skipped. Pass `--force` to overwrite
17
+ # files when upgrading.
18
+ class InstallGenerator < ::Rails::Generators::Base
19
+ source_root File.expand_path("templates", __dir__)
20
+
21
+ GEM_ROOT = File.expand_path("../../..", __dir__)
22
+
23
+ THEMES = %w[plain tailwind bootstrap5 bootstrap3].freeze
24
+
25
+ # Chrome + loading partials the gem ships fallbacks for under
26
+ # `app/views/turbo_overlay/`. For the `plain` theme the install
27
+ # generator copies those fallbacks directly so we don't have two
28
+ # canonical copies of the same markup. Themed (`tailwind`,
29
+ # `bootstrap5`, `bootstrap3`) installs source from the
30
+ # per-theme template directory.
31
+ #
32
+ # `_loading.html.erb` is a single shared partial — the same body
33
+ # renders inside modal, drawer, popover, and hint chromes, with
34
+ # chrome-context CSS handling the size differences. Apps that
35
+ # want chrome-specific loading markup can add
36
+ # `_loading.html+<variant>.erb` and the existing lookup will
37
+ # prefer it over the shared file.
38
+ FALLBACK_PARTIALS = %w[
39
+ _modal.html.erb
40
+ _drawer.html.erb
41
+ _popover.html.erb
42
+ _hint.html.erb
43
+ _loading.html.erb
44
+ ].freeze
45
+
46
+ # Confirm partials have no gem-side fallback — they're optional
47
+ # (only used when `register(application, confirm: true)`) and
48
+ # require per-theme markup. Always source from the theme directory.
49
+ # A single `_confirm.html.erb` serves both modal- and popover-style
50
+ # confirms; add `_confirm.html+<variant>.erb` for chrome-specific tuning.
51
+ THEME_ONLY_PARTIALS = %w[
52
+ _confirm.html.erb
53
+ ].freeze
54
+
55
+ class_option :theme,
56
+ type: :string,
57
+ default: "plain",
58
+ desc: "Chrome theme to scaffold. One of: #{THEMES.join(", ")}."
59
+
60
+ class_option :skip_layout_inject,
61
+ type: :boolean,
62
+ default: false,
63
+ desc: "Skip injecting `<%= overlay_stack_tag %>` into application.html.erb"
64
+
65
+ class_option :skip_javascript,
66
+ type: :boolean,
67
+ default: false,
68
+ desc: "Skip wiring the Stimulus registration"
69
+
70
+ class_option :skip_stylesheet,
71
+ type: :boolean,
72
+ default: false,
73
+ desc: "Skip wiring the stylesheet import"
74
+
75
+ class_option :skip_chrome,
76
+ type: :boolean,
77
+ default: false,
78
+ desc: "Skip copying the modal/drawer chrome partials into app/views/turbo_overlay/"
79
+
80
+ def validate_theme
81
+ @theme = options[:theme].to_s
82
+ unless THEMES.include?(@theme)
83
+ raise Thor::Error,
84
+ "Theme '#{@theme}' not recognized. Choose from: #{THEMES.join(", ")}."
85
+ end
86
+ end
87
+
88
+ def copy_initializer
89
+ template "initializer.rb.tt", "config/initializers/turbo_overlay.rb"
90
+ end
91
+
92
+ def copy_chrome_partials
93
+ return if options[:skip_chrome]
94
+
95
+ FALLBACK_PARTIALS.each do |filename|
96
+ dest = "app/views/turbo_overlay/#{filename}"
97
+ if @theme == "plain"
98
+ copy_gem_fallback_partial(filename, dest)
99
+ else
100
+ copy_file chrome_source_path(filename), dest
101
+ end
102
+ end
103
+
104
+ THEME_ONLY_PARTIALS.each do |filename|
105
+ copy_file chrome_source_path(filename),
106
+ "app/views/turbo_overlay/#{filename}"
107
+ end
108
+ end
109
+
110
+ def inject_stack_tag
111
+ if options[:skip_layout_inject]
112
+ @layout_instructions_only = true
113
+ return
114
+ end
115
+
116
+ layout_path = locate_application_layout
117
+ unless layout_path
118
+ @layout_instructions_only = true
119
+ say_status :skip, "no application.html.erb found; see post-install instructions for `overlay_stack_tag`", :yellow
120
+ return
121
+ end
122
+
123
+ contents = File.read(File.join(destination_root, layout_path))
124
+ if contents.include?("overlay_stack_tag") || contents.include?("overlay_frame_tags")
125
+ say_status :identical, layout_path, :blue
126
+ return
127
+ end
128
+
129
+ inject_into_file layout_path, before: %r{</body>} do
130
+ " <%= overlay_stack_tag %>\n "
131
+ end
132
+ end
133
+
134
+ def wire_javascript
135
+ if options[:skip_javascript]
136
+ @js_instructions_only = true
137
+ return
138
+ end
139
+
140
+ @js_setup = detect_js_setup
141
+ case @js_setup
142
+ when :importmap
143
+ wire_importmap_stimulus_entry
144
+ when :jsbundling
145
+ @js_instructions_only = true
146
+ else
147
+ @js_instructions_only = true
148
+ end
149
+ end
150
+
151
+ def wire_stylesheet
152
+ if options[:skip_stylesheet]
153
+ @css_instructions_only = true
154
+ return
155
+ end
156
+
157
+ @css_setup = detect_css_setup
158
+ case @css_setup
159
+ when :sprockets
160
+ wire_sprockets_stylesheet
161
+ when :propshaft
162
+ wire_propshaft_stylesheet
163
+ when :cssbundling
164
+ @css_instructions_only = true
165
+ else
166
+ @css_instructions_only = true
167
+ end
168
+ end
169
+
170
+ def show_post_install_message
171
+ say "\nTurbo Overlay installed with the #{@theme} theme.\n"
172
+ say "\nSetup checklist:\n"
173
+
174
+ print_controller_step
175
+ print_layout_step
176
+ print_js_step
177
+ print_css_step
178
+
179
+ say "\nUsage:\n"
180
+ say <<~MSG
181
+ Open views as overlays:
182
+
183
+ <%= modal_link_to "New", new_thing_path %>
184
+ <%= drawer_link_to "Filters", filters_path %>
185
+ <%= popover_link_to "Edit", edit_thing_path(@thing) %>
186
+
187
+ Add hover-hint previews with `hint: true` (and `hint_url:` for
188
+ content from a separate URL):
189
+
190
+ <%= hint_link_to "User", user_path(@user) %>
191
+ <%= modal_link_to "Edit", path, hint: true, hint_url: hint_path %>
192
+ MSG
193
+ end
194
+
195
+ private
196
+
197
+ def chrome_source_path(filename)
198
+ "chrome/#{@theme}/#{filename}"
199
+ end
200
+
201
+ def copy_gem_fallback_partial(filename, destination_relative)
202
+ source = File.join(GEM_ROOT, "app/views/turbo_overlay", filename)
203
+ unless File.exist?(source)
204
+ say_status :missing, source, :red
205
+ return
206
+ end
207
+ create_file destination_relative, File.read(source)
208
+ end
209
+
210
+ def locate_application_layout
211
+ %w[
212
+ app/views/layouts/application.html.erb
213
+ app/views/layouts/application.html.haml
214
+ app/views/layouts/application.html.slim
215
+ ].find { |p| File.exist?(File.join(destination_root, p)) }
216
+ end
217
+
218
+ def detect_js_setup
219
+ return :importmap if File.exist?(File.join(destination_root, "config/importmap.rb"))
220
+
221
+ pkg = File.join(destination_root, "package.json")
222
+ if File.exist?(pkg)
223
+ json = File.read(pkg)
224
+ return :jsbundling if %w[esbuild rollup webpack bun].any? { |b| json.include?(%("#{b}")) }
225
+ end
226
+
227
+ :unknown
228
+ end
229
+
230
+ def detect_css_setup
231
+ gemfile = File.join(destination_root, "Gemfile")
232
+ if File.exist?(gemfile)
233
+ content = File.read(gemfile)
234
+ return :cssbundling if content.match?(/^\s*gem\s+["'](cssbundling-rails|dartsass-rails|tailwindcss-rails|sassc-rails)["']/)
235
+ return :propshaft if content.match?(/^\s*gem\s+["']propshaft["']/)
236
+ return :sprockets if content.match?(/^\s*gem\s+["']sprockets-rails["']/)
237
+ end
238
+
239
+ # Fall back to looking for a stylesheet file.
240
+ return :sprockets if File.exist?(File.join(destination_root, "app/assets/stylesheets/application.css"))
241
+ return :sprockets if File.exist?(File.join(destination_root, "app/assets/stylesheets/application.scss"))
242
+
243
+ :unknown
244
+ end
245
+
246
+ def wire_importmap_stimulus_entry
247
+ candidates = %w[
248
+ app/javascript/controllers/index.js
249
+ app/javascript/application.js
250
+ ]
251
+ path = candidates.find { |p| File.exist?(File.join(destination_root, p)) }
252
+ unless path
253
+ @js_instructions_only = true
254
+ return
255
+ end
256
+
257
+ contents = File.read(File.join(destination_root, path))
258
+ if contents.include?(%(from "turbo_overlay")) || contents.include?(%(from 'turbo_overlay'))
259
+ say_status :identical, path, :blue
260
+ return
261
+ end
262
+
263
+ append_to_file path, <<~JS
264
+
265
+ import { register as registerTurboOverlay } from "turbo_overlay"
266
+ registerTurboOverlay(application, { confirm: true })
267
+ JS
268
+ end
269
+
270
+ def wire_sprockets_stylesheet
271
+ candidates = %w[
272
+ app/assets/stylesheets/application.css
273
+ app/assets/stylesheets/application.css.scss
274
+ app/assets/stylesheets/application.scss
275
+ ]
276
+ path = candidates.find { |p| File.exist?(File.join(destination_root, p)) }
277
+ unless path
278
+ @css_instructions_only = true
279
+ return
280
+ end
281
+
282
+ contents = File.read(File.join(destination_root, path))
283
+ if contents.include?("turbo_overlay")
284
+ say_status :identical, path, :blue
285
+ return
286
+ end
287
+
288
+ if path.end_with?(".css")
289
+ # Manifest-style: inject `*= require turbo_overlay` into the
290
+ # sprockets require block. Fall back to appending an import.
291
+ if contents.include?("*= require_tree")
292
+ inject_into_file path, before: " *= require_tree" do
293
+ " *= require turbo_overlay\n"
294
+ end
295
+ elsif contents.match?(/\*=\s+require_self/)
296
+ inject_into_file path, after: /\*=\s+require_self\n/ do
297
+ " *= require turbo_overlay\n"
298
+ end
299
+ else
300
+ append_to_file path, %(\n@import "turbo_overlay";\n)
301
+ end
302
+ else
303
+ append_to_file path, %(\n@import "turbo_overlay";\n)
304
+ end
305
+ end
306
+
307
+ def wire_propshaft_stylesheet
308
+ # Propshaft doesn't rewrite `@import` URLs to digested paths,
309
+ # so we can't inject `@import "turbo_overlay.css"` into the
310
+ # app's manifest CSS — the browser would 404 on the
311
+ # un-digested URL. Inject a `stylesheet_link_tag` into the
312
+ # application layout instead so propshaft emits a separately
313
+ # digested `<link>` for the gem's CSS.
314
+ layout_path = locate_application_layout
315
+ unless layout_path && layout_path.end_with?(".erb")
316
+ @css_instructions_only = true
317
+ return
318
+ end
319
+
320
+ contents = File.read(File.join(destination_root, layout_path))
321
+ if contents.include?(%(stylesheet_link_tag "turbo_overlay")) ||
322
+ contents.include?(%(stylesheet_link_tag 'turbo_overlay'))
323
+ say_status :identical, layout_path, :blue
324
+ return
325
+ end
326
+
327
+ # Insert after the first existing `stylesheet_link_tag` line we
328
+ # find; otherwise fall back to printing instructions.
329
+ first_link = contents.lines.find { |l| l.include?("stylesheet_link_tag") }
330
+ unless first_link
331
+ @css_instructions_only = true
332
+ return
333
+ end
334
+
335
+ indent = first_link[/^\s*/]
336
+ new_line = %(#{indent}<%= stylesheet_link_tag "turbo_overlay", "data-turbo-track": "reload" %>\n)
337
+
338
+ # Thor's `after:` matches a String literally; pass the raw line.
339
+ inject_into_file layout_path, after: first_link do
340
+ new_line
341
+ end
342
+ end
343
+
344
+ def print_controller_step
345
+ # The generator never edits ApplicationController, so this is
346
+ # always a manual step.
347
+ say "\n #{marker(:todo)} 1. Controller concern"
348
+ say step_body(<<~BODY)
349
+ Add to ApplicationController:
350
+
351
+ class ApplicationController < ActionController::Base
352
+ include TurboOverlay::Controller
353
+ end
354
+
355
+ This auto-installs the overlay layout swap and preserves Turbo's
356
+ `turbo_rails/frame` layout for plain frame requests. For custom
357
+ layouts, see "A note on custom layouts" in the README.
358
+ BODY
359
+ end
360
+
361
+ def print_layout_step
362
+ if @layout_instructions_only
363
+ say "\n #{marker(:todo)} 2. Layout stack tag"
364
+ say step_body(<<~BODY)
365
+ Add to your application layout, just before </body>:
366
+
367
+ <%= overlay_stack_tag %>
368
+
369
+ This renders the slots overlays mount into. Without it, modal /
370
+ drawer / popover / hint links will navigate full-page instead
371
+ of opening as overlays.
372
+ BODY
373
+ else
374
+ say "\n #{marker(:done)} 2. Layout stack tag"
375
+ say step_body("`<%= overlay_stack_tag %>` is present in your application layout.")
376
+ end
377
+ end
378
+
379
+ def print_js_step
380
+ if @js_instructions_only
381
+ say "\n #{marker(:todo)} 3. JavaScript wiring"
382
+ say step_body(<<~BODY)
383
+ Add to your Stimulus entry (typically
384
+ app/javascript/controllers/index.js or your bundler's equivalent):
385
+
386
+ import { register as registerTurboOverlay } from "turbo_overlay"
387
+ registerTurboOverlay(application, { confirm: true })
388
+
389
+ jsbundling-rails apps: add the gem's `app/javascript` directory
390
+ to your bundler's resolve paths, OR run
391
+ `bin/rails g turbo_overlay:eject --js` to copy the controllers
392
+ into your app.
393
+ BODY
394
+ else
395
+ say "\n #{marker(:done)} 3. JavaScript wiring"
396
+ say step_body("Registered with your Stimulus entry.")
397
+ end
398
+ end
399
+
400
+ def print_css_step
401
+ if @css_instructions_only
402
+ say "\n #{marker(:todo)} 4. Stylesheet wiring"
403
+ say step_body(<<~BODY)
404
+ Add to your stylesheet. Pick the option that matches your setup:
405
+
406
+ # propshaft — add to app/views/layouts/application.html.erb:
407
+ <%= stylesheet_link_tag "turbo_overlay", "data-turbo-track": "reload" %>
408
+
409
+ # sprockets manifest — add to app/assets/stylesheets/application.css:
410
+ *= require turbo_overlay
411
+
412
+ # cssbundling / dartsass / tailwind v4 — add to your source CSS:
413
+ @import "turbo_overlay";
414
+
415
+ Notes:
416
+ - Propshaft does not rewrite CSS `@import` URLs to digested
417
+ asset paths; use a separate `stylesheet_link_tag` instead so
418
+ the gem's CSS is served with a fingerprinted URL.
419
+ - cssbundling apps may also need to add the gem's
420
+ `app/assets/stylesheets` directory to the bundler's load
421
+ paths, OR run `bin/rails g turbo_overlay:eject --css` to
422
+ copy the stylesheet into your app.
423
+ BODY
424
+ else
425
+ say "\n #{marker(:done)} 4. Stylesheet wiring"
426
+ say step_body("Imported into your application stylesheet.")
427
+ end
428
+ end
429
+
430
+ def marker(state)
431
+ case state
432
+ when :done then set_color("[done]", :green)
433
+ when :todo then set_color("[todo]", :yellow)
434
+ end
435
+ end
436
+
437
+ # Indent each non-empty line so step bodies sit under the marker.
438
+ def step_body(text)
439
+ text.lines.map { |l| l.chomp.empty? ? "" : " #{l.chomp}" }.join("\n")
440
+ end
441
+ end
442
+ end
443
+ end
@@ -0,0 +1,13 @@
1
+ <%# Confirm dialog body, wrapped in the matching chrome at template-
2
+ emission time by `overlay_stack_tag`. Used for both modal- and
3
+ popover-style confirms; add `_confirm.html+<variant>.erb` for
4
+ chrome-specific tuning. JS only depends on:
5
+ [data-turbo-overlay-confirm-message] — text insertion point
6
+ [data-turbo-overlay-confirm-cancel] — cancels (resolves false)
7
+ [data-turbo-overlay-confirm-accept] — accepts (resolves true)
8
+ %>
9
+ <p data-turbo-overlay-confirm-message></p>
10
+ <div style="text-align:right;margin-top:15px;">
11
+ <button type="button" class="btn btn-default" data-turbo-overlay-confirm-cancel>Cancel</button>
12
+ <button type="button" class="btn btn-primary" data-turbo-overlay-confirm-accept>OK</button>
13
+ </div>
@@ -0,0 +1,50 @@
1
+ <% loading = local_assigns.fetch(:loading, false) %>
2
+ <% position = turbo_overlay_position || TurboOverlay.configuration.drawer.position %>
3
+ <% backdrop = turbo_overlay_backdrop? %>
4
+ <% close_button = local_assigns.fetch(:close) { overlay_close? } %>
5
+ <dialog class="<%= class_names("turbo-overlay", "turbo-overlay--drawer", "turbo-overlay--drawer-#{position}", "turbo-overlay-scaffold", "turbo-overlay--loading": loading, "turbo-overlay--no-backdrop": !backdrop) %>"
6
+ <% if loading %>
7
+ role="status"
8
+ aria-live="polite"
9
+ aria-label="Loading"
10
+ <% else %>
11
+ data-controller="turbo-overlay"
12
+ data-turbo-overlay-id-value="<%= turbo_overlay_id %>"
13
+ data-turbo-overlay-type-value="drawer"
14
+ data-turbo-overlay-backdrop-value="<%= backdrop %>"
15
+ <% if turbo_overlay_keep_open_on_redirect? %>data-turbo-overlay-keep-open-on-redirect="true"<% end %>
16
+ data-action="cancel->turbo-overlay#cancel click->turbo-overlay#backdropClick"
17
+ aria-labelledby="turbo-drawer-title-<%= turbo_overlay_id %>"
18
+ <% end %>>
19
+ <div class="panel panel-default turbo-drawer-panel" style="position:relative;">
20
+ <% if !loading && content_for?(:overlay_title) %>
21
+ <div class="panel-heading">
22
+ <% if close_button %>
23
+ <button type="button"
24
+ class="close"
25
+ data-action="turbo-overlay#close"
26
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
27
+ <% end %>
28
+ <h4 id="turbo-drawer-title-<%= turbo_overlay_id %>" class="panel-title">
29
+ <%= yield(:overlay_title) %>
30
+ </h4>
31
+ </div>
32
+ <% elsif !loading && close_button %>
33
+ <button type="button"
34
+ class="close"
35
+ style="position:absolute; top:8px; right:12px; z-index:1;"
36
+ data-action="turbo-overlay#close"
37
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
38
+ <% end %>
39
+
40
+ <div class="panel-body turbo-drawer-body">
41
+ <%= yield %>
42
+ </div>
43
+
44
+ <% if !loading && content_for?(:overlay_footer) %>
45
+ <div class="panel-footer text-right">
46
+ <%= yield(:overlay_footer) %>
47
+ </div>
48
+ <% end %>
49
+ </div>
50
+ </dialog>
@@ -0,0 +1,9 @@
1
+ <% loading = local_assigns.fetch(:loading, false) %>
2
+ <div popover="manual"
3
+ class="<%= class_names("turbo-overlay-hint", "popover", "turbo-overlay--loading": loading) %>"
4
+ style="display:block; position:fixed; max-width:20rem;"
5
+ <% if loading %>role="status" aria-live="polite" aria-label="Loading"<% else %>role="tooltip"<% end %>>
6
+ <div class="popover-content">
7
+ <%= yield %>
8
+ </div>
9
+ </div>
@@ -0,0 +1,9 @@
1
+ <%# Body content for an overlay's loading placeholder. Wrapped in the
2
+ matching chrome at template-emission time by `overlay_stack_tag`.
3
+ Sizing comes from the gem's CSS so the same partial fits modal,
4
+ drawer, popover, and hint contexts. Add `_loading.html+<variant>.erb`
5
+ if you need chrome-specific markup.
6
+ %>
7
+ <div class="turbo-overlay-loading__body" style="display:flex;align-items:center;justify-content:center;">
8
+ <span class="turbo-overlay-loading__spinner" aria-hidden="true"></span>
9
+ </div>
@@ -0,0 +1,49 @@
1
+ <% loading = local_assigns.fetch(:loading, false) %>
2
+ <% close_button = local_assigns.fetch(:close) { overlay_close? } %>
3
+ <dialog class="<%= class_names("turbo-overlay", "turbo-overlay--modal", "turbo-overlay-scaffold", "turbo-overlay--loading": loading) %>"
4
+ <% if loading %>
5
+ role="status"
6
+ aria-live="polite"
7
+ aria-label="Loading"
8
+ <% else %>
9
+ data-controller="turbo-overlay"
10
+ data-turbo-overlay-id-value="<%= turbo_overlay_id %>"
11
+ data-turbo-overlay-type-value="modal"
12
+ <% if turbo_overlay_keep_open_on_redirect? %>data-turbo-overlay-keep-open-on-redirect="true"<% end %>
13
+ data-action="cancel->turbo-overlay#cancel click->turbo-overlay#backdropClick"
14
+ aria-labelledby="turbo-modal-title-<%= turbo_overlay_id %>"
15
+ <% end %>>
16
+ <div class="modal-dialog modal-lg" role="document">
17
+ <div class="modal-content" style="position:relative;">
18
+ <% if !loading && content_for?(:overlay_title) %>
19
+ <div class="modal-header">
20
+ <% if close_button %>
21
+ <button type="button"
22
+ class="close"
23
+ data-action="turbo-overlay#close"
24
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
25
+ <% end %>
26
+ <h4 id="turbo-modal-title-<%= turbo_overlay_id %>" class="modal-title">
27
+ <%= yield(:overlay_title) %>
28
+ </h4>
29
+ </div>
30
+ <% elsif !loading && close_button %>
31
+ <button type="button"
32
+ class="close"
33
+ style="position:absolute; top:8px; right:12px; z-index:1;"
34
+ data-action="turbo-overlay#close"
35
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
36
+ <% end %>
37
+
38
+ <div class="modal-body">
39
+ <%= yield %>
40
+ </div>
41
+
42
+ <% if !loading && content_for?(:overlay_footer) %>
43
+ <div class="modal-footer">
44
+ <%= yield(:overlay_footer) %>
45
+ </div>
46
+ <% end %>
47
+ </div>
48
+ </div>
49
+ </dialog>
@@ -0,0 +1,54 @@
1
+ <% loading = local_assigns.fetch(:loading, false) %>
2
+ <% position = turbo_overlay_position || TurboOverlay.configuration.popover.position %>
3
+ <% align = turbo_overlay_align || TurboOverlay.configuration.popover.align %>
4
+ <% offset = turbo_overlay_offset || TurboOverlay.configuration.popover.offset %>
5
+ <% close_button = local_assigns.fetch(:close) { defined?(@_overlay_close) && @_overlay_close != false } %>
6
+ <dialog popover="manual"
7
+ class="<%= class_names("turbo-overlay", "turbo-overlay--popover", "turbo-overlay--no-backdrop", "popover", "turbo-overlay--loading": loading) %>"
8
+ style="display:block; position:fixed; max-width:22rem; margin:0; padding:0;"
9
+ <% if loading %>
10
+ role="status"
11
+ aria-live="polite"
12
+ aria-label="Loading"
13
+ <% else %>
14
+ data-controller="turbo-overlay"
15
+ data-turbo-overlay-id-value="<%= turbo_overlay_id %>"
16
+ data-turbo-overlay-type-value="popover"
17
+ data-turbo-overlay-backdrop-value="false"
18
+ data-turbo-overlay-position-value="<%= position %>"
19
+ data-turbo-overlay-align-value="<%= align %>"
20
+ data-turbo-overlay-offset-value="<%= offset %>"
21
+ <% if turbo_overlay_keep_open_on_redirect? %>data-turbo-overlay-keep-open-on-redirect="true"<% end %>
22
+ data-action="cancel->turbo-overlay#cancel"
23
+ aria-labelledby="turbo-popover-title-<%= turbo_overlay_id %>"
24
+ <% end %>>
25
+ <div style="position:relative;">
26
+ <% if !loading && content_for?(:overlay_title) %>
27
+ <h3 class="popover-title" id="turbo-popover-title-<%= turbo_overlay_id %>" style="display:flex; align-items:center; justify-content:space-between;">
28
+ <span><%= yield(:overlay_title) %></span>
29
+ <% if close_button %>
30
+ <button type="button"
31
+ class="close"
32
+ data-action="turbo-overlay#close"
33
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
34
+ <% end %>
35
+ </h3>
36
+ <% elsif !loading && close_button %>
37
+ <button type="button"
38
+ class="close"
39
+ style="position:absolute; top:6px; right:10px; z-index:1;"
40
+ data-action="turbo-overlay#close"
41
+ aria-label="Close"><span aria-hidden="true">&times;</span></button>
42
+ <% end %>
43
+
44
+ <div class="popover-content">
45
+ <%= yield %>
46
+ </div>
47
+
48
+ <% if !loading && content_for?(:overlay_footer) %>
49
+ <div class="popover-footer" style="border-top:1px solid #e5e7eb; padding:0.5rem 0.75rem; display:flex; justify-content:flex-end; gap:0.5rem;">
50
+ <%= yield(:overlay_footer) %>
51
+ </div>
52
+ <% end %>
53
+ </div>
54
+ </dialog>
@@ -0,0 +1,13 @@
1
+ <%# Confirm dialog body, wrapped in the matching chrome at template-
2
+ emission time by `overlay_stack_tag`. Used for both modal- and
3
+ popover-style confirms; add `_confirm.html+<variant>.erb` for
4
+ chrome-specific tuning. JS only depends on:
5
+ [data-turbo-overlay-confirm-message] — text insertion point
6
+ [data-turbo-overlay-confirm-cancel] — cancels (resolves false)
7
+ [data-turbo-overlay-confirm-accept] — accepts (resolves true)
8
+ %>
9
+ <p data-turbo-overlay-confirm-message></p>
10
+ <div class="d-flex justify-content-end gap-2 mt-3">
11
+ <button type="button" class="btn btn-secondary" data-turbo-overlay-confirm-cancel>Cancel</button>
12
+ <button type="button" class="btn btn-primary" data-turbo-overlay-confirm-accept>OK</button>
13
+ </div>