@pyreon/toast 0.11.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/lib/index.js ADDED
@@ -0,0 +1,362 @@
1
+ import { computed, effect, onCleanup, signal } from "@pyreon/reactivity";
2
+ import { For, Portal } from "@pyreon/core";
3
+ import { jsx, jsxs } from "@pyreon/core/jsx-runtime";
4
+
5
+ //#region src/toast.ts
6
+ let _idCounter = 0;
7
+ const DEFAULT_DURATION = 4e3;
8
+ /**
9
+ * Module-level signal holding the active toast stack.
10
+ * Consumed by the `Toaster` component.
11
+ */
12
+ const _toasts = signal([]);
13
+ function generateId() {
14
+ return `pyreon-toast-${++_idCounter}`;
15
+ }
16
+ function startTimer(t) {
17
+ if (t.duration <= 0) return;
18
+ t.timerStart = Date.now();
19
+ t.remaining = t.duration;
20
+ t.timer = setTimeout(() => dismiss(t.id), t.duration);
21
+ }
22
+ function addToast(message, options = {}) {
23
+ const id = generateId();
24
+ const t = {
25
+ id,
26
+ message,
27
+ type: options.type ?? "info",
28
+ duration: options.duration ?? DEFAULT_DURATION,
29
+ dismissible: options.dismissible ?? true,
30
+ action: options.action,
31
+ onDismiss: options.onDismiss,
32
+ state: "entering",
33
+ timer: void 0,
34
+ remaining: 0,
35
+ timerStart: 0
36
+ };
37
+ startTimer(t);
38
+ _toasts.set([..._toasts(), t]);
39
+ return id;
40
+ }
41
+ function dismiss(id) {
42
+ const current = _toasts();
43
+ if (id === void 0) {
44
+ for (const t of current) {
45
+ if (t.timer !== void 0) clearTimeout(t.timer);
46
+ t.onDismiss?.();
47
+ }
48
+ _toasts.set([]);
49
+ return;
50
+ }
51
+ const match = current.find((item) => item.id === id);
52
+ if (!match) return;
53
+ if (match.timer !== void 0) clearTimeout(match.timer);
54
+ match.onDismiss?.();
55
+ _toasts.set(current.filter((item) => item.id !== id));
56
+ }
57
+ function updateToast(id, updates) {
58
+ const current = _toasts();
59
+ const idx = current.findIndex((item) => item.id === id);
60
+ if (idx === -1) return;
61
+ const t = current[idx];
62
+ if (t.timer !== void 0) clearTimeout(t.timer);
63
+ const updated = {
64
+ ...t,
65
+ message: updates.message ?? t.message,
66
+ type: updates.type ?? t.type,
67
+ duration: updates.duration ?? t.duration,
68
+ timer: void 0,
69
+ remaining: 0,
70
+ timerStart: 0
71
+ };
72
+ updated.duration = updates.duration ?? t.duration;
73
+ startTimer(updated);
74
+ const next = [...current];
75
+ next[idx] = updated;
76
+ _toasts.set(next);
77
+ }
78
+ function _pauseAll() {
79
+ for (const t of _toasts()) if (t.timer !== void 0) {
80
+ clearTimeout(t.timer);
81
+ t.remaining = Math.max(0, t.remaining - (Date.now() - t.timerStart));
82
+ t.timer = void 0;
83
+ }
84
+ }
85
+ function _resumeAll() {
86
+ for (const t of _toasts()) if (t.duration > 0 && t.timer === void 0 && t.remaining > 0) {
87
+ t.timerStart = Date.now();
88
+ t.timer = setTimeout(() => dismiss(t.id), t.remaining);
89
+ }
90
+ }
91
+ /**
92
+ * Show a toast notification.
93
+ *
94
+ * @example
95
+ * toast("Saved!")
96
+ * toast("Error occurred", { type: "error", duration: 6000 })
97
+ *
98
+ * @returns The toast id — pass to `toast.dismiss(id)` to remove it.
99
+ */
100
+ function toast(message, options) {
101
+ return addToast(message, options);
102
+ }
103
+ function shortcut(type) {
104
+ return (message, options) => addToast(message, {
105
+ ...options,
106
+ type
107
+ });
108
+ }
109
+ /** Show a success toast. */
110
+ toast.success = shortcut("success");
111
+ /** Show an error toast. */
112
+ toast.error = shortcut("error");
113
+ /** Show a warning toast. */
114
+ toast.warning = shortcut("warning");
115
+ /** Show an info toast. */
116
+ toast.info = shortcut("info");
117
+ /** Show a persistent loading toast. Returns id for later update/dismiss. */
118
+ toast.loading = (message, options) => addToast(message, {
119
+ ...options,
120
+ type: "info",
121
+ duration: 0
122
+ });
123
+ /** Update an existing toast (message, type, duration). */
124
+ toast.update = (id, updates) => updateToast(id, updates);
125
+ /** Dismiss a specific toast by id, or all toasts if no id is given. */
126
+ toast.dismiss = dismiss;
127
+ /**
128
+ * Show a loading toast that updates on promise resolution or rejection.
129
+ *
130
+ * @example
131
+ * toast.promise(saveTodo(), {
132
+ * loading: "Saving...",
133
+ * success: "Saved!",
134
+ * error: "Failed to save",
135
+ * })
136
+ */
137
+ toast.promise = function toastPromise(promise, opts) {
138
+ const id = addToast(opts.loading, {
139
+ type: "info",
140
+ duration: 0
141
+ });
142
+ promise.then((data) => {
143
+ updateToast(id, {
144
+ message: typeof opts.success === "function" ? opts.success(data) : opts.success,
145
+ type: "success",
146
+ duration: DEFAULT_DURATION
147
+ });
148
+ }, (err) => {
149
+ updateToast(id, {
150
+ message: typeof opts.error === "function" ? opts.error(err) : opts.error,
151
+ type: "error",
152
+ duration: DEFAULT_DURATION
153
+ });
154
+ });
155
+ return promise;
156
+ };
157
+ /** @internal Reset state for testing. */
158
+ function _reset() {
159
+ const current = _toasts();
160
+ for (const t of current) if (t.timer !== void 0) clearTimeout(t.timer);
161
+ _toasts.set([]);
162
+ _idCounter = 0;
163
+ }
164
+
165
+ //#endregion
166
+ //#region src/styles.ts
167
+ /**
168
+ * Minimal CSS for the toast container and items.
169
+ * Injected into the DOM once when the Toaster component mounts.
170
+ */
171
+ const toastStyles = `
172
+ .pyreon-toast-container {
173
+ position: fixed;
174
+ z-index: 9999;
175
+ pointer-events: none;
176
+ display: flex;
177
+ flex-direction: column;
178
+ }
179
+
180
+ .pyreon-toast {
181
+ pointer-events: auto;
182
+ background: #fff;
183
+ color: #1a1a1a;
184
+ padding: 12px 16px;
185
+ border-radius: 8px;
186
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 8px;
190
+ font-size: 14px;
191
+ line-height: 1.4;
192
+ opacity: 1;
193
+ transform: translateY(0);
194
+ transition: opacity 200ms ease, transform 200ms ease, max-height 200ms ease;
195
+ max-height: 200px;
196
+ overflow: hidden;
197
+ }
198
+
199
+ .pyreon-toast--entering {
200
+ opacity: 0;
201
+ transform: translateY(-8px);
202
+ }
203
+
204
+ .pyreon-toast--exiting {
205
+ opacity: 0;
206
+ max-height: 0;
207
+ padding-top: 0;
208
+ padding-bottom: 0;
209
+ }
210
+
211
+ .pyreon-toast--info { border-left: 4px solid #3b82f6; }
212
+ .pyreon-toast--success { border-left: 4px solid #22c55e; }
213
+ .pyreon-toast--warning { border-left: 4px solid #f59e0b; }
214
+ .pyreon-toast--error { border-left: 4px solid #ef4444; }
215
+
216
+ .pyreon-toast__message { flex: 1; }
217
+
218
+ .pyreon-toast__action {
219
+ background: none;
220
+ border: 1px solid #e5e7eb;
221
+ border-radius: 4px;
222
+ padding: 4px 8px;
223
+ font-size: 13px;
224
+ cursor: pointer;
225
+ color: #3b82f6;
226
+ white-space: nowrap;
227
+ }
228
+
229
+ .pyreon-toast__action:hover {
230
+ background: #f3f4f6;
231
+ }
232
+
233
+ .pyreon-toast__dismiss {
234
+ background: none;
235
+ border: none;
236
+ cursor: pointer;
237
+ padding: 2px 4px;
238
+ font-size: 16px;
239
+ color: #9ca3af;
240
+ line-height: 1;
241
+ }
242
+
243
+ .pyreon-toast__dismiss:hover {
244
+ color: #4b5563;
245
+ }
246
+ `;
247
+
248
+ //#endregion
249
+ //#region src/toaster.tsx
250
+ let _styleInjected = false;
251
+ function injectStyles() {
252
+ if (_styleInjected) return;
253
+ _styleInjected = true;
254
+ const style = document.createElement("style");
255
+ style.setAttribute("data-pyreon-toast", "");
256
+ style.textContent = toastStyles;
257
+ document.head.appendChild(style);
258
+ }
259
+ function getContainerStyle(position, gap, offset) {
260
+ const [vertical, horizontal] = position.split("-");
261
+ let style = `gap: ${gap}px;`;
262
+ if (vertical === "top") style += ` top: ${offset}px;`;
263
+ else {
264
+ style += ` bottom: ${offset}px;`;
265
+ style += " flex-direction: column-reverse;";
266
+ }
267
+ if (horizontal === "left") style += ` left: ${offset}px;`;
268
+ else if (horizontal === "center") style += " left: 50%; transform: translateX(-50%);";
269
+ else style += ` right: ${offset}px;`;
270
+ return style;
271
+ }
272
+ /**
273
+ * Render component for toast notifications. Place once at your app root.
274
+ *
275
+ * @example
276
+ * ```tsx
277
+ * function App() {
278
+ * return (
279
+ * <>
280
+ * <Toaster position="bottom-right" />
281
+ * <MyApp />
282
+ * </>
283
+ * )
284
+ * }
285
+ * ```
286
+ */
287
+ function Toaster(props) {
288
+ const position = props?.position ?? "top-right";
289
+ const max = props?.max ?? 5;
290
+ const gap = props?.gap ?? 8;
291
+ const offset = props?.offset ?? 16;
292
+ injectStyles();
293
+ effect(() => {
294
+ if (!_toasts().some((t) => t.state === "entering")) return;
295
+ const raf = requestAnimationFrame(() => {
296
+ const current = _toasts();
297
+ let changed = false;
298
+ const next = current.map((t) => {
299
+ if (t.state === "entering") {
300
+ changed = true;
301
+ return {
302
+ ...t,
303
+ state: "visible"
304
+ };
305
+ }
306
+ return t;
307
+ });
308
+ if (changed) _toasts.set(next);
309
+ });
310
+ onCleanup(() => cancelAnimationFrame(raf));
311
+ });
312
+ const visibleToasts = computed(() => _toasts().slice(-max));
313
+ const containerStyle = getContainerStyle(position, gap, offset);
314
+ return /* @__PURE__ */ jsx(Portal, {
315
+ target: document.body,
316
+ children: /* @__PURE__ */ jsx("section", {
317
+ class: "pyreon-toast-container",
318
+ style: containerStyle,
319
+ "aria-label": "Notifications",
320
+ "aria-live": "polite",
321
+ onMouseEnter: _pauseAll,
322
+ onMouseLeave: _resumeAll,
323
+ children: /* @__PURE__ */ jsx(For, {
324
+ each: visibleToasts,
325
+ by: (t) => t.id,
326
+ children: (t) => /* @__PURE__ */ jsx(ToastItem, { toast: t })
327
+ })
328
+ })
329
+ });
330
+ }
331
+ function ToastItem({ toast: t }) {
332
+ const stateClass = t.state === "entering" ? " pyreon-toast--entering" : t.state === "exiting" ? " pyreon-toast--exiting" : "";
333
+ return /* @__PURE__ */ jsxs("div", {
334
+ class: `pyreon-toast pyreon-toast--${t.type}${stateClass}`,
335
+ role: "alert",
336
+ "aria-atomic": "true",
337
+ "data-toast-id": t.id,
338
+ children: [
339
+ /* @__PURE__ */ jsx("div", {
340
+ class: "pyreon-toast__message",
341
+ children: typeof t.message === "string" ? t.message : t.message
342
+ }),
343
+ t.action && /* @__PURE__ */ jsx("button", {
344
+ type: "button",
345
+ class: "pyreon-toast__action",
346
+ onClick: t.action.onClick,
347
+ children: t.action.label
348
+ }),
349
+ t.dismissible && /* @__PURE__ */ jsx("button", {
350
+ type: "button",
351
+ class: "pyreon-toast__dismiss",
352
+ onClick: () => toast.dismiss(t.id),
353
+ "aria-label": "Dismiss",
354
+ children: "×"
355
+ })
356
+ ]
357
+ });
358
+ }
359
+
360
+ //#endregion
361
+ export { Toaster, _reset, _toasts, toast };
362
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/toast.ts","../src/styles.ts","../src/toaster.tsx"],"sourcesContent":["import type { VNodeChild } from \"@pyreon/core\"\nimport { signal } from \"@pyreon/reactivity\"\nimport type { Toast, ToastOptions, ToastPromiseOptions, ToastType } from \"./types\"\n\n// ─── State ───────────────────────────────────────────────────────────────────\n\nlet _idCounter = 0\nconst DEFAULT_DURATION = 4000\n\n/**\n * Module-level signal holding the active toast stack.\n * Consumed by the `Toaster` component.\n */\nexport const _toasts = signal<Toast[]>([])\n\n// ─── Internal helpers ────────────────────────────────────────────────────────\n\nfunction generateId(): string {\n return `pyreon-toast-${++_idCounter}`\n}\n\nfunction startTimer(t: Toast): void {\n if (t.duration <= 0) return\n t.timerStart = Date.now()\n t.remaining = t.duration\n t.timer = setTimeout(() => dismiss(t.id), t.duration)\n}\n\nfunction addToast(message: string | VNodeChild, options: ToastOptions = {}): string {\n const id = generateId()\n const t: Toast = {\n id,\n message,\n type: options.type ?? \"info\",\n duration: options.duration ?? DEFAULT_DURATION,\n dismissible: options.dismissible ?? true,\n action: options.action,\n onDismiss: options.onDismiss,\n state: \"entering\",\n timer: undefined,\n remaining: 0,\n timerStart: 0,\n }\n\n startTimer(t)\n _toasts.set([..._toasts(), t])\n\n return id\n}\n\nfunction dismiss(id?: string): void {\n const current = _toasts()\n\n if (id === undefined) {\n // Clear all\n for (const t of current) {\n if (t.timer !== undefined) clearTimeout(t.timer)\n t.onDismiss?.()\n }\n _toasts.set([])\n return\n }\n\n const match = current.find((item) => item.id === id)\n if (!match) return\n\n if (match.timer !== undefined) clearTimeout(match.timer)\n match.onDismiss?.()\n _toasts.set(current.filter((item) => item.id !== id))\n}\n\nfunction updateToast(\n id: string,\n updates: Partial<Pick<Toast, \"message\" | \"type\" | \"duration\">>,\n): void {\n const current = _toasts()\n const idx = current.findIndex((item) => item.id === id)\n if (idx === -1) return\n\n const t = current[idx] as Toast\n if (t.timer !== undefined) clearTimeout(t.timer)\n\n const updated: Toast = {\n ...t,\n message: updates.message ?? t.message,\n type: updates.type ?? t.type,\n duration: updates.duration ?? t.duration,\n timer: undefined,\n remaining: 0,\n timerStart: 0,\n }\n\n const duration = updates.duration ?? t.duration\n updated.duration = duration\n startTimer(updated)\n\n const next = [...current]\n next[idx] = updated\n _toasts.set(next)\n}\n\n// ─── Pause / resume (for hover) ─────────────────────────────────────────────\n\nexport function _pauseAll(): void {\n for (const t of _toasts()) {\n if (t.timer !== undefined) {\n clearTimeout(t.timer)\n t.remaining = Math.max(0, t.remaining - (Date.now() - t.timerStart))\n t.timer = undefined\n }\n }\n}\n\nexport function _resumeAll(): void {\n for (const t of _toasts()) {\n if (t.duration > 0 && t.timer === undefined && t.remaining > 0) {\n t.timerStart = Date.now()\n t.timer = setTimeout(() => dismiss(t.id), t.remaining)\n }\n }\n}\n\n// ─── Public imperative API ───────────────────────────────────────────────────\n\n/**\n * Show a toast notification.\n *\n * @example\n * toast(\"Saved!\")\n * toast(\"Error occurred\", { type: \"error\", duration: 6000 })\n *\n * @returns The toast id — pass to `toast.dismiss(id)` to remove it.\n */\nexport function toast(message: string | VNodeChild, options?: ToastOptions): string {\n return addToast(message, options)\n}\n\nfunction shortcut(type: ToastType) {\n return (message: string | VNodeChild, options?: Omit<ToastOptions, \"type\">): string =>\n addToast(message, { ...options, type })\n}\n\n/** Show a success toast. */\ntoast.success = shortcut(\"success\")\n\n/** Show an error toast. */\ntoast.error = shortcut(\"error\")\n\n/** Show a warning toast. */\ntoast.warning = shortcut(\"warning\")\n\n/** Show an info toast. */\ntoast.info = shortcut(\"info\")\n\n/** Show a persistent loading toast. Returns id for later update/dismiss. */\ntoast.loading = (\n message: string | VNodeChild,\n options?: Omit<ToastOptions, \"type\" | \"duration\">,\n): string => addToast(message, { ...options, type: \"info\", duration: 0 })\n\n/** Update an existing toast (message, type, duration). */\ntoast.update = (\n id: string,\n updates: Partial<Pick<ToastOptions, \"type\" | \"duration\">> & { message?: string | VNodeChild },\n): void => updateToast(id, updates)\n\n/** Dismiss a specific toast by id, or all toasts if no id is given. */\ntoast.dismiss = dismiss\n\n/**\n * Show a loading toast that updates on promise resolution or rejection.\n *\n * @example\n * toast.promise(saveTodo(), {\n * loading: \"Saving...\",\n * success: \"Saved!\",\n * error: \"Failed to save\",\n * })\n */\ntoast.promise = function toastPromise<T>(\n promise: Promise<T>,\n opts: ToastPromiseOptions<T>,\n): Promise<T> {\n const id = addToast(opts.loading, { type: \"info\", duration: 0 })\n\n promise.then(\n (data) => {\n const msg = typeof opts.success === \"function\" ? opts.success(data) : opts.success\n updateToast(id, { message: msg, type: \"success\", duration: DEFAULT_DURATION })\n },\n (err: unknown) => {\n const msg = typeof opts.error === \"function\" ? opts.error(err) : opts.error\n updateToast(id, { message: msg, type: \"error\", duration: DEFAULT_DURATION })\n },\n )\n\n return promise\n}\n\n// ─── Test utilities ──────────────────────────────────────────────────────────\n\n/** @internal Reset state for testing. */\nexport function _reset(): void {\n const current = _toasts()\n for (const t of current) {\n if (t.timer !== undefined) clearTimeout(t.timer)\n }\n _toasts.set([])\n _idCounter = 0\n}\n","/**\n * Minimal CSS for the toast container and items.\n * Injected into the DOM once when the Toaster component mounts.\n */\nexport const toastStyles = /* css */ `\n.pyreon-toast-container {\n position: fixed;\n z-index: 9999;\n pointer-events: none;\n display: flex;\n flex-direction: column;\n}\n\n.pyreon-toast {\n pointer-events: auto;\n background: #fff;\n color: #1a1a1a;\n padding: 12px 16px;\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 14px;\n line-height: 1.4;\n opacity: 1;\n transform: translateY(0);\n transition: opacity 200ms ease, transform 200ms ease, max-height 200ms ease;\n max-height: 200px;\n overflow: hidden;\n}\n\n.pyreon-toast--entering {\n opacity: 0;\n transform: translateY(-8px);\n}\n\n.pyreon-toast--exiting {\n opacity: 0;\n max-height: 0;\n padding-top: 0;\n padding-bottom: 0;\n}\n\n.pyreon-toast--info { border-left: 4px solid #3b82f6; }\n.pyreon-toast--success { border-left: 4px solid #22c55e; }\n.pyreon-toast--warning { border-left: 4px solid #f59e0b; }\n.pyreon-toast--error { border-left: 4px solid #ef4444; }\n\n.pyreon-toast__message { flex: 1; }\n\n.pyreon-toast__action {\n background: none;\n border: 1px solid #e5e7eb;\n border-radius: 4px;\n padding: 4px 8px;\n font-size: 13px;\n cursor: pointer;\n color: #3b82f6;\n white-space: nowrap;\n}\n\n.pyreon-toast__action:hover {\n background: #f3f4f6;\n}\n\n.pyreon-toast__dismiss {\n background: none;\n border: none;\n cursor: pointer;\n padding: 2px 4px;\n font-size: 16px;\n color: #9ca3af;\n line-height: 1;\n}\n\n.pyreon-toast__dismiss:hover {\n color: #4b5563;\n}\n`\n","import type { VNodeChild } from \"@pyreon/core\"\nimport { For, Portal } from \"@pyreon/core\"\nimport { computed, effect, onCleanup } from \"@pyreon/reactivity\"\nimport { toastStyles } from \"./styles\"\nimport { _pauseAll, _resumeAll, _toasts, toast } from \"./toast\"\nimport type { Toast, ToasterProps, ToastPosition } from \"./types\"\n\n// ─── Style injection ─────────────────────────────────────────────────────────\n\nlet _styleInjected = false\n\nfunction injectStyles(): void {\n if (_styleInjected) return\n _styleInjected = true\n\n const style = document.createElement(\"style\")\n style.setAttribute(\"data-pyreon-toast\", \"\")\n style.textContent = toastStyles\n document.head.appendChild(style)\n}\n\n// ─── Position helpers ────────────────────────────────────────────────────────\n\nfunction getContainerStyle(position: ToastPosition, gap: number, offset: number): string {\n const [vertical, horizontal] = position.split(\"-\") as [string, string]\n\n let style = `gap: ${gap}px;`\n\n if (vertical === \"top\") {\n style += ` top: ${offset}px;`\n } else {\n style += ` bottom: ${offset}px;`\n style += \" flex-direction: column-reverse;\"\n }\n\n if (horizontal === \"left\") {\n style += ` left: ${offset}px;`\n } else if (horizontal === \"center\") {\n style += \" left: 50%; transform: translateX(-50%);\"\n } else {\n style += ` right: ${offset}px;`\n }\n\n return style\n}\n\n// ─── Toaster component ──────────────────────────────────────────────────────\n\n/**\n * Render component for toast notifications. Place once at your app root.\n *\n * @example\n * ```tsx\n * function App() {\n * return (\n * <>\n * <Toaster position=\"bottom-right\" />\n * <MyApp />\n * </>\n * )\n * }\n * ```\n */\nexport function Toaster(props?: ToasterProps): VNodeChild {\n const position = props?.position ?? \"top-right\"\n const max = props?.max ?? 5\n const gap = props?.gap ?? 8\n const offset = props?.offset ?? 16\n\n injectStyles()\n\n // Promote \"entering\" toasts to \"visible\" on next frame.\n // Only runs when there are actually entering toasts (early return guard).\n effect(() => {\n const toasts = _toasts()\n const hasEntering = toasts.some((t) => t.state === \"entering\")\n if (!hasEntering) return\n\n const raf = requestAnimationFrame(() => {\n const current = _toasts()\n let changed = false\n const next = current.map((t) => {\n if (t.state === \"entering\") {\n changed = true\n return { ...t, state: \"visible\" as const }\n }\n return t\n })\n if (changed) _toasts.set(next)\n })\n\n onCleanup(() => cancelAnimationFrame(raf))\n })\n\n // Computed visible toasts — only the most recent `max` items\n const visibleToasts = computed(() => _toasts().slice(-max))\n\n const containerStyle = getContainerStyle(position, gap, offset)\n\n return (\n <Portal target={document.body}>\n <section\n class=\"pyreon-toast-container\"\n style={containerStyle}\n aria-label=\"Notifications\"\n aria-live=\"polite\"\n onMouseEnter={_pauseAll}\n onMouseLeave={_resumeAll}\n >\n <For each={visibleToasts} by={(t: Toast) => t.id}>\n {(t: Toast) => <ToastItem toast={t} />}\n </For>\n </section>\n </Portal>\n )\n}\n\n// ─── Toast item ─────────────────────────────────────────────────────────────\n\nfunction ToastItem({ toast: t }: { toast: Toast }): VNodeChild {\n const stateClass =\n t.state === \"entering\"\n ? \" pyreon-toast--entering\"\n : t.state === \"exiting\"\n ? \" pyreon-toast--exiting\"\n : \"\"\n\n return (\n <div\n class={`pyreon-toast pyreon-toast--${t.type}${stateClass}`}\n role=\"alert\"\n aria-atomic=\"true\"\n data-toast-id={t.id}\n >\n <div class=\"pyreon-toast__message\">\n {typeof t.message === \"string\" ? t.message : t.message}\n </div>\n {t.action && (\n <button type=\"button\" class=\"pyreon-toast__action\" onClick={t.action.onClick}>\n {t.action.label}\n </button>\n )}\n {t.dismissible && (\n <button\n type=\"button\"\n class=\"pyreon-toast__dismiss\"\n onClick={() => toast.dismiss(t.id)}\n aria-label=\"Dismiss\"\n >\n ×\n </button>\n )}\n </div>\n )\n}\n"],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ import * as _pyreon_reactivity0 from "@pyreon/reactivity";
2
+ import { VNodeChild } from "@pyreon/core";
3
+
4
+ //#region src/types.d.ts
5
+ type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
6
+ type ToastType = "info" | "success" | "warning" | "error";
7
+ interface ToastOptions {
8
+ /** Toast variant — controls styling. */
9
+ type?: ToastType;
10
+ /** Auto-dismiss delay in ms. Default: 4000. Set 0 for persistent. */
11
+ duration?: number;
12
+ /** Screen position. Default: inherited from Toaster. */
13
+ position?: ToastPosition;
14
+ /** Whether the toast shows a dismiss button. Default: true. */
15
+ dismissible?: boolean;
16
+ /** Optional action button. */
17
+ action?: {
18
+ label: string;
19
+ onClick: () => void;
20
+ };
21
+ /** Called when the toast is dismissed (manually or by timeout). */
22
+ onDismiss?: () => void;
23
+ }
24
+ interface ToasterProps {
25
+ /** Default position for all toasts. Default: "top-right". */
26
+ position?: ToastPosition;
27
+ /** Maximum visible toasts. Default: 5. */
28
+ max?: number;
29
+ /** Gap between toasts in px. Default: 8. */
30
+ gap?: number;
31
+ /** Offset from viewport edge in px. Default: 16. */
32
+ offset?: number;
33
+ }
34
+ interface ToastPromiseOptions<T> {
35
+ loading: string | VNodeChild;
36
+ success: string | VNodeChild | ((data: T) => string | VNodeChild);
37
+ error: string | VNodeChild | ((err: unknown) => string | VNodeChild);
38
+ }
39
+ type ToastState = "entering" | "visible" | "exiting";
40
+ interface Toast {
41
+ id: string;
42
+ message: string | VNodeChild;
43
+ type: ToastType;
44
+ duration: number;
45
+ dismissible: boolean;
46
+ action: {
47
+ label: string;
48
+ onClick: () => void;
49
+ } | undefined;
50
+ onDismiss: (() => void) | undefined;
51
+ state: ToastState;
52
+ timer: ReturnType<typeof setTimeout> | undefined;
53
+ /** Remaining ms when timer was paused (hover). */
54
+ remaining: number;
55
+ /** Timestamp when current timer started. */
56
+ timerStart: number;
57
+ }
58
+ //#endregion
59
+ //#region src/toast.d.ts
60
+ /**
61
+ * Module-level signal holding the active toast stack.
62
+ * Consumed by the `Toaster` component.
63
+ */
64
+ declare const _toasts: _pyreon_reactivity0.Signal<Toast[]>;
65
+ /**
66
+ * Show a toast notification.
67
+ *
68
+ * @example
69
+ * toast("Saved!")
70
+ * toast("Error occurred", { type: "error", duration: 6000 })
71
+ *
72
+ * @returns The toast id — pass to `toast.dismiss(id)` to remove it.
73
+ */
74
+ declare function toast(message: string | VNodeChild, options?: ToastOptions): string;
75
+ declare namespace toast {
76
+ var success: (message: string | VNodeChild, options?: Omit<ToastOptions, "type">) => string;
77
+ var error: (message: string | VNodeChild, options?: Omit<ToastOptions, "type">) => string;
78
+ var warning: (message: string | VNodeChild, options?: Omit<ToastOptions, "type">) => string;
79
+ var info: (message: string | VNodeChild, options?: Omit<ToastOptions, "type">) => string;
80
+ var loading: (message: string | VNodeChild, options?: Omit<ToastOptions, "type" | "duration">) => string;
81
+ var update: (id: string, updates: Partial<Pick<ToastOptions, "type" | "duration">> & {
82
+ message?: string | VNodeChild;
83
+ }) => void;
84
+ var dismiss: (id?: string) => void;
85
+ var promise: <T>(promise: Promise<T>, opts: ToastPromiseOptions<T>) => Promise<T>;
86
+ }
87
+ /** @internal Reset state for testing. */
88
+ declare function _reset(): void;
89
+ //#endregion
90
+ //#region src/toaster.d.ts
91
+ /**
92
+ * Render component for toast notifications. Place once at your app root.
93
+ *
94
+ * @example
95
+ * ```tsx
96
+ * function App() {
97
+ * return (
98
+ * <>
99
+ * <Toaster position="bottom-right" />
100
+ * <MyApp />
101
+ * </>
102
+ * )
103
+ * }
104
+ * ```
105
+ */
106
+ declare function Toaster(props?: ToasterProps): VNodeChild;
107
+ //#endregion
108
+ export { type Toast, type ToastOptions, type ToastPosition, type ToastPromiseOptions, type ToastState, type ToastType, Toaster, type ToasterProps, _reset, _toasts, toast };
109
+ //# sourceMappingURL=index2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/toast.ts","../../../src/toaster.tsx"],"mappings":";;;;KAIY,aAAA;AAAA,KAQA,SAAA;AAAA,UAEK,YAAA;EAVL;EAYV,IAAA,GAAO,SAAA;;EAEP,QAAA;EAduB;EAgBvB,QAAA,GAAW,aAAA;EARQ;EAUnB,WAAA;EAVmB;EAYnB,MAAA;IAAW,KAAA;IAAe,OAAA;EAAA;EAJF;EAMxB,SAAA;AAAA;AAAA,UAGe,YAAA;EATf;EAWA,QAAA,GAAW,aAAA;EATX;EAWA,GAAA;EATW;EAWX,GAAA;EATA;EAWA,MAAA;AAAA;AAAA,UAGe,mBAAA;EACf,OAAA,WAAkB,UAAA;EAClB,OAAA,WAAkB,UAAA,KAAe,IAAA,EAAM,CAAA,cAAe,UAAA;EACtD,KAAA,WAAgB,UAAA,KAAe,GAAA,uBAA0B,UAAA;AAAA;AAAA,KAK/C,UAAA;AAAA,UAEK,KAAA;EACf,EAAA;EACA,OAAA,WAAkB,UAAA;EAClB,IAAA,EAAM,SAAA;EACN,QAAA;EACA,WAAA;EACA,MAAA;IAAU,KAAA;IAAe,OAAA;EAAA;EACzB,SAAA;EACA,KAAA,EAAO,UAAA;EACP,KAAA,EAAO,UAAA,QAAkB,UAAA;EAhBgC;EAkBzD,SAAA;EAlBmE;EAoBnE,UAAA;AAAA;;;;;AA3DF;;cCSa,OAAA,EAAO,mBAAA,CAAA,MAAA,CAAA,KAAA;ADDpB;;;;;AAEA;;;;AAFA,iBCyHgB,KAAA,CAAM,OAAA,WAAkB,UAAA,EAAY,OAAA,GAAU,YAAA;AAAA,kBAA9C,KAAA;EAAA,gCAKY,UAAA,EAAU,OAAA,GAAY,IAAA,CAAK,YAAA;EAAA,8BAA3B,UAAA,EAAU,OAAA,GAAY,IAAA,CAAK,YAAA;EAAA,gCAA3B,UAAA,EAAU,OAAA,GAAY,IAAA,CAAK,YAAA;EAAA,6BAA3B,UAAA,EAAU,OAAA,GAAY,IAAA,CAAK,YAAA;EAAA,gCAkBnC,UAAA,EAAU,OAAA,GAClB,IAAA,CAAK,YAAA;EAAA,yBAKL,OAAA,EACD,OAAA,CAAQ,IAAA,CAAK,YAAA;IAAwC,OAAA,YAAmB,UAAA;EAAA;EAAA;mBAgB5C,OAAA,EAC5B,OAAA,CAAQ,CAAA,GAAE,IAAA,EACb,mBAAA,CAAoB,CAAA,MACzB,OAAA,CAAQ,CAAA;AAAA;;iBAoBK,MAAA,CAAA;;;;;ADtMhB;;;;;AAQA;;;;;AAEA;;;iBEiDgB,OAAA,CAAQ,KAAA,GAAQ,YAAA,GAAe,UAAA"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@pyreon/toast",
3
+ "version": "0.11.0",
4
+ "description": "Imperative toast notifications for Pyreon — no provider needed",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/pyreon/pyreon.git",
9
+ "directory": "packages/fundamentals/toast"
10
+ },
11
+ "homepage": "https://github.com/pyreon/fundamentals/tree/main/packages/toast#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/pyreon/pyreon/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "files": [
19
+ "lib",
20
+ "src",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "type": "module",
25
+ "main": "./lib/index.js",
26
+ "module": "./lib/index.js",
27
+ "types": "./lib/types/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "bun": "./src/index.ts",
31
+ "import": "./lib/index.js",
32
+ "types": "./lib/types/index.d.ts"
33
+ }
34
+ },
35
+ "sideEffects": false,
36
+ "scripts": {
37
+ "build": "vl_rolldown_build",
38
+ "dev": "vl_rolldown_build-watch",
39
+ "test": "vitest run",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "biome check ."
42
+ },
43
+ "peerDependencies": {
44
+ "@pyreon/core": "^0.11.0",
45
+ "@pyreon/reactivity": "^0.11.0"
46
+ },
47
+ "devDependencies": {
48
+ "@happy-dom/global-registrator": "^20.8.3",
49
+ "@pyreon/core": "^0.11.0",
50
+ "@pyreon/reactivity": "^0.11.0",
51
+ "@vitus-labs/tools-lint": "^1.11.0"
52
+ }
53
+ }
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @pyreon/toast — imperative toast notifications for Pyreon.
3
+ *
4
+ * No provider needed — call `toast()` from anywhere in your app,
5
+ * and render `<Toaster />` once at the root.
6
+ *
7
+ * @example
8
+ * import { toast, Toaster } from "@pyreon/toast"
9
+ *
10
+ * // In your root component:
11
+ * function App() {
12
+ * return (
13
+ * <>
14
+ * <Toaster position="top-right" />
15
+ * <button onClick={() => toast.success("Saved!")}>Save</button>
16
+ * </>
17
+ * )
18
+ * }
19
+ *
20
+ * // Anywhere in your app:
21
+ * toast("Hello!")
22
+ * toast.error("Something went wrong")
23
+ * toast.promise(fetchData(), {
24
+ * loading: "Loading...",
25
+ * success: "Done!",
26
+ * error: "Failed",
27
+ * })
28
+ */
29
+
30
+ export { _reset, _toasts, toast } from "./toast"
31
+ export { Toaster } from "./toaster"
32
+ export type {
33
+ Toast,
34
+ ToasterProps,
35
+ ToastOptions,
36
+ ToastPosition,
37
+ ToastPromiseOptions,
38
+ ToastState,
39
+ ToastType,
40
+ } from "./types"
package/src/styles.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Minimal CSS for the toast container and items.
3
+ * Injected into the DOM once when the Toaster component mounts.
4
+ */
5
+ export const toastStyles = /* css */ `
6
+ .pyreon-toast-container {
7
+ position: fixed;
8
+ z-index: 9999;
9
+ pointer-events: none;
10
+ display: flex;
11
+ flex-direction: column;
12
+ }
13
+
14
+ .pyreon-toast {
15
+ pointer-events: auto;
16
+ background: #fff;
17
+ color: #1a1a1a;
18
+ padding: 12px 16px;
19
+ border-radius: 8px;
20
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 8px;
24
+ font-size: 14px;
25
+ line-height: 1.4;
26
+ opacity: 1;
27
+ transform: translateY(0);
28
+ transition: opacity 200ms ease, transform 200ms ease, max-height 200ms ease;
29
+ max-height: 200px;
30
+ overflow: hidden;
31
+ }
32
+
33
+ .pyreon-toast--entering {
34
+ opacity: 0;
35
+ transform: translateY(-8px);
36
+ }
37
+
38
+ .pyreon-toast--exiting {
39
+ opacity: 0;
40
+ max-height: 0;
41
+ padding-top: 0;
42
+ padding-bottom: 0;
43
+ }
44
+
45
+ .pyreon-toast--info { border-left: 4px solid #3b82f6; }
46
+ .pyreon-toast--success { border-left: 4px solid #22c55e; }
47
+ .pyreon-toast--warning { border-left: 4px solid #f59e0b; }
48
+ .pyreon-toast--error { border-left: 4px solid #ef4444; }
49
+
50
+ .pyreon-toast__message { flex: 1; }
51
+
52
+ .pyreon-toast__action {
53
+ background: none;
54
+ border: 1px solid #e5e7eb;
55
+ border-radius: 4px;
56
+ padding: 4px 8px;
57
+ font-size: 13px;
58
+ cursor: pointer;
59
+ color: #3b82f6;
60
+ white-space: nowrap;
61
+ }
62
+
63
+ .pyreon-toast__action:hover {
64
+ background: #f3f4f6;
65
+ }
66
+
67
+ .pyreon-toast__dismiss {
68
+ background: none;
69
+ border: none;
70
+ cursor: pointer;
71
+ padding: 2px 4px;
72
+ font-size: 16px;
73
+ color: #9ca3af;
74
+ line-height: 1;
75
+ }
76
+
77
+ .pyreon-toast__dismiss:hover {
78
+ color: #4b5563;
79
+ }
80
+ `