@alepha/ui 0.11.3 → 0.11.5

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 (42) hide show
  1. package/dist/AlephaMantineProvider-Ba88lMeq.js +3 -0
  2. package/dist/AlephaMantineProvider-Be0DAazb.js +150 -0
  3. package/dist/AlephaMantineProvider-Be0DAazb.js.map +1 -0
  4. package/dist/index.d.ts +289 -225
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +650 -729
  7. package/dist/index.js.map +1 -1
  8. package/package.json +14 -12
  9. package/src/RootRouter.ts +1 -1
  10. package/src/components/buttons/ActionButton.tsx +542 -0
  11. package/src/components/buttons/BurgerButton.tsx +20 -0
  12. package/src/components/{DarkModeButton.tsx → buttons/DarkModeButton.tsx} +27 -14
  13. package/src/components/buttons/LanguageButton.tsx +28 -0
  14. package/src/components/buttons/OmnibarButton.tsx +32 -0
  15. package/src/components/buttons/ToggleSidebarButton.tsx +28 -0
  16. package/src/components/dialogs/AlertDialog.tsx +10 -10
  17. package/src/components/dialogs/ConfirmDialog.tsx +18 -18
  18. package/src/components/dialogs/PromptDialog.tsx +5 -3
  19. package/src/components/{Control.tsx → form/Control.tsx} +6 -3
  20. package/src/components/{ControlDate.tsx → form/ControlDate.tsx} +4 -1
  21. package/src/components/{ControlSelect.tsx → form/ControlSelect.tsx} +4 -1
  22. package/src/components/{TypeForm.tsx → form/TypeForm.tsx} +8 -6
  23. package/src/components/layout/AdminShell.tsx +97 -0
  24. package/src/components/{AlephaMantineProvider.tsx → layout/AlephaMantineProvider.tsx} +30 -10
  25. package/src/components/layout/AppBar.tsx +133 -0
  26. package/src/components/layout/Omnibar.tsx +43 -0
  27. package/src/components/layout/Sidebar.tsx +410 -0
  28. package/src/components/table/DataTable.tsx +63 -0
  29. package/src/constants/ui.ts +8 -0
  30. package/src/index.ts +89 -24
  31. package/src/services/DialogService.tsx +13 -32
  32. package/src/services/ToastService.tsx +16 -4
  33. package/src/utils/parseInput.ts +1 -1
  34. package/dist/AlephaMantineProvider-DDbIijPF.js +0 -96
  35. package/dist/AlephaMantineProvider-DDbIijPF.js.map +0 -1
  36. package/dist/AlephaMantineProvider-pOu8hOzK.js +0 -3
  37. package/src/components/Action.tsx +0 -345
  38. package/src/components/DataTable.css +0 -199
  39. package/src/components/DataTable.tsx +0 -724
  40. package/src/components/Omnibar.tsx +0 -77
  41. package/src/components/Sidebar.css +0 -217
  42. package/src/components/Sidebar.tsx +0 -255
@@ -1,724 +0,0 @@
1
- import {
2
- ActionIcon,
3
- Badge,
4
- Box,
5
- Center,
6
- Checkbox,
7
- Flex,
8
- Group,
9
- Loader,
10
- Menu,
11
- Pagination,
12
- Paper,
13
- ScrollArea,
14
- Select,
15
- Table,
16
- type TableProps,
17
- Text,
18
- TextInput,
19
- Tooltip,
20
- } from "@mantine/core";
21
- import {
22
- IconChevronDown,
23
- IconChevronUp,
24
- IconColumns,
25
- IconDownload,
26
- IconRefresh,
27
- IconSearch,
28
- IconX,
29
- } from "@tabler/icons-react";
30
- import type React from "react";
31
- import { useCallback, useMemo, useState } from "react";
32
-
33
- // Types
34
- export type SortDirection = "asc" | "desc" | null;
35
-
36
- export interface DataTableColumn<T = any> {
37
- accessor: keyof T | string;
38
- title?: string;
39
- width?: number | string;
40
- sortable?: boolean;
41
- filterable?: boolean;
42
- hidden?: boolean;
43
- render?: (value: any, record: T, index: number) => React.ReactNode;
44
- renderHeader?: () => React.ReactNode;
45
- align?: "left" | "center" | "right";
46
- ellipsis?: boolean;
47
- className?: string;
48
- headerClassName?: string;
49
- }
50
-
51
- export interface DataTableSort {
52
- column: string;
53
- direction: SortDirection;
54
- }
55
-
56
- export interface DataTableFilter {
57
- column: string;
58
- value: string;
59
- operator?: "contains" | "equals" | "startsWith" | "endsWith";
60
- }
61
-
62
- export interface DataTableProps<T = any> extends Omit<TableProps, "data"> {
63
- // Data
64
- data: T[];
65
- columns: DataTableColumn<T>[];
66
- loading?: boolean;
67
- emptyMessage?: string;
68
-
69
- // Selection
70
- selectable?: boolean;
71
- selectedRows?: T[];
72
- onRowSelect?: (rows: T[]) => void;
73
-
74
- // Sorting
75
- sortable?: boolean;
76
- sort?: DataTableSort;
77
- onSortChange?: (sort: DataTableSort) => void;
78
-
79
- // Filtering
80
- filterable?: boolean;
81
- filters?: DataTableFilter[];
82
- onFiltersChange?: (filters: DataTableFilter[]) => void;
83
- filterPlaceholder?: string;
84
-
85
- // Pagination
86
- paginate?: boolean;
87
- page?: number;
88
- pageSize?: number;
89
- totalRecords?: number;
90
- pageSizeOptions?: number[];
91
- onPageChange?: (page: number) => void;
92
- onPageSizeChange?: (size: number) => void;
93
-
94
- // Row actions
95
- rowActions?: (record: T, index: number) => React.ReactNode;
96
- onRowClick?: (record: T, index: number) => void;
97
- rowClassName?: (record: T, index: number) => string;
98
-
99
- // Features
100
- showHeader?: boolean;
101
- showFooter?: boolean;
102
- stickyHeader?: boolean;
103
- striped?: boolean;
104
- highlightOnHover?: boolean;
105
-
106
- // Toolbar
107
- showToolbar?: boolean;
108
- title?: string;
109
- actions?: React.ReactNode;
110
- showColumnToggle?: boolean;
111
- showRefresh?: boolean;
112
- onRefresh?: () => void;
113
- showExport?: boolean;
114
- onExport?: () => void;
115
-
116
- // Layout
117
- height?: number | string;
118
- minHeight?: number | string;
119
- maxHeight?: number | string;
120
- }
121
-
122
- function getNestedValue(obj: any, path: string): any {
123
- return path.split(".").reduce((acc, part) => acc?.[part], obj);
124
- }
125
-
126
- export default function DataTable<T = any>({
127
- data = [],
128
- columns: initialColumns = [],
129
- loading = false,
130
- emptyMessage = "No data available",
131
-
132
- selectable = false,
133
- selectedRows = [],
134
- onRowSelect,
135
-
136
- sortable = false,
137
- sort,
138
- onSortChange,
139
-
140
- filterable = false,
141
- filters = [],
142
- onFiltersChange,
143
- filterPlaceholder = "Search...",
144
-
145
- paginate = false,
146
- page = 1,
147
- pageSize = 10,
148
- totalRecords,
149
- pageSizeOptions = [10, 25, 50, 100],
150
- onPageChange,
151
- onPageSizeChange,
152
-
153
- rowActions,
154
- onRowClick,
155
- rowClassName,
156
-
157
- showHeader = true,
158
- showFooter = true,
159
- stickyHeader = false,
160
- striped = false,
161
- highlightOnHover = true,
162
-
163
- showToolbar = true,
164
- title,
165
- actions,
166
- showColumnToggle = true,
167
- showRefresh = false,
168
- onRefresh,
169
- showExport = false,
170
- onExport,
171
-
172
- height,
173
- minHeight,
174
- maxHeight,
175
-
176
- ...tableProps
177
- }: DataTableProps<T>) {
178
- // State management - use controlled/uncontrolled pattern
179
- const [hiddenColumns, setHiddenColumns] = useState<Set<string>>(new Set());
180
- const [globalFilter, setGlobalFilter] = useState("");
181
-
182
- // Use props directly if provided, otherwise use internal state
183
- const [internalPage, setInternalPage] = useState(page);
184
- const [internalPageSize, setInternalPageSize] = useState(pageSize);
185
- const [internalSort, setInternalSort] = useState<DataTableSort | undefined>(
186
- sort,
187
- );
188
- const [internalFilters, setInternalFilters] =
189
- useState<DataTableFilter[]>(filters);
190
- const [internalSelectedRows, setInternalSelectedRows] =
191
- useState<T[]>(selectedRows);
192
-
193
- // Determine if component is controlled
194
- const currentPage = onPageChange ? page : internalPage;
195
- const currentPageSize = onPageSizeChange ? pageSize : internalPageSize;
196
- const currentSort = onSortChange ? sort : internalSort;
197
- const currentFilters = onFiltersChange ? filters : internalFilters;
198
- const currentSelectedRows = onRowSelect ? selectedRows : internalSelectedRows;
199
-
200
- // Filter columns
201
- const visibleColumns = useMemo(
202
- () =>
203
- initialColumns.filter(
204
- (col) => !col.hidden && !hiddenColumns.has(String(col.accessor)),
205
- ),
206
- [initialColumns, hiddenColumns],
207
- );
208
-
209
- // Process data
210
- const processedData = useMemo(() => {
211
- let result = [...data];
212
-
213
- // Apply global filter
214
- if (filterable && globalFilter) {
215
- result = result.filter((row) => {
216
- return visibleColumns.some((col) => {
217
- const value = getNestedValue(row, String(col.accessor));
218
- return String(value)
219
- .toLowerCase()
220
- .includes(globalFilter.toLowerCase());
221
- });
222
- });
223
- }
224
-
225
- // Apply column filters
226
- if (filterable && currentFilters.length > 0) {
227
- result = result.filter((row) => {
228
- return currentFilters.every((filter) => {
229
- const value = String(
230
- getNestedValue(row, filter.column),
231
- ).toLowerCase();
232
- const filterValue = filter.value.toLowerCase();
233
-
234
- switch (filter.operator) {
235
- case "equals":
236
- return value === filterValue;
237
- case "startsWith":
238
- return value.startsWith(filterValue);
239
- case "endsWith":
240
- return value.endsWith(filterValue);
241
- default:
242
- return value.includes(filterValue);
243
- }
244
- });
245
- });
246
- }
247
-
248
- // Apply sorting
249
- if (sortable && currentSort?.direction) {
250
- result.sort((a, b) => {
251
- const aVal = getNestedValue(a, currentSort.column);
252
- const bVal = getNestedValue(b, currentSort.column);
253
-
254
- if (aVal === bVal) return 0;
255
- if (aVal === null || aVal === undefined) return 1;
256
- if (bVal === null || bVal === undefined) return -1;
257
-
258
- const comparison = aVal < bVal ? -1 : 1;
259
- return currentSort.direction === "asc" ? comparison : -comparison;
260
- });
261
- }
262
-
263
- return result;
264
- }, [
265
- data,
266
- visibleColumns,
267
- filterable,
268
- globalFilter,
269
- currentFilters,
270
- sortable,
271
- currentSort,
272
- ]);
273
-
274
- // Paginate data
275
- const paginatedData = useMemo(() => {
276
- if (!paginate) return processedData;
277
-
278
- const startIndex = (currentPage - 1) * currentPageSize;
279
- return processedData.slice(startIndex, startIndex + currentPageSize);
280
- }, [processedData, paginate, currentPage, currentPageSize]);
281
-
282
- // Total pages
283
- const totalPages = useMemo(() => {
284
- const total = totalRecords ?? processedData.length;
285
- return Math.ceil(total / currentPageSize);
286
- }, [totalRecords, processedData.length, currentPageSize]);
287
-
288
- // Handlers
289
- const handleSort = useCallback(
290
- (column: string) => {
291
- if (!sortable) return;
292
-
293
- const newSort: DataTableSort = {
294
- column,
295
- direction:
296
- currentSort?.column === column
297
- ? currentSort.direction === "asc"
298
- ? "desc"
299
- : currentSort.direction === "desc"
300
- ? null
301
- : "asc"
302
- : "asc",
303
- };
304
-
305
- if (onSortChange) {
306
- onSortChange(newSort);
307
- } else {
308
- setInternalSort(newSort);
309
- }
310
- },
311
- [sortable, currentSort, onSortChange],
312
- );
313
-
314
- const handlePageChange = useCallback(
315
- (newPage: number) => {
316
- if (onPageChange) {
317
- onPageChange(newPage);
318
- } else {
319
- setInternalPage(newPage);
320
- }
321
- },
322
- [onPageChange],
323
- );
324
-
325
- const handlePageSizeChange = useCallback(
326
- (size: string | null) => {
327
- const newSize = Number(size) || currentPageSize;
328
-
329
- if (onPageSizeChange) {
330
- onPageSizeChange(newSize);
331
- onPageChange?.(1);
332
- } else {
333
- setInternalPageSize(newSize);
334
- setInternalPage(1);
335
- }
336
- },
337
- [currentPageSize, onPageSizeChange, onPageChange],
338
- );
339
-
340
- const handleSelectAll = useCallback(
341
- (checked: boolean) => {
342
- const newSelection = checked ? paginatedData : [];
343
-
344
- if (onRowSelect) {
345
- onRowSelect(newSelection);
346
- } else {
347
- setInternalSelectedRows(newSelection);
348
- }
349
- },
350
- [paginatedData, onRowSelect],
351
- );
352
-
353
- const handleSelectRow = useCallback(
354
- (row: T, checked: boolean) => {
355
- const newSelection = checked
356
- ? [...currentSelectedRows, row]
357
- : currentSelectedRows.filter((r) => r !== row);
358
-
359
- if (onRowSelect) {
360
- onRowSelect(newSelection);
361
- } else {
362
- setInternalSelectedRows(newSelection);
363
- }
364
- },
365
- [currentSelectedRows, onRowSelect],
366
- );
367
-
368
- const handleColumnToggle = useCallback(
369
- (column: string) => {
370
- const newHidden = new Set(hiddenColumns);
371
- if (newHidden.has(column)) {
372
- newHidden.delete(column);
373
- } else {
374
- newHidden.add(column);
375
- }
376
- setHiddenColumns(newHidden);
377
- },
378
- [hiddenColumns],
379
- );
380
-
381
- const isAllSelected =
382
- currentSelectedRows.length > 0 &&
383
- currentSelectedRows.length === paginatedData.length;
384
- const isIndeterminate =
385
- currentSelectedRows.length > 0 &&
386
- currentSelectedRows.length < paginatedData.length;
387
-
388
- // Render sort icon
389
- const renderSortIcon = (column: string) => {
390
- if (!sortable) return null;
391
-
392
- if (currentSort?.column === column) {
393
- if (currentSort.direction === "asc") {
394
- return (
395
- <IconChevronUp className="alepha-datatable-sort-icon" size={16} />
396
- );
397
- }
398
- if (currentSort.direction === "desc") {
399
- return (
400
- <IconChevronDown className="alepha-datatable-sort-icon" size={16} />
401
- );
402
- }
403
- }
404
-
405
- return (
406
- <IconChevronUp
407
- className="alepha-datatable-sort-icon-inactive"
408
- size={16}
409
- />
410
- );
411
- };
412
-
413
- // Render toolbar
414
- const toolbar = showToolbar &&
415
- (title ||
416
- actions ||
417
- filterable ||
418
- showColumnToggle ||
419
- showRefresh ||
420
- showExport) && (
421
- <Paper className="alepha-datatable-toolbar" p="md" mb="sm">
422
- <Flex justify="space-between" align="center" gap="md">
423
- <Group>
424
- {title && (
425
- <Text size="lg" fw={600}>
426
- {title}
427
- </Text>
428
- )}
429
- {currentSelectedRows.length > 0 && (
430
- <Badge color="blue" variant="light">
431
- {currentSelectedRows.length} selected
432
- </Badge>
433
- )}
434
- </Group>
435
-
436
- <Group>
437
- {filterable && (
438
- <TextInput
439
- placeholder={filterPlaceholder}
440
- value={globalFilter}
441
- onChange={(e) => setGlobalFilter(e.target.value)}
442
- leftSection={<IconSearch size={16} />}
443
- rightSection={
444
- globalFilter && (
445
- <ActionIcon
446
- size="xs"
447
- variant="subtle"
448
- onClick={() => setGlobalFilter("")}
449
- >
450
- <IconX size={14} />
451
- </ActionIcon>
452
- )
453
- }
454
- className="alepha-datatable-search-input"
455
- />
456
- )}
457
-
458
- {showColumnToggle && (
459
- <Menu position="bottom-end">
460
- <Menu.Target>
461
- <Tooltip label="Toggle columns">
462
- <ActionIcon variant="subtle">
463
- <IconColumns size={20} />
464
- </ActionIcon>
465
- </Tooltip>
466
- </Menu.Target>
467
- <Menu.Dropdown>
468
- <Menu.Label>Visible columns</Menu.Label>
469
- {initialColumns.map((col) => (
470
- <Menu.Item
471
- key={String(col.accessor)}
472
- onClick={() => handleColumnToggle(String(col.accessor))}
473
- leftSection={
474
- <Checkbox
475
- checked={
476
- !hiddenColumns.has(String(col.accessor)) &&
477
- !col.hidden
478
- }
479
- readOnly
480
- size="xs"
481
- />
482
- }
483
- >
484
- {col.title || String(col.accessor)}
485
- </Menu.Item>
486
- ))}
487
- </Menu.Dropdown>
488
- </Menu>
489
- )}
490
-
491
- {showRefresh && (
492
- <Tooltip label="Refresh">
493
- <ActionIcon
494
- variant="subtle"
495
- onClick={onRefresh}
496
- loading={loading}
497
- >
498
- <IconRefresh size={20} />
499
- </ActionIcon>
500
- </Tooltip>
501
- )}
502
-
503
- {showExport && (
504
- <Tooltip label="Export">
505
- <ActionIcon variant="subtle" onClick={onExport}>
506
- <IconDownload size={20} />
507
- </ActionIcon>
508
- </Tooltip>
509
- )}
510
-
511
- {actions}
512
- </Group>
513
- </Flex>
514
- </Paper>
515
- );
516
-
517
- // Render table
518
- const tableContent = (
519
- <Table
520
- striped={striped}
521
- highlightOnHover={highlightOnHover}
522
- stickyHeader={stickyHeader}
523
- className="alepha-datatable-table"
524
- {...tableProps}
525
- >
526
- {showHeader && (
527
- <Table.Thead>
528
- <Table.Tr>
529
- {selectable && (
530
- <Table.Th className="alepha-datatable-checkbox-column">
531
- <Checkbox
532
- checked={isAllSelected}
533
- indeterminate={isIndeterminate}
534
- onChange={(e) => handleSelectAll(e.currentTarget.checked)}
535
- />
536
- </Table.Th>
537
- )}
538
-
539
- {visibleColumns.map((column) => (
540
- <Table.Th
541
- key={String(column.accessor)}
542
- className={`alepha-datatable-th ${column.headerClassName || ""}`}
543
- style={{
544
- width: column.width,
545
- textAlign: column.align,
546
- cursor: column.sortable && sortable ? "pointer" : "default",
547
- }}
548
- onClick={() =>
549
- column.sortable && handleSort(String(column.accessor))
550
- }
551
- >
552
- <Group
553
- gap="xs"
554
- justify={
555
- column.align === "center"
556
- ? "center"
557
- : column.align === "right"
558
- ? "flex-end"
559
- : "flex-start"
560
- }
561
- >
562
- {column.renderHeader
563
- ? column.renderHeader()
564
- : column.title || String(column.accessor)}
565
- {column.sortable && renderSortIcon(String(column.accessor))}
566
- </Group>
567
- </Table.Th>
568
- ))}
569
-
570
- {rowActions && (
571
- <Table.Th className="alepha-datatable-actions-column">
572
- Actions
573
- </Table.Th>
574
- )}
575
- </Table.Tr>
576
- </Table.Thead>
577
- )}
578
-
579
- <Table.Tbody>
580
- {loading ? (
581
- <Table.Tr>
582
- <Table.Td
583
- colSpan={
584
- visibleColumns.length +
585
- (selectable ? 1 : 0) +
586
- (rowActions ? 1 : 0)
587
- }
588
- >
589
- <Center py="xl">
590
- <Loader size="sm" />
591
- </Center>
592
- </Table.Td>
593
- </Table.Tr>
594
- ) : paginatedData.length === 0 ? (
595
- <Table.Tr>
596
- <Table.Td
597
- colSpan={
598
- visibleColumns.length +
599
- (selectable ? 1 : 0) +
600
- (rowActions ? 1 : 0)
601
- }
602
- >
603
- <Center py="xl">
604
- <Text c="dimmed">{emptyMessage}</Text>
605
- </Center>
606
- </Table.Td>
607
- </Table.Tr>
608
- ) : (
609
- paginatedData.map((row, index) => {
610
- const isSelected = currentSelectedRows.includes(row);
611
- return (
612
- <Table.Tr
613
- key={index}
614
- className={`alepha-datatable-tr ${isSelected ? "alepha-datatable-selected" : ""} ${rowClassName?.(row, index) || ""}`}
615
- onClick={() => onRowClick?.(row, index)}
616
- style={{ cursor: onRowClick ? "pointer" : "default" }}
617
- >
618
- {selectable && (
619
- <Table.Td className="alepha-datatable-checkbox-column">
620
- <Checkbox
621
- checked={isSelected}
622
- onChange={(e) =>
623
- handleSelectRow(row, e.currentTarget.checked)
624
- }
625
- onClick={(e) => e.stopPropagation()}
626
- />
627
- </Table.Td>
628
- )}
629
-
630
- {visibleColumns.map((column) => {
631
- const value = getNestedValue(row, String(column.accessor));
632
- return (
633
- <Table.Td
634
- key={String(column.accessor)}
635
- className={column.className}
636
- style={{
637
- textAlign: column.align,
638
- ...(column.ellipsis && {
639
- maxWidth: column.width,
640
- overflow: "hidden",
641
- textOverflow: "ellipsis",
642
- whiteSpace: "nowrap",
643
- }),
644
- }}
645
- >
646
- {column.render ? column.render(value, row, index) : value}
647
- </Table.Td>
648
- );
649
- })}
650
-
651
- {rowActions && (
652
- <Table.Td className="alepha-datatable-actions-column">
653
- {rowActions(row, index)}
654
- </Table.Td>
655
- )}
656
- </Table.Tr>
657
- );
658
- })
659
- )}
660
- </Table.Tbody>
661
-
662
- {showFooter && paginate && (
663
- <Table.Tfoot>
664
- <Table.Tr>
665
- <Table.Td
666
- colSpan={
667
- visibleColumns.length +
668
- (selectable ? 1 : 0) +
669
- (rowActions ? 1 : 0)
670
- }
671
- >
672
- <Flex justify="space-between" align="center" py="xs">
673
- <Group gap="xs">
674
- <Text size="sm" c="dimmed">
675
- Showing {(currentPage - 1) * currentPageSize + 1} to{" "}
676
- {Math.min(
677
- currentPage * currentPageSize,
678
- totalRecords ?? processedData.length,
679
- )}{" "}
680
- of {totalRecords ?? processedData.length} records
681
- </Text>
682
-
683
- <Select
684
- size="xs"
685
- value={String(currentPageSize)}
686
- onChange={handlePageSizeChange}
687
- data={pageSizeOptions.map((size) => ({
688
- value: String(size),
689
- label: `${size} / page`,
690
- }))}
691
- className="alepha-datatable-page-size-select"
692
- />
693
- </Group>
694
-
695
- <Pagination
696
- size="sm"
697
- value={currentPage}
698
- onChange={handlePageChange}
699
- total={totalPages}
700
- siblings={1}
701
- boundaries={1}
702
- />
703
- </Flex>
704
- </Table.Td>
705
- </Table.Tr>
706
- </Table.Tfoot>
707
- )}
708
- </Table>
709
- );
710
-
711
- return (
712
- <Box className="alepha-datatable-container">
713
- {toolbar}
714
-
715
- {height || maxHeight ? (
716
- <ScrollArea.Autosize mah={maxHeight} h={height} mih={minHeight}>
717
- {tableContent}
718
- </ScrollArea.Autosize>
719
- ) : (
720
- tableContent
721
- )}
722
- </Box>
723
- );
724
- }