modal_stack 0.3.0 → 0.4.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +113 -32
- data/app/assets/javascripts/modal_stack.js +488 -50
- data/app/assets/stylesheets/modal_stack/bootstrap.css +113 -3
- data/app/assets/stylesheets/modal_stack/tailwind_v3.css +63 -2
- data/app/assets/stylesheets/modal_stack/tailwind_v4.css +63 -2
- data/app/assets/stylesheets/modal_stack/vanilla.css +121 -3
- data/app/javascript/modal_stack/controllers/modal_stack_back_link_controller.js +32 -0
- data/app/javascript/modal_stack/controllers/modal_stack_controller.js +50 -0
- data/app/javascript/modal_stack/install.js +7 -1
- data/app/javascript/modal_stack/orchestrator.js +53 -7
- data/app/javascript/modal_stack/orchestrator.test.js +96 -0
- data/app/javascript/modal_stack/runtime.js +167 -5
- data/app/javascript/modal_stack/runtime.test.js +83 -0
- data/app/javascript/modal_stack/state.js +319 -34
- data/app/javascript/modal_stack/state.test.js +394 -9
- data/app/views/modal_stack/_dialog.html.erb +1 -0
- data/app/views/modal_stack/_panel.html.erb +4 -0
- data/lib/generators/modal_stack/install/templates/initializer.rb +9 -0
- data/lib/generators/modal_stack/views/templates/_dialog.html.erb +9 -0
- data/lib/generators/modal_stack/views/templates/_panel.html.erb +18 -0
- data/lib/generators/modal_stack/views/views_generator.rb +50 -0
- data/lib/modal_stack/capybara.rb +21 -0
- data/lib/modal_stack/configuration.rb +37 -16
- data/lib/modal_stack/engine.rb +2 -0
- data/lib/modal_stack/helpers/modal_back_link_helper.rb +48 -0
- data/lib/modal_stack/helpers/modal_stack_assets_helper.rb +1 -1
- data/lib/modal_stack/helpers/modal_stack_container_helper.rb +42 -15
- data/lib/modal_stack/turbo_streams_extension.rb +56 -0
- data/lib/modal_stack/version.rb +1 -1
- data/lib/modal_stack.rb +5 -1
- metadata +9 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* :root { --modal-stack-radius: 4px; }
|
|
11
11
|
*
|
|
12
12
|
* Variants: [data-variant="modal" | "drawer" | "bottom_sheet" | "confirmation"]
|
|
13
|
-
* Drawer side: [data-side="left" | "right"]
|
|
13
|
+
* Drawer side: [data-side="left" | "right" | "top" | "bottom"]
|
|
14
14
|
* Sizes: [data-modal-stack-size="sm" | "md" | "lg" | "xl"]
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
--modal-stack-size-lg: 800px;
|
|
33
33
|
--modal-stack-size-xl: 1140px;
|
|
34
34
|
--modal-stack-drawer-width: 400px;
|
|
35
|
+
--modal-stack-drawer-height: 22rem; /* 352px */
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
body[data-modal-stack-locked] {
|
|
@@ -94,6 +95,7 @@ body[data-modal-stack-locked] {
|
|
|
94
95
|
max-width: 92vw;
|
|
95
96
|
max-height: min(85vh, 720px);
|
|
96
97
|
overflow-y: auto;
|
|
98
|
+
overscroll-behavior: contain;
|
|
97
99
|
background: var(--modal-stack-bg);
|
|
98
100
|
color: var(--modal-stack-fg);
|
|
99
101
|
border-radius: var(--modal-stack-radius);
|
|
@@ -114,7 +116,7 @@ body[data-modal-stack-locked] {
|
|
|
114
116
|
|
|
115
117
|
[data-modal-stack-target="layer"][data-leaving] {
|
|
116
118
|
opacity: 0;
|
|
117
|
-
transform: translate(-50%, -
|
|
119
|
+
transform: translate(-50%, -44%) scale(0.97);
|
|
118
120
|
pointer-events: none;
|
|
119
121
|
}
|
|
120
122
|
|
|
@@ -142,7 +144,14 @@ body[data-modal-stack-locked] {
|
|
|
142
144
|
/* --- Drawer (Bootstrap offcanvas-flavored) ----------------------- */
|
|
143
145
|
|
|
144
146
|
[data-modal-stack-target="layer"][data-variant="drawer"] {
|
|
147
|
+
left: 0;
|
|
145
148
|
top: 0;
|
|
149
|
+
max-width: 100vw;
|
|
150
|
+
max-height: 100vh;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="right"],
|
|
154
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="left"] {
|
|
146
155
|
height: 100vh;
|
|
147
156
|
max-height: none;
|
|
148
157
|
width: var(--modal-stack-drawer-width);
|
|
@@ -162,6 +171,30 @@ body[data-modal-stack-locked] {
|
|
|
162
171
|
border-radius: 0 var(--modal-stack-radius) var(--modal-stack-radius) 0;
|
|
163
172
|
}
|
|
164
173
|
|
|
174
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="top"] {
|
|
175
|
+
left: 0;
|
|
176
|
+
top: 0;
|
|
177
|
+
width: 100vw;
|
|
178
|
+
max-width: none;
|
|
179
|
+
height: var(--modal-stack-drawer-height);
|
|
180
|
+
max-height: 85vh;
|
|
181
|
+
transform: translateY(0);
|
|
182
|
+
border-radius: 0 0 var(--modal-stack-radius) var(--modal-stack-radius);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="bottom"] {
|
|
186
|
+
left: 0;
|
|
187
|
+
top: auto;
|
|
188
|
+
bottom: 0;
|
|
189
|
+
width: 100vw;
|
|
190
|
+
max-width: none;
|
|
191
|
+
height: var(--modal-stack-drawer-height);
|
|
192
|
+
max-height: 85vh;
|
|
193
|
+
transform: translateY(0);
|
|
194
|
+
border-radius: var(--modal-stack-radius) var(--modal-stack-radius) 0 0;
|
|
195
|
+
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
196
|
+
}
|
|
197
|
+
|
|
165
198
|
@starting-style {
|
|
166
199
|
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="right"] {
|
|
167
200
|
opacity: 0;
|
|
@@ -171,6 +204,14 @@ body[data-modal-stack-locked] {
|
|
|
171
204
|
opacity: 0;
|
|
172
205
|
transform: translateX(-100%);
|
|
173
206
|
}
|
|
207
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="top"] {
|
|
208
|
+
opacity: 0;
|
|
209
|
+
transform: translateY(-100%);
|
|
210
|
+
}
|
|
211
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="bottom"] {
|
|
212
|
+
opacity: 0;
|
|
213
|
+
transform: translateY(100%);
|
|
214
|
+
}
|
|
174
215
|
}
|
|
175
216
|
|
|
176
217
|
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="right"][data-leaving] {
|
|
@@ -183,6 +224,16 @@ body[data-modal-stack-locked] {
|
|
|
183
224
|
transform: translateX(-100%);
|
|
184
225
|
}
|
|
185
226
|
|
|
227
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="top"][data-leaving] {
|
|
228
|
+
opacity: 0;
|
|
229
|
+
transform: translateY(-100%);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="bottom"][data-leaving] {
|
|
233
|
+
opacity: 0;
|
|
234
|
+
transform: translateY(100%);
|
|
235
|
+
}
|
|
236
|
+
|
|
186
237
|
/* --- Bottom sheet ------------------------------------------------ */
|
|
187
238
|
|
|
188
239
|
[data-modal-stack-target="layer"][data-variant="bottom_sheet"] {
|
|
@@ -194,6 +245,7 @@ body[data-modal-stack-locked] {
|
|
|
194
245
|
transform: translate(0, 0);
|
|
195
246
|
border-radius: var(--modal-stack-radius) var(--modal-stack-radius) 0 0;
|
|
196
247
|
max-height: 90vh;
|
|
248
|
+
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
197
249
|
}
|
|
198
250
|
|
|
199
251
|
@starting-style {
|
|
@@ -222,10 +274,68 @@ body[data-modal-stack-locked] {
|
|
|
222
274
|
gap: 0.5rem;
|
|
223
275
|
}
|
|
224
276
|
|
|
277
|
+
/* Path frames are transparent layout wrappers — CSS transitions can hook
|
|
278
|
+
on [data-modal-stack-frame][data-transition][data-direction] to
|
|
279
|
+
animate slide/fade. */
|
|
280
|
+
[data-modal-stack-frame] {
|
|
281
|
+
display: contents;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
[data-modal-stack-target="layer"]:has([data-modal-stack-frame][data-transition="slide"]),
|
|
285
|
+
[data-modal-stack-target="layer"]:has([data-modal-stack-frame][data-transition="fade"]) {
|
|
286
|
+
overflow: hidden;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
[data-modal-stack-frame][data-transition="slide"] {
|
|
290
|
+
display: block;
|
|
291
|
+
translate: 0 0;
|
|
292
|
+
transition: translate var(--modal-stack-duration) var(--modal-stack-ease);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
[data-modal-stack-frame][data-transition="fade"] {
|
|
296
|
+
display: block;
|
|
297
|
+
opacity: 1;
|
|
298
|
+
transition: opacity var(--modal-stack-duration) var(--modal-stack-ease);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@starting-style {
|
|
302
|
+
[data-modal-stack-frame][data-transition="slide"][data-direction="forward"] {
|
|
303
|
+
translate: 100% 0;
|
|
304
|
+
}
|
|
305
|
+
[data-modal-stack-frame][data-transition="slide"][data-direction="back"] {
|
|
306
|
+
translate: -100% 0;
|
|
307
|
+
}
|
|
308
|
+
[data-modal-stack-frame][data-transition="fade"] {
|
|
309
|
+
opacity: 0;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.modal-stack__panel-back {
|
|
314
|
+
font: inherit;
|
|
315
|
+
color: inherit;
|
|
316
|
+
background: transparent;
|
|
317
|
+
border: 0;
|
|
318
|
+
padding: 0;
|
|
319
|
+
cursor: pointer;
|
|
320
|
+
align-self: start;
|
|
321
|
+
justify-self: start;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.modal-stack__panel-back:focus-visible {
|
|
325
|
+
outline: 2px solid currentColor;
|
|
326
|
+
outline-offset: 4px;
|
|
327
|
+
border-radius: 2px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
[data-modal-stack-target="layer"][data-frame-depth="1"] .modal-stack__panel-back {
|
|
331
|
+
display: none;
|
|
332
|
+
}
|
|
333
|
+
|
|
225
334
|
@media (prefers-reduced-motion: reduce) {
|
|
226
335
|
#modal-stack-root,
|
|
227
336
|
#modal-stack-root::backdrop,
|
|
228
|
-
[data-modal-stack-target="layer"]
|
|
337
|
+
[data-modal-stack-target="layer"],
|
|
338
|
+
[data-modal-stack-frame][data-transition] {
|
|
229
339
|
transition-duration: 1ms !important;
|
|
230
340
|
}
|
|
231
341
|
}
|
|
@@ -118,6 +118,7 @@ body[data-modal-stack-locked] {
|
|
|
118
118
|
max-width: 92vw;
|
|
119
119
|
max-height: min(85vh, 720px);
|
|
120
120
|
overflow-y: auto;
|
|
121
|
+
overscroll-behavior: contain;
|
|
121
122
|
background: var(--modal-stack-bg);
|
|
122
123
|
color: var(--modal-stack-fg);
|
|
123
124
|
border-radius: var(--modal-stack-radius);
|
|
@@ -138,7 +139,7 @@ body[data-modal-stack-locked] {
|
|
|
138
139
|
|
|
139
140
|
[data-modal-stack-target="layer"][data-leaving] {
|
|
140
141
|
opacity: 0;
|
|
141
|
-
transform: translate(-50%, -
|
|
142
|
+
transform: translate(-50%, -44%) scale(0.97);
|
|
142
143
|
pointer-events: none;
|
|
143
144
|
}
|
|
144
145
|
|
|
@@ -219,6 +220,7 @@ body[data-modal-stack-locked] {
|
|
|
219
220
|
max-height: 85vh;
|
|
220
221
|
transform: translateY(0);
|
|
221
222
|
border-radius: var(--modal-stack-radius) var(--modal-stack-radius) 0 0;
|
|
223
|
+
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
@starting-style {
|
|
@@ -271,6 +273,7 @@ body[data-modal-stack-locked] {
|
|
|
271
273
|
transform: translate(0, 0);
|
|
272
274
|
border-radius: var(--modal-stack-radius) var(--modal-stack-radius) 0 0;
|
|
273
275
|
max-height: 90vh;
|
|
276
|
+
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
274
277
|
}
|
|
275
278
|
|
|
276
279
|
@starting-style {
|
|
@@ -299,12 +302,70 @@ body[data-modal-stack-locked] {
|
|
|
299
302
|
gap: 14px;
|
|
300
303
|
}
|
|
301
304
|
|
|
305
|
+
/* Path frames are transparent layout wrappers — CSS transitions can hook
|
|
306
|
+
on [data-modal-stack-frame][data-transition][data-direction] to
|
|
307
|
+
animate slide/fade. */
|
|
308
|
+
[data-modal-stack-frame] {
|
|
309
|
+
display: contents;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
[data-modal-stack-target="layer"]:has([data-modal-stack-frame][data-transition="slide"]),
|
|
313
|
+
[data-modal-stack-target="layer"]:has([data-modal-stack-frame][data-transition="fade"]) {
|
|
314
|
+
overflow: hidden;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
[data-modal-stack-frame][data-transition="slide"] {
|
|
318
|
+
display: block;
|
|
319
|
+
translate: 0 0;
|
|
320
|
+
transition: translate var(--modal-stack-duration) var(--modal-stack-ease);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
[data-modal-stack-frame][data-transition="fade"] {
|
|
324
|
+
display: block;
|
|
325
|
+
opacity: 1;
|
|
326
|
+
transition: opacity var(--modal-stack-duration) var(--modal-stack-ease);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@starting-style {
|
|
330
|
+
[data-modal-stack-frame][data-transition="slide"][data-direction="forward"] {
|
|
331
|
+
translate: 100% 0;
|
|
332
|
+
}
|
|
333
|
+
[data-modal-stack-frame][data-transition="slide"][data-direction="back"] {
|
|
334
|
+
translate: -100% 0;
|
|
335
|
+
}
|
|
336
|
+
[data-modal-stack-frame][data-transition="fade"] {
|
|
337
|
+
opacity: 0;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.modal-stack__panel-back {
|
|
342
|
+
font: inherit;
|
|
343
|
+
color: inherit;
|
|
344
|
+
background: transparent;
|
|
345
|
+
border: 0;
|
|
346
|
+
padding: 0;
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
align-self: start;
|
|
349
|
+
justify-self: start;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.modal-stack__panel-back:focus-visible {
|
|
353
|
+
outline: 2px solid currentColor;
|
|
354
|
+
outline-offset: 4px;
|
|
355
|
+
border-radius: 2px;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
[data-modal-stack-target="layer"][data-frame-depth="1"] .modal-stack__panel-back {
|
|
359
|
+
display: none;
|
|
360
|
+
}
|
|
361
|
+
|
|
302
362
|
/* --- Reduced motion ---------------------------------------------- */
|
|
303
363
|
|
|
304
364
|
@media (prefers-reduced-motion: reduce) {
|
|
305
365
|
#modal-stack-root,
|
|
306
366
|
#modal-stack-root::backdrop,
|
|
307
|
-
[data-modal-stack-target="layer"]
|
|
367
|
+
[data-modal-stack-target="layer"],
|
|
368
|
+
[data-modal-stack-frame][data-transition] {
|
|
308
369
|
transition-duration: 1ms !important;
|
|
309
370
|
}
|
|
310
371
|
}
|
|
@@ -119,6 +119,7 @@ body[data-modal-stack-locked] {
|
|
|
119
119
|
max-width: 92vw;
|
|
120
120
|
max-height: min(85vh, 720px);
|
|
121
121
|
overflow-y: auto;
|
|
122
|
+
overscroll-behavior: contain;
|
|
122
123
|
background: var(--modal-stack-bg);
|
|
123
124
|
color: var(--modal-stack-fg);
|
|
124
125
|
border-radius: var(--modal-stack-radius);
|
|
@@ -139,7 +140,7 @@ body[data-modal-stack-locked] {
|
|
|
139
140
|
|
|
140
141
|
[data-modal-stack-target="layer"][data-leaving] {
|
|
141
142
|
opacity: 0;
|
|
142
|
-
transform: translate(-50%, -
|
|
143
|
+
transform: translate(-50%, -44%) scale(0.97);
|
|
143
144
|
pointer-events: none;
|
|
144
145
|
}
|
|
145
146
|
|
|
@@ -220,6 +221,7 @@ body[data-modal-stack-locked] {
|
|
|
220
221
|
max-height: 85vh;
|
|
221
222
|
transform: translateY(0);
|
|
222
223
|
border-radius: var(--modal-stack-radius) var(--modal-stack-radius) 0 0;
|
|
224
|
+
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
@starting-style {
|
|
@@ -272,6 +274,7 @@ body[data-modal-stack-locked] {
|
|
|
272
274
|
transform: translate(0, 0);
|
|
273
275
|
border-radius: var(--modal-stack-radius) var(--modal-stack-radius) 0 0;
|
|
274
276
|
max-height: 90vh;
|
|
277
|
+
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
@starting-style {
|
|
@@ -300,12 +303,70 @@ body[data-modal-stack-locked] {
|
|
|
300
303
|
gap: 14px;
|
|
301
304
|
}
|
|
302
305
|
|
|
306
|
+
/* Path frames are transparent layout wrappers — CSS transitions can hook
|
|
307
|
+
on [data-modal-stack-frame][data-transition][data-direction] to
|
|
308
|
+
animate slide/fade. */
|
|
309
|
+
[data-modal-stack-frame] {
|
|
310
|
+
display: contents;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
[data-modal-stack-target="layer"]:has([data-modal-stack-frame][data-transition="slide"]),
|
|
314
|
+
[data-modal-stack-target="layer"]:has([data-modal-stack-frame][data-transition="fade"]) {
|
|
315
|
+
overflow: hidden;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
[data-modal-stack-frame][data-transition="slide"] {
|
|
319
|
+
display: block;
|
|
320
|
+
translate: 0 0;
|
|
321
|
+
transition: translate var(--modal-stack-duration) var(--modal-stack-ease);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
[data-modal-stack-frame][data-transition="fade"] {
|
|
325
|
+
display: block;
|
|
326
|
+
opacity: 1;
|
|
327
|
+
transition: opacity var(--modal-stack-duration) var(--modal-stack-ease);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@starting-style {
|
|
331
|
+
[data-modal-stack-frame][data-transition="slide"][data-direction="forward"] {
|
|
332
|
+
translate: 100% 0;
|
|
333
|
+
}
|
|
334
|
+
[data-modal-stack-frame][data-transition="slide"][data-direction="back"] {
|
|
335
|
+
translate: -100% 0;
|
|
336
|
+
}
|
|
337
|
+
[data-modal-stack-frame][data-transition="fade"] {
|
|
338
|
+
opacity: 0;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.modal-stack__panel-back {
|
|
343
|
+
font: inherit;
|
|
344
|
+
color: inherit;
|
|
345
|
+
background: transparent;
|
|
346
|
+
border: 0;
|
|
347
|
+
padding: 0;
|
|
348
|
+
cursor: pointer;
|
|
349
|
+
align-self: start;
|
|
350
|
+
justify-self: start;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.modal-stack__panel-back:focus-visible {
|
|
354
|
+
outline: 2px solid currentColor;
|
|
355
|
+
outline-offset: 4px;
|
|
356
|
+
border-radius: 2px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
[data-modal-stack-target="layer"][data-frame-depth="1"] .modal-stack__panel-back {
|
|
360
|
+
display: none;
|
|
361
|
+
}
|
|
362
|
+
|
|
303
363
|
/* --- Reduced motion ---------------------------------------------- */
|
|
304
364
|
|
|
305
365
|
@media (prefers-reduced-motion: reduce) {
|
|
306
366
|
#modal-stack-root,
|
|
307
367
|
#modal-stack-root::backdrop,
|
|
308
|
-
[data-modal-stack-target="layer"]
|
|
368
|
+
[data-modal-stack-target="layer"],
|
|
369
|
+
[data-modal-stack-frame][data-transition] {
|
|
309
370
|
transition-duration: 1ms !important;
|
|
310
371
|
}
|
|
311
372
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* --modal-stack-* tokens on :root to retheme.
|
|
6
6
|
*
|
|
7
7
|
* Variants: [data-variant="modal" | "drawer" | "bottom_sheet" | "confirmation"]
|
|
8
|
-
* Drawer side: [data-side="left" | "right"]
|
|
8
|
+
* Drawer side: [data-side="left" | "right" | "top" | "bottom"]
|
|
9
9
|
* Sizes: [data-modal-stack-size="sm" | "md" | "lg" | "xl"]
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
--modal-stack-size-lg: 720px;
|
|
28
28
|
--modal-stack-size-xl: 960px;
|
|
29
29
|
--modal-stack-drawer-width: 400px;
|
|
30
|
+
--modal-stack-drawer-height: 22rem; /* 352px */
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
body[data-modal-stack-locked] {
|
|
@@ -89,6 +90,7 @@ body[data-modal-stack-locked] {
|
|
|
89
90
|
max-width: 92vw;
|
|
90
91
|
max-height: min(85vh, 720px);
|
|
91
92
|
overflow-y: auto;
|
|
93
|
+
overscroll-behavior: contain;
|
|
92
94
|
background: var(--modal-stack-bg);
|
|
93
95
|
color: var(--modal-stack-fg);
|
|
94
96
|
border-radius: var(--modal-stack-radius);
|
|
@@ -109,7 +111,7 @@ body[data-modal-stack-locked] {
|
|
|
109
111
|
|
|
110
112
|
[data-modal-stack-target="layer"][data-leaving] {
|
|
111
113
|
opacity: 0;
|
|
112
|
-
transform: translate(-50%, -
|
|
114
|
+
transform: translate(-50%, -44%) scale(0.97);
|
|
113
115
|
pointer-events: none;
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -135,7 +137,14 @@ body[data-modal-stack-locked] {
|
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
[data-modal-stack-target="layer"][data-variant="drawer"] {
|
|
140
|
+
left: 0;
|
|
138
141
|
top: 0;
|
|
142
|
+
max-width: 100vw;
|
|
143
|
+
max-height: 100vh;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="right"],
|
|
147
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="left"] {
|
|
139
148
|
height: 100vh;
|
|
140
149
|
max-height: none;
|
|
141
150
|
width: var(--modal-stack-drawer-width);
|
|
@@ -155,6 +164,30 @@ body[data-modal-stack-locked] {
|
|
|
155
164
|
border-radius: 0 var(--modal-stack-radius) var(--modal-stack-radius) 0;
|
|
156
165
|
}
|
|
157
166
|
|
|
167
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="top"] {
|
|
168
|
+
left: 0;
|
|
169
|
+
top: 0;
|
|
170
|
+
width: 100vw;
|
|
171
|
+
max-width: none;
|
|
172
|
+
height: var(--modal-stack-drawer-height);
|
|
173
|
+
max-height: 85vh;
|
|
174
|
+
transform: translateY(0);
|
|
175
|
+
border-radius: 0 0 var(--modal-stack-radius) var(--modal-stack-radius);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="bottom"] {
|
|
179
|
+
left: 0;
|
|
180
|
+
top: auto;
|
|
181
|
+
bottom: 0;
|
|
182
|
+
width: 100vw;
|
|
183
|
+
max-width: none;
|
|
184
|
+
height: var(--modal-stack-drawer-height);
|
|
185
|
+
max-height: 85vh;
|
|
186
|
+
transform: translateY(0);
|
|
187
|
+
border-radius: var(--modal-stack-radius) var(--modal-stack-radius) 0 0;
|
|
188
|
+
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
158
191
|
@starting-style {
|
|
159
192
|
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="right"] {
|
|
160
193
|
opacity: 0;
|
|
@@ -164,6 +197,14 @@ body[data-modal-stack-locked] {
|
|
|
164
197
|
opacity: 0;
|
|
165
198
|
transform: translateX(-100%);
|
|
166
199
|
}
|
|
200
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="top"] {
|
|
201
|
+
opacity: 0;
|
|
202
|
+
transform: translateY(-100%);
|
|
203
|
+
}
|
|
204
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="bottom"] {
|
|
205
|
+
opacity: 0;
|
|
206
|
+
transform: translateY(100%);
|
|
207
|
+
}
|
|
167
208
|
}
|
|
168
209
|
|
|
169
210
|
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="right"][data-leaving] {
|
|
@@ -176,6 +217,16 @@ body[data-modal-stack-locked] {
|
|
|
176
217
|
transform: translateX(-100%);
|
|
177
218
|
}
|
|
178
219
|
|
|
220
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="top"][data-leaving] {
|
|
221
|
+
opacity: 0;
|
|
222
|
+
transform: translateY(-100%);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="bottom"][data-leaving] {
|
|
226
|
+
opacity: 0;
|
|
227
|
+
transform: translateY(100%);
|
|
228
|
+
}
|
|
229
|
+
|
|
179
230
|
[data-modal-stack-target="layer"][data-variant="bottom_sheet"] {
|
|
180
231
|
top: auto;
|
|
181
232
|
bottom: 0;
|
|
@@ -185,6 +236,7 @@ body[data-modal-stack-locked] {
|
|
|
185
236
|
transform: translate(0, 0);
|
|
186
237
|
border-radius: var(--modal-stack-radius) var(--modal-stack-radius) 0 0;
|
|
187
238
|
max-height: 90vh;
|
|
239
|
+
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
188
240
|
}
|
|
189
241
|
|
|
190
242
|
@starting-style {
|
|
@@ -209,10 +261,76 @@ body[data-modal-stack-locked] {
|
|
|
209
261
|
gap: 12px;
|
|
210
262
|
}
|
|
211
263
|
|
|
264
|
+
/* Path frames live inside the layer as transparent wrappers. Switching
|
|
265
|
+
to `display: contents` keeps the wrapper layout-invisible so adding
|
|
266
|
+
the modal_path feature doesn't change visuals for layers that never
|
|
267
|
+
navigate forward. Frame-level CSS transitions (slide / fade) can hook
|
|
268
|
+
on [data-modal-stack-frame][data-transition][data-direction]. */
|
|
269
|
+
[data-modal-stack-frame] {
|
|
270
|
+
display: contents;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Frame transitions — the JS runtime sets [data-transition] + [data-direction]
|
|
274
|
+
on new frame wrappers and removes them after the animation. While the
|
|
275
|
+
attribute is present, the frame is promoted from display:contents to a
|
|
276
|
+
block context so translate/opacity apply. The :has() rule clips the layer
|
|
277
|
+
so off-screen frames don't overflow the panel area. */
|
|
278
|
+
|
|
279
|
+
[data-modal-stack-target="layer"]:has([data-modal-stack-frame][data-transition="slide"]),
|
|
280
|
+
[data-modal-stack-target="layer"]:has([data-modal-stack-frame][data-transition="fade"]) {
|
|
281
|
+
overflow: hidden;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
[data-modal-stack-frame][data-transition="slide"] {
|
|
285
|
+
display: block;
|
|
286
|
+
translate: 0 0;
|
|
287
|
+
transition: translate var(--modal-stack-duration) var(--modal-stack-ease);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
[data-modal-stack-frame][data-transition="fade"] {
|
|
291
|
+
display: block;
|
|
292
|
+
opacity: 1;
|
|
293
|
+
transition: opacity var(--modal-stack-duration) var(--modal-stack-ease);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@starting-style {
|
|
297
|
+
[data-modal-stack-frame][data-transition="slide"][data-direction="forward"] {
|
|
298
|
+
translate: 100% 0;
|
|
299
|
+
}
|
|
300
|
+
[data-modal-stack-frame][data-transition="slide"][data-direction="back"] {
|
|
301
|
+
translate: -100% 0;
|
|
302
|
+
}
|
|
303
|
+
[data-modal-stack-frame][data-transition="fade"] {
|
|
304
|
+
opacity: 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.modal-stack__panel-back {
|
|
309
|
+
font: inherit;
|
|
310
|
+
color: inherit;
|
|
311
|
+
background: transparent;
|
|
312
|
+
border: 0;
|
|
313
|
+
padding: 0;
|
|
314
|
+
cursor: pointer;
|
|
315
|
+
align-self: start;
|
|
316
|
+
justify-self: start;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.modal-stack__panel-back:focus-visible {
|
|
320
|
+
outline: 2px solid currentColor;
|
|
321
|
+
outline-offset: 4px;
|
|
322
|
+
border-radius: 2px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
[data-modal-stack-target="layer"][data-frame-depth="1"] .modal-stack__panel-back {
|
|
326
|
+
display: none;
|
|
327
|
+
}
|
|
328
|
+
|
|
212
329
|
@media (prefers-reduced-motion: reduce) {
|
|
213
330
|
#modal-stack-root,
|
|
214
331
|
#modal-stack-root::backdrop,
|
|
215
|
-
[data-modal-stack-target="layer"]
|
|
332
|
+
[data-modal-stack-target="layer"],
|
|
333
|
+
[data-modal-stack-frame][data-transition] {
|
|
216
334
|
transition-duration: 1ms !important;
|
|
217
335
|
}
|
|
218
336
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// Backed by `<%= modal_back_link %>`. Triggers orchestrator.pathBack on
|
|
4
|
+
// click; falls back to a no-op when no modal-stack controller is on the
|
|
5
|
+
// page (e.g. server-side rendered link viewed outside a modal).
|
|
6
|
+
export class ModalStackBackLinkController extends Controller {
|
|
7
|
+
static values = {
|
|
8
|
+
steps: { type: Number, default: 1 },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
trigger(event) {
|
|
12
|
+
const stackController = this.#stackController();
|
|
13
|
+
if (!stackController) return;
|
|
14
|
+
|
|
15
|
+
if (event) {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
event.stopPropagation();
|
|
18
|
+
}
|
|
19
|
+
stackController.orchestrator.pathBack({
|
|
20
|
+
steps: this.stepsValue > 0 ? this.stepsValue : 1,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#stackController() {
|
|
25
|
+
const stack = document.querySelector('[data-controller~="modal-stack"]');
|
|
26
|
+
if (!stack) return null;
|
|
27
|
+
return this.application.getControllerForElementAndIdentifier(
|
|
28
|
+
stack,
|
|
29
|
+
"modal-stack",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -83,6 +83,17 @@ export class ModalStackController extends Controller {
|
|
|
83
83
|
return this.orchestrator.prefetch(url);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Stimulus action — wire up via data-action="click->modal-stack#pathBack"
|
|
87
|
+
// on any button/link inside a modal panel.
|
|
88
|
+
pathBack(event) {
|
|
89
|
+
if (event) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
event.stopPropagation();
|
|
92
|
+
}
|
|
93
|
+
const steps = readSteps(event);
|
|
94
|
+
return this.orchestrator.pathBack({ steps });
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
#topLayer() {
|
|
87
98
|
const layers = this.orchestrator.layers;
|
|
88
99
|
return layers[layers.length - 1] ?? null;
|
|
@@ -136,6 +147,21 @@ export class ModalStackController extends Controller {
|
|
|
136
147
|
StreamActions.modal_close_all = guarded("modal_close_all", function (orch) {
|
|
137
148
|
return orch.closeAll();
|
|
138
149
|
});
|
|
150
|
+
|
|
151
|
+
StreamActions.modal_path_to = guarded("modal_path_to", function (orch) {
|
|
152
|
+
return orch.pathTo(frameFromStreamElement(this), {
|
|
153
|
+
fragment: this.templateContent.cloneNode(true),
|
|
154
|
+
transition: this.dataset.transition || null,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
StreamActions.modal_path_back = guarded("modal_path_back", function (orch) {
|
|
159
|
+
const steps = parsePositiveInt(this.dataset.steps, 1);
|
|
160
|
+
return orch.pathBack({
|
|
161
|
+
steps,
|
|
162
|
+
transition: this.dataset.transition || null,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
139
165
|
}
|
|
140
166
|
}
|
|
141
167
|
|
|
@@ -186,3 +212,27 @@ function generateLayerId() {
|
|
|
186
212
|
}
|
|
187
213
|
return `ms-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
188
214
|
}
|
|
215
|
+
|
|
216
|
+
function frameFromStreamElement(el) {
|
|
217
|
+
return {
|
|
218
|
+
url: el.dataset.url || window.location.href,
|
|
219
|
+
stale: el.dataset.stale === "true" || el.dataset.stale === "1",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parsePositiveInt(raw, fallback) {
|
|
224
|
+
const n = Number.parseInt(raw, 10);
|
|
225
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Steps for pathBack come from either Stimulus action params
|
|
229
|
+
// (data-modal-stack-steps-param) or a plain data-steps attribute on
|
|
230
|
+
// the action target, e.g. <button data-modal-stack-steps-param="2">.
|
|
231
|
+
function readSteps(event) {
|
|
232
|
+
const params = event?.params;
|
|
233
|
+
if (params && Number.isFinite(params.steps) && params.steps > 0) {
|
|
234
|
+
return params.steps;
|
|
235
|
+
}
|
|
236
|
+
const target = event?.currentTarget ?? event?.target;
|
|
237
|
+
return parsePositiveInt(target?.dataset?.steps, 1);
|
|
238
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ModalStackBackLinkController } from "./controllers/modal_stack_back_link_controller.js";
|
|
1
2
|
import { ModalStackController } from "./controllers/modal_stack_controller.js";
|
|
2
3
|
import { ModalStackLinkController } from "./controllers/modal_stack_link_controller.js";
|
|
3
4
|
|
|
@@ -9,7 +10,12 @@ export function install(application) {
|
|
|
9
10
|
}
|
|
10
11
|
application.register("modal-stack", ModalStackController);
|
|
11
12
|
application.register("modal-stack-link", ModalStackLinkController);
|
|
13
|
+
application.register("modal-stack-back-link", ModalStackBackLinkController);
|
|
12
14
|
return application;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
ModalStackBackLinkController,
|
|
19
|
+
ModalStackController,
|
|
20
|
+
ModalStackLinkController,
|
|
21
|
+
};
|