hcg-toast 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HTML Code Generator (https://www.html-code-generator.com/)
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.
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # hcg-toast
2
+
3
+ A lightweight, zero-dependency JavaScript toast / notification library. Show success, error, warning, and info messages with auto-dismiss, sticky toasts, six screen positions, stack limits, pause on hover, optional progress bars, and custom icons.
4
+
5
+ ![hcg-toast — JavaScript toast notifications with success, error, info, and warning types](https://raw.githubusercontent.com/html-code-generator/hcg-toast/main/toast-notification.png)
6
+
7
+ - Zero dependencies, ~6 KB unminified
8
+ - UMD build - works with `<script>` tags, CommonJS, and bundlers
9
+ - Global API: `toast()` with shorthand helpers and `toast.create()` for custom instances
10
+ - Custom icons via `icon`, `iconHtml`, or hide with `icon: false`
11
+ - Accessible: `role="alert"` / `role="status"` with `aria-live="off"` on non-errors (avoids browser freezes during rapid bursts); errors use `aria-live="assertive"`
12
+ - TypeScript definitions included
13
+
14
+ ## Links
15
+
16
+ - **Documentation & live demo:** [html-code-generator.com/javascript/toast-notification](https://www.html-code-generator.com/javascript/toast-notification)
17
+ - **npm:** [npmjs.com/package/hcg-toast](https://www.npmjs.com/package/hcg-toast)
18
+ - **GitHub:** [github.com/html-code-generator/hcg-toast](https://github.com/html-code-generator/hcg-toast)
19
+
20
+ ## Demo
21
+
22
+ Try the [live demo and full documentation](https://www.html-code-generator.com/javascript/toast-notification), or open `index.html` from the GitHub repo over a local server for a working example.
23
+
24
+ ```bash
25
+ npx serve .
26
+ ```
27
+
28
+ ## Installation
29
+
30
+ Install from npm:
31
+
32
+ ```bash
33
+ npm install hcg-toast
34
+ ```
35
+
36
+ Or download `hcg-toast.js` and `hcg-toast.css` and include them on your page:
37
+
38
+ ```html
39
+ <link rel="stylesheet" href="hcg-toast.css">
40
+ <script src="hcg-toast.js"></script>
41
+ ```
42
+
43
+ In a module / Node environment:
44
+
45
+ ```js
46
+ require('hcg-toast/hcg-toast.css');
47
+ const toast = require('hcg-toast');
48
+ ```
49
+
50
+ Or with ESM:
51
+
52
+ ```js
53
+ import toast from 'hcg-toast';
54
+ import 'hcg-toast/hcg-toast.css';
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ### Singleton (default)
60
+
61
+ ```html
62
+ <link rel="stylesheet" href="hcg-toast.css">
63
+ <script src="hcg-toast.js"></script>
64
+ <script>
65
+ toast.success('Changes saved!');
66
+ toast.error('Upload failed', { title: 'Error', duration: 6000 });
67
+ toast.dismiss(id);
68
+ toast.dismissAll();
69
+ toast.configure({ position: 'bottom-right', maxToasts: 5 });
70
+ </script>
71
+ ```
72
+
73
+ ### Typed helpers
74
+
75
+ Shorthand methods set the toast type for you so you do not need to pass `type` on every call. Each helper uses the matching icon and accent color.
76
+
77
+ ```js
78
+ toast.success('Profile updated.');
79
+ toast.error('Could not save changes.');
80
+ toast.warning('You have unsaved edits.');
81
+ toast.info('Sync complete.');
82
+
83
+ // equivalent to toast(message, { type: 'success' })
84
+ toast('Saved!', { type: 'success', title: 'Done' });
85
+ ```
86
+
87
+ Each call to `show()` (or a typed helper) returns a numeric toast `id` you can pass to `dismiss(id)`.
88
+
89
+ ### Custom instance
90
+
91
+ ```js
92
+ const bottomToast = toast.create({
93
+ position: 'bottom-center',
94
+ maxToasts: 3,
95
+ });
96
+
97
+ bottomToast.info('Processing…', { duration: 0, closable: true });
98
+ bottomToast.dismissAll();
99
+ bottomToast.destroy();
100
+ ```
101
+
102
+ ### Custom icons
103
+
104
+ By default each type shows a built-in icon (info, success, warning, error). Override per toast or set a default on the instance:
105
+
106
+ ```js
107
+ // Emoji or text icon
108
+ toast.success('Backup complete.', { icon: '💾' });
109
+
110
+ // SVG or HTML icon (trusted markup only)
111
+ toast.info('Syncing files…', {
112
+ iconHtml: '<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 3a7 7 0 100 14A7 7 0 0010 3z"/></svg>'
113
+ });
114
+
115
+ // Hide the icon column
116
+ toast('Plain message.', { icon: false });
117
+
118
+ // Default icon for all toasts on an instance
119
+ const stars = toast.create({ icon: '⭐' });
120
+ stars.info('You have a new message.');
121
+ ```
122
+
123
+ `iconHtml` takes precedence over `icon` when both are set. Only pass trusted HTML to `iconHtml`.
124
+
125
+ ## Options
126
+
127
+ | Option | Type | Default | Description |
128
+ | ---------------- | ----------------------------------------- | ------------- | -------------------------------------------------------- |
129
+ | `message` | `string` | required | Main body text |
130
+ | `title` | `string` | - | Optional heading above the message |
131
+ | `type` | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` | Toast variant (icon and accent color) |
132
+ | `duration` | `number` | `4000` | Auto-dismiss delay in ms; `0` = sticky until dismissed |
133
+ | `closable` | `boolean` | `true` | Show a close button |
134
+ | `pauseOnHover` | `boolean` | `true` | Pause the auto-dismiss timer while hovered |
135
+ | `position` | see below | `'top-right'` | Screen position for this instance's container |
136
+ | `maxToasts` | `number` | `5` | Max visible toasts; oldest dismissed when exceeded (per call or instance default) |
137
+ | `showProgress` | `boolean` | `true` | Show a progress bar when `duration > 0` |
138
+ | `icon` | `string \| false` | type default | Custom icon text or emoji; `false` hides the icon |
139
+ | `iconHtml` | `string` | - | Custom icon HTML (e.g. SVG); overrides `icon` |
140
+ | `ariaLive` | `'off' \| 'polite' \| 'assertive'` | `'off'` | Live region on each toast; errors default to `'assertive'` when unset |
141
+ | `onShow` | `(toast) => void` | - | Called after the enter class is applied (animation start) |
142
+ | `onDismiss` | `(toast, reason) => void` | - | Called when removed; `reason`: `'timeout'`, `'close'`, `'api'` |
143
+
144
+ **Positions:** `top-left`, `top-center`, `top-right`, `bottom-left`, `bottom-center`, `bottom-right`
145
+
146
+ ## Instance methods
147
+
148
+ | Method | Returns | Description |
149
+ | --------------- | -------------- | ----------------------------------------------------- |
150
+ | `show(msg, opts)` | `number \| null` | Show a toast; returns id |
151
+ | `success(msg, opts)` | `number \| null` | Shorthand for `type: 'success'` |
152
+ | `error(msg, opts)` | `number \| null` | Shorthand for `type: 'error'` |
153
+ | `warning(msg, opts)` | `number \| null` | Shorthand for `type: 'warning'` |
154
+ | `info(msg, opts)` | `number \| null` | Shorthand for `type: 'info'` |
155
+ | `dismiss(id)` | `void` | Remove one toast with exit animation |
156
+ | `dismissAll()` | `void` | Remove all toasts immediately |
157
+ | `configure(opts)` | `void` | Merge default options for this instance |
158
+ | `destroy()` | `void` | Dismiss all, remove container, detach instance |
159
+
160
+ The default singleton exposes the same methods on `toast` itself, plus `toast.create()`. Calling `toast.destroy()` removes the default instance so the next call recreates it.
161
+
162
+ ## Styling
163
+
164
+ All default styles live in `hcg-toast.css`, using plain single-class selectors and CSS custom properties on `.hcg-toast-container`. Edit that file directly, or override any rule from your own stylesheet (loaded after it). Available class hooks:
165
+
166
+ - `.hcg-toast-container` - fixed-position stack host (position modifiers: `--top-right`, `--bottom-center`, etc.)
167
+ - `.hcg-toast-item` - individual toast (type modifiers: `--info`, `--success`, `--warning`, `--error`)
168
+ - `.hcg-toast-icon`, `.hcg-toast-body`, `.hcg-toast-title`, `.hcg-toast-message`
169
+ - `.hcg-toast-close` - dismiss button
170
+ - `.hcg-toast-progress-track` / `.hcg-toast-progress` - auto-dismiss track and shrinking bar (shown when `duration > 0` and `showProgress: true`)
171
+ - `.hcg-toast-item--enter` / `.hcg-toast-item--exit` - animation states
172
+
173
+ ```css
174
+ .hcg-toast-container {
175
+ --hcg-toast-radius: 8px;
176
+ --hcg-toast-success-accent: #10b981;
177
+ }
178
+ ```
179
+
180
+ ## Multiple instances
181
+
182
+ Create as many toast containers as you need. Each call to `toast.create()` returns a fully independent instance with its own position, defaults, stack, and API. The shared `hcg-toast.css` styles them all, so you only link it once.
183
+
184
+ ```js
185
+ const topRight = toast.create({ position: 'top-right' });
186
+ const bottomCenter = toast.create({ position: 'bottom-center', maxToasts: 2 });
187
+
188
+ topRight.success('Saved!');
189
+ bottomCenter.info('Syncing…', { duration: 0 });
190
+
191
+ bottomCenter.destroy();
192
+ ```
193
+
194
+ Notes when running several instances:
195
+
196
+ - Each instance injects its own `.hcg-toast-container` when first used.
197
+ - `toast.configure()` only affects the default singleton, not custom instances.
198
+ - Call each instance's `destroy()` when removing it permanently.
199
+
200
+ ## Browser support
201
+
202
+ Works in all modern browsers that support CSS transitions and `requestAnimationFrame` (Chrome, Firefox, Safari, Edge). Respects `prefers-reduced-motion: reduce` by disabling enter/exit and progress animations (the progress track stays visible for timed toasts).
203
+
204
+ ## License
205
+
206
+ MIT - HTML Code Generator (https://www.html-code-generator.com/)
package/hcg-toast.css ADDED
@@ -0,0 +1,254 @@
1
+ /*!
2
+ * hcg-toast v1.0.0
3
+ * Default styles for the toast / notification library.
4
+ * Edit freely or override these rules from your own stylesheet.
5
+ * @see https://www.html-code-generator.com/javascript/toast-notification
6
+ * @license MIT
7
+ */
8
+
9
+ .hcg-toast-container {
10
+ --hcg-toast-gap: 10px;
11
+ --hcg-toast-width: min(360px, calc(100vw - 32px));
12
+ --hcg-toast-radius: 12px;
13
+ --hcg-toast-shadow: 0 10px 30px rgba(15, 23, 42, 0.12);
14
+ --hcg-toast-z: 9999;
15
+ --hcg-toast-bg: #ffffff;
16
+ --hcg-toast-border: #e5e7eb;
17
+ --hcg-toast-text: #1f2937;
18
+ --hcg-toast-title: #111827;
19
+ --hcg-toast-muted: #6b7280;
20
+ --hcg-toast-info-accent: #6366f1;
21
+ --hcg-toast-success-accent: #059669;
22
+ --hcg-toast-warning-accent: #d97706;
23
+ --hcg-toast-error-accent: #dc2626;
24
+
25
+ position: fixed;
26
+ z-index: var(--hcg-toast-z);
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: var(--hcg-toast-gap);
30
+ width: var(--hcg-toast-width);
31
+ pointer-events: none;
32
+ }
33
+
34
+ .hcg-toast-container--top-left {
35
+ top: 20px;
36
+ left: 20px;
37
+ align-items: flex-start;
38
+ }
39
+
40
+ .hcg-toast-container--top-center {
41
+ top: 20px;
42
+ left: 50%;
43
+ transform: translateX(-50%);
44
+ align-items: center;
45
+ }
46
+
47
+ .hcg-toast-container--top-right {
48
+ top: 20px;
49
+ right: 20px;
50
+ align-items: flex-end;
51
+ }
52
+
53
+ .hcg-toast-container--bottom-left {
54
+ bottom: 20px;
55
+ left: 20px;
56
+ flex-direction: column-reverse;
57
+ align-items: flex-start;
58
+ }
59
+
60
+ .hcg-toast-container--bottom-center {
61
+ bottom: 20px;
62
+ left: 50%;
63
+ transform: translateX(-50%);
64
+ flex-direction: column-reverse;
65
+ align-items: center;
66
+ }
67
+
68
+ .hcg-toast-container--bottom-right {
69
+ bottom: 20px;
70
+ right: 20px;
71
+ flex-direction: column-reverse;
72
+ align-items: flex-end;
73
+ }
74
+
75
+ .hcg-toast-item {
76
+ position: relative;
77
+ display: flex;
78
+ align-items: flex-start;
79
+ gap: 12px;
80
+ width: 100%;
81
+ padding: 14px 16px 16px;
82
+ background: var(--hcg-toast-bg);
83
+ border: 1px solid var(--hcg-toast-border);
84
+ border-radius: var(--hcg-toast-radius);
85
+ box-shadow: var(--hcg-toast-shadow);
86
+ color: var(--hcg-toast-text);
87
+ overflow: hidden;
88
+ pointer-events: auto;
89
+ opacity: 0;
90
+ transform: translateY(-8px) scale(0.98);
91
+ transition: opacity 0.25s ease, transform 0.25s ease;
92
+ }
93
+
94
+ .hcg-toast-container--bottom-left .hcg-toast-item,
95
+ .hcg-toast-container--bottom-center .hcg-toast-item,
96
+ .hcg-toast-container--bottom-right .hcg-toast-item {
97
+ transform: translateY(8px) scale(0.98);
98
+ }
99
+
100
+ .hcg-toast-item--enter {
101
+ opacity: 1;
102
+ transform: translateY(0) scale(1);
103
+ }
104
+
105
+ .hcg-toast-item--exit {
106
+ opacity: 0;
107
+ transform: translateY(-8px) scale(0.98);
108
+ }
109
+
110
+ .hcg-toast-container--bottom-left .hcg-toast-item--exit,
111
+ .hcg-toast-container--bottom-center .hcg-toast-item--exit,
112
+ .hcg-toast-container--bottom-right .hcg-toast-item--exit {
113
+ transform: translateY(8px) scale(0.98);
114
+ }
115
+
116
+ .hcg-toast-icon {
117
+ flex-shrink: 0;
118
+ width: 22px;
119
+ height: 22px;
120
+ display: inline-flex;
121
+ align-items: center;
122
+ justify-content: center;
123
+ border-radius: 999px;
124
+ font-size: 0.78rem;
125
+ font-weight: 700;
126
+ line-height: 1;
127
+ margin-top: 1px;
128
+ }
129
+
130
+ .hcg-toast-item--info .hcg-toast-icon {
131
+ color: var(--hcg-toast-info-accent);
132
+ background: #eef2ff;
133
+ }
134
+
135
+ .hcg-toast-item--success .hcg-toast-icon {
136
+ color: var(--hcg-toast-success-accent);
137
+ background: #ecfdf5;
138
+ }
139
+
140
+ .hcg-toast-item--warning .hcg-toast-icon {
141
+ color: var(--hcg-toast-warning-accent);
142
+ background: #fffbeb;
143
+ }
144
+
145
+ .hcg-toast-item--error .hcg-toast-icon {
146
+ color: var(--hcg-toast-error-accent);
147
+ background: #fef2f2;
148
+ }
149
+
150
+ .hcg-toast-icon svg {
151
+ display: block;
152
+ width: 14px;
153
+ height: 14px;
154
+ }
155
+
156
+ .hcg-toast-body {
157
+ flex: 1;
158
+ min-width: 0;
159
+ }
160
+
161
+ .hcg-toast-title {
162
+ font-size: 0.88rem;
163
+ font-weight: 700;
164
+ color: var(--hcg-toast-title);
165
+ margin-bottom: 2px;
166
+ line-height: 1.35;
167
+ }
168
+
169
+ .hcg-toast-message {
170
+ font-size: 0.86rem;
171
+ line-height: 1.55;
172
+ color: var(--hcg-toast-text);
173
+ word-wrap: break-word;
174
+ }
175
+
176
+ .hcg-toast-close {
177
+ flex-shrink: 0;
178
+ background: none;
179
+ border: none;
180
+ color: var(--hcg-toast-muted);
181
+ cursor: pointer;
182
+ font-size: 1.15rem;
183
+ line-height: 1;
184
+ padding: 2px 4px;
185
+ border-radius: 6px;
186
+ transition: color 0.15s, background 0.15s;
187
+ }
188
+
189
+ .hcg-toast-close:hover {
190
+ color: #374151;
191
+ background: #f3f4f6;
192
+ }
193
+
194
+ .hcg-toast-close:focus-visible {
195
+ outline: none;
196
+ box-shadow: 0 0 0 3px #c7d2fe;
197
+ }
198
+
199
+ .hcg-toast-progress-track {
200
+ position: absolute;
201
+ left: 0;
202
+ right: 0;
203
+ bottom: 0;
204
+ z-index: 2;
205
+ height: 5px;
206
+ background: rgba(15, 23, 42, 0.08);
207
+ overflow: hidden;
208
+ pointer-events: none;
209
+ border-radius: 0 0 calc(var(--hcg-toast-radius) - 1px) calc(var(--hcg-toast-radius) - 1px);
210
+ }
211
+
212
+ .hcg-toast-progress {
213
+ height: 100%;
214
+ width: 100%;
215
+ transform-origin: left center;
216
+ pointer-events: none;
217
+ }
218
+
219
+ .hcg-toast-item--info .hcg-toast-progress {
220
+ background: var(--hcg-toast-info-accent);
221
+ }
222
+
223
+ .hcg-toast-item--success .hcg-toast-progress {
224
+ background: var(--hcg-toast-success-accent);
225
+ }
226
+
227
+ .hcg-toast-item--warning .hcg-toast-progress {
228
+ background: var(--hcg-toast-warning-accent);
229
+ }
230
+
231
+ .hcg-toast-item--error .hcg-toast-progress {
232
+ background: var(--hcg-toast-error-accent);
233
+ }
234
+
235
+ @keyframes hcg-toast-progress-shrink {
236
+ from { transform: scaleX(1); }
237
+ to { transform: scaleX(0); }
238
+ }
239
+
240
+ @media (prefers-reduced-motion: reduce) {
241
+ .hcg-toast-item {
242
+ transition: none;
243
+ transform: none;
244
+ }
245
+
246
+ .hcg-toast-item--enter,
247
+ .hcg-toast-item--exit {
248
+ transform: none;
249
+ }
250
+
251
+ .hcg-toast-progress {
252
+ animation: none;
253
+ }
254
+ }
package/hcg-toast.d.ts ADDED
@@ -0,0 +1,81 @@
1
+ export type HcgToastType = 'info' | 'success' | 'warning' | 'error';
2
+
3
+ export type HcgToastPosition =
4
+ | 'top-left'
5
+ | 'top-center'
6
+ | 'top-right'
7
+ | 'bottom-left'
8
+ | 'bottom-center'
9
+ | 'bottom-right';
10
+
11
+ export type HcgToastDismissReason = 'timeout' | 'close' | 'api';
12
+
13
+ export type HcgToastAriaLive = 'off' | 'polite' | 'assertive';
14
+
15
+ export interface HcgToastShown {
16
+ id: number;
17
+ message: string;
18
+ title?: string;
19
+ type: HcgToastType;
20
+ duration: number;
21
+ closable: boolean;
22
+ pauseOnHover: boolean;
23
+ position: HcgToastPosition;
24
+ maxToasts: number;
25
+ showProgress: boolean;
26
+ icon?: string | false;
27
+ iconHtml?: string;
28
+ ariaLive?: HcgToastAriaLive;
29
+ onShow?: (toast: HcgToastShown) => void;
30
+ onDismiss?: (toast: HcgToastShown, reason: HcgToastDismissReason) => void;
31
+ }
32
+
33
+ export interface HcgToastOptions {
34
+ message?: string;
35
+ title?: string;
36
+ type?: HcgToastType;
37
+ duration?: number;
38
+ closable?: boolean;
39
+ pauseOnHover?: boolean;
40
+ position?: HcgToastPosition;
41
+ maxToasts?: number;
42
+ showProgress?: boolean;
43
+ icon?: string | false;
44
+ iconHtml?: string;
45
+ ariaLive?: HcgToastAriaLive;
46
+ onShow?: (toast: HcgToastShown) => void;
47
+ onDismiss?: (toast: HcgToastShown, reason: HcgToastDismissReason) => void;
48
+ }
49
+
50
+ export interface HcgToastInstance {
51
+ show(message: string, options?: HcgToastOptions): number | null;
52
+ success(message: string, options?: HcgToastOptions): number | null;
53
+ error(message: string, options?: HcgToastOptions): number | null;
54
+ warning(message: string, options?: HcgToastOptions): number | null;
55
+ info(message: string, options?: HcgToastOptions): number | null;
56
+ dismiss(id: number): void;
57
+ dismissAll(): void;
58
+ configure(options: HcgToastOptions): void;
59
+ destroy(): void;
60
+ }
61
+
62
+ export interface HcgToastFn {
63
+ (message: string, options?: HcgToastOptions): number | null;
64
+ success(message: string, options?: HcgToastOptions): number | null;
65
+ error(message: string, options?: HcgToastOptions): number | null;
66
+ warning(message: string, options?: HcgToastOptions): number | null;
67
+ info(message: string, options?: HcgToastOptions): number | null;
68
+ dismiss(id: number): void;
69
+ dismissAll(): void;
70
+ configure(options: HcgToastOptions): void;
71
+ destroy(): void;
72
+ create(options?: HcgToastOptions): HcgToastInstance;
73
+ }
74
+
75
+ declare const toast: HcgToastFn;
76
+
77
+ export default toast;
78
+
79
+ declare global {
80
+ const toast: HcgToastFn;
81
+ }
package/hcg-toast.js ADDED
@@ -0,0 +1,418 @@
1
+ /*!
2
+ * hcg-toast v1.0.0
3
+ * Lightweight vanilla JavaScript toast / notification library. Zero dependencies.
4
+ * Requires hcg-toast.css for the default styling.
5
+ * When changing this file, also update README.md and website-page.html.
6
+ * @see https://www.html-code-generator.com/javascript/toast-notification
7
+ * @license MIT
8
+ */
9
+ (function (root, factory) {
10
+ const api = factory();
11
+ if (typeof module === 'object' && module.exports) {
12
+ module.exports = api;
13
+ } else {
14
+ root.toast = api;
15
+ }
16
+ }(
17
+ typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this,
18
+ () => {
19
+
20
+ const DEFAULT_DURATION = 4000;
21
+ const DEFAULT_MAX_TOASTS = 5;
22
+ const DEFAULT_POSITION = 'top-right';
23
+ const EXIT_MS = 250;
24
+
25
+ const POSITIONS = [
26
+ 'top-left', 'top-center', 'top-right',
27
+ 'bottom-left', 'bottom-center', 'bottom-right',
28
+ ];
29
+
30
+ const TYPE_ICONS = {
31
+ info: 'ℹ',
32
+ success: '✓',
33
+ warning: '⚠',
34
+ error: '✕',
35
+ };
36
+
37
+ const positionClass = (pos) => `hcg-toast-container--${pos}`;
38
+
39
+ const normalizePosition = (value) => (
40
+ POSITIONS.includes(value) ? value : DEFAULT_POSITION
41
+ );
42
+
43
+ const mergeOptions = (defaults, overrides) => {
44
+ const merged = { ...defaults, ...overrides };
45
+ merged.position = normalizePosition(merged.position);
46
+ return merged;
47
+ };
48
+
49
+ const applyA11y = (el, options) => {
50
+ const live = options.ariaLive === 'polite' || options.ariaLive === 'assertive' || options.ariaLive === 'off'
51
+ ? options.ariaLive
52
+ : (options.type === 'error' ? 'assertive' : 'off');
53
+ el.setAttribute('role', options.type === 'error' ? 'alert' : 'status');
54
+ el.setAttribute('aria-live', live);
55
+ };
56
+
57
+ const createToastInstance = (initialOptions = {}) => {
58
+ let _toastId = 0;
59
+ let destroyed = false;
60
+ let container = null;
61
+ const toasts = new Map();
62
+
63
+ let defaults = mergeOptions({
64
+ type: 'info',
65
+ duration: DEFAULT_DURATION,
66
+ closable: true,
67
+ pauseOnHover: true,
68
+ position: DEFAULT_POSITION,
69
+ maxToasts: DEFAULT_MAX_TOASTS,
70
+ showProgress: true,
71
+ }, initialOptions);
72
+
73
+ let containerListenersAttached = false;
74
+
75
+ const getToastEntry = (host, e) => {
76
+ const item = e.target.closest('.hcg-toast-item');
77
+ if (!item || !host.contains(item) || item.contains(e.relatedTarget)) return null;
78
+ return toasts.get(Number(item.dataset.toastId)) || null;
79
+ };
80
+
81
+ const attachContainerListeners = (host) => {
82
+ if (containerListenersAttached) return;
83
+ containerListenersAttached = true;
84
+
85
+ host.addEventListener('click', (e) => {
86
+ if (!e.target.closest('.hcg-toast-close')) return;
87
+ const item = e.target.closest('.hcg-toast-item');
88
+ if (!item) return;
89
+ removeEntry(Number(item.dataset.toastId), 'close');
90
+ });
91
+
92
+ host.addEventListener('mouseover', (e) => {
93
+ const entry = getToastEntry(host, e);
94
+ if (entry && entry.options.pauseOnHover && entry.options.duration > 0) {
95
+ pauseTimer(entry);
96
+ }
97
+ });
98
+
99
+ host.addEventListener('mouseout', (e) => {
100
+ const entry = getToastEntry(host, e);
101
+ if (entry && entry.options.pauseOnHover && entry.options.duration > 0) {
102
+ resumeTimer(entry);
103
+ }
104
+ });
105
+ };
106
+
107
+ const cancelEntryFrame = (entry) => {
108
+ if (entry.rafId != null) {
109
+ cancelAnimationFrame(entry.rafId);
110
+ entry.rafId = null;
111
+ }
112
+ };
113
+
114
+ const ensureContainer = () => {
115
+ if (destroyed) return null;
116
+ if (container && document.body.contains(container)) {
117
+ return container;
118
+ }
119
+ container = document.createElement('div');
120
+ container.className = `hcg-toast-container ${positionClass(defaults.position)}`;
121
+ document.body.appendChild(container);
122
+ containerListenersAttached = false;
123
+ attachContainerListeners(container);
124
+ return container;
125
+ };
126
+
127
+ const updateContainerPosition = () => {
128
+ if (!container) return;
129
+ container.className = `hcg-toast-container ${positionClass(defaults.position)}`;
130
+ };
131
+
132
+ const clearTimer = (entry) => {
133
+ if (entry.timerId != null) {
134
+ clearTimeout(entry.timerId);
135
+ entry.timerId = null;
136
+ }
137
+ };
138
+
139
+ const removeEntry = (id, reason, immediate = false) => {
140
+ const entry = toasts.get(id);
141
+ if (!entry || entry.removing) return;
142
+ entry.removing = true;
143
+ clearTimer(entry);
144
+
145
+ const { el, options } = entry;
146
+
147
+ const finish = () => {
148
+ cancelEntryFrame(entry);
149
+ if (el.parentNode) el.parentNode.removeChild(el);
150
+ toasts.delete(id);
151
+ if (typeof options.onDismiss === 'function') {
152
+ options.onDismiss({ id, ...options }, reason);
153
+ }
154
+ };
155
+
156
+ if (immediate) {
157
+ finish();
158
+ return;
159
+ }
160
+
161
+ el.classList.remove('hcg-toast-item--enter');
162
+ el.classList.add('hcg-toast-item--exit');
163
+
164
+ let done = false;
165
+ const onEnd = (e) => {
166
+ if (e.target !== el || done) return;
167
+ done = true;
168
+ el.removeEventListener('transitionend', onEnd);
169
+ finish();
170
+ };
171
+
172
+ el.addEventListener('transitionend', onEnd);
173
+ setTimeout(() => {
174
+ if (!done) {
175
+ done = true;
176
+ el.removeEventListener('transitionend', onEnd);
177
+ finish();
178
+ }
179
+ }, EXIT_MS + 50);
180
+ };
181
+
182
+ const startProgressAnimation = (entry) => {
183
+ if (!entry.progressEl) return;
184
+ const ms = entry.remaining != null ? entry.remaining : entry.options.duration;
185
+ const bar = entry.progressEl;
186
+ bar.style.animation = 'none';
187
+ void bar.offsetWidth;
188
+ bar.style.animation = `hcg-toast-progress-shrink ${ms}ms linear forwards`;
189
+ };
190
+
191
+ const scheduleDismiss = (entry) => {
192
+ clearTimer(entry);
193
+ if (entry.options.duration <= 0 || entry.paused) return;
194
+
195
+ const remaining = entry.remaining != null
196
+ ? entry.remaining
197
+ : entry.options.duration;
198
+
199
+ entry.remaining = remaining;
200
+ entry.timerStarted = Date.now();
201
+
202
+ entry.timerId = setTimeout(() => {
203
+ removeEntry(entry.id, 'timeout');
204
+ }, remaining);
205
+
206
+ startProgressAnimation(entry);
207
+ };
208
+
209
+ const pauseTimer = (entry) => {
210
+ if (entry.paused || entry.options.duration <= 0) return;
211
+ if (entry.timerStarted == null) return;
212
+ entry.paused = true;
213
+ clearTimer(entry);
214
+ const elapsed = Date.now() - entry.timerStarted;
215
+ entry.remaining = Math.max(0, (entry.remaining ?? entry.options.duration) - elapsed);
216
+ if (entry.progressEl) {
217
+ entry.progressEl.style.animationPlayState = 'paused';
218
+ }
219
+ };
220
+
221
+ const resumeTimer = (entry) => {
222
+ if (!entry.paused || entry.options.duration <= 0) return;
223
+ entry.paused = false;
224
+ scheduleDismiss(entry);
225
+ };
226
+
227
+ const enforceMaxToasts = (maxToasts) => {
228
+ while (toasts.size >= maxToasts) {
229
+ const oldest = toasts.keys().next().value;
230
+ if (oldest == null) break;
231
+ removeEntry(oldest, 'api', true);
232
+ }
233
+ };
234
+
235
+ const buildToastEl = (options, id) => {
236
+ const el = document.createElement('div');
237
+ el.className = `hcg-toast-item hcg-toast-item--${options.type}`;
238
+ applyA11y(el, options);
239
+ el.dataset.toastId = String(id);
240
+
241
+ if (options.icon !== false) {
242
+ const iconEl = document.createElement('span');
243
+ iconEl.className = 'hcg-toast-icon';
244
+ iconEl.setAttribute('aria-hidden', 'true');
245
+
246
+ if (options.iconHtml) {
247
+ iconEl.innerHTML = options.iconHtml;
248
+ } else if (typeof options.icon === 'string') {
249
+ iconEl.textContent = options.icon;
250
+ } else {
251
+ iconEl.textContent = TYPE_ICONS[options.type] || TYPE_ICONS.info;
252
+ }
253
+
254
+ el.appendChild(iconEl);
255
+ }
256
+
257
+ const body = document.createElement('div');
258
+ body.className = 'hcg-toast-body';
259
+
260
+ if (options.title) {
261
+ const titleEl = document.createElement('div');
262
+ titleEl.className = 'hcg-toast-title';
263
+ titleEl.textContent = options.title;
264
+ body.appendChild(titleEl);
265
+ }
266
+
267
+ const messageEl = document.createElement('div');
268
+ messageEl.className = 'hcg-toast-message';
269
+ messageEl.textContent = options.message;
270
+ body.appendChild(messageEl);
271
+
272
+ el.appendChild(body);
273
+
274
+ if (options.closable) {
275
+ const closeBtn = document.createElement('button');
276
+ closeBtn.type = 'button';
277
+ closeBtn.className = 'hcg-toast-close';
278
+ closeBtn.setAttribute('aria-label', 'Dismiss notification');
279
+ closeBtn.innerHTML = '&times;';
280
+ el.appendChild(closeBtn);
281
+ }
282
+
283
+ let progressEl = null;
284
+ if (options.duration > 0 && options.showProgress) {
285
+ const trackEl = document.createElement('div');
286
+ trackEl.className = 'hcg-toast-progress-track';
287
+
288
+ progressEl = document.createElement('div');
289
+ progressEl.className = 'hcg-toast-progress';
290
+
291
+ trackEl.appendChild(progressEl);
292
+ el.appendChild(trackEl);
293
+ }
294
+
295
+ return { el, progressEl };
296
+ };
297
+
298
+ const show = (message, options = {}) => {
299
+ if (destroyed) return null;
300
+
301
+ const merged = mergeOptions(defaults, {
302
+ ...options,
303
+ message: message != null ? String(message) : options.message,
304
+ });
305
+
306
+ if (!merged.message) return null;
307
+
308
+ const host = ensureContainer();
309
+ if (!host) return null;
310
+
311
+ const id = ++_toastId;
312
+ const { el, progressEl } = buildToastEl(merged, id);
313
+
314
+ const entry = {
315
+ id,
316
+ el,
317
+ progressEl,
318
+ options: merged,
319
+ paused: false,
320
+ remaining: merged.duration,
321
+ removing: false,
322
+ timerId: null,
323
+ timerStarted: null,
324
+ rafId: null,
325
+ };
326
+
327
+ enforceMaxToasts(merged.maxToasts);
328
+
329
+ toasts.set(id, entry);
330
+ host.appendChild(el);
331
+
332
+ entry.rafId = requestAnimationFrame(() => {
333
+ entry.rafId = null;
334
+ if (!toasts.has(id)) return;
335
+ el.classList.add('hcg-toast-item--enter');
336
+ if (merged.duration > 0) {
337
+ scheduleDismiss(entry);
338
+ }
339
+ if (typeof merged.onShow === 'function') {
340
+ merged.onShow({ id, ...merged });
341
+ }
342
+ });
343
+
344
+ return id;
345
+ };
346
+
347
+ const typedShow = (type) => (message, options = {}) => (
348
+ show(message, { ...options, type })
349
+ );
350
+
351
+ const dismiss = (id) => {
352
+ if (id == null) return;
353
+ removeEntry(id, 'api');
354
+ };
355
+
356
+ const dismissAll = () => {
357
+ [...toasts.keys()].forEach((id) => removeEntry(id, 'api', true));
358
+ };
359
+
360
+ const configure = (partialOptions = {}) => {
361
+ if (destroyed) return;
362
+ defaults = mergeOptions(defaults, partialOptions);
363
+ updateContainerPosition();
364
+ };
365
+
366
+ const destroy = () => {
367
+ if (destroyed) return;
368
+ dismissAll();
369
+ if (container && container.parentNode) {
370
+ container.parentNode.removeChild(container);
371
+ }
372
+ container = null;
373
+ containerListenersAttached = false;
374
+ destroyed = true;
375
+ };
376
+
377
+ return {
378
+ show,
379
+ success: typedShow('success'),
380
+ error: typedShow('error'),
381
+ warning: typedShow('warning'),
382
+ info: typedShow('info'),
383
+ dismiss,
384
+ dismissAll,
385
+ configure,
386
+ destroy,
387
+ };
388
+ };
389
+
390
+ let defaultInstance = null;
391
+
392
+ const getDefaultInstance = () => {
393
+ if (!defaultInstance) {
394
+ defaultInstance = createToastInstance();
395
+ }
396
+ return defaultInstance;
397
+ };
398
+
399
+ const toastFn = (message, options) => getDefaultInstance().show(message, options);
400
+
401
+ toastFn.success = (message, options) => getDefaultInstance().success(message, options);
402
+ toastFn.error = (message, options) => getDefaultInstance().error(message, options);
403
+ toastFn.warning = (message, options) => getDefaultInstance().warning(message, options);
404
+ toastFn.info = (message, options) => getDefaultInstance().info(message, options);
405
+ toastFn.dismiss = (id) => getDefaultInstance().dismiss(id);
406
+ toastFn.dismissAll = () => getDefaultInstance().dismissAll();
407
+ toastFn.configure = (options) => getDefaultInstance().configure(options);
408
+ toastFn.destroy = () => {
409
+ if (defaultInstance) {
410
+ defaultInstance.destroy();
411
+ defaultInstance = null;
412
+ }
413
+ };
414
+ toastFn.create = (options) => createToastInstance(options);
415
+
416
+ return toastFn;
417
+ }
418
+ ));
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "hcg-toast",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight vanilla JavaScript toast notification library. Success, error, warning, and info types, auto-dismiss, sticky toasts, six positions, stack limits, pause on hover, and TypeScript definitions. Zero dependencies.",
5
+ "main": "hcg-toast.js",
6
+ "types": "hcg-toast.d.ts",
7
+ "style": "hcg-toast.css",
8
+ "files": [
9
+ "hcg-toast.js",
10
+ "hcg-toast.css",
11
+ "hcg-toast.d.ts",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "test": "echo \"No tests specified\" && exit 0"
17
+ },
18
+ "keywords": [
19
+ "toast",
20
+ "notification",
21
+ "toast-notification",
22
+ "hcg-toast",
23
+ "alert",
24
+ "snackbar",
25
+ "vanilla-js",
26
+ "no-dependencies",
27
+ "accessible",
28
+ "aria",
29
+ "typescript"
30
+ ],
31
+ "author": "HTML Code Generator (https://www.html-code-generator.com/)",
32
+ "license": "MIT",
33
+ "homepage": "https://www.html-code-generator.com/javascript/toast-notification",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/html-code-generator/hcg-toast.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/html-code-generator/hcg-toast/issues"
40
+ }
41
+ }