@tsiky/components-r19 1.0.0 → 1.2.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.
- package/index.ts +35 -33
- package/package.json +1 -1
- package/src/components/AnnouncementPanel/FlexRowContainer.css +17 -17
- package/src/components/AnnouncementPanel/FlexRowContainer.stories.tsx +329 -329
- package/src/components/AnnouncementPanel/FlexRowContainer.tsx +24 -24
- package/src/components/AnnouncementPanel/ListBox/CounterListBox.css +56 -56
- package/src/components/AnnouncementPanel/ListBox/CounterListBox.stories.tsx +292 -292
- package/src/components/AnnouncementPanel/ListBox/CounterListBox.tsx +106 -106
- package/src/components/AnnouncementPanel/ListBox/SimpleListBox.css +57 -57
- package/src/components/AnnouncementPanel/ListBox/SimpleListBox.stories.tsx +189 -189
- package/src/components/AnnouncementPanel/ListBox/SimpleListBox.tsx +138 -138
- package/src/components/AnnouncementPanel/ListBox/TrendListBox.css +61 -61
- package/src/components/AnnouncementPanel/ListBox/TrendListBox.stories.tsx +257 -257
- package/src/components/AnnouncementPanel/ListBox/TrendListBox.tsx +90 -90
- package/src/components/AnnouncementPanel/ListBox/index.ts +3 -3
- package/src/components/AnnouncementPanel/ListContentContainer.css +23 -23
- package/src/components/AnnouncementPanel/ListContentContainer.stories.tsx +212 -212
- package/src/components/AnnouncementPanel/ListContentContainer.tsx +33 -33
- package/src/components/AnnouncementPanel/index.ts +3 -3
- package/src/components/Charts/area-chart-admission/AreaChartAdmission.tsx +129 -89
- package/src/components/Charts/bar-chart/BarChart.tsx +171 -132
- package/src/components/Charts/boxplot-chart/BoxPlotChart.tsx +114 -114
- package/src/components/Charts/mixed-chart/MixedChart.tsx +65 -9
- package/src/components/Charts/sankey-adaptation/sankey.tsx +70 -70
- package/src/components/Charts/sankey-chart/SankeyChart.tsx +183 -155
- package/src/components/Confirmationpopup/ConfirmationPopup.module.css +88 -0
- package/src/components/Confirmationpopup/ConfirmationPopup.stories.tsx +94 -0
- package/src/components/Confirmationpopup/ConfirmationPopup.tsx +47 -0
- package/src/components/Confirmationpopup/index.ts +6 -0
- package/src/components/Confirmationpopup/useConfirmationPopup.ts +48 -0
- package/src/components/DayStatCard/DayStatCard.tsx +96 -69
- package/src/components/DraggableSwitcher/DraggableSwitcherButton.tsx +58 -58
- package/src/components/DraggableSwitcher/context/useDraggableSwitcher.tsx +45 -45
- package/src/components/DraggableSwitcher/index.ts +2 -2
- package/src/components/DynamicInput/input/SelectInput.tsx +75 -75
- package/src/components/DynamicInput/input/assets/SelectInput.module.css +95 -95
- package/src/components/DynamicTable/TableCell.tsx +38 -30
- package/src/components/DynamicTable/TableHeader.tsx +39 -34
- package/src/components/DynamicTable/TableauDynamique.module.css +1333 -1287
- package/src/components/DynamicTable/TableauDynamique.tsx +154 -154
- package/src/components/DynamicTable/tools/tableTypes.ts +63 -63
- package/src/components/Grid/Grid.tsx +5 -0
- package/src/components/Grid/grid.css +285 -285
- package/src/components/Header/Header.tsx +4 -2
- package/src/components/Header/header.css +61 -31
- package/src/components/MetricsPanel/MetricsPanel.module.css +688 -636
- package/src/components/MetricsPanel/MetricsPanel.tsx +220 -282
- package/src/components/MetricsPanel/renderers/CompactRenderer.tsx +148 -125
- package/src/components/NavBar/NavBar.tsx +1 -1
- package/src/components/NavItem/NavItem.tsx +58 -58
- package/src/components/PeriodRange/PeriodRange.module.css +158 -158
- package/src/components/PeriodRange/PeriodRange.tsx +130 -130
- package/src/components/SearchBar/SearchBar.css +40 -40
- package/src/components/SelectFilter/SelectFilter.module.css +249 -0
- package/src/components/SelectFilter/SelectFilter.stories.tsx +321 -0
- package/src/components/SelectFilter/SelectFilter.tsx +219 -0
- package/src/components/SelectFilter/index.ts +2 -0
- package/src/components/SelectFilter/types.ts +19 -0
- package/src/components/TranslationKey/TranslationKey.css +272 -272
- package/src/components/TranslationKey/TranslationKey.tsx +266 -245
- package/src/components/TrendList/TrendList.tsx +72 -45
|
@@ -1,69 +1,96 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { CSSProperties } from 'react';
|
|
3
|
-
import './DayStatCard.css';
|
|
4
|
-
import { CircularProgress } from '../CircularProgress/CircularProgress';
|
|
5
|
-
import type { CircularProgressProps } from '../CircularProgress/CircularProgress';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
import './DayStatCard.css';
|
|
4
|
+
import { CircularProgress } from '../CircularProgress/CircularProgress';
|
|
5
|
+
import type { CircularProgressProps } from '../CircularProgress/CircularProgress';
|
|
6
|
+
import { Move } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export type DayStatCardProps = {
|
|
9
|
+
background?: string;
|
|
10
|
+
headerTop: string;
|
|
11
|
+
headerBottom?: string;
|
|
12
|
+
headerTopStyle?: CSSProperties;
|
|
13
|
+
headerBottomStyle?: CSSProperties;
|
|
14
|
+
progress1: CircularProgressProps;
|
|
15
|
+
progress2: CircularProgressProps;
|
|
16
|
+
progress3: CircularProgressProps;
|
|
17
|
+
style?: CSSProperties;
|
|
18
|
+
size?: number;
|
|
19
|
+
className?: string;
|
|
20
|
+
draggable?: boolean; // nouvelle prop
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const DayStatCard: React.FC<DayStatCardProps> = ({
|
|
24
|
+
size = 200,
|
|
25
|
+
background = '#fff',
|
|
26
|
+
headerTop,
|
|
27
|
+
headerBottom,
|
|
28
|
+
headerTopStyle,
|
|
29
|
+
headerBottomStyle,
|
|
30
|
+
progress1,
|
|
31
|
+
progress2,
|
|
32
|
+
progress3,
|
|
33
|
+
style,
|
|
34
|
+
className = '',
|
|
35
|
+
draggable = false,
|
|
36
|
+
}) => {
|
|
37
|
+
const progress1WithDefaults = { size: size * 0.5, ...progress1 };
|
|
38
|
+
const progress2WithDefaults = { size: size * 0.4, ...progress2 };
|
|
39
|
+
const progress3WithDefaults = { size: size * 0.4, ...progress3 };
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
style={{ width: size, background, position: 'relative', ...style }}
|
|
44
|
+
className={`stat-card ${className}`}
|
|
45
|
+
>
|
|
46
|
+
{/* Header toujours visible */}
|
|
47
|
+
<div className='stat-card-header'>
|
|
48
|
+
<div className='stat-card-header-top' style={headerTopStyle}>
|
|
49
|
+
{headerTop}
|
|
50
|
+
</div>
|
|
51
|
+
{headerBottom && (
|
|
52
|
+
<div className='stat-card-header-bottom' style={headerBottomStyle}>
|
|
53
|
+
{headerBottom}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Body */}
|
|
59
|
+
<div className='stat-card-body' style={{ opacity: draggable ? 0 : 1 }}>
|
|
60
|
+
{/* Première ligne - Progress Centré */}
|
|
61
|
+
<div className='progress-row-single'>
|
|
62
|
+
<CircularProgress {...progress1WithDefaults} />
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Deuxième ligne - Deux Progress côte à côte */}
|
|
66
|
+
<div className='progress-row-double'>
|
|
67
|
+
<div className='progress-item'>
|
|
68
|
+
<CircularProgress {...progress2WithDefaults} />
|
|
69
|
+
</div>
|
|
70
|
+
<div className='progress-item'>
|
|
71
|
+
<CircularProgress {...progress3WithDefaults} />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Overlay Move */}
|
|
77
|
+
{draggable && (
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
top: 0,
|
|
82
|
+
left: 0,
|
|
83
|
+
width: '100%',
|
|
84
|
+
height: '100%',
|
|
85
|
+
display: 'flex',
|
|
86
|
+
alignItems: 'center',
|
|
87
|
+
justifyContent: 'center',
|
|
88
|
+
pointerEvents: 'none',
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<Move size={32} strokeWidth={1.5} style={{ opacity: 0.8 }} />
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
3
|
-
import { useDraggableSwitcher } from './context/useDraggableSwitcher';
|
|
4
|
-
import { Switcher } from '../Switcher/Switcher';
|
|
5
|
-
|
|
6
|
-
export interface DraggableSwitcherButtonProps {
|
|
7
|
-
leftIcon?: string;
|
|
8
|
-
rightIcon?: string;
|
|
9
|
-
colors?: {
|
|
10
|
-
bg?: string;
|
|
11
|
-
activeBg?: string;
|
|
12
|
-
border?: string;
|
|
13
|
-
circle?: string;
|
|
14
|
-
icon?: string;
|
|
15
|
-
icon2?: string;
|
|
16
|
-
};
|
|
17
|
-
size?: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const DraggableSwitcherButton = ({
|
|
21
|
-
leftIcon = 'Move',
|
|
22
|
-
rightIcon = 'MoveDiagonal',
|
|
23
|
-
colors = {
|
|
24
|
-
bg: '#e5e7eb',
|
|
25
|
-
activeBg: '#10b981',
|
|
26
|
-
border: '#ffffff',
|
|
27
|
-
circle: '#ffffff',
|
|
28
|
-
icon: '#10b981',
|
|
29
|
-
icon2: '#ffffff',
|
|
30
|
-
},
|
|
31
|
-
size = 24,
|
|
32
|
-
}: DraggableSwitcherButtonProps) => {
|
|
33
|
-
const { isDraggable, switchDraggable } = useDraggableSwitcher();
|
|
34
|
-
const [mounted, setMounted] = useState(false);
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
setMounted(true);
|
|
38
|
-
}, []);
|
|
39
|
-
|
|
40
|
-
if (!mounted) return null;
|
|
41
|
-
|
|
42
|
-
const handleToggle = (checked: boolean) => {
|
|
43
|
-
switchDraggable(checked);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<Switcher
|
|
48
|
-
leftIcon={leftIcon}
|
|
49
|
-
rightIcon={rightIcon}
|
|
50
|
-
checked={isDraggable}
|
|
51
|
-
onToggle={handleToggle}
|
|
52
|
-
colors={colors}
|
|
53
|
-
size={size}
|
|
54
|
-
/>
|
|
55
|
-
);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export default DraggableSwitcherButton;
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useDraggableSwitcher } from './context/useDraggableSwitcher';
|
|
4
|
+
import { Switcher } from '../Switcher/Switcher';
|
|
5
|
+
|
|
6
|
+
export interface DraggableSwitcherButtonProps {
|
|
7
|
+
leftIcon?: string;
|
|
8
|
+
rightIcon?: string;
|
|
9
|
+
colors?: {
|
|
10
|
+
bg?: string;
|
|
11
|
+
activeBg?: string;
|
|
12
|
+
border?: string;
|
|
13
|
+
circle?: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
icon2?: string;
|
|
16
|
+
};
|
|
17
|
+
size?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DraggableSwitcherButton = ({
|
|
21
|
+
leftIcon = 'Move',
|
|
22
|
+
rightIcon = 'MoveDiagonal',
|
|
23
|
+
colors = {
|
|
24
|
+
bg: '#e5e7eb',
|
|
25
|
+
activeBg: '#10b981',
|
|
26
|
+
border: '#ffffff',
|
|
27
|
+
circle: '#ffffff',
|
|
28
|
+
icon: '#10b981',
|
|
29
|
+
icon2: '#ffffff',
|
|
30
|
+
},
|
|
31
|
+
size = 24,
|
|
32
|
+
}: DraggableSwitcherButtonProps) => {
|
|
33
|
+
const { isDraggable, switchDraggable } = useDraggableSwitcher();
|
|
34
|
+
const [mounted, setMounted] = useState(false);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setMounted(true);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
if (!mounted) return null;
|
|
41
|
+
|
|
42
|
+
const handleToggle = (checked: boolean) => {
|
|
43
|
+
switchDraggable(checked);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Switcher
|
|
48
|
+
leftIcon={leftIcon}
|
|
49
|
+
rightIcon={rightIcon}
|
|
50
|
+
checked={isDraggable}
|
|
51
|
+
onToggle={handleToggle}
|
|
52
|
+
colors={colors}
|
|
53
|
+
size={size}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default DraggableSwitcherButton;
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { createContext, useContext, useState, useEffect } from 'react';
|
|
3
|
-
import type { ReactNode } from 'react';
|
|
4
|
-
|
|
5
|
-
interface DraggableContextType {
|
|
6
|
-
isDraggable: boolean;
|
|
7
|
-
switchDraggable: (value: boolean) => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const DraggableContext = createContext<DraggableContextType | undefined>(undefined);
|
|
11
|
-
|
|
12
|
-
export const DraggableProvider = ({ children }: { children: ReactNode }) => {
|
|
13
|
-
const [isDraggable, setIsDraggable] = useState(false);
|
|
14
|
-
|
|
15
|
-
// Charger depuis localStorage au premier rendu
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
const stored = localStorage.getItem('isDraggable');
|
|
18
|
-
if (stored !== null) {
|
|
19
|
-
setIsDraggable(stored === 'true'); // localStorage stocke toujours des strings
|
|
20
|
-
}
|
|
21
|
-
}, []);
|
|
22
|
-
|
|
23
|
-
// Sauvegarder dans localStorage quand ça change
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
localStorage.setItem('isDraggable', String(isDraggable));
|
|
26
|
-
}, [isDraggable]);
|
|
27
|
-
|
|
28
|
-
const switchDraggable = (value: boolean) => {
|
|
29
|
-
setIsDraggable(value);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<DraggableContext.Provider value={{ isDraggable, switchDraggable }}>
|
|
34
|
-
{children}
|
|
35
|
-
</DraggableContext.Provider>
|
|
36
|
-
);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const useDraggableSwitcher = () => {
|
|
40
|
-
const context = useContext(DraggableContext);
|
|
41
|
-
if (!context) {
|
|
42
|
-
throw new Error('useDraggableSwitcher must be used inside a DraggableProvider');
|
|
43
|
-
}
|
|
44
|
-
return context;
|
|
45
|
-
};
|
|
1
|
+
'use client';
|
|
2
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface DraggableContextType {
|
|
6
|
+
isDraggable: boolean;
|
|
7
|
+
switchDraggable: (value: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DraggableContext = createContext<DraggableContextType | undefined>(undefined);
|
|
11
|
+
|
|
12
|
+
export const DraggableProvider = ({ children }: { children: ReactNode }) => {
|
|
13
|
+
const [isDraggable, setIsDraggable] = useState(false);
|
|
14
|
+
|
|
15
|
+
// Charger depuis localStorage au premier rendu
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const stored = localStorage.getItem('isDraggable');
|
|
18
|
+
if (stored !== null) {
|
|
19
|
+
setIsDraggable(stored === 'true'); // localStorage stocke toujours des strings
|
|
20
|
+
}
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
// Sauvegarder dans localStorage quand ça change
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
localStorage.setItem('isDraggable', String(isDraggable));
|
|
26
|
+
}, [isDraggable]);
|
|
27
|
+
|
|
28
|
+
const switchDraggable = (value: boolean) => {
|
|
29
|
+
setIsDraggable(value);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<DraggableContext.Provider value={{ isDraggable, switchDraggable }}>
|
|
34
|
+
{children}
|
|
35
|
+
</DraggableContext.Provider>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const useDraggableSwitcher = () => {
|
|
40
|
+
const context = useContext(DraggableContext);
|
|
41
|
+
if (!context) {
|
|
42
|
+
throw new Error('useDraggableSwitcher must be used inside a DraggableProvider');
|
|
43
|
+
}
|
|
44
|
+
return context;
|
|
45
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './DraggableSwitcherButton';
|
|
2
|
-
export * from './context/useDraggableSwitcher';
|
|
1
|
+
export * from './DraggableSwitcherButton';
|
|
2
|
+
export * from './context/useDraggableSwitcher';
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import styles from './assets/SelectInput.module.css';
|
|
4
|
-
|
|
5
|
-
interface SelectInputProps {
|
|
6
|
-
field: any;
|
|
7
|
-
value: any;
|
|
8
|
-
onChange: (value: any) => void;
|
|
9
|
-
onBlur: () => void;
|
|
10
|
-
error?: string;
|
|
11
|
-
touched?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const SelectInput: React.FC<SelectInputProps> = ({
|
|
15
|
-
field,
|
|
16
|
-
value,
|
|
17
|
-
onChange,
|
|
18
|
-
onBlur,
|
|
19
|
-
error,
|
|
20
|
-
touched,
|
|
21
|
-
}) => {
|
|
22
|
-
const hasError = Boolean(touched && error);
|
|
23
|
-
const displayLabel = field.label || '';
|
|
24
|
-
const placeholder = field.placeholder || '';
|
|
25
|
-
|
|
26
|
-
// Trouve le label de l'option sélectionnée si présente
|
|
27
|
-
const selectedOptionLabel =
|
|
28
|
-
field.options?.find((o: any) => String(o.value) === String(value))?.label ?? '';
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<div
|
|
32
|
-
className={`${styles.wrapper} ${hasError ? styles.wrapperInvalid : ''} ${
|
|
33
|
-
value ? styles.wrapperHasValue : ''
|
|
34
|
-
}`}
|
|
35
|
-
>
|
|
36
|
-
{/* Select natif (masqué visuellement mais accessible) */}
|
|
37
|
-
<select
|
|
38
|
-
id={field.name}
|
|
39
|
-
name={field.name}
|
|
40
|
-
className={styles.nativeSelect}
|
|
41
|
-
value={value ?? ''}
|
|
42
|
-
onChange={(e) => onChange(e.target.value)}
|
|
43
|
-
onBlur={onBlur}
|
|
44
|
-
aria-invalid={hasError}
|
|
45
|
-
aria-label={displayLabel}
|
|
46
|
-
>
|
|
47
|
-
{/* Placeholder : visible quand le select est fermé, caché dans la liste */}
|
|
48
|
-
<option value='' disabled hidden>
|
|
49
|
-
{placeholder || `Sélectionnez ${displayLabel?.toLowerCase() || ''}`}
|
|
50
|
-
</option>
|
|
51
|
-
|
|
52
|
-
{field.options?.map((option: any, idx: number) => (
|
|
53
|
-
<option key={idx} value={option.value} className={styles.optionStyle}>
|
|
54
|
-
{option.label}
|
|
55
|
-
</option>
|
|
56
|
-
))}
|
|
57
|
-
</select>
|
|
58
|
-
|
|
59
|
-
{/* Couche visuelle */}
|
|
60
|
-
<div className={styles.display}>
|
|
61
|
-
{displayLabel && <span className={styles.topLabel}>{displayLabel}</span>}
|
|
62
|
-
<span
|
|
63
|
-
className={`${styles.valueLine} ${value ? styles.valueActive : styles.valuePlaceholder}`}
|
|
64
|
-
>
|
|
65
|
-
{value ? selectedOptionLabel : placeholder}
|
|
66
|
-
</span>
|
|
67
|
-
|
|
68
|
-
{/* caret (SVG) */}
|
|
69
|
-
<span className={styles.caret} aria-hidden='true'></span>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
{hasError && <span className={styles.error}>{error}</span>}
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
};
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import styles from './assets/SelectInput.module.css';
|
|
4
|
+
|
|
5
|
+
interface SelectInputProps {
|
|
6
|
+
field: any;
|
|
7
|
+
value: any;
|
|
8
|
+
onChange: (value: any) => void;
|
|
9
|
+
onBlur: () => void;
|
|
10
|
+
error?: string;
|
|
11
|
+
touched?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const SelectInput: React.FC<SelectInputProps> = ({
|
|
15
|
+
field,
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
onBlur,
|
|
19
|
+
error,
|
|
20
|
+
touched,
|
|
21
|
+
}) => {
|
|
22
|
+
const hasError = Boolean(touched && error);
|
|
23
|
+
const displayLabel = field.label || '';
|
|
24
|
+
const placeholder = field.placeholder || '';
|
|
25
|
+
|
|
26
|
+
// Trouve le label de l'option sélectionnée si présente
|
|
27
|
+
const selectedOptionLabel =
|
|
28
|
+
field.options?.find((o: any) => String(o.value) === String(value))?.label ?? '';
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className={`${styles.wrapper} ${hasError ? styles.wrapperInvalid : ''} ${
|
|
33
|
+
value ? styles.wrapperHasValue : ''
|
|
34
|
+
}`}
|
|
35
|
+
>
|
|
36
|
+
{/* Select natif (masqué visuellement mais accessible) */}
|
|
37
|
+
<select
|
|
38
|
+
id={field.name}
|
|
39
|
+
name={field.name}
|
|
40
|
+
className={styles.nativeSelect}
|
|
41
|
+
value={value ?? ''}
|
|
42
|
+
onChange={(e) => onChange(e.target.value)}
|
|
43
|
+
onBlur={onBlur}
|
|
44
|
+
aria-invalid={hasError}
|
|
45
|
+
aria-label={displayLabel}
|
|
46
|
+
>
|
|
47
|
+
{/* Placeholder : visible quand le select est fermé, caché dans la liste */}
|
|
48
|
+
<option value='' disabled hidden>
|
|
49
|
+
{placeholder || `Sélectionnez ${displayLabel?.toLowerCase() || ''}`}
|
|
50
|
+
</option>
|
|
51
|
+
|
|
52
|
+
{field.options?.map((option: any, idx: number) => (
|
|
53
|
+
<option key={idx} value={option.value} className={styles.optionStyle}>
|
|
54
|
+
{option.label}
|
|
55
|
+
</option>
|
|
56
|
+
))}
|
|
57
|
+
</select>
|
|
58
|
+
|
|
59
|
+
{/* Couche visuelle */}
|
|
60
|
+
<div className={styles.display}>
|
|
61
|
+
{displayLabel && <span className={styles.topLabel}>{displayLabel}</span>}
|
|
62
|
+
<span
|
|
63
|
+
className={`${styles.valueLine} ${value ? styles.valueActive : styles.valuePlaceholder}`}
|
|
64
|
+
>
|
|
65
|
+
{value ? selectedOptionLabel : placeholder}
|
|
66
|
+
</span>
|
|
67
|
+
|
|
68
|
+
{/* caret (SVG) */}
|
|
69
|
+
<span className={styles.caret} aria-hidden='true'></span>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{hasError && <span className={styles.error}>{error}</span>}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|