@scripso-homepad/ui 0.4.2 → 0.4.3
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-WDSMJWKC.js → chunk-66WR57JJ.js} +35 -3
- package/dist/chunk-66WR57JJ.js.map +1 -0
- package/dist/index.cjs +2260 -385
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -1
- package/dist/index.d.ts +133 -1
- package/dist/index.js +1960 -97
- package/dist/index.js.map +1 -1
- package/dist/web/index.cjs +30650 -1009
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.css +78 -0
- package/dist/web/index.css.map +1 -0
- package/dist/web/index.d.cts +267 -6
- package/dist/web/index.d.ts +267 -6
- package/dist/web/index.js +30179 -601
- package/dist/web/index.js.map +1 -1
- package/package.json +5 -4
- package/src/components/Calendar.stories.tsx +337 -0
- package/src/components/Calendar.tsx +441 -0
- package/src/components/DatePicker.stories.tsx +219 -0
- package/src/components/DatePicker.tsx +366 -0
- package/src/components/Select.stories.tsx +134 -0
- package/src/components/Select.tsx +462 -0
- package/src/css.d.ts +1 -0
- package/src/icons/CalendarIcon.tsx +28 -0
- package/src/icons/CalendarIcon.web.tsx +34 -0
- package/src/icons/ChevronDownIcon.tsx +11 -4
- package/src/icons/ChevronDownIcon.web.tsx +6 -4
- package/src/icons/ChevronNavIcons.tsx +44 -0
- package/src/icons/ChevronNavIcons.web.tsx +57 -0
- package/src/icons/calendarIconPaths.ts +2 -0
- package/src/icons/chevronDownPath.ts +1 -0
- package/src/icons/chevronNavPaths.ts +2 -0
- package/src/index.ts +41 -0
- package/src/theme/calendar.ts +357 -0
- package/src/theme/select.ts +249 -0
- package/src/utils/calendarDays.ts +273 -0
- package/src/utils/calendarLocaleContext.tsx +95 -0
- package/src/utils/calendarLocalization.ts +187 -0
- package/src/utils/countryDropdownScrollStyles.ts +53 -13
- package/src/utils/formatMultiSelectDisplay.ts +56 -0
- package/src/web/components/Badge.stories.tsx +43 -0
- package/src/web/components/Badge.tsx +35 -0
- package/src/web/components/Pagination.stories.tsx +78 -0
- package/src/web/components/Pagination.tsx +153 -0
- package/src/web/components/StatusBadge.tsx +20 -0
- package/src/web/components/Table.stories.tsx +415 -0
- package/src/web/components/Table.tsx +3 -0
- package/src/web/components/TableActionButton.tsx +67 -0
- package/src/web/components/TableActionsMenu.tsx +208 -0
- package/src/web/components/TableIconText.tsx +23 -0
- package/src/web/components/TableRowStatusActions.tsx +33 -0
- package/src/web/components/TableStatusActionsCell.tsx +29 -0
- package/src/web/components/pagination-utils.ts +39 -0
- package/src/web/components/table/DataTable.tsx +123 -0
- package/src/web/components/table/TablePrimitives.tsx +214 -0
- package/src/web/components/table/TableScrollArea.tsx +51 -0
- package/src/web/components/table/constants.ts +59 -0
- package/src/web/components/table/data-table-class-names.ts +26 -0
- package/src/web/components/table/index.ts +44 -0
- package/src/web/components/table/table-row-context.tsx +19 -0
- package/src/web/components/table/use-horizontal-scroll-state.ts +72 -0
- package/src/web/components/table-scroll.css +81 -0
- package/src/web/hooks/useOnClickOutside.ts +20 -5
- package/src/web/icons/TableMenuActivateIcon.tsx +27 -0
- package/src/web/icons/TableMenuCheckIcon.tsx +27 -0
- package/src/web/icons/TableMenuCloseIcon.tsx +27 -0
- package/src/web/icons/TableMenuEditIcon.tsx +27 -0
- package/src/web/icons/TableMenuInfoIcon.tsx +27 -0
- package/src/web/icons/TableMenuRefreshIcon.tsx +27 -0
- package/src/web/icons/TableMenuSuspendIcon.tsx +34 -0
- package/src/web/icons/TableMenuTrashIcon.tsx +27 -0
- package/src/web/icons/TableMoreIcon.tsx +41 -0
- package/src/web/icons/TableSortIcon.tsx +37 -0
- package/src/web/index.ts +74 -0
- package/src/web/layout/AppHeader.stories.tsx +2 -0
- package/src/web/layout/AppHeader.tsx +18 -14
- package/src/web/layout/DashboardLayout.stories.tsx +1 -0
- package/src/web/layout/DashboardLayout.tsx +4 -4
- package/src/web/layout/story-helpers.tsx +10 -2
- package/dist/chunk-WDSMJWKC.js.map +0 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { fn } from '@storybook/test';
|
|
3
|
+
import { Mail, Phone } from 'lucide-react';
|
|
4
|
+
import { useMemo, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { WebLayoutCanvas } from '../layout/story-helpers';
|
|
7
|
+
import { TableMenuCheckIcon } from '../icons/TableMenuCheckIcon';
|
|
8
|
+
import { TableMenuCloseIcon } from '../icons/TableMenuCloseIcon';
|
|
9
|
+
import { TableMenuInfoIcon } from '../icons/TableMenuInfoIcon';
|
|
10
|
+
import { TableMenuRefreshIcon } from '../icons/TableMenuRefreshIcon';
|
|
11
|
+
import { TableMenuTrashIcon } from '../icons/TableMenuTrashIcon';
|
|
12
|
+
import { Pagination } from './Pagination';
|
|
13
|
+
import { StatusBadge } from './StatusBadge';
|
|
14
|
+
import {
|
|
15
|
+
DataTable,
|
|
16
|
+
Table,
|
|
17
|
+
TableActionsCell,
|
|
18
|
+
TableBody,
|
|
19
|
+
TableCell,
|
|
20
|
+
TableHead,
|
|
21
|
+
TableHeader,
|
|
22
|
+
TableRow,
|
|
23
|
+
type TableSortDirection,
|
|
24
|
+
} from './Table';
|
|
25
|
+
import { TableActionButton } from './TableActionButton';
|
|
26
|
+
import { TableActionsMenu } from './TableActionsMenu';
|
|
27
|
+
import { TableIconText } from './TableIconText';
|
|
28
|
+
|
|
29
|
+
const PAGE_SIZE = 4;
|
|
30
|
+
|
|
31
|
+
const armenianPaginationLabels = {
|
|
32
|
+
previousPage: 'Նախորդ էջ',
|
|
33
|
+
nextPage: 'Հաջորդ էջ',
|
|
34
|
+
page: (pageNumber: number) => `Էջ ${pageNumber}`,
|
|
35
|
+
showing: ({ start, end, total }: { start: number; end: number; total: number }) =>
|
|
36
|
+
`Ցուցադրված է ${start}-${end}-ը ${total}-ից`,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const statusKeys = ['pending', 'rejected', 'payment', 'approved'] as const;
|
|
40
|
+
type RequestStatus = (typeof statusKeys)[number];
|
|
41
|
+
|
|
42
|
+
const statusConfig: Record<
|
|
43
|
+
RequestStatus,
|
|
44
|
+
{ label: string; tone: 'info' | 'danger' | 'warning' | 'success' }
|
|
45
|
+
> = {
|
|
46
|
+
pending: { label: 'Սպասում է հաստատման', tone: 'info' },
|
|
47
|
+
rejected: { label: 'Մերժված', tone: 'danger' },
|
|
48
|
+
payment: { label: 'Սպասում է վճարման', tone: 'warning' },
|
|
49
|
+
approved: { label: 'Հաստատված', tone: 'success' },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const requestTemplates = [
|
|
53
|
+
{
|
|
54
|
+
organization: '«Էլիտ Հոլդինգ»',
|
|
55
|
+
contact: 'Արմեն Գրիգորյան',
|
|
56
|
+
email: 'a.grigoryan@mail.am',
|
|
57
|
+
phone: '+374 (91) 44-55-66',
|
|
58
|
+
date: '24.06.2026',
|
|
59
|
+
status: 'pending' as const,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
organization: '«Անի-1» Կառավարչություն',
|
|
63
|
+
contact: 'Աննա Հովհաննիսյան',
|
|
64
|
+
email: 'info@maqurtun.am',
|
|
65
|
+
phone: '+374 (77) 11-22-33',
|
|
66
|
+
date: '23.06.2026',
|
|
67
|
+
status: 'rejected' as const,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
organization: '«Արագած» Համատիրություն',
|
|
71
|
+
contact: 'Կարեն Դավթյան',
|
|
72
|
+
email: 'k.davtyan@elite.am',
|
|
73
|
+
phone: '+374 (55) 88-99-00',
|
|
74
|
+
date: '21.06.2026',
|
|
75
|
+
status: 'payment' as const,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
organization: '«Մաքուր տուն» ՍՊԸ',
|
|
79
|
+
contact: 'Սուրեն Մնացականյան',
|
|
80
|
+
email: 'suren.m@gmail.com',
|
|
81
|
+
phone: '+374 (94) 77-55-11',
|
|
82
|
+
date: '19.06.2026',
|
|
83
|
+
status: 'approved' as const,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const allRequests = Array.from({ length: 12 }, (_, index) => {
|
|
88
|
+
const template = requestTemplates[index % requestTemplates.length];
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: String(index + 1),
|
|
92
|
+
...template,
|
|
93
|
+
organization: index < requestTemplates.length
|
|
94
|
+
? template.organization
|
|
95
|
+
: `${template.organization} ${Math.floor(index / requestTemplates.length) + 1}`,
|
|
96
|
+
status: statusKeys[index % statusKeys.length],
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function getStoryMenuItems(status: RequestStatus) {
|
|
101
|
+
switch (status) {
|
|
102
|
+
case 'pending':
|
|
103
|
+
return [];
|
|
104
|
+
case 'payment':
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
id: 'details',
|
|
108
|
+
label: 'Դիտել մանրամասները',
|
|
109
|
+
icon: <TableMenuInfoIcon />,
|
|
110
|
+
onSelect: fn(),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'resend',
|
|
114
|
+
label: 'Կրկին ուղարկել հղումը',
|
|
115
|
+
icon: <TableMenuRefreshIcon />,
|
|
116
|
+
onSelect: fn(),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
case 'rejected':
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
id: 'remove',
|
|
123
|
+
label: 'Հեռացնել',
|
|
124
|
+
icon: <TableMenuTrashIcon />,
|
|
125
|
+
onSelect: fn(),
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
case 'approved':
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
id: 'details',
|
|
132
|
+
label: 'Դիտել մանրամասները',
|
|
133
|
+
icon: <TableMenuInfoIcon />,
|
|
134
|
+
onSelect: fn(),
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
default:
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const meta = {
|
|
143
|
+
title: 'Web Components/Table',
|
|
144
|
+
parameters: {
|
|
145
|
+
layout: 'fullscreen',
|
|
146
|
+
webLayout: true,
|
|
147
|
+
},
|
|
148
|
+
} satisfies Meta;
|
|
149
|
+
|
|
150
|
+
export default meta;
|
|
151
|
+
type Story = StoryObj<typeof meta>;
|
|
152
|
+
|
|
153
|
+
function RequestsTableStory() {
|
|
154
|
+
const [page, setPage] = useState(1);
|
|
155
|
+
|
|
156
|
+
const pageRequests = useMemo(() => {
|
|
157
|
+
const start = (page - 1) * PAGE_SIZE;
|
|
158
|
+
|
|
159
|
+
return allRequests.slice(start, start + PAGE_SIZE);
|
|
160
|
+
}, [page]);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<WebLayoutCanvas className="flex h-screen flex-col">
|
|
164
|
+
<div className="mx-auto flex min-h-0 w-[90%] flex-1 flex-col py-8">
|
|
165
|
+
<DataTable
|
|
166
|
+
fillHeight
|
|
167
|
+
minBodyHeight={0}
|
|
168
|
+
className="min-h-0 flex-1"
|
|
169
|
+
minWidth={980}
|
|
170
|
+
footer={
|
|
171
|
+
<Pagination
|
|
172
|
+
page={page}
|
|
173
|
+
pageSize={PAGE_SIZE}
|
|
174
|
+
total={allRequests.length}
|
|
175
|
+
onPageChange={setPage}
|
|
176
|
+
labels={armenianPaginationLabels}
|
|
177
|
+
variant="attached"
|
|
178
|
+
/>
|
|
179
|
+
}
|
|
180
|
+
>
|
|
181
|
+
<Table>
|
|
182
|
+
<TableHeader>
|
|
183
|
+
<TableRow className="hover:bg-transparent">
|
|
184
|
+
<TableHead>Կազմակերպություն</TableHead>
|
|
185
|
+
<TableHead>Կոնտակտային Անձ</TableHead>
|
|
186
|
+
<TableHead>Էլ. Փոստ</TableHead>
|
|
187
|
+
<TableHead>Հեռախոսահամար</TableHead>
|
|
188
|
+
<TableHead>Հայտի Ամսաթիվ</TableHead>
|
|
189
|
+
<TableHead>Կարգավիճակ</TableHead>
|
|
190
|
+
<TableHead stickyRight className="w-px p-0" aria-hidden />
|
|
191
|
+
</TableRow>
|
|
192
|
+
</TableHeader>
|
|
193
|
+
<TableBody>
|
|
194
|
+
{pageRequests.map((request) => {
|
|
195
|
+
const status = statusConfig[request.status];
|
|
196
|
+
const isPending = request.status === 'pending';
|
|
197
|
+
const menuItems = getStoryMenuItems(request.status);
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<TableRow key={request.id}>
|
|
201
|
+
<TableCell variant="emphasis">{request.organization}</TableCell>
|
|
202
|
+
<TableCell>{request.contact}</TableCell>
|
|
203
|
+
<TableCell variant="strong">
|
|
204
|
+
<TableIconText icon={<Mail className="size-3.5" />}>{request.email}</TableIconText>
|
|
205
|
+
</TableCell>
|
|
206
|
+
<TableCell variant="strong">
|
|
207
|
+
<TableIconText icon={<Phone className="size-3.5" />}>{request.phone}</TableIconText>
|
|
208
|
+
</TableCell>
|
|
209
|
+
<TableCell variant="muted">{request.date}</TableCell>
|
|
210
|
+
<TableCell>
|
|
211
|
+
<StatusBadge tone={status.tone}>{status.label}</StatusBadge>
|
|
212
|
+
</TableCell>
|
|
213
|
+
<TableActionsCell>
|
|
214
|
+
{isPending ? (
|
|
215
|
+
<>
|
|
216
|
+
<TableActionButton
|
|
217
|
+
variant="approve"
|
|
218
|
+
icon={<TableMenuCheckIcon />}
|
|
219
|
+
onClick={fn()}
|
|
220
|
+
>
|
|
221
|
+
Հաստատել
|
|
222
|
+
</TableActionButton>
|
|
223
|
+
<TableActionButton
|
|
224
|
+
variant="reject"
|
|
225
|
+
icon={<TableMenuCloseIcon />}
|
|
226
|
+
onClick={fn()}
|
|
227
|
+
>
|
|
228
|
+
Մերժել
|
|
229
|
+
</TableActionButton>
|
|
230
|
+
</>
|
|
231
|
+
) : (
|
|
232
|
+
<TableActionsMenu
|
|
233
|
+
triggerAriaLabel="Գործողություններ"
|
|
234
|
+
items={menuItems}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
</TableActionsCell>
|
|
238
|
+
</TableRow>
|
|
239
|
+
);
|
|
240
|
+
})}
|
|
241
|
+
</TableBody>
|
|
242
|
+
</Table>
|
|
243
|
+
</DataTable>
|
|
244
|
+
</div>
|
|
245
|
+
</WebLayoutCanvas>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const WithPagination: Story = {
|
|
250
|
+
render: () => <RequestsTableStory />,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export const RequestsOverview: Story = {
|
|
254
|
+
...WithPagination,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const buildingTemplates = [
|
|
258
|
+
{
|
|
259
|
+
name: '«Էլիտ Հոլդինգ»',
|
|
260
|
+
address: 'ք. Երևան, Աբովյան 12, շենք 3',
|
|
261
|
+
apartments: 48,
|
|
262
|
+
plan: 'Premium',
|
|
263
|
+
monthlyPrice: '119,900',
|
|
264
|
+
discount: '5%',
|
|
265
|
+
totalMonthly: '113,905',
|
|
266
|
+
paymentStatus: { label: 'Վճարված', tone: 'success' as const },
|
|
267
|
+
status: { label: 'Ակտիվ', tone: 'success' as const },
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: '«Անի-1» Կառավարչություն',
|
|
271
|
+
address: 'ք. Երևան, Մաշտոցի պող. 45, բն. 1-120',
|
|
272
|
+
apartments: 120,
|
|
273
|
+
plan: 'Standard',
|
|
274
|
+
monthlyPrice: '71,910',
|
|
275
|
+
discount: '—',
|
|
276
|
+
totalMonthly: '71,910',
|
|
277
|
+
paymentStatus: { label: 'Հետաձգված', tone: 'danger' as const },
|
|
278
|
+
status: { label: 'Կասեցված', tone: 'neutral' as const },
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: '«Արագած» Համատիրություն',
|
|
282
|
+
address: 'ք. Գյումրի, Անկախության 8',
|
|
283
|
+
apartments: 32,
|
|
284
|
+
plan: 'Basic',
|
|
285
|
+
monthlyPrice: '42,415',
|
|
286
|
+
discount: '10%',
|
|
287
|
+
totalMonthly: '38,174',
|
|
288
|
+
paymentStatus: { label: 'Սպասում է', tone: 'warning' as const },
|
|
289
|
+
status: { label: 'Ակտիվ', tone: 'success' as const },
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: '«Մաքուր տուն» ՍՊԸ',
|
|
293
|
+
address: 'ք. Երևան, Կոմիտասի պող. 28, շենք 7, մուտք 2',
|
|
294
|
+
apartments: 64,
|
|
295
|
+
plan: 'Premium',
|
|
296
|
+
monthlyPrice: '95,200',
|
|
297
|
+
discount: '—',
|
|
298
|
+
totalMonthly: '95,200',
|
|
299
|
+
paymentStatus: { label: 'Վճարված', tone: 'success' as const },
|
|
300
|
+
status: { label: 'Ակտիվ', tone: 'success' as const },
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const largeTableRows = Array.from({ length: 48 }, (_, index) => {
|
|
305
|
+
const template = buildingTemplates[index % buildingTemplates.length];
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
id: String(index + 1),
|
|
309
|
+
...template,
|
|
310
|
+
name:
|
|
311
|
+
index < buildingTemplates.length
|
|
312
|
+
? template.name
|
|
313
|
+
: `${template.name} ${Math.floor(index / buildingTemplates.length) + 1}`,
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const largeTableMenuItems = [
|
|
318
|
+
{
|
|
319
|
+
id: 'edit',
|
|
320
|
+
label: 'Խմբագրել',
|
|
321
|
+
icon: <TableMenuInfoIcon />,
|
|
322
|
+
onSelect: fn(),
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
function LargeScrollableTableStory() {
|
|
327
|
+
const [apartmentsSort, setApartmentsSort] = useState<TableSortDirection | null>(null);
|
|
328
|
+
|
|
329
|
+
const sortedRows = useMemo(() => {
|
|
330
|
+
if (!apartmentsSort) {
|
|
331
|
+
return largeTableRows;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return [...largeTableRows].sort((left, right) => {
|
|
335
|
+
const difference = left.apartments - right.apartments;
|
|
336
|
+
|
|
337
|
+
return apartmentsSort === 'asc' ? difference : -difference;
|
|
338
|
+
});
|
|
339
|
+
}, [apartmentsSort]);
|
|
340
|
+
|
|
341
|
+
const handleApartmentsSort = () => {
|
|
342
|
+
setApartmentsSort((current) => {
|
|
343
|
+
if (current === null) {
|
|
344
|
+
return 'asc';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (current === 'asc') {
|
|
348
|
+
return 'desc';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null;
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<WebLayoutCanvas className="flex h-screen flex-col">
|
|
357
|
+
<div className="mx-auto flex min-h-0 w-[90%] flex-1 flex-col py-8">
|
|
358
|
+
<DataTable fillHeight minBodyHeight={0} className="min-h-0 flex-1" minWidth={1480}>
|
|
359
|
+
<Table>
|
|
360
|
+
<TableHeader>
|
|
361
|
+
<TableRow className="hover:bg-transparent">
|
|
362
|
+
<TableHead className="min-w-48">Կազմակերպություն</TableHead>
|
|
363
|
+
<TableHead className="min-w-64">Հասցե</TableHead>
|
|
364
|
+
<TableHead
|
|
365
|
+
className="min-w-28"
|
|
366
|
+
sortable
|
|
367
|
+
sortDirection={apartmentsSort}
|
|
368
|
+
onSort={handleApartmentsSort}
|
|
369
|
+
>
|
|
370
|
+
Բնակարանների քանակ
|
|
371
|
+
</TableHead>
|
|
372
|
+
<TableHead className="min-w-32">Պլան</TableHead>
|
|
373
|
+
<TableHead className="min-w-36">Ամսական գին</TableHead>
|
|
374
|
+
<TableHead className="min-w-28">Զեղչ</TableHead>
|
|
375
|
+
<TableHead className="min-w-40">Ընդամենը ամսական</TableHead>
|
|
376
|
+
<TableHead className="min-w-40">Վճարման կարգավիճակ</TableHead>
|
|
377
|
+
<TableHead className="min-w-32">Կարգավիճակ</TableHead>
|
|
378
|
+
<TableHead stickyRight className="w-px p-0" aria-hidden />
|
|
379
|
+
</TableRow>
|
|
380
|
+
</TableHeader>
|
|
381
|
+
<TableBody>
|
|
382
|
+
{sortedRows.map((row, index) => (
|
|
383
|
+
<TableRow key={row.id} disabled={index % 5 === 1}>
|
|
384
|
+
<TableCell variant="emphasis">{row.name}</TableCell>
|
|
385
|
+
<TableCell variant="default">{row.address}</TableCell>
|
|
386
|
+
<TableCell>{row.apartments}</TableCell>
|
|
387
|
+
<TableCell>{row.plan}</TableCell>
|
|
388
|
+
<TableCell>{row.monthlyPrice} ֏</TableCell>
|
|
389
|
+
<TableCell variant="muted">{row.discount}</TableCell>
|
|
390
|
+
<TableCell variant="strong">{row.totalMonthly} ֏</TableCell>
|
|
391
|
+
<TableCell>
|
|
392
|
+
<StatusBadge tone={row.paymentStatus.tone}>{row.paymentStatus.label}</StatusBadge>
|
|
393
|
+
</TableCell>
|
|
394
|
+
<TableCell>
|
|
395
|
+
<StatusBadge tone={row.status.tone}>{row.status.label}</StatusBadge>
|
|
396
|
+
</TableCell>
|
|
397
|
+
<TableActionsCell>
|
|
398
|
+
<TableActionsMenu
|
|
399
|
+
triggerAriaLabel="Գործողություններ"
|
|
400
|
+
items={largeTableMenuItems}
|
|
401
|
+
/>
|
|
402
|
+
</TableActionsCell>
|
|
403
|
+
</TableRow>
|
|
404
|
+
))}
|
|
405
|
+
</TableBody>
|
|
406
|
+
</Table>
|
|
407
|
+
</DataTable>
|
|
408
|
+
</div>
|
|
409
|
+
</WebLayoutCanvas>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export const LargeScrollableTable: Story = {
|
|
414
|
+
render: () => <LargeScrollableTableStory />,
|
|
415
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../utils/cn';
|
|
4
|
+
|
|
5
|
+
export type TableActionButtonVariant = 'approve' | 'reject' | 'menu';
|
|
6
|
+
|
|
7
|
+
export type TableActionButtonProps = {
|
|
8
|
+
variant: TableActionButtonVariant;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
ariaLabel?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
} & Pick<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'aria-expanded' | 'aria-haspopup' | 'aria-controls'>;
|
|
15
|
+
|
|
16
|
+
const actionButtonClassName =
|
|
17
|
+
'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
|
+
|
|
19
|
+
const variantClasses: Record<TableActionButtonVariant, string> = {
|
|
20
|
+
approve: actionButtonClassName,
|
|
21
|
+
reject: actionButtonClassName,
|
|
22
|
+
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
|
+
};
|
|
24
|
+
|
|
25
|
+
const inlineActionIconClassName =
|
|
26
|
+
'inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-4';
|
|
27
|
+
|
|
28
|
+
export const TableActionButton = forwardRef<HTMLButtonElement, TableActionButtonProps>(
|
|
29
|
+
function TableActionButton(
|
|
30
|
+
{
|
|
31
|
+
variant,
|
|
32
|
+
children,
|
|
33
|
+
icon,
|
|
34
|
+
onClick,
|
|
35
|
+
ariaLabel,
|
|
36
|
+
className,
|
|
37
|
+
disabled = false,
|
|
38
|
+
...props
|
|
39
|
+
},
|
|
40
|
+
ref,
|
|
41
|
+
) {
|
|
42
|
+
const isInlineAction = variant === 'approve' || variant === 'reject';
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
ref={ref}
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={onClick}
|
|
49
|
+
disabled={disabled}
|
|
50
|
+
aria-label={ariaLabel}
|
|
51
|
+
className={cn(
|
|
52
|
+
variantClasses[variant],
|
|
53
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{icon ? (
|
|
59
|
+
<span className={isInlineAction ? inlineActionIconClassName : 'inline-flex shrink-0'}>
|
|
60
|
+
{icon}
|
|
61
|
+
</span>
|
|
62
|
+
) : null}
|
|
63
|
+
{children}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
);
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
|
|
5
|
+
import { useOnClickOutside } from '../hooks/useOnClickOutside';
|
|
6
|
+
import { TableMenuCheckIcon } from '../icons/TableMenuCheckIcon';
|
|
7
|
+
import { TableMenuCloseIcon } from '../icons/TableMenuCloseIcon';
|
|
8
|
+
import { TableMoreIcon } from '../icons/TableMoreIcon';
|
|
9
|
+
import { cn } from '../utils/cn';
|
|
10
|
+
import { TableActionButton } from './TableActionButton';
|
|
11
|
+
|
|
12
|
+
const MENU_WIDTH_PX = 231;
|
|
13
|
+
const MENU_OFFSET_PX = 4;
|
|
14
|
+
const MENU_VIEWPORT_MARGIN_PX = 8;
|
|
15
|
+
|
|
16
|
+
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
17
|
+
|
|
18
|
+
export type TableActionsMenuItemTone = 'default' | 'success' | 'danger';
|
|
19
|
+
|
|
20
|
+
export type TableActionsMenuItem = {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
icon?: ReactNode;
|
|
24
|
+
onSelect: () => void;
|
|
25
|
+
tone?: TableActionsMenuItemTone;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type TableActionsMenuProps = {
|
|
30
|
+
items: TableActionsMenuItem[];
|
|
31
|
+
triggerAriaLabel: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
menuClassName?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type MenuPosition = {
|
|
37
|
+
top: number;
|
|
38
|
+
left: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function clampMenuLeft(left: number): number {
|
|
42
|
+
if (typeof window === 'undefined') {
|
|
43
|
+
return left;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const minLeft = MENU_VIEWPORT_MARGIN_PX;
|
|
47
|
+
const maxLeft = Math.max(minLeft, window.innerWidth - MENU_WIDTH_PX - MENU_VIEWPORT_MARGIN_PX);
|
|
48
|
+
|
|
49
|
+
return Math.min(Math.max(left, minLeft), maxLeft);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveMenuItemIcon(item: TableActionsMenuItem): ReactNode | null {
|
|
53
|
+
if (item.icon) {
|
|
54
|
+
return item.icon;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
switch (item.tone) {
|
|
58
|
+
case 'success':
|
|
59
|
+
return <TableMenuCheckIcon />;
|
|
60
|
+
case 'danger':
|
|
61
|
+
return <TableMenuCloseIcon />;
|
|
62
|
+
default:
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function TableActionsMenu({
|
|
68
|
+
items,
|
|
69
|
+
triggerAriaLabel,
|
|
70
|
+
className,
|
|
71
|
+
menuClassName,
|
|
72
|
+
}: TableActionsMenuProps) {
|
|
73
|
+
const [open, setOpen] = useState(false);
|
|
74
|
+
const [menuPosition, setMenuPosition] = useState<MenuPosition | null>(null);
|
|
75
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
76
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
77
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
78
|
+
const menuId = useId();
|
|
79
|
+
|
|
80
|
+
const closeMenu = useCallback(() => {
|
|
81
|
+
setOpen(false);
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const updateMenuPosition = useCallback(() => {
|
|
85
|
+
const trigger = triggerRef.current;
|
|
86
|
+
if (!trigger) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rect = trigger.getBoundingClientRect();
|
|
91
|
+
setMenuPosition({
|
|
92
|
+
top: rect.bottom + MENU_OFFSET_PX,
|
|
93
|
+
left: clampMenuLeft(rect.right - MENU_WIDTH_PX),
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
useOnClickOutside([rootRef, menuRef], closeMenu, open);
|
|
98
|
+
|
|
99
|
+
useIsomorphicLayoutEffect(() => {
|
|
100
|
+
if (!open) {
|
|
101
|
+
setMenuPosition(null);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
updateMenuPosition();
|
|
106
|
+
}, [open, updateMenuPosition]);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!open) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const closeOnScroll = () => {
|
|
114
|
+
closeMenu();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
window.addEventListener('resize', updateMenuPosition);
|
|
118
|
+
window.addEventListener('scroll', closeOnScroll, true);
|
|
119
|
+
|
|
120
|
+
const closeOnEscape = (event: KeyboardEvent) => {
|
|
121
|
+
if (event.key === 'Escape') {
|
|
122
|
+
closeMenu();
|
|
123
|
+
triggerRef.current?.focus();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
document.addEventListener('keydown', closeOnEscape);
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
window.removeEventListener('resize', updateMenuPosition);
|
|
131
|
+
window.removeEventListener('scroll', closeOnScroll, true);
|
|
132
|
+
document.removeEventListener('keydown', closeOnEscape);
|
|
133
|
+
};
|
|
134
|
+
}, [closeMenu, open, updateMenuPosition]);
|
|
135
|
+
|
|
136
|
+
if (items.length === 0) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const menu =
|
|
141
|
+
open && menuPosition != null ? (
|
|
142
|
+
<div
|
|
143
|
+
ref={menuRef}
|
|
144
|
+
id={menuId}
|
|
145
|
+
role="menu"
|
|
146
|
+
style={{
|
|
147
|
+
position: 'fixed',
|
|
148
|
+
top: menuPosition.top,
|
|
149
|
+
left: menuPosition.left,
|
|
150
|
+
width: MENU_WIDTH_PX,
|
|
151
|
+
}}
|
|
152
|
+
className={cn(
|
|
153
|
+
'z-50 overflow-hidden rounded-2xl border border-storm-gray-50 bg-white shadow-[0_4px_20px_0_rgba(0,0,0,0.05)]',
|
|
154
|
+
menuClassName,
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
{items.map((item, index) => {
|
|
158
|
+
const icon = resolveMenuItemIcon(item);
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<button
|
|
162
|
+
key={item.id}
|
|
163
|
+
type="button"
|
|
164
|
+
role="menuitem"
|
|
165
|
+
disabled={item.disabled}
|
|
166
|
+
onClick={() => {
|
|
167
|
+
if (item.disabled) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
closeMenu();
|
|
172
|
+
item.onSelect();
|
|
173
|
+
}}
|
|
174
|
+
className={cn(
|
|
175
|
+
'flex w-full cursor-pointer items-center gap-2.5 px-3 py-4 text-left font-sans text-sm font-medium leading-none text-storm-gray-900 transition-colors hover:bg-storm-gray-0',
|
|
176
|
+
index > 0 && 'border-t border-storm-gray-50',
|
|
177
|
+
item.disabled && 'cursor-not-allowed opacity-50',
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
{icon ? (
|
|
181
|
+
<span className="inline-flex size-5 shrink-0 items-center justify-center">
|
|
182
|
+
{icon}
|
|
183
|
+
</span>
|
|
184
|
+
) : null}
|
|
185
|
+
<span>{item.label}</span>
|
|
186
|
+
</button>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
</div>
|
|
190
|
+
) : null;
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div ref={rootRef} className={cn('relative', className)}>
|
|
194
|
+
<TableActionButton
|
|
195
|
+
ref={triggerRef}
|
|
196
|
+
variant="menu"
|
|
197
|
+
icon={<TableMoreIcon className="size-6" />}
|
|
198
|
+
ariaLabel={triggerAriaLabel}
|
|
199
|
+
onClick={() => setOpen((current) => !current)}
|
|
200
|
+
aria-expanded={open}
|
|
201
|
+
aria-haspopup="menu"
|
|
202
|
+
aria-controls={menuId}
|
|
203
|
+
/>
|
|
204
|
+
|
|
205
|
+
{typeof document !== 'undefined' && menu != null ? createPortal(menu, document.body) : null}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|