@pindoba/astro-toast 0.0.0-alpha-20260426220647

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/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright 2025 Eduardo Lopes
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
8
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
9
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
10
+ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
11
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
12
+ DEALINGS IN THE SOFTWARE.
package/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ // Do not write code directly here, instead use the `src` folder!
2
+ // Then, use this file to export everything you want your user to access.
3
+
4
+ import Toaster from "./src/Toaster.astro";
5
+
6
+ export default Toaster;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@pindoba/astro-toast",
3
+ "version": "0.0.0-alpha-20260426220647",
4
+ "sideEffects": false,
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./index.ts",
9
+ "./runtime": "./src/runtime.ts",
10
+ "./demos": "./src/demos/index.ts",
11
+ "./demos/default": "./src/demos/default.astro",
12
+ "./demos/emphasis": "./src/demos/emphasis.astro",
13
+ "./demos/promise": "./src/demos/promise.astro",
14
+ "./demos/positions": "./src/demos/positions.astro",
15
+ "./demos/stacked": "./src/demos/stacked.astro",
16
+ "./demos/dedupe": "./src/demos/dedupe.astro",
17
+ "./demos/actions": "./src/demos/actions.astro",
18
+ "./demos/history": "./src/demos/history.astro"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "index.ts"
23
+ ],
24
+ "keywords": [
25
+ "astro-component"
26
+ ],
27
+ "devDependencies": {
28
+ "@astrojs/check": "^0.9.8",
29
+ "astro": "^6.1.9",
30
+ "typescript": "^6.0.3"
31
+ },
32
+ "peerDependencies": {
33
+ "astro": "^4.0.0"
34
+ },
35
+ "dependencies": {
36
+ "@iconify-json/lucide": "^1.2.103",
37
+ "astro-icon": "^1.1.5",
38
+ "@pindoba/astro-button": "0.0.0-alpha-20260426220647",
39
+ "@pindoba/astro-dialog": "0.0.0-alpha-20260426220647",
40
+ "@pindoba/astro-group": "0.0.0-alpha-20260426220647",
41
+ "@pindoba/astro-radio": "0.0.0-alpha-20260426220647",
42
+ "@pindoba/core-toast": "0.0.0-alpha-20260426220647",
43
+ "@pindoba/styles-toast": "0.0.0-alpha-20260426220647",
44
+ "@pindoba/styled-system": "0.0.0-alpha-20260426220647",
45
+ "@pindoba/astro-alert": "0.0.0-alpha-20260426220647",
46
+ "@pindoba/astro-badge": "0.0.0-alpha-20260426220647"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "scripts": {
52
+ "check": "astro check",
53
+ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
54
+ }
55
+ }
@@ -0,0 +1,435 @@
1
+ ---
2
+ import Alert from "@pindoba/astro-alert";
3
+ import Badge from "@pindoba/astro-badge";
4
+ import Button from "@pindoba/astro-button";
5
+ import Dialog from "@pindoba/astro-dialog";
6
+ import DialogManagerScript from "@pindoba/astro-dialog/script";
7
+ import Group from "@pindoba/astro-group";
8
+ import Radio from "@pindoba/astro-radio";
9
+ import { Icon } from "astro-icon/components";
10
+ import { connectToaster } from "@pindoba/core-toast";
11
+ import type { ToastPosition, ToasterConfig } from "@pindoba/core-toast";
12
+ import type { AlertFeedback } from "@pindoba/astro-alert";
13
+
14
+ export interface ToasterProps extends Partial<ToasterConfig> {
15
+ class?: string;
16
+ }
17
+
18
+ const {
19
+ position = "bottom-right",
20
+ stacked = true,
21
+ expandOnHover = true,
22
+ duration = 4000,
23
+ limit = 5,
24
+ pauseOnHover = true,
25
+ swipeToDismiss = true,
26
+ showProgressBar = true,
27
+ enableHistory = false,
28
+ maxHistoryItems = 50,
29
+ historyDrawer = "right",
30
+ defaultEmphasis = "secondary",
31
+ storageKey,
32
+ class: className,
33
+ }: ToasterProps = Astro.props;
34
+
35
+ const config: Partial<ToasterConfig> = {
36
+ position,
37
+ stacked,
38
+ expandOnHover,
39
+ duration,
40
+ limit,
41
+ pauseOnHover,
42
+ swipeToDismiss,
43
+ showProgressBar,
44
+ enableHistory,
45
+ maxHistoryItems,
46
+ historyDrawer,
47
+ defaultEmphasis,
48
+ storageKey,
49
+ };
50
+
51
+ const api = connectToaster({ class: className });
52
+ const positions: ToastPosition[] = [
53
+ "top-left",
54
+ "top-right",
55
+ "top-center",
56
+ "bottom-left",
57
+ "bottom-right",
58
+ "bottom-center",
59
+ ];
60
+
61
+ const types = [
62
+ "primary",
63
+ "success",
64
+ "danger",
65
+ "warning",
66
+ "neutral",
67
+ "loading",
68
+ ] as const;
69
+ const typeFeedbackMap: Record<(typeof types)[number], AlertFeedback> = {
70
+ primary: "default",
71
+ success: "success",
72
+ danger: "danger",
73
+ warning: "warning",
74
+ neutral: "neutral",
75
+ loading: "loading",
76
+ };
77
+ // Badge has no "loading" — fall back to primary for the dedupe chip.
78
+ const badgeFeedbackMap: Record<
79
+ (typeof types)[number],
80
+ "success" | "danger" | "warning" | "neutral" | "primary"
81
+ > = {
82
+ primary: "primary",
83
+ success: "success",
84
+ danger: "danger",
85
+ warning: "warning",
86
+ neutral: "neutral",
87
+ loading: "primary",
88
+ };
89
+ // History items reuse Alert's feedback variants directly.
90
+ const historyFeedbackMap: Record<
91
+ (typeof types)[number],
92
+ "success" | "danger" | "warning" | "default" | "neutral" | "loading"
93
+ > = {
94
+ primary: "default",
95
+ success: "success",
96
+ danger: "danger",
97
+ warning: "warning",
98
+ neutral: "neutral",
99
+ loading: "loading",
100
+ };
101
+ ---
102
+
103
+ {
104
+ /*
105
+ astro-icon scans the *visible* page output to build its SVG sprite — icons
106
+ that only appear inside <template> tags (toast row + history item templates
107
+ below) are skipped, so cloned templates would render as empty stamps. Force
108
+ the scanner to include them by mounting a hidden block of Icon refs.
109
+ */
110
+ }
111
+ <div hidden aria-hidden="true">
112
+ <Icon name="lucide:x" />
113
+ <Icon name="lucide:check" />
114
+ <Icon name="lucide:triangle-alert" />
115
+ <Icon name="lucide:badge-alert" />
116
+ <Icon name="lucide:loader-circle" />
117
+ <Icon name="lucide:megaphone" />
118
+ <Icon name="lucide:bell" />
119
+ <Icon name="lucide:check-check" />
120
+ <Icon name="lucide:trash-2" />
121
+ </div>
122
+
123
+ <div
124
+ {...api.rootProps}
125
+ data-toaster-root
126
+ data-toaster-config={JSON.stringify(config)}
127
+ >
128
+ {
129
+ positions.map((pos) => (
130
+ <div
131
+ class={api.viewportClass(pos)}
132
+ data-toast-viewport={pos}
133
+ data-position={pos}
134
+ data-stacked={stacked ? "true" : "false"}
135
+ data-expanded="false"
136
+ data-empty="true"
137
+ style="display: none;"
138
+ role="region"
139
+ aria-label={`Toasts ${pos}`}
140
+ />
141
+ ))
142
+ }
143
+
144
+ {
145
+ /* Templates per type × variant × emphasis. attach-toaster picks at runtime. */
146
+ }
147
+ {
148
+ types.flatMap((type) =>
149
+ (["with-title", "no-title"] as const).flatMap((variant) =>
150
+ (["primary", "secondary", "tertiary"] as const).map((emphasis) => (
151
+ <template
152
+ data-toast-template={type}
153
+ data-toast-variant={variant}
154
+ data-toast-emphasis={emphasis}
155
+ >
156
+ <div
157
+ class={api.classes.row}
158
+ data-state="active"
159
+ data-feedback={type}
160
+ data-emphasis={emphasis}
161
+ role={type === "danger" ? "alert" : "status"}
162
+ aria-live={type === "danger" ? "assertive" : "polite"}
163
+ aria-atomic="true"
164
+ >
165
+ <Alert
166
+ feedback={typeFeedbackMap[type]}
167
+ emphasis={emphasis}
168
+ title={
169
+ variant === "with-title" ? "TITLE_PLACEHOLDER" : undefined
170
+ }
171
+ passThrough={{
172
+ title: { props: { "data-slot": "title" } },
173
+ }}
174
+ >
175
+ <span data-slot="content" />
176
+ <Fragment slot="actions">
177
+ <span data-slot="actions" data-action-mount />
178
+ <Button
179
+ size="xs"
180
+ emphasis="ghost"
181
+ data-slot="action-template"
182
+ aria-hidden="true"
183
+ style="display:none"
184
+ />
185
+ <Button
186
+ size="xs"
187
+ shape="square"
188
+ emphasis="ghost"
189
+ aria-label="Dismiss"
190
+ data-slot="dismiss"
191
+ >
192
+ <Icon name="lucide:x" width={14} height={14} />
193
+ </Button>
194
+ </Fragment>
195
+ </Alert>
196
+ <span
197
+ class={api.classes.dedupeBadge}
198
+ data-slot="dedupe"
199
+ data-count="1"
200
+ >
201
+ <Badge
202
+ size="sm"
203
+ feedback={badgeFeedbackMap[type]}
204
+ emphasis="primary"
205
+ >
206
+ <span data-slot="dedupe-content" />
207
+ </Badge>
208
+ </span>
209
+ {showProgressBar && (
210
+ <span class={api.classes.progressBar} data-slot="progress" />
211
+ )}
212
+ </div>
213
+ </template>
214
+ )),
215
+ ),
216
+ )
217
+ }
218
+
219
+ {/* Custom toast template (no Alert wrapper) */}
220
+ <template data-toast-template="custom">
221
+ <div
222
+ class={api.classes.row}
223
+ data-state="active"
224
+ data-feedback="neutral"
225
+ role="status"
226
+ aria-live="polite"
227
+ >
228
+ <div data-slot="content"></div>
229
+ <span class={api.classes.dedupeBadge} data-slot="dedupe" data-count="1">
230
+ <Badge size="sm" feedback="neutral" emphasis="primary">
231
+ <span data-slot="dedupe-content"></span>
232
+ </Badge>
233
+ </span>
234
+ {
235
+ showProgressBar && (
236
+ <span class={api.classes.progressBar} data-slot="progress" />
237
+ )
238
+ }
239
+ </div>
240
+ </template>
241
+
242
+ {/* Toast Center dialog (always rendered when enableHistory) */}
243
+ {enableHistory && <DialogManagerScript />}
244
+ {
245
+ enableHistory && (
246
+ <Dialog
247
+ id="pindoba-toast-center"
248
+ title="Notifications"
249
+ drawer={historyDrawer || undefined}
250
+ data-toast-center-dialog
251
+ passThrough={{
252
+ wrapper: { style: { minHeight: 0, overflow: "hidden" } },
253
+ content: {
254
+ style: { minHeight: 0, overflow: "hidden", flex: "1 1 auto" },
255
+ },
256
+ }}
257
+ >
258
+ <div class={api.classes.centerBody}>
259
+ <div class={api.classes.centerTabs} role="tablist">
260
+ <Group>
261
+ <Radio
262
+ id="pindoba-toast-center-tab-all"
263
+ appearance="button"
264
+ size="sm"
265
+ name="pindoba-toast-center-filter"
266
+ value="all"
267
+ checked
268
+ data-toast-center-tab="all"
269
+ >
270
+ All
271
+ <Badge
272
+ size="sm"
273
+ feedback="neutral"
274
+ class={api.classes.centerCount}
275
+ >
276
+ <span data-toast-center-count="all">0</span>
277
+ </Badge>
278
+ </Radio>
279
+ <Radio
280
+ id="pindoba-toast-center-tab-unread"
281
+ appearance="button"
282
+ size="sm"
283
+ name="pindoba-toast-center-filter"
284
+ value="unread"
285
+ data-toast-center-tab="unread"
286
+ >
287
+ Unread
288
+ <Badge
289
+ size="sm"
290
+ feedback="primary"
291
+ class={api.classes.centerCount}
292
+ >
293
+ <span data-toast-center-count="unread">0</span>
294
+ </Badge>
295
+ </Radio>
296
+ </Group>
297
+ <div class={api.classes.centerActions}>
298
+ <Button emphasis="ghost" size="sm" data-toast-center-mark-all>
299
+ <Icon name="lucide:check-check" width={16} height={16} />
300
+ Mark all read
301
+ </Button>
302
+ <Button emphasis="ghost" size="sm" data-toast-center-clear>
303
+ <Icon name="lucide:trash-2" width={16} height={16} />
304
+ Clear
305
+ </Button>
306
+ </div>
307
+ </div>
308
+ <ul class={api.classes.centerList} data-toast-center-list />
309
+ <p class={api.classes.centerEmpty} data-toast-center-empty>
310
+ No notifications.
311
+ </p>
312
+ </div>
313
+ {types.flatMap((type) =>
314
+ (["with-title", "no-title"] as const).flatMap((variant) =>
315
+ (["primary", "secondary", "tertiary"] as const).map((emphasis) => (
316
+ <template
317
+ data-toast-center-item-template={type}
318
+ data-toast-center-item-variant={variant}
319
+ data-toast-center-item-emphasis={emphasis}
320
+ >
321
+ <li class={api.classes.centerItem} data-has-recurrences="false">
322
+ {variant === "with-title" ? (
323
+ <Alert
324
+ feedback={historyFeedbackMap[type]}
325
+ emphasis={emphasis}
326
+ title="TITLE_PLACEHOLDER"
327
+ passThrough={{
328
+ title: { props: { "data-history-slot": "title" } },
329
+ }}
330
+ >
331
+ <span data-history-slot="content" />
332
+ <div
333
+ class={api.classes.centerRecurrences}
334
+ data-history-slot="recurrences"
335
+ >
336
+ <p
337
+ class={api.classes.centerRecurrencesSummary}
338
+ data-history-slot="recurrences-summary"
339
+ />
340
+ <ul
341
+ class={api.classes.centerRecurrenceList}
342
+ data-history-slot="recurrences-list"
343
+ />
344
+ </div>
345
+ <Fragment slot="actions">
346
+ <span
347
+ class={api.classes.centerLatestTime}
348
+ data-history-slot="latest-time"
349
+ />
350
+ </Fragment>
351
+ </Alert>
352
+ ) : (
353
+ <Alert
354
+ feedback={historyFeedbackMap[type]}
355
+ emphasis={emphasis}
356
+ >
357
+ <span data-history-slot="content" />
358
+ <div
359
+ class={api.classes.centerRecurrences}
360
+ data-history-slot="recurrences"
361
+ >
362
+ <p
363
+ class={api.classes.centerRecurrencesSummary}
364
+ data-history-slot="recurrences-summary"
365
+ />
366
+ <ul
367
+ class={api.classes.centerRecurrenceList}
368
+ data-history-slot="recurrences-list"
369
+ />
370
+ </div>
371
+ <Fragment slot="actions">
372
+ <span
373
+ class={api.classes.centerLatestTime}
374
+ data-history-slot="latest-time"
375
+ />
376
+ </Fragment>
377
+ </Alert>
378
+ )}
379
+ <span
380
+ class={api.classes.centerUnreadDot}
381
+ data-history-slot="unread-dot"
382
+ />
383
+ </li>
384
+ </template>
385
+ )),
386
+ ),
387
+ )}
388
+ <template data-history-recurrence-item>
389
+ <li
390
+ class={api.classes.centerRecurrenceItem}
391
+ data-history-slot="recurrence-row"
392
+ >
393
+ <span data-history-slot="recurrence-time" />
394
+ </li>
395
+ </template>
396
+ </Dialog>
397
+ )
398
+ }
399
+ </div>
400
+
401
+ <script>
402
+ import {
403
+ attachToaster,
404
+ markAllAsRead,
405
+ clearHistory,
406
+ } from "@pindoba/core-toast";
407
+ import type { ToasterConfig } from "@pindoba/core-toast";
408
+
409
+ // Multiple Toasters share one store — attach each one but only the first
410
+ // claim wins (handled inside attachToaster, which also portals viewports
411
+ // and the Toast Center dialog to document.body). Skip roots that don't
412
+ // carry an Astro config attribute — those belong to Svelte/React Toasters
413
+ // that configure themselves via their own onMount/effect.
414
+ const roots = document.querySelectorAll<HTMLElement>(
415
+ "[data-toaster-root][data-toaster-config]",
416
+ );
417
+ for (const root of Array.from(roots)) {
418
+ const raw = root.getAttribute("data-toaster-config");
419
+ let config: Partial<ToasterConfig> = {};
420
+ try {
421
+ config = raw ? (JSON.parse(raw) as Partial<ToasterConfig>) : {};
422
+ } catch {
423
+ /* ignore */
424
+ }
425
+ attachToaster(root, config);
426
+ }
427
+
428
+ // Bind Toast Center action buttons. Close is provided by the pindoba Dialog.
429
+ document
430
+ .querySelector("[data-toast-center-mark-all]")
431
+ ?.addEventListener("click", () => markAllAsRead());
432
+ document
433
+ .querySelector("[data-toast-center-clear]")
434
+ ?.addEventListener("click", () => clearHistory());
435
+ </script>
@@ -0,0 +1,39 @@
1
+ ---
2
+ import Button from "@pindoba/astro-button";
3
+ import Toaster from "../Toaster.astro";
4
+ import { stack } from "@pindoba/styled-system/patterns";
5
+ ---
6
+
7
+ <div class={stack({ gap: "sm", direction: "row" })}>
8
+ <Button data-toast-action="with-action">With action button</Button>
9
+ <Button data-toast-action="no-title">Title-less</Button>
10
+ </div>
11
+
12
+ <Toaster position="bottom-right" />
13
+
14
+ <script>
15
+ import { toast } from "@pindoba/astro-toast/runtime";
16
+
17
+ document
18
+ .querySelectorAll<HTMLElement>("[data-toast-action]")
19
+ .forEach((btn) => {
20
+ btn.addEventListener("click", () => {
21
+ const action = btn.dataset.toastAction;
22
+ if (action === "with-action") {
23
+ toast.neutral({
24
+ title: "Email archived",
25
+ content: "Tap undo to restore it.",
26
+ action: {
27
+ label: "Undo",
28
+ onClick: () => toast.success({ title: "Restored" }),
29
+ },
30
+ });
31
+ } else if (action === "no-title") {
32
+ toast.warning({
33
+ content:
34
+ "Title-less toasts use the row layout, like a compact alert.",
35
+ });
36
+ }
37
+ });
38
+ });
39
+ </script>
@@ -0,0 +1,26 @@
1
+ ---
2
+ import Button from "@pindoba/astro-button";
3
+ import Toaster from "../Toaster.astro";
4
+ import { stack } from "@pindoba/styled-system/patterns";
5
+ ---
6
+
7
+ <div class={stack({ gap: "sm", direction: "row" })}>
8
+ <Button data-toast-dedupe>Fire same id</Button>
9
+ </div>
10
+
11
+ <Toaster position="bottom-right" />
12
+
13
+ <script>
14
+ import { toast } from "@pindoba/astro-toast/runtime";
15
+
16
+ document
17
+ .querySelector("[data-toast-dedupe]")
18
+ ?.addEventListener("click", () => {
19
+ toast.success({
20
+ id: "dedupe-demo",
21
+ title: "Saved",
22
+ content:
23
+ "Same id — count badge ticks up and the toast pops to the front.",
24
+ });
25
+ });
26
+ </script>
@@ -0,0 +1,45 @@
1
+ ---
2
+ import Button from "@pindoba/astro-button";
3
+ import Toaster from "../Toaster.astro";
4
+ import { stack } from "@pindoba/styled-system/patterns";
5
+ ---
6
+
7
+ <div class={stack({ gap: "sm", direction: "row", wrap: "wrap" })}>
8
+ <Button data-toast-demo="primary">Primary</Button>
9
+ <Button data-toast-demo="success">Success</Button>
10
+ <Button data-toast-demo="danger">Danger</Button>
11
+ <Button data-toast-demo="warning">Warning</Button>
12
+ <Button data-toast-demo="neutral">Neutral</Button>
13
+ </div>
14
+
15
+ <Toaster position="bottom-right" />
16
+
17
+ <script>
18
+ import { toast } from "@pindoba/astro-toast/runtime";
19
+
20
+ document.querySelectorAll<HTMLElement>("[data-toast-demo]").forEach((btn) => {
21
+ btn.addEventListener("click", () => {
22
+ const type = btn.dataset.toastDemo;
23
+ if (type === "primary") {
24
+ toast.primary({
25
+ title: "New mention",
26
+ content: "Jane tagged you in a doc.",
27
+ });
28
+ } else if (type === "success") {
29
+ toast.success({ title: "Saved", content: "Your changes are live." });
30
+ } else if (type === "danger") {
31
+ toast.danger({
32
+ title: "Couldn't save",
33
+ content: "Network is unreachable.",
34
+ });
35
+ } else if (type === "warning") {
36
+ toast.warning({
37
+ title: "Heads up",
38
+ content: "Disk is almost full.",
39
+ });
40
+ } else if (type === "neutral") {
41
+ toast.neutral({ title: "FYI", content: "Cache rebuilt." });
42
+ }
43
+ });
44
+ });
45
+ </script>
@@ -0,0 +1,43 @@
1
+ ---
2
+ import Button from "@pindoba/astro-button";
3
+ import Toaster from "../Toaster.astro";
4
+ import { stack } from "@pindoba/styled-system/patterns";
5
+ ---
6
+
7
+ <div class={stack({ gap: "sm", direction: "row", wrap: "wrap" })}>
8
+ <Button data-emphasis-demo="default">Default (secondary)</Button>
9
+ <Button data-emphasis-demo="primary">Primary (loud)</Button>
10
+ <Button data-emphasis-demo="tertiary">Tertiary (quietest)</Button>
11
+ </div>
12
+
13
+ <Toaster position="bottom-right" />
14
+
15
+ <script>
16
+ import { toast } from "@pindoba/astro-toast/runtime";
17
+
18
+ document
19
+ .querySelectorAll<HTMLElement>("[data-emphasis-demo]")
20
+ .forEach((btn) => {
21
+ btn.addEventListener("click", () => {
22
+ const variant = btn.dataset.emphasisDemo;
23
+ if (variant === "default") {
24
+ toast.success({
25
+ title: "Saved",
26
+ content: "Default toasts use secondary emphasis.",
27
+ });
28
+ } else if (variant === "primary") {
29
+ toast.danger({
30
+ title: "Outage detected",
31
+ content: "Reserve primary for can't-miss messages.",
32
+ emphasis: "primary",
33
+ });
34
+ } else if (variant === "tertiary") {
35
+ toast.neutral({
36
+ title: "Heads up",
37
+ content: "Tertiary is the quietest variant.",
38
+ emphasis: "tertiary",
39
+ });
40
+ }
41
+ });
42
+ });
43
+ </script>
@@ -0,0 +1,76 @@
1
+ ---
2
+ import Button from "@pindoba/astro-button";
3
+ import Toaster from "../Toaster.astro";
4
+ import { stack } from "@pindoba/styled-system/patterns";
5
+ ---
6
+
7
+ <div class={stack({ gap: "sm", direction: "row" })}>
8
+ <Button data-toast-history="push">Push toast</Button>
9
+ <Button data-toast-history="open">
10
+ Open notifications (<span data-history-unread-count>0</span>)
11
+ </Button>
12
+ <Button data-toast-history="mark-read">Mark all read</Button>
13
+ <Button data-toast-history="clear">Clear history</Button>
14
+ </div>
15
+
16
+ <Toaster
17
+ position="bottom-right"
18
+ enableHistory={true}
19
+ storageKey="pindoba-toast-demo"
20
+ />
21
+
22
+ <script>
23
+ import {
24
+ toast,
25
+ toggleCenter,
26
+ markAllAsRead,
27
+ clearHistory,
28
+ $unreadCount,
29
+ } from "@pindoba/astro-toast/runtime";
30
+
31
+ $unreadCount.subscribe((count) => {
32
+ document
33
+ .querySelectorAll<HTMLElement>("[data-history-unread-count]")
34
+ .forEach((el) => (el.textContent = String(count)));
35
+ });
36
+
37
+ type DemoToast = {
38
+ type: "primary" | "success" | "danger" | "warning" | "neutral";
39
+ title: string;
40
+ content: string;
41
+ };
42
+ const samples: DemoToast[] = [
43
+ { type: "success", title: "Saved", content: "Changes published." },
44
+ { type: "danger", title: "Failed", content: "Unable to reach server." },
45
+ { type: "warning", title: "Heads up", content: "Disk almost full." },
46
+ { type: "neutral", title: "FYI", content: "Cache rebuilt." },
47
+ {
48
+ type: "primary",
49
+ title: "New mention",
50
+ content: "Jane tagged you in a doc.",
51
+ },
52
+ { type: "success", title: "Deployed", content: "Build finished in 12s." },
53
+ { type: "warning", title: "Slow query", content: "Check the indexes." },
54
+ { type: "danger", title: "Payment failed", content: "Card was declined." },
55
+ { type: "neutral", title: "Reminder", content: "Standup in 5 minutes." },
56
+ { type: "primary", title: "Sync complete", content: "Latest data is in." },
57
+ ];
58
+
59
+ document
60
+ .querySelectorAll<HTMLElement>("[data-toast-history]")
61
+ .forEach((btn) => {
62
+ btn.addEventListener("click", () => {
63
+ const action = btn.dataset.toastHistory;
64
+ if (action === "push") {
65
+ const sample = samples[Math.floor(Math.random() * samples.length)]!;
66
+ toast[sample.type]({ title: sample.title, content: sample.content });
67
+ } else if (action === "open") {
68
+ toggleCenter(true);
69
+ } else if (action === "mark-read") {
70
+ markAllAsRead();
71
+ } else if (action === "clear") {
72
+ clearHistory();
73
+ }
74
+ });
75
+ });
76
+ </script>
@@ -0,0 +1,8 @@
1
+ export { default as Default } from "./default.astro";
2
+ export { default as Emphasis } from "./emphasis.astro";
3
+ export { default as Promise } from "./promise.astro";
4
+ export { default as Positions } from "./positions.astro";
5
+ export { default as Stacked } from "./stacked.astro";
6
+ export { default as Dedupe } from "./dedupe.astro";
7
+ export { default as Actions } from "./actions.astro";
8
+ export { default as History } from "./history.astro";
@@ -0,0 +1,34 @@
1
+ ---
2
+ import Button from "@pindoba/astro-button";
3
+ import Toaster from "../Toaster.astro";
4
+ import { grid } from "@pindoba/styled-system/patterns";
5
+ ---
6
+
7
+ <div class={grid({ columns: 3, gap: "sm" })}>
8
+ <Button data-toast-position="top-left">Top left</Button>
9
+ <Button data-toast-position="top-center">Top center</Button>
10
+ <Button data-toast-position="top-right">Top right</Button>
11
+ <Button data-toast-position="bottom-left">Bottom left</Button>
12
+ <Button data-toast-position="bottom-center">Bottom center</Button>
13
+ <Button data-toast-position="bottom-right">Bottom right</Button>
14
+ </div>
15
+
16
+ <Toaster position="bottom-right" />
17
+
18
+ <script>
19
+ import { toast } from "@pindoba/astro-toast/runtime";
20
+ import type { ToastPosition } from "@pindoba/astro-toast/runtime";
21
+
22
+ document
23
+ .querySelectorAll<HTMLElement>("[data-toast-position]")
24
+ .forEach((btn) => {
25
+ btn.addEventListener("click", () => {
26
+ const position = btn.dataset.toastPosition as ToastPosition;
27
+ toast.neutral({
28
+ title: position,
29
+ content: "Per-toast position override.",
30
+ position,
31
+ });
32
+ });
33
+ });
34
+ </script>
@@ -0,0 +1,47 @@
1
+ ---
2
+ import Button from "@pindoba/astro-button";
3
+ import Toaster from "../Toaster.astro";
4
+ import { stack } from "@pindoba/styled-system/patterns";
5
+ ---
6
+
7
+ <div class={stack({ gap: "sm", direction: "row" })}>
8
+ <Button data-toast-demo="promise-resolve">Resolve in 1.5s</Button>
9
+ <Button data-toast-demo="promise-reject">Reject in 1.5s</Button>
10
+ </div>
11
+
12
+ <Toaster position="bottom-right" />
13
+
14
+ <script>
15
+ import { toast } from "@pindoba/astro-toast/runtime";
16
+
17
+ document.querySelectorAll<HTMLElement>("[data-toast-demo]").forEach((btn) => {
18
+ btn.addEventListener("click", () => {
19
+ const action = btn.dataset.toastDemo;
20
+ if (action === "promise-resolve") {
21
+ toast.promise(
22
+ new Promise<string>((res) => setTimeout(() => res("ok"), 1500)),
23
+ {
24
+ loading: "Uploading…",
25
+ success: "Uploaded",
26
+ error: "Upload failed",
27
+ },
28
+ );
29
+ } else if (action === "promise-reject") {
30
+ toast
31
+ .promise(
32
+ new Promise<never>((_, rej) =>
33
+ setTimeout(() => rej(new Error("503")), 1500),
34
+ ),
35
+ {
36
+ loading: "Uploading…",
37
+ success: "Uploaded",
38
+ error: "Upload failed — try again",
39
+ },
40
+ )
41
+ .catch(() => {
42
+ /* swallow — toast already showed */
43
+ });
44
+ }
45
+ });
46
+ });
47
+ </script>
@@ -0,0 +1,29 @@
1
+ ---
2
+ import Button from "@pindoba/astro-button";
3
+ import Toaster from "../Toaster.astro";
4
+ import { stack } from "@pindoba/styled-system/patterns";
5
+ ---
6
+
7
+ <div class={stack({ gap: "sm", direction: "row" })}>
8
+ <Button data-toast-spawn>Spawn random toast</Button>
9
+ </div>
10
+
11
+ <Toaster position="bottom-right" stacked={true} expandOnHover={true} />
12
+
13
+ <script>
14
+ import { toast, getToastStore } from "@pindoba/astro-toast/runtime";
15
+
16
+ document
17
+ .querySelector("[data-toast-spawn]")
18
+ ?.addEventListener("click", () => {
19
+ // Reapply the demo's Toaster config — needed when several demos share
20
+ // one document and the user wants to see this demo's settings now.
21
+ getToastStore().setConfig({ stacked: true, expandOnHover: true });
22
+ const types = ["success", "danger", "warning", "neutral"] as const;
23
+ const type = types[Math.floor(Math.random() * types.length)];
24
+ toast[type]({
25
+ title: type,
26
+ content: `Spawned at ${new Date().toLocaleTimeString()}`,
27
+ });
28
+ });
29
+ </script>
package/src/runtime.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Astro runtime exports — consumers import from `@pindoba/astro-toast/runtime`.
3
+ * Includes the imperative `toast()` API, history/center handles, and the
4
+ * `getToaster()` accessor.
5
+ */
6
+ export {
7
+ toast,
8
+ attachToaster,
9
+ getToaster,
10
+ getToastStore,
11
+ $history,
12
+ $isCenterOpen,
13
+ $unreadCount,
14
+ markAsRead,
15
+ markAllAsRead,
16
+ clearHistory,
17
+ toggleCenter,
18
+ getToastHistoryHandles,
19
+ } from "@pindoba/core-toast";
20
+ export type {
21
+ ToastInput,
22
+ ToastEntry,
23
+ ToastType,
24
+ ToastPosition,
25
+ ToastContent,
26
+ ToastContentRenderer,
27
+ ToastAction,
28
+ ToasterConfig,
29
+ HistoryEntry,
30
+ UseToastHistory,
31
+ } from "@pindoba/core-toast";