@streamscloud/kit 0.29.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/files/app-upload-activity.svelte.d.ts +20 -0
- package/dist/core/files/app-upload-activity.svelte.js +34 -0
- package/dist/core/files/file-types.d.ts +7 -0
- package/dist/core/files/file-types.js +8 -0
- package/dist/core/files/file-validation-rules.d.ts +1 -0
- package/dist/core/files/file-validation-rules.js +32 -38
- package/dist/core/files/index.d.ts +4 -3
- package/dist/core/files/index.js +2 -1
- package/dist/core/files/upload-media-store.svelte.d.ts +39 -9
- package/dist/core/files/upload-media-store.svelte.js +71 -14
- package/dist/core/files/upload-types.d.ts +16 -0
- package/dist/core/media/index.d.ts +2 -0
- package/dist/core/media/index.js +1 -0
- package/dist/core/media/video-probe.d.ts +28 -0
- package/dist/core/media/video-probe.js +82 -0
- package/dist/ui/file-uploader/cmp.file-row.svelte +19 -15
- package/dist/ui/file-uploader/cmp.file-row.svelte.d.ts +9 -8
- package/dist/ui/file-uploader/cmp.file-upload-progress.svelte +10 -31
- package/dist/ui/file-uploader/cmp.file-upload-progress.svelte.d.ts +2 -3
- package/dist/ui/file-uploader/cmp.upload-progress-toaster.svelte +132 -0
- package/dist/ui/file-uploader/cmp.upload-progress-toaster.svelte.d.ts +27 -0
- package/dist/ui/file-uploader/file-type-colors.d.ts +5 -0
- package/dist/ui/file-uploader/file-type-colors.js +11 -1
- package/dist/ui/file-uploader/file-type-stamp.svelte +71 -0
- package/dist/ui/file-uploader/file-type-stamp.svelte.d.ts +8 -0
- package/dist/ui/file-uploader/index.d.ts +3 -1
- package/dist/ui/file-uploader/index.js +2 -1
- package/dist/ui/file-uploader/types.d.ts +16 -0
- package/dist/ui/file-uploader/types.js +1 -0
- package/dist/ui/file-uploader/upload-progress-toaster-localization.d.ts +4 -0
- package/dist/ui/file-uploader/upload-progress-toaster-localization.js +23 -0
- package/dist/ui/navigation/cmp.nav-menu-item.svelte +6 -7
- package/dist/ui/navigation/cmp.nav-menu-item.svelte.d.ts +4 -3
- package/dist/ui/navigation/cmp.nav-rail-item.svelte +7 -8
- package/dist/ui/navigation/cmp.nav-rail-item.svelte.d.ts +5 -4
- package/package.json +1 -1
- package/dist/ui/file-uploader/to-uploading-file.d.ts +0 -9
- package/dist/ui/file-uploader/to-uploading-file.js +0 -24
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
<script lang="ts">import { StringHelper } from '../../core/utils';
|
|
2
2
|
import { Icon } from '../icon';
|
|
3
|
+
import { fileTypeColor, fileTypeFromName } from './file-type-colors';
|
|
4
|
+
import FileTypeStamp from './file-type-stamp.svelte';
|
|
3
5
|
import { FileUploaderLocalization } from './file-uploader-localization';
|
|
4
6
|
import IconDismiss from '@fluentui/svg-icons/icons/dismiss_12_regular.svg?raw';
|
|
5
|
-
import IconDocument from '@fluentui/svg-icons/icons/document_20_regular.svg?raw';
|
|
6
7
|
const { file, removable = true, on } = $props();
|
|
7
8
|
const localization = new FileUploaderLocalization();
|
|
8
9
|
const isUploading = $derived(file.status === 'uploading' || file.status === 'queued');
|
|
9
10
|
const isError = $derived(file.status === 'error');
|
|
10
|
-
const
|
|
11
|
+
const fraction = $derived(file.status === 'success' ? 1 : (file.progress ?? 0));
|
|
12
|
+
const percent = $derived(Math.max(0, Math.min(100, Math.round(fraction * 100))));
|
|
11
13
|
const sizeText = $derived(StringHelper.toFileSizeString(file.file.size));
|
|
14
|
+
const accent = $derived(fileTypeColor(fileTypeFromName(file.file.name)));
|
|
12
15
|
</script>
|
|
13
16
|
|
|
14
|
-
<div class="file-row" class:file-row--uploading={isUploading} class:file-row--error={isError}>
|
|
15
|
-
<span class="file-
|
|
16
|
-
<
|
|
17
|
+
<div class="file-row" class:file-row--uploading={isUploading} class:file-row--error={isError} style:--_--file-row--accent={accent}>
|
|
18
|
+
<span class="file-row__stamp">
|
|
19
|
+
<FileTypeStamp fileName={file.file.name} size="sm" />
|
|
17
20
|
</span>
|
|
18
21
|
|
|
19
22
|
<div class="file-row__meta">
|
|
@@ -40,13 +43,14 @@ const sizeText = $derived(StringHelper.toFileSizeString(file.file.size));
|
|
|
40
43
|
|
|
41
44
|
<!--
|
|
42
45
|
@component
|
|
43
|
-
Compact row representation of
|
|
44
|
-
- **`queued` / `uploading`** — filename + percent subtitle + thin progress bar.
|
|
46
|
+
Compact row representation of a file being uploaded. Three states driven by `file.status`:
|
|
47
|
+
- **`queued` / `uploading`** — colored extension stamp + filename + percent subtitle + thin progress bar.
|
|
45
48
|
- **`success`** — filename + size + optional × remove button.
|
|
46
49
|
- **`error`** — filename + red error subtitle + optional × remove button.
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
The stamp is auto-colored from the filename's extension via the `file-type-colors` palette (and
|
|
52
|
+
falls back to a generic document icon for a file with no extension). Feed it `UploadMediaStore.files`
|
|
53
|
+
directly, or a plain `{ file, status }` literal for a static list.
|
|
50
54
|
|
|
51
55
|
### CSS Custom Properties
|
|
52
56
|
| Property | Description | Default |
|
|
@@ -54,16 +58,18 @@ Use directly with `UploadMediaStore.files`, or wrap a plain `File` via `toUpload
|
|
|
54
58
|
| `--sc-kit--file-row--background` | Row background | `var(--sc-kit--color--bg--field-alt)` |
|
|
55
59
|
| `--sc-kit--file-row--border-color` | Row border | `var(--sc-kit--color--border)` |
|
|
56
60
|
| `--sc-kit--file-row--border-radius` | Row radius | `var(--sc-kit--radius--md)` |
|
|
61
|
+
| `--sc-kit--file-row--accent` | Stamp + bar-fill color (override auto-derived) | per file extension |
|
|
57
62
|
| `--sc-kit--file-row--bar--background` | Progress-bar track | `var(--sc-kit--color--bg--active)` |
|
|
58
|
-
| `--sc-kit--file-row--bar--fill` | Progress-bar fill | `var(--sc-kit--
|
|
63
|
+
| `--sc-kit--file-row--bar--fill` | Progress-bar fill | `var(--sc-kit--file-row--accent)` |
|
|
59
64
|
-->
|
|
60
65
|
|
|
61
66
|
<style>.file-row {
|
|
62
67
|
--_fr--background: var(--sc-kit--file-row--background, var(--sc-kit--color--bg--field-alt));
|
|
63
68
|
--_fr--border-color: var(--sc-kit--file-row--border-color, var(--sc-kit--color--border));
|
|
64
69
|
--_fr--border-radius: var(--sc-kit--file-row--border-radius, var(--sc-kit--radius--md));
|
|
70
|
+
--_fr--accent: var(--sc-kit--file-row--accent, var(--_--file-row--accent, var(--sc-kit--color--accent)));
|
|
65
71
|
--_fr--bar-background: var(--sc-kit--file-row--bar--background, var(--sc-kit--color--bg--active));
|
|
66
|
-
--_fr--bar-fill: var(--sc-kit--file-row--bar--fill, var(--
|
|
72
|
+
--_fr--bar-fill: var(--sc-kit--file-row--bar--fill, var(--_fr--accent));
|
|
67
73
|
display: grid;
|
|
68
74
|
grid-template-columns: auto 1fr auto;
|
|
69
75
|
align-items: center;
|
|
@@ -82,11 +88,9 @@ Use directly with `UploadMediaStore.files`, or wrap a plain `File` via `toUpload
|
|
|
82
88
|
.file-row--error {
|
|
83
89
|
border-color: var(--sc-kit--color--danger);
|
|
84
90
|
}
|
|
85
|
-
.file-
|
|
86
|
-
--sc-kit--
|
|
91
|
+
.file-row__stamp {
|
|
92
|
+
--sc-kit--file-type-stamp--accent: var(--sc-kit--file-row--accent);
|
|
87
93
|
display: inline-flex;
|
|
88
|
-
align-items: center;
|
|
89
|
-
justify-content: center;
|
|
90
94
|
grid-row: 1;
|
|
91
95
|
}
|
|
92
96
|
.file-row__meta {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { UploadFileView } from './types';
|
|
2
2
|
type Props = {
|
|
3
|
-
|
|
4
|
-
file: UploadingFile;
|
|
3
|
+
file: UploadFileView;
|
|
5
4
|
/** Show the trailing × remove button. Auto-hidden while uploading. @default true */
|
|
6
5
|
removable?: boolean;
|
|
7
6
|
on?: {
|
|
@@ -9,13 +8,14 @@ type Props = {
|
|
|
9
8
|
};
|
|
10
9
|
};
|
|
11
10
|
/**
|
|
12
|
-
* Compact row representation of
|
|
13
|
-
* - **`queued` / `uploading`** — filename + percent subtitle + thin progress bar.
|
|
11
|
+
* Compact row representation of a file being uploaded. Three states driven by `file.status`:
|
|
12
|
+
* - **`queued` / `uploading`** — colored extension stamp + filename + percent subtitle + thin progress bar.
|
|
14
13
|
* - **`success`** — filename + size + optional × remove button.
|
|
15
14
|
* - **`error`** — filename + red error subtitle + optional × remove button.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* The stamp is auto-colored from the filename's extension via the `file-type-colors` palette (and
|
|
17
|
+
* falls back to a generic document icon for a file with no extension). Feed it `UploadMediaStore.files`
|
|
18
|
+
* directly, or a plain `{ file, status }` literal for a static list.
|
|
19
19
|
*
|
|
20
20
|
* ### CSS Custom Properties
|
|
21
21
|
* | Property | Description | Default |
|
|
@@ -23,8 +23,9 @@ type Props = {
|
|
|
23
23
|
* | `--sc-kit--file-row--background` | Row background | `var(--sc-kit--color--bg--field-alt)` |
|
|
24
24
|
* | `--sc-kit--file-row--border-color` | Row border | `var(--sc-kit--color--border)` |
|
|
25
25
|
* | `--sc-kit--file-row--border-radius` | Row radius | `var(--sc-kit--radius--md)` |
|
|
26
|
+
* | `--sc-kit--file-row--accent` | Stamp + bar-fill color (override auto-derived) | per file extension |
|
|
26
27
|
* | `--sc-kit--file-row--bar--background` | Progress-bar track | `var(--sc-kit--color--bg--active)` |
|
|
27
|
-
* | `--sc-kit--file-row--bar--fill` | Progress-bar fill | `var(--sc-kit--
|
|
28
|
+
* | `--sc-kit--file-row--bar--fill` | Progress-bar fill | `var(--sc-kit--file-row--accent)` |
|
|
28
29
|
*/
|
|
29
30
|
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
30
31
|
type Cmp = ReturnType<typeof Cmp>;
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
<script lang="ts">import { StringHelper } from '../../core/utils';
|
|
2
2
|
import { fileTypeColor, fileTypeFromName } from './file-type-colors';
|
|
3
|
+
import FileTypeStamp from './file-type-stamp.svelte';
|
|
3
4
|
const { file } = $props();
|
|
4
|
-
const
|
|
5
|
+
const progress = $derived(file.status === 'success' ? 1 : (file.progress ?? 0));
|
|
5
6
|
const accent = $derived(fileTypeColor(fileTypeFromName(file.file.name)));
|
|
6
|
-
const percent = $derived(Math.max(0, Math.min(100, Math.round(
|
|
7
|
+
const percent = $derived(Math.max(0, Math.min(100, Math.round(progress * 100))));
|
|
7
8
|
const sizeText = $derived(file.status === 'success'
|
|
8
9
|
? StringHelper.toFileSizeString(file.file.size)
|
|
9
|
-
: `${StringHelper.toFileSizeString(file.file.size *
|
|
10
|
+
: `${StringHelper.toFileSizeString(file.file.size * progress)} of ${StringHelper.toFileSizeString(file.file.size)}`);
|
|
10
11
|
const isError = $derived(file.status === 'error');
|
|
11
12
|
</script>
|
|
12
13
|
|
|
13
|
-
<div class="file-upload-progress" class:file-upload-progress--error={isError} style:--
|
|
14
|
-
<span class="file-upload-progress__stamp"
|
|
15
|
-
<
|
|
14
|
+
<div class="file-upload-progress" class:file-upload-progress--error={isError} style:--_--file-upload-progress--accent={accent}>
|
|
15
|
+
<span class="file-upload-progress__stamp">
|
|
16
|
+
<FileTypeStamp fileName={file.file.name} />
|
|
16
17
|
</span>
|
|
17
18
|
<div class="file-upload-progress__body">
|
|
18
19
|
<div class="file-upload-progress__head">
|
|
@@ -54,7 +55,7 @@ on the host (e.g. to force a brand color regardless of extension).
|
|
|
54
55
|
--_fup--border-color: var(--sc-kit--file-upload-progress--border-color, var(--sc-kit--color--border));
|
|
55
56
|
--_fup--border-radius: var(--sc-kit--file-upload-progress--border-radius, var(--sc-kit--radius--md));
|
|
56
57
|
--_fup--bar-background: var(--sc-kit--file-upload-progress--bar--background, var(--sc-kit--color--bg--active));
|
|
57
|
-
--_fup--accent: var(--sc-kit--file-upload-progress--accent, var(--
|
|
58
|
+
--_fup--accent: var(--sc-kit--file-upload-progress--accent, var(--_--file-upload-progress--accent, var(--sc-kit--color--accent)));
|
|
58
59
|
display: flex;
|
|
59
60
|
align-items: flex-start;
|
|
60
61
|
gap: var(--sc-kit--space--3);
|
|
@@ -67,31 +68,9 @@ on the host (e.g. to force a brand color regardless of extension).
|
|
|
67
68
|
border-color: var(--sc-kit--color--danger);
|
|
68
69
|
}
|
|
69
70
|
.file-upload-progress__stamp {
|
|
71
|
+
--sc-kit--file-type-stamp--accent: var(--sc-kit--file-upload-progress--accent);
|
|
72
|
+
display: inline-flex;
|
|
70
73
|
flex-shrink: 0;
|
|
71
|
-
width: 2.25rem;
|
|
72
|
-
height: 2.75rem;
|
|
73
|
-
border-radius: var(--sc-kit--radius--md);
|
|
74
|
-
display: flex;
|
|
75
|
-
align-items: flex-end;
|
|
76
|
-
justify-content: center;
|
|
77
|
-
padding-bottom: 0.25rem;
|
|
78
|
-
background: var(--_fup--accent);
|
|
79
|
-
color: #fff;
|
|
80
|
-
font-size: 0.5313rem;
|
|
81
|
-
font-weight: var(--sc-kit--font-weight--bold);
|
|
82
|
-
letter-spacing: var(--sc-kit--letter-spacing--wide);
|
|
83
|
-
position: relative;
|
|
84
|
-
}
|
|
85
|
-
.file-upload-progress__stamp::before {
|
|
86
|
-
content: "";
|
|
87
|
-
position: absolute;
|
|
88
|
-
top: 0;
|
|
89
|
-
right: 0;
|
|
90
|
-
width: 0.75rem;
|
|
91
|
-
height: 0.75rem;
|
|
92
|
-
background: rgba(255, 255, 255, 0.35);
|
|
93
|
-
clip-path: polygon(100% 0, 0 0, 100% 100%);
|
|
94
|
-
border-top-right-radius: var(--sc-kit--radius--md);
|
|
95
74
|
}
|
|
96
75
|
.file-upload-progress__body {
|
|
97
76
|
flex: 1;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { UploadFileView } from './types';
|
|
2
2
|
type Props = {
|
|
3
|
-
|
|
4
|
-
file: UploadingFile;
|
|
3
|
+
file: UploadFileView;
|
|
5
4
|
};
|
|
6
5
|
/**
|
|
7
6
|
* Rich progress card for a single file currently being uploaded. Shows a colored extension stamp
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script lang="ts">import { AppUploadActivity } from '../../core/files';
|
|
2
|
+
import { Toastr } from '../toast';
|
|
3
|
+
import FileRow from './cmp.file-row.svelte';
|
|
4
|
+
import { UploadProgressToasterLocalization } from './upload-progress-toaster-localization';
|
|
5
|
+
const { delayMs = 3000, containerId = '' } = $props();
|
|
6
|
+
const localization = new UploadProgressToasterLocalization();
|
|
7
|
+
const files = $derived(AppUploadActivity.files);
|
|
8
|
+
const settled = $derived(files.filter((f) => f.status === 'success' || f.status === 'error').length);
|
|
9
|
+
const failed = $derived(files.filter((f) => f.status === 'error').length);
|
|
10
|
+
// Carrying the containerId with the id, or a containerId changed mid-run dismisses in the wrong container and leaks the toast.
|
|
11
|
+
let toast = null;
|
|
12
|
+
const dismiss = () => {
|
|
13
|
+
if (toast !== null) {
|
|
14
|
+
Toastr.dismiss(toast.id, toast.containerId);
|
|
15
|
+
toast = null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
// A boolean, not a file count: the delay timer must not restart every time a run gains a file.
|
|
19
|
+
const shouldShow = $derived(AppUploadActivity.isUploading && files.length > 0);
|
|
20
|
+
$effect(() => {
|
|
21
|
+
if (!shouldShow) {
|
|
22
|
+
dismiss();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Without this guard a re-run (e.g. delayMs changed mid-run) pushes a second toast and leaks the first.
|
|
26
|
+
if (toast !== null) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
const id = Toastr.info(progress, { duration: 0, closable: false, swipeable: false, containerId });
|
|
31
|
+
toast = { id, containerId };
|
|
32
|
+
}, delayMs);
|
|
33
|
+
return () => clearTimeout(timer);
|
|
34
|
+
});
|
|
35
|
+
$effect(() => () => dismiss());
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
{#snippet progress()}
|
|
39
|
+
{#if files.length}
|
|
40
|
+
<div class="upload-progress-toaster">
|
|
41
|
+
<div class="upload-progress-toaster__title">
|
|
42
|
+
<span class="upload-progress-toaster__count">{localization.title(settled, files.length)}</span>
|
|
43
|
+
{#if failed > 0}
|
|
44
|
+
<span class="upload-progress-toaster__failed">{localization.failed(failed)}</span>
|
|
45
|
+
{/if}
|
|
46
|
+
</div>
|
|
47
|
+
<div class="upload-progress-toaster__files">
|
|
48
|
+
{#each files as file (file.id)}
|
|
49
|
+
<FileRow file={file} removable={false} />
|
|
50
|
+
{/each}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
{/if}
|
|
54
|
+
{/snippet}
|
|
55
|
+
|
|
56
|
+
<!--
|
|
57
|
+
@component
|
|
58
|
+
Headless controller — renders nothing on its own. Mount it once (app root) and every
|
|
59
|
+
`UploadMediaStore.upload()` anywhere in the app reports into a single sticky toast: a
|
|
60
|
+
`'settled of total'` title, a failure count when something errored, and one `FileRow` per file.
|
|
61
|
+
Dismissed as soon as the last run finishes. Runs shorter than `delayMs` never show it, and
|
|
62
|
+
concurrent runs share one toast rather than stacking. `UploadMediaStore.upload()` registers itself;
|
|
63
|
+
an app uploading outside the store reports in via `AppUploadActivity.beginRun()` / `endRun()`.
|
|
64
|
+
|
|
65
|
+
The row list scrolls past `--sc-kit--upload-progress-toaster--files--max-height`, so a bulk run
|
|
66
|
+
at the store's default concurrency of 20 can never push the toast off screen.
|
|
67
|
+
|
|
68
|
+
### CSS Custom Properties
|
|
69
|
+
| Property | Description | Default |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `--sc-kit--upload-progress-toaster--gap` | Gap between the title and the file rows | `var(--sc-kit--space--2)` |
|
|
72
|
+
| `--sc-kit--upload-progress-toaster--failed--color` | Color of the failure count | `var(--sc-kit--color--danger)` |
|
|
73
|
+
| `--sc-kit--upload-progress-toaster--files--max-height` | Height at which the row list starts scrolling | `240px` |
|
|
74
|
+
-->
|
|
75
|
+
|
|
76
|
+
<style>.upload-progress-toaster {
|
|
77
|
+
--_upload-progress-toaster--gap: var(--sc-kit--upload-progress-toaster--gap, var(--sc-kit--space--2));
|
|
78
|
+
--_upload-progress-toaster--failed-color: var(--sc-kit--upload-progress-toaster--failed--color, var(--sc-kit--color--danger));
|
|
79
|
+
--_upload-progress-toaster--files-max-height: var(--sc-kit--upload-progress-toaster--files--max-height, 15rem);
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
gap: var(--_upload-progress-toaster--gap);
|
|
83
|
+
min-width: 0;
|
|
84
|
+
}
|
|
85
|
+
.upload-progress-toaster__title {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: baseline;
|
|
88
|
+
gap: var(--sc-kit--space--2);
|
|
89
|
+
font-weight: var(--sc-kit--font-weight--medium);
|
|
90
|
+
}
|
|
91
|
+
.upload-progress-toaster__count {
|
|
92
|
+
text-overflow: ellipsis;
|
|
93
|
+
max-width: 100%;
|
|
94
|
+
white-space: nowrap;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
}
|
|
97
|
+
.upload-progress-toaster__failed {
|
|
98
|
+
flex-shrink: 0;
|
|
99
|
+
color: var(--_upload-progress-toaster--failed-color);
|
|
100
|
+
}
|
|
101
|
+
.upload-progress-toaster__files {
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
gap: var(--_upload-progress-toaster--gap);
|
|
105
|
+
max-height: var(--_upload-progress-toaster--files-max-height);
|
|
106
|
+
overflow-y: auto;
|
|
107
|
+
overscroll-behavior: contain;
|
|
108
|
+
--_cross-browser-scrollbar--thumb-color: transparent;
|
|
109
|
+
--_cross-browser-scrollbar--track-color: transparent;
|
|
110
|
+
}
|
|
111
|
+
.upload-progress-toaster__files:hover {
|
|
112
|
+
--_cross-browser-scrollbar--thumb-color: var(--scrollbar--thumb-color, light-dark(#d1d5db, #4b5563));
|
|
113
|
+
--_cross-browser-scrollbar--track-color: var(--scrollbar--track-color, transparent);
|
|
114
|
+
}
|
|
115
|
+
.upload-progress-toaster__files::-webkit-scrollbar {
|
|
116
|
+
width: 6px;
|
|
117
|
+
height: 6px;
|
|
118
|
+
}
|
|
119
|
+
.upload-progress-toaster__files::-webkit-scrollbar-track {
|
|
120
|
+
background: var(--_cross-browser-scrollbar--track-color);
|
|
121
|
+
border-radius: 100vw;
|
|
122
|
+
}
|
|
123
|
+
.upload-progress-toaster__files::-webkit-scrollbar-thumb {
|
|
124
|
+
background: var(--_cross-browser-scrollbar--thumb-color);
|
|
125
|
+
border-radius: 100vw;
|
|
126
|
+
}
|
|
127
|
+
@supports (scrollbar-color: transparent transparent) {
|
|
128
|
+
.upload-progress-toaster__files {
|
|
129
|
+
scrollbar-color: var(--_cross-browser-scrollbar--thumb-color) var(--_cross-browser-scrollbar--track-color);
|
|
130
|
+
scrollbar-width: thin;
|
|
131
|
+
}
|
|
132
|
+
}</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
/** How long a run must last before the toast appears, so quick uploads never flash it. @default 3000 */
|
|
3
|
+
delayMs?: number;
|
|
4
|
+
/** Routes the toast to a specific `<Toaster id="...">`. @default '' */
|
|
5
|
+
containerId?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Headless controller — renders nothing on its own. Mount it once (app root) and every
|
|
9
|
+
* `UploadMediaStore.upload()` anywhere in the app reports into a single sticky toast: a
|
|
10
|
+
* `'settled of total'` title, a failure count when something errored, and one `FileRow` per file.
|
|
11
|
+
* Dismissed as soon as the last run finishes. Runs shorter than `delayMs` never show it, and
|
|
12
|
+
* concurrent runs share one toast rather than stacking. `UploadMediaStore.upload()` registers itself;
|
|
13
|
+
* an app uploading outside the store reports in via `AppUploadActivity.beginRun()` / `endRun()`.
|
|
14
|
+
*
|
|
15
|
+
* The row list scrolls past `--sc-kit--upload-progress-toaster--files--max-height`, so a bulk run
|
|
16
|
+
* at the store's default concurrency of 20 can never push the toast off screen.
|
|
17
|
+
*
|
|
18
|
+
* ### CSS Custom Properties
|
|
19
|
+
* | Property | Description | Default |
|
|
20
|
+
* |---|---|---|
|
|
21
|
+
* | `--sc-kit--upload-progress-toaster--gap` | Gap between the title and the file rows | `var(--sc-kit--space--2)` |
|
|
22
|
+
* | `--sc-kit--upload-progress-toaster--failed--color` | Color of the failure count | `var(--sc-kit--color--danger)` |
|
|
23
|
+
* | `--sc-kit--upload-progress-toaster--files--max-height` | Height at which the row list starts scrolling | `240px` |
|
|
24
|
+
*/
|
|
25
|
+
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
26
|
+
type Cmp = ReturnType<typeof Cmp>;
|
|
27
|
+
export default Cmp;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export declare const fileTypeColors: Record<string, string>;
|
|
2
|
+
/**
|
|
3
|
+
* Extension of a filename, lowercased, or `''` when the tail after the last dot is not
|
|
4
|
+
* extension-shaped — `'Interview 2024.01.15 raw'` and `'.gitignore'` have no extension, while any
|
|
5
|
+
* entry of `fileTypeColors` counts regardless of length (`.sketch`).
|
|
6
|
+
*/
|
|
2
7
|
export declare const fileTypeFromName: (name: string) => string;
|
|
3
8
|
export declare const fileTypeColor: (type: string) => string;
|
|
@@ -31,8 +31,18 @@ export const fileTypeColors = {
|
|
|
31
31
|
js: '#eab308',
|
|
32
32
|
ts: '#3178c6'
|
|
33
33
|
};
|
|
34
|
+
const EXTENSION_SHAPE = /^[a-z0-9]{1,5}$/;
|
|
35
|
+
/**
|
|
36
|
+
* Extension of a filename, lowercased, or `''` when the tail after the last dot is not
|
|
37
|
+
* extension-shaped — `'Interview 2024.01.15 raw'` and `'.gitignore'` have no extension, while any
|
|
38
|
+
* entry of `fileTypeColors` counts regardless of length (`.sketch`).
|
|
39
|
+
*/
|
|
34
40
|
export const fileTypeFromName = (name) => {
|
|
35
41
|
const dot = name.lastIndexOf('.');
|
|
36
|
-
|
|
42
|
+
if (dot < 0) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
const tail = name.slice(dot + 1).toLowerCase();
|
|
46
|
+
return fileTypeColors[tail] !== undefined || EXTENSION_SHAPE.test(tail) ? tail : '';
|
|
37
47
|
};
|
|
38
48
|
export const fileTypeColor = (type) => fileTypeColors[type.toLowerCase()] ?? '#6b7280';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts">import { Icon } from '../icon';
|
|
2
|
+
import { fileTypeColor, fileTypeFromName } from './file-type-colors';
|
|
3
|
+
import IconDocument from '@fluentui/svg-icons/icons/document_20_regular.svg?raw';
|
|
4
|
+
const { fileName, size = 'md' } = $props();
|
|
5
|
+
const fileType = $derived(fileTypeFromName(fileName));
|
|
6
|
+
const label = $derived(fileType.slice(0, 4).toUpperCase());
|
|
7
|
+
const accent = $derived(fileTypeColor(fileType));
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
{#if label}
|
|
11
|
+
<span class="file-type-stamp file-type-stamp--{size}" aria-hidden="true" style:--_--file-type-stamp--accent-derived={accent}>
|
|
12
|
+
<span class="file-type-stamp__label">{label}</span>
|
|
13
|
+
</span>
|
|
14
|
+
{:else}
|
|
15
|
+
<span class="file-type-stamp__fallback" aria-hidden="true">
|
|
16
|
+
<Icon src={IconDocument} />
|
|
17
|
+
</span>
|
|
18
|
+
{/if}
|
|
19
|
+
|
|
20
|
+
<style>.file-type-stamp {
|
|
21
|
+
--_fts--accent: var(--sc-kit--file-type-stamp--accent, var(--_--file-type-stamp--accent-derived, var(--sc-kit--color--accent)));
|
|
22
|
+
--_fts--width: 2.25rem;
|
|
23
|
+
--_fts--height: 2.75rem;
|
|
24
|
+
--_fts--fold-size: 0.75rem;
|
|
25
|
+
--_fts--font-size: 0.5313rem;
|
|
26
|
+
--_fts--padding-bottom: 0.25rem;
|
|
27
|
+
--_fts--border-radius: var(--sc-kit--radius--md);
|
|
28
|
+
position: relative;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: flex-end;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
flex-shrink: 0;
|
|
33
|
+
width: var(--_fts--width);
|
|
34
|
+
height: var(--_fts--height);
|
|
35
|
+
padding-bottom: var(--_fts--padding-bottom);
|
|
36
|
+
background: var(--_fts--accent);
|
|
37
|
+
border-radius: var(--_fts--border-radius);
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
}
|
|
40
|
+
.file-type-stamp--sm {
|
|
41
|
+
--_fts--width: 1.75rem;
|
|
42
|
+
--_fts--height: 2.125rem;
|
|
43
|
+
--_fts--fold-size: 0.5625rem;
|
|
44
|
+
--_fts--font-size: 0.4375rem;
|
|
45
|
+
--_fts--padding-bottom: 0.1875rem;
|
|
46
|
+
--_fts--border-radius: var(--sc-kit--radius--sm);
|
|
47
|
+
}
|
|
48
|
+
.file-type-stamp::before {
|
|
49
|
+
content: "";
|
|
50
|
+
position: absolute;
|
|
51
|
+
top: 0;
|
|
52
|
+
right: 0;
|
|
53
|
+
width: var(--_fts--fold-size);
|
|
54
|
+
height: var(--_fts--fold-size);
|
|
55
|
+
background: rgba(255, 255, 255, 0.35);
|
|
56
|
+
clip-path: polygon(100% 0, 0 0, 100% 100%);
|
|
57
|
+
}
|
|
58
|
+
.file-type-stamp__label {
|
|
59
|
+
color: #ffffff;
|
|
60
|
+
font-size: var(--_fts--font-size);
|
|
61
|
+
font-weight: var(--sc-kit--font-weight--bold);
|
|
62
|
+
letter-spacing: var(--sc-kit--letter-spacing--wide);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.file-type-stamp__fallback {
|
|
66
|
+
--sc-kit--icon--color: var(--sc-kit--color--text--muted);
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
}</style>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { default as FileRow } from './cmp.file-row.svelte';
|
|
2
2
|
export { default as FileUploader } from './cmp.file-uploader.svelte';
|
|
3
3
|
export { default as FileUploadProgress } from './cmp.file-upload-progress.svelte';
|
|
4
|
+
export { default as UploadProgressToaster } from './cmp.upload-progress-toaster.svelte';
|
|
4
5
|
export { FileUploaderLocalization } from './file-uploader-localization';
|
|
6
|
+
export { UploadProgressToasterLocalization } from './upload-progress-toaster-localization';
|
|
5
7
|
export { fileTypeColor, fileTypeColors, fileTypeFromName } from './file-type-colors';
|
|
6
|
-
export {
|
|
8
|
+
export type { UploadFileView } from './types';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as FileRow } from './cmp.file-row.svelte';
|
|
2
2
|
export { default as FileUploader } from './cmp.file-uploader.svelte';
|
|
3
3
|
export { default as FileUploadProgress } from './cmp.file-upload-progress.svelte';
|
|
4
|
+
export { default as UploadProgressToaster } from './cmp.upload-progress-toaster.svelte';
|
|
4
5
|
export { FileUploaderLocalization } from './file-uploader-localization';
|
|
6
|
+
export { UploadProgressToasterLocalization } from './upload-progress-toaster-localization';
|
|
5
7
|
export { fileTypeColor, fileTypeColors, fileTypeFromName } from './file-type-colors';
|
|
6
|
-
export { toUploadingFile } from './to-uploading-file';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `FileRow` / `FileUploadProgress` need in order to draw a file — nothing more. `UploadingFile`
|
|
3
|
+
* from `UploadMediaStore` is structurally assignable, so store-driven lists pass straight through;
|
|
4
|
+
* a static list writes the literal it actually has (`{ file, status: 'success' }`) with no
|
|
5
|
+
* placeholder ids, blob ids or preview URLs.
|
|
6
|
+
*/
|
|
7
|
+
export type UploadFileView = {
|
|
8
|
+
file: File;
|
|
9
|
+
/** 0..1 — only meaningful while queued / uploading. @default 0 */
|
|
10
|
+
progress?: number;
|
|
11
|
+
} & ({
|
|
12
|
+
status: 'queued' | 'uploading' | 'success';
|
|
13
|
+
} | {
|
|
14
|
+
status: 'error';
|
|
15
|
+
error: string;
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AppLocale } from '../../core/locale';
|
|
2
|
+
const loc = {
|
|
3
|
+
titleSingle: {
|
|
4
|
+
en: 'Uploading 1 file',
|
|
5
|
+
no: 'Laster opp 1 fil'
|
|
6
|
+
},
|
|
7
|
+
titleMany: {
|
|
8
|
+
en: (settled, total) => `Uploading ${settled} of ${total} files`,
|
|
9
|
+
no: (settled, total) => `Laster opp ${settled} av ${total} filer`
|
|
10
|
+
},
|
|
11
|
+
failed: {
|
|
12
|
+
en: (count) => `${count} failed`,
|
|
13
|
+
no: (count) => `${count} feilet`
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export class UploadProgressToasterLocalization {
|
|
17
|
+
title = (settled, total) => {
|
|
18
|
+
return total === 1 ? loc.titleSingle[AppLocale.current] : loc.titleMany[AppLocale.current](settled, total);
|
|
19
|
+
};
|
|
20
|
+
failed = (count) => {
|
|
21
|
+
return loc.failed[AppLocale.current](count);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -3,11 +3,11 @@ const { label, icon, href, active = false, activeStyle = 'plain', disabled = fal
|
|
|
3
3
|
const activeFilled = $derived(active && activeStyle === 'filled');
|
|
4
4
|
const badgeText = $derived(typeof badge === 'number' && badge > 99 ? '99+' : badge);
|
|
5
5
|
const hasBadge = $derived(badge !== undefined && badge !== null && badge !== '');
|
|
6
|
-
const handleClick = () => {
|
|
6
|
+
const handleClick = (event) => {
|
|
7
7
|
if (disabled) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
|
-
on?.click?.();
|
|
10
|
+
on?.click?.(event);
|
|
11
11
|
};
|
|
12
12
|
</script>
|
|
13
13
|
|
|
@@ -21,18 +21,16 @@ const handleClick = () => {
|
|
|
21
21
|
<span class="nav-menu-item__label">{label}</span>
|
|
22
22
|
{/snippet}
|
|
23
23
|
|
|
24
|
-
{#if href}
|
|
24
|
+
{#if href && !disabled}
|
|
25
25
|
<a
|
|
26
26
|
class="nav-menu-item"
|
|
27
27
|
class:nav-menu-item--active={active}
|
|
28
28
|
class:nav-menu-item--active-filled={activeFilled}
|
|
29
|
-
class:nav-menu-item--disabled={disabled}
|
|
30
29
|
href={href}
|
|
31
30
|
target={target}
|
|
32
31
|
rel={rel}
|
|
33
32
|
aria-current={active ? 'page' : undefined}
|
|
34
|
-
|
|
35
|
-
tabindex={disabled ? -1 : undefined}>
|
|
33
|
+
onclick={handleClick}>
|
|
36
34
|
{@render content()}
|
|
37
35
|
</a>
|
|
38
36
|
{:else}
|
|
@@ -53,7 +51,8 @@ const handleClick = () => {
|
|
|
53
51
|
@component
|
|
54
52
|
NavMenuItem — single row inside a `NavMenuGroup`. Icon + label, optional badge in the top-right of
|
|
55
53
|
the icon (numbers > 99 truncate to `99+`). The icon accepts an SVG source string or a snippet.
|
|
56
|
-
Renders as `<a>` when `href` is set, `<button>` otherwise.
|
|
54
|
+
Renders as `<a>` when `href` is set and enabled, `<button>` otherwise; `on.click` fires in both forms
|
|
55
|
+
with the `MouseEvent` and the link default is left intact. Active state is consumer-controlled
|
|
57
56
|
(`active` prop) — wire it to your router's pathname matching upstream. `activeStyle` picks how the
|
|
58
57
|
active row reads: `'plain'` (default) leaves color as the only signal and keeps the hover background
|
|
59
58
|
working on the active row, `'filled'` plates it and suppresses hover there.
|
|
@@ -3,7 +3,7 @@ type Props = {
|
|
|
3
3
|
label: string;
|
|
4
4
|
/** Icon — string SVG source, `{ src, color?, size? }` object, or custom snippet. */
|
|
5
5
|
icon: IconProp;
|
|
6
|
-
/** Render as `<a href
|
|
6
|
+
/** Render as `<a href>` — unless `disabled`, which falls back to the button form. `on.click` still fires; `event.preventDefault()` keeps the click from navigating. */
|
|
7
7
|
href?: string;
|
|
8
8
|
/** Highlight as the current item. Consumer-controlled. @default false */
|
|
9
9
|
active?: boolean;
|
|
@@ -17,13 +17,14 @@ type Props = {
|
|
|
17
17
|
/** Anchor rel — only relevant when `href` is set. */
|
|
18
18
|
rel?: string;
|
|
19
19
|
on?: {
|
|
20
|
-
click?: () => void;
|
|
20
|
+
click?: (event: MouseEvent) => void;
|
|
21
21
|
};
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
24
|
* NavMenuItem — single row inside a `NavMenuGroup`. Icon + label, optional badge in the top-right of
|
|
25
25
|
* the icon (numbers > 99 truncate to `99+`). The icon accepts an SVG source string or a snippet.
|
|
26
|
-
* Renders as `<a>` when `href` is set, `<button>` otherwise.
|
|
26
|
+
* Renders as `<a>` when `href` is set and enabled, `<button>` otherwise; `on.click` fires in both forms
|
|
27
|
+
* with the `MouseEvent` and the link default is left intact. Active state is consumer-controlled
|
|
27
28
|
* (`active` prop) — wire it to your router's pathname matching upstream. `activeStyle` picks how the
|
|
28
29
|
* active row reads: `'plain'` (default) leaves color as the only signal and keeps the hover background
|
|
29
30
|
* working on the active row, `'filled'` plates it and suppresses hover there.
|