toastify 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 90839e2783e1a2fb7011195f55d7fef2d41407a7e6d0559f4d00751315895b2a
4
+ data.tar.gz: 916288f67f30353a27a67de7168dc146f6109a0592c8218f24493655d46dd43a
5
+ SHA512:
6
+ metadata.gz: 16da282d92daea5d5d67d4d70a5ef6db58cec9cf877d4c785cb7b3cf9ef21b805cce68a60da09180c726570e217a80dce45ce66039ab7c81e7d5964c8d6e7a5e
7
+ data.tar.gz: 3e27ec8a357376fb25bc22b64f9ac370b83f39f0bbba2522c031f03e7726fb8b37dd14f9ca70014cc4f230a73133278ed5919d3cc39c22dfaf4328b9b3ef313c
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,120 @@
1
+ # Toastify
2
+
3
+ `toastify` packages a lightweight toast notification system for Rails applications.
4
+
5
+ Compatible with Rails 4+ and also integrates seamlessly with Turbo Stream requests.
6
+ It can also be used directly in JavaScript (e.g., `Toastify.success("Saved!")`).
7
+
8
+ ### Example Images
9
+
10
+ <img width="346" height="85" alt="Light Theme Example" src="https://github.com/user-attachments/assets/b7a3123a-89a3-4f49-97ff-aa25f4b870d0" />
11
+
12
+ <img width="338" height="87" alt="Dark Theme Example" src="https://github.com/user-attachments/assets/65671ac6-91d7-471f-9c03-6458023f3ae2" />
13
+
14
+ <img width="339" height="83" alt="Colored Theme Example" src="https://github.com/user-attachments/assets/1c99ffdb-dc3c-4fef-aa16-b7eff08e0cde" />
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem "toastify"
22
+ ```
23
+
24
+ Then run:
25
+
26
+ ```bash
27
+ bundle install
28
+ bin/rails generate toastify:install
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ Simply add the `<%= toastify_tag %>` tag to your layout file `app/views/layouts/application.html.erb`. You do **not** need to install any JavaScript or CSS manually.
34
+
35
+ ```erb
36
+ <body>
37
+ <%= yield %>
38
+ <%= toastify_tag %>
39
+ </body>
40
+ ```
41
+
42
+ Then, assign ordinary flash messages in your controllers to trigger toasts:
43
+
44
+ ```ruby
45
+ def create
46
+ @post = Post.create!(post_params)
47
+ redirect_to posts_path, success: "Post created successfully!"
48
+ end
49
+ ```
50
+
51
+ If you are using Turbo Streams:
52
+
53
+ ```ruby
54
+ def update
55
+ @post.update!(post_params)
56
+ flash.now[:success] = "Updated successfully!"
57
+ respond_to do |format|
58
+ format.js
59
+ format.turbo_stream
60
+ end
61
+ end
62
+ ```
63
+
64
+ ## Configuration
65
+
66
+ ### Global Defaults
67
+
68
+ You can configure global defaults in the generated initializer file `config/initializers/toastify.rb`:
69
+
70
+ ```ruby
71
+ Rails.application.config.toastify = {
72
+ position: "top-right", # top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
73
+ auto_close: 5000, # duration in milliseconds
74
+ theme: "light", # light, dark, colored
75
+ transition: "slide", # slide, bounce, zoom, flip, fade
76
+ close_button: true, # true, false
77
+ pause_on_hover: true, # true, false
78
+ draggable: true # true, false
79
+ }
80
+ ```
81
+
82
+ ### Per-Request Overrides
83
+
84
+ You can also customize the toast behaviors on a per-request basis by setting these specific `flash` keys in your controllers:
85
+
86
+ | Property | Type | Default | Description |
87
+ | --- | --- | --- | --- |
88
+ | `flash[:toast_position]` | `String` | `"top-right"` | Position of the toast (`top-right`, `top-left`, `top-center`, `bottom-right`, `bottom-left`, `bottom-center`). |
89
+ | `flash[:toast_duration]` | `Integer` | `5000` | Duration in milliseconds before the toast auto-closes. |
90
+ | `flash[:toast_theme]` | `String` | `"light"` | Visual theme of the toast (`light`, `dark`, or `colored`). |
91
+ | `flash[:toast_transition]` | `String` | `"slide"` | Animation type (`slide`, `zoom`, `flip`, `bounce`). |
92
+ | `flash[:toast_close_button]`| `Boolean` | `true` | Show or hide the close button. |
93
+ | `flash[:toast_pause_on_hover]`| `Boolean` | `true` | Pause auto-closing when the mouse hovers over the toast. |
94
+ | `flash[:toast_draggable]` | `Boolean` | `true` | Allow the toast to be dragged to close. |
95
+
96
+ ## JavaScript usage
97
+
98
+ Toastify exposes a global object if you wish to trigger toasts manually from your JavaScript code:
99
+
100
+ ```javascript
101
+ Toastify.success("Saved!")
102
+ Toastify.error("Failed to save", { autoClose: 8000, theme: "colored" })
103
+ Toastify.info("Syncing...", { position: "bottom-center", transition: "zoom" })
104
+ Toastify.warning("Low storage", { theme: "dark", transition: "flip" })
105
+ ```
106
+
107
+ ### JavaScript Configuration Options
108
+
109
+ When calling the JavaScript methods (e.g., `Toastify.success()`, `Toastify.show()`), you can pass an `options` object as the second argument with the following properties:
110
+
111
+ | Property | Type | Default | Description |
112
+ | --- | --- | --- | --- |
113
+ | `position` | `String` | `"top-right"` | Position of the toast (`top-right`, `top-left`, `top-center`, `bottom-right`, `bottom-left`, `bottom-center`). |
114
+ | `autoClose` | `Number` | `5000` | Duration in milliseconds before the toast auto-closes. |
115
+ | `theme` | `String` | `"light"` | Visual theme of the toast (`light`, `dark`, or `colored`). |
116
+ | `transition` | `String` | `"slide"` | Animation type (`slide`, `zoom`, `flip`, `bounce`). |
117
+ | `closeButton` | `Boolean` | `true` | Show or hide the close button. |
118
+ | `pauseOnHover` | `Boolean` | `true` | Pause auto-closing when the mouse hovers over the toast. |
119
+ | `draggable` | `Boolean` | `true` | Allow the toast to be dragged to close. |
120
+ | `type` | `String` | `"default"` | Toast styling type (`success`, `error`, `warning`, `info`, `default`). Automatically applied when using helper methods like `.success()`. |
@@ -0,0 +1,13 @@
1
+ require "rails/generators"
2
+
3
+ module Toastify
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def copy_initializer
9
+ copy_file "toastify.rb", "config/initializers/toastify.rb"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.config.toastify = {
4
+ # position: "top-right", # top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
5
+ # auto_close: 5000, # duration in milliseconds
6
+ # theme: "light", # light, dark, colored
7
+ # transition: "slide", # slide, bounce, zoom, flip, fade
8
+ # close_button: true, # true, false
9
+ # pause_on_hover: true, # true, false
10
+ # draggable: true # true, false
11
+ }
@@ -0,0 +1,93 @@
1
+ module Toastify
2
+ module ApplicationHelper
3
+ def self.toastify_css
4
+ @toastify_css ||= begin
5
+ path = File.expand_path("assets/toastify.css", __dir__)
6
+ File.read(path)
7
+ end
8
+ end
9
+
10
+ def self.toastify_js
11
+ @toastify_js ||= begin
12
+ path = File.expand_path("assets/toastify.js", __dir__)
13
+ File.read(path)
14
+ end
15
+ end
16
+
17
+ def toastify_tag
18
+ html = []
19
+
20
+ # On full HTML page loads, render the infrastructure
21
+ unless request.format.turbo_stream?
22
+ css_content = Toastify::ApplicationHelper.toastify_css
23
+ js_content = Toastify::ApplicationHelper.toastify_js
24
+
25
+ html << "<style>#{css_content}</style>"
26
+ html << "<div id=\"toast-container-root\" data-turbo-permanent></div>"
27
+ html << "<div id=\"flash-outlet\" data-turbo-cache=\"false\"></div>"
28
+ html << "<script type=\"module\">"
29
+ html << js_content
30
+ html << "</script>"
31
+ end
32
+
33
+ # For both HTML and Turbo Streams, execute the toasts if present
34
+ if (scripts = toastify_script_tag).present?
35
+ html << scripts
36
+ end
37
+
38
+ html.join("\n").html_safe
39
+ end
40
+
41
+ def toastify_script_tag
42
+ config = Rails.application.config.try(:toastify) || {}
43
+ default_position = config[:position] || "top-right"
44
+ default_auto_close = config[:auto_close] || 5000
45
+ default_theme = config[:theme] || "light"
46
+ default_transition = config[:transition] || "slide"
47
+ default_close_button = config.key?(:close_button) ? config[:close_button] : true
48
+ default_pause_on_hover = config.key?(:pause_on_hover) ? config[:pause_on_hover] : true
49
+ default_draggable = config.key?(:draggable) ? config[:draggable] : true
50
+
51
+ type_map = {
52
+ "notice" => "info",
53
+ "success" => "success",
54
+ "alert" => "warning",
55
+ "error" => "error",
56
+ "info" => "info",
57
+ "warning" => "warning",
58
+ }
59
+
60
+ script_lines = []
61
+ flash.each do |flash_type, message|
62
+ next if flash_type.to_s.start_with?("toast_")
63
+
64
+ type = type_map.fetch(flash_type.to_s, "default")
65
+ position = flash[:toast_position] || default_position
66
+ auto_close = flash[:toast_duration] || default_auto_close
67
+ theme = flash[:toast_theme] || default_theme
68
+ transition = flash[:toast_transition] || default_transition
69
+ close_button = flash[:toast_close_button].nil? ? default_close_button : flash[:toast_close_button]
70
+ pause_on_hover = flash[:toast_pause_on_hover].nil? ? default_pause_on_hover : flash[:toast_pause_on_hover]
71
+ draggable = flash[:toast_draggable].nil? ? default_draggable : flash[:toast_draggable]
72
+
73
+ safe_message = j(message.to_s)
74
+
75
+ script_lines << "window.Toastify && window.Toastify.show('#{safe_message}', { type: '#{j(type.to_s)}', position: '#{j(position.to_s)}', autoClose: #{auto_close.to_i}, theme: '#{j(theme.to_s)}', transition: '#{j(transition.to_s)}', closeButton: #{!!close_button}, pauseOnHover: #{!!pause_on_hover}, draggable: #{!!draggable} });"
76
+ end
77
+
78
+ # Discard so it doesn't show up again
79
+ flash.keys.reject { |type| type.to_s.start_with?("toast_") }.each do |type|
80
+ flash.discard(type)
81
+ end
82
+
83
+ return nil if script_lines.empty?
84
+
85
+ html = []
86
+ html << "<script type=\"module\">"
87
+ html << " " + script_lines.join("\n ")
88
+ html << "</script>"
89
+
90
+ html.join("\n").html_safe
91
+ end
92
+ end
93
+ 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
+ }
@@ -0,0 +1,185 @@
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
+
49
+ // Use the permanent root if available, otherwise fall back to body
50
+ const root = document.getElementById("toast-container-root") || document.body;
51
+ root.appendChild(el);
52
+
53
+ containers[position] = el;
54
+ return el;
55
+ }
56
+
57
+ function getEnterAnim(transition, position) {
58
+ const map = ENTER_ANIMS[transition] || ENTER_ANIMS.slide
59
+ if (position.includes("right")) return map.right
60
+ if (position.includes("left")) return map.left
61
+ if (position.startsWith("bottom")) return map.bottom
62
+ return map.center
63
+ }
64
+
65
+ function dismiss(toast) {
66
+ if (!toast || toast._dismissing) return
67
+ toast._dismissing = true
68
+ clearTimeout(toast._timer)
69
+
70
+ toast.classList.add("Toastify__toast--exit")
71
+ toast.addEventListener("animationend", () => {
72
+ toast.remove()
73
+ }, { once: true })
74
+ }
75
+
76
+ function show(message, options = {}) {
77
+ const opts = { ...defaults, ...options }
78
+ const { position, autoClose, theme, transition, closeButton, pauseOnHover, draggable, type = TYPES.DEFAULT } = opts
79
+
80
+ const container = getContainer(position)
81
+ const isBottom = position.startsWith("bottom")
82
+
83
+ const toast = document.createElement("div")
84
+ toast.className = [
85
+ "Toastify__toast",
86
+ `Toastify__toast--${type}`,
87
+ theme === THEMES.DARK ? "Toastify__toast--dark" : "",
88
+ theme === THEMES.COLORED ? "Toastify__toast--colored" : "",
89
+ ].filter(Boolean).join(" ")
90
+
91
+ const enterAnim = getEnterAnim(transition, position)
92
+ toast.style.setProperty("--enter-anim", enterAnim)
93
+ toast.classList.add("Toastify__toast--enter")
94
+
95
+ const progressBar = autoClose
96
+ ? `<div class="Toastify__progress-bar Toastify__progress-bar--${type}" style="animation-duration:${autoClose}ms;"></div>`
97
+ : ""
98
+
99
+ const closeBtn = closeButton
100
+ ? `<button class="Toastify__close-button" aria-label="Close toast">&#x2715;</button>`
101
+ : ""
102
+
103
+ toast.innerHTML = `
104
+ <div class="Toastify__toast-body">
105
+ <div class="Toastify__toast-icon">${ICONS[type] || ICONS.default}</div>
106
+ <div class="Toastify__toast-message">${message}</div>
107
+ </div>
108
+ ${closeBtn}
109
+ ${progressBar}
110
+ `
111
+
112
+ toast.querySelector(".Toastify__close-button")?.addEventListener("click", () => dismiss(toast))
113
+
114
+ if (autoClose) {
115
+ toast._timer = setTimeout(() => dismiss(toast), autoClose)
116
+ }
117
+
118
+ if (pauseOnHover && autoClose) {
119
+ const pb = toast.querySelector(".Toastify__progress-bar")
120
+ toast.addEventListener("mouseenter", () => {
121
+ clearTimeout(toast._timer)
122
+ pb?.style.setProperty("animation-play-state", "paused")
123
+ })
124
+ toast.addEventListener("mouseleave", () => {
125
+ pb?.style.setProperty("animation-play-state", "running")
126
+ toast._timer = setTimeout(() => dismiss(toast), autoClose / 2)
127
+ })
128
+ }
129
+
130
+ if (draggable) {
131
+ let startX = 0
132
+ let currentX = 0
133
+ let dragging = false
134
+
135
+ toast.addEventListener("pointerdown", (e) => {
136
+ if (e.target.closest(".Toastify__close-button")) return;
137
+ startX = e.clientX
138
+ dragging = true
139
+ toast.style.transition = "none"
140
+ toast.setPointerCapture(e.pointerId)
141
+ })
142
+
143
+ toast.addEventListener("pointermove", (e) => {
144
+ if (!dragging) return
145
+ currentX = e.clientX - startX
146
+ toast.style.transform = `translateX(${currentX}px)`
147
+ toast.style.opacity = `${1 - Math.abs(currentX) / 150}`
148
+ })
149
+
150
+ toast.addEventListener("pointerup", () => {
151
+ if (!dragging) return
152
+ dragging = false
153
+ toast.style.transition = ""
154
+ if (Math.abs(currentX) > 80) {
155
+ dismiss(toast)
156
+ } else {
157
+ toast.style.transform = ""
158
+ toast.style.opacity = ""
159
+ }
160
+ currentX = 0
161
+ })
162
+ }
163
+
164
+ if (isBottom) container.appendChild(toast)
165
+ else container.insertBefore(toast, container.firstChild)
166
+
167
+ return toast
168
+ }
169
+
170
+ const Toastify = {
171
+ success: (msg, opts) => show(msg, { ...opts, type: TYPES.SUCCESS }),
172
+ error: (msg, opts) => show(msg, { ...opts, type: TYPES.ERROR }),
173
+ warning: (msg, opts) => show(msg, { ...opts, type: TYPES.WARNING }),
174
+ info: (msg, opts) => show(msg, { ...opts, type: TYPES.INFO }),
175
+ show: (msg, opts) => show(msg, opts),
176
+ dismiss,
177
+ POSITIONS,
178
+ THEMES,
179
+ TRANSITIONS,
180
+ defaults,
181
+ }
182
+
183
+ window.Toastify = Toastify
184
+
185
+ export default Toastify
@@ -0,0 +1,29 @@
1
+ module Toastify
2
+ module Controller
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ after_action :append_toastify_to_stream
7
+ end
8
+
9
+ private
10
+
11
+ def append_toastify_to_stream
12
+ return unless request.format.turbo_stream?
13
+
14
+ toast_flashes = flash.reject { |type, _| type.to_s.start_with?("toast_") }
15
+ return unless toast_flashes.any?
16
+
17
+ script_content = helpers.toastify_script_tag
18
+ return if script_content.blank?
19
+
20
+ stream_tag = turbo_stream.append("flash-outlet", script_content)
21
+
22
+ if response.body.is_a?(String)
23
+ response.body += stream_tag.to_s
24
+ else
25
+ response.body = response.body.to_s + stream_tag.to_s
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ module Toastify
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Toastify
4
+
5
+ initializer "toastify.helper" do
6
+ ActiveSupport.on_load(:action_view) do
7
+ include Toastify::ApplicationHelper
8
+ end
9
+ end
10
+
11
+ initializer "toastify.controller" do
12
+ ActiveSupport.on_load(:action_controller) do
13
+ include Toastify::Controller
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Toastify
2
+ VERSION = "1.0.0"
3
+ end
data/lib/toastify.rb ADDED
@@ -0,0 +1,7 @@
1
+ require "toastify/version"
2
+ require "toastify/engine"
3
+ require "toastify/application_helper"
4
+ require "toastify/controller"
5
+
6
+ module Toastify
7
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: toastify
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - vasanthakumar-a
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-06 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: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ description: A lightweight Rails gem providing a toast notification system. Compatible
28
+ with Rails 4, 5, 6, 7+ and also integrates seamlessly with Turbo Stream requests.
29
+ email:
30
+ - vasanthakumara117@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - MIT-LICENSE
36
+ - README.md
37
+ - lib/generators/toastify/install/install_generator.rb
38
+ - lib/generators/toastify/install/templates/toastify.rb
39
+ - lib/toastify.rb
40
+ - lib/toastify/application_helper.rb
41
+ - lib/toastify/assets/toastify.css
42
+ - lib/toastify/assets/toastify.js
43
+ - lib/toastify/controller.rb
44
+ - lib/toastify/engine.rb
45
+ - lib/toastify/version.rb
46
+ homepage: https://github.com/vasanthakumar-a/toastify
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ source_code_uri: https://github.com/vasanthakumar-a/toastify
51
+ changelog_uri: https://github.com/vasanthakumar-a/toastify/blob/main/CHANGELOG.md
52
+ bug_tracker_uri: https://github.com/vasanthakumar-a/toastify/issues
53
+ documentation_uri: https://github.com/vasanthakumar-a/toastify#readme
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 1.9.3
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.4.19
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Toast notifications for Rails.
73
+ test_files: []