@scripso-homepad/ui 0.4.9 → 0.4.10

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.
@@ -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="font-sans text-lg font-bold text-slate-blue">Toast preview</h2>
31
+ <p className="font-sans text-sm 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 text-sm 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 text-sm 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 text-sm 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 text-sm 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 text-sm 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,6 @@
1
+ import { Toaster } from './Toaster';
2
+
3
+ /** Global toast outlet styled to match the HomePad design system. */
4
+ export function ToasterProvider() {
5
+ return <Toaster />;
6
+ }
@@ -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
+ }
@@ -0,0 +1,141 @@
1
+ .homepad-toaster {
2
+ --width: 409px;
3
+ --gap: 12px;
4
+ --border-radius: 20px;
5
+ font-family: var(--font-sans, 'Noto Sans Armenian', ui-sans-serif, system-ui, sans-serif);
6
+ }
7
+
8
+ .homepad-toaster [data-sonner-toast][data-styled='true'] {
9
+ align-items: center;
10
+ gap: 16px;
11
+ width: 409px;
12
+ min-height: 72px;
13
+ height: auto;
14
+ padding: 12px 44px 12px 12px;
15
+ background: #fff;
16
+ border: 1px solid var(--color-storm-gray-50, #eceef0);
17
+ border-radius: 20px;
18
+ box-shadow: 0 0 20px 0 #0000000d;
19
+ color: var(--color-slate-blue, #182e3c);
20
+ overflow: visible;
21
+ }
22
+
23
+ .homepad-toaster [data-sonner-toast][data-styled='true'][data-front='true'] {
24
+ height: auto !important;
25
+ }
26
+
27
+ .homepad-toaster
28
+ [data-sonner-toast][data-styled='true'][data-mounted='true'][data-expanded='true'] {
29
+ height: auto !important;
30
+ }
31
+
32
+ .homepad-toaster [data-sonner-toast][data-styled='true'] [data-icon] {
33
+ width: auto;
34
+ height: auto;
35
+ margin: 0;
36
+ }
37
+
38
+ .homepad-toaster [data-sonner-toast][data-styled='true'] [data-content] {
39
+ gap: 2px;
40
+ min-width: 0;
41
+ flex: 1;
42
+ align-self: center;
43
+ overflow: visible;
44
+ }
45
+
46
+ .homepad-toaster .homepad-toast__title {
47
+ font-family: var(--font-sans, 'Noto Sans Armenian', ui-sans-serif, system-ui, sans-serif);
48
+ font-size: 14px;
49
+ font-weight: 700;
50
+ line-height: 22px;
51
+ letter-spacing: 0;
52
+ color: var(--color-slate-blue, #182e3c);
53
+ }
54
+
55
+ .homepad-toaster .homepad-toast__description {
56
+ font-family: var(--font-sans, 'Noto Sans Armenian', ui-sans-serif, system-ui, sans-serif);
57
+ font-size: 12px;
58
+ font-weight: 400;
59
+ line-height: 1.4;
60
+ letter-spacing: 0;
61
+ color: var(--color-slate-blue, #182e3c);
62
+ }
63
+
64
+ .homepad-toaster .homepad-toast__icon-badge {
65
+ display: flex;
66
+ width: 48px;
67
+ height: 48px;
68
+ flex-shrink: 0;
69
+ align-items: center;
70
+ justify-content: center;
71
+ padding: 12px;
72
+ border-radius: 12px;
73
+ box-sizing: border-box;
74
+ }
75
+
76
+ .homepad-toaster .homepad-toast__icon-badge svg {
77
+ width: 24px;
78
+ height: 24px;
79
+ margin: 0;
80
+ flex-shrink: 0;
81
+ }
82
+
83
+ .homepad-toaster .homepad-toast__icon-badge--success {
84
+ background: var(--color-emerald-green-0, #f0fbf5);
85
+ }
86
+
87
+ .homepad-toaster .homepad-toast__icon-badge--error {
88
+ background: var(--color-ruby-red-0, #fff5f6);
89
+ }
90
+
91
+ .homepad-toaster .homepad-toast__icon-badge--warning {
92
+ background: #f2a8001a;
93
+ }
94
+
95
+ .homepad-toaster .homepad-toast__icon-badge--info,
96
+ .homepad-toaster .homepad-toast__icon-badge--default {
97
+ background: #0f52c71a;
98
+ }
99
+
100
+ .homepad-toaster [data-sonner-toast][data-styled='true'] [data-close-button] {
101
+ top: 50%;
102
+ right: 12px;
103
+ left: auto;
104
+ width: 20px;
105
+ height: 20px;
106
+ transform: translateY(-50%);
107
+ border: 0;
108
+ background: transparent;
109
+ box-shadow: none;
110
+ cursor: pointer;
111
+ }
112
+
113
+ .homepad-toaster [data-sonner-toast][data-styled='true']:hover [data-close-button]:hover {
114
+ background: transparent;
115
+ border-color: transparent;
116
+ cursor: pointer;
117
+ }
118
+
119
+ .homepad-toaster [data-sonner-toast][data-styled='true'] [data-close-button] svg {
120
+ width: 20px;
121
+ height: 20px;
122
+ }
123
+
124
+ @media (max-width: 767px) {
125
+ .homepad-toaster {
126
+ --width: calc(100vw - 16px);
127
+ }
128
+
129
+ .homepad-toaster[data-sonner-toaster] {
130
+ right: var(--mobile-offset-right, 8px);
131
+ left: var(--mobile-offset-left, 8px);
132
+ width: calc(100vw - 16px);
133
+ max-width: calc(100vw - 16px);
134
+ }
135
+
136
+ .homepad-toaster [data-sonner-toast][data-styled='true'] {
137
+ right: 0;
138
+ left: 0;
139
+ width: 100%;
140
+ }
141
+ }
@@ -0,0 +1,24 @@
1
+ import type { SVGProps } from 'react';
2
+
3
+ export function TimelineCheckIcon({ className, ...props }: SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg
6
+ width="16"
7
+ height="16"
8
+ viewBox="0 0 16 16"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ className={className}
12
+ aria-hidden
13
+ {...props}
14
+ >
15
+ <path
16
+ d="M13.3346 4L6.0013 11.3333L2.66797 8"
17
+ stroke="#279D54"
18
+ strokeWidth="1.5"
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ />
22
+ </svg>
23
+ );
24
+ }
@@ -0,0 +1,119 @@
1
+ import type { SVGProps } from 'react';
2
+
3
+ function ToastCloseIcon(props: SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg
6
+ width="20"
7
+ height="20"
8
+ viewBox="0 0 20 20"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ aria-hidden
12
+ {...props}
13
+ >
14
+ <path
15
+ d="M15 5L5 15M5 5L15 15"
16
+ stroke="#B5BCC1"
17
+ strokeWidth="2"
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
+ />
21
+ </svg>
22
+ );
23
+ }
24
+
25
+ function ToastSuccessIcon(props: SVGProps<SVGSVGElement>) {
26
+ return (
27
+ <svg
28
+ width="24"
29
+ height="24"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ xmlns="http://www.w3.org/2000/svg"
33
+ aria-hidden
34
+ {...props}
35
+ >
36
+ <path
37
+ d="M20 6L9 17L4 12"
38
+ stroke="#279D54"
39
+ strokeWidth="2"
40
+ strokeLinecap="round"
41
+ strokeLinejoin="round"
42
+ />
43
+ </svg>
44
+ );
45
+ }
46
+
47
+ function ToastErrorIcon(props: SVGProps<SVGSVGElement>) {
48
+ return (
49
+ <svg
50
+ width="24"
51
+ height="24"
52
+ viewBox="0 0 24 24"
53
+ fill="none"
54
+ xmlns="http://www.w3.org/2000/svg"
55
+ aria-hidden
56
+ {...props}
57
+ >
58
+ <path
59
+ d="M18 6L6 18M6 6L18 18"
60
+ stroke="#AE0011"
61
+ strokeWidth="2"
62
+ strokeLinecap="round"
63
+ strokeLinejoin="round"
64
+ />
65
+ </svg>
66
+ );
67
+ }
68
+
69
+ function ToastWarningIcon(props: SVGProps<SVGSVGElement>) {
70
+ return (
71
+ <svg
72
+ width="24"
73
+ height="24"
74
+ viewBox="0 0 24 24"
75
+ fill="none"
76
+ xmlns="http://www.w3.org/2000/svg"
77
+ aria-hidden
78
+ {...props}
79
+ >
80
+ <path
81
+ d="M12.0001 9.00022V13.0002M12.0001 17.0002H12.0101M21.7301 18.0002L13.7301 4.00022C13.5556 3.69243 13.3027 3.43641 12.997 3.25829C12.6913 3.08017 12.3438 2.98633 11.9901 2.98633C11.6363 2.98633 11.2888 3.08017 10.9831 3.25829C10.6774 3.43641 10.4245 3.69243 10.2501 4.00022L2.25005 18.0002C2.07373 18.3056 1.98128 18.6521 1.98206 19.0047C1.98284 19.3573 2.07683 19.7035 2.2545 20.008C2.43217 20.3126 2.6872 20.5648 2.99375 20.7391C3.30029 20.9133 3.64746 21.0034 4.00005 21.0002H20.0001C20.351 20.9999 20.6956 20.9072 20.9993 20.7315C21.3031 20.5558 21.5553 20.3033 21.7306 19.9993C21.9059 19.6954 21.9981 19.3506 21.998 18.9997C21.9979 18.6488 21.9055 18.3041 21.7301 18.0002Z"
82
+ stroke="#F2A800"
83
+ strokeWidth="2"
84
+ strokeLinecap="round"
85
+ strokeLinejoin="round"
86
+ />
87
+ </svg>
88
+ );
89
+ }
90
+
91
+ function ToastInfoIcon(props: SVGProps<SVGSVGElement>) {
92
+ return (
93
+ <svg
94
+ width="24"
95
+ height="24"
96
+ viewBox="0 0 24 24"
97
+ fill="none"
98
+ xmlns="http://www.w3.org/2000/svg"
99
+ aria-hidden
100
+ {...props}
101
+ >
102
+ <path
103
+ d="M12 16V12M12 8H12.01M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
104
+ stroke="#0F52C7"
105
+ strokeWidth="2"
106
+ strokeLinecap="round"
107
+ strokeLinejoin="round"
108
+ />
109
+ </svg>
110
+ );
111
+ }
112
+
113
+ export {
114
+ ToastCloseIcon,
115
+ ToastErrorIcon,
116
+ ToastInfoIcon,
117
+ ToastSuccessIcon,
118
+ ToastWarningIcon,
119
+ };
package/src/web/index.ts CHANGED
@@ -99,6 +99,26 @@ export type { TableIconTextProps } from './components/TableIconText';
99
99
  export { Modal } from './components/Modal';
100
100
  export type { ModalFooterAlign, ModalFooterLayout, ModalProps } from './components/Modal';
101
101
 
102
+ export { Drawer } from './components/Drawer';
103
+ export type { DrawerProps } from './components/Drawer';
104
+
105
+ export { HistoryTimeline } from './components/HistoryTimeline';
106
+ export type { HistoryTimelineItem, HistoryTimelineProps } from './components/HistoryTimeline';
107
+
108
+ export { Toaster } from './components/Toaster';
109
+ export type { HomepadToasterProps } from './components/Toaster';
110
+
111
+ export { ToasterProvider } from './components/ToasterProvider';
112
+
113
+ export { toast } from './toast';
114
+
115
+ export {
116
+ runMutationWithToast,
117
+ type MutationToastMessages,
118
+ type MutationToastResult,
119
+ type RunMutationWithToastOptions,
120
+ } from './mutation-toast';
121
+
102
122
  export { ConfirmModal } from './components/ConfirmModal';
103
123
  export type { ConfirmModalProps } from './components/ConfirmModal';
104
124
 
@@ -0,0 +1,33 @@
1
+ import { toast } from './toast';
2
+
3
+ export type MutationToastMessages = {
4
+ success: string;
5
+ error: string;
6
+ };
7
+
8
+ export type MutationToastResult = {
9
+ success: boolean;
10
+ errorMessage?: string;
11
+ };
12
+
13
+ export type RunMutationWithToastOptions = {
14
+ getErrorMessage?: (error: unknown) => string;
15
+ };
16
+
17
+ export async function runMutationWithToast(
18
+ action: () => Promise<unknown>,
19
+ messages: MutationToastMessages,
20
+ options?: RunMutationWithToastOptions,
21
+ ): Promise<MutationToastResult> {
22
+ try {
23
+ await action();
24
+ toast.success(messages.success);
25
+
26
+ return { success: true };
27
+ } catch (error) {
28
+ const errorMessage = options?.getErrorMessage?.(error) ?? messages.error;
29
+ toast.error(errorMessage);
30
+
31
+ return { success: false, errorMessage };
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ export { toast } from 'sonner';