@addev-be/ui 0.3.5 → 0.4.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/assets/icons/circle-check.svg +1 -0
- package/assets/icons/circle-info.svg +1 -0
- package/assets/icons/circle-xmark.svg +1 -0
- package/assets/icons/triangle-exclamation.svg +1 -0
- package/package.json +1 -1
- package/src/Icons.tsx +12 -4
- package/src/components/auth/LoginForm.tsx +83 -0
- package/src/components/auth/LoginPage.tsx +32 -0
- package/src/components/auth/PasswordRecoveryForm.tsx +52 -0
- package/src/components/auth/PasswordResetForm.tsx +112 -0
- package/src/components/auth/index.ts +4 -0
- package/src/components/auth/styles.ts +14 -0
- package/src/components/data/DataGrid/DataGridRowTemplate.tsx +69 -0
- package/src/components/data/DataGrid/VirtualScroller.tsx +7 -6
- package/src/components/data/DataGrid/hooks/useDataGrid.tsx +14 -0
- package/src/components/data/DataGrid/index.tsx +2 -82
- package/src/components/data/DataGrid/types.ts +9 -0
- package/src/components/forms/VerticalLabel.tsx +20 -0
- package/src/components/forms/index.ts +1 -1
- package/src/components/forms/styles.ts +12 -1
- package/src/components/index.ts +2 -0
- package/src/components/search/QuickSearchBar.tsx +31 -29
- package/src/components/search/styles.ts +37 -18
- package/src/components/ui/Card/index.tsx +14 -0
- package/src/components/ui/Card/styles.ts +35 -0
- package/src/components/ui/Message/index.tsx +57 -0
- package/src/components/ui/Message/styles.ts +40 -0
- package/src/components/ui/index.ts +3 -0
- package/src/providers/ThemeProvider/ThemeProvider.ts +8 -0
- package/src/services/WebSocketService.ts +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-111 111-47-47c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l64 64c9.4 9.4 24.6 9.4 33.9 0L369 209z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336c-13.3 0-24 10.7-24 24s10.7 24 24 24h80c13.3 0 24-10.7 24-24s-10.7-24-24-24h-8V248c0-13.3-10.7-24-24-24H216c-13.3 0-24 10.7-24 24s10.7 24 24 24h24v64H216zm40-144a32 32 0 1 0 0-64 32 32 0 1 0 0 64z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M248.4 84.3c1.6-2.7 4.5-4.3 7.6-4.3s6 1.6 7.6 4.3L461.9 410c1.4 2.3 2.1 4.9 2.1 7.5c0 8-6.5 14.5-14.5 14.5H62.5c-8 0-14.5-6.5-14.5-14.5c0-2.7 .7-5.3 2.1-7.5L248.4 84.3zm-41-25L9.1 385c-6 9.8-9.1 21-9.1 32.5C0 452 28 480 62.5 480h387c34.5 0 62.5-28 62.5-62.5c0-11.5-3.2-22.7-9.1-32.5L304.6 59.3C294.3 42.4 275.9 32 256 32s-38.3 10.4-48.6 27.3zM288 368a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm-8-184c0-13.3-10.7-24-24-24s-24 10.7-24 24v96c0 13.3 10.7 24 24 24s24-10.7 24-24V184z"/></svg>
|
package/package.json
CHANGED
package/src/Icons.tsx
CHANGED
|
@@ -10,6 +10,9 @@ import ArrowsRotateIcon from '../assets/icons/arrows-rotate.svg?react';
|
|
|
10
10
|
import ArrowsUpDownIcon from '../assets/icons/arrows-up-down.svg?react';
|
|
11
11
|
import CheckIcon from '../assets/icons/check.svg?react';
|
|
12
12
|
import ChevronDownIcon from '../assets/icons/chevron-down.svg?react';
|
|
13
|
+
import CircleCheckIcon from '../assets/icons/circle-check.svg?react';
|
|
14
|
+
import CircleInfoIcon from '../assets/icons/circle-info.svg?react';
|
|
15
|
+
import CircleXMarkIcon from '../assets/icons/circle-xmark.svg?react';
|
|
13
16
|
import CopyIcon from '../assets/icons/copy.svg?react';
|
|
14
17
|
import DownIcon from '../assets/icons/down.svg?react';
|
|
15
18
|
import EllipsisIcon from '../assets/icons/ellipsis.svg?react';
|
|
@@ -32,6 +35,7 @@ import TableFooterIcon from '../assets/icons/table-footer.svg?react';
|
|
|
32
35
|
import TableFooterSlashIcon from '../assets/icons/table-footer-slash.svg?react';
|
|
33
36
|
import TableIcon from '../assets/icons/table.svg?react';
|
|
34
37
|
import TallyIcon from '../assets/icons/tally.svg?react';
|
|
38
|
+
import TriangleExclamationIcon from '../assets/icons/triangle-exclamation.svg?react';
|
|
35
39
|
import UpIcon from '../assets/icons/up.svg?react';
|
|
36
40
|
import UserTieIcon from '../assets/icons/user-tie.svg?react';
|
|
37
41
|
import XBarIcon from '../assets/icons/x-bar.svg?react';
|
|
@@ -70,16 +74,17 @@ export const LoadingIcon: FC<IconFCProps> = ({ className, ...props }) => (
|
|
|
70
74
|
);
|
|
71
75
|
|
|
72
76
|
export {
|
|
73
|
-
ArrowDownAZIcon,
|
|
74
77
|
ArrowDown19Icon,
|
|
78
|
+
ArrowDownAZIcon,
|
|
75
79
|
ArrowDownBigSmallIcon,
|
|
76
|
-
ArrowUpZAIcon,
|
|
77
|
-
ArrowUpBigSmallIcon,
|
|
78
|
-
ArrowUp91Icon,
|
|
79
80
|
ArrowsRotateIcon,
|
|
80
81
|
ArrowsUpDownIcon,
|
|
82
|
+
ArrowUp91Icon,
|
|
83
|
+
ArrowUpBigSmallIcon,
|
|
84
|
+
ArrowUpZAIcon,
|
|
81
85
|
CheckIcon,
|
|
82
86
|
ChevronDownIcon,
|
|
87
|
+
CircleXMarkIcon,
|
|
83
88
|
CopyIcon,
|
|
84
89
|
DownIcon,
|
|
85
90
|
EllipsisIcon,
|
|
@@ -105,4 +110,7 @@ export {
|
|
|
105
110
|
UpIcon,
|
|
106
111
|
UserTieIcon,
|
|
107
112
|
XBarIcon,
|
|
113
|
+
CircleCheckIcon,
|
|
114
|
+
CircleInfoIcon,
|
|
115
|
+
TriangleExclamationIcon,
|
|
108
116
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Button, Input } from '../forms';
|
|
2
|
+
import { Card, Message } from '../ui';
|
|
3
|
+
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Link, redirect } from 'react-router-dom';
|
|
5
|
+
|
|
6
|
+
import { FormContainer } from './styles';
|
|
7
|
+
import { StackedLabel } from '../forms/VerticalLabel';
|
|
8
|
+
import { useAuthentication } from '../../providers';
|
|
9
|
+
|
|
10
|
+
export const LoginForm: FC = () => {
|
|
11
|
+
const [username, setUsername] = useState(
|
|
12
|
+
localStorage.getItem('username') || ''
|
|
13
|
+
);
|
|
14
|
+
const [password, setPassword] = useState('');
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState('');
|
|
17
|
+
const usernameInputRef = useRef<HTMLInputElement>(null);
|
|
18
|
+
const passwordInputRef = useRef<HTMLInputElement>(null);
|
|
19
|
+
|
|
20
|
+
const { login } = useAuthentication();
|
|
21
|
+
|
|
22
|
+
const onLoginClicked = useCallback(() => {
|
|
23
|
+
setError('');
|
|
24
|
+
setIsLoading(true);
|
|
25
|
+
login(username, password).then((success) => {
|
|
26
|
+
setIsLoading(false);
|
|
27
|
+
if (success) {
|
|
28
|
+
redirect('/');
|
|
29
|
+
} else {
|
|
30
|
+
setError('Identifiants invalides');
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}, [login, password, username]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (usernameInputRef.current && passwordInputRef.current) {
|
|
37
|
+
const input = !username
|
|
38
|
+
? usernameInputRef.current
|
|
39
|
+
: passwordInputRef.current;
|
|
40
|
+
input.select();
|
|
41
|
+
input.focus();
|
|
42
|
+
}
|
|
43
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
if (isLoading) return <div>Chargement...</div>;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Card>
|
|
50
|
+
<FormContainer>
|
|
51
|
+
<StackedLabel label="Adresse e-mail / Nom d'utilisateur">
|
|
52
|
+
<Input
|
|
53
|
+
ref={usernameInputRef}
|
|
54
|
+
type="email"
|
|
55
|
+
autoComplete="email"
|
|
56
|
+
required
|
|
57
|
+
value={username}
|
|
58
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
59
|
+
/>
|
|
60
|
+
</StackedLabel>
|
|
61
|
+
|
|
62
|
+
<StackedLabel label="Mot de passe">
|
|
63
|
+
<Input
|
|
64
|
+
ref={passwordInputRef}
|
|
65
|
+
type="password"
|
|
66
|
+
autoComplete="current-password"
|
|
67
|
+
required
|
|
68
|
+
value={password}
|
|
69
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
70
|
+
/>
|
|
71
|
+
</StackedLabel>
|
|
72
|
+
|
|
73
|
+
{error && <Message type="error">{error}</Message>}
|
|
74
|
+
|
|
75
|
+
<Button color="primary" onClick={onLoginClicked}>
|
|
76
|
+
Se connecter
|
|
77
|
+
</Button>
|
|
78
|
+
|
|
79
|
+
<Link to="/recover">Mot de passe oublié ?</Link>
|
|
80
|
+
</FormContainer>
|
|
81
|
+
</Card>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { Outlet } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
export const LoginPage: FC = () => {
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex flex-row h-full w-full">
|
|
7
|
+
<div className="relative hidden w-0 flex-1 lg:block">
|
|
8
|
+
<img className="absolute inset-0 h-full w-full object-cover" alt="" />
|
|
9
|
+
</div>
|
|
10
|
+
<div className="flex flex-1 flex-col justify-space-evenly overflow-y-auto px-4 py-12 sm:px-6 lg:flex-none lg:px-24 xl:px-36">
|
|
11
|
+
<div className="mx-auto w-full max-w-sm lg:max-w-96 lg:w-96 xl:max-w-[32rem] xl:w-[32rem]">
|
|
12
|
+
<div className="text-center">
|
|
13
|
+
<img
|
|
14
|
+
className="h-20 w-auto inline-block"
|
|
15
|
+
src="/logo192.png"
|
|
16
|
+
alt="Burotel"
|
|
17
|
+
/>
|
|
18
|
+
<h2 className="mt-8 text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
|
19
|
+
Connectez-vous
|
|
20
|
+
</h2>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-[640px]">
|
|
24
|
+
<div className="bg-white px-6 py-12 shadow sm:rounded-lg sm:px-12">
|
|
25
|
+
<Outlet />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Button, Input } from '../forms';
|
|
2
|
+
import { FC, useCallback, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { Card } from '../ui';
|
|
5
|
+
import { FormContainer } from './styles';
|
|
6
|
+
import { Link } from 'react-router-dom';
|
|
7
|
+
import { Message } from '../ui/Message';
|
|
8
|
+
import { StackedLabel } from '../forms/VerticalLabel';
|
|
9
|
+
import { useAuthentication } from '../../providers';
|
|
10
|
+
|
|
11
|
+
export const PasswordRecoveryForm: FC = () => {
|
|
12
|
+
const [email, setEmail] = useState('');
|
|
13
|
+
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
14
|
+
const { sendRecoveryKey } = useAuthentication();
|
|
15
|
+
|
|
16
|
+
const onSubmitClicked = useCallback(() => {
|
|
17
|
+
sendRecoveryKey(email).then(() => {
|
|
18
|
+
setIsSubmitted(true);
|
|
19
|
+
});
|
|
20
|
+
}, [email, sendRecoveryKey]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Card>
|
|
24
|
+
<FormContainer>
|
|
25
|
+
{isSubmitted ? (
|
|
26
|
+
<>
|
|
27
|
+
<Message type="success">
|
|
28
|
+
Un e-mail de réinitialisation de mot de passe vous a été envoyé.
|
|
29
|
+
</Message>
|
|
30
|
+
<Link to="/">Retour</Link>
|
|
31
|
+
</>
|
|
32
|
+
) : (
|
|
33
|
+
<>
|
|
34
|
+
<StackedLabel label="Adresse e-mail">
|
|
35
|
+
<Input
|
|
36
|
+
type="email"
|
|
37
|
+
autoComplete="email"
|
|
38
|
+
required
|
|
39
|
+
value={email}
|
|
40
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
41
|
+
/>
|
|
42
|
+
</StackedLabel>
|
|
43
|
+
|
|
44
|
+
<Button color="primary" onClick={onSubmitClicked}>
|
|
45
|
+
Envoyer un lien de récupération
|
|
46
|
+
</Button>
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
</FormContainer>
|
|
50
|
+
</Card>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Button, Card, Input, useAuthentication } from '@addev-be/ui';
|
|
2
|
+
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Link, useParams } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
import { FormContainer } from './styles';
|
|
6
|
+
import { Message } from '../ui/Message';
|
|
7
|
+
import { StackedLabel } from '../forms/VerticalLabel';
|
|
8
|
+
|
|
9
|
+
export const PasswordResetForm: FC = () => {
|
|
10
|
+
const { key } = useParams<{ key: string }>();
|
|
11
|
+
const [password1, setPassword1] = useState('');
|
|
12
|
+
const [password2, setPassword2] = useState('');
|
|
13
|
+
const [keyStatus, setKeyStatus] = useState(-1);
|
|
14
|
+
const [status, setStatus] = useState(-1);
|
|
15
|
+
const [error, setError] = useState('');
|
|
16
|
+
const { resetPassword, checkRecoveryKey } = useAuthentication();
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (key) {
|
|
20
|
+
checkRecoveryKey(key).then((status) => {
|
|
21
|
+
setKeyStatus(status);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}, [checkRecoveryKey, key]);
|
|
25
|
+
|
|
26
|
+
const onSubmitClicked = useCallback(() => {
|
|
27
|
+
if (key) {
|
|
28
|
+
if (password1 !== password2) {
|
|
29
|
+
setError('Les mots de passe ne sont pas identiques');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
resetPassword(key, password1).then((status) => {
|
|
33
|
+
setStatus(status);
|
|
34
|
+
setError(
|
|
35
|
+
status === 4
|
|
36
|
+
? 'Le mot de passe est trop faible (min. 8 caractères requis)'
|
|
37
|
+
: ''
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}, [key, password1, password2, resetPassword]);
|
|
42
|
+
|
|
43
|
+
const content = useMemo(() => {
|
|
44
|
+
if (keyStatus < 0) {
|
|
45
|
+
return <p>Chargement...</p>;
|
|
46
|
+
}
|
|
47
|
+
if (keyStatus > 0) {
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<Message type="error">
|
|
51
|
+
La clé de récupération fournie est invalide ou expirée
|
|
52
|
+
</Message>
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
switch (status) {
|
|
58
|
+
case 0:
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<Message type="success">
|
|
62
|
+
Votre mot de passe a été réinitialisé avec succès. Vous pouvez
|
|
63
|
+
maintenant vous connecter.
|
|
64
|
+
</Message>
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
default:
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}, [keyStatus, status]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Card>
|
|
75
|
+
<FormContainer>
|
|
76
|
+
{content ?? (
|
|
77
|
+
<>
|
|
78
|
+
<StackedLabel label="Nouveau mot de passe">
|
|
79
|
+
<Input
|
|
80
|
+
type="password"
|
|
81
|
+
autoComplete="current-password"
|
|
82
|
+
required
|
|
83
|
+
value={password1}
|
|
84
|
+
onChange={(e) => setPassword1(e.target.value)}
|
|
85
|
+
/>
|
|
86
|
+
</StackedLabel>
|
|
87
|
+
<StackedLabel label="Confirmation du mot de passe">
|
|
88
|
+
<Input
|
|
89
|
+
type="password"
|
|
90
|
+
autoComplete="current-password"
|
|
91
|
+
required
|
|
92
|
+
value={password2}
|
|
93
|
+
onChange={(e) => setPassword2(e.target.value)}
|
|
94
|
+
/>
|
|
95
|
+
</StackedLabel>
|
|
96
|
+
|
|
97
|
+
{error && (
|
|
98
|
+
<Message className="mt-4" type="error">
|
|
99
|
+
{error}
|
|
100
|
+
</Message>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<Button color="primary" onClick={onSubmitClicked}>
|
|
104
|
+
Réinitialiser le mot de passe
|
|
105
|
+
</Button>
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
<Link to="/">Retour</Link>
|
|
109
|
+
</FormContainer>
|
|
110
|
+
</Card>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const FormContainer = styled.form.attrs({
|
|
4
|
+
className: 'FormContainer',
|
|
5
|
+
onSubmit: (e) => e.preventDefault(),
|
|
6
|
+
})`
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: var(--space-4);
|
|
10
|
+
|
|
11
|
+
& > a {
|
|
12
|
+
text-align: center;
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as styles from './styles';
|
|
2
|
+
|
|
3
|
+
import { DataGridCell } from './DataGridCell';
|
|
4
|
+
import { DataGridRowTemplateProps } from './types';
|
|
5
|
+
import { useContext } from 'react';
|
|
6
|
+
|
|
7
|
+
export const DataGridRowTemplate = <R,>({
|
|
8
|
+
row,
|
|
9
|
+
rowIndex,
|
|
10
|
+
context,
|
|
11
|
+
}: DataGridRowTemplateProps<R>) => {
|
|
12
|
+
const { visibleColumns, rowKeyGetter, toggleSelection, ...props } =
|
|
13
|
+
useContext(context);
|
|
14
|
+
|
|
15
|
+
if (!row) {
|
|
16
|
+
return (
|
|
17
|
+
<styles.DataGridRow key={`loading-row-${rowIndex}`}>
|
|
18
|
+
{!!props.selectable && (
|
|
19
|
+
<styles.LoadingCell className="animate-pulse">
|
|
20
|
+
<div />
|
|
21
|
+
</styles.LoadingCell>
|
|
22
|
+
)}
|
|
23
|
+
{visibleColumns.map((_, index) => (
|
|
24
|
+
<styles.LoadingCell
|
|
25
|
+
className="animate-pulse"
|
|
26
|
+
key={`loading-${rowIndex}-${index}`}
|
|
27
|
+
>
|
|
28
|
+
<div />
|
|
29
|
+
</styles.LoadingCell>
|
|
30
|
+
))}
|
|
31
|
+
</styles.DataGridRow>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const key = rowKeyGetter(row);
|
|
35
|
+
const selected = props.selectedKeys.includes(key);
|
|
36
|
+
const { className, style } = props.rowClassNameGetter?.(row) ?? {
|
|
37
|
+
className: '',
|
|
38
|
+
style: undefined,
|
|
39
|
+
};
|
|
40
|
+
return (
|
|
41
|
+
<styles.DataGridRow key={key}>
|
|
42
|
+
{!!props.selectable && (
|
|
43
|
+
<styles.SelectionCell
|
|
44
|
+
key="__select_checkbox__"
|
|
45
|
+
onClick={() => toggleSelection(key)}
|
|
46
|
+
>
|
|
47
|
+
<input
|
|
48
|
+
type="checkbox"
|
|
49
|
+
value={key as string}
|
|
50
|
+
checked={selected}
|
|
51
|
+
readOnly
|
|
52
|
+
/>
|
|
53
|
+
</styles.SelectionCell>
|
|
54
|
+
)}
|
|
55
|
+
{visibleColumns.map(([key, col], index) => (
|
|
56
|
+
<DataGridCell
|
|
57
|
+
key={`loading-${rowIndex}-${index}`}
|
|
58
|
+
{...(index === 0 ? { className, style } : {})}
|
|
59
|
+
row={row}
|
|
60
|
+
rowIndex={rowIndex}
|
|
61
|
+
column={col}
|
|
62
|
+
columnIndex={index}
|
|
63
|
+
context={context}
|
|
64
|
+
columnKey={key}
|
|
65
|
+
/>
|
|
66
|
+
))}
|
|
67
|
+
</styles.DataGridRow>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import * as styles from './styles';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { DataGridContext } from './types';
|
|
3
|
+
import { DataGridContext, DataGridRowTemplateProps } from './types';
|
|
4
|
+
import { FC, useContext } from 'react';
|
|
6
5
|
|
|
7
6
|
type VirtualScrollerProps<R> = {
|
|
8
7
|
showAllRows?: boolean;
|
|
9
|
-
rowTemplate:
|
|
8
|
+
rowTemplate: FC<DataGridRowTemplateProps<R>>;
|
|
10
9
|
hasFooter?: boolean;
|
|
11
10
|
context: DataGridContext<R>;
|
|
12
11
|
onRangeChange?: (startIndex: number, length: number) => void;
|
|
@@ -22,7 +21,7 @@ export const VirtualScroller = <R,>(props: VirtualScrollerProps<R>) => {
|
|
|
22
21
|
gridTemplateColumns,
|
|
23
22
|
} = useContext(props.context);
|
|
24
23
|
const {
|
|
25
|
-
rowTemplate,
|
|
24
|
+
rowTemplate: RowTemplate,
|
|
26
25
|
// hasFooter, onRangeChange
|
|
27
26
|
} = props;
|
|
28
27
|
|
|
@@ -39,7 +38,9 @@ export const VirtualScroller = <R,>(props: VirtualScrollerProps<R>) => {
|
|
|
39
38
|
$topPadding={topPadding}
|
|
40
39
|
$rowHeight={rowHeight}
|
|
41
40
|
>
|
|
42
|
-
{visibleRows.map(
|
|
41
|
+
{visibleRows.map((row, index) => (
|
|
42
|
+
<RowTemplate row={row} rowIndex={index} context={props.context} />
|
|
43
|
+
))}
|
|
43
44
|
</styles.VirtualScrollerRowsContainer>
|
|
44
45
|
</styles.VirtualScrollerContainer>
|
|
45
46
|
);
|
|
@@ -92,6 +92,17 @@ export const useDataGrid = <R,>(
|
|
|
92
92
|
onSelectionChange?.(selectedKeys);
|
|
93
93
|
}, [onSelectionChange, selectedKeys]);
|
|
94
94
|
|
|
95
|
+
const toggleSelection = useCallback(
|
|
96
|
+
(key: string) => {
|
|
97
|
+
if (selectedKeys.includes(key)) {
|
|
98
|
+
setSelectedKeys(selectedKeys.filter((p) => p !== key));
|
|
99
|
+
} else {
|
|
100
|
+
setSelectedKeys([...selectedKeys, key]);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[selectedKeys, setSelectedKeys]
|
|
104
|
+
);
|
|
105
|
+
|
|
95
106
|
/** ROWS FILTERING **/
|
|
96
107
|
|
|
97
108
|
const [filters, setFilters] = useState<DataGridFilters>({});
|
|
@@ -240,6 +251,7 @@ export const useDataGrid = <R,>(
|
|
|
240
251
|
footers,
|
|
241
252
|
setFooters,
|
|
242
253
|
footerFunctions,
|
|
254
|
+
toggleSelection,
|
|
243
255
|
...override,
|
|
244
256
|
}),
|
|
245
257
|
[
|
|
@@ -265,6 +277,7 @@ export const useDataGrid = <R,>(
|
|
|
265
277
|
gridTemplateColumns,
|
|
266
278
|
footers,
|
|
267
279
|
footerFunctions,
|
|
280
|
+
toggleSelection,
|
|
268
281
|
override,
|
|
269
282
|
]
|
|
270
283
|
);
|
|
@@ -298,6 +311,7 @@ export const useDataGrid = <R,>(
|
|
|
298
311
|
gridTemplateColumns: '',
|
|
299
312
|
footers: {},
|
|
300
313
|
setFooters: () => {},
|
|
314
|
+
toggleSelection: () => {},
|
|
301
315
|
}),
|
|
302
316
|
[]
|
|
303
317
|
);
|
|
@@ -2,11 +2,10 @@ import * as styles from './styles';
|
|
|
2
2
|
|
|
3
3
|
import { DataGridContextProps, DataGridProps } from './types';
|
|
4
4
|
|
|
5
|
-
import { DataGridCell } from './DataGridCell';
|
|
6
5
|
import { DataGridFooter } from './DataGridFooter';
|
|
7
6
|
import { DataGridHeader } from './DataGridHeader';
|
|
7
|
+
import { DataGridRowTemplate } from './DataGridRowTemplate';
|
|
8
8
|
import { VirtualScroller } from './VirtualScroller';
|
|
9
|
-
import { useCallback } from 'react';
|
|
10
9
|
import { useDataGrid } from './hooks';
|
|
11
10
|
|
|
12
11
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -25,95 +24,16 @@ export const DataGrid = <R,>({
|
|
|
25
24
|
} = props;
|
|
26
25
|
const [contextProps, DataGridContext] = useDataGrid(props, contextOverride);
|
|
27
26
|
const {
|
|
28
|
-
selectedKeys,
|
|
29
|
-
setSelectedKeys,
|
|
30
27
|
columns,
|
|
31
|
-
visibleColumns,
|
|
32
28
|
rowHeight = 32,
|
|
33
29
|
headerRowHeight = 40,
|
|
34
30
|
scrollableRef,
|
|
35
31
|
onScroll,
|
|
36
|
-
rowKeyGetter,
|
|
37
32
|
} = contextProps;
|
|
38
33
|
|
|
39
34
|
const hasFooter = Object.values(columns).some((col) => col.footer);
|
|
40
35
|
|
|
41
|
-
const
|
|
42
|
-
(key: string) => {
|
|
43
|
-
if (selectedKeys.includes(key)) {
|
|
44
|
-
setSelectedKeys(selectedKeys.filter((p) => p !== key));
|
|
45
|
-
} else {
|
|
46
|
-
setSelectedKeys([...selectedKeys, key]);
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
[selectedKeys, setSelectedKeys]
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const rowTemplate = useCallback(
|
|
53
|
-
(row: R, rowIndex: number) => {
|
|
54
|
-
if (!row) {
|
|
55
|
-
return (
|
|
56
|
-
<styles.DataGridRow key={`loading-row-${rowIndex}`}>
|
|
57
|
-
{!!props.selectable && (
|
|
58
|
-
<styles.LoadingCell className="animate-pulse">
|
|
59
|
-
<div />
|
|
60
|
-
</styles.LoadingCell>
|
|
61
|
-
)}
|
|
62
|
-
{visibleColumns.map((_, index) => (
|
|
63
|
-
<styles.LoadingCell
|
|
64
|
-
className="animate-pulse"
|
|
65
|
-
key={`loading-${rowIndex}-${index}`}
|
|
66
|
-
>
|
|
67
|
-
<div />
|
|
68
|
-
</styles.LoadingCell>
|
|
69
|
-
))}
|
|
70
|
-
</styles.DataGridRow>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
const key = rowKeyGetter(row);
|
|
74
|
-
const { className, style } = props.rowClassNameGetter?.(row) ?? {
|
|
75
|
-
className: '',
|
|
76
|
-
style: undefined,
|
|
77
|
-
};
|
|
78
|
-
return (
|
|
79
|
-
<styles.DataGridRow key={key}>
|
|
80
|
-
{!!props.selectable && (
|
|
81
|
-
<styles.SelectionCell
|
|
82
|
-
key="__select_checkbox__"
|
|
83
|
-
onClick={() => toggleSelection(key)}
|
|
84
|
-
>
|
|
85
|
-
<input
|
|
86
|
-
type="checkbox"
|
|
87
|
-
value={key as string}
|
|
88
|
-
checked={selectedKeys.includes(key)}
|
|
89
|
-
readOnly
|
|
90
|
-
/>
|
|
91
|
-
</styles.SelectionCell>
|
|
92
|
-
)}
|
|
93
|
-
{visibleColumns.map(([key, col], index) => (
|
|
94
|
-
<DataGridCell
|
|
95
|
-
key={`loading-${rowIndex}-${index}`}
|
|
96
|
-
{...(index === 0 ? { className, style } : {})}
|
|
97
|
-
row={row}
|
|
98
|
-
rowIndex={rowIndex}
|
|
99
|
-
column={col}
|
|
100
|
-
columnIndex={index}
|
|
101
|
-
context={DataGridContext}
|
|
102
|
-
columnKey={key}
|
|
103
|
-
/>
|
|
104
|
-
))}
|
|
105
|
-
</styles.DataGridRow>
|
|
106
|
-
);
|
|
107
|
-
},
|
|
108
|
-
[
|
|
109
|
-
DataGridContext,
|
|
110
|
-
props,
|
|
111
|
-
rowKeyGetter,
|
|
112
|
-
selectedKeys,
|
|
113
|
-
toggleSelection,
|
|
114
|
-
visibleColumns,
|
|
115
|
-
]
|
|
116
|
-
);
|
|
36
|
+
const rowTemplate = DataGridRowTemplate;
|
|
117
37
|
|
|
118
38
|
return (
|
|
119
39
|
<DataGridContext.Provider value={contextProps}>
|
|
@@ -131,6 +131,7 @@ export type DataGridContextProps<R> = DataGridProps<R> & {
|
|
|
131
131
|
length: number;
|
|
132
132
|
rowKeyGetter: (row: R) => string;
|
|
133
133
|
gridTemplateColumns: string;
|
|
134
|
+
toggleSelection: (key: string) => void;
|
|
134
135
|
};
|
|
135
136
|
|
|
136
137
|
export type DataGridContext<R> = Context<DataGridContextProps<R>>;
|
|
@@ -265,3 +266,11 @@ export type DataGridFilterCheckbox = {
|
|
|
265
266
|
values: DataGridFilterValue[];
|
|
266
267
|
level: number;
|
|
267
268
|
};
|
|
269
|
+
|
|
270
|
+
export type DataGridRowTemplateProps<R> = {
|
|
271
|
+
row: R | null;
|
|
272
|
+
rowIndex: number;
|
|
273
|
+
selected?: boolean;
|
|
274
|
+
toggleSelection?: () => void;
|
|
275
|
+
context: DataGridContext<R>;
|
|
276
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FC, HTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
import { StackedLabelContainer } from './styles';
|
|
4
|
+
|
|
5
|
+
type StackedLabelProps = HTMLAttributes<HTMLDivElement> & {
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const StackedLabel: FC<StackedLabelProps> = ({
|
|
10
|
+
label,
|
|
11
|
+
children,
|
|
12
|
+
...props
|
|
13
|
+
}) => {
|
|
14
|
+
return (
|
|
15
|
+
<StackedLabelContainer {...props}>
|
|
16
|
+
<label>{label}</label>
|
|
17
|
+
{children}
|
|
18
|
+
</StackedLabelContainer>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -15,6 +15,17 @@ export const inputStyle = css`
|
|
|
15
15
|
}
|
|
16
16
|
`;
|
|
17
17
|
|
|
18
|
-
export const Input = styled.input
|
|
18
|
+
export const Input = styled.input.attrs({
|
|
19
|
+
className: 'Input',
|
|
20
|
+
})`
|
|
19
21
|
${inputStyle}
|
|
20
22
|
`;
|
|
23
|
+
|
|
24
|
+
export const StackedLabelContainer = styled.div.attrs({
|
|
25
|
+
className: 'StackedLabelContainer',
|
|
26
|
+
})`
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
gap: var(--space-1);
|
|
30
|
+
color: var(--color-gray-900);
|
|
31
|
+
`;
|
package/src/components/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import * as styles from './styles';
|
|
2
|
+
|
|
1
3
|
import { SearchResults, SearchTypeDefinitions } from './types';
|
|
2
4
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
5
|
|
|
4
6
|
import { Dropdown } from '../layout';
|
|
5
|
-
import {
|
|
7
|
+
import { Input } from '../forms';
|
|
6
8
|
import { QuickSearchResults } from './QuickSearchResults';
|
|
7
9
|
import { useDebounce } from '@uidotdev/usehooks';
|
|
8
10
|
import { useGlobalSearchRequestHandler } from '../../services';
|
|
@@ -18,8 +20,10 @@ export const QuickSearchBar = <R,>({ definitions }: QuickSearchBarProps<R>) => {
|
|
|
18
20
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
21
|
const [results, setResults] = useState<SearchResults<R> | null>(null);
|
|
20
22
|
|
|
23
|
+
const fakeInputRef = useRef<HTMLInputElement | null>(null);
|
|
21
24
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
22
|
-
const rect =
|
|
25
|
+
const rect = fakeInputRef.current?.getBoundingClientRect() ?? new DOMRect();
|
|
26
|
+
const destRect = new DOMRect(rect.x, rect.y, rect.width, 0);
|
|
23
27
|
const globalSearch = useGlobalSearchRequestHandler();
|
|
24
28
|
|
|
25
29
|
useEffect(() => {
|
|
@@ -36,41 +40,39 @@ export const QuickSearchBar = <R,>({ definitions }: QuickSearchBarProps<R>) => {
|
|
|
36
40
|
}, [globalSearch, debouncedTerm]);
|
|
37
41
|
|
|
38
42
|
const onFocus = useCallback(() => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
const input = inputRef.current;
|
|
46
|
-
input?.addEventListener('focus', onFocus);
|
|
47
|
-
return () => {
|
|
48
|
-
input?.removeEventListener('focus', onFocus);
|
|
49
|
-
};
|
|
50
|
-
}, [onFocus]);
|
|
43
|
+
setDropdownVisible(true);
|
|
44
|
+
requestAnimationFrame(() => {
|
|
45
|
+
inputRef.current?.focus();
|
|
46
|
+
});
|
|
47
|
+
}, []);
|
|
51
48
|
|
|
52
49
|
return (
|
|
53
50
|
<>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
value={term}
|
|
57
|
-
onChange={(e) => setTerm(e.target.value)}
|
|
58
|
-
ref={inputRef}
|
|
59
|
-
$opened={dropdownVisible}
|
|
60
|
-
/>
|
|
61
|
-
{results && dropdownVisible && rect && (
|
|
51
|
+
<Input type="text" ref={fakeInputRef} value={term} onFocus={onFocus} />
|
|
52
|
+
{dropdownVisible && rect && (
|
|
62
53
|
<Dropdown
|
|
63
|
-
$sourceRect={
|
|
54
|
+
$sourceRect={destRect}
|
|
64
55
|
onClose={() => setDropdownVisible(false)}
|
|
65
56
|
$width={rect.width}
|
|
66
|
-
$height={[250, 400]}
|
|
57
|
+
$height={[results ? 250 : rect.height, 400]}
|
|
67
58
|
$autoPositionY={false}
|
|
68
59
|
>
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
<styles.QuickSearchDropdownContainer>
|
|
61
|
+
<Input
|
|
62
|
+
type="text"
|
|
63
|
+
ref={inputRef}
|
|
64
|
+
value={term}
|
|
65
|
+
onChange={(e) => setTerm(e.target.value)}
|
|
66
|
+
onClick={(e) => e.stopPropagation()}
|
|
67
|
+
/>
|
|
68
|
+
{results && (
|
|
69
|
+
<QuickSearchResults
|
|
70
|
+
results={results}
|
|
71
|
+
definitions={definitions}
|
|
72
|
+
term={debouncedTerm}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
</styles.QuickSearchDropdownContainer>
|
|
74
76
|
</Dropdown>
|
|
75
77
|
)}
|
|
76
78
|
</>
|
|
@@ -1,41 +1,64 @@
|
|
|
1
|
-
import { Input } from '../forms';
|
|
2
1
|
import styled from 'styled-components';
|
|
3
2
|
|
|
4
|
-
export const
|
|
3
|
+
export const QuickSearchDropdownContainer = styled.div.attrs({
|
|
4
|
+
className: 'QuickSearchDropdownContainer',
|
|
5
|
+
})`
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
height: 100%;
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
export const QuickSearchResultsContainer = styled.div.attrs({
|
|
12
|
+
className: 'QuickSearchResultsContainer',
|
|
13
|
+
})`
|
|
5
14
|
display: flex;
|
|
6
15
|
flex-direction: row;
|
|
7
16
|
height: 100%;
|
|
17
|
+
flex: 1;
|
|
18
|
+
padding: var(--space-2) 0;
|
|
19
|
+
overflow: hidden;
|
|
8
20
|
`;
|
|
9
21
|
|
|
10
|
-
export const QuickSearchResultsListContainer = styled.div
|
|
22
|
+
export const QuickSearchResultsListContainer = styled.div.attrs({
|
|
23
|
+
className: 'QuickSearchResultsListContainer',
|
|
24
|
+
})`
|
|
11
25
|
display: flex;
|
|
12
26
|
flex-direction: column;
|
|
13
27
|
padding: var(--space-2);
|
|
14
|
-
border-right: 1px solid var(--color-
|
|
15
|
-
flex:
|
|
28
|
+
border-right: 1px solid var(--color-neutral-200);
|
|
29
|
+
flex: 1;
|
|
16
30
|
overflow: auto;
|
|
17
31
|
`;
|
|
18
32
|
|
|
19
|
-
export const QuickSearchResultsTitle = styled.div
|
|
33
|
+
export const QuickSearchResultsTitle = styled.div.attrs({
|
|
34
|
+
className: 'QuickSearchResultsTitle',
|
|
35
|
+
})`
|
|
20
36
|
margin: 0;
|
|
21
37
|
margin-bottom: var(--space-1);
|
|
22
38
|
&:not(:first-child) {
|
|
23
39
|
margin-top: var(--space-2);
|
|
24
40
|
}
|
|
25
41
|
font-weight: bold;
|
|
26
|
-
font-size: var(--text-
|
|
42
|
+
font-size: var(--text-sm);
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 120%;
|
|
45
|
+
color: var(--color-neutral-500);
|
|
27
46
|
`;
|
|
28
47
|
|
|
29
|
-
export const QuickSearchResultsItem = styled.div
|
|
48
|
+
export const QuickSearchResultsItem = styled.div.attrs({
|
|
49
|
+
className: 'QuickSearchResultsItem',
|
|
50
|
+
})`
|
|
30
51
|
padding: var(--space-2) var(--space-3);
|
|
31
52
|
cursor: pointer;
|
|
32
53
|
border-radius: 4px;
|
|
33
54
|
&:hover {
|
|
34
|
-
background-color: var(--color-
|
|
55
|
+
background-color: var(--color-neutral-100);
|
|
35
56
|
}
|
|
36
57
|
`;
|
|
37
58
|
|
|
38
|
-
export const QuickSearchResultsDetailsContainer = styled.div
|
|
59
|
+
export const QuickSearchResultsDetailsContainer = styled.div.attrs({
|
|
60
|
+
className: 'QuickSearchResultsDetailsContainer',
|
|
61
|
+
})`
|
|
39
62
|
display: flex;
|
|
40
63
|
flex-direction: column;
|
|
41
64
|
padding: var(--space-2);
|
|
@@ -46,15 +69,11 @@ export const QuickSearchResultsDetailsDivider = styled.hr`
|
|
|
46
69
|
margin: var(--space-2) 0;
|
|
47
70
|
height: 1px;
|
|
48
71
|
border: none;
|
|
49
|
-
background-color: var(--color-
|
|
72
|
+
background-color: var(--color-neutral-200);
|
|
50
73
|
`;
|
|
51
74
|
|
|
52
|
-
export const QuickSearchResultDetailsTitle = styled.div
|
|
75
|
+
export const QuickSearchResultDetailsTitle = styled.div.attrs({
|
|
76
|
+
className: 'QuickSearchResultDetailsTitle',
|
|
77
|
+
})`
|
|
53
78
|
margin: 0;
|
|
54
79
|
`;
|
|
55
|
-
|
|
56
|
-
export const QuickSearchBarInput = styled(Input)<{ $opened?: boolean }>`
|
|
57
|
-
position: relative;
|
|
58
|
-
width: 100%;
|
|
59
|
-
z-index: ${({ $opened }) => ($opened ? 10000 : 0)};
|
|
60
|
-
`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CardContainer, CardFooter, CardHeader } from './styles';
|
|
2
|
+
import { FC, HTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
type CardFC = FC<HTMLAttributes<HTMLDivElement>> & {
|
|
5
|
+
Header: typeof CardHeader;
|
|
6
|
+
Footer: typeof CardFooter;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const Card: CardFC = ({ children }) => {
|
|
10
|
+
return <CardContainer>{children}</CardContainer>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
Card.Header = CardHeader;
|
|
14
|
+
Card.Footer = CardFooter;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const CardContainer = styled.div.attrs({
|
|
4
|
+
className: 'CardContainer',
|
|
5
|
+
})`
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
background-color: var(--color-white);
|
|
9
|
+
border-radius: var(--rounded-md);
|
|
10
|
+
box-shadow: var(--shadow-md);
|
|
11
|
+
padding: var(--space-4);
|
|
12
|
+
border: 1px solid var(--color-neutral-100);
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export const CardHeader = styled.div.attrs({
|
|
16
|
+
className: 'CardHeader',
|
|
17
|
+
})`
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: row;
|
|
20
|
+
align-items: center;
|
|
21
|
+
padding-bottom: var(--space-2);
|
|
22
|
+
border-bottom: 1px solid var(--color-neutral-200);
|
|
23
|
+
margin-bottom: var(--space-2);
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export const CardFooter = styled.div.attrs({
|
|
27
|
+
className: 'CardFooter',
|
|
28
|
+
})`
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: row;
|
|
31
|
+
align-items: center;
|
|
32
|
+
padding-top: var(--space-2);
|
|
33
|
+
border-top: 1px solid var(--color-neutral-200);
|
|
34
|
+
margin-top: var(--space-2);
|
|
35
|
+
`;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as styles from './styles';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CircleCheckIcon,
|
|
5
|
+
CircleInfoIcon,
|
|
6
|
+
CircleXMarkIcon,
|
|
7
|
+
TriangleExclamationIcon,
|
|
8
|
+
} from '../../../Icons';
|
|
9
|
+
import { FC, PropsWithChildren } from 'react';
|
|
10
|
+
|
|
11
|
+
import { ThemeColor } from '../../../providers';
|
|
12
|
+
|
|
13
|
+
type MessageType = 'success' | 'info' | 'warning' | 'error';
|
|
14
|
+
|
|
15
|
+
type MessageProps = PropsWithChildren<{
|
|
16
|
+
className?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
type: MessageType;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
const colorsMap: Record<
|
|
22
|
+
MessageType,
|
|
23
|
+
{
|
|
24
|
+
baseColor: ThemeColor;
|
|
25
|
+
iconComponent: FC<{ className?: string }>;
|
|
26
|
+
}
|
|
27
|
+
> = {
|
|
28
|
+
success: {
|
|
29
|
+
baseColor: 'green',
|
|
30
|
+
iconComponent: CircleCheckIcon,
|
|
31
|
+
},
|
|
32
|
+
info: {
|
|
33
|
+
baseColor: 'sky',
|
|
34
|
+
iconComponent: CircleInfoIcon,
|
|
35
|
+
},
|
|
36
|
+
warning: {
|
|
37
|
+
baseColor: 'amber',
|
|
38
|
+
iconComponent: TriangleExclamationIcon,
|
|
39
|
+
},
|
|
40
|
+
error: {
|
|
41
|
+
baseColor: 'red',
|
|
42
|
+
iconComponent: CircleXMarkIcon,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Message: FC<MessageProps> = ({ children, title, type }) => {
|
|
47
|
+
const { iconComponent: Icon, baseColor = 'neutral' } = colorsMap[type];
|
|
48
|
+
return (
|
|
49
|
+
<styles.MessageContainer $baseColor={baseColor}>
|
|
50
|
+
<Icon className="MessageIcon" />
|
|
51
|
+
<styles.MessageContent>
|
|
52
|
+
{title && <h3>{title}</h3>}
|
|
53
|
+
<div>{children}</div>
|
|
54
|
+
</styles.MessageContent>
|
|
55
|
+
</styles.MessageContainer>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
import { ThemeColor } from '../../../providers';
|
|
4
|
+
|
|
5
|
+
export const MessageContainer = styled.div.attrs({
|
|
6
|
+
className: 'MessageContainer',
|
|
7
|
+
})<{
|
|
8
|
+
$baseColor: ThemeColor;
|
|
9
|
+
}>`
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: row;
|
|
12
|
+
box-shadow: var(--shadow-md);
|
|
13
|
+
border-radius: var(--rounded-md);
|
|
14
|
+
padding: var(--space-4);
|
|
15
|
+
gap: var(--space-4);
|
|
16
|
+
|
|
17
|
+
${({ $baseColor }) => css`
|
|
18
|
+
border: 1px solid var(--color-${$baseColor}-300);
|
|
19
|
+
background-color: var(--color-${$baseColor}-100);
|
|
20
|
+
color: var(--color-${$baseColor}-600);
|
|
21
|
+
|
|
22
|
+
.MessageIcon {
|
|
23
|
+
fill: var(--color-${$baseColor}-600);
|
|
24
|
+
}
|
|
25
|
+
`}
|
|
26
|
+
|
|
27
|
+
.MessageIcon {
|
|
28
|
+
width: var(--space-5);
|
|
29
|
+
height: var(--space-5);
|
|
30
|
+
flex-shrink: 0;
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
export const MessageContent = styled.div.attrs({
|
|
35
|
+
className: 'MessageContent',
|
|
36
|
+
})`
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
flex-grow: 1;
|
|
40
|
+
`;
|
|
@@ -52,4 +52,12 @@ export const ThemeProvider = styled.div<ThemeProviderProps>`
|
|
|
52
52
|
getThemeValuesCss('shadow', $theme.shadows),
|
|
53
53
|
].join('');
|
|
54
54
|
}}
|
|
55
|
+
|
|
56
|
+
a, a:visited {
|
|
57
|
+
color: var(--color-primary-500);
|
|
58
|
+
}
|
|
59
|
+
a:active,
|
|
60
|
+
a:hover {
|
|
61
|
+
color: var(--color-primary-700);
|
|
62
|
+
}
|
|
55
63
|
`;
|