@pyreon/toast 0.11.3 → 0.11.5
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/analysis/index.js.html +1 -1
- package/lib/index.js +2 -3
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/tests/toast.test.ts +348 -1
- package/src/toaster.tsx +2 -4
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"e47bdc66-1","name":"toast.ts"},{"uid":"e47bdc66-3","name":"styles.ts"},{"uid":"e47bdc66-5","name":"toaster.tsx"},{"uid":"e47bdc66-7","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"e47bdc66-1":{"renderedLength":4188,"gzipLength":1428,"brotliLength":0,"metaUid":"e47bdc66-0"},"e47bdc66-3":{"renderedLength":1641,"gzipLength":689,"brotliLength":0,"metaUid":"e47bdc66-2"},"e47bdc66-5":{"renderedLength":3150,"gzipLength":1253,"brotliLength":0,"metaUid":"e47bdc66-4"},"e47bdc66-7":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"e47bdc66-6"}},"nodeMetas":{"e47bdc66-0":{"id":"/src/toast.ts","moduleParts":{"index.js":"e47bdc66-1"},"imported":[{"uid":"e47bdc66-8"}],"importedBy":[{"uid":"e47bdc66-6"},{"uid":"e47bdc66-4"}]},"e47bdc66-2":{"id":"/src/styles.ts","moduleParts":{"index.js":"e47bdc66-3"},"imported":[],"importedBy":[{"uid":"e47bdc66-4"}]},"e47bdc66-4":{"id":"/src/toaster.tsx","moduleParts":{"index.js":"e47bdc66-5"},"imported":[{"uid":"e47bdc66-9"},{"uid":"e47bdc66-8"},{"uid":"e47bdc66-2"},{"uid":"e47bdc66-0"},{"uid":"e47bdc66-10"}],"importedBy":[{"uid":"e47bdc66-6"}]},"e47bdc66-6":{"id":"/src/index.ts","moduleParts":{"index.js":"e47bdc66-7"},"imported":[{"uid":"e47bdc66-0"},{"uid":"e47bdc66-4"}],"importedBy":[],"isEntry":true},"e47bdc66-8":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"e47bdc66-0"},{"uid":"e47bdc66-4"}]},"e47bdc66-9":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"e47bdc66-4"}]},"e47bdc66-10":{"id":"@pyreon/core/jsx-runtime","moduleParts":{},"imported":[],"importedBy":[{"uid":"e47bdc66-4"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|
package/lib/index.js
CHANGED
|
@@ -247,10 +247,9 @@ const toastStyles = `
|
|
|
247
247
|
|
|
248
248
|
//#endregion
|
|
249
249
|
//#region src/toaster.tsx
|
|
250
|
-
let _styleInjected = false;
|
|
251
250
|
function injectStyles() {
|
|
252
|
-
if (
|
|
253
|
-
|
|
251
|
+
if (typeof document === "undefined") return;
|
|
252
|
+
if (document.querySelector("style[data-pyreon-toast]")) return;
|
|
254
253
|
const style = document.createElement("style");
|
|
255
254
|
style.setAttribute("data-pyreon-toast", "");
|
|
256
255
|
style.textContent = toastStyles;
|
package/lib/index.js.map
CHANGED
|
@@ -1 +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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"}
|
|
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\nfunction injectStyles(): void {\n if (typeof document === \"undefined\") return\n if (document.querySelector(\"style[data-pyreon-toast]\")) return\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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"}
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +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;;;
|
|
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;;;iBE+CgB,OAAA,CAAQ,KAAA,GAAQ,YAAA,GAAe,UAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/toast",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"description": "Imperative toast notifications for Pyreon — no provider needed",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"lint": "biome check ."
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@pyreon/core": "^0.11.
|
|
45
|
-
"@pyreon/reactivity": "^0.11.
|
|
44
|
+
"@pyreon/core": "^0.11.5",
|
|
45
|
+
"@pyreon/reactivity": "^0.11.5"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@happy-dom/global-registrator": "^20.8.3",
|
|
49
|
-
"@pyreon/core": "^0.11.
|
|
50
|
-
"@pyreon/reactivity": "^0.11.
|
|
49
|
+
"@pyreon/core": "^0.11.5",
|
|
50
|
+
"@pyreon/reactivity": "^0.11.5",
|
|
51
51
|
"@vitus-labs/tools-lint": "^1.11.0"
|
|
52
52
|
}
|
|
53
53
|
}
|
package/src/tests/toast.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
2
|
-
import { _reset, _toasts, toast } from "../toast"
|
|
2
|
+
import { _pauseAll, _reset, _resumeAll, _toasts, toast } from "../toast"
|
|
3
3
|
|
|
4
4
|
/** Helper — get toast at index with non-null assertion (tests verify length first). */
|
|
5
5
|
function at(index: number) {
|
|
@@ -239,6 +239,353 @@ describe("toast.promise", () => {
|
|
|
239
239
|
})
|
|
240
240
|
})
|
|
241
241
|
|
|
242
|
+
describe("toast.loading", () => {
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
vi.useFakeTimers()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
afterEach(() => {
|
|
248
|
+
vi.useRealTimers()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it("creates a persistent toast with type info", () => {
|
|
252
|
+
const id = toast.loading("Please wait...")
|
|
253
|
+
expect(_toasts().length).toBe(1)
|
|
254
|
+
expect(at(0).message).toBe("Please wait...")
|
|
255
|
+
expect(at(0).type).toBe("info")
|
|
256
|
+
expect(at(0).duration).toBe(0)
|
|
257
|
+
|
|
258
|
+
// Should not auto-dismiss even after a long time
|
|
259
|
+
vi.advanceTimersByTime(30000)
|
|
260
|
+
expect(_toasts().length).toBe(1)
|
|
261
|
+
expect(at(0).id).toBe(id)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it("returns an id that can be dismissed manually", () => {
|
|
265
|
+
const id = toast.loading("Loading data...")
|
|
266
|
+
expect(_toasts().length).toBe(1)
|
|
267
|
+
|
|
268
|
+
toast.dismiss(id)
|
|
269
|
+
expect(_toasts().length).toBe(0)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it("accepts options like onDismiss and action", () => {
|
|
273
|
+
const onDismiss = vi.fn()
|
|
274
|
+
const id = toast.loading("Loading...", { onDismiss })
|
|
275
|
+
toast.dismiss(id)
|
|
276
|
+
expect(onDismiss).toHaveBeenCalledOnce()
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("can be updated via toast.update after creation", () => {
|
|
280
|
+
const id = toast.loading("Step 1...")
|
|
281
|
+
expect(at(0).message).toBe("Step 1...")
|
|
282
|
+
|
|
283
|
+
toast.update(id, { message: "Step 2..." })
|
|
284
|
+
expect(at(0).message).toBe("Step 2...")
|
|
285
|
+
expect(at(0).type).toBe("info")
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe("toast.update", () => {
|
|
290
|
+
beforeEach(() => {
|
|
291
|
+
vi.useFakeTimers()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
afterEach(() => {
|
|
295
|
+
vi.useRealTimers()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it("updates message of existing toast", () => {
|
|
299
|
+
const id = toast("Original")
|
|
300
|
+
toast.update(id, { message: "Updated" })
|
|
301
|
+
expect(at(0).message).toBe("Updated")
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it("updates type of existing toast", () => {
|
|
305
|
+
const id = toast("Hello")
|
|
306
|
+
expect(at(0).type).toBe("info")
|
|
307
|
+
|
|
308
|
+
toast.update(id, { type: "success" })
|
|
309
|
+
expect(at(0).type).toBe("success")
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it("updates duration and restarts timer", () => {
|
|
313
|
+
const id = toast("Hello", { duration: 0 }) // persistent
|
|
314
|
+
expect(at(0).duration).toBe(0)
|
|
315
|
+
|
|
316
|
+
vi.advanceTimersByTime(5000)
|
|
317
|
+
expect(_toasts().length).toBe(1) // still there, duration was 0
|
|
318
|
+
|
|
319
|
+
toast.update(id, { duration: 2000 })
|
|
320
|
+
expect(at(0).duration).toBe(2000)
|
|
321
|
+
|
|
322
|
+
vi.advanceTimersByTime(1999)
|
|
323
|
+
expect(_toasts().length).toBe(1)
|
|
324
|
+
|
|
325
|
+
vi.advanceTimersByTime(1)
|
|
326
|
+
expect(_toasts().length).toBe(0) // auto-dismissed after new duration
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it("is a no-op for unknown id", () => {
|
|
330
|
+
toast("Hello")
|
|
331
|
+
toast.update("nonexistent-id", { message: "Should not crash" })
|
|
332
|
+
expect(at(0).message).toBe("Hello")
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it("clears old timer when updating", () => {
|
|
336
|
+
const id = toast("Hello", { duration: 1000 })
|
|
337
|
+
|
|
338
|
+
vi.advanceTimersByTime(500)
|
|
339
|
+
// Update resets the timer with the same duration
|
|
340
|
+
toast.update(id, { message: "Updated" })
|
|
341
|
+
|
|
342
|
+
vi.advanceTimersByTime(500)
|
|
343
|
+
// Old timer would have fired at 1000ms total, but update reset it
|
|
344
|
+
expect(_toasts().length).toBe(1)
|
|
345
|
+
|
|
346
|
+
vi.advanceTimersByTime(500)
|
|
347
|
+
// Now the new timer fires at 1000ms from update
|
|
348
|
+
expect(_toasts().length).toBe(0)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it("can update multiple fields at once", () => {
|
|
352
|
+
const id = toast("Loading...", { duration: 0 })
|
|
353
|
+
toast.update(id, { message: "Done!", type: "success", duration: 3000 })
|
|
354
|
+
|
|
355
|
+
expect(at(0).message).toBe("Done!")
|
|
356
|
+
expect(at(0).type).toBe("success")
|
|
357
|
+
expect(at(0).duration).toBe(3000)
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe("pause/resume", () => {
|
|
362
|
+
beforeEach(() => {
|
|
363
|
+
vi.useFakeTimers()
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
afterEach(() => {
|
|
367
|
+
vi.useRealTimers()
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it("_pauseAll stops all timers", () => {
|
|
371
|
+
toast("First", { duration: 4000 })
|
|
372
|
+
toast("Second", { duration: 4000 })
|
|
373
|
+
|
|
374
|
+
vi.advanceTimersByTime(2000) // halfway
|
|
375
|
+
|
|
376
|
+
_pauseAll()
|
|
377
|
+
|
|
378
|
+
// Advance well past the original duration — should not dismiss
|
|
379
|
+
vi.advanceTimersByTime(10000)
|
|
380
|
+
expect(_toasts().length).toBe(2)
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it("_resumeAll restarts timers with remaining time", () => {
|
|
384
|
+
toast("Hello", { duration: 4000 })
|
|
385
|
+
|
|
386
|
+
vi.advanceTimersByTime(3000) // 1000ms remaining
|
|
387
|
+
_pauseAll()
|
|
388
|
+
|
|
389
|
+
vi.advanceTimersByTime(5000) // paused, nothing happens
|
|
390
|
+
expect(_toasts().length).toBe(1)
|
|
391
|
+
|
|
392
|
+
_resumeAll()
|
|
393
|
+
|
|
394
|
+
vi.advanceTimersByTime(999) // almost there
|
|
395
|
+
expect(_toasts().length).toBe(1)
|
|
396
|
+
|
|
397
|
+
vi.advanceTimersByTime(1) // now it fires
|
|
398
|
+
expect(_toasts().length).toBe(0)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it("_pauseAll is no-op for persistent toasts (duration 0)", () => {
|
|
402
|
+
toast("Persistent", { duration: 0 })
|
|
403
|
+
|
|
404
|
+
_pauseAll()
|
|
405
|
+
_resumeAll()
|
|
406
|
+
|
|
407
|
+
vi.advanceTimersByTime(10000)
|
|
408
|
+
expect(_toasts().length).toBe(1)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it("_resumeAll is no-op when no toasts are paused", () => {
|
|
412
|
+
toast("Hello", { duration: 4000 })
|
|
413
|
+
|
|
414
|
+
// Call resume without pause — should not cause issues
|
|
415
|
+
_resumeAll()
|
|
416
|
+
|
|
417
|
+
vi.advanceTimersByTime(4000)
|
|
418
|
+
expect(_toasts().length).toBe(0)
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
describe("max queue behavior", () => {
|
|
423
|
+
it("adding more toasts than max still stores all in _toasts", () => {
|
|
424
|
+
// The Toaster component limits visible toasts via computed slice,
|
|
425
|
+
// but the underlying signal holds all toasts
|
|
426
|
+
for (let i = 0; i < 10; i++) {
|
|
427
|
+
toast(`Toast ${i}`, { duration: 0 })
|
|
428
|
+
}
|
|
429
|
+
expect(_toasts().length).toBe(10)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it("each toast gets a unique id", () => {
|
|
433
|
+
const ids = new Set<string>()
|
|
434
|
+
for (let i = 0; i < 20; i++) {
|
|
435
|
+
ids.add(toast(`Toast ${i}`, { duration: 0 }))
|
|
436
|
+
}
|
|
437
|
+
expect(ids.size).toBe(20)
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
describe("toast.promise with rejected promise", () => {
|
|
442
|
+
beforeEach(() => {
|
|
443
|
+
vi.useFakeTimers()
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
afterEach(() => {
|
|
447
|
+
vi.useRealTimers()
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it("uses function error handler with error argument", async () => {
|
|
451
|
+
const err = new Error("network failure")
|
|
452
|
+
const promise = Promise.reject(err)
|
|
453
|
+
|
|
454
|
+
toast
|
|
455
|
+
.promise(promise, {
|
|
456
|
+
loading: "Saving...",
|
|
457
|
+
success: "Saved!",
|
|
458
|
+
error: (e: unknown) => `Failed: ${(e as Error).message}`,
|
|
459
|
+
})
|
|
460
|
+
.catch(() => {})
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
await promise
|
|
464
|
+
} catch {
|
|
465
|
+
// expected
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
469
|
+
|
|
470
|
+
expect(at(0).message).toBe("Failed: network failure")
|
|
471
|
+
expect(at(0).type).toBe("error")
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it("resolved promise toast gets auto-dismiss timer", async () => {
|
|
475
|
+
const promise = Promise.resolve("ok")
|
|
476
|
+
|
|
477
|
+
toast.promise(promise, {
|
|
478
|
+
loading: "Loading...",
|
|
479
|
+
success: "Done!",
|
|
480
|
+
error: "Failed",
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
// Loading toast is persistent (duration 0)
|
|
484
|
+
expect(at(0).duration).toBe(0)
|
|
485
|
+
|
|
486
|
+
await promise
|
|
487
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
488
|
+
|
|
489
|
+
// After resolve, toast gets default duration (4000ms)
|
|
490
|
+
expect(at(0).duration).toBe(4000)
|
|
491
|
+
|
|
492
|
+
vi.advanceTimersByTime(4000)
|
|
493
|
+
expect(_toasts().length).toBe(0)
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it("rejected promise toast gets auto-dismiss timer", async () => {
|
|
497
|
+
const promise = Promise.reject(new Error("fail"))
|
|
498
|
+
|
|
499
|
+
toast
|
|
500
|
+
.promise(promise, {
|
|
501
|
+
loading: "Loading...",
|
|
502
|
+
success: "Done!",
|
|
503
|
+
error: "Failed",
|
|
504
|
+
})
|
|
505
|
+
.catch(() => {})
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
await promise
|
|
509
|
+
} catch {
|
|
510
|
+
// expected
|
|
511
|
+
}
|
|
512
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
513
|
+
|
|
514
|
+
expect(at(0).duration).toBe(4000)
|
|
515
|
+
expect(at(0).type).toBe("error")
|
|
516
|
+
|
|
517
|
+
vi.advanceTimersByTime(4000)
|
|
518
|
+
expect(_toasts().length).toBe(0)
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
describe("dismiss callback behavior", () => {
|
|
523
|
+
beforeEach(() => {
|
|
524
|
+
vi.useFakeTimers()
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
afterEach(() => {
|
|
528
|
+
vi.useRealTimers()
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it("onDismiss is called on auto-dismiss timeout", () => {
|
|
532
|
+
const onDismiss = vi.fn()
|
|
533
|
+
toast("Hello", { onDismiss, duration: 2000 })
|
|
534
|
+
|
|
535
|
+
vi.advanceTimersByTime(2000)
|
|
536
|
+
expect(onDismiss).toHaveBeenCalledOnce()
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it("onDismiss is not called twice on manual dismiss after timeout", () => {
|
|
540
|
+
const onDismiss = vi.fn()
|
|
541
|
+
const id = toast("Hello", { onDismiss, duration: 2000 })
|
|
542
|
+
|
|
543
|
+
toast.dismiss(id)
|
|
544
|
+
expect(onDismiss).toHaveBeenCalledOnce()
|
|
545
|
+
|
|
546
|
+
// The timeout timer was cleared, so it should not fire again
|
|
547
|
+
vi.advanceTimersByTime(2000)
|
|
548
|
+
expect(onDismiss).toHaveBeenCalledOnce()
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
it("onDismiss is called for each toast when dismissing all", () => {
|
|
552
|
+
const cb1 = vi.fn()
|
|
553
|
+
const cb2 = vi.fn()
|
|
554
|
+
const cb3 = vi.fn()
|
|
555
|
+
const callbacks = [cb1, cb2, cb3]
|
|
556
|
+
toast("A", { onDismiss: cb1, duration: 0 })
|
|
557
|
+
toast("B", { onDismiss: cb2, duration: 0 })
|
|
558
|
+
toast("C", { onDismiss: cb3, duration: 0 })
|
|
559
|
+
|
|
560
|
+
toast.dismiss()
|
|
561
|
+
|
|
562
|
+
for (const cb of callbacks) {
|
|
563
|
+
expect(cb).toHaveBeenCalledOnce()
|
|
564
|
+
}
|
|
565
|
+
})
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
describe("toast initial state", () => {
|
|
569
|
+
it("toast starts in entering state", () => {
|
|
570
|
+
toast("Hello")
|
|
571
|
+
expect(at(0).state).toBe("entering")
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it("toast has correct initial timer fields", () => {
|
|
575
|
+
toast("Hello", { duration: 4000 })
|
|
576
|
+
const t = at(0)
|
|
577
|
+
expect(t.remaining).toBe(4000)
|
|
578
|
+
expect(t.timerStart).toBeGreaterThan(0)
|
|
579
|
+
expect(t.timer).toBeDefined()
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it("persistent toast has no timer", () => {
|
|
583
|
+
toast("Hello", { duration: 0 })
|
|
584
|
+
const t = at(0)
|
|
585
|
+
expect(t.timer).toBeUndefined()
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
|
|
242
589
|
describe("Toaster renders", () => {
|
|
243
590
|
it("Toaster is a function component", async () => {
|
|
244
591
|
const { Toaster } = await import("../toaster")
|
package/src/toaster.tsx
CHANGED
|
@@ -7,11 +7,9 @@ import type { Toast, ToasterProps, ToastPosition } from "./types"
|
|
|
7
7
|
|
|
8
8
|
// ─── Style injection ─────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
let _styleInjected = false
|
|
11
|
-
|
|
12
10
|
function injectStyles(): void {
|
|
13
|
-
if (
|
|
14
|
-
|
|
11
|
+
if (typeof document === "undefined") return
|
|
12
|
+
if (document.querySelector("style[data-pyreon-toast]")) return
|
|
15
13
|
|
|
16
14
|
const style = document.createElement("style")
|
|
17
15
|
style.setAttribute("data-pyreon-toast", "")
|