srcdev-nuxt-components 9.1.55 → 9.1.57

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 (104) hide show
  1. package/.claude/settings.json +30 -2
  2. package/.claude/skills/components/alert-content-inner.md +60 -0
  3. package/.claude/skills/components/display-dialog.md +80 -0
  4. package/.claude/skills/components/display-prompt.md +163 -0
  5. package/.claude/skills/components/display-toast.md +235 -0
  6. package/.claude/skills/index.md +7 -3
  7. package/.claude/skills/theming-colour-ramps.md +328 -0
  8. package/.claude/skills/theming-override-default.md +115 -186
  9. package/.claude/skills/theming-partial-override.md +88 -75
  10. package/README.md +166 -0
  11. package/app/app.config.ts +44 -0
  12. package/app/assets/styles/setup/01.config/_head.css +3 -3
  13. package/app/assets/styles/setup/02.colours/_amber.css +12 -10
  14. package/app/assets/styles/setup/02.colours/_blue.css +10 -9
  15. package/app/assets/styles/setup/02.colours/_green.css +12 -11
  16. package/app/assets/styles/setup/02.colours/_orange.css +12 -10
  17. package/app/assets/styles/setup/02.colours/_red.css +10 -9
  18. package/app/assets/styles/setup/02.colours/_slate.css +12 -11
  19. package/app/assets/styles/setup/02.colours/_sunset.css +12 -10
  20. package/app/assets/styles/setup/02.colours/_theme-params.css +18 -0
  21. package/app/assets/styles/setup/02.colours/index.css +1 -0
  22. package/app/assets/styles/setup/03.theming/_default.css +87 -0
  23. package/app/assets/styles/setup/03.theming/_error.css +14 -0
  24. package/app/assets/styles/setup/03.theming/_success.css +4 -0
  25. package/app/assets/styles/setup/03.theming/_theme-slots.css +16 -0
  26. package/app/assets/styles/setup/03.theming/_warning.css +13 -0
  27. package/app/assets/styles/setup/03.theming/index.css +6 -4
  28. package/app/assets/styles/setup/03.theming/theme-ramp.css +19 -0
  29. package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +0 -7
  30. package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
  31. package/app/assets/styles/setup/index.css +1 -1
  32. package/app/components/01.atoms/display-dialog/DisplayDialog.vue +74 -24
  33. package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +148 -14
  34. package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +3 -3
  35. package/app/components/01.atoms/prompt/DisplayPrompt.vue +113 -0
  36. package/app/components/01.atoms/prompt/stories/DisplayPrompt.stories.ts +112 -0
  37. package/app/components/01.atoms/prompt/tests/DisplayPrompt.spec.ts +205 -0
  38. package/app/components/01.atoms/toast/DisplayToast.vue +327 -0
  39. package/app/components/01.atoms/toast/DisplayToastProvider.vue +362 -0
  40. package/app/components/01.atoms/toast/stories/DisplayToast.stories.ts +195 -0
  41. package/app/components/01.atoms/toast/stories/DisplayToastProvider.stories.ts +142 -0
  42. package/app/components/01.atoms/toast/tests/DisplayToast.spec.ts +266 -0
  43. package/app/components/01.atoms/toast/tests/DisplayToastProvider.spec.ts +256 -0
  44. package/app/components/02.molecules/action-menu/ActionMenu.vue +8 -1
  45. package/app/components/02.molecules/action-menu/ActionMenuItemCore.vue +11 -3
  46. package/app/components/02.molecules/alert-content/AlertContent.vue +67 -0
  47. package/app/components/02.molecules/alert-content/AlertContentInner.vue +153 -0
  48. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +131 -0
  49. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +135 -0
  50. package/app/components/02.molecules/alert-content/tests/AlertContentInner.spec.ts +135 -0
  51. package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +78 -0
  52. package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +132 -0
  53. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +149 -0
  54. package/app/components/05.forms/input-button/InputButtonCore.vue +35 -39
  55. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +8 -8
  56. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioCore.vue +5 -5
  57. package/app/components/05.forms/input-copy/InputCopyCore.vue +7 -7
  58. package/app/components/05.forms/input-number/InputNumberCore.vue +5 -5
  59. package/app/components/05.forms/input-select/InputSelectCore.vue +6 -6
  60. package/app/components/05.forms/input-text/InputTextCore.vue +6 -6
  61. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +2 -2
  62. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +5 -5
  63. package/app/components/05.forms/pending-effect/PendingEffect.vue +60 -60
  64. package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +10 -10
  65. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +4 -4
  66. package/app/components/display-theme-switch/DisplayThemeSwitch.vue +4 -4
  67. package/app/components/display-tooltip/DisplayTooltip.vue +4 -4
  68. package/app/composables/useBodyLock.ts +21 -0
  69. package/app/composables/useDialogControls.ts +11 -11
  70. package/app/composables/useToastQueue.ts +39 -0
  71. package/app/layouts/default.vue +1 -1
  72. package/app/pages/ui/display-dialog.vue +275 -127
  73. package/app/pages/ui/display-prompt.vue +255 -52
  74. package/app/pages/ui/display-toast.vue +278 -299
  75. package/app/pages/ui/mask-element.vue +25 -3
  76. package/app/pages/ui/settings.vue +9 -35
  77. package/app/types/app-config.d.ts +41 -0
  78. package/app/types/components/display-prompt.d.ts +11 -0
  79. package/app/types/components/display-toast.d.ts +11 -9
  80. package/app/types/components/index.ts +2 -0
  81. package/app/types/components/semantic-theme.d.ts +1 -0
  82. package/nuxt.config.ts +16 -0
  83. package/package.json +8 -4
  84. package/ramps.config.mjs +28 -0
  85. package/scripts/check-ramps.mjs +50 -0
  86. package/scripts/generate-consumer-ramps.mjs +100 -0
  87. package/scripts/generate-ramps.mjs +92 -0
  88. package/app/assets/styles/setup/03.theming/default/_dark.css +0 -156
  89. package/app/assets/styles/setup/03.theming/default/_light.css +0 -166
  90. package/app/assets/styles/setup/03.theming/default/index.css +0 -2
  91. package/app/assets/styles/setup/03.theming/error/_dark.css +0 -56
  92. package/app/assets/styles/setup/03.theming/error/_light.css +0 -74
  93. package/app/assets/styles/setup/03.theming/error/index.css +0 -2
  94. package/app/assets/styles/setup/03.theming/success/_dark.css +0 -50
  95. package/app/assets/styles/setup/03.theming/success/_light.css +0 -65
  96. package/app/assets/styles/setup/03.theming/success/index.css +0 -2
  97. package/app/assets/styles/setup/03.theming/warning/_dark.css +0 -50
  98. package/app/assets/styles/setup/03.theming/warning/_light.css +0 -64
  99. package/app/assets/styles/setup/03.theming/warning/index.css +0 -2
  100. package/app/components/display-prompt/DisplayPromptCore.vue +0 -228
  101. package/app/components/display-prompt/variants/DisplayPromptError.vue +0 -53
  102. package/app/components/display-toast/DisplayToast.vue +0 -447
  103. package/app/components/display-toast/molecules/DefaultToastContent.vue +0 -163
  104. package/app/components/display-toast/stories/DisplayToast.stories.ts +0 -380
@@ -0,0 +1,362 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <TransitionGroup
4
+ tag="div"
5
+ class="display-toast-provider"
6
+ :class="[position, fullWidth ? 'full-width' : alignment]"
7
+ @before-leave="onBeforeLeave"
8
+ @after-leave="onAfterLeave"
9
+ @after-enter="onAfterEnter"
10
+ >
11
+ <div
12
+ v-for="entry in visibleEntries"
13
+ :key="entry.id"
14
+ class="display-toast-provider-item"
15
+ :style="{
16
+ '--_reveal': revealDurationFor(entry) + 'ms',
17
+ '--_duration': displayDurationFor(entry) + 'ms',
18
+ }"
19
+ :data-theme="themeFor(entry)"
20
+ :role="roleFor(entry)"
21
+ :aria-live="ariaLiveFor(entry)"
22
+ tabindex="0"
23
+ :aria-describedby="'toast-message-' + entry.id"
24
+ @keydown.escape="handleDismiss(entry.id)"
25
+ >
26
+ <component
27
+ :is="maskedFor(entry) ? AlertMaskedContent : AlertContent"
28
+ :theme="themeFor(entry)"
29
+ :custom-icon="entry.config.content?.customIcon"
30
+ :content-id="'toast-message-' + entry.id"
31
+ :dismissible="!autoDismissFor(entry)"
32
+ @dismiss="handleDismiss(entry.id)"
33
+ >
34
+ <template v-if="entry.config.content?.title || entry.config.content?.text" #title>
35
+ {{ entry.config.content?.title || entry.config.content?.text }}
36
+ </template>
37
+ <template v-if="entry.config.content?.description" #content>
38
+ {{ entry.config.content?.description }}
39
+ </template>
40
+ </component>
41
+ <div v-if="autoDismissFor(entry)" class="display-toast-provider-progress"></div>
42
+ </div>
43
+ </TransitionGroup>
44
+ </Teleport>
45
+ </template>
46
+
47
+ <script setup lang="ts">
48
+ import AlertContent from "~/components/02.molecules/alert-content/AlertContent.vue";
49
+ import AlertMaskedContent from "~/components/02.molecules/alert-masked-content/AlertMaskedContent.vue";
50
+ import type {
51
+ DisplayToastTheme,
52
+ DisplayToastPosition,
53
+ DisplayToastAlignment,
54
+ ToastQueueEntry,
55
+ } from "~/types/components";
56
+
57
+ interface Props {
58
+ position?: DisplayToastPosition;
59
+ alignment?: DisplayToastAlignment;
60
+ fullWidth?: boolean;
61
+ maxVisible?: number;
62
+ }
63
+
64
+ const props = withDefaults(defineProps<Props>(), {
65
+ position: "top",
66
+ alignment: "right",
67
+ fullWidth: false,
68
+ maxVisible: 1,
69
+ });
70
+
71
+ const { queue, promote, dismiss } = useToastQueueProvider();
72
+
73
+ const visibleEntries = computed<ToastQueueEntry[]>(() =>
74
+ (queue.value as ToastQueueEntry[]).filter((e) => e.status === "visible")
75
+ );
76
+
77
+ const themeFor = (entry: ToastQueueEntry): DisplayToastTheme => entry.config.appearance?.theme ?? "info";
78
+ const maskedFor = (entry: ToastQueueEntry) => entry.config.appearance?.masked ?? false;
79
+ const autoDismissFor = (entry: ToastQueueEntry) => entry.config.behavior?.autoDismiss ?? true;
80
+ const displayDurationFor = (entry: ToastQueueEntry) => entry.config.behavior?.duration ?? 5000;
81
+ const revealDurationFor = (entry: ToastQueueEntry) => entry.config.behavior?.revealDuration ?? 550;
82
+ const roleFor = (entry: ToastQueueEntry) => (["error", "warning"].includes(themeFor(entry)) ? "alert" : "status");
83
+ const ariaLiveFor = (entry: ToastQueueEntry) =>
84
+ ["error", "warning"].includes(themeFor(entry)) ? "assertive" : "polite";
85
+
86
+ const timers = new Map<string, ReturnType<typeof setTimeout>>();
87
+
88
+ const handleDismiss = (id: string) => {
89
+ const timer = timers.get(id);
90
+ if (timer !== undefined) {
91
+ clearTimeout(timer);
92
+ timers.delete(id);
93
+ }
94
+ dismiss(id);
95
+ };
96
+
97
+ const startTimer = (entry: ToastQueueEntry) => {
98
+ if (!autoDismissFor(entry)) return;
99
+ const timer = setTimeout(() => {
100
+ timers.delete(entry.id);
101
+ dismiss(entry.id);
102
+ }, displayDurationFor(entry));
103
+ timers.set(entry.id, timer);
104
+ };
105
+
106
+ watch(
107
+ queue,
108
+ (newQueue) => {
109
+ const entries = newQueue as ToastQueueEntry[];
110
+
111
+ // Clear timers for entries that no longer exist (e.g. clear()).
112
+ const entryIds = new Set(entries.map((e) => e.id));
113
+ for (const [id, timer] of timers) {
114
+ if (!entryIds.has(id)) {
115
+ clearTimeout(timer);
116
+ timers.delete(id);
117
+ }
118
+ }
119
+
120
+ const visibleCount = entries.filter((e) => e.status === "visible").length;
121
+ const slots = props.maxVisible - visibleCount;
122
+ const pending = entries.filter((e) => e.status === "pending");
123
+ for (let i = 0; i < Math.min(slots, pending.length); i++) {
124
+ promote(pending[i]!.id);
125
+ startTimer(pending[i]!);
126
+ }
127
+ },
128
+ { immediate: true }
129
+ );
130
+
131
+ let _leavingCount = 0;
132
+ let _containerEl: HTMLElement | null = null;
133
+
134
+ const onBeforeLeave = (el: Element) => {
135
+ const htmlEl = el as HTMLElement;
136
+ if (_leavingCount === 0) {
137
+ _containerEl = htmlEl.parentElement as HTMLElement;
138
+ if (_containerEl) {
139
+ _containerEl.style.minWidth = `${_containerEl.offsetWidth}px`;
140
+ }
141
+ }
142
+ _leavingCount++;
143
+ htmlEl.style.top = `${htmlEl.offsetTop}px`;
144
+ htmlEl.style.width = `${htmlEl.offsetWidth}px`;
145
+ };
146
+
147
+ const onAfterLeave = () => {
148
+ _leavingCount--;
149
+ if (_leavingCount === 0 && _containerEl) {
150
+ _containerEl.style.minWidth = "";
151
+ _containerEl = null;
152
+ }
153
+ };
154
+
155
+ const onAfterEnter = (el: Element) => {
156
+ (el as HTMLElement).focus();
157
+ };
158
+
159
+ onUnmounted(() => {
160
+ timers.forEach(clearTimeout);
161
+ timers.clear();
162
+ });
163
+ </script>
164
+
165
+ <style lang="css">
166
+ @layer components {
167
+ @keyframes hideTop {
168
+ 0% {
169
+ opacity: 1;
170
+ transform: translateY(0);
171
+ }
172
+ 100% {
173
+ opacity: 0;
174
+ transform: translateY(-30px);
175
+ }
176
+ }
177
+
178
+ @keyframes hideBottom {
179
+ 0% {
180
+ opacity: 1;
181
+ transform: translateY(0);
182
+ }
183
+ 100% {
184
+ opacity: 0;
185
+ transform: translateY(30px);
186
+ }
187
+ }
188
+
189
+ @keyframes progress {
190
+ to {
191
+ transform: scaleX(1);
192
+ }
193
+ }
194
+
195
+ @keyframes showTop {
196
+ from {
197
+ opacity: 0;
198
+ transform: translateY(-30px);
199
+ }
200
+ to {
201
+ opacity: 1;
202
+ transform: translateY(0);
203
+ }
204
+ }
205
+
206
+ @keyframes showBottom {
207
+ from {
208
+ opacity: 0;
209
+ transform: translateY(30px);
210
+ }
211
+ to {
212
+ opacity: 1;
213
+ transform: translateY(0);
214
+ }
215
+ }
216
+
217
+ .display-toast-provider {
218
+ --_gutter: 12px;
219
+ @media (width >= 600px) {
220
+ --_gutter: 24px;
221
+ }
222
+
223
+ position: fixed;
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: 8px;
227
+ z-index: 100;
228
+
229
+ inset-inline: var(--_gutter);
230
+ margin-inline: auto;
231
+ width: max-content;
232
+ max-width: calc(100% - 2 * var(--_gutter));
233
+
234
+ &.top {
235
+ inset-block-start: var(--_gutter);
236
+ }
237
+
238
+ &.bottom {
239
+ inset-block-end: var(--_gutter);
240
+ flex-direction: column-reverse;
241
+ }
242
+
243
+ @media (width >= 600px) {
244
+ &.left {
245
+ inset-inline-start: var(--_gutter);
246
+ inset-inline-end: unset;
247
+ margin-inline: 0;
248
+ }
249
+
250
+ &.right {
251
+ inset-inline-end: var(--_gutter);
252
+ inset-inline-start: unset;
253
+ margin-inline: 0;
254
+ }
255
+
256
+ &.center:not(.full-width) {
257
+ inset-inline: 0;
258
+ margin-inline: auto;
259
+ }
260
+
261
+ &.full-width {
262
+ inset-inline: var(--_gutter);
263
+ width: unset;
264
+ max-width: unset;
265
+ }
266
+ }
267
+ }
268
+
269
+ .display-toast-provider-item {
270
+ --_reveal: 550ms;
271
+ --_duration: 5000ms;
272
+
273
+ display: block;
274
+ position: relative;
275
+ overflow: hidden;
276
+
277
+ &:focus {
278
+ outline: 2px solid var(--theme-ring);
279
+ outline-offset: 2px;
280
+ }
281
+
282
+ &:focus:not(:focus-visible) {
283
+ outline: none;
284
+ }
285
+
286
+ /* TransitionGroup enter/leave */
287
+ .display-toast-provider.top & {
288
+ &.v-enter-from {
289
+ opacity: 0;
290
+ transform: translateY(-30px);
291
+ }
292
+
293
+ &.v-enter-active {
294
+ @supports (animation-timing-function: linear(0, 1)) {
295
+ animation: showTop var(--_reveal) var(--spring-easing) forwards;
296
+ }
297
+
298
+ @supports not (animation-timing-function: linear(0, 1)) {
299
+ animation: showTop calc(var(--_reveal) / 2) linear forwards;
300
+ }
301
+ }
302
+
303
+ &.v-leave-active {
304
+ position: absolute;
305
+
306
+ @supports (animation-timing-function: linear(0, 1)) {
307
+ animation: hideTop var(--_reveal) var(--spring-easing) forwards;
308
+ }
309
+
310
+ @supports not (animation-timing-function: linear(0, 1)) {
311
+ animation: hideTop calc(var(--_reveal) / 2) linear forwards;
312
+ }
313
+ }
314
+ }
315
+
316
+ .display-toast-provider.bottom & {
317
+ &.v-enter-from {
318
+ opacity: 0;
319
+ transform: translateY(30px);
320
+ }
321
+
322
+ &.v-enter-active {
323
+ @supports (animation-timing-function: linear(0, 1)) {
324
+ animation: showBottom var(--_reveal) var(--spring-easing) forwards;
325
+ }
326
+
327
+ @supports not (animation-timing-function: linear(0, 1)) {
328
+ animation: showBottom calc(var(--_reveal) / 2) linear forwards;
329
+ }
330
+ }
331
+
332
+ &.v-leave-active {
333
+ position: absolute;
334
+
335
+ @supports (animation-timing-function: linear(0, 1)) {
336
+ animation: hideBottom var(--_reveal) var(--spring-easing) forwards;
337
+ }
338
+
339
+ @supports not (animation-timing-function: linear(0, 1)) {
340
+ animation: hideBottom calc(var(--_reveal) / 2) linear forwards;
341
+ }
342
+ }
343
+ }
344
+
345
+ &.v-move {
346
+ transition: transform 0.3s ease;
347
+ }
348
+
349
+ .display-toast-provider-progress {
350
+ position: absolute;
351
+ inset-block-end: 4px;
352
+ inset-inline: 15px 8px;
353
+ height: 3px;
354
+ transform: scaleX(0);
355
+ transform-origin: right;
356
+ background: var(--theme-accent);
357
+ border-radius: inherit;
358
+ animation: progress var(--_duration) linear forwards;
359
+ }
360
+ }
361
+ }
362
+ </style>
@@ -0,0 +1,195 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { ref, onMounted } from "vue";
3
+ import StorybookComponent from "../DisplayToast.vue";
4
+
5
+ interface ToastStoryArgs {
6
+ theme: "info" | "success" | "warning" | "error";
7
+ position: "top" | "bottom";
8
+ alignment: "left" | "center" | "right";
9
+ fullWidth: boolean;
10
+ autoDismiss: boolean;
11
+ duration: number;
12
+ revealDuration: number;
13
+ title: string;
14
+ description: string;
15
+ customIcon: string;
16
+ }
17
+
18
+ export default {
19
+ title: "Atoms/DisplayToast",
20
+ component: StorybookComponent,
21
+ argTypes: {
22
+ theme: {
23
+ control: { type: "select" },
24
+ options: ["info", "success", "warning", "error"],
25
+ description: "Toast theme/variant",
26
+ table: { category: "Appearance" },
27
+ },
28
+ position: {
29
+ control: { type: "select" },
30
+ options: ["top", "bottom"],
31
+ description: "Vertical position of toast",
32
+ table: { category: "Appearance" },
33
+ },
34
+ alignment: {
35
+ control: { type: "select" },
36
+ options: ["left", "center", "right"],
37
+ description: "Horizontal alignment of toast",
38
+ table: { category: "Appearance" },
39
+ },
40
+ fullWidth: {
41
+ control: { type: "boolean" },
42
+ description: "Whether toast spans full width",
43
+ table: { category: "Appearance" },
44
+ },
45
+ autoDismiss: {
46
+ control: { type: "boolean" },
47
+ description: "Auto dismiss toast after duration",
48
+ table: { category: "Behaviour" },
49
+ },
50
+ duration: {
51
+ control: { type: "range", min: 1000, max: 10000, step: 500 },
52
+ description: "Auto dismiss duration in milliseconds",
53
+ table: { category: "Behaviour" },
54
+ },
55
+ revealDuration: {
56
+ control: { type: "range", min: 100, max: 1000, step: 50 },
57
+ description: "Animation duration for reveal/hide",
58
+ table: { category: "Behaviour" },
59
+ },
60
+ title: {
61
+ control: { type: "text" },
62
+ description: "Toast title text",
63
+ table: { category: "Content" },
64
+ },
65
+ description: {
66
+ control: { type: "text" },
67
+ description: "Toast description text shown below the title",
68
+ table: { category: "Content" },
69
+ },
70
+ customIcon: {
71
+ control: { type: "text" },
72
+ description: "Custom icon name (Iconify, e.g. 'akar-icons:check-box')",
73
+ table: { category: "Content" },
74
+ },
75
+ config: { table: { disable: true } },
76
+ modelValue: { table: { disable: true } },
77
+ },
78
+ args: {
79
+ theme: "info",
80
+ position: "top",
81
+ alignment: "right",
82
+ fullWidth: false,
83
+ autoDismiss: false,
84
+ duration: 3000,
85
+ revealDuration: 300,
86
+ title: "Notification",
87
+ description: "This is a toast notification message.",
88
+ customIcon: "",
89
+ },
90
+ } as Meta<ToastStoryArgs>;
91
+
92
+ const Template: StoryFn<ToastStoryArgs> = (args) => ({
93
+ components: { StorybookComponent },
94
+ setup() {
95
+ const isActive = ref(false);
96
+
97
+ onMounted(() => {
98
+ setTimeout(() => {
99
+ isActive.value = true;
100
+ }, 500);
101
+ });
102
+
103
+ return { args, isActive };
104
+ },
105
+ template: `
106
+ <div style="position: relative; height: 200px; padding: 20px;">
107
+ <button
108
+ @click="isActive = true"
109
+ style="padding: 8px 16px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer;"
110
+ >
111
+ Trigger Toast
112
+ </button>
113
+
114
+ <StorybookComponent
115
+ v-model="isActive"
116
+ :config="{
117
+ appearance: {
118
+ theme: args.theme,
119
+ position: args.position,
120
+ alignment: args.alignment,
121
+ fullWidth: args.fullWidth,
122
+ },
123
+ behavior: {
124
+ autoDismiss: args.autoDismiss,
125
+ duration: args.duration,
126
+ revealDuration: args.revealDuration,
127
+ },
128
+ content: {
129
+ title: args.title,
130
+ description: args.description,
131
+ ...(args.customIcon && { customIcon: args.customIcon }),
132
+ },
133
+ }"
134
+ />
135
+ </div>
136
+ `,
137
+ });
138
+
139
+ export const Default = Template.bind({});
140
+
141
+ export const SuccessToast = Template.bind({});
142
+ SuccessToast.args = {
143
+ theme: "success",
144
+ title: "Changes saved",
145
+ description: "Your changes have been saved successfully.",
146
+ customIcon: "akar-icons:check-box",
147
+ autoDismiss: false,
148
+ };
149
+
150
+ export const ErrorToast = Template.bind({});
151
+ ErrorToast.args = {
152
+ theme: "error",
153
+ title: "Something went wrong",
154
+ description: "Please try again or contact support if the problem persists.",
155
+ autoDismiss: false,
156
+ position: "top",
157
+ alignment: "center",
158
+ };
159
+
160
+ export const WarningToast = Template.bind({});
161
+ WarningToast.args = {
162
+ theme: "warning",
163
+ title: "Action required",
164
+ description: "Please review the highlighted fields before continuing.",
165
+ autoDismiss: false,
166
+ };
167
+
168
+ export const AutoDismiss = Template.bind({});
169
+ AutoDismiss.args = {
170
+ theme: "info",
171
+ title: "Auto-dismissing",
172
+ description: "This toast will dismiss itself after 3 seconds.",
173
+ autoDismiss: true,
174
+ duration: 3000,
175
+ };
176
+
177
+ export const BottomCenter = Template.bind({});
178
+ BottomCenter.args = {
179
+ theme: "info",
180
+ title: "Bottom centre",
181
+ description: "Positioned at the bottom centre of the viewport.",
182
+ position: "bottom",
183
+ alignment: "center",
184
+ autoDismiss: false,
185
+ };
186
+
187
+ export const FullWidth = Template.bind({});
188
+ FullWidth.args = {
189
+ theme: "warning",
190
+ title: "Full width warning",
191
+ description: "This toast spans the full width of the viewport.",
192
+ fullWidth: true,
193
+ position: "top",
194
+ autoDismiss: false,
195
+ };
@@ -0,0 +1,142 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { useToastQueue } from "~/composables/useToastQueue";
3
+ import DisplayToastProvider from "../DisplayToastProvider.vue";
4
+ import type { DisplayToastTheme, DisplayToastPosition, DisplayToastAlignment } from "~/types/components";
5
+
6
+ type StoryArgs = {
7
+ position: DisplayToastPosition;
8
+ alignment: DisplayToastAlignment;
9
+ fullWidth: boolean;
10
+ maxVisible: number;
11
+ theme: DisplayToastTheme;
12
+ autoDismiss: boolean;
13
+ duration: number;
14
+ };
15
+
16
+ const themes = ["info", "success", "warning", "error"] as const;
17
+
18
+ const messages: Record<DisplayToastTheme, { title: string; description: string }> = {
19
+ info: { title: "Information", description: "This is an informational notification." },
20
+ success: { title: "Success!", description: "Your action completed successfully." },
21
+ warning: { title: "Warning", description: "Please review this before continuing." },
22
+ error: { title: "Error", description: "Something went wrong. Please try again." },
23
+ };
24
+
25
+ export default {
26
+ title: "Atoms/DisplayToastProvider",
27
+ component: DisplayToastProvider,
28
+ argTypes: {
29
+ position: {
30
+ control: { type: "select" },
31
+ options: ["top", "bottom"],
32
+ description: "Vertical position of toasts",
33
+ table: { category: "Provider" },
34
+ },
35
+ alignment: {
36
+ control: { type: "select" },
37
+ options: ["left", "center", "right"],
38
+ description: "Horizontal alignment of toasts",
39
+ table: { category: "Provider" },
40
+ },
41
+ fullWidth: {
42
+ control: "boolean",
43
+ description: "Toasts span full viewport width",
44
+ table: { category: "Provider" },
45
+ },
46
+ maxVisible: {
47
+ control: { type: "number", min: 1, max: 5 },
48
+ description: "Max toasts shown simultaneously",
49
+ table: { category: "Provider" },
50
+ },
51
+ theme: {
52
+ control: { type: "select" },
53
+ options: ["info", "success", "warning", "error"],
54
+ description: "Theme for triggered toast",
55
+ table: { category: "Toast" },
56
+ },
57
+ autoDismiss: {
58
+ control: "boolean",
59
+ description: "Auto-dismiss triggered toast",
60
+ table: { category: "Toast" },
61
+ },
62
+ duration: {
63
+ control: { type: "range", min: 1000, max: 10000, step: 500 },
64
+ description: "Auto-dismiss duration in ms",
65
+ table: { category: "Toast" },
66
+ },
67
+ },
68
+ args: {
69
+ position: "top",
70
+ alignment: "right",
71
+ fullWidth: false,
72
+ maxVisible: 1,
73
+ theme: "info",
74
+ autoDismiss: true,
75
+ duration: 4000,
76
+ },
77
+ } as Meta<StoryArgs>;
78
+
79
+ const Template: StoryFn<StoryArgs> = (args) => ({
80
+ components: { DisplayToastProvider },
81
+ setup() {
82
+ const { show, clear } = useToastQueue();
83
+
84
+ const triggerToast = () => {
85
+ show({
86
+ appearance: { theme: args.theme },
87
+ behavior: { autoDismiss: args.autoDismiss, duration: args.duration },
88
+ content: messages[args.theme],
89
+ });
90
+ };
91
+
92
+ const queueAll = () => {
93
+ themes.forEach((theme) => {
94
+ show({
95
+ appearance: { theme },
96
+ behavior: { autoDismiss: args.autoDismiss, duration: args.duration },
97
+ content: messages[theme],
98
+ });
99
+ });
100
+ };
101
+
102
+ return { args, triggerToast, queueAll, clear };
103
+ },
104
+ template: `
105
+ <div style="padding: 2rem; min-height: 200px;">
106
+ <div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
107
+ <button @click="triggerToast" style="padding: 0.6rem 1.4rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px;">Trigger Toast</button>
108
+ <button @click="queueAll" style="padding: 0.6rem 1.4rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px;">Queue All Themes</button>
109
+ <button @click="clear" style="padding: 0.6rem 1.4rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px;">Clear Queue</button>
110
+ </div>
111
+ <DisplayToastProvider
112
+ :position="args.position"
113
+ :alignment="args.alignment"
114
+ :full-width="args.fullWidth"
115
+ :max-visible="args.maxVisible"
116
+ />
117
+ </div>
118
+ `,
119
+ });
120
+
121
+ export const Default = Template.bind({});
122
+
123
+ export const BottomLeft = Template.bind({});
124
+ BottomLeft.args = {
125
+ position: "bottom",
126
+ alignment: "left",
127
+ theme: "success",
128
+ autoDismiss: false,
129
+ };
130
+
131
+ export const Stacked = Template.bind({});
132
+ Stacked.args = {
133
+ maxVisible: 3,
134
+ autoDismiss: false,
135
+ };
136
+
137
+ export const FullWidth = Template.bind({});
138
+ FullWidth.args = {
139
+ fullWidth: true,
140
+ theme: "warning",
141
+ autoDismiss: false,
142
+ };