@classic-homes/theme-svelte 1.40.0 → 1.41.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.
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import type { FileMetadata, MediaItem } from '../types/components.js';
17
17
  import { cn } from '../utils.js';
18
+ import { acceptLabel, matchesAccept } from '../utils/file-accept.js';
18
19
  import { formatDuration } from '../utils/media.js';
19
20
  import Button from './Button.svelte';
20
21
  import Lightbox from './media/Lightbox.svelte';
@@ -29,7 +30,12 @@
29
30
  maxFiles?: number;
30
31
  /** Maximum file size in bytes (default: 10MB) */
31
32
  maxSizeBytes?: number;
32
- /** Accepted MIME types */
33
+ /**
34
+ * Accepted file types, as `<input accept>` entries: MIME types
35
+ * (`application/pdf`), wildcards (`image/*`) or extensions (`.dxf`,
36
+ * matched case-insensitively against the file name). A file passes when
37
+ * any entry matches.
38
+ */
33
39
  acceptedTypes?: string[];
34
40
  /** Whether upload is disabled */
35
41
  disabled?: boolean;
@@ -107,29 +113,9 @@
107
113
  /**
108
114
  * Readable label for the accepted types shown in the drop zone.
109
115
  * Uses the explicit `typesLabel` when provided, otherwise groups the
110
- * accepted MIME types into friendly names (Images, PDF, Videos, ...).
116
+ * accepted types into friendly names (Images, PDF, Videos, DXF, ...).
111
117
  */
112
- const acceptedTypesLabel = $derived.by(() => {
113
- if (typesLabel) return typesLabel;
114
-
115
- const groups: string[] = [];
116
- const add = (name: string) => {
117
- if (!groups.includes(name)) groups.push(name);
118
- };
119
-
120
- for (const type of acceptedTypes) {
121
- if (type.startsWith('image/')) add('Images');
122
- else if (type === 'application/pdf') add('PDF');
123
- else if (type.startsWith('video/')) add('Videos');
124
- else if (type.startsWith('audio/')) add('Audio');
125
- else {
126
- const subtype = type.split('/')[1];
127
- add(subtype ? subtype.toUpperCase() : type);
128
- }
129
- }
130
-
131
- return groups.join(', ');
132
- });
118
+ const acceptedTypesLabel = $derived(typesLabel || acceptLabel(acceptedTypes));
133
119
 
134
120
  // Helper to generate unique ID
135
121
  function generateId(): string {
@@ -143,17 +129,11 @@
143
129
  return `File "${file.name}" exceeds maximum size of ${maxMB}MB`;
144
130
  }
145
131
 
146
- // Check if file type is accepted
147
- const isAccepted = acceptedTypes.some((type) => {
148
- if (type.endsWith('/*')) {
149
- const category = type.slice(0, -2);
150
- return file.type.startsWith(category);
151
- }
152
- return file.type === type;
153
- });
154
-
155
- if (!isAccepted) {
156
- return `File type "${file.type}" is not allowed`;
132
+ // Same rules as the native `accept` attribute: extensions, wildcards, MIME types
133
+ if (!matchesAccept(file, acceptedTypes)) {
134
+ return file.type
135
+ ? `File type "${file.type}" is not allowed`
136
+ : `File "${file.name}" is not an allowed type`;
157
137
  }
158
138
 
159
139
  return null;
@@ -22,7 +22,12 @@ interface Props {
22
22
  maxFiles?: number;
23
23
  /** Maximum file size in bytes (default: 10MB) */
24
24
  maxSizeBytes?: number;
25
- /** Accepted MIME types */
25
+ /**
26
+ * Accepted file types, as `<input accept>` entries: MIME types
27
+ * (`application/pdf`), wildcards (`image/*`) or extensions (`.dxf`,
28
+ * matched case-insensitively against the file name). A file passes when
29
+ * any entry matches.
30
+ */
26
31
  acceptedTypes?: string[];
27
32
  /** Whether upload is disabled */
28
33
  disabled?: boolean;
@@ -116,6 +116,7 @@ export type { PanelEdge, PanelMode, PanelVariant, SnapPoint, CardConfig, Positio
116
116
  export { cn } from './utils.js';
117
117
  export { parseDate, toISOString, formatDate, formatDateTime, isSameDay } from './utils/date.js';
118
118
  export { generateId, computeDescribedBy } from './utils/form.js';
119
+ export { matchesAccept, acceptLabel, type AcceptableFile } from './utils/file-accept.js';
119
120
  export { formatDuration, formatDurationSpoken, inferMediaKind, parseEmbedUrl, buildEmbedSrc, getEmbedThumbnail, getMediaThumbnail, getMediaLabel, isTimeBasedMedia, isSpatialMedia, isHlsSource, resolveAspectRatio, toMediaSources, HLS_MIME_TYPE, DASH_MIME_TYPE, type ParsedEmbed, type EmbedSrcOptions, } from './utils/media.js';
120
121
  export { cloudflareStream, cloudflareStreamUrls, cloudflareStreamThumbnail, cloudflareStreamHost, type CloudflareStreamItemOptions, type CloudflareStreamUrlOptions, type CloudflareStreamUrls, type CloudflareStreamThumbnailOptions, } from './utils/media.js';
121
122
  export { matterportTour, buildMatterportSrc, fetchMatterportMetadata, type MatterportTourOptions, type MatterportMetadata, } from './utils/media.js';
package/dist/lib/index.js CHANGED
@@ -147,6 +147,7 @@ export { cn } from './utils.js';
147
147
  export { parseDate, toISOString, formatDate, formatDateTime, isSameDay } from './utils/date.js';
148
148
  // Form utilities
149
149
  export { generateId, computeDescribedBy } from './utils/form.js';
150
+ export { matchesAccept, acceptLabel } from './utils/file-accept.js';
150
151
  // Media utilities
151
152
  export { formatDuration, formatDurationSpoken, inferMediaKind, parseEmbedUrl, buildEmbedSrc, getEmbedThumbnail, getMediaThumbnail, getMediaLabel, isTimeBasedMedia, isSpatialMedia, isHlsSource, resolveAspectRatio, toMediaSources, HLS_MIME_TYPE, DASH_MIME_TYPE, } from './utils/media.js';
152
153
  // Cloudflare Stream helpers
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Matching and labelling for a list of HTML `accept` entries, as used by
3
+ * `FileUpload`'s `acceptedTypes`. Mirrors the native `<input accept>` rules:
4
+ *
5
+ * - `.dxf` matches a file whose name ends in `.dxf`, case-insensitively
6
+ * (whatever MIME type the browser guessed, often `application/octet-stream` or '')
7
+ * - `image/*` matches any MIME type in the `image` major type
8
+ * - `application/pdf` matches that MIME type exactly, case-insensitively
9
+ *
10
+ * A file is accepted when any entry matches. An empty list accepts everything,
11
+ * as an absent `accept` attribute does.
12
+ */
13
+ /** The parts of a `File` the matcher reads. */
14
+ export interface AcceptableFile {
15
+ name: string;
16
+ type: string;
17
+ }
18
+ /** Whether `file` matches any of the `accept` entries. */
19
+ export declare function matchesAccept(file: AcceptableFile, accept: readonly string[]): boolean;
20
+ /**
21
+ * Readable label for `accept` entries: MIME groups become friendly names
22
+ * (Images, PDF, Videos, Audio), other MIME types their uppercased subtype and
23
+ * extensions their uppercased name (`.dxf` → DXF). Duplicates collapse, so
24
+ * `['application/pdf', '.pdf']` reads "PDF".
25
+ */
26
+ export declare function acceptLabel(accept: readonly string[]): string;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Matching and labelling for a list of HTML `accept` entries, as used by
3
+ * `FileUpload`'s `acceptedTypes`. Mirrors the native `<input accept>` rules:
4
+ *
5
+ * - `.dxf` matches a file whose name ends in `.dxf`, case-insensitively
6
+ * (whatever MIME type the browser guessed, often `application/octet-stream` or '')
7
+ * - `image/*` matches any MIME type in the `image` major type
8
+ * - `application/pdf` matches that MIME type exactly, case-insensitively
9
+ *
10
+ * A file is accepted when any entry matches. An empty list accepts everything,
11
+ * as an absent `accept` attribute does.
12
+ */
13
+ function entries(accept) {
14
+ return accept.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
15
+ }
16
+ /** Whether `file` matches any of the `accept` entries. */
17
+ export function matchesAccept(file, accept) {
18
+ const list = entries(accept);
19
+ if (list.length === 0)
20
+ return true;
21
+ const name = file.name.toLowerCase();
22
+ const type = file.type.trim().toLowerCase();
23
+ return list.some((entry) => {
24
+ if (entry.startsWith('.'))
25
+ return name.endsWith(entry) && name.length > entry.length;
26
+ if (entry === '*' || entry === '*/*')
27
+ return true;
28
+ if (entry.endsWith('/*'))
29
+ return type.startsWith(entry.slice(0, -1));
30
+ return type === entry;
31
+ });
32
+ }
33
+ /**
34
+ * Readable label for `accept` entries: MIME groups become friendly names
35
+ * (Images, PDF, Videos, Audio), other MIME types their uppercased subtype and
36
+ * extensions their uppercased name (`.dxf` → DXF). Duplicates collapse, so
37
+ * `['application/pdf', '.pdf']` reads "PDF".
38
+ */
39
+ export function acceptLabel(accept) {
40
+ const groups = [];
41
+ const add = (label) => {
42
+ if (!groups.includes(label))
43
+ groups.push(label);
44
+ };
45
+ for (const raw of accept) {
46
+ const type = raw.trim().toLowerCase();
47
+ if (!type)
48
+ continue;
49
+ if (type.startsWith('.'))
50
+ add(type.slice(1).toUpperCase());
51
+ else if (type === '*' || type === '*/*')
52
+ add('Any file');
53
+ else if (type.startsWith('image/'))
54
+ add('Images');
55
+ else if (type === 'application/pdf')
56
+ add('PDF');
57
+ else if (type.startsWith('video/'))
58
+ add('Videos');
59
+ else if (type.startsWith('audio/'))
60
+ add('Audio');
61
+ else {
62
+ const subtype = type.split('/')[1];
63
+ add(subtype ? subtype.toUpperCase() : raw.trim());
64
+ }
65
+ }
66
+ return groups.join(', ');
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/theme-svelte",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "Svelte components for the Classic theme system",
5
5
  "type": "module",
6
6
  "svelte": "./dist/lib/index.js",