@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.
- package/dist/{chunk-DAOLVE64.js → chunk-YSDLHEJ7.js} +204 -14
- package/dist/chunk-YSDLHEJ7.js.map +1 -0
- package/dist/index.cjs +130 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +176 -323
- package/dist/index.js.map +1 -1
- package/dist/web/index.cjs +882 -30
- 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 +90 -3
- package/dist/web/index.d.ts +90 -3
- package/dist/web/index.js +645 -19
- package/dist/web/index.js.map +1 -1
- package/package.json +9 -3
- package/src/components/Button.tsx +10 -3
- package/src/components/DatePicker.tsx +37 -3
- package/src/components/Select.tsx +1 -1
- package/src/web/components/ConfirmModal.stories.tsx +55 -0
- package/src/web/components/ConfirmModal.tsx +116 -0
- 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 +130 -0
- package/src/web/components/Modal.tsx +162 -0
- package/src/web/components/TableActionButton.tsx +9 -4
- 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/icons/TimelineCheckIcon.tsx +24 -0
- package/src/web/icons/ToastIcons.tsx +119 -0
- package/src/web/index.ts +26 -0
- package/src/web/mutation-toast.ts +33 -0
- package/src/web/toast.ts +1 -0
- 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<
|
|
14
|
+
} & Pick<
|
|
15
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
16
|
+
'onClick' | 'title' | 'aria-expanded' | 'aria-haspopup' | 'aria-controls'
|
|
17
|
+
>;
|
|
15
18
|
|
|
16
|
-
const
|
|
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:
|
|
21
|
-
reject:
|
|
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
|
+
};
|