@pyreon/toast 0.11.5 → 0.11.7
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 +8 -8
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +3 -3
- package/package.json +16 -16
- package/src/index.ts +3 -3
- package/src/tests/toast.test.ts +172 -172
- package/src/toast.ts +19 -19
- package/src/toaster.tsx +26 -26
- package/src/types.ts +9 -9
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
|
|
14
|
+
import { toast, Toaster } from '@pyreon/toast'
|
|
15
15
|
|
|
16
16
|
// Place once at app root
|
|
17
|
-
|
|
17
|
+
;<Toaster position="top-right" />
|
|
18
18
|
|
|
19
19
|
// Show toasts from anywhere — no provider needed
|
|
20
|
-
toast.success(
|
|
21
|
-
toast.error(
|
|
22
|
-
toast(
|
|
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:
|
|
27
|
-
success:
|
|
28
|
-
error:
|
|
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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"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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 =
|
|
6
|
-
type ToastType =
|
|
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 =
|
|
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.
|
|
3
|
+
"version": "0.11.7",
|
|
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
|
-
"
|
|
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": "
|
|
42
|
-
},
|
|
43
|
-
"peerDependencies": {
|
|
44
|
-
"@pyreon/core": "^0.11.5",
|
|
45
|
-
"@pyreon/reactivity": "^0.11.5"
|
|
41
|
+
"lint": "oxlint ."
|
|
46
42
|
},
|
|
47
43
|
"devDependencies": {
|
|
48
44
|
"@happy-dom/global-registrator": "^20.8.3",
|
|
49
|
-
"@pyreon/core": "^0.11.
|
|
50
|
-
"@pyreon/reactivity": "^0.11.
|
|
45
|
+
"@pyreon/core": "^0.11.7",
|
|
46
|
+
"@pyreon/reactivity": "^0.11.7",
|
|
51
47
|
"@vitus-labs/tools-lint": "^1.11.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@pyreon/core": "^0.11.7",
|
|
51
|
+
"@pyreon/reactivity": "^0.11.7"
|
|
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
|
|
31
|
-
export { Toaster } from
|
|
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
|
|
40
|
+
} from './types'
|
package/src/tests/toast.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from
|
|
2
|
-
import { _pauseAll, _reset, _resumeAll, _toasts, toast } from
|
|
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(
|
|
20
|
-
it(
|
|
21
|
-
toast(
|
|
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(
|
|
23
|
+
expect(at(0).message).toBe('Hello')
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
it(
|
|
27
|
-
const id = toast(
|
|
28
|
-
expect(typeof id).toBe(
|
|
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(
|
|
33
|
-
toast(
|
|
34
|
-
expect(at(0).type).toBe(
|
|
32
|
+
it('defaults to type info', () => {
|
|
33
|
+
toast('Hello')
|
|
34
|
+
expect(at(0).type).toBe('info')
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
it(
|
|
38
|
-
toast(
|
|
37
|
+
it('defaults dismissible to true', () => {
|
|
38
|
+
toast('Hello')
|
|
39
39
|
expect(at(0).dismissible).toBe(true)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
it(
|
|
43
|
-
toast(
|
|
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(
|
|
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(
|
|
52
|
-
it(
|
|
53
|
-
toast.success(
|
|
54
|
-
expect(at(0).type).toBe(
|
|
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(
|
|
58
|
-
toast.error(
|
|
59
|
-
expect(at(0).type).toBe(
|
|
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(
|
|
63
|
-
toast.warning(
|
|
64
|
-
expect(at(0).type).toBe(
|
|
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(
|
|
68
|
-
toast.info(
|
|
69
|
-
expect(at(0).type).toBe(
|
|
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(
|
|
74
|
-
it(
|
|
75
|
-
const id1 = toast(
|
|
76
|
-
toast(
|
|
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(
|
|
81
|
+
expect(at(0).message).toBe('Second')
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
-
it(
|
|
85
|
-
toast(
|
|
86
|
-
toast(
|
|
87
|
-
toast(
|
|
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(
|
|
95
|
-
toast(
|
|
96
|
-
toast.dismiss(
|
|
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(
|
|
100
|
+
it('calls onDismiss callback when dismissing by id', () => {
|
|
101
101
|
const onDismiss = vi.fn()
|
|
102
|
-
const id = toast(
|
|
102
|
+
const id = toast('Hello', { onDismiss })
|
|
103
103
|
toast.dismiss(id)
|
|
104
104
|
expect(onDismiss).toHaveBeenCalledOnce()
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
-
it(
|
|
107
|
+
it('calls onDismiss for all toasts when dismissing all', () => {
|
|
108
108
|
const onDismiss1 = vi.fn()
|
|
109
109
|
const onDismiss2 = vi.fn()
|
|
110
|
-
toast(
|
|
111
|
-
toast(
|
|
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(
|
|
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(
|
|
128
|
-
toast(
|
|
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(
|
|
139
|
-
toast(
|
|
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(
|
|
149
|
-
toast(
|
|
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(
|
|
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(
|
|
166
|
-
const promise = Promise.resolve(
|
|
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:
|
|
170
|
-
success:
|
|
171
|
-
error:
|
|
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(
|
|
176
|
-
expect(at(0).type).toBe(
|
|
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(
|
|
185
|
-
expect(at(0).type).toBe(
|
|
184
|
+
expect(at(0).message).toBe('Done!')
|
|
185
|
+
expect(at(0).type).toBe('success')
|
|
186
186
|
})
|
|
187
187
|
|
|
188
|
-
it(
|
|
189
|
-
const promise = Promise.reject(new Error(
|
|
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:
|
|
195
|
-
success:
|
|
196
|
-
error:
|
|
194
|
+
loading: 'Loading...',
|
|
195
|
+
success: 'Done!',
|
|
196
|
+
error: 'Failed',
|
|
197
197
|
})
|
|
198
198
|
.catch(() => {})
|
|
199
199
|
|
|
200
|
-
expect(at(0).message).toBe(
|
|
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(
|
|
212
|
-
expect(at(0).type).toBe(
|
|
211
|
+
expect(at(0).message).toBe('Failed')
|
|
212
|
+
expect(at(0).type).toBe('error')
|
|
213
213
|
})
|
|
214
214
|
|
|
215
|
-
it(
|
|
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:
|
|
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(
|
|
227
|
+
expect(at(0).message).toBe('Result: 42')
|
|
228
228
|
})
|
|
229
229
|
|
|
230
|
-
it(
|
|
231
|
-
const promise = Promise.resolve(
|
|
230
|
+
it('returns the original promise', async () => {
|
|
231
|
+
const promise = Promise.resolve('value')
|
|
232
232
|
const result = toast.promise(promise, {
|
|
233
|
-
loading:
|
|
234
|
-
success:
|
|
235
|
-
error:
|
|
233
|
+
loading: 'Loading...',
|
|
234
|
+
success: 'Done!',
|
|
235
|
+
error: 'Failed',
|
|
236
236
|
})
|
|
237
237
|
|
|
238
|
-
expect(await result).toBe(
|
|
238
|
+
expect(await result).toBe('value')
|
|
239
239
|
})
|
|
240
240
|
})
|
|
241
241
|
|
|
242
|
-
describe(
|
|
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(
|
|
252
|
-
const id = toast.loading(
|
|
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(
|
|
255
|
-
expect(at(0).type).toBe(
|
|
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(
|
|
265
|
-
const id = toast.loading(
|
|
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(
|
|
272
|
+
it('accepts options like onDismiss and action', () => {
|
|
273
273
|
const onDismiss = vi.fn()
|
|
274
|
-
const id = toast.loading(
|
|
274
|
+
const id = toast.loading('Loading...', { onDismiss })
|
|
275
275
|
toast.dismiss(id)
|
|
276
276
|
expect(onDismiss).toHaveBeenCalledOnce()
|
|
277
277
|
})
|
|
278
278
|
|
|
279
|
-
it(
|
|
280
|
-
const id = toast.loading(
|
|
281
|
-
expect(at(0).message).toBe(
|
|
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:
|
|
284
|
-
expect(at(0).message).toBe(
|
|
285
|
-
expect(at(0).type).toBe(
|
|
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(
|
|
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(
|
|
299
|
-
const id = toast(
|
|
300
|
-
toast.update(id, { message:
|
|
301
|
-
expect(at(0).message).toBe(
|
|
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(
|
|
305
|
-
const id = toast(
|
|
306
|
-
expect(at(0).type).toBe(
|
|
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:
|
|
309
|
-
expect(at(0).type).toBe(
|
|
308
|
+
toast.update(id, { type: 'success' })
|
|
309
|
+
expect(at(0).type).toBe('success')
|
|
310
310
|
})
|
|
311
311
|
|
|
312
|
-
it(
|
|
313
|
-
const id = toast(
|
|
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(
|
|
330
|
-
toast(
|
|
331
|
-
toast.update(
|
|
332
|
-
expect(at(0).message).toBe(
|
|
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(
|
|
336
|
-
const id = toast(
|
|
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:
|
|
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(
|
|
352
|
-
const id = toast(
|
|
353
|
-
toast.update(id, { message:
|
|
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(
|
|
356
|
-
expect(at(0).type).toBe(
|
|
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(
|
|
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(
|
|
371
|
-
toast(
|
|
372
|
-
toast(
|
|
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(
|
|
384
|
-
toast(
|
|
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(
|
|
402
|
-
toast(
|
|
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(
|
|
412
|
-
toast(
|
|
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(
|
|
423
|
-
it(
|
|
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(
|
|
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(
|
|
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(
|
|
451
|
-
const err = new Error(
|
|
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:
|
|
457
|
-
success:
|
|
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(
|
|
471
|
-
expect(at(0).type).toBe(
|
|
470
|
+
expect(at(0).message).toBe('Failed: network failure')
|
|
471
|
+
expect(at(0).type).toBe('error')
|
|
472
472
|
})
|
|
473
473
|
|
|
474
|
-
it(
|
|
475
|
-
const promise = Promise.resolve(
|
|
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:
|
|
479
|
-
success:
|
|
480
|
-
error:
|
|
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(
|
|
497
|
-
const promise = Promise.reject(new Error(
|
|
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:
|
|
502
|
-
success:
|
|
503
|
-
error:
|
|
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(
|
|
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(
|
|
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(
|
|
531
|
+
it('onDismiss is called on auto-dismiss timeout', () => {
|
|
532
532
|
const onDismiss = vi.fn()
|
|
533
|
-
toast(
|
|
533
|
+
toast('Hello', { onDismiss, duration: 2000 })
|
|
534
534
|
|
|
535
535
|
vi.advanceTimersByTime(2000)
|
|
536
536
|
expect(onDismiss).toHaveBeenCalledOnce()
|
|
537
537
|
})
|
|
538
538
|
|
|
539
|
-
it(
|
|
539
|
+
it('onDismiss is not called twice on manual dismiss after timeout', () => {
|
|
540
540
|
const onDismiss = vi.fn()
|
|
541
|
-
const id = toast(
|
|
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(
|
|
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(
|
|
557
|
-
toast(
|
|
558
|
-
toast(
|
|
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(
|
|
569
|
-
it(
|
|
570
|
-
toast(
|
|
571
|
-
expect(at(0).state).toBe(
|
|
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(
|
|
575
|
-
toast(
|
|
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(
|
|
583
|
-
toast(
|
|
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(
|
|
590
|
-
it(
|
|
591
|
-
const { Toaster } = await import(
|
|
592
|
-
expect(typeof Toaster).toBe(
|
|
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
|
|
2
|
-
import { signal } from
|
|
3
|
-
import type { Toast, ToastOptions, ToastPromiseOptions, ToastType } from
|
|
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 ??
|
|
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:
|
|
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,
|
|
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,
|
|
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(
|
|
144
|
+
toast.success = shortcut('success')
|
|
145
145
|
|
|
146
146
|
/** Show an error toast. */
|
|
147
|
-
toast.error = shortcut(
|
|
147
|
+
toast.error = shortcut('error')
|
|
148
148
|
|
|
149
149
|
/** Show a warning toast. */
|
|
150
|
-
toast.warning = shortcut(
|
|
150
|
+
toast.warning = shortcut('warning')
|
|
151
151
|
|
|
152
152
|
/** Show an info toast. */
|
|
153
|
-
toast.info = shortcut(
|
|
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,
|
|
159
|
-
): string => addToast(message, { ...options, type:
|
|
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,
|
|
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:
|
|
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 ===
|
|
189
|
-
updateToast(id, { message: msg, type:
|
|
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 ===
|
|
193
|
-
updateToast(id, { message: msg, type:
|
|
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
|
|
2
|
-
import { For, Portal } from
|
|
3
|
-
import { computed, effect, onCleanup } from
|
|
4
|
-
import { toastStyles } from
|
|
5
|
-
import { _pauseAll, _resumeAll, _toasts, toast } from
|
|
6
|
-
import type { Toast, ToasterProps, ToastPosition } from
|
|
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 ===
|
|
12
|
-
if (document.querySelector(
|
|
11
|
+
if (typeof document === 'undefined') return
|
|
12
|
+
if (document.querySelector('style[data-pyreon-toast]')) return
|
|
13
13
|
|
|
14
|
-
const style = document.createElement(
|
|
15
|
-
style.setAttribute(
|
|
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(
|
|
23
|
+
const [vertical, horizontal] = position.split('-') as [string, string]
|
|
24
24
|
|
|
25
25
|
let style = `gap: ${gap}px;`
|
|
26
26
|
|
|
27
|
-
if (vertical ===
|
|
27
|
+
if (vertical === 'top') {
|
|
28
28
|
style += ` top: ${offset}px;`
|
|
29
29
|
} else {
|
|
30
30
|
style += ` bottom: ${offset}px;`
|
|
31
|
-
style +=
|
|
31
|
+
style += ' flex-direction: column-reverse;'
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
if (horizontal ===
|
|
34
|
+
if (horizontal === 'left') {
|
|
35
35
|
style += ` left: ${offset}px;`
|
|
36
|
-
} else if (horizontal ===
|
|
37
|
-
style +=
|
|
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 ??
|
|
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 ===
|
|
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 ===
|
|
81
|
+
if (t.state === 'entering') {
|
|
82
82
|
changed = true
|
|
83
|
-
return { ...t, state:
|
|
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 ===
|
|
121
|
-
?
|
|
122
|
-
: t.state ===
|
|
123
|
-
?
|
|
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 ===
|
|
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
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
2
|
|
|
3
3
|
// ─── Public types ────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
export type ToastPosition =
|
|
6
|
-
|
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
6
|
+
| 'top-left'
|
|
7
|
+
| 'top-center'
|
|
8
|
+
| 'top-right'
|
|
9
|
+
| 'bottom-left'
|
|
10
|
+
| 'bottom-center'
|
|
11
|
+
| 'bottom-right'
|
|
12
12
|
|
|
13
|
-
export type ToastType =
|
|
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 =
|
|
49
|
+
export type ToastState = 'entering' | 'visible' | 'exiting'
|
|
50
50
|
|
|
51
51
|
export interface Toast {
|
|
52
52
|
id: string
|