@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.
Files changed (38) hide show
  1. package/dist/core/files/app-upload-activity.svelte.d.ts +20 -0
  2. package/dist/core/files/app-upload-activity.svelte.js +34 -0
  3. package/dist/core/files/file-types.d.ts +7 -0
  4. package/dist/core/files/file-types.js +8 -0
  5. package/dist/core/files/file-validation-rules.d.ts +1 -0
  6. package/dist/core/files/file-validation-rules.js +32 -38
  7. package/dist/core/files/index.d.ts +4 -3
  8. package/dist/core/files/index.js +2 -1
  9. package/dist/core/files/upload-media-store.svelte.d.ts +39 -9
  10. package/dist/core/files/upload-media-store.svelte.js +71 -14
  11. package/dist/core/files/upload-types.d.ts +16 -0
  12. package/dist/core/media/index.d.ts +2 -0
  13. package/dist/core/media/index.js +1 -0
  14. package/dist/core/media/video-probe.d.ts +28 -0
  15. package/dist/core/media/video-probe.js +82 -0
  16. package/dist/ui/file-uploader/cmp.file-row.svelte +19 -15
  17. package/dist/ui/file-uploader/cmp.file-row.svelte.d.ts +9 -8
  18. package/dist/ui/file-uploader/cmp.file-upload-progress.svelte +10 -31
  19. package/dist/ui/file-uploader/cmp.file-upload-progress.svelte.d.ts +2 -3
  20. package/dist/ui/file-uploader/cmp.upload-progress-toaster.svelte +132 -0
  21. package/dist/ui/file-uploader/cmp.upload-progress-toaster.svelte.d.ts +27 -0
  22. package/dist/ui/file-uploader/file-type-colors.d.ts +5 -0
  23. package/dist/ui/file-uploader/file-type-colors.js +11 -1
  24. package/dist/ui/file-uploader/file-type-stamp.svelte +71 -0
  25. package/dist/ui/file-uploader/file-type-stamp.svelte.d.ts +8 -0
  26. package/dist/ui/file-uploader/index.d.ts +3 -1
  27. package/dist/ui/file-uploader/index.js +2 -1
  28. package/dist/ui/file-uploader/types.d.ts +16 -0
  29. package/dist/ui/file-uploader/types.js +1 -0
  30. package/dist/ui/file-uploader/upload-progress-toaster-localization.d.ts +4 -0
  31. package/dist/ui/file-uploader/upload-progress-toaster-localization.js +23 -0
  32. package/dist/ui/navigation/cmp.nav-menu-item.svelte +6 -7
  33. package/dist/ui/navigation/cmp.nav-menu-item.svelte.d.ts +4 -3
  34. package/dist/ui/navigation/cmp.nav-rail-item.svelte +7 -8
  35. package/dist/ui/navigation/cmp.nav-rail-item.svelte.d.ts +5 -4
  36. package/package.json +1 -1
  37. package/dist/ui/file-uploader/to-uploading-file.d.ts +0 -9
  38. 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 percent = $derived(Math.max(0, Math.min(100, Math.round(file.progress * 100))));
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-row__icon" aria-hidden="true">
16
- <Icon src={IconDocument} />
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 an `UploadingFile`. Three states driven by `file.status`:
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
- Use directly with `UploadMediaStore.files`, or wrap a plain `File` via `toUploadingFile()` from
49
- `@streamscloud/kit/core/files` for ad-hoc lists.
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--color--accent)` |
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(--sc-kit--color--accent));
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-row__icon {
86
- --sc-kit--icon--color: var(--sc-kit--color--text--muted);
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 { UploadingFile } from '../../core/files';
1
+ import type { UploadFileView } from './types';
2
2
  type Props = {
3
- /** Canonical file shape produced by `UploadMediaStore` (or via `toUploadingFile()` for ad-hoc usage). */
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 an `UploadingFile`. Three states driven by `file.status`:
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
- * Use directly with `UploadMediaStore.files`, or wrap a plain `File` via `toUploadingFile()` from
18
- * `@streamscloud/kit/core/files` for ad-hoc lists.
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--color--accent)` |
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 ext = $derived(fileTypeFromName(file.file.name).toUpperCase());
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(file.progress * 100))));
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 * file.progress)} of ${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:--_fup--accent-default={accent}>
14
- <span class="file-upload-progress__stamp" aria-hidden="true">
15
- <span class="file-upload-progress__ext">{ext}</span>
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(--_fup--accent-default, var(--sc-kit--color--accent)));
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 { UploadingFile } from '../../core/files';
1
+ import type { UploadFileView } from './types';
2
2
  type Props = {
3
- /** Canonical file shape produced by `UploadMediaStore` (or via `toUploadingFile()` for ad-hoc usage). */
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
- return dot >= 0 ? name.slice(dot + 1).toLowerCase() : '';
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>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ fileName: string;
3
+ /** @default 'md' */
4
+ size?: 'sm' | 'md';
5
+ };
6
+ declare const FileTypeStamp: import("svelte").Component<Props, {}, "">;
7
+ type FileTypeStamp = ReturnType<typeof FileTypeStamp>;
8
+ export default FileTypeStamp;
@@ -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 { toUploadingFile } from './to-uploading-file';
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,4 @@
1
+ export declare class UploadProgressToasterLocalization {
2
+ title: (settled: number, total: number) => string;
3
+ failed: (count: number) => string;
4
+ }
@@ -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
- aria-disabled={disabled || undefined}
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. Active state is consumer-controlled
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>`. Mutually exclusive with `on.click`. */
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. Active state is consumer-controlled
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.