@scripso-homepad/ui 0.4.9 → 0.4.11
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/index.cjs +245 -127
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +247 -129
- package/dist/index.js.map +1 -1
- package/dist/web/index.cjs +451 -48
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.css +161 -0
- package/dist/web/index.css.map +1 -1
- package/dist/web/index.d.cts +50 -2
- package/dist/web/index.d.ts +50 -2
- package/dist/web/index.js +444 -49
- package/dist/web/index.js.map +1 -1
- package/package.json +10 -3
- package/src/components/Calendar.tsx +10 -2
- package/src/components/DatePicker.tsx +40 -6
- package/src/components/Select.tsx +133 -38
- package/src/theme/select.ts +1 -1
- package/src/utils/calendarDays.ts +15 -6
- package/src/web/components/Badge.tsx +1 -1
- package/src/web/components/ConfirmModal.tsx +2 -2
- package/src/web/components/Drawer.stories.tsx +234 -0
- package/src/web/components/Drawer.tsx +169 -0
- package/src/web/components/HistoryTimeline.tsx +46 -0
- package/src/web/components/Modal.stories.tsx +1 -1
- package/src/web/components/Modal.tsx +30 -13
- package/src/web/components/Pagination.tsx +2 -2
- package/src/web/components/TableActionButton.tsx +4 -4
- package/src/web/components/TableActionsMenu.tsx +1 -1
- package/src/web/components/TableIconText.tsx +1 -1
- package/src/web/components/Toaster.stories.tsx +106 -0
- package/src/web/components/Toaster.tsx +92 -0
- package/src/web/components/ToasterProvider.tsx +6 -0
- package/src/web/components/drawer-scroll.css +41 -0
- package/src/web/components/sonner-toast.css +141 -0
- package/src/web/components/table/constants.ts +2 -2
- package/src/web/icons/TimelineCheckIcon.tsx +24 -0
- package/src/web/icons/ToastIcons.tsx +119 -0
- package/src/web/index.ts +20 -0
- package/src/web/layout/AppHeader.tsx +1 -1
- package/src/web/layout/BuildingSelect.tsx +2 -2
- package/src/web/layout/DashboardLayout.stories.tsx +1 -1
- package/src/web/layout/SidebarNavItem.tsx +1 -1
- package/src/web/layout/SidebarUserCard.tsx +3 -3
- package/src/web/mutation-toast.ts +33 -0
- package/src/web/styles/typography.css +99 -0
- package/src/web/toast.ts +1 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { X } from 'lucide-react';
|
|
2
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../utils/cn';
|
|
5
|
+
|
|
6
|
+
import './drawer-scroll.css';
|
|
7
|
+
|
|
8
|
+
const DRAWER_TRANSITION_MS = 300;
|
|
9
|
+
|
|
10
|
+
export type DrawerProps = {
|
|
11
|
+
open: boolean;
|
|
12
|
+
title: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
footer?: ReactNode;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
closeAriaLabel?: string;
|
|
18
|
+
widthClassName?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
headerClassName?: string;
|
|
21
|
+
contentClassName?: string;
|
|
22
|
+
footerClassName?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function Drawer({
|
|
26
|
+
open,
|
|
27
|
+
title,
|
|
28
|
+
subtitle,
|
|
29
|
+
children,
|
|
30
|
+
footer,
|
|
31
|
+
onClose,
|
|
32
|
+
closeAriaLabel = 'Close',
|
|
33
|
+
widthClassName = 'w-full md:w-[464px]',
|
|
34
|
+
className,
|
|
35
|
+
headerClassName,
|
|
36
|
+
contentClassName,
|
|
37
|
+
footerClassName,
|
|
38
|
+
}: DrawerProps) {
|
|
39
|
+
const [isPresent, setIsPresent] = useState(open);
|
|
40
|
+
const [isAnimatedIn, setIsAnimatedIn] = useState(false);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (open) {
|
|
44
|
+
setIsPresent(true);
|
|
45
|
+
|
|
46
|
+
const frame = window.requestAnimationFrame(() => {
|
|
47
|
+
window.requestAnimationFrame(() => {
|
|
48
|
+
setIsAnimatedIn(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
window.cancelAnimationFrame(frame);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setIsAnimatedIn(false);
|
|
58
|
+
|
|
59
|
+
const timeout = window.setTimeout(() => {
|
|
60
|
+
setIsPresent(false);
|
|
61
|
+
}, DRAWER_TRANSITION_MS);
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
window.clearTimeout(timeout);
|
|
65
|
+
};
|
|
66
|
+
}, [open]);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!isPresent) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const previousOverflow = document.body.style.overflow;
|
|
74
|
+
document.body.style.overflow = 'hidden';
|
|
75
|
+
|
|
76
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
77
|
+
if (event.key === 'Escape') {
|
|
78
|
+
onClose();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
document.body.style.overflow = previousOverflow;
|
|
86
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
87
|
+
};
|
|
88
|
+
}, [isPresent, onClose]);
|
|
89
|
+
|
|
90
|
+
if (!isPresent) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
className="fixed inset-0 z-50 flex justify-end p-2 max-md:p-0"
|
|
97
|
+
role="presentation"
|
|
98
|
+
>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
aria-label={closeAriaLabel}
|
|
102
|
+
className={cn(
|
|
103
|
+
'absolute inset-0 bg-storm-gray-850/40 backdrop-blur-[12px] transition-opacity duration-300 ease-in-out',
|
|
104
|
+
isAnimatedIn ? 'opacity-100' : 'opacity-0',
|
|
105
|
+
)}
|
|
106
|
+
onClick={onClose}
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
<aside
|
|
110
|
+
role="dialog"
|
|
111
|
+
aria-modal="true"
|
|
112
|
+
aria-labelledby="homepad-drawer-title"
|
|
113
|
+
{...(subtitle ? { 'aria-describedby': 'homepad-drawer-subtitle' } : {})}
|
|
114
|
+
className={cn(
|
|
115
|
+
'relative z-10 flex h-[calc(100dvh-16px)] min-h-0 flex-col overflow-hidden rounded-[16px] bg-white shadow-2xl transition-transform duration-300 ease-in-out will-change-transform max-md:h-dvh max-md:rounded-none',
|
|
116
|
+
isAnimatedIn ? 'translate-x-0' : 'translate-x-full',
|
|
117
|
+
widthClassName,
|
|
118
|
+
className,
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
<header
|
|
122
|
+
className={cn(
|
|
123
|
+
'flex h-[87px] shrink-0 items-start justify-between border-b border-storm-gray-50 pt-6 pr-6 pb-4 pl-6',
|
|
124
|
+
headerClassName,
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
<div className="min-w-0 flex flex-col gap-1">
|
|
128
|
+
<h2
|
|
129
|
+
id="homepad-drawer-title"
|
|
130
|
+
className="typography-title-sm tracking-normal text-black"
|
|
131
|
+
>
|
|
132
|
+
{title}
|
|
133
|
+
</h2>
|
|
134
|
+
{subtitle ? (
|
|
135
|
+
<p
|
|
136
|
+
id="homepad-drawer-subtitle"
|
|
137
|
+
className="typography-body tracking-normal text-storm-gray-400"
|
|
138
|
+
>
|
|
139
|
+
{subtitle}
|
|
140
|
+
</p>
|
|
141
|
+
) : null}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
aria-label={closeAriaLabel}
|
|
147
|
+
className="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-lg text-storm-gray-400 transition-colors hover:bg-storm-gray-50 hover:text-storm-gray-600"
|
|
148
|
+
onClick={onClose}
|
|
149
|
+
>
|
|
150
|
+
<X className="size-5" strokeWidth={1.75} />
|
|
151
|
+
</button>
|
|
152
|
+
</header>
|
|
153
|
+
|
|
154
|
+
<div
|
|
155
|
+
className={cn(
|
|
156
|
+
'drawer-scroll-area min-h-0 flex-1 overflow-y-auto px-6 pt-6 pb-0',
|
|
157
|
+
contentClassName,
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
160
|
+
{children}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{footer ? (
|
|
164
|
+
<footer className={cn('shrink-0 px-6 py-5', footerClassName)}>{footer}</footer>
|
|
165
|
+
) : null}
|
|
166
|
+
</aside>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { TimelineCheckIcon } from '../icons/TimelineCheckIcon';
|
|
2
|
+
|
|
3
|
+
export type HistoryTimelineItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type HistoryTimelineProps = {
|
|
10
|
+
events: HistoryTimelineItem[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function HistoryTimeline({ events }: HistoryTimelineProps) {
|
|
14
|
+
return (
|
|
15
|
+
<ol className="m-0 flex list-none flex-col gap-1 p-0">
|
|
16
|
+
{events.map((event, index) => {
|
|
17
|
+
const isLast = index === events.length - 1;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<li key={event.id} className="flex items-start gap-3">
|
|
21
|
+
<div className="flex w-8 shrink-0 flex-col items-center">
|
|
22
|
+
<div className="flex size-8 shrink-0 items-center justify-center rounded-[28px] border border-storm-gray-50 bg-white">
|
|
23
|
+
<TimelineCheckIcon className="size-4 shrink-0" />
|
|
24
|
+
</div>
|
|
25
|
+
{!isLast ? (
|
|
26
|
+
<div
|
|
27
|
+
aria-hidden
|
|
28
|
+
className="mt-1 h-[31px] w-px shrink-0 border-l border-dashed border-storm-gray-100"
|
|
29
|
+
/>
|
|
30
|
+
) : null}
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div className="flex min-w-0 flex-1 flex-col gap-2 pt-1.5">
|
|
34
|
+
<p className="typography-14 font-medium tracking-normal text-slate-blue">
|
|
35
|
+
{event.title}
|
|
36
|
+
</p>
|
|
37
|
+
<p className="typography-caption tracking-normal text-storm-gray-300">
|
|
38
|
+
{event.timestamp}
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
</li>
|
|
42
|
+
);
|
|
43
|
+
})}
|
|
44
|
+
</ol>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -106,7 +106,7 @@ function FooterAlignmentsStory() {
|
|
|
106
106
|
<button
|
|
107
107
|
key={value}
|
|
108
108
|
type="button"
|
|
109
|
-
className="rounded-lg border border-storm-gray-50 px-3 py-1
|
|
109
|
+
className="rounded-lg border border-storm-gray-50 px-3 py-1 typography-14"
|
|
110
110
|
onClick={() => setAlign(value)}
|
|
111
111
|
>
|
|
112
112
|
{value}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, type ReactNode } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
|
|
3
4
|
import { Button } from '../../components/Button';
|
|
4
5
|
import { cn } from '../utils/cn';
|
|
@@ -22,6 +23,7 @@ export type ModalProps = {
|
|
|
22
23
|
cancelDisabled?: boolean;
|
|
23
24
|
confirmDisabled?: boolean;
|
|
24
25
|
closeOnBackdrop?: boolean;
|
|
26
|
+
closeOnEscape?: boolean;
|
|
25
27
|
containerClassName?: string;
|
|
26
28
|
contentClassName?: string;
|
|
27
29
|
buttonClassName?: string;
|
|
@@ -49,6 +51,7 @@ export function Modal({
|
|
|
49
51
|
cancelDisabled = false,
|
|
50
52
|
confirmDisabled = false,
|
|
51
53
|
closeOnBackdrop = true,
|
|
54
|
+
closeOnEscape = true,
|
|
52
55
|
containerClassName,
|
|
53
56
|
contentClassName,
|
|
54
57
|
buttonClassName,
|
|
@@ -58,8 +61,11 @@ export function Modal({
|
|
|
58
61
|
return;
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
const previousOverflow = document.body.style.overflow;
|
|
65
|
+
document.body.style.overflow = 'hidden';
|
|
66
|
+
|
|
61
67
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
62
|
-
if (event.key === 'Escape') {
|
|
68
|
+
if (event.key === 'Escape' && closeOnEscape) {
|
|
63
69
|
onCancel();
|
|
64
70
|
}
|
|
65
71
|
};
|
|
@@ -67,9 +73,10 @@ export function Modal({
|
|
|
67
73
|
window.addEventListener('keydown', handleKeyDown);
|
|
68
74
|
|
|
69
75
|
return () => {
|
|
76
|
+
document.body.style.overflow = previousOverflow;
|
|
70
77
|
window.removeEventListener('keydown', handleKeyDown);
|
|
71
78
|
};
|
|
72
|
-
}, [open, onCancel]);
|
|
79
|
+
}, [open, onCancel, closeOnEscape]);
|
|
73
80
|
|
|
74
81
|
if (!open) {
|
|
75
82
|
return null;
|
|
@@ -77,14 +84,22 @@ export function Modal({
|
|
|
77
84
|
|
|
78
85
|
const sharedButtonClassName = cn(
|
|
79
86
|
'btn h-13 justify-center!',
|
|
80
|
-
footerLayout === 'split' && 'min-w-0 flex-1',
|
|
81
|
-
buttonClassName,
|
|
87
|
+
footerLayout === 'split' && 'min-w-0 flex-1 basis-0',
|
|
88
|
+
footerLayout !== 'split' && buttonClassName,
|
|
82
89
|
);
|
|
83
90
|
|
|
84
|
-
|
|
91
|
+
const modal = (
|
|
85
92
|
<div
|
|
86
|
-
className="fixed inset-0 z-
|
|
87
|
-
onClick={
|
|
93
|
+
className="fixed inset-0 z-[100] flex items-center justify-center p-4 backdrop-blur-[12px] bg-storm-gray-850/40"
|
|
94
|
+
onClick={
|
|
95
|
+
closeOnBackdrop
|
|
96
|
+
? (event) => {
|
|
97
|
+
if (event.target === event.currentTarget) {
|
|
98
|
+
onCancel();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
: undefined
|
|
102
|
+
}
|
|
88
103
|
role="presentation"
|
|
89
104
|
>
|
|
90
105
|
<div
|
|
@@ -93,14 +108,14 @@ export function Modal({
|
|
|
93
108
|
aria-labelledby="homepad-modal-title"
|
|
94
109
|
{...(subtitle ? { 'aria-describedby': 'homepad-modal-subtitle' } : {})}
|
|
95
110
|
className={cn(
|
|
96
|
-
'flex w-full max-w-2xl flex-col gap-6 rounded-2xl bg-white p-4 opacity-100 md:p-6',
|
|
111
|
+
'flex max-h-[min(90dvh,calc(100dvh-2rem))] w-full max-w-2xl flex-col gap-6 rounded-2xl bg-white p-4 opacity-100 md:p-6',
|
|
97
112
|
containerClassName,
|
|
98
113
|
)}
|
|
99
114
|
onClick={(event) => {
|
|
100
115
|
event.stopPropagation();
|
|
101
116
|
}}
|
|
102
117
|
>
|
|
103
|
-
<header className={cn('flex', icon ? 'items-center gap-2.5' : undefined)}>
|
|
118
|
+
<header className={cn('flex shrink-0', icon ? 'items-center gap-2.5' : undefined)}>
|
|
104
119
|
{icon ? (
|
|
105
120
|
<div
|
|
106
121
|
className={cn(
|
|
@@ -116,14 +131,14 @@ export function Modal({
|
|
|
116
131
|
<div className="flex min-w-0 flex-col gap-2">
|
|
117
132
|
<h2
|
|
118
133
|
id="homepad-modal-title"
|
|
119
|
-
className="
|
|
134
|
+
className="typography-title-sm tracking-normal text-black"
|
|
120
135
|
>
|
|
121
136
|
{title}
|
|
122
137
|
</h2>
|
|
123
138
|
{subtitle ? (
|
|
124
139
|
<p
|
|
125
140
|
id="homepad-modal-subtitle"
|
|
126
|
-
className="
|
|
141
|
+
className="typography-caption tracking-normal text-storm-gray-400"
|
|
127
142
|
>
|
|
128
143
|
{subtitle}
|
|
129
144
|
</p>
|
|
@@ -131,11 +146,11 @@ export function Modal({
|
|
|
131
146
|
</div>
|
|
132
147
|
</header>
|
|
133
148
|
|
|
134
|
-
<div className={cn('min-w-0', contentClassName)}>{children}</div>
|
|
149
|
+
<div className={cn('min-h-0 min-w-0 flex-1 overflow-y-auto', contentClassName)}>{children}</div>
|
|
135
150
|
|
|
136
151
|
<footer
|
|
137
152
|
className={cn(
|
|
138
|
-
'flex gap-4',
|
|
153
|
+
'flex shrink-0 gap-4',
|
|
139
154
|
footerLayout === 'split' ? 'w-full' : footerAlignClasses[footerAlign],
|
|
140
155
|
)}
|
|
141
156
|
>
|
|
@@ -159,4 +174,6 @@ export function Modal({
|
|
|
159
174
|
</div>
|
|
160
175
|
</div>
|
|
161
176
|
);
|
|
177
|
+
|
|
178
|
+
return typeof document !== 'undefined' ? createPortal(modal, document.body) : modal;
|
|
162
179
|
}
|
|
@@ -23,7 +23,7 @@ const defaultLabels: PaginationLabels = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const paginationControlClassName =
|
|
26
|
-
'inline-flex size-7 items-center justify-center rounded-lg border border-storm-gray-50 p-1
|
|
26
|
+
'inline-flex size-7 items-center justify-center rounded-lg border border-storm-gray-50 p-1 typography-12 font-medium text-storm-gray-500 transition-colors';
|
|
27
27
|
|
|
28
28
|
const paginationHoverClassName = 'cursor-pointer hover:bg-storm-gray-0';
|
|
29
29
|
|
|
@@ -78,7 +78,7 @@ export function Pagination({
|
|
|
78
78
|
className,
|
|
79
79
|
)}
|
|
80
80
|
>
|
|
81
|
-
<p className="
|
|
81
|
+
<p className="typography-12 font-medium text-storm-gray-200">
|
|
82
82
|
{mergedLabels.showing({ start, end, total })}
|
|
83
83
|
</p>
|
|
84
84
|
|
|
@@ -16,12 +16,12 @@ export type TableActionButtonProps = {
|
|
|
16
16
|
'onClick' | 'title' | 'aria-expanded' | 'aria-haspopup' | 'aria-controls'
|
|
17
17
|
>;
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
'inline-flex cursor-pointer items-center gap-1 rounded-lg border border-storm-gray-50 bg-white px-3 py-2
|
|
19
|
+
const inlineActionClassName =
|
|
20
|
+
'inline-flex cursor-pointer items-center gap-1 rounded-lg border border-storm-gray-50 bg-white px-3 py-2 typography-12 font-medium text-storm-gray-900 transition-colors hover:bg-storm-gray-0';
|
|
21
21
|
|
|
22
22
|
const variantClasses: Record<TableActionButtonVariant, string> = {
|
|
23
|
-
approve:
|
|
24
|
-
reject:
|
|
23
|
+
approve: inlineActionClassName,
|
|
24
|
+
reject: inlineActionClassName,
|
|
25
25
|
menu: 'inline-flex size-7 cursor-pointer items-center justify-center rounded-lg text-storm-gray-500 transition-colors hover:bg-storm-gray-0 hover:text-storm-gray-900',
|
|
26
26
|
};
|
|
27
27
|
|
|
@@ -172,7 +172,7 @@ export function TableActionsMenu({
|
|
|
172
172
|
item.onSelect();
|
|
173
173
|
}}
|
|
174
174
|
className={cn(
|
|
175
|
-
'flex h-[51px] w-full cursor-pointer items-center gap-2.5 px-3 text-left
|
|
175
|
+
'flex h-[51px] w-full cursor-pointer items-center gap-2.5 px-3 text-left typography-14 font-medium text-storm-gray-900 transition-colors hover:bg-storm-gray-0',
|
|
176
176
|
index > 0 && 'border-t border-storm-gray-50',
|
|
177
177
|
item.disabled && 'cursor-not-allowed opacity-50',
|
|
178
178
|
)}
|
|
@@ -12,7 +12,7 @@ export function TableIconText({ icon, children, className }: TableIconTextProps)
|
|
|
12
12
|
return (
|
|
13
13
|
<span
|
|
14
14
|
className={cn(
|
|
15
|
-
'inline-flex items-center gap-2
|
|
15
|
+
'inline-flex items-center gap-2 typography-14 font-medium tracking-normal text-storm-gray-900',
|
|
16
16
|
className,
|
|
17
17
|
)}
|
|
18
18
|
>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
import { WebLayoutCanvas } from '../layout/story-helpers';
|
|
5
|
+
import { toast } from '../toast';
|
|
6
|
+
|
|
7
|
+
import { Toaster } from './Toaster';
|
|
8
|
+
|
|
9
|
+
const toastCopy = {
|
|
10
|
+
title: 'Title',
|
|
11
|
+
description: 'Description',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const meta = {
|
|
15
|
+
title: 'Web Components/Toaster',
|
|
16
|
+
component: Toaster,
|
|
17
|
+
parameters: {
|
|
18
|
+
layout: 'fullscreen',
|
|
19
|
+
webLayout: true,
|
|
20
|
+
},
|
|
21
|
+
} satisfies Meta<typeof Toaster>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
function ToastTriggerPanel() {
|
|
27
|
+
return (
|
|
28
|
+
<WebLayoutCanvas className="min-h-screen bg-storm-gray-100 p-6">
|
|
29
|
+
<div className="mx-auto flex w-full max-w-md flex-col gap-3 rounded-2xl border border-storm-gray-50 bg-white p-6 shadow-sm">
|
|
30
|
+
<h2 className="typography-title-sm text-slate-blue">Toast preview</h2>
|
|
31
|
+
<p className="typography-body text-storm-gray-400">
|
|
32
|
+
Click a button to show a toast in the bottom-right corner.
|
|
33
|
+
</p>
|
|
34
|
+
<div className="flex flex-col gap-2">
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
className="rounded-xl border border-storm-gray-50 px-4 py-2 text-left typography-14 font-medium text-slate-blue transition-colors hover:bg-storm-gray-0"
|
|
38
|
+
onClick={() => toast.success(toastCopy.title, { description: toastCopy.description })}
|
|
39
|
+
>
|
|
40
|
+
Success
|
|
41
|
+
</button>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
className="rounded-xl border border-storm-gray-50 px-4 py-2 text-left typography-14 font-medium text-slate-blue transition-colors hover:bg-storm-gray-0"
|
|
45
|
+
onClick={() => toast.error(toastCopy.title, { description: toastCopy.description })}
|
|
46
|
+
>
|
|
47
|
+
Error
|
|
48
|
+
</button>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
className="rounded-xl border border-storm-gray-50 px-4 py-2 text-left typography-14 font-medium text-slate-blue transition-colors hover:bg-storm-gray-0"
|
|
52
|
+
onClick={() => toast.warning(toastCopy.title, { description: toastCopy.description })}
|
|
53
|
+
>
|
|
54
|
+
Warning
|
|
55
|
+
</button>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
className="rounded-xl border border-storm-gray-50 px-4 py-2 text-left typography-14 font-medium text-slate-blue transition-colors hover:bg-storm-gray-0"
|
|
59
|
+
onClick={() => toast.info(toastCopy.title, { description: toastCopy.description })}
|
|
60
|
+
>
|
|
61
|
+
Info
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<Toaster />
|
|
66
|
+
</WebLayoutCanvas>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function AllTypesStory() {
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
toast.success(toastCopy.title, { description: toastCopy.description });
|
|
73
|
+
toast.error(toastCopy.title, { description: toastCopy.description });
|
|
74
|
+
toast.warning(toastCopy.title, { description: toastCopy.description });
|
|
75
|
+
toast.info(toastCopy.title, { description: toastCopy.description });
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<WebLayoutCanvas className="min-h-screen bg-storm-gray-100">
|
|
80
|
+
<Toaster />
|
|
81
|
+
</WebLayoutCanvas>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const Interactive: Story = {
|
|
86
|
+
render: () => <ToastTriggerPanel />,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const AllTypes: Story = {
|
|
90
|
+
render: () => <AllTypesStory />,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const TitleOnly: Story = {
|
|
94
|
+
render: () => (
|
|
95
|
+
<WebLayoutCanvas className="min-h-screen bg-storm-gray-100 p-6">
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
className="rounded-xl border border-storm-gray-50 bg-white px-4 py-2 typography-14 font-medium text-slate-blue"
|
|
99
|
+
onClick={() => toast.success('Request approved.')}
|
|
100
|
+
>
|
|
101
|
+
Show title-only toast
|
|
102
|
+
</button>
|
|
103
|
+
<Toaster />
|
|
104
|
+
</WebLayoutCanvas>
|
|
105
|
+
),
|
|
106
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Toaster as SonnerToaster, type ToasterProps } from 'sonner';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ToastCloseIcon,
|
|
6
|
+
ToastErrorIcon,
|
|
7
|
+
ToastInfoIcon,
|
|
8
|
+
ToastSuccessIcon,
|
|
9
|
+
ToastWarningIcon,
|
|
10
|
+
} from '../icons/ToastIcons';
|
|
11
|
+
|
|
12
|
+
import 'sonner/dist/styles.css';
|
|
13
|
+
import './sonner-toast.css';
|
|
14
|
+
|
|
15
|
+
function ToastIconBadge({
|
|
16
|
+
tone,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
tone: 'success' | 'error' | 'warning' | 'info' | 'default';
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<div className={`homepad-toast__icon-badge homepad-toast__icon-badge--${tone}`}>
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const homepadToastIcons = {
|
|
30
|
+
success: (
|
|
31
|
+
<ToastIconBadge tone="success">
|
|
32
|
+
<ToastSuccessIcon />
|
|
33
|
+
</ToastIconBadge>
|
|
34
|
+
),
|
|
35
|
+
error: (
|
|
36
|
+
<ToastIconBadge tone="error">
|
|
37
|
+
<ToastErrorIcon />
|
|
38
|
+
</ToastIconBadge>
|
|
39
|
+
),
|
|
40
|
+
warning: (
|
|
41
|
+
<ToastIconBadge tone="warning">
|
|
42
|
+
<ToastWarningIcon />
|
|
43
|
+
</ToastIconBadge>
|
|
44
|
+
),
|
|
45
|
+
info: (
|
|
46
|
+
<ToastIconBadge tone="info">
|
|
47
|
+
<ToastInfoIcon />
|
|
48
|
+
</ToastIconBadge>
|
|
49
|
+
),
|
|
50
|
+
close: <ToastCloseIcon />,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type HomepadToasterProps = Omit<ToasterProps, 'richColors'>;
|
|
54
|
+
|
|
55
|
+
export function Toaster({
|
|
56
|
+
className,
|
|
57
|
+
position = 'bottom-right',
|
|
58
|
+
theme = 'light',
|
|
59
|
+
closeButton = true,
|
|
60
|
+
gap = 12,
|
|
61
|
+
offset = 20,
|
|
62
|
+
toastOptions,
|
|
63
|
+
icons,
|
|
64
|
+
...props
|
|
65
|
+
}: HomepadToasterProps) {
|
|
66
|
+
return (
|
|
67
|
+
<SonnerToaster
|
|
68
|
+
className={['homepad-toaster', className].filter(Boolean).join(' ')}
|
|
69
|
+
theme={theme}
|
|
70
|
+
position={position}
|
|
71
|
+
closeButton={closeButton}
|
|
72
|
+
gap={gap}
|
|
73
|
+
offset={offset}
|
|
74
|
+
mobileOffset={8}
|
|
75
|
+
icons={{ ...homepadToastIcons, ...icons }}
|
|
76
|
+
toastOptions={{
|
|
77
|
+
duration: 4000,
|
|
78
|
+
classNames: {
|
|
79
|
+
toast: 'homepad-toast',
|
|
80
|
+
title: 'homepad-toast__title',
|
|
81
|
+
description: 'homepad-toast__description',
|
|
82
|
+
content: 'homepad-toast__content',
|
|
83
|
+
icon: 'homepad-toast__icon',
|
|
84
|
+
closeButton: 'homepad-toast__close',
|
|
85
|
+
...toastOptions?.classNames,
|
|
86
|
+
},
|
|
87
|
+
...toastOptions,
|
|
88
|
+
}}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
.drawer-scroll-area {
|
|
2
|
+
scrollbar-color: var(--color-storm-gray-200, #d5d9dd) transparent;
|
|
3
|
+
scrollbar-width: thin;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.drawer-scroll-area::-webkit-scrollbar {
|
|
7
|
+
width: 8px;
|
|
8
|
+
-webkit-appearance: none;
|
|
9
|
+
appearance: none;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.drawer-scroll-area::-webkit-scrollbar-button,
|
|
13
|
+
.drawer-scroll-area::-webkit-scrollbar-button:single-button,
|
|
14
|
+
.drawer-scroll-area::-webkit-scrollbar-button:double-button,
|
|
15
|
+
.drawer-scroll-area::-webkit-scrollbar-button:vertical:decrement,
|
|
16
|
+
.drawer-scroll-area::-webkit-scrollbar-button:vertical:increment,
|
|
17
|
+
.drawer-scroll-area::-webkit-scrollbar-button:vertical:start:decrement,
|
|
18
|
+
.drawer-scroll-area::-webkit-scrollbar-button:vertical:end:increment,
|
|
19
|
+
.drawer-scroll-area::-webkit-scrollbar-button:start:decrement,
|
|
20
|
+
.drawer-scroll-area::-webkit-scrollbar-button:end:increment,
|
|
21
|
+
.drawer-scroll-area::-webkit-scrollbar-button:decrement,
|
|
22
|
+
.drawer-scroll-area::-webkit-scrollbar-button:increment {
|
|
23
|
+
display: none;
|
|
24
|
+
width: 0;
|
|
25
|
+
height: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.drawer-scroll-area::-webkit-scrollbar-track {
|
|
29
|
+
background: transparent;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.drawer-scroll-area::-webkit-scrollbar-thumb {
|
|
33
|
+
background-color: var(--color-storm-gray-200, #d5d9dd);
|
|
34
|
+
border-radius: 9999px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@media (max-width: 767px) {
|
|
38
|
+
.drawer-scroll-area::-webkit-scrollbar {
|
|
39
|
+
width: 4px;
|
|
40
|
+
}
|
|
41
|
+
}
|