@app-studio/web 0.9.30 → 0.9.32
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/Text/Text/Text.view.d.ts +1 -0
- package/dist/web.cjs.development.js +3 -3
- package/dist/web.cjs.development.js.map +1 -1
- package/dist/web.cjs.production.min.js +1 -1
- package/dist/web.cjs.production.min.js.map +1 -1
- package/dist/web.esm.js +3 -3
- package/dist/web.esm.js.map +1 -1
- package/dist/web.umd.development.js +3 -3
- package/dist/web.umd.development.js.map +1 -1
- package/dist/web.umd.production.min.js +1 -1
- package/dist/web.umd.production.min.js.map +1 -1
- package/docs/components/Accordion.mdx +158 -0
- package/docs/components/Alert.mdx +123 -0
- package/docs/components/AspectRatio.mdx +55 -0
- package/docs/components/Avatar.mdx +85 -0
- package/docs/components/Background.mdx +522 -0
- package/docs/components/Badge.mdx +220 -0
- package/docs/components/Button.mdx +272 -0
- package/docs/components/Calendar.mdx +274 -0
- package/docs/components/Card.mdx +341 -0
- package/docs/components/Carousel.mdx +411 -0
- package/docs/components/Center.mdx +474 -0
- package/docs/components/Chart.mdx +232 -0
- package/docs/components/ChatInput.mdx +373 -0
- package/docs/components/Checkbox.mdx +66 -0
- package/docs/components/ColorInput.mdx +209 -0
- package/docs/components/ComboBox.mdx +364 -0
- package/docs/components/Command.mdx +252 -0
- package/docs/components/ContextMenu.mdx +219 -0
- package/docs/components/CountryPicker.mdx +123 -0
- package/docs/components/DatePicker.mdx +77 -0
- package/docs/components/DragAndDrop.mdx +539 -0
- package/docs/components/DropdownMenu.mdx +205 -0
- package/docs/components/File.mdx +8 -0
- package/docs/components/Flow.mdx +257 -0
- package/docs/components/Form.mdx +681 -0
- package/docs/components/Formik.mdx +621 -0
- package/docs/components/Gradient.mdx +271 -0
- package/docs/components/Horizontal.mdx +40 -0
- package/docs/components/HoverCard.mdx +140 -0
- package/docs/components/Icon.mdx +438 -0
- package/docs/components/Label.mdx +438 -0
- package/docs/components/Link.mdx +83 -0
- package/docs/components/Loader.mdx +527 -0
- package/docs/components/Menubar.mdx +124 -0
- package/docs/components/Message.mdx +571 -0
- package/docs/components/Modal.mdx +533 -0
- package/docs/components/NavigationMenu.mdx +165 -0
- package/docs/components/Pagination.mdx +150 -0
- package/docs/components/Password.mdx +121 -0
- package/docs/components/Resizable.mdx +148 -0
- package/docs/components/Select.mdx +126 -0
- package/docs/components/Separator.mdx +121 -0
- package/docs/components/Sidebar.mdx +147 -0
- package/docs/components/Slider.mdx +232 -0
- package/docs/components/Switch.mdx +62 -0
- package/docs/components/Table.mdx +409 -0
- package/docs/components/Tabs.mdx +215 -0
- package/docs/components/TagInput.mdx +528 -0
- package/docs/components/Text.mdx +163 -0
- package/docs/components/TextArea.mdx +136 -0
- package/docs/components/TextField.mdx +225 -0
- package/docs/components/Title.mdx +535 -0
- package/docs/components/Toast.mdx +165 -0
- package/docs/components/Toggle.mdx +141 -0
- package/docs/components/ToggleGroup.mdx +165 -0
- package/docs/components/Tooltip.mdx +191 -0
- package/docs/components/Tree.mdx +340 -0
- package/docs/components/Uploader.mdx +426 -0
- package/docs/components/Vertical.mdx +566 -0
- package/package.json +1 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# Uploader
|
|
2
|
+
|
|
3
|
+
A versatile file upload component with support for single and multiple file uploads, drag & drop functionality, file validation, preview capabilities, and progress tracking. Perfect for handling images, videos, documents, and any file type with built-in error handling and customizable UI.
|
|
4
|
+
|
|
5
|
+
### **Import**
|
|
6
|
+
```tsx
|
|
7
|
+
import { Uploader } from '@app-studio/web';
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
### **Basic Usage**
|
|
11
|
+
```tsx
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { Uploader } from '@app-studio/web';
|
|
14
|
+
import { UploadIcon } from '@app-studio/web';
|
|
15
|
+
|
|
16
|
+
export const BasicUploader = () => {
|
|
17
|
+
const handleFileSelect = (file: File) => {
|
|
18
|
+
console.log('Selected file:', file);
|
|
19
|
+
// Handle the uploaded file
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Uploader
|
|
24
|
+
icon={<UploadIcon widthHeight={32} />}
|
|
25
|
+
text="Click to upload a file"
|
|
26
|
+
onFileSelect={handleFileSelect}
|
|
27
|
+
maxSize={10 * 1024 * 1024} // 10MB limit
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### **Multiple File Upload**
|
|
34
|
+
```tsx
|
|
35
|
+
import React, { useState } from 'react';
|
|
36
|
+
import { Uploader } from '@app-studio/web';
|
|
37
|
+
import { UploadIcon } from '@app-studio/web';
|
|
38
|
+
|
|
39
|
+
export const MultipleFileUploader = () => {
|
|
40
|
+
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
|
41
|
+
|
|
42
|
+
const handleMultipleFileSelect = (files: File[]) => {
|
|
43
|
+
console.log('Selected files:', files);
|
|
44
|
+
setSelectedFiles(files);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Uploader
|
|
49
|
+
icon={<UploadIcon widthHeight={32} />}
|
|
50
|
+
text="Click to upload multiple files"
|
|
51
|
+
multiple={true}
|
|
52
|
+
onMultipleFileSelect={handleMultipleFileSelect}
|
|
53
|
+
maxSize={50 * 1024 * 1024} // 50MB limit per file
|
|
54
|
+
accept="image/*,video/*,.pdf,.doc,.docx"
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### **Image Upload with Preview**
|
|
61
|
+
```tsx
|
|
62
|
+
import React, { useState } from 'react';
|
|
63
|
+
import { Uploader } from '@app-studio/web';
|
|
64
|
+
import { ImageIcon } from '@app-studio/web';
|
|
65
|
+
|
|
66
|
+
export const ImageUploader = () => {
|
|
67
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
68
|
+
const [progress, setProgress] = useState(0);
|
|
69
|
+
|
|
70
|
+
const handleImageSelect = (file: File) => {
|
|
71
|
+
console.log('Selected image:', file);
|
|
72
|
+
|
|
73
|
+
// Simulate upload progress
|
|
74
|
+
setIsLoading(true);
|
|
75
|
+
setProgress(0);
|
|
76
|
+
|
|
77
|
+
const interval = setInterval(() => {
|
|
78
|
+
setProgress((prev) => {
|
|
79
|
+
if (prev >= 100) {
|
|
80
|
+
clearInterval(interval);
|
|
81
|
+
setIsLoading(false);
|
|
82
|
+
return 100;
|
|
83
|
+
}
|
|
84
|
+
return prev + 10;
|
|
85
|
+
});
|
|
86
|
+
}, 200);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Uploader
|
|
91
|
+
accept="image/*"
|
|
92
|
+
icon={<ImageIcon widthHeight={32} />}
|
|
93
|
+
text="Upload Image"
|
|
94
|
+
onFileSelect={handleImageSelect}
|
|
95
|
+
isLoading={isLoading}
|
|
96
|
+
progress={progress}
|
|
97
|
+
fileType="image"
|
|
98
|
+
maxSize={5 * 1024 * 1024} // 5MB limit
|
|
99
|
+
containerProps={{
|
|
100
|
+
height: progress === 100 ? '200px' : 'auto',
|
|
101
|
+
minHeight: '120px'
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### **Video Upload with Thumbnail**
|
|
109
|
+
```tsx
|
|
110
|
+
import React, { useState } from 'react';
|
|
111
|
+
import { Uploader } from '@app-studio/web';
|
|
112
|
+
import { VideoIcon } from '@app-studio/web';
|
|
113
|
+
|
|
114
|
+
export const VideoUploader = () => {
|
|
115
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
116
|
+
const [progress, setProgress] = useState(0);
|
|
117
|
+
|
|
118
|
+
const handleVideoSelect = (file: File) => {
|
|
119
|
+
console.log('Selected video:', file);
|
|
120
|
+
|
|
121
|
+
// Simulate upload progress
|
|
122
|
+
setIsLoading(true);
|
|
123
|
+
setProgress(0);
|
|
124
|
+
|
|
125
|
+
const interval = setInterval(() => {
|
|
126
|
+
setProgress((prev) => {
|
|
127
|
+
if (prev >= 100) {
|
|
128
|
+
clearInterval(interval);
|
|
129
|
+
setIsLoading(false);
|
|
130
|
+
return 100;
|
|
131
|
+
}
|
|
132
|
+
return prev + 5;
|
|
133
|
+
});
|
|
134
|
+
}, 300);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Uploader
|
|
139
|
+
accept="video/*"
|
|
140
|
+
icon={<VideoIcon widthHeight={32} />}
|
|
141
|
+
text="Upload Video"
|
|
142
|
+
onFileSelect={handleVideoSelect}
|
|
143
|
+
isLoading={isLoading}
|
|
144
|
+
progress={progress}
|
|
145
|
+
fileType="video"
|
|
146
|
+
maxSize={100 * 1024 * 1024} // 100MB limit
|
|
147
|
+
containerProps={{
|
|
148
|
+
height: progress === 100 ? '200px' : 'auto',
|
|
149
|
+
minHeight: '120px'
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### **File Validation**
|
|
157
|
+
```tsx
|
|
158
|
+
import React from 'react';
|
|
159
|
+
import { Uploader } from '@app-studio/web';
|
|
160
|
+
import { UploadIcon } from '@app-studio/web';
|
|
161
|
+
|
|
162
|
+
export const ValidatedUploader = () => {
|
|
163
|
+
const validateFile = (file: File): string | null => {
|
|
164
|
+
// Custom validation logic
|
|
165
|
+
if (file.size > 10 * 1024 * 1024) {
|
|
166
|
+
return 'File size must be less than 10MB';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!file.type.startsWith('image/')) {
|
|
170
|
+
return 'Only image files are allowed';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (file.name.length > 50) {
|
|
174
|
+
return 'File name must be less than 50 characters';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null; // No validation errors
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const handleFileSelect = (file: File) => {
|
|
181
|
+
console.log('Valid file selected:', file);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const handleError = (error: string) => {
|
|
185
|
+
console.error('Upload error:', error);
|
|
186
|
+
// Show error message to user
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<Uploader
|
|
191
|
+
icon={<UploadIcon widthHeight={32} />}
|
|
192
|
+
text="Upload Image (Max 10MB)"
|
|
193
|
+
accept="image/*"
|
|
194
|
+
onFileSelect={handleFileSelect}
|
|
195
|
+
validateFile={validateFile}
|
|
196
|
+
onError={handleError}
|
|
197
|
+
maxSize={10 * 1024 * 1024}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### **Custom Rendering**
|
|
204
|
+
```tsx
|
|
205
|
+
import React from 'react';
|
|
206
|
+
import { Uploader } from '@app-studio/web';
|
|
207
|
+
import { View, Text } from 'app-studio';
|
|
208
|
+
import { CheckIcon, ErrorIcon } from '@app-studio/web';
|
|
209
|
+
|
|
210
|
+
export const CustomUploader = () => {
|
|
211
|
+
const handleFileSelect = (file: File) => {
|
|
212
|
+
console.log('File selected:', file);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const customErrorRenderer = ({ errorMessage }: any) => (
|
|
216
|
+
<View
|
|
217
|
+
padding={8}
|
|
218
|
+
backgroundColor="color.red.50"
|
|
219
|
+
borderRadius={4}
|
|
220
|
+
marginTop={8}
|
|
221
|
+
>
|
|
222
|
+
<Text color="color.red.600" fontSize={12}>
|
|
223
|
+
<ErrorIcon widthHeight={16} /> {errorMessage}
|
|
224
|
+
</Text>
|
|
225
|
+
</View>
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const customFileRenderer = ({ selectedFile }: any) => (
|
|
229
|
+
<View
|
|
230
|
+
padding={12}
|
|
231
|
+
backgroundColor="color.green.50"
|
|
232
|
+
borderRadius={4}
|
|
233
|
+
marginTop={8}
|
|
234
|
+
>
|
|
235
|
+
<Text color="color.green.700" fontSize={14}>
|
|
236
|
+
<CheckIcon widthHeight={16} /> {selectedFile.name}
|
|
237
|
+
</Text>
|
|
238
|
+
<Text color="color.green.600" fontSize={12}>
|
|
239
|
+
Size: {Math.round(selectedFile.size / 1024)} KB
|
|
240
|
+
</Text>
|
|
241
|
+
</View>
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<Uploader
|
|
246
|
+
text="Drop your file here or click to browse"
|
|
247
|
+
onFileSelect={handleFileSelect}
|
|
248
|
+
renderError={customErrorRenderer}
|
|
249
|
+
renderFile={customFileRenderer}
|
|
250
|
+
views={{
|
|
251
|
+
container: {
|
|
252
|
+
border: '2px dashed',
|
|
253
|
+
borderColor: 'color.blue.300',
|
|
254
|
+
backgroundColor: 'color.blue.50',
|
|
255
|
+
borderRadius: 12,
|
|
256
|
+
padding: 24,
|
|
257
|
+
},
|
|
258
|
+
text: {
|
|
259
|
+
color: 'color.blue.700',
|
|
260
|
+
fontWeight: 'medium',
|
|
261
|
+
},
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### **Integration with ChatInput**
|
|
269
|
+
The Uploader component is seamlessly integrated into the ChatInput component for file attachments:
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
import React, { useState } from 'react';
|
|
273
|
+
import { ChatInput } from '@app-studio/web';
|
|
274
|
+
import { View } from 'app-studio';
|
|
275
|
+
|
|
276
|
+
export const ChatWithFileUpload = () => {
|
|
277
|
+
const [inputValue, setInputValue] = useState('');
|
|
278
|
+
|
|
279
|
+
const handleSubmit = (message: string, options?: any) => {
|
|
280
|
+
console.log('Message with files:', message);
|
|
281
|
+
console.log('Upload options:', options);
|
|
282
|
+
setInputValue('');
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<View width="100%">
|
|
287
|
+
<ChatInput
|
|
288
|
+
onSubmit={handleSubmit}
|
|
289
|
+
value={inputValue}
|
|
290
|
+
onChange={setInputValue}
|
|
291
|
+
placeholder="Type a message or upload files..."
|
|
292
|
+
hideAttachments={false} // Enable file upload button
|
|
293
|
+
/>
|
|
294
|
+
</View>
|
|
295
|
+
);
|
|
296
|
+
};
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### **Props**
|
|
300
|
+
|
|
301
|
+
| Prop | Type | Default | Description |
|
|
302
|
+
| ---- | ---- | ------- | ----------- |
|
|
303
|
+
| accept | string | '*/*' | File types to accept (MIME types) |
|
|
304
|
+
| icon | React.ReactNode | undefined | Icon to display in the upload area |
|
|
305
|
+
| text | string | undefined | Text to display in the upload area |
|
|
306
|
+
| maxSize | number | 100MB | Maximum file size in bytes |
|
|
307
|
+
| onFileSelect | `(file: File) => void` | undefined | Callback when a single file is selected |
|
|
308
|
+
| onMultipleFileSelect | `(files: File[]) => void` | undefined | Callback when multiple files are selected |
|
|
309
|
+
| validateFile | `(file: File) => string \| null` | undefined | Custom validation function |
|
|
310
|
+
| isLoading | boolean | false | Whether the uploader is in loading state |
|
|
311
|
+
| progress | number | 0 | Upload progress (0-100) |
|
|
312
|
+
| fileType | `'video' \| 'image' \| 'file'` | undefined | Type of file for preview rendering |
|
|
313
|
+
| multiple | boolean | false | Allow multiple file selection |
|
|
314
|
+
| onError | `(error: string) => void` | undefined | Error callback function |
|
|
315
|
+
| thumbnail | string | undefined | Thumbnail URL for preview |
|
|
316
|
+
|
|
317
|
+
### **Custom Rendering Props**
|
|
318
|
+
|
|
319
|
+
| Prop | Type | Description |
|
|
320
|
+
| ---- | ---- | ----------- |
|
|
321
|
+
| renderVideo | `(props: any) => React.ReactNode` | Custom video preview renderer |
|
|
322
|
+
| renderText | `(props: any) => React.ReactNode` | Custom text renderer |
|
|
323
|
+
| renderImage | `(props: any) => React.ReactNode` | Custom image preview renderer |
|
|
324
|
+
| renderError | `(props: any) => React.ReactNode` | Custom error message renderer |
|
|
325
|
+
| renderFile | `(props: any) => React.ReactNode` | Custom file info renderer |
|
|
326
|
+
| renderProgress | `(props: any) => React.ReactNode` | Custom progress indicator renderer |
|
|
327
|
+
|
|
328
|
+
### **View Customization**
|
|
329
|
+
|
|
330
|
+
The `views` prop allows you to customize different parts of the component:
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
<Uploader
|
|
334
|
+
// ...other props
|
|
335
|
+
views={{
|
|
336
|
+
container: { /* styles for the main container */ },
|
|
337
|
+
view: { /* styles for the content wrapper */ },
|
|
338
|
+
image: { /* styles for image preview */ },
|
|
339
|
+
horizontal: { /* styles for horizontal layout */ },
|
|
340
|
+
text: { /* styles for text elements */ },
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### **File Type Detection**
|
|
346
|
+
|
|
347
|
+
The Uploader automatically detects file types and renders appropriate previews:
|
|
348
|
+
|
|
349
|
+
- **Images**: Shows image preview with proper scaling
|
|
350
|
+
- **Videos**: Generates thumbnail and shows video preview
|
|
351
|
+
- **Files**: Displays file name and size information
|
|
352
|
+
|
|
353
|
+
### **Error Handling**
|
|
354
|
+
|
|
355
|
+
The component provides comprehensive error handling:
|
|
356
|
+
|
|
357
|
+
- **File size validation**: Automatic validation against `maxSize` prop
|
|
358
|
+
- **Custom validation**: Use `validateFile` prop for custom rules
|
|
359
|
+
- **Error callbacks**: Handle errors with `onError` callback
|
|
360
|
+
- **Visual feedback**: Error messages displayed in the UI
|
|
361
|
+
|
|
362
|
+
### **Best Practices**
|
|
363
|
+
|
|
364
|
+
**File Size Limits:**
|
|
365
|
+
- Set appropriate `maxSize` limits based on your use case
|
|
366
|
+
- Consider server-side validation as well
|
|
367
|
+
- Provide clear feedback about size limits to users
|
|
368
|
+
|
|
369
|
+
**File Type Restrictions:**
|
|
370
|
+
- Use the `accept` prop to limit file types
|
|
371
|
+
- Implement additional validation in `validateFile` if needed
|
|
372
|
+
- Consider security implications of allowed file types
|
|
373
|
+
|
|
374
|
+
**User Experience:**
|
|
375
|
+
- Provide clear instructions with the `text` prop
|
|
376
|
+
- Use appropriate icons to indicate file types
|
|
377
|
+
- Show progress feedback for large uploads
|
|
378
|
+
- Handle errors gracefully with user-friendly messages
|
|
379
|
+
|
|
380
|
+
**Performance:**
|
|
381
|
+
- Consider lazy loading for large file previews
|
|
382
|
+
- Implement proper cleanup for object URLs
|
|
383
|
+
- Use appropriate file size limits to prevent memory issues
|
|
384
|
+
|
|
385
|
+
### **Accessibility**
|
|
386
|
+
|
|
387
|
+
The Uploader component includes built-in accessibility features:
|
|
388
|
+
|
|
389
|
+
- Keyboard navigation support
|
|
390
|
+
- ARIA attributes for screen readers
|
|
391
|
+
- Focus management
|
|
392
|
+
- Proper contrast for visual elements
|
|
393
|
+
|
|
394
|
+
### **Integration Examples**
|
|
395
|
+
|
|
396
|
+
**With Form Libraries:**
|
|
397
|
+
```tsx
|
|
398
|
+
// Integration with Formik
|
|
399
|
+
import { useFormikContext } from 'formik';
|
|
400
|
+
|
|
401
|
+
const FormikUploader = ({ name, ...props }) => {
|
|
402
|
+
const { setFieldValue } = useFormikContext();
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<Uploader
|
|
406
|
+
onFileSelect={(file) => setFieldValue(name, file)}
|
|
407
|
+
{...props}
|
|
408
|
+
/>
|
|
409
|
+
);
|
|
410
|
+
};
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**With State Management:**
|
|
414
|
+
```tsx
|
|
415
|
+
// Integration with Redux or other state management
|
|
416
|
+
const ConnectedUploader = () => {
|
|
417
|
+
const dispatch = useDispatch();
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<Uploader
|
|
421
|
+
onFileSelect={(file) => dispatch(uploadFile(file))}
|
|
422
|
+
onMultipleFileSelect={(files) => dispatch(uploadMultipleFiles(files))}
|
|
423
|
+
/>
|
|
424
|
+
);
|
|
425
|
+
};
|
|
426
|
+
```
|