uibee 2.5.7 → 2.5.8
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/src/components/index.d.ts +1 -1
- package/dist/src/components/index.js +1 -1
- package/dist/src/components/toast/toastItem.d.ts +2 -0
- package/dist/src/components/toast/toastItem.js +38 -0
- package/dist/src/components/toast/toaster.d.ts +2 -2
- package/dist/src/components/toast/toaster.js +69 -76
- package/dist/src/globals.css +68 -47
- package/package.json +1 -1
- package/src/components/index.ts +1 -1
- package/src/components/toast/toastItem.tsx +61 -0
- package/src/components/toast/toaster.tsx +90 -99
- package/src/types/components.d.ts +17 -13
|
@@ -14,5 +14,5 @@ export { default as PageContainer } from './container/page';
|
|
|
14
14
|
export { default as Highlight } from './container/highlight';
|
|
15
15
|
export { default as VersionTag } from './version/version';
|
|
16
16
|
export { default as LoginPage } from './login/loginPage';
|
|
17
|
-
export { default as Toaster,
|
|
17
|
+
export { default as Toaster, toast } from './toast/toaster';
|
|
18
18
|
export { default as Button } from './buttons/button';
|
|
@@ -20,6 +20,6 @@ export { default as Highlight } from './container/highlight';
|
|
|
20
20
|
// Other components
|
|
21
21
|
export { default as VersionTag } from './version/version';
|
|
22
22
|
export { default as LoginPage } from './login/loginPage';
|
|
23
|
-
export { default as Toaster,
|
|
23
|
+
export { default as Toaster, toast } from './toast/toaster';
|
|
24
24
|
// Buttons
|
|
25
25
|
export { default as Button } from './buttons/button';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AlertCircle, AlertTriangle, CheckCircle, Info, X } from 'lucide-react';
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
+
export default function Toast({ toast, index, expanded, onRemove, onHeight, offset, frontHeight }) {
|
|
5
|
+
const [mounted, setMounted] = useState(false);
|
|
6
|
+
const ref = useRef(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
requestAnimationFrame(() => setMounted(true));
|
|
9
|
+
}, []);
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
if (ref.current) {
|
|
12
|
+
onHeight(toast.id, ref.current.offsetHeight);
|
|
13
|
+
}
|
|
14
|
+
}, [toast.message, onHeight, toast.id, expanded]);
|
|
15
|
+
const isVisible = mounted && !toast.exiting;
|
|
16
|
+
const isFront = index === 0;
|
|
17
|
+
const collapsedOffset = index * 10 + (index > 0 ? (frontHeight - 60) : 0);
|
|
18
|
+
return (_jsx("li", { ref: ref, className: 'absolute bottom-0 right-0 w-full transition-all duration-300 ease-out pointer-events-auto', style: {
|
|
19
|
+
transform: isVisible
|
|
20
|
+
? `translateY(${expanded ? -offset : -collapsedOffset}px) scale(${expanded ? 1 : 1 - index * 0.05})`
|
|
21
|
+
: 'translateY(20px) scale(0.9)',
|
|
22
|
+
opacity: isVisible ? 1 : 0,
|
|
23
|
+
zIndex: toast.id
|
|
24
|
+
}, children: _jsxs("div", { className: 'flex items-center space-x-4 rounded-lg p-4 shadow-lg border-2 border-login-400 bg-login-700', children: [_jsx("div", { className: 'shrink-0', children: Icon(toast.type) }), _jsx("p", { className: `flex-1 text-sm font-semibold text-login-800 dark:text-login-100 min-w-0
|
|
25
|
+
${!expanded && !isFront ? 'truncate' : ''}`, children: toast.message }), _jsx("button", { onClick: onRemove, className: 'hover:text-login-200 text-login-400', children: _jsx(X, { className: 'h-5 w-5' }) })] }) }));
|
|
26
|
+
}
|
|
27
|
+
function Icon(type) {
|
|
28
|
+
switch (type) {
|
|
29
|
+
case 'info':
|
|
30
|
+
return _jsx(Info, { className: 'h-6 w-6 text-blue-500' });
|
|
31
|
+
case 'success':
|
|
32
|
+
return _jsx(CheckCircle, { className: 'h-6 w-6 text-green-500' });
|
|
33
|
+
case 'warning':
|
|
34
|
+
return _jsx(AlertTriangle, { className: 'h-6 w-6 text-yellow-500' });
|
|
35
|
+
case 'error':
|
|
36
|
+
return _jsx(AlertCircle, { className: 'h-6 w-6 text-red-500' });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { ToastType } from 'uibee/components';
|
|
2
|
-
export declare function
|
|
1
|
+
import type { ToastType } from 'uibee/components';
|
|
2
|
+
export declare function toast(message: string, type: ToastType, duration?: number): void;
|
|
3
3
|
export default function Toaster(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,89 +1,82 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import ToastItem from 'toastItem';
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
let idCounter = 0;
|
|
7
|
+
export function toast(message, type, duration = 4000) {
|
|
8
|
+
const id = ++idCounter;
|
|
9
|
+
listeners.forEach(listener => listener({ id, message, type, expiresAt: Date.now() + duration }));
|
|
8
10
|
}
|
|
9
11
|
export default function Toaster() {
|
|
10
12
|
const [toasts, setToasts] = useState([]);
|
|
11
|
-
const
|
|
12
|
-
const [
|
|
13
|
-
const pauseTimes = useRef({});
|
|
14
|
-
const mainToastRef = useRef(null);
|
|
15
|
-
const [mainToastPosition, setMainToastPosition] = useState(null);
|
|
13
|
+
const [expanded, setExpanded] = useState(false);
|
|
14
|
+
const [heights, setHeights] = useState({});
|
|
16
15
|
useEffect(() => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return newToasts;
|
|
22
|
-
});
|
|
23
|
-
};
|
|
24
|
-
observers.push(listener);
|
|
16
|
+
function addToast(toast) {
|
|
17
|
+
setToasts(prev => [toast, ...prev]);
|
|
18
|
+
}
|
|
19
|
+
listeners.add(addToast);
|
|
25
20
|
return () => {
|
|
26
|
-
|
|
27
|
-
if (idx > -1)
|
|
28
|
-
observers.splice(idx, 1);
|
|
29
|
-
Object.values(timers.current).forEach(clearTimeout);
|
|
21
|
+
listeners.delete(addToast);
|
|
30
22
|
};
|
|
31
23
|
}, []);
|
|
32
24
|
useEffect(() => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
setToasts(prev => prev.filter(t => t.id !== toast.id));
|
|
49
|
-
delete timers.current[toast.id];
|
|
50
|
-
}, toast.remaining);
|
|
51
|
-
setToasts(prev => prev.map(t => t.id === toast.id ? { ...t, created: Date.now() } : t));
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}, [isHovered, toasts]);
|
|
56
|
-
// Track main toast position for stacking
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
setToasts(prev => prev.map(toast => {
|
|
27
|
+
if (expanded)
|
|
28
|
+
return { ...toast, pausedAt: toast.pausedAt || now };
|
|
29
|
+
if (!toast.pausedAt)
|
|
30
|
+
return toast;
|
|
31
|
+
return { ...toast, expiresAt: toast.expiresAt + (now - toast.pausedAt), pausedAt: undefined };
|
|
32
|
+
}));
|
|
33
|
+
}, [expanded]);
|
|
34
|
+
function removeToast(id) {
|
|
35
|
+
setToasts(prev => prev.map(toast => toast.id === id ? { ...toast, exiting: true } : toast));
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
setToasts(prev => prev.filter(toast => toast.id !== id));
|
|
38
|
+
}, 300);
|
|
39
|
+
}
|
|
57
40
|
useEffect(() => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
41
|
+
const timer = setInterval(() => {
|
|
42
|
+
if (expanded)
|
|
43
|
+
return;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
setToasts(prev => {
|
|
46
|
+
const toastsToExit = prev.filter(toast => !toast.exiting && !toast.pausedAt && toast.expiresAt <= now);
|
|
47
|
+
if (toastsToExit.length === 0)
|
|
48
|
+
return prev;
|
|
49
|
+
toastsToExit.forEach(toast => {
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
setToasts(current => current.filter(item => item.id !== toast.id));
|
|
52
|
+
}, 300);
|
|
53
|
+
});
|
|
54
|
+
return prev.map(toast => toastsToExit.find(exitToast => exitToast.id === toast.id) ? { ...toast, exiting: true } : toast);
|
|
63
55
|
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
zIndex: 100 - idx,
|
|
74
|
-
transform: `scale(${1 - idx * 0.05})`,
|
|
75
|
-
}, children: [_jsx("span", { className: 'shrink-0 w-10 h-10 flex items-center justify-center', children: _jsx(ToastIcon, { type: toast.type }) }), _jsxs("div", { className: 'pr-1 pb-1', children: [_jsx("span", { className: 'font-bold', children: toast.title }), (idx === 0 || isHovered) &&
|
|
76
|
-
_jsx("span", { className: 'text-sm line-clamp-3', children: toast.description })] })] }, `${toast.id}-${idx}`))) }));
|
|
77
|
-
}
|
|
78
|
-
function ToastIcon({ type }) {
|
|
79
|
-
switch (type) {
|
|
80
|
-
case 'success':
|
|
81
|
-
return _jsx(CircleCheck, { className: 'text-green-300/70' });
|
|
82
|
-
case 'warning':
|
|
83
|
-
return _jsx(CircleAlert, { className: 'text-yellow-300/70' });
|
|
84
|
-
case 'error':
|
|
85
|
-
return _jsx(CircleX, { className: 'text-red-300/70' });
|
|
86
|
-
case 'info':
|
|
87
|
-
return _jsx(Info, { className: 'text-blue-300/70' });
|
|
56
|
+
}, 100);
|
|
57
|
+
return () => clearInterval(timer);
|
|
58
|
+
}, [expanded]);
|
|
59
|
+
function onHeight(id, height) {
|
|
60
|
+
setHeights(prev => {
|
|
61
|
+
if (prev[id] === height)
|
|
62
|
+
return prev;
|
|
63
|
+
return { ...prev, [id]: height };
|
|
64
|
+
});
|
|
88
65
|
}
|
|
66
|
+
const visibleToasts = toasts.slice(0, expanded ? 10 : 3);
|
|
67
|
+
const frontHeight = heights[visibleToasts[0]?.id] || 60;
|
|
68
|
+
const offsets = useMemo(() => {
|
|
69
|
+
let currentOffset = 0;
|
|
70
|
+
return visibleToasts.map(toast => {
|
|
71
|
+
const height = heights[toast.id] || 60;
|
|
72
|
+
const offset = currentOffset;
|
|
73
|
+
currentOffset += height + 16;
|
|
74
|
+
return offset;
|
|
75
|
+
});
|
|
76
|
+
}, [visibleToasts, heights]);
|
|
77
|
+
const totalHeight = offsets.length > 0
|
|
78
|
+
? offsets[offsets.length - 1] + (heights[visibleToasts[visibleToasts.length - 1]?.id] || 60)
|
|
79
|
+
: 0;
|
|
80
|
+
return (_jsx("ul", { className: `fixed bottom-4 right-4 z-9999 w-full max-w-sm flex flex-col items-end transition-all duration-300 ease-out
|
|
81
|
+
${expanded ? 'pointer-events-auto' : 'pointer-events-none'}`, style: { height: expanded ? totalHeight + 'px' : 'auto' }, onMouseEnter: () => setExpanded(true), onMouseLeave: () => setExpanded(false), children: visibleToasts.map((toast, index) => (_jsx(ToastItem, { toast: toast, index: index, expanded: expanded, onRemove: () => removeToast(toast.id), onHeight: onHeight, offset: offsets[index], frontHeight: frontHeight }, toast.id))) }));
|
|
89
82
|
}
|
package/dist/src/globals.css
CHANGED
|
@@ -7,13 +7,12 @@
|
|
|
7
7
|
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
8
8
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
|
9
9
|
"Courier New", monospace;
|
|
10
|
-
--color-red-300: oklch(80.8% 0.114 19.571);
|
|
11
10
|
--color-red-500: oklch(63.7% 0.237 25.331);
|
|
12
11
|
--color-red-700: oklch(50.5% 0.213 27.518);
|
|
13
12
|
--color-red-800: oklch(44.4% 0.177 26.899);
|
|
14
|
-
--color-yellow-
|
|
15
|
-
--color-green-
|
|
16
|
-
--color-blue-
|
|
13
|
+
--color-yellow-500: oklch(79.5% 0.184 86.047);
|
|
14
|
+
--color-green-500: oklch(72.3% 0.219 149.579);
|
|
15
|
+
--color-blue-500: oklch(62.3% 0.214 259.815);
|
|
17
16
|
--color-gray-400: oklch(70.7% 0.022 261.325);
|
|
18
17
|
--color-black: #000;
|
|
19
18
|
--color-white: #fff;
|
|
@@ -58,8 +57,10 @@
|
|
|
58
57
|
--color-login-700: #1a1a1a;
|
|
59
58
|
--color-login-600: #212121;
|
|
60
59
|
--color-login-500: #323232;
|
|
60
|
+
--color-login-400: #424242;
|
|
61
61
|
--color-login-300: #5e5e5e;
|
|
62
62
|
--color-login-200: #727272;
|
|
63
|
+
--color-login-100: #b0b0b0;
|
|
63
64
|
--color-login-50: #ededed;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -326,6 +327,9 @@
|
|
|
326
327
|
.right-4 {
|
|
327
328
|
right: calc(var(--spacing) * 4);
|
|
328
329
|
}
|
|
330
|
+
.bottom-0 {
|
|
331
|
+
bottom: calc(var(--spacing) * 0);
|
|
332
|
+
}
|
|
329
333
|
.bottom-4 {
|
|
330
334
|
bottom: calc(var(--spacing) * 4);
|
|
331
335
|
}
|
|
@@ -353,6 +357,9 @@
|
|
|
353
357
|
.z-900 {
|
|
354
358
|
z-index: 900;
|
|
355
359
|
}
|
|
360
|
+
.z-9999 {
|
|
361
|
+
z-index: 9999;
|
|
362
|
+
}
|
|
356
363
|
.col-start-3 {
|
|
357
364
|
grid-column-start: 3;
|
|
358
365
|
}
|
|
@@ -402,12 +409,6 @@
|
|
|
402
409
|
padding-top: 2rem;
|
|
403
410
|
}
|
|
404
411
|
}
|
|
405
|
-
.line-clamp-3 {
|
|
406
|
-
overflow: hidden;
|
|
407
|
-
display: -webkit-box;
|
|
408
|
-
-webkit-box-orient: vertical;
|
|
409
|
-
-webkit-line-clamp: 3;
|
|
410
|
-
}
|
|
411
412
|
.block {
|
|
412
413
|
display: block;
|
|
413
414
|
}
|
|
@@ -432,15 +433,15 @@
|
|
|
432
433
|
.h-4 {
|
|
433
434
|
height: calc(var(--spacing) * 4);
|
|
434
435
|
}
|
|
436
|
+
.h-5 {
|
|
437
|
+
height: calc(var(--spacing) * 5);
|
|
438
|
+
}
|
|
435
439
|
.h-6 {
|
|
436
440
|
height: calc(var(--spacing) * 6);
|
|
437
441
|
}
|
|
438
442
|
.h-8 {
|
|
439
443
|
height: calc(var(--spacing) * 8);
|
|
440
444
|
}
|
|
441
|
-
.h-10 {
|
|
442
|
-
height: calc(var(--spacing) * 10);
|
|
443
|
-
}
|
|
444
445
|
.h-11 {
|
|
445
446
|
height: calc(var(--spacing) * 11);
|
|
446
447
|
}
|
|
@@ -513,9 +514,6 @@
|
|
|
513
514
|
.w-max {
|
|
514
515
|
width: max-content;
|
|
515
516
|
}
|
|
516
|
-
.w-sm {
|
|
517
|
-
width: var(--container-sm);
|
|
518
|
-
}
|
|
519
517
|
.max-w-2xl {
|
|
520
518
|
max-width: var(--container-2xl);
|
|
521
519
|
}
|
|
@@ -534,9 +532,15 @@
|
|
|
534
532
|
.max-w-md {
|
|
535
533
|
max-width: var(--container-md);
|
|
536
534
|
}
|
|
535
|
+
.max-w-sm {
|
|
536
|
+
max-width: var(--container-sm);
|
|
537
|
+
}
|
|
537
538
|
.max-w-xs {
|
|
538
539
|
max-width: var(--container-xs);
|
|
539
540
|
}
|
|
541
|
+
.min-w-0 {
|
|
542
|
+
min-width: calc(var(--spacing) * 0);
|
|
543
|
+
}
|
|
540
544
|
.min-w-\[6ch\] {
|
|
541
545
|
min-width: 6ch;
|
|
542
546
|
}
|
|
@@ -621,9 +625,6 @@
|
|
|
621
625
|
.flex-col {
|
|
622
626
|
flex-direction: column;
|
|
623
627
|
}
|
|
624
|
-
.flex-col-reverse {
|
|
625
|
-
flex-direction: column-reverse;
|
|
626
|
-
}
|
|
627
628
|
.flex-row {
|
|
628
629
|
flex-direction: row;
|
|
629
630
|
}
|
|
@@ -666,6 +667,13 @@
|
|
|
666
667
|
.gap-4 {
|
|
667
668
|
gap: calc(var(--spacing) * 4);
|
|
668
669
|
}
|
|
670
|
+
.space-x-4 {
|
|
671
|
+
:where(& > :not(:last-child)) {
|
|
672
|
+
--tw-space-x-reverse: 0;
|
|
673
|
+
margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));
|
|
674
|
+
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
669
677
|
.truncate {
|
|
670
678
|
overflow: hidden;
|
|
671
679
|
text-overflow: ellipsis;
|
|
@@ -709,6 +717,10 @@
|
|
|
709
717
|
border-style: var(--tw-border-style);
|
|
710
718
|
border-width: 0px;
|
|
711
719
|
}
|
|
720
|
+
.border-2 {
|
|
721
|
+
border-style: var(--tw-border-style);
|
|
722
|
+
border-width: 2px;
|
|
723
|
+
}
|
|
712
724
|
.border-\[0\.10rem\] {
|
|
713
725
|
border-style: var(--tw-border-style);
|
|
714
726
|
border-width: 0.10rem;
|
|
@@ -724,6 +736,9 @@
|
|
|
724
736
|
.border-login-200 {
|
|
725
737
|
border-color: var(--color-login-200);
|
|
726
738
|
}
|
|
739
|
+
.border-login-400 {
|
|
740
|
+
border-color: var(--color-login-400);
|
|
741
|
+
}
|
|
727
742
|
.bg-\[\#181818f0\] {
|
|
728
743
|
background-color: #181818f0;
|
|
729
744
|
}
|
|
@@ -823,6 +838,9 @@
|
|
|
823
838
|
.p-3 {
|
|
824
839
|
padding: calc(var(--spacing) * 3);
|
|
825
840
|
}
|
|
841
|
+
.p-4 {
|
|
842
|
+
padding: calc(var(--spacing) * 4);
|
|
843
|
+
}
|
|
826
844
|
.p-8 {
|
|
827
845
|
padding: calc(var(--spacing) * 8);
|
|
828
846
|
}
|
|
@@ -874,15 +892,9 @@
|
|
|
874
892
|
.pt-4 {
|
|
875
893
|
padding-top: calc(var(--spacing) * 4);
|
|
876
894
|
}
|
|
877
|
-
.pr-1 {
|
|
878
|
-
padding-right: calc(var(--spacing) * 1);
|
|
879
|
-
}
|
|
880
895
|
.pr-4 {
|
|
881
896
|
padding-right: calc(var(--spacing) * 4);
|
|
882
897
|
}
|
|
883
|
-
.pb-1 {
|
|
884
|
-
padding-bottom: calc(var(--spacing) * 1);
|
|
885
|
-
}
|
|
886
898
|
.pb-2\.5 {
|
|
887
899
|
padding-bottom: calc(var(--spacing) * 2.5);
|
|
888
900
|
}
|
|
@@ -961,29 +973,23 @@
|
|
|
961
973
|
.whitespace-nowrap {
|
|
962
974
|
white-space: nowrap;
|
|
963
975
|
}
|
|
964
|
-
.text-blue-
|
|
965
|
-
color: color-
|
|
966
|
-
@supports (color: color-mix(in lab, red, red)) {
|
|
967
|
-
color: color-mix(in oklab, var(--color-blue-300) 70%, transparent);
|
|
968
|
-
}
|
|
976
|
+
.text-blue-500 {
|
|
977
|
+
color: var(--color-blue-500);
|
|
969
978
|
}
|
|
970
|
-
.text-green-
|
|
971
|
-
color: color-
|
|
972
|
-
@supports (color: color-mix(in lab, red, red)) {
|
|
973
|
-
color: color-mix(in oklab, var(--color-green-300) 70%, transparent);
|
|
974
|
-
}
|
|
979
|
+
.text-green-500 {
|
|
980
|
+
color: var(--color-green-500);
|
|
975
981
|
}
|
|
976
982
|
.text-login {
|
|
977
983
|
color: var(--color-login);
|
|
978
984
|
}
|
|
979
|
-
.text-login-
|
|
980
|
-
color: var(--color-login-
|
|
985
|
+
.text-login-400 {
|
|
986
|
+
color: var(--color-login-400);
|
|
981
987
|
}
|
|
982
|
-
.text-
|
|
983
|
-
color: color-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
988
|
+
.text-login-800 {
|
|
989
|
+
color: var(--color-login-800);
|
|
990
|
+
}
|
|
991
|
+
.text-red-500 {
|
|
992
|
+
color: var(--color-red-500);
|
|
987
993
|
}
|
|
988
994
|
.text-red-500\/50 {
|
|
989
995
|
color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 50%, transparent);
|
|
@@ -997,11 +1003,8 @@
|
|
|
997
1003
|
.text-white {
|
|
998
1004
|
color: var(--color-white);
|
|
999
1005
|
}
|
|
1000
|
-
.text-yellow-
|
|
1001
|
-
color: color-
|
|
1002
|
-
@supports (color: color-mix(in lab, red, red)) {
|
|
1003
|
-
color: color-mix(in oklab, var(--color-yellow-300) 70%, transparent);
|
|
1004
|
-
}
|
|
1006
|
+
.text-yellow-500 {
|
|
1007
|
+
color: var(--color-yellow-500);
|
|
1005
1008
|
}
|
|
1006
1009
|
.italic {
|
|
1007
1010
|
font-style: italic;
|
|
@@ -1271,6 +1274,13 @@
|
|
|
1271
1274
|
}
|
|
1272
1275
|
}
|
|
1273
1276
|
}
|
|
1277
|
+
.hover\:text-login-200 {
|
|
1278
|
+
&:hover {
|
|
1279
|
+
@media (hover: hover) {
|
|
1280
|
+
color: var(--color-login-200);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1274
1284
|
.hover\:text-red-800 {
|
|
1275
1285
|
&:hover {
|
|
1276
1286
|
@media (hover: hover) {
|
|
@@ -1355,6 +1365,11 @@
|
|
|
1355
1365
|
gap: calc(var(--spacing) * 6);
|
|
1356
1366
|
}
|
|
1357
1367
|
}
|
|
1368
|
+
.dark\:text-login-100 {
|
|
1369
|
+
@media (prefers-color-scheme: dark) {
|
|
1370
|
+
color: var(--color-login-100);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1358
1373
|
}
|
|
1359
1374
|
:root {
|
|
1360
1375
|
transition: background 0.4s,
|
|
@@ -1449,6 +1464,11 @@
|
|
|
1449
1464
|
syntax: "*";
|
|
1450
1465
|
inherits: false;
|
|
1451
1466
|
}
|
|
1467
|
+
@property --tw-space-x-reverse {
|
|
1468
|
+
syntax: "*";
|
|
1469
|
+
inherits: false;
|
|
1470
|
+
initial-value: 0;
|
|
1471
|
+
}
|
|
1452
1472
|
@property --tw-border-style {
|
|
1453
1473
|
syntax: "*";
|
|
1454
1474
|
inherits: false;
|
|
@@ -1599,6 +1619,7 @@
|
|
|
1599
1619
|
--tw-rotate-z: initial;
|
|
1600
1620
|
--tw-skew-x: initial;
|
|
1601
1621
|
--tw-skew-y: initial;
|
|
1622
|
+
--tw-space-x-reverse: 0;
|
|
1602
1623
|
--tw-border-style: solid;
|
|
1603
1624
|
--tw-leading: initial;
|
|
1604
1625
|
--tw-font-weight: initial;
|
package/package.json
CHANGED
package/src/components/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { default as Highlight } from './container/highlight'
|
|
|
25
25
|
// Other components
|
|
26
26
|
export { default as VersionTag } from './version/version'
|
|
27
27
|
export { default as LoginPage } from './login/loginPage'
|
|
28
|
-
export { default as Toaster,
|
|
28
|
+
export { default as Toaster, toast } from './toast/toaster'
|
|
29
29
|
|
|
30
30
|
// Buttons
|
|
31
31
|
export { default as Button } from './buttons/button'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AlertCircle, AlertTriangle, CheckCircle, Info, X } from 'lucide-react'
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
3
|
+
import type { Toast, ToastType, ToastProps } from 'uibee/components'
|
|
4
|
+
|
|
5
|
+
export default function Toast({ toast, index, expanded, onRemove, onHeight, offset, frontHeight }: ToastProps) {
|
|
6
|
+
const [mounted, setMounted] = useState(false)
|
|
7
|
+
const ref = useRef<HTMLLIElement>(null)
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
requestAnimationFrame(() => setMounted(true))
|
|
11
|
+
}, [])
|
|
12
|
+
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
if (ref.current) {
|
|
15
|
+
onHeight(toast.id, ref.current.offsetHeight)
|
|
16
|
+
}
|
|
17
|
+
}, [toast.message, onHeight, toast.id, expanded])
|
|
18
|
+
|
|
19
|
+
const isVisible = mounted && !toast.exiting
|
|
20
|
+
const isFront = index === 0
|
|
21
|
+
const collapsedOffset = index * 10 + (index > 0 ? (frontHeight - 60) : 0)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<li
|
|
25
|
+
ref={ref}
|
|
26
|
+
className='absolute bottom-0 right-0 w-full transition-all duration-300 ease-out pointer-events-auto'
|
|
27
|
+
style={{
|
|
28
|
+
transform: isVisible
|
|
29
|
+
? `translateY(${expanded ? -offset : -collapsedOffset}px) scale(${expanded ? 1 : 1 - index * 0.05})`
|
|
30
|
+
: 'translateY(20px) scale(0.9)',
|
|
31
|
+
opacity: isVisible ? 1 : 0,
|
|
32
|
+
zIndex: toast.id
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<div className='flex items-center space-x-4 rounded-lg p-4 shadow-lg border-2 border-login-400 bg-login-700'>
|
|
36
|
+
<div className='shrink-0'>{Icon(toast.type)}</div>
|
|
37
|
+
<p className={`flex-1 text-sm font-semibold text-login-800 dark:text-login-100 min-w-0
|
|
38
|
+
${!expanded && !isFront ? 'truncate' : ''}`}
|
|
39
|
+
>
|
|
40
|
+
{toast.message}
|
|
41
|
+
</p>
|
|
42
|
+
<button onClick={onRemove} className='hover:text-login-200 text-login-400'>
|
|
43
|
+
<X className='h-5 w-5' />
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</li>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Icon(type: ToastType) {
|
|
51
|
+
switch (type) {
|
|
52
|
+
case 'info':
|
|
53
|
+
return <Info className='h-6 w-6 text-blue-500' />
|
|
54
|
+
case 'success':
|
|
55
|
+
return <CheckCircle className='h-6 w-6 text-green-500' />
|
|
56
|
+
case 'warning':
|
|
57
|
+
return <AlertTriangle className='h-6 w-6 text-yellow-500' />
|
|
58
|
+
case 'error':
|
|
59
|
+
return <AlertCircle className='h-6 w-6 text-red-500' />
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,123 +1,114 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
3
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
4
|
+
import type { Toast, ToastType } from 'uibee/components'
|
|
5
|
+
import ToastItem from '@components/toast/toastItem'
|
|
6
6
|
|
|
7
|
+
const listeners = new Set<(toast: Toast) => void>()
|
|
8
|
+
let idCounter = 0
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
observers.forEach((observer) => observer({ title, description, type }))
|
|
10
|
+
export function toast(message: string, type: ToastType, duration = 4000) {
|
|
11
|
+
const id = ++idCounter
|
|
12
|
+
listeners.forEach(listener => listener({ id, message, type, expiresAt: Date.now() + duration }))
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export default function Toaster() {
|
|
15
|
-
const [toasts, setToasts] = useState<
|
|
16
|
-
const
|
|
17
|
-
const [
|
|
18
|
-
const pauseTimes = useRef<{ [id: number]: number }>({})
|
|
19
|
-
const mainToastRef = useRef<HTMLDivElement>(null)
|
|
20
|
-
const [mainToastPosition, setMainToastPosition] = useState<{ top: number; right: number } | null>(null)
|
|
16
|
+
const [toasts, setToasts] = useState<Toast[]>([])
|
|
17
|
+
const [expanded, setExpanded] = useState(false)
|
|
18
|
+
const [heights, setHeights] = useState<Record<number, number>>({})
|
|
21
19
|
|
|
22
20
|
useEffect(() => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
setToasts(prev => {
|
|
26
|
-
const newToasts = prev.concat({ id, type, title, description, remaining: 3000, created: Date.now() }).slice(-3)
|
|
27
|
-
return newToasts
|
|
28
|
-
})
|
|
21
|
+
function addToast(toast: Toast) {
|
|
22
|
+
setToasts(prev => [toast, ...prev])
|
|
29
23
|
}
|
|
30
|
-
|
|
24
|
+
|
|
25
|
+
listeners.add(addToast)
|
|
26
|
+
|
|
31
27
|
return () => {
|
|
32
|
-
|
|
33
|
-
if (idx > -1) observers.splice(idx, 1)
|
|
34
|
-
Object.values(timers.current).forEach(clearTimeout)
|
|
28
|
+
listeners.delete(addToast)
|
|
35
29
|
}
|
|
36
30
|
}, [])
|
|
37
31
|
|
|
38
32
|
useEffect(() => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
setToasts(prev => prev.filter(t => t.id !== toast.id))
|
|
54
|
-
delete timers.current[toast.id]
|
|
55
|
-
}, toast.remaining)
|
|
56
|
-
setToasts(prev => prev.map(t => t.id === toast.id ? { ...t, created: Date.now() } : t))
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
}, [isHovered, toasts])
|
|
33
|
+
const now = Date.now()
|
|
34
|
+
setToasts(prev => prev.map(toast => {
|
|
35
|
+
if (expanded) return { ...toast, pausedAt: toast.pausedAt || now }
|
|
36
|
+
if (!toast.pausedAt) return toast
|
|
37
|
+
return { ...toast, expiresAt: toast.expiresAt + (now - toast.pausedAt), pausedAt: undefined }
|
|
38
|
+
}))
|
|
39
|
+
}, [expanded])
|
|
40
|
+
|
|
41
|
+
function removeToast(id: number) {
|
|
42
|
+
setToasts(prev => prev.map(toast => toast.id === id ? { ...toast, exiting: true } : toast))
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
setToasts(prev => prev.filter(toast => toast.id !== id))
|
|
45
|
+
}, 300)
|
|
46
|
+
}
|
|
61
47
|
|
|
62
|
-
// Track main toast position for stacking
|
|
63
48
|
useEffect(() => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
49
|
+
const timer = setInterval(() => {
|
|
50
|
+
if (expanded) return
|
|
51
|
+
const now = Date.now()
|
|
52
|
+
setToasts(prev => {
|
|
53
|
+
const toastsToExit = prev.filter(toast => !toast.exiting && !toast.pausedAt && toast.expiresAt <= now)
|
|
54
|
+
if (toastsToExit.length === 0) return prev
|
|
55
|
+
|
|
56
|
+
toastsToExit.forEach(toast => {
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
setToasts(current => current.filter(item => item.id !== toast.id))
|
|
59
|
+
}, 300)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return prev.map(toast => toastsToExit.find(exitToast => exitToast.id === toast.id) ? { ...toast, exiting: true } : toast)
|
|
69
63
|
})
|
|
70
|
-
}
|
|
71
|
-
|
|
64
|
+
}, 100)
|
|
65
|
+
return () => clearInterval(timer)
|
|
66
|
+
}, [expanded])
|
|
67
|
+
|
|
68
|
+
function onHeight(id: number, height: number) {
|
|
69
|
+
setHeights(prev => {
|
|
70
|
+
if (prev[id] === height) return prev
|
|
71
|
+
return { ...prev, [id]: height }
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const visibleToasts = toasts.slice(0, expanded ? 10 : 3)
|
|
76
|
+
const frontHeight = heights[visibleToasts[0]?.id] || 60
|
|
77
|
+
|
|
78
|
+
const offsets = useMemo(() => {
|
|
79
|
+
let currentOffset = 0
|
|
80
|
+
return visibleToasts.map(toast => {
|
|
81
|
+
const height = heights[toast.id] || 60
|
|
82
|
+
const offset = currentOffset
|
|
83
|
+
currentOffset += height + 16
|
|
84
|
+
return offset
|
|
85
|
+
})
|
|
86
|
+
}, [visibleToasts, heights])
|
|
87
|
+
|
|
88
|
+
const totalHeight = offsets.length > 0
|
|
89
|
+
? offsets[offsets.length - 1] + (heights[visibleToasts[visibleToasts.length - 1]?.id] || 60)
|
|
90
|
+
: 0
|
|
72
91
|
|
|
73
|
-
const bgClasses = ['bg-login-600', 'bg-login-700', 'bg-login-800']
|
|
74
92
|
return (
|
|
75
|
-
<
|
|
76
|
-
className={`fixed bottom-4 right-4 z-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
<ul
|
|
94
|
+
className={`fixed bottom-4 right-4 z-9999 w-full max-w-sm flex flex-col items-end transition-all duration-300 ease-out
|
|
95
|
+
${expanded ? 'pointer-events-auto' : 'pointer-events-none'}`}
|
|
96
|
+
style={{ height: expanded ? totalHeight + 'px' : 'auto' }}
|
|
97
|
+
onMouseEnter={() => setExpanded(true)}
|
|
98
|
+
onMouseLeave={() => setExpanded(false)}
|
|
79
99
|
>
|
|
80
|
-
{
|
|
81
|
-
<
|
|
82
|
-
key={
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
position: 'fixed',
|
|
92
|
-
top: mainToastPosition ? `${mainToastPosition.top - idx * 8}px` : 'auto',
|
|
93
|
-
zIndex: 100 - idx,
|
|
94
|
-
transform: `scale(${1 - idx * 0.05})`,
|
|
95
|
-
}}
|
|
96
|
-
>
|
|
97
|
-
<span className='shrink-0 w-10 h-10 flex items-center justify-center'>
|
|
98
|
-
<ToastIcon type={toast.type} />
|
|
99
|
-
</span>
|
|
100
|
-
<div className='pr-1 pb-1'>
|
|
101
|
-
<span className='font-bold'>{toast.title}</span>
|
|
102
|
-
{(idx === 0 || isHovered) &&
|
|
103
|
-
<span className='text-sm line-clamp-3'>{toast.description}</span>
|
|
104
|
-
}
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
100
|
+
{visibleToasts.map((toast, index) => (
|
|
101
|
+
<ToastItem
|
|
102
|
+
key={toast.id}
|
|
103
|
+
toast={toast}
|
|
104
|
+
index={index}
|
|
105
|
+
expanded={expanded}
|
|
106
|
+
onRemove={() => removeToast(toast.id)}
|
|
107
|
+
onHeight={onHeight}
|
|
108
|
+
offset={offsets[index]}
|
|
109
|
+
frontHeight={frontHeight}
|
|
110
|
+
/>
|
|
107
111
|
))}
|
|
108
|
-
</
|
|
112
|
+
</ul>
|
|
109
113
|
)
|
|
110
114
|
}
|
|
111
|
-
|
|
112
|
-
function ToastIcon({ type }: { type?: ToastType }) {
|
|
113
|
-
switch (type) {
|
|
114
|
-
case 'success':
|
|
115
|
-
return <CircleCheck className='text-green-300/70' />
|
|
116
|
-
case 'warning':
|
|
117
|
-
return <CircleAlert className='text-yellow-300/70' />
|
|
118
|
-
case 'error':
|
|
119
|
-
return <CircleX className='text-red-300/70' />
|
|
120
|
-
case 'info':
|
|
121
|
-
return <Info className='text-blue-300/70' />
|
|
122
|
-
}
|
|
123
|
-
}
|
|
@@ -9,28 +9,32 @@ declare module 'uibee/components' {
|
|
|
9
9
|
handleSubmit?: (formData: FormData) => void
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export type ToastType = '
|
|
13
|
-
export interface ToastProps {
|
|
14
|
-
id: number
|
|
15
|
-
type: ToastType
|
|
16
|
-
title: string
|
|
17
|
-
description?: string
|
|
18
|
-
}
|
|
12
|
+
export type ToastType = 'info' | 'success' | 'warning' | 'error'
|
|
19
13
|
|
|
20
|
-
|
|
14
|
+
interface Toast {
|
|
15
|
+
id: number
|
|
16
|
+
message: string
|
|
21
17
|
type: ToastType
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
expiresAt: number
|
|
19
|
+
pausedAt?: number
|
|
20
|
+
exiting?: boolean
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
interface ToastProps {
|
|
24
|
+
toast: Toast,
|
|
25
|
+
index: number,
|
|
26
|
+
expanded: boolean,
|
|
27
|
+
onRemove: () => void,
|
|
28
|
+
onHeight: (id: number, height: number) => void,
|
|
29
|
+
offset: number,
|
|
30
|
+
frontHeight: number
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export type Language = 'no' | 'en'
|
|
31
34
|
|
|
32
35
|
export default function LoginPage(props: LoginPageProps): JSX.Element;
|
|
33
|
-
export default function Toaster(
|
|
36
|
+
export default function Toaster(): JSX.Element;
|
|
37
|
+
export default function toast(message: string, type: ToastType, duration?: number): void;
|
|
34
38
|
export default function LanguageToggle(props: { lang: Language }): JSX.Element;
|
|
35
39
|
export default function ThemeSwitch(props: { className?: string }): JSX.Element;
|
|
36
40
|
}
|