@scripso-homepad/ui 0.4.8 → 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.
Files changed (39) hide show
  1. package/dist/{chunk-DAOLVE64.js → chunk-YSDLHEJ7.js} +204 -14
  2. package/dist/chunk-YSDLHEJ7.js.map +1 -0
  3. package/dist/index.cjs +130 -87
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +2 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +176 -323
  8. package/dist/index.js.map +1 -1
  9. package/dist/web/index.cjs +882 -30
  10. package/dist/web/index.cjs.map +1 -1
  11. package/dist/web/index.css +161 -0
  12. package/dist/web/index.css.map +1 -1
  13. package/dist/web/index.d.cts +90 -3
  14. package/dist/web/index.d.ts +90 -3
  15. package/dist/web/index.js +645 -19
  16. package/dist/web/index.js.map +1 -1
  17. package/package.json +9 -3
  18. package/src/components/Button.tsx +10 -3
  19. package/src/components/DatePicker.tsx +37 -3
  20. package/src/components/Select.tsx +1 -1
  21. package/src/web/components/ConfirmModal.stories.tsx +55 -0
  22. package/src/web/components/ConfirmModal.tsx +116 -0
  23. package/src/web/components/Drawer.stories.tsx +234 -0
  24. package/src/web/components/Drawer.tsx +169 -0
  25. package/src/web/components/HistoryTimeline.tsx +46 -0
  26. package/src/web/components/Modal.stories.tsx +130 -0
  27. package/src/web/components/Modal.tsx +162 -0
  28. package/src/web/components/TableActionButton.tsx +9 -4
  29. package/src/web/components/Toaster.stories.tsx +106 -0
  30. package/src/web/components/Toaster.tsx +92 -0
  31. package/src/web/components/ToasterProvider.tsx +6 -0
  32. package/src/web/components/drawer-scroll.css +41 -0
  33. package/src/web/components/sonner-toast.css +141 -0
  34. package/src/web/icons/TimelineCheckIcon.tsx +24 -0
  35. package/src/web/icons/ToastIcons.tsx +119 -0
  36. package/src/web/index.ts +26 -0
  37. package/src/web/mutation-toast.ts +33 -0
  38. package/src/web/toast.ts +1 -0
  39. package/dist/chunk-DAOLVE64.js.map +0 -1
@@ -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="font-sans text-[18px] font-bold leading-6 tracking-normal text-black"
131
+ >
132
+ {title}
133
+ </h2>
134
+ {subtitle ? (
135
+ <p
136
+ id="homepad-drawer-subtitle"
137
+ className="font-sans text-sm font-normal leading-[19px] 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="font-sans text-sm font-medium leading-[19px] tracking-normal text-slate-blue">
35
+ {event.title}
36
+ </p>
37
+ <p className="font-sans text-xs font-normal leading-4 tracking-normal text-storm-gray-300">
38
+ {event.timestamp}
39
+ </p>
40
+ </div>
41
+ </li>
42
+ );
43
+ })}
44
+ </ol>
45
+ );
46
+ }
@@ -0,0 +1,130 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+ import { useState } from 'react';
4
+
5
+ import { Input } from '../../components/Input';
6
+ import { TableMenuEditIcon } from '../icons/TableMenuEditIcon';
7
+ import { WebLayoutCanvas } from '../layout/story-helpers';
8
+
9
+ import { Modal, type ModalFooterAlign } from './Modal';
10
+
11
+ const meta = {
12
+ title: 'Web Components/Modal',
13
+ component: Modal,
14
+ parameters: {
15
+ layout: 'fullscreen',
16
+ webLayout: true,
17
+ },
18
+ args: {
19
+ open: true,
20
+ title: 'Նոր շենքի ավելացում',
21
+ subtitle: 'Լրացրեք տվյալները՝ նոր շենք ավելացնելու համար',
22
+ cancelText: 'Չեղարկել',
23
+ confirmText: 'Ավելացնել շենքը',
24
+ onCancel: fn(),
25
+ onConfirm: fn(),
26
+ footerAlign: 'right' as ModalFooterAlign,
27
+ },
28
+ } satisfies Meta<typeof Modal>;
29
+
30
+ export default meta;
31
+ type Story = StoryObj<typeof meta>;
32
+
33
+ function AddBuildingModalBody() {
34
+ return (
35
+ <div className="flex flex-col gap-4">
36
+ <div className="grid gap-4 md:grid-cols-2">
37
+ <Input label="Կազմակերպություն" value="" onChangeText={() => undefined} className="w-full" />
38
+ <Input label="Հասցե" value="" onChangeText={() => undefined} className="w-full" />
39
+ </div>
40
+ <Input label="Կոնտակտային անձ" value="" onChangeText={() => undefined} className="w-full" />
41
+ <div className="grid gap-4 md:grid-cols-2">
42
+ <Input label="Էլ. փոստ" value="" onChangeText={() => undefined} className="w-full" />
43
+ <Input label="Հեռախոսահամար" value="" onChangeText={() => undefined} className="w-full" />
44
+ </div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export const Default: Story = {
50
+ args: {
51
+ children: null,
52
+ },
53
+ render: (args) => (
54
+ <WebLayoutCanvas className="min-h-screen bg-storm-gray-50 p-6">
55
+ <Modal {...args}>
56
+ <AddBuildingModalBody />
57
+ </Modal>
58
+ </WebLayoutCanvas>
59
+ ),
60
+ };
61
+
62
+ export const WithIcon: Story = {
63
+ args: {
64
+ children: null,
65
+ title: 'Շենքի խմբագրում',
66
+ subtitle: 'Թարմացրեք շենքի տվյալները',
67
+ icon: <TableMenuEditIcon />,
68
+ },
69
+ render: (args) => (
70
+ <WebLayoutCanvas className="min-h-screen bg-storm-gray-50 p-6">
71
+ <Modal {...args}>
72
+ <AddBuildingModalBody />
73
+ </Modal>
74
+ </WebLayoutCanvas>
75
+ ),
76
+ };
77
+
78
+ export const SplitFooter: Story = {
79
+ args: {
80
+ children: null,
81
+ footerLayout: 'split',
82
+ },
83
+ render: (args) => (
84
+ <WebLayoutCanvas className="min-h-screen bg-storm-gray-50 p-6">
85
+ <Modal {...args}>
86
+ <AddBuildingModalBody />
87
+ </Modal>
88
+ </WebLayoutCanvas>
89
+ ),
90
+ };
91
+
92
+ export const FooterAlignments: Story = {
93
+ args: {
94
+ children: null,
95
+ },
96
+ render: () => <FooterAlignmentsStory />,
97
+ };
98
+
99
+ function FooterAlignmentsStory() {
100
+ const [align, setAlign] = useState<ModalFooterAlign>('right');
101
+
102
+ return (
103
+ <WebLayoutCanvas className="min-h-screen bg-storm-gray-50 p-6">
104
+ <div className="mb-4 flex gap-2">
105
+ {(['left', 'center', 'right'] as const).map((value) => (
106
+ <button
107
+ key={value}
108
+ type="button"
109
+ className="rounded-lg border border-storm-gray-50 px-3 py-1 text-sm"
110
+ onClick={() => setAlign(value)}
111
+ >
112
+ {value}
113
+ </button>
114
+ ))}
115
+ </div>
116
+ <Modal
117
+ open
118
+ title="Նոր շենքի ավելացում"
119
+ subtitle="Լրացրեք տվյալները՝ նոր շենք ավելացնելու համար"
120
+ cancelText="Չեղարկել"
121
+ confirmText="Ավելացնել շենքը"
122
+ footerAlign={align}
123
+ onCancel={fn()}
124
+ onConfirm={fn()}
125
+ >
126
+ <AddBuildingModalBody />
127
+ </Modal>
128
+ </WebLayoutCanvas>
129
+ );
130
+ }
@@ -0,0 +1,162 @@
1
+ import { useEffect, type ReactNode } from 'react';
2
+
3
+ import { Button } from '../../components/Button';
4
+ import { cn } from '../utils/cn';
5
+
6
+ export type ModalFooterAlign = 'left' | 'center' | 'right';
7
+ export type ModalFooterLayout = 'inline' | 'split';
8
+
9
+ export type ModalProps = {
10
+ open: boolean;
11
+ title: string;
12
+ subtitle?: string;
13
+ icon?: ReactNode;
14
+ iconContainerClassName?: string;
15
+ children: ReactNode;
16
+ cancelText: string;
17
+ confirmText: string;
18
+ onCancel: () => void;
19
+ onConfirm: () => void;
20
+ footerAlign?: ModalFooterAlign;
21
+ footerLayout?: ModalFooterLayout;
22
+ cancelDisabled?: boolean;
23
+ confirmDisabled?: boolean;
24
+ closeOnBackdrop?: boolean;
25
+ containerClassName?: string;
26
+ contentClassName?: string;
27
+ buttonClassName?: string;
28
+ };
29
+
30
+ const footerAlignClasses: Record<ModalFooterAlign, string> = {
31
+ left: 'justify-start',
32
+ center: 'justify-center',
33
+ right: 'justify-end',
34
+ };
35
+
36
+ export function Modal({
37
+ open,
38
+ title,
39
+ subtitle,
40
+ icon,
41
+ iconContainerClassName,
42
+ children,
43
+ cancelText,
44
+ confirmText,
45
+ onCancel,
46
+ onConfirm,
47
+ footerAlign = 'right',
48
+ footerLayout = 'inline',
49
+ cancelDisabled = false,
50
+ confirmDisabled = false,
51
+ closeOnBackdrop = true,
52
+ containerClassName,
53
+ contentClassName,
54
+ buttonClassName,
55
+ }: ModalProps) {
56
+ useEffect(() => {
57
+ if (!open) {
58
+ return;
59
+ }
60
+
61
+ const handleKeyDown = (event: KeyboardEvent) => {
62
+ if (event.key === 'Escape') {
63
+ onCancel();
64
+ }
65
+ };
66
+
67
+ window.addEventListener('keydown', handleKeyDown);
68
+
69
+ return () => {
70
+ window.removeEventListener('keydown', handleKeyDown);
71
+ };
72
+ }, [open, onCancel]);
73
+
74
+ if (!open) {
75
+ return null;
76
+ }
77
+
78
+ const sharedButtonClassName = cn(
79
+ 'btn h-13 justify-center!',
80
+ footerLayout === 'split' && 'min-w-0 flex-1 basis-0',
81
+ footerLayout !== 'split' && buttonClassName,
82
+ );
83
+
84
+ return (
85
+ <div
86
+ className="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-[12px] bg-storm-gray-850/40"
87
+ onClick={closeOnBackdrop ? onCancel : undefined}
88
+ role="presentation"
89
+ >
90
+ <div
91
+ role="dialog"
92
+ aria-modal="true"
93
+ aria-labelledby="homepad-modal-title"
94
+ {...(subtitle ? { 'aria-describedby': 'homepad-modal-subtitle' } : {})}
95
+ className={cn(
96
+ 'flex w-full max-w-2xl flex-col gap-6 rounded-2xl bg-white p-4 opacity-100 md:p-6',
97
+ containerClassName,
98
+ )}
99
+ onClick={(event) => {
100
+ event.stopPropagation();
101
+ }}
102
+ >
103
+ <header className={cn('flex', icon ? 'items-center gap-2.5' : undefined)}>
104
+ {icon ? (
105
+ <div
106
+ className={cn(
107
+ 'flex size-11 shrink-0 items-center justify-center rounded-lg bg-storm-gray-50 p-3 opacity-100',
108
+ iconContainerClassName,
109
+ )}
110
+ >
111
+ <div className="flex size-5 shrink-0 items-center justify-center [&>*]:size-5 [&_svg]:size-5">
112
+ {icon}
113
+ </div>
114
+ </div>
115
+ ) : null}
116
+ <div className="flex min-w-0 flex-col gap-2">
117
+ <h2
118
+ id="homepad-modal-title"
119
+ className="font-sans text-lg font-bold leading-6 tracking-normal text-black"
120
+ >
121
+ {title}
122
+ </h2>
123
+ {subtitle ? (
124
+ <p
125
+ id="homepad-modal-subtitle"
126
+ className="font-sans text-xs font-normal leading-4 tracking-normal text-storm-gray-400"
127
+ >
128
+ {subtitle}
129
+ </p>
130
+ ) : null}
131
+ </div>
132
+ </header>
133
+
134
+ <div className={cn('min-w-0', contentClassName)}>{children}</div>
135
+
136
+ <footer
137
+ className={cn(
138
+ 'flex shrink-0 gap-4',
139
+ footerLayout === 'split' ? 'w-full' : footerAlignClasses[footerAlign],
140
+ )}
141
+ >
142
+ <Button
143
+ title={cancelText}
144
+ onPress={onCancel}
145
+ variant="gray"
146
+ disabled={cancelDisabled}
147
+ className={sharedButtonClassName}
148
+ textClassName="text-center!"
149
+ />
150
+ <Button
151
+ title={confirmText}
152
+ onPress={onConfirm}
153
+ variant="primary"
154
+ disabled={confirmDisabled}
155
+ className={sharedButtonClassName}
156
+ textClassName="text-center!"
157
+ />
158
+ </footer>
159
+ </div>
160
+ </div>
161
+ );
162
+ }
@@ -11,14 +11,17 @@ export type TableActionButtonProps = {
11
11
  ariaLabel?: string;
12
12
  className?: string;
13
13
  disabled?: boolean;
14
- } & Pick<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'aria-expanded' | 'aria-haspopup' | 'aria-controls'>;
14
+ } & Pick<
15
+ ButtonHTMLAttributes<HTMLButtonElement>,
16
+ 'onClick' | 'title' | 'aria-expanded' | 'aria-haspopup' | 'aria-controls'
17
+ >;
15
18
 
16
- const actionButtonClassName =
19
+ const inlineActionClassName =
17
20
  'inline-flex cursor-pointer items-center gap-1 rounded-lg border border-storm-gray-50 bg-white px-3 py-2 font-sans text-xs font-medium leading-none text-storm-gray-900 transition-colors hover:bg-storm-gray-0';
18
21
 
19
22
  const variantClasses: Record<TableActionButtonVariant, string> = {
20
- approve: actionButtonClassName,
21
- reject: actionButtonClassName,
23
+ approve: inlineActionClassName,
24
+ reject: inlineActionClassName,
22
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',
23
26
  };
24
27
 
@@ -40,6 +43,7 @@ export const TableActionButton = forwardRef<HTMLButtonElement, TableActionButton
40
43
  ref,
41
44
  ) {
42
45
  const isInlineAction = variant === 'approve' || variant === 'reject';
46
+ const hasLabel = Boolean(children);
43
47
 
44
48
  return (
45
49
  <button
@@ -50,6 +54,7 @@ export const TableActionButton = forwardRef<HTMLButtonElement, TableActionButton
50
54
  aria-label={ariaLabel}
51
55
  className={cn(
52
56
  variantClasses[variant],
57
+ isInlineAction && !hasLabel && 'px-2',
53
58
  disabled && 'cursor-not-allowed opacity-50',
54
59
  className,
55
60
  )}
@@ -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
+ };