@rozie-ui/toast-solid 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.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/index.cjs +482 -0
- package/dist/index.d.cts +51 -0
- package/dist/index.d.mts +51 -0
- package/dist/index.mjs +477 -0
- package/package.json +66 -0
- package/src/Toaster.tsx +683 -0
- package/src/index.ts +5 -0
- package/src/themes/base.css +57 -0
- package/src/themes/bootstrap.css +27 -0
- package/src/themes/material.css +28 -0
- package/src/themes/shadcn.css +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 One Learning Community LTD
|
|
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,85 @@
|
|
|
1
|
+
# @rozie-ui/toast-solid
|
|
2
|
+
|
|
3
|
+
Idiomatic **solid** `Toaster` — a headless, accessible toast / notification host (a live-region queue with per-toast auto-dismiss timers, hover-to-pause, six corner positions, and a per-toast close button) compiled from one [Rozie](https://github.com/One-Learning-Community/rozie.js) source. It is **not** a global singleton + context system: the host owns the queue + timers as internal state and exposes an imperative `show` / `dismiss` / `clear` handle you drive via `ref` — "call from anywhere" is your app's wiring concern (stash the ref). Every visual value is a CSS custom property, so it re-skins to any design system. This package is generated; do not edit `src/` by hand.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @rozie-ui/toast-solid
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies: `solid-js`. Install them alongside this package.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Toaster, type ToasterHandle } from '@rozie-ui/toast-solid';
|
|
17
|
+
|
|
18
|
+
export function Demo() {
|
|
19
|
+
let toaster: ToasterHandle | undefined;
|
|
20
|
+
// The ref callback receives the HANDLE object (not the DOM node).
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<button onClick={() => toaster?.show({ message: 'Saved!', type: 'success' })}>Save</button>
|
|
24
|
+
<button onClick={() => toaster?.show({ message: 'Something failed', type: 'error' })}>Fail</button>
|
|
25
|
+
|
|
26
|
+
{/* Mount the host once (typically near the app root). */}
|
|
27
|
+
<Toaster ref={(h) => (toaster = h)} position="bottom-right" duration={4000} />
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Theming
|
|
34
|
+
|
|
35
|
+
Every visual value is a `--rozie-toast-*` CSS custom property — override any of them at any ancestor scope. Ready-made design-system bridges ship in the package:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import '@rozie-ui/toast-solid/themes/shadcn.css'; // or material.css, bootstrap.css, base.css
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Props
|
|
42
|
+
|
|
43
|
+
| Name | Type | Default | Two-way (model) | Required |
|
|
44
|
+
| --- | --- | --- | :---: | :---: |
|
|
45
|
+
| `position` | `String` | `"bottom-right"` | | |
|
|
46
|
+
| `duration` | `Number` | `4000` | | |
|
|
47
|
+
| `max` | `Number` | `0` | | |
|
|
48
|
+
| `disablePauseOnHover` | `Boolean` | `false` | | |
|
|
49
|
+
| `ariaLabel` | `String` | `null` | | |
|
|
50
|
+
| `disableSwipe` | `Boolean` | `false` | | |
|
|
51
|
+
| `stacked` | `Boolean` | `false` | | |
|
|
52
|
+
|
|
53
|
+
## Events
|
|
54
|
+
|
|
55
|
+
| Event | Description |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `dismissed` | Fired exactly once per toast, at dismissal initiation (before the exit animation runs). Payload is ONE object `{ toast, reason }` — `toast` is the full queue entry, `reason` is `'timeout'` (auto-dismiss), `'swipe'` (pointer swipe past threshold), `'close'` (the built-in close button), or `'api'` (the `dismiss(id)` verb). `clear()` removes every toast immediately and does NOT fire `dismissed` (documented bulk behavior). |
|
|
58
|
+
|
|
59
|
+
## Imperative handle
|
|
60
|
+
|
|
61
|
+
The component has no events — its primary API is an imperative handle (declared once in the Rozie source via `$expose`). Grab a handle with the native ref mechanism and call the methods directly. None of the verbs overrides an inherited host-element member, so the Lit custom element emits no ROZ137 warning:
|
|
62
|
+
|
|
63
|
+
| Method | Description |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `show` | Enqueue a toast. Accepts `{ message, type, duration, id }` (all optional — `message` defaults to `''`, `type` to `'info'`, `duration` to the `duration` prop). Returns the toast `id`. A non-sticky toast (duration > 0) auto-dismisses; `duration: 0` makes it sticky. |
|
|
66
|
+
| `dismiss` | Remove a single toast by the `id` returned from `show` (routes through the exit lifecycle with reason `'api'` — fires `dismissed`, plays the exit animation, then removes it). |
|
|
67
|
+
| `clear` | Remove every visible toast at once immediately (no exit animation) and clear all pending auto-dismiss timers. Does NOT fire `dismissed`. |
|
|
68
|
+
| `patch` | Update an existing toast in place. Accepts `(id, { message, type, duration })` — only the keys you pass are merged into the matching entry. Returns `true` if the id existed, `false` otherwise (no throw). Including a `duration` key clears and restarts that toast's auto-dismiss timer (`0` makes it sticky; a positive value arms/re-arms it); omitting `duration` leaves a running timer untouched. |
|
|
69
|
+
| `promise` | Sugar over `show`/`patch` for an async operation: `promise(p, { loading, success, error })` immediately shows a `{ type: 'loading', duration: 0 }` toast and returns its `id` SYNCHRONOUSLY. On resolve it patches the SAME toast to `{ type: 'success', message: resolve(success, value) }` (the auto-dismiss timer starts AT SETTLE); on reject, likewise with `error`. `success`/`error` accept a string or a `(value) => string` function. Never resurrects a toast dismissed while `p` was still pending, and never returns a derived promise — your own `.then`/`.catch` on `p` still fire. |
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { Toaster, type ToasterHandle } from '@rozie-ui/toast-solid';
|
|
73
|
+
|
|
74
|
+
let toaster: ToasterHandle | undefined;
|
|
75
|
+
// The ref callback receives the HANDLE object (not the DOM node).
|
|
76
|
+
<Toaster ref={(h) => (toaster = h)} />;
|
|
77
|
+
toaster?.show({ message: 'Saved', type: 'success' });
|
|
78
|
+
toaster?.clear();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Slots
|
|
82
|
+
|
|
83
|
+
| Slot | Params |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| toast | toast, dismiss |
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
let Solid_js = require("solid-js");
|
|
6
|
+
let _solid_primitives_keyed = require("@solid-primitives/keyed");
|
|
7
|
+
let _rozie_runtime_solid = require("@rozie/runtime-solid");
|
|
8
|
+
//#region src/Toaster.tsx
|
|
9
|
+
(0, _rozie_runtime_solid.__rozieInjectStyle)("Toaster-12d4265c", `@media (prefers-reduced-motion: reduce) {
|
|
10
|
+
.rozie-toast[data-rozie-s-12d4265c] {
|
|
11
|
+
animation-name: rozie-toast-fade-in;
|
|
12
|
+
animation-duration: 1ms;
|
|
13
|
+
}
|
|
14
|
+
.rozie-toast--exiting[data-rozie-s-12d4265c] {
|
|
15
|
+
animation-name: rozie-toast-fade-out;
|
|
16
|
+
animation-duration: 1ms;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
.rozie-toaster[data-rozie-s-12d4265c] {
|
|
20
|
+
position: fixed;
|
|
21
|
+
z-index: var(--rozie-toast-z, 9999);
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: var(--rozie-toast-gap, 0.5rem);
|
|
25
|
+
padding: var(--rozie-toast-region-padding, 1rem);
|
|
26
|
+
max-width: var(--rozie-toast-max-width, calc(100vw - 2rem));
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
font: var(--rozie-toast-font, inherit);
|
|
29
|
+
}
|
|
30
|
+
.rozie-toaster[data-rozie-s-12d4265c] > *[data-rozie-s-12d4265c] {
|
|
31
|
+
pointer-events: auto;
|
|
32
|
+
}
|
|
33
|
+
.rozie-toaster--top-left[data-rozie-s-12d4265c] { top: 0; left: 0; align-items: flex-start; }
|
|
34
|
+
.rozie-toaster--top-right[data-rozie-s-12d4265c] { top: 0; right: 0; align-items: flex-end; }
|
|
35
|
+
.rozie-toaster--top-center[data-rozie-s-12d4265c] { top: 0; left: 50%; transform: translateX(-50%); align-items: center; }
|
|
36
|
+
.rozie-toaster--bottom-left[data-rozie-s-12d4265c] { bottom: 0; left: 0; align-items: flex-start; flex-direction: column-reverse; }
|
|
37
|
+
.rozie-toaster--bottom-right[data-rozie-s-12d4265c] { bottom: 0; right: 0; align-items: flex-end; flex-direction: column-reverse; }
|
|
38
|
+
.rozie-toaster--bottom-center[data-rozie-s-12d4265c] { bottom: 0; left: 50%; transform: translateX(-50%); align-items: center; flex-direction: column-reverse; }
|
|
39
|
+
.rozie-toaster--stacked[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c] {
|
|
40
|
+
grid-area: 1 / 1;
|
|
41
|
+
z-index: calc(100 - var(--rozie-toast-depth, 0));
|
|
42
|
+
}
|
|
43
|
+
.rozie-toaster--stacked[data-rozie-s-12d4265c]:not([data-rozie-s-12d4265c]:hover):not([data-rozie-s-12d4265c]:focus-within) {
|
|
44
|
+
display: grid;
|
|
45
|
+
}
|
|
46
|
+
.rozie-toaster--stacked[data-rozie-s-12d4265c]:not([data-rozie-s-12d4265c]:hover):not([data-rozie-s-12d4265c]:focus-within) .rozie-toast[data-rozie-s-12d4265c] {
|
|
47
|
+
transform:
|
|
48
|
+
translateY(calc(var(--rozie-toast-depth, 0) * var(--rozie-toast-stack-offset, 8px)))
|
|
49
|
+
scale(calc(1 - var(--rozie-toast-depth, 0) * var(--rozie-toast-stack-scale-step, 0.05)));
|
|
50
|
+
opacity: calc(1 - min(1, max(0, var(--rozie-toast-depth, 0) - 2)));
|
|
51
|
+
}
|
|
52
|
+
.rozie-toaster--stacked.rozie-toaster--bottom-left[data-rozie-s-12d4265c]:not([data-rozie-s-12d4265c]:hover):not([data-rozie-s-12d4265c]:focus-within) .rozie-toast[data-rozie-s-12d4265c],
|
|
53
|
+
.rozie-toaster--stacked.rozie-toaster--bottom-right[data-rozie-s-12d4265c]:not([data-rozie-s-12d4265c]:hover):not([data-rozie-s-12d4265c]:focus-within) .rozie-toast[data-rozie-s-12d4265c],
|
|
54
|
+
.rozie-toaster--stacked.rozie-toaster--bottom-center[data-rozie-s-12d4265c]:not([data-rozie-s-12d4265c]:hover):not([data-rozie-s-12d4265c]:focus-within) .rozie-toast[data-rozie-s-12d4265c] {
|
|
55
|
+
transform:
|
|
56
|
+
translateY(calc(var(--rozie-toast-depth, 0) * var(--rozie-toast-stack-offset, 8px) * -1))
|
|
57
|
+
scale(calc(1 - var(--rozie-toast-depth, 0) * var(--rozie-toast-stack-scale-step, 0.05)));
|
|
58
|
+
}
|
|
59
|
+
.rozie-toast[data-rozie-s-12d4265c] {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: var(--rozie-toast-content-gap, 0.75rem);
|
|
63
|
+
min-width: var(--rozie-toast-min-width, 16rem);
|
|
64
|
+
max-width: var(--rozie-toast-toast-max-width, 24rem);
|
|
65
|
+
padding: var(--rozie-toast-padding, 0.75rem 1rem);
|
|
66
|
+
color: var(--rozie-toast-color, #fff);
|
|
67
|
+
background: var(--rozie-toast-bg, #333);
|
|
68
|
+
border-radius: var(--rozie-toast-radius, 0.5rem);
|
|
69
|
+
box-shadow: var(--rozie-toast-shadow, 0 6px 20px rgba(0, 0, 0, 0.25));
|
|
70
|
+
/* Swipe: page scroll stays alive on touch along the axis the toast does
|
|
71
|
+
NOT move on. The transition here drives the spring-back (the active-drag
|
|
72
|
+
:style sets an inline \`transition: none\` to track the finger 1:1;
|
|
73
|
+
releasing it without a further gesture falls back to this transition). */
|
|
74
|
+
touch-action: pan-y;
|
|
75
|
+
transition: transform 200ms ease, opacity 200ms ease;
|
|
76
|
+
}
|
|
77
|
+
.rozie-toaster--top-center[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c],
|
|
78
|
+
.rozie-toaster--bottom-center[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c] {
|
|
79
|
+
touch-action: pan-x;
|
|
80
|
+
}
|
|
81
|
+
.rozie-toast--success[data-rozie-s-12d4265c] { background: var(--rozie-toast-success-bg, #16a34a); }
|
|
82
|
+
.rozie-toast--error[data-rozie-s-12d4265c] { background: var(--rozie-toast-error-bg, #dc2626); }
|
|
83
|
+
.rozie-toast--warning[data-rozie-s-12d4265c] { background: var(--rozie-toast-warning-bg, #ca8a04); }
|
|
84
|
+
.rozie-toast--info[data-rozie-s-12d4265c] { background: var(--rozie-toast-info-bg, var(--rozie-toast-bg, #333)); }
|
|
85
|
+
from[data-rozie-s-12d4265c] { opacity: 0; transform: translateY(-0.5rem); }
|
|
86
|
+
to[data-rozie-s-12d4265c] { opacity: 1; transform: translateY(0); }
|
|
87
|
+
from[data-rozie-s-12d4265c] { opacity: 0; transform: translateY(0.5rem); }
|
|
88
|
+
to[data-rozie-s-12d4265c] { opacity: 1; transform: translateY(0); }
|
|
89
|
+
from[data-rozie-s-12d4265c] { opacity: 1; transform: translateY(0); }
|
|
90
|
+
to[data-rozie-s-12d4265c] { opacity: 0; transform: translateY(-0.5rem); }
|
|
91
|
+
from[data-rozie-s-12d4265c] { opacity: 1; transform: translateY(0); }
|
|
92
|
+
to[data-rozie-s-12d4265c] { opacity: 0; transform: translateY(0.5rem); }
|
|
93
|
+
.rozie-toast[data-rozie-s-12d4265c] {
|
|
94
|
+
animation: rozie-toast-enter var(--rozie-toast-enter-duration, 200ms) ease-out;
|
|
95
|
+
}
|
|
96
|
+
.rozie-toaster--bottom-left[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c],
|
|
97
|
+
.rozie-toaster--bottom-right[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c],
|
|
98
|
+
.rozie-toaster--bottom-center[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c] {
|
|
99
|
+
animation-name: rozie-toast-enter-from-bottom;
|
|
100
|
+
}
|
|
101
|
+
.rozie-toast--exiting[data-rozie-s-12d4265c] {
|
|
102
|
+
animation: rozie-toast-exit var(--rozie-toast-exit-duration, 200ms) ease-in forwards;
|
|
103
|
+
}
|
|
104
|
+
.rozie-toaster--bottom-left[data-rozie-s-12d4265c] .rozie-toast--exiting[data-rozie-s-12d4265c],
|
|
105
|
+
.rozie-toaster--bottom-right[data-rozie-s-12d4265c] .rozie-toast--exiting[data-rozie-s-12d4265c],
|
|
106
|
+
.rozie-toaster--bottom-center[data-rozie-s-12d4265c] .rozie-toast--exiting[data-rozie-s-12d4265c] {
|
|
107
|
+
animation-name: rozie-toast-exit-to-bottom;
|
|
108
|
+
}
|
|
109
|
+
from[data-rozie-s-12d4265c] { opacity: 0; }
|
|
110
|
+
to[data-rozie-s-12d4265c] { opacity: 1; }
|
|
111
|
+
from[data-rozie-s-12d4265c] { opacity: 1; }
|
|
112
|
+
to[data-rozie-s-12d4265c] { opacity: 0; }
|
|
113
|
+
from[data-rozie-s-12d4265c] { opacity: 1; transform: translateX(0); }
|
|
114
|
+
to[data-rozie-s-12d4265c] { opacity: 0; transform: translateX(calc(var(--rozie-toast-swipe-exit, 1) * 100%)); }
|
|
115
|
+
from[data-rozie-s-12d4265c] { opacity: 1; transform: translateY(0); }
|
|
116
|
+
to[data-rozie-s-12d4265c] { opacity: 0; transform: translateY(calc(var(--rozie-toast-swipe-exit, 1) * 100%)); }
|
|
117
|
+
.rozie-toast--exiting.rozie-toast--swipe-exit[data-rozie-s-12d4265c] {
|
|
118
|
+
animation-name: rozie-toast-swipe-exit-x;
|
|
119
|
+
}
|
|
120
|
+
.rozie-toaster--top-center[data-rozie-s-12d4265c] .rozie-toast--exiting.rozie-toast--swipe-exit[data-rozie-s-12d4265c],
|
|
121
|
+
.rozie-toaster--bottom-center[data-rozie-s-12d4265c] .rozie-toast--exiting.rozie-toast--swipe-exit[data-rozie-s-12d4265c] {
|
|
122
|
+
animation-name: rozie-toast-swipe-exit-y;
|
|
123
|
+
}
|
|
124
|
+
.rozie-toast-spinner[data-rozie-s-12d4265c] {
|
|
125
|
+
flex: 0 0 auto;
|
|
126
|
+
width: var(--rozie-toast-spinner-size, 1em);
|
|
127
|
+
height: var(--rozie-toast-spinner-size, 1em);
|
|
128
|
+
border: 2px solid color-mix(in srgb, var(--rozie-toast-spinner-color, currentColor) 25%, transparent);
|
|
129
|
+
border-top-color: var(--rozie-toast-spinner-color, currentColor);
|
|
130
|
+
border-radius: 50%;
|
|
131
|
+
animation: rozie-toast-spin 0.75s linear infinite;
|
|
132
|
+
}
|
|
133
|
+
to[data-rozie-s-12d4265c] { transform: rotate(360deg); }
|
|
134
|
+
.rozie-toast-message[data-rozie-s-12d4265c] {
|
|
135
|
+
flex: 1 1 auto;
|
|
136
|
+
font-size: var(--rozie-toast-font-size, 0.9rem);
|
|
137
|
+
}
|
|
138
|
+
.rozie-toast-close[data-rozie-s-12d4265c] {
|
|
139
|
+
flex: 0 0 auto;
|
|
140
|
+
display: inline-flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
justify-content: center;
|
|
143
|
+
width: var(--rozie-toast-close-size, 1.25rem);
|
|
144
|
+
height: var(--rozie-toast-close-size, 1.25rem);
|
|
145
|
+
padding: 0;
|
|
146
|
+
font-size: 1.1rem;
|
|
147
|
+
line-height: 1;
|
|
148
|
+
color: inherit;
|
|
149
|
+
background: transparent;
|
|
150
|
+
border: none;
|
|
151
|
+
border-radius: 0.25rem;
|
|
152
|
+
opacity: var(--rozie-toast-close-opacity, 0.75);
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
}
|
|
155
|
+
.rozie-toast-close[data-rozie-s-12d4265c]:hover {
|
|
156
|
+
opacity: 1;
|
|
157
|
+
}`);
|
|
158
|
+
function Toaster(_props) {
|
|
159
|
+
const [local, attrs] = (0, Solid_js.splitProps)((0, Solid_js.mergeProps)({
|
|
160
|
+
position: "bottom-right",
|
|
161
|
+
duration: 4e3,
|
|
162
|
+
max: 0,
|
|
163
|
+
disablePauseOnHover: false,
|
|
164
|
+
ariaLabel: null,
|
|
165
|
+
disableSwipe: false,
|
|
166
|
+
stacked: false
|
|
167
|
+
}, _props), [
|
|
168
|
+
"position",
|
|
169
|
+
"duration",
|
|
170
|
+
"max",
|
|
171
|
+
"disablePauseOnHover",
|
|
172
|
+
"ariaLabel",
|
|
173
|
+
"disableSwipe",
|
|
174
|
+
"stacked",
|
|
175
|
+
"ref"
|
|
176
|
+
]);
|
|
177
|
+
(0, Solid_js.onMount)(() => {
|
|
178
|
+
local.ref?.({
|
|
179
|
+
show,
|
|
180
|
+
dismiss,
|
|
181
|
+
clear,
|
|
182
|
+
patch,
|
|
183
|
+
promise
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
const [toasts, setToasts] = (0, Solid_js.createSignal)([]);
|
|
187
|
+
const [seq, setSeq] = (0, Solid_js.createSignal)(0);
|
|
188
|
+
const [swipe, setSwipe] = (0, Solid_js.createSignal)(null);
|
|
189
|
+
const [swipeGesture, setSwipeGesture] = (0, Solid_js.createSignal)(null);
|
|
190
|
+
(0, Solid_js.onCleanup)(() => {
|
|
191
|
+
unmounted = true;
|
|
192
|
+
teardownTimers();
|
|
193
|
+
});
|
|
194
|
+
let timers = {};
|
|
195
|
+
let exitFailsafes = {};
|
|
196
|
+
let unmounted = false;
|
|
197
|
+
let seqLocal = 0;
|
|
198
|
+
let paused = false;
|
|
199
|
+
function startTimer(toast) {
|
|
200
|
+
if (!toast || !toast.duration || toast.duration <= 0) return;
|
|
201
|
+
if (typeof window === "undefined") return;
|
|
202
|
+
const existing = timers[toast.id];
|
|
203
|
+
if (existing && existing.handle != null) window.clearTimeout(existing.handle);
|
|
204
|
+
const remaining = toast.duration;
|
|
205
|
+
const handle = window.setTimeout(() => dismissBegin(toast.id, "timeout"), remaining);
|
|
206
|
+
timers[toast.id] = {
|
|
207
|
+
handle,
|
|
208
|
+
startedAt: Date.now(),
|
|
209
|
+
remaining
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function clearTimer(id) {
|
|
213
|
+
const entry = timers[id];
|
|
214
|
+
if (entry && entry.handle != null && typeof window !== "undefined") window.clearTimeout(entry.handle);
|
|
215
|
+
delete timers[id];
|
|
216
|
+
}
|
|
217
|
+
function pauseTimers() {
|
|
218
|
+
paused = true;
|
|
219
|
+
if (typeof window === "undefined") return;
|
|
220
|
+
for (const id in timers) {
|
|
221
|
+
const entry = timers[id];
|
|
222
|
+
if (entry.handle == null) continue;
|
|
223
|
+
window.clearTimeout(entry.handle);
|
|
224
|
+
const elapsed = Date.now() - entry.startedAt;
|
|
225
|
+
const remaining = Math.max(0, entry.remaining - elapsed);
|
|
226
|
+
timers[id] = {
|
|
227
|
+
handle: null,
|
|
228
|
+
startedAt: entry.startedAt,
|
|
229
|
+
remaining
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function resumeTimers() {
|
|
234
|
+
paused = false;
|
|
235
|
+
if (typeof window === "undefined") return;
|
|
236
|
+
for (const id in timers) {
|
|
237
|
+
const entry = timers[id];
|
|
238
|
+
if (entry.handle != null) continue;
|
|
239
|
+
if (entry.remaining == null || entry.remaining <= 0) {
|
|
240
|
+
dismissBegin(id, "timeout");
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const remaining = entry.remaining;
|
|
244
|
+
const handle = window.setTimeout(() => dismissBegin(id, "timeout"), remaining);
|
|
245
|
+
timers[id] = {
|
|
246
|
+
handle,
|
|
247
|
+
startedAt: Date.now(),
|
|
248
|
+
remaining
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function teardownTimers() {
|
|
253
|
+
if (typeof window !== "undefined") {
|
|
254
|
+
for (const id in timers) {
|
|
255
|
+
const entry = timers[id];
|
|
256
|
+
if (entry.handle != null) window.clearTimeout(entry.handle);
|
|
257
|
+
}
|
|
258
|
+
for (const id in exitFailsafes) if (exitFailsafes[id] != null) window.clearTimeout(exitFailsafes[id]);
|
|
259
|
+
}
|
|
260
|
+
timers = {};
|
|
261
|
+
exitFailsafes = {};
|
|
262
|
+
}
|
|
263
|
+
function show(input) {
|
|
264
|
+
const t = input || {};
|
|
265
|
+
let id;
|
|
266
|
+
if (t.id != null) id = String(t.id);
|
|
267
|
+
else {
|
|
268
|
+
const s = Math.max(seq(), seqLocal);
|
|
269
|
+
id = "t" + s;
|
|
270
|
+
seqLocal = s + 1;
|
|
271
|
+
setSeq(s + 1);
|
|
272
|
+
}
|
|
273
|
+
const toast = {
|
|
274
|
+
id,
|
|
275
|
+
message: t.message != null ? t.message : "",
|
|
276
|
+
type: t.type || "info",
|
|
277
|
+
duration: t.duration != null ? t.duration : local.duration
|
|
278
|
+
};
|
|
279
|
+
setToasts(toasts().concat([toast]).slice(local.max > 0 ? Math.max(0, toasts().length + 1 - local.max) : 0));
|
|
280
|
+
startTimer(toast);
|
|
281
|
+
return id;
|
|
282
|
+
}
|
|
283
|
+
const EXIT_FAILSAFE_MS = 350;
|
|
284
|
+
function removeToast(id) {
|
|
285
|
+
if (typeof window !== "undefined" && exitFailsafes[id] != null) window.clearTimeout(exitFailsafes[id]);
|
|
286
|
+
delete exitFailsafes[id];
|
|
287
|
+
setToasts(toasts().filter((t) => t.id !== id));
|
|
288
|
+
}
|
|
289
|
+
function dismissBegin(id, reason, extra) {
|
|
290
|
+
const entry = toasts().find((t) => t.id === id);
|
|
291
|
+
if (!entry || entry.exiting) return;
|
|
292
|
+
clearTimer(id);
|
|
293
|
+
_props.onDismissed?.({
|
|
294
|
+
toast: entry,
|
|
295
|
+
reason
|
|
296
|
+
});
|
|
297
|
+
setToasts(toasts().map((t) => t.id === id ? {
|
|
298
|
+
...t,
|
|
299
|
+
exiting: true,
|
|
300
|
+
...extra || {}
|
|
301
|
+
} : t));
|
|
302
|
+
if (typeof window === "undefined") removeToast(id);
|
|
303
|
+
else exitFailsafes[id] = window.setTimeout(() => removeToast(id), EXIT_FAILSAFE_MS);
|
|
304
|
+
}
|
|
305
|
+
function dismiss(id) {
|
|
306
|
+
dismissBegin(id, "api");
|
|
307
|
+
}
|
|
308
|
+
function clear() {
|
|
309
|
+
teardownTimers();
|
|
310
|
+
setToasts([]);
|
|
311
|
+
}
|
|
312
|
+
function patch(id, changes) {
|
|
313
|
+
const c = changes || {};
|
|
314
|
+
let existed = false;
|
|
315
|
+
const next = toasts().map((t) => {
|
|
316
|
+
if (t.id !== id) return t;
|
|
317
|
+
if (t.exiting) return t;
|
|
318
|
+
existed = true;
|
|
319
|
+
const merged = { ...t };
|
|
320
|
+
if (c.message !== void 0) merged.message = c.message;
|
|
321
|
+
if (c.type !== void 0) merged.type = c.type;
|
|
322
|
+
if (c.duration !== void 0) merged.duration = c.duration;
|
|
323
|
+
return merged;
|
|
324
|
+
});
|
|
325
|
+
if (!existed) return false;
|
|
326
|
+
setToasts(next);
|
|
327
|
+
if (c.duration !== void 0) {
|
|
328
|
+
clearTimer(id);
|
|
329
|
+
const patched = next.find((t) => t.id === id);
|
|
330
|
+
if (paused) {
|
|
331
|
+
if (patched && patched.duration > 0 && typeof window !== "undefined") timers[id] = {
|
|
332
|
+
handle: null,
|
|
333
|
+
startedAt: Date.now(),
|
|
334
|
+
remaining: patched.duration
|
|
335
|
+
};
|
|
336
|
+
} else startTimer(patched);
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
function settlePromise(id, type, messageOrFn, value) {
|
|
341
|
+
if (unmounted) return;
|
|
342
|
+
const entry = toasts().find((t) => t.id === id);
|
|
343
|
+
if (!entry || entry.exiting) return;
|
|
344
|
+
patch(id, {
|
|
345
|
+
type,
|
|
346
|
+
message: typeof messageOrFn === "function" ? messageOrFn(value) : messageOrFn,
|
|
347
|
+
duration: local.duration
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function promise(p, opts) {
|
|
351
|
+
const o = opts || {};
|
|
352
|
+
const id = show({
|
|
353
|
+
type: "loading",
|
|
354
|
+
duration: 0,
|
|
355
|
+
message: o.loading
|
|
356
|
+
});
|
|
357
|
+
if (p && typeof p.then === "function") p.then((value) => settlePromise(id, "success", o.success, value)).catch((err) => settlePromise(id, "error", o.error, err));
|
|
358
|
+
return id;
|
|
359
|
+
}
|
|
360
|
+
function swipeAxisFor(position) {
|
|
361
|
+
return position === "top-center" || position === "bottom-center" ? "y" : "x";
|
|
362
|
+
}
|
|
363
|
+
function swipeSignFor(position) {
|
|
364
|
+
if (position === "top-right" || position === "bottom-right") return 1;
|
|
365
|
+
if (position === "top-left" || position === "bottom-left") return -1;
|
|
366
|
+
if (position === "bottom-center") return 1;
|
|
367
|
+
return -1;
|
|
368
|
+
}
|
|
369
|
+
function onToastPointerDown(t, event) {
|
|
370
|
+
if (local.disableSwipe) return;
|
|
371
|
+
if (event.button != null && event.button !== 0) return;
|
|
372
|
+
if (event.target && event.target.closest ? event.target.closest("button, a") : null) return;
|
|
373
|
+
const axis = swipeAxisFor(local.position);
|
|
374
|
+
const sign = swipeSignFor(local.position);
|
|
375
|
+
const el = event.currentTarget;
|
|
376
|
+
const size = axis === "x" ? el.offsetWidth : el.offsetHeight;
|
|
377
|
+
setSwipeGesture({
|
|
378
|
+
id: t.id,
|
|
379
|
+
axis,
|
|
380
|
+
sign,
|
|
381
|
+
size,
|
|
382
|
+
startX: event.clientX,
|
|
383
|
+
startY: event.clientY,
|
|
384
|
+
startTime: Date.now()
|
|
385
|
+
});
|
|
386
|
+
if (el && el.setPointerCapture) try {
|
|
387
|
+
el.setPointerCapture(event.pointerId);
|
|
388
|
+
} catch (e) {}
|
|
389
|
+
}
|
|
390
|
+
function onToastPointerMove(t, event) {
|
|
391
|
+
if (local.disableSwipe) return;
|
|
392
|
+
const gesture = swipeGesture();
|
|
393
|
+
if (!gesture || gesture.id !== t.id) return;
|
|
394
|
+
const raw = gesture.axis === "x" ? event.clientX - gesture.startX : event.clientY - gesture.startY;
|
|
395
|
+
const d = raw * gesture.sign > 0 ? raw : raw * .15;
|
|
396
|
+
setSwipe({
|
|
397
|
+
id: t.id,
|
|
398
|
+
d,
|
|
399
|
+
axis: gesture.axis,
|
|
400
|
+
sign: gesture.sign,
|
|
401
|
+
size: gesture.size
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function onToastPointerUp(t, event) {
|
|
405
|
+
if (local.disableSwipe) return;
|
|
406
|
+
const gesture = swipeGesture();
|
|
407
|
+
setSwipeGesture(null);
|
|
408
|
+
const dragState = swipe();
|
|
409
|
+
setSwipe(null);
|
|
410
|
+
if (!gesture || gesture.id !== t.id || !dragState) return;
|
|
411
|
+
const elapsed = Math.max(1, Date.now() - gesture.startTime);
|
|
412
|
+
const magnitude = dragState.d * gesture.sign;
|
|
413
|
+
const velocity = magnitude / elapsed;
|
|
414
|
+
if (magnitude > 0 && (magnitude > gesture.size * .45 || velocity > .11)) dismissBegin(t.id, "swipe", { swipeExitSign: gesture.sign });
|
|
415
|
+
}
|
|
416
|
+
function onToastPointerCancel(t) {
|
|
417
|
+
if (local.disableSwipe) return;
|
|
418
|
+
if (swipeGesture() && swipeGesture().id === t.id) setSwipeGesture(null);
|
|
419
|
+
if (swipe() && swipe().id === t.id) setSwipe(null);
|
|
420
|
+
}
|
|
421
|
+
function depth(t) {
|
|
422
|
+
const idx = toasts().findIndex((x) => x.id === t.id);
|
|
423
|
+
return idx === -1 ? 0 : toasts().length - 1 - idx;
|
|
424
|
+
}
|
|
425
|
+
function toastStyle(t) {
|
|
426
|
+
const depthDecl = "--rozie-toast-depth: " + depth(t) + ";";
|
|
427
|
+
if (t.exiting) return t.swipeExitSign != null ? depthDecl + " --rozie-toast-swipe-exit: " + t.swipeExitSign + ";" : depthDecl;
|
|
428
|
+
const dragState = swipe();
|
|
429
|
+
if (!dragState || dragState.id !== t.id) return depthDecl;
|
|
430
|
+
const translate = dragState.axis === "x" ? "translateX(" + dragState.d + "px)" : "translateY(" + dragState.d + "px)";
|
|
431
|
+
const magnitude = dragState.d * dragState.sign;
|
|
432
|
+
const opacity = magnitude > 0 && dragState.size > 0 ? Math.max(.3, 1 - magnitude / dragState.size) : 1;
|
|
433
|
+
return depthDecl + " transform: " + translate + "; opacity: " + opacity + "; transition: none;";
|
|
434
|
+
}
|
|
435
|
+
function onMouseEnter() {
|
|
436
|
+
if (local.disablePauseOnHover) return;
|
|
437
|
+
pauseTimers();
|
|
438
|
+
}
|
|
439
|
+
function onMouseLeave() {
|
|
440
|
+
if (local.disablePauseOnHover) return;
|
|
441
|
+
resumeTimers();
|
|
442
|
+
}
|
|
443
|
+
function regionLabel() {
|
|
444
|
+
return local.ariaLabel != null ? local.ariaLabel : "Notifications";
|
|
445
|
+
}
|
|
446
|
+
function liveFor(type) {
|
|
447
|
+
return type === "error" || type === "warning" ? "assertive" : "polite";
|
|
448
|
+
}
|
|
449
|
+
return <>
|
|
450
|
+
<div role="region" aria-label={(0, _rozie_runtime_solid.rozieAttr)(regionLabel())} {...attrs} class={"rozie-toaster " + (0, _rozie_runtime_solid.rozieClass)("rozie-toaster--" + local.position + (local.stacked ? " rozie-toaster--stacked" : "")) + (attrs.class ? " " + attrs.class : "")} {...(0, _rozie_runtime_solid.mergeListeners)({
|
|
451
|
+
onMouseEnter: ($event) => {
|
|
452
|
+
onMouseEnter();
|
|
453
|
+
},
|
|
454
|
+
onMouseLeave: ($event) => {
|
|
455
|
+
onMouseLeave();
|
|
456
|
+
}
|
|
457
|
+
}, attrs)} data-rozie-s-12d4265c="">
|
|
458
|
+
|
|
459
|
+
<_solid_primitives_keyed.Key each={toasts()} by={(t) => t.id}>{(t) => <div role="status" aria-live={(0, _rozie_runtime_solid.rozieAttr)(liveFor(t().type))} class={"rozie-toast " + (0, _rozie_runtime_solid.rozieClass)("rozie-toast--" + t().type + (t().exiting ? " rozie-toast--exiting" : "") + (t().swipeExitSign != null ? " rozie-toast--swipe-exit" : ""))} style={(0, _rozie_runtime_solid.parseInlineStyle)(toastStyle(t()))} onAnimationEnd={($event) => {
|
|
460
|
+
t().exiting && removeToast(t().id);
|
|
461
|
+
}} onPointerDown={($event) => {
|
|
462
|
+
onToastPointerDown(t(), $event);
|
|
463
|
+
}} onPointerMove={($event) => {
|
|
464
|
+
onToastPointerMove(t(), $event);
|
|
465
|
+
}} onPointerUp={($event) => {
|
|
466
|
+
onToastPointerUp(t(), $event);
|
|
467
|
+
}} onPointerCancel={($event) => {
|
|
468
|
+
onToastPointerCancel(t());
|
|
469
|
+
}} data-rozie-s-12d4265c="">
|
|
470
|
+
{(_props.toastSlot ?? _props.slots?.["toast"])?.({
|
|
471
|
+
toast: t(),
|
|
472
|
+
dismiss
|
|
473
|
+
}) ?? <>{<Solid_js.Show when={t().type === "loading"}><span class={"rozie-toast-spinner"} aria-hidden="true" data-rozie-s-12d4265c="" /></Solid_js.Show>}<span class={"rozie-toast-message"} data-rozie-s-12d4265c="">{(0, _rozie_runtime_solid.rozieDisplay)(t().message)}</span><button type="button" aria-label="Dismiss" class={"rozie-toast-close"} onClick={($event) => {
|
|
474
|
+
dismissBegin(t().id, "close");
|
|
475
|
+
}} data-rozie-s-12d4265c="">×</button></>}
|
|
476
|
+
</div>}</_solid_primitives_keyed.Key>
|
|
477
|
+
</div>
|
|
478
|
+
</>;
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
exports.Toaster = Toaster;
|
|
482
|
+
exports.default = Toaster;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { JSX } from "solid-js";
|
|
2
|
+
|
|
3
|
+
//#region src/Toaster.d.ts
|
|
4
|
+
interface ToastSlotCtx {
|
|
5
|
+
toast: any;
|
|
6
|
+
dismiss: any;
|
|
7
|
+
}
|
|
8
|
+
interface ToasterProps {
|
|
9
|
+
/**
|
|
10
|
+
* Which corner the toast stack renders in: `'top-left'`, `'top-right'`, `'top-center'`, `'bottom-left'`, `'bottom-right'`, or `'bottom-center'`. Drives the fixed-position layout and the stack direction.
|
|
11
|
+
*/
|
|
12
|
+
position?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Default auto-dismiss time in milliseconds, applied to any toast that does not pass its own `duration`. `0` (or a per-toast `duration` of `0`) makes the toast sticky — it stays until explicitly dismissed.
|
|
15
|
+
*/
|
|
16
|
+
duration?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Maximum number of visible toasts (`0` = unlimited). When the queue exceeds this, the oldest toasts drop off the stack.
|
|
19
|
+
*/
|
|
20
|
+
max?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Opt **out** of pausing the auto-dismiss timers while the pointer is over the stack. By default hovering pauses every timer and leaving restarts them; set this to keep toasts dismissing on schedule regardless of hover.
|
|
23
|
+
*/
|
|
24
|
+
disablePauseOnHover?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Accessible name for the live region (`role="region"`), applied as its `aria-label`. Defaults to `'Notifications'` when not set, so assistive tech can navigate to the toast stack as a landmark.
|
|
27
|
+
*/
|
|
28
|
+
ariaLabel?: (string) | null;
|
|
29
|
+
/**
|
|
30
|
+
* Opt **out** of pointer swipe-to-dismiss. By default, dragging a toast past 45% of its own width/height (direction auto-derived from `position`) or a fast flick dismisses it with reason `'swipe'`; a short drag springs back. A drag starting on the close button (or any button/link) never swipes.
|
|
31
|
+
*/
|
|
32
|
+
disableSwipe?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Opt **in** to a sonner-style collapsed stack: a single-cell grid overlay with depth-driven transforms (toasts at depth 3+ fade to invisible), newest on top. Hovering the region or moving keyboard focus into it expands to the normal flex-column stack; leaving re-collapses. `false` (default) renders the plain flex column at all times.
|
|
35
|
+
*/
|
|
36
|
+
stacked?: boolean;
|
|
37
|
+
onDismissed?: (...args: unknown[]) => void;
|
|
38
|
+
toastSlot?: (ctx: ToastSlotCtx) => JSX.Element;
|
|
39
|
+
slots?: Record<string, (ctx: any) => JSX.Element>;
|
|
40
|
+
ref?: (h: ToasterHandle) => void;
|
|
41
|
+
}
|
|
42
|
+
interface ToasterHandle {
|
|
43
|
+
show: (...args: any[]) => any;
|
|
44
|
+
dismiss: (...args: any[]) => any;
|
|
45
|
+
clear: (...args: any[]) => any;
|
|
46
|
+
patch: (...args: any[]) => any;
|
|
47
|
+
promise: (...args: any[]) => any;
|
|
48
|
+
}
|
|
49
|
+
declare function Toaster(_props: ToasterProps): JSX.Element;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { Toaster, Toaster as default, type ToasterHandle };
|