srcdev-nuxt-components 6.1.33 → 6.1.34

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.
@@ -1,29 +1,35 @@
1
1
  <template>
2
2
  <Teleport to="body">
3
3
  <div
4
- v-if="privateToastState"
4
+ v-if="privateDisplayToast"
5
+ ref="toastElement"
5
6
  class="display-toast"
6
7
  :class="[
7
8
  elementClasses,
8
9
  cssStateClass,
10
+ positionClasses,
9
11
  {
10
12
  'has-theme': !slots.default,
11
- 'auto-dismiss': autoDismiss,
12
13
  },
13
14
  ]"
14
15
  :data-theme="theme"
16
+ :role="toastRole"
17
+ :aria-live="ariaLive"
18
+ :tabindex="slots.default ? undefined : '0'"
19
+ :aria-describedby="slots.default ? undefined : 'toast-message-' + toastId"
20
+ @keydown.escape="setDismissToast"
15
21
  >
16
22
  <slot v-if="slots.default"></slot>
17
23
 
18
24
  <div v-else class="display-toast-inner">
19
25
  <div class="toast-icon" aria-hidden="true">
20
26
  <slot name="customToastIcon">
21
- <Icon :name="defaultThemeIcons[props.theme] ?? 'akar-icons:info'" class="icon" />
27
+ <Icon :name="customIcon || defaultThemeIcons[theme] || 'akar-icons:info'" class="icon" />
22
28
  </slot>
23
29
  </div>
24
- <div class="toast-message">{{ toastDisplayText }}</div>
30
+ <div class="toast-message" :id="'toast-message-' + toastId">{{ toastDisplayText }}</div>
25
31
  <div class="toast-action">
26
- <button @click.prevent="updateToHiding()">
32
+ <button @click.prevent="setDismissToast()">
27
33
  <Icon name="material-symbols:close" class="icon" />
28
34
  <span class="sr-only">Close</span>
29
35
  </button>
@@ -33,37 +39,103 @@
33
39
  </div>
34
40
  </Teleport>
35
41
  </template>
42
+
43
+ <script lang="ts">
44
+ /**
45
+ * DisplayToast - Configurable toast notification component
46
+ *
47
+ * Example usage with config object:
48
+ * <DisplayToast
49
+ * v-model="showToast"
50
+ * :config="{
51
+ * appearance: { theme: 'success', position: 'top', alignment: 'right' },
52
+ * behavior: { autoDismiss: true, duration: 3000 },
53
+ * content: { text: 'Operation completed successfully!' }
54
+ * }"
55
+ * />
56
+ *
57
+ * Types exported for use in other components:
58
+ * - DisplayToastConfig
59
+ * - DisplayToastProps
60
+ * - DisplayToastTheme
61
+ * - DisplayToastAppearanceConfig
62
+ * - DisplayToastBehaviorConfig
63
+ * - DisplayToastContentConfig
64
+ * - ToastSlots
65
+ */
66
+
67
+ export type DisplayToastTheme =
68
+ | "primary"
69
+ | "secondary"
70
+ | "tertiary"
71
+ | "ghost"
72
+ | "error"
73
+ | "info"
74
+ | "success"
75
+ | "warning"
76
+
77
+ export type DisplayToastPosition = "top" | "bottom"
78
+ export type DisplayToastAlignment = "left" | "center" | "right"
79
+
80
+ export interface DisplayToastAppearanceConfig {
81
+ theme?: DisplayToastTheme
82
+ position?: DisplayToastPosition
83
+ alignment?: DisplayToastAlignment
84
+ fullWidth?: boolean
85
+ }
86
+
87
+ export interface DisplayToastBehaviorConfig {
88
+ autoDismiss?: boolean
89
+ duration?: number
90
+ revealDuration?: number
91
+ }
92
+
93
+ export interface DisplayToastContentConfig {
94
+ text?: string
95
+ customIcon?: string
96
+ }
97
+
98
+ export interface DisplayToastConfig {
99
+ appearance?: DisplayToastAppearanceConfig
100
+ behavior?: DisplayToastBehaviorConfig
101
+ content?: DisplayToastContentConfig
102
+ }
103
+
104
+ export interface DisplayToastProps {
105
+ config?: DisplayToastConfig
106
+ styleClassPassthrough?: string | string[]
107
+ }
108
+
109
+ export interface ToastSlots {
110
+ default?(props?: {}): any
111
+ customToastIcon?(props?: {}): any
112
+ }
113
+ </script>
114
+
36
115
  <script setup lang="ts">
37
- const props = defineProps({
38
- theme: {
39
- type: String as PropType<"primary" | "secondary" | "tertiary" | "ghost" | "error" | "info" | "success" | "warning">,
40
- default: "ghost",
41
- validator(value: string) {
42
- return ["primary", "secondary", "tertiary", "ghost", "error", "info", "success", "warning"].includes(value)
116
+ const props = withDefaults(defineProps<DisplayToastProps>(), {
117
+ config: () => ({
118
+ appearance: {
119
+ theme: "ghost" as DisplayToastTheme,
120
+ position: "top" as DisplayToastPosition,
121
+ alignment: "right" as DisplayToastAlignment,
122
+ fullWidth: false,
43
123
  },
44
- },
45
- revealDuration: {
46
- type: Number,
47
- default: 550,
48
- },
49
- autoDismiss: {
50
- type: Boolean,
51
- default: true,
52
- },
53
- duration: {
54
- type: Number,
55
- default: 5000,
56
- },
57
- toastDisplayText: {
58
- type: String,
59
- default: "",
60
- },
61
- styleClassPassthrough: {
62
- type: [String, Array] as PropType<string | string[]>,
63
- default: () => [],
64
- },
124
+ behavior: {
125
+ autoDismiss: true,
126
+ duration: 5000,
127
+ revealDuration: 550,
128
+ },
129
+ content: {
130
+ text: "",
131
+ customIcon: undefined,
132
+ },
133
+ }),
134
+ styleClassPassthrough: () => [],
65
135
  })
66
136
 
137
+ const slots = defineSlots<ToastSlots>()
138
+
67
139
  const defaultThemeIcons = {
68
140
  primary: "akar-icons:info",
69
141
  secondary: "akar-icons:info",
@@ -75,51 +147,70 @@ const defaultThemeIcons = {
75
147
  warning: "akar-icons:circle-alert",
76
148
  }
77
149
 
78
- const slots = useSlots()
79
150
  const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
80
151
 
81
- // single state ref
82
- const state = ref<"idle" | "entering" | "visible" | "hiding">("idle")
83
- const cssStateClass = computed(() => {
84
- return state.value !== "idle" && !props.autoDismiss ? state.value : ""
152
+ // Computed properties for accessing config values with defaults
153
+ const theme = computed(() => props.config?.appearance?.theme ?? "ghost")
154
+ const position = computed(() => props.config?.appearance?.position ?? "top")
155
+ const alignment = computed(() => props.config?.appearance?.alignment ?? "right")
156
+ const fullWidth = computed(() => props.config?.appearance?.fullWidth ?? false)
157
+ const autoDismiss = computed(() => props.config?.behavior?.autoDismiss ?? true)
158
+ const duration = computed(() => props.config?.behavior?.duration ?? 5000)
159
+ const revealDuration = computed(() => props.config?.behavior?.revealDuration ?? 550)
160
+ const toastDisplayText = computed(() => props.config?.content?.text ?? "")
161
+ const customIcon = computed(() => props.config?.content?.customIcon)
162
+
163
+ // Computed classes for positioning
164
+ const positionClasses = computed(() => {
165
+ const classes = []
166
+ classes.push(position.value)
167
+ if (fullWidth.value) {
168
+ classes.push("full-width")
169
+ } else {
170
+ classes.push(alignment.value)
171
+ }
172
+ return classes
85
173
  })
86
174
 
87
- // external toggle
88
- const publicToastState = defineModel<boolean>({ default: false })
175
+ /*
176
+ * Accessibility setup
177
+ */
178
+ const toastId = useId()
179
+ const toastElement = ref<HTMLElement>()
89
180
 
90
- // computed helpers
91
- const privateToastState = ref(false)
181
+ // Determine appropriate ARIA attributes based on theme
182
+ const toastRole = computed(() => {
183
+ return ["error", "warning"].includes(theme.value) ? "alert" : "status"
184
+ })
92
185
 
93
- const revealDurationInt = computed(() => props.revealDuration)
94
- const revealDuration = computed(() => revealDurationInt.value + "ms")
95
- const displayDurationInt = computed(() => props.duration)
96
- const displayDuration = computed(() => displayDurationInt.value + "ms")
186
+ const ariaLive = computed(() => {
187
+ return ["error", "warning"].includes(theme.value) ? "assertive" : "polite"
188
+ })
97
189
 
98
- const progressDurationInt = computed(() => Math.floor(displayDurationInt.value - revealDurationInt.value / 2))
99
- const progressDuration = computed(() => progressDurationInt.value + "ms")
190
+ /*
191
+ * Setup component state
192
+ */
193
+ const externalTriggerModel = defineModel<boolean>({ default: false })
194
+ const privateDisplayToast = ref(false)
195
+ const transitionalState = ref(false)
196
+ const cssStateClass = computed(() => {
197
+ return transitionalState.value ? "show" : "hide"
198
+ })
100
199
 
101
- const removeToast = () => {
102
- publicToastState.value = false
103
- privateToastState.value = false
104
- }
105
-
106
- const updateToIdle = () => {
107
- state.value = "idle"
108
- removeToast()
109
- }
110
- const updateToEntering = async () => {
111
- privateToastState.value = true
112
- state.value = "entering"
113
- await useSleep(revealDurationInt.value)
114
- updateToVisible()
115
- }
116
- const updateToVisible = () => {
117
- state.value = "visible"
118
- }
119
- const updateToHiding = async () => {
120
- state.value = "hiding"
121
- await useSleep(revealDurationInt.value)
122
- updateToIdle()
200
+ /*
201
+ * Computed properties for durations (in ms for CSS)
202
+ */
203
+ const revealDurationMs = computed(() => revealDuration.value + "ms")
204
+ const displayDurationMs = computed(() => duration.value + "ms")
205
+
206
+ /*
207
+ * Lifecycle hooks
208
+ */
209
+ const setDismissToast = async () => {
210
+ transitionalState.value = false
211
+ await useSleep(revealDuration.value)
212
+ externalTriggerModel.value = false
213
+ privateDisplayToast.value = false
123
214
  }
124
215
 
125
216
  watch(
@@ -130,65 +221,31 @@ watch(
130
221
  )
131
222
 
132
223
  watch(
133
- () => publicToastState.value,
224
+ () => externalTriggerModel.value,
134
225
  async (newValue, previousValue) => {
135
- if (props.autoDismiss) {
136
- privateToastState.value = newValue
137
- await useSleep(displayDurationInt.value)
138
- updateToIdle()
139
- return
140
- }
141
-
142
- if (!previousValue && newValue && state.value === "idle") {
143
- updateToEntering()
144
- }
226
+ if (newValue) {
227
+ privateDisplayToast.value = true
228
+ transitionalState.value = true
229
+
230
+ // Focus management for accessibility when not using custom slots
231
+ if (!slots.default) {
232
+ await nextTick()
233
+ // Wait for animation to start before focusing
234
+ setTimeout(() => {
235
+ toastElement.value?.focus()
236
+ }, 100)
237
+ }
145
238
 
146
- if (previousValue && !newValue && state.value == "visible") {
147
- updateToHiding()
239
+ if (autoDismiss.value) {
240
+ await useSleep(duration.value)
241
+ setDismissToast()
242
+ }
148
243
  }
149
244
  }
150
245
  )
151
246
  </script>
152
247
 
153
248
  <style scoped lang="css">
154
- @keyframes slide-in {
155
- from {
156
- opacity: 0;
157
- visibility: hidden;
158
- transform: translateY(20px);
159
- }
160
- to {
161
- opacity: 1;
162
- visibility: visible;
163
- transform: translateY(0);
164
- }
165
- }
166
-
167
- @keyframes slide-out {
168
- from {
169
- opacity: 1;
170
- visibility: visible;
171
- transform: translateY(0);
172
- }
173
- to {
174
- opacity: 0;
175
- visibility: hidden;
176
- transform: translateY(20px);
177
- }
178
- }
179
-
180
- @keyframes slide-in-out {
181
- 5% {
182
- opacity: 1;
183
- visibility: visible;
184
- transform: translateY(0);
185
- }
186
- 95% {
187
- opacity: 1;
188
- transform: translateY(0);
189
- }
190
- }
191
-
192
249
  @keyframes show {
193
250
  to {
194
251
  opacity: 1;
@@ -226,35 +283,22 @@ watch(
226
283
 
227
284
  z-index: 100;
228
285
 
229
- &.auto-dismiss {
230
- /* first run slide-in, then slide-out after a delay */
231
- animation: slide-in 400ms var(--spring-in-easing) forwards,
232
- slide-out 400ms var(--spring-out-easing) forwards v-bind(displayDuration);
286
+ /* Focus styles for accessibility */
287
+ &:focus {
288
+ outline: 2px solid var(--colour-theme-3, #007acc);
289
+ outline-offset: 2px;
233
290
  }
234
291
 
235
- &:not(&.auto-dismiss) {
236
- &.show,
237
- &.entering {
238
- animation: show v-bind(revealDuration) var(--spring-easing) forwards;
239
- }
240
-
241
- &.visible {
242
- /* if you want a steady state style, add here */
243
- opacity: 1;
244
- visibility: visible;
245
- transform: translateY(0);
246
- }
292
+ &:focus:not(:focus-visible) {
293
+ outline: none;
294
+ }
247
295
 
248
- &.hide,
249
- &.hiding {
250
- animation: hide v-bind(revealDuration) var(--spring-easing) forwards;
251
- }
296
+ &.show {
297
+ animation: show v-bind(revealDurationMs) var(--spring-easing) forwards;
252
298
  }
253
299
 
254
- &:hover {
255
- .display-toast-progress {
256
- animation-play-state: paused;
257
- }
300
+ &.hide {
301
+ animation: hide v-bind(revealDurationMs) var(--spring-easing) forwards;
258
302
  }
259
303
 
260
304
  &.full-width {
@@ -272,8 +316,9 @@ watch(
272
316
  }
273
317
 
274
318
  &.center {
275
- left: 50%;
276
- /* transform: translateX(-50%); */
319
+ inset-inline: 0;
320
+ margin-inline: auto;
321
+ width: max-content;
277
322
  }
278
323
  }
279
324
 
@@ -289,7 +334,6 @@ watch(
289
334
  /*
290
335
  * Styles for the display toast component
291
336
  */
292
-
293
337
  &.has-theme {
294
338
  padding-inline-start: 6px;
295
339
  background-color: var(--colour-theme-8);
@@ -369,7 +413,8 @@ watch(
369
413
  vertical-align: middle;
370
414
  }
371
415
 
372
- &:hover {
416
+ &:hover,
417
+ &:focus-visible {
373
418
  box-shadow: none;
374
419
  background-color: var(--colour-theme-8);
375
420
  color: var(--colour-theme-0);
@@ -391,7 +436,7 @@ watch(
391
436
  transform-origin: right;
392
437
  background: linear-gradient(to right, var(--colour-theme-2), var(--colour-theme-8));
393
438
  border-radius: inherit;
394
- animation: progress v-bind(progressDuration) linear forwards;
439
+ animation: progress v-bind(displayDurationMs) linear forwards;
395
440
  }
396
441
  }
397
442
  </style>
@@ -2,3 +2,4 @@ export * from "../components/responsive-header/ResponsiveHeader.vue"
2
2
  export * from "../components/display-chip/DisplayChip.vue"
3
3
  export * from "../components/carousel-basic/CarouselBasic.vue"
4
4
  export * from "../components/image-galleries/SliderGallery.vue"
5
+ export * from "../components/display-toast/DisplayToast.vue"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "6.1.33",
4
+ "version": "6.1.34",
5
5
  "main": "nuxt.config.ts",
6
6
  "scripts": {
7
7
  "clean": "rm -rf .nuxt && rm -rf .output && rm -rf .playground/.nuxt && rm -rf .playground/.output",