turbo-toastify 0.1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +71 -0
- data/lib/generators/turbo_toastify/install/install_generator.rb +44 -0
- data/lib/generators/turbo_toastify/install/templates/app/assets/stylesheets/toastify.css +166 -0
- data/lib/generators/turbo_toastify/install/templates/app/javascript/controllers/toast_controller.js +34 -0
- data/lib/generators/turbo_toastify/install/templates/app/javascript/toastify/index.js +179 -0
- data/lib/generators/turbo_toastify/install/templates/app/views/layouts/_turbo_toastify_flash_outlet.html.erb +2 -0
- data/lib/generators/turbo_toastify/install/templates/app/views/shared/_flash.html.erb +30 -0
- data/lib/generators/turbo_toastify/install/templates/config/initializers/turbo_toastify.rb +8 -0
- data/lib/turbo_toastify/engine.rb +5 -0
- data/lib/turbo_toastify/version.rb +3 -0
- data/lib/turbo_toastify.rb +5 -0
- metadata +101 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d52c19b644d49600aa0bf3ac7127768065a2f73b4daf10e84dc5fd592a5037e8
|
|
4
|
+
data.tar.gz: 8623ae30e548456643d15c31f650987219fa41df89be799fd6d872bc60b50869
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: df9d531081540d063bedd1ec69696603efddcd59a22361ab476f829821e7e658c3511a4e31e3cce973b467eb7422b5697e1975268f399d3af7dd4eb19a627529
|
|
7
|
+
data.tar.gz: 1878dc6b243898e35ebc5c45e4a40b5a133b65e0b65e0033e9f3349635c69824b3cc4ba06dcdca2ab0fcd74b8018ef0b9e0e2250e814ed978264dd3e1155160c
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# TurboToastify
|
|
2
|
+
|
|
3
|
+
`turbo-toastify` packages a lightweight toast notification system for Rails applications using Turbo + Stimulus.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "turbo-toastify"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
bin/rails generate turbo_toastify:install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## What the installer adds
|
|
21
|
+
|
|
22
|
+
- `app/javascript/toastify/index.js` (core toast engine)
|
|
23
|
+
- `app/javascript/controllers/toast_controller.js` (Stimulus bridge)
|
|
24
|
+
- `app/assets/stylesheets/toastify.css`
|
|
25
|
+
- `app/views/shared/_flash.html.erb`
|
|
26
|
+
- `config/initializers/turbo_toastify.rb`
|
|
27
|
+
|
|
28
|
+
It also attempts to:
|
|
29
|
+
|
|
30
|
+
- insert a permanent Turbo flash outlet in `app/views/layouts/application.html.erb`
|
|
31
|
+
- include `<%= stylesheet_link_tag "toastify", "data-turbo-track": "reload" %>` in your layout
|
|
32
|
+
- add an importmap pin for `toastify/index` when `config/importmap.rb` is present
|
|
33
|
+
|
|
34
|
+
## Layout requirements
|
|
35
|
+
|
|
36
|
+
Make sure your layout includes:
|
|
37
|
+
|
|
38
|
+
```erb
|
|
39
|
+
<div id="flash-outlet"></div>
|
|
40
|
+
<%= render "shared/flash" %>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Controller usage
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
def create
|
|
47
|
+
@post = Post.create!(post_params)
|
|
48
|
+
redirect_to posts_path, notice: "Post created successfully!"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def update
|
|
52
|
+
@post.update!(post_params)
|
|
53
|
+
respond_to do |format|
|
|
54
|
+
format.turbo_stream do
|
|
55
|
+
render turbo_stream: [
|
|
56
|
+
turbo_stream.replace(@post),
|
|
57
|
+
turbo_stream.append("flash-outlet", partial: "shared/flash")
|
|
58
|
+
]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## JavaScript usage
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
TurboToastify.success("Saved!")
|
|
68
|
+
TurboToastify.error("Failed to save", { autoClose: 8000, theme: "colored" })
|
|
69
|
+
TurboToastify.info("Syncing...", { position: "bottom-center", transition: "zoom" })
|
|
70
|
+
TurboToastify.warning("Low storage", { theme: "dark", transition: "flip" })
|
|
71
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module TurboToastify
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
def copy_assets_and_templates
|
|
9
|
+
copy_file "app/javascript/toastify/index.js", "app/javascript/toastify/index.js"
|
|
10
|
+
copy_file "app/javascript/controllers/toast_controller.js", "app/javascript/controllers/toast_controller.js"
|
|
11
|
+
copy_file "app/assets/stylesheets/toastify.css", "app/assets/stylesheets/toastify.css"
|
|
12
|
+
copy_file "app/views/shared/_flash.html.erb", "app/views/shared/_flash.html.erb"
|
|
13
|
+
copy_file "config/initializers/turbo_toastify.rb", "config/initializers/turbo_toastify.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_importmap_pin
|
|
17
|
+
importmap = "config/importmap.rb"
|
|
18
|
+
return unless File.exist?(importmap)
|
|
19
|
+
|
|
20
|
+
pin_line = 'pin "toastify/index", to: "toastify/index.js"'
|
|
21
|
+
return if File.read(importmap).include?(pin_line)
|
|
22
|
+
|
|
23
|
+
append_to_file importmap, "\n#{pin_line}\n"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update_application_layout
|
|
27
|
+
layout_path = "app/views/layouts/application.html.erb"
|
|
28
|
+
return unless File.exist?(layout_path)
|
|
29
|
+
|
|
30
|
+
layout = File.read(layout_path)
|
|
31
|
+
|
|
32
|
+
stylesheet_line = '<%= stylesheet_link_tag "toastify", "data-turbo-track": "reload" %>'
|
|
33
|
+
unless layout.include?(stylesheet_line)
|
|
34
|
+
inject_into_file layout_path, " #{stylesheet_line}\n", before: "</head>"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
outlet_markup = " <div id=\"flash-outlet\"></div>\n <%= render \"shared/flash\" %>\n"
|
|
38
|
+
unless layout.include?("<div id=\"flash-outlet\"></div>")
|
|
39
|
+
inject_into_file layout_path, outlet_markup, before: "<%= yield %>"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
.Toastify__toast-container {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 9999;
|
|
4
|
+
width: 320px;
|
|
5
|
+
padding: 4px;
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.Toastify__toast-container--top-right { top: 1rem; right: 1rem; }
|
|
10
|
+
.Toastify__toast-container--top-left { top: 1rem; left: 1rem; }
|
|
11
|
+
.Toastify__toast-container--top-center { top: 1rem; left: 50%; transform: translateX(-50%); }
|
|
12
|
+
.Toastify__toast-container--bottom-right { bottom: 1rem; right: 1rem; }
|
|
13
|
+
.Toastify__toast-container--bottom-left { bottom: 1rem; left: 1rem; }
|
|
14
|
+
.Toastify__toast-container--bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); }
|
|
15
|
+
|
|
16
|
+
.Toastify__toast {
|
|
17
|
+
position: relative;
|
|
18
|
+
min-height: 64px;
|
|
19
|
+
border-radius: 4px;
|
|
20
|
+
box-shadow: 0 1px 10px rgba(0, 0, 0, .1), 0 2px 15px rgba(0, 0, 0, .05);
|
|
21
|
+
display: flex;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
align-items: stretch;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
margin-bottom: 8px;
|
|
26
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
27
|
+
font-size: 14px;
|
|
28
|
+
cursor: default;
|
|
29
|
+
background: #fff;
|
|
30
|
+
color: #333;
|
|
31
|
+
touch-action: none;
|
|
32
|
+
user-select: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.Toastify__toast--dark { background: #121212; color: #fff; }
|
|
36
|
+
|
|
37
|
+
.Toastify__toast--colored.Toastify__toast--success { background: #07bc0c; color: #fff; }
|
|
38
|
+
.Toastify__toast--colored.Toastify__toast--error { background: #e74c3c; color: #fff; }
|
|
39
|
+
.Toastify__toast--colored.Toastify__toast--warning { background: #f1c40f; color: #333; }
|
|
40
|
+
.Toastify__toast--colored.Toastify__toast--info { background: #3498db; color: #fff; }
|
|
41
|
+
.Toastify__toast--colored.Toastify__toast--default { background: #fff; color: #333; }
|
|
42
|
+
|
|
43
|
+
.Toastify__toast-body {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 10px;
|
|
47
|
+
padding: 12px;
|
|
48
|
+
flex: 1;
|
|
49
|
+
line-height: 1.4;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.Toastify__toast-icon {
|
|
53
|
+
flex-shrink: 0;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.Toastify__toast--success .Toastify__toast-icon { color: #07bc0c; }
|
|
59
|
+
.Toastify__toast--error .Toastify__toast-icon { color: #e74c3c; }
|
|
60
|
+
.Toastify__toast--warning .Toastify__toast-icon { color: #f1c40f; }
|
|
61
|
+
.Toastify__toast--info .Toastify__toast-icon { color: #3498db; }
|
|
62
|
+
.Toastify__toast--default .Toastify__toast-icon { color: #555; }
|
|
63
|
+
|
|
64
|
+
.Toastify__toast--colored .Toastify__toast-icon { color: inherit; }
|
|
65
|
+
|
|
66
|
+
.Toastify__close-button {
|
|
67
|
+
background: transparent;
|
|
68
|
+
border: none;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
color: inherit;
|
|
71
|
+
opacity: 0.7;
|
|
72
|
+
font-size: 14px;
|
|
73
|
+
padding: 10px;
|
|
74
|
+
align-self: flex-start;
|
|
75
|
+
transition: opacity .15s;
|
|
76
|
+
flex-shrink: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.Toastify__close-button:hover { opacity: 1; }
|
|
80
|
+
|
|
81
|
+
.Toastify__progress-bar {
|
|
82
|
+
position: absolute;
|
|
83
|
+
bottom: 0;
|
|
84
|
+
left: 0;
|
|
85
|
+
height: 4px;
|
|
86
|
+
width: 100%;
|
|
87
|
+
transform-origin: left;
|
|
88
|
+
animation: Toastify__trackProgress linear 1 forwards;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.Toastify__progress-bar--success { background: #07bc0c; }
|
|
92
|
+
.Toastify__progress-bar--error { background: #e74c3c; }
|
|
93
|
+
.Toastify__progress-bar--warning { background: #f1c40f; }
|
|
94
|
+
.Toastify__progress-bar--info { background: #3498db; }
|
|
95
|
+
.Toastify__progress-bar--default { background: linear-gradient(to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55); }
|
|
96
|
+
|
|
97
|
+
.Toastify__toast--dark .Toastify__progress-bar { background: #bb86fc; }
|
|
98
|
+
.Toastify__toast--colored .Toastify__progress-bar { background: rgba(255, 255, 255, .7); }
|
|
99
|
+
|
|
100
|
+
@keyframes Toastify__trackProgress {
|
|
101
|
+
0% { transform: scaleX(1); }
|
|
102
|
+
100% { transform: scaleX(0); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.Toastify__toast--enter {
|
|
106
|
+
animation-name: var(--enter-anim);
|
|
107
|
+
animation-duration: .5s;
|
|
108
|
+
animation-fill-mode: both;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.Toastify__toast--exit {
|
|
112
|
+
animation-name: Toastify__slideOut;
|
|
113
|
+
animation-duration: .3s;
|
|
114
|
+
animation-fill-mode: both;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@keyframes Toastify__slideInRight {
|
|
118
|
+
from { transform: translateX(110%); }
|
|
119
|
+
to { transform: translateX(0); }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@keyframes Toastify__slideInLeft {
|
|
123
|
+
from { transform: translateX(-110%); }
|
|
124
|
+
to { transform: translateX(0); }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@keyframes Toastify__slideInDown {
|
|
128
|
+
from { transform: translateY(-110%); }
|
|
129
|
+
to { transform: translateY(0); }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@keyframes Toastify__slideInUp {
|
|
133
|
+
from { transform: translateY(110%); }
|
|
134
|
+
to { transform: translateY(0); }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@keyframes Toastify__bounceInRight {
|
|
138
|
+
from, 60%, 75%, 90%, to { animation-timing-function: cubic-bezier(.215, .61, .355, 1); }
|
|
139
|
+
0% { opacity: 0; transform: translate3d(3000px, 0, 0); }
|
|
140
|
+
60% { opacity: 1; transform: translate3d(-25px, 0, 0); }
|
|
141
|
+
75% { transform: translate3d(10px, 0, 0); }
|
|
142
|
+
90% { transform: translate3d(-5px, 0, 0); }
|
|
143
|
+
to { transform: none; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@keyframes Toastify__zoomIn {
|
|
147
|
+
from { opacity: 0; transform: scale3d(.3, .3, .3); }
|
|
148
|
+
50% { opacity: 1; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@keyframes Toastify__flipIn {
|
|
152
|
+
from { transform: perspective(400px) rotate3d(1, 0, 0, 90deg); animation-timing-function: ease-in; opacity: 0; }
|
|
153
|
+
40% { transform: perspective(400px) rotate3d(1, 0, 0, -20deg); animation-timing-function: ease-in; }
|
|
154
|
+
60% { transform: perspective(400px) rotate3d(1, 0, 0, 10deg); opacity: 1; }
|
|
155
|
+
80% { transform: perspective(400px) rotate3d(1, 0, 0, -5deg); }
|
|
156
|
+
to { transform: perspective(400px); }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@keyframes Toastify__fadeIn {
|
|
160
|
+
from { opacity: 0; }
|
|
161
|
+
to { opacity: 1; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@keyframes Toastify__slideOut {
|
|
165
|
+
to { transform: translateX(110%); opacity: 0; }
|
|
166
|
+
}
|
data/lib/generators/turbo_toastify/install/templates/app/javascript/controllers/toast_controller.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import TurboToastify from "../toastify/index"
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static values = {
|
|
6
|
+
message: String,
|
|
7
|
+
type: { type: String, default: "default" },
|
|
8
|
+
position: { type: String, default: "top-right" },
|
|
9
|
+
autoClose: { type: Number, default: 5000 },
|
|
10
|
+
theme: { type: String, default: "light" },
|
|
11
|
+
transition: { type: String, default: "slide" },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
requestAnimationFrame(() => {
|
|
16
|
+
if (this.messageValue) {
|
|
17
|
+
TurboToastify[this.typeValue]?.(this.messageValue, {
|
|
18
|
+
position: this.positionValue,
|
|
19
|
+
autoClose: this.autoCloseValue,
|
|
20
|
+
theme: this.themeValue,
|
|
21
|
+
transition: this.transitionValue,
|
|
22
|
+
}) ?? TurboToastify.show(this.messageValue, {
|
|
23
|
+
type: this.typeValue,
|
|
24
|
+
position: this.positionValue,
|
|
25
|
+
autoClose: this.autoCloseValue,
|
|
26
|
+
theme: this.themeValue,
|
|
27
|
+
transition: this.transitionValue,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.element.remove()
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const POSITIONS = {
|
|
2
|
+
TOP_RIGHT: "top-right",
|
|
3
|
+
TOP_LEFT: "top-left",
|
|
4
|
+
TOP_CENTER: "top-center",
|
|
5
|
+
BOTTOM_RIGHT: "bottom-right",
|
|
6
|
+
BOTTOM_LEFT: "bottom-left",
|
|
7
|
+
BOTTOM_CENTER: "bottom-center",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const THEMES = { LIGHT: "light", DARK: "dark", COLORED: "colored" }
|
|
11
|
+
const TRANSITIONS = { SLIDE: "slide", BOUNCE: "bounce", ZOOM: "zoom", FLIP: "flip", FADE: "fade" }
|
|
12
|
+
const TYPES = { SUCCESS: "success", ERROR: "error", WARNING: "warning", INFO: "info", DEFAULT: "default" }
|
|
13
|
+
|
|
14
|
+
const ICONS = {
|
|
15
|
+
success: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><circle cx="12" cy="12" r="10"/><path d="M8 12l3 3 5-5"/></svg>`,
|
|
16
|
+
error: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`,
|
|
17
|
+
warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
|
18
|
+
info: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
|
|
19
|
+
default: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ENTER_ANIMS = {
|
|
23
|
+
slide: { right: "Toastify__slideInRight", left: "Toastify__slideInLeft", center: "Toastify__slideInDown", bottom: "Toastify__slideInUp" },
|
|
24
|
+
bounce: { right: "Toastify__bounceInRight", left: "Toastify__bounceInRight", center: "Toastify__bounceInRight", bottom: "Toastify__bounceInRight" },
|
|
25
|
+
zoom: { right: "Toastify__zoomIn", left: "Toastify__zoomIn", center: "Toastify__zoomIn", bottom: "Toastify__zoomIn" },
|
|
26
|
+
flip: { right: "Toastify__flipIn", left: "Toastify__flipIn", center: "Toastify__flipIn", bottom: "Toastify__flipIn" },
|
|
27
|
+
fade: { right: "Toastify__fadeIn", left: "Toastify__fadeIn", center: "Toastify__fadeIn", bottom: "Toastify__fadeIn" },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaults = {
|
|
31
|
+
position: POSITIONS.TOP_RIGHT,
|
|
32
|
+
autoClose: 5000,
|
|
33
|
+
theme: THEMES.LIGHT,
|
|
34
|
+
transition: TRANSITIONS.SLIDE,
|
|
35
|
+
closeButton: true,
|
|
36
|
+
pauseOnHover: true,
|
|
37
|
+
draggable: true,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const containers = {}
|
|
41
|
+
|
|
42
|
+
function getContainer(position) {
|
|
43
|
+
if (containers[position]) return containers[position]
|
|
44
|
+
|
|
45
|
+
const el = document.createElement("div")
|
|
46
|
+
el.className = `Toastify__toast-container Toastify__toast-container--${position}`
|
|
47
|
+
el.setAttribute("data-position", position)
|
|
48
|
+
document.body.appendChild(el)
|
|
49
|
+
containers[position] = el
|
|
50
|
+
return el
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getEnterAnim(transition, position) {
|
|
54
|
+
const map = ENTER_ANIMS[transition] || ENTER_ANIMS.slide
|
|
55
|
+
if (position.includes("right")) return map.right
|
|
56
|
+
if (position.includes("left")) return map.left
|
|
57
|
+
if (position.startsWith("bottom")) return map.bottom
|
|
58
|
+
return map.center
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function dismiss(toast) {
|
|
62
|
+
if (!toast || toast._dismissing) return
|
|
63
|
+
toast._dismissing = true
|
|
64
|
+
clearTimeout(toast._timer)
|
|
65
|
+
|
|
66
|
+
toast.classList.add("Toastify__toast--exit")
|
|
67
|
+
toast.addEventListener("animationend", () => {
|
|
68
|
+
toast.remove()
|
|
69
|
+
}, { once: true })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function show(message, options = {}) {
|
|
73
|
+
const opts = { ...defaults, ...options }
|
|
74
|
+
const { position, autoClose, theme, transition, closeButton, pauseOnHover, draggable, type = TYPES.DEFAULT } = opts
|
|
75
|
+
|
|
76
|
+
const container = getContainer(position)
|
|
77
|
+
const isBottom = position.startsWith("bottom")
|
|
78
|
+
|
|
79
|
+
const toast = document.createElement("div")
|
|
80
|
+
toast.className = [
|
|
81
|
+
"Toastify__toast",
|
|
82
|
+
`Toastify__toast--${type}`,
|
|
83
|
+
theme === THEMES.DARK ? "Toastify__toast--dark" : "",
|
|
84
|
+
theme === THEMES.COLORED ? "Toastify__toast--colored" : "",
|
|
85
|
+
].filter(Boolean).join(" ")
|
|
86
|
+
|
|
87
|
+
const enterAnim = getEnterAnim(transition, position)
|
|
88
|
+
toast.style.setProperty("--enter-anim", enterAnim)
|
|
89
|
+
toast.classList.add("Toastify__toast--enter")
|
|
90
|
+
|
|
91
|
+
const progressBar = autoClose
|
|
92
|
+
? `<div class="Toastify__progress-bar Toastify__progress-bar--${type}" style="animation-duration:${autoClose}ms;"></div>`
|
|
93
|
+
: ""
|
|
94
|
+
|
|
95
|
+
const closeBtn = closeButton
|
|
96
|
+
? `<button class="Toastify__close-button" aria-label="Close toast">✕</button>`
|
|
97
|
+
: ""
|
|
98
|
+
|
|
99
|
+
toast.innerHTML = `
|
|
100
|
+
<div class="Toastify__toast-body">
|
|
101
|
+
<div class="Toastify__toast-icon">${ICONS[type] || ICONS.default}</div>
|
|
102
|
+
<div class="Toastify__toast-message">${message}</div>
|
|
103
|
+
</div>
|
|
104
|
+
${closeBtn}
|
|
105
|
+
${progressBar}
|
|
106
|
+
`
|
|
107
|
+
|
|
108
|
+
toast.querySelector(".Toastify__close-button")?.addEventListener("click", () => dismiss(toast))
|
|
109
|
+
|
|
110
|
+
if (autoClose) {
|
|
111
|
+
toast._timer = setTimeout(() => dismiss(toast), autoClose)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (pauseOnHover && autoClose) {
|
|
115
|
+
const pb = toast.querySelector(".Toastify__progress-bar")
|
|
116
|
+
toast.addEventListener("mouseenter", () => {
|
|
117
|
+
clearTimeout(toast._timer)
|
|
118
|
+
pb?.style.setProperty("animation-play-state", "paused")
|
|
119
|
+
})
|
|
120
|
+
toast.addEventListener("mouseleave", () => {
|
|
121
|
+
pb?.style.setProperty("animation-play-state", "running")
|
|
122
|
+
toast._timer = setTimeout(() => dismiss(toast), autoClose / 2)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (draggable) {
|
|
127
|
+
let startX = 0
|
|
128
|
+
let currentX = 0
|
|
129
|
+
let dragging = false
|
|
130
|
+
|
|
131
|
+
toast.addEventListener("pointerdown", (e) => {
|
|
132
|
+
startX = e.clientX
|
|
133
|
+
dragging = true
|
|
134
|
+
toast.style.transition = "none"
|
|
135
|
+
toast.setPointerCapture(e.pointerId)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
toast.addEventListener("pointermove", (e) => {
|
|
139
|
+
if (!dragging) return
|
|
140
|
+
currentX = e.clientX - startX
|
|
141
|
+
toast.style.transform = `translateX(${currentX}px)`
|
|
142
|
+
toast.style.opacity = `${1 - Math.abs(currentX) / 150}`
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
toast.addEventListener("pointerup", () => {
|
|
146
|
+
dragging = false
|
|
147
|
+
toast.style.transition = ""
|
|
148
|
+
if (Math.abs(currentX) > 80) {
|
|
149
|
+
dismiss(toast)
|
|
150
|
+
} else {
|
|
151
|
+
toast.style.transform = ""
|
|
152
|
+
toast.style.opacity = ""
|
|
153
|
+
}
|
|
154
|
+
currentX = 0
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isBottom) container.appendChild(toast)
|
|
159
|
+
else container.insertBefore(toast, container.firstChild)
|
|
160
|
+
|
|
161
|
+
return toast
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const TurboToastify = {
|
|
165
|
+
success: (msg, opts) => show(msg, { ...opts, type: TYPES.SUCCESS }),
|
|
166
|
+
error: (msg, opts) => show(msg, { ...opts, type: TYPES.ERROR }),
|
|
167
|
+
warning: (msg, opts) => show(msg, { ...opts, type: TYPES.WARNING }),
|
|
168
|
+
info: (msg, opts) => show(msg, { ...opts, type: TYPES.INFO }),
|
|
169
|
+
show: (msg, opts) => show(msg, opts),
|
|
170
|
+
dismiss,
|
|
171
|
+
POSITIONS,
|
|
172
|
+
THEMES,
|
|
173
|
+
TRANSITIONS,
|
|
174
|
+
defaults,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
window.TurboToastify = TurboToastify
|
|
178
|
+
|
|
179
|
+
export default TurboToastify
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%
|
|
2
|
+
config = Rails.application.config.x.turbo_toastify || {}
|
|
3
|
+
default_position = config[:position] || "top-right"
|
|
4
|
+
default_auto_close = config[:auto_close] || 5000
|
|
5
|
+
default_theme = config[:theme] || "light"
|
|
6
|
+
default_transition = config[:transition] || "slide"
|
|
7
|
+
|
|
8
|
+
type_map = {
|
|
9
|
+
"notice" => "info",
|
|
10
|
+
"success" => "success",
|
|
11
|
+
"alert" => "warning",
|
|
12
|
+
"error" => "error",
|
|
13
|
+
"info" => "info",
|
|
14
|
+
"warning" => "warning",
|
|
15
|
+
}
|
|
16
|
+
%>
|
|
17
|
+
<% flash.each do |flash_type, message| %>
|
|
18
|
+
<% next if flash_type.to_s.start_with?("toast_") %>
|
|
19
|
+
<span
|
|
20
|
+
data-controller="toast"
|
|
21
|
+
data-toast-message-value="<%= message %>"
|
|
22
|
+
data-toast-type-value="<%= type_map.fetch(flash_type.to_s, "default") %>"
|
|
23
|
+
data-toast-position-value="<%= flash[:toast_position] || default_position %>"
|
|
24
|
+
data-toast-auto-close-value="<%= flash[:toast_duration] || default_auto_close %>"
|
|
25
|
+
data-toast-theme-value="<%= flash[:toast_theme] || default_theme %>"
|
|
26
|
+
data-toast-transition-value="<%= flash[:toast_transition] || default_transition %>"
|
|
27
|
+
style="display:none;"
|
|
28
|
+
aria-hidden="true">
|
|
29
|
+
</span>
|
|
30
|
+
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Configure defaults for toasts emitted from the flash partial.
|
|
2
|
+
# These values are read server-side and injected into Stimulus values.
|
|
3
|
+
Rails.application.config.x.turbo_toastify = {
|
|
4
|
+
position: "top-right",
|
|
5
|
+
auto_close: 5000,
|
|
6
|
+
theme: "light",
|
|
7
|
+
transition: "slide"
|
|
8
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: turbo-toastify
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- TurboToastify
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: stimulus-rails
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: turbo-rails
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.0'
|
|
55
|
+
description: A Rails gem that installs a framework-agnostic toast engine with Turbo
|
|
56
|
+
and Stimulus integration.
|
|
57
|
+
email:
|
|
58
|
+
- vasanthakumara117@gmail.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- MIT-LICENSE
|
|
64
|
+
- README.md
|
|
65
|
+
- lib/generators/turbo_toastify/install/install_generator.rb
|
|
66
|
+
- lib/generators/turbo_toastify/install/templates/app/assets/stylesheets/toastify.css
|
|
67
|
+
- lib/generators/turbo_toastify/install/templates/app/javascript/controllers/toast_controller.js
|
|
68
|
+
- lib/generators/turbo_toastify/install/templates/app/javascript/toastify/index.js
|
|
69
|
+
- lib/generators/turbo_toastify/install/templates/app/views/layouts/_turbo_toastify_flash_outlet.html.erb
|
|
70
|
+
- lib/generators/turbo_toastify/install/templates/app/views/shared/_flash.html.erb
|
|
71
|
+
- lib/generators/turbo_toastify/install/templates/config/initializers/turbo_toastify.rb
|
|
72
|
+
- lib/turbo_toastify.rb
|
|
73
|
+
- lib/turbo_toastify/engine.rb
|
|
74
|
+
- lib/turbo_toastify/version.rb
|
|
75
|
+
homepage: https://example.com/turbo-toastify
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata:
|
|
79
|
+
homepage_uri: https://example.com/turbo-toastify
|
|
80
|
+
source_code_uri: https://example.com/turbo-toastify
|
|
81
|
+
changelog_uri: https://example.com/turbo-toastify/CHANGELOG.md
|
|
82
|
+
post_install_message:
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '3.0'
|
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
requirements: []
|
|
97
|
+
rubygems_version: 3.4.19
|
|
98
|
+
signing_key:
|
|
99
|
+
specification_version: 4
|
|
100
|
+
summary: Turbo and Stimulus friendly toast notifications for Rails.
|
|
101
|
+
test_files: []
|