@c2n/toast 0.0.6
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 +41 -0
- package/dist/toast.js +488 -0
- package/package.json +61 -0
- package/types/src/toast-controller.d.ts +62 -0
- package/types/src/toast-controller.d.ts.map +1 -0
- package/types/src/toast.d.ts +145 -0
- package/types/src/toast.d.ts.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Nguyen Thai Vinh
|
|
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,41 @@
|
|
|
1
|
+
# @c2n/toast
|
|
2
|
+
|
|
3
|
+
Notification cards and a stack manager for bursts of notifications.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @c2n/toast
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { toast, getToastRegion } from '@c2n/toast'
|
|
11
|
+
|
|
12
|
+
toast.show({ message: 'New message received', variant: 'info' })
|
|
13
|
+
const id = toast.show({ id: 'upload', message: 'Uploading…', duration: 0 })
|
|
14
|
+
toast.update(id, { message: 'Upload complete', variant: 'success', duration: 5000 })
|
|
15
|
+
getToastRegion().maxVisible = 3
|
|
16
|
+
getToastRegion().position = 'bottom-right'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`toast.show()` returns an ID. Reusing an ID updates its card. `toast.update()` restarts its duration, `toast.dismiss(id)` removes one item, and `toast.clear()` removes everything. Call the manager in the browser; importing is SSR-safe.
|
|
20
|
+
|
|
21
|
+
Three cards are visible by default. Overflow waits in FIFO order and receives its full timeout after becoming visible. Timers pause on hover, keyboard focus, hidden documents and region disconnection. Duration defaults to 5 seconds; 0 is persistent. Managed toasts are dismissible by default.
|
|
22
|
+
|
|
23
|
+
For a scoped stack, use `<c2-toast-region>` and call `show`, `updateToast`, `dismiss`, `clear`, `pause`, or `resume`. Inspect `count` and `queuedCount`. Six `position` values cover top/bottom and left/center/right. `inline` embeds the stack in normal flow.
|
|
24
|
+
|
|
25
|
+
`actionLabel` displays an action button. Region `toast-action` events contain `{ id, toast }` and are followed by dismissal. `toast-dismiss` contains `{ id, toast, reason }`, where reason is `timeout`, `close`, `action`, `programmatic`, or `clear`.
|
|
26
|
+
|
|
27
|
+
Standalone `<c2-toast message="Saved" variant="success" dismissible>` cards do not auto-expire. They support `heading`, `action-label`, `close-label`, and the default, `icon`, `action`, and `close-icon` slots. Standalone dismissal hides the card and emits `toast-close`.
|
|
28
|
+
|
|
29
|
+
Theme through `--c2-toast__container--*`, `--c2-toast__icon--*`, and the variables documented in `custom-elements.json`. Region layout uses `--c2-toast-region__container--{width,gap,offset,z-index}`. Managed cards inherit toast variables from their region.
|
|
30
|
+
|
|
31
|
+
Run queue and timer regression checks with `node --test packages/components/toast/scripts/toast-controller.test.ts` from the repository root.
|
|
32
|
+
|
|
33
|
+
## Animations
|
|
34
|
+
|
|
35
|
+
Configure the region's `enterAnimation` (default `slide-up`), `exitAnimation` (default `fade`), and `animationDuration` (default 200 ms). Attribute equivalents are `enter-animation`, `exit-animation`, and `animation-duration`. Effects: `none`, `fade`, `slide-up`, `slide-down`, `slide-left`, `slide-right`, and `scale`. Directions describe movement: slide-down enters from above and exits below.
|
|
36
|
+
|
|
37
|
+
Closing cards keep their slot until the exit completes. The next queued card then starts its lifetime, and `toast-dismiss` fires after removal. Updating a closing card cancels its removal. Reduced-motion preferences disable effects. These animations apply to managed toasts.
|
|
38
|
+
|
|
39
|
+
## Countdown and alignment
|
|
40
|
+
|
|
41
|
+
Icons are centered vertically by default; use `--c2-toast__icon--align-self: flex-start` for top alignment. Timed managed toasts show a thin, shrinking countdown bar by default. Set `region.showProgress = false` to hide it for a region, or override `showProgress` per notification. Persistent notifications have no countdown. The bar shares the dismissal clock, including pauses, queue promotion, and lifetime resets on updates. Theme it with `--c2-toast__progress--{height,color,background}`. Standalone cards support `show-progress` and a static `progress` fraction (0–1).
|
package/dist/toast.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { LitElement, html, isServer, nothing, unsafeCSS } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
import { styleMap } from "lit/directives/style-map.js";
|
|
4
|
+
import { repeat } from "lit/directives/repeat.js";
|
|
5
|
+
//#region src/toast-controller.ts
|
|
6
|
+
/** FIFO queue. Timers run only for visible entries and preserve remaining time while paused. */
|
|
7
|
+
var ToastController = class {
|
|
8
|
+
constructor(onDismiss = () => {}, beforeDismiss) {
|
|
9
|
+
this.entries = [];
|
|
10
|
+
this.sequence = 0;
|
|
11
|
+
this.limit = 3;
|
|
12
|
+
this.pauses = /* @__PURE__ */ new Set();
|
|
13
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
14
|
+
this.beforeDismiss = beforeDismiss;
|
|
15
|
+
this.onDismiss = onDismiss;
|
|
16
|
+
}
|
|
17
|
+
get visible() {
|
|
18
|
+
return this.entries.slice(0, this.limit).map((entry) => ({ ...entry.toast }));
|
|
19
|
+
}
|
|
20
|
+
get queuedCount() {
|
|
21
|
+
return Math.max(0, this.entries.length - this.limit);
|
|
22
|
+
}
|
|
23
|
+
get count() {
|
|
24
|
+
return this.entries.length;
|
|
25
|
+
}
|
|
26
|
+
set maxVisible(value) {
|
|
27
|
+
this.limit = Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 3;
|
|
28
|
+
this.sync();
|
|
29
|
+
}
|
|
30
|
+
subscribe(listener) {
|
|
31
|
+
this.listeners.add(listener);
|
|
32
|
+
return () => {
|
|
33
|
+
this.listeners.delete(listener);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
show(options) {
|
|
37
|
+
let id = options.id;
|
|
38
|
+
if (!id) do
|
|
39
|
+
id = `toast-${++this.sequence}`;
|
|
40
|
+
while (this.entries.some((entry) => entry.toast.id === id));
|
|
41
|
+
if (this.entries.some((entry) => entry.toast.id === id)) {
|
|
42
|
+
this.update(id, options);
|
|
43
|
+
return id;
|
|
44
|
+
}
|
|
45
|
+
const duration = this.normalizeDuration(options.duration ?? 5e3);
|
|
46
|
+
this.entries.push({
|
|
47
|
+
toast: {
|
|
48
|
+
id,
|
|
49
|
+
message: options.message,
|
|
50
|
+
heading: options.heading ?? "",
|
|
51
|
+
variant: options.variant ?? "neutral",
|
|
52
|
+
duration,
|
|
53
|
+
dismissible: options.dismissible ?? true,
|
|
54
|
+
noIcon: options.noIcon ?? false,
|
|
55
|
+
showProgress: options.showProgress,
|
|
56
|
+
actionLabel: options.actionLabel ?? ""
|
|
57
|
+
},
|
|
58
|
+
remaining: duration,
|
|
59
|
+
started: 0
|
|
60
|
+
});
|
|
61
|
+
this.sync();
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
/** Updates in place and restarts its visible lifetime. Queued entries retain their place. */
|
|
65
|
+
update(id, options) {
|
|
66
|
+
const entry = this.entries.find((item) => item.toast.id === id);
|
|
67
|
+
if (!entry) return false;
|
|
68
|
+
this.stop(entry);
|
|
69
|
+
entry.closing = void 0;
|
|
70
|
+
const defined = Object.fromEntries(Object.entries(options).filter(([key, value]) => key !== "id" && value !== void 0));
|
|
71
|
+
entry.toast = {
|
|
72
|
+
...entry.toast,
|
|
73
|
+
...defined,
|
|
74
|
+
id
|
|
75
|
+
};
|
|
76
|
+
entry.toast.duration = this.normalizeDuration(entry.toast.duration);
|
|
77
|
+
entry.remaining = entry.toast.duration;
|
|
78
|
+
this.sync();
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
/** Current visible lifetime, derived from the same clock used for dismissal. */
|
|
82
|
+
getTiming(id) {
|
|
83
|
+
const entry = this.entries.find((entry) => entry.toast.id === id);
|
|
84
|
+
if (!entry) return;
|
|
85
|
+
const running = entry.timer !== void 0 && !entry.closing;
|
|
86
|
+
return {
|
|
87
|
+
duration: entry.toast.duration,
|
|
88
|
+
remaining: Math.max(0, entry.remaining - (running ? Date.now() - entry.started : 0)),
|
|
89
|
+
running
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
isClosing(id) {
|
|
93
|
+
return !!this.entries.find((entry) => entry.toast.id === id)?.closing;
|
|
94
|
+
}
|
|
95
|
+
dismiss(id, reason = "programmatic") {
|
|
96
|
+
const index = this.entries.findIndex((entry) => entry.toast.id === id);
|
|
97
|
+
if (index < 0 || this.entries[index].closing) return false;
|
|
98
|
+
const entry = this.entries[index];
|
|
99
|
+
const token = {};
|
|
100
|
+
entry.closing = token;
|
|
101
|
+
this.stop(entry);
|
|
102
|
+
const finish = () => {
|
|
103
|
+
if (entry.closing !== token) return;
|
|
104
|
+
const current = this.entries.indexOf(entry);
|
|
105
|
+
if (current < 0) return;
|
|
106
|
+
this.entries.splice(current, 1);
|
|
107
|
+
this.sync();
|
|
108
|
+
this.onDismiss({ ...entry.toast }, reason);
|
|
109
|
+
};
|
|
110
|
+
const exit = index < this.limit ? this.beforeDismiss?.({ ...entry.toast }) : void 0;
|
|
111
|
+
if (exit) {
|
|
112
|
+
this.sync();
|
|
113
|
+
exit.then(finish, finish);
|
|
114
|
+
} else finish();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
clear() {
|
|
118
|
+
this.entries.slice().reverse().forEach((entry) => this.dismiss(entry.toast.id, "clear"));
|
|
119
|
+
}
|
|
120
|
+
pause(reason = "manual") {
|
|
121
|
+
if (this.pauses.has(reason)) return;
|
|
122
|
+
this.pauses.add(reason);
|
|
123
|
+
this.sync();
|
|
124
|
+
}
|
|
125
|
+
resume(reason = "manual") {
|
|
126
|
+
if (!this.pauses.delete(reason)) return;
|
|
127
|
+
this.sync();
|
|
128
|
+
}
|
|
129
|
+
normalizeDuration(value) {
|
|
130
|
+
return Number.isFinite(value) ? Math.max(0, value) : 5e3;
|
|
131
|
+
}
|
|
132
|
+
stop(entry) {
|
|
133
|
+
if (entry.timer === void 0) return;
|
|
134
|
+
clearTimeout(entry.timer);
|
|
135
|
+
entry.timer = void 0;
|
|
136
|
+
entry.remaining = Math.max(0, entry.remaining - (Date.now() - entry.started));
|
|
137
|
+
}
|
|
138
|
+
sync() {
|
|
139
|
+
this.entries.forEach((entry, index) => {
|
|
140
|
+
if (entry.closing || this.pauses.size || index >= this.limit || entry.toast.duration === 0) this.stop(entry);
|
|
141
|
+
else if (entry.timer === void 0) {
|
|
142
|
+
entry.started = Date.now();
|
|
143
|
+
entry.timer = setTimeout(() => this.dismiss(entry.toast.id, "timeout"), entry.remaining);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
this.listeners.forEach((listener) => listener());
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/toast.scss?inline
|
|
151
|
+
var toast_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: block;\n min-width: 0;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.toast {\n position: relative;\n display: flex;\n align-items: flex-start;\n box-sizing: border-box;\n background: var(--c2-toast__container--background,#ffffff);\n color: var(--c2-toast__container--color,#18181b);\n border: var(--c2-toast__container--border,1px solid #e4e4e7);\n border-radius: var(--c2-toast__container--border-radius,10px);\n padding: var(--c2-toast__container--padding,14px 16px);\n gap: var(--c2-toast__container--gap,12px);\n box-shadow: var(--c2-toast__container--box-shadow,0 4px 16px rgba(0, 0, 0, 0.1));\n font-family: var(--c2-toast__container--font-family,inherit);\n font-size: var(--c2-toast__container--font-size,14px);\n line-height: var(--c2-toast__container--line-height,1.5);\n}\n\n.content {\n flex: 1;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.heading {\n font-weight: var(--c2-toast__heading--font-weight,600);\n}\n\n.icon {\n align-self: var(--c2-toast__icon--align-self,center);\n --c2-feather-icon--size: var(--c2-toast__icon--size,20px);\n --c2-mat-icon--font-size: var(--c2-toast__icon--size,20px);\n display: inline-flex;\n flex: none;\n width: var(--c2-toast__icon--size,20px);\n height: var(--c2-toast__icon--size,20px);\n color: var(--c2-toast__icon--color,#71717a);\n}\n\n.icon svg,\n::slotted([slot=icon]) {\n width: 100%;\n height: 100%;\n}\n\n:host([variant=info]) .icon {\n --c2-feather-icon--size: var(--c2-toast__icon--size,20px);\n --c2-mat-icon--font-size: var(--c2-toast__icon--size,20px);\n color: var(--c2-toast__icon__info--color,#2563eb);\n}\n\n:host([variant=success]) .icon {\n --c2-feather-icon--size: var(--c2-toast__icon--size,20px);\n --c2-mat-icon--font-size: var(--c2-toast__icon--size,20px);\n color: var(--c2-toast__icon__success--color,#15803d);\n}\n\n:host([variant=warning]) .icon {\n --c2-feather-icon--size: var(--c2-toast__icon--size,20px);\n --c2-mat-icon--font-size: var(--c2-toast__icon--size,20px);\n color: var(--c2-toast__icon__warning--color,#a16207);\n}\n\n:host([variant=error]) .icon {\n --c2-feather-icon--size: var(--c2-toast__icon--size,20px);\n --c2-mat-icon--font-size: var(--c2-toast__icon--size,20px);\n color: var(--c2-toast__icon__error--color,#dc2626);\n}\n\nbutton {\n border: none;\n background: transparent;\n font: inherit;\n cursor: pointer;\n border-radius: 4px;\n}\nbutton:focus-visible {\n outline: var(--c2-toast__button__focus--outline,2px solid #476ef9);\n outline-offset: 2px;\n}\n\n.action {\n display: block;\n color: var(--c2-toast__action--color,#2563eb);\n padding: 4px 0;\n text-decoration: underline;\n}\n\n.close {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 4px;\n color: var(--c2-toast__close--color,#71717a);\n}\n.close:hover {\n background: var(--c2-toast__close__hover--background,#f4f4f5);\n}\n.close svg,\n.close ::slotted(*) {\n width: 100%;\n height: 100%;\n}\n\n@media (forced-colors: active) {\n .toast {\n border-color: CanvasText;\n }\n}\n.progress {\n position: absolute;\n inset-inline: 0;\n bottom: 0;\n height: var(--c2-toast__progress--height,3px);\n background: var(--c2-toast__progress--background,rgba(37, 99, 235, 0.15));\n border-radius: 0 0 var(--c2-toast__container--border-radius,10px) var(--c2-toast__container--border-radius,10px);\n overflow: hidden;\n}\n\n.progress__bar {\n height: 100%;\n background: var(--c2-toast__progress--color,#2563eb);\n transform-origin: left;\n}\n\n:host(:dir(rtl)) .progress__bar {\n transform-origin: right;\n}";
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/toast-region.scss?inline
|
|
154
|
+
var toast_region_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: block;\n position: fixed;\n z-index: var(--c2-toast-region__container--z-index,1000);\n width: var(--c2-toast-region__container--width,360px);\n max-width: calc(100vw - 2 * var(--c2-toast-region__container--offset,20px));\n bottom: max(var(--c2-toast-region__container--offset,20px), env(safe-area-inset-bottom));\n right: max(var(--c2-toast-region__container--offset,20px), env(safe-area-inset-right));\n pointer-events: none;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host([position^=top-]) {\n bottom: auto;\n top: max(var(--c2-toast-region__container--offset,20px), env(safe-area-inset-top));\n}\n\n:host([position$=-left]) {\n right: auto;\n left: max(var(--c2-toast-region__container--offset,20px), env(safe-area-inset-left));\n}\n\n:host([position$=-center]) {\n right: auto;\n left: 50%;\n transform: translateX(-50%);\n}\n\n:host([inline]) {\n position: static;\n transform: none;\n max-width: 100%;\n}\n\nsection {\n max-height: calc(100dvh - 2 * var(--c2-toast-region__container--offset,20px));\n overflow: auto;\n pointer-events: auto;\n}\n\n.stack {\n display: flex;\n flex-direction: column;\n gap: var(--c2-toast-region__container--gap,12px);\n}";
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
|
|
157
|
+
function __decorate(decorators, target, key, desc) {
|
|
158
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
159
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
160
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
161
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/toast.ts
|
|
165
|
+
var Toast = class Toast extends LitElement {
|
|
166
|
+
constructor(..._args) {
|
|
167
|
+
super(..._args);
|
|
168
|
+
this.message = "";
|
|
169
|
+
this.heading = "";
|
|
170
|
+
this.variant = "neutral";
|
|
171
|
+
this.noIcon = false;
|
|
172
|
+
this.showProgress = false;
|
|
173
|
+
this.progress = 1;
|
|
174
|
+
this.dismissible = false;
|
|
175
|
+
this.actionLabel = "";
|
|
176
|
+
this.closeLabel = "Dismiss notification";
|
|
177
|
+
this.managed = false;
|
|
178
|
+
}
|
|
179
|
+
static {
|
|
180
|
+
this.styles = unsafeCSS(toast_default);
|
|
181
|
+
}
|
|
182
|
+
/** Requests dismissal. Standalone toasts are hidden; a region removes managed toasts. */
|
|
183
|
+
dismiss() {
|
|
184
|
+
this.dispatchEvent(new CustomEvent("toast-close", {
|
|
185
|
+
bubbles: true,
|
|
186
|
+
composed: true,
|
|
187
|
+
detail: { reason: "close" }
|
|
188
|
+
}));
|
|
189
|
+
if (!this.managed) this.hidden = true;
|
|
190
|
+
}
|
|
191
|
+
handleKeydown(event) {
|
|
192
|
+
if (event.key === "Escape" && this.dismissible && !event.defaultPrevented) {
|
|
193
|
+
event.stopPropagation();
|
|
194
|
+
this.dismiss();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
renderIcon() {
|
|
198
|
+
const mark = this.variant === "success" ? html`<path d="m7 12 3 3 7-7"></path>` : this.variant === "error" ? html`<path d="m9 9 6 6m0-6-6 6"></path>` : html`<path d="M12 11v5m0-9v1"></path>`;
|
|
199
|
+
return html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
200
|
+
<circle cx="12" cy="12" r="9"></circle>
|
|
201
|
+
${mark}
|
|
202
|
+
</svg>`;
|
|
203
|
+
}
|
|
204
|
+
render() {
|
|
205
|
+
return html`<div class="toast" part="container" role=${this.managed ? nothing : "status"} aria-atomic="true" @keydown=${this.handleKeydown}>
|
|
206
|
+
${this.noIcon ? nothing : html`<span class="icon" part="icon" aria-hidden="true"><slot name="icon">${this.renderIcon()}</slot></span>`}
|
|
207
|
+
<div class="content">
|
|
208
|
+
${this.heading ? html`<div class="heading" part="heading">${this.heading}</div>` : nothing}
|
|
209
|
+
<div part="message"><slot>${this.message}</slot></div>
|
|
210
|
+
<slot name="action"
|
|
211
|
+
>${this.actionLabel ? html`<button type="button" class="action" part="action" @click=${() => this.dispatchEvent(new CustomEvent("toast-action", {
|
|
212
|
+
bubbles: true,
|
|
213
|
+
composed: true
|
|
214
|
+
}))}>${this.actionLabel}</button>` : nothing}</slot
|
|
215
|
+
>
|
|
216
|
+
</div>
|
|
217
|
+
${this.dismissible ? html`<button type="button" class="close" part="close" aria-label=${this.closeLabel} @click=${this.dismiss}>
|
|
218
|
+
<slot name="close-icon"
|
|
219
|
+
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m6 6 12 12M6 18 18 6"></path></svg
|
|
220
|
+
></slot>
|
|
221
|
+
</button>` : nothing}
|
|
222
|
+
${this.showProgress ? html`<div class="progress" part="progress" aria-hidden="true"><div class="progress__bar" part="progress-bar" style=${styleMap({ transform: `scaleX(${Number.isFinite(this.progress) ? Math.max(0, Math.min(1, this.progress)) : 1})` })}></div></div>` : nothing}
|
|
223
|
+
</div>`;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
__decorate([property()], Toast.prototype, "message", void 0);
|
|
227
|
+
__decorate([property()], Toast.prototype, "heading", void 0);
|
|
228
|
+
__decorate([property({ reflect: true })], Toast.prototype, "variant", void 0);
|
|
229
|
+
__decorate([property({
|
|
230
|
+
type: Boolean,
|
|
231
|
+
attribute: "no-icon"
|
|
232
|
+
})], Toast.prototype, "noIcon", void 0);
|
|
233
|
+
__decorate([property({
|
|
234
|
+
type: Boolean,
|
|
235
|
+
attribute: "show-progress"
|
|
236
|
+
})], Toast.prototype, "showProgress", void 0);
|
|
237
|
+
__decorate([property({ type: Number })], Toast.prototype, "progress", void 0);
|
|
238
|
+
__decorate([property({ type: Boolean })], Toast.prototype, "dismissible", void 0);
|
|
239
|
+
__decorate([property({ attribute: "action-label" })], Toast.prototype, "actionLabel", void 0);
|
|
240
|
+
__decorate([property({ attribute: "close-label" })], Toast.prototype, "closeLabel", void 0);
|
|
241
|
+
__decorate([property({ type: Boolean })], Toast.prototype, "managed", void 0);
|
|
242
|
+
Toast = __decorate([customElement("c2-toast")], Toast);
|
|
243
|
+
var ToastRegion = class ToastRegion extends LitElement {
|
|
244
|
+
static {
|
|
245
|
+
this.styles = unsafeCSS(toast_region_default);
|
|
246
|
+
}
|
|
247
|
+
constructor() {
|
|
248
|
+
super();
|
|
249
|
+
this.position = "bottom-right";
|
|
250
|
+
this.maxVisible = 3;
|
|
251
|
+
this.label = "Notifications";
|
|
252
|
+
this.inline = false;
|
|
253
|
+
this.enterAnimation = "slide-up";
|
|
254
|
+
this.exitAnimation = "fade";
|
|
255
|
+
this.animationDuration = 200;
|
|
256
|
+
this.showProgress = true;
|
|
257
|
+
this.entered = /* @__PURE__ */ new WeakSet();
|
|
258
|
+
this.animations = /* @__PURE__ */ new Map();
|
|
259
|
+
this.exits = /* @__PURE__ */ new Map();
|
|
260
|
+
this.controller = new ToastController((toast, reason) => {
|
|
261
|
+
this.dispatchEvent(new CustomEvent("toast-dismiss", {
|
|
262
|
+
bubbles: true,
|
|
263
|
+
composed: true,
|
|
264
|
+
detail: {
|
|
265
|
+
id: toast.id,
|
|
266
|
+
toast,
|
|
267
|
+
reason
|
|
268
|
+
}
|
|
269
|
+
}));
|
|
270
|
+
}, (toast) => this.animateExit(toast.id));
|
|
271
|
+
this.visibilityChanged = () => {
|
|
272
|
+
if (this.ownerDocument.hidden) this.controller.pause("hidden");
|
|
273
|
+
else this.controller.resume("hidden");
|
|
274
|
+
};
|
|
275
|
+
this.updateProgress = () => {
|
|
276
|
+
const view = this.ownerDocument?.defaultView;
|
|
277
|
+
if (this.progressFrame !== void 0) view?.cancelAnimationFrame(this.progressFrame);
|
|
278
|
+
this.progressFrame = void 0;
|
|
279
|
+
if (!this.isConnected || !view) return;
|
|
280
|
+
let running = false;
|
|
281
|
+
for (const element of this.renderRoot.querySelectorAll("c2-toast")) {
|
|
282
|
+
if (!element.showProgress) continue;
|
|
283
|
+
const timing = this.controller.getTiming(element.dataset.toastId);
|
|
284
|
+
if (!timing || timing.duration <= 0) continue;
|
|
285
|
+
element.progress = timing.remaining / timing.duration;
|
|
286
|
+
running ||= timing.running;
|
|
287
|
+
}
|
|
288
|
+
if (running) this.progressFrame = view.requestAnimationFrame(this.updateProgress);
|
|
289
|
+
};
|
|
290
|
+
this.controller.pause("disconnected");
|
|
291
|
+
}
|
|
292
|
+
connectedCallback() {
|
|
293
|
+
super.connectedCallback();
|
|
294
|
+
if (isServer) return;
|
|
295
|
+
this.unsubscribe = this.controller.subscribe(() => {
|
|
296
|
+
for (const [id, animation] of this.exits) if (!this.controller.isClosing(id)) {
|
|
297
|
+
animation.cancel();
|
|
298
|
+
this.exits.delete(id);
|
|
299
|
+
}
|
|
300
|
+
this.requestUpdate();
|
|
301
|
+
});
|
|
302
|
+
this.controller.maxVisible = this.maxVisible;
|
|
303
|
+
this.visibilityChanged();
|
|
304
|
+
this.controller.resume("disconnected");
|
|
305
|
+
this.ownerDocument.addEventListener("visibilitychange", this.visibilityChanged);
|
|
306
|
+
}
|
|
307
|
+
disconnectedCallback() {
|
|
308
|
+
super.disconnectedCallback();
|
|
309
|
+
if (isServer) return;
|
|
310
|
+
this.controller.pause("disconnected");
|
|
311
|
+
if (this.progressFrame !== void 0) this.ownerDocument.defaultView?.cancelAnimationFrame(this.progressFrame);
|
|
312
|
+
this.progressFrame = void 0;
|
|
313
|
+
this.animations.forEach((animation) => animation.cancel());
|
|
314
|
+
this.animations.clear();
|
|
315
|
+
this.controller.resume("hover");
|
|
316
|
+
this.controller.resume("focus");
|
|
317
|
+
this.unsubscribe?.();
|
|
318
|
+
this.ownerDocument.removeEventListener("visibilitychange", this.visibilityChanged);
|
|
319
|
+
}
|
|
320
|
+
updated(changed) {
|
|
321
|
+
if (changed.has("maxVisible")) this.controller.maxVisible = this.maxVisible;
|
|
322
|
+
if (!this.renderRoot.querySelector(":focus-within")) this.controller.resume("focus");
|
|
323
|
+
this.updateProgress();
|
|
324
|
+
for (const element of this.renderRoot.querySelectorAll("c2-toast")) {
|
|
325
|
+
if (this.entered.has(element)) continue;
|
|
326
|
+
this.entered.add(element);
|
|
327
|
+
if (!this.controller.isClosing(element.dataset.toastId)) this.animateToast(element, this.enterAnimation, false);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
animateToast(element, effect, exiting) {
|
|
331
|
+
this.animations.get(element)?.cancel();
|
|
332
|
+
const duration = Number.isFinite(this.animationDuration) ? Math.max(0, this.animationDuration) : 200;
|
|
333
|
+
if (!this.isConnected || effect === "none" || !duration || this.ownerDocument.defaultView?.matchMedia("(prefers-reduced-motion: reduce)").matches || !element.animate) return;
|
|
334
|
+
const hidden = {
|
|
335
|
+
opacity: 0,
|
|
336
|
+
transform: {
|
|
337
|
+
"slide-up": `translateY(${exiting ? "-" : ""}24px)`,
|
|
338
|
+
"slide-down": `translateY(${exiting ? "" : "-"}24px)`,
|
|
339
|
+
"slide-left": `translateX(${exiting ? "-" : ""}24px)`,
|
|
340
|
+
"slide-right": `translateX(${exiting ? "" : "-"}24px)`,
|
|
341
|
+
scale: "scale(0.9)"
|
|
342
|
+
}[effect] ?? "none"
|
|
343
|
+
};
|
|
344
|
+
const visible = {
|
|
345
|
+
opacity: 1,
|
|
346
|
+
transform: "none"
|
|
347
|
+
};
|
|
348
|
+
const animation = element.animate(exiting ? [visible, hidden] : [hidden, visible], {
|
|
349
|
+
duration,
|
|
350
|
+
easing: exiting ? "ease-in" : "ease-out",
|
|
351
|
+
fill: exiting ? "forwards" : "backwards"
|
|
352
|
+
});
|
|
353
|
+
this.animations.set(element, animation);
|
|
354
|
+
animation.finished.catch(() => {}).finally(() => {
|
|
355
|
+
if (this.animations.get(element) === animation) this.animations.delete(element);
|
|
356
|
+
});
|
|
357
|
+
return animation;
|
|
358
|
+
}
|
|
359
|
+
animateExit(id) {
|
|
360
|
+
const element = Array.from(this.renderRoot.querySelectorAll("c2-toast")).find((element) => element.dataset.toastId === id);
|
|
361
|
+
if (!element) return;
|
|
362
|
+
const animation = this.animateToast(element, this.exitAnimation, true);
|
|
363
|
+
if (!animation) return;
|
|
364
|
+
this.exits.set(id, animation);
|
|
365
|
+
return animation.finished.catch(() => {}).finally(() => {
|
|
366
|
+
if (this.exits.get(id) === animation) this.exits.delete(id);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/** Number of notifications waiting for a visible slot. */
|
|
370
|
+
get queuedCount() {
|
|
371
|
+
return this.controller.queuedCount;
|
|
372
|
+
}
|
|
373
|
+
/** Number of visible and queued notifications. */
|
|
374
|
+
get count() {
|
|
375
|
+
return this.controller.count;
|
|
376
|
+
}
|
|
377
|
+
/** Adds a notification or updates the one with the same ID. Returns its ID. */
|
|
378
|
+
show(options) {
|
|
379
|
+
return this.controller.show(options);
|
|
380
|
+
}
|
|
381
|
+
/** Updates content in place and restarts its visible lifetime. */
|
|
382
|
+
updateToast(id, options) {
|
|
383
|
+
return this.controller.update(id, options);
|
|
384
|
+
}
|
|
385
|
+
/** Removes a visible or queued toast. */
|
|
386
|
+
dismiss(id, reason = "programmatic") {
|
|
387
|
+
return this.controller.dismiss(id, reason);
|
|
388
|
+
}
|
|
389
|
+
/** Removes all visible and queued notifications and cancels their timers. */
|
|
390
|
+
clear() {
|
|
391
|
+
this.controller.clear();
|
|
392
|
+
}
|
|
393
|
+
/** Pauses all visible timers until resume is called. */
|
|
394
|
+
pause() {
|
|
395
|
+
this.controller.pause();
|
|
396
|
+
}
|
|
397
|
+
resume() {
|
|
398
|
+
this.controller.resume();
|
|
399
|
+
}
|
|
400
|
+
handleFocusout() {
|
|
401
|
+
queueMicrotask(() => {
|
|
402
|
+
if (!this.renderRoot.querySelector(":focus-within")) this.controller.resume("focus");
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
handleAction(event, id) {
|
|
406
|
+
event.stopPropagation();
|
|
407
|
+
const toast = this.controller.visible.find((item) => item.id === id);
|
|
408
|
+
if (!toast) return;
|
|
409
|
+
this.dispatchEvent(new CustomEvent("toast-action", {
|
|
410
|
+
bubbles: true,
|
|
411
|
+
composed: true,
|
|
412
|
+
detail: {
|
|
413
|
+
id,
|
|
414
|
+
toast
|
|
415
|
+
}
|
|
416
|
+
}));
|
|
417
|
+
this.dismiss(id, "action");
|
|
418
|
+
}
|
|
419
|
+
render() {
|
|
420
|
+
return html`<section
|
|
421
|
+
aria-label=${this.label}
|
|
422
|
+
@mouseenter=${() => this.controller.pause("hover")}
|
|
423
|
+
@mouseleave=${() => this.controller.resume("hover")}
|
|
424
|
+
@focusin=${() => this.controller.pause("focus")}
|
|
425
|
+
@focusout=${this.handleFocusout}
|
|
426
|
+
>
|
|
427
|
+
<div class="stack" part="stack" aria-live="polite" aria-relevant="additions text" aria-atomic="false">
|
|
428
|
+
${repeat(this.controller.visible, (item) => item.id, (item) => html`<c2-toast
|
|
429
|
+
managed
|
|
430
|
+
data-toast-id=${item.id}
|
|
431
|
+
?inert=${this.controller.isClosing(item.id)}
|
|
432
|
+
.message=${item.message}
|
|
433
|
+
.heading=${item.heading}
|
|
434
|
+
.variant=${item.variant}
|
|
435
|
+
.noIcon=${item.noIcon}
|
|
436
|
+
.showProgress=${item.duration > 0 && (item.showProgress ?? this.showProgress)}
|
|
437
|
+
.dismissible=${item.dismissible}
|
|
438
|
+
.actionLabel=${item.actionLabel}
|
|
439
|
+
@toast-close=${(event) => {
|
|
440
|
+
event.stopPropagation();
|
|
441
|
+
this.dismiss(item.id, "close");
|
|
442
|
+
}}
|
|
443
|
+
@toast-action=${(event) => this.handleAction(event, item.id)}
|
|
444
|
+
></c2-toast>`)}
|
|
445
|
+
</div>
|
|
446
|
+
</section>`;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
__decorate([property({ reflect: true })], ToastRegion.prototype, "position", void 0);
|
|
450
|
+
__decorate([property({
|
|
451
|
+
type: Number,
|
|
452
|
+
attribute: "max-visible"
|
|
453
|
+
})], ToastRegion.prototype, "maxVisible", void 0);
|
|
454
|
+
__decorate([property()], ToastRegion.prototype, "label", void 0);
|
|
455
|
+
__decorate([property({
|
|
456
|
+
type: Boolean,
|
|
457
|
+
reflect: true
|
|
458
|
+
})], ToastRegion.prototype, "inline", void 0);
|
|
459
|
+
__decorate([property({ attribute: "enter-animation" })], ToastRegion.prototype, "enterAnimation", void 0);
|
|
460
|
+
__decorate([property({ attribute: "exit-animation" })], ToastRegion.prototype, "exitAnimation", void 0);
|
|
461
|
+
__decorate([property({
|
|
462
|
+
type: Number,
|
|
463
|
+
attribute: "animation-duration"
|
|
464
|
+
})], ToastRegion.prototype, "animationDuration", void 0);
|
|
465
|
+
__decorate([property({
|
|
466
|
+
type: Boolean,
|
|
467
|
+
attribute: "show-progress"
|
|
468
|
+
})], ToastRegion.prototype, "showProgress", void 0);
|
|
469
|
+
ToastRegion = __decorate([customElement("c2-toast-region")], ToastRegion);
|
|
470
|
+
var defaultRegion;
|
|
471
|
+
/** Creates a shared region on first use. Safe to import during SSR; call only in a browser. */
|
|
472
|
+
function getToastRegion() {
|
|
473
|
+
if (typeof document === "undefined" || !document.body) throw new Error("Toast notifications require a document body.");
|
|
474
|
+
if (!defaultRegion?.isConnected) {
|
|
475
|
+
defaultRegion = document.createElement("c2-toast-region");
|
|
476
|
+
document.body.append(defaultRegion);
|
|
477
|
+
}
|
|
478
|
+
return defaultRegion;
|
|
479
|
+
}
|
|
480
|
+
/** Convenience manager for the shared region. Use an explicit ToastRegion for scoped stacks. */
|
|
481
|
+
var toast = {
|
|
482
|
+
show: (options) => getToastRegion().show(options),
|
|
483
|
+
update: (id, options) => defaultRegion?.updateToast(id, options) ?? false,
|
|
484
|
+
dismiss: (id) => defaultRegion?.dismiss(id) ?? false,
|
|
485
|
+
clear: () => defaultRegion?.clear()
|
|
486
|
+
};
|
|
487
|
+
//#endregion
|
|
488
|
+
export { Toast, ToastRegion, getToastRegion, toast };
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@c2n/toast",
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/toast.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"default": "./dist/toast.js",
|
|
9
|
+
"types": "./types/src/toast.d.ts"
|
|
10
|
+
},
|
|
11
|
+
"./custom-elements.json": "./custom-elements.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"types"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"toast",
|
|
19
|
+
"web component",
|
|
20
|
+
"lit"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "code2nguyen@gmail.com",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"registry": "https://registry.npmjs.org",
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/code2nguyen/web-components.git"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"dev": "vite",
|
|
34
|
+
"build": "wireit",
|
|
35
|
+
"build:only": "vite build",
|
|
36
|
+
"type-check": "wireit"
|
|
37
|
+
},
|
|
38
|
+
"wireit": {
|
|
39
|
+
"type-check": {
|
|
40
|
+
"dependencies": [
|
|
41
|
+
"../../core:build"
|
|
42
|
+
],
|
|
43
|
+
"command": "tsc -p tsconfig.lib.json --composite false"
|
|
44
|
+
},
|
|
45
|
+
"build": {
|
|
46
|
+
"dependencies": [
|
|
47
|
+
"type-check"
|
|
48
|
+
],
|
|
49
|
+
"command": "vite build"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@c2n/core": "0.0.6",
|
|
54
|
+
"lit": "3.3.3"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@c2n/config": "*"
|
|
58
|
+
},
|
|
59
|
+
"customElements": "custom-elements.json",
|
|
60
|
+
"gitHead": "2921054bc75299da66dad11c1e374246ff88a51e"
|
|
61
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type ToastVariant = 'neutral' | 'info' | 'success' | 'warning' | 'error';
|
|
2
|
+
export type ToastDismissReason = 'timeout' | 'close' | 'action' | 'programmatic' | 'clear';
|
|
3
|
+
export interface ToastOptions {
|
|
4
|
+
/** Reusing an ID updates the existing toast instead of adding a duplicate. */
|
|
5
|
+
id?: string;
|
|
6
|
+
message: string;
|
|
7
|
+
heading?: string;
|
|
8
|
+
variant?: ToastVariant;
|
|
9
|
+
/** Visible lifetime in milliseconds; 0 keeps the toast until dismissed. */
|
|
10
|
+
duration?: number;
|
|
11
|
+
dismissible?: boolean;
|
|
12
|
+
/** Hide the leading icon. */
|
|
13
|
+
noIcon?: boolean;
|
|
14
|
+
/** Overrides the region countdown-bar setting for this toast. */
|
|
15
|
+
showProgress?: boolean;
|
|
16
|
+
actionLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ToastRecord {
|
|
19
|
+
id: string;
|
|
20
|
+
message: string;
|
|
21
|
+
heading: string;
|
|
22
|
+
variant: ToastVariant;
|
|
23
|
+
duration: number;
|
|
24
|
+
dismissible: boolean;
|
|
25
|
+
noIcon: boolean;
|
|
26
|
+
showProgress?: boolean;
|
|
27
|
+
actionLabel: string;
|
|
28
|
+
}
|
|
29
|
+
/** FIFO queue. Timers run only for visible entries and preserve remaining time while paused. */
|
|
30
|
+
export declare class ToastController {
|
|
31
|
+
private entries;
|
|
32
|
+
private sequence;
|
|
33
|
+
private limit;
|
|
34
|
+
private pauses;
|
|
35
|
+
private listeners;
|
|
36
|
+
private onDismiss;
|
|
37
|
+
private beforeDismiss?;
|
|
38
|
+
constructor(onDismiss?: (toast: ToastRecord, reason: ToastDismissReason) => void, beforeDismiss?: (toast: ToastRecord) => Promise<unknown> | undefined);
|
|
39
|
+
get visible(): readonly ToastRecord[];
|
|
40
|
+
get queuedCount(): number;
|
|
41
|
+
get count(): number;
|
|
42
|
+
set maxVisible(value: number);
|
|
43
|
+
subscribe(listener: () => void): () => void;
|
|
44
|
+
show(options: ToastOptions): string;
|
|
45
|
+
/** Updates in place and restarts its visible lifetime. Queued entries retain their place. */
|
|
46
|
+
update(id: string, options: Partial<Omit<ToastOptions, 'id'>>): boolean;
|
|
47
|
+
/** Current visible lifetime, derived from the same clock used for dismissal. */
|
|
48
|
+
getTiming(id: string): {
|
|
49
|
+
remaining: number;
|
|
50
|
+
duration: number;
|
|
51
|
+
running: boolean;
|
|
52
|
+
} | undefined;
|
|
53
|
+
isClosing(id: string): boolean;
|
|
54
|
+
dismiss(id: string, reason?: ToastDismissReason): boolean;
|
|
55
|
+
clear(): void;
|
|
56
|
+
pause(reason?: string): void;
|
|
57
|
+
resume(reason?: string): void;
|
|
58
|
+
private normalizeDuration;
|
|
59
|
+
private stop;
|
|
60
|
+
private sync;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=toast-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toast-controller.d.ts","sourceRoot":"","sources":["../../src/toast-controller.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAC/E,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,cAAc,GAAG,OAAO,CAAA;AAE1F,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,6BAA6B;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,iEAAiE;IACjE,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,YAAY,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,OAAO,CAAA;IACpB,MAAM,EAAE,OAAO,CAAA;IACf,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACpB;AAUD,gGAAgG;AAChG,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,SAAS,CAAwB;IAEzC,OAAO,CAAC,SAAS,CAA0D;IAE3E,OAAO,CAAC,aAAa,CAAC,CAAsD;gBAG1E,SAAS,GAAE,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,kBAAkB,KAAK,IAAe,EAC9E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS;IAMtE,IAAI,OAAO,IAAI,SAAS,WAAW,EAAE,CAEpC;IACD,IAAI,WAAW,WAEd;IACD,IAAI,KAAK,WAER;IAED,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,EAG3B;IAED,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI;IAO9B,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM;IA+BnC,6FAA6F;IAC7F,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,GAAG,OAAO;IAcvE,gFAAgF;IAChF,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS;IAW5F,SAAS,CAAC,EAAE,EAAE,MAAM;IAIpB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,GAAE,kBAAmC,GAAG,OAAO;IAuBzE,KAAK;IAQL,KAAK,CAAC,MAAM,SAAW;IAKvB,MAAM,CAAC,MAAM,SAAW;IAKxB,OAAO,CAAC,iBAAiB;IAGzB,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,IAAI;CAWb"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import { type ToastOptions, type ToastDismissReason } from './toast-controller.js';
|
|
3
|
+
export type ToastAnimation = 'none' | 'fade' | 'slide-up' | 'slide-down' | 'slide-left' | 'slide-right' | 'scale';
|
|
4
|
+
export type { ToastOptions, ToastRecord, ToastVariant, ToastDismissReason } from './toast-controller.js';
|
|
5
|
+
/**
|
|
6
|
+
* A notification card. Use c2-toast-region to manage stacking, queueing and timed dismissal.
|
|
7
|
+
* @tag c2-toast
|
|
8
|
+
* @slot default - Message content, replacing message.
|
|
9
|
+
* @slot icon - Leading icon, with a variant-specific SVG default.
|
|
10
|
+
* @slot action - Custom action content, replacing the action-label button.
|
|
11
|
+
* @slot close-icon - Replaces the dismiss button icon.
|
|
12
|
+
* @event {CustomEvent} toast-close - Dismiss requested by the close button or Escape; detail contains reason.
|
|
13
|
+
* @event {CustomEvent} toast-action - The action-label button was activated.
|
|
14
|
+
* @cssproperty {color} [--c2-toast__container--background=#ffffff]
|
|
15
|
+
* @cssproperty {color} [--c2-toast__container--color=#18181b]
|
|
16
|
+
* @cssproperty {border} [--c2-toast__container--border=1px solid #e4e4e7]
|
|
17
|
+
* @cssproperty {border-radius} [--c2-toast__container--border-radius=10px]
|
|
18
|
+
* @cssproperty {padding} [--c2-toast__container--padding=14px 16px]
|
|
19
|
+
* @cssproperty {pixel} [--c2-toast__container--gap=12px]
|
|
20
|
+
* @cssproperty {box-shadow} [--c2-toast__container--box-shadow=0 4px 16px rgba(0, 0, 0, 0.1)]
|
|
21
|
+
* @cssproperty {font-family} [--c2-toast__container--font-family=inherit]
|
|
22
|
+
* @cssproperty {font-size} [--c2-toast__container--font-size=14px]
|
|
23
|
+
* @cssproperty {line-height} [--c2-toast__container--line-height=1.5]
|
|
24
|
+
* @cssproperty {font-weight} [--c2-toast__heading--font-weight=600]
|
|
25
|
+
* @cssproperty {align-self} [--c2-toast__icon--align-self=center]
|
|
26
|
+
* @cssproperty {pixel} [--c2-toast__progress--height=3px]
|
|
27
|
+
* @cssproperty {color} [--c2-toast__progress--color=#2563eb]
|
|
28
|
+
* @cssproperty {color} [--c2-toast__progress--background=rgba(37, 99, 235, 0.15)]
|
|
29
|
+
* @cssproperty {pixel} [--c2-toast__icon--size=20px]
|
|
30
|
+
* @cssproperty {color} [--c2-toast__icon--color=#71717a]
|
|
31
|
+
* @cssproperty {color} [--c2-toast__icon__info--color=#2563eb]
|
|
32
|
+
* @cssproperty {color} [--c2-toast__icon__success--color=#15803d]
|
|
33
|
+
* @cssproperty {color} [--c2-toast__icon__warning--color=#a16207]
|
|
34
|
+
* @cssproperty {color} [--c2-toast__icon__error--color=#dc2626]
|
|
35
|
+
* @cssproperty {color} [--c2-toast__action--color=#2563eb]
|
|
36
|
+
* @cssproperty {color} [--c2-toast__close--color=#71717a]
|
|
37
|
+
* @cssproperty {color} [--c2-toast__close__hover--background=#f4f4f5]
|
|
38
|
+
* @cssproperty {outline} [--c2-toast__button__focus--outline=2px solid #476ef9]
|
|
39
|
+
*/
|
|
40
|
+
export declare class Toast extends LitElement {
|
|
41
|
+
static styles: import("lit").CSSResult;
|
|
42
|
+
/** Plain-text message; the default slot can replace it. */
|
|
43
|
+
message: string;
|
|
44
|
+
/** Optional title above the message. */
|
|
45
|
+
heading: string;
|
|
46
|
+
/** Semantic icon variant. */
|
|
47
|
+
variant: 'neutral' | 'info' | 'success' | 'warning' | 'error';
|
|
48
|
+
/** Hides the leading icon and removes its space from the layout. */
|
|
49
|
+
noIcon: boolean;
|
|
50
|
+
/** Displays a countdown bar. Managed toasts receive progress from their region. */
|
|
51
|
+
showProgress: boolean;
|
|
52
|
+
/** Remaining fraction from 0 to 1. Standalone cards display this value without starting a timer. */
|
|
53
|
+
progress: number;
|
|
54
|
+
/** Shows a keyboard-accessible dismiss button. */
|
|
55
|
+
dismissible: boolean;
|
|
56
|
+
/** Label for the optional action button. */
|
|
57
|
+
actionLabel: string;
|
|
58
|
+
/** Accessible name of the dismiss button. */
|
|
59
|
+
closeLabel: string;
|
|
60
|
+
/** Set by a region to prevent duplicate announcements. */
|
|
61
|
+
managed: boolean;
|
|
62
|
+
/** Requests dismissal. Standalone toasts are hidden; a region removes managed toasts. */
|
|
63
|
+
dismiss(): void;
|
|
64
|
+
private handleKeydown;
|
|
65
|
+
private renderIcon;
|
|
66
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Fixed notification stack with a FIFO overflow queue and independent visible lifetimes.
|
|
70
|
+
* @tag c2-toast-region
|
|
71
|
+
* @internalcomponent c2-toast
|
|
72
|
+
* @event {CustomEvent} toast-dismiss - A toast was removed; detail contains id, toast and reason.
|
|
73
|
+
* @event {CustomEvent} toast-action - Action activated; detail contains id and toast. The toast is then dismissed.
|
|
74
|
+
* @cssproperty {pixel} [--c2-toast-region__container--width=360px]
|
|
75
|
+
* @cssproperty {pixel} [--c2-toast-region__container--gap=12px]
|
|
76
|
+
* @cssproperty {pixel} [--c2-toast-region__container--offset=20px]
|
|
77
|
+
* @cssproperty {number} [--c2-toast-region__container--z-index=1000]
|
|
78
|
+
*/
|
|
79
|
+
export declare class ToastRegion extends LitElement {
|
|
80
|
+
static styles: import("lit").CSSResult;
|
|
81
|
+
/** Viewport corner or center edge at which notifications appear. */
|
|
82
|
+
position: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
83
|
+
/** Maximum visible notifications; the rest wait without running their timers. */
|
|
84
|
+
maxVisible: number;
|
|
85
|
+
/** Accessible region name. */
|
|
86
|
+
label: string;
|
|
87
|
+
/** Inline layout for embedded notification stacks. */
|
|
88
|
+
inline: boolean;
|
|
89
|
+
/** Entrance effect. Directions describe movement: slide-down enters from above. */
|
|
90
|
+
enterAnimation: ToastAnimation;
|
|
91
|
+
/** Exit effect. Directions describe movement: slide-right exits toward the right. */
|
|
92
|
+
exitAnimation: ToastAnimation;
|
|
93
|
+
/** Entrance and exit duration in milliseconds. Zero disables animation. */
|
|
94
|
+
animationDuration: number;
|
|
95
|
+
/** Shows a remaining-time bar on timed notifications by default; set false to hide it. */
|
|
96
|
+
showProgress: boolean;
|
|
97
|
+
private progressFrame?;
|
|
98
|
+
private entered;
|
|
99
|
+
private animations;
|
|
100
|
+
private exits;
|
|
101
|
+
private controller;
|
|
102
|
+
private unsubscribe?;
|
|
103
|
+
private visibilityChanged;
|
|
104
|
+
constructor();
|
|
105
|
+
connectedCallback(): void;
|
|
106
|
+
disconnectedCallback(): void;
|
|
107
|
+
protected updated(changed: PropertyValues): void;
|
|
108
|
+
private updateProgress;
|
|
109
|
+
private animateToast;
|
|
110
|
+
private animateExit;
|
|
111
|
+
/** Number of notifications waiting for a visible slot. */
|
|
112
|
+
get queuedCount(): number;
|
|
113
|
+
/** Number of visible and queued notifications. */
|
|
114
|
+
get count(): number;
|
|
115
|
+
/** Adds a notification or updates the one with the same ID. Returns its ID. */
|
|
116
|
+
show(options: ToastOptions): string;
|
|
117
|
+
/** Updates content in place and restarts its visible lifetime. */
|
|
118
|
+
updateToast(id: string, options: Partial<Omit<ToastOptions, 'id'>>): boolean;
|
|
119
|
+
/** Removes a visible or queued toast. */
|
|
120
|
+
dismiss(id: string, reason?: ToastDismissReason): boolean;
|
|
121
|
+
/** Removes all visible and queued notifications and cancels their timers. */
|
|
122
|
+
clear(): void;
|
|
123
|
+
/** Pauses all visible timers until resume is called. */
|
|
124
|
+
pause(): void;
|
|
125
|
+
resume(): void;
|
|
126
|
+
private handleFocusout;
|
|
127
|
+
private handleAction;
|
|
128
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
129
|
+
}
|
|
130
|
+
/** Creates a shared region on first use. Safe to import during SSR; call only in a browser. */
|
|
131
|
+
export declare function getToastRegion(): ToastRegion;
|
|
132
|
+
/** Convenience manager for the shared region. Use an explicit ToastRegion for scoped stacks. */
|
|
133
|
+
export declare const toast: {
|
|
134
|
+
show: (options: ToastOptions) => string;
|
|
135
|
+
update: (id: string, options: Partial<Omit<ToastOptions, "id">>) => boolean;
|
|
136
|
+
dismiss: (id: string) => boolean;
|
|
137
|
+
clear: () => void | undefined;
|
|
138
|
+
};
|
|
139
|
+
declare global {
|
|
140
|
+
interface HTMLElementTagNameMap {
|
|
141
|
+
'c2-toast': Toast;
|
|
142
|
+
'c2-toast-region': ToastRegion;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=toast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["../../src/toast.ts"],"names":[],"mappings":"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"}
|