@linagora/twake-mui 18.4.1 → 18.5.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # @linagora/twake-mui [18.5.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@18.4.1...@linagora/twake-mui@18.5.0) (2026-10-27)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **twake-mui:** Take the line heights and tracking of the small styles from the design ([bab4fa1](https://github.com/linagora/twake-ui/commit/bab4fa147fd55735c8e4396c364e19bc696099db))
7
+
8
+
9
+ ### Features
10
+
11
+ * **twake-mui:** Add a body3 typography variant ([b1110b3](https://github.com/linagora/twake-ui/commit/b1110b328f29ad49714b138cceaf887236dad667))
12
+ * **twake-mui:** Add an xsmall Button size ([6ca4db8](https://github.com/linagora/twake-ui/commit/6ca4db8105c37d87bec419b8a4bfe563e4d40098))
13
+ * **twake-mui:** Set the button text to 14px at every size ([6587b37](https://github.com/linagora/twake-ui/commit/6587b377eea92cece727309b3ff76cec8e8dfd3e))
14
+
1
15
  ## @linagora/twake-mui [18.4.1](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@18.4.0...@linagora/twake-mui@18.4.1) (2026-10-23)
2
16
 
3
17
 
@@ -4,13 +4,16 @@ export const NavPrimaryText = styled('span', {
4
4
  })(({ theme, disableMobileView }) => ({
5
5
  fontSize: theme.typography.pxToRem(14),
6
6
  fontWeight: 500,
7
- letterSpacing: '.15px',
7
+ lineHeight: 1.313,
8
+ letterSpacing: '.25px',
8
9
  ...(!disableMobileView && {
9
10
  [theme.breakpoints.down('lg')]: {
10
11
  display: 'block',
11
12
  textAlign: 'center',
12
13
  whiteSpace: 'nowrap',
13
- fontSize: theme.typography.pxToRem(12)
14
+ fontSize: theme.typography.pxToRem(12),
15
+ // The design line height is for the sidebar, the bottom bar keeps its rows
16
+ lineHeight: 'inherit'
14
17
  }
15
18
  })
16
19
  }));
@@ -54,7 +54,7 @@ export const makeTypography = () => {
54
54
  subtitle2: {
55
55
  fontSize: 14,
56
56
  fontWeight: 600,
57
- lineHeight: 1.334,
57
+ lineHeight: 1.313,
58
58
  letterSpacing: '0.25px'
59
59
  },
60
60
  body1: {
@@ -69,23 +69,30 @@ export const makeTypography = () => {
69
69
  lineHeight: 1.313,
70
70
  letterSpacing: '0.25px'
71
71
  },
72
+ body3: {
73
+ fontSize: 14,
74
+ fontWeight: 400,
75
+ lineHeight: 1.313,
76
+ letterSpacing: '0.25px'
77
+ },
72
78
  button: {
73
- fontSize: 16,
79
+ fontSize: 14,
74
80
  fontWeight: 500,
75
81
  lineHeight: 1.313,
82
+ letterSpacing: '0.1px',
76
83
  textTransform: 'inherit'
77
84
  },
78
85
  caption: {
79
86
  fontSize: 12,
80
87
  fontWeight: 500,
81
- lineHeight: 1.313,
88
+ lineHeight: 1.317,
82
89
  letterSpacing: '0.5px'
83
90
  },
84
91
  overline: {
85
92
  fontSize: 11,
86
93
  fontWeight: 500,
87
- lineHeight: '16px',
88
- letterSpacing: '0.5px',
94
+ lineHeight: '13px',
95
+ letterSpacing: '0.4px',
89
96
  textTransform: 'inherit'
90
97
  }
91
98
  };
@@ -212,14 +212,28 @@ export const overrides = {
212
212
  }
213
213
  }
214
214
  })),
215
+ {
216
+ props: { size: 'xsmall' },
217
+ style: {
218
+ minWidth: 'auto',
219
+ minHeight: 24,
220
+ padding: '4px 8px',
221
+ fontSize: theme.typography.pxToRem(11),
222
+ lineHeight: '16px',
223
+ letterSpacing: '0.5px'
224
+ }
225
+ },
226
+ {
227
+ props: { size: 'xsmall', variant: 'outlined' },
228
+ style: { padding: '3px 7px' }
229
+ },
215
230
  // Vertical paddings sit 1px under cozy-ui's so the outlined border
216
231
  // still fits the fixed heights cozy-ui forces (36, 40, 48).
217
232
  {
218
233
  props: { size: 'small' },
219
234
  style: {
220
235
  minHeight: 36,
221
- padding: '9px 16px',
222
- fontSize: theme.typography.pxToRem(13)
236
+ padding: '9px 16px'
223
237
  }
224
238
  },
225
239
  {
@@ -230,8 +244,7 @@ export const overrides = {
230
244
  props: { size: 'large' },
231
245
  style: {
232
246
  minHeight: 48,
233
- padding: '13px 32px',
234
- fontSize: theme.typography.pxToRem(15)
247
+ padding: '13px 32px'
235
248
  }
236
249
  },
237
250
  {
@@ -257,6 +270,10 @@ export const overrides = {
257
270
  props: { loadingPosition: 'end', variant: 'text' },
258
271
  style: { right: 8 }
259
272
  },
273
+ {
274
+ props: { loadingPosition: 'end', size: 'xsmall' },
275
+ style: { right: 8 }
276
+ },
260
277
  {
261
278
  props: { loadingPosition: 'end', size: 'small' },
262
279
  style: { right: 16 }
@@ -983,6 +1000,7 @@ export const overrides = {
983
1000
  }
984
1001
  },
985
1002
  MuiTypography: {
1003
+ defaultProps: { variantMapping: { body3: 'p' } },
986
1004
  styleOverrides: {
987
1005
  root: ({ theme }) => ({
988
1006
  variants: [
@@ -1,5 +1,6 @@
1
1
  import paletteJson from '@linagora/twake-css/palette.json';
2
2
  import { Shadows, TypographyVariantsOptions } from '@mui/material/styles';
3
+ import type { CSSProperties } from 'react';
3
4
  export type PaletteJson = typeof paletteJson;
4
5
  export type PaletteColor = PaletteJson['Primary'];
5
6
  export type GreyPalette = PaletteJson['Grey'];
@@ -69,6 +70,12 @@ declare module '@mui/material/styles' {
69
70
  interface CssThemeVariables {
70
71
  enabled: true;
71
72
  }
73
+ interface TypographyVariants {
74
+ body3: CSSProperties;
75
+ }
76
+ interface TypographyVariantsOptions {
77
+ body3?: CSSProperties;
78
+ }
72
79
  interface TypeBackground {
73
80
  contrast: string;
74
81
  }
@@ -82,6 +89,11 @@ declare module '@mui/material/styles' {
82
89
  border?: Partial<BorderPalette>;
83
90
  }
84
91
  }
92
+ declare module '@mui/material/Typography' {
93
+ interface TypographyPropsVariantOverrides {
94
+ body3: true;
95
+ }
96
+ }
85
97
  declare module '@mui/material/IconButton' {
86
98
  interface IconButtonPropsSizeOverrides {
87
99
  xsmall: true;
@@ -97,4 +109,7 @@ declare module '@mui/material/Button' {
97
109
  interface ButtonPropsVariantOverrides {
98
110
  ghost: true;
99
111
  }
112
+ interface ButtonPropsSizeOverrides {
113
+ xsmall: true;
114
+ }
100
115
  }
@@ -3,7 +3,7 @@ import { Box, Stack } from '@mui/material';
3
3
  import React from 'react';
4
4
  import Button from '../components/Button';
5
5
  const variants = ['contained', 'outlined', 'ghost', 'text'];
6
- const sizes = ['small', 'medium', 'large'];
6
+ const sizes = ['xsmall', 'small', 'medium', 'large'];
7
7
  const colors = [
8
8
  'inherit',
9
9
  'primary',
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj<typeof meta>;
5
+ export declare const Default: Story;
6
+ export declare const Modes: Story;
7
+ export declare const MonthPicker: Story;
@@ -0,0 +1,98 @@
1
+ import { Paper, Stack } from '@mui/material';
2
+ import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV2';
3
+ import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
4
+ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
5
+ import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
6
+ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
7
+ import { TimePicker } from '@mui/x-date-pickers/TimePicker';
8
+ import { enUS } from 'date-fns/locale';
9
+ import React, { useState } from 'react';
10
+ const description = `
11
+ To pick a date, a time or a month, use [MUI X Date Pickers](https://mui.com/x/react-date-pickers/), installed in the app with \`date-fns\`. Their popups are built on MUI components that twake-mui themes. Their field is a \`PickersTextField\`, which twake-mui does not theme yet: its radius, border colors and gutters still differ from a \`TextField\`.
12
+
13
+ Wrap the app once in a \`LocalizationProvider\`, with the date-fns locale and the picker texts of the user's language. Twake apps use date-fns 2.30.0, so import the adapter from \`@mui/x-date-pickers/AdapterDateFnsV2\`:
14
+
15
+ \`\`\`jsx
16
+ import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV2'
17
+ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'
18
+ import { frFR } from '@mui/x-date-pickers/locales'
19
+ import { fr } from 'date-fns/locale'
20
+
21
+ <LocalizationProvider
22
+ dateAdapter={AdapterDateFns}
23
+ adapterLocale={fr}
24
+ localeText={frFR.components.MuiLocalizationProvider.defaultProps.localeText}
25
+ >
26
+ <App />
27
+ </LocalizationProvider>
28
+ \`\`\`
29
+ `;
30
+ const MIN_DATE = new Date(2025, 0, 10);
31
+ const getErrorMessage = (error) => {
32
+ if (error === 'minDate')
33
+ return 'The date must be on or after 01/10/2025';
34
+ if (error === 'invalidDate')
35
+ return 'Invalid date';
36
+ return null;
37
+ };
38
+ const ExampleDatePicker = () => {
39
+ const [value, setValue] = useState(new Date(2025, 0, 8));
40
+ const [error, setError] = useState(null);
41
+ return (React.createElement(DatePicker, { label: "Expiration date", value: value, minDate: MIN_DATE, onChange: setValue, onError: setError, slotProps: {
42
+ textField: { helperText: getErrorMessage(error) },
43
+ actionBar: { actions: ['clear', 'today', 'cancel', 'accept'] }
44
+ } }));
45
+ };
46
+ const ExampleModes = () => (React.createElement(Stack, { spacing: 2 },
47
+ React.createElement(TimePicker, { label: "Time", defaultValue: new Date(2025, 0, 15, 14, 30) }),
48
+ React.createElement(DateTimePicker, { label: "Date and time", defaultValue: new Date(2025, 0, 15, 14, 30) })));
49
+ const ExampleMonthPicker = () => {
50
+ const [value, setValue] = useState(new Date(2025, 7));
51
+ return (React.createElement(Stack, { spacing: 2 },
52
+ React.createElement(Paper, null,
53
+ React.createElement(DateCalendar, { views: ['year', 'month'], openTo: "month", value: value, onChange: setValue })),
54
+ React.createElement(DatePicker, { label: "Month", views: ['year', 'month'], openTo: "month", value: value, onChange: setValue })));
55
+ };
56
+ const meta = {
57
+ title: 'DatePicker',
58
+ tags: ['autodocs'],
59
+ parameters: {
60
+ layout: 'centered',
61
+ docs: { description: { component: description } }
62
+ },
63
+ decorators: [
64
+ (Story) => (React.createElement(LocalizationProvider, { dateAdapter: AdapterDateFns, adapterLocale: enUS },
65
+ React.createElement(Story, null)))
66
+ ]
67
+ };
68
+ export default meta;
69
+ export const Default = {
70
+ parameters: {
71
+ docs: {
72
+ description: {
73
+ story: 'A date field with `minDate`: `onError` gives the validation error, shown in `slotProps.textField.helperText`, and `slotProps.actionBar.actions` adds the clear and today buttons. The value is before `minDate`, so the error message shows.'
74
+ }
75
+ }
76
+ },
77
+ render: () => React.createElement(ExampleDatePicker, null)
78
+ };
79
+ export const Modes = {
80
+ parameters: {
81
+ docs: {
82
+ description: {
83
+ story: 'A time field with `TimePicker`, and a date and time field with `DateTimePicker`.'
84
+ }
85
+ }
86
+ },
87
+ render: () => React.createElement(ExampleModes, null)
88
+ };
89
+ export const MonthPicker = {
90
+ parameters: {
91
+ docs: {
92
+ description: {
93
+ story: "A month picker, inline with `DateCalendar` or as a field with `DatePicker`, both with `views={['year', 'month']}` and `openTo=\"month\"`."
94
+ }
95
+ }
96
+ },
97
+ render: () => React.createElement(ExampleMonthPicker, null)
98
+ };
@@ -0,0 +1,11 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ interface FileInputArgs {
3
+ disabled: boolean;
4
+ isReadOnly: boolean;
5
+ }
6
+ declare const meta: Meta<FileInputArgs>;
7
+ export default meta;
8
+ type Story = StoryObj<typeof meta>;
9
+ export declare const Default: Story;
10
+ export declare const InMenu: Story;
11
+ export declare const Field: Story;
@@ -0,0 +1,135 @@
1
+ import { CrossSmall, Icon, Upload } from '@linagora/twake-icons';
2
+ import { ListItemIcon, MenuList, Paper, Stack, Typography } from '@mui/material';
3
+ import { MuiFileInput } from 'mui-file-input';
4
+ import React, { useState } from 'react';
5
+ import ActionsMenuItem from '../components/ActionsMenu/ActionsMenuItem';
6
+ import Button from '../components/Button';
7
+ import ListItemText from '../components/ListItemText';
8
+ import AlertProvider from '../providers/AlertProvider';
9
+ import { useAlert } from '../providers/AlertProvider/useAlert';
10
+ const description = `
11
+ To open the file picker from a button or a menu item, render the trigger as a \`label\`, with \`component="label"\`, and put a hidden \`input type="file"\` inside it. The label opens the picker on click, Enter and Space.
12
+
13
+ \`\`\`jsx
14
+ const handleChange = event => {
15
+ const files = Array.from(event.target.files)
16
+ event.target.value = ''
17
+ onUpload(files)
18
+ }
19
+
20
+ <Button component="label" variant="primary" startIcon={<Icon icon={Upload} />}>
21
+ Upload
22
+ <input hidden type="file" multiple data-testid="upload-btn" onChange={handleChange} />
23
+ </Button>
24
+ \`\`\`
25
+
26
+ - Resetting \`event.target.value\` lets the user pick the same file again.
27
+ - \`disabled\` on the trigger also blocks the picker.
28
+ - To keep the picker closed, call \`event.preventDefault()\` in the \`input\`'s \`onClick\`.
29
+ - In tests, fire the change on the input: \`fireEvent.change(getByTestId('upload-btn'), { target: { files: [file] } })\`.
30
+
31
+ #### File field with the chosen files
32
+
33
+ For a form field that displays the chosen files, with size limits or a clear button, install [\`mui-file-input\`](https://viclafouch.github.io/mui-file-input/) in the app and use \`MuiFileInput\`, as in the **Field** story below. It is a \`TextField\`, so twake-mui themes it.
34
+ `;
35
+ const getFileNames = (files) => files.length > 0 ? files.map(file => file.name).join(', ') : 'No file chosen';
36
+ const readFiles = (event) => {
37
+ const files = Array.from(event.target.files ?? []);
38
+ event.target.value = '';
39
+ return files;
40
+ };
41
+ const UploadButton = ({ disabled }) => {
42
+ const [files, setFiles] = useState([]);
43
+ const handleChange = (event) => {
44
+ setFiles(readFiles(event));
45
+ };
46
+ return (React.createElement(Stack, { spacing: 2, sx: { alignItems: 'center' } },
47
+ React.createElement(Button, { component: "label", variant: "primary", disabled: disabled, startIcon: React.createElement(Icon, { icon: Upload }) },
48
+ "Upload",
49
+ React.createElement("input", { hidden: true, type: "file", multiple: true, "data-testid": "upload-btn", onChange: handleChange })),
50
+ React.createElement(Typography, { variant: "caption" }, getFileNames(files))));
51
+ };
52
+ const UploadMenuItem = ({ isReadOnly }) => {
53
+ const { showAlert } = useAlert();
54
+ const [files, setFiles] = useState([]);
55
+ const handleClick = (event) => {
56
+ if (!isReadOnly)
57
+ return;
58
+ event.preventDefault();
59
+ showAlert({
60
+ message: 'This is a read-only folder. You cannot perform this action.',
61
+ severity: 'warning',
62
+ duration: 4000
63
+ });
64
+ };
65
+ const handleChange = (event) => {
66
+ setFiles(readFiles(event));
67
+ };
68
+ return (React.createElement(Stack, { spacing: 2, sx: { alignItems: 'center' } },
69
+ React.createElement(Paper, null,
70
+ React.createElement(MenuList, null,
71
+ React.createElement(ActionsMenuItem, { component: "label" },
72
+ React.createElement(ListItemIcon, null,
73
+ React.createElement(Icon, { icon: Upload })),
74
+ React.createElement(ListItemText, { primary: "Upload files" }),
75
+ React.createElement("input", { hidden: true, type: "file", multiple: true, "data-testid": "upload-btn", onClick: handleClick, onChange: handleChange })))),
76
+ React.createElement(Typography, { variant: "caption" }, getFileNames(files))));
77
+ };
78
+ const MAX_FILE_SIZE = 5 * 1024 * 1024;
79
+ const AttachmentsField = () => {
80
+ const [files, setFiles] = useState([]);
81
+ const hasTooLargeFile = files.some(file => file.size > MAX_FILE_SIZE);
82
+ return (React.createElement(MuiFileInput, { multiple: true, label: "Attachments", value: files, onChange: setFiles, error: hasTooLargeFile, helperText: hasTooLargeFile
83
+ ? 'Each file must be under 5 MB'
84
+ : 'PDF or images, 5 MB max per file', slotProps: { htmlInput: { accept: 'image/*,.pdf' } }, clearIconButtonProps: {
85
+ title: 'Remove',
86
+ children: React.createElement(Icon, { icon: CrossSmall })
87
+ }, sx: { width: 360 } }));
88
+ };
89
+ const meta = {
90
+ title: 'FileInput',
91
+ tags: ['autodocs'],
92
+ parameters: {
93
+ layout: 'centered',
94
+ docs: { description: { component: description } }
95
+ },
96
+ args: { disabled: false, isReadOnly: false }
97
+ };
98
+ export default meta;
99
+ export const Default = {
100
+ argTypes: { isReadOnly: { table: { disable: true } } },
101
+ parameters: {
102
+ docs: {
103
+ description: {
104
+ story: 'An upload button. With `disabled`, the file picker does not open.'
105
+ }
106
+ }
107
+ },
108
+ render: args => React.createElement(UploadButton, { disabled: args.disabled })
109
+ };
110
+ export const InMenu = {
111
+ argTypes: { disabled: { table: { disable: true } } },
112
+ parameters: {
113
+ docs: {
114
+ description: {
115
+ story: 'An upload item in a menu. With `isReadOnly`, the click shows an alert and the file picker does not open.'
116
+ }
117
+ }
118
+ },
119
+ render: args => (React.createElement(AlertProvider, null,
120
+ React.createElement(UploadMenuItem, { isReadOnly: args.isReadOnly })))
121
+ };
122
+ export const Field = {
123
+ argTypes: {
124
+ disabled: { table: { disable: true } },
125
+ isReadOnly: { table: { disable: true } }
126
+ },
127
+ parameters: {
128
+ docs: {
129
+ description: {
130
+ story: 'An attachments field with `mui-file-input`: PDF or images only, a clear button, and an error message when a file is over 5 MB.'
131
+ }
132
+ }
133
+ },
134
+ render: () => React.createElement(AttachmentsField, null)
135
+ };
@@ -13,6 +13,7 @@ const variants = [
13
13
  'subtitle2',
14
14
  'body1',
15
15
  'body2',
16
+ 'body3',
16
17
  'caption'
17
18
  ];
18
19
  const colors = [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linagora/twake-mui",
3
3
  "type": "module",
4
- "version": "18.4.1",
4
+ "version": "18.5.0",
5
5
  "description": "MUI v9 theme system for Twake applications",
6
6
  "author": "Twake",
7
7
  "license": "MIT",
@@ -60,13 +60,13 @@
60
60
  "twake-i18n": ">=0.7.1"
61
61
  },
62
62
  "dependencies": {
63
- "@mui/utils": "^9.2.0",
63
+ "@mui/utils": "9.2.0",
64
64
  "@react-spring/web": "10.1.2",
65
65
  "@use-gesture/react": "10.3.1",
66
- "classnames": "^2.5.1",
66
+ "classnames": "2.5.1",
67
67
  "cozy-minilog": "^3.10.1",
68
- "pdf-lib": "^1.17.1",
69
- "react-markdown": "^10.1.0",
68
+ "pdf-lib": "1.17.1",
69
+ "react-markdown": "10.1.0",
70
70
  "react-use-measure": "2.1.7",
71
71
  "react-virtuoso": "4.13.0",
72
72
  "remark-gfm": "4.0.1",
@@ -77,34 +77,37 @@
77
77
  "devDependencies": {
78
78
  "@argos-ci/cli": "6.9.5",
79
79
  "@argos-ci/storybook": "6.4.1",
80
- "@emotion/react": "^11.5.0",
81
- "@emotion/styled": "^11.3.0",
80
+ "@emotion/react": "11.14.0",
81
+ "@emotion/styled": "11.14.1",
82
82
  "@linagora/twake-css": "^1.0.0",
83
83
  "@linagora/twake-icons": "^2.10.0",
84
- "@mui/icons-material": "^9.2.0",
85
- "@mui/lab": "^9.0.0-beta.6",
86
- "@mui/material": "^9.2.0",
84
+ "@mui/icons-material": "9.2.0",
85
+ "@mui/lab": "9.0.0-beta.6",
86
+ "@mui/material": "9.2.0",
87
+ "@mui/x-date-pickers": "9.16.0",
87
88
  "@storybook/addon-docs": "10.3.5",
88
89
  "@storybook/addon-vitest": "10.3.5",
89
90
  "@storybook/builder-vite": "10.3.5",
90
91
  "@storybook/react": "10.3.5",
91
92
  "@storybook/react-vite": "10.3.5",
92
- "@testing-library/react": "^16.3.3",
93
- "@types/classnames": "^2.3.1",
93
+ "@testing-library/react": "16.3.3",
94
+ "@types/classnames": "2.3.4",
94
95
  "@types/lodash": "4.17.24",
95
96
  "@types/react": "19.3.0",
96
- "@types/react-dom": "^19.3.0",
97
+ "@types/react-dom": "19.3.0",
97
98
  "@vitest/browser-playwright": "4.1.11",
98
99
  "cozy-intent": "^2.31.1",
99
100
  "cozy-tsconfig": "^2.0.0",
101
+ "date-fns": "2.30.0",
100
102
  "eslint": "10.2.1",
101
103
  "eslint-config-cozy-app": "7.0.0",
102
104
  "eslint-plugin-storybook": "10.3.5",
103
- "jsdom": "^30.1.2",
105
+ "jsdom": "30.1.2",
104
106
  "lodash": "4.18.1",
107
+ "mui-file-input": "9.0.0",
105
108
  "playwright": "1.64.1",
106
- "react": "^19.3.0",
107
- "react-dom": "^19.3.0",
109
+ "react": "19.3.0",
110
+ "react-dom": "19.3.0",
108
111
  "storybook": "10.3.5",
109
112
  "twake-i18n": "^0.7.1",
110
113
  "vite": "8.3.1",