ultimate_turbo_modal 3.2.1 → 3.4.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.
@@ -0,0 +1,610 @@
1
+ /* ========================================
2
+ All styles scoped to dialog.utmr to
3
+ prevent conflicts with end-user CSS.
4
+ ======================================== */
5
+
6
+ dialog.utmr {
7
+ .hidden {
8
+ display: none;
9
+ }
10
+
11
+ .sr-only {
12
+ position: absolute;
13
+ width: 1px;
14
+ height: 1px;
15
+ padding: 0;
16
+ margin: -1px;
17
+ overflow: hidden;
18
+ clip: rect(0, 0, 0, 0);
19
+ white-space: nowrap;
20
+ border-width: 0;
21
+ }
22
+
23
+ /* ========================================
24
+ Dialog reset (shared modal + drawer)
25
+ ======================================== */
26
+
27
+ &.modal-container,
28
+ &.drawer-container {
29
+ position: fixed;
30
+ inset: 0;
31
+ padding: 0;
32
+ margin: 0;
33
+ border: none;
34
+ background: transparent;
35
+ max-width: 100vw;
36
+ max-height: 100dvh;
37
+ width: 100%;
38
+ height: 100%;
39
+ overflow-y: auto;
40
+ }
41
+
42
+ /* ========================================
43
+ Modal styles
44
+ ======================================== */
45
+
46
+ /* Backdrop transitions */
47
+ &.modal-container::backdrop {
48
+ opacity: 0;
49
+ transition: opacity 300ms ease-out;
50
+ }
51
+ &.modal-container[data-overlay="true"]::backdrop {
52
+ background-color: rgba(17, 24, 39, 0.7);
53
+ }
54
+ &.modal-container[data-overlay="false"]::backdrop {
55
+ background-color: transparent;
56
+ }
57
+ &.modal-container[data-overlay="true"][data-entered]::backdrop {
58
+ opacity: 1;
59
+ }
60
+ &.modal-container[data-closing]::backdrop {
61
+ transition-duration: 200ms;
62
+ transition-timing-function: ease-in;
63
+ }
64
+
65
+ /* Modal enter/leave transitions */
66
+ &.modal-container:not([data-enter-ready]):not([data-entered]) .modal-inner {
67
+ visibility: hidden;
68
+ }
69
+
70
+ .modal-inner {
71
+ display: flex;
72
+ padding: 0.25rem;
73
+ padding-top: 10vh;
74
+ justify-content: center;
75
+ align-items: flex-start;
76
+ min-height: 100%;
77
+ opacity: 0;
78
+ translate: 0 1rem;
79
+ transition: opacity 300ms ease-out, translate 300ms ease-out, scale 300ms ease-out;
80
+
81
+ @media (min-width: 640px) {
82
+ padding: 1rem;
83
+ padding-bottom: 10vh;
84
+ align-items: center;
85
+ translate: none;
86
+ scale: 0.95;
87
+ }
88
+ }
89
+
90
+ &.modal-container[data-entered] .modal-inner {
91
+ opacity: 1;
92
+ translate: none;
93
+ scale: 1;
94
+ }
95
+
96
+ &.modal-container[data-closing] .modal-inner {
97
+ transition-duration: 200ms;
98
+ transition-timing-function: ease-in;
99
+ }
100
+
101
+ /* Modal conditional styling via data attributes */
102
+ &.modal-container {
103
+ &[data-header="false"] .modal-header {
104
+ position: absolute;
105
+ }
106
+
107
+ &[data-header-divider="true"] .modal-header {
108
+ border-bottom-width: 1px;
109
+ }
110
+
111
+ &[data-footer-divider="true"] .modal-footer {
112
+ border-top-width: 1px;
113
+ }
114
+
115
+ &[data-padding="true"] .modal-main {
116
+ padding: 1rem;
117
+ padding-top: 0.5rem;
118
+ }
119
+
120
+ .modal-main {
121
+ overflow-y: auto;
122
+ max-height: 75vh;
123
+ }
124
+
125
+ &[data-title="false"] .modal-title-h {
126
+ display: none;
127
+ }
128
+
129
+ &[data-close-button="false"] .modal-close {
130
+ display: none;
131
+ }
132
+ }
133
+
134
+ .modal-content {
135
+ overflow: hidden;
136
+ position: relative;
137
+ background-color: #ffffff;
138
+ transition-property: all;
139
+ text-align: left;
140
+ border-radius: 0.5rem;
141
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
142
+ max-height: 100vh;
143
+
144
+ @media (min-width: 640px) {
145
+ max-width: 48rem;
146
+ }
147
+ }
148
+
149
+ .modal-header {
150
+ display: flex;
151
+ padding-top: 1rem;
152
+ padding-bottom: 1rem;
153
+ justify-content: space-between;
154
+ align-items: center;
155
+ width: 100%;
156
+ border-top-left-radius: 0.25rem;
157
+ border-top-right-radius: 0.25rem;
158
+ }
159
+
160
+ .modal-title {
161
+ line-height: 1.75rem;
162
+ font-weight: 600;
163
+ padding-left: 1rem;
164
+ }
165
+
166
+ .modal-footer {
167
+ display: flex;
168
+ padding: 1rem;
169
+ border-bottom-right-radius: 0.25rem;
170
+ border-bottom-left-radius: 0.25rem;
171
+ }
172
+
173
+ .modal-close {
174
+ margin-right: 1rem;
175
+ }
176
+
177
+ .modal-close-button {
178
+ display: inline-flex;
179
+ padding: 0.375rem;
180
+ margin-left: auto;
181
+ background-color: transparent;
182
+ font-size: 0.875rem;
183
+ line-height: 1.25rem;
184
+ align-items: center;
185
+ border-radius: 0.5rem;
186
+ }
187
+
188
+ .modal-close-icon {
189
+ width: 1.25rem;
190
+ height: 1.25rem;
191
+ }
192
+
193
+ /* ========================================
194
+ Confirm dialog styles
195
+ Scoped to [data-utmr-confirm] so it can reuse the modal container,
196
+ backdrop and enter/leave transitions while overriding the layout.
197
+ ======================================== */
198
+
199
+ &[data-utmr-confirm] {
200
+ /* Real gutter on mobile: the card below is full width. */
201
+ .modal-inner {
202
+ padding: 1rem;
203
+ padding-top: 10vh;
204
+
205
+ @media (min-width: 640px) {
206
+ padding: 1rem;
207
+ padding-bottom: 10vh;
208
+ }
209
+ }
210
+
211
+ /* Sized rather than shrink-to-fit, so a one-word prompt and a three-line
212
+ one come out the same. The floor is written as min(20rem, 100%) so it can
213
+ never push the card wider than the space it has on a narrow screen. */
214
+ .confirm-content {
215
+ position: relative;
216
+ overflow: hidden;
217
+ width: 100%;
218
+ min-width: min(20rem, 100%);
219
+ background-color: #ffffff;
220
+ text-align: left;
221
+ border-radius: 0.5rem;
222
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
223
+
224
+ @media (min-width: 640px) {
225
+ max-width: 28rem;
226
+ }
227
+ }
228
+
229
+ .confirm-header {
230
+ display: flex;
231
+ align-items: flex-start;
232
+ justify-content: space-between;
233
+ gap: 1rem;
234
+ width: 100%;
235
+ padding: 1.5rem 1.5rem 0;
236
+ }
237
+
238
+ .confirm-title-h {
239
+ font-size: 1rem;
240
+ line-height: 1.5rem;
241
+ font-weight: 600;
242
+ }
243
+
244
+ .confirm-main {
245
+ padding: 0;
246
+ }
247
+
248
+ .confirm-body {
249
+ font-size: 0.875rem;
250
+ line-height: 1.5rem;
251
+ }
252
+
253
+ .confirm-footer {
254
+ padding: 1.5rem;
255
+ }
256
+
257
+ /* Stacked and full width on mobile, inline and right aligned from 640px.
258
+ Reversed so the primary action sits on top. */
259
+ .confirm-actions {
260
+ display: flex;
261
+ width: 100%;
262
+ flex-direction: column-reverse;
263
+ gap: 0.5rem;
264
+
265
+ @media (min-width: 640px) {
266
+ flex-direction: row;
267
+ justify-content: flex-end;
268
+ gap: 0.75rem;
269
+ }
270
+ }
271
+
272
+ .confirm-cancel,
273
+ .confirm-accept {
274
+ display: inline-flex;
275
+ width: 100%;
276
+ justify-content: center;
277
+ align-items: center;
278
+ padding: 0.625rem 1rem;
279
+ font-size: 0.875rem;
280
+ line-height: 1.25rem;
281
+ font-weight: 600;
282
+ border-radius: 0.375rem;
283
+ cursor: pointer;
284
+ transition: background-color 150ms ease-in-out;
285
+
286
+ @media (min-width: 640px) {
287
+ width: auto;
288
+ padding: 0.5rem 1rem;
289
+ }
290
+ }
291
+
292
+ .confirm-cancel {
293
+ background-color: #ffffff;
294
+ color: #111827;
295
+ border: 1px solid #D1D5DB;
296
+
297
+ &:hover {
298
+ background-color: #F9FAFB;
299
+ }
300
+ }
301
+
302
+ .confirm-accept {
303
+ background-color: #4F46E5;
304
+ color: #ffffff;
305
+ border: 1px solid transparent;
306
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
307
+
308
+ &:hover {
309
+ background-color: #6366F1;
310
+ }
311
+ }
312
+
313
+ /* Destructive variant, set from `variant: :danger` */
314
+ &[data-utmr-confirm-variant="danger"] .confirm-accept {
315
+ background-color: #DC2626;
316
+
317
+ &:hover {
318
+ background-color: #EF4444;
319
+ }
320
+ }
321
+
322
+ /* Options, matching the modal's data-attribute switches */
323
+ &[data-header="false"] .confirm-header {
324
+ display: none;
325
+ }
326
+
327
+ &[data-title="false"] .confirm-title-h {
328
+ display: none;
329
+ }
330
+
331
+ &[data-padding="true"] .confirm-main {
332
+ padding: 0.5rem 1.5rem 0;
333
+ }
334
+
335
+ &[data-header-divider="true"] .confirm-header {
336
+ padding-bottom: 1rem;
337
+ border-bottom-width: 1px;
338
+ border-bottom-style: solid;
339
+ }
340
+
341
+ &[data-footer-divider="true"] .confirm-footer {
342
+ border-top-width: 1px;
343
+ border-top-style: solid;
344
+ }
345
+ }
346
+
347
+ /* ========================================
348
+ Drawer styles
349
+ ======================================== */
350
+
351
+ /* Drawer backdrop transitions */
352
+ &.drawer-container::backdrop {
353
+ background: transparent;
354
+ opacity: 0;
355
+ transition: opacity 300ms ease-out;
356
+ }
357
+ &.drawer-container[data-overlay="true"]::backdrop {
358
+ background-color: rgba(17, 24, 39, 0.7);
359
+ }
360
+ &.drawer-container[data-overlay="true"][data-entered]::backdrop {
361
+ opacity: 1;
362
+ }
363
+ &.drawer-container[data-closing]::backdrop {
364
+ transition-duration: 200ms;
365
+ transition-timing-function: ease-in;
366
+ }
367
+
368
+ /* Drawer sizing via data attributes */
369
+ &.drawer-container {
370
+ --utmr-gutter: 2.5rem;
371
+
372
+ @media (min-width: 640px) {
373
+ --utmr-gutter: 4rem;
374
+ }
375
+ }
376
+ &.drawer-container[data-drawer-size="xs"] { --utmr-w: 16rem; }
377
+ &.drawer-container[data-drawer-size="sm"] { --utmr-w: 20rem; }
378
+ &.drawer-container[data-drawer-size="md"],
379
+ &.drawer-container[data-drawer-size=""] { --utmr-w: 24rem; }
380
+ &.drawer-container[data-drawer-size="lg"] { --utmr-w: 28rem; }
381
+ &.drawer-container[data-drawer-size="xl"] { --utmr-w: 42rem; }
382
+ &.drawer-container[data-drawer-size="2xl"] { --utmr-w: 56rem; }
383
+ &.drawer-container[data-drawer-size="full"] { --utmr-w: 100vw; }
384
+
385
+ /* Drawer direction */
386
+ &.drawer-container[data-drawer="left"] { --utmr-hide: -100% 0; }
387
+ &.drawer-container[data-drawer="left"] .drawer-panel { left: 0; }
388
+ &.drawer-container[data-drawer="right"] { --utmr-hide: 100% 0; }
389
+ &.drawer-container[data-drawer="right"] .drawer-panel { right: 0; }
390
+
391
+ /* Drawer panel transitions */
392
+ .drawer-panel {
393
+ position: absolute;
394
+ top: 0;
395
+ bottom: 0;
396
+ height: 100%;
397
+ width: min(var(--utmr-w), calc(100vw - var(--utmr-gutter)));
398
+ translate: var(--utmr-hide);
399
+ transition: translate 250ms ease-in-out;
400
+ will-change: translate;
401
+
402
+ @media (min-width: 640px) {
403
+ transition-duration: 400ms;
404
+ }
405
+ }
406
+ &.drawer-container:not([data-enter-ready]):not([data-entered]) .drawer-panel {
407
+ visibility: hidden;
408
+ }
409
+ &.drawer-container[data-entered] .drawer-panel {
410
+ translate: 0;
411
+ }
412
+ &.drawer-container[data-closing] .drawer-panel {
413
+ translate: var(--utmr-hide);
414
+ }
415
+
416
+ /* Drawer conditional styling via data attributes */
417
+ &.drawer-container {
418
+ &[data-header="false"] .drawer-header {
419
+ display: none;
420
+ }
421
+
422
+ &[data-header-divider="true"] .drawer-header {
423
+ padding-bottom: 1rem;
424
+ border-bottom-width: 1px;
425
+ }
426
+
427
+ &[data-footer-divider="true"] .drawer-footer {
428
+ border-top-width: 1px;
429
+ }
430
+
431
+ &[data-padding="true"] .drawer-main {
432
+ padding: 1rem;
433
+ padding-top: 0.5rem;
434
+ }
435
+
436
+ &[data-title="false"] .drawer-title-h {
437
+ display: none;
438
+ }
439
+
440
+ &[data-close-button="false"] .drawer-close {
441
+ display: none;
442
+ }
443
+ }
444
+
445
+ .drawer-wrapper {
446
+ position: absolute;
447
+ inset: 0;
448
+ overflow: hidden;
449
+ }
450
+
451
+ .drawer-content {
452
+ position: relative;
453
+ display: flex;
454
+ flex-direction: column;
455
+ height: 100%;
456
+ width: 100%;
457
+ background-color: #ffffff;
458
+ box-shadow:
459
+ 0 20px 25px -5px rgba(0, 0, 0, 0.1),
460
+ 0 8px 10px -6px rgba(0, 0, 0, 0.1);
461
+ }
462
+ &.drawer-container[data-padding="true"] .drawer-content {
463
+ padding-top: 1.5rem;
464
+ padding-bottom: 1.5rem;
465
+ }
466
+
467
+ .drawer-header {
468
+ display: flex;
469
+ padding-left: 1rem;
470
+ padding-right: 1rem;
471
+ align-items: flex-start;
472
+ justify-content: space-between;
473
+ width: 100%;
474
+
475
+ @media (min-width: 640px) {
476
+ padding-left: 1.5rem;
477
+ padding-right: 1.5rem;
478
+ }
479
+ }
480
+
481
+ .drawer-title {
482
+ line-height: 1.75rem;
483
+ font-weight: 600;
484
+ }
485
+
486
+ .drawer-main {
487
+ position: relative;
488
+ flex: 1;
489
+ overflow-y: auto;
490
+ }
491
+ &.drawer-container[data-padding="true"] .drawer-main {
492
+ margin-top: 1.5rem;
493
+ }
494
+
495
+ .drawer-footer {
496
+ display: flex;
497
+ flex-shrink: 0;
498
+ padding: 1rem;
499
+
500
+ @media (min-width: 640px) {
501
+ padding-left: 1.5rem;
502
+ padding-right: 1.5rem;
503
+ }
504
+ }
505
+
506
+ .drawer-close {
507
+ margin-left: 0.75rem;
508
+ display: flex;
509
+ height: 1.75rem;
510
+ align-items: center;
511
+ }
512
+
513
+ .drawer-close-button {
514
+ position: relative;
515
+ display: inline-flex;
516
+ padding: 0.375rem;
517
+ background-color: transparent;
518
+ border-radius: 0.375rem;
519
+ align-items: center;
520
+ }
521
+
522
+ .drawer-close-hit-area {
523
+ position: absolute;
524
+ inset: -0.625rem;
525
+ }
526
+
527
+ .drawer-close-icon {
528
+ width: 1.5rem;
529
+ height: 1.5rem;
530
+ }
531
+ }
532
+
533
+ /* ========================================
534
+ Dark / light mode (requires ancestor
535
+ context so must stay outside dialog.utmr)
536
+ ======================================== */
537
+
538
+ .dark dialog.utmr {
539
+ &::backdrop {
540
+ background-color: rgba(17, 24, 39, 0.8);
541
+ }
542
+
543
+ .modal-header,
544
+ .modal-footer,
545
+ .drawer-header,
546
+ .drawer-footer {
547
+ border-color: #4B5563;
548
+ }
549
+
550
+ .modal-content,
551
+ .drawer-content {
552
+ background-color: #1F2937;
553
+ color: #ffffff;
554
+ }
555
+
556
+ .confirm-content {
557
+ background-color: #1F2937;
558
+ color: #ffffff;
559
+ }
560
+
561
+ .confirm-body {
562
+ color: #9CA3AF;
563
+ }
564
+
565
+ .confirm-header,
566
+ .confirm-footer {
567
+ border-color: #4B5563;
568
+ }
569
+
570
+ .confirm-cancel {
571
+ background-color: #374151;
572
+ color: #F3F4F6;
573
+ border-color: #4B5563;
574
+
575
+ &:hover {
576
+ background-color: #4B5563;
577
+ }
578
+ }
579
+
580
+ .modal-close-button:hover,
581
+ .modal-close-icon:hover,
582
+ .drawer-close-button:hover,
583
+ .drawer-close-icon:hover {
584
+ background-color: #6B7280;
585
+ color: #ffffff;
586
+ }
587
+ }
588
+
589
+ :not(.dark) dialog.utmr {
590
+ .confirm-body {
591
+ color: #6B7280;
592
+ }
593
+
594
+ .confirm-header,
595
+ .confirm-footer {
596
+ border-color: #E5E7EB;
597
+ }
598
+
599
+ .modal-close-button,
600
+ .modal-close-icon,
601
+ .drawer-close-button,
602
+ .drawer-close-icon {
603
+ color: #9CA3AF;
604
+
605
+ &:hover {
606
+ color: #111827;
607
+ background-color: #E5E7EB;
608
+ }
609
+ }
610
+ }
@@ -86,6 +86,50 @@ module UltimateTurboModal
86
86
  end
87
87
  end
88
88
 
89
+ # The vanilla flavor needs a stylesheet; the other flavors are styled
90
+ # entirely by the classes defined in their flavor file.
91
+ def add_vanilla_stylesheet
92
+ layout_path = rails_root_join("app", "views", "layouts", "application.html.erb")
93
+ link_tag = "<%= stylesheet_link_tag \"#{package_name}\", \"data-turbo-track\": \"reload\" %>\n"
94
+
95
+ say "\nAttempting to add the vanilla stylesheet to #{layout_path}...", :yellow
96
+
97
+ unless File.exist?(layout_path)
98
+ say "❌ Layout file not found at #{layout_path}.", :red
99
+ say " Please manually add the following line inside the <head> of your main layout:", :yellow
100
+ say " #{link_tag.strip}\n", :cyan
101
+ return
102
+ end
103
+
104
+ file_content = File.read(layout_path)
105
+
106
+ if file_content.match?(/stylesheet_link_tag\s+["']#{Regexp.escape(package_name)}["']/)
107
+ say "⏩ Stylesheet tag already exists.", :blue
108
+ return
109
+ end
110
+
111
+ # Insert before the first existing stylesheet_link_tag. Where the two
112
+ # stylesheets collide at equal specificity the later one wins, so the
113
+ # defaults have to load first for the app's own styles to override them.
114
+ existing_tag = file_content.scan(/^.*stylesheet_link_tag.*\n/).first
115
+
116
+ if existing_tag
117
+ insert_into_file layout_path, "#{indentation_of(existing_tag)}#{link_tag}", before: existing_tag
118
+ say "✅ Added stylesheet tag to the layout.", :green
119
+ elsif file_content.match?(%r{</head>})
120
+ insert_into_file layout_path, " #{link_tag}", before: %r{^\s*</head>}
121
+ say "✅ Added stylesheet tag before </head>.", :green
122
+ else
123
+ say "❌ Could not find a <head> section in #{layout_path}.", :red
124
+ say " Please manually add the following line inside the <head> of your main layout:", :yellow
125
+ say " #{link_tag.strip}\n", :cyan
126
+ end
127
+ end
128
+
129
+ def indentation_of(line)
130
+ line[/\A[ \t]*/]
131
+ end
132
+
89
133
  def uses_importmaps?
90
134
  File.exist?(rails_root_join("config", "importmap.rb"))
91
135
  end
@@ -108,6 +108,11 @@ module UltimateTurboModal
108
108
  end
109
109
  end
110
110
 
111
+ # Step 5: Add the stylesheet to the layout (vanilla flavor only)
112
+ def setup_stylesheet
113
+ add_vanilla_stylesheet if @framework == "vanilla"
114
+ end
115
+
111
116
  def copy_initializer_and_flavor
112
117
  say "\nCreating initializer for `#{@framework}` flavor...", :green
113
118
  copy_file "ultimate_turbo_modal.rb", "config/initializers/ultimate_turbo_modal.rb"
@@ -122,6 +127,9 @@ module UltimateTurboModal
122
127
  def show_readme
123
128
  say "\nUltimateTurboModal installation complete!\n", :magenta
124
129
  say "Please review the initializer files, ensure JS is set up, and check your layout file.", :magenta
130
+ say "\nOptional: to render `data-turbo-confirm` prompts as modals instead of", :magenta
131
+ say "the browser's confirm dialog, add this to your layout:", :magenta
132
+ say " <%= modal_confirm_template %>", :cyan
125
133
  say "Don't forget to restart your Rails server!", :yellow
126
134
  end
127
135
 
@@ -8,7 +8,7 @@
8
8
  # Set to "" or omit entirely if you handle all styling via classes and external CSS.
9
9
  module UltimateTurboModal::Flavors
10
10
  class Custom < UltimateTurboModal::Base
11
- STYLES = "html:has(dialog#modal-container[open]) { overflow: hidden; }"
11
+ STYLES = "html:has(dialog.utmr[open]) { overflow: hidden; }"
12
12
 
13
13
  MODAL_DIALOG_CLASSES = ""
14
14
  MODAL_INNER_CLASSES = ""
@@ -23,6 +23,15 @@ module UltimateTurboModal::Flavors
23
23
  MODAL_CLOSE_SR_CLASSES = ""
24
24
  MODAL_CLOSE_ICON_CLASSES = ""
25
25
 
26
+ # Confirm dialog constants
27
+ # Only the slots that have no modal equivalent are listed. Every other slot
28
+ # (dialog, inner, content, header, title, main, footer) falls back to the
29
+ # matching MODAL_* class above; define a CONFIRM_* one to override it.
30
+ CONFIRM_BODY_CLASSES = ""
31
+ CONFIRM_ACTIONS_CLASSES = ""
32
+ CONFIRM_CANCEL_CLASSES = ""
33
+ CONFIRM_ACCEPT_CLASSES = ""
34
+
26
35
  # Drawer constants
27
36
  DRAWER_DIALOG_CLASSES = MODAL_DIALOG_CLASSES
28
37
  DRAWER_WRAPPER_CLASSES = ""