@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.
@@ -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":"528f7250-1","name":"toast.ts"},{"uid":"528f7250-3","name":"styles.ts"},{"uid":"528f7250-5","name":"toaster.tsx"},{"uid":"528f7250-7","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"528f7250-1":{"renderedLength":4188,"gzipLength":1428,"brotliLength":0,"metaUid":"528f7250-0"},"528f7250-3":{"renderedLength":1641,"gzipLength":689,"brotliLength":0,"metaUid":"528f7250-2"},"528f7250-5":{"renderedLength":3120,"gzipLength":1243,"brotliLength":0,"metaUid":"528f7250-4"},"528f7250-7":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"528f7250-6"}},"nodeMetas":{"528f7250-0":{"id":"/src/toast.ts","moduleParts":{"index.js":"528f7250-1"},"imported":[{"uid":"528f7250-8"}],"importedBy":[{"uid":"528f7250-6"},{"uid":"528f7250-4"}]},"528f7250-2":{"id":"/src/styles.ts","moduleParts":{"index.js":"528f7250-3"},"imported":[],"importedBy":[{"uid":"528f7250-4"}]},"528f7250-4":{"id":"/src/toaster.tsx","moduleParts":{"index.js":"528f7250-5"},"imported":[{"uid":"528f7250-9"},{"uid":"528f7250-8"},{"uid":"528f7250-2"},{"uid":"528f7250-0"},{"uid":"528f7250-10"}],"importedBy":[{"uid":"528f7250-6"}]},"528f7250-6":{"id":"/src/index.ts","moduleParts":{"index.js":"528f7250-7"},"imported":[{"uid":"528f7250-0"},{"uid":"528f7250-4"}],"importedBy":[],"isEntry":true},"528f7250-8":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"528f7250-0"},{"uid":"528f7250-4"}]},"528f7250-9":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"528f7250-4"}]},"528f7250-10":{"id":"@pyreon/core/jsx-runtime","moduleParts":{},"imported":[],"importedBy":[{"uid":"528f7250-4"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
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 (_styleInjected) return;
253
- _styleInjected = true;
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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"}
@@ -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;;;iBEiDgB,OAAA,CAAQ,KAAA,GAAQ,YAAA,GAAe,UAAA"}
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",
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.3",
45
- "@pyreon/reactivity": "^0.11.3"
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.3",
50
- "@pyreon/reactivity": "^0.11.3",
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
  }
@@ -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 (_styleInjected) return
14
- _styleInjected = true
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", "")