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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f96f9c76f0dba518bc7f30f1d1301f518bf05c113f2b3cbe3003e8c6be6500dc
4
+ data.tar.gz: 0af6b6007b41538970e9afb5cca4fc2347e4c92aefa8482b3b07d6419bdfb06f
5
+ SHA512:
6
+ metadata.gz: 60d02ea17dd6f4ed9afab004c1bd53faedc4f3f597bea8dbc7278503bb65a6512b7291a91438da275232aefd8ed1d496c2be39c017f74579288d19aa64f10946
7
+ data.tar.gz: 352be4c2a0084c7811e3ae1c2b84127b60d9dd3163036be4f308f4fd1636c75378e1dec38a6a93a5fdc6ae09f668022ad46fb7aea1045ad485a990f6b9407d44
data/CHANGELOG.md ADDED
@@ -0,0 +1,436 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ Big iteration cycle ahead of the first public release. Highlights:
6
+
7
+ ### Changed
8
+ - **Smooth close on form-submit redirects.** Two improvements to the
9
+ close-on-redirect path, both default-on; the existing
10
+ `keep_overlay_open_on_redirect` / per-form opt-out covers both.
11
+ Also fixes a pre-existing flash where Turbo would render the
12
+ redirect target's HTML into the still-open overlay before the
13
+ submit-end handler closed it — same-origin fetch follows preserve
14
+ the `Turbo-Frame` / `X-Turbo-Overlay` headers, so the redirect
15
+ target was being wrapped in overlay layout by the controller
16
+ concern and morphed into the dialog. The per-dialog listener now
17
+ stops the `turbo:before-fetch-response` event before Turbo's
18
+ StreamObserver and FormSubmission can process it.
19
+ - **Same-page redirects morph the host page behind the overlay
20
+ before closing.** When the redirect target's pathname matches the
21
+ URL the overlay was opened from, the gem fetches the target,
22
+ morphs `document.body` (excluding the `overlay_stack_tag`
23
+ container so the open dialog is preserved), updates the URL via
24
+ `history.replaceState`, then animates the close. No more
25
+ flash-of-stale-content while the overlay closes. Modal/drawer
26
+ only; falls back to the existing close-then-visit path when
27
+ another overlay is open in the stack, when the fetch fails, or
28
+ when `Turbo.morphChildren` is unavailable. Uses
29
+ `window.Turbo.morphChildren` (public API in Turbo 8) so morph
30
+ runs through Turbo's own Idiomorph copy and `data-turbo-permanent`
31
+ + `turbo:before-morph-*` events compose normally — no new
32
+ dependency. The opener URL is captured on the dialog as
33
+ `data-turbo-overlay-opener-url` on Stimulus connect; the morph-
34
+ attribute preservation hook keeps it intact through in-overlay
35
+ form re-renders.
36
+ - **Different-page redirects await the close animation before
37
+ navigating.** Previously `Turbo.visit` fired immediately after
38
+ starting the close, so the new page could paint behind a
39
+ still-closing overlay. The submit-end handler now `await`s the
40
+ overlay close (which returns a `Promise`) before invoking
41
+ `Turbo.visit`. The close `Promise` resolves on `animationend`,
42
+ the 400ms fallback timer, the reduced-motion shortcut, or the
43
+ no-dialog shortcut — same lifecycle as before, just observable.
44
+ - **Form submissions inside an overlay close it on redirect by default.**
45
+ When a descendant form submits and the response is a followed
46
+ redirect (`fetchResponse.redirected === true`), the overlay closes
47
+ and the browser visits the redirect target via `Turbo.visit`. Apps
48
+ previously relying on explicit `turbo_stream.overlay(:close)`
49
+ responses keep working unchanged — the stream-action path runs
50
+ before any redirect would. Validation failures
51
+ (`:unprocessable_entity`, 422) don't redirect, so in-place form
52
+ re-renders are untouched. Two opt-outs, finest-grained wins:
53
+ - **Per-overlay (link helper):**
54
+ `modal_link_to "Wizard", path, keep_overlay_open_on_redirect: true`
55
+ (also on `drawer_link_to` and `popover_link_to`). Round-trips
56
+ through the `X-Turbo-Overlay-Keep-Open` request header to a
57
+ `data-turbo-overlay-keep-open-on-redirect="true"` attribute on
58
+ the `<dialog>`.
59
+ - **Per-form (data attribute):**
60
+ `<form data-turbo-overlay-keep-open-on-redirect="true">` opts
61
+ out a single form while sibling forms in the same overlay still
62
+ close on redirect.
63
+ Detection is a per-dialog `turbo:submit-end` listener installed
64
+ in the gem's Stimulus controller — scoped to descendants of the
65
+ dialog, auto-cleaned on disconnect, never document-scoped. The
66
+ decision lives in `app/javascript/turbo_overlay/submit_close.js`
67
+ (`shouldCloseOnRedirect`) as a pure function. Hosts whose form
68
+ redirects previously fell into a broken frame-replacement state
69
+ (the redirect target had no matching `turbo_overlay_<type>_<id>`
70
+ frame) get correct behavior now.
71
+ - **In-overlay form re-renders now morph the dialog in place.**
72
+ `overlay_response_wrapper` emits a `<turbo-stream action="replace"
73
+ method="morph">` on a frame re-render (form validation failure
74
+ inside an open overlay) instead of a bare `<turbo-frame>`. Idiomorph
75
+ preserves the `<dialog>` node identity, top-layer membership,
76
+ popover anchor coordinates, stack registration, and the
77
+ document-level ESC / outside-click / reflow handlers the per-dialog
78
+ Stimulus controller installed on first open. Previously Turbo
79
+ replaced the frame's contents wholesale; for popovers this detached
80
+ the new dialog from its anchor and re-rendered it centered, and for
81
+ every overlay type it leaked the original controller's document
82
+ handlers. The after_action that sets `text/vnd.turbo-stream.html`
83
+ on initial-open responses now also runs on frame re-renders so
84
+ Turbo processes the embedded `<turbo-stream>`. `request.format`
85
+ stays `:turbo_stream` on re-renders — apps' `respond_to`
86
+ `format.turbo_stream` branches keep running on successful saves;
87
+ implicit `render :edit` calls still find `edit.html.erb` via
88
+ Rails' format-fallback. A `turbo:before-morph-attribute` hook in
89
+ `setup.js` preserves the two attributes the JS owns on overlay
90
+ dialogs (`open` and inline `style`) so idiomorph doesn't strip them.
91
+ - **Auto-installed overlay `layout` proc.** Including
92
+ `TurboOverlay::Controller` now declares `layout -> { turbo_overlay_layout }`
93
+ on its own, so host apps no longer hand-write a `resolve_layout`
94
+ method. The proc also re-installs Turbo's `"turbo_rails/frame"`
95
+ layout for plain turbo-frame requests, so including the concern
96
+ does not regress Turbo's frame-layout optimization. Apps with a
97
+ custom layout method call `turbo_overlay_layout` from it (see
98
+ README "A note on custom layouts").
99
+ - **Bundled layouts moved to `app/views/layouts/turbo_overlay/{modal,drawer,popover,hint}.html.erb`**
100
+ to mirror `turbo_rails/frame.html.erb`. Layout names returned by
101
+ the helper are now `"turbo_overlay/modal"`, `"turbo_overlay/drawer"`,
102
+ `"turbo_overlay/popover"`, `"turbo_overlay/hint"`.
103
+
104
+ ### Fixed
105
+ - **Drag-out text selections no longer dismiss the overlay.** Press,
106
+ drag, and release across the dialog boundary used to be reported by
107
+ the browser as a click on the dialog itself (W3C: click target is
108
+ the lowest common ancestor of mousedown and mouseup), which the
109
+ backdrop-click handler treated as a dismissal. The handler now
110
+ consults the mousedown target and suppresses dismissal when the
111
+ selection started inside the dialog content.
112
+
113
+ - **Body-portaled widgets (flatpickr, Select2, Tippy, Tom Select) no
114
+ longer dismiss popovers.** Configure
115
+ `TurboOverlay.configuration.allowed_click_outside_selectors` with
116
+ the widget's portal selectors; clicks inside any matching element
117
+ are ignored by the popover's outside-click handler and the
118
+ modal/drawer backdrop-click handler. Per-overlay override via
119
+ `data-turbo-overlay-allow-click-outside` (CSV) on the dialog.
120
+ Malformed selectors are skipped with a one-time console warning, so
121
+ a single typo can't disable dismissal for the rest of the list.
122
+
123
+ - **Hover prefetch of in-overlay links no longer replaces the open
124
+ overlay.** Turbo's hover prefetch sends the enclosing frame's id
125
+ in the `Turbo-Frame` header. The controller concern was falling
126
+ into its in-overlay form-re-render branch on that header, routing
127
+ the prefetched URL through the overlay layout and returning a
128
+ turbo-stream that morphed the open overlay's contents on receipt.
129
+ The concern now skips the Turbo-Frame fallback when
130
+ `X-Sec-Purpose: prefetch` is set — explicit overlay opens
131
+ (`X-Turbo-Overlay` header) are unaffected. The same guard runs in
132
+ `turbo_overlay_frame_re_render?` so the response Content-Type
133
+ stays consistent with the body. As a complementary bandwidth save,
134
+ dismiss links (`modal_dismiss_link_to` etc.) rendered inside an
135
+ overlay now set `data-turbo-prefetch="false"` since their click
136
+ is preventDefault'd by the Stimulus action.
137
+ - **Closing the last advance overlay no longer leaves Turbo's
138
+ progress bar stuck at the top of the page.** When the entry
139
+ beneath the advance overlay has Turbo's restoration state (e.g.
140
+ the page that was loaded via Turbo Drive before opening any
141
+ overlay), the gem cancels the proposed restore visit — but
142
+ `FetchRequest#perform` had already started on a microtask and
143
+ would call `visit.requestStarted()` after our handler returned,
144
+ scheduling a `.turbo-progress-bar` timer that never gets cleared
145
+ (canceled visits don't fire `visitCompleted`). The gem now stubs
146
+ `visit.requestStarted` to a no-op before canceling and clears
147
+ the stray `aria-busy` attribute that
148
+ `Session#visitStarted` left on `<html>`.
149
+ - **Closing the top of a stacked advance overlay no longer tears
150
+ down the whole stack.** Two compounded bugs:
151
+ (a) The gem's `turbo:before-cache` handler ran
152
+ `tearDownAllOverlays` for every cache snapshot — including the
153
+ one Turbo Drive's `historyPoppedWithEmptyState` triggers
154
+ synchronously on every popstate without a visit. Now gated on a
155
+ `_realVisitInProgress` flag set in `turbo:before-visit` so only
156
+ real visits clear overlays.
157
+ (b) For popstates where the popped entry carries Turbo's own
158
+ restoration state (e.g. backing out of an advance overlay to a
159
+ Turbo-loaded page), Turbo proposes a restore visit that would
160
+ load the cached snapshot and replace the page. The gem now
161
+ cancels that restore visit from `turbo:visit` (action="restore")
162
+ when an advance overlay is involved — leaving the overlay close
163
+ to the popstate handler. Visit cancellation aborts the queued
164
+ render before `cacheSnapshot()` runs, so no `before-cache` side
165
+ effects either.
166
+
167
+ ### Removed
168
+ - `modal/drawer/popover/hint.layout_name` configuration and the
169
+ matching `modal_layout_name` / `drawer_layout_name` /
170
+ `popover_layout_name` / `hint_layout_name` controller helpers.
171
+ Layout names are now hard-coded. Host apps that want a different
172
+ layout file can place their own at
173
+ `app/views/layouts/turbo_overlay/modal.html.erb` (Rails view
174
+ precedence wins over the gem's copy).
175
+
176
+ ### Added
177
+ - **`turbo_stream.overlay(:close, visit: ...)` — server-driven
178
+ post-close navigation.** Pair a close stream with a `Turbo.visit`
179
+ to the host page that runs after the close animation completes.
180
+ Useful for stream-driven flows where there's no form submission
181
+ to ride a redirect on (ActionCable broadcasts, async job
182
+ completion). Accepts `visit:` (URL) and optional
183
+ `visit_action: :advance | :replace`.
184
+ - **`modal_button_to` / `drawer_button_to` / `popover_button_to` — open
185
+ an overlay from a non-GET action.** `button_to` counterparts to the
186
+ existing link helpers; same option vocabulary. Use when the overlay
187
+ should be the result of a POST/PATCH/PUT/DELETE — creating a record,
188
+ deleting an item, kicking off a wizard. The submit hook reads the
189
+ form's overlay data attrs and emits the same `X-Turbo-Overlay-*`
190
+ request headers a `*_link_to` click would. `advance:` and hint
191
+ options aren't exposed (non-GET doesn't push history; hints are a
192
+ hover-on-link mechanism).
193
+ - **Popovers auto-close when their anchor scrolls out of view.** A
194
+ popover whose trigger isn't visible reads as a floating widget with
195
+ no obvious connection to anything; matching Bootstrap / MUI /
196
+ native iOS UIPopover, the popover now collapses when its anchor
197
+ exits the viewport. Short ~50ms debounce so momentum scrolls that
198
+ briefly clip the edge don't dismiss. The popover continues to track
199
+ the anchor through the dismissal so it slides offscreen alongside
200
+ the trigger rather than getting glued to the viewport.
201
+ - **Popover positioning moved to compositor-thread transforms.**
202
+ Replaced the per-scroll `style.top`/`style.left` writes with a
203
+ single `transform: translate(...)`. Eliminates the one-frame lag
204
+ ("springy" feel) on smooth/momentum scrolling. Popover open/close
205
+ keyframes compose with the positioning transform via
206
+ `animation-composition: add`.
207
+ - **`TurboOverlay.visit(url, options)` — open overlays from JavaScript.**
208
+ Programmatic counterpart to `modal_link_to` / `drawer_link_to` /
209
+ `popover_link_to` for non-anchor triggers (Google Maps markers, SVG
210
+ hit regions, custom elements). Full option parity with the link
211
+ helpers; popovers require an `anchor` element for positioning.
212
+ Exposed as a named export from the package and as `window.TurboOverlay`
213
+ for non-bundler callers. Reuses the existing fetch pipeline — same
214
+ headers, same loading placeholder, same lifecycle events.
215
+ - **URL advance for modals and drawers.** Pass `advance: true` on
216
+ `modal_link_to` / `drawer_link_to` (or set
217
+ `c.modal.advance = true` / `c.drawer.advance = true` in the
218
+ initializer) to push a history entry when the overlay opens.
219
+ Browser-back closes the top overlay instead of navigating away
220
+ from the page beneath. Per-link override accepts `true` (push the
221
+ link's href), a String (push a custom URL), or `false` (opt out
222
+ when the type default is on). Popovers and hints never advance —
223
+ they're ephemeral and shouldn't churn browser history.
224
+ - **Popover overlay type.** `popover_link_to "Edit", path` opens its
225
+ target as a non-modal `<dialog>` anchored to the clicked link.
226
+ Per-link `position:`, `align:`, `offset:`; auto-flips on viewport
227
+ overflow; ESC and click-outside dismiss. Opening a second popover
228
+ dismisses the previous one — modals and drawers still stack on top.
229
+ - **Hover hints.** `hint_link_to "User", user_path(@user)` (or
230
+ `hint: true` / `hint_url:` on any overlay link helper) shows a
231
+ preview popover after ~250ms hover. Content comes from a `+hint`
232
+ variant template (`show.html+hint.erb`) that `overlay_stack_tag`
233
+ auto-emits on hintable requests. Piggy-backs on Turbo's hover
234
+ prefetch — one fetch warms navigation and seeds the hint. Falls
235
+ back to its own `fetch()` on prefetch-disabled sites; negative-caches
236
+ no-hint responses; ships a pending placeholder for slow controllers.
237
+ - **Loading state for overlay clicks.** Every modal/drawer/popover/hint
238
+ click drops a placeholder dialog matching the eventual chrome,
239
+ morphed in-place when the real response lands. ESC/backdrop-click
240
+ on a placeholder cancels the in-flight fetch via `AbortController`.
241
+ - **Themed `data-turbo-confirm`.** `register(application, { confirm: true })`
242
+ routes confirm prompts through the gem's themed dialog. Pick modal
243
+ or popover style globally (`config.confirm.style`) or per-link
244
+ (`data-turbo-confirm-style`). Falls back to `window.confirm` when
245
+ the template is absent.
246
+ - **Overlay lifecycle JS events:** `turbo-overlay:shown`,
247
+ `turbo-overlay:before-close`, `turbo-overlay:closed`,
248
+ `turbo-overlay:hint-shown`, `turbo-overlay:hint-ready`. All bubble
249
+ from the dialog (or document, for hint events) so apps can wire
250
+ autofocus, analytics, and cleanup without monkey-patching.
251
+ - **Drawer per-link options.** `position:` (`:left`/`:right`/`:top`/`:bottom`)
252
+ overrides the configured default. `backdrop: false` opens the drawer
253
+ non-modally so the host page stays interactive.
254
+ - **Default close button** in modal/drawer chrome — floating top-right
255
+ when no header, inside the header when `overlay_title` is set.
256
+ Suppress with `<% overlay_close false %>`, `close: false` on the
257
+ link, or a `close: false` partial local.
258
+ - **App-owned chrome partials.** Install drops `_modal.html.erb`,
259
+ `_drawer.html.erb`, `_popover.html.erb`, `_hint.html.erb`, and
260
+ body-only `_confirm.html.erb` + `_loading.html.erb` into
261
+ `app/views/turbo_overlay/`. Tailwind content scanners pick them up
262
+ automatically.
263
+ - **Bootstrap 3 drawer support** as a vanilla dialog styled with BS3
264
+ panel classes.
265
+ - **Dark-mode classes on the Tailwind theme.**
266
+
267
+ ### Changed
268
+ - **Single native `<dialog>` JS controller for every theme.** Bootstrap
269
+ themes keep their visual classes but no longer require
270
+ `window.bootstrap` or jQuery — the `<dialog>` element drives
271
+ open/close, stacking, and focus management.
272
+ - **Animations on by default.** Modals fade/scale, drawers slide from
273
+ their configured edge, backdrops fade. All honor
274
+ `prefers-reduced-motion: reduce`.
275
+ - **CSS ships as a real stylesheet asset** (propshaft / sprockets /
276
+ bundler-friendly) — the interim `turbo_overlay_styles` view helper
277
+ is gone.
278
+ - **Stimulus controllers shipped from the gem with importmap auto-pin.**
279
+ Bundler apps reference the gem's `app/javascript` directly or use
280
+ `bin/rails g turbo_overlay:eject --js` to copy locally.
281
+ - **Backdrop click dismisses by default.** Opt out with
282
+ `data-turbo-overlay-backdrop-dismiss-value="false"`.
283
+ - **Stacked overlay links target `_top`** so modal/drawer links inside
284
+ an open overlay stack a new one instead of replacing the current
285
+ frame.
286
+ - **Helpers renamed for namespacing.** `current_overlay_*` →
287
+ `turbo_overlay_*`; `close_button:` → `close:` on link helpers;
288
+ hint predicates moved to the `overlay_` namespace. Hard renames, no
289
+ aliases.
290
+ - **Install footprint shrunk** to one `_loading.html.erb` and one
291
+ `_confirm.html.erb` per theme; chrome-specific overrides via
292
+ `_loading.html+<variant>.erb` / `_confirm.html+<variant>.erb` still
293
+ win when present.
294
+ - **Chrome wrapping moved into `overlay_stack_tag`.** Confirm/loading
295
+ partials are body-only; the chrome wraps them at template-emission
296
+ time. Adds a `loading:` local to chrome partials that drops the
297
+ Stimulus controller wiring, close button, and title/footer slots,
298
+ and switches ARIA to `role="status"`.
299
+
300
+ ### Removed
301
+ - `window.bootstrap` and jQuery requirements for the Bootstrap themes.
302
+ - Per-theme overlay controllers (one shared `overlay_controller.js`).
303
+ - Inline `<style>` blocks from every shipped overlay layout.
304
+ - Inline `turbo_overlay_hint do … end` capture helper — the `+hint`
305
+ variant template is the canonical and only path now.
306
+ - Config knobs without real consumers: `OverlayTypeConfig#frame_id`,
307
+ `#stimulus_identifier`, `HintConfig#enabled`, `#template_id`.
308
+
309
+ ### Fixed
310
+ - **Form re-render keeps the overlay open** when a submission inside
311
+ an open overlay responds `:unprocessable_entity` — the new dialog
312
+ is re-opened in the same mode after Turbo's frame replacement.
313
+ - **Back/forward navigation no longer restores broken-state overlays.**
314
+ `turbo:before-cache` tears down every overlay frame and aborts
315
+ in-flight fetches so the cached snapshot has no overlay state to
316
+ restore.
317
+ - **Bootstrap5 modal renders with the themed background** — `.modal-dialog`
318
+ is now wrapped in `.modal.d-block.position-static` so Bootstrap's
319
+ `--bs-modal-*` variables cascade.
320
+ - **Hint detection fixes:** `+hint` variant auto-render only fires
321
+ when a real `+hint` sibling file exists on disk; prefetch detection
322
+ uses `X-Sec-Purpose` (the prefix Turbo can actually set); pending
323
+ placeholder dismisses on no-template / errored responses with
324
+ negative caching to prevent stuck spinners.
325
+ - **Popover-style confirm works for `link_to … data-turbo-method`** —
326
+ the originating element is captured on click so the popover has an
327
+ anchor when Turbo's submitter is null.
328
+ - Stacked overlay close animation is now reliable across themes.
329
+ - **Popover inside an open modal is interactive again.** HTML's
330
+ modal-dialog inertness algorithm blocks every non-descendant of the
331
+ topmost modal from receiving input — even top-layer popovers added
332
+ via `showPopover()` afterwards. Popovers opened from inside a modal
333
+ now use `showModal()` themselves (with a transparent `::backdrop`)
334
+ so they become the topmost modal and stay interactive.
335
+ - **Popover and hint positioning no longer drifts toward the viewport
336
+ center.** UA `[popover]` styles include `inset: 0; margin: auto` —
337
+ setting only `top`/`left` left the leftover space to be distributed
338
+ via the auto margins, so the dialog rendered partway between the
339
+ trigger and the viewport edge. Now sets `right: auto; bottom: auto;
340
+ margin: 0` before measuring so the dialog stays anchored.
341
+ - **Popover anchored to wrapping inline link uses the clicked line.**
342
+ `getBoundingClientRect()` on a multi-line `<a>` returns the union
343
+ of every line box. The anchor math now picks the line containing
344
+ the recorded click point.
345
+ - **Popover follows the trigger while a parent overlay animates.**
346
+ Opening a popover while the drawer it's inside was mid-slide-in
347
+ used to anchor against the still-moving rect. The positioner now
348
+ re-runs each frame until the anchor stabilizes (~600ms cap).
349
+ - **Non-modal drawer trigger inside another overlay no longer
350
+ dismisses the parent.** The link helper writes
351
+ `data-turbo-overlay-backdrop="false"` on triggers to signal the
352
+ fetch hook; the bootstrap5 modal chrome's wrapper marker shared
353
+ that exact attribute name, so a bubbled click on the trigger was
354
+ treated as a backdrop click. Chrome marker renamed to
355
+ `data-turbo-overlay-backdrop-zone`.
356
+
357
+ ## 0.3.0
358
+
359
+ **Stacking support.** Overlays now stack: open a modal/drawer from inside another and the new one slides on top instead of replacing. Dismissing affects only the topmost overlay; the layer beneath is revealed. This is a breaking internal change — public helper *signatures* are preserved but the underlying transport, layouts, and Stimulus controllers were rewritten.
360
+
361
+ ### Added
362
+ - Stacked overlays. Native `<dialog>.showModal()` stacks via the browser top layer; Bootstrap 5/3 themes manually bump z-index per stack depth.
363
+ - `turbo_stream.overlay(:close, scope: :all)` closes every open overlay; `turbo_stream.overlay(:close, id: "...")` targets one by id; `turbo_stream.overlay(:close, scope: :all, type: :modal)` closes all modals only. Default `:close` (no args) still closes the topmost.
364
+ - `modal_link_to` / `drawer_link_to` accept `overlay_id:` to set a stable id for later targeting from server code.
365
+ - `current_overlay_id` controller method (and view helper) — returns the id of the overlay being rendered, useful for `turbo_stream.overlay(:close, id: current_overlay_id)`.
366
+ - `overlay_stack_tag` view helper — emits the single host-page stack container.
367
+ - `config.stack_id` (default `"turbo_overlay_stack"`).
368
+
369
+ ### Changed
370
+ - **Transport switched from turbo-frame replacement to turbo-stream append.** Each opened overlay is appended to the host-page stack container and wrapped in its own `<turbo-frame id="turbo_overlay_<type>_<id>">` for in-place form re-rendering.
371
+ - Variant detection now reads the `X-Turbo-Overlay` request header (set by the link helper's JS hook) instead of `Turbo-Frame`.
372
+ - Modal and drawer Stimulus controllers unified into one `turbo-overlay` controller per theme. `turbo-modal` / `turbo-drawer` identifiers no longer used; both are now `turbo-overlay`.
373
+ - `aria-labelledby` ids in shipped layouts now include the overlay id so stacked dialogs don't collide.
374
+ - Stream API: `turbo_stream.overlay(:close)` now means "close the top overlay." Previously it closed every overlay; in single-overlay setups this is observationally identical.
375
+
376
+ ### Removed
377
+ - The dual-frame (`turbo_modal` + `turbo_drawer`) install model. `overlay_frame_tags` is retained as a deprecated alias for `overlay_stack_tag` for one minor cycle.
378
+ - Per-type Stimulus controllers (`turbo_modal_controller.js`, `turbo_drawer_controller.js`).
379
+
380
+ ### Requires
381
+ - Turbo 8+ (turbo-rails 2+) for `data-turbo-stream="true"` GET request support.
382
+
383
+ ### Migration from 0.2.x
384
+ 1. Re-run the install generator with `--force` to overwrite the layout files and JS controllers:
385
+ ```sh
386
+ bin/rails g turbo_overlay:install --theme <your-theme> --force
387
+ ```
388
+ 2. In your application layout, replace `<%= overlay_frame_tags %>` with `<%= overlay_stack_tag %>`. (The old helper still works but warns.)
389
+ 3. Remove the old per-type Stimulus controller files: `app/javascript/controllers/turbo_modal_controller.js` and `turbo_drawer_controller.js`.
390
+ 4. If you customized the modal/drawer layouts, port your changes to the new layout primitives — note the wrapping helper changed from `turbo_frame_tag` to `overlay_response_wrapper(:modal|:drawer)`, the Stimulus identifier changed from `turbo-modal` / `turbo-drawer` to `turbo-overlay`, and `aria-labelledby` ids include `<%= current_overlay_id %>`.
391
+ 5. If you have controllers that explicitly responded with `turbo_stream.overlay(:close)` after a successful action — no change needed; it now closes the top overlay (same observed behavior in non-stacked apps).
392
+
393
+ ## 0.2.0
394
+
395
+ ### Added
396
+ - **Drawer support.** Side-anchored overlay (`:left`, `:right`,
397
+ `:top`, `:bottom`) that mirrors the modal pattern with its own
398
+ frame, request variant, layout, and Stimulus controller.
399
+ - `config.drawer` configuration block. New default: drawer frame
400
+ `turbo_drawer`, variant `:drawer`, layout `turbo_drawer`, position
401
+ `:right`.
402
+ - Controller helpers: `drawer_request?`, `drawer_frame_id`,
403
+ `drawer_layout_name`, plus a generic `overlay_request?` that
404
+ returns true for any open overlay.
405
+ - View helpers: `drawer_link_to`, `drawer_dismiss_link_to`.
406
+ - `overlay_frame_tags` view helper that emits the receiving
407
+ turbo-frames for all configured overlay types in one call. Drop it
408
+ into the application layout once.
409
+ - Drawer themes ship for tailwind, bootstrap5, and plain. Bootstrap 3
410
+ is intentionally not shipped (no native drawer/offcanvas
411
+ primitive); the generator auto-skips drawer install for that theme.
412
+
413
+ ### Changed
414
+ - **Install generator unified.** `bin/rails g turbo_overlay:install`
415
+ now installs both modal and drawer by default. Pass `--skip-modal`
416
+ or `--skip-drawer` to install only one. `install_drawer` removed —
417
+ one entry point handles every combination.
418
+ - Initializer template now includes both `config.modal` and
419
+ `config.drawer` blocks.
420
+ - Generator injects `<%= overlay_frame_tags %>` instead of an
421
+ explicit `turbo_frame_tag`. Both forms are still detected so
422
+ re-runs are idempotent.
423
+
424
+ ## 0.1.0
425
+
426
+ ### Added
427
+ - Initial release.
428
+ - Controller concern that detects overlay-frame requests, sets a
429
+ `request.variant`, and exposes `modal_request?` / `modal_layout_name`.
430
+ - View helpers `modal_link_to`, `modal_dismiss_link_to`, and generic
431
+ `overlay_title` / `overlay_footer` content helpers.
432
+ - Stream helper for `turbo_stream.overlay(:close)`. Polymorphic —
433
+ closes any open overlay regardless of type.
434
+ - Install generator with four shipped themes for modals: `tailwind`,
435
+ `bootstrap5`, `bootstrap3`, `plain`. Auto-injects the modal frame
436
+ and Stimulus controller wiring.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joel Schneider
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.