@theseam/ui-common 1.0.2-beta.60 → 1.0.2-beta.63

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,537 @@
1
+ import { faFile, faFilePdf, faFileImage, faFileWord, faFileExcel, faUpload, faTimes } from '@fortawesome/free-solid-svg-icons';
2
+ import * as i0 from '@angular/core';
3
+ import { input, booleanAttribute, output, signal, computed, Directive, viewChild, ChangeDetectionStrategy, Component, inject, ChangeDetectorRef, effect, forwardRef } from '@angular/core';
4
+ import * as i1 from '@theseam/ui-common/icon';
5
+ import { TheSeamIconModule } from '@theseam/ui-common/icon';
6
+ import { NgTemplateOutlet } from '@angular/common';
7
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
8
+
9
+ function seamFileItemFromFile(file, id) {
10
+ return {
11
+ name: file.name,
12
+ size: file.size,
13
+ type: file.type,
14
+ source: { kind: 'file', file },
15
+ id,
16
+ };
17
+ }
18
+ function seamFileItemFromUrl(url, opts = {}) {
19
+ return {
20
+ name: opts.name ?? _basenameFromUrl(url) ?? url,
21
+ size: opts.size,
22
+ type: opts.type,
23
+ source: { kind: 'url', url },
24
+ id: opts.id,
25
+ thumbnailUrl: opts.thumbnailUrl,
26
+ };
27
+ }
28
+ function _basenameFromUrl(url) {
29
+ // Strip query string and fragment before pulling the final path segment.
30
+ const hashIdx = url.indexOf('#');
31
+ const noHash = hashIdx >= 0 ? url.slice(0, hashIdx) : url;
32
+ const queryIdx = noHash.indexOf('?');
33
+ const path = queryIdx >= 0 ? noHash.slice(0, queryIdx) : noHash;
34
+ // Find the last path segment (after the last /)
35
+ const lastSlash = path.lastIndexOf('/');
36
+ if (lastSlash < 0)
37
+ return null;
38
+ const basename = path.slice(lastSlash + 1);
39
+ // If there's an actual filename after the last slash, decode and return it
40
+ if (basename) {
41
+ try {
42
+ return decodeURIComponent(basename);
43
+ }
44
+ catch {
45
+ return basename;
46
+ }
47
+ }
48
+ // No basename (e.g., trailing slash or just protocol/domain), return null
49
+ return null;
50
+ }
51
+ /**
52
+ * Extracts native `File` objects from items whose source is `file`. Items
53
+ * backed by a URL or a Blob are ignored. Useful for submit-side mapping
54
+ * when the consumer only cares about newly-uploaded blobs.
55
+ */
56
+ function seamFilesFromItems(items) {
57
+ const files = [];
58
+ for (const item of items) {
59
+ if (item.source.kind === 'file') {
60
+ files.push(item.source.file);
61
+ }
62
+ }
63
+ return files;
64
+ }
65
+ const WORD_MIMES = new Set([
66
+ 'application/msword',
67
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
68
+ ]);
69
+ const EXCEL_MIMES = new Set([
70
+ 'application/vnd.ms-excel',
71
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
72
+ 'text/csv',
73
+ ]);
74
+ /**
75
+ * Maps a MIME type to a built-in SeamIcon. Returns a generic file icon for
76
+ * unknown, empty, or missing types. Returns SeamIcon (not IconDefinition) so
77
+ * the icon set can change later without a breaking signature change.
78
+ */
79
+ function iconForMime(type) {
80
+ if (!type)
81
+ return faFile;
82
+ if (type === 'application/pdf')
83
+ return faFilePdf;
84
+ if (type.startsWith('image/'))
85
+ return faFileImage;
86
+ if (WORD_MIMES.has(type))
87
+ return faFileWord;
88
+ if (EXCEL_MIMES.has(type))
89
+ return faFileExcel;
90
+ return faFile;
91
+ }
92
+
93
+ /**
94
+ * Validates a batch of files against accept / maxSize / maxFiles.
95
+ *
96
+ * `accept` is parsed as the standard comma-separated list: `.ext`, `mime/*`,
97
+ * or `mime/subtype`. Matching against `file.type` is case-insensitive; when
98
+ * `file.type` is empty, extension tokens (`.csv`) are tried against the file
99
+ * name. Each rejected file accumulates ALL applicable reasons rather than
100
+ * short-circuiting, so consumers can display comprehensive errors.
101
+ *
102
+ * `maxFiles` caps the total accepted count; files past the cap are rejected
103
+ * with reason `'count'` in arrival order.
104
+ */
105
+ function validateFiles(files, opts) {
106
+ const acceptTokens = _parseAccept(opts.accept);
107
+ const accepted = [];
108
+ const rejected = [];
109
+ for (const file of files) {
110
+ const reasons = [];
111
+ if (acceptTokens.length > 0 && !_matchesAccept(file, acceptTokens)) {
112
+ reasons.push('type');
113
+ }
114
+ if (opts.maxSize !== null && file.size > opts.maxSize) {
115
+ reasons.push('size');
116
+ }
117
+ if (reasons.length > 0) {
118
+ rejected.push({ file, reasons });
119
+ continue;
120
+ }
121
+ if (opts.maxFiles !== null && accepted.length >= opts.maxFiles) {
122
+ rejected.push({ file, reasons: ['count'] });
123
+ continue;
124
+ }
125
+ accepted.push(file);
126
+ }
127
+ return { accepted, rejected };
128
+ }
129
+ function _parseAccept(accept) {
130
+ return accept
131
+ .split(',')
132
+ .map((t) => t.trim().toLowerCase())
133
+ .filter((t) => t.length > 0);
134
+ }
135
+ function _matchesAccept(file, tokens) {
136
+ const mime = file.type.toLowerCase();
137
+ const name = file.name.toLowerCase();
138
+ for (const token of tokens) {
139
+ if (token.startsWith('.')) {
140
+ if (name.endsWith(token))
141
+ return true;
142
+ continue;
143
+ }
144
+ if (!mime)
145
+ continue;
146
+ if (token.endsWith('/*')) {
147
+ const prefix = token.slice(0, -1); // keep the slash
148
+ if (mime.startsWith(prefix))
149
+ return true;
150
+ continue;
151
+ }
152
+ if (token === mime)
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+
158
+ class TheSeamFileDropZoneDirective {
159
+ accept = input('', ...(ngDevMode ? [{ debugName: "accept" }] : []));
160
+ maxSize = input(null, ...(ngDevMode ? [{ debugName: "maxSize" }] : []));
161
+ maxFiles = input(null, ...(ngDevMode ? [{ debugName: "maxFiles" }] : []));
162
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
163
+ seamFileDrop = output();
164
+ seamFileDropRejected = output();
165
+ /** Counter-based dragenter/leave tracking to avoid child-element flicker. */
166
+ _dragDepth = signal(0, ...(ngDevMode ? [{ debugName: "_dragDepth" }] : []));
167
+ _isOver = computed(() => !this.disabled() && this._dragDepth() > 0, ...(ngDevMode ? [{ debugName: "_isOver" }] : []));
168
+ _onDragEnter(event) {
169
+ if (this.disabled())
170
+ return;
171
+ event.preventDefault();
172
+ this._dragDepth.update((n) => n + 1);
173
+ }
174
+ _onDragOver(event) {
175
+ if (this.disabled())
176
+ return;
177
+ // preventDefault is required for the drop event to fire.
178
+ event.preventDefault();
179
+ }
180
+ _onDragLeave(event) {
181
+ if (this.disabled())
182
+ return;
183
+ this._dragDepth.update((n) => Math.max(0, n - 1));
184
+ }
185
+ _onDrop(event) {
186
+ if (this.disabled())
187
+ return;
188
+ event.preventDefault();
189
+ this._dragDepth.set(0);
190
+ const files = event.dataTransfer ? Array.from(event.dataTransfer.files) : [];
191
+ if (files.length === 0)
192
+ return;
193
+ const { accepted, rejected } = validateFiles(files, {
194
+ accept: this.accept(),
195
+ maxSize: this.maxSize(),
196
+ maxFiles: this.maxFiles(),
197
+ });
198
+ this.seamFileDrop.emit(accepted);
199
+ if (rejected.length > 0)
200
+ this.seamFileDropRejected.emit(rejected);
201
+ }
202
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamFileDropZoneDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
203
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.15", type: TheSeamFileDropZoneDirective, isStandalone: true, selector: "[seamFileDropZone]", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { seamFileDrop: "seamFileDrop", seamFileDropRejected: "seamFileDropRejected" }, host: { listeners: { "dragenter": "_onDragEnter($event)", "dragover": "_onDragOver($event)", "dragleave": "_onDragLeave($event)", "drop": "_onDrop($event)" }, properties: { "class.seam-file-drop-zone--over": "_isOver()" } }, ngImport: i0 });
204
+ }
205
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamFileDropZoneDirective, decorators: [{
206
+ type: Directive,
207
+ args: [{
208
+ selector: '[seamFileDropZone]',
209
+ host: {
210
+ '[class.seam-file-drop-zone--over]': '_isOver()',
211
+ '(dragenter)': '_onDragEnter($event)',
212
+ '(dragover)': '_onDragOver($event)',
213
+ '(dragleave)': '_onDragLeave($event)',
214
+ '(drop)': '_onDrop($event)',
215
+ },
216
+ }]
217
+ }], propDecorators: { accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], seamFileDrop: [{ type: i0.Output, args: ["seamFileDrop"] }], seamFileDropRejected: [{ type: i0.Output, args: ["seamFileDropRejected"] }] } });
218
+
219
+ class TheSeamFileInputComponent {
220
+ multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
221
+ accept = input('', ...(ngDevMode ? [{ debugName: "accept" }] : []));
222
+ maxSize = input(null, ...(ngDevMode ? [{ debugName: "maxSize" }] : []));
223
+ maxFiles = input(null, ...(ngDevMode ? [{ debugName: "maxFiles" }] : []));
224
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
225
+ hideErrors = input(false, ...(ngDevMode ? [{ debugName: "hideErrors", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
226
+ promptText = input('Choose a file', ...(ngDevMode ? [{ debugName: "promptText" }] : []));
227
+ promptSuffix = input('or drag it here', ...(ngDevMode ? [{ debugName: "promptSuffix" }] : []));
228
+ filesAdded = output();
229
+ rejected = output();
230
+ _faUpload = faUpload;
231
+ _lastRejections = signal([], ...(ngDevMode ? [{ debugName: "_lastRejections" }] : []));
232
+ _effectiveMaxFiles = computed(() => {
233
+ const explicit = this.maxFiles();
234
+ if (!this.multiple()) {
235
+ return explicit !== null ? Math.min(explicit, 1) : 1;
236
+ }
237
+ return explicit;
238
+ }, ...(ngDevMode ? [{ debugName: "_effectiveMaxFiles" }] : []));
239
+ _errorMessage = computed(() => _formatErrors(this._lastRejections(), this.maxSize(), this._effectiveMaxFiles()), ...(ngDevMode ? [{ debugName: "_errorMessage" }] : []));
240
+ _nativeInput = viewChild.required('native');
241
+ _openPicker() {
242
+ if (this.disabled())
243
+ return;
244
+ this._nativeInput().nativeElement.click();
245
+ }
246
+ _onFilesDropped(files) {
247
+ this._lastRejections.set([]);
248
+ if (files.length > 0)
249
+ this.filesAdded.emit(files);
250
+ }
251
+ _onRejected(rejections) {
252
+ this._lastRejections.set(rejections);
253
+ this.rejected.emit(rejections);
254
+ }
255
+ _onNativeChange(event) {
256
+ const nativeInput = event.target;
257
+ const files = nativeInput.files ? Array.from(nativeInput.files) : [];
258
+ // Clear the value so the same file can be re-selected next time.
259
+ nativeInput.value = '';
260
+ if (files.length === 0)
261
+ return;
262
+ const { accepted, rejected } = validateFiles(files, {
263
+ accept: this.accept(),
264
+ maxSize: this.maxSize(),
265
+ maxFiles: this._effectiveMaxFiles(),
266
+ });
267
+ if (rejected.length > 0) {
268
+ this._lastRejections.set(rejected);
269
+ this.rejected.emit(rejected);
270
+ }
271
+ else {
272
+ this._lastRejections.set([]);
273
+ }
274
+ if (accepted.length > 0)
275
+ this.filesAdded.emit(accepted);
276
+ }
277
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
278
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TheSeamFileInputComponent, isStandalone: true, selector: "seam-file-input", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hideErrors: { classPropertyName: "hideErrors", publicName: "hideErrors", isSignal: true, isRequired: false, transformFunction: null }, promptText: { classPropertyName: "promptText", publicName: "promptText", isSignal: true, isRequired: false, transformFunction: null }, promptSuffix: { classPropertyName: "promptSuffix", publicName: "promptSuffix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filesAdded: "filesAdded", rejected: "rejected" }, viewQueries: [{ propertyName: "_nativeInput", first: true, predicate: ["native"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"seam-file-input__zone\"\n seamFileDropZone\n [accept]=\"accept()\"\n [maxSize]=\"maxSize()\"\n [maxFiles]=\"_effectiveMaxFiles()\"\n [disabled]=\"disabled()\"\n (seamFileDrop)=\"_onFilesDropped($event)\"\n (seamFileDropRejected)=\"_onRejected($event)\"\n role=\"button\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"_openPicker()\"\n (keydown.enter)=\"_openPicker(); $event.preventDefault()\"\n (keydown.space)=\"_openPicker(); $event.preventDefault()\"\n>\n <span class=\"seam-file-input__icon\">\n <seam-icon [icon]=\"_faUpload\"></seam-icon>\n </span>\n <p class=\"seam-file-input__prompt\">\n <strong>{{ promptText() }}</strong> {{ promptSuffix() }}\n </p>\n</div>\n\n<input\n #native\n type=\"file\"\n hidden\n [multiple]=\"multiple()\"\n [attr.accept]=\"accept() || null\"\n (change)=\"_onNativeChange($event)\"\n/>\n\n@if (!hideErrors() && _errorMessage(); as msg) {\n <p class=\"seam-file-input__errors\">{{ msg }}</p>\n}\n", styles: [":host{display:block}.seam-file-input__zone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;min-height:160px;padding:24px 20px;text-align:center;border:1.5px dashed #ced4da;border-radius:6px;background:#fff;color:#495057;cursor:pointer;transition:border-color .15s ease,background-color .15s ease}.seam-file-input__zone:focus-visible{outline:2px solid #357ebd;outline-offset:2px}.seam-file-input__zone.seam-file-drop-zone--over{border-color:#357ebd;background:#f5faff}.seam-file-input__zone.seam-file-drop-zone--over .seam-file-input__icon{border-color:#357ebd;color:#357ebd}.seam-file-input__zone[tabindex=\"-1\"]{cursor:not-allowed;opacity:.6}.seam-file-input__icon{display:inline-flex;align-items:center;justify-content:center;width:48px;height:48px;border-radius:50%;background:#fff;border:1px solid #e9ecef;color:#6c757d;transition:border-color .15s ease,color .15s ease}.seam-file-input__prompt{margin:0;font-size:14px;font-weight:500;color:#343a40}.seam-file-input__prompt strong{color:#357ebd;font-weight:600}.seam-file-input__errors{margin:8px 0 0;color:#dc3545;font-size:12px}\n"], dependencies: [{ kind: "directive", type: TheSeamFileDropZoneDirective, selector: "[seamFileDropZone]", inputs: ["accept", "maxSize", "maxFiles", "disabled"], outputs: ["seamFileDrop", "seamFileDropRejected"] }, { kind: "ngmodule", type: TheSeamIconModule }, { kind: "component", type: i1.IconComponent, selector: "seam-icon", inputs: ["grayscaleOnDisable", "disabled", "iconClass", "icon", "size", "showDefaultOnError", "defaultIcon", "iconType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
279
+ }
280
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamFileInputComponent, decorators: [{
281
+ type: Component,
282
+ args: [{ selector: 'seam-file-input', imports: [TheSeamFileDropZoneDirective, TheSeamIconModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"seam-file-input__zone\"\n seamFileDropZone\n [accept]=\"accept()\"\n [maxSize]=\"maxSize()\"\n [maxFiles]=\"_effectiveMaxFiles()\"\n [disabled]=\"disabled()\"\n (seamFileDrop)=\"_onFilesDropped($event)\"\n (seamFileDropRejected)=\"_onRejected($event)\"\n role=\"button\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"_openPicker()\"\n (keydown.enter)=\"_openPicker(); $event.preventDefault()\"\n (keydown.space)=\"_openPicker(); $event.preventDefault()\"\n>\n <span class=\"seam-file-input__icon\">\n <seam-icon [icon]=\"_faUpload\"></seam-icon>\n </span>\n <p class=\"seam-file-input__prompt\">\n <strong>{{ promptText() }}</strong> {{ promptSuffix() }}\n </p>\n</div>\n\n<input\n #native\n type=\"file\"\n hidden\n [multiple]=\"multiple()\"\n [attr.accept]=\"accept() || null\"\n (change)=\"_onNativeChange($event)\"\n/>\n\n@if (!hideErrors() && _errorMessage(); as msg) {\n <p class=\"seam-file-input__errors\">{{ msg }}</p>\n}\n", styles: [":host{display:block}.seam-file-input__zone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;min-height:160px;padding:24px 20px;text-align:center;border:1.5px dashed #ced4da;border-radius:6px;background:#fff;color:#495057;cursor:pointer;transition:border-color .15s ease,background-color .15s ease}.seam-file-input__zone:focus-visible{outline:2px solid #357ebd;outline-offset:2px}.seam-file-input__zone.seam-file-drop-zone--over{border-color:#357ebd;background:#f5faff}.seam-file-input__zone.seam-file-drop-zone--over .seam-file-input__icon{border-color:#357ebd;color:#357ebd}.seam-file-input__zone[tabindex=\"-1\"]{cursor:not-allowed;opacity:.6}.seam-file-input__icon{display:inline-flex;align-items:center;justify-content:center;width:48px;height:48px;border-radius:50%;background:#fff;border:1px solid #e9ecef;color:#6c757d;transition:border-color .15s ease,color .15s ease}.seam-file-input__prompt{margin:0;font-size:14px;font-weight:500;color:#343a40}.seam-file-input__prompt strong{color:#357ebd;font-weight:600}.seam-file-input__errors{margin:8px 0 0;color:#dc3545;font-size:12px}\n"] }]
283
+ }], propDecorators: { multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], hideErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideErrors", required: false }] }], promptText: [{ type: i0.Input, args: [{ isSignal: true, alias: "promptText", required: false }] }], promptSuffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "promptSuffix", required: false }] }], filesAdded: [{ type: i0.Output, args: ["filesAdded"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], _nativeInput: [{ type: i0.ViewChild, args: ['native', { isSignal: true }] }] } });
284
+ function _formatErrors(rejections, maxSize, maxFiles) {
285
+ if (rejections.length === 0)
286
+ return null;
287
+ const firstReason = rejections[0].reasons[0];
288
+ switch (firstReason) {
289
+ case 'type':
290
+ return 'File type not accepted.';
291
+ case 'size': {
292
+ const mb = maxSize !== null ? (maxSize / (1024 * 1024)).toFixed(1) : null;
293
+ return mb
294
+ ? `File exceeds the maximum size (${mb} MB).`
295
+ : 'File exceeds the maximum size.';
296
+ }
297
+ case 'count':
298
+ return maxFiles !== null
299
+ ? `Only ${maxFiles} file(s) can be added.`
300
+ : 'Too many files selected.';
301
+ default:
302
+ return 'File could not be accepted.';
303
+ }
304
+ }
305
+
306
+ class TheSeamFileTileComponent {
307
+ _cdr = inject(ChangeDetectorRef);
308
+ item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
309
+ variant = input('row', ...(ngDevMode ? [{ debugName: "variant" }] : []));
310
+ showName = input(true, ...(ngDevMode ? [{ debugName: "showName", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
311
+ showMeta = input(true, ...(ngDevMode ? [{ debugName: "showMeta", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
312
+ removable = input(true, ...(ngDevMode ? [{ debugName: "removable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
313
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
314
+ remove = output();
315
+ itemClick = output();
316
+ _faTimes = faTimes;
317
+ _mimeIcon = computed(() => iconForMime(this.item().type), ...(ngDevMode ? [{ debugName: "_mimeIcon" }] : []));
318
+ _metaLine = computed(() => _formatMeta(this.item()), ...(ngDevMode ? [{ debugName: "_metaLine" }] : []));
319
+ _showRemoveBtn = computed(() => this.removable() && !this.disabled(), ...(ngDevMode ? [{ debugName: "_showRemoveBtn" }] : []));
320
+ /** True when itemClick is observed AND the tile is not disabled. */
321
+ _isInteractive = computed(() => this._clickObserved() && !this.disabled(), ...(ngDevMode ? [{ debugName: "_isInteractive" }] : []));
322
+ /**
323
+ * Thumbnail URL for image items. Tracked across item changes so object
324
+ * URLs are revoked when the item changes or the component is destroyed.
325
+ */
326
+ _ownedObjectUrl = null;
327
+ _pendingObjectUrl = null;
328
+ _thumbUrl = computed(() => {
329
+ const item = this.item();
330
+ if (item.thumbnailUrl)
331
+ return item.thumbnailUrl;
332
+ const isImage = _isImageMime(item.type);
333
+ if ((item.source.kind === 'file' || item.source.kind === 'blob') &&
334
+ isImage) {
335
+ const blob = item.source.kind === 'file' ? item.source.file : item.source.blob;
336
+ const url = URL.createObjectURL(blob);
337
+ this._pendingObjectUrl = url;
338
+ return url;
339
+ }
340
+ if (item.source.kind === 'url' && _looksLikeImage(item)) {
341
+ return item.source.url;
342
+ }
343
+ return null;
344
+ }, ...(ngDevMode ? [{ debugName: "_thumbUrl" }] : []));
345
+ /**
346
+ * True once we detect that a consumer has wired (itemClick).
347
+ * Detected in ngAfterViewInit — by that point the parent's template binding
348
+ * has called subscribe() on the OutputEmitterRef, populating its internal
349
+ * `listeners` array (Option A: access via internal field on Angular 20's
350
+ * OutputEmitterRef). If the internal shape is absent, conservatively stays
351
+ * false (opt-out default — no unwanted role=button on inert tiles).
352
+ */
353
+ _clickObserved = signal(false, ...(ngDevMode ? [{ debugName: "_clickObserved" }] : []));
354
+ constructor() {
355
+ // When _thumbUrl changes, revoke the previous owned URL (if any).
356
+ effect(() => {
357
+ // Read the signal so this effect re-runs when the thumbnail changes.
358
+ this._thumbUrl();
359
+ const previous = this._ownedObjectUrl;
360
+ this._ownedObjectUrl = this._pendingObjectUrl;
361
+ this._pendingObjectUrl = null;
362
+ if (previous && previous !== this._ownedObjectUrl) {
363
+ URL.revokeObjectURL(previous);
364
+ }
365
+ });
366
+ // Destroy cleanup: revoke the last owned URL.
367
+ effect((onCleanup) => {
368
+ onCleanup(() => {
369
+ if (this._ownedObjectUrl) {
370
+ URL.revokeObjectURL(this._ownedObjectUrl);
371
+ this._ownedObjectUrl = null;
372
+ }
373
+ });
374
+ });
375
+ }
376
+ ngAfterViewInit() {
377
+ // Detect whether (itemClick) is bound by checking the OutputEmitterRef's
378
+ // internal listeners array. By ngAfterViewInit, the parent's template
379
+ // bindings (including event bindings via subscribe()) have already been
380
+ // applied during the parent's change-detection pass.
381
+ //
382
+ // Option A: access (this.itemClick as any).listeners which is the internal
383
+ // array in Angular 20's OutputEmitterRef (null initially, an array once
384
+ // subscribed). If the internal shape changes, we conservatively keep false.
385
+ const ref = this.itemClick;
386
+ const observed = ref.listeners !== undefined ? (ref.listeners?.length ?? 0) > 0 : false;
387
+ if (observed) {
388
+ this._clickObserved.set(true);
389
+ this._cdr.markForCheck();
390
+ }
391
+ }
392
+ _onRemove(event) {
393
+ event.stopPropagation();
394
+ this.remove.emit(this.item());
395
+ }
396
+ _onBodyClick() {
397
+ if (!this._clickObserved() || this.disabled())
398
+ return;
399
+ this.itemClick.emit(this.item());
400
+ }
401
+ _onBodyKey(event) {
402
+ if (!this._clickObserved() || this.disabled())
403
+ return;
404
+ if (event.key === 'Enter' || event.key === ' ') {
405
+ event.preventDefault();
406
+ this.itemClick.emit(this.item());
407
+ }
408
+ }
409
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamFileTileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
410
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TheSeamFileTileComponent, isStandalone: true, selector: "seam-file-tile", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, showName: { classPropertyName: "showName", publicName: "showName", isSignal: true, isRequired: false, transformFunction: null }, showMeta: { classPropertyName: "showMeta", publicName: "showMeta", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove", itemClick: "itemClick" }, ngImport: i0, template: "<div\n class=\"seam-file-tile\"\n [class.seam-file-tile--row]=\"variant() === 'row'\"\n [class.seam-file-tile--preview]=\"variant() === 'preview'\"\n [class.seam-file-tile--clickable]=\"_isInteractive()\"\n>\n @if (variant() === 'row') {\n @if (_isInteractive()) {\n <div\n class=\"seam-file-tile__clickable-body\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"_onBodyClick()\"\n (keydown)=\"_onBodyKey($event)\"\n >\n <ng-container *ngTemplateOutlet=\"rowContent\"></ng-container>\n </div>\n } @else {\n <ng-container *ngTemplateOutlet=\"rowContent\"></ng-container>\n }\n\n @if (_showRemoveBtn()) {\n <button\n type=\"button\"\n class=\"seam-file-tile__remove\"\n (click)=\"_onRemove($event)\"\n title=\"Remove file\"\n >\n <seam-icon [icon]=\"_faTimes\"></seam-icon>\n </button>\n }\n } @else {\n @if (_showRemoveBtn()) {\n <button\n type=\"button\"\n class=\"seam-file-tile__remove seam-file-tile__remove--overlay\"\n (click)=\"_onRemove($event)\"\n title=\"Remove file\"\n >\n <seam-icon [icon]=\"_faTimes\"></seam-icon>\n </button>\n }\n @if (_isInteractive()) {\n <div\n class=\"seam-file-tile__clickable-body\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"_onBodyClick()\"\n (keydown)=\"_onBodyKey($event)\"\n >\n <ng-container *ngTemplateOutlet=\"previewContent\"></ng-container>\n </div>\n } @else {\n <ng-container *ngTemplateOutlet=\"previewContent\"></ng-container>\n }\n }\n</div>\n\n<ng-template #rowContent>\n <span\n class=\"seam-file-tile__visual\"\n [class.seam-file-tile__visual--image]=\"!!_thumbUrl()\"\n >\n @if (_thumbUrl(); as url) {\n <img class=\"seam-file-tile__thumb\" [src]=\"url\" [alt]=\"item().name\" />\n } @else {\n <seam-icon [icon]=\"_mimeIcon()\"></seam-icon>\n }\n </span>\n <div class=\"seam-file-tile__body\">\n <div class=\"seam-file-tile__name\" [attr.title]=\"item().name\">\n {{ item().name }}\n </div>\n @if (showMeta() && _metaLine(); as meta) {\n <div class=\"seam-file-tile__meta\">{{ meta }}</div>\n }\n </div>\n</ng-template>\n\n<ng-template #previewContent>\n <span class=\"seam-file-tile__preview-media\">\n @if (_thumbUrl(); as url) {\n <img class=\"seam-file-tile__thumb\" [src]=\"url\" [alt]=\"item().name\" />\n } @else {\n <span class=\"seam-file-tile__visual\">\n <seam-icon [icon]=\"_mimeIcon()\"></seam-icon>\n </span>\n }\n </span>\n @if (showName()) {\n <div class=\"seam-file-tile__preview-name\" [attr.title]=\"item().name\">\n {{ item().name }}\n </div>\n }\n</ng-template>\n", styles: [":host{display:block}.seam-file-tile{box-sizing:border-box;font-family:inherit}.seam-file-tile--row{display:flex;align-items:center;gap:12px;padding:10px 12px;background:#fff;border:1px solid #e9ecef;border-radius:6px}.seam-file-tile__visual{flex:0 0 auto;width:36px;height:36px;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;background:#f1f3f5;color:#6c757d;overflow:hidden}.seam-file-tile__visual--image{background:transparent}.seam-file-tile__thumb{width:100%;height:100%;object-fit:cover}.seam-file-tile__body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:2px}.seam-file-tile__name{font-size:13px;font-weight:500;color:#343a40;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.seam-file-tile__meta{font-size:11px;color:#6c757d}.seam-file-tile__remove{flex:0 0 auto;width:28px;height:28px;border:0;background:transparent;color:#6c757d;border-radius:4px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.seam-file-tile__remove seam-icon{display:inline-flex;align-items:center;justify-content:center;line-height:1}.seam-file-tile__remove seam-icon ::ng-deep .svg-inline--fa{display:block;vertical-align:middle;overflow:hidden}.seam-file-tile__remove:hover{background:#0000000f;color:#dc3545}.seam-file-tile--preview{position:relative;display:inline-block;border:1px solid #e9ecef;border-radius:6px;background:#fff;padding:8px}.seam-file-tile--preview .seam-file-tile__preview-media{display:block;width:100%;max-width:180px}.seam-file-tile--preview .seam-file-tile__preview-media .seam-file-tile__thumb{width:100%;height:auto;display:block;border-radius:3px}.seam-file-tile--preview .seam-file-tile__preview-media .seam-file-tile__visual{width:48px;height:48px;margin:0 auto}.seam-file-tile--preview .seam-file-tile__preview-name{font-size:11px;color:#6c757d;margin-top:6px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.seam-file-tile--preview .seam-file-tile__remove--overlay{position:absolute;top:4px;right:4px;width:22px;height:22px;border-radius:50%;background:#212529b3;color:#fff}.seam-file-tile--preview .seam-file-tile__remove--overlay:hover{background:#dc3545;color:#fff}.seam-file-tile--clickable .seam-file-tile__clickable-body{cursor:pointer;outline:none}.seam-file-tile--clickable .seam-file-tile__clickable-body:focus-visible{outline:2px solid #357ebd;outline-offset:2px;border-radius:4px}.seam-file-tile--clickable.seam-file-tile--row:hover{background:#00000008}.seam-file-tile--clickable.seam-file-tile--preview:hover{box-shadow:0 0 0 1px #357ebd inset}.seam-file-tile--row.seam-file-tile--clickable .seam-file-tile__clickable-body{display:flex;align-items:center;gap:12px;flex:1 1 auto;min-width:0}.seam-file-tile--preview.seam-file-tile--clickable .seam-file-tile__clickable-body{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: TheSeamIconModule }, { kind: "component", type: i1.IconComponent, selector: "seam-icon", inputs: ["grayscaleOnDisable", "disabled", "iconClass", "icon", "size", "showDefaultOnError", "defaultIcon", "iconType"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
411
+ }
412
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamFileTileComponent, decorators: [{
413
+ type: Component,
414
+ args: [{ selector: 'seam-file-tile', imports: [TheSeamIconModule, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"seam-file-tile\"\n [class.seam-file-tile--row]=\"variant() === 'row'\"\n [class.seam-file-tile--preview]=\"variant() === 'preview'\"\n [class.seam-file-tile--clickable]=\"_isInteractive()\"\n>\n @if (variant() === 'row') {\n @if (_isInteractive()) {\n <div\n class=\"seam-file-tile__clickable-body\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"_onBodyClick()\"\n (keydown)=\"_onBodyKey($event)\"\n >\n <ng-container *ngTemplateOutlet=\"rowContent\"></ng-container>\n </div>\n } @else {\n <ng-container *ngTemplateOutlet=\"rowContent\"></ng-container>\n }\n\n @if (_showRemoveBtn()) {\n <button\n type=\"button\"\n class=\"seam-file-tile__remove\"\n (click)=\"_onRemove($event)\"\n title=\"Remove file\"\n >\n <seam-icon [icon]=\"_faTimes\"></seam-icon>\n </button>\n }\n } @else {\n @if (_showRemoveBtn()) {\n <button\n type=\"button\"\n class=\"seam-file-tile__remove seam-file-tile__remove--overlay\"\n (click)=\"_onRemove($event)\"\n title=\"Remove file\"\n >\n <seam-icon [icon]=\"_faTimes\"></seam-icon>\n </button>\n }\n @if (_isInteractive()) {\n <div\n class=\"seam-file-tile__clickable-body\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"_onBodyClick()\"\n (keydown)=\"_onBodyKey($event)\"\n >\n <ng-container *ngTemplateOutlet=\"previewContent\"></ng-container>\n </div>\n } @else {\n <ng-container *ngTemplateOutlet=\"previewContent\"></ng-container>\n }\n }\n</div>\n\n<ng-template #rowContent>\n <span\n class=\"seam-file-tile__visual\"\n [class.seam-file-tile__visual--image]=\"!!_thumbUrl()\"\n >\n @if (_thumbUrl(); as url) {\n <img class=\"seam-file-tile__thumb\" [src]=\"url\" [alt]=\"item().name\" />\n } @else {\n <seam-icon [icon]=\"_mimeIcon()\"></seam-icon>\n }\n </span>\n <div class=\"seam-file-tile__body\">\n <div class=\"seam-file-tile__name\" [attr.title]=\"item().name\">\n {{ item().name }}\n </div>\n @if (showMeta() && _metaLine(); as meta) {\n <div class=\"seam-file-tile__meta\">{{ meta }}</div>\n }\n </div>\n</ng-template>\n\n<ng-template #previewContent>\n <span class=\"seam-file-tile__preview-media\">\n @if (_thumbUrl(); as url) {\n <img class=\"seam-file-tile__thumb\" [src]=\"url\" [alt]=\"item().name\" />\n } @else {\n <span class=\"seam-file-tile__visual\">\n <seam-icon [icon]=\"_mimeIcon()\"></seam-icon>\n </span>\n }\n </span>\n @if (showName()) {\n <div class=\"seam-file-tile__preview-name\" [attr.title]=\"item().name\">\n {{ item().name }}\n </div>\n }\n</ng-template>\n", styles: [":host{display:block}.seam-file-tile{box-sizing:border-box;font-family:inherit}.seam-file-tile--row{display:flex;align-items:center;gap:12px;padding:10px 12px;background:#fff;border:1px solid #e9ecef;border-radius:6px}.seam-file-tile__visual{flex:0 0 auto;width:36px;height:36px;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;background:#f1f3f5;color:#6c757d;overflow:hidden}.seam-file-tile__visual--image{background:transparent}.seam-file-tile__thumb{width:100%;height:100%;object-fit:cover}.seam-file-tile__body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:2px}.seam-file-tile__name{font-size:13px;font-weight:500;color:#343a40;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.seam-file-tile__meta{font-size:11px;color:#6c757d}.seam-file-tile__remove{flex:0 0 auto;width:28px;height:28px;border:0;background:transparent;color:#6c757d;border-radius:4px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.seam-file-tile__remove seam-icon{display:inline-flex;align-items:center;justify-content:center;line-height:1}.seam-file-tile__remove seam-icon ::ng-deep .svg-inline--fa{display:block;vertical-align:middle;overflow:hidden}.seam-file-tile__remove:hover{background:#0000000f;color:#dc3545}.seam-file-tile--preview{position:relative;display:inline-block;border:1px solid #e9ecef;border-radius:6px;background:#fff;padding:8px}.seam-file-tile--preview .seam-file-tile__preview-media{display:block;width:100%;max-width:180px}.seam-file-tile--preview .seam-file-tile__preview-media .seam-file-tile__thumb{width:100%;height:auto;display:block;border-radius:3px}.seam-file-tile--preview .seam-file-tile__preview-media .seam-file-tile__visual{width:48px;height:48px;margin:0 auto}.seam-file-tile--preview .seam-file-tile__preview-name{font-size:11px;color:#6c757d;margin-top:6px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.seam-file-tile--preview .seam-file-tile__remove--overlay{position:absolute;top:4px;right:4px;width:22px;height:22px;border-radius:50%;background:#212529b3;color:#fff}.seam-file-tile--preview .seam-file-tile__remove--overlay:hover{background:#dc3545;color:#fff}.seam-file-tile--clickable .seam-file-tile__clickable-body{cursor:pointer;outline:none}.seam-file-tile--clickable .seam-file-tile__clickable-body:focus-visible{outline:2px solid #357ebd;outline-offset:2px;border-radius:4px}.seam-file-tile--clickable.seam-file-tile--row:hover{background:#00000008}.seam-file-tile--clickable.seam-file-tile--preview:hover{box-shadow:0 0 0 1px #357ebd inset}.seam-file-tile--row.seam-file-tile--clickable .seam-file-tile__clickable-body{display:flex;align-items:center;gap:12px;flex:1 1 auto;min-width:0}.seam-file-tile--preview.seam-file-tile--clickable .seam-file-tile__clickable-body{display:block;width:100%}\n"] }]
415
+ }], ctorParameters: () => [], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], showName: [{ type: i0.Input, args: [{ isSignal: true, alias: "showName", required: false }] }], showMeta: [{ type: i0.Input, args: [{ isSignal: true, alias: "showMeta", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], remove: [{ type: i0.Output, args: ["remove"] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
416
+ function _formatMeta(item) {
417
+ const parts = [];
418
+ if (item.size !== undefined)
419
+ parts.push(_formatBytes(item.size));
420
+ if (item.type)
421
+ parts.push(item.type);
422
+ return parts.join(' · ');
423
+ }
424
+ function _formatBytes(bytes) {
425
+ if (bytes < 1024)
426
+ return `${bytes} B`;
427
+ if (bytes < 1024 * 1024)
428
+ return `${(bytes / 1024).toFixed(1)} KB`;
429
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
430
+ }
431
+ function _isImageMime(type) {
432
+ return !!type && type.toLowerCase().startsWith('image/');
433
+ }
434
+ const IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i;
435
+ function _looksLikeImage(item) {
436
+ if (_isImageMime(item.type))
437
+ return true;
438
+ if (item.source.kind === 'url' && IMAGE_EXT.test(item.source.url))
439
+ return true;
440
+ return false;
441
+ }
442
+
443
+ class TheSeamFileFieldComponent {
444
+ multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
445
+ accept = input('', ...(ngDevMode ? [{ debugName: "accept" }] : []));
446
+ maxSize = input(null, ...(ngDevMode ? [{ debugName: "maxSize" }] : []));
447
+ maxFiles = input(null, ...(ngDevMode ? [{ debugName: "maxFiles" }] : []));
448
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
449
+ previewMode = input(false, ...(ngDevMode ? [{ debugName: "previewMode", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
450
+ showTileName = input(true, ...(ngDevMode ? [{ debugName: "showTileName", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
451
+ promptText = input('Choose a file', ...(ngDevMode ? [{ debugName: "promptText" }] : []));
452
+ promptSuffix = input('or drag it here', ...(ngDevMode ? [{ debugName: "promptSuffix" }] : []));
453
+ replaceText = input('choose a different file', ...(ngDevMode ? [{ debugName: "replaceText" }] : []));
454
+ hideErrors = input(false, ...(ngDevMode ? [{ debugName: "hideErrors", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
455
+ rejected = output();
456
+ _items = signal([], ...(ngDevMode ? [{ debugName: "_items" }] : []));
457
+ _cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_cvaDisabled" }] : []));
458
+ _effectiveDisabled = computed(() => this.disabled() || this._cvaDisabled(), ...(ngDevMode ? [{ debugName: "_effectiveDisabled" }] : []));
459
+ _hasFile = computed(() => !this.multiple() && this._items().length > 0, ...(ngDevMode ? [{ debugName: "_hasFile" }] : []));
460
+ _remainingMaxFiles = computed(() => {
461
+ const max = this.maxFiles();
462
+ if (!this.multiple()) {
463
+ // Single-mode: cap at 1 always. If explicit lower, honor.
464
+ return max !== null ? Math.min(max, 1) : 1;
465
+ }
466
+ if (max === null)
467
+ return null;
468
+ return Math.max(0, max - this._items().length);
469
+ }, ...(ngDevMode ? [{ debugName: "_remainingMaxFiles" }] : []));
470
+ _tileVariant = computed(() => this.previewMode() ? 'preview' : 'row', ...(ngDevMode ? [{ debugName: "_tileVariant" }] : []));
471
+ _onChange = () => undefined;
472
+ _onTouched = () => undefined;
473
+ writeValue(value) {
474
+ this._items.set(value ?? []);
475
+ }
476
+ registerOnChange(fn) {
477
+ this._onChange = fn;
478
+ }
479
+ registerOnTouched(fn) {
480
+ this._onTouched = fn;
481
+ }
482
+ setDisabledState(isDisabled) {
483
+ this._cvaDisabled.set(isDisabled);
484
+ }
485
+ _onFilesAdded(files) {
486
+ if (files.length === 0)
487
+ return;
488
+ const added = files.map((f) => seamFileItemFromFile(f));
489
+ if (!this.multiple()) {
490
+ this._items.set(added.slice(0, 1));
491
+ }
492
+ else {
493
+ const remaining = this._remainingMaxFiles();
494
+ const toAdd = remaining !== null ? added.slice(0, remaining) : added;
495
+ if (toAdd.length === 0)
496
+ return;
497
+ this._items.update((prev) => [...prev, ...toAdd]);
498
+ }
499
+ this._emit();
500
+ }
501
+ _onRejected(rejections) {
502
+ this.rejected.emit(rejections);
503
+ }
504
+ _onTileRemove(item) {
505
+ this._items.update((prev) => prev.filter((i) => i !== item));
506
+ this._emit();
507
+ }
508
+ _emit() {
509
+ this._onChange(this._items());
510
+ this._onTouched();
511
+ }
512
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamFileFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
513
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TheSeamFileFieldComponent, isStandalone: true, selector: "seam-file-field", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, previewMode: { classPropertyName: "previewMode", publicName: "previewMode", isSignal: true, isRequired: false, transformFunction: null }, showTileName: { classPropertyName: "showTileName", publicName: "showTileName", isSignal: true, isRequired: false, transformFunction: null }, promptText: { classPropertyName: "promptText", publicName: "promptText", isSignal: true, isRequired: false, transformFunction: null }, promptSuffix: { classPropertyName: "promptSuffix", publicName: "promptSuffix", isSignal: true, isRequired: false, transformFunction: null }, replaceText: { classPropertyName: "replaceText", publicName: "replaceText", isSignal: true, isRequired: false, transformFunction: null }, hideErrors: { classPropertyName: "hideErrors", publicName: "hideErrors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rejected: "rejected" }, providers: [
514
+ {
515
+ provide: NG_VALUE_ACCESSOR,
516
+ useExisting: forwardRef(() => TheSeamFileFieldComponent),
517
+ multi: true,
518
+ },
519
+ ], ngImport: i0, template: "@if (_hasFile()) {\n <seam-file-tile\n [item]=\"_items()[0]\"\n [variant]=\"_tileVariant()\"\n [showName]=\"showTileName()\"\n [disabled]=\"_effectiveDisabled()\"\n (remove)=\"_onTileRemove($event)\"\n ></seam-file-tile>\n <button\n type=\"button\"\n class=\"seam-file-field__replace\"\n [disabled]=\"_effectiveDisabled()\"\n (click)=\"inputComponentWhenFilled._openPicker()\"\n >\n or <strong>{{ replaceText() }}</strong>\n </button>\n <!-- Hidden mounted input so the replace button has a picker to delegate to. -->\n <seam-file-input\n #inputComponentWhenFilled\n hidden\n [multiple]=\"multiple()\"\n [accept]=\"accept()\"\n [maxSize]=\"maxSize()\"\n [maxFiles]=\"_remainingMaxFiles()\"\n [disabled]=\"_effectiveDisabled()\"\n [hideErrors]=\"true\"\n (filesAdded)=\"_onFilesAdded($event)\"\n (rejected)=\"_onRejected($event)\"\n ></seam-file-input>\n} @else {\n <seam-file-input\n #inputComponent\n [multiple]=\"multiple()\"\n [accept]=\"accept()\"\n [maxSize]=\"maxSize()\"\n [maxFiles]=\"_remainingMaxFiles()\"\n [disabled]=\"_effectiveDisabled()\"\n [hideErrors]=\"hideErrors()\"\n [promptText]=\"promptText()\"\n [promptSuffix]=\"promptSuffix()\"\n (filesAdded)=\"_onFilesAdded($event)\"\n (rejected)=\"_onRejected($event)\"\n ></seam-file-input>\n\n @if (multiple() && _items().length > 0) {\n <div\n class=\"seam-file-field__tiles\"\n [class.seam-file-field__tiles--preview]=\"previewMode()\"\n >\n @for (item of _items(); track item.id ?? item.name) {\n <seam-file-tile\n [item]=\"item\"\n [variant]=\"_tileVariant()\"\n [showName]=\"showTileName()\"\n [disabled]=\"_effectiveDisabled()\"\n (remove)=\"_onTileRemove($event)\"\n ></seam-file-tile>\n }\n </div>\n }\n}\n", styles: [":host{display:block}.seam-file-field__replace{display:block;margin-top:8px;padding:8px 12px;width:100%;background:#f8f9fa;border:1px dashed #ced4da;border-radius:6px;font-size:12px;color:#6c757d;text-align:center;cursor:pointer}.seam-file-field__replace strong{color:#357ebd;font-weight:600}.seam-file-field__replace:disabled{opacity:.6;cursor:not-allowed}.seam-file-field__replace:focus-visible{outline:2px solid #357ebd;outline-offset:2px}seam-file-input[hidden]{display:none}.seam-file-field__tiles{display:flex;flex-direction:column;gap:6px;margin-top:8px}.seam-file-field__tiles--preview{flex-direction:row;flex-wrap:wrap;gap:8px}\n"], dependencies: [{ kind: "component", type: TheSeamFileInputComponent, selector: "seam-file-input", inputs: ["multiple", "accept", "maxSize", "maxFiles", "disabled", "hideErrors", "promptText", "promptSuffix"], outputs: ["filesAdded", "rejected"] }, { kind: "component", type: TheSeamFileTileComponent, selector: "seam-file-tile", inputs: ["item", "variant", "showName", "showMeta", "removable", "disabled"], outputs: ["remove", "itemClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
520
+ }
521
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamFileFieldComponent, decorators: [{
522
+ type: Component,
523
+ args: [{ selector: 'seam-file-field', imports: [TheSeamFileInputComponent, TheSeamFileTileComponent], providers: [
524
+ {
525
+ provide: NG_VALUE_ACCESSOR,
526
+ useExisting: forwardRef(() => TheSeamFileFieldComponent),
527
+ multi: true,
528
+ },
529
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (_hasFile()) {\n <seam-file-tile\n [item]=\"_items()[0]\"\n [variant]=\"_tileVariant()\"\n [showName]=\"showTileName()\"\n [disabled]=\"_effectiveDisabled()\"\n (remove)=\"_onTileRemove($event)\"\n ></seam-file-tile>\n <button\n type=\"button\"\n class=\"seam-file-field__replace\"\n [disabled]=\"_effectiveDisabled()\"\n (click)=\"inputComponentWhenFilled._openPicker()\"\n >\n or <strong>{{ replaceText() }}</strong>\n </button>\n <!-- Hidden mounted input so the replace button has a picker to delegate to. -->\n <seam-file-input\n #inputComponentWhenFilled\n hidden\n [multiple]=\"multiple()\"\n [accept]=\"accept()\"\n [maxSize]=\"maxSize()\"\n [maxFiles]=\"_remainingMaxFiles()\"\n [disabled]=\"_effectiveDisabled()\"\n [hideErrors]=\"true\"\n (filesAdded)=\"_onFilesAdded($event)\"\n (rejected)=\"_onRejected($event)\"\n ></seam-file-input>\n} @else {\n <seam-file-input\n #inputComponent\n [multiple]=\"multiple()\"\n [accept]=\"accept()\"\n [maxSize]=\"maxSize()\"\n [maxFiles]=\"_remainingMaxFiles()\"\n [disabled]=\"_effectiveDisabled()\"\n [hideErrors]=\"hideErrors()\"\n [promptText]=\"promptText()\"\n [promptSuffix]=\"promptSuffix()\"\n (filesAdded)=\"_onFilesAdded($event)\"\n (rejected)=\"_onRejected($event)\"\n ></seam-file-input>\n\n @if (multiple() && _items().length > 0) {\n <div\n class=\"seam-file-field__tiles\"\n [class.seam-file-field__tiles--preview]=\"previewMode()\"\n >\n @for (item of _items(); track item.id ?? item.name) {\n <seam-file-tile\n [item]=\"item\"\n [variant]=\"_tileVariant()\"\n [showName]=\"showTileName()\"\n [disabled]=\"_effectiveDisabled()\"\n (remove)=\"_onTileRemove($event)\"\n ></seam-file-tile>\n }\n </div>\n }\n}\n", styles: [":host{display:block}.seam-file-field__replace{display:block;margin-top:8px;padding:8px 12px;width:100%;background:#f8f9fa;border:1px dashed #ced4da;border-radius:6px;font-size:12px;color:#6c757d;text-align:center;cursor:pointer}.seam-file-field__replace strong{color:#357ebd;font-weight:600}.seam-file-field__replace:disabled{opacity:.6;cursor:not-allowed}.seam-file-field__replace:focus-visible{outline:2px solid #357ebd;outline-offset:2px}seam-file-input[hidden]{display:none}.seam-file-field__tiles{display:flex;flex-direction:column;gap:6px;margin-top:8px}.seam-file-field__tiles--preview{flex-direction:row;flex-wrap:wrap;gap:8px}\n"] }]
530
+ }], propDecorators: { multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], previewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewMode", required: false }] }], showTileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTileName", required: false }] }], promptText: [{ type: i0.Input, args: [{ isSignal: true, alias: "promptText", required: false }] }], promptSuffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "promptSuffix", required: false }] }], replaceText: [{ type: i0.Input, args: [{ isSignal: true, alias: "replaceText", required: false }] }], hideErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideErrors", required: false }] }], rejected: [{ type: i0.Output, args: ["rejected"] }] } });
531
+
532
+ /**
533
+ * Generated bundle index. Do not edit.
534
+ */
535
+
536
+ export { TheSeamFileDropZoneDirective, TheSeamFileFieldComponent, TheSeamFileInputComponent, TheSeamFileTileComponent, iconForMime, seamFileItemFromFile, seamFileItemFromUrl, seamFilesFromItems };
537
+ //# sourceMappingURL=theseam-ui-common-file-input.mjs.map