tycho-components 0.0.1

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 (67) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.cjs +28 -0
  3. package/.eslintrc.json +31 -0
  4. package/.gitlab-ci.yml +14 -0
  5. package/.storybook/main.ts +32 -0
  6. package/.storybook/preview-head.html +4 -0
  7. package/.storybook/preview.css +6 -0
  8. package/.storybook/preview.tsx +29 -0
  9. package/README.md +93 -0
  10. package/package.json +66 -0
  11. package/src/AppColorpicker/AppColorpicker.tsx +69 -0
  12. package/src/AppColorpicker/index.tsx +3 -0
  13. package/src/AppColorpicker/style.scss +38 -0
  14. package/src/AppEditable/AppEditable.tsx +280 -0
  15. package/src/AppEditable/AppEditableField.ts +7 -0
  16. package/src/AppEditable/FormField.ts +26 -0
  17. package/src/AppEditable/FormFieldOption.ts +38 -0
  18. package/src/AppEditable/index.tsx +3 -0
  19. package/src/AppEditable/style.scss +94 -0
  20. package/src/AppModal/AppModal.tsx +93 -0
  21. package/src/AppModal/AppModalConfirm.tsx +62 -0
  22. package/src/AppModal/AppModalRemove.tsx +51 -0
  23. package/src/AppModal/index.tsx +3 -0
  24. package/src/AppModal/style.scss +65 -0
  25. package/src/AppToast/AppToast.tsx +94 -0
  26. package/src/AppToast/ToastMessage.ts +9 -0
  27. package/src/AppToast/index.tsx +3 -0
  28. package/src/AppToast/style.scss +0 -0
  29. package/src/Dummy/Dummy.stories.tsx +21 -0
  30. package/src/Dummy/Dummy.tsx +16 -0
  31. package/src/Dummy/index.tsx +3 -0
  32. package/src/Dummy/styles.scss +6 -0
  33. package/src/Participants/ParticipantCreate/ParticipantCreate.tsx +86 -0
  34. package/src/Participants/ParticipantCreate/index.tsx +3 -0
  35. package/src/Participants/ParticipantCreate/style.scss +32 -0
  36. package/src/Participants/ParticipantRemove/ParticipantRemove.tsx +51 -0
  37. package/src/Participants/ParticipantRemove/index.tsx +3 -0
  38. package/src/Participants/ParticipantRemove/style.scss +32 -0
  39. package/src/Participants/Participants.stories.tsx +45 -0
  40. package/src/Participants/Participants.tsx +145 -0
  41. package/src/Participants/index.tsx +3 -0
  42. package/src/Participants/style.scss +44 -0
  43. package/src/Participants/types/Participant.ts +43 -0
  44. package/src/Participants/types/ParticipantService.ts +18 -0
  45. package/src/configs/CommonContext.tsx +23 -0
  46. package/src/configs/CookieStorage.ts +36 -0
  47. package/src/configs/Localization.ts +28 -0
  48. package/src/configs/MessageUtils.ts +60 -0
  49. package/src/configs/Storage.ts +21 -0
  50. package/src/configs/api.ts +49 -0
  51. package/src/configs/localization/CommonTexts.ts +26 -0
  52. package/src/configs/localization/ParticipantsTexts.ts +40 -0
  53. package/src/configs/store/actions.ts +12 -0
  54. package/src/configs/store/reducer.ts +22 -0
  55. package/src/configs/store/store.ts +9 -0
  56. package/src/configs/store/types.ts +16 -0
  57. package/src/index.ts +4 -0
  58. package/src/react-app-env.d.ts +5 -0
  59. package/src/styles/_variables.scss +67 -0
  60. package/src/styles/bootstrap.min.css +9871 -0
  61. package/src/styles/main.scss +57 -0
  62. package/src/vite-env.d.ts +13 -0
  63. package/stories/Configure.mdx +171 -0
  64. package/stories/StorybookUtils.tsx +40 -0
  65. package/tsconfig.json +31 -0
  66. package/tsconfig.node.json +10 -0
  67. package/vite.config.ts +30 -0
@@ -0,0 +1,65 @@
1
+ @use 'tycho-storybook/src/styles/main' as *;
2
+
3
+ .modal-container {
4
+ .header {
5
+ display: flex;
6
+ align-items: center;
7
+ border-bottom: 1px solid var(--border-subtle-1);
8
+ padding: var(--spacing-300) var(--spacing-200) var(--spacing-150);
9
+
10
+ .titles {
11
+ .title {
12
+ @include label-large-2;
13
+ display: block;
14
+ }
15
+
16
+ .subtitle {
17
+ display: block;
18
+ @include body-medium-1;
19
+ color: var(--text-secondary);
20
+ }
21
+ }
22
+
23
+ .ds-icon {
24
+ margin-left: auto;
25
+ margin-bottom: auto;
26
+ }
27
+ }
28
+
29
+ .body {
30
+ padding: var(--spacing-300) var(--spacing-200) var(--spacing-150);
31
+ }
32
+
33
+ .footer {
34
+ display: flex;
35
+ justify-content: end;
36
+ gap: 10px;
37
+ padding: var(--spacing-200) var(--spacing-200) var(--spacing-150);
38
+ }
39
+
40
+ &.modal-remove {
41
+ .body {
42
+ display: flex;
43
+ gap: 16px;
44
+
45
+ > .ds-icon {
46
+ color: var(--icon-warning);
47
+ }
48
+
49
+ .texts {
50
+ display: flex;
51
+ flex-direction: column;
52
+
53
+ > .title {
54
+ @include subtitle-medium-2;
55
+ color: var(--text-primary);
56
+ }
57
+
58
+ > .subtitle {
59
+ @include body-medium-1;
60
+ color: var(--text-secondary);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,94 @@
1
+ import CommonContext from '@/configs/CommonContext';
2
+ import { message } from '@/configs/store/actions';
3
+ import { useContext, useEffect } from 'react';
4
+ import ReactLoading from 'react-loading';
5
+ import { ToastContainer, toast } from 'react-toastify';
6
+ import 'react-toastify/dist/ReactToastify.css';
7
+ import { EMPTY_TOAST } from './ToastMessage';
8
+
9
+ export default function AppToast() {
10
+ const { dispatch, state } = useContext(CommonContext);
11
+
12
+ const handleClose = () => {
13
+ toast.dismiss();
14
+ dispatch(message(EMPTY_TOAST));
15
+ };
16
+
17
+ const handleClipboard = () => {
18
+ navigator.clipboard.writeText(state.message.value);
19
+ };
20
+
21
+ const getLoading = () => (
22
+ <div className="d-flex">
23
+ <ReactLoading
24
+ type="spinningBubbles"
25
+ color="blue"
26
+ height={24}
27
+ width={24}
28
+ />
29
+ <span className="ms-3">Loading...</span>
30
+ </div>
31
+ );
32
+
33
+ const attachCloseToEscape = () => {
34
+ const closeOnEscape = (e: any) => {
35
+ if (e.keyCode === 27) handleClose();
36
+ };
37
+
38
+ window.addEventListener('keydown', closeOnEscape);
39
+ return () => window.removeEventListener('keydown', closeOnEscape);
40
+ };
41
+
42
+ useEffect(() => {
43
+ attachCloseToEscape();
44
+ }, []);
45
+
46
+ useEffect(() => {
47
+ if (state.toastLoading) {
48
+ toast(getLoading(), {
49
+ position: 'top-right',
50
+ autoClose: false,
51
+ hideProgressBar: false,
52
+ closeOnClick: false,
53
+ pauseOnHover: true,
54
+ draggable: false,
55
+ progress: undefined,
56
+ theme: 'light',
57
+ });
58
+ } else {
59
+ toast.dismiss();
60
+ }
61
+ }, [state.toastLoading]);
62
+
63
+ useEffect(() => {
64
+ if (state.message && state.message.value !== '') {
65
+ switch (state.message.type) {
66
+ case 'error':
67
+ toast.error(state.message.value, {
68
+ onClose: () => handleClose(),
69
+ onClick: () => handleClipboard(),
70
+ });
71
+ break;
72
+ case 'warning':
73
+ toast.warning(state.message.value, {
74
+ onClose: () => handleClose(),
75
+ });
76
+ break;
77
+ case 'success':
78
+ toast.success(state.message.value, {
79
+ onClose: () => handleClose(),
80
+ });
81
+ break;
82
+ default:
83
+ toast(state.message.value, {
84
+ onClose: () => handleClose(),
85
+ });
86
+ break;
87
+ }
88
+ } else {
89
+ dispatch(message(EMPTY_TOAST));
90
+ }
91
+ }, [state.message]);
92
+
93
+ return <ToastContainer closeOnClick />;
94
+ }
@@ -0,0 +1,9 @@
1
+ export default interface ToastMessage {
2
+ value: string;
3
+ type: string;
4
+ }
5
+
6
+ export const EMPTY_TOAST = {
7
+ value: '',
8
+ type: '',
9
+ };
@@ -0,0 +1,3 @@
1
+ import AppToast from './AppToast';
2
+
3
+ export default AppToast;
File without changes
@@ -0,0 +1,21 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import Dummy, { DummyModes } from './Dummy';
3
+
4
+ const meta = {
5
+ title: 'Components/Dummy',
6
+ component: Dummy,
7
+ parameters: {
8
+ layout: 'centered',
9
+ },
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ mode: { control: 'select', options: DummyModes },
13
+ },
14
+ } satisfies Meta<typeof Dummy>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ export const Primary: Story = {
20
+ args: {},
21
+ };
@@ -0,0 +1,16 @@
1
+ import cx from 'classnames';
2
+ import './styles.scss';
3
+
4
+ export const DummyModes = ['white', 'blue'] as const;
5
+ type DummyModes = (typeof DummyModes)[number];
6
+
7
+ export type Props = {
8
+ className?: string;
9
+ mode?: DummyModes;
10
+ };
11
+
12
+ export default function Dummy({ className, mode = 'blue' }: Props) {
13
+ const getClassNames = cx('ds-dummy', className, mode);
14
+
15
+ return <div className={getClassNames}>aaaa</div>;
16
+ }
@@ -0,0 +1,3 @@
1
+ import Dummy from './Dummy';
2
+
3
+ export default Dummy;
@@ -0,0 +1,6 @@
1
+ @use '../styles/main' as *;
2
+
3
+ .ds-dummy {
4
+ display: flex;
5
+ align-items: center;
6
+ }
@@ -0,0 +1,86 @@
1
+ import AppModal from '@/AppModal';
2
+ import CommonContext from '@/configs/CommonContext';
3
+ import { toastLoading } from '@/configs/store/actions';
4
+ import { useContext, useRef, useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import Participant, {
7
+ EMPTY_PARTICIPANT,
8
+ ParticipantCreateRequest,
9
+ } from '../types/Participant';
10
+ import ParticipantService from '../types/ParticipantService';
11
+ import { useForm } from 'react-hook-form';
12
+ import { TFunction } from 'i18next';
13
+ import * as yup from 'yup';
14
+ import { yupResolver } from '@hookform/resolvers/yup';
15
+ import './style.scss';
16
+ import { TextField } from 'tycho-storybook';
17
+
18
+ type Props = {
19
+ document: string;
20
+ order: number;
21
+ onClose: () => void;
22
+ onCreate: (p: Participant) => void;
23
+ };
24
+
25
+ export default function ParticipantCreate({
26
+ document,
27
+ order,
28
+ onClose,
29
+ onCreate,
30
+ }: Props) {
31
+ const { t } = useTranslation('participants');
32
+ const { dispatch, state } = useContext(CommonContext);
33
+
34
+ const createdForm = useForm<ParticipantCreateRequest>({
35
+ resolver: yupResolver(getFormSchema(t)),
36
+ mode: 'onChange',
37
+ });
38
+
39
+ const handleAdd = () => {
40
+ if (state.toastLoading) return;
41
+ dispatch(toastLoading(true));
42
+ ParticipantService.add(document, createdForm.getValues())
43
+ .then((r) => {
44
+ onCreate(r.data);
45
+ })
46
+ .finally(() => {
47
+ dispatch(toastLoading(false));
48
+ });
49
+ };
50
+
51
+ return (
52
+ <AppModal
53
+ title={t('modal.add.title')}
54
+ className="modal-participant"
55
+ close={onClose}
56
+ confirm={handleAdd}
57
+ disableConfirm={!createdForm.formState.isValid}
58
+ >
59
+ <div className="participant-container">
60
+ <TextField
61
+ label={t('modal.input.code')}
62
+ attr="code"
63
+ createdForm={createdForm}
64
+ showEndAdornment={false}
65
+ placeholder={t('common:generic.placeholder')}
66
+ required
67
+ />
68
+ <TextField
69
+ label={t('modal.input.name')}
70
+ attr="name"
71
+ createdForm={createdForm}
72
+ showEndAdornment={false}
73
+ placeholder={t('common:generic.placeholder')}
74
+ />
75
+ </div>
76
+ </AppModal>
77
+ );
78
+ }
79
+
80
+ const getFormSchema = (
81
+ t: TFunction
82
+ ): yup.ObjectSchema<ParticipantCreateRequest> =>
83
+ yup.object().shape({
84
+ code: yup.string().required(t('common:validation.required')),
85
+ name: yup.string().required(t('common:validation.required')),
86
+ });
@@ -0,0 +1,3 @@
1
+ import ParticipantCreate from './ParticipantCreate';
2
+
3
+ export default ParticipantCreate;
@@ -0,0 +1,32 @@
1
+ .participants-container {
2
+ .header {
3
+ display: flex;
4
+ background-color: var(--color-secondary);
5
+ padding: 8px 16px;
6
+
7
+ .actions {
8
+ margin-left: auto;
9
+ margin-top: auto;
10
+ margin-bottom: auto;
11
+
12
+ .action {
13
+ border: var(--border-default);
14
+ padding: 0px 16px;
15
+ }
16
+ }
17
+ }
18
+
19
+ .body {
20
+ height: 90vh;
21
+ overflow-y: auto;
22
+ padding: var(--spacing-small);
23
+ }
24
+
25
+ .footer {
26
+ margin-top: 16px;
27
+ padding-top: 16px;
28
+ display: flex;
29
+ justify-content: right;
30
+ border-top: var(--border-default);
31
+ }
32
+ }
@@ -0,0 +1,51 @@
1
+ import AppModalRemove from '@/AppModal/AppModalRemove';
2
+ import CommonContext from '@/configs/CommonContext';
3
+ import { toastLoading } from '@/configs/store/actions';
4
+ import { useContext } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import Participant from '../types/Participant';
7
+ import ParticipantService from '../types/ParticipantService';
8
+ import './style.scss';
9
+
10
+ type Props = {
11
+ participant: Participant;
12
+ participants: Participant[];
13
+ document: string;
14
+ onClose: () => void;
15
+ onChange: (p: Participant[]) => void;
16
+ };
17
+
18
+ export default function ParticipantRemove({
19
+ participant,
20
+ participants,
21
+ document,
22
+ onClose,
23
+ onChange,
24
+ }: Props) {
25
+ const { t } = useTranslation('participants');
26
+ const { dispatch, state } = useContext(CommonContext);
27
+
28
+ const handleRemove = () => {
29
+ if (state.toastLoading || !participant) return;
30
+ dispatch(toastLoading(true));
31
+ ParticipantService.remove(document, participant.code)
32
+ .then(() => {
33
+ const list =
34
+ participants?.filter((p) => p.code !== participant.code) || [];
35
+
36
+ onChange(list);
37
+ })
38
+ .finally(() => {
39
+ dispatch(toastLoading(false));
40
+ });
41
+ };
42
+
43
+ return (
44
+ <AppModalRemove
45
+ title={t('modal.remove.title')}
46
+ subtitle={t('modal.remove.description')}
47
+ onClose={onClose}
48
+ onConfirm={handleRemove}
49
+ />
50
+ );
51
+ }
@@ -0,0 +1,3 @@
1
+ import ParticipantRemove from './ParticipantRemove';
2
+
3
+ export default ParticipantRemove;
@@ -0,0 +1,32 @@
1
+ .participants-container {
2
+ .header {
3
+ display: flex;
4
+ background-color: var(--color-secondary);
5
+ padding: 8px 16px;
6
+
7
+ .actions {
8
+ margin-left: auto;
9
+ margin-top: auto;
10
+ margin-bottom: auto;
11
+
12
+ .action {
13
+ border: var(--border-default);
14
+ padding: 0px 16px;
15
+ }
16
+ }
17
+ }
18
+
19
+ .body {
20
+ height: 90vh;
21
+ overflow-y: auto;
22
+ padding: var(--spacing-small);
23
+ }
24
+
25
+ .footer {
26
+ margin-top: 16px;
27
+ padding-top: 16px;
28
+ display: flex;
29
+ justify-content: right;
30
+ border-top: var(--border-default);
31
+ }
32
+ }
@@ -0,0 +1,45 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import Participants from './Participants';
3
+ import type Participant from './types/Participant';
4
+ import { action } from '@storybook/addon-actions';
5
+
6
+ const meta: Meta<typeof Participants> = {
7
+ title: 'Components/Participants',
8
+ component: Participants,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ document: { control: 'text' },
12
+ },
13
+ };
14
+
15
+ export default meta;
16
+
17
+ type Story = StoryObj<typeof Participants>;
18
+
19
+ // Mock data for stories
20
+ const mockParticipants: Participant[] = [
21
+ {
22
+ code: 'P001',
23
+ name: 'Alice Johnson',
24
+ age: '29',
25
+ gender: 'F',
26
+ role: 'Interviewer',
27
+ order: 1,
28
+ },
29
+ {
30
+ code: 'P002',
31
+ name: 'Bob Smith',
32
+ age: '35',
33
+ gender: 'M',
34
+ role: 'Interviewee',
35
+ order: 2,
36
+ },
37
+ ];
38
+
39
+ export const Primary: Story = {
40
+ args: {
41
+ document: 'doc-001',
42
+ participants: mockParticipants,
43
+ onChange: action('Participants list changed'),
44
+ },
45
+ };
@@ -0,0 +1,145 @@
1
+ import AppEditable from '@/AppEditable';
2
+ import AppEditableField from '@/AppEditable/AppEditableField';
3
+ import CommonContext from '@/configs/CommonContext';
4
+ import { dispatchMessage } from '@/configs/MessageUtils';
5
+ import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons';
6
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7
+ import { useContext, useEffect, useState } from 'react';
8
+ import { Button, Form } from 'react-bootstrap';
9
+ import { useTranslation } from 'react-i18next';
10
+ import ParticipantCreate from './ParticipantCreate';
11
+ import ParticipantRemove from './ParticipantRemove';
12
+ import './style.scss';
13
+ import Participant, { PARTICIPANT_FIELDS } from './types/Participant';
14
+ import ParticipantService from './types/ParticipantService';
15
+
16
+ type Props = {
17
+ document: string;
18
+ onChange: (p: Participant[]) => void;
19
+ participants: Participant[];
20
+ };
21
+
22
+ export default function Participants({
23
+ document,
24
+ participants,
25
+ onChange,
26
+ }: Props) {
27
+ const { t } = useTranslation('participants');
28
+ const { dispatch, state } = useContext(CommonContext);
29
+
30
+ const [participant, setParticipant] = useState<Participant>();
31
+ const [openRemove, setOpenRemove] = useState(false);
32
+ const [openCreate, setOpenCreate] = useState(false);
33
+
34
+ const handleSave = (field: AppEditableField) => {
35
+ if (!field.ref) return;
36
+
37
+ ParticipantService.update(field)
38
+ .then(() => {
39
+ dispatchMessage({ key: 'update.success', dispatch, t });
40
+ const { name, value } = field;
41
+
42
+ const thisParticipants = participants;
43
+ const idx = participants.findIndex((p) => p.code === field.ref);
44
+
45
+ // updates the participant
46
+ const thisParticipant = { ...thisParticipants[idx], [name]: value };
47
+ thisParticipants[idx] = { ...thisParticipant };
48
+ setParticipant(thisParticipant);
49
+
50
+ onChange(thisParticipants);
51
+ })
52
+ .catch((err) => {
53
+ console.log(err);
54
+ });
55
+ };
56
+
57
+ useEffect(() => {
58
+ if (participants.length > 0) setParticipant(participants[0]);
59
+ }, []);
60
+
61
+ return (
62
+ <div className="participants-container">
63
+ <div className="header">
64
+ <h3>{t('label.title')}</h3>
65
+ <div className="actions">
66
+ <button
67
+ type="button"
68
+ className="action"
69
+ onClick={() => setOpenCreate(true)}
70
+ >
71
+ <FontAwesomeIcon icon={faPlus} title={t('button.label.add')} />
72
+ <span className="ms-2">add new participant</span>
73
+ </button>
74
+ </div>
75
+ </div>
76
+ <div className="body">
77
+ {participant && (
78
+ <>
79
+ <Form.Select
80
+ onChange={(e) =>
81
+ setParticipant(participants[Number(e.target.value)])
82
+ }
83
+ >
84
+ {participants.map((el, idx) => (
85
+ <option value={idx} key={idx}>
86
+ {`${el.code} - ${el.name}`}
87
+ </option>
88
+ ))}
89
+ </Form.Select>
90
+ <AppEditable
91
+ translation="participants"
92
+ group="participant"
93
+ save={handleSave}
94
+ item={{ ...participant, uid: document }}
95
+ fields={PARTICIPANT_FIELDS}
96
+ className="fields"
97
+ reference="code"
98
+ />
99
+ <div className="footer">
100
+ <Button
101
+ variant="danger"
102
+ onClick={() => {
103
+ setParticipant(participant);
104
+ setOpenRemove(true);
105
+ }}
106
+ >
107
+ <FontAwesomeIcon icon={faTimes} />
108
+ <span className="ms-1">Remove</span>
109
+ </Button>
110
+ </div>
111
+ </>
112
+ )}
113
+ </div>
114
+ {participant && openRemove && (
115
+ <ParticipantRemove
116
+ document={document}
117
+ onChange={(list) => {
118
+ onChange(list);
119
+ setParticipant(
120
+ participants.length > 0 ? participants[0] : undefined
121
+ );
122
+ setOpenRemove(false);
123
+ }}
124
+ participant={participant}
125
+ participants={participants}
126
+ onClose={() => setOpenRemove(false)}
127
+ />
128
+ )}
129
+
130
+ {openCreate && (
131
+ <ParticipantCreate
132
+ document={document}
133
+ order={participants.length || 1}
134
+ onCreate={(p) => {
135
+ const thisParticipants = participants ? [...participants, p] : [p];
136
+ onChange(thisParticipants);
137
+ setParticipant(p);
138
+ setOpenCreate(false);
139
+ }}
140
+ onClose={() => setOpenCreate(false)}
141
+ />
142
+ )}
143
+ </div>
144
+ );
145
+ }
@@ -0,0 +1,3 @@
1
+ import Participants from './Participants';
2
+
3
+ export default Participants;
@@ -0,0 +1,44 @@
1
+ .participants-container {
2
+ .header {
3
+ display: flex;
4
+ background-color: var(--color-secondary);
5
+ padding: 8px 16px;
6
+
7
+ .actions {
8
+ margin-left: auto;
9
+ margin-top: auto;
10
+ margin-bottom: auto;
11
+
12
+ .action {
13
+ border: var(--border-default);
14
+ padding: 0px 16px;
15
+ }
16
+ }
17
+ }
18
+
19
+ .body {
20
+ height: 90vh;
21
+ overflow-y: auto;
22
+ padding: var(--spacing-small);
23
+ }
24
+
25
+ .footer {
26
+ margin-top: 16px;
27
+ padding-top: 16px;
28
+ display: flex;
29
+ justify-content: right;
30
+ border-top: var(--border-default);
31
+ }
32
+ }
33
+
34
+ .modal-participant {
35
+ width: 20vw;
36
+ max-width: 20vw;
37
+
38
+ .body {
39
+ .participant-container {
40
+ display: flex;
41
+ gap: 8px;
42
+ }
43
+ }
44
+ }