@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.
Files changed (208) hide show
  1. package/README.md +94 -0
  2. package/dist/components/cards/Card.stories.svelte +82 -0
  3. package/dist/components/cards/Card.stories.svelte.d.ts +19 -0
  4. package/dist/components/cards/Card.svelte +28 -0
  5. package/dist/components/cards/Card.svelte.d.ts +12 -0
  6. package/dist/components/cards/NoteCard.stories.svelte +94 -0
  7. package/dist/components/cards/NoteCard.stories.svelte.d.ts +19 -0
  8. package/dist/components/cards/NoteCard.svelte +89 -0
  9. package/dist/components/cards/NoteCard.svelte.d.ts +18 -0
  10. package/dist/components/cards/ProductCard.stories.svelte +98 -0
  11. package/dist/components/cards/ProductCard.stories.svelte.d.ts +19 -0
  12. package/dist/components/cards/ProductCard.svelte +150 -0
  13. package/dist/components/cards/ProductCard.svelte.d.ts +22 -0
  14. package/dist/components/cards/ProjectCard.stories.svelte +88 -0
  15. package/dist/components/cards/ProjectCard.stories.svelte.d.ts +19 -0
  16. package/dist/components/cards/ProjectCard.svelte +109 -0
  17. package/dist/components/cards/ProjectCard.svelte.d.ts +20 -0
  18. package/dist/components/cards/index.d.ts +4 -0
  19. package/dist/components/cards/index.js +4 -0
  20. package/dist/components/data/Accordion.stories.svelte +316 -0
  21. package/dist/components/data/Accordion.stories.svelte.d.ts +19 -0
  22. package/dist/components/data/Accordion.svelte +23 -0
  23. package/dist/components/data/Accordion.svelte.d.ts +9 -0
  24. package/dist/components/data/AccordionItem.svelte +112 -0
  25. package/dist/components/data/AccordionItem.svelte.d.ts +11 -0
  26. package/dist/components/data/Table.composition.stories.svelte +67 -0
  27. package/dist/components/data/Table.composition.stories.svelte.d.ts +19 -0
  28. package/dist/components/data/Table.stories.svelte +137 -0
  29. package/dist/components/data/Table.stories.svelte.d.ts +19 -0
  30. package/dist/components/data/Table.svelte +83 -0
  31. package/dist/components/data/Table.svelte.d.ts +14 -0
  32. package/dist/components/data/Tabs.stories.svelte +386 -0
  33. package/dist/components/data/Tabs.stories.svelte.d.ts +19 -0
  34. package/dist/components/data/Tabs.svelte +142 -0
  35. package/dist/components/data/Tabs.svelte.d.ts +19 -0
  36. package/dist/components/data/index.d.ts +4 -0
  37. package/dist/components/data/index.js +4 -0
  38. package/dist/components/feedback/Modal.stories.svelte +192 -0
  39. package/dist/components/feedback/Modal.stories.svelte.d.ts +4 -0
  40. package/dist/components/feedback/Modal.svelte +185 -0
  41. package/dist/components/feedback/Modal.svelte.d.ts +19 -0
  42. package/dist/components/feedback/Toast.stories.svelte +203 -0
  43. package/dist/components/feedback/Toast.stories.svelte.d.ts +19 -0
  44. package/dist/components/feedback/Toast.svelte +109 -0
  45. package/dist/components/feedback/Toast.svelte.d.ts +15 -0
  46. package/dist/components/feedback/ToastRegion.stories.svelte +193 -0
  47. package/dist/components/feedback/ToastRegion.stories.svelte.d.ts +19 -0
  48. package/dist/components/feedback/ToastRegion.svelte +102 -0
  49. package/dist/components/feedback/ToastRegion.svelte.d.ts +9 -0
  50. package/dist/components/feedback/index.d.ts +3 -0
  51. package/dist/components/feedback/index.js +3 -0
  52. package/dist/components/forms/Checkbox.stories.svelte +103 -0
  53. package/dist/components/forms/Checkbox.stories.svelte.d.ts +19 -0
  54. package/dist/components/forms/Checkbox.svelte +150 -0
  55. package/dist/components/forms/Checkbox.svelte.d.ts +11 -0
  56. package/dist/components/forms/Field.stories.svelte +113 -0
  57. package/dist/components/forms/Field.stories.svelte.d.ts +19 -0
  58. package/dist/components/forms/Field.svelte +77 -0
  59. package/dist/components/forms/Field.svelte.d.ts +17 -0
  60. package/dist/components/forms/Input.stories.svelte +58 -0
  61. package/dist/components/forms/Input.stories.svelte.d.ts +19 -0
  62. package/dist/components/forms/Input.svelte +64 -0
  63. package/dist/components/forms/Input.svelte.d.ts +9 -0
  64. package/dist/components/forms/InputWrap.composition.stories.svelte +32 -0
  65. package/dist/components/forms/InputWrap.composition.stories.svelte.d.ts +19 -0
  66. package/dist/components/forms/InputWrap.stories.svelte +53 -0
  67. package/dist/components/forms/InputWrap.stories.svelte.d.ts +19 -0
  68. package/dist/components/forms/InputWrap.svelte +128 -0
  69. package/dist/components/forms/InputWrap.svelte.d.ts +21 -0
  70. package/dist/components/forms/Radio.stories.svelte +70 -0
  71. package/dist/components/forms/Radio.stories.svelte.d.ts +19 -0
  72. package/dist/components/forms/Radio.svelte +109 -0
  73. package/dist/components/forms/Radio.svelte.d.ts +9 -0
  74. package/dist/components/forms/RadioGroup.stories.svelte +115 -0
  75. package/dist/components/forms/RadioGroup.stories.svelte.d.ts +19 -0
  76. package/dist/components/forms/RadioGroup.svelte +116 -0
  77. package/dist/components/forms/RadioGroup.svelte.d.ts +24 -0
  78. package/dist/components/forms/Select.stories.svelte +168 -0
  79. package/dist/components/forms/Select.stories.svelte.d.ts +19 -0
  80. package/dist/components/forms/Select.svelte +262 -0
  81. package/dist/components/forms/Select.svelte.d.ts +23 -0
  82. package/dist/components/forms/Switch.stories.svelte +86 -0
  83. package/dist/components/forms/Switch.stories.svelte.d.ts +19 -0
  84. package/dist/components/forms/Switch.svelte +113 -0
  85. package/dist/components/forms/Switch.svelte.d.ts +11 -0
  86. package/dist/components/forms/Textarea.stories.svelte +40 -0
  87. package/dist/components/forms/Textarea.stories.svelte.d.ts +19 -0
  88. package/dist/components/forms/Textarea.svelte +66 -0
  89. package/dist/components/forms/Textarea.svelte.d.ts +9 -0
  90. package/dist/components/forms/field-context.d.ts +7 -0
  91. package/dist/components/forms/field-context.js +1 -0
  92. package/dist/components/forms/index.d.ts +9 -0
  93. package/dist/components/forms/index.js +9 -0
  94. package/dist/components/layout/Container.stories.svelte +67 -0
  95. package/dist/components/layout/Container.stories.svelte.d.ts +19 -0
  96. package/dist/components/layout/Container.svelte +52 -0
  97. package/dist/components/layout/Container.svelte.d.ts +14 -0
  98. package/dist/components/layout/Grid.stories.svelte +109 -0
  99. package/dist/components/layout/Grid.stories.svelte.d.ts +19 -0
  100. package/dist/components/layout/Grid.svelte +54 -0
  101. package/dist/components/layout/Grid.svelte.d.ts +19 -0
  102. package/dist/components/layout/Inline.stories.svelte +136 -0
  103. package/dist/components/layout/Inline.stories.svelte.d.ts +19 -0
  104. package/dist/components/layout/Inline.svelte +46 -0
  105. package/dist/components/layout/Inline.svelte.d.ts +19 -0
  106. package/dist/components/layout/Prose.stories.svelte +423 -0
  107. package/dist/components/layout/Prose.stories.svelte.d.ts +19 -0
  108. package/dist/components/layout/Prose.svelte +176 -0
  109. package/dist/components/layout/Prose.svelte.d.ts +12 -0
  110. package/dist/components/layout/Rule.stories.svelte +80 -0
  111. package/dist/components/layout/Rule.stories.svelte.d.ts +19 -0
  112. package/dist/components/layout/Rule.svelte +33 -0
  113. package/dist/components/layout/Rule.svelte.d.ts +9 -0
  114. package/dist/components/layout/Spread.stories.svelte +118 -0
  115. package/dist/components/layout/Spread.stories.svelte.d.ts +19 -0
  116. package/dist/components/layout/Spread.svelte +38 -0
  117. package/dist/components/layout/Spread.svelte.d.ts +16 -0
  118. package/dist/components/layout/Stack.stories.svelte +90 -0
  119. package/dist/components/layout/Stack.stories.svelte.d.ts +19 -0
  120. package/dist/components/layout/Stack.svelte +37 -0
  121. package/dist/components/layout/Stack.svelte.d.ts +16 -0
  122. package/dist/components/layout/index.d.ts +7 -0
  123. package/dist/components/layout/index.js +7 -0
  124. package/dist/components/navigation/Breadcrumb.stories.svelte +122 -0
  125. package/dist/components/navigation/Breadcrumb.stories.svelte.d.ts +19 -0
  126. package/dist/components/navigation/Breadcrumb.svelte +70 -0
  127. package/dist/components/navigation/Breadcrumb.svelte.d.ts +13 -0
  128. package/dist/components/navigation/Nav.stories.svelte +323 -0
  129. package/dist/components/navigation/Nav.stories.svelte.d.ts +19 -0
  130. package/dist/components/navigation/Nav.svelte +257 -0
  131. package/dist/components/navigation/Nav.svelte.d.ts +21 -0
  132. package/dist/components/navigation/index.d.ts +2 -0
  133. package/dist/components/navigation/index.js +2 -0
  134. package/dist/components/patterns/ActivityRow.stories.svelte +45 -0
  135. package/dist/components/patterns/ActivityRow.stories.svelte.d.ts +19 -0
  136. package/dist/components/patterns/ActivityRow.svelte +69 -0
  137. package/dist/components/patterns/ActivityRow.svelte.d.ts +16 -0
  138. package/dist/components/patterns/Alert.stories.svelte +63 -0
  139. package/dist/components/patterns/Alert.stories.svelte.d.ts +19 -0
  140. package/dist/components/patterns/Alert.svelte +91 -0
  141. package/dist/components/patterns/Alert.svelte.d.ts +16 -0
  142. package/dist/components/patterns/CtaBlock.stories.svelte +62 -0
  143. package/dist/components/patterns/CtaBlock.stories.svelte.d.ts +19 -0
  144. package/dist/components/patterns/CtaBlock.svelte +80 -0
  145. package/dist/components/patterns/CtaBlock.svelte.d.ts +16 -0
  146. package/dist/components/patterns/KvList.stories.svelte +48 -0
  147. package/dist/components/patterns/KvList.stories.svelte.d.ts +19 -0
  148. package/dist/components/patterns/KvList.svelte +65 -0
  149. package/dist/components/patterns/KvList.svelte.d.ts +15 -0
  150. package/dist/components/patterns/PageHero.stories.svelte +62 -0
  151. package/dist/components/patterns/PageHero.stories.svelte.d.ts +19 -0
  152. package/dist/components/patterns/PageHero.svelte +62 -0
  153. package/dist/components/patterns/PageHero.svelte.d.ts +14 -0
  154. package/dist/components/patterns/ProgressBar.stories.svelte +83 -0
  155. package/dist/components/patterns/ProgressBar.stories.svelte.d.ts +19 -0
  156. package/dist/components/patterns/ProgressBar.svelte +71 -0
  157. package/dist/components/patterns/ProgressBar.svelte.d.ts +13 -0
  158. package/dist/components/patterns/SectionFoot.stories.svelte +37 -0
  159. package/dist/components/patterns/SectionFoot.stories.svelte.d.ts +19 -0
  160. package/dist/components/patterns/SectionFoot.svelte +70 -0
  161. package/dist/components/patterns/SectionFoot.svelte.d.ts +15 -0
  162. package/dist/components/patterns/SectionHead.stories.svelte +67 -0
  163. package/dist/components/patterns/SectionHead.stories.svelte.d.ts +19 -0
  164. package/dist/components/patterns/SectionHead.svelte +54 -0
  165. package/dist/components/patterns/SectionHead.svelte.d.ts +14 -0
  166. package/dist/components/patterns/StatCard.stories.svelte +59 -0
  167. package/dist/components/patterns/StatCard.stories.svelte.d.ts +19 -0
  168. package/dist/components/patterns/StatCard.svelte +57 -0
  169. package/dist/components/patterns/StatCard.svelte.d.ts +15 -0
  170. package/dist/components/patterns/index.d.ts +9 -0
  171. package/dist/components/patterns/index.js +9 -0
  172. package/dist/components/primitives/Button.stories.svelte +132 -0
  173. package/dist/components/primitives/Button.stories.svelte.d.ts +19 -0
  174. package/dist/components/primitives/Button.svelte +142 -0
  175. package/dist/components/primitives/Button.svelte.d.ts +16 -0
  176. package/dist/components/primitives/Heading.stories.svelte +137 -0
  177. package/dist/components/primitives/Heading.stories.svelte.d.ts +19 -0
  178. package/dist/components/primitives/Heading.svelte +107 -0
  179. package/dist/components/primitives/Heading.svelte.d.ts +23 -0
  180. package/dist/components/primitives/Led.stories.svelte +63 -0
  181. package/dist/components/primitives/Led.stories.svelte.d.ts +19 -0
  182. package/dist/components/primitives/Led.svelte +65 -0
  183. package/dist/components/primitives/Led.svelte.d.ts +11 -0
  184. package/dist/components/primitives/TagPill.stories.svelte +90 -0
  185. package/dist/components/primitives/TagPill.stories.svelte.d.ts +19 -0
  186. package/dist/components/primitives/TagPill.svelte +44 -0
  187. package/dist/components/primitives/TagPill.svelte.d.ts +9 -0
  188. package/dist/components/primitives/Text.stories.svelte +252 -0
  189. package/dist/components/primitives/Text.stories.svelte.d.ts +19 -0
  190. package/dist/components/primitives/Text.svelte +101 -0
  191. package/dist/components/primitives/Text.svelte.d.ts +25 -0
  192. package/dist/components/primitives/index.d.ts +5 -0
  193. package/dist/components/primitives/index.js +5 -0
  194. package/dist/index.d.ts +10 -0
  195. package/dist/index.js +10 -0
  196. package/dist/stores/toast.d.ts +19 -0
  197. package/dist/stores/toast.js +22 -0
  198. package/dist/storybook-utils.d.ts +11 -0
  199. package/dist/storybook-utils.js +29 -0
  200. package/dist/tokens/ColorSwatch.svelte +73 -0
  201. package/dist/tokens/ColorSwatch.svelte.d.ts +10 -0
  202. package/dist/tokens/layout.css +144 -0
  203. package/dist/tokens/patterns.css +281 -0
  204. package/dist/tokens/tokens.css +96 -0
  205. package/dist/tokens/tokens.stories.svelte +107 -0
  206. package/dist/tokens/tokens.stories.svelte.d.ts +18 -0
  207. package/dist/tokens/typography.css +159 -0
  208. 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;
@@ -0,0 +1,3 @@
1
+ export { default as Modal } from './Modal.svelte';
2
+ export { default as Toast } from './Toast.svelte';
3
+ export { default as ToastRegion } from './ToastRegion.svelte';
@@ -0,0 +1,3 @@
1
+ export { default as Modal } from './Modal.svelte';
2
+ export { default as Toast } from './Toast.svelte';
3
+ export { default as ToastRegion } from './ToastRegion.svelte';