@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
@@ -0,0 +1,321 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import SelectFilter from './SelectFilter';
4
+ import type { SelectFilterProps, SelectOption } from './types';
5
+
6
+ const mockCountries: SelectOption[] = [
7
+ { value: 'fr', label: 'France' },
8
+ { value: 'us', label: 'United States' },
9
+ { value: 'de', label: 'Germany' },
10
+ { value: 'it', label: 'Italy' },
11
+ { value: 'es', label: 'Spain' },
12
+ { value: 'uk', label: 'United Kingdom' },
13
+ { value: 'jp', label: 'Japan' },
14
+ { value: 'ca', label: 'Canada' },
15
+ { value: 'au', label: 'Australia' },
16
+ { value: 'br', label: 'Brazil' },
17
+ ];
18
+
19
+ const mockUsers: SelectOption[] = [
20
+ { value: 1, label: 'John Doe - john@example.com' },
21
+ { value: 2, label: 'Jane Smith - jane@example.com' },
22
+ { value: 3, label: 'Bob Johnson - bob@example.com' },
23
+ { value: 4, label: 'Alice Brown - alice@example.com' },
24
+ { value: 5, label: 'Charlie Wilson - charlie@example.com' },
25
+ ];
26
+
27
+ const mockLongLabels: SelectOption[] = [
28
+ { value: 1041, label: 'MEDECINE VASCULAIRE HOSPITALISATION COMPLETE (1041)' },
29
+ { value: 1042, label: 'CARDIOLOGIE HOSPITALISATION COMPLETE (1042)' },
30
+ { value: 1043, label: 'CHIRURGIE THORACIQUE ET CARDIO-VASCULAIRE (1043)' },
31
+ { value: 1044, label: 'NEUROLOGIE HOSPITALISATION COMPLETE (1044)' },
32
+ { value: 1045, label: 'NEUROCHIRURGIE HOSPITALISATION COMPLETE (1045)' },
33
+ ];
34
+
35
+ type SelectFilterStoryProps = Omit<SelectFilterProps, 'onChange'> & {
36
+ onChange?: (value: string | number) => void;
37
+ };
38
+
39
+ const meta: Meta<SelectFilterStoryProps> = {
40
+ title: 'Components/SelectFilter',
41
+ component: SelectFilter,
42
+ parameters: {
43
+ layout: 'centered',
44
+ },
45
+ tags: ['autodocs'],
46
+ argTypes: {
47
+ size: {
48
+ control: { type: 'radio' },
49
+ options: ['sm', 'md', 'lg'],
50
+ },
51
+ variant: {
52
+ control: { type: 'select' },
53
+ options: ['default', 'outline', 'filled'],
54
+ },
55
+ disabled: {
56
+ control: { type: 'boolean' },
57
+ },
58
+ loading: {
59
+ control: { type: 'boolean' },
60
+ },
61
+ error: {
62
+ control: { type: 'boolean' },
63
+ },
64
+ width: {
65
+ control: { type: 'text' },
66
+ description: 'Classes Tailwind pour la largeur (ex: w-64, w-80, w-full)',
67
+ },
68
+ },
69
+ args: {
70
+ placeholder: 'Sélectionner une option...',
71
+ options: mockCountries,
72
+ width: 'w-80',
73
+ },
74
+ };
75
+
76
+ export default meta;
77
+ type Story = StoryObj<SelectFilterStoryProps>;
78
+
79
+ const SelectWithState = (args: SelectFilterStoryProps) => {
80
+ const [value, setValue] = useState<string | number>('');
81
+
82
+ const handleChange = (newValue: string | number, _option: SelectOption) => {
83
+ setValue(newValue);
84
+ };
85
+
86
+ return (
87
+ <div className={args.width || 'w-80'}>
88
+ <SelectFilter {...args} value={value} onChange={handleChange} />
89
+ <div className='mt-4 text-sm text-gray-600'>Valeur sélectionnée: {value || 'Aucune'}</div>
90
+ </div>
91
+ );
92
+ };
93
+
94
+ export const Default: Story = {
95
+ render: (args) => <SelectWithState {...args} />,
96
+ };
97
+
98
+ export const WithCustomPlaceholder: Story = {
99
+ render: (args) => <SelectWithState {...args} />,
100
+ args: {
101
+ placeholder: 'Choisissez un pays...',
102
+ },
103
+ };
104
+
105
+ export const Small: Story = {
106
+ render: (args) => <SelectWithState {...args} />,
107
+ args: {
108
+ size: 'sm',
109
+ },
110
+ };
111
+
112
+ export const Medium: Story = {
113
+ render: (args) => <SelectWithState {...args} />,
114
+ args: {
115
+ size: 'md',
116
+ },
117
+ };
118
+
119
+ export const Large: Story = {
120
+ render: (args) => <SelectWithState {...args} />,
121
+ args: {
122
+ size: 'lg',
123
+ },
124
+ };
125
+
126
+ export const Outline: Story = {
127
+ render: (args) => <SelectWithState {...args} />,
128
+ args: {
129
+ variant: 'outline',
130
+ },
131
+ };
132
+
133
+ export const Filled: Story = {
134
+ render: (args) => <SelectWithState {...args} />,
135
+ args: {
136
+ variant: 'filled',
137
+ },
138
+ };
139
+
140
+ export const Disabled: Story = {
141
+ args: {
142
+ disabled: true,
143
+ },
144
+ };
145
+
146
+ export const Loading: Story = {
147
+ args: {
148
+ loading: true,
149
+ },
150
+ };
151
+
152
+ export const Error: Story = {
153
+ args: {
154
+ error: true,
155
+ },
156
+ };
157
+
158
+ export const WithUserData: Story = {
159
+ render: (args) => <SelectWithState {...args} />,
160
+ args: {
161
+ options: mockUsers,
162
+ placeholder: 'Rechercher un utilisateur...',
163
+ },
164
+ };
165
+
166
+ export const WithDisabledOptions: Story = {
167
+ render: (args) => <SelectWithState {...args} />,
168
+ args: {
169
+ options: [
170
+ { value: 'fr', label: 'France' },
171
+ { value: 'us', label: 'United States' },
172
+ { value: 'de', label: 'Germany', disabled: true },
173
+ { value: 'it', label: 'Italy' },
174
+ { value: 'es', label: 'Spain', disabled: true },
175
+ ],
176
+ },
177
+ };
178
+
179
+ export const WithLongLabels: Story = {
180
+ render: (args) => <SelectWithState {...args} />,
181
+ args: {
182
+ options: mockLongLabels,
183
+ placeholder: 'Sélectionnez un service médical...',
184
+ width: 'w-96',
185
+ },
186
+ };
187
+
188
+ export const DifferentWidths: Story = {
189
+ render: () => {
190
+ const [value1, setValue1] = useState('');
191
+ const [value2, setValue2] = useState('');
192
+ const [value3, setValue3] = useState('');
193
+
194
+ return (
195
+ <div className='space-y-6 max-w-2xl'>
196
+ <div>
197
+ <label className='block text-sm font-medium text-gray-700 mb-2'>
198
+ Largeur normale (w-64)
199
+ </label>
200
+ <SelectFilter
201
+ options={mockCountries}
202
+ value={value1}
203
+ onChange={(newValue, _option) => setValue1(newValue.toString())}
204
+ placeholder='Pays...'
205
+ width='w-64'
206
+ />
207
+ </div>
208
+
209
+ <div>
210
+ <label className='block text-sm font-medium text-gray-700 mb-2'>
211
+ Largeur moyenne (w-80)
212
+ </label>
213
+ <SelectFilter
214
+ options={mockUsers}
215
+ value={value2}
216
+ onChange={(newValue, _option) => setValue2(newValue.toString())}
217
+ placeholder='Utilisateurs...'
218
+ width='w-80'
219
+ />
220
+ </div>
221
+
222
+ <div>
223
+ <label className='block text-sm font-medium text-gray-700 mb-2'>
224
+ Largeur pour labels longs (w-96)
225
+ </label>
226
+ <SelectFilter
227
+ options={mockLongLabels}
228
+ value={value3}
229
+ onChange={(newValue, _option) => setValue3(newValue.toString())}
230
+ placeholder='Services médicaux...'
231
+ width='w-96'
232
+ />
233
+ </div>
234
+
235
+ <div className='p-4 bg-gray-50 rounded-lg'>
236
+ <div className='text-sm space-y-2'>
237
+ <div>Pays sélectionné: {value1 || 'Aucun'}</div>
238
+ <div>Utilisateur sélectionné: {value2 || 'Aucun'}</div>
239
+ <div>Service sélectionné: {value3 || 'Aucun'}</div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ );
244
+ },
245
+ };
246
+
247
+ export const MultipleSelects: Story = {
248
+ render: () => {
249
+ const [country, setCountry] = useState('');
250
+ const [user, setUser] = useState('');
251
+
252
+ return (
253
+ <div className='space-y-4 w-80'>
254
+ <div>
255
+ <label className='block text-sm font-medium text-gray-700 mb-2'>Pays</label>
256
+ <SelectFilter
257
+ options={mockCountries}
258
+ value={country}
259
+ onChange={(newValue, _option) => setCountry(newValue.toString())}
260
+ placeholder='Choisir un pays'
261
+ />
262
+ </div>
263
+
264
+ <div>
265
+ <label className='block text-sm font-medium text-gray-700 mb-2'>Utilisateur</label>
266
+ <SelectFilter
267
+ options={mockUsers}
268
+ value={user}
269
+ onChange={(newValue, _option) => setUser(newValue.toString())}
270
+ placeholder='Choisir un utilisateur'
271
+ variant='outline'
272
+ />
273
+ </div>
274
+
275
+ <div className='p-4 bg-gray-50 rounded-lg'>
276
+ <div className='text-sm'>
277
+ <div>Pays sélectionné: {country || 'Aucun'}</div>
278
+ <div>Utilisateur sélectionné: {user || 'Aucun'}</div>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ );
283
+ },
284
+ };
285
+
286
+ export const AsyncLoading: Story = {
287
+ render: () => {
288
+ const [options, setOptions] = useState<SelectOption[]>([]);
289
+ const [loading, setLoading] = useState(false);
290
+ const [value, setValue] = useState('');
291
+
292
+ const loadOptions = () => {
293
+ setLoading(true);
294
+ setTimeout(() => {
295
+ setOptions(mockCountries);
296
+ setLoading(false);
297
+ }, 2000);
298
+ };
299
+
300
+ return (
301
+ <div className='w-80'>
302
+ <SelectFilter
303
+ options={options}
304
+ value={value}
305
+ onChange={(newValue, _option) => setValue(newValue.toString())}
306
+ loading={loading}
307
+ placeholder='Cliquez pour charger les options'
308
+ />
309
+ <div className='mt-2 text-xs text-gray-500'>
310
+ <button
311
+ type='button'
312
+ onClick={loadOptions}
313
+ className='text-blue-600 hover:text-blue-800 underline'
314
+ >
315
+ Charger les options
316
+ </button>
317
+ </div>
318
+ </div>
319
+ );
320
+ },
321
+ };
@@ -0,0 +1,219 @@
1
+ import React, { useState, useRef, useEffect, useMemo } from 'react';
2
+ import type { SelectFilterProps, SelectOption } from './types';
3
+ import styles from './SelectFilter.module.css';
4
+
5
+ const SelectFilter: React.FC<SelectFilterProps> = ({
6
+ options,
7
+ value,
8
+ onChange,
9
+ placeholder = 'Sélectionner une option...',
10
+ disabled = false,
11
+ loading = false,
12
+ error = false,
13
+ className = '',
14
+ size = 'md',
15
+ variant = 'default',
16
+ width = 'w-full',
17
+ }) => {
18
+ const [isOpen, setIsOpen] = useState(false);
19
+ const [searchTerm, setSearchTerm] = useState('');
20
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
21
+ const inputRef = useRef<HTMLInputElement>(null);
22
+ const dropdownRef = useRef<HTMLDivElement>(null);
23
+
24
+ const filteredOptions = useMemo(() => {
25
+ if (!searchTerm) return options;
26
+ return options.filter((option) =>
27
+ option.label.toLowerCase().includes(searchTerm.toLowerCase())
28
+ );
29
+ }, [options, searchTerm]);
30
+
31
+ const selectedOption = options.find((opt) => opt.value === value);
32
+
33
+ useEffect(() => {
34
+ const handleClickOutside = (event: MouseEvent) => {
35
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
36
+ setIsOpen(false);
37
+ setSearchTerm('');
38
+ }
39
+ };
40
+
41
+ document.addEventListener('mousedown', handleClickOutside);
42
+ return () => document.removeEventListener('mousedown', handleClickOutside);
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ setHighlightedIndex(0);
47
+ }, [filteredOptions]);
48
+
49
+ const handleSelect = (option: SelectOption) => {
50
+ if (option.disabled) return;
51
+ onChange?.(option.value, option);
52
+ setIsOpen(false);
53
+ setSearchTerm('');
54
+ };
55
+
56
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
57
+ setSearchTerm(e.target.value);
58
+ if (!isOpen) setIsOpen(true);
59
+ };
60
+
61
+ const handleKeyDown = (e: React.KeyboardEvent) => {
62
+ if (!isOpen) {
63
+ if (e.key === 'ArrowDown' || e.key === 'Enter') {
64
+ setIsOpen(true);
65
+ }
66
+ return;
67
+ }
68
+
69
+ switch (e.key) {
70
+ case 'ArrowDown':
71
+ e.preventDefault();
72
+ setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : prev));
73
+ break;
74
+ case 'ArrowUp':
75
+ e.preventDefault();
76
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev));
77
+ break;
78
+ case 'Enter':
79
+ e.preventDefault();
80
+ if (filteredOptions[highlightedIndex]) {
81
+ handleSelect(filteredOptions[highlightedIndex]);
82
+ }
83
+ break;
84
+ case 'Escape':
85
+ setIsOpen(false);
86
+ setSearchTerm('');
87
+ break;
88
+ }
89
+ };
90
+
91
+ const getButtonClasses = () => {
92
+ const baseClasses = [
93
+ styles.selectButton,
94
+ styles[size],
95
+ styles[variant],
96
+ disabled ? styles.disabled : '',
97
+ error ? styles.error : '',
98
+ className,
99
+ ]
100
+ .filter(Boolean)
101
+ .join(' ');
102
+
103
+ return baseClasses;
104
+ };
105
+
106
+ const getWidthClass = () => {
107
+ switch (width) {
108
+ case 'w-64':
109
+ return styles.width64;
110
+ case 'w-80':
111
+ return styles.width80;
112
+ case 'w-96':
113
+ return styles.width96;
114
+ default:
115
+ return styles.widthFull;
116
+ }
117
+ };
118
+
119
+ const getOptionClasses = (option: SelectOption, index: number) => {
120
+ return [
121
+ styles.option,
122
+ index === highlightedIndex ? styles.optionHighlighted : '',
123
+ option.value === value ? styles.optionSelected : '',
124
+ option.disabled ? styles.optionDisabled : '',
125
+ ]
126
+ .filter(Boolean)
127
+ .join(' ');
128
+ };
129
+
130
+ return (
131
+ <div className={`${styles.container} ${getWidthClass()}`} ref={dropdownRef}>
132
+ <button
133
+ className={getButtonClasses()}
134
+ onClick={() => !disabled && setIsOpen(!isOpen)}
135
+ onKeyDown={handleKeyDown}
136
+ tabIndex={disabled ? -1 : 0}
137
+ type='button'
138
+ aria-haspopup='listbox'
139
+ aria-expanded={isOpen}
140
+ disabled={disabled}
141
+ >
142
+ <div className={styles.buttonContent}>
143
+ <span className={`${styles.label} ${!selectedOption ? styles.placeholder : ''}`}>
144
+ {selectedOption ? selectedOption.label : placeholder}
145
+ </span>
146
+ <div className={styles.iconsContainer}>
147
+ {loading && <div className={styles.loadingSpinner} />}
148
+ <svg
149
+ className={`${styles.arrowIcon} ${isOpen ? styles.arrowIconOpen : ''}`}
150
+ fill='none'
151
+ stroke='currentColor'
152
+ viewBox='0 0 24 24'
153
+ >
154
+ <path
155
+ strokeLinecap='round'
156
+ strokeLinejoin='round'
157
+ strokeWidth={2}
158
+ d='M19 9l-7 7-7-7'
159
+ />
160
+ </svg>
161
+ </div>
162
+ </div>
163
+ </button>
164
+
165
+ {isOpen && (
166
+ <div className={`${styles.dropdown} ${getWidthClass()}`}>
167
+ <div className={styles.searchContainer}>
168
+ <input
169
+ ref={inputRef}
170
+ type='text'
171
+ value={searchTerm}
172
+ onChange={handleInputChange}
173
+ placeholder='Rechercher...'
174
+ className={styles.searchInput}
175
+ autoFocus
176
+ />
177
+ </div>
178
+
179
+ <div className={styles.optionsList}>
180
+ {filteredOptions.length === 0 ? (
181
+ <div className={styles.noResults}>Aucun résultat trouvé</div>
182
+ ) : (
183
+ <ul role='listbox'>
184
+ {filteredOptions.map((option, index) => (
185
+ <li
186
+ key={option.value}
187
+ className={getOptionClasses(option, index)}
188
+ onClick={() => handleSelect(option)}
189
+ onMouseEnter={() => setHighlightedIndex(index)}
190
+ role='option'
191
+ aria-selected={option.value === value}
192
+ aria-disabled={option.disabled}
193
+ >
194
+ <div className={styles.buttonContent}>
195
+ <span className={styles.optionLabel} title={option.label}>
196
+ {option.label}
197
+ </span>
198
+ {option.value === value && (
199
+ <svg className={styles.checkIcon} fill='currentColor' viewBox='0 0 20 20'>
200
+ <path
201
+ fillRule='evenodd'
202
+ d='M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z'
203
+ clipRule='evenodd'
204
+ />
205
+ </svg>
206
+ )}
207
+ </div>
208
+ </li>
209
+ ))}
210
+ </ul>
211
+ )}
212
+ </div>
213
+ </div>
214
+ )}
215
+ </div>
216
+ );
217
+ };
218
+
219
+ export default SelectFilter;
@@ -0,0 +1,2 @@
1
+ export { default as SelectFilter } from './SelectFilter';
2
+ export type { SelectFilterProps, SelectOption } from './types';
@@ -0,0 +1,19 @@
1
+ export interface SelectOption {
2
+ value: string | number;
3
+ label: string;
4
+ disabled?: boolean;
5
+ }
6
+
7
+ export interface SelectFilterProps {
8
+ options: SelectOption[];
9
+ value?: string | number;
10
+ onChange?: (value: string | number, option: SelectOption) => void;
11
+ placeholder?: string;
12
+ disabled?: boolean;
13
+ loading?: boolean;
14
+ error?: boolean;
15
+ className?: string;
16
+ size?: 'sm' | 'md' | 'lg';
17
+ variant?: 'default' | 'outline' | 'filled';
18
+ width?: 'w-full' | 'w-64' | 'w-80' | 'w-96';
19
+ }