@streamscloud/kit 0.30.0 → 0.32.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 (40) 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/select/_select-trigger.scss +16 -12
  33. package/dist/ui/select/cmp.input-suggest.svelte +495 -0
  34. package/dist/ui/select/cmp.input-suggest.svelte.d.ts +97 -0
  35. package/dist/ui/select/index.d.ts +3 -1
  36. package/dist/ui/select/index.js +1 -0
  37. package/dist/ui/select/types.d.ts +10 -0
  38. package/package.json +1 -1
  39. package/dist/ui/file-uploader/to-uploading-file.d.ts +0 -9
  40. 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
+ }
@@ -6,7 +6,7 @@
6
6
  // Shared trigger shape for Singleselect / SingleselectAsync / (later) Multiselect.
7
7
  // Public CSS API lives under `--sc-kit--select--trigger--*` so consumers can override
8
8
  // once and have all select variants pick it up.
9
- @mixin select-trigger {
9
+ @mixin select-trigger($chevron: true) {
10
10
  --_sel--height: var(--sc-kit--select--trigger--height, var(--_sel--size-height, var(--sc-kit--field--height--md)));
11
11
  --_sel--padding-inline: var(--sc-kit--select--trigger--padding-inline, var(--_sel--size-padding-inline, var(--sc-kit--field--padding-inline--md)));
12
12
  --_sel--gap: var(--sc-kit--select--trigger--gap, var(--sc-kit--space--2));
@@ -164,12 +164,14 @@
164
164
  }
165
165
  }
166
166
 
167
- &__chevron {
168
- transition: transform var(--sc-kit--duration--fast) var(--sc-kit--ease--default);
169
- }
167
+ @if $chevron {
168
+ &__chevron {
169
+ transition: transform var(--sc-kit--duration--fast) var(--sc-kit--ease--default);
170
+ }
170
171
 
171
- &--open &__chevron {
172
- transform: rotate(180deg);
172
+ &--open &__chevron {
173
+ transform: rotate(180deg);
174
+ }
173
175
  }
174
176
 
175
177
  &__spinner {
@@ -193,12 +195,14 @@
193
195
  // reflow to additional rows below. (Spinner positioning lives in MultiselectAsync's
194
196
  // local style block — Svelte's per-component CSS-usage check otherwise flags this rule
195
197
  // as unused in Singleselect / sync Multiselect.)
196
- &--multi &__chevron {
197
- position: absolute;
198
- inset-block-start: calc(var(--_sel--height) / 2);
199
- inset-inline-end: var(--_sel--padding-inline);
200
- transform: translateY(-50%);
201
- margin: 0;
198
+ @if $chevron {
199
+ &--multi &__chevron {
200
+ position: absolute;
201
+ inset-block-start: calc(var(--_sel--height) / 2);
202
+ inset-inline-end: var(--_sel--padding-inline);
203
+ transform: translateY(-50%);
204
+ margin: 0;
205
+ }
202
206
  }
203
207
 
204
208
  // Inline-chip area visible inside the trigger when value is non-empty (inline mode).