@selvajs/ui 6.0.0 → 6.1.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.
- package/dist/components/compute/AppLayout.svelte +1 -0
- package/dist/components/compute/ComputeApp.svelte +5 -0
- package/dist/components/compute/ComputeApp.svelte.d.ts +3 -0
- package/dist/components/layout/Pagination.svelte +65 -0
- package/dist/components/layout/Pagination.svelte.d.ts +12 -0
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.js +1 -0
- package/dist/components/viewer/Viewer.svelte +7 -3
- package/dist/components/viewer/Viewer.svelte.d.ts +2 -0
- package/dist/constants.js +2 -2
- package/package.json +12 -12
- package/src/lib/components/compute/AppLayout.svelte +1 -0
- package/src/lib/components/compute/ComputeApp.svelte +5 -0
- package/src/lib/components/layout/Pagination.svelte +65 -0
- package/src/lib/components/layout/index.ts +1 -0
- package/src/lib/components/viewer/Viewer.svelte +7 -3
- package/src/lib/constants.ts +2 -2
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { hexToOklch } from '../../utils/color';
|
|
16
16
|
import AppShell from '../layout/AppShell.svelte';
|
|
17
17
|
import AppLayout from './AppLayout.svelte';
|
|
18
|
+
import { type ViewerConfig } from '../viewer/Viewer.svelte';
|
|
18
19
|
import StateDisplay from '../primitives/StateDisplay.svelte';
|
|
19
20
|
import { setClientSlot, type ClientSlot } from '../../contexts/clientSlotContext.svelte';
|
|
20
21
|
import type { Locale } from '../../i18n/messages';
|
|
@@ -68,6 +69,8 @@
|
|
|
68
69
|
}) => void;
|
|
69
70
|
/** Hands the live three.js viewer to the host once it mounts. See `Viewer.svelte` for the contract. */
|
|
70
71
|
onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
|
|
72
|
+
/** Viewer chrome and defaults. `backgroundColor` and `showSceneManager` are set by the layout. */
|
|
73
|
+
viewerConfig?: ViewerConfig;
|
|
71
74
|
headerRight?: Snippet;
|
|
72
75
|
// Replaces the built-in header; takes precedence over `headerRight`.
|
|
73
76
|
header?: Snippet;
|
|
@@ -106,6 +109,7 @@
|
|
|
106
109
|
header,
|
|
107
110
|
onReady,
|
|
108
111
|
onViewerReady,
|
|
112
|
+
viewerConfig = {},
|
|
109
113
|
externalScopeKey,
|
|
110
114
|
clientSlot,
|
|
111
115
|
lang
|
|
@@ -239,6 +243,7 @@
|
|
|
239
243
|
values={session.values}
|
|
240
244
|
logoUrl={logo}
|
|
241
245
|
{onViewerReady}
|
|
246
|
+
{viewerConfig}
|
|
242
247
|
{panelActions}
|
|
243
248
|
{showSaveButton}
|
|
244
249
|
{showLoadButton}
|
|
@@ -4,6 +4,7 @@ import type { SolveFn } from '@selvajs/solve/shared';
|
|
|
4
4
|
import type { PresetLabels } from '../../types/presetLabels';
|
|
5
5
|
import type { RetainedSolveResult, SolveSession } from '@selvajs/solve/client';
|
|
6
6
|
import type { ThreeViewer } from '@selvajs/visualization/render';
|
|
7
|
+
import { type ViewerConfig } from '../viewer/Viewer.svelte';
|
|
7
8
|
import { type ClientSlot } from '../../contexts/clientSlotContext.svelte';
|
|
8
9
|
import type { Locale } from '../../i18n/messages';
|
|
9
10
|
import type { Snippet } from 'svelte';
|
|
@@ -52,6 +53,8 @@ interface Props {
|
|
|
52
53
|
}) => void;
|
|
53
54
|
/** Hands the live three.js viewer to the host once it mounts. See `Viewer.svelte` for the contract. */
|
|
54
55
|
onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
|
|
56
|
+
/** Viewer chrome and defaults. `backgroundColor` and `showSceneManager` are set by the layout. */
|
|
57
|
+
viewerConfig?: ViewerConfig;
|
|
55
58
|
headerRight?: Snippet;
|
|
56
59
|
header?: Snippet;
|
|
57
60
|
externalScopeKey?: string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from '../primitives';
|
|
3
|
+
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Current 1-based page. Bindable. */
|
|
7
|
+
page: number;
|
|
8
|
+
total: number;
|
|
9
|
+
perPage?: number;
|
|
10
|
+
/** Plural noun for the range readout: "1–25 of 122 members". */
|
|
11
|
+
label?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
page = $bindable(1),
|
|
17
|
+
total,
|
|
18
|
+
perPage = 25,
|
|
19
|
+
label = 'items',
|
|
20
|
+
class: className = ''
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
|
|
23
|
+
const pageCount = $derived(Math.max(1, Math.ceil(total / perPage)));
|
|
24
|
+
|
|
25
|
+
// A filter can shrink `total` under the current page — clamp on read so the
|
|
26
|
+
// list never renders an empty slice while the control still says "page 4".
|
|
27
|
+
$effect(() => {
|
|
28
|
+
if (page > pageCount) page = pageCount;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const first = $derived(total === 0 ? 0 : (page - 1) * perPage + 1);
|
|
32
|
+
const last = $derived(Math.min(page * perPage, total));
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
{#if pageCount > 1}
|
|
36
|
+
<div class={`gap-4 flex items-center justify-between ${className}`}>
|
|
37
|
+
<p class="text-xs text-muted-foreground">
|
|
38
|
+
{first}–{last} of {total}
|
|
39
|
+
{label}
|
|
40
|
+
</p>
|
|
41
|
+
<div class="gap-1 flex items-center">
|
|
42
|
+
<Button
|
|
43
|
+
size="sm"
|
|
44
|
+
variant="outline"
|
|
45
|
+
disabled={page <= 1}
|
|
46
|
+
onclick={() => (page -= 1)}
|
|
47
|
+
aria-label="Previous page"
|
|
48
|
+
class="h-8 w-8 p-0"
|
|
49
|
+
>
|
|
50
|
+
<ChevronLeft class="h-4 w-4" />
|
|
51
|
+
</Button>
|
|
52
|
+
<span class="px-2 font-mono text-xs text-muted-foreground">{page} / {pageCount}</span>
|
|
53
|
+
<Button
|
|
54
|
+
size="sm"
|
|
55
|
+
variant="outline"
|
|
56
|
+
disabled={page >= pageCount}
|
|
57
|
+
onclick={() => (page += 1)}
|
|
58
|
+
aria-label="Next page"
|
|
59
|
+
class="h-8 w-8 p-0"
|
|
60
|
+
>
|
|
61
|
+
<ChevronRight class="h-4 w-4" />
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
{/if}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Current 1-based page. Bindable. */
|
|
3
|
+
page: number;
|
|
4
|
+
total: number;
|
|
5
|
+
perPage?: number;
|
|
6
|
+
/** Plural noun for the range readout: "1–25 of 122 members". */
|
|
7
|
+
label?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const Pagination: import("svelte").Component<Props, {}, "page">;
|
|
11
|
+
type Pagination = ReturnType<typeof Pagination>;
|
|
12
|
+
export default Pagination;
|
|
@@ -8,3 +8,4 @@ export { default as SideNav } from './SideNav.svelte';
|
|
|
8
8
|
export type { SideNavItem } from './SideNav.svelte';
|
|
9
9
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
10
10
|
export { default as EmptyState } from './EmptyState.svelte';
|
|
11
|
+
export { default as Pagination } from './Pagination.svelte';
|
|
@@ -6,3 +6,4 @@ export { default as SubNav } from './SubNav.svelte';
|
|
|
6
6
|
export { default as SideNav } from './SideNav.svelte';
|
|
7
7
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
8
8
|
export { default as EmptyState } from './EmptyState.svelte';
|
|
9
|
+
export { default as Pagination } from './Pagination.svelte';
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
showDisplayMenu?: boolean;
|
|
51
51
|
enableMeshClick?: boolean;
|
|
52
52
|
backgroundColor?: string;
|
|
53
|
+
/** Initial state of the edge overlay and its display-menu checkmark. Default false. */
|
|
54
|
+
showEdges?: boolean;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
interface Props {
|
|
@@ -83,7 +85,8 @@
|
|
|
83
85
|
showGridToggle: true,
|
|
84
86
|
showDisplayMenu: true,
|
|
85
87
|
enableMeshClick: true,
|
|
86
|
-
backgroundColor: '#E6E6E6'
|
|
88
|
+
backgroundColor: '#E6E6E6',
|
|
89
|
+
showEdges: false
|
|
87
90
|
};
|
|
88
91
|
|
|
89
92
|
let {
|
|
@@ -136,7 +139,8 @@
|
|
|
136
139
|
let measureActive = $state(false);
|
|
137
140
|
let gridVisible = $state(false);
|
|
138
141
|
let renderStyle: Look = $state('technical');
|
|
139
|
-
|
|
142
|
+
// Seeded once, then owned by the menu toggle.
|
|
143
|
+
let edgesVisible = $state(untrack(() => viewerConfig.showEdges) ?? defaultViewerConfig.showEdges);
|
|
140
144
|
let selectedMeshMetadata: Record<string, any> | null = $state(null);
|
|
141
145
|
let selectedMeshName: string | null = $state(null);
|
|
142
146
|
|
|
@@ -385,7 +389,7 @@
|
|
|
385
389
|
<img
|
|
386
390
|
src={logoUrl}
|
|
387
391
|
alt=""
|
|
388
|
-
class="h-
|
|
392
|
+
class="h-10 max-w-40 drop-shadow-sm sm:h-14 sm:max-w-56 w-auto object-contain opacity-80"
|
|
389
393
|
/>
|
|
390
394
|
</div>
|
|
391
395
|
{/if}
|
|
@@ -9,6 +9,8 @@ export interface ViewerConfig {
|
|
|
9
9
|
showDisplayMenu?: boolean;
|
|
10
10
|
enableMeshClick?: boolean;
|
|
11
11
|
backgroundColor?: string;
|
|
12
|
+
/** Initial state of the edge overlay and its display-menu checkmark. Default false. */
|
|
13
|
+
showEdges?: boolean;
|
|
12
14
|
}
|
|
13
15
|
interface Props {
|
|
14
16
|
meshes: any[];
|
package/dist/constants.js
CHANGED
|
@@ -2,8 +2,8 @@ export const APP_DEFAULTS = {
|
|
|
2
2
|
// File upload limits
|
|
3
3
|
// TEMP (dev): raised 150 MB → 300 MB so large dev file-widget inputs aren't
|
|
4
4
|
// blocked client-side. The server request cap (COMPUTE_REQUEST_MAX_BYTES)
|
|
5
|
-
// still defaults to
|
|
6
|
-
// anything over ~
|
|
5
|
+
// still defaults to 256 MB, and base64 inflates a raw file by ~4/3, so
|
|
6
|
+
// anything over ~192 MB is rejected with a 413 after the client accepts it.
|
|
7
7
|
// Revert to 150 before release.
|
|
8
8
|
FILE_UPLOAD: {
|
|
9
9
|
MAX_SIZE_MB: 300,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/ui",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.1",
|
|
4
4
|
"description": "Shared UI components and utilities for Selva applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "VektorNode",
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://selva.dev",
|
|
8
8
|
"bugs": "https://github.com/VektorNode/selva/issues",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"svelte",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"svelte": "^5",
|
|
60
60
|
"tailwind-variants": "^3.3.1",
|
|
61
61
|
"three": "^0.185.1",
|
|
62
|
+
"@selvajs/compute": "^4.0.2",
|
|
62
63
|
"@selvajs/schemas": "^5.0.0",
|
|
63
|
-
"@selvajs/
|
|
64
|
-
"@selvajs/
|
|
65
|
-
"@selvajs/visualization": "^1.0.0"
|
|
64
|
+
"@selvajs/solve": "^1.0.3",
|
|
65
|
+
"@selvajs/visualization": "^1.0.1"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
68
68
|
"three": {
|
|
@@ -76,25 +76,25 @@
|
|
|
76
76
|
"clsx": "^2.1.1",
|
|
77
77
|
"mode-watcher": "^1.1.0",
|
|
78
78
|
"paneforge": "^1.0.2",
|
|
79
|
-
"svelte-sonner": "^1.
|
|
79
|
+
"svelte-sonner": "^1.2.1",
|
|
80
80
|
"tailwind-merge": "^3.5.0",
|
|
81
81
|
"tw-animate-css": "^1.4.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"@internationalized/date": "^3.12.3",
|
|
85
85
|
"@sveltejs/kit": "2.70.2",
|
|
86
|
-
"@sveltejs/vite-plugin-svelte": "^7.
|
|
86
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
87
87
|
"@types/three": "^0.185.4",
|
|
88
88
|
"bits-ui": "^2.18.0",
|
|
89
|
-
"rimraf": "^6.
|
|
90
|
-
"svelte": "5.56.
|
|
89
|
+
"rimraf": "^6.1.3",
|
|
90
|
+
"svelte": "5.56.9",
|
|
91
91
|
"tailwind-variants": "^3.3.1",
|
|
92
92
|
"vitest": "^4.1.10",
|
|
93
|
-
"@selvajs/compute": "4.0.0",
|
|
94
93
|
"@selvajs/config": "0.0.4",
|
|
95
94
|
"@selvajs/schemas": "5.0.0",
|
|
96
|
-
"@selvajs/
|
|
97
|
-
"@selvajs/solve": "1.0.
|
|
95
|
+
"@selvajs/compute": "4.0.2",
|
|
96
|
+
"@selvajs/solve": "1.0.3",
|
|
97
|
+
"@selvajs/visualization": "1.0.1"
|
|
98
98
|
},
|
|
99
99
|
"scripts": {
|
|
100
100
|
"predev": "node ../../scripts/sync-shared-assets.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { hexToOklch } from '../../utils/color';
|
|
16
16
|
import AppShell from '../layout/AppShell.svelte';
|
|
17
17
|
import AppLayout from './AppLayout.svelte';
|
|
18
|
+
import { type ViewerConfig } from '../viewer/Viewer.svelte';
|
|
18
19
|
import StateDisplay from '../primitives/StateDisplay.svelte';
|
|
19
20
|
import { setClientSlot, type ClientSlot } from '../../contexts/clientSlotContext.svelte';
|
|
20
21
|
import type { Locale } from '../../i18n/messages';
|
|
@@ -68,6 +69,8 @@
|
|
|
68
69
|
}) => void;
|
|
69
70
|
/** Hands the live three.js viewer to the host once it mounts. See `Viewer.svelte` for the contract. */
|
|
70
71
|
onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
|
|
72
|
+
/** Viewer chrome and defaults. `backgroundColor` and `showSceneManager` are set by the layout. */
|
|
73
|
+
viewerConfig?: ViewerConfig;
|
|
71
74
|
headerRight?: Snippet;
|
|
72
75
|
// Replaces the built-in header; takes precedence over `headerRight`.
|
|
73
76
|
header?: Snippet;
|
|
@@ -106,6 +109,7 @@
|
|
|
106
109
|
header,
|
|
107
110
|
onReady,
|
|
108
111
|
onViewerReady,
|
|
112
|
+
viewerConfig = {},
|
|
109
113
|
externalScopeKey,
|
|
110
114
|
clientSlot,
|
|
111
115
|
lang
|
|
@@ -239,6 +243,7 @@
|
|
|
239
243
|
values={session.values}
|
|
240
244
|
logoUrl={logo}
|
|
241
245
|
{onViewerReady}
|
|
246
|
+
{viewerConfig}
|
|
242
247
|
{panelActions}
|
|
243
248
|
{showSaveButton}
|
|
244
249
|
{showLoadButton}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from '../primitives';
|
|
3
|
+
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Current 1-based page. Bindable. */
|
|
7
|
+
page: number;
|
|
8
|
+
total: number;
|
|
9
|
+
perPage?: number;
|
|
10
|
+
/** Plural noun for the range readout: "1–25 of 122 members". */
|
|
11
|
+
label?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
page = $bindable(1),
|
|
17
|
+
total,
|
|
18
|
+
perPage = 25,
|
|
19
|
+
label = 'items',
|
|
20
|
+
class: className = ''
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
|
|
23
|
+
const pageCount = $derived(Math.max(1, Math.ceil(total / perPage)));
|
|
24
|
+
|
|
25
|
+
// A filter can shrink `total` under the current page — clamp on read so the
|
|
26
|
+
// list never renders an empty slice while the control still says "page 4".
|
|
27
|
+
$effect(() => {
|
|
28
|
+
if (page > pageCount) page = pageCount;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const first = $derived(total === 0 ? 0 : (page - 1) * perPage + 1);
|
|
32
|
+
const last = $derived(Math.min(page * perPage, total));
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
{#if pageCount > 1}
|
|
36
|
+
<div class={`gap-4 flex items-center justify-between ${className}`}>
|
|
37
|
+
<p class="text-xs text-muted-foreground">
|
|
38
|
+
{first}–{last} of {total}
|
|
39
|
+
{label}
|
|
40
|
+
</p>
|
|
41
|
+
<div class="gap-1 flex items-center">
|
|
42
|
+
<Button
|
|
43
|
+
size="sm"
|
|
44
|
+
variant="outline"
|
|
45
|
+
disabled={page <= 1}
|
|
46
|
+
onclick={() => (page -= 1)}
|
|
47
|
+
aria-label="Previous page"
|
|
48
|
+
class="h-8 w-8 p-0"
|
|
49
|
+
>
|
|
50
|
+
<ChevronLeft class="h-4 w-4" />
|
|
51
|
+
</Button>
|
|
52
|
+
<span class="px-2 font-mono text-xs text-muted-foreground">{page} / {pageCount}</span>
|
|
53
|
+
<Button
|
|
54
|
+
size="sm"
|
|
55
|
+
variant="outline"
|
|
56
|
+
disabled={page >= pageCount}
|
|
57
|
+
onclick={() => (page += 1)}
|
|
58
|
+
aria-label="Next page"
|
|
59
|
+
class="h-8 w-8 p-0"
|
|
60
|
+
>
|
|
61
|
+
<ChevronRight class="h-4 w-4" />
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
{/if}
|
|
@@ -8,3 +8,4 @@ export { default as SideNav } from './SideNav.svelte';
|
|
|
8
8
|
export type { SideNavItem } from './SideNav.svelte';
|
|
9
9
|
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
10
10
|
export { default as EmptyState } from './EmptyState.svelte';
|
|
11
|
+
export { default as Pagination } from './Pagination.svelte';
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
showDisplayMenu?: boolean;
|
|
51
51
|
enableMeshClick?: boolean;
|
|
52
52
|
backgroundColor?: string;
|
|
53
|
+
/** Initial state of the edge overlay and its display-menu checkmark. Default false. */
|
|
54
|
+
showEdges?: boolean;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
interface Props {
|
|
@@ -83,7 +85,8 @@
|
|
|
83
85
|
showGridToggle: true,
|
|
84
86
|
showDisplayMenu: true,
|
|
85
87
|
enableMeshClick: true,
|
|
86
|
-
backgroundColor: '#E6E6E6'
|
|
88
|
+
backgroundColor: '#E6E6E6',
|
|
89
|
+
showEdges: false
|
|
87
90
|
};
|
|
88
91
|
|
|
89
92
|
let {
|
|
@@ -136,7 +139,8 @@
|
|
|
136
139
|
let measureActive = $state(false);
|
|
137
140
|
let gridVisible = $state(false);
|
|
138
141
|
let renderStyle: Look = $state('technical');
|
|
139
|
-
|
|
142
|
+
// Seeded once, then owned by the menu toggle.
|
|
143
|
+
let edgesVisible = $state(untrack(() => viewerConfig.showEdges) ?? defaultViewerConfig.showEdges);
|
|
140
144
|
let selectedMeshMetadata: Record<string, any> | null = $state(null);
|
|
141
145
|
let selectedMeshName: string | null = $state(null);
|
|
142
146
|
|
|
@@ -385,7 +389,7 @@
|
|
|
385
389
|
<img
|
|
386
390
|
src={logoUrl}
|
|
387
391
|
alt=""
|
|
388
|
-
class="h-
|
|
392
|
+
class="h-10 max-w-40 drop-shadow-sm sm:h-14 sm:max-w-56 w-auto object-contain opacity-80"
|
|
389
393
|
/>
|
|
390
394
|
</div>
|
|
391
395
|
{/if}
|
package/src/lib/constants.ts
CHANGED
|
@@ -2,8 +2,8 @@ export const APP_DEFAULTS = {
|
|
|
2
2
|
// File upload limits
|
|
3
3
|
// TEMP (dev): raised 150 MB → 300 MB so large dev file-widget inputs aren't
|
|
4
4
|
// blocked client-side. The server request cap (COMPUTE_REQUEST_MAX_BYTES)
|
|
5
|
-
// still defaults to
|
|
6
|
-
// anything over ~
|
|
5
|
+
// still defaults to 256 MB, and base64 inflates a raw file by ~4/3, so
|
|
6
|
+
// anything over ~192 MB is rejected with a 413 after the client accepts it.
|
|
7
7
|
// Revert to 150 before release.
|
|
8
8
|
FILE_UPLOAD: {
|
|
9
9
|
MAX_SIZE_MB: 300,
|