@pyreon/toast 0.11.4 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,21 +11,21 @@ bun add @pyreon/toast
11
11
  ## Usage
12
12
 
13
13
  ```tsx
14
- import { toast, Toaster } from "@pyreon/toast"
14
+ import { toast, Toaster } from '@pyreon/toast'
15
15
 
16
16
  // Place once at app root
17
- <Toaster position="top-right" />
17
+ ;<Toaster position="top-right" />
18
18
 
19
19
  // Show toasts from anywhere — no provider needed
20
- toast.success("Saved!")
21
- toast.error("Connection failed")
22
- toast("Custom message", { duration: 8000 })
20
+ toast.success('Saved!')
21
+ toast.error('Connection failed')
22
+ toast('Custom message', { duration: 8000 })
23
23
 
24
24
  // Promise pattern
25
25
  toast.promise(saveData(), {
26
- loading: "Saving...",
27
- success: "Done!",
28
- error: "Failed",
26
+ loading: 'Saving...',
27
+ success: 'Done!',
28
+ error: 'Failed',
29
29
  })
30
30
  ```
31
31
 
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\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
+ {"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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"}
@@ -2,8 +2,8 @@ import * as _pyreon_reactivity0 from "@pyreon/reactivity";
2
2
  import { VNodeChild } from "@pyreon/core";
3
3
 
4
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";
5
+ type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
6
+ type ToastType = 'info' | 'success' | 'warning' | 'error';
7
7
  interface ToastOptions {
8
8
  /** Toast variant — controls styling. */
9
9
  type?: ToastType;
@@ -36,7 +36,7 @@ interface ToastPromiseOptions<T> {
36
36
  success: string | VNodeChild | ((data: T) => string | VNodeChild);
37
37
  error: string | VNodeChild | ((err: unknown) => string | VNodeChild);
38
38
  }
39
- type ToastState = "entering" | "visible" | "exiting";
39
+ type ToastState = 'entering' | 'visible' | 'exiting';
40
40
  interface Toast {
41
41
  id: string;
42
42
  message: string | VNodeChild;
package/package.json CHANGED
@@ -1,20 +1,17 @@
1
1
  {
2
2
  "name": "@pyreon/toast",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "Imperative toast notifications for Pyreon — no provider needed",
5
+ "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/toast#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/pyreon/pyreon/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/pyreon/pyreon.git",
9
13
  "directory": "packages/fundamentals/toast"
10
14
  },
11
- "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/toast#readme",
12
- "bugs": {
13
- "url": "https://github.com/pyreon/pyreon/issues"
14
- },
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
15
  "files": [
19
16
  "lib",
20
17
  "src",
@@ -22,6 +19,7 @@
22
19
  "LICENSE"
23
20
  ],
24
21
  "type": "module",
22
+ "sideEffects": false,
25
23
  "main": "./lib/index.js",
26
24
  "module": "./lib/index.js",
27
25
  "types": "./lib/types/index.d.ts",
@@ -32,22 +30,24 @@
32
30
  "types": "./lib/types/index.d.ts"
33
31
  }
34
32
  },
35
- "sideEffects": false,
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
36
  "scripts": {
37
37
  "build": "vl_rolldown_build",
38
38
  "dev": "vl_rolldown_build-watch",
39
39
  "test": "vitest run",
40
40
  "typecheck": "tsc --noEmit",
41
- "lint": "biome check ."
42
- },
43
- "peerDependencies": {
44
- "@pyreon/core": "^0.11.4",
45
- "@pyreon/reactivity": "^0.11.4"
41
+ "lint": "oxlint ."
46
42
  },
47
43
  "devDependencies": {
48
44
  "@happy-dom/global-registrator": "^20.8.3",
49
- "@pyreon/core": "^0.11.4",
50
- "@pyreon/reactivity": "^0.11.4",
45
+ "@pyreon/core": "^0.11.6",
46
+ "@pyreon/reactivity": "^0.11.6",
51
47
  "@vitus-labs/tools-lint": "^1.11.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@pyreon/core": "^0.11.6",
51
+ "@pyreon/reactivity": "^0.11.6"
52
52
  }
53
53
  }
package/src/index.ts CHANGED
@@ -27,8 +27,8 @@
27
27
  * })
28
28
  */
29
29
 
30
- export { _reset, _toasts, toast } from "./toast"
31
- export { Toaster } from "./toaster"
30
+ export { _reset, _toasts, toast } from './toast'
31
+ export { Toaster } from './toaster'
32
32
  export type {
33
33
  Toast,
34
34
  ToasterProps,
@@ -37,4 +37,4 @@ export type {
37
37
  ToastPromiseOptions,
38
38
  ToastState,
39
39
  ToastType,
40
- } from "./types"
40
+ } from './types'
@@ -1,5 +1,5 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
- import { _pauseAll, _reset, _resumeAll, _toasts, toast } from "../toast"
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
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) {
@@ -16,106 +16,106 @@ afterEach(() => {
16
16
  _reset()
17
17
  })
18
18
 
19
- describe("toast()", () => {
20
- it("adds a toast to the stack", () => {
21
- toast("Hello")
19
+ describe('toast()', () => {
20
+ it('adds a toast to the stack', () => {
21
+ toast('Hello')
22
22
  expect(_toasts().length).toBe(1)
23
- expect(at(0).message).toBe("Hello")
23
+ expect(at(0).message).toBe('Hello')
24
24
  })
25
25
 
26
- it("returns the toast id", () => {
27
- const id = toast("Hello")
28
- expect(typeof id).toBe("string")
26
+ it('returns the toast id', () => {
27
+ const id = toast('Hello')
28
+ expect(typeof id).toBe('string')
29
29
  expect(id).toMatch(/^pyreon-toast-/)
30
30
  })
31
31
 
32
- it("defaults to type info", () => {
33
- toast("Hello")
34
- expect(at(0).type).toBe("info")
32
+ it('defaults to type info', () => {
33
+ toast('Hello')
34
+ expect(at(0).type).toBe('info')
35
35
  })
36
36
 
37
- it("defaults dismissible to true", () => {
38
- toast("Hello")
37
+ it('defaults dismissible to true', () => {
38
+ toast('Hello')
39
39
  expect(at(0).dismissible).toBe(true)
40
40
  })
41
41
 
42
- it("respects custom options", () => {
43
- toast("Hello", { type: "error", duration: 0, dismissible: false })
42
+ it('respects custom options', () => {
43
+ toast('Hello', { type: 'error', duration: 0, dismissible: false })
44
44
  const t = at(0)
45
- expect(t.type).toBe("error")
45
+ expect(t.type).toBe('error')
46
46
  expect(t.duration).toBe(0)
47
47
  expect(t.dismissible).toBe(false)
48
48
  })
49
49
  })
50
50
 
51
- describe("toast.success/error/warning/info", () => {
52
- it("toast.success sets type to success", () => {
53
- toast.success("Done")
54
- expect(at(0).type).toBe("success")
51
+ describe('toast.success/error/warning/info', () => {
52
+ it('toast.success sets type to success', () => {
53
+ toast.success('Done')
54
+ expect(at(0).type).toBe('success')
55
55
  })
56
56
 
57
- it("toast.error sets type to error", () => {
58
- toast.error("Failed")
59
- expect(at(0).type).toBe("error")
57
+ it('toast.error sets type to error', () => {
58
+ toast.error('Failed')
59
+ expect(at(0).type).toBe('error')
60
60
  })
61
61
 
62
- it("toast.warning sets type to warning", () => {
63
- toast.warning("Watch out")
64
- expect(at(0).type).toBe("warning")
62
+ it('toast.warning sets type to warning', () => {
63
+ toast.warning('Watch out')
64
+ expect(at(0).type).toBe('warning')
65
65
  })
66
66
 
67
- it("toast.info sets type to info", () => {
68
- toast.info("FYI")
69
- expect(at(0).type).toBe("info")
67
+ it('toast.info sets type to info', () => {
68
+ toast.info('FYI')
69
+ expect(at(0).type).toBe('info')
70
70
  })
71
71
  })
72
72
 
73
- describe("toast.dismiss", () => {
74
- it("removes a specific toast by id", () => {
75
- const id1 = toast("First")
76
- toast("Second")
73
+ describe('toast.dismiss', () => {
74
+ it('removes a specific toast by id', () => {
75
+ const id1 = toast('First')
76
+ toast('Second')
77
77
  expect(_toasts().length).toBe(2)
78
78
 
79
79
  toast.dismiss(id1)
80
80
  expect(_toasts().length).toBe(1)
81
- expect(at(0).message).toBe("Second")
81
+ expect(at(0).message).toBe('Second')
82
82
  })
83
83
 
84
- it("clears all toasts when no id is given", () => {
85
- toast("First")
86
- toast("Second")
87
- toast("Third")
84
+ it('clears all toasts when no id is given', () => {
85
+ toast('First')
86
+ toast('Second')
87
+ toast('Third')
88
88
  expect(_toasts().length).toBe(3)
89
89
 
90
90
  toast.dismiss()
91
91
  expect(_toasts().length).toBe(0)
92
92
  })
93
93
 
94
- it("is a no-op for unknown id", () => {
95
- toast("Hello")
96
- toast.dismiss("unknown-id")
94
+ it('is a no-op for unknown id', () => {
95
+ toast('Hello')
96
+ toast.dismiss('unknown-id')
97
97
  expect(_toasts().length).toBe(1)
98
98
  })
99
99
 
100
- it("calls onDismiss callback when dismissing by id", () => {
100
+ it('calls onDismiss callback when dismissing by id', () => {
101
101
  const onDismiss = vi.fn()
102
- const id = toast("Hello", { onDismiss })
102
+ const id = toast('Hello', { onDismiss })
103
103
  toast.dismiss(id)
104
104
  expect(onDismiss).toHaveBeenCalledOnce()
105
105
  })
106
106
 
107
- it("calls onDismiss for all toasts when dismissing all", () => {
107
+ it('calls onDismiss for all toasts when dismissing all', () => {
108
108
  const onDismiss1 = vi.fn()
109
109
  const onDismiss2 = vi.fn()
110
- toast("First", { onDismiss: onDismiss1 })
111
- toast("Second", { onDismiss: onDismiss2 })
110
+ toast('First', { onDismiss: onDismiss1 })
111
+ toast('Second', { onDismiss: onDismiss2 })
112
112
  toast.dismiss()
113
113
  expect(onDismiss1).toHaveBeenCalledOnce()
114
114
  expect(onDismiss2).toHaveBeenCalledOnce()
115
115
  })
116
116
  })
117
117
 
118
- describe("auto-dismiss", () => {
118
+ describe('auto-dismiss', () => {
119
119
  beforeEach(() => {
120
120
  vi.useFakeTimers()
121
121
  })
@@ -124,8 +124,8 @@ describe("auto-dismiss", () => {
124
124
  vi.useRealTimers()
125
125
  })
126
126
 
127
- it("auto-dismisses after default duration (4000ms)", () => {
128
- toast("Hello")
127
+ it('auto-dismisses after default duration (4000ms)', () => {
128
+ toast('Hello')
129
129
  expect(_toasts().length).toBe(1)
130
130
 
131
131
  vi.advanceTimersByTime(3999)
@@ -135,8 +135,8 @@ describe("auto-dismiss", () => {
135
135
  expect(_toasts().length).toBe(0)
136
136
  })
137
137
 
138
- it("auto-dismisses after custom duration", () => {
139
- toast("Hello", { duration: 2000 })
138
+ it('auto-dismisses after custom duration', () => {
139
+ toast('Hello', { duration: 2000 })
140
140
 
141
141
  vi.advanceTimersByTime(1999)
142
142
  expect(_toasts().length).toBe(1)
@@ -145,15 +145,15 @@ describe("auto-dismiss", () => {
145
145
  expect(_toasts().length).toBe(0)
146
146
  })
147
147
 
148
- it("does not auto-dismiss when duration is 0", () => {
149
- toast("Persistent", { duration: 0 })
148
+ it('does not auto-dismiss when duration is 0', () => {
149
+ toast('Persistent', { duration: 0 })
150
150
 
151
151
  vi.advanceTimersByTime(10000)
152
152
  expect(_toasts().length).toBe(1)
153
153
  })
154
154
  })
155
155
 
156
- describe("toast.promise", () => {
156
+ describe('toast.promise', () => {
157
157
  beforeEach(() => {
158
158
  vi.useFakeTimers()
159
159
  })
@@ -162,18 +162,18 @@ describe("toast.promise", () => {
162
162
  vi.useRealTimers()
163
163
  })
164
164
 
165
- it("creates a loading toast that updates on resolve", async () => {
166
- const promise = Promise.resolve("data")
165
+ it('creates a loading toast that updates on resolve', async () => {
166
+ const promise = Promise.resolve('data')
167
167
 
168
168
  toast.promise(promise, {
169
- loading: "Loading...",
170
- success: "Done!",
171
- error: "Failed",
169
+ loading: 'Loading...',
170
+ success: 'Done!',
171
+ error: 'Failed',
172
172
  })
173
173
 
174
174
  expect(_toasts().length).toBe(1)
175
- expect(at(0).message).toBe("Loading...")
176
- expect(at(0).type).toBe("info")
175
+ expect(at(0).message).toBe('Loading...')
176
+ expect(at(0).type).toBe('info')
177
177
  expect(at(0).duration).toBe(0) // persistent while loading
178
178
 
179
179
  await promise
@@ -181,23 +181,23 @@ describe("toast.promise", () => {
181
181
  await vi.advanceTimersByTimeAsync(0)
182
182
 
183
183
  expect(_toasts().length).toBe(1)
184
- expect(at(0).message).toBe("Done!")
185
- expect(at(0).type).toBe("success")
184
+ expect(at(0).message).toBe('Done!')
185
+ expect(at(0).type).toBe('success')
186
186
  })
187
187
 
188
- it("creates a loading toast that updates on reject", async () => {
189
- const promise = Promise.reject(new Error("oops"))
188
+ it('creates a loading toast that updates on reject', async () => {
189
+ const promise = Promise.reject(new Error('oops'))
190
190
 
191
191
  // Prevent unhandled rejection
192
192
  toast
193
193
  .promise(promise, {
194
- loading: "Loading...",
195
- success: "Done!",
196
- error: "Failed",
194
+ loading: 'Loading...',
195
+ success: 'Done!',
196
+ error: 'Failed',
197
197
  })
198
198
  .catch(() => {})
199
199
 
200
- expect(at(0).message).toBe("Loading...")
200
+ expect(at(0).message).toBe('Loading...')
201
201
 
202
202
  try {
203
203
  await promise
@@ -208,15 +208,15 @@ describe("toast.promise", () => {
208
208
  await vi.advanceTimersByTimeAsync(0)
209
209
 
210
210
  expect(_toasts().length).toBe(1)
211
- expect(at(0).message).toBe("Failed")
212
- expect(at(0).type).toBe("error")
211
+ expect(at(0).message).toBe('Failed')
212
+ expect(at(0).type).toBe('error')
213
213
  })
214
214
 
215
- it("supports function form for success/error messages", async () => {
215
+ it('supports function form for success/error messages', async () => {
216
216
  const promise = Promise.resolve(42)
217
217
 
218
218
  toast.promise(promise, {
219
- loading: "Calculating...",
219
+ loading: 'Calculating...',
220
220
  success: (data) => `Result: ${data}`,
221
221
  error: (err) => `Error: ${err}`,
222
222
  })
@@ -224,22 +224,22 @@ describe("toast.promise", () => {
224
224
  await promise
225
225
  await vi.advanceTimersByTimeAsync(0)
226
226
 
227
- expect(at(0).message).toBe("Result: 42")
227
+ expect(at(0).message).toBe('Result: 42')
228
228
  })
229
229
 
230
- it("returns the original promise", async () => {
231
- const promise = Promise.resolve("value")
230
+ it('returns the original promise', async () => {
231
+ const promise = Promise.resolve('value')
232
232
  const result = toast.promise(promise, {
233
- loading: "Loading...",
234
- success: "Done!",
235
- error: "Failed",
233
+ loading: 'Loading...',
234
+ success: 'Done!',
235
+ error: 'Failed',
236
236
  })
237
237
 
238
- expect(await result).toBe("value")
238
+ expect(await result).toBe('value')
239
239
  })
240
240
  })
241
241
 
242
- describe("toast.loading", () => {
242
+ describe('toast.loading', () => {
243
243
  beforeEach(() => {
244
244
  vi.useFakeTimers()
245
245
  })
@@ -248,11 +248,11 @@ describe("toast.loading", () => {
248
248
  vi.useRealTimers()
249
249
  })
250
250
 
251
- it("creates a persistent toast with type info", () => {
252
- const id = toast.loading("Please wait...")
251
+ it('creates a persistent toast with type info', () => {
252
+ const id = toast.loading('Please wait...')
253
253
  expect(_toasts().length).toBe(1)
254
- expect(at(0).message).toBe("Please wait...")
255
- expect(at(0).type).toBe("info")
254
+ expect(at(0).message).toBe('Please wait...')
255
+ expect(at(0).type).toBe('info')
256
256
  expect(at(0).duration).toBe(0)
257
257
 
258
258
  // Should not auto-dismiss even after a long time
@@ -261,32 +261,32 @@ describe("toast.loading", () => {
261
261
  expect(at(0).id).toBe(id)
262
262
  })
263
263
 
264
- it("returns an id that can be dismissed manually", () => {
265
- const id = toast.loading("Loading data...")
264
+ it('returns an id that can be dismissed manually', () => {
265
+ const id = toast.loading('Loading data...')
266
266
  expect(_toasts().length).toBe(1)
267
267
 
268
268
  toast.dismiss(id)
269
269
  expect(_toasts().length).toBe(0)
270
270
  })
271
271
 
272
- it("accepts options like onDismiss and action", () => {
272
+ it('accepts options like onDismiss and action', () => {
273
273
  const onDismiss = vi.fn()
274
- const id = toast.loading("Loading...", { onDismiss })
274
+ const id = toast.loading('Loading...', { onDismiss })
275
275
  toast.dismiss(id)
276
276
  expect(onDismiss).toHaveBeenCalledOnce()
277
277
  })
278
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...")
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
282
 
283
- toast.update(id, { message: "Step 2..." })
284
- expect(at(0).message).toBe("Step 2...")
285
- expect(at(0).type).toBe("info")
283
+ toast.update(id, { message: 'Step 2...' })
284
+ expect(at(0).message).toBe('Step 2...')
285
+ expect(at(0).type).toBe('info')
286
286
  })
287
287
  })
288
288
 
289
- describe("toast.update", () => {
289
+ describe('toast.update', () => {
290
290
  beforeEach(() => {
291
291
  vi.useFakeTimers()
292
292
  })
@@ -295,22 +295,22 @@ describe("toast.update", () => {
295
295
  vi.useRealTimers()
296
296
  })
297
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")
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
302
  })
303
303
 
304
- it("updates type of existing toast", () => {
305
- const id = toast("Hello")
306
- expect(at(0).type).toBe("info")
304
+ it('updates type of existing toast', () => {
305
+ const id = toast('Hello')
306
+ expect(at(0).type).toBe('info')
307
307
 
308
- toast.update(id, { type: "success" })
309
- expect(at(0).type).toBe("success")
308
+ toast.update(id, { type: 'success' })
309
+ expect(at(0).type).toBe('success')
310
310
  })
311
311
 
312
- it("updates duration and restarts timer", () => {
313
- const id = toast("Hello", { duration: 0 }) // persistent
312
+ it('updates duration and restarts timer', () => {
313
+ const id = toast('Hello', { duration: 0 }) // persistent
314
314
  expect(at(0).duration).toBe(0)
315
315
 
316
316
  vi.advanceTimersByTime(5000)
@@ -326,18 +326,18 @@ describe("toast.update", () => {
326
326
  expect(_toasts().length).toBe(0) // auto-dismissed after new duration
327
327
  })
328
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")
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
333
  })
334
334
 
335
- it("clears old timer when updating", () => {
336
- const id = toast("Hello", { duration: 1000 })
335
+ it('clears old timer when updating', () => {
336
+ const id = toast('Hello', { duration: 1000 })
337
337
 
338
338
  vi.advanceTimersByTime(500)
339
339
  // Update resets the timer with the same duration
340
- toast.update(id, { message: "Updated" })
340
+ toast.update(id, { message: 'Updated' })
341
341
 
342
342
  vi.advanceTimersByTime(500)
343
343
  // Old timer would have fired at 1000ms total, but update reset it
@@ -348,17 +348,17 @@ describe("toast.update", () => {
348
348
  expect(_toasts().length).toBe(0)
349
349
  })
350
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 })
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
354
 
355
- expect(at(0).message).toBe("Done!")
356
- expect(at(0).type).toBe("success")
355
+ expect(at(0).message).toBe('Done!')
356
+ expect(at(0).type).toBe('success')
357
357
  expect(at(0).duration).toBe(3000)
358
358
  })
359
359
  })
360
360
 
361
- describe("pause/resume", () => {
361
+ describe('pause/resume', () => {
362
362
  beforeEach(() => {
363
363
  vi.useFakeTimers()
364
364
  })
@@ -367,9 +367,9 @@ describe("pause/resume", () => {
367
367
  vi.useRealTimers()
368
368
  })
369
369
 
370
- it("_pauseAll stops all timers", () => {
371
- toast("First", { duration: 4000 })
372
- toast("Second", { duration: 4000 })
370
+ it('_pauseAll stops all timers', () => {
371
+ toast('First', { duration: 4000 })
372
+ toast('Second', { duration: 4000 })
373
373
 
374
374
  vi.advanceTimersByTime(2000) // halfway
375
375
 
@@ -380,8 +380,8 @@ describe("pause/resume", () => {
380
380
  expect(_toasts().length).toBe(2)
381
381
  })
382
382
 
383
- it("_resumeAll restarts timers with remaining time", () => {
384
- toast("Hello", { duration: 4000 })
383
+ it('_resumeAll restarts timers with remaining time', () => {
384
+ toast('Hello', { duration: 4000 })
385
385
 
386
386
  vi.advanceTimersByTime(3000) // 1000ms remaining
387
387
  _pauseAll()
@@ -398,8 +398,8 @@ describe("pause/resume", () => {
398
398
  expect(_toasts().length).toBe(0)
399
399
  })
400
400
 
401
- it("_pauseAll is no-op for persistent toasts (duration 0)", () => {
402
- toast("Persistent", { duration: 0 })
401
+ it('_pauseAll is no-op for persistent toasts (duration 0)', () => {
402
+ toast('Persistent', { duration: 0 })
403
403
 
404
404
  _pauseAll()
405
405
  _resumeAll()
@@ -408,8 +408,8 @@ describe("pause/resume", () => {
408
408
  expect(_toasts().length).toBe(1)
409
409
  })
410
410
 
411
- it("_resumeAll is no-op when no toasts are paused", () => {
412
- toast("Hello", { duration: 4000 })
411
+ it('_resumeAll is no-op when no toasts are paused', () => {
412
+ toast('Hello', { duration: 4000 })
413
413
 
414
414
  // Call resume without pause — should not cause issues
415
415
  _resumeAll()
@@ -419,8 +419,8 @@ describe("pause/resume", () => {
419
419
  })
420
420
  })
421
421
 
422
- describe("max queue behavior", () => {
423
- it("adding more toasts than max still stores all in _toasts", () => {
422
+ describe('max queue behavior', () => {
423
+ it('adding more toasts than max still stores all in _toasts', () => {
424
424
  // The Toaster component limits visible toasts via computed slice,
425
425
  // but the underlying signal holds all toasts
426
426
  for (let i = 0; i < 10; i++) {
@@ -429,7 +429,7 @@ describe("max queue behavior", () => {
429
429
  expect(_toasts().length).toBe(10)
430
430
  })
431
431
 
432
- it("each toast gets a unique id", () => {
432
+ it('each toast gets a unique id', () => {
433
433
  const ids = new Set<string>()
434
434
  for (let i = 0; i < 20; i++) {
435
435
  ids.add(toast(`Toast ${i}`, { duration: 0 }))
@@ -438,7 +438,7 @@ describe("max queue behavior", () => {
438
438
  })
439
439
  })
440
440
 
441
- describe("toast.promise with rejected promise", () => {
441
+ describe('toast.promise with rejected promise', () => {
442
442
  beforeEach(() => {
443
443
  vi.useFakeTimers()
444
444
  })
@@ -447,14 +447,14 @@ describe("toast.promise with rejected promise", () => {
447
447
  vi.useRealTimers()
448
448
  })
449
449
 
450
- it("uses function error handler with error argument", async () => {
451
- const err = new Error("network failure")
450
+ it('uses function error handler with error argument', async () => {
451
+ const err = new Error('network failure')
452
452
  const promise = Promise.reject(err)
453
453
 
454
454
  toast
455
455
  .promise(promise, {
456
- loading: "Saving...",
457
- success: "Saved!",
456
+ loading: 'Saving...',
457
+ success: 'Saved!',
458
458
  error: (e: unknown) => `Failed: ${(e as Error).message}`,
459
459
  })
460
460
  .catch(() => {})
@@ -467,17 +467,17 @@ describe("toast.promise with rejected promise", () => {
467
467
 
468
468
  await vi.advanceTimersByTimeAsync(0)
469
469
 
470
- expect(at(0).message).toBe("Failed: network failure")
471
- expect(at(0).type).toBe("error")
470
+ expect(at(0).message).toBe('Failed: network failure')
471
+ expect(at(0).type).toBe('error')
472
472
  })
473
473
 
474
- it("resolved promise toast gets auto-dismiss timer", async () => {
475
- const promise = Promise.resolve("ok")
474
+ it('resolved promise toast gets auto-dismiss timer', async () => {
475
+ const promise = Promise.resolve('ok')
476
476
 
477
477
  toast.promise(promise, {
478
- loading: "Loading...",
479
- success: "Done!",
480
- error: "Failed",
478
+ loading: 'Loading...',
479
+ success: 'Done!',
480
+ error: 'Failed',
481
481
  })
482
482
 
483
483
  // Loading toast is persistent (duration 0)
@@ -493,14 +493,14 @@ describe("toast.promise with rejected promise", () => {
493
493
  expect(_toasts().length).toBe(0)
494
494
  })
495
495
 
496
- it("rejected promise toast gets auto-dismiss timer", async () => {
497
- const promise = Promise.reject(new Error("fail"))
496
+ it('rejected promise toast gets auto-dismiss timer', async () => {
497
+ const promise = Promise.reject(new Error('fail'))
498
498
 
499
499
  toast
500
500
  .promise(promise, {
501
- loading: "Loading...",
502
- success: "Done!",
503
- error: "Failed",
501
+ loading: 'Loading...',
502
+ success: 'Done!',
503
+ error: 'Failed',
504
504
  })
505
505
  .catch(() => {})
506
506
 
@@ -512,14 +512,14 @@ describe("toast.promise with rejected promise", () => {
512
512
  await vi.advanceTimersByTimeAsync(0)
513
513
 
514
514
  expect(at(0).duration).toBe(4000)
515
- expect(at(0).type).toBe("error")
515
+ expect(at(0).type).toBe('error')
516
516
 
517
517
  vi.advanceTimersByTime(4000)
518
518
  expect(_toasts().length).toBe(0)
519
519
  })
520
520
  })
521
521
 
522
- describe("dismiss callback behavior", () => {
522
+ describe('dismiss callback behavior', () => {
523
523
  beforeEach(() => {
524
524
  vi.useFakeTimers()
525
525
  })
@@ -528,17 +528,17 @@ describe("dismiss callback behavior", () => {
528
528
  vi.useRealTimers()
529
529
  })
530
530
 
531
- it("onDismiss is called on auto-dismiss timeout", () => {
531
+ it('onDismiss is called on auto-dismiss timeout', () => {
532
532
  const onDismiss = vi.fn()
533
- toast("Hello", { onDismiss, duration: 2000 })
533
+ toast('Hello', { onDismiss, duration: 2000 })
534
534
 
535
535
  vi.advanceTimersByTime(2000)
536
536
  expect(onDismiss).toHaveBeenCalledOnce()
537
537
  })
538
538
 
539
- it("onDismiss is not called twice on manual dismiss after timeout", () => {
539
+ it('onDismiss is not called twice on manual dismiss after timeout', () => {
540
540
  const onDismiss = vi.fn()
541
- const id = toast("Hello", { onDismiss, duration: 2000 })
541
+ const id = toast('Hello', { onDismiss, duration: 2000 })
542
542
 
543
543
  toast.dismiss(id)
544
544
  expect(onDismiss).toHaveBeenCalledOnce()
@@ -548,14 +548,14 @@ describe("dismiss callback behavior", () => {
548
548
  expect(onDismiss).toHaveBeenCalledOnce()
549
549
  })
550
550
 
551
- it("onDismiss is called for each toast when dismissing all", () => {
551
+ it('onDismiss is called for each toast when dismissing all', () => {
552
552
  const cb1 = vi.fn()
553
553
  const cb2 = vi.fn()
554
554
  const cb3 = vi.fn()
555
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 })
556
+ toast('A', { onDismiss: cb1, duration: 0 })
557
+ toast('B', { onDismiss: cb2, duration: 0 })
558
+ toast('C', { onDismiss: cb3, duration: 0 })
559
559
 
560
560
  toast.dismiss()
561
561
 
@@ -565,30 +565,30 @@ describe("dismiss callback behavior", () => {
565
565
  })
566
566
  })
567
567
 
568
- describe("toast initial state", () => {
569
- it("toast starts in entering state", () => {
570
- toast("Hello")
571
- expect(at(0).state).toBe("entering")
568
+ describe('toast initial state', () => {
569
+ it('toast starts in entering state', () => {
570
+ toast('Hello')
571
+ expect(at(0).state).toBe('entering')
572
572
  })
573
573
 
574
- it("toast has correct initial timer fields", () => {
575
- toast("Hello", { duration: 4000 })
574
+ it('toast has correct initial timer fields', () => {
575
+ toast('Hello', { duration: 4000 })
576
576
  const t = at(0)
577
577
  expect(t.remaining).toBe(4000)
578
578
  expect(t.timerStart).toBeGreaterThan(0)
579
579
  expect(t.timer).toBeDefined()
580
580
  })
581
581
 
582
- it("persistent toast has no timer", () => {
583
- toast("Hello", { duration: 0 })
582
+ it('persistent toast has no timer', () => {
583
+ toast('Hello', { duration: 0 })
584
584
  const t = at(0)
585
585
  expect(t.timer).toBeUndefined()
586
586
  })
587
587
  })
588
588
 
589
- describe("Toaster renders", () => {
590
- it("Toaster is a function component", async () => {
591
- const { Toaster } = await import("../toaster")
592
- expect(typeof Toaster).toBe("function")
589
+ describe('Toaster renders', () => {
590
+ it('Toaster is a function component', async () => {
591
+ const { Toaster } = await import('../toaster')
592
+ expect(typeof Toaster).toBe('function')
593
593
  })
594
594
  })
package/src/toast.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { VNodeChild } from "@pyreon/core"
2
- import { signal } from "@pyreon/reactivity"
3
- import type { Toast, ToastOptions, ToastPromiseOptions, ToastType } from "./types"
1
+ import type { VNodeChild } from '@pyreon/core'
2
+ import { signal } from '@pyreon/reactivity'
3
+ import type { Toast, ToastOptions, ToastPromiseOptions, ToastType } from './types'
4
4
 
5
5
  // ─── State ───────────────────────────────────────────────────────────────────
6
6
 
@@ -31,12 +31,12 @@ function addToast(message: string | VNodeChild, options: ToastOptions = {}): str
31
31
  const t: Toast = {
32
32
  id,
33
33
  message,
34
- type: options.type ?? "info",
34
+ type: options.type ?? 'info',
35
35
  duration: options.duration ?? DEFAULT_DURATION,
36
36
  dismissible: options.dismissible ?? true,
37
37
  action: options.action,
38
38
  onDismiss: options.onDismiss,
39
- state: "entering",
39
+ state: 'entering',
40
40
  timer: undefined,
41
41
  remaining: 0,
42
42
  timerStart: 0,
@@ -71,7 +71,7 @@ function dismiss(id?: string): void {
71
71
 
72
72
  function updateToast(
73
73
  id: string,
74
- updates: Partial<Pick<Toast, "message" | "type" | "duration">>,
74
+ updates: Partial<Pick<Toast, 'message' | 'type' | 'duration'>>,
75
75
  ): void {
76
76
  const current = _toasts()
77
77
  const idx = current.findIndex((item) => item.id === id)
@@ -136,32 +136,32 @@ export function toast(message: string | VNodeChild, options?: ToastOptions): str
136
136
  }
137
137
 
138
138
  function shortcut(type: ToastType) {
139
- return (message: string | VNodeChild, options?: Omit<ToastOptions, "type">): string =>
139
+ return (message: string | VNodeChild, options?: Omit<ToastOptions, 'type'>): string =>
140
140
  addToast(message, { ...options, type })
141
141
  }
142
142
 
143
143
  /** Show a success toast. */
144
- toast.success = shortcut("success")
144
+ toast.success = shortcut('success')
145
145
 
146
146
  /** Show an error toast. */
147
- toast.error = shortcut("error")
147
+ toast.error = shortcut('error')
148
148
 
149
149
  /** Show a warning toast. */
150
- toast.warning = shortcut("warning")
150
+ toast.warning = shortcut('warning')
151
151
 
152
152
  /** Show an info toast. */
153
- toast.info = shortcut("info")
153
+ toast.info = shortcut('info')
154
154
 
155
155
  /** Show a persistent loading toast. Returns id for later update/dismiss. */
156
156
  toast.loading = (
157
157
  message: string | VNodeChild,
158
- options?: Omit<ToastOptions, "type" | "duration">,
159
- ): string => addToast(message, { ...options, type: "info", duration: 0 })
158
+ options?: Omit<ToastOptions, 'type' | 'duration'>,
159
+ ): string => addToast(message, { ...options, type: 'info', duration: 0 })
160
160
 
161
161
  /** Update an existing toast (message, type, duration). */
162
162
  toast.update = (
163
163
  id: string,
164
- updates: Partial<Pick<ToastOptions, "type" | "duration">> & { message?: string | VNodeChild },
164
+ updates: Partial<Pick<ToastOptions, 'type' | 'duration'>> & { message?: string | VNodeChild },
165
165
  ): void => updateToast(id, updates)
166
166
 
167
167
  /** Dismiss a specific toast by id, or all toasts if no id is given. */
@@ -181,16 +181,16 @@ toast.promise = function toastPromise<T>(
181
181
  promise: Promise<T>,
182
182
  opts: ToastPromiseOptions<T>,
183
183
  ): Promise<T> {
184
- const id = addToast(opts.loading, { type: "info", duration: 0 })
184
+ const id = addToast(opts.loading, { type: 'info', duration: 0 })
185
185
 
186
186
  promise.then(
187
187
  (data) => {
188
- const msg = typeof opts.success === "function" ? opts.success(data) : opts.success
189
- updateToast(id, { message: msg, type: "success", duration: DEFAULT_DURATION })
188
+ const msg = typeof opts.success === 'function' ? opts.success(data) : opts.success
189
+ updateToast(id, { message: msg, type: 'success', duration: DEFAULT_DURATION })
190
190
  },
191
191
  (err: unknown) => {
192
- const msg = typeof opts.error === "function" ? opts.error(err) : opts.error
193
- updateToast(id, { message: msg, type: "error", duration: DEFAULT_DURATION })
192
+ const msg = typeof opts.error === 'function' ? opts.error(err) : opts.error
193
+ updateToast(id, { message: msg, type: 'error', duration: DEFAULT_DURATION })
194
194
  },
195
195
  )
196
196
 
package/src/toaster.tsx CHANGED
@@ -1,18 +1,18 @@
1
- import type { VNodeChild } from "@pyreon/core"
2
- import { For, Portal } from "@pyreon/core"
3
- import { computed, effect, onCleanup } from "@pyreon/reactivity"
4
- import { toastStyles } from "./styles"
5
- import { _pauseAll, _resumeAll, _toasts, toast } from "./toast"
6
- import type { Toast, ToasterProps, ToastPosition } from "./types"
1
+ import type { VNodeChild } from '@pyreon/core'
2
+ import { For, Portal } from '@pyreon/core'
3
+ import { computed, effect, onCleanup } from '@pyreon/reactivity'
4
+ import { toastStyles } from './styles'
5
+ import { _pauseAll, _resumeAll, _toasts, toast } from './toast'
6
+ import type { Toast, ToasterProps, ToastPosition } from './types'
7
7
 
8
8
  // ─── Style injection ─────────────────────────────────────────────────────────
9
9
 
10
10
  function injectStyles(): void {
11
- if (typeof document === "undefined") return
12
- if (document.querySelector("style[data-pyreon-toast]")) return
11
+ if (typeof document === 'undefined') return
12
+ if (document.querySelector('style[data-pyreon-toast]')) return
13
13
 
14
- const style = document.createElement("style")
15
- style.setAttribute("data-pyreon-toast", "")
14
+ const style = document.createElement('style')
15
+ style.setAttribute('data-pyreon-toast', '')
16
16
  style.textContent = toastStyles
17
17
  document.head.appendChild(style)
18
18
  }
@@ -20,21 +20,21 @@ function injectStyles(): void {
20
20
  // ─── Position helpers ────────────────────────────────────────────────────────
21
21
 
22
22
  function getContainerStyle(position: ToastPosition, gap: number, offset: number): string {
23
- const [vertical, horizontal] = position.split("-") as [string, string]
23
+ const [vertical, horizontal] = position.split('-') as [string, string]
24
24
 
25
25
  let style = `gap: ${gap}px;`
26
26
 
27
- if (vertical === "top") {
27
+ if (vertical === 'top') {
28
28
  style += ` top: ${offset}px;`
29
29
  } else {
30
30
  style += ` bottom: ${offset}px;`
31
- style += " flex-direction: column-reverse;"
31
+ style += ' flex-direction: column-reverse;'
32
32
  }
33
33
 
34
- if (horizontal === "left") {
34
+ if (horizontal === 'left') {
35
35
  style += ` left: ${offset}px;`
36
- } else if (horizontal === "center") {
37
- style += " left: 50%; transform: translateX(-50%);"
36
+ } else if (horizontal === 'center') {
37
+ style += ' left: 50%; transform: translateX(-50%);'
38
38
  } else {
39
39
  style += ` right: ${offset}px;`
40
40
  }
@@ -60,7 +60,7 @@ function getContainerStyle(position: ToastPosition, gap: number, offset: number)
60
60
  * ```
61
61
  */
62
62
  export function Toaster(props?: ToasterProps): VNodeChild {
63
- const position = props?.position ?? "top-right"
63
+ const position = props?.position ?? 'top-right'
64
64
  const max = props?.max ?? 5
65
65
  const gap = props?.gap ?? 8
66
66
  const offset = props?.offset ?? 16
@@ -71,16 +71,16 @@ export function Toaster(props?: ToasterProps): VNodeChild {
71
71
  // Only runs when there are actually entering toasts (early return guard).
72
72
  effect(() => {
73
73
  const toasts = _toasts()
74
- const hasEntering = toasts.some((t) => t.state === "entering")
74
+ const hasEntering = toasts.some((t) => t.state === 'entering')
75
75
  if (!hasEntering) return
76
76
 
77
77
  const raf = requestAnimationFrame(() => {
78
78
  const current = _toasts()
79
79
  let changed = false
80
80
  const next = current.map((t) => {
81
- if (t.state === "entering") {
81
+ if (t.state === 'entering') {
82
82
  changed = true
83
- return { ...t, state: "visible" as const }
83
+ return { ...t, state: 'visible' as const }
84
84
  }
85
85
  return t
86
86
  })
@@ -117,11 +117,11 @@ export function Toaster(props?: ToasterProps): VNodeChild {
117
117
 
118
118
  function ToastItem({ toast: t }: { toast: Toast }): VNodeChild {
119
119
  const stateClass =
120
- t.state === "entering"
121
- ? " pyreon-toast--entering"
122
- : t.state === "exiting"
123
- ? " pyreon-toast--exiting"
124
- : ""
120
+ t.state === 'entering'
121
+ ? ' pyreon-toast--entering'
122
+ : t.state === 'exiting'
123
+ ? ' pyreon-toast--exiting'
124
+ : ''
125
125
 
126
126
  return (
127
127
  <div
@@ -131,7 +131,7 @@ function ToastItem({ toast: t }: { toast: Toast }): VNodeChild {
131
131
  data-toast-id={t.id}
132
132
  >
133
133
  <div class="pyreon-toast__message">
134
- {typeof t.message === "string" ? t.message : t.message}
134
+ {typeof t.message === 'string' ? t.message : t.message}
135
135
  </div>
136
136
  {t.action && (
137
137
  <button type="button" class="pyreon-toast__action" onClick={t.action.onClick}>
package/src/types.ts CHANGED
@@ -1,16 +1,16 @@
1
- import type { VNodeChild } from "@pyreon/core"
1
+ import type { VNodeChild } from '@pyreon/core'
2
2
 
3
3
  // ─── Public types ────────────────────────────────────────────────────────────
4
4
 
5
5
  export type ToastPosition =
6
- | "top-left"
7
- | "top-center"
8
- | "top-right"
9
- | "bottom-left"
10
- | "bottom-center"
11
- | "bottom-right"
6
+ | 'top-left'
7
+ | 'top-center'
8
+ | 'top-right'
9
+ | 'bottom-left'
10
+ | 'bottom-center'
11
+ | 'bottom-right'
12
12
 
13
- export type ToastType = "info" | "success" | "warning" | "error"
13
+ export type ToastType = 'info' | 'success' | 'warning' | 'error'
14
14
 
15
15
  export interface ToastOptions {
16
16
  /** Toast variant — controls styling. */
@@ -46,7 +46,7 @@ export interface ToastPromiseOptions<T> {
46
46
 
47
47
  // ─── Internal types ──────────────────────────────────────────────────────────
48
48
 
49
- export type ToastState = "entering" | "visible" | "exiting"
49
+ export type ToastState = 'entering' | 'visible' | 'exiting'
50
50
 
51
51
  export interface Toast {
52
52
  id: string