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 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
+ }
@@ -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">&#x2715;</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,2 @@
1
+ <div id="flash-outlet"></div>
2
+ <%= render "shared/flash" %>
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ module TurboToastify
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace TurboToastify
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module TurboToastify
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,5 @@
1
+ require "turbo_toastify/version"
2
+ require "turbo_toastify/engine"
3
+
4
+ module TurboToastify
5
+ end
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: []