@umbra.ui/core 0.1.17 → 0.1.19
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/dist/components/controls/Button/Button.vue +417 -0
- package/dist/components/controls/Button/README.md +348 -0
- package/dist/components/controls/Button/theme.css +200 -0
- package/dist/components/controls/Checkbox/Checkbox.vue +164 -0
- package/dist/components/controls/Checkbox/README.md +441 -0
- package/dist/components/controls/Checkbox/theme.css +36 -0
- package/dist/components/controls/Dropdown/Dropdown.vue +476 -0
- package/dist/components/controls/Dropdown/README.md +370 -0
- package/dist/components/controls/Dropdown/theme.css +50 -0
- package/dist/components/controls/Dropdown/types.ts +6 -0
- package/dist/components/controls/IconButton/IconButton.vue +267 -0
- package/dist/components/controls/IconButton/README.md +502 -0
- package/dist/components/controls/IconButton/theme.css +89 -0
- package/dist/components/controls/Radio/README.md +591 -0
- package/dist/components/controls/Radio/Radio.vue +89 -0
- package/dist/components/controls/Radio/theme.css +14 -0
- package/dist/components/controls/RangeSlider/README.md +608 -0
- package/dist/components/controls/RangeSlider/RangeSlider.vue +535 -0
- package/dist/components/controls/RangeSlider/theme.css +80 -0
- package/dist/components/controls/SegmentedControl/README.md +587 -0
- package/dist/components/controls/SegmentedControl/SegmentedControl.vue +284 -0
- package/dist/components/controls/SegmentedControl/theme.css +60 -0
- package/dist/components/controls/SegmentedControl/types.ts +5 -0
- package/dist/components/controls/Slider/README.md +627 -0
- package/dist/components/controls/Slider/Slider.vue +260 -0
- package/dist/components/controls/Slider/theme.css +74 -0
- package/dist/components/controls/Stepper/README.md +601 -0
- package/dist/components/controls/Stepper/Stepper.vue +103 -0
- package/dist/components/controls/Stepper/theme.css +53 -0
- package/dist/components/controls/Switch/README.md +667 -0
- package/dist/components/controls/Switch/Switch.vue +127 -0
- package/dist/components/controls/Switch/theme.css +42 -0
- package/dist/components/dialogs/Alert/Alert.vue +218 -0
- package/dist/components/dialogs/Alert/README.md +450 -0
- package/dist/components/dialogs/Alert/theme.css +44 -0
- package/dist/components/dialogs/Alert/types.ts +11 -0
- package/dist/components/dialogs/Toast/README.md +522 -0
- package/dist/components/dialogs/Toast/Toast.vue +296 -0
- package/dist/components/dialogs/Toast/ToastContainer.vue +330 -0
- package/dist/components/dialogs/Toast/theme.css +44 -0
- package/dist/components/dialogs/Toast/types.ts +46 -0
- package/dist/components/dialogs/Toast/useToast.ts +127 -0
- package/dist/components/indicators/ProgressBar/ProgressBar.vue +98 -0
- package/dist/components/indicators/ProgressBar/README.md +744 -0
- package/dist/components/indicators/ProgressBar/theme.css +36 -0
- package/dist/components/indicators/Tooltip/README.md +723 -0
- package/dist/components/indicators/Tooltip/TooltipProvider.vue +142 -0
- package/dist/components/indicators/Tooltip/theme.css +18 -0
- package/dist/components/indicators/Tooltip/tooltip.ts +48 -0
- package/dist/components/indicators/Tooltip/types.ts +15 -0
- package/dist/components/indicators/Tooltip/useTooltip.ts +71 -0
- package/dist/components/inputs/AutogrowTextView/AutogrowTextView.vue +110 -0
- package/dist/components/inputs/AutogrowTextView/README.md +643 -0
- package/dist/components/inputs/AutogrowTextView/theme.css +28 -0
- package/dist/components/inputs/InputCard/InputCard.vue +600 -0
- package/dist/components/inputs/InputCard/README.md +636 -0
- package/dist/components/inputs/InputEmail/InputEmail.vue +698 -0
- package/dist/components/inputs/InputEmail/README.md +764 -0
- package/dist/components/inputs/InputNumber/InputNumber.vue +300 -0
- package/dist/components/inputs/InputNumber/README.md +749 -0
- package/dist/components/inputs/InputPhone/InputPhone.vue +645 -0
- package/dist/components/inputs/InputPhone/README.md +636 -0
- package/dist/components/inputs/InputSecure/InputSecure.vue +646 -0
- package/dist/components/inputs/InputSecure/README.md +771 -0
- package/dist/components/inputs/InputText/InputText.vue +225 -0
- package/dist/components/inputs/InputText/README.md +844 -0
- package/dist/components/inputs/OTP/OTP.vue +349 -0
- package/dist/components/inputs/OTP/README.md +736 -0
- package/dist/components/inputs/OTP/theme.css +50 -0
- package/dist/components/inputs/StringCapture/README.md +718 -0
- package/dist/components/inputs/StringCapture/StringCapture.vue +315 -0
- package/dist/components/inputs/StringCapture/theme.css +86 -0
- package/dist/components/inputs/Tags/README.md +897 -0
- package/dist/components/inputs/Tags/TagBar.vue +793 -0
- package/dist/components/inputs/Tags/TagCreation.vue +219 -0
- package/dist/components/inputs/Tags/TagPicker.vue +380 -0
- package/dist/components/inputs/Tags/tag-bar-styles.ts +354 -0
- package/dist/components/inputs/Tags/theme.css +121 -0
- package/dist/components/inputs/Tags/types.ts +346 -0
- package/dist/components/inputs/search/README.md +759 -0
- package/dist/components/inputs/search/SearchBar.vue +394 -0
- package/dist/components/inputs/search/SearchResults.vue +310 -0
- package/dist/components/inputs/search/theme.css +187 -0
- package/dist/components/inputs/search/types.ts +8 -0
- package/dist/components/inputs/theme.css +102 -0
- package/dist/components/menus/ActionMenu/ActionMenu.vue +383 -0
- package/dist/components/menus/ActionMenu/README.md +825 -0
- package/dist/components/menus/ActionMenu/theme.css +93 -0
- package/dist/components/models/Popover/Popover.vue +551 -0
- package/dist/components/models/Popover/README.md +885 -0
- package/dist/components/models/Popover/theme.css +52 -0
- package/dist/components/models/Sheet/README.md +1159 -0
- package/dist/components/models/Sheet/Sheet.vue +465 -0
- package/dist/components/models/Sheet/theme.css +72 -0
- package/dist/components/models/Sidebar/README.md +1228 -0
- package/dist/components/models/Sidebar/Sidebar.vue +480 -0
- package/dist/components/models/Sidebar/theme.css +90 -0
- package/dist/components/navigation/adaptive/AdaptiveLayout.vue +779 -0
- package/dist/components/navigation/adaptive/AdaptiveLayoutBreadcrumbs.vue +192 -0
- package/dist/components/navigation/adaptive/AdaptiveLayoutMenuButton.vue +149 -0
- package/dist/components/navigation/adaptive/README.md +768 -0
- package/dist/components/navigation/adaptive/types.ts +19 -0
- package/dist/components/navigation/adaptive/useAdaptiveLayout.ts +89 -0
- package/dist/components/navigation/adaptive/useBreakpoints.ts +41 -0
- package/dist/components/navigation/adaptive/useContainerMonitor.ts +214 -0
- package/dist/components/navigation/adaptive/useViewAnimation.ts +721 -0
- package/dist/components/navigation/adaptive/useViewResize.ts +211 -0
- package/dist/components/navigation/navstack/NavigationStack.vue +180 -0
- package/dist/components/navigation/navstack/README.md +994 -0
- package/dist/components/navigation/navstack/useNavigationStack.ts +164 -0
- package/dist/components/navigation/slideover/README.md +1275 -0
- package/dist/components/navigation/slideover/SlideoverController.vue +287 -0
- package/dist/components/navigation/slideover/useSlideoverController.ts +320 -0
- package/dist/components/navigation/splitview/README.md +1115 -0
- package/dist/components/navigation/splitview/SplitViewController.vue +176 -0
- package/dist/components/navigation/splitview/useSplitViewController.ts +388 -0
- package/dist/components/navigation/tabcontroller/README.md +919 -0
- package/dist/components/navigation/tabcontroller/TabController.vue +307 -0
- package/dist/components/navigation/tabcontroller/TabItem.vue +57 -0
- package/dist/components/navigation/tabcontroller/types.ts +24 -0
- package/dist/components/navigation/tabcontroller/useTabController.ts +18 -0
- package/dist/components/navigation/theme.css +91 -0
- package/dist/components/navigation/types.ts +7 -0
- package/dist/components/pickers/CollectionPicker/CollectionPicker.vue +398 -0
- package/dist/components/pickers/CollectionPicker/README.md +1115 -0
- package/dist/components/pickers/CollectionPicker/theme.css +14 -0
- package/dist/components/pickers/CollectionPicker/types.ts +11 -0
- package/dist/components/pickers/ColorPicker/ColorPicker.vue +376 -0
- package/dist/components/pickers/ColorPicker/README.md +1439 -0
- package/dist/components/pickers/ColorPicker/colors.ts +299 -0
- package/dist/components/pickers/ColorPicker/theme.css +32 -0
- package/dist/components/pickers/DatePicker/DatePicker.vue +660 -0
- package/dist/components/pickers/DatePicker/README.md +1195 -0
- package/dist/components/pickers/DatePicker/theme.css +22 -0
- package/dist/components/pickers/FilePicker/FilePicker.vue +534 -0
- package/dist/components/pickers/FilePicker/README.md +1542 -0
- package/dist/components/pickers/FilePicker/theme.css +48 -0
- package/dist/components/pickers/FilePicker/types.ts +10 -0
- package/dist/components/pickers/IconPicker/IconPicker.vue +327 -0
- package/dist/components/pickers/IconPicker/README.md +1161 -0
- package/dist/components/pickers/IconPicker/theme.css +28 -0
- package/dist/components/pickers/theme.css +82 -0
- package/dist/components/views/MarkdownViewer/MarkdownViewer.vue +442 -0
- package/dist/components/views/MarkdownViewer/README.md +833 -0
- package/dist/components/views/MarkdownViewer/theme.css +130 -0
- package/package.json +4 -3
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
<!-- Toast.vue -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import tinycolor from "tinycolor2";
|
|
4
|
+
import { computed, ref } from "vue";
|
|
5
|
+
import { icons, type IconKey, XmarkIcon } from "@umbra.ui/icons";
|
|
6
|
+
import type { ToastInstance } from "./types";
|
|
7
|
+
import { IconButton } from "@umbra.ui/core";
|
|
8
|
+
import "./theme.css";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
toast: ToastInstance;
|
|
12
|
+
toastStyle?: "bar" | "full" | "notification";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
16
|
+
toastStyle: "bar",
|
|
17
|
+
});
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
dismiss: [id: string];
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
const containerEl = ref<HTMLElement>();
|
|
23
|
+
|
|
24
|
+
// Fixed icon computation
|
|
25
|
+
const icon = computed(() => {
|
|
26
|
+
const typeIconMap: Record<string, IconKey> = {
|
|
27
|
+
success: "circle-check",
|
|
28
|
+
error: "triangle-warning",
|
|
29
|
+
warning: "triangle-warning",
|
|
30
|
+
info: "circle-info",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
props.toast.iconName ||
|
|
35
|
+
typeIconMap[props.toast.toastType as string] ||
|
|
36
|
+
"circle-info"
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const IconComponent = computed(() => {
|
|
41
|
+
const iconName = icon.value as keyof typeof icons;
|
|
42
|
+
return icons[iconName] || null;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Simplified color system using CSS custom properties
|
|
46
|
+
const toastTypeClass = computed(() => {
|
|
47
|
+
const validTypes = ["success", "error", "warning", "info"];
|
|
48
|
+
return validTypes.includes(props.toast.toastType as string)
|
|
49
|
+
? props.toast.toastType
|
|
50
|
+
: "custom";
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const backgroundColor = computed(() => {
|
|
54
|
+
switch (props.toast.toastType) {
|
|
55
|
+
case "success":
|
|
56
|
+
return "var(--toast-success-bg)";
|
|
57
|
+
case "error":
|
|
58
|
+
return "var(--toast-error-bg)";
|
|
59
|
+
case "warning":
|
|
60
|
+
return "var(--toast-warning-bg)";
|
|
61
|
+
case "info":
|
|
62
|
+
return "var(--toast-info-bg)";
|
|
63
|
+
default:
|
|
64
|
+
return props.toast.toastType;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const textColor = computed(() => {
|
|
69
|
+
// Get the actual color value from CSS custom property
|
|
70
|
+
const bgColor = backgroundColor.value;
|
|
71
|
+
let actualColor = bgColor;
|
|
72
|
+
|
|
73
|
+
if (bgColor.startsWith("var(")) {
|
|
74
|
+
// Extract the CSS variable name
|
|
75
|
+
const varName = bgColor.slice(4, -1); // removes 'var(' and ')'
|
|
76
|
+
// Get the computed value from the document
|
|
77
|
+
actualColor = getComputedStyle(document.documentElement)
|
|
78
|
+
.getPropertyValue(varName)
|
|
79
|
+
.trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const color = tinycolor(actualColor);
|
|
83
|
+
return color.isLight() ? "black" : "white";
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const customColor = computed(() => {
|
|
87
|
+
const validTypes = ["success", "error", "warning", "info"];
|
|
88
|
+
if (!validTypes.includes(props.toast.toastType as string)) {
|
|
89
|
+
return props.toast.toastType;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Accessibility: ARIA label
|
|
95
|
+
const ariaLabel = computed(() => {
|
|
96
|
+
const type = props.toast.toastType || "notification";
|
|
97
|
+
return `${type} notification: ${props.toast.title}${
|
|
98
|
+
props.toast.description ? ". " + props.toast.description : ""
|
|
99
|
+
}`;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const handleClick = () => {
|
|
103
|
+
if (props.toast.dismissible) {
|
|
104
|
+
emit("dismiss", props.toast.id);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleDismiss = () => {
|
|
109
|
+
emit("dismiss", props.toast.id);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Keyboard accessibility
|
|
113
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
114
|
+
if (
|
|
115
|
+
props.toast.dismissible &&
|
|
116
|
+
(event.key === "Escape" || event.key === "Enter")
|
|
117
|
+
) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
handleDismiss();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<template>
|
|
125
|
+
<div
|
|
126
|
+
ref="containerEl"
|
|
127
|
+
:class="[
|
|
128
|
+
$style.container,
|
|
129
|
+
$style[toast.position?.toLowerCase() || 'top'],
|
|
130
|
+
$style[toastTypeClass],
|
|
131
|
+
$style[
|
|
132
|
+
`toastStyle${
|
|
133
|
+
props.toastStyle.charAt(0).toUpperCase() + props.toastStyle.slice(1)
|
|
134
|
+
}`
|
|
135
|
+
],
|
|
136
|
+
]"
|
|
137
|
+
:style="{
|
|
138
|
+
'--text-color': textColor,
|
|
139
|
+
...(customColor && { '--custom-color': customColor }),
|
|
140
|
+
cursor: toast.dismissible ? 'pointer' : 'default',
|
|
141
|
+
}"
|
|
142
|
+
role="alert"
|
|
143
|
+
:aria-label="ariaLabel"
|
|
144
|
+
:aria-live="toast.toastType === 'error' ? 'assertive' : 'polite'"
|
|
145
|
+
:tabindex="toast.dismissible ? 0 : -1"
|
|
146
|
+
@click="handleClick"
|
|
147
|
+
@keydown="handleKeyDown"
|
|
148
|
+
>
|
|
149
|
+
<component
|
|
150
|
+
v-if="IconComponent"
|
|
151
|
+
:is="IconComponent"
|
|
152
|
+
:class="$style.icon"
|
|
153
|
+
aria-hidden="true"
|
|
154
|
+
:color="textColor"
|
|
155
|
+
/>
|
|
156
|
+
<div :class="$style.labels">
|
|
157
|
+
<p :class="$style.title">{{ toast.title }}</p>
|
|
158
|
+
<p v-if="toast.description" :class="$style.description">
|
|
159
|
+
{{ toast.description }}
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<IconButton
|
|
164
|
+
iconName="xmark"
|
|
165
|
+
:class="$style.closeButton"
|
|
166
|
+
@click.stop="handleDismiss"
|
|
167
|
+
buttonType="plain"
|
|
168
|
+
buttonSize="12"
|
|
169
|
+
:buttonStyle="textColor"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
</template>
|
|
173
|
+
|
|
174
|
+
<style module>
|
|
175
|
+
.container {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 0.706rem;
|
|
179
|
+
padding: 0.706rem;
|
|
180
|
+
width: 100%;
|
|
181
|
+
border-radius: 0.5rem;
|
|
182
|
+
box-shadow: 0 4px 12px var(--toast-shadow);
|
|
183
|
+
transition: scale 0.2s ease-in-out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Type variants with proper color system */
|
|
187
|
+
.success {
|
|
188
|
+
background-color: var(--toast-success-bg);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.error {
|
|
192
|
+
background-color: var(--toast-error-bg);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.warning {
|
|
196
|
+
background-color: var(--toast-warning-bg);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.info {
|
|
200
|
+
background-color: var(--toast-info-bg);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.custom {
|
|
204
|
+
background-color: var(--custom-color, var(--toast-custom-bg));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Focus styles for accessibility */
|
|
208
|
+
.container:focus {
|
|
209
|
+
outline: 2px solid currentColor;
|
|
210
|
+
outline-offset: 2px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.container:focus:not(:focus-visible) {
|
|
214
|
+
outline: none;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.icon {
|
|
218
|
+
flex-shrink: 0;
|
|
219
|
+
width: 1.5rem;
|
|
220
|
+
height: 1.5rem;
|
|
221
|
+
transition: transform 0.2s ease-in-out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.labels {
|
|
225
|
+
flex: 1;
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
gap: 0.235rem;
|
|
229
|
+
transition: transform 0.2s ease-in-out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.labels p {
|
|
233
|
+
color: var(--text-color);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.toastStyleBar:hover .labels,
|
|
237
|
+
.toastStyleBar:hover .icon {
|
|
238
|
+
transform: translateX(0.25rem);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.title {
|
|
242
|
+
font-weight: 600;
|
|
243
|
+
margin: 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.description {
|
|
247
|
+
font-size: 0.875rem;
|
|
248
|
+
opacity: var(--toast-description-opacity);
|
|
249
|
+
margin: 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.closeButton {
|
|
253
|
+
transition: transform 0.2s ease-in-out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.toastStyleBar:hover .closeButton {
|
|
257
|
+
transform: translateX(-0.25rem);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.closeButton:focus {
|
|
261
|
+
outline: 2px solid currentColor;
|
|
262
|
+
outline-offset: 2px;
|
|
263
|
+
border-radius: 0.25rem;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Reduced motion support */
|
|
267
|
+
@media (prefers-reduced-motion: reduce) {
|
|
268
|
+
.container {
|
|
269
|
+
transition-duration: 0.15s;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Toast style variants */
|
|
274
|
+
.toastStyleBar {
|
|
275
|
+
border-radius: 0;
|
|
276
|
+
box-shadow: none;
|
|
277
|
+
border-bottom: 1px solid var(--toast-border);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.toastStyleFull {
|
|
281
|
+
border-radius: 0.5rem;
|
|
282
|
+
box-shadow: 0 4px 12px var(--toast-shadow);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.toastStyleNotification {
|
|
286
|
+
border-radius: 0.5rem;
|
|
287
|
+
box-shadow: 0 4px 12px var(--toast-shadow);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.toastStyleNotification:hover {
|
|
291
|
+
scale: 1.02;
|
|
292
|
+
}
|
|
293
|
+
.toastStyleFull:hover {
|
|
294
|
+
scale: 1.005;
|
|
295
|
+
}
|
|
296
|
+
</style>
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
<!-- ToastContainer.vue -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed, ref, watch, nextTick, onMounted, onUnmounted } from "vue";
|
|
4
|
+
import { useToast } from "./useToast";
|
|
5
|
+
import Toast from "./Toast.vue";
|
|
6
|
+
import type { ToastInstance } from "./types";
|
|
7
|
+
import { gsap } from "gsap";
|
|
8
|
+
import { Flip } from "gsap/Flip";
|
|
9
|
+
|
|
10
|
+
// Register GSAP plugin
|
|
11
|
+
gsap.registerPlugin(Flip);
|
|
12
|
+
|
|
13
|
+
const { toasts, removeToast, toastStyle } = useToast();
|
|
14
|
+
const containerRef = ref<HTMLElement>();
|
|
15
|
+
|
|
16
|
+
// Track which toasts have been animated in
|
|
17
|
+
const animatedToasts = new Set<string>();
|
|
18
|
+
|
|
19
|
+
// Track auto-dismiss timeouts
|
|
20
|
+
const autoDismissTimeouts = new Map<string, NodeJS.Timeout>();
|
|
21
|
+
|
|
22
|
+
const handleDismiss = async (id: string) => {
|
|
23
|
+
// Clear any auto-dismiss timeout for this toast
|
|
24
|
+
const existingTimeout = autoDismissTimeouts.get(id);
|
|
25
|
+
if (existingTimeout) {
|
|
26
|
+
clearTimeout(existingTimeout);
|
|
27
|
+
autoDismissTimeouts.delete(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Find the toast element that's being removed
|
|
31
|
+
const toastEl = containerRef.value?.querySelector(`[data-toast-id="${id}"]`);
|
|
32
|
+
|
|
33
|
+
if (toastEl) {
|
|
34
|
+
// Get the toast position for animation direction
|
|
35
|
+
const isTop = toastEl.parentElement?.classList.contains("topGroup");
|
|
36
|
+
|
|
37
|
+
// Animate out the specific toast
|
|
38
|
+
await gsap.to(toastEl, {
|
|
39
|
+
opacity: 0,
|
|
40
|
+
y: isTop ? -100 : 100,
|
|
41
|
+
scale: 0.95,
|
|
42
|
+
filter: "blur(8px)",
|
|
43
|
+
duration: 0.4,
|
|
44
|
+
ease: "power2.in",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Capture state of remaining toasts
|
|
48
|
+
const remainingToasts = containerRef.value?.querySelectorAll(
|
|
49
|
+
'.toast-wrapper:not([data-toast-id="' + id + '"])'
|
|
50
|
+
);
|
|
51
|
+
const state = remainingToasts ? Flip.getState(remainingToasts) : null;
|
|
52
|
+
|
|
53
|
+
// Remove the toast
|
|
54
|
+
removeToast(id);
|
|
55
|
+
animatedToasts.delete(id);
|
|
56
|
+
|
|
57
|
+
// Animate remaining toasts to new positions
|
|
58
|
+
await nextTick();
|
|
59
|
+
if (remainingToasts && remainingToasts.length > 0 && state) {
|
|
60
|
+
Flip.from(state, {
|
|
61
|
+
duration: 0.4,
|
|
62
|
+
ease: "power2.inOut",
|
|
63
|
+
stagger: 0.01,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Fallback if element not found
|
|
68
|
+
removeToast(id);
|
|
69
|
+
animatedToasts.delete(id);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Group toasts by position for proper stacking
|
|
74
|
+
const toastsByPosition = computed(() => {
|
|
75
|
+
const grouped: Record<string, ToastInstance[]> = {
|
|
76
|
+
Top: [],
|
|
77
|
+
Bottom: [],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
toasts.value.forEach((toast) => {
|
|
81
|
+
const position = toast.position || "Top";
|
|
82
|
+
grouped[position].push(toast);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return grouped;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Animate new toasts
|
|
89
|
+
const animateNewToasts = async () => {
|
|
90
|
+
await nextTick();
|
|
91
|
+
|
|
92
|
+
const newToasts = containerRef.value?.querySelectorAll(".toast-wrapper");
|
|
93
|
+
let delay = 0;
|
|
94
|
+
|
|
95
|
+
newToasts?.forEach((toastEl) => {
|
|
96
|
+
const id = toastEl.getAttribute("data-toast-id");
|
|
97
|
+
if (id && !animatedToasts.has(id)) {
|
|
98
|
+
animatedToasts.add(id);
|
|
99
|
+
|
|
100
|
+
// Determine direction based on position
|
|
101
|
+
const isTop = toastEl.parentElement?.classList.contains("topGroup");
|
|
102
|
+
|
|
103
|
+
gsap.fromTo(
|
|
104
|
+
toastEl,
|
|
105
|
+
{
|
|
106
|
+
opacity: 0,
|
|
107
|
+
y: isTop ? -100 : 100,
|
|
108
|
+
scale: 0.9,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
opacity: 1,
|
|
112
|
+
y: 0,
|
|
113
|
+
scale: 1,
|
|
114
|
+
duration: 0.5,
|
|
115
|
+
delay: delay,
|
|
116
|
+
ease: "back.out(1.2)",
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
delay += 0.05;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Setup auto-dismiss for toasts
|
|
126
|
+
const setupAutoDismiss = (toast: ToastInstance) => {
|
|
127
|
+
// Clear any existing timeout for this toast
|
|
128
|
+
const existingTimeout = autoDismissTimeouts.get(toast.id);
|
|
129
|
+
if (existingTimeout) {
|
|
130
|
+
clearTimeout(existingTimeout);
|
|
131
|
+
autoDismissTimeouts.delete(toast.id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Only setup auto-dismiss if duration is greater than 0
|
|
135
|
+
if (toast.duration && toast.duration > 0) {
|
|
136
|
+
const timeout = setTimeout(() => {
|
|
137
|
+
handleDismiss(toast.id);
|
|
138
|
+
autoDismissTimeouts.delete(toast.id);
|
|
139
|
+
}, toast.duration);
|
|
140
|
+
|
|
141
|
+
autoDismissTimeouts.set(toast.id, timeout);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Watch for changes in toasts
|
|
146
|
+
watch(
|
|
147
|
+
() => toasts.value,
|
|
148
|
+
(newToasts) => {
|
|
149
|
+
animateNewToasts();
|
|
150
|
+
|
|
151
|
+
// Setup auto-dismiss for new toasts
|
|
152
|
+
newToasts.forEach((toast) => {
|
|
153
|
+
if (!autoDismissTimeouts.has(toast.id)) {
|
|
154
|
+
setupAutoDismiss(toast);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
{ deep: true }
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
onMounted(() => {
|
|
162
|
+
animateNewToasts();
|
|
163
|
+
|
|
164
|
+
// Setup auto-dismiss for existing toasts
|
|
165
|
+
toasts.value.forEach((toast) => {
|
|
166
|
+
setupAutoDismiss(toast);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
onUnmounted(() => {
|
|
171
|
+
// Clear all timeouts when component is unmounted
|
|
172
|
+
autoDismissTimeouts.forEach((timeout) => {
|
|
173
|
+
clearTimeout(timeout);
|
|
174
|
+
});
|
|
175
|
+
autoDismissTimeouts.clear();
|
|
176
|
+
});
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<template>
|
|
180
|
+
<Teleport to="body">
|
|
181
|
+
<div
|
|
182
|
+
v-if="toasts.length > 0"
|
|
183
|
+
ref="containerRef"
|
|
184
|
+
:class="[
|
|
185
|
+
$style.container,
|
|
186
|
+
$style[
|
|
187
|
+
`toastStyle${
|
|
188
|
+
toastStyle.charAt(0).toUpperCase() + toastStyle.slice(1)
|
|
189
|
+
}`
|
|
190
|
+
],
|
|
191
|
+
]"
|
|
192
|
+
role="region"
|
|
193
|
+
aria-label="Notifications"
|
|
194
|
+
aria-live="polite"
|
|
195
|
+
>
|
|
196
|
+
<!-- Top positioned toasts -->
|
|
197
|
+
<div :class="[$style.toastGroup, $style.topGroup, 'topGroup']">
|
|
198
|
+
<div
|
|
199
|
+
v-for="(toast, index) in toastsByPosition.Top"
|
|
200
|
+
:key="toast.id"
|
|
201
|
+
:data-toast-id="toast.id"
|
|
202
|
+
:class="$style.toastWrapper"
|
|
203
|
+
class="toast-wrapper"
|
|
204
|
+
:style="{
|
|
205
|
+
zIndex: 999 - index,
|
|
206
|
+
}"
|
|
207
|
+
>
|
|
208
|
+
<Toast
|
|
209
|
+
:toast="toast"
|
|
210
|
+
:toastStyle="toastStyle"
|
|
211
|
+
@dismiss="handleDismiss"
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<!-- Bottom positioned toasts -->
|
|
217
|
+
<div :class="[$style.toastGroup, $style.bottomGroup, 'bottomGroup']">
|
|
218
|
+
<div
|
|
219
|
+
v-for="(toast, index) in toastsByPosition.Bottom"
|
|
220
|
+
:key="toast.id"
|
|
221
|
+
:data-toast-id="toast.id"
|
|
222
|
+
:class="$style.toastWrapper"
|
|
223
|
+
class="toast-wrapper"
|
|
224
|
+
:style="{
|
|
225
|
+
zIndex: 999 - index,
|
|
226
|
+
}"
|
|
227
|
+
>
|
|
228
|
+
<Toast
|
|
229
|
+
:toast="toast"
|
|
230
|
+
:toastStyle="toastStyle"
|
|
231
|
+
@dismiss="handleDismiss"
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</Teleport>
|
|
237
|
+
</template>
|
|
238
|
+
|
|
239
|
+
<style module>
|
|
240
|
+
.container {
|
|
241
|
+
position: fixed;
|
|
242
|
+
top: 0;
|
|
243
|
+
left: 0;
|
|
244
|
+
right: 0;
|
|
245
|
+
bottom: 0;
|
|
246
|
+
pointer-events: none;
|
|
247
|
+
z-index: 999;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.toastGroup {
|
|
251
|
+
position: absolute;
|
|
252
|
+
left: 0;
|
|
253
|
+
right: 0;
|
|
254
|
+
display: flex;
|
|
255
|
+
flex-direction: column;
|
|
256
|
+
align-items: center;
|
|
257
|
+
pointer-events: none;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.toastStyleBar .toastGroup {
|
|
261
|
+
padding: 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.toastStyleFull .toastGroup {
|
|
265
|
+
padding: 1rem;
|
|
266
|
+
gap: 1rem;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.toastStyleNotification .toastGroup {
|
|
270
|
+
gap: 1rem;
|
|
271
|
+
padding: 1rem;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.topGroup {
|
|
275
|
+
top: 0;
|
|
276
|
+
display: flex;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.toastStyleNotification .topGroup {
|
|
280
|
+
justify-content: end;
|
|
281
|
+
align-items: end;
|
|
282
|
+
flex-direction: column;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.bottomGroup {
|
|
286
|
+
bottom: 0;
|
|
287
|
+
flex-direction: column-reverse;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.toastStyleNotification .bottomGroup {
|
|
291
|
+
justify-content: start;
|
|
292
|
+
align-items: end;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.toastWrapper {
|
|
296
|
+
pointer-events: auto;
|
|
297
|
+
width: 100%;
|
|
298
|
+
display: flex;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.toastStyleBar .toastWrapper {
|
|
302
|
+
width: 100%;
|
|
303
|
+
max-width: 100%;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.toastStyleFull .toastWrapper {
|
|
307
|
+
width: 100%;
|
|
308
|
+
max-width: 100%;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.toastStyleNotification .toastWrapper {
|
|
312
|
+
max-width: 360px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Focus management */
|
|
316
|
+
.toastWrapper:focus-within {
|
|
317
|
+
z-index: 1000;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* Responsive adjustments */
|
|
321
|
+
@media (max-width: 640px) {
|
|
322
|
+
.toastGroup {
|
|
323
|
+
padding: 0.5rem;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.toastWrapper {
|
|
327
|
+
max-width: calc(100vw - 1rem);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* Light theme using Colors */
|
|
2
|
+
:root {
|
|
3
|
+
/* Toast type colors */
|
|
4
|
+
--toast-success-bg: #30a46c; /* green9 - success color */
|
|
5
|
+
--toast-error-bg: #e5484d; /* red9 - error color */
|
|
6
|
+
--toast-warning-bg: #ffe629; /* yellow9 - warning color */
|
|
7
|
+
--toast-info-bg: #5b5bd6; /* violet9 - info color */
|
|
8
|
+
--toast-custom-bg: #6b7280; /* gray9 - fallback for custom colors */
|
|
9
|
+
|
|
10
|
+
/* Toast shadow colors */
|
|
11
|
+
--toast-shadow: rgba(
|
|
12
|
+
0,
|
|
13
|
+
0,
|
|
14
|
+
0,
|
|
15
|
+
0.08
|
|
16
|
+
); /* blackA6 - lighter shadow for light mode */
|
|
17
|
+
--toast-border: rgba(
|
|
18
|
+
0,
|
|
19
|
+
0,
|
|
20
|
+
0,
|
|
21
|
+
0.08
|
|
22
|
+
); /* blackA6 - lighter border for light mode */
|
|
23
|
+
|
|
24
|
+
/* Toast description opacity */
|
|
25
|
+
--toast-description-opacity: 0.8; /* slightly more visible in light mode */
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Dark theme */
|
|
29
|
+
.dark,
|
|
30
|
+
.dark-theme {
|
|
31
|
+
/* Toast type colors */
|
|
32
|
+
--toast-success-bg: #30a46c; /* green9 - success color */
|
|
33
|
+
--toast-error-bg: #e5484d; /* red9 - error color */
|
|
34
|
+
--toast-warning-bg: #ffe629; /* yellow9 - warning color */
|
|
35
|
+
--toast-info-bg: #5b5bd6; /* violet9 - info color */
|
|
36
|
+
--toast-custom-bg: #6b7280; /* gray9 - fallback for custom colors */
|
|
37
|
+
|
|
38
|
+
/* Toast shadow colors */
|
|
39
|
+
--toast-shadow: rgba(0, 0, 0, 0.15); /* Original dark mode value */
|
|
40
|
+
--toast-border: rgba(0, 0, 0, 0.15); /* Original dark mode value */
|
|
41
|
+
|
|
42
|
+
/* Toast description opacity */
|
|
43
|
+
--toast-description-opacity: 0.9; /* Original dark mode value */
|
|
44
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// types.ts
|
|
2
|
+
export type BarPosition = "Top" | "Bottom";
|
|
3
|
+
export type ToastType = "success" | "error" | "warning" | "info";
|
|
4
|
+
|
|
5
|
+
export interface ToastAction {
|
|
6
|
+
label: string;
|
|
7
|
+
action: () => void;
|
|
8
|
+
style?: "primary" | "secondary";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ToastOptions {
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
iconName?: string;
|
|
15
|
+
position?: BarPosition;
|
|
16
|
+
toastType?: ToastType | string; // string allows custom colors
|
|
17
|
+
duration?: number; // Auto dismiss duration in ms (0 = no auto dismiss)
|
|
18
|
+
dismissible?: boolean;
|
|
19
|
+
actions?: ToastAction[]; // Optional action buttons
|
|
20
|
+
onDismiss?: () => void; // Callback when toast is dismissed
|
|
21
|
+
className?: string; // Custom CSS class
|
|
22
|
+
role?: "alert" | "status"; // ARIA role
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ToastInstance
|
|
26
|
+
extends Required<
|
|
27
|
+
Omit<ToastOptions, "actions" | "onDismiss" | "className" | "role">
|
|
28
|
+
> {
|
|
29
|
+
id: string;
|
|
30
|
+
show: boolean;
|
|
31
|
+
actions?: ToastAction[];
|
|
32
|
+
onDismiss?: () => void;
|
|
33
|
+
className?: string;
|
|
34
|
+
role?: "alert" | "status";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Preset configurations for common scenarios
|
|
38
|
+
export const ToastPresets = {
|
|
39
|
+
quick: { duration: 2000 } as Partial<ToastOptions>,
|
|
40
|
+
persistent: { duration: 0, dismissible: true } as Partial<ToastOptions>,
|
|
41
|
+
important: {
|
|
42
|
+
duration: 0,
|
|
43
|
+
dismissible: false,
|
|
44
|
+
role: "alert",
|
|
45
|
+
} as Partial<ToastOptions>,
|
|
46
|
+
} as const;
|