@tsiky/components-r19 1.0.0 → 1.1.0

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 (32) hide show
  1. package/index.ts +35 -33
  2. package/package.json +1 -1
  3. package/src/components/Charts/area-chart-admission/AreaChartAdmission.tsx +123 -89
  4. package/src/components/Charts/bar-chart/BarChart.tsx +167 -132
  5. package/src/components/Charts/mixed-chart/MixedChart.tsx +65 -9
  6. package/src/components/Charts/sankey-chart/SankeyChart.tsx +183 -155
  7. package/src/components/Confirmationpopup/ConfirmationPopup.module.css +88 -0
  8. package/src/components/Confirmationpopup/ConfirmationPopup.stories.tsx +94 -0
  9. package/src/components/Confirmationpopup/ConfirmationPopup.tsx +47 -0
  10. package/src/components/Confirmationpopup/index.ts +6 -0
  11. package/src/components/Confirmationpopup/useConfirmationPopup.ts +48 -0
  12. package/src/components/DayStatCard/DayStatCard.tsx +96 -69
  13. package/src/components/DynamicTable/AdvancedFilters.tsx +196 -196
  14. package/src/components/DynamicTable/ColumnSorter.tsx +185 -185
  15. package/src/components/DynamicTable/Pagination.tsx +115 -115
  16. package/src/components/DynamicTable/TableauDynamique.module.css +1287 -1287
  17. package/src/components/DynamicTable/filters/SelectFilter.tsx +69 -69
  18. package/src/components/EntryControl/EntryControl.tsx +117 -117
  19. package/src/components/Grid/Grid.tsx +5 -0
  20. package/src/components/Header/Header.tsx +4 -2
  21. package/src/components/Header/header.css +61 -31
  22. package/src/components/MetricsPanel/MetricsPanel.module.css +688 -636
  23. package/src/components/MetricsPanel/MetricsPanel.tsx +220 -282
  24. package/src/components/MetricsPanel/renderers/CompactRenderer.tsx +148 -125
  25. package/src/components/NavBar/NavBar.tsx +1 -1
  26. package/src/components/SelectFilter/SelectFilter.module.css +249 -0
  27. package/src/components/SelectFilter/SelectFilter.stories.tsx +321 -0
  28. package/src/components/SelectFilter/SelectFilter.tsx +219 -0
  29. package/src/components/SelectFilter/index.ts +2 -0
  30. package/src/components/SelectFilter/types.ts +19 -0
  31. package/src/components/TranslationKey/TranslationKey.tsx +265 -245
  32. package/src/components/TrendList/TrendList.tsx +72 -45
@@ -1,185 +1,185 @@
1
- 'use client';
2
-
3
- import React, { useEffect, useRef, useState } from 'react';
4
- import { ChevronsUpDown, ChevronUp, ChevronDown, X } from 'lucide-react';
5
- import type { SortConfig, TableColumn } from './tools/tableTypes';
6
- import styles from './TableauDynamique.module.css';
7
-
8
- interface ColumnSorterProps<T = unknown> {
9
- columns: TableColumn<T>[];
10
- sortConfig: SortConfig;
11
- onSort: (sortConfig: SortConfig) => void;
12
- onToggleFilters?: () => void;
13
- filtersActive?: boolean;
14
- showFilters?: boolean;
15
- }
16
-
17
- const isAlphaUpperCase = (s: string) => {
18
- const letters = s.replace(/[^A-Za-zÀ-ÖØ-öø-ÿ]/g, '');
19
- return letters.length > 0 && letters === letters.toUpperCase();
20
- };
21
-
22
- const ColumnSorter = <T,>({ columns, sortConfig, onSort }: ColumnSorterProps<T>) => {
23
- const [isOpen, setIsOpen] = useState(false);
24
- const sortableColumns = columns.filter((col) => col.sortable);
25
- const wrapperRef = useRef<HTMLDivElement | null>(null);
26
-
27
- const handleSort = (key: string | null, direction: 'asc' | 'desc') => {
28
- onSort({ key, direction });
29
- setIsOpen(false);
30
- };
31
-
32
- const toggleOpen = () => setIsOpen((v) => !v);
33
-
34
- const handleCancel = () => {
35
- setIsOpen(false);
36
- };
37
-
38
- // fermer au clic extérieur + Échap (déjà en place)
39
- useEffect(() => {
40
- if (!isOpen) return;
41
-
42
- const onPointerDown = (e: PointerEvent) => {
43
- if (!wrapperRef.current) return;
44
- const target = e.target as Node | null;
45
- if (target && !wrapperRef.current.contains(target)) {
46
- setIsOpen(false);
47
- }
48
- };
49
-
50
- const onKeyDown = (e: KeyboardEvent) => {
51
- if (e.key === 'Escape') {
52
- setIsOpen(false);
53
- }
54
- };
55
-
56
- document.addEventListener('pointerdown', onPointerDown);
57
- document.addEventListener('keydown', onKeyDown);
58
-
59
- return () => {
60
- document.removeEventListener('pointerdown', onPointerDown);
61
- document.removeEventListener('keydown', onKeyDown);
62
- };
63
- }, [isOpen]);
64
-
65
- // --> NO SCROLL on the table container while dropdown is open
66
- useEffect(() => {
67
- const tableWrapper = document.querySelector('[data-table-wrapper]') as HTMLElement | null;
68
- if (!tableWrapper) return;
69
-
70
- // sauvegarde la valeur inline actuelle pour restaurer plus tard
71
- const previousInline = tableWrapper.style.overflow ?? '';
72
-
73
- if (isOpen) {
74
- // masquer scrollbars (on cache à la fois X et Y)
75
- tableWrapper.style.overflow = 'hidden';
76
- } else {
77
- // restaurer à la fermeture
78
- tableWrapper.style.overflow = previousInline;
79
- }
80
-
81
- // cleanup si le composant se démonte pendant que c'est ouvert
82
- return () => {
83
- tableWrapper.style.overflow = previousInline;
84
- };
85
- }, [isOpen]);
86
-
87
- return (
88
- <div className={styles.columnSorterContainer}>
89
- <div className={styles.sorterControls}>
90
- {sortableColumns.length > 0 && (
91
- <div className={styles.sortDropdownWrapper} ref={wrapperRef}>
92
- <button
93
- className={styles.toggleSortButton}
94
- onClick={toggleOpen}
95
- aria-expanded={isOpen}
96
- aria-haspopup='true'
97
- type='button'
98
- >
99
- <ChevronsUpDown size={14} />
100
- <span
101
- className={`${styles.buttonTextCompact} ${
102
- isAlphaUpperCase('Sort') ? styles.uppercaseSmall : ''
103
- }`}
104
- >
105
- Sort
106
- </span>
107
- {sortConfig.key &&
108
- (sortConfig.direction === 'asc' ? (
109
- <ChevronUp size={14} />
110
- ) : (
111
- <ChevronDown size={14} />
112
- ))}
113
- </button>
114
-
115
- {isOpen && (
116
- <div className={`${styles.sortDropdown} ${styles.sortDropdownInline}`} role='menu'>
117
- <div className={styles.sortDropdownHeader}>
118
- <span>Trier par:</span>
119
- <button className={styles.closeButton} onClick={handleCancel} aria-label='Fermer'>
120
- <X size={16} />
121
- </button>
122
- </div>
123
-
124
- <div className={styles.sortDropdownContent}>
125
- {sortableColumns.map((column) => {
126
- const label = String(column.label ?? column.id);
127
- const smallUpper = isAlphaUpperCase(label);
128
- return (
129
- <div
130
- key={column.id}
131
- className={styles.sortDropdownItem}
132
- role='menuitem'
133
- tabIndex={0}
134
- onKeyDown={(e) => {
135
- if (e.key === 'Enter') handleSort(column.id, 'asc');
136
- }}
137
- >
138
- <span
139
- className={`${styles.columnSorterLabel} ${smallUpper ? styles.uppercaseSmall : ''}`}
140
- title={label}
141
- aria-label={label}
142
- >
143
- {label}
144
- </span>
145
-
146
- <div className={styles.sortDirectionButtons}>
147
- <button
148
- className={`${styles.sortDirectionButton} ${
149
- sortConfig.key === column.id && sortConfig.direction === 'asc'
150
- ? styles.activeSort
151
- : ''
152
- }`}
153
- onClick={() => handleSort(column.id, 'asc')}
154
- title='Trier par ordre croissant'
155
- type='button'
156
- >
157
- <ChevronUp size={12} />
158
- </button>
159
- <button
160
- className={`${styles.sortDirectionButton} ${
161
- sortConfig.key === column.id && sortConfig.direction === 'desc'
162
- ? styles.activeSort
163
- : ''
164
- }`}
165
- onClick={() => handleSort(column.id, 'desc')}
166
- title='Trier par ordre décroissant'
167
- type='button'
168
- >
169
- <ChevronDown size={12} />
170
- </button>
171
- </div>
172
- </div>
173
- );
174
- })}
175
- </div>
176
- </div>
177
- )}
178
- </div>
179
- )}
180
- </div>
181
- </div>
182
- );
183
- };
184
-
185
- export default ColumnSorter;
1
+ 'use client';
2
+
3
+ import React, { useEffect, useRef, useState } from 'react';
4
+ import { ChevronsUpDown, ChevronUp, ChevronDown, X } from 'lucide-react';
5
+ import type { SortConfig, TableColumn } from './tools/tableTypes';
6
+ import styles from './TableauDynamique.module.css';
7
+
8
+ interface ColumnSorterProps<T = unknown> {
9
+ columns: TableColumn<T>[];
10
+ sortConfig: SortConfig;
11
+ onSort: (sortConfig: SortConfig) => void;
12
+ onToggleFilters?: () => void;
13
+ filtersActive?: boolean;
14
+ showFilters?: boolean;
15
+ }
16
+
17
+ const isAlphaUpperCase = (s: string) => {
18
+ const letters = s.replace(/[^A-Za-zÀ-ÖØ-öø-ÿ]/g, '');
19
+ return letters.length > 0 && letters === letters.toUpperCase();
20
+ };
21
+
22
+ const ColumnSorter = <T,>({ columns, sortConfig, onSort }: ColumnSorterProps<T>) => {
23
+ const [isOpen, setIsOpen] = useState(false);
24
+ const sortableColumns = columns.filter((col) => col.sortable);
25
+ const wrapperRef = useRef<HTMLDivElement | null>(null);
26
+
27
+ const handleSort = (key: string | null, direction: 'asc' | 'desc') => {
28
+ onSort({ key, direction });
29
+ setIsOpen(false);
30
+ };
31
+
32
+ const toggleOpen = () => setIsOpen((v) => !v);
33
+
34
+ const handleCancel = () => {
35
+ setIsOpen(false);
36
+ };
37
+
38
+ // fermer au clic extérieur + Échap (déjà en place)
39
+ useEffect(() => {
40
+ if (!isOpen) return;
41
+
42
+ const onPointerDown = (e: PointerEvent) => {
43
+ if (!wrapperRef.current) return;
44
+ const target = e.target as Node | null;
45
+ if (target && !wrapperRef.current.contains(target)) {
46
+ setIsOpen(false);
47
+ }
48
+ };
49
+
50
+ const onKeyDown = (e: KeyboardEvent) => {
51
+ if (e.key === 'Escape') {
52
+ setIsOpen(false);
53
+ }
54
+ };
55
+
56
+ document.addEventListener('pointerdown', onPointerDown);
57
+ document.addEventListener('keydown', onKeyDown);
58
+
59
+ return () => {
60
+ document.removeEventListener('pointerdown', onPointerDown);
61
+ document.removeEventListener('keydown', onKeyDown);
62
+ };
63
+ }, [isOpen]);
64
+
65
+ // --> NO SCROLL on the table container while dropdown is open
66
+ useEffect(() => {
67
+ const tableWrapper = document.querySelector('[data-table-wrapper]') as HTMLElement | null;
68
+ if (!tableWrapper) return;
69
+
70
+ // sauvegarde la valeur inline actuelle pour restaurer plus tard
71
+ const previousInline = tableWrapper.style.overflow ?? '';
72
+
73
+ if (isOpen) {
74
+ // masquer scrollbars (on cache à la fois X et Y)
75
+ tableWrapper.style.overflow = 'hidden';
76
+ } else {
77
+ // restaurer à la fermeture
78
+ tableWrapper.style.overflow = previousInline;
79
+ }
80
+
81
+ // cleanup si le composant se démonte pendant que c'est ouvert
82
+ return () => {
83
+ tableWrapper.style.overflow = previousInline;
84
+ };
85
+ }, [isOpen]);
86
+
87
+ return (
88
+ <div className={styles.columnSorterContainer}>
89
+ <div className={styles.sorterControls}>
90
+ {sortableColumns.length > 0 && (
91
+ <div className={styles.sortDropdownWrapper} ref={wrapperRef}>
92
+ <button
93
+ className={styles.toggleSortButton}
94
+ onClick={toggleOpen}
95
+ aria-expanded={isOpen}
96
+ aria-haspopup='true'
97
+ type='button'
98
+ >
99
+ <ChevronsUpDown size={14} />
100
+ <span
101
+ className={`${styles.buttonTextCompact} ${
102
+ isAlphaUpperCase('Sort') ? styles.uppercaseSmall : ''
103
+ }`}
104
+ >
105
+ Sort
106
+ </span>
107
+ {sortConfig.key &&
108
+ (sortConfig.direction === 'asc' ? (
109
+ <ChevronUp size={14} />
110
+ ) : (
111
+ <ChevronDown size={14} />
112
+ ))}
113
+ </button>
114
+
115
+ {isOpen && (
116
+ <div className={`${styles.sortDropdown} ${styles.sortDropdownInline}`} role='menu'>
117
+ <div className={styles.sortDropdownHeader}>
118
+ <span>Trier par:</span>
119
+ <button className={styles.closeButton} onClick={handleCancel} aria-label='Fermer'>
120
+ <X size={16} />
121
+ </button>
122
+ </div>
123
+
124
+ <div className={styles.sortDropdownContent}>
125
+ {sortableColumns.map((column) => {
126
+ const label = String(column.label ?? column.id);
127
+ const smallUpper = isAlphaUpperCase(label);
128
+ return (
129
+ <div
130
+ key={column.id}
131
+ className={styles.sortDropdownItem}
132
+ role='menuitem'
133
+ tabIndex={0}
134
+ onKeyDown={(e) => {
135
+ if (e.key === 'Enter') handleSort(column.id, 'asc');
136
+ }}
137
+ >
138
+ <span
139
+ className={`${styles.columnSorterLabel} ${smallUpper ? styles.uppercaseSmall : ''}`}
140
+ title={label}
141
+ aria-label={label}
142
+ >
143
+ {label}
144
+ </span>
145
+
146
+ <div className={styles.sortDirectionButtons}>
147
+ <button
148
+ className={`${styles.sortDirectionButton} ${
149
+ sortConfig.key === column.id && sortConfig.direction === 'asc'
150
+ ? styles.activeSort
151
+ : ''
152
+ }`}
153
+ onClick={() => handleSort(column.id, 'asc')}
154
+ title='Trier par ordre croissant'
155
+ type='button'
156
+ >
157
+ <ChevronUp size={12} />
158
+ </button>
159
+ <button
160
+ className={`${styles.sortDirectionButton} ${
161
+ sortConfig.key === column.id && sortConfig.direction === 'desc'
162
+ ? styles.activeSort
163
+ : ''
164
+ }`}
165
+ onClick={() => handleSort(column.id, 'desc')}
166
+ title='Trier par ordre décroissant'
167
+ type='button'
168
+ >
169
+ <ChevronDown size={12} />
170
+ </button>
171
+ </div>
172
+ </div>
173
+ );
174
+ })}
175
+ </div>
176
+ </div>
177
+ )}
178
+ </div>
179
+ )}
180
+ </div>
181
+ </div>
182
+ );
183
+ };
184
+
185
+ export default ColumnSorter;
@@ -1,115 +1,115 @@
1
- 'use client';
2
-
3
- import React from 'react';
4
- import styles from './TableauDynamique.module.css';
5
-
6
- interface PaginationProps {
7
- currentPage: number;
8
- totalPages: number;
9
- pageSize: number;
10
- totalItems: number;
11
- onPageChange: (page: number) => void;
12
- onPageSizeChange?: (size: number) => void;
13
- onNewPage?: (page: number) => void;
14
- }
15
-
16
- const Pagination: React.FC<PaginationProps> = ({
17
- currentPage,
18
- totalPages,
19
- pageSize,
20
- totalItems,
21
- onPageChange,
22
- onPageSizeChange,
23
- onNewPage,
24
- }) => {
25
- const safeTotalPages = Math.max(1, Math.floor(totalPages));
26
- const safeCurrentPage = Math.min(Math.max(1, Math.floor(currentPage)), safeTotalPages);
27
-
28
- const handlePrev = () => safeCurrentPage > 1 && onPageChange(safeCurrentPage - 1);
29
- const handleNext = () => {
30
- if (onNewPage && safeCurrentPage === safeTotalPages) {
31
- onNewPage(safeTotalPages + 1);
32
- return;
33
- }
34
- safeCurrentPage < safeTotalPages && onPageChange(safeCurrentPage + 1);
35
- };
36
-
37
- const startIndex = totalItems > 0 ? (safeCurrentPage - 1) * pageSize + 1 : 0;
38
- const endIndex = totalItems > 0 ? Math.min(safeCurrentPage * pageSize, totalItems) : 0;
39
-
40
- const getPageNumbers = () => {
41
- const pages: number[] = [];
42
- const maxVisible = 5;
43
- let start = Math.max(1, safeCurrentPage - Math.floor(maxVisible / 2));
44
- let end = start + maxVisible - 1;
45
-
46
- if (end > safeTotalPages) {
47
- end = safeTotalPages;
48
- start = Math.max(1, end - maxVisible + 1);
49
- }
50
-
51
- for (let i = start; i <= end; i++) pages.push(i);
52
- return pages;
53
- };
54
-
55
- return (
56
- <div className={styles.paginationContainer} role='navigation' aria-label='Pagination'>
57
- <div className={styles.leftGroup}>
58
- {onPageSizeChange && (
59
- <div className={styles.pageSizeControls}>
60
- <label className={styles.pageSizeLabel}>Show</label>
61
- <select
62
- value={pageSize}
63
- onChange={(e) => onPageSizeChange(Number(e.target.value))}
64
- className={styles.pageSizeSelect}
65
- aria-label='Taille de page'
66
- >
67
- {[5, 10, 20, 50].map((size) => (
68
- <option key={size} value={size}>
69
- {size}
70
- </option>
71
- ))}
72
- </select>
73
- <span className={styles.pageSizeSuffix}>entries</span>
74
- </div>
75
- )}
76
- </div>
77
-
78
- <div className={styles.paginationInfo} aria-live='polite'>
79
- {startIndex}-{endIndex} out of {totalItems}
80
- </div>
81
-
82
- <div className={styles.paginationControls}>
83
- <button
84
- onClick={handlePrev}
85
- disabled={safeCurrentPage === 1}
86
- className={`${styles.paginationButton} ${styles.navigationButton} ${safeCurrentPage === 1 ? styles.disabledButton : ''}`}
87
- aria-label='Page précédente'
88
- >
89
- «
90
- </button>
91
-
92
- {getPageNumbers().map((page) => (
93
- <button
94
- key={page}
95
- onClick={() => onPageChange(page)}
96
- aria-current={safeCurrentPage === page ? 'page' : undefined}
97
- className={`${styles.paginationButton} ${safeCurrentPage === page ? styles.activeButton : ''}`}
98
- >
99
- {page}
100
- </button>
101
- ))}
102
-
103
- <button
104
- onClick={handleNext}
105
- className={`${styles.paginationButton} ${styles.navigationButton}`}
106
- aria-label='Page suivante'
107
- >
108
- »
109
- </button>
110
- </div>
111
- </div>
112
- );
113
- };
114
-
115
- export default Pagination;
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import styles from './TableauDynamique.module.css';
5
+
6
+ interface PaginationProps {
7
+ currentPage: number;
8
+ totalPages: number;
9
+ pageSize: number;
10
+ totalItems: number;
11
+ onPageChange: (page: number) => void;
12
+ onPageSizeChange?: (size: number) => void;
13
+ onNewPage?: (page: number) => void;
14
+ }
15
+
16
+ const Pagination: React.FC<PaginationProps> = ({
17
+ currentPage,
18
+ totalPages,
19
+ pageSize,
20
+ totalItems,
21
+ onPageChange,
22
+ onPageSizeChange,
23
+ onNewPage,
24
+ }) => {
25
+ const safeTotalPages = Math.max(1, Math.floor(totalPages));
26
+ const safeCurrentPage = Math.min(Math.max(1, Math.floor(currentPage)), safeTotalPages);
27
+
28
+ const handlePrev = () => safeCurrentPage > 1 && onPageChange(safeCurrentPage - 1);
29
+ const handleNext = () => {
30
+ if (onNewPage && safeCurrentPage === safeTotalPages) {
31
+ onNewPage(safeTotalPages + 1);
32
+ return;
33
+ }
34
+ safeCurrentPage < safeTotalPages && onPageChange(safeCurrentPage + 1);
35
+ };
36
+
37
+ const startIndex = totalItems > 0 ? (safeCurrentPage - 1) * pageSize + 1 : 0;
38
+ const endIndex = totalItems > 0 ? Math.min(safeCurrentPage * pageSize, totalItems) : 0;
39
+
40
+ const getPageNumbers = () => {
41
+ const pages: number[] = [];
42
+ const maxVisible = 5;
43
+ let start = Math.max(1, safeCurrentPage - Math.floor(maxVisible / 2));
44
+ let end = start + maxVisible - 1;
45
+
46
+ if (end > safeTotalPages) {
47
+ end = safeTotalPages;
48
+ start = Math.max(1, end - maxVisible + 1);
49
+ }
50
+
51
+ for (let i = start; i <= end; i++) pages.push(i);
52
+ return pages;
53
+ };
54
+
55
+ return (
56
+ <div className={styles.paginationContainer} role='navigation' aria-label='Pagination'>
57
+ <div className={styles.leftGroup}>
58
+ {onPageSizeChange && (
59
+ <div className={styles.pageSizeControls}>
60
+ <label className={styles.pageSizeLabel}>Show</label>
61
+ <select
62
+ value={pageSize}
63
+ onChange={(e) => onPageSizeChange(Number(e.target.value))}
64
+ className={styles.pageSizeSelect}
65
+ aria-label='Taille de page'
66
+ >
67
+ {[5, 10, 20, 50].map((size) => (
68
+ <option key={size} value={size}>
69
+ {size}
70
+ </option>
71
+ ))}
72
+ </select>
73
+ <span className={styles.pageSizeSuffix}>entries</span>
74
+ </div>
75
+ )}
76
+ </div>
77
+
78
+ <div className={styles.paginationInfo} aria-live='polite'>
79
+ {startIndex}-{endIndex} out of {totalItems}
80
+ </div>
81
+
82
+ <div className={styles.paginationControls}>
83
+ <button
84
+ onClick={handlePrev}
85
+ disabled={safeCurrentPage === 1}
86
+ className={`${styles.paginationButton} ${styles.navigationButton} ${safeCurrentPage === 1 ? styles.disabledButton : ''}`}
87
+ aria-label='Page précédente'
88
+ >
89
+ «
90
+ </button>
91
+
92
+ {getPageNumbers().map((page) => (
93
+ <button
94
+ key={page}
95
+ onClick={() => onPageChange(page)}
96
+ aria-current={safeCurrentPage === page ? 'page' : undefined}
97
+ className={`${styles.paginationButton} ${safeCurrentPage === page ? styles.activeButton : ''}`}
98
+ >
99
+ {page}
100
+ </button>
101
+ ))}
102
+
103
+ <button
104
+ onClick={handleNext}
105
+ className={`${styles.paginationButton} ${styles.navigationButton}`}
106
+ aria-label='Page suivante'
107
+ >
108
+ »
109
+ </button>
110
+ </div>
111
+ </div>
112
+ );
113
+ };
114
+
115
+ export default Pagination;