@hyvor/design 1.1.18 → 1.1.19-beta-file-uploader.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/Base/Base.svelte +2 -0
- package/dist/components/EmojiPicker/EmojiSelector.svelte +0 -2
- package/dist/components/FileUploader/FileUploader.svelte +137 -0
- package/dist/components/FileUploader/FileUploader.svelte.d.ts +3 -0
- package/dist/components/FileUploader/FileUploaderProvider.svelte +8 -0
- package/dist/components/FileUploader/FileUploaderProvider.svelte.d.ts +18 -0
- package/dist/components/FileUploader/Preview/Meta.svelte +30 -0
- package/dist/components/FileUploader/Preview/Meta.svelte.d.ts +7 -0
- package/dist/components/FileUploader/Preview/Preview.svelte +308 -0
- package/dist/components/FileUploader/Preview/Preview.svelte.d.ts +3 -0
- package/dist/components/FileUploader/TabUpload/TabUpload.svelte +284 -0
- package/dist/components/FileUploader/TabUpload/TabUpload.svelte.d.ts +3 -0
- package/dist/components/FileUploader/file-uploader.d.ts +56 -0
- package/dist/components/FileUploader/file-uploader.js +92 -0
- package/dist/components/FileUploader/helpers.d.ts +6 -0
- package/dist/components/FileUploader/helpers.js +31 -0
- package/dist/components/HyvorBar/BarUserPreview.svelte +0 -5
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import DarkProvider from './../Dark/DarkProvider.svelte';
|
|
5
5
|
import '../../index.js';
|
|
6
6
|
import ToastProvider from '../Toast/ToastProvider.svelte';
|
|
7
|
+
import FileUploaderProvider from '../FileUploader/FileUploaderProvider.svelte';
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
dark?: boolean;
|
|
@@ -23,3 +24,4 @@
|
|
|
23
24
|
|
|
24
25
|
<ToastProvider />
|
|
25
26
|
<ConfirmModalProvider />
|
|
27
|
+
<FileUploaderProvider />
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import IconCardImage from '@hyvor/icons/IconCardImage';
|
|
3
|
+
import IconCaretLeft from '@hyvor/icons/IconCaretLeft';
|
|
4
|
+
import IconCloudUpload from '@hyvor/icons/IconCloudUpload';
|
|
5
|
+
import Button from '../Button/Button.svelte';
|
|
6
|
+
import Modal from '../Modal/Modal.svelte';
|
|
7
|
+
import TabNav from '../TabNav/TabNav.svelte';
|
|
8
|
+
import TabNavItem from '../TabNav/TabNavItem.svelte';
|
|
9
|
+
import {
|
|
10
|
+
clearSelectedFile,
|
|
11
|
+
closeFileUploader,
|
|
12
|
+
getFileUploaderConfig,
|
|
13
|
+
selectedFile
|
|
14
|
+
} from './file-uploader.js';
|
|
15
|
+
import TabUpload from './TabUpload/TabUpload.svelte';
|
|
16
|
+
import Preview from './Preview/Preview.svelte';
|
|
17
|
+
|
|
18
|
+
const config = getFileUploaderConfig();
|
|
19
|
+
|
|
20
|
+
let tab = $state('upload');
|
|
21
|
+
|
|
22
|
+
function onClose() {
|
|
23
|
+
closeFileUploader();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function handleBack() {
|
|
27
|
+
clearSelectedFile();
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div class="image-uploader">
|
|
32
|
+
<Modal
|
|
33
|
+
show={true}
|
|
34
|
+
size="large"
|
|
35
|
+
closeOnEscape={false}
|
|
36
|
+
closeOnOutsideClick={false}
|
|
37
|
+
on:cancel={onClose}
|
|
38
|
+
>
|
|
39
|
+
{#snippet title()}
|
|
40
|
+
<div>
|
|
41
|
+
{#if $selectedFile}
|
|
42
|
+
<Button color="input" onclick={handleBack}>
|
|
43
|
+
{#snippet start()}
|
|
44
|
+
<IconCaretLeft va />
|
|
45
|
+
{/snippet}
|
|
46
|
+
Back
|
|
47
|
+
</Button>
|
|
48
|
+
{:else}
|
|
49
|
+
<TabNav bind:active={tab}>
|
|
50
|
+
<TabNavItem name="upload">
|
|
51
|
+
{#snippet start()}
|
|
52
|
+
<IconCloudUpload />
|
|
53
|
+
{/snippet}
|
|
54
|
+
Upload
|
|
55
|
+
</TabNavItem>
|
|
56
|
+
|
|
57
|
+
<!-- <TabNavItem name="media">
|
|
58
|
+
{#snippet start()}
|
|
59
|
+
<IconCardImage />
|
|
60
|
+
{/snippet}
|
|
61
|
+
Media Library
|
|
62
|
+
</TabNavItem> -->
|
|
63
|
+
|
|
64
|
+
{#if config.type === 'image'}
|
|
65
|
+
<!-- <TabNavItem name="unsplash">
|
|
66
|
+
{#snippet start()}
|
|
67
|
+
<svg
|
|
68
|
+
role="img"
|
|
69
|
+
width="1em"
|
|
70
|
+
height="1em"
|
|
71
|
+
fill="currentColor"
|
|
72
|
+
viewBox="0 0 24 24"
|
|
73
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
74
|
+
><path
|
|
75
|
+
d="M7.5 6.75V0h9v6.75h-9zm9 3.75H24V24H0V10.5h7.5v6.75h9V10.5z"
|
|
76
|
+
/></svg
|
|
77
|
+
>
|
|
78
|
+
{/snippet}
|
|
79
|
+
Unsplash
|
|
80
|
+
</TabNavItem>
|
|
81
|
+
<TabNavItem name="excalidraw">
|
|
82
|
+
{#snippet start()}
|
|
83
|
+
<ExcalidrawIcon />
|
|
84
|
+
{/snippet}
|
|
85
|
+
Excalidraw
|
|
86
|
+
</TabNavItem> -->
|
|
87
|
+
{/if}
|
|
88
|
+
</TabNav>
|
|
89
|
+
{/if}
|
|
90
|
+
</div>
|
|
91
|
+
{/snippet}
|
|
92
|
+
<div class="body" style:position={selectedFile ? 'relative' : undefined}>
|
|
93
|
+
{#if tab === 'upload'}
|
|
94
|
+
<TabUpload />
|
|
95
|
+
{/if}
|
|
96
|
+
|
|
97
|
+
{#if $selectedFile}
|
|
98
|
+
<Preview />
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
<!-- {#if tab === 'upload'}
|
|
102
|
+
<TabUpload {type} on:select={handleSelect} />
|
|
103
|
+
{:else if tab === 'media' && type !== 'any'}
|
|
104
|
+
<Media {type} on:select={handleSelect} />
|
|
105
|
+
{:else if tab === 'unsplash'}
|
|
106
|
+
<Unsplash on:select={handleSelect} />
|
|
107
|
+
{:else if tab === 'excalidraw'}
|
|
108
|
+
<Excalidraw on:select={handleSelect} />
|
|
109
|
+
{/if}
|
|
110
|
+
|
|
111
|
+
-->
|
|
112
|
+
</div>
|
|
113
|
+
</Modal>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<style>.image-uploader :global(.wrap) {
|
|
117
|
+
z-index: 1000 !important;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.image-uploader :global(.inner) {
|
|
121
|
+
height: 100%;
|
|
122
|
+
width: 1100px !important;
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
}
|
|
126
|
+
.image-uploader :global(.inner) :global(> .content) {
|
|
127
|
+
flex: 1;
|
|
128
|
+
padding-top: 0;
|
|
129
|
+
min-height: 0;
|
|
130
|
+
display: flex;
|
|
131
|
+
flex-direction: column;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.body {
|
|
135
|
+
flex: 1;
|
|
136
|
+
min-height: 0;
|
|
137
|
+
}</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const FileUploaderProvider: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type FileUploaderProvider = InstanceType<typeof FileUploaderProvider>;
|
|
18
|
+
export default FileUploaderProvider;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
name: string;
|
|
4
|
+
children?: import('svelte').Snippet;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { name, children }: Props = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div class="meta">
|
|
11
|
+
<div class="name">{name}</div>
|
|
12
|
+
<div class="value">{@render children?.()}</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
.meta {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
padding: 0 15px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.name {
|
|
23
|
+
font-size: 13px;
|
|
24
|
+
color: var(--text-light);
|
|
25
|
+
margin-bottom: 10px;
|
|
26
|
+
}
|
|
27
|
+
.value {
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Meta from './Meta.svelte';
|
|
3
|
+
import IconCheckAll from '@hyvor/icons/IconCheckAll';
|
|
4
|
+
import IconCloudUpload from '@hyvor/icons/IconCloudUpload';
|
|
5
|
+
|
|
6
|
+
import { onDestroy, onMount } from 'svelte';
|
|
7
|
+
import { completeFileUpload, getFileUploaderConfig, selectedFile } from '../file-uploader.js';
|
|
8
|
+
import TextInput from '../../TextInput/TextInput.svelte';
|
|
9
|
+
import Switch from '../../Switch/Switch.svelte';
|
|
10
|
+
import Button from '../../Button/Button.svelte';
|
|
11
|
+
import Loader from '../../Loader/Loader.svelte';
|
|
12
|
+
import toast from '../../Toast/toast.js';
|
|
13
|
+
import { byteFormatter, toKebabCase } from '../helpers.js';
|
|
14
|
+
|
|
15
|
+
const config = getFileUploaderConfig();
|
|
16
|
+
let file = $selectedFile!;
|
|
17
|
+
|
|
18
|
+
const fileUrl = getFileUrl();
|
|
19
|
+
|
|
20
|
+
function getFileUrl(): string {
|
|
21
|
+
if (file.upload) {
|
|
22
|
+
return URL.createObjectURL(file.upload.blob);
|
|
23
|
+
}
|
|
24
|
+
return ''; // TODO
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let imageSize = $state(getInitialImageSize());
|
|
28
|
+
|
|
29
|
+
function getInitialImageSize() {
|
|
30
|
+
if (file.upload) {
|
|
31
|
+
return file.upload.blob.size;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let imageName = $state(toKebabCase(getInitialImageName()));
|
|
37
|
+
|
|
38
|
+
function getInitialImageName() {
|
|
39
|
+
if (file.upload && file.upload.blob instanceof File) {
|
|
40
|
+
return file.upload.blob.name;
|
|
41
|
+
}
|
|
42
|
+
// TODO: add other
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let nameError = $state('');
|
|
47
|
+
|
|
48
|
+
let imgEl: HTMLImageElement | undefined = $state();
|
|
49
|
+
|
|
50
|
+
let width = $state(0);
|
|
51
|
+
let height = $state(0);
|
|
52
|
+
|
|
53
|
+
function getShouldUpload() {
|
|
54
|
+
if (file.from === 'excalidraw' || file.from == 'upload') return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getCanChangeUpload() {
|
|
59
|
+
return file.from === 'upload' && file.upload?.type === 'url' && file.upload?.fetchedUrl;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getHosting() {
|
|
63
|
+
if (file.from === 'upload') {
|
|
64
|
+
if (file.upload?.type === 'url' && file.upload?.fetchedUrl) {
|
|
65
|
+
const fetchedUrl = file.upload.fetchedUrl!;
|
|
66
|
+
const domain = new URL(fetchedUrl).hostname;
|
|
67
|
+
return `External (${domain})`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (file.from === 'unsplash') {
|
|
72
|
+
return 'Unsplash';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (file.from === 'media') {
|
|
76
|
+
return 'Media Library';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let shouldUpload = $state(getShouldUpload());
|
|
83
|
+
const canChangeUpload = getCanChangeUpload();
|
|
84
|
+
const hosting = getHosting();
|
|
85
|
+
|
|
86
|
+
function handleImageLoad() {
|
|
87
|
+
if (!imgEl) return;
|
|
88
|
+
width = imgEl.naturalWidth;
|
|
89
|
+
height = imgEl.naturalHeight;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function tryGetSize() {
|
|
93
|
+
fetch(fileUrl)
|
|
94
|
+
.then((res) => res.blob())
|
|
95
|
+
.then((blob) => {
|
|
96
|
+
imageSize = blob.size;
|
|
97
|
+
})
|
|
98
|
+
.catch((err) => {
|
|
99
|
+
console.error(err);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let isUploading = $state(false);
|
|
104
|
+
|
|
105
|
+
function handleUpload() {
|
|
106
|
+
if (shouldUpload && file.upload) {
|
|
107
|
+
// TODO: other blobs should be handled
|
|
108
|
+
isUploading = true;
|
|
109
|
+
|
|
110
|
+
if (imageName.length > 255) {
|
|
111
|
+
toast.error('Image name is too long');
|
|
112
|
+
isUploading = false;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
config
|
|
117
|
+
.uploader(file.upload.blob, imageName)
|
|
118
|
+
.then((res) => {
|
|
119
|
+
completeFileUpload({
|
|
120
|
+
url: res.url,
|
|
121
|
+
selectedFile: file
|
|
122
|
+
});
|
|
123
|
+
})
|
|
124
|
+
.catch((err) => {
|
|
125
|
+
toast.error(err.message || 'Failed to upload image');
|
|
126
|
+
})
|
|
127
|
+
.finally(() => {
|
|
128
|
+
isUploading = false;
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
completeFileUpload({
|
|
132
|
+
url: file.upload!.fetchedUrl!,
|
|
133
|
+
selectedFile: file
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function handleNameChange() {
|
|
139
|
+
nameError = '';
|
|
140
|
+
if (imageName.length > 255) {
|
|
141
|
+
nameError = 'Too long';
|
|
142
|
+
} else if (imageName.includes('/')) {
|
|
143
|
+
nameError = '/ is not allowed';
|
|
144
|
+
}
|
|
145
|
+
console.log(imageName);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
onMount(() => {
|
|
149
|
+
if (imageSize === null) {
|
|
150
|
+
tryGetSize();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
onDestroy(() => {
|
|
155
|
+
URL.revokeObjectURL(fileUrl);
|
|
156
|
+
});
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<div class="selected-image">
|
|
160
|
+
{#if isUploading}
|
|
161
|
+
<Loader full>Uploading...</Loader>
|
|
162
|
+
{:else}
|
|
163
|
+
<div class="img-wrap">
|
|
164
|
+
{#if file.type === 'audio'}
|
|
165
|
+
<audio src={fileUrl} controls></audio>
|
|
166
|
+
{:else if file.type === 'image'}
|
|
167
|
+
<img src={fileUrl} alt="Editing" bind:this={imgEl} onload={handleImageLoad} />
|
|
168
|
+
{:else}
|
|
169
|
+
No preview available
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="top-bar">
|
|
174
|
+
<div class="meta">
|
|
175
|
+
{#if file.type === 'image'}
|
|
176
|
+
<Meta name="Dimensions (px)">
|
|
177
|
+
{width} x {height}
|
|
178
|
+
</Meta>
|
|
179
|
+
{/if}
|
|
180
|
+
<Meta name="File Size">
|
|
181
|
+
{#if imageSize !== null}
|
|
182
|
+
{byteFormatter(imageSize)}
|
|
183
|
+
{:else}
|
|
184
|
+
Unknown
|
|
185
|
+
{/if}
|
|
186
|
+
</Meta>
|
|
187
|
+
<div class="name-editor">
|
|
188
|
+
<div class="name">
|
|
189
|
+
Name
|
|
190
|
+
{#if nameError}
|
|
191
|
+
<span class="name-error">Error: {nameError}</span>
|
|
192
|
+
{/if}
|
|
193
|
+
</div>
|
|
194
|
+
<TextInput
|
|
195
|
+
bind:value={imageName}
|
|
196
|
+
on:input={handleNameChange}
|
|
197
|
+
placeholder="Image Name"
|
|
198
|
+
state={nameError ? 'error' : 'default'}
|
|
199
|
+
disabled={!shouldUpload}
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
{#if hosting}
|
|
203
|
+
<Meta name="Hosting">
|
|
204
|
+
{hosting}
|
|
205
|
+
</Meta>
|
|
206
|
+
{/if}
|
|
207
|
+
</div>
|
|
208
|
+
{#if file.from !== 'media'}
|
|
209
|
+
<div class="upload-switch">
|
|
210
|
+
Upload to Media Library
|
|
211
|
+
<Switch bind:checked={shouldUpload} disabled={!canChangeUpload} />
|
|
212
|
+
</div>
|
|
213
|
+
{/if}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div class="footer">
|
|
217
|
+
<Button on:click={handleUpload}>
|
|
218
|
+
{shouldUpload ? 'Upload' : 'Select'}
|
|
219
|
+
{#snippet end()}
|
|
220
|
+
{#if shouldUpload}
|
|
221
|
+
<IconCloudUpload />
|
|
222
|
+
{:else}
|
|
223
|
+
<IconCheckAll />
|
|
224
|
+
{/if}
|
|
225
|
+
{/snippet}
|
|
226
|
+
</Button>
|
|
227
|
+
</div>
|
|
228
|
+
{/if}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<style>
|
|
232
|
+
.selected-image {
|
|
233
|
+
position: absolute;
|
|
234
|
+
z-index: 1000000;
|
|
235
|
+
top: 0;
|
|
236
|
+
left: 0;
|
|
237
|
+
width: 100%;
|
|
238
|
+
height: 100%;
|
|
239
|
+
background-color: var(--box-background);
|
|
240
|
+
display: flex;
|
|
241
|
+
flex-direction: column;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.top-bar {
|
|
245
|
+
padding: 10px 25px;
|
|
246
|
+
border-top: 1px solid var(--border);
|
|
247
|
+
border-bottom: 1px solid var(--border);
|
|
248
|
+
margin: 0 -25px;
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.meta {
|
|
254
|
+
display: flex;
|
|
255
|
+
flex: 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.img-wrap {
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
justify-content: center;
|
|
262
|
+
height: 100%;
|
|
263
|
+
padding: 25px;
|
|
264
|
+
flex: 1;
|
|
265
|
+
min-height: 0;
|
|
266
|
+
min-width: 0;
|
|
267
|
+
margin-bottom: 20px;
|
|
268
|
+
}
|
|
269
|
+
.img-wrap :global(img) {
|
|
270
|
+
display: block;
|
|
271
|
+
max-width: 100%;
|
|
272
|
+
}
|
|
273
|
+
img {
|
|
274
|
+
max-width: 100%;
|
|
275
|
+
max-height: 100%;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.upload-switch {
|
|
279
|
+
display: inline-flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
gap: 10px;
|
|
282
|
+
font-size: 14px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.footer {
|
|
286
|
+
padding: 5px 25px;
|
|
287
|
+
padding-top: 15px;
|
|
288
|
+
text-align: center;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.name-editor {
|
|
292
|
+
display: flex;
|
|
293
|
+
flex-direction: column;
|
|
294
|
+
padding: 0 15px;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.name {
|
|
298
|
+
font-size: 13px;
|
|
299
|
+
color: var(--text-light);
|
|
300
|
+
margin-bottom: 5px;
|
|
301
|
+
display: inline-flex;
|
|
302
|
+
gap: 4px;
|
|
303
|
+
}
|
|
304
|
+
.name .name-error {
|
|
305
|
+
color: var(--red-dark);
|
|
306
|
+
font-size: 12px;
|
|
307
|
+
}
|
|
308
|
+
</style>
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import IconArrowReturnLeft from '@hyvor/icons/IconArrowReturnLeft';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
4
|
+
import {
|
|
5
|
+
getFileUploaderConfig,
|
|
6
|
+
getMimeNamesJoined,
|
|
7
|
+
setSelectedFile,
|
|
8
|
+
validateMimeType,
|
|
9
|
+
type UploadType
|
|
10
|
+
} from '../file-uploader.js';
|
|
11
|
+
import { byteFormatter, isValidUrl } from '../helpers.js';
|
|
12
|
+
import toast from '../../Toast/toast.js';
|
|
13
|
+
import Loader from '../../Loader/Loader.svelte';
|
|
14
|
+
import TextInput from '../../TextInput/TextInput.svelte';
|
|
15
|
+
import Button from '../../Button/Button.svelte';
|
|
16
|
+
|
|
17
|
+
let isUploading = $state(false);
|
|
18
|
+
|
|
19
|
+
const config = getFileUploaderConfig();
|
|
20
|
+
|
|
21
|
+
let inputEl: HTMLInputElement;
|
|
22
|
+
let byUrlInputEl: HTMLInputElement | undefined = $state();
|
|
23
|
+
|
|
24
|
+
let byUrl = $state('');
|
|
25
|
+
let isDragging = $state(false);
|
|
26
|
+
|
|
27
|
+
function getCtrl() {
|
|
28
|
+
const platform =
|
|
29
|
+
(navigator as any)?.userAgentData?.platform || navigator?.platform || 'unknown';
|
|
30
|
+
return platform.match(/mac/i) ? '⌘' : 'Ctrl';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getSelectedType(blob: Blob | null = null): UploadType {
|
|
34
|
+
if (config.type === 'file' && blob) {
|
|
35
|
+
if (blob.type.indexOf('image') === 0) return 'image';
|
|
36
|
+
if (blob.type.indexOf('audio') === 0) return 'audio';
|
|
37
|
+
}
|
|
38
|
+
return config.type;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleFetch() {
|
|
42
|
+
isUploading = true;
|
|
43
|
+
|
|
44
|
+
fetch(byUrl)
|
|
45
|
+
.then((res) => res.blob())
|
|
46
|
+
.then((blob) => {
|
|
47
|
+
if (!validateMimeType(blob.type)) {
|
|
48
|
+
const names = getMimeNamesJoined();
|
|
49
|
+
toast.error(
|
|
50
|
+
`Only ${names} files are allowed. Current file type is ${blob.type}`
|
|
51
|
+
);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setSelectedFile({
|
|
56
|
+
type: getSelectedType(blob),
|
|
57
|
+
from: 'upload',
|
|
58
|
+
upload: {
|
|
59
|
+
type: 'url',
|
|
60
|
+
fetchedUrl: byUrl,
|
|
61
|
+
blob
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
})
|
|
65
|
+
.catch((err) => {
|
|
66
|
+
toast.error(`Failed to fetch file from the URL`);
|
|
67
|
+
})
|
|
68
|
+
.finally(() => {
|
|
69
|
+
isUploading = false;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function handlePaste(e: ClipboardEvent) {
|
|
74
|
+
// first check if there's any text (url)
|
|
75
|
+
const text = e.clipboardData?.getData('text/plain') || '';
|
|
76
|
+
if (isValidUrl(text) && (e.target as HTMLElement).tagName !== 'INPUT') {
|
|
77
|
+
byUrl = text;
|
|
78
|
+
handleFetch();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// only looking for images
|
|
83
|
+
if (config.type === 'audio') return;
|
|
84
|
+
|
|
85
|
+
const items = e.clipboardData?.items;
|
|
86
|
+
if (!items) return;
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < items.length; i++) {
|
|
89
|
+
const item = items[i]!;
|
|
90
|
+
if (item.type.indexOf('image') === 0) {
|
|
91
|
+
const blob = item.getAsFile();
|
|
92
|
+
if (!blob) continue;
|
|
93
|
+
|
|
94
|
+
setSelectedFile({
|
|
95
|
+
type: getSelectedType(blob),
|
|
96
|
+
from: 'upload',
|
|
97
|
+
upload: { type: 'paste', blob }
|
|
98
|
+
});
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleDragEnter(e: DragEvent) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
isDragging = true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handleDragLeave(e: DragEvent) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
e.stopPropagation();
|
|
113
|
+
isDragging = false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handleDragDrop(e: DragEvent) {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
|
|
120
|
+
isDragging = false;
|
|
121
|
+
|
|
122
|
+
if (!e.dataTransfer) return;
|
|
123
|
+
const files = e.dataTransfer.files;
|
|
124
|
+
const file = getFileFromFiles(files);
|
|
125
|
+
if (!file) return;
|
|
126
|
+
|
|
127
|
+
setSelectedFile({
|
|
128
|
+
type: getSelectedType(file),
|
|
129
|
+
from: 'upload',
|
|
130
|
+
upload: { type: 'dnd', blob: file }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function handleUploadClick() {
|
|
135
|
+
inputEl?.click();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function handleInputChange(e: any) {
|
|
139
|
+
const file = getFileFromFiles(e.target.files);
|
|
140
|
+
if (!file) return;
|
|
141
|
+
|
|
142
|
+
setSelectedFile({
|
|
143
|
+
type: getSelectedType(file),
|
|
144
|
+
from: 'upload',
|
|
145
|
+
upload: { type: 'browse', blob: file }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getFileFromFiles(files: FileList | null): File | null {
|
|
150
|
+
if (!files || files.length === 0) {
|
|
151
|
+
toast.error('No files selected');
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const file = files[0];
|
|
155
|
+
if (!file) {
|
|
156
|
+
toast.error('No files selected');
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const maxBytes = config.maxFileSizeInMB * 1024 * 1024;
|
|
161
|
+
if (file.size > maxBytes) {
|
|
162
|
+
toast.error('File size exceeds the limit of ' + byteFormatter(maxBytes));
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (validateMimeType(file.type) === false) {
|
|
167
|
+
const names = getMimeNamesJoined();
|
|
168
|
+
toast.error(`Only ${names} files are allowed. Current file type is ${file.type}`);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return file;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
onMount(() => {
|
|
176
|
+
byUrl && byUrlInputEl && byUrlInputEl.focus();
|
|
177
|
+
});
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<svelte:window
|
|
181
|
+
onpaste={handlePaste}
|
|
182
|
+
ondragenter={handleDragEnter}
|
|
183
|
+
ondragover={handleDragEnter}
|
|
184
|
+
ondragleave={handleDragLeave}
|
|
185
|
+
ondragexit={handleDragLeave}
|
|
186
|
+
/>
|
|
187
|
+
|
|
188
|
+
<div class="tab">
|
|
189
|
+
<input
|
|
190
|
+
type="file"
|
|
191
|
+
accept={config.type === 'audio' ? 'audio/*' : config.type === 'image' ? 'image/*' : '*'}
|
|
192
|
+
style="display:none"
|
|
193
|
+
bind:this={inputEl}
|
|
194
|
+
onchange={handleInputChange}
|
|
195
|
+
/>
|
|
196
|
+
|
|
197
|
+
{#if isUploading}
|
|
198
|
+
<Loader full />
|
|
199
|
+
{:else}
|
|
200
|
+
<div class="upload-wrap">
|
|
201
|
+
<div
|
|
202
|
+
class="upload-area"
|
|
203
|
+
onclick={handleUploadClick}
|
|
204
|
+
ondrop={handleDragDrop}
|
|
205
|
+
role="button"
|
|
206
|
+
tabindex="0"
|
|
207
|
+
onkeyup={(e) => e.key === 'Enter' && handleUploadClick()}
|
|
208
|
+
>
|
|
209
|
+
{#if isDragging}
|
|
210
|
+
Drop here!
|
|
211
|
+
{:else}
|
|
212
|
+
Drag and drop, paste ({getCtrl()} + v), or click to upload
|
|
213
|
+
{/if}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{#if config.type === 'image'}
|
|
218
|
+
<div class="by-url-wrap">
|
|
219
|
+
<div class="title">or, Upload by URL</div>
|
|
220
|
+
|
|
221
|
+
<div class="input-button">
|
|
222
|
+
<TextInput
|
|
223
|
+
block
|
|
224
|
+
placeholder="Enter image URL"
|
|
225
|
+
bind:value={byUrl}
|
|
226
|
+
on:keyup={(e) => e.key === 'Enter' && handleFetch()}
|
|
227
|
+
bind:input={byUrlInputEl}
|
|
228
|
+
/>
|
|
229
|
+
<Button disabled={byUrl.trim() === ''} on:click={handleFetch}>
|
|
230
|
+
Fetch {#snippet end()}
|
|
231
|
+
<IconArrowReturnLeft />
|
|
232
|
+
{/snippet}
|
|
233
|
+
</Button>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
{/if}
|
|
237
|
+
{/if}
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<style>.tab {
|
|
241
|
+
height: 100%;
|
|
242
|
+
display: flex;
|
|
243
|
+
flex-direction: column;
|
|
244
|
+
padding-bottom: 15px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.upload-wrap {
|
|
248
|
+
flex: 1;
|
|
249
|
+
width: 100%;
|
|
250
|
+
height: 100%;
|
|
251
|
+
}
|
|
252
|
+
.upload-wrap .upload-area {
|
|
253
|
+
background-color: var(--input);
|
|
254
|
+
width: 100%;
|
|
255
|
+
height: 100%;
|
|
256
|
+
border-radius: 20px;
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
justify-content: center;
|
|
260
|
+
font-size: 14px;
|
|
261
|
+
color: var(--text-light);
|
|
262
|
+
transition: 0.2s box-shadow;
|
|
263
|
+
cursor: pointer;
|
|
264
|
+
}
|
|
265
|
+
.upload-wrap .upload-area:hover {
|
|
266
|
+
box-shadow: 0 0 0 2px var(--accent-light);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.by-url-wrap {
|
|
270
|
+
margin-top: 15px;
|
|
271
|
+
}
|
|
272
|
+
.by-url-wrap .title {
|
|
273
|
+
font-size: 0.9rem;
|
|
274
|
+
font-weight: 500;
|
|
275
|
+
color: var(--text-light);
|
|
276
|
+
margin-bottom: 10px;
|
|
277
|
+
padding-left: 5px;
|
|
278
|
+
text-align: center;
|
|
279
|
+
}
|
|
280
|
+
.by-url-wrap .input-button {
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
gap: 10px;
|
|
284
|
+
}</style>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export declare let fileUploaderConfig: import("svelte/store").Writable<FileUploaderConfigInternal | null>;
|
|
2
|
+
export declare let selectedFile: import("svelte/store").Writable<SelectedFile | null>;
|
|
3
|
+
export type UploadType = 'image' | 'audio' | 'file';
|
|
4
|
+
export interface FileUploaderConfig {
|
|
5
|
+
/**
|
|
6
|
+
* image:
|
|
7
|
+
* - shows unsplash and excalidraw tabs
|
|
8
|
+
* audio:
|
|
9
|
+
* - shows audio preview
|
|
10
|
+
* file
|
|
11
|
+
* - allows any file type
|
|
12
|
+
* - preview tries to detect file type (image/audio/other)
|
|
13
|
+
*/
|
|
14
|
+
type: UploadType;
|
|
15
|
+
uploader: (file: Blob, name: string | null) => Promise<{
|
|
16
|
+
url: string;
|
|
17
|
+
}>;
|
|
18
|
+
allowedMimeTypes?: string[];
|
|
19
|
+
maxFileSizeInMB?: number;
|
|
20
|
+
}
|
|
21
|
+
export type SelectedFileFrom = 'upload' | 'media' | 'unsplash' | 'excalidraw';
|
|
22
|
+
export type SelectedFileUploadType = 'paste' | 'dnd' | 'browse' | 'url';
|
|
23
|
+
export interface UnsplashImage {
|
|
24
|
+
url: string;
|
|
25
|
+
author: string;
|
|
26
|
+
author_url: string;
|
|
27
|
+
title: string | null;
|
|
28
|
+
alt: string | null;
|
|
29
|
+
}
|
|
30
|
+
export interface SelectedFile {
|
|
31
|
+
type: UploadType;
|
|
32
|
+
from: SelectedFileFrom;
|
|
33
|
+
upload?: {
|
|
34
|
+
type: SelectedFileUploadType;
|
|
35
|
+
blob: Blob;
|
|
36
|
+
fetchedUrl?: string;
|
|
37
|
+
};
|
|
38
|
+
unsplash?: UnsplashImage;
|
|
39
|
+
}
|
|
40
|
+
export interface UploadedFile {
|
|
41
|
+
url: string;
|
|
42
|
+
selectedFile: SelectedFile;
|
|
43
|
+
}
|
|
44
|
+
export type FileUploaderConfigInternal = Required<FileUploaderConfig> & {
|
|
45
|
+
onCancel: () => void;
|
|
46
|
+
onUpload: (file: UploadedFile) => void;
|
|
47
|
+
};
|
|
48
|
+
export declare function uploadFile(config: FileUploaderConfig): Promise<UploadedFile | null>;
|
|
49
|
+
export declare function getFileUploaderConfig(): FileUploaderConfigInternal;
|
|
50
|
+
export declare function closeFileUploader(): void;
|
|
51
|
+
export declare function completeFileUpload(file: UploadedFile): void;
|
|
52
|
+
export declare function setSelectedFile(file: SelectedFile): void;
|
|
53
|
+
export declare function clearSelectedFile(): void;
|
|
54
|
+
export declare function validateMimeType(mimeType: string): boolean;
|
|
55
|
+
export declare function getMimeNames(): string[];
|
|
56
|
+
export declare function getMimeNamesJoined(): string;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { get, writable } from "svelte/store";
|
|
2
|
+
export let fileUploaderConfig = writable(null);
|
|
3
|
+
export let selectedFile = writable(null);
|
|
4
|
+
const defaults = {
|
|
5
|
+
type: 'image',
|
|
6
|
+
uploader: null,
|
|
7
|
+
allowedMimeTypes: [],
|
|
8
|
+
maxFileSizeInMB: 10,
|
|
9
|
+
};
|
|
10
|
+
// UploadedFile is uploaded
|
|
11
|
+
// null means cancelled
|
|
12
|
+
export function uploadFile(config) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const finalConfig = {
|
|
15
|
+
...defaults,
|
|
16
|
+
...config,
|
|
17
|
+
onCancel: () => {
|
|
18
|
+
resolve(null);
|
|
19
|
+
},
|
|
20
|
+
onUpload: (file) => {
|
|
21
|
+
resolve(file);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
if (config.allowedMimeTypes === undefined) {
|
|
25
|
+
// set based on type
|
|
26
|
+
if (finalConfig.type === 'image') {
|
|
27
|
+
finalConfig.allowedMimeTypes = ALLOWED_MIME_TYPES_IMAGE;
|
|
28
|
+
}
|
|
29
|
+
else if (finalConfig.type === 'audio') {
|
|
30
|
+
finalConfig.allowedMimeTypes = ALLOWED_MIME_TYPES_AUDIO;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
fileUploaderConfig.set(finalConfig);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// to be used internally when the modal is opened
|
|
37
|
+
export function getFileUploaderConfig() {
|
|
38
|
+
return get(fileUploaderConfig);
|
|
39
|
+
}
|
|
40
|
+
export function closeFileUploader() {
|
|
41
|
+
const config = getFileUploaderConfig();
|
|
42
|
+
config.onCancel();
|
|
43
|
+
fileUploaderConfig.set(null);
|
|
44
|
+
clearSelectedFile();
|
|
45
|
+
}
|
|
46
|
+
export function completeFileUpload(file) {
|
|
47
|
+
const config = getFileUploaderConfig();
|
|
48
|
+
config.onUpload(file);
|
|
49
|
+
closeFileUploader();
|
|
50
|
+
}
|
|
51
|
+
export function setSelectedFile(file) {
|
|
52
|
+
selectedFile.set(file);
|
|
53
|
+
}
|
|
54
|
+
export function clearSelectedFile() {
|
|
55
|
+
selectedFile.set(null);
|
|
56
|
+
}
|
|
57
|
+
const ALLOWED_MIME_TYPES_IMAGE = [
|
|
58
|
+
'image/gif',
|
|
59
|
+
'image/jpeg',
|
|
60
|
+
'image/png',
|
|
61
|
+
'image/svg+xml',
|
|
62
|
+
'image/webp',
|
|
63
|
+
'image/apng',
|
|
64
|
+
'image/avif'
|
|
65
|
+
];
|
|
66
|
+
const ALLOWED_MIME_TYPES_AUDIO = [
|
|
67
|
+
'audio/mpeg',
|
|
68
|
+
'audio/ogg',
|
|
69
|
+
'audio/wav',
|
|
70
|
+
'audio/webm'
|
|
71
|
+
];
|
|
72
|
+
export function validateMimeType(mimeType) {
|
|
73
|
+
const config = getFileUploaderConfig();
|
|
74
|
+
if (config.allowedMimeTypes.length === 0) {
|
|
75
|
+
return true; // all mime types allowed
|
|
76
|
+
}
|
|
77
|
+
return config.allowedMimeTypes.includes(mimeType);
|
|
78
|
+
}
|
|
79
|
+
export function getMimeNames() {
|
|
80
|
+
const config = getFileUploaderConfig();
|
|
81
|
+
if (config.allowedMimeTypes.length === 0) {
|
|
82
|
+
return []; // all mime types allowed
|
|
83
|
+
}
|
|
84
|
+
return config.allowedMimeTypes.map(m => m.split('/')[1]?.split('+')[0]);
|
|
85
|
+
}
|
|
86
|
+
export function getMimeNamesJoined() {
|
|
87
|
+
const names = getMimeNames();
|
|
88
|
+
if (names.length === 0) {
|
|
89
|
+
return 'any';
|
|
90
|
+
}
|
|
91
|
+
return names.join(', ').toUpperCase();
|
|
92
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function isValidUrl(url) {
|
|
2
|
+
try {
|
|
3
|
+
new URL(url);
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
catch (e) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* https://stackoverflow.com/a/18650828
|
|
12
|
+
*/
|
|
13
|
+
export function byteFormatter(bytes) {
|
|
14
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
15
|
+
if (bytes == 0)
|
|
16
|
+
return '0 Bytes';
|
|
17
|
+
const i = parseInt(String(Math.floor(Math.log(bytes) / Math.log(1000))));
|
|
18
|
+
return Math.round(bytes / Math.pow(1000, i)) + ' ' + sizes[i];
|
|
19
|
+
}
|
|
20
|
+
export function toKebabCase(str) {
|
|
21
|
+
if (!str) {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
return str
|
|
25
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
26
|
+
.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`) // Add hyphen before capital letters and convert them to lowercase
|
|
27
|
+
.replace(/_+/g, '-') // Replace underscores with hyphens
|
|
28
|
+
.replace(/--+/g, '-') // Replace multiple hyphens with a single one
|
|
29
|
+
.replace(/^-|-$|^-+|-+$/g, '') // Remove leading and trailing hyphens
|
|
30
|
+
.toLowerCase(); // Ensure everything is in lowercase
|
|
31
|
+
}
|
|
@@ -25,6 +25,7 @@ export { default as FormControl } from './FormControl/FormControl.svelte';
|
|
|
25
25
|
export { default as InputGroup } from './FormControl/InputGroup.svelte';
|
|
26
26
|
export { default as Label } from './FormControl/Label.svelte';
|
|
27
27
|
export { default as Validation } from './FormControl/Validation.svelte';
|
|
28
|
+
export { uploadFile, type FileUploaderConfig, type UploadedFile as FileUploaderUploadedFile, type SelectedFile as FileUploaderSelectedFile, } from './FileUploader/file-uploader.js';
|
|
28
29
|
export { default as HyvorBar } from './HyvorBar/HyvorBar.svelte';
|
|
29
30
|
export { bar as hyvorBar } from './HyvorBar/bar.js';
|
|
30
31
|
export { default as IconButton } from './IconButton/IconButton.svelte';
|
package/dist/components/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export { default as FormControl } from './FormControl/FormControl.svelte';
|
|
|
25
25
|
export { default as InputGroup } from './FormControl/InputGroup.svelte';
|
|
26
26
|
export { default as Label } from './FormControl/Label.svelte';
|
|
27
27
|
export { default as Validation } from './FormControl/Validation.svelte';
|
|
28
|
+
export { uploadFile, } from './FileUploader/file-uploader.js';
|
|
28
29
|
export { default as HyvorBar } from './HyvorBar/HyvorBar.svelte';
|
|
29
30
|
export { bar as hyvorBar } from './HyvorBar/bar.js';
|
|
30
31
|
export { default as IconButton } from './IconButton/IconButton.svelte';
|
package/package.json
CHANGED