@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.
- package/dist/index.cjs +123 -84
- 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 +123 -84
- package/dist/index.js.map +1 -1
- package/dist/web/index.cjs +413 -22
- 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 +48 -1
- package/dist/web/index.d.ts +48 -1
- package/dist/web/index.js +406 -23
- package/dist/web/index.js.map +1 -1
- package/package.json +9 -3
- package/src/components/DatePicker.tsx +37 -3
- 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.tsx +5 -5
- package/src/web/components/TableActionButton.tsx +3 -3
- 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 +20 -0
- package/src/web/mutation-toast.ts +33 -0
- package/src/web/toast.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scripso-homepad/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cross-platform UI components for Homepad (React Web + React Native)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"import": "./src/web/index.ts",
|
|
30
30
|
"require": "./dist/web/index.cjs",
|
|
31
31
|
"default": "./src/web/index.ts"
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
|
+
"./web/table-scroll.css": "./src/web/components/table-scroll.css"
|
|
33
34
|
},
|
|
34
35
|
"files": [
|
|
35
36
|
"dist",
|
|
@@ -51,7 +52,8 @@
|
|
|
51
52
|
"react-native": ">=0.74",
|
|
52
53
|
"react-native-svg": ">=13",
|
|
53
54
|
"react-native-web": ">=0.19",
|
|
54
|
-
"react-router-dom": ">=6"
|
|
55
|
+
"react-router-dom": ">=6",
|
|
56
|
+
"sonner": ">=2.0.0"
|
|
55
57
|
},
|
|
56
58
|
"peerDependenciesMeta": {
|
|
57
59
|
"react-native-svg": {
|
|
@@ -65,6 +67,9 @@
|
|
|
65
67
|
},
|
|
66
68
|
"lucide-react": {
|
|
67
69
|
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"sonner": {
|
|
72
|
+
"optional": true
|
|
68
73
|
}
|
|
69
74
|
},
|
|
70
75
|
"dependencies": {
|
|
@@ -90,6 +95,7 @@
|
|
|
90
95
|
"react-native-svg": "^15.11.2",
|
|
91
96
|
"react-native-web": "^0.19.13",
|
|
92
97
|
"react-router-dom": "^7.9.1",
|
|
98
|
+
"sonner": "^2.0.7",
|
|
93
99
|
"storybook": "^8.4.7",
|
|
94
100
|
"tailwindcss": "^4.1.13",
|
|
95
101
|
"tsup": "^8.3.5",
|
|
@@ -51,6 +51,8 @@ type DatePickerFieldProps = {
|
|
|
51
51
|
containerStyle?: StyleProp<ViewStyle>;
|
|
52
52
|
className?: string;
|
|
53
53
|
fieldClassName?: string;
|
|
54
|
+
fieldStyle?: StyleProp<ViewStyle>;
|
|
55
|
+
fieldOutlineStyle?: StyleProp<ViewStyle>;
|
|
54
56
|
panelClassName?: string;
|
|
55
57
|
labelClassName?: string;
|
|
56
58
|
errorClassName?: string;
|
|
@@ -95,6 +97,8 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
95
97
|
containerStyle,
|
|
96
98
|
className,
|
|
97
99
|
fieldClassName,
|
|
100
|
+
fieldStyle,
|
|
101
|
+
fieldOutlineStyle,
|
|
98
102
|
panelClassName,
|
|
99
103
|
labelClassName,
|
|
100
104
|
errorClassName,
|
|
@@ -224,22 +228,39 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
const isFluidField = Boolean(fieldStyle) || Boolean(fieldOutlineStyle);
|
|
232
|
+
|
|
227
233
|
return (
|
|
228
|
-
<View
|
|
234
|
+
<View
|
|
235
|
+
ref={wrapperRef}
|
|
236
|
+
style={[styles.wrapper, isFluidField && styles.wrapperFluid, containerStyle]}
|
|
237
|
+
>
|
|
229
238
|
{label ? (
|
|
230
239
|
<Label disabled={disabled} style={styles.label} className={labelClassName}>
|
|
231
240
|
{label}
|
|
232
241
|
</Label>
|
|
233
242
|
) : null}
|
|
234
243
|
|
|
235
|
-
<View
|
|
244
|
+
<View
|
|
245
|
+
style={[
|
|
246
|
+
fieldStyles.outline,
|
|
247
|
+
styles.fieldOutline,
|
|
248
|
+
isFluidField && styles.fieldOutlineFluid,
|
|
249
|
+
fieldOutlineStyle,
|
|
250
|
+
]}
|
|
251
|
+
>
|
|
236
252
|
<Pressable
|
|
237
253
|
ref={triggerRef}
|
|
238
254
|
accessibilityRole="button"
|
|
239
255
|
accessibilityState={{ disabled, expanded: open }}
|
|
240
256
|
disabled={disabled}
|
|
241
257
|
onPress={openPicker}
|
|
242
|
-
style={[
|
|
258
|
+
style={[
|
|
259
|
+
calendarFieldMetrics.container,
|
|
260
|
+
isFluidField && styles.fieldContainerFluid,
|
|
261
|
+
fieldStyles.container,
|
|
262
|
+
fieldStyle,
|
|
263
|
+
]}
|
|
243
264
|
>
|
|
244
265
|
<Text
|
|
245
266
|
style={[calendarFieldMetrics.value, valueTextStyle]}
|
|
@@ -327,11 +348,24 @@ const styles = StyleSheet.create({
|
|
|
327
348
|
maxWidth: "100%",
|
|
328
349
|
gap: SELECT_LABEL_GAP,
|
|
329
350
|
},
|
|
351
|
+
wrapperFluid: {
|
|
352
|
+
width: "100%",
|
|
353
|
+
},
|
|
330
354
|
fieldOutline: {
|
|
331
355
|
width: CALENDAR_FIELD_WIDTH,
|
|
332
356
|
maxWidth: "100%",
|
|
333
357
|
alignSelf: "flex-start",
|
|
334
358
|
},
|
|
359
|
+
fieldOutlineFluid: {
|
|
360
|
+
width: "100%",
|
|
361
|
+
alignSelf: "stretch",
|
|
362
|
+
},
|
|
363
|
+
fieldContainerFluid: {
|
|
364
|
+
width: "100%",
|
|
365
|
+
minWidth: 0,
|
|
366
|
+
maxWidth: "100%",
|
|
367
|
+
alignSelf: "stretch",
|
|
368
|
+
},
|
|
335
369
|
label: {
|
|
336
370
|
...calendarLabelTypography,
|
|
337
371
|
},
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { fn } from '@storybook/test';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Button } from '../../components/Button';
|
|
6
|
+
import { WebLayoutCanvas } from '../layout/story-helpers';
|
|
7
|
+
|
|
8
|
+
import { Drawer } from './Drawer';
|
|
9
|
+
import { HistoryTimeline } from './HistoryTimeline';
|
|
10
|
+
import { StatusBadge } from './StatusBadge';
|
|
11
|
+
|
|
12
|
+
const historyEvents = [
|
|
13
|
+
{
|
|
14
|
+
id: 'received',
|
|
15
|
+
title: 'Հայտը ստացվել է',
|
|
16
|
+
timestamp: '24.06.2026, 14:20',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'approved',
|
|
20
|
+
title: 'Հայտը հաստատվել է, սպասում է վճարման',
|
|
21
|
+
timestamp: '24.06.2026, 15:05',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
title: 'Web Components/Drawer',
|
|
27
|
+
component: Drawer,
|
|
28
|
+
parameters: {
|
|
29
|
+
layout: 'fullscreen',
|
|
30
|
+
webLayout: true,
|
|
31
|
+
},
|
|
32
|
+
args: {
|
|
33
|
+
open: true,
|
|
34
|
+
title: 'Հայտ #00042',
|
|
35
|
+
subtitle: 'Ստացվել է 24.06.2026, 14:20',
|
|
36
|
+
closeAriaLabel: 'Փակել',
|
|
37
|
+
onClose: fn(),
|
|
38
|
+
},
|
|
39
|
+
} satisfies Meta<typeof Drawer>;
|
|
40
|
+
|
|
41
|
+
export default meta;
|
|
42
|
+
type Story = StoryObj<typeof meta>;
|
|
43
|
+
|
|
44
|
+
function DetailField({ label, value }: { label: string; value: string }) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex items-center justify-between gap-4">
|
|
47
|
+
<span className="min-w-0 flex-1 font-sans text-sm font-medium leading-[19px] tracking-normal text-storm-gray-500">
|
|
48
|
+
{label}
|
|
49
|
+
</span>
|
|
50
|
+
<span className="shrink-0 text-right font-sans text-sm font-medium leading-[14px] tracking-normal text-storm-gray-900">
|
|
51
|
+
{value}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function DetailsSection({ title, children }: { title: string; children: React.ReactNode }) {
|
|
58
|
+
return (
|
|
59
|
+
<section className="flex flex-col gap-3">
|
|
60
|
+
<h3 className="font-sans text-xs font-semibold uppercase leading-4 tracking-normal text-storm-gray-200">
|
|
61
|
+
{title}
|
|
62
|
+
</h3>
|
|
63
|
+
<div className="flex flex-col gap-4">{children}</div>
|
|
64
|
+
</section>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function RequestDetailsBody() {
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<div className="flex flex-col gap-8 pb-6">
|
|
72
|
+
<div className="flex items-center justify-between gap-4">
|
|
73
|
+
<span className="font-sans text-sm font-medium leading-[19px] tracking-normal text-storm-gray-500">
|
|
74
|
+
Կարգավիճակ
|
|
75
|
+
</span>
|
|
76
|
+
<StatusBadge tone="warning">Սպասում է վճարման</StatusBadge>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<DetailsSection title="Կազմակերպության տվյալներ">
|
|
80
|
+
<DetailField label="Հասցե" value="Արաբկիր 12" />
|
|
81
|
+
<DetailField label="Կազմակերպություն" value="Manuk's org" />
|
|
82
|
+
<DetailField label="Սակագնային պլան" value="150+ Apartments" />
|
|
83
|
+
<DetailField label="Անհատական գին" value="10 ֏" />
|
|
84
|
+
<DetailField label="Բնակարանների առավելագույն քանակ" value="120" />
|
|
85
|
+
</DetailsSection>
|
|
86
|
+
|
|
87
|
+
<DetailsSection title="Կոնտակտային անձ">
|
|
88
|
+
<DetailField label="Անուն / Ազգանուն" value="Manuk Grigoryan" />
|
|
89
|
+
<DetailField label="Էլ. փոստ" value="manuk@example.com" />
|
|
90
|
+
<DetailField label="Հեռախոսահամար" value="+374 99 000000" />
|
|
91
|
+
</DetailsSection>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<section className="-mx-6 flex flex-col justify-between border-t border-dashed border-storm-gray-50 px-6 pt-6 pb-0">
|
|
95
|
+
<div className="flex flex-col gap-3">
|
|
96
|
+
<h3 className="font-sans text-xs font-semibold uppercase leading-4 tracking-normal text-storm-gray-200">
|
|
97
|
+
Գործողությունների պատմություն
|
|
98
|
+
</h3>
|
|
99
|
+
<HistoryTimeline events={historyEvents} />
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
</>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const footerButton = (
|
|
107
|
+
<div className="w-full">
|
|
108
|
+
<Button
|
|
109
|
+
title="Կրկին ուղարկել վճարման հղումը"
|
|
110
|
+
onPress={fn()}
|
|
111
|
+
variant="primary"
|
|
112
|
+
className="btn shrink-0 justify-center!"
|
|
113
|
+
textClassName="text-center!"
|
|
114
|
+
style={{
|
|
115
|
+
width: '100%',
|
|
116
|
+
height: 52,
|
|
117
|
+
minHeight: 52,
|
|
118
|
+
maxHeight: 52,
|
|
119
|
+
paddingTop: 0,
|
|
120
|
+
paddingBottom: 0,
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
function DrawerStoryShell({
|
|
127
|
+
open,
|
|
128
|
+
onClose,
|
|
129
|
+
footer,
|
|
130
|
+
title,
|
|
131
|
+
subtitle,
|
|
132
|
+
}: {
|
|
133
|
+
open: boolean;
|
|
134
|
+
onClose: () => void;
|
|
135
|
+
footer?: React.ReactNode;
|
|
136
|
+
title?: string;
|
|
137
|
+
subtitle?: string;
|
|
138
|
+
}) {
|
|
139
|
+
return (
|
|
140
|
+
<WebLayoutCanvas className="min-h-screen bg-storm-gray-100">
|
|
141
|
+
<Drawer
|
|
142
|
+
open={open}
|
|
143
|
+
title={title ?? 'Հայտ #00042'}
|
|
144
|
+
subtitle={subtitle ?? 'Ստացվել է 24.06.2026, 14:20'}
|
|
145
|
+
closeAriaLabel="Փակել"
|
|
146
|
+
onClose={onClose}
|
|
147
|
+
footer={footer}
|
|
148
|
+
>
|
|
149
|
+
<RequestDetailsBody />
|
|
150
|
+
</Drawer>
|
|
151
|
+
</WebLayoutCanvas>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const Default: Story = {
|
|
156
|
+
args: {
|
|
157
|
+
children: null,
|
|
158
|
+
},
|
|
159
|
+
render: (args) => (
|
|
160
|
+
<DrawerStoryShell open={args.open} onClose={args.onClose} title={args.title} subtitle={args.subtitle} />
|
|
161
|
+
),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const WithFooter: Story = {
|
|
165
|
+
args: {
|
|
166
|
+
children: null,
|
|
167
|
+
},
|
|
168
|
+
render: (args) => (
|
|
169
|
+
<DrawerStoryShell
|
|
170
|
+
open={args.open}
|
|
171
|
+
onClose={args.onClose}
|
|
172
|
+
title={args.title}
|
|
173
|
+
subtitle={args.subtitle}
|
|
174
|
+
footer={footerButton}
|
|
175
|
+
/>
|
|
176
|
+
),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const WithoutSubtitle: Story = {
|
|
180
|
+
args: {
|
|
181
|
+
children: null,
|
|
182
|
+
subtitle: undefined,
|
|
183
|
+
title: 'Կարգավորումներ',
|
|
184
|
+
},
|
|
185
|
+
render: (args) => (
|
|
186
|
+
<WebLayoutCanvas className="min-h-screen bg-storm-gray-100">
|
|
187
|
+
<Drawer
|
|
188
|
+
open={args.open}
|
|
189
|
+
title={args.title}
|
|
190
|
+
closeAriaLabel="Փակել"
|
|
191
|
+
onClose={args.onClose}
|
|
192
|
+
>
|
|
193
|
+
<p className="font-sans text-sm leading-[19px] text-storm-gray-500">
|
|
194
|
+
Drawer content without a subtitle line in the header.
|
|
195
|
+
</p>
|
|
196
|
+
</Drawer>
|
|
197
|
+
</WebLayoutCanvas>
|
|
198
|
+
),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
function InteractiveDrawerStory() {
|
|
202
|
+
const [open, setOpen] = useState(false);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<WebLayoutCanvas className="min-h-screen bg-storm-gray-100 p-6">
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
className="rounded-xl border border-storm-gray-50 bg-white px-4 py-2 text-sm font-medium text-slate-blue"
|
|
209
|
+
onClick={() => setOpen(true)}
|
|
210
|
+
>
|
|
211
|
+
Բացել drawer-ը
|
|
212
|
+
</button>
|
|
213
|
+
|
|
214
|
+
<Drawer
|
|
215
|
+
open={open}
|
|
216
|
+
title="Հայտ #00042"
|
|
217
|
+
subtitle="Ստացվել է 24.06.2026, 14:20"
|
|
218
|
+
closeAriaLabel="Փակել"
|
|
219
|
+
onClose={() => setOpen(false)}
|
|
220
|
+
footer={footerButton}
|
|
221
|
+
>
|
|
222
|
+
<RequestDetailsBody />
|
|
223
|
+
</Drawer>
|
|
224
|
+
</WebLayoutCanvas>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const Interactive: Story = {
|
|
229
|
+
args: {
|
|
230
|
+
children: null,
|
|
231
|
+
open: false,
|
|
232
|
+
},
|
|
233
|
+
render: () => <InteractiveDrawerStory />,
|
|
234
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -77,8 +77,8 @@ export function Modal({
|
|
|
77
77
|
|
|
78
78
|
const sharedButtonClassName = cn(
|
|
79
79
|
'btn h-13 justify-center!',
|
|
80
|
-
footerLayout === 'split' && 'min-w-0 flex-1',
|
|
81
|
-
buttonClassName,
|
|
80
|
+
footerLayout === 'split' && 'min-w-0 flex-1 basis-0',
|
|
81
|
+
footerLayout !== 'split' && buttonClassName,
|
|
82
82
|
);
|
|
83
83
|
|
|
84
84
|
return (
|
|
@@ -116,14 +116,14 @@ export function Modal({
|
|
|
116
116
|
<div className="flex min-w-0 flex-col gap-2">
|
|
117
117
|
<h2
|
|
118
118
|
id="homepad-modal-title"
|
|
119
|
-
className="font-sans text-lg font-bold leading-
|
|
119
|
+
className="font-sans text-lg font-bold leading-6 tracking-normal text-black"
|
|
120
120
|
>
|
|
121
121
|
{title}
|
|
122
122
|
</h2>
|
|
123
123
|
{subtitle ? (
|
|
124
124
|
<p
|
|
125
125
|
id="homepad-modal-subtitle"
|
|
126
|
-
className="font-sans text-xs font-normal leading-
|
|
126
|
+
className="font-sans text-xs font-normal leading-4 tracking-normal text-storm-gray-400"
|
|
127
127
|
>
|
|
128
128
|
{subtitle}
|
|
129
129
|
</p>
|
|
@@ -135,7 +135,7 @@ export function Modal({
|
|
|
135
135
|
|
|
136
136
|
<footer
|
|
137
137
|
className={cn(
|
|
138
|
-
'flex gap-4',
|
|
138
|
+
'flex shrink-0 gap-4',
|
|
139
139
|
footerLayout === 'split' ? 'w-full' : footerAlignClasses[footerAlign],
|
|
140
140
|
)}
|
|
141
141
|
>
|
|
@@ -16,12 +16,12 @@ export type TableActionButtonProps = {
|
|
|
16
16
|
'onClick' | 'title' | 'aria-expanded' | 'aria-haspopup' | 'aria-controls'
|
|
17
17
|
>;
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const inlineActionClassName =
|
|
20
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';
|
|
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
|
|