@dxlbnl/ui 0.1.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 +94 -0
- package/dist/components/cards/Card.stories.svelte +82 -0
- package/dist/components/cards/Card.stories.svelte.d.ts +19 -0
- package/dist/components/cards/Card.svelte +28 -0
- package/dist/components/cards/Card.svelte.d.ts +12 -0
- package/dist/components/cards/NoteCard.stories.svelte +94 -0
- package/dist/components/cards/NoteCard.stories.svelte.d.ts +19 -0
- package/dist/components/cards/NoteCard.svelte +89 -0
- package/dist/components/cards/NoteCard.svelte.d.ts +18 -0
- package/dist/components/cards/ProductCard.stories.svelte +98 -0
- package/dist/components/cards/ProductCard.stories.svelte.d.ts +19 -0
- package/dist/components/cards/ProductCard.svelte +150 -0
- package/dist/components/cards/ProductCard.svelte.d.ts +22 -0
- package/dist/components/cards/ProjectCard.stories.svelte +88 -0
- package/dist/components/cards/ProjectCard.stories.svelte.d.ts +19 -0
- package/dist/components/cards/ProjectCard.svelte +109 -0
- package/dist/components/cards/ProjectCard.svelte.d.ts +20 -0
- package/dist/components/cards/index.d.ts +4 -0
- package/dist/components/cards/index.js +4 -0
- package/dist/components/data/Accordion.stories.svelte +316 -0
- package/dist/components/data/Accordion.stories.svelte.d.ts +19 -0
- package/dist/components/data/Accordion.svelte +23 -0
- package/dist/components/data/Accordion.svelte.d.ts +9 -0
- package/dist/components/data/AccordionItem.svelte +112 -0
- package/dist/components/data/AccordionItem.svelte.d.ts +11 -0
- package/dist/components/data/Table.composition.stories.svelte +67 -0
- package/dist/components/data/Table.composition.stories.svelte.d.ts +19 -0
- package/dist/components/data/Table.stories.svelte +137 -0
- package/dist/components/data/Table.stories.svelte.d.ts +19 -0
- package/dist/components/data/Table.svelte +83 -0
- package/dist/components/data/Table.svelte.d.ts +14 -0
- package/dist/components/data/Tabs.stories.svelte +386 -0
- package/dist/components/data/Tabs.stories.svelte.d.ts +19 -0
- package/dist/components/data/Tabs.svelte +142 -0
- package/dist/components/data/Tabs.svelte.d.ts +19 -0
- package/dist/components/data/index.d.ts +4 -0
- package/dist/components/data/index.js +4 -0
- package/dist/components/feedback/Modal.stories.svelte +192 -0
- package/dist/components/feedback/Modal.stories.svelte.d.ts +4 -0
- package/dist/components/feedback/Modal.svelte +185 -0
- package/dist/components/feedback/Modal.svelte.d.ts +19 -0
- package/dist/components/feedback/Toast.stories.svelte +203 -0
- package/dist/components/feedback/Toast.stories.svelte.d.ts +19 -0
- package/dist/components/feedback/Toast.svelte +109 -0
- package/dist/components/feedback/Toast.svelte.d.ts +15 -0
- package/dist/components/feedback/ToastRegion.stories.svelte +193 -0
- package/dist/components/feedback/ToastRegion.stories.svelte.d.ts +19 -0
- package/dist/components/feedback/ToastRegion.svelte +102 -0
- package/dist/components/feedback/ToastRegion.svelte.d.ts +9 -0
- package/dist/components/feedback/index.d.ts +3 -0
- package/dist/components/feedback/index.js +3 -0
- package/dist/components/forms/Checkbox.stories.svelte +103 -0
- package/dist/components/forms/Checkbox.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Checkbox.svelte +150 -0
- package/dist/components/forms/Checkbox.svelte.d.ts +11 -0
- package/dist/components/forms/Field.stories.svelte +113 -0
- package/dist/components/forms/Field.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Field.svelte +77 -0
- package/dist/components/forms/Field.svelte.d.ts +17 -0
- package/dist/components/forms/Input.stories.svelte +58 -0
- package/dist/components/forms/Input.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Input.svelte +64 -0
- package/dist/components/forms/Input.svelte.d.ts +9 -0
- package/dist/components/forms/InputWrap.composition.stories.svelte +32 -0
- package/dist/components/forms/InputWrap.composition.stories.svelte.d.ts +19 -0
- package/dist/components/forms/InputWrap.stories.svelte +53 -0
- package/dist/components/forms/InputWrap.stories.svelte.d.ts +19 -0
- package/dist/components/forms/InputWrap.svelte +128 -0
- package/dist/components/forms/InputWrap.svelte.d.ts +21 -0
- package/dist/components/forms/Radio.stories.svelte +70 -0
- package/dist/components/forms/Radio.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Radio.svelte +109 -0
- package/dist/components/forms/Radio.svelte.d.ts +9 -0
- package/dist/components/forms/RadioGroup.stories.svelte +115 -0
- package/dist/components/forms/RadioGroup.stories.svelte.d.ts +19 -0
- package/dist/components/forms/RadioGroup.svelte +116 -0
- package/dist/components/forms/RadioGroup.svelte.d.ts +24 -0
- package/dist/components/forms/Select.stories.svelte +168 -0
- package/dist/components/forms/Select.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Select.svelte +262 -0
- package/dist/components/forms/Select.svelte.d.ts +23 -0
- package/dist/components/forms/Switch.stories.svelte +86 -0
- package/dist/components/forms/Switch.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Switch.svelte +113 -0
- package/dist/components/forms/Switch.svelte.d.ts +11 -0
- package/dist/components/forms/Textarea.stories.svelte +40 -0
- package/dist/components/forms/Textarea.stories.svelte.d.ts +19 -0
- package/dist/components/forms/Textarea.svelte +66 -0
- package/dist/components/forms/Textarea.svelte.d.ts +9 -0
- package/dist/components/forms/field-context.d.ts +7 -0
- package/dist/components/forms/field-context.js +1 -0
- package/dist/components/forms/index.d.ts +9 -0
- package/dist/components/forms/index.js +9 -0
- package/dist/components/layout/Container.stories.svelte +67 -0
- package/dist/components/layout/Container.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Container.svelte +52 -0
- package/dist/components/layout/Container.svelte.d.ts +14 -0
- package/dist/components/layout/Grid.stories.svelte +109 -0
- package/dist/components/layout/Grid.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Grid.svelte +54 -0
- package/dist/components/layout/Grid.svelte.d.ts +19 -0
- package/dist/components/layout/Inline.stories.svelte +136 -0
- package/dist/components/layout/Inline.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Inline.svelte +46 -0
- package/dist/components/layout/Inline.svelte.d.ts +19 -0
- package/dist/components/layout/Prose.stories.svelte +423 -0
- package/dist/components/layout/Prose.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Prose.svelte +176 -0
- package/dist/components/layout/Prose.svelte.d.ts +12 -0
- package/dist/components/layout/Rule.stories.svelte +80 -0
- package/dist/components/layout/Rule.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Rule.svelte +33 -0
- package/dist/components/layout/Rule.svelte.d.ts +9 -0
- package/dist/components/layout/Spread.stories.svelte +118 -0
- package/dist/components/layout/Spread.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Spread.svelte +38 -0
- package/dist/components/layout/Spread.svelte.d.ts +16 -0
- package/dist/components/layout/Stack.stories.svelte +90 -0
- package/dist/components/layout/Stack.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Stack.svelte +37 -0
- package/dist/components/layout/Stack.svelte.d.ts +16 -0
- package/dist/components/layout/index.d.ts +7 -0
- package/dist/components/layout/index.js +7 -0
- package/dist/components/navigation/Breadcrumb.stories.svelte +122 -0
- package/dist/components/navigation/Breadcrumb.stories.svelte.d.ts +19 -0
- package/dist/components/navigation/Breadcrumb.svelte +70 -0
- package/dist/components/navigation/Breadcrumb.svelte.d.ts +13 -0
- package/dist/components/navigation/Nav.stories.svelte +323 -0
- package/dist/components/navigation/Nav.stories.svelte.d.ts +19 -0
- package/dist/components/navigation/Nav.svelte +257 -0
- package/dist/components/navigation/Nav.svelte.d.ts +21 -0
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.js +2 -0
- package/dist/components/patterns/ActivityRow.stories.svelte +45 -0
- package/dist/components/patterns/ActivityRow.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/ActivityRow.svelte +69 -0
- package/dist/components/patterns/ActivityRow.svelte.d.ts +16 -0
- package/dist/components/patterns/Alert.stories.svelte +63 -0
- package/dist/components/patterns/Alert.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/Alert.svelte +91 -0
- package/dist/components/patterns/Alert.svelte.d.ts +16 -0
- package/dist/components/patterns/CtaBlock.stories.svelte +62 -0
- package/dist/components/patterns/CtaBlock.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/CtaBlock.svelte +80 -0
- package/dist/components/patterns/CtaBlock.svelte.d.ts +16 -0
- package/dist/components/patterns/KvList.stories.svelte +48 -0
- package/dist/components/patterns/KvList.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/KvList.svelte +65 -0
- package/dist/components/patterns/KvList.svelte.d.ts +15 -0
- package/dist/components/patterns/PageHero.stories.svelte +62 -0
- package/dist/components/patterns/PageHero.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/PageHero.svelte +62 -0
- package/dist/components/patterns/PageHero.svelte.d.ts +14 -0
- package/dist/components/patterns/ProgressBar.stories.svelte +83 -0
- package/dist/components/patterns/ProgressBar.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/ProgressBar.svelte +71 -0
- package/dist/components/patterns/ProgressBar.svelte.d.ts +13 -0
- package/dist/components/patterns/SectionFoot.stories.svelte +37 -0
- package/dist/components/patterns/SectionFoot.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/SectionFoot.svelte +70 -0
- package/dist/components/patterns/SectionFoot.svelte.d.ts +15 -0
- package/dist/components/patterns/SectionHead.stories.svelte +67 -0
- package/dist/components/patterns/SectionHead.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/SectionHead.svelte +54 -0
- package/dist/components/patterns/SectionHead.svelte.d.ts +14 -0
- package/dist/components/patterns/StatCard.stories.svelte +59 -0
- package/dist/components/patterns/StatCard.stories.svelte.d.ts +19 -0
- package/dist/components/patterns/StatCard.svelte +57 -0
- package/dist/components/patterns/StatCard.svelte.d.ts +15 -0
- package/dist/components/patterns/index.d.ts +9 -0
- package/dist/components/patterns/index.js +9 -0
- package/dist/components/primitives/Button.stories.svelte +132 -0
- package/dist/components/primitives/Button.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Button.svelte +142 -0
- package/dist/components/primitives/Button.svelte.d.ts +16 -0
- package/dist/components/primitives/Heading.stories.svelte +137 -0
- package/dist/components/primitives/Heading.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Heading.svelte +107 -0
- package/dist/components/primitives/Heading.svelte.d.ts +23 -0
- package/dist/components/primitives/Led.stories.svelte +63 -0
- package/dist/components/primitives/Led.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Led.svelte +65 -0
- package/dist/components/primitives/Led.svelte.d.ts +11 -0
- package/dist/components/primitives/TagPill.stories.svelte +90 -0
- package/dist/components/primitives/TagPill.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/TagPill.svelte +44 -0
- package/dist/components/primitives/TagPill.svelte.d.ts +9 -0
- package/dist/components/primitives/Text.stories.svelte +252 -0
- package/dist/components/primitives/Text.stories.svelte.d.ts +19 -0
- package/dist/components/primitives/Text.svelte +101 -0
- package/dist/components/primitives/Text.svelte.d.ts +25 -0
- package/dist/components/primitives/index.d.ts +5 -0
- package/dist/components/primitives/index.js +5 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/stores/toast.d.ts +19 -0
- package/dist/stores/toast.js +22 -0
- package/dist/storybook-utils.d.ts +11 -0
- package/dist/storybook-utils.js +29 -0
- package/dist/tokens/ColorSwatch.svelte +73 -0
- package/dist/tokens/ColorSwatch.svelte.d.ts +10 -0
- package/dist/tokens/layout.css +144 -0
- package/dist/tokens/patterns.css +281 -0
- package/dist/tokens/tokens.css +96 -0
- package/dist/tokens/tokens.stories.svelte +107 -0
- package/dist/tokens/tokens.stories.svelte.d.ts +18 -0
- package/dist/tokens/typography.css +159 -0
- package/package.json +62 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
3
|
+
import { expect, within } from "storybook/test";
|
|
4
|
+
import { resolveTokenColor } from "../../storybook-utils.js";
|
|
5
|
+
import Toast from "./Toast.svelte";
|
|
6
|
+
|
|
7
|
+
const { Story } = defineMeta({
|
|
8
|
+
title: "Feedback/Toast",
|
|
9
|
+
component: Toast,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
});
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!-- AC 16, 23, 26, 27, 29 — success variant: role=status, icon "ok", border-color=--ok -->
|
|
15
|
+
<Story
|
|
16
|
+
name="Success"
|
|
17
|
+
args={{
|
|
18
|
+
id: "toast-1",
|
|
19
|
+
message: "Build completed successfully.",
|
|
20
|
+
variant: "success",
|
|
21
|
+
ondismiss: () => {},
|
|
22
|
+
}}
|
|
23
|
+
play={async ({ canvasElement }) => {
|
|
24
|
+
const canvas = within(canvasElement);
|
|
25
|
+
|
|
26
|
+
// AC 16: ok variant renders role="status"
|
|
27
|
+
const toast = canvas.getByRole("status");
|
|
28
|
+
await expect(toast).toBeVisible();
|
|
29
|
+
|
|
30
|
+
// AC 26: message text visible
|
|
31
|
+
await expect(
|
|
32
|
+
canvas.getByText("Build completed successfully."),
|
|
33
|
+
).toBeVisible();
|
|
34
|
+
|
|
35
|
+
// AC 27: close button present, enabled
|
|
36
|
+
const closeBtn = canvas.getByRole("button", {
|
|
37
|
+
name: /Dismiss notification/i,
|
|
38
|
+
});
|
|
39
|
+
await expect(closeBtn).toBeVisible();
|
|
40
|
+
await expect(closeBtn).toBeEnabled();
|
|
41
|
+
|
|
42
|
+
// AC 22, 23: .toast-icon with aria-hidden="true" and text "ok"
|
|
43
|
+
const icon = canvasElement.querySelector(".toast-icon");
|
|
44
|
+
await expect(icon).not.toBeNull();
|
|
45
|
+
await expect(icon!.getAttribute("aria-hidden")).toBe("true");
|
|
46
|
+
await expect(icon!.textContent!.trim()).toBe("ok");
|
|
47
|
+
|
|
48
|
+
// AC 29: border-color matches var(--ok)
|
|
49
|
+
const okColor = resolveTokenColor("--ok");
|
|
50
|
+
await expect(getComputedStyle(toast).borderColor).toBe(okColor);
|
|
51
|
+
|
|
52
|
+
// AC 32: background-color matches var(--bg-elev)
|
|
53
|
+
const bgElev = resolveTokenColor("--bg-elev");
|
|
54
|
+
await expect(getComputedStyle(toast).backgroundColor).toBe(bgElev);
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<!-- AC 17, 24, 30 — warning variant: role=status, icon "!!", border-color=--amber -->
|
|
59
|
+
<Story
|
|
60
|
+
name="Warning"
|
|
61
|
+
args={{
|
|
62
|
+
id: "toast-2",
|
|
63
|
+
message: "+12V rail at 88% capacity.",
|
|
64
|
+
variant: "warning",
|
|
65
|
+
ondismiss: () => {},
|
|
66
|
+
}}
|
|
67
|
+
play={async ({ canvasElement }) => {
|
|
68
|
+
const canvas = within(canvasElement);
|
|
69
|
+
|
|
70
|
+
// AC 17: amber variant renders role="status"
|
|
71
|
+
const toast = canvas.getByRole("status");
|
|
72
|
+
await expect(toast).toBeVisible();
|
|
73
|
+
|
|
74
|
+
// AC 26: message text visible
|
|
75
|
+
await expect(canvas.getByText(/12V rail at 88%/i)).toBeVisible();
|
|
76
|
+
|
|
77
|
+
// AC 24: .toast-icon text is "!!"
|
|
78
|
+
const icon = canvasElement.querySelector(".toast-icon");
|
|
79
|
+
await expect(icon).not.toBeNull();
|
|
80
|
+
await expect(icon!.textContent!.trim()).toBe("!!");
|
|
81
|
+
|
|
82
|
+
// AC 30: border-color matches var(--amber)
|
|
83
|
+
const amberColor = resolveTokenColor("--amber");
|
|
84
|
+
await expect(getComputedStyle(toast).borderColor).toBe(amberColor);
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<!-- AC 18, 20, 25, 31 — error variant: role=alert, aria-live=assertive, icon "err", border-color=--danger -->
|
|
89
|
+
<Story
|
|
90
|
+
name="Error"
|
|
91
|
+
args={{
|
|
92
|
+
id: "toast-3",
|
|
93
|
+
message: "Thermal protection triggered.",
|
|
94
|
+
variant: "error",
|
|
95
|
+
ondismiss: () => {},
|
|
96
|
+
}}
|
|
97
|
+
play={async ({ canvasElement }) => {
|
|
98
|
+
const canvas = within(canvasElement);
|
|
99
|
+
|
|
100
|
+
// AC 18: danger variant renders role="alert"
|
|
101
|
+
const toast = canvas.getByRole("alert");
|
|
102
|
+
await expect(toast).toBeVisible();
|
|
103
|
+
|
|
104
|
+
// AC 26: message text visible
|
|
105
|
+
await expect(
|
|
106
|
+
canvas.getByText("Thermal protection triggered."),
|
|
107
|
+
).toBeVisible();
|
|
108
|
+
|
|
109
|
+
// AC 20: danger has aria-live="assertive"
|
|
110
|
+
await expect(toast.getAttribute("aria-live")).toBe("assertive");
|
|
111
|
+
|
|
112
|
+
// AC 25: .toast-icon text is "err"
|
|
113
|
+
const icon = canvasElement.querySelector(".toast-icon");
|
|
114
|
+
await expect(icon).not.toBeNull();
|
|
115
|
+
await expect(icon!.textContent!.trim()).toBe("err");
|
|
116
|
+
|
|
117
|
+
// AC 31: border-color matches var(--danger)
|
|
118
|
+
const dangerColor = resolveTokenColor("--danger");
|
|
119
|
+
await expect(getComputedStyle(toast).borderColor).toBe(dangerColor);
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<!-- AC 27, 28 — clicking the close button calls ondismiss (no-op in story; no throw) -->
|
|
124
|
+
<Story
|
|
125
|
+
name="Manual Close"
|
|
126
|
+
args={{
|
|
127
|
+
id: "toast-4",
|
|
128
|
+
message: "Click × to dismiss.",
|
|
129
|
+
variant: "success",
|
|
130
|
+
ondismiss: () => {},
|
|
131
|
+
}}
|
|
132
|
+
play={async ({ canvasElement, userEvent }) => {
|
|
133
|
+
const canvas = within(canvasElement);
|
|
134
|
+
|
|
135
|
+
// AC 27: close button present
|
|
136
|
+
const toast = canvas.getByRole("status");
|
|
137
|
+
await expect(toast).toBeVisible();
|
|
138
|
+
|
|
139
|
+
const closeBtn = canvas.getByRole("button", {
|
|
140
|
+
name: /Dismiss notification/i,
|
|
141
|
+
});
|
|
142
|
+
await expect(closeBtn).toBeVisible();
|
|
143
|
+
|
|
144
|
+
// AC 28: clicking button calls ondismiss — the no-op callback should not throw
|
|
145
|
+
await userEvent.click(closeBtn);
|
|
146
|
+
// If ondismiss threw, userEvent.click would have rejected — reaching here means success
|
|
147
|
+
await expect(closeBtn).toBeInTheDocument();
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
<!-- AC 33 — long message: toast visible and max-width ≤ 400px -->
|
|
152
|
+
<Story
|
|
153
|
+
name="Long Message"
|
|
154
|
+
args={{
|
|
155
|
+
id: "toast-5",
|
|
156
|
+
message:
|
|
157
|
+
"This is a longer notification message that tests how the toast handles wrapping text within its maximum width constraint.",
|
|
158
|
+
variant: "warning",
|
|
159
|
+
ondismiss: () => {},
|
|
160
|
+
}}
|
|
161
|
+
play={async ({ canvasElement }) => {
|
|
162
|
+
const canvas = within(canvasElement);
|
|
163
|
+
|
|
164
|
+
// AC 17: still role=status for warning
|
|
165
|
+
const toast = canvas.getByRole("status");
|
|
166
|
+
await expect(toast).toBeVisible();
|
|
167
|
+
|
|
168
|
+
// AC 26: full message present in DOM
|
|
169
|
+
await expect(
|
|
170
|
+
canvas.getByText(
|
|
171
|
+
"This is a longer notification message that tests how the toast handles wrapping text within its maximum width constraint.",
|
|
172
|
+
),
|
|
173
|
+
).toBeVisible();
|
|
174
|
+
|
|
175
|
+
// AC 33: max-width is 400px
|
|
176
|
+
await expect(getComputedStyle(toast).maxWidth).toBe("400px");
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
<!-- AC 19, 21, 22 — ARIA attributes on success variant -->
|
|
181
|
+
<Story
|
|
182
|
+
name="Aria Attributes"
|
|
183
|
+
args={{
|
|
184
|
+
id: "toast-6",
|
|
185
|
+
message: "ARIA check.",
|
|
186
|
+
variant: "success",
|
|
187
|
+
ondismiss: () => {},
|
|
188
|
+
}}
|
|
189
|
+
play={async ({ canvasElement }) => {
|
|
190
|
+
const canvas = within(canvasElement);
|
|
191
|
+
|
|
192
|
+
// AC 19: ok/amber has aria-live="polite"
|
|
193
|
+
const toast = canvas.getByRole("status");
|
|
194
|
+
await expect(toast.getAttribute("aria-live")).toBe("polite");
|
|
195
|
+
|
|
196
|
+
// AC 21: aria-atomic="true"
|
|
197
|
+
await expect(toast.getAttribute("aria-atomic")).toBe("true");
|
|
198
|
+
|
|
199
|
+
// AC 22: .toast-icon has aria-hidden="true"
|
|
200
|
+
const icon = canvasElement.querySelector(".toast-icon");
|
|
201
|
+
await expect(icon!.getAttribute("aria-hidden")).toBe("true");
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Toast from "./Toast.svelte";
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const Toast: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
+
[evt: string]: CustomEvent<any>;
|
|
17
|
+
}, {}, {}, string>;
|
|
18
|
+
type Toast = InstanceType<typeof Toast>;
|
|
19
|
+
export default Toast;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import type { ToastVariant } from "../../stores/toast.js";
|
|
4
|
+
import Button from "../primitives/Button.svelte";
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Unique toast ID (used by the dismiss callback). */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Text content of the notification. */
|
|
10
|
+
message: string;
|
|
11
|
+
/** Colour variant — also sets the ARIA live region type. @default 'success' */
|
|
12
|
+
variant?: ToastVariant;
|
|
13
|
+
/** Called with the toast `id` when the dismiss button is clicked. */
|
|
14
|
+
ondismiss: (id: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
id,
|
|
19
|
+
message,
|
|
20
|
+
variant = "success",
|
|
21
|
+
ondismiss,
|
|
22
|
+
...rest
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
const ICONS: Record<ToastVariant, string> = {
|
|
26
|
+
success: "ok",
|
|
27
|
+
warning: "!!",
|
|
28
|
+
error: "err",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let role = $derived(variant === "error" ? "alert" : "status");
|
|
32
|
+
let ariaLive: "assertive" | "polite" = $derived(
|
|
33
|
+
variant === "error" ? "assertive" : "polite",
|
|
34
|
+
);
|
|
35
|
+
let icon = $derived(ICONS[variant]);
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div
|
|
39
|
+
class="toast toast--{variant}"
|
|
40
|
+
{role}
|
|
41
|
+
aria-live={ariaLive}
|
|
42
|
+
aria-atomic="true"
|
|
43
|
+
{...rest}
|
|
44
|
+
>
|
|
45
|
+
<span class="toast-icon" aria-hidden="true">{icon}</span>
|
|
46
|
+
<span class="toast-message">{message}</span>
|
|
47
|
+
<div class="toast-close-wrap">
|
|
48
|
+
<Button
|
|
49
|
+
variant="ghost"
|
|
50
|
+
type="button"
|
|
51
|
+
aria-label="Dismiss notification"
|
|
52
|
+
onclick={() => ondismiss(id)}>×</Button
|
|
53
|
+
>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<style>
|
|
58
|
+
.toast {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 10px;
|
|
62
|
+
padding: 12px 14px;
|
|
63
|
+
min-width: 260px;
|
|
64
|
+
max-width: 400px;
|
|
65
|
+
border: 1px solid;
|
|
66
|
+
background: var(--bg-elev);
|
|
67
|
+
font-size: var(--t-body);
|
|
68
|
+
line-height: 1.4;
|
|
69
|
+
pointer-events: all;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.toast--success {
|
|
73
|
+
border-color: var(--ok);
|
|
74
|
+
}
|
|
75
|
+
.toast--warning {
|
|
76
|
+
border-color: var(--amber);
|
|
77
|
+
}
|
|
78
|
+
.toast--error {
|
|
79
|
+
border-color: var(--danger);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.toast-icon {
|
|
83
|
+
font-family: var(--mono);
|
|
84
|
+
font-size: var(--t-micro);
|
|
85
|
+
letter-spacing: 0.08em;
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
flex-shrink: 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.toast--success .toast-icon {
|
|
91
|
+
color: var(--ok);
|
|
92
|
+
}
|
|
93
|
+
.toast--warning .toast-icon {
|
|
94
|
+
color: var(--amber);
|
|
95
|
+
}
|
|
96
|
+
.toast--error .toast-icon {
|
|
97
|
+
color: var(--danger);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.toast-message {
|
|
101
|
+
flex: 1;
|
|
102
|
+
color: var(--ink-dim);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.toast-close-wrap {
|
|
106
|
+
flex-shrink: 0;
|
|
107
|
+
margin-left: auto;
|
|
108
|
+
}
|
|
109
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
import type { ToastVariant } from "../../stores/toast.js";
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** Unique toast ID (used by the dismiss callback). */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Text content of the notification. */
|
|
7
|
+
message: string;
|
|
8
|
+
/** Colour variant — also sets the ARIA live region type. @default 'success' */
|
|
9
|
+
variant?: ToastVariant;
|
|
10
|
+
/** Called with the toast `id` when the dismiss button is clicked. */
|
|
11
|
+
ondismiss: (id: string) => void;
|
|
12
|
+
}
|
|
13
|
+
declare const Toast: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type Toast = ReturnType<typeof Toast>;
|
|
15
|
+
export default Toast;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
3
|
+
import { expect, within, waitFor } from "storybook/test";
|
|
4
|
+
import ToastRegion from "./ToastRegion.svelte";
|
|
5
|
+
import { toast } from "../../stores/toast.js";
|
|
6
|
+
|
|
7
|
+
// AC 69: no component: in defineMeta — ToastRegion is a singleton portal (composition-only)
|
|
8
|
+
const { Story } = defineMeta({
|
|
9
|
+
title: "Feedback/ToastRegion",
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
});
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!-- AC 39, 40, 66 — push one toast and verify it renders in the region -->
|
|
15
|
+
<Story name="Single Toast"
|
|
16
|
+
play={async ({ canvasElement, userEvent }) => {
|
|
17
|
+
const canvas = within(canvasElement);
|
|
18
|
+
|
|
19
|
+
// AC 40: push a toast and it appears
|
|
20
|
+
const id = toast.push('Hello from ToastRegion.', { variant: 'success' });
|
|
21
|
+
|
|
22
|
+
// AC 39: ToastRegion renders one Toast per active store item
|
|
23
|
+
await waitFor(() => expect(canvas.getByRole('status')).toBeVisible());
|
|
24
|
+
await expect(canvas.getByText('Hello from ToastRegion.')).toBeVisible();
|
|
25
|
+
|
|
26
|
+
// AC 27: close button present
|
|
27
|
+
await expect(canvas.getByRole('button', { name: /Dismiss notification/i })).toBeVisible();
|
|
28
|
+
|
|
29
|
+
// AC 66: region has aria-label="Notifications"
|
|
30
|
+
const region = canvasElement.querySelector('.toast-region');
|
|
31
|
+
await expect(region).not.toBeNull();
|
|
32
|
+
await expect(region!.getAttribute('aria-label')).toBe('Notifications');
|
|
33
|
+
|
|
34
|
+
// Cleanup (AC 73: store cleanup to avoid state leak between tests)
|
|
35
|
+
toast.dismiss(id);
|
|
36
|
+
}}>
|
|
37
|
+
<ToastRegion position="bottom-right" />
|
|
38
|
+
</Story>
|
|
39
|
+
|
|
40
|
+
<!-- AC 39, 41 — three variants: success + warning = status (×2), error = alert (×1) -->
|
|
41
|
+
<Story name="Three Variants"
|
|
42
|
+
play={async ({ canvasElement }) => {
|
|
43
|
+
const canvas = within(canvasElement);
|
|
44
|
+
|
|
45
|
+
// Push three toasts with different variants
|
|
46
|
+
const id1 = toast.push('Build passed.', { variant: 'success' });
|
|
47
|
+
const id2 = toast.push('Memory at 85%.', { variant: 'warning' });
|
|
48
|
+
const id3 = toast.push('Connection lost.', { variant: 'error' });
|
|
49
|
+
|
|
50
|
+
// AC 39: all items render
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
const statuses = canvas.getAllByRole('status');
|
|
53
|
+
expect(statuses.length).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// AC 16+17: success and warning get role=status
|
|
57
|
+
const statuses = canvas.getAllByRole('status');
|
|
58
|
+
await expect(statuses.length).toBe(2);
|
|
59
|
+
|
|
60
|
+
// AC 18: danger gets role=alert
|
|
61
|
+
const alerts = canvas.getAllByRole('alert');
|
|
62
|
+
await expect(alerts.length).toBe(1);
|
|
63
|
+
|
|
64
|
+
// AC 26: all three messages visible
|
|
65
|
+
await expect(canvas.getByText('Build passed.')).toBeVisible();
|
|
66
|
+
await expect(canvas.getByText('Memory at 85%.')).toBeVisible();
|
|
67
|
+
await expect(canvas.getByText('Connection lost.')).toBeVisible();
|
|
68
|
+
|
|
69
|
+
// Cleanup (AC 73)
|
|
70
|
+
toast.dismiss(id1);
|
|
71
|
+
toast.dismiss(id2);
|
|
72
|
+
toast.dismiss(id3);
|
|
73
|
+
}}>
|
|
74
|
+
<ToastRegion position="bottom-right" />
|
|
75
|
+
</Story>
|
|
76
|
+
|
|
77
|
+
<!-- AC 42 — with limit=3, pushing 5 toasts shows only the newest 3 -->
|
|
78
|
+
<Story name="Stack Limit"
|
|
79
|
+
play={async ({ canvasElement }) => {
|
|
80
|
+
const canvas = within(canvasElement);
|
|
81
|
+
|
|
82
|
+
// Push 5 toasts
|
|
83
|
+
const ids = [
|
|
84
|
+
toast.push('Toast 1', { variant: 'success' }),
|
|
85
|
+
toast.push('Toast 2', { variant: 'success' }),
|
|
86
|
+
toast.push('Toast 3', { variant: 'success' }),
|
|
87
|
+
toast.push('Toast 4', { variant: 'success' }),
|
|
88
|
+
toast.push('Toast 5', { variant: 'success' }),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// AC 42: only 3 toasts are visible (oldest 2 dismissed by limit enforcement)
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
const visible = canvas.getAllByRole('status');
|
|
94
|
+
expect(visible.length).toBe(3);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// The newest 3 messages should be present
|
|
98
|
+
await expect(canvas.getByText('Toast 3')).toBeVisible();
|
|
99
|
+
await expect(canvas.getByText('Toast 4')).toBeVisible();
|
|
100
|
+
await expect(canvas.getByText('Toast 5')).toBeVisible();
|
|
101
|
+
|
|
102
|
+
// The oldest 2 should have been dismissed from the DOM
|
|
103
|
+
await expect(canvas.queryByText('Toast 1')).toBeNull();
|
|
104
|
+
await expect(canvas.queryByText('Toast 2')).toBeNull();
|
|
105
|
+
|
|
106
|
+
// Cleanup remaining (Toast 1+2 already dismissed by limit enforcement)
|
|
107
|
+
toast.dismiss(ids[2]);
|
|
108
|
+
toast.dismiss(ids[3]);
|
|
109
|
+
toast.dismiss(ids[4]);
|
|
110
|
+
}}>
|
|
111
|
+
<ToastRegion position="bottom-right" limit={3} />
|
|
112
|
+
</Story>
|
|
113
|
+
|
|
114
|
+
<!-- AC 44 — toast with duration:1000 auto-dismisses after ~1000ms -->
|
|
115
|
+
<Story name="Auto-Dismiss"
|
|
116
|
+
play={async ({ canvasElement }) => {
|
|
117
|
+
const canvas = within(canvasElement);
|
|
118
|
+
|
|
119
|
+
// AC 44: push with short duration
|
|
120
|
+
const id = toast.push('Short-lived.', { variant: 'success', duration: 1000 });
|
|
121
|
+
|
|
122
|
+
// Immediately visible
|
|
123
|
+
await waitFor(() => expect(canvas.getByText('Short-lived.')).toBeVisible());
|
|
124
|
+
|
|
125
|
+
// AC 44: wait 1200ms — toast should have been auto-dismissed
|
|
126
|
+
await new Promise((r) => setTimeout(r, 1200));
|
|
127
|
+
|
|
128
|
+
await expect(canvas.queryByText('Short-lived.')).toBeNull();
|
|
129
|
+
|
|
130
|
+
// Cleanup (should already be gone but dismiss is a no-op if not present — AC 13)
|
|
131
|
+
toast.dismiss(id);
|
|
132
|
+
}}>
|
|
133
|
+
<ToastRegion position="bottom-right" />
|
|
134
|
+
</Story>
|
|
135
|
+
|
|
136
|
+
<!-- AC 41, 45, 46 — manual close removes toast and cancels its auto-dismiss timer -->
|
|
137
|
+
<Story name="Manual Dismiss via Button"
|
|
138
|
+
play={async ({ canvasElement }) => {
|
|
139
|
+
const canvas = within(canvasElement);
|
|
140
|
+
|
|
141
|
+
// AC 46: push with a real timer so we can verify cancellation
|
|
142
|
+
const id = toast.push('Manual dismiss only.', { variant: 'warning', duration: 800 });
|
|
143
|
+
|
|
144
|
+
await waitFor(() => expect(canvas.getByText('Manual dismiss only.')).toBeVisible());
|
|
145
|
+
|
|
146
|
+
// AC 41: clicking close button removes the toast
|
|
147
|
+
const closeBtn = canvas.getByRole('button', { name: /Dismiss notification/i });
|
|
148
|
+
await expect(closeBtn).toBeVisible();
|
|
149
|
+
closeBtn.click();
|
|
150
|
+
|
|
151
|
+
// Toast dismissed immediately
|
|
152
|
+
await waitFor(() => expect(canvas.queryByText('Manual dismiss only.')).toBeNull());
|
|
153
|
+
|
|
154
|
+
// AC 46: wait past the original 800ms duration — toast must not reappear
|
|
155
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
156
|
+
await expect(canvas.queryByText('Manual dismiss only.')).toBeNull();
|
|
157
|
+
|
|
158
|
+
// Cleanup (no-op — already gone)
|
|
159
|
+
toast.dismiss(id);
|
|
160
|
+
}}>
|
|
161
|
+
<ToastRegion position="bottom-right" />
|
|
162
|
+
</Story>
|
|
163
|
+
|
|
164
|
+
<!-- AC 48, 51 — position="top-left" renders .toast-region--top-left -->
|
|
165
|
+
<Story name="Position Top-Left"
|
|
166
|
+
play={async ({ canvasElement }) => {
|
|
167
|
+
const canvas = within(canvasElement);
|
|
168
|
+
|
|
169
|
+
const id = toast.push('Top-left toast.', { variant: 'success' });
|
|
170
|
+
|
|
171
|
+
await waitFor(() => expect(canvas.getByText('Top-left toast.')).toBeVisible());
|
|
172
|
+
|
|
173
|
+
// AC 51: correct position class applied
|
|
174
|
+
const region = canvasElement.querySelector('.toast-region--top-left');
|
|
175
|
+
await expect(region).not.toBeNull();
|
|
176
|
+
|
|
177
|
+
// Cleanup (AC 73)
|
|
178
|
+
toast.dismiss(id);
|
|
179
|
+
}}>
|
|
180
|
+
<ToastRegion position="top-left" />
|
|
181
|
+
</Story>
|
|
182
|
+
|
|
183
|
+
<!-- AC 37 — when no toasts are pushed the region is empty (or SSR-unrendered) -->
|
|
184
|
+
<Story name="No Toasts"
|
|
185
|
+
play={async ({ canvasElement }) => {
|
|
186
|
+
// AC 37: no toast elements rendered when store is empty
|
|
187
|
+
// The region may or may not be present (mounted guard), but no role=status/alert
|
|
188
|
+
await expect(
|
|
189
|
+
canvasElement.querySelectorAll('[role="status"], [role="alert"]').length
|
|
190
|
+
).toBe(0);
|
|
191
|
+
}}>
|
|
192
|
+
<ToastRegion position="bottom-right" />
|
|
193
|
+
</Story>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import ToastRegion from "./ToastRegion.svelte";
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const ToastRegion: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
+
[evt: string]: CustomEvent<any>;
|
|
17
|
+
}, {}, {}, string>;
|
|
18
|
+
type ToastRegion = InstanceType<typeof ToastRegion>;
|
|
19
|
+
export default ToastRegion;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { toast } from '../../stores/toast.js'
|
|
3
|
+
import Toast from './Toast.svelte'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Viewport corner where the toast stack is anchored. @default 'bottom-right' */
|
|
7
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
|
|
8
|
+
/** Maximum toasts visible at once — oldest are dismissed when exceeded. @default 5 */
|
|
9
|
+
limit?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { position = 'bottom-right', limit = 5 }: Props = $props()
|
|
13
|
+
|
|
14
|
+
let mounted = $state(false)
|
|
15
|
+
|
|
16
|
+
$effect(() => {
|
|
17
|
+
mounted = true
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Dismiss oldest items when store exceeds limit
|
|
21
|
+
$effect(() => {
|
|
22
|
+
const all = $toast
|
|
23
|
+
if (all.length > limit) {
|
|
24
|
+
const toRemove = all.slice(0, all.length - limit)
|
|
25
|
+
toRemove.forEach((t) => toast.dismiss(t.id))
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
let visibleToasts = $derived($toast.slice(-limit))
|
|
30
|
+
|
|
31
|
+
function handleDismiss(id: string) {
|
|
32
|
+
toast.dismiss(id)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Auto-dismiss timers — recreated whenever visibleToasts changes
|
|
36
|
+
$effect(() => {
|
|
37
|
+
const timers: ReturnType<typeof setTimeout>[] = []
|
|
38
|
+
for (const item of visibleToasts) {
|
|
39
|
+
if (item.duration > 0) {
|
|
40
|
+
const t = setTimeout(() => toast.dismiss(item.id), item.duration)
|
|
41
|
+
timers.push(t)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return () => timers.forEach(clearTimeout)
|
|
45
|
+
})
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
{#if mounted}
|
|
49
|
+
<div
|
|
50
|
+
class="toast-region toast-region--{position}"
|
|
51
|
+
role="region"
|
|
52
|
+
aria-label="Notifications"
|
|
53
|
+
>
|
|
54
|
+
{#each visibleToasts as item (item.id)}
|
|
55
|
+
<Toast
|
|
56
|
+
id={item.id}
|
|
57
|
+
message={item.message}
|
|
58
|
+
variant={item.variant}
|
|
59
|
+
ondismiss={handleDismiss}
|
|
60
|
+
/>
|
|
61
|
+
{/each}
|
|
62
|
+
</div>
|
|
63
|
+
{/if}
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
.toast-region {
|
|
67
|
+
position: fixed;
|
|
68
|
+
z-index: 9000;
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
gap: 8px;
|
|
72
|
+
padding: 16px;
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
max-width: 440px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.toast-region--bottom-right {
|
|
78
|
+
bottom: 0;
|
|
79
|
+
right: 0;
|
|
80
|
+
align-items: flex-end;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.toast-region--bottom-left {
|
|
84
|
+
bottom: 0;
|
|
85
|
+
left: 0;
|
|
86
|
+
align-items: flex-start;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.toast-region--top-right {
|
|
90
|
+
top: 0;
|
|
91
|
+
right: 0;
|
|
92
|
+
align-items: flex-end;
|
|
93
|
+
flex-direction: column-reverse;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.toast-region--top-left {
|
|
97
|
+
top: 0;
|
|
98
|
+
left: 0;
|
|
99
|
+
align-items: flex-start;
|
|
100
|
+
flex-direction: column-reverse;
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Viewport corner where the toast stack is anchored. @default 'bottom-right' */
|
|
3
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
4
|
+
/** Maximum toasts visible at once — oldest are dismissed when exceeded. @default 5 */
|
|
5
|
+
limit?: number;
|
|
6
|
+
}
|
|
7
|
+
declare const ToastRegion: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type ToastRegion = ReturnType<typeof ToastRegion>;
|
|
9
|
+
export default ToastRegion;
|