@alann-estrada-ksh/sileo-vue 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,66 +1,144 @@
1
- <div align="center">
2
- <h1>Sileo</h1>
3
- <p>An opinionated toast component for Vue.</p>
4
- <p><a href="https://sileo.aaryan.design">Try Out</a> &nbsp; / &nbsp; <a href="https://sileo.aaryan.design/docs">Docs</a></p>
5
- <video src="https://github.com/user-attachments/assets/a292d310-9189-490a-9f9d-d0a1d09defce"></video>
6
- </div>
7
-
8
- ### Installation
9
-
10
- ```bash
11
- npm i sileo
12
- ```
13
-
14
- ### Getting Started
15
-
16
- ```ts
17
- import { sileo, Toaster } from "sileo";
18
-
19
- const notify = () => {
20
- sileo.info({
21
- title: "Saved",
22
- description: "Your changes were stored.",
23
- });
24
- };
25
- ```
26
-
27
- ```vue
28
- <template>
29
- <Toaster position="top-right" />
30
- <button @click="notify">Show toast</button>
31
- </template>
32
- ```
33
-
34
- ### Actionable Toast Philosophy
35
-
36
- Buttons in toasts should be used for immediate, high-value follow-up actions.
37
-
38
- - Good: actions directly related to the event, such as "Open", "Undo", or "Share" when the toast represents shareable content.
39
- - Avoid: unrelated or low-value actions that belong in full UI flows (dialogs, pages, settings panes).
40
- - Rule of thumb: if the action cannot be understood without extra context, it should not live inside a toast button.
41
-
42
- ### Grouping and Stacking
43
-
44
- To avoid notification overload, `Toaster` can group bursts of toasts:
45
-
46
- ```vue
47
- <Toaster :grouping="true" :group-threshold="3" />
48
- ```
49
-
50
- ### Render In Custom Container
51
-
52
- You can choose where the toaster is teleported (useful for modal/root stacking conflicts):
53
-
54
- ```vue
55
- <Toaster :container="'#sileo-toaster'" />
56
- ```
57
-
58
- ### Methods
59
-
60
- - `sileo.info`, `sileo.success`, `sileo.warning`, `sileo.error`, `sileo.action`
61
- - `sileo.loading` for independent loading notifications
62
- - `sileo.promise` for async lifecycle notifications
63
- - `sileo.dismiss(id?)` and `sileo.clear(position?)`
64
- - `sileo.configure(...)` for persistent global defaults
65
-
66
- For detailed docs, click here: https://sileo.aaryan.design
1
+ <div align="center">
2
+ <h1>Sileo</h1>
3
+ <p>An opinionated toast component for Vue.</p>
4
+ <p><a href="https://sileo.aaryan.design">Try Out</a> &nbsp; / &nbsp; <a href="https://sileo.aaryan.design/docs">Docs</a></p>
5
+ <video src="https://github.com/user-attachments/assets/a292d310-9189-490a-9f9d-d0a1d09defce"></video>
6
+ </div>
7
+
8
+ ### Installation
9
+
10
+ ```bash
11
+ npm i sileo
12
+ ```
13
+
14
+ ### Documentation
15
+
16
+ Start here:
17
+
18
+ - [Docs Index](docs/index.md)
19
+ - [Getting Started](docs/getting-started.md)
20
+ - [API Reference](docs/api-reference.md)
21
+ - [Examples](docs/examples.md)
22
+ - [Styling and Theming](docs/styling.md)
23
+ - [Advanced Features](docs/advanced-features.md)
24
+ - [Implementation Notes](docs/implementation-notes.md)
25
+ - [Migration Guide](docs/migration-guide.md)
26
+
27
+ ### Local Example (In-Repo)
28
+
29
+ You can run a demo app directly from this repository to validate behavior locally:
30
+
31
+ ```bash
32
+ npm run example:install
33
+ npm run example:dev
34
+ ```
35
+
36
+ The example lives in `example/` and imports the package from local source so you can validate animations, icons, grouping, promise flow, swipe behavior, hooks and accessibility without publishing.
37
+
38
+ ### Getting Started
39
+
40
+ ```ts
41
+ import { sileo, Toaster } from "@alann-estrada-KSH/sileo-vue";
42
+ ```
43
+
44
+ ```ts
45
+
46
+ const notify = () => {
47
+ sileo.info({
48
+ title: "Saved",
49
+ description: "Your changes were stored.",
50
+ });
51
+ };
52
+ ```
53
+
54
+ ```vue
55
+ <template>
56
+ <Toaster position="top-right" />
57
+ <button @click="notify">Show toast</button>
58
+ </template>
59
+ ```
60
+
61
+ ### Actionable Toast Philosophy
62
+
63
+ Buttons in toasts should be used for immediate, high-value follow-up actions.
64
+
65
+ - Good: actions directly related to the event, such as "Open", "Undo", or "Share" when the toast represents shareable content.
66
+ - Avoid: unrelated or low-value actions that belong in full UI flows (dialogs, pages, settings panes).
67
+ - Rule of thumb: if the action cannot be understood without extra context, it should not live inside a toast button.
68
+
69
+ ### Grouping and Stacking
70
+
71
+ To avoid notification overload, `Toaster` can group bursts of toasts:
72
+
73
+ ```vue
74
+ <Toaster :grouping="true" :group-threshold="3" />
75
+ ```
76
+
77
+ ### Render In Custom Container
78
+
79
+ You can choose where the toaster is teleported (useful for modal/root stacking conflicts):
80
+
81
+ ```vue
82
+ <Toaster :container="'#sileo-toaster'" />
83
+ ```
84
+
85
+ ### Methods
86
+
87
+ - `sileo.info`, `sileo.success`, `sileo.warning`, `sileo.error`, `sileo.action`
88
+ - `sileo.loading` for independent loading notifications
89
+ - `sileo.promise` for async lifecycle notifications
90
+ - `sileo.dismiss(id?)` and `sileo.clear(position?)`
91
+ - `sileo.configure(...)` for persistent global defaults
92
+
93
+ ### New Runtime Enhancements
94
+
95
+ - Dynamic content height animation (no fixed collapse spacing)
96
+ - Swipe-to-dismiss for touch devices (`swipeToDismiss` per toast)
97
+ - Accessibility live-region control (`aria-live` on `Toaster`)
98
+ - Lifecycle hooks (`hooks.onShow`, `hooks.onExpand`, `hooks.onCollapse`, `hooks.onDismiss`)
99
+
100
+ ### Lifecycle Hooks
101
+
102
+ ```ts
103
+ sileo.success({
104
+ title: "Saved",
105
+ description: "All changes stored.",
106
+ hooks: {
107
+ onShow: (ctx) => console.log("show", ctx),
108
+ onExpand: (ctx) => console.log("expand", ctx),
109
+ onCollapse: (ctx) => console.log("collapse", ctx),
110
+ onDismiss: (ctx) => console.log("dismiss", ctx),
111
+ },
112
+ });
113
+ ```
114
+
115
+ ### Accessibility
116
+
117
+ ```vue
118
+ <Toaster aria-live="polite" />
119
+ ```
120
+
121
+ Allowed values:
122
+ - `polite` (default)
123
+ - `assertive`
124
+ - `off`
125
+
126
+ ### Swipe To Dismiss
127
+
128
+ ```ts
129
+ sileo.info({
130
+ title: "Upload complete",
131
+ swipeToDismiss: true,
132
+ });
133
+ ```
134
+
135
+ Disable per toast:
136
+
137
+ ```ts
138
+ sileo.action({
139
+ title: "Pinned alert",
140
+ swipeToDismiss: false,
141
+ });
142
+ ```
143
+
144
+ For detailed docs, click here: https://sileo.aaryan.design
package/dist/index.d.mts CHANGED
@@ -17,6 +17,17 @@ interface SileoAutopilot {
17
17
  expand?: number;
18
18
  collapse?: number;
19
19
  }
20
+ interface SileoLifecycleContext {
21
+ id: string;
22
+ instanceId: string;
23
+ state: SileoState;
24
+ }
25
+ interface SileoLifecycleHooks {
26
+ onShow?: (ctx: SileoLifecycleContext) => void;
27
+ onExpand?: (ctx: SileoLifecycleContext) => void;
28
+ onCollapse?: (ctx: SileoLifecycleContext) => void;
29
+ onDismiss?: (ctx: SileoLifecycleContext) => void;
30
+ }
20
31
  declare const SILEO_POSITIONS: readonly ["top-left", "top-center", "top-right", "bottom-left", "bottom-center", "bottom-right"];
21
32
  type SileoPosition = (typeof SILEO_POSITIONS)[number];
22
33
  interface SileoOptions {
@@ -31,8 +42,10 @@ interface SileoOptions {
31
42
  fill?: string;
32
43
  roundness?: number;
33
44
  autopilot?: boolean | SileoAutopilot;
45
+ swipeToDismiss?: boolean;
34
46
  button?: SileoButton;
35
47
  groupKey?: string;
48
+ hooks?: SileoLifecycleHooks;
36
49
  }
37
50
  type SileoOffsetValue = number | string;
38
51
  type SileoOffsetConfig = Partial<Record<"top" | "right" | "bottom" | "left", SileoOffsetValue>>;
@@ -73,6 +86,10 @@ declare const Toaster: vue.DefineComponent<vue.ExtractPropTypes<{
73
86
  type: NumberConstructor;
74
87
  default: number;
75
88
  };
89
+ ariaLive: {
90
+ type: PropType<"off" | "polite" | "assertive">;
91
+ default: string;
92
+ };
76
93
  }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
77
94
  [key: string]: any;
78
95
  }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
@@ -89,11 +106,16 @@ declare const Toaster: vue.DefineComponent<vue.ExtractPropTypes<{
89
106
  type: NumberConstructor;
90
107
  default: number;
91
108
  };
109
+ ariaLive: {
110
+ type: PropType<"off" | "polite" | "assertive">;
111
+ default: string;
112
+ };
92
113
  }>> & Readonly<{}>, {
93
114
  grouping: boolean;
94
115
  groupThreshold: number;
116
+ ariaLive: "off" | "polite" | "assertive";
95
117
  }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
96
118
 
97
119
  export { Toaster, sileo };
98
- export type { SileoAutopilot, SileoButton, SileoOffsetConfig, SileoOffsetValue, SileoOptions, SileoPosition, SileoState, SileoStyles };
120
+ export type { SileoAutopilot, SileoButton, SileoLifecycleContext, SileoLifecycleHooks, SileoOffsetConfig, SileoOffsetValue, SileoOptions, SileoPosition, SileoState, SileoStyles };
99
121
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sources":["../src/types.ts","../src/toast.ts"],"sourcesContent":["import type { VNodeChild } from \"vue\";\n\nexport type SileoState =\n\t| \"success\"\n\t| \"loading\"\n\t| \"error\"\n\t| \"warning\"\n\t| \"info\"\n\t| \"action\";\n\nexport interface SileoStyles {\n\ttitle?: string;\n\tdescription?: string;\n\tbadge?: string;\n\tbutton?: string;\n\ttoast?: string;\n}\n\nexport interface SileoButton {\n\ttitle: string;\n\tonClick?: () => void;\n}\n\nexport interface SileoAutopilot {\n\texpand?: number;\n\tcollapse?: number;\n}\n\nexport const SILEO_POSITIONS = [\n\t\"top-left\",\n\t\"top-center\",\n\t\"top-right\",\n\t\"bottom-left\",\n\t\"bottom-center\",\n\t\"bottom-right\",\n] as const;\n\nexport type SileoPosition = (typeof SILEO_POSITIONS)[number];\n\nexport interface SileoOptions {\n\tid?: string;\n\ttitle?: string;\n\tdescription?: VNodeChild | string;\n\ttype?: SileoState;\n\tposition?: SileoPosition;\n\tduration?: number | null;\n\ticon?: VNodeChild | null;\n\tstyles?: SileoStyles;\n\tfill?: string;\n\troundness?: number;\n\tautopilot?: boolean | SileoAutopilot;\n\tbutton?: SileoButton;\n\tgroupKey?: string;\n}\n\nexport type SileoOffsetValue = number | string;\nexport type SileoOffsetConfig = Partial<\n\tRecord<\"top\" | \"right\" | \"bottom\" | \"left\", SileoOffsetValue>\n>;\n","import {\n Teleport,\n computed,\n defineComponent,\n h,\n onBeforeUnmount,\n onMounted,\n ref,\n watch,\n type CSSProperties,\n type PropType,\n} from \"vue\";\nimport {\n AUTOPILOT_COLLAPSE_DELAY,\n AUTOPILOT_EXPAND_DELAY,\n DEFAULT_ROUNDNESS,\n DEFAULT_TOAST_DURATION,\n EXIT_DURATION,\n GROUP_THRESHOLD,\n} from \"./constants\";\nimport type {\n SileoOffsetConfig,\n SileoOffsetValue,\n SileoOptions,\n SileoPosition,\n SileoState,\n} from \"./types\";\n\ninterface SileoItem extends Omit<SileoOptions, \"type\"> {\n id: string;\n instanceId: string;\n state: SileoState;\n createdAt: number;\n exiting?: boolean;\n}\n\ntype AutopilotConfig = {\n expand: number;\n collapse: number;\n};\n\ntype Listener = (toasts: SileoItem[]) => void;\n\nconst store = {\n toasts: [] as SileoItem[],\n listeners: new Set<Listener>(),\n position: \"top-right\" as SileoPosition,\n options: undefined as Partial<SileoOptions> | undefined,\n\n emit() {\n for (const listener of this.listeners) listener(this.toasts);\n },\n\n update(updater: (prev: SileoItem[]) => SileoItem[]) {\n this.toasts = updater(this.toasts);\n this.emit();\n },\n};\n\nconst dismissalTimers = new Map<string, ReturnType<typeof setTimeout>>();\nlet sequence = 0;\n\nconst nextId = () => `sileo-${++sequence}-${Date.now().toString(36)}`;\nconst nextInstanceId = () => `instance-${Math.random().toString(36).slice(2, 10)}-${Date.now().toString(36)}`;\n\nconst clearTimer = (instanceId: string) => {\n const timer = dismissalTimers.get(instanceId);\n if (timer !== undefined) {\n clearTimeout(timer);\n dismissalTimers.delete(instanceId);\n }\n};\n\nconst removeByInstance = (instanceId: string) => {\n clearTimer(instanceId);\n store.update((prev) => prev.filter((item) => item.instanceId !== instanceId));\n};\n\nconst dismissByInstance = (instanceId: string) => {\n const target = store.toasts.find(\n (item) => item.instanceId === instanceId && !item.exiting,\n );\n if (!target) return;\n\n clearTimer(instanceId);\n store.update((prev) =>\n prev.map((item) =>\n item.instanceId === instanceId ? { ...item, exiting: true } : item,\n ),\n );\n\n globalThis.setTimeout(() => {\n removeByInstance(instanceId);\n }, EXIT_DURATION);\n};\n\nconst scheduleDismiss = (item: SileoItem) => {\n clearTimer(item.instanceId);\n if (item.duration === null) return;\n const duration = item.duration ?? DEFAULT_TOAST_DURATION;\n if (duration <= 0) return;\n\n const timer = globalThis.setTimeout(() => {\n dismissByInstance(item.instanceId);\n }, duration);\n\n dismissalTimers.set(item.instanceId, timer);\n};\n\nconst mergeOptions = (options: SileoOptions): SileoOptions => ({\n ...store.options,\n ...options,\n styles: { ...store.options?.styles, ...options.styles },\n});\n\nconst buildItem = (options: SileoOptions, state: SileoState): SileoItem => ({\n id: options.id ?? nextId(),\n instanceId: nextInstanceId(),\n state,\n title: options.title,\n description: options.description,\n position: options.position ?? store.position,\n duration: options.duration,\n icon: options.icon,\n styles: options.styles,\n fill: options.fill,\n roundness: options.roundness,\n autopilot: options.autopilot,\n button: options.button,\n groupKey: options.groupKey,\n createdAt: Date.now(),\n});\n\nconst createToast = (raw: SileoOptions, forcedState?: SileoState) => {\n const merged = mergeOptions(raw);\n const state = forcedState ?? merged.type ?? \"info\";\n const hasCustomId = Boolean(merged.id);\n\n if (hasCustomId) {\n const existing = store.toasts.find(\n (item) => item.id === merged.id && !item.exiting,\n );\n if (existing) {\n const replacement = buildItem(merged, state);\n replacement.id = existing.id;\n store.update((prev) =>\n prev.map((item) =>\n item.instanceId === existing.instanceId ? replacement : item,\n ),\n );\n clearTimer(existing.instanceId);\n scheduleDismiss(replacement);\n return existing.id;\n }\n }\n\n const item = buildItem(merged, state);\n store.update((prev) => [...prev, item]);\n scheduleDismiss(item);\n return item.id;\n};\n\nconst dismissToast = (id?: string) => {\n if (!id) {\n for (const item of store.toasts) dismissByInstance(item.instanceId);\n return;\n }\n\n const active = store.toasts\n .filter((item) => item.id === id && !item.exiting)\n .sort((a, b) => b.createdAt - a.createdAt)[0];\n\n if (!active) return;\n dismissByInstance(active.instanceId);\n};\n\nconst clearToasts = (position?: SileoPosition) => {\n const removed = position\n ? store.toasts.filter((item) => item.position === position)\n : [...store.toasts];\n\n for (const item of removed) clearTimer(item.instanceId);\n\n store.update((prev) =>\n position ? prev.filter((item) => item.position !== position) : [],\n );\n};\n\nconst resolveTheme = (theme: \"light\" | \"dark\" | \"system\" | undefined) => {\n if (theme === \"light\" || theme === \"dark\") return theme;\n if (typeof window === \"undefined\") return \"light\";\n if (typeof window.matchMedia !== \"function\") return \"light\";\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n ? \"dark\"\n : \"light\";\n};\n\nconst normalizeAutopilot = (\n value: SileoOptions[\"autopilot\"],\n): AutopilotConfig | null => {\n if (value === false) return null;\n if (value === true || value === undefined) {\n return {\n expand: AUTOPILOT_EXPAND_DELAY,\n collapse: AUTOPILOT_COLLAPSE_DELAY,\n };\n }\n\n const expand = Number.isFinite(value.expand)\n ? Math.max(0, Number(value.expand))\n : AUTOPILOT_EXPAND_DELAY;\n const collapse = Number.isFinite(value.collapse)\n ? Math.max(expand + 200, Number(value.collapse))\n : AUTOPILOT_COLLAPSE_DELAY;\n\n return { expand, collapse };\n};\n\nconst renderStateIcon = (state: SileoState) => {\n const common = {\n viewBox: \"0 0 16 16\",\n width: \"14\",\n height: \"14\",\n \"aria-hidden\": \"true\",\n focusable: \"false\",\n } as const;\n\n if (state === \"success\") {\n return h(\"svg\", common, [\n h(\"path\", {\n d: \"M3.2 8.4L6.5 11.4L12.8 4.9\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"1.8\",\n \"stroke-linecap\": \"round\",\n \"stroke-linejoin\": \"round\",\n }),\n ]);\n }\n if (state === \"error\") {\n return h(\"svg\", common, [\n h(\"path\", {\n d: \"M5 5L11 11M11 5L5 11\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"1.8\",\n \"stroke-linecap\": \"round\",\n }),\n ]);\n }\n if (state === \"warning\") {\n return h(\"svg\", common, [\n h(\"path\", {\n d: \"M8 3.2L13 12.5H3L8 3.2Z\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"1.4\",\n \"stroke-linejoin\": \"round\",\n }),\n h(\"path\", {\n d: \"M8 6.2V9\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"1.5\",\n \"stroke-linecap\": \"round\",\n }),\n h(\"circle\", { cx: \"8\", cy: \"11\", r: \"0.9\", fill: \"currentColor\" }),\n ]);\n }\n if (state === \"action\") {\n return h(\"svg\", common, [\n h(\"path\", {\n d: \"M3.5 8H12.5M9.2 4.8L12.4 8L9.2 11.2\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"1.8\",\n \"stroke-linecap\": \"round\",\n \"stroke-linejoin\": \"round\",\n }),\n ]);\n }\n if (state === \"loading\") {\n return h(\"svg\", { ...common, \"data-sileo-icon\": \"spin\" }, [\n h(\"circle\", {\n cx: \"8\",\n cy: \"8\",\n r: \"5\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"1.6\",\n opacity: \"0.35\",\n }),\n h(\"path\", {\n d: \"M8 3A5 5 0 0 1 13 8\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"1.8\",\n \"stroke-linecap\": \"round\",\n }),\n ]);\n }\n\n return h(\"svg\", common, [\n h(\"circle\", {\n cx: \"8\",\n cy: \"8\",\n r: \"5.2\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"1.6\",\n }),\n h(\"path\", {\n d: \"M8 6.8V10.2\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"1.6\",\n \"stroke-linecap\": \"round\",\n }),\n h(\"circle\", { cx: \"8\", cy: \"4.9\", r: \"0.9\", fill: \"currentColor\" }),\n ]);\n};\n\nconst getOffsetStyle = (\n position: SileoPosition,\n offset?: SileoOffsetValue | SileoOffsetConfig,\n): CSSProperties | undefined => {\n if (offset === undefined) return undefined;\n\n const value =\n typeof offset === \"object\"\n ? offset\n : { top: offset, right: offset, bottom: offset, left: offset };\n\n const style: CSSProperties = {};\n const px = (v: SileoOffsetValue) => (typeof v === \"number\" ? `${v}px` : v);\n\n if (position.startsWith(\"top\") && value.top !== undefined) style.top = px(value.top);\n if (position.startsWith(\"bottom\") && value.bottom !== undefined) {\n style.bottom = px(value.bottom);\n }\n if (position.endsWith(\"left\") && value.left !== undefined) style.left = px(value.left);\n if (position.endsWith(\"right\") && value.right !== undefined) {\n style.right = px(value.right);\n }\n\n return style;\n};\n\nexport interface SileoPromiseOptions<T = unknown> {\n loading: SileoOptions;\n success: SileoOptions | ((data: T) => SileoOptions);\n error: SileoOptions | ((error: unknown) => SileoOptions);\n action?: SileoOptions | ((data: T) => SileoOptions);\n position?: SileoPosition;\n}\n\nexport interface SileoToasterProps {\n position?: SileoPosition;\n offset?: SileoOffsetValue | SileoOffsetConfig;\n options?: Partial<SileoOptions>;\n theme?: \"light\" | \"dark\" | \"system\";\n container?: string | HTMLElement;\n grouping?: boolean;\n groupThreshold?: number;\n}\n\nexport const sileo = {\n show: (opts: SileoOptions) => createToast(opts, opts.type ?? \"info\"),\n success: (opts: SileoOptions) => createToast(opts, \"success\"),\n error: (opts: SileoOptions) => createToast(opts, \"error\"),\n warning: (opts: SileoOptions) => createToast(opts, \"warning\"),\n info: (opts: SileoOptions) => createToast(opts, \"info\"),\n action: (opts: SileoOptions) => createToast(opts, \"action\"),\n loading: (opts: SileoOptions) =>\n createToast({ ...opts, duration: opts.duration ?? null }, \"loading\"),\n\n promise: async <T,>(\n promise: Promise<T> | (() => Promise<T>),\n opts: SileoPromiseOptions<T>,\n ): Promise<T> => {\n const id = createToast(\n { ...opts.loading, duration: null, position: opts.position },\n \"loading\",\n );\n try {\n const data = await (typeof promise === \"function\" ? promise() : promise);\n const next = opts.action\n ? typeof opts.action === \"function\"\n ? opts.action(data)\n : opts.action\n : typeof opts.success === \"function\"\n ? opts.success(data)\n : opts.success;\n createToast({ ...next, id }, opts.action ? \"action\" : \"success\");\n return data;\n } catch (error) {\n const next =\n typeof opts.error === \"function\" ? opts.error(error) : opts.error;\n createToast({ ...next, id }, \"error\");\n throw error;\n }\n },\n\n configure: (options: Partial<SileoOptions> & { position?: SileoPosition }) => {\n if (options.position) {\n store.position = options.position;\n }\n store.options = {\n ...store.options,\n ...options,\n styles: { ...store.options?.styles, ...options.styles },\n };\n },\n\n dismiss: dismissToast,\n clear: clearToasts,\n};\n\nexport const Toaster = defineComponent({\n name: \"SileoToaster\",\n props: {\n position: String as PropType<SileoPosition | undefined>,\n offset: [Number, String, Object] as PropType<\n SileoOffsetValue | SileoOffsetConfig\n >,\n options: Object as PropType<Partial<SileoOptions>>,\n theme: String as PropType<\"light\" | \"dark\" | \"system\">,\n container: [String, Object] as PropType<string | HTMLElement>,\n grouping: { type: Boolean, default: false },\n groupThreshold: { type: Number, default: GROUP_THRESHOLD },\n },\n setup(props, { slots }) {\n const toasts = ref<SileoItem[]>(store.toasts);\n const expandedGroups = ref<Record<string, boolean>>({});\n const expandedToasts = ref<Record<string, boolean>>({});\n const autopilotTimers = new Map<string, ReturnType<typeof setTimeout>[]>();\n const activeToastInstanceId = ref<string | undefined>(undefined);\n\n const setExpanded = (instanceId: string, expanded: boolean) => {\n expandedToasts.value[instanceId] = expanded;\n };\n\n const clearAutopilotTimers = (instanceId: string) => {\n const timers = autopilotTimers.get(instanceId);\n if (!timers) return;\n for (const timer of timers) clearTimeout(timer);\n autopilotTimers.delete(instanceId);\n };\n\n const scheduleAutopilot = (item: SileoItem) => {\n clearAutopilotTimers(item.instanceId);\n const config = normalizeAutopilot(item.autopilot);\n if (!item.description || !config) {\n setExpanded(item.instanceId, false);\n return;\n }\n\n if (\n activeToastInstanceId.value !== undefined &&\n activeToastInstanceId.value !== item.instanceId\n ) {\n setExpanded(item.instanceId, false);\n return;\n }\n\n setExpanded(item.instanceId, false);\n const timers: ReturnType<typeof setTimeout>[] = [];\n\n timers.push(\n globalThis.setTimeout(() => {\n setExpanded(item.instanceId, true);\n }, config.expand),\n );\n\n const duration = item.duration ?? DEFAULT_TOAST_DURATION;\n if (item.duration !== null) {\n const maxCollapse = Math.max(\n config.expand + 350,\n duration - EXIT_DURATION - 150,\n );\n const collapseAt = Math.min(config.collapse, maxCollapse);\n if (collapseAt > config.expand) {\n timers.push(\n globalThis.setTimeout(() => {\n setExpanded(item.instanceId, false);\n }, collapseAt),\n );\n }\n } else if (config.collapse > config.expand) {\n timers.push(\n globalThis.setTimeout(() => {\n setExpanded(item.instanceId, false);\n }, config.collapse),\n );\n }\n\n autopilotTimers.set(item.instanceId, timers);\n };\n\n const listener: Listener = (next) => {\n toasts.value = next;\n };\n\n onMounted(() => {\n store.listeners.add(listener);\n if (props.position) store.position = props.position;\n if (props.options) {\n store.options = {\n ...store.options,\n ...props.options,\n styles: { ...store.options?.styles, ...props.options.styles },\n };\n }\n });\n\n onBeforeUnmount(() => {\n store.listeners.delete(listener);\n for (const instanceId of autopilotTimers.keys()) {\n clearAutopilotTimers(instanceId);\n }\n });\n\n watch(\n () => props.position,\n (value) => {\n if (value) store.position = value;\n },\n );\n\n watch(\n () => props.options,\n (value) => {\n if (!value) return;\n store.options = {\n ...store.options,\n ...value,\n styles: { ...store.options?.styles, ...value.styles },\n };\n },\n { deep: true },\n );\n\n watch(\n () => toasts.value,\n (items) => {\n const active = new Set(items.map((item) => item.instanceId));\n const latest = [...items]\n .filter((item) => !item.exiting)\n .sort((a, b) => b.createdAt - a.createdAt)[0];\n\n activeToastInstanceId.value = latest?.instanceId;\n\n for (const item of items) {\n if (expandedToasts.value[item.instanceId] === undefined) {\n setExpanded(item.instanceId, false);\n }\n }\n\n for (const item of items) {\n scheduleAutopilot(item);\n }\n\n for (const instanceId of Object.keys(expandedToasts.value)) {\n if (!active.has(instanceId)) {\n clearAutopilotTimers(instanceId);\n delete expandedToasts.value[instanceId];\n }\n }\n },\n { immediate: true },\n );\n\n watch(\n () => activeToastInstanceId.value,\n (activeInstanceId) => {\n for (const instanceId of Object.keys(expandedToasts.value)) {\n if (instanceId !== activeInstanceId) {\n setExpanded(instanceId, false);\n }\n }\n\n if (!activeInstanceId) return;\n const item = toasts.value.find((toast) => toast.instanceId === activeInstanceId);\n if (item) {\n scheduleAutopilot(item);\n }\n },\n );\n\n const groupedByPosition = computed(() => {\n const map = new Map<SileoPosition, SileoItem[]>();\n for (const item of toasts.value) {\n const pos = item.position ?? store.position;\n const list = map.get(pos);\n if (list) {\n list.push(item);\n } else {\n map.set(pos, [item]);\n }\n }\n return map;\n });\n\n const renderToast = (item: SileoItem) => {\n const roundness = `${Math.max(0, item.roundness ?? DEFAULT_ROUNDNESS)}px`;\n const isExpanded = Boolean(expandedToasts.value[item.instanceId]);\n const contentVisible = isExpanded && item.state !== \"loading\";\n return h(\n \"article\",\n {\n key: item.instanceId,\n \"data-sileo-toast\": \"true\",\n \"data-state\": item.state,\n \"data-exiting\": item.exiting ? \"true\" : \"false\",\n \"data-expanded\": isExpanded ? \"true\" : \"false\",\n class: [\"sileo-toast\", item.styles?.toast],\n style: {\n \"--sileo-fill\": item.fill,\n \"--sileo-roundness\": roundness,\n } as CSSProperties,\n onMouseenter: () => {\n activeToastInstanceId.value = item.instanceId;\n setExpanded(item.instanceId, true);\n },\n onMouseleave: () => {\n const autopilot = normalizeAutopilot(item.autopilot);\n const latest = [...toasts.value]\n .filter((toast) => !toast.exiting)\n .sort((a, b) => b.createdAt - a.createdAt)[0];\n activeToastInstanceId.value = latest?.instanceId;\n if (autopilot !== null) setExpanded(item.instanceId, false);\n },\n },\n [\n h(\"div\", { class: \"sileo-head\" }, [\n h(\n \"span\",\n {\n class: [\"sileo-badge\", item.styles?.badge],\n \"data-sileo-badge\": \"true\",\n },\n item.icon ?? renderStateIcon(item.state),\n ),\n h(\n \"p\",\n {\n class: [\"sileo-title\", item.styles?.title],\n \"data-sileo-title\": \"true\",\n },\n item.title,\n ),\n h(\n \"button\",\n {\n type: \"button\",\n class: \"sileo-dismiss\",\n onClick: () => dismissByInstance(item.instanceId),\n \"aria-label\": \"Dismiss toast\",\n },\n \"x\",\n ),\n ]),\n item.description || item.button\n ? h(\n \"div\",\n {\n class: \"sileo-content\",\n \"data-sileo-content\": \"true\",\n \"data-visible\": contentVisible ? \"true\" : \"false\",\n },\n [\n item.description\n ? h(\n \"div\",\n {\n class: [\"sileo-description\", item.styles?.description],\n \"data-sileo-description\": \"true\",\n },\n [item.description],\n )\n : null,\n item.button\n ? h(\n \"button\",\n {\n type: \"button\",\n class: [\"sileo-action\", item.styles?.button],\n \"data-sileo-button\": \"true\",\n onClick: (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n item.button?.onClick?.();\n },\n },\n item.button.title,\n )\n : null,\n ],\n )\n : null,\n ],\n );\n };\n\n const renderPosition = (position: SileoPosition, items: SileoItem[]) => {\n const buckets = new Map<string, SileoItem[]>();\n for (const item of items) {\n const key = item.groupKey ?? \"__default__\";\n const list = buckets.get(key);\n if (list) {\n list.push(item);\n } else {\n buckets.set(key, [item]);\n }\n }\n\n const children: ReturnType<typeof h>[] = [];\n for (const [bucketKey, bucketItems] of buckets) {\n const visible = bucketItems.filter((item) => !item.exiting);\n const expandKey = `${position}:${bucketKey}`;\n const shouldGroup =\n props.grouping &&\n visible.length > props.groupThreshold &&\n !expandedGroups.value[expandKey];\n\n if (shouldGroup) {\n const label =\n bucketKey === \"__default__\"\n ? `${visible.length} notifications`\n : `${visible.length} ${bucketKey} notifications`;\n children.push(\n h(\n \"button\",\n {\n key: expandKey,\n type: \"button\",\n \"data-sileo-group\": \"true\",\n class: \"sileo-group\",\n onMouseenter: () => {\n expandedGroups.value[expandKey] = true;\n },\n onClick: () => {\n expandedGroups.value[expandKey] = true;\n },\n },\n label,\n ),\n );\n continue;\n }\n\n for (const item of bucketItems) {\n children.push(renderToast(item));\n }\n }\n\n return h(\n \"section\",\n {\n key: position,\n \"data-sileo-viewport\": \"true\",\n \"data-position\": position,\n \"data-theme\": resolveTheme(props.theme),\n style: getOffsetStyle(position, props.offset),\n },\n children,\n );\n };\n\n return () => {\n const viewports = Array.from(groupedByPosition.value.entries()).map(\n ([position, items]) => renderPosition(position, items),\n );\n\n const portalTarget = props.container ?? \"body\";\n\n return h(\"div\", { class: \"sileo-root\" }, [\n slots.default?.(),\n h(Teleport, { to: portalTarget }, viewports),\n ]);\n };\n },\n});\n"],"names":[],"mappings":";;;AACO,KAAA,UAAA;AACA,UAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,WAAA;AACP;AACA;AACA;AACO,UAAA,cAAA;AACP;AACA;AACA;AACO,cAAA,eAAA;AACA,KAAA,aAAA,WAAA,eAAA;AACA,UAAA,YAAA;AACP;AACA;AACA,kBAAA,UAAA;AACA,WAAA,UAAA;AACA,eAAA,aAAA;AACA;AACA,WAAA,UAAA;AACA,aAAA,WAAA;AACA;AACA;AACA,0BAAA,cAAA;AACA,aAAA,WAAA;AACA;AACA;AACO,KAAA,gBAAA;AACA,KAAA,iBAAA,GAAA,OAAA,CAAA,MAAA,sCAAA,gBAAA;;ACjCA,UAAA,mBAAA;AACP,aAAA,YAAA;AACA,aAAA,YAAA,iBAAA,YAAA;AACA,WAAA,YAAA,wBAAA,YAAA;AACA,aAAA,YAAA,iBAAA,YAAA;AACA,eAAA,aAAA;AACA;AAUO,cAAA,KAAA;AACP,iBAAA,YAAA;AACA,oBAAA,YAAA;AACA,kBAAA,YAAA;AACA,oBAAA,YAAA;AACA,iBAAA,YAAA;AACA,mBAAA,YAAA;AACA,oBAAA,YAAA;AACA,0BAAA,OAAA,aAAA,OAAA,YAAA,mBAAA,QAAA,OAAA;AACA,yBAAA,OAAA,CAAA,YAAA;AACA,mBAAA,aAAA;AACA;AACA;AACA,uBAAA,aAAA;AACA;AACO,cAAA,OAAA,EAAuB,GAAa,iBAAiB,GAAa,CAAA,gBAAA;AACzE,cAAA,QAAA,CAAA,aAAA;AACA,YAAA,QAAA,CAAA,gBAAA,GAAA,iBAAA;AACA,aAAA,QAAA,CAAA,OAAA,CAAA,YAAA;AACA,WAAA,QAAA;AACA,eAAA,QAAA,UAAA,WAAA;AACA;AACA,cAAA,kBAAA;AACA;AACA;AACA;AACA,cAAA,iBAAA;AACA;AACA;AACA,UAAU,GAAa,CAAA,KAAA,CAAO,GAAa,CAAA,YAAA,EAAe,GAAa,CAAA,eAAA;AACvE;AACA,gBAAgB,GAAa,CAAA,qBAAA,EAAwB,GAAa,CAAA,qBAAA,cAAoC,GAAa,uBAAuB,GAAa,CAAA,gBAAA;AACvJ,cAAA,QAAA,CAAA,aAAA;AACA,YAAA,QAAA,CAAA,gBAAA,GAAA,iBAAA;AACA,aAAA,QAAA,CAAA,OAAA,CAAA,YAAA;AACA,WAAA,QAAA;AACA,eAAA,QAAA,UAAA,WAAA;AACA;AACA,cAAA,kBAAA;AACA;AACA;AACA;AACA,cAAA,iBAAA;AACA;AACA;AACA,MAAA,QAAA;AACA;AACA;AACA,uBAAuB,GAAa,CAAA,uBAAA;;;;"}
1
+ {"version":3,"file":"index.d.mts","sources":["../src/types.ts","../src/toast.ts"],"sourcesContent":["import type { VNodeChild } from \"vue\";\r\n\r\nexport type SileoState =\r\n\t| \"success\"\r\n\t| \"loading\"\r\n\t| \"error\"\r\n\t| \"warning\"\r\n\t| \"info\"\r\n\t| \"action\";\r\n\r\nexport interface SileoStyles {\r\n\ttitle?: string;\r\n\tdescription?: string;\r\n\tbadge?: string;\r\n\tbutton?: string;\r\n\ttoast?: string;\r\n}\r\n\r\nexport interface SileoButton {\r\n\ttitle: string;\r\n\tonClick?: () => void;\r\n}\r\n\r\nexport interface SileoAutopilot {\r\n\texpand?: number;\r\n\tcollapse?: number;\r\n}\r\n\r\nexport interface SileoLifecycleContext {\r\n\tid: string;\r\n\tinstanceId: string;\r\n\tstate: SileoState;\r\n}\r\n\r\nexport interface SileoLifecycleHooks {\r\n\tonShow?: (ctx: SileoLifecycleContext) => void;\r\n\tonExpand?: (ctx: SileoLifecycleContext) => void;\r\n\tonCollapse?: (ctx: SileoLifecycleContext) => void;\r\n\tonDismiss?: (ctx: SileoLifecycleContext) => void;\r\n}\r\n\r\nexport const SILEO_POSITIONS = [\r\n\t\"top-left\",\r\n\t\"top-center\",\r\n\t\"top-right\",\r\n\t\"bottom-left\",\r\n\t\"bottom-center\",\r\n\t\"bottom-right\",\r\n] as const;\r\n\r\nexport type SileoPosition = (typeof SILEO_POSITIONS)[number];\r\n\r\nexport interface SileoOptions {\r\n\tid?: string;\r\n\ttitle?: string;\r\n\tdescription?: VNodeChild | string;\r\n\ttype?: SileoState;\r\n\tposition?: SileoPosition;\r\n\tduration?: number | null;\r\n\ticon?: VNodeChild | null;\r\n\tstyles?: SileoStyles;\r\n\tfill?: string;\r\n\troundness?: number;\r\n\tautopilot?: boolean | SileoAutopilot;\r\n\tswipeToDismiss?: boolean;\r\n\tbutton?: SileoButton;\r\n\tgroupKey?: string;\r\n\thooks?: SileoLifecycleHooks;\r\n}\r\n\r\nexport type SileoOffsetValue = number | string;\r\nexport type SileoOffsetConfig = Partial<\r\n\tRecord<\"top\" | \"right\" | \"bottom\" | \"left\", SileoOffsetValue>\r\n>;\r\n","import {\r\n Teleport,\r\n computed,\r\n defineComponent,\r\n h,\r\n onBeforeUnmount,\r\n onMounted,\r\n ref,\r\n watch,\r\n type ComponentPublicInstance,\r\n type CSSProperties,\r\n type PropType,\r\n} from \"vue\";\r\nimport {\r\n AUTOPILOT_COLLAPSE_DELAY,\r\n AUTOPILOT_EXPAND_DELAY,\r\n BLUR_RATIO,\r\n DEFAULT_ROUNDNESS,\r\n DEFAULT_TOAST_DURATION,\r\n EXIT_DURATION,\r\n GROUP_THRESHOLD,\r\n MIN_EXPAND_RATIO,\r\n PILL_PADDING,\r\n TOAST_HEIGHT,\r\n TOAST_WIDTH,\r\n} from \"./constants\";\r\nimport type {\r\n SileoLifecycleContext,\r\n SileoOffsetConfig,\r\n SileoOffsetValue,\r\n SileoOptions,\r\n SileoPosition,\r\n SileoState,\r\n} from \"./types\";\r\n\r\ninterface SileoItem extends Omit<SileoOptions, \"type\"> {\r\n id: string;\r\n instanceId: string;\r\n state: SileoState;\r\n createdAt: number;\r\n exiting?: boolean;\r\n}\r\n\r\ntype AutopilotConfig = {\r\n expand: number;\r\n collapse: number;\r\n};\r\n\r\ntype Listener = (toasts: SileoItem[]) => void;\r\n\r\nconst store = {\r\n toasts: [] as SileoItem[],\r\n listeners: new Set<Listener>(),\r\n position: \"top-right\" as SileoPosition,\r\n options: undefined as Partial<SileoOptions> | undefined,\r\n\r\n emit() {\r\n for (const listener of this.listeners) listener(this.toasts);\r\n },\r\n\r\n update(updater: (prev: SileoItem[]) => SileoItem[]) {\r\n this.toasts = updater(this.toasts);\r\n this.emit();\r\n },\r\n};\r\n\r\nconst dismissalTimers = new Map<string, ReturnType<typeof setTimeout>>();\r\nlet sequence = 0;\r\n\r\nconst nextId = () => `sileo-${++sequence}-${Date.now().toString(36)}`;\r\nconst nextInstanceId = () => `instance-${Math.random().toString(36).slice(2, 10)}-${Date.now().toString(36)}`;\r\n\r\nconst clearTimer = (instanceId: string) => {\r\n const timer = dismissalTimers.get(instanceId);\r\n if (timer !== undefined) {\r\n clearTimeout(timer);\r\n dismissalTimers.delete(instanceId);\r\n }\r\n};\r\n\r\nconst removeByInstance = (instanceId: string) => {\r\n clearTimer(instanceId);\r\n store.update((prev) => prev.filter((item) => item.instanceId !== instanceId));\r\n};\r\n\r\nconst dismissByInstance = (instanceId: string) => {\r\n const target = store.toasts.find(\r\n (item) => item.instanceId === instanceId && !item.exiting,\r\n );\r\n if (!target) return;\r\n\r\n clearTimer(instanceId);\r\n target.hooks?.onDismiss?.(toLifecycleContext(target));\r\n store.update((prev) =>\r\n prev.map((item) =>\r\n item.instanceId === instanceId ? { ...item, exiting: true } : item,\r\n ),\r\n );\r\n\r\n globalThis.setTimeout(() => {\r\n removeByInstance(instanceId);\r\n }, EXIT_DURATION);\r\n};\r\n\r\nconst scheduleDismiss = (item: SileoItem) => {\r\n clearTimer(item.instanceId);\r\n if (item.duration === null) return;\r\n const duration = item.duration ?? DEFAULT_TOAST_DURATION;\r\n if (duration <= 0) return;\r\n\r\n const timer = globalThis.setTimeout(() => {\r\n dismissByInstance(item.instanceId);\r\n }, duration);\r\n\r\n dismissalTimers.set(item.instanceId, timer);\r\n};\r\n\r\nconst mergeOptions = (options: SileoOptions): SileoOptions => ({\r\n ...store.options,\r\n ...options,\r\n styles: { ...store.options?.styles, ...options.styles },\r\n});\r\n\r\nconst buildItem = (options: SileoOptions, state: SileoState): SileoItem => ({\r\n id: options.id ?? nextId(),\r\n instanceId: nextInstanceId(),\r\n state,\r\n title: options.title,\r\n description: options.description,\r\n position: options.position ?? store.position,\r\n duration: options.duration,\r\n icon: options.icon,\r\n styles: options.styles,\r\n fill: options.fill,\r\n roundness: options.roundness,\r\n autopilot: options.autopilot,\r\n swipeToDismiss: options.swipeToDismiss,\r\n button: options.button,\r\n groupKey: options.groupKey,\r\n hooks: options.hooks,\r\n createdAt: Date.now(),\r\n});\r\n\r\nconst createToast = (raw: SileoOptions, forcedState?: SileoState) => {\r\n const merged = mergeOptions(raw);\r\n const state = forcedState ?? merged.type ?? \"info\";\r\n const hasCustomId = Boolean(merged.id);\r\n\r\n if (hasCustomId) {\r\n const existing = store.toasts.find(\r\n (item) => item.id === merged.id && !item.exiting,\r\n );\r\n if (existing) {\r\n const replacement = buildItem(merged, state);\r\n replacement.id = existing.id;\r\n store.update((prev) =>\r\n prev.map((item) =>\r\n item.instanceId === existing.instanceId ? replacement : item,\r\n ),\r\n );\r\n clearTimer(existing.instanceId);\r\n scheduleDismiss(replacement);\r\n replacement.hooks?.onShow?.(toLifecycleContext(replacement));\r\n return existing.id;\r\n }\r\n }\r\n\r\n const item = buildItem(merged, state);\r\n store.update((prev) => [...prev, item]);\r\n scheduleDismiss(item);\r\n item.hooks?.onShow?.(toLifecycleContext(item));\r\n return item.id;\r\n};\r\n\r\nconst dismissToast = (id?: string) => {\r\n if (!id) {\r\n for (const item of store.toasts) dismissByInstance(item.instanceId);\r\n return;\r\n }\r\n\r\n const active = store.toasts\r\n .filter((item) => item.id === id && !item.exiting)\r\n .sort((a, b) => b.createdAt - a.createdAt)[0];\r\n\r\n if (!active) return;\r\n dismissByInstance(active.instanceId);\r\n};\r\n\r\nconst clearToasts = (position?: SileoPosition) => {\r\n const removed = position\r\n ? store.toasts.filter((item) => item.position === position)\r\n : [...store.toasts];\r\n\r\n for (const item of removed) clearTimer(item.instanceId);\r\n\r\n store.update((prev) =>\r\n position ? prev.filter((item) => item.position !== position) : [],\r\n );\r\n};\r\n\r\nconst toLifecycleContext = (item: SileoItem): SileoLifecycleContext => ({\r\n id: item.id,\r\n instanceId: item.instanceId,\r\n state: item.state,\r\n});\r\n\r\nconst resolveTheme = (theme: \"light\" | \"dark\" | \"system\" | undefined) => {\r\n if (theme === \"light\" || theme === \"dark\") return theme;\r\n if (typeof window === \"undefined\") return \"light\";\r\n if (typeof window.matchMedia !== \"function\") return \"light\";\r\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches\r\n ? \"dark\"\r\n : \"light\";\r\n};\r\n\r\nconst normalizeAutopilot = (\r\n value: SileoOptions[\"autopilot\"],\r\n): AutopilotConfig | null => {\r\n if (value === false) return null;\r\n if (value === true || value === undefined) {\r\n return {\r\n expand: AUTOPILOT_EXPAND_DELAY,\r\n collapse: AUTOPILOT_COLLAPSE_DELAY,\r\n };\r\n }\r\n\r\n const expand = Number.isFinite(value.expand)\r\n ? Math.max(0, Number(value.expand))\r\n : AUTOPILOT_EXPAND_DELAY;\r\n const collapse = Number.isFinite(value.collapse)\r\n ? Math.max(expand + 200, Number(value.collapse))\r\n : AUTOPILOT_COLLAPSE_DELAY;\r\n\r\n return { expand, collapse };\r\n};\r\n\r\nconst renderStateIcon = (state: SileoState) => {\r\n const common = {\r\n viewBox: \"0 0 16 16\",\r\n width: \"14\",\r\n height: \"14\",\r\n \"aria-hidden\": \"true\",\r\n focusable: \"false\",\r\n } as const;\r\n\r\n if (state === \"success\") {\r\n return h(\"svg\", common, [\r\n h(\"path\", {\r\n d: \"M3.2 8.4L6.5 11.4L12.8 4.9\",\r\n fill: \"none\",\r\n stroke: \"currentColor\",\r\n \"stroke-width\": \"1.8\",\r\n \"stroke-linecap\": \"round\",\r\n \"stroke-linejoin\": \"round\",\r\n }),\r\n ]);\r\n }\r\n if (state === \"error\") {\r\n return h(\"svg\", common, [\r\n h(\"path\", {\r\n d: \"M5 5L11 11M11 5L5 11\",\r\n fill: \"none\",\r\n stroke: \"currentColor\",\r\n \"stroke-width\": \"1.8\",\r\n \"stroke-linecap\": \"round\",\r\n }),\r\n ]);\r\n }\r\n if (state === \"warning\") {\r\n return h(\"svg\", common, [\r\n h(\"path\", {\r\n d: \"M8 3.2L13 12.5H3L8 3.2Z\",\r\n fill: \"none\",\r\n stroke: \"currentColor\",\r\n \"stroke-width\": \"1.4\",\r\n \"stroke-linejoin\": \"round\",\r\n }),\r\n h(\"path\", {\r\n d: \"M8 6.2V9\",\r\n fill: \"none\",\r\n stroke: \"currentColor\",\r\n \"stroke-width\": \"1.5\",\r\n \"stroke-linecap\": \"round\",\r\n }),\r\n h(\"circle\", { cx: \"8\", cy: \"11\", r: \"0.9\", fill: \"currentColor\" }),\r\n ]);\r\n }\r\n if (state === \"action\") {\r\n return h(\"svg\", common, [\r\n h(\"path\", {\r\n d: \"M3.5 8H12.5M9.2 4.8L12.4 8L9.2 11.2\",\r\n fill: \"none\",\r\n stroke: \"currentColor\",\r\n \"stroke-width\": \"1.8\",\r\n \"stroke-linecap\": \"round\",\r\n \"stroke-linejoin\": \"round\",\r\n }),\r\n ]);\r\n }\r\n if (state === \"loading\") {\r\n return h(\"svg\", { ...common, \"data-sileo-icon\": \"spin\" }, [\r\n h(\"circle\", {\r\n cx: \"8\",\r\n cy: \"8\",\r\n r: \"5\",\r\n fill: \"none\",\r\n stroke: \"currentColor\",\r\n \"stroke-width\": \"1.6\",\r\n opacity: \"0.35\",\r\n }),\r\n h(\"path\", {\r\n d: \"M8 3A5 5 0 0 1 13 8\",\r\n fill: \"none\",\r\n stroke: \"currentColor\",\r\n \"stroke-width\": \"1.8\",\r\n \"stroke-linecap\": \"round\",\r\n }),\r\n ]);\r\n }\r\n\r\n return h(\"svg\", common, [\r\n h(\"circle\", {\r\n cx: \"8\",\r\n cy: \"8\",\r\n r: \"5.2\",\r\n fill: \"none\",\r\n stroke: \"currentColor\",\r\n \"stroke-width\": \"1.6\",\r\n }),\r\n h(\"path\", {\r\n d: \"M8 6.8V10.2\",\r\n fill: \"none\",\r\n stroke: \"currentColor\",\r\n \"stroke-width\": \"1.6\",\r\n \"stroke-linecap\": \"round\",\r\n }),\r\n h(\"circle\", { cx: \"8\", cy: \"4.9\", r: \"0.9\", fill: \"currentColor\" }),\r\n ]);\r\n};\r\n\r\nconst getOffsetStyle = (\r\n position: SileoPosition,\r\n offset?: SileoOffsetValue | SileoOffsetConfig,\r\n): CSSProperties | undefined => {\r\n if (offset === undefined) return undefined;\r\n\r\n const value =\r\n typeof offset === \"object\"\r\n ? offset\r\n : { top: offset, right: offset, bottom: offset, left: offset };\r\n\r\n const style: CSSProperties = {};\r\n const px = (v: SileoOffsetValue) => (typeof v === \"number\" ? `${v}px` : v);\r\n\r\n if (position.startsWith(\"top\") && value.top !== undefined) style.top = px(value.top);\r\n if (position.startsWith(\"bottom\") && value.bottom !== undefined) {\r\n style.bottom = px(value.bottom);\r\n }\r\n if (position.endsWith(\"left\") && value.left !== undefined) style.left = px(value.left);\r\n if (position.endsWith(\"right\") && value.right !== undefined) {\r\n style.right = px(value.right);\r\n }\r\n\r\n return style;\r\n};\r\n\r\nexport interface SileoPromiseOptions<T = unknown> {\r\n loading: SileoOptions;\r\n success: SileoOptions | ((data: T) => SileoOptions);\r\n error: SileoOptions | ((error: unknown) => SileoOptions);\r\n action?: SileoOptions | ((data: T) => SileoOptions);\r\n position?: SileoPosition;\r\n}\r\n\r\nexport interface SileoToasterProps {\r\n position?: SileoPosition;\r\n offset?: SileoOffsetValue | SileoOffsetConfig;\r\n options?: Partial<SileoOptions>;\r\n theme?: \"light\" | \"dark\" | \"system\";\r\n container?: string | HTMLElement;\r\n grouping?: boolean;\r\n groupThreshold?: number;\r\n ariaLive?: \"off\" | \"polite\" | \"assertive\";\r\n}\r\n\r\nexport const sileo = {\r\n show: (opts: SileoOptions) => createToast(opts, opts.type ?? \"info\"),\r\n success: (opts: SileoOptions) => createToast(opts, \"success\"),\r\n error: (opts: SileoOptions) => createToast(opts, \"error\"),\r\n warning: (opts: SileoOptions) => createToast(opts, \"warning\"),\r\n info: (opts: SileoOptions) => createToast(opts, \"info\"),\r\n action: (opts: SileoOptions) => createToast(opts, \"action\"),\r\n loading: (opts: SileoOptions) =>\r\n createToast({ ...opts, duration: opts.duration ?? null }, \"loading\"),\r\n\r\n promise: async <T,>(\r\n promise: Promise<T> | (() => Promise<T>),\r\n opts: SileoPromiseOptions<T>,\r\n ): Promise<T> => {\r\n const id = createToast(\r\n { ...opts.loading, duration: null, position: opts.position },\r\n \"loading\",\r\n );\r\n try {\r\n const data = await (typeof promise === \"function\" ? promise() : promise);\r\n const next = opts.action\r\n ? typeof opts.action === \"function\"\r\n ? opts.action(data)\r\n : opts.action\r\n : typeof opts.success === \"function\"\r\n ? opts.success(data)\r\n : opts.success;\r\n createToast({ ...next, id }, opts.action ? \"action\" : \"success\");\r\n return data;\r\n } catch (error) {\r\n const next =\r\n typeof opts.error === \"function\" ? opts.error(error) : opts.error;\r\n createToast({ ...next, id }, \"error\");\r\n throw error;\r\n }\r\n },\r\n\r\n configure: (options: Partial<SileoOptions> & { position?: SileoPosition }) => {\r\n if (options.position) {\r\n store.position = options.position;\r\n }\r\n store.options = {\r\n ...store.options,\r\n ...options,\r\n styles: { ...store.options?.styles, ...options.styles },\r\n };\r\n },\r\n\r\n dismiss: dismissToast,\r\n clear: clearToasts,\r\n};\r\n\r\nexport const Toaster = defineComponent({\r\n name: \"SileoToaster\",\r\n props: {\r\n position: String as PropType<SileoPosition | undefined>,\r\n offset: [Number, String, Object] as PropType<\r\n SileoOffsetValue | SileoOffsetConfig\r\n >,\r\n options: Object as PropType<Partial<SileoOptions>>,\r\n theme: String as PropType<\"light\" | \"dark\" | \"system\">,\r\n container: [String, Object] as PropType<string | HTMLElement>,\r\n grouping: { type: Boolean, default: false },\r\n groupThreshold: { type: Number, default: GROUP_THRESHOLD },\r\n ariaLive: {\r\n type: String as PropType<\"off\" | \"polite\" | \"assertive\">,\r\n default: \"polite\",\r\n },\r\n },\r\n setup(props, { slots }) {\r\n const toasts = ref<SileoItem[]>(store.toasts);\r\n const expandedGroups = ref<Record<string, boolean>>({});\r\n const expandedToasts = ref<Record<string, boolean>>({});\r\n const autopilotTimers = new Map<string, ReturnType<typeof setTimeout>[]>();\r\n const activeToastInstanceId = ref<string | undefined>(undefined);\r\n const contentHeights = ref<Record<string, number>>({});\r\n const contentRefs = new Map<string, HTMLElement>();\r\n const contentObservers = new Map<string, ResizeObserver>();\r\n\r\n // Pill inner measurement (drives SVG pill width)\r\n const readyToasts = ref<Record<string, boolean>>({});\r\n const pillWidths = ref<Record<string, number>>({});\r\n const pillInnerRefs = new Map<string, HTMLElement>();\r\n const pillInnerObservers = new Map<string, ResizeObserver>();\r\n\r\n const measureContentHeight = (instanceId: string) => {\r\n const el = contentRefs.get(instanceId);\r\n if (!el) return;\r\n const measured = Math.max(0, el.scrollHeight);\r\n if (contentHeights.value[instanceId] !== measured) {\r\n contentHeights.value[instanceId] = measured;\r\n }\r\n };\r\n\r\n const setContentRef = (instanceId: string, el: HTMLElement | null) => {\r\n if (el) {\r\n contentRefs.set(instanceId, el);\r\n measureContentHeight(instanceId);\r\n if (typeof ResizeObserver === \"function\") {\r\n if (!contentObservers.has(instanceId)) {\r\n const observer = new ResizeObserver(() => {\r\n measureContentHeight(instanceId);\r\n });\r\n contentObservers.set(instanceId, observer);\r\n }\r\n contentObservers.get(instanceId)?.observe(el);\r\n }\r\n return;\r\n }\r\n\r\n const previous = contentRefs.get(instanceId);\r\n const observer = contentObservers.get(instanceId);\r\n if (previous && observer) observer.unobserve(previous);\r\n contentRefs.delete(instanceId);\r\n };\r\n\r\n const clearContentObserver = (instanceId: string) => {\r\n const observer = contentObservers.get(instanceId);\r\n if (observer) {\r\n observer.disconnect();\r\n contentObservers.delete(instanceId);\r\n }\r\n contentRefs.delete(instanceId);\r\n delete contentHeights.value[instanceId];\r\n };\r\n\r\n const measurePillInner = (instanceId: string) => {\r\n const el = pillInnerRefs.get(instanceId);\r\n if (!el) return;\r\n const w = el.scrollWidth;\r\n if (pillWidths.value[instanceId] !== w) {\r\n pillWidths.value[instanceId] = w;\r\n }\r\n };\r\n\r\n const setPillInnerRef = (instanceId: string, el: HTMLElement | null) => {\r\n if (el) {\r\n pillInnerRefs.set(instanceId, el);\r\n measurePillInner(instanceId);\r\n if (typeof ResizeObserver === 'function' && !pillInnerObservers.has(instanceId)) {\r\n const observer = new ResizeObserver(() => measurePillInner(instanceId));\r\n pillInnerObservers.set(instanceId, observer);\r\n observer.observe(el);\r\n }\r\n return;\r\n }\r\n const prev = pillInnerRefs.get(instanceId);\r\n const obs = pillInnerObservers.get(instanceId);\r\n if (prev && obs) obs.unobserve(prev);\r\n pillInnerRefs.delete(instanceId);\r\n };\r\n\r\n const clearPillInnerObserver = (instanceId: string) => {\r\n const obs = pillInnerObservers.get(instanceId);\r\n if (obs) { obs.disconnect(); pillInnerObservers.delete(instanceId); }\r\n pillInnerRefs.delete(instanceId);\r\n delete pillWidths.value[instanceId];\r\n };\r\n\r\n const scheduleReady = (instanceId: string) => {\r\n globalThis.requestAnimationFrame(() => {\r\n if (pillInnerRefs.has(instanceId) || toasts.value.some(t => t.instanceId === instanceId)) {\r\n readyToasts.value[instanceId] = true;\r\n }\r\n });\r\n };\r\n\r\n const setExpanded = (instanceId: string, expanded: boolean) => {\r\n const previous = expandedToasts.value[instanceId];\r\n if (previous === expanded) return;\r\n\r\n expandedToasts.value[instanceId] = expanded;\r\n if (previous === undefined) return;\r\n\r\n const item = toasts.value.find((toast) => toast.instanceId === instanceId);\r\n if (!item) return;\r\n\r\n if (expanded) {\r\n item.hooks?.onExpand?.(toLifecycleContext(item));\r\n } else {\r\n item.hooks?.onCollapse?.(toLifecycleContext(item));\r\n }\r\n };\r\n\r\n const clearAutopilotTimers = (instanceId: string) => {\r\n const timers = autopilotTimers.get(instanceId);\r\n if (!timers) return;\r\n for (const timer of timers) clearTimeout(timer);\r\n autopilotTimers.delete(instanceId);\r\n };\r\n\r\n const scheduleAutopilot = (item: SileoItem) => {\r\n clearAutopilotTimers(item.instanceId);\r\n const config = normalizeAutopilot(item.autopilot);\r\n if (!item.description || !config) {\r\n setExpanded(item.instanceId, false);\r\n return;\r\n }\r\n\r\n if (\r\n activeToastInstanceId.value !== undefined &&\r\n activeToastInstanceId.value !== item.instanceId\r\n ) {\r\n setExpanded(item.instanceId, false);\r\n return;\r\n }\r\n\r\n setExpanded(item.instanceId, false);\r\n const timers: ReturnType<typeof setTimeout>[] = [];\r\n\r\n timers.push(\r\n globalThis.setTimeout(() => {\r\n setExpanded(item.instanceId, true);\r\n }, config.expand),\r\n );\r\n\r\n const duration = item.duration ?? DEFAULT_TOAST_DURATION;\r\n if (item.duration !== null) {\r\n const maxCollapse = Math.max(\r\n config.expand + 350,\r\n duration - EXIT_DURATION - 150,\r\n );\r\n const collapseAt = Math.min(config.collapse, maxCollapse);\r\n if (collapseAt > config.expand) {\r\n timers.push(\r\n globalThis.setTimeout(() => {\r\n setExpanded(item.instanceId, false);\r\n }, collapseAt),\r\n );\r\n }\r\n } else if (config.collapse > config.expand) {\r\n timers.push(\r\n globalThis.setTimeout(() => {\r\n setExpanded(item.instanceId, false);\r\n }, config.collapse),\r\n );\r\n }\r\n\r\n autopilotTimers.set(item.instanceId, timers);\r\n };\r\n\r\n const listener: Listener = (next) => {\r\n toasts.value = next;\r\n };\r\n\r\n onMounted(() => {\r\n store.listeners.add(listener);\r\n if (props.position) store.position = props.position;\r\n if (props.options) {\r\n store.options = {\r\n ...store.options,\r\n ...props.options,\r\n styles: { ...store.options?.styles, ...props.options.styles },\r\n };\r\n }\r\n });\r\n\r\n onBeforeUnmount(() => {\r\n store.listeners.delete(listener);\r\n for (const instanceId of autopilotTimers.keys()) {\r\n clearAutopilotTimers(instanceId);\r\n }\r\n for (const instanceId of contentObservers.keys()) {\r\n clearContentObserver(instanceId);\r\n }\r\n for (const instanceId of pillInnerObservers.keys()) {\r\n clearPillInnerObserver(instanceId);\r\n }\r\n });\r\n\r\n watch(\r\n () => props.position,\r\n (value) => {\r\n if (value) store.position = value;\r\n },\r\n );\r\n\r\n watch(\r\n () => props.options,\r\n (value) => {\r\n if (!value) return;\r\n store.options = {\r\n ...store.options,\r\n ...value,\r\n styles: { ...store.options?.styles, ...value.styles },\r\n };\r\n },\r\n { deep: true },\r\n );\r\n\r\n watch(\r\n () => toasts.value,\r\n (items) => {\r\n const active = new Set(items.map((item) => item.instanceId));\r\n const latest = [...items]\r\n .filter((item) => !item.exiting)\r\n .sort((a, b) => b.createdAt - a.createdAt)[0];\r\n\r\n activeToastInstanceId.value = latest?.instanceId;\r\n\r\n for (const item of items) {\r\n if (expandedToasts.value[item.instanceId] === undefined) {\r\n setExpanded(item.instanceId, false);\r\n }\r\n }\r\n\r\n for (const instanceId of Object.keys(expandedToasts.value)) {\r\n if (!active.has(instanceId)) {\r\n clearAutopilotTimers(instanceId);\r\n delete expandedToasts.value[instanceId];\r\n clearContentObserver(instanceId);\r\n clearPillInnerObserver(instanceId);\r\n delete readyToasts.value[instanceId];\r\n }\r\n }\r\n },\r\n { immediate: true },\r\n );\r\n\r\n watch(\r\n () => activeToastInstanceId.value,\r\n (activeInstanceId) => {\r\n for (const instanceId of Object.keys(expandedToasts.value)) {\r\n if (instanceId !== activeInstanceId) {\r\n clearAutopilotTimers(instanceId);\r\n setExpanded(instanceId, false);\r\n }\r\n }\r\n\r\n if (!activeInstanceId) return;\r\n const item = toasts.value.find((toast) => toast.instanceId === activeInstanceId);\r\n if (item) {\r\n scheduleAutopilot(item);\r\n }\r\n },\r\n );\r\n\r\n const groupedByPosition = computed(() => {\r\n const map = new Map<SileoPosition, SileoItem[]>();\r\n for (const item of toasts.value) {\r\n const pos = item.position ?? store.position;\r\n const list = map.get(pos);\r\n if (list) {\r\n list.push(item);\r\n } else {\r\n map.set(pos, [item]);\r\n }\r\n }\r\n return map;\r\n });\r\n\r\n const renderToast = (item: SileoItem) => {\r\n const r = Math.max(0, item.roundness ?? DEFAULT_ROUNDNESS);\r\n const isExpanded = Boolean(expandedToasts.value[item.instanceId]);\r\n const isReady = Boolean(readyToasts.value[item.instanceId]);\r\n const swipeEnabled = item.swipeToDismiss ?? true;\r\n const pos = item.position ?? store.position;\r\n const align: 'left' | 'center' | 'right' =\r\n pos.endsWith('left') ? 'left'\r\n : pos.endsWith('right') ? 'right'\r\n : 'center';\r\n const expandDir: 'top' | 'bottom' = pos.startsWith('top') ? 'bottom' : 'top';\r\n\r\n const hasDesc = Boolean(item.description || item.button);\r\n const isLoading = item.state === 'loading';\r\n const isOpen = hasDesc && isExpanded && !isLoading;\r\n\r\n const blur = r * BLUR_RATIO;\r\n const rawPillW = pillWidths.value[item.instanceId] ?? 0;\r\n const resolvedPillWidth = Math.max(rawPillW + PILL_PADDING, TOAST_HEIGHT);\r\n const pillH = TOAST_HEIGHT + blur * 3;\r\n\r\n const pillX = align === 'right' ? TOAST_WIDTH - resolvedPillWidth\r\n : align === 'center' ? (TOAST_WIDTH - resolvedPillWidth) / 2\r\n : 0;\r\n\r\n const contentH = contentHeights.value[item.instanceId] ?? 0;\r\n const minExpanded = TOAST_HEIGHT * MIN_EXPAND_RATIO;\r\n const expanded = hasDesc ? Math.max(minExpanded, TOAST_HEIGHT + contentH) : minExpanded;\r\n const expandedContent = Math.max(0, expanded - TOAST_HEIGHT);\r\n const svgH = Math.max(expanded, TOAST_HEIGHT);\r\n\r\n const resolvedTheme = resolveTheme(props.theme);\r\n const fillColor = item.fill ?? (resolvedTheme === 'dark' ? '#f2f2f2' : '#1a1a1a');\r\n const filterId = `sileo-gooey-${item.instanceId.replace(/[^a-z0-9]/gi, '-')}`;\r\n\r\n const rootStyle: CSSProperties & Record<string, string> = {\r\n '--_h': `${isOpen ? expanded : TOAST_HEIGHT}px`,\r\n '--_pw': `${resolvedPillWidth}px`,\r\n '--_px': `${pillX}px`,\r\n '--_ht': `translateY(${isOpen ? (expandDir === 'bottom' ? 3 : -3) : 0}px) scale(${isOpen ? 0.9 : 1})`,\r\n '--_co': isOpen ? '1' : '0',\r\n };\r\n\r\n const swipeState = {\r\n startY: 0,\r\n pointerId: -1,\r\n dragging: false,\r\n };\r\n\r\n const resetSwipe = (el: HTMLElement) => {\r\n el.style.transform = \"\";\r\n el.style.opacity = \"\";\r\n el.style.transition = \"\";\r\n };\r\n\r\n return h(\r\n \"button\",\r\n {\r\n key: item.instanceId,\r\n type: \"button\",\r\n \"data-sileo-toast\": \"true\",\r\n \"data-state\": item.state,\r\n \"data-exiting\": item.exiting ? \"true\" : \"false\",\r\n \"data-expanded\": isExpanded ? \"true\" : \"false\",\r\n \"data-ready\": isReady ? \"true\" : \"false\",\r\n class: item.styles?.toast,\r\n style: rootStyle,\r\n onMouseenter: () => {\r\n activeToastInstanceId.value = item.instanceId;\r\n setExpanded(item.instanceId, true);\r\n },\r\n onMouseleave: () => {\r\n const autopilot = normalizeAutopilot(item.autopilot);\r\n const latest = [...toasts.value]\r\n .filter((toast) => !toast.exiting)\r\n .sort((a, b) => b.createdAt - a.createdAt)[0];\r\n activeToastInstanceId.value = latest?.instanceId;\r\n if (autopilot !== null) setExpanded(item.instanceId, false);\r\n },\r\n onPointerdown: (e: PointerEvent) => {\r\n if (!swipeEnabled || item.state === \"loading\") return;\r\n const target = e.target as HTMLElement;\r\n if (target.closest(\"[data-sileo-button='true'], .sileo-dismiss\")) {\r\n return;\r\n }\r\n const el = e.currentTarget as HTMLElement;\r\n swipeState.startY = e.clientY;\r\n swipeState.pointerId = e.pointerId;\r\n swipeState.dragging = true;\r\n el.setPointerCapture(e.pointerId);\r\n },\r\n onPointermove: (e: PointerEvent) => {\r\n if (!swipeEnabled || !swipeState.dragging) return;\r\n if (e.pointerId !== swipeState.pointerId) return;\r\n const el = e.currentTarget as HTMLElement;\r\n const deltaY = e.clientY - swipeState.startY;\r\n const clamped = Math.max(-22, Math.min(22, deltaY));\r\n el.style.transform = `translateY(${clamped}px)`;\r\n const alpha = 1 - Math.min(0.35, Math.abs(clamped) / 60);\r\n el.style.opacity = String(alpha);\r\n },\r\n onPointerup: (e: PointerEvent) => {\r\n if (!swipeEnabled || !swipeState.dragging) return;\r\n if (e.pointerId !== swipeState.pointerId) return;\r\n const el = e.currentTarget as HTMLElement;\r\n swipeState.dragging = false;\r\n const deltaY = e.clientY - swipeState.startY;\r\n if (el.hasPointerCapture(e.pointerId)) {\r\n el.releasePointerCapture(e.pointerId);\r\n }\r\n if (Math.abs(deltaY) > 34) {\r\n dismissByInstance(item.instanceId);\r\n return;\r\n }\r\n el.style.transition = \"transform 150ms ease, opacity 150ms ease\";\r\n resetSwipe(el);\r\n },\r\n onPointercancel: (e: PointerEvent) => {\r\n if (!swipeEnabled || !swipeState.dragging) return;\r\n if (e.pointerId !== swipeState.pointerId) return;\r\n const el = e.currentTarget as HTMLElement;\r\n swipeState.dragging = false;\r\n if (el.hasPointerCapture(e.pointerId)) {\r\n el.releasePointerCapture(e.pointerId);\r\n }\r\n el.style.transition = \"transform 150ms ease, opacity 150ms ease\";\r\n resetSwipe(el);\r\n },\r\n },\r\n [\r\n // SVG gooey canvas\r\n h(\"div\", {\r\n \"data-sileo-canvas\": \"true\",\r\n \"data-edge\": expandDir,\r\n style: { filter: `url(#${filterId})` } as CSSProperties,\r\n }, [\r\n h(\"svg\", {\r\n \"data-sileo-svg\": \"true\",\r\n width: String(TOAST_WIDTH),\r\n height: String(svgH),\r\n viewBox: `0 0 ${TOAST_WIDTH} ${svgH}`,\r\n xmlns: \"http://www.w3.org/2000/svg\",\r\n \"aria-hidden\": \"true\",\r\n focusable: \"false\",\r\n }, [\r\n h(\"defs\", {}, [\r\n h(\"filter\", {\r\n id: filterId,\r\n x: \"-20%\",\r\n y: \"-20%\",\r\n width: \"140%\",\r\n height: \"140%\",\r\n \"color-interpolation-filters\": \"sRGB\",\r\n }, [\r\n h(\"feGaussianBlur\", {\r\n in: \"SourceGraphic\",\r\n stdDeviation: String(blur),\r\n result: \"blur\",\r\n }),\r\n h(\"feColorMatrix\", {\r\n in: \"blur\",\r\n mode: \"matrix\",\r\n values: \"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -10\",\r\n result: \"goo\",\r\n }),\r\n h(\"feComposite\", {\r\n in: \"SourceGraphic\",\r\n in2: \"goo\",\r\n operator: \"atop\",\r\n }),\r\n ]),\r\n ]),\r\n h(\"rect\", {\r\n \"data-sileo-pill\": \"true\",\r\n rx: String(r),\r\n ry: String(r),\r\n fill: fillColor,\r\n style: {\r\n x: `${pillX}px`,\r\n width: `${resolvedPillWidth}px`,\r\n height: `${isOpen ? pillH : TOAST_HEIGHT}px`,\r\n } as CSSProperties,\r\n }),\r\n h(\"rect\", {\r\n \"data-sileo-body\": \"true\",\r\n width: String(TOAST_WIDTH),\r\n rx: String(r),\r\n ry: String(r),\r\n fill: fillColor,\r\n style: {\r\n y: `${TOAST_HEIGHT}px`,\r\n height: `${isOpen ? expandedContent : 0}px`,\r\n opacity: isOpen ? \"1\" : \"0\",\r\n } as CSSProperties,\r\n }),\r\n ]),\r\n ]),\r\n\r\n // Header\r\n h(\"div\", {\r\n \"data-sileo-header\": \"true\",\r\n \"data-edge\": expandDir,\r\n }, [\r\n h(\"div\", { \"data-sileo-header-stack\": \"true\" }, [\r\n h(\"div\", {\r\n \"data-sileo-header-inner\": \"true\",\r\n ref: (el: Element | ComponentPublicInstance | null) => {\r\n setPillInnerRef(item.instanceId, el as HTMLElement | null);\r\n if (el) scheduleReady(item.instanceId);\r\n },\r\n }, [\r\n h(\"div\", {\r\n \"data-sileo-badge\": \"true\",\r\n \"data-state\": item.state,\r\n class: item.styles?.badge,\r\n }, item.icon ?? renderStateIcon(item.state)),\r\n h(\"span\", {\r\n \"data-sileo-title\": \"true\",\r\n \"data-state\": item.state,\r\n class: item.styles?.title,\r\n }, item.title),\r\n h(\"button\", {\r\n type: \"button\",\r\n class: \"sileo-dismiss\",\r\n onClick: (e: MouseEvent) => {\r\n e.stopPropagation();\r\n dismissByInstance(item.instanceId);\r\n },\r\n \"aria-label\": \"Dismiss toast\",\r\n }, \"×\"),\r\n ]),\r\n ]),\r\n ]),\r\n\r\n // Content\r\n hasDesc\r\n ? h(\"div\", {\r\n \"data-sileo-content\": \"true\",\r\n \"data-edge\": expandDir,\r\n \"data-visible\": isOpen ? \"true\" : \"false\",\r\n }, [\r\n h(\"div\", {\r\n \"data-sileo-description\": \"true\",\r\n class: item.styles?.description,\r\n ref: (el: Element | ComponentPublicInstance | null) => {\r\n setContentRef(item.instanceId, el as HTMLElement | null);\r\n },\r\n }, [\r\n item.description\r\n ? h(\"span\", {}, item.description)\r\n : null,\r\n item.button\r\n ? h(\"button\", {\r\n type: \"button\",\r\n \"data-sileo-button\": \"true\",\r\n \"data-state\": item.state,\r\n class: item.styles?.button,\r\n onClick: (e: MouseEvent) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n item.button?.onClick?.();\r\n },\r\n }, item.button.title)\r\n : null,\r\n ]),\r\n ])\r\n : null,\r\n ],\r\n );\r\n };\r\n\r\n const renderPosition = (position: SileoPosition, items: SileoItem[]) => {\r\n const buckets = new Map<string, SileoItem[]>();\r\n for (const item of items) {\r\n const key = item.groupKey ?? \"__default__\";\r\n const list = buckets.get(key);\r\n if (list) {\r\n list.push(item);\r\n } else {\r\n buckets.set(key, [item]);\r\n }\r\n }\r\n\r\n const children: ReturnType<typeof h>[] = [];\r\n for (const [bucketKey, bucketItems] of buckets) {\r\n const visible = bucketItems.filter((item) => !item.exiting);\r\n const expandKey = `${position}:${bucketKey}`;\r\n const shouldGroup =\r\n props.grouping &&\r\n visible.length > props.groupThreshold &&\r\n !expandedGroups.value[expandKey];\r\n\r\n if (shouldGroup) {\r\n const label =\r\n bucketKey === \"__default__\"\r\n ? `${visible.length} notifications`\r\n : `${visible.length} ${bucketKey} notifications`;\r\n children.push(\r\n h(\r\n \"button\",\r\n {\r\n key: expandKey,\r\n type: \"button\",\r\n \"data-sileo-group\": \"true\",\r\n class: \"sileo-group\",\r\n onMouseenter: () => {\r\n expandedGroups.value[expandKey] = true;\r\n },\r\n onClick: () => {\r\n expandedGroups.value[expandKey] = true;\r\n },\r\n },\r\n label,\r\n ),\r\n );\r\n continue;\r\n }\r\n\r\n for (const item of bucketItems) {\r\n children.push(renderToast(item));\r\n }\r\n }\r\n\r\n return h(\r\n \"section\",\r\n {\r\n key: position,\r\n \"data-sileo-viewport\": \"true\",\r\n \"data-position\": position,\r\n \"data-theme\": resolveTheme(props.theme),\r\n \"aria-live\": props.ariaLive,\r\n \"aria-atomic\": \"true\",\r\n role: \"status\",\r\n style: getOffsetStyle(position, props.offset),\r\n },\r\n children,\r\n );\r\n };\r\n\r\n return () => {\r\n const viewports = Array.from(groupedByPosition.value.entries()).map(\r\n ([position, items]) => renderPosition(position, items),\r\n );\r\n\r\n const portalTarget = props.container ?? \"body\";\r\n\r\n return h(\"div\", { class: \"sileo-root\" }, [\r\n slots.default?.(),\r\n h(Teleport, { to: portalTarget }, viewports),\r\n ]);\r\n };\r\n },\r\n});\r\n"],"names":[],"mappings":";;;AACO,KAAA,UAAA;AACA,UAAA,WAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACO,UAAA,WAAA;AACP;AACA;AACA;AACO,UAAA,cAAA;AACP;AACA;AACA;AACO,UAAA,qBAAA;AACP;AACA;AACA,WAAA,UAAA;AACA;AACO,UAAA,mBAAA;AACP,mBAAA,qBAAA;AACA,qBAAA,qBAAA;AACA,uBAAA,qBAAA;AACA,sBAAA,qBAAA;AACA;AACO,cAAA,eAAA;AACA,KAAA,aAAA,WAAA,eAAA;AACA,UAAA,YAAA;AACP;AACA;AACA,kBAAA,UAAA;AACA,WAAA,UAAA;AACA,eAAA,aAAA;AACA;AACA,WAAA,UAAA;AACA,aAAA,WAAA;AACA;AACA;AACA,0BAAA,cAAA;AACA;AACA,aAAA,WAAA;AACA;AACA,YAAA,mBAAA;AACA;AACO,KAAA,gBAAA;AACA,KAAA,iBAAA,GAAA,OAAA,CAAA,MAAA,sCAAA,gBAAA;;AC9CA,UAAA,mBAAA;AACP,aAAA,YAAA;AACA,aAAA,YAAA,iBAAA,YAAA;AACA,WAAA,YAAA,wBAAA,YAAA;AACA,aAAA,YAAA,iBAAA,YAAA;AACA,eAAA,aAAA;AACA;AAWO,cAAA,KAAA;AACP,iBAAA,YAAA;AACA,oBAAA,YAAA;AACA,kBAAA,YAAA;AACA,oBAAA,YAAA;AACA,iBAAA,YAAA;AACA,mBAAA,YAAA;AACA,oBAAA,YAAA;AACA,0BAAA,OAAA,aAAA,OAAA,YAAA,mBAAA,QAAA,OAAA;AACA,yBAAA,OAAA,CAAA,YAAA;AACA,mBAAA,aAAA;AACA;AACA;AACA,uBAAA,aAAA;AACA;AACO,cAAA,OAAA,EAAuB,GAAa,iBAAiB,GAAa,CAAA,gBAAA;AACzE,cAAA,QAAA,CAAA,aAAA;AACA,YAAA,QAAA,CAAA,gBAAA,GAAA,iBAAA;AACA,aAAA,QAAA,CAAA,OAAA,CAAA,YAAA;AACA,WAAA,QAAA;AACA,eAAA,QAAA,UAAA,WAAA;AACA;AACA,cAAA,kBAAA;AACA;AACA;AACA;AACA,cAAA,iBAAA;AACA;AACA;AACA;AACA,cAAA,QAAA;AACA;AACA;AACA,UAAU,GAAa,CAAA,KAAA,CAAO,GAAa,CAAA,YAAA,EAAe,GAAa,CAAA,eAAA;AACvE;AACA,gBAAgB,GAAa,CAAA,qBAAA,EAAwB,GAAa,CAAA,qBAAA,cAAoC,GAAa,uBAAuB,GAAa,CAAA,gBAAA;AACvJ,cAAA,QAAA,CAAA,aAAA;AACA,YAAA,QAAA,CAAA,gBAAA,GAAA,iBAAA;AACA,aAAA,QAAA,CAAA,OAAA,CAAA,YAAA;AACA,WAAA,QAAA;AACA,eAAA,QAAA,UAAA,WAAA;AACA;AACA,cAAA,kBAAA;AACA;AACA;AACA;AACA,cAAA,iBAAA;AACA;AACA;AACA;AACA,cAAA,QAAA;AACA;AACA;AACA,MAAA,QAAA;AACA;AACA;AACA;AACA,uBAAuB,GAAa,CAAA,uBAAA;;;;"}
package/dist/index.d.ts CHANGED
@@ -17,6 +17,17 @@ interface SileoAutopilot {
17
17
  expand?: number;
18
18
  collapse?: number;
19
19
  }
20
+ interface SileoLifecycleContext {
21
+ id: string;
22
+ instanceId: string;
23
+ state: SileoState;
24
+ }
25
+ interface SileoLifecycleHooks {
26
+ onShow?: (ctx: SileoLifecycleContext) => void;
27
+ onExpand?: (ctx: SileoLifecycleContext) => void;
28
+ onCollapse?: (ctx: SileoLifecycleContext) => void;
29
+ onDismiss?: (ctx: SileoLifecycleContext) => void;
30
+ }
20
31
  declare const SILEO_POSITIONS: readonly ["top-left", "top-center", "top-right", "bottom-left", "bottom-center", "bottom-right"];
21
32
  type SileoPosition = (typeof SILEO_POSITIONS)[number];
22
33
  interface SileoOptions {
@@ -31,8 +42,10 @@ interface SileoOptions {
31
42
  fill?: string;
32
43
  roundness?: number;
33
44
  autopilot?: boolean | SileoAutopilot;
45
+ swipeToDismiss?: boolean;
34
46
  button?: SileoButton;
35
47
  groupKey?: string;
48
+ hooks?: SileoLifecycleHooks;
36
49
  }
37
50
  type SileoOffsetValue = number | string;
38
51
  type SileoOffsetConfig = Partial<Record<"top" | "right" | "bottom" | "left", SileoOffsetValue>>;
@@ -73,6 +86,10 @@ declare const Toaster: vue.DefineComponent<vue.ExtractPropTypes<{
73
86
  type: NumberConstructor;
74
87
  default: number;
75
88
  };
89
+ ariaLive: {
90
+ type: PropType<"off" | "polite" | "assertive">;
91
+ default: string;
92
+ };
76
93
  }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
77
94
  [key: string]: any;
78
95
  }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
@@ -89,11 +106,16 @@ declare const Toaster: vue.DefineComponent<vue.ExtractPropTypes<{
89
106
  type: NumberConstructor;
90
107
  default: number;
91
108
  };
109
+ ariaLive: {
110
+ type: PropType<"off" | "polite" | "assertive">;
111
+ default: string;
112
+ };
92
113
  }>> & Readonly<{}>, {
93
114
  grouping: boolean;
94
115
  groupThreshold: number;
116
+ ariaLive: "off" | "polite" | "assertive";
95
117
  }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
96
118
 
97
119
  export { Toaster, sileo };
98
- export type { SileoAutopilot, SileoButton, SileoOffsetConfig, SileoOffsetValue, SileoOptions, SileoPosition, SileoState, SileoStyles };
120
+ export type { SileoAutopilot, SileoButton, SileoLifecycleContext, SileoLifecycleHooks, SileoOffsetConfig, SileoOffsetValue, SileoOptions, SileoPosition, SileoState, SileoStyles };
99
121
  //# sourceMappingURL=index.d.ts.map