tycho-components 0.37.2 → 0.38.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/dist/common/AppDropzone/AppDropzone.d.ts +1 -0
- package/dist/common/AppDropzone/AppDropzoneBody.d.ts +1 -0
- package/dist/common/AppDropzone/AppDropzoneBody.js +68 -17
- package/dist/common/AppDropzone/UploadService.d.ts +2 -0
- package/dist/common/AppDropzone/UploadService.js +10 -6
- package/dist/common/AppDropzone/style.scss +22 -4
- package/dist/configs/localization/UploadTexts.d.ts +30 -0
- package/dist/configs/localization/UploadTexts.js +30 -0
- package/dist/configs/types/User.d.ts +1 -0
- package/package.json +3 -3
|
@@ -7,6 +7,7 @@ type Props = {
|
|
|
7
7
|
accept: Record<string, string[]>;
|
|
8
8
|
messages?: Partial<Record<'label.dropzone' | 'label.uploaded.file', string>>;
|
|
9
9
|
onDrop?: () => void;
|
|
10
|
+
/** Initial value for the keep-original-name checkbox (default false). */
|
|
10
11
|
keepName?: boolean;
|
|
11
12
|
title?: string;
|
|
12
13
|
alternativeButton?: {
|
|
@@ -7,6 +7,7 @@ export type AppDropzoneBodyProps = {
|
|
|
7
7
|
accept: Record<string, string[]>;
|
|
8
8
|
messages?: Partial<Record<'label.dropzone' | 'label.uploaded.file', string>>;
|
|
9
9
|
onDrop?: () => void;
|
|
10
|
+
/** Initial value for the keep-original-name checkbox (default false). */
|
|
10
11
|
keepName?: boolean;
|
|
11
12
|
/** When false, the confirm button is hidden (e.g. when used inside modal with its own confirm) */
|
|
12
13
|
showConfirmButton?: boolean;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import Checkbox from '@mui/material/Checkbox';
|
|
3
|
+
import FormControlLabel from '@mui/material/FormControlLabel';
|
|
2
4
|
import { forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState, } from 'react';
|
|
3
5
|
import Dropzone from 'react-dropzone';
|
|
4
6
|
import { useTranslation } from 'react-i18next';
|
|
@@ -6,16 +8,25 @@ import { Button } from 'tycho-storybook';
|
|
|
6
8
|
import CommonContext from '../../configs/CommonContext';
|
|
7
9
|
import { toastLoading } from '../../configs/store/actions';
|
|
8
10
|
import { useMessageUtils } from '../../configs/useMessageUtils';
|
|
11
|
+
import AppModalConfirm from '../AppModal/AppModalConfirm';
|
|
9
12
|
import UploadService from './UploadService';
|
|
10
13
|
import './style.scss';
|
|
11
|
-
function
|
|
14
|
+
function isFileAlreadyExistsError(err) {
|
|
15
|
+
return (err?.response?.status === 409 &&
|
|
16
|
+
err?.response?.data?.description === 'upload.file.already.exists');
|
|
17
|
+
}
|
|
18
|
+
function AppDropzoneBodyInner({ folder, onSuccess, onError, accept, messages, onDrop, keepName = false, showConfirmButton = true, disablePreview = false, disableConfirm = false, }, ref) {
|
|
12
19
|
const { t } = useTranslation('upload');
|
|
13
20
|
const { state, dispatch } = useContext(CommonContext);
|
|
14
21
|
const { dispatchError } = useMessageUtils();
|
|
15
22
|
const [file, setFile] = useState();
|
|
16
23
|
const [preview, setPreview] = useState();
|
|
24
|
+
const [keepOriginalName, setKeepOriginalName] = useState(keepName);
|
|
25
|
+
const [replaceConfirmOpen, setReplaceConfirmOpen] = useState(false);
|
|
17
26
|
const fileRef = useRef();
|
|
27
|
+
const keepOriginalNameRef = useRef(keepOriginalName);
|
|
18
28
|
fileRef.current = file;
|
|
29
|
+
keepOriginalNameRef.current = keepOriginalName;
|
|
19
30
|
const isImageFile = (f) => {
|
|
20
31
|
return f.type?.startsWith('image/') ?? false;
|
|
21
32
|
};
|
|
@@ -31,29 +42,63 @@ function AppDropzoneBodyInner({ folder, onSuccess, onError, accept, messages, on
|
|
|
31
42
|
}
|
|
32
43
|
setPreview(undefined);
|
|
33
44
|
}, [file, disablePreview]);
|
|
34
|
-
const
|
|
45
|
+
const buildUploadFormData = (currentFile, replace) => {
|
|
46
|
+
const data = new FormData();
|
|
47
|
+
data.append('file', currentFile);
|
|
48
|
+
data.append('folder', folder);
|
|
49
|
+
if (keepOriginalNameRef.current) {
|
|
50
|
+
data.append('keepOriginalName', 'true');
|
|
51
|
+
}
|
|
52
|
+
if (replace) {
|
|
53
|
+
data.append('replace', 'true');
|
|
54
|
+
}
|
|
55
|
+
return data;
|
|
56
|
+
};
|
|
57
|
+
const executeUpload = async (currentFile, replace) => {
|
|
58
|
+
const response = await UploadService.execute(buildUploadFormData(currentFile, replace));
|
|
59
|
+
onSuccess(response.data);
|
|
60
|
+
};
|
|
61
|
+
const upload = async (replace = false) => {
|
|
35
62
|
const currentFile = fileRef.current;
|
|
36
63
|
if (!currentFile || state.toastLoading)
|
|
37
64
|
return;
|
|
38
65
|
dispatch(toastLoading(true));
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
try {
|
|
67
|
+
if (keepOriginalNameRef.current && !replace) {
|
|
68
|
+
const validateData = new FormData();
|
|
69
|
+
validateData.append('folder', folder);
|
|
70
|
+
validateData.append('filename', currentFile.name);
|
|
71
|
+
validateData.append('keepOriginalName', 'true');
|
|
72
|
+
try {
|
|
73
|
+
await UploadService.validate(validateData);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (isFileAlreadyExistsError(err)) {
|
|
77
|
+
dispatch(toastLoading(false));
|
|
78
|
+
setReplaceConfirmOpen(true);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
dispatchError({ err, t, key: 'upload' });
|
|
82
|
+
onError?.(err);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
await executeUpload(currentFile, replace);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (!replace && isFileAlreadyExistsError(err)) {
|
|
90
|
+
setReplaceConfirmOpen(true);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
dispatchError({ err, t, key: 'upload' });
|
|
49
94
|
onError?.(err);
|
|
50
|
-
}
|
|
51
|
-
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
52
97
|
dispatch(toastLoading(false));
|
|
53
|
-
}
|
|
98
|
+
}
|
|
54
99
|
};
|
|
55
100
|
useImperativeHandle(ref, () => ({
|
|
56
|
-
upload,
|
|
101
|
+
upload: () => upload(false),
|
|
57
102
|
}));
|
|
58
103
|
const handleDrop = (files) => {
|
|
59
104
|
if (files.length === 0)
|
|
@@ -64,11 +109,17 @@ function AppDropzoneBodyInner({ folder, onSuccess, onError, accept, messages, on
|
|
|
64
109
|
const handleCancel = () => {
|
|
65
110
|
setFile(undefined);
|
|
66
111
|
};
|
|
112
|
+
const handleReplaceConfirm = () => {
|
|
113
|
+
setReplaceConfirmOpen(false);
|
|
114
|
+
upload(true);
|
|
115
|
+
};
|
|
67
116
|
const resolveMessage = (key) => {
|
|
68
117
|
const customMessage = messages?.[key]?.trim();
|
|
69
118
|
return customMessage || t(key);
|
|
70
119
|
};
|
|
71
|
-
return (_jsxs("div", { className: "dropzone-container", children: [!file && (_jsx(Dropzone, { onDrop: (acceptedFiles) => handleDrop(acceptedFiles), accept: accept, maxFiles: 1, children: ({ getRootProps, getInputProps }) => (_jsxs("div", { ...getRootProps(), className: "dropzone", children: [_jsx("input", { ...getInputProps() }), _jsx("span", { children: resolveMessage('label.dropzone') })] })) })), file && (_jsxs("div", { className: "uploaded-file", children: [preview && _jsx("img", { className: "preview", src: preview, alt: "" }), !preview && (_jsxs(_Fragment, { children: [_jsx("b", { children: resolveMessage('label.uploaded.file') }), _jsx("span", { children: file.name })] })), showConfirmButton && (_jsxs("div", { className: "buttons", children: [_jsx(Button, { onClick: handleCancel, text: t('common:button.cancel'), size: "small", mode: "outlined", color: "danger" }), _jsx(Button, { onClick: upload, text: t('common:button.confirm'), size: "small", mode: "outlined", disabled: disableConfirm })] }))] }))
|
|
120
|
+
return (_jsxs("div", { className: "dropzone-container", children: [!file && (_jsx(Dropzone, { onDrop: (acceptedFiles) => handleDrop(acceptedFiles), accept: accept, maxFiles: 1, children: ({ getRootProps, getInputProps }) => (_jsxs("div", { ...getRootProps(), className: "dropzone", children: [_jsx("input", { ...getInputProps() }), _jsx("span", { children: resolveMessage('label.dropzone') })] })) })), file && (_jsxs("div", { className: "uploaded-file", children: [preview && _jsx("img", { className: "preview", src: preview, alt: "" }), !preview && (_jsxs(_Fragment, { children: [_jsx("b", { children: resolveMessage('label.uploaded.file') }), _jsx("span", { children: file.name })] })), showConfirmButton && (_jsxs("div", { className: "buttons", children: [_jsx(Button, { onClick: handleCancel, text: t('common:button.cancel'), size: "small", mode: "outlined", color: "danger" }), _jsx(Button, { onClick: () => upload(false), text: t('common:button.confirm'), size: "small", mode: "outlined", disabled: disableConfirm })] }))] })), _jsx(FormControlLabel, { className: "keep-name-checkbox", control: _jsx(Checkbox, { checked: keepOriginalName, onChange: (_, checked) => setKeepOriginalName(checked) }), label: t('label.keep.original.name') }), replaceConfirmOpen && (_jsx(AppModalConfirm, { title: t('replace.confirm.title'), subtitle: t('replace.confirm.subtitle', {
|
|
121
|
+
0: fileRef.current?.name ?? '',
|
|
122
|
+
}), onClose: () => setReplaceConfirmOpen(false), onConfirm: handleReplaceConfirm }))] }));
|
|
72
123
|
}
|
|
73
124
|
const AppDropzoneBody = forwardRef(AppDropzoneBodyInner);
|
|
74
125
|
export default AppDropzoneBody;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { UploadedFile } from './UploadedFile';
|
|
2
2
|
declare function execute(data: FormData): Promise<import("axios").AxiosResponse<UploadedFile, any, {}>>;
|
|
3
|
+
declare function validate(data: FormData): Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
3
4
|
declare const UploadService: {
|
|
4
5
|
execute: typeof execute;
|
|
6
|
+
validate: typeof validate;
|
|
5
7
|
};
|
|
6
8
|
export default UploadService;
|
|
@@ -5,14 +5,18 @@ const getJwtToken = () => {
|
|
|
5
5
|
const cookie = Cookies.get(JWT_TOKEN);
|
|
6
6
|
return cookie === 'undefined' ? '' : cookie;
|
|
7
7
|
};
|
|
8
|
+
const authHeaders = () => ({
|
|
9
|
+
Authorization: `Bearer ${getJwtToken() || ''}`,
|
|
10
|
+
});
|
|
8
11
|
function execute(data) {
|
|
9
|
-
const token = getJwtToken() || '';
|
|
10
|
-
const header = {
|
|
11
|
-
Authorization: `Bearer ${token}`,
|
|
12
|
-
};
|
|
13
12
|
return axios.post(import.meta.env.VITE_APP_UPLOAD_URL, data, {
|
|
14
|
-
headers:
|
|
13
|
+
headers: authHeaders(),
|
|
15
14
|
});
|
|
16
15
|
}
|
|
17
|
-
|
|
16
|
+
function validate(data) {
|
|
17
|
+
return axios.post(`${import.meta.env.VITE_APP_UPLOAD_URL}/validate`, data, {
|
|
18
|
+
headers: authHeaders(),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const UploadService = { execute, validate };
|
|
18
22
|
export default UploadService;
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
.dropzone-container {
|
|
2
2
|
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
3
4
|
align-items: center;
|
|
4
5
|
justify-content: center;
|
|
6
|
+
gap: var(--spacing-200);
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
width: 100%;
|
|
9
|
+
max-width: 100%;
|
|
10
|
+
min-width: 0;
|
|
5
11
|
|
|
6
12
|
.dropzone {
|
|
7
13
|
display: flex;
|
|
8
14
|
align-items: center;
|
|
9
15
|
justify-content: center;
|
|
10
16
|
text-align: center;
|
|
17
|
+
box-sizing: border-box;
|
|
11
18
|
width: 100%;
|
|
19
|
+
max-width: 100%;
|
|
12
20
|
padding: var(--spacing-700);
|
|
13
21
|
border-radius: var(--radius-100);
|
|
14
22
|
border: 2px dashed var(--border-subtle-3);
|
|
@@ -26,17 +34,27 @@
|
|
|
26
34
|
display: flex;
|
|
27
35
|
flex-direction: column;
|
|
28
36
|
align-items: center;
|
|
37
|
+
box-sizing: border-box;
|
|
29
38
|
width: 100%;
|
|
39
|
+
max-width: 100%;
|
|
30
40
|
text-align: center;
|
|
31
41
|
border: 1px solid var(--border-subtle-3);
|
|
32
42
|
border-radius: var(--radius-100);
|
|
33
|
-
padding:
|
|
34
|
-
gap:
|
|
43
|
+
padding: var(--spacing-200);
|
|
44
|
+
gap: var(--spacing-200);
|
|
35
45
|
|
|
36
46
|
.buttons {
|
|
37
47
|
display: flex;
|
|
38
|
-
gap:
|
|
39
|
-
margin-top:
|
|
48
|
+
gap: var(--spacing-200);
|
|
49
|
+
margin-top: var(--spacing-200);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.keep-name-checkbox {
|
|
54
|
+
align-self: flex-start;
|
|
55
|
+
|
|
56
|
+
> .MuiCheckbox-root {
|
|
57
|
+
padding: 0px 8px;
|
|
40
58
|
}
|
|
41
59
|
}
|
|
42
60
|
}
|
|
@@ -3,21 +3,51 @@ export declare const UploadTexts: {
|
|
|
3
3
|
'label.dropzone': string;
|
|
4
4
|
'label.uploaded.file': string;
|
|
5
5
|
'label.confirm': string;
|
|
6
|
+
'label.keep.original.name': string;
|
|
6
7
|
'modal.title': string;
|
|
7
8
|
'error.uploading.image': string;
|
|
9
|
+
'upload.file.already.exists': string;
|
|
10
|
+
'replace.confirm.title': string;
|
|
11
|
+
'replace.confirm.subtitle': string;
|
|
12
|
+
'upload.file.read.error': string;
|
|
13
|
+
'upload.file.type.detect.error': string;
|
|
14
|
+
'upload.file.type.unknown': string;
|
|
15
|
+
'upload.file.threat.detected': string;
|
|
16
|
+
'upload.file.write.error': string;
|
|
17
|
+
'upload.file.name.required': string;
|
|
8
18
|
};
|
|
9
19
|
'pt-BR': {
|
|
10
20
|
'label.dropzone': string;
|
|
11
21
|
'label.uploaded.file': string;
|
|
12
22
|
'label.confirm': string;
|
|
23
|
+
'label.keep.original.name': string;
|
|
13
24
|
'modal.title': string;
|
|
14
25
|
'error.uploading.image': string;
|
|
26
|
+
'upload.file.already.exists': string;
|
|
27
|
+
'replace.confirm.title': string;
|
|
28
|
+
'replace.confirm.subtitle': string;
|
|
29
|
+
'upload.file.read.error': string;
|
|
30
|
+
'upload.file.type.detect.error': string;
|
|
31
|
+
'upload.file.type.unknown': string;
|
|
32
|
+
'upload.file.threat.detected': string;
|
|
33
|
+
'upload.file.write.error': string;
|
|
34
|
+
'upload.file.name.required': string;
|
|
15
35
|
};
|
|
16
36
|
it: {
|
|
17
37
|
'label.dropzone': string;
|
|
18
38
|
'label.uploaded.file': string;
|
|
19
39
|
'label.confirm': string;
|
|
40
|
+
'label.keep.original.name': string;
|
|
20
41
|
'modal.title': string;
|
|
21
42
|
'error.uploading.image': string;
|
|
43
|
+
'upload.file.already.exists': string;
|
|
44
|
+
'replace.confirm.title': string;
|
|
45
|
+
'replace.confirm.subtitle': string;
|
|
46
|
+
'upload.file.read.error': string;
|
|
47
|
+
'upload.file.type.detect.error': string;
|
|
48
|
+
'upload.file.type.unknown': string;
|
|
49
|
+
'upload.file.threat.detected': string;
|
|
50
|
+
'upload.file.write.error': string;
|
|
51
|
+
'upload.file.name.required': string;
|
|
22
52
|
};
|
|
23
53
|
};
|
|
@@ -3,21 +3,51 @@ export const UploadTexts = {
|
|
|
3
3
|
'label.dropzone': 'Drag and drop or click to upload a file',
|
|
4
4
|
'label.uploaded.file': 'You are uploading the following file:',
|
|
5
5
|
'label.confirm': 'Press confirm to continue',
|
|
6
|
+
'label.keep.original.name': 'Keep original file name',
|
|
6
7
|
'modal.title': 'Upload a file',
|
|
7
8
|
'error.uploading.image': 'An error occurred while uploading a file. Contact the administrator.',
|
|
9
|
+
'upload.file.already.exists': 'A file named "{{0}}" already exists in this folder.',
|
|
10
|
+
'replace.confirm.title': 'Replace existing file?',
|
|
11
|
+
'replace.confirm.subtitle': 'A file named "{{0}}" already exists. Do you want to replace it?',
|
|
12
|
+
'upload.file.read.error': 'Could not read the uploaded file.',
|
|
13
|
+
'upload.file.type.detect.error': 'Could not detect the file type.',
|
|
14
|
+
'upload.file.type.unknown': 'Unknown file type; upload is not allowed.',
|
|
15
|
+
'upload.file.threat.detected': 'This file type is not allowed.',
|
|
16
|
+
'upload.file.write.error': 'Could not save the uploaded file.',
|
|
17
|
+
'upload.file.name.required': 'A file name is required for validation.',
|
|
8
18
|
},
|
|
9
19
|
'pt-BR': {
|
|
10
20
|
'label.dropzone': 'Arraste e solte ou clique para enviar um arquivo.',
|
|
11
21
|
'label.uploaded.file': 'Você está enviando o seguinte arquivo:',
|
|
12
22
|
'label.confirm': 'Pressione confirmar para continuar',
|
|
23
|
+
'label.keep.original.name': 'Manter o nome original do arquivo',
|
|
13
24
|
'modal.title': 'Upload de arquivo',
|
|
14
25
|
'error.uploading.image': 'Ocorreu um erro ao enviar o arquivo. Entre em contato com o administrador.',
|
|
26
|
+
'upload.file.already.exists': 'Já existe um arquivo chamado "{{0}}" nesta pasta.',
|
|
27
|
+
'replace.confirm.title': 'Substituir arquivo existente?',
|
|
28
|
+
'replace.confirm.subtitle': 'Já existe um arquivo chamado "{{0}}". Deseja substituí-lo?',
|
|
29
|
+
'upload.file.read.error': 'Não foi possível ler o arquivo enviado.',
|
|
30
|
+
'upload.file.type.detect.error': 'Não foi possível detectar o tipo do arquivo.',
|
|
31
|
+
'upload.file.type.unknown': 'Tipo de arquivo desconhecido; o envio não é permitido.',
|
|
32
|
+
'upload.file.threat.detected': 'Este tipo de arquivo não é permitido.',
|
|
33
|
+
'upload.file.write.error': 'Não foi possível salvar o arquivo enviado.',
|
|
34
|
+
'upload.file.name.required': 'É necessário informar o nome do arquivo para validação.',
|
|
15
35
|
},
|
|
16
36
|
it: {
|
|
17
37
|
'label.dropzone': 'Trascina e rilascia o clicca per caricare un file',
|
|
18
38
|
'label.uploaded.file': 'Stai caricando il seguente file:',
|
|
19
39
|
'label.confirm': 'Premi conferma per continuare',
|
|
40
|
+
'label.keep.original.name': 'Mantieni il nome originale del file',
|
|
20
41
|
'modal.title': 'Carica un file',
|
|
21
42
|
'error.uploading.image': "Si è verificato un errore durante il caricamento del file. Contatta l'amministratore.",
|
|
43
|
+
'upload.file.already.exists': 'Esiste già un file chiamato "{{0}}" in questa cartella.',
|
|
44
|
+
'replace.confirm.title': 'Sostituire il file esistente?',
|
|
45
|
+
'replace.confirm.subtitle': 'Esiste già un file chiamato "{{0}}". Vuoi sostituirlo?',
|
|
46
|
+
'upload.file.read.error': 'Impossibile leggere il file caricato.',
|
|
47
|
+
'upload.file.type.detect.error': 'Impossibile rilevare il tipo di file.',
|
|
48
|
+
'upload.file.type.unknown': 'Tipo di file sconosciuto; il caricamento non è consentito.',
|
|
49
|
+
'upload.file.threat.detected': 'Questo tipo di file non è consentito.',
|
|
50
|
+
'upload.file.write.error': 'Impossibile salvare il file caricato.',
|
|
51
|
+
'upload.file.name.required': 'Il nome del file è obbligatorio per la convalida.',
|
|
22
52
|
},
|
|
23
53
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tycho-components",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.38.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"react-i18next": "^13.0.2",
|
|
83
83
|
"react-router-dom": "^6.14.2",
|
|
84
84
|
"react-toastify": "^9.1.3",
|
|
85
|
-
"tycho-storybook": "0.10.
|
|
85
|
+
"tycho-storybook": "0.10.16",
|
|
86
86
|
"wavesurfer-react": "^2.2.2",
|
|
87
87
|
"wavesurfer.js": "^6.6.3"
|
|
88
88
|
},
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"react-toastify": "^9.1.3",
|
|
112
112
|
"sass-embedded": "^1.97.2",
|
|
113
113
|
"storybook": "^10.1.11",
|
|
114
|
-
"tycho-storybook": "^0.10.
|
|
114
|
+
"tycho-storybook": "^0.10.16",
|
|
115
115
|
"typescript": "^5.7.3",
|
|
116
116
|
"vite": "^7.0.0",
|
|
117
117
|
"vitest": "^3.2.6",
|