@streamscloud/kit 0.9.14 → 0.9.16

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.
@@ -0,0 +1,12 @@
1
+ export declare class FileValidationLocalization {
2
+ get invalidFile(): string;
3
+ get unsupportedType(): string;
4
+ get audioOnly(): string;
5
+ get maxSize(): (size: string) => string;
6
+ get extensions(): (list: string) => string;
7
+ get maxDimensions(): (width: number, height: number) => string;
8
+ get minDimensions(): (width: number, height: number) => string;
9
+ get maxDuration(): (seconds: number) => string;
10
+ get aspectRatio(): (ratios: string) => string;
11
+ get maxBitrate(): (kbps: number) => string;
12
+ }
@@ -0,0 +1,75 @@
1
+ import { AppLocale } from '../locale';
2
+ export class FileValidationLocalization {
3
+ get invalidFile() {
4
+ return loc.invalidFile[AppLocale.current];
5
+ }
6
+ get unsupportedType() {
7
+ return loc.unsupportedType[AppLocale.current];
8
+ }
9
+ get audioOnly() {
10
+ return loc.audioOnly[AppLocale.current];
11
+ }
12
+ get maxSize() {
13
+ return loc.maxSize[AppLocale.current];
14
+ }
15
+ get extensions() {
16
+ return loc.extensions[AppLocale.current];
17
+ }
18
+ get maxDimensions() {
19
+ return loc.maxDimensions[AppLocale.current];
20
+ }
21
+ get minDimensions() {
22
+ return loc.minDimensions[AppLocale.current];
23
+ }
24
+ get maxDuration() {
25
+ return loc.maxDuration[AppLocale.current];
26
+ }
27
+ get aspectRatio() {
28
+ return loc.aspectRatio[AppLocale.current];
29
+ }
30
+ get maxBitrate() {
31
+ return loc.maxBitrate[AppLocale.current];
32
+ }
33
+ }
34
+ const loc = {
35
+ invalidFile: {
36
+ en: 'Invalid file',
37
+ no: 'Ugyldig fil'
38
+ },
39
+ unsupportedType: {
40
+ en: 'Unsupported file type',
41
+ no: 'Filtypen støttes ikke'
42
+ },
43
+ audioOnly: {
44
+ en: 'Audio files only',
45
+ no: 'Kun lydfiler'
46
+ },
47
+ maxSize: {
48
+ en: (size) => `Maximum file size: ${size}`,
49
+ no: (size) => `Maks filstørrelse: ${size}`
50
+ },
51
+ extensions: {
52
+ en: (list) => `Allowed file types: ${list}`,
53
+ no: (list) => `Tillatte filtyper: ${list}`
54
+ },
55
+ maxDimensions: {
56
+ en: (width, height) => `Maximum dimensions: ${width}×${height}px`,
57
+ no: (width, height) => `Maks oppløsning: ${width}×${height}px`
58
+ },
59
+ minDimensions: {
60
+ en: (width, height) => `Minimum dimensions: ${width}×${height}px`,
61
+ no: (width, height) => `Min oppløsning: ${width}×${height}px`
62
+ },
63
+ maxDuration: {
64
+ en: (seconds) => `Maximum duration: ${seconds}s`,
65
+ no: (seconds) => `Maks varighet: ${seconds}s`
66
+ },
67
+ aspectRatio: {
68
+ en: (ratios) => `Allowed aspect ratio: ${ratios}`,
69
+ no: (ratios) => `Tillatt sideforhold: ${ratios}`
70
+ },
71
+ maxBitrate: {
72
+ en: (kbps) => `Maximum bitrate: ${kbps} kbps`,
73
+ no: (kbps) => `Maks bitrate: ${kbps} kbps`
74
+ }
75
+ };
@@ -0,0 +1,8 @@
1
+ import type { FileValidationRule, FileValidationRuleSets } from './file-validation-types';
2
+ export declare const toRuleSets: (rules: FileValidationRule[] | FileValidationRuleSets) => FileValidationRuleSets;
3
+ /**
4
+ * Union of every rule's `accept` across all sets, de-duplicated — the native picker filter /
5
+ * drag-over hint a containing cmp shows so a file matching any set can be selected.
6
+ */
7
+ export declare const deriveAccept: (rules: FileValidationRule[] | FileValidationRuleSets) => string;
8
+ export declare const pickRuleSet: (file: File, sets: FileValidationRuleSets) => FileValidationRule[] | null;
@@ -0,0 +1,24 @@
1
+ import { matchesAcceptedFileTypes } from './file-types';
2
+ const isRuleSets = (rules) => Array.isArray(rules[0]);
3
+ export const toRuleSets = (rules) => rules.length === 0 ? [] : isRuleSets(rules) ? rules : [rules];
4
+ const acceptTokens = (set) => set
5
+ .flatMap((rule) => (rule.accept ? rule.accept.split(',') : []))
6
+ .map((token) => token.trim())
7
+ .filter(Boolean);
8
+ /**
9
+ * Union of every rule's `accept` across all sets, de-duplicated — the native picker filter /
10
+ * drag-over hint a containing cmp shows so a file matching any set can be selected.
11
+ */
12
+ export const deriveAccept = (rules) => {
13
+ const tokens = toRuleSets(rules).flatMap(acceptTokens);
14
+ return [...new Set(tokens)].join(',');
15
+ };
16
+ export const pickRuleSet = (file, sets) => {
17
+ for (const set of sets) {
18
+ const accept = acceptTokens(set).join(',');
19
+ if (!accept || matchesAcceptedFileTypes(file, accept)) {
20
+ return set;
21
+ }
22
+ }
23
+ return null;
24
+ };
@@ -1,30 +1,39 @@
1
1
  import type { FileValidationRule } from './file-validation-types';
2
2
  /**
3
- * Built-in rule factories. Each takes a constraint plus the consumer-supplied localized message
4
- * kit never ships English error strings. Returned rule is a callable function; the `Mime`
5
- * factory additionally attaches `.accept` so containing UI cmps (`FileUploader`,
6
- * `OpenFileButton`) can auto-derive the native picker filter / drag-over hint.
3
+ * Built-in rule factories. Each takes a constraint and an optional `message`; when omitted, the
4
+ * rule falls back to a localized default from `FileValidationLocalization` (kit ships no English
5
+ * literals — defaults follow `AppLocale`). The `Mime` factory additionally attaches `.accept` so
6
+ * containing UI cmps (`FileUploader`, `OpenFileButton`) can auto-derive the native picker filter /
7
+ * drag-over hint.
7
8
  *
8
9
  * @example
9
10
  * ```ts
10
11
  * const rules = [
11
- * FileRules.Mime('image/*', 'Please pick an image.'),
12
+ * FileRules.Mime('image/*'),
12
13
  * FileRules.MaxSize(5 * 1024 * 1024, 'Image must be under 5 MB.')
13
14
  * ];
14
15
  * const results = await FileValidator.validateMany(files, rules);
15
16
  * ```
16
17
  */
17
18
  export declare const FileRules: {
18
- MaxSize: (bytes: number, message: string) => FileValidationRule;
19
- Mime: (accept: string, message: string) => FileValidationRule;
19
+ AudioBitrate: (maxKbps: number, message?: string) => FileValidationRule;
20
+ AudioDuration: (maxSeconds: number, message?: string) => FileValidationRule;
21
+ AudioOnly: (message?: string) => FileValidationRule;
22
+ Extensions: (extensions: string[], message?: string) => FileValidationRule;
20
23
  ImageDimensions: (max: {
21
24
  width: number;
22
25
  height: number;
23
- }, message: string) => FileValidationRule;
26
+ }, message?: string) => FileValidationRule;
24
27
  ImageMinDimensions: (min: {
25
28
  width: number;
26
29
  height: number;
27
- }, message: string) => FileValidationRule;
28
- AudioDuration: (maxSeconds: number, message: string) => FileValidationRule;
29
- VideoDuration: (maxSeconds: number, message: string) => FileValidationRule;
30
+ }, message?: string) => FileValidationRule;
31
+ MaxSize: (bytes: number, message?: string) => FileValidationRule;
32
+ Mime: (accept: string, message?: string) => FileValidationRule;
33
+ VideoAspectRatio: (ratios: string[], message?: string, tolerancePercent?: number) => FileValidationRule;
34
+ VideoDimensions: (max: {
35
+ width: number;
36
+ height: number;
37
+ }, message?: string) => FileValidationRule;
38
+ VideoDuration: (maxSeconds: number, message?: string) => FileValidationRule;
30
39
  };
@@ -1,4 +1,7 @@
1
+ import { StringHelper } from '../utils';
2
+ import { FileHelper } from './file-helper';
1
3
  import { matchesAcceptedFileTypes } from './file-types';
4
+ import { FileValidationLocalization } from './file-validation-localization';
2
5
  const readImageDimensions = (file) => new Promise((resolve, reject) => {
3
6
  const url = URL.createObjectURL(file);
4
7
  const img = new Image();
@@ -26,42 +29,96 @@ const readMediaDuration = (file, kind) => new Promise((resolve, reject) => {
26
29
  };
27
30
  el.src = url;
28
31
  });
32
+ const readVideoDimensions = (file) => new Promise((resolve, reject) => {
33
+ const url = URL.createObjectURL(file);
34
+ const video = document.createElement('video');
35
+ video.preload = 'metadata';
36
+ video.onloadedmetadata = () => {
37
+ URL.revokeObjectURL(url);
38
+ resolve({ width: video.videoWidth, height: video.videoHeight });
39
+ };
40
+ video.onerror = () => {
41
+ URL.revokeObjectURL(url);
42
+ reject(new Error('Failed to read video dimensions'));
43
+ };
44
+ video.src = url;
45
+ });
46
+ const parseRatio = (ratio) => {
47
+ const [w, h] = ratio.split(':').map((part) => parseFloat(part));
48
+ return h ? w / h : NaN;
49
+ };
50
+ const formatExtensions = (extensions) => extensions.map((ext) => '.' + ext.replace(/^\./, '')).join(', ');
51
+ const messages = new FileValidationLocalization();
29
52
  /**
30
- * Built-in rule factories. Each takes a constraint plus the consumer-supplied localized message
31
- * kit never ships English error strings. Returned rule is a callable function; the `Mime`
32
- * factory additionally attaches `.accept` so containing UI cmps (`FileUploader`,
33
- * `OpenFileButton`) can auto-derive the native picker filter / drag-over hint.
53
+ * Built-in rule factories. Each takes a constraint and an optional `message`; when omitted, the
54
+ * rule falls back to a localized default from `FileValidationLocalization` (kit ships no English
55
+ * literals — defaults follow `AppLocale`). The `Mime` factory additionally attaches `.accept` so
56
+ * containing UI cmps (`FileUploader`, `OpenFileButton`) can auto-derive the native picker filter /
57
+ * drag-over hint.
34
58
  *
35
59
  * @example
36
60
  * ```ts
37
61
  * const rules = [
38
- * FileRules.Mime('image/*', 'Please pick an image.'),
62
+ * FileRules.Mime('image/*'),
39
63
  * FileRules.MaxSize(5 * 1024 * 1024, 'Image must be under 5 MB.')
40
64
  * ];
41
65
  * const results = await FileValidator.validateMany(files, rules);
42
66
  * ```
43
67
  */
44
68
  export const FileRules = {
45
- MaxSize: (bytes, message) => (file) => file.size > bytes ? message : null,
46
- Mime: (accept, message) => {
47
- const rule = (file) => (matchesAcceptedFileTypes(file, accept) ? null : message);
48
- rule.accept = accept;
49
- return rule;
69
+ AudioBitrate: (maxKbps, message) => async (file) => {
70
+ const duration = await readMediaDuration(file, 'audio');
71
+ if (!duration) {
72
+ return null;
73
+ }
74
+ const kbps = (file.size * 8) / duration / 1000;
75
+ return kbps > maxKbps ? (message ?? messages.maxBitrate(maxKbps)) : null;
76
+ },
77
+ AudioDuration: (maxSeconds, message) => async (file) => {
78
+ const duration = await readMediaDuration(file, 'audio');
79
+ return duration > maxSeconds ? (message ?? messages.maxDuration(maxSeconds)) : null;
80
+ },
81
+ AudioOnly: (message) => async (file) => {
82
+ const { width, height } = await readVideoDimensions(file);
83
+ return width > 0 || height > 0 ? (message ?? messages.audioOnly) : null;
84
+ },
85
+ Extensions: (extensions, message) => (file) => {
86
+ const extension = FileHelper.getFileExtension(file.name).toLowerCase();
87
+ const allowed = extensions.map((ext) => ext.replace(/^\./, '').toLowerCase());
88
+ return allowed.includes(extension) ? null : (message ?? messages.extensions(formatExtensions(extensions)));
50
89
  },
51
90
  ImageDimensions: (max, message) => async (file) => {
52
91
  const { width, height } = await readImageDimensions(file);
53
- return width > max.width || height > max.height ? message : null;
92
+ return width > max.width || height > max.height ? (message ?? messages.maxDimensions(max.width, max.height)) : null;
54
93
  },
55
94
  ImageMinDimensions: (min, message) => async (file) => {
56
95
  const { width, height } = await readImageDimensions(file);
57
- return width < min.width || height < min.height ? message : null;
96
+ return width < min.width || height < min.height ? (message ?? messages.minDimensions(min.width, min.height)) : null;
58
97
  },
59
- AudioDuration: (maxSeconds, message) => async (file) => {
60
- const duration = await readMediaDuration(file, 'audio');
61
- return duration > maxSeconds ? message : null;
98
+ MaxSize: (bytes, message) => (file) => file.size > bytes ? (message ?? messages.maxSize(StringHelper.toFileSizeString(bytes))) : null,
99
+ Mime: (accept, message) => {
100
+ const rule = (file) => (matchesAcceptedFileTypes(file, accept) ? null : (message ?? messages.unsupportedType));
101
+ rule.accept = accept;
102
+ return rule;
103
+ },
104
+ VideoAspectRatio: (ratios, message, tolerancePercent = 1) => async (file) => {
105
+ const { width, height } = await readVideoDimensions(file);
106
+ if (!width || !height) {
107
+ return message ?? messages.aspectRatio(ratios.join(', '));
108
+ }
109
+ const actual = width / height;
110
+ const matches = ratios.some((ratio) => {
111
+ const target = parseRatio(ratio);
112
+ return Number.isFinite(target) && Math.abs((actual / target) * 100 - 100) <= tolerancePercent;
113
+ });
114
+ return matches ? null : (message ?? messages.aspectRatio(ratios.join(', ')));
115
+ },
116
+ VideoDimensions: (max, message) => async (file) => {
117
+ const { width, height } = await readVideoDimensions(file);
118
+ return width > max.width || height > max.height ? (message ?? messages.maxDimensions(max.width, max.height)) : null;
62
119
  },
63
120
  VideoDuration: (maxSeconds, message) => async (file) => {
64
121
  const duration = await readMediaDuration(file, 'video');
65
- return duration > maxSeconds ? message : null;
122
+ return duration > maxSeconds ? (message ?? messages.maxDuration(maxSeconds)) : null;
66
123
  }
67
124
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Single async validation rule. Returns `null` (or empty array) when the file is valid,
3
- * a string (or array of strings) when invalid. The consumer supplies the localized message
4
- * when constructing the rule kit ships only rule factories, never English defaults.
3
+ * a string (or array of strings) when invalid. The message is supplied by the rule factory;
4
+ * when omitted it falls back to a localized default (`FileValidationLocalization`).
5
5
  *
6
6
  * Optional `accept` property: when set (only the `FileRules.Mime` factory does this), the
7
7
  * containing UI cmps (`FileUploader`, `OpenFileButton`) auto-derive the native picker filter
@@ -10,6 +10,13 @@
10
10
  export type FileValidationRule = ((file: File) => string | string[] | null | Promise<string | string[] | null>) & {
11
11
  accept?: string;
12
12
  };
13
+ /**
14
+ * One or more rule sets. A flat `FileValidationRule[]` is a single set applied to every file.
15
+ * A `FileValidationRule[][]` is several sets: per file, the first set whose `accept` matches
16
+ * (a set with no `accept` rule matches anything) is applied — lets one picker accept different
17
+ * file types with different rules. A file matching no set is rejected as an unsupported type.
18
+ */
19
+ export type FileValidationRuleSets = FileValidationRule[][];
13
20
  /** Aggregate validation result for a single file across one or more rules. */
14
21
  export type FileValidationResult = {
15
22
  file: File;
@@ -1,5 +1,5 @@
1
- import type { FileValidationResult, FileValidationRule } from './file-validation-types';
1
+ import type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from './file-validation-types';
2
2
  export declare class FileValidator {
3
- static validate(file: File, rules: FileValidationRule[]): Promise<FileValidationResult>;
4
- static validateMany(files: File[], rules: FileValidationRule[]): Promise<FileValidationResult[]>;
3
+ static validate(file: File, rules: FileValidationRule[] | FileValidationRuleSets): Promise<FileValidationResult>;
4
+ static validateMany(files: File[], rules: FileValidationRule[] | FileValidationRuleSets): Promise<FileValidationResult[]>;
5
5
  }
@@ -1,9 +1,23 @@
1
+ import { FileValidationLocalization } from './file-validation-localization';
2
+ import { pickRuleSet, toRuleSets } from './file-validation-rule-sets';
1
3
  const toArray = (v) => (v === null ? [] : Array.isArray(v) ? v : [v]);
4
+ const messages = new FileValidationLocalization();
2
5
  export class FileValidator {
3
6
  static async validate(file, rules) {
7
+ const sets = toRuleSets(rules);
8
+ const set = sets.length > 1 ? pickRuleSet(file, sets) : (sets[0] ?? []);
9
+ if (set === null) {
10
+ return { file, isValid: false, errors: [messages.unsupportedType] };
11
+ }
4
12
  const errors = [];
5
- for (const rule of rules) {
6
- const result = await rule(file);
13
+ for (const rule of set) {
14
+ let result;
15
+ try {
16
+ result = await rule(file);
17
+ }
18
+ catch {
19
+ return { file, isValid: false, errors: [messages.invalidFile] };
20
+ }
7
21
  const msgs = toArray(result);
8
22
  if (msgs.length > 0) {
9
23
  errors.push(...msgs);
@@ -1,5 +1,5 @@
1
1
  export type { BlobWithName, FileWithBlobUrl, FileWithUploadUrl } from './types';
2
- export type { FileValidationResult, FileValidationRule } from './file-validation-types';
2
+ export type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from './file-validation-types';
3
3
  export type { BlobKind, BlobKinds, BlobUpload, BlobUploadStrategy, UploadingFile, UploadingFileStatus } from './upload-types';
4
4
  export type { UploadMediaStoreOptions } from './upload-media-store.svelte';
5
5
  export { toBlobWithName, toFileWithUploadUrl } from './types';
@@ -8,6 +8,7 @@ export { openFile } from './open-file';
8
8
  export { FileHelper } from './file-helper';
9
9
  export { downloadBlob, downloadFromUrl, downloadJson, fetchFile } from './file-service';
10
10
  export { AcceptFileType, matchesAcceptedFileTypes } from './file-types';
11
+ export { deriveAccept } from './file-validation-rule-sets';
11
12
  export { resizeBlob, resizeImage } from './image-resizer';
12
13
  export { FileWithBlobDataHelper } from './file-with-blob-data-helper';
13
14
  export { FilesProvider } from './files-provider';
@@ -5,6 +5,7 @@ export { openFile } from './open-file';
5
5
  export { FileHelper } from './file-helper';
6
6
  export { downloadBlob, downloadFromUrl, downloadJson, fetchFile } from './file-service';
7
7
  export { AcceptFileType, matchesAcceptedFileTypes } from './file-types';
8
+ export { deriveAccept } from './file-validation-rule-sets';
8
9
  export { resizeBlob, resizeImage } from './image-resizer';
9
10
  export { FileWithBlobDataHelper } from './file-with-blob-data-helper';
10
11
  export { FilesProvider } from './files-provider';
@@ -4,6 +4,7 @@ import { untrack } from 'svelte';
4
4
  let { dialog } = $props();
5
5
  let dialogElement = $state();
6
6
  const isActive = $derived(Dialogs.active(dialog.containerId)?.id === dialog.id);
7
+ const isStacked = $derived(Dialogs.dialogs(dialog.containerId).findIndex((d) => d.id === dialog.id) > 0);
7
8
  const canDismiss = $derived(!dialog.controller.settings.nonCancelable && dialog.controller.settings.closeOnEsc);
8
9
  const DialogView = $derived(dialog.view);
9
10
  // TEMP: Workaround for Chromium bug where preventDefault() on cancel event
@@ -65,6 +66,7 @@ const isStretchContent = $derived(position === 'full-screen' || position === 'fu
65
66
  bind:this={dialogElement}
66
67
  class="dialog-container"
67
68
  class:dialog-container--inactive={!isActive}
69
+ class:dialog-container--stacked={isStacked}
68
70
  class:dialog-container--size-small={size === 'small'}
69
71
  class:dialog-container--size-medium={size === 'medium'}
70
72
  class:dialog-container--size-default={size === 'default'}
@@ -150,6 +152,9 @@ DialogContainer — wraps the native `<dialog>` element with sizing, positioning
150
152
  background: var(--_dialog-container--backdrop);
151
153
  animation: dialog-container-backdrop-fade-in var(--sc-kit--duration--slow) var(--sc-kit--ease--out) both;
152
154
  }
155
+ .dialog-container--stacked::backdrop {
156
+ animation: none;
157
+ }
153
158
  .dialog-container--inactive::backdrop {
154
159
  background: transparent;
155
160
  }
@@ -1,13 +1,8 @@
1
- <script lang="ts">import { FileValidator } from '../../core/files';
1
+ <script lang="ts">import { deriveAccept, FileValidator } from '../../core/files';
2
2
  import { Icon } from '../icon';
3
3
  import IconArrowUpload from '@fluentui/svg-icons/icons/arrow_upload_24_regular.svg?raw';
4
4
  const { multiple = false, validationRules, disabled = false, title = '', description = '', on } = $props();
5
- // Auto-derive the native picker `accept` from any Mime-style rule that exposes its accept
6
- // string. Single source of truth — consumer writes the MIME pattern once on the rule.
7
- const accept = $derived(validationRules
8
- ?.map((r) => r.accept)
9
- .filter((a) => !!a)
10
- .join(',') ?? '');
5
+ const accept = $derived(deriveAccept(validationRules ?? []));
11
6
  let inputRef = $state.raw(undefined);
12
7
  let dragOver = $state(false);
13
8
  // Snapshot of MIME types captured at dragenter — DataTransfer.files is empty during drag
@@ -1,13 +1,16 @@
1
- import type { FileValidationResult, FileValidationRule } from '../../core/files';
1
+ import type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from '../../core/files';
2
2
  type Props = {
3
3
  /** Allow multi-file selection. @default false */
4
4
  multiple?: boolean;
5
5
  /**
6
- * Validation rules applied to each picked / dropped file. The native picker filter and
7
- * drag-over visual hint are auto-derived from any `FileRules.Mime(...)` rule's `accept`
8
- * pass it once via the rule, the cmp wires the rest.
6
+ * Validation rules applied to each picked / dropped file a single set
7
+ * (`FileValidationRule[]`) or several sets (`FileValidationRule[][]`). With several sets,
8
+ * each file is validated by the first set whose `accept` matches it (a set with no `accept`
9
+ * rule matches anything; a file matching none is rejected). The native picker filter and
10
+ * drag-over visual hint are auto-derived from every set's `FileRules.Mime(...)` rule's
11
+ * `accept` — pass it once via the rule, the cmp wires the rest.
9
12
  */
10
- validationRules?: FileValidationRule[];
13
+ validationRules?: FileValidationRule[] | FileValidationRuleSets;
11
14
  disabled?: boolean;
12
15
  /** Headline shown inside the drop zone. */
13
16
  title?: string;
@@ -1,11 +1,7 @@
1
- <script lang="ts">import { FileValidator, openFile } from '../../core/files';
1
+ <script lang="ts">import { deriveAccept, FileValidator, openFile } from '../../core/files';
2
2
  import { Button } from '../button';
3
3
  const { size = 'md', variant = 'secondary', disabled = false, multiple = false, capture, validationRules, icon, iconPosition = 'leading', children, 'aria-label': ariaLabel, on } = $props();
4
- // Auto-derive native picker `accept` from any Mime-style rule that exposes its accept string.
5
- const accept = $derived(validationRules
6
- ?.map((r) => r.accept)
7
- .filter((a) => !!a)
8
- .join(',') ?? '');
4
+ const accept = $derived(deriveAccept(validationRules ?? []));
9
5
  const handleClick = async () => {
10
6
  const files = await openFile({ multiple, accept, capture });
11
7
  if (files.length === 0) {
@@ -1,4 +1,4 @@
1
- import type { FileValidationResult, FileValidationRule } from '../../core/files';
1
+ import type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from '../../core/files';
2
2
  import type { ButtonSize, ButtonVariant } from '../button';
3
3
  import type { IconProp } from '../icon';
4
4
  import type { Snippet } from 'svelte';
@@ -12,10 +12,13 @@ type Props = {
12
12
  /** Optional file-picker capture hint (mobile camera / mic). */
13
13
  capture?: 'user' | 'environment';
14
14
  /**
15
- * Rules applied to picked files. The native picker `accept` filter is auto-derived from
16
- * any `FileRules.Mime(...)` rule's `accept` pass the MIME pattern once via the rule.
15
+ * Rules applied to picked files a single set (`FileValidationRule[]`) or several sets
16
+ * (`FileValidationRule[][]`). With several sets, each file is validated by the first set
17
+ * whose `accept` matches it (a set with no `accept` rule matches anything; a file matching
18
+ * none is rejected). The native picker `accept` filter is auto-derived from every set's
19
+ * `FileRules.Mime(...)` rule — pass the MIME pattern once via the rule.
17
20
  */
18
- validationRules?: FileValidationRule[];
21
+ validationRules?: FileValidationRule[] | FileValidationRuleSets;
19
22
  /** Icon — forwarded to the underlying Button. */
20
23
  icon?: IconProp;
21
24
  /** Icon position. @default 'leading' */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.9.14",
3
+ "version": "0.9.16",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",