@cdevhub/ngx-tw 0.2.1 → 0.3.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 (139) hide show
  1. package/README.md +264 -49
  2. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +11 -7
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
  4. package/fesm2022/cdevhub-ngx-tw-alert.mjs +43 -29
  5. package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -1
  6. package/fesm2022/cdevhub-ngx-tw-aspect-ratio.mjs +103 -0
  7. package/fesm2022/cdevhub-ngx-tw-aspect-ratio.mjs.map +1 -0
  8. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +56 -31
  9. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
  10. package/fesm2022/cdevhub-ngx-tw-badge.mjs +61 -35
  11. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
  12. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +154 -93
  13. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
  14. package/fesm2022/cdevhub-ngx-tw-button.mjs +24 -15
  15. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
  16. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +3 -3
  17. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
  18. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +408 -215
  19. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
  20. package/fesm2022/cdevhub-ngx-tw-card.mjs +33 -24
  21. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
  22. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +144 -79
  23. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
  24. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +99 -47
  25. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
  26. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +34 -20
  27. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
  28. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +46 -29
  29. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
  30. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +195 -110
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
  32. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +140 -73
  33. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
  34. package/fesm2022/cdevhub-ngx-tw-core.mjs +16 -11
  35. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
  36. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +393 -149
  37. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
  38. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +305 -150
  39. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
  40. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +56 -43
  41. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  42. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +51 -33
  43. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  44. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +905 -0
  45. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -0
  46. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +49 -26
  47. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  48. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +133 -74
  49. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  50. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +2 -2
  51. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-icon.mjs +28 -17
  53. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  54. package/fesm2022/cdevhub-ngx-tw-input.mjs +37 -21
  55. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  56. package/fesm2022/cdevhub-ngx-tw-item.mjs +33 -24
  57. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-menu.mjs +63 -48
  59. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +622 -0
  61. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -0
  62. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +137 -71
  63. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-popover.mjs +101 -46
  65. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +57 -30
  67. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  68. package/fesm2022/cdevhub-ngx-tw-radio.mjs +108 -57
  69. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  70. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +46 -26
  71. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-select.mjs +217 -122
  73. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-separator.mjs +21 -12
  75. package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +58 -44
  77. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +25 -14
  79. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-slider.mjs +155 -79
  81. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-sort.mjs +44 -25
  83. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  84. package/fesm2022/cdevhub-ngx-tw-spinner.mjs +29 -16
  85. package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -1
  86. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -42
  87. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  88. package/fesm2022/cdevhub-ngx-tw-stat.mjs +107 -64
  89. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  90. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +113 -47
  91. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  92. package/fesm2022/cdevhub-ngx-tw-switch.mjs +61 -32
  93. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +88 -42
  95. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-table.mjs +266 -118
  97. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +101 -51
  99. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +798 -0
  101. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -0
  102. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +11 -7
  103. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-theme.mjs +22 -14
  105. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +243 -95
  107. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +149 -82
  109. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-toast.mjs +89 -55
  111. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  112. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +50 -28
  113. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  114. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +1129 -0
  115. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -0
  116. package/fesm2022/cdevhub-ngx-tw-tree.mjs +554 -0
  117. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -0
  118. package/fesm2022/cdevhub-ngx-tw.mjs +7 -0
  119. package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -1
  120. package/package.json +45 -20
  121. package/theme/index.css +10 -2
  122. package/types/cdevhub-ngx-tw-aspect-ratio.d.ts +40 -0
  123. package/types/cdevhub-ngx-tw-calendar.d.ts +39 -8
  124. package/types/cdevhub-ngx-tw-command-palette.d.ts +7 -0
  125. package/types/cdevhub-ngx-tw-date-picker.d.ts +36 -10
  126. package/types/cdevhub-ngx-tw-file-upload.d.ts +250 -0
  127. package/types/cdevhub-ngx-tw-form-field.d.ts +22 -3
  128. package/types/cdevhub-ngx-tw-icon-lucide.d.ts +21 -3
  129. package/types/cdevhub-ngx-tw-number-input.d.ts +130 -0
  130. package/types/cdevhub-ngx-tw-popover.d.ts +8 -0
  131. package/types/cdevhub-ngx-tw-stepper.d.ts +30 -2
  132. package/types/cdevhub-ngx-tw-tab-nav.d.ts +8 -0
  133. package/types/cdevhub-ngx-tw-table.d.ts +30 -1
  134. package/types/cdevhub-ngx-tw-tabs.d.ts +8 -0
  135. package/types/cdevhub-ngx-tw-tags-input.d.ts +217 -0
  136. package/types/cdevhub-ngx-tw-time-picker.d.ts +31 -6
  137. package/types/cdevhub-ngx-tw-transfer.d.ts +330 -0
  138. package/types/cdevhub-ngx-tw-tree.d.ts +189 -0
  139. package/types/cdevhub-ngx-tw.d.ts +7 -0
@@ -0,0 +1,905 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, TemplateRef, Directive, input, booleanAttribute, output, ElementRef, DestroyRef, signal, contentChild, viewChild, computed, afterNextRender, isDevMode, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { NgTemplateOutlet } from '@angular/common';
4
+ import { NgControl, NgForm, FormGroupDirective, Validators } from '@angular/forms';
5
+ import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
6
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
+ import { isObservable, merge } from 'rxjs';
8
+ import { tv } from 'tailwind-variants';
9
+ import { TW_ERROR_STATE_MATCHER } from '@cdevhub/ngx-tw/core';
10
+ import { FormFieldControl, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
11
+ import { ButtonDirective } from '@cdevhub/ngx-tw/button';
12
+ import { IconComponent } from '@cdevhub/ngx-tw/icon';
13
+ import { ProgressBarComponent } from '@cdevhub/ngx-tw/progress-bar';
14
+
15
+ /**
16
+ * `tw-file-upload` — selects one or more files via button / click / drag-and-drop.
17
+ *
18
+ * Surfaces the selected files as a `File[]` value through `ControlValueAccessor`
19
+ * so the component works identically with template-driven `ngModel`, reactive
20
+ * `[formControl]`, and signal-based `[formField]` strategies. Integrates with
21
+ * `<tw-form-field>` for label / hint / error chrome.
22
+ *
23
+ * Built-in validation (`accept`, `maxSize`, `maxFiles`) rejects files at the
24
+ * UI layer and surfaces them via the `fileRejected` output and `LiveAnnouncer`.
25
+ * These rejections do NOT flow through `NgControl.errors`, so they cannot be
26
+ * matched with `[twError match="size"]`. Use Angular `Validator`s on the bound
27
+ * `NgControl` for error-key-driven UI (cross-field or async validation).
28
+ *
29
+ * The component does NOT issue HTTP requests. Consumers own the upload pipeline
30
+ * and push progress / status back into the component via `setItemProgress` and
31
+ * `setItemStatus`.
32
+ */
33
+ // ── Structural slot directive ────────────────────────────────────
34
+ /**
35
+ * Structural directive that captures a template used to render each item row.
36
+ * Apply with `*twFileUploadItem="let item"` to override the default row layout.
37
+ *
38
+ * The template context exposes the current `FileUploadItem` as both `$implicit`
39
+ * and `item`, so both `let-item` and `let-item="item"` work.
40
+ */
41
+ class FileUploadItemDirective {
42
+ /** @internal Reference to the captured template — read by the host component. */
43
+ templateRef = inject(TemplateRef);
44
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: FileUploadItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
45
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: FileUploadItemDirective, isStandalone: true, selector: "ng-template[twFileUploadItem]", ngImport: i0 });
46
+ }
47
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: FileUploadItemDirective, decorators: [{
48
+ type: Directive,
49
+ args: [{ selector: 'ng-template[twFileUploadItem]' }]
50
+ }] });
51
+ // ── tv() config ──────────────────────────────────────────────────
52
+ const fileUploadVariants = tv({
53
+ slots: {
54
+ // `relative` scopes the hidden `<input type="file" class="sr-only">` —
55
+ // its `position: absolute` + `margin: -1px` would otherwise resolve
56
+ // against the nearest positioned ancestor (often `<body>`), extending
57
+ // the document and producing runaway horizontal/vertical scroll.
58
+ root: 'relative flex flex-col gap-3 w-full',
59
+ // 2px dashed border on the outline variant is the dropzone's defining
60
+ // affordance — codified deviation from CLAUDE.md's "2px = active state
61
+ // indicator only" rule. The empty dropzone IS an awaiting-input affordance.
62
+ dropzone: 'relative rounded-lg transition-[border-color,background-color,transform] duration-200 motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 cursor-pointer',
63
+ dropzoneInner: 'flex flex-col items-center justify-center text-center gap-3',
64
+ illustration: 'text-fg-subtle shrink-0',
65
+ headline: 'font-semibold text-fg empty:hidden',
66
+ description: 'text-fg-muted empty:hidden',
67
+ list: 'flex flex-col gap-1 m-0 p-0 list-none empty:hidden',
68
+ item: 'flex items-start gap-3 px-3 py-2 rounded-md hover:bg-surface-muted transition-colors duration-200 motion-reduce:transition-none',
69
+ itemIcon: 'shrink-0 text-fg-muted mt-0.5',
70
+ itemBody: 'flex-1 min-w-0 flex flex-col gap-1',
71
+ itemName: 'text-sm font-medium text-fg truncate',
72
+ itemMeta: 'text-xs text-fg-muted',
73
+ itemProgress: 'mt-1',
74
+ itemActions: 'flex items-center gap-1 shrink-0',
75
+ },
76
+ variants: {
77
+ variant: {
78
+ outline: { dropzone: 'border-2 border-dashed border-border bg-transparent' },
79
+ soft: { dropzone: 'border border-border bg-surface-muted' },
80
+ },
81
+ size: {
82
+ xs: { dropzone: 'p-2', headline: 'text-xs', description: 'text-2xs' },
83
+ sm: { dropzone: 'p-3', headline: 'text-sm', description: 'text-xs' },
84
+ md: { dropzone: 'p-4', headline: 'text-sm', description: 'text-xs' },
85
+ lg: { dropzone: 'p-6', headline: 'text-sm', description: 'text-xs' },
86
+ xl: { dropzone: 'p-8', headline: 'text-sm', description: 'text-xs' },
87
+ },
88
+ state: {
89
+ idle: {},
90
+ // 1% scale-up — below Tailwind's smallest `scale-95` step but visually
91
+ // correct for a subtle drag-over lift; the alternative is no lift.
92
+ draggingValid: {
93
+ dropzone: 'border-primary-500 bg-primary-50 dark:bg-primary-900/20 scale-[1.01]',
94
+ },
95
+ draggingInvalid: {
96
+ dropzone: 'border-error-500 bg-error-50 dark:bg-error-900/20',
97
+ },
98
+ error: { dropzone: 'border-error-500' },
99
+ },
100
+ disabled: {
101
+ true: { dropzone: 'opacity-50 pointer-events-none cursor-not-allowed' },
102
+ false: {},
103
+ },
104
+ },
105
+ defaultVariants: {
106
+ variant: 'outline',
107
+ size: 'md',
108
+ state: 'idle',
109
+ disabled: false,
110
+ },
111
+ }, { twMerge: true });
112
+ // ── Module-level counters & helpers ─────────────────────────────
113
+ let nextHostId = 0;
114
+ let nextItemSeq = 0;
115
+ const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
116
+ function formatBytes(n) {
117
+ if (!Number.isFinite(n) || n < 0)
118
+ return '0 B';
119
+ if (n < 1024)
120
+ return `${n} B`;
121
+ let v = n;
122
+ let i = 0;
123
+ while (v >= 1024 && i < BYTE_UNITS.length - 1) {
124
+ v /= 1024;
125
+ i += 1;
126
+ }
127
+ return `${v < 10 ? v.toFixed(1) : Math.round(v)} ${BYTE_UNITS[i]}`;
128
+ }
129
+ function parseAcceptList(accept) {
130
+ if (!accept)
131
+ return [];
132
+ return accept
133
+ .split(',')
134
+ .map((s) => s.trim().toLowerCase())
135
+ .filter(Boolean);
136
+ }
137
+ function matchesAccept(file, patterns) {
138
+ if (patterns.length === 0)
139
+ return true;
140
+ const type = file.type.toLowerCase();
141
+ const name = file.name.toLowerCase();
142
+ for (const pattern of patterns) {
143
+ if (pattern.startsWith('.')) {
144
+ if (name.endsWith(pattern))
145
+ return true;
146
+ }
147
+ else if (pattern.endsWith('/*')) {
148
+ const prefix = pattern.slice(0, -1);
149
+ if (type.startsWith(prefix))
150
+ return true;
151
+ }
152
+ else if (type === pattern) {
153
+ return true;
154
+ }
155
+ }
156
+ return false;
157
+ }
158
+ function iconNameFor(file) {
159
+ const type = file.type.toLowerCase();
160
+ if (type.startsWith('image/'))
161
+ return 'image';
162
+ if (type === 'application/pdf')
163
+ return 'file-text';
164
+ if (type.startsWith('text/'))
165
+ return 'file-text';
166
+ return 'file';
167
+ }
168
+ function clampPercent(n) {
169
+ if (!Number.isFinite(n))
170
+ return 0;
171
+ if (n < 0)
172
+ return 0;
173
+ if (n > 100)
174
+ return 100;
175
+ return Math.round(n);
176
+ }
177
+ // ── FileUploadComponent ─────────────────────────────────────────
178
+ class FileUploadComponent extends FormFieldControl {
179
+ // ── Inputs ──
180
+ /** When true, the user can select more than one file. Mirrors to the hidden `<input multiple>` attribute. Defaults to `false`. */
181
+ multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
182
+ /** Comma-separated list of accepted file types using the native `<input accept>` syntax (e.g., `'image/*,.pdf'`). Forwarded to the hidden input and used to reject dropped files that do not match. Defaults to `undefined` (accept any). */
183
+ accept = input(undefined, /* @ts-ignore */
184
+ ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
185
+ /** Maximum size per file, in bytes. Files exceeding this are rejected with reason `'size'` and never reach `value`. Defaults to `undefined` (no limit). */
186
+ maxSize = input(undefined, /* @ts-ignore */
187
+ ...(ngDevMode ? [{ debugName: "maxSize" }] : /* istanbul ignore next */ []));
188
+ /** Maximum total file count. Drops that would push the count over this limit are rejected with reason `'count'`. Ignored when `multiple` is `false` (the limit is always 1). Defaults to `undefined` (no limit). */
189
+ maxFiles = input(undefined, /* @ts-ignore */
190
+ ...(ngDevMode ? [{ debugName: "maxFiles" }] : /* istanbul ignore next */ []));
191
+ /** Visual style of the drop zone. `'outline'` renders a dashed-bordered transparent region; `'soft'` renders a filled muted background. Defaults to `'outline'`. */
192
+ variant = input('outline', /* @ts-ignore */
193
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
194
+ /** Controls overall density: drop-zone padding, icon scale, headline typography. Defaults to `'md'`. */
195
+ size = input('md', /* @ts-ignore */
196
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
197
+ /** When true, blocks file selection (button, click, drop, keyboard) and applies muted styling. Reactive forms also propagate `disabled` via `setDisabledState`. Defaults to `false`. */
198
+ disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled',
199
+ transform: booleanAttribute });
200
+ /** Marks the control as required. Mirrors to `aria-required`. Also inferred from `Validators.required` on a bound `NgControl`. Defaults to `false`. */
201
+ requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required',
202
+ transform: booleanAttribute });
203
+ /** Headline text rendered inside the drop zone (e.g., `'Drag files here'`). Projected `[twFileUploadHeadline]` content takes precedence over this input. */
204
+ label = input(undefined, /* @ts-ignore */
205
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
206
+ /** Secondary text rendered under the label (e.g., `'PDF up to 10MB'`). Projected `[twFileUploadDescription]` content takes precedence. */
207
+ description = input(undefined, /* @ts-ignore */
208
+ ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
209
+ /** Label rendered inside the trigger button. Defaults to `'Choose files'`. Use `'Choose file'` when `multiple` is `false` (consumer responsibility). */
210
+ triggerLabel = input('Choose files', /* @ts-ignore */
211
+ ...(ngDevMode ? [{ debugName: "triggerLabel" }] : /* istanbul ignore next */ []));
212
+ /** Accessible name applied to the host when no visible label is projected. Mirrored to `aria-label`. */
213
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
214
+ /** ID of an external element that labels the control. Mirrored to `aria-labelledby`. */
215
+ ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
216
+ /** ID of an external element that describes the control. Form-field merges its hint/error ids alongside. */
217
+ ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
218
+ /** Optional `name` attribute on the hidden `<input type="file">` for native form submissions. */
219
+ name = input(undefined, /* @ts-ignore */
220
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
221
+ /** Id on the host element. Auto-generated as `tw-file-upload-N` when not provided. Used by the form-field's `<label for>` attribute. */
222
+ idInput = input(undefined, { ...(ngDevMode ? { debugName: "idInput" } : /* istanbul ignore next */ {}), alias: 'id' });
223
+ /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, the component uses the `TW_ERROR_STATE_MATCHER` token's value. */
224
+ errorStateMatcher = input(undefined, /* @ts-ignore */
225
+ ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
226
+ // ── Outputs ──
227
+ /** Fires once per add operation with the items that were accepted and appended. Does not fire for rejected files (see `fileRejected`). Does not fire on `writeValue`. */
228
+ filesAdded = output();
229
+ /** Fires when a file is removed via the remove button or programmatic `remove()`. Does not fire on `writeValue`. */
230
+ fileRemoved = output();
231
+ /** Fires for each file that failed validation during add. Payload includes a human-readable message suitable for assistive-tech announcement. */
232
+ fileRejected = output();
233
+ /** Fires when all files are cleared at once via programmatic `clear()`. Does not fire on `writeValue`, even when the new value is empty. */
234
+ cleared = output();
235
+ // ── Injections ──
236
+ elementRef = inject((ElementRef));
237
+ focusMonitor = inject(FocusMonitor);
238
+ liveAnnouncer = inject(LiveAnnouncer);
239
+ destroyRef = inject(DestroyRef);
240
+ ngControl = inject(NgControl, { optional: true, self: true });
241
+ parentForm = inject(NgForm, { optional: true });
242
+ parentFormGroup = inject(FormGroupDirective, { optional: true });
243
+ defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
244
+ // ── Internal mutable state ──
245
+ itemsSignal = signal([], /* @ts-ignore */
246
+ ...(ngDevMode ? [{ debugName: "itemsSignal" }] : /* istanbul ignore next */ []));
247
+ cvaDisabled = signal(false, /* @ts-ignore */
248
+ ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
249
+ isDraggingSignal = signal(false, /* @ts-ignore */
250
+ ...(ngDevMode ? [{ debugName: "isDraggingSignal" }] : /* istanbul ignore next */ []));
251
+ dragHasInvalidFilesSignal = signal(false, /* @ts-ignore */
252
+ ...(ngDevMode ? [{ debugName: "dragHasInvalidFilesSignal" }] : /* istanbul ignore next */ []));
253
+ _focused = signal(false, /* @ts-ignore */
254
+ ...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
255
+ _ngControlRev = signal(0, /* @ts-ignore */
256
+ ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
257
+ _formSubmitRev = signal(0, /* @ts-ignore */
258
+ ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
259
+ describedByIdsSignal = signal([], /* @ts-ignore */
260
+ ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
261
+ labelledByIdsSignal = signal([], /* @ts-ignore */
262
+ ...(ngDevMode ? [{ debugName: "labelledByIdsSignal" }] : /* istanbul ignore next */ []));
263
+ dragCounter = 0;
264
+ onChange = () => { };
265
+ onTouched = () => { };
266
+ // ── Template refs ──
267
+ /** @internal Captured custom-item template (from `*twFileUploadItem`). When absent, the default row template renders. */
268
+ itemSlot = contentChild(FileUploadItemDirective, /* @ts-ignore */
269
+ ...(ngDevMode ? [{ debugName: "itemSlot" }] : /* istanbul ignore next */ []));
270
+ /** @internal Dropzone wrapper — used to scope the form-field container-click guard. */
271
+ dropzoneRef = viewChild('dropzone', /* @ts-ignore */
272
+ ...(ngDevMode ? [{ debugName: "dropzoneRef" }] : /* istanbul ignore next */ []));
273
+ /** @internal The visible trigger button — the keyboard-operable control that receives focus on container click and after the last item is removed. */
274
+ triggerRef = viewChild('trigger', /* @ts-ignore */
275
+ ...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
276
+ /** @internal Hidden native input — used as the OS picker trigger. */
277
+ hiddenInputRef = viewChild('hiddenInput', /* @ts-ignore */
278
+ ...(ngDevMode ? [{ debugName: "hiddenInputRef" }] : /* istanbul ignore next */ []));
279
+ // ── Ids ──
280
+ uid = nextHostId++;
281
+ /** @internal Auto-generated host id used when `[id]` is not supplied. */
282
+ hostId = `tw-file-upload-${this.uid}`;
283
+ /** @internal */
284
+ headlineId = `${this.hostId}-headline`;
285
+ /** @internal */
286
+ descriptionId = `${this.hostId}-description`;
287
+ /** @internal */
288
+ hiddenInputId = `${this.hostId}-input`;
289
+ // ── Public readonly signals ──
290
+ /** Current items with their progress and status. Snapshot of internal state. */
291
+ items = this.itemsSignal.asReadonly();
292
+ /** True while a valid file drag is over the drop zone. */
293
+ isDragging = this.isDraggingSignal.asReadonly();
294
+ /** Convenience alias for `items().length`. Useful for counter hints. */
295
+ valueLength = computed(() => this.itemsSignal().length, /* @ts-ignore */
296
+ ...(ngDevMode ? [{ debugName: "valueLength" }] : /* istanbul ignore next */ []));
297
+ // ── FormFieldControl signals ──
298
+ /** @internal */
299
+ id = computed(() => this.idInput() ?? this.hostId, /* @ts-ignore */
300
+ ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
301
+ /** @internal */
302
+ value = computed(() => {
303
+ const list = this.itemsSignal();
304
+ return list.length ? list.map((i) => i.file) : null;
305
+ }, /* @ts-ignore */
306
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
307
+ /** @internal */
308
+ focused = this._focused.asReadonly();
309
+ /** @internal */
310
+ empty = computed(() => this.itemsSignal().length === 0, /* @ts-ignore */
311
+ ...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
312
+ /** @internal */
313
+ disabled = computed(() => {
314
+ this._ngControlRev();
315
+ return this.disabledInput() || this.cvaDisabled() || !!this.ngControl?.disabled;
316
+ }, /* @ts-ignore */
317
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
318
+ /** @internal */
319
+ required = computed(() => {
320
+ this._ngControlRev();
321
+ if (this.requiredInput())
322
+ return true;
323
+ const ctrl = this.ngControl?.control;
324
+ return !!ctrl?.hasValidator(Validators.required);
325
+ }, /* @ts-ignore */
326
+ ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
327
+ /** @internal */
328
+ errorState = computed(() => {
329
+ this._ngControlRev();
330
+ this._formSubmitRev();
331
+ this._focused();
332
+ const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
333
+ const form = this.parentFormGroup ??
334
+ this.parentForm;
335
+ return matcher.isErrorState(this.ngControl?.control ?? null, form);
336
+ }, /* @ts-ignore */
337
+ ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
338
+ /** @internal Validation errors map from the bound `NgControl`. */
339
+ errors = computed(() => {
340
+ this._ngControlRev();
341
+ return this.ngControl?.control?.errors ?? null;
342
+ }, /* @ts-ignore */
343
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
344
+ /** @internal */
345
+ controlType = 'file-upload';
346
+ /** @internal */
347
+ userAriaDescribedBy = computed(() => this.ariaDescribedby(), /* @ts-ignore */
348
+ ...(ngDevMode ? [{ debugName: "userAriaDescribedBy" }] : /* istanbul ignore next */ []));
349
+ /** @internal */
350
+ userAriaLabelledby = computed(() => this.ariaLabelledby(), /* @ts-ignore */
351
+ ...(ngDevMode ? [{ debugName: "userAriaLabelledby" }] : /* istanbul ignore next */ []));
352
+ // ── Effective ARIA ──
353
+ //
354
+ // When wrapped by `<tw-form-field>`, the form-field calls `setDescribedByIds`
355
+ // / `setLabelledByIds` with the FULLY MERGED id list (hint/error/label + the
356
+ // consumer's `aria-*by` ids). In that case the pushed ids alone are the
357
+ // source of truth. When standalone, fall back to the consumer-supplied
358
+ // `aria-*by` input and the internal description id.
359
+ /** @internal */
360
+ effectiveAriaLabelledby = computed(() => {
361
+ const pushed = this.labelledByIdsSignal();
362
+ if (pushed.length)
363
+ return pushed.join(' ');
364
+ return this.ariaLabelledby() || undefined;
365
+ }, /* @ts-ignore */
366
+ ...(ngDevMode ? [{ debugName: "effectiveAriaLabelledby" }] : /* istanbul ignore next */ []));
367
+ /** @internal */
368
+ effectiveAriaDescribedby = computed(() => {
369
+ const pushed = this.describedByIdsSignal();
370
+ if (pushed.length)
371
+ return pushed.join(' ');
372
+ const merged = [];
373
+ const external = this.ariaDescribedby();
374
+ if (external) {
375
+ for (const id of external.split(/\s+/).filter(Boolean))
376
+ merged.push(id);
377
+ }
378
+ if (this.description())
379
+ merged.push(this.descriptionId);
380
+ return merged.length ? merged.join(' ') : undefined;
381
+ }, /* @ts-ignore */
382
+ ...(ngDevMode ? [{ debugName: "effectiveAriaDescribedby" }] : /* istanbul ignore next */ []));
383
+ // ── Variant computeds ──
384
+ stateVariant = computed(() => {
385
+ if (this.disabled())
386
+ return 'idle';
387
+ if (this.isDraggingSignal()) {
388
+ return this.dragHasInvalidFilesSignal() ? 'draggingInvalid' : 'draggingValid';
389
+ }
390
+ if (this.errorState())
391
+ return 'error';
392
+ return 'idle';
393
+ }, /* @ts-ignore */
394
+ ...(ngDevMode ? [{ debugName: "stateVariant" }] : /* istanbul ignore next */ []));
395
+ variantResult = computed(() => fileUploadVariants({
396
+ variant: this.variant(),
397
+ size: this.size(),
398
+ state: this.stateVariant(),
399
+ disabled: this.disabled(),
400
+ }), /* @ts-ignore */
401
+ ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
402
+ /** @internal */
403
+ rootClasses = computed(() => this.variantResult().root(), /* @ts-ignore */
404
+ ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
405
+ /** @internal */
406
+ dropzoneClasses = computed(() => this.variantResult().dropzone(), /* @ts-ignore */
407
+ ...(ngDevMode ? [{ debugName: "dropzoneClasses" }] : /* istanbul ignore next */ []));
408
+ /** @internal */
409
+ dropzoneInnerClasses = computed(() => this.variantResult().dropzoneInner(), /* @ts-ignore */
410
+ ...(ngDevMode ? [{ debugName: "dropzoneInnerClasses" }] : /* istanbul ignore next */ []));
411
+ /** @internal */
412
+ illustrationClasses = computed(() => this.variantResult().illustration(), /* @ts-ignore */
413
+ ...(ngDevMode ? [{ debugName: "illustrationClasses" }] : /* istanbul ignore next */ []));
414
+ /** @internal */
415
+ headlineClasses = computed(() => this.variantResult().headline(), /* @ts-ignore */
416
+ ...(ngDevMode ? [{ debugName: "headlineClasses" }] : /* istanbul ignore next */ []));
417
+ /** @internal */
418
+ descriptionClasses = computed(() => this.variantResult().description(), /* @ts-ignore */
419
+ ...(ngDevMode ? [{ debugName: "descriptionClasses" }] : /* istanbul ignore next */ []));
420
+ /** @internal */
421
+ listClasses = computed(() => this.variantResult().list(), /* @ts-ignore */
422
+ ...(ngDevMode ? [{ debugName: "listClasses" }] : /* istanbul ignore next */ []));
423
+ /** @internal */
424
+ itemClasses = computed(() => this.variantResult().item(), /* @ts-ignore */
425
+ ...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
426
+ /** @internal */
427
+ itemIconClasses = computed(() => this.variantResult().itemIcon(), /* @ts-ignore */
428
+ ...(ngDevMode ? [{ debugName: "itemIconClasses" }] : /* istanbul ignore next */ []));
429
+ /** @internal */
430
+ itemBodyClasses = computed(() => this.variantResult().itemBody(), /* @ts-ignore */
431
+ ...(ngDevMode ? [{ debugName: "itemBodyClasses" }] : /* istanbul ignore next */ []));
432
+ /** @internal */
433
+ itemNameClasses = computed(() => this.variantResult().itemName(), /* @ts-ignore */
434
+ ...(ngDevMode ? [{ debugName: "itemNameClasses" }] : /* istanbul ignore next */ []));
435
+ /** @internal */
436
+ itemMetaClasses = computed(() => this.variantResult().itemMeta(), /* @ts-ignore */
437
+ ...(ngDevMode ? [{ debugName: "itemMetaClasses" }] : /* istanbul ignore next */ []));
438
+ /** @internal */
439
+ itemProgressClasses = computed(() => this.variantResult().itemProgress(), /* @ts-ignore */
440
+ ...(ngDevMode ? [{ debugName: "itemProgressClasses" }] : /* istanbul ignore next */ []));
441
+ /** @internal */
442
+ itemActionsClasses = computed(() => this.variantResult().itemActions(), /* @ts-ignore */
443
+ ...(ngDevMode ? [{ debugName: "itemActionsClasses" }] : /* istanbul ignore next */ []));
444
+ /** @internal Mapping component size → icon size for the illustration. */
445
+ iconSize = computed(() => {
446
+ const s = this.size();
447
+ if (s === 'xs')
448
+ return 'sm';
449
+ if (s === 'sm')
450
+ return 'md';
451
+ if (s === 'md')
452
+ return 'lg';
453
+ return 'xl';
454
+ }, /* @ts-ignore */
455
+ ...(ngDevMode ? [{ debugName: "iconSize" }] : /* istanbul ignore next */ []));
456
+ // ── Construction ──
457
+ constructor() {
458
+ super();
459
+ // Runtime CVA wiring — incompatible with a static NG_VALUE_ACCESSOR provider
460
+ // because `inject(NgControl, { self: true })` for matcher integration would
461
+ // create a circular DI on the same element.
462
+ if (this.ngControl) {
463
+ this.ngControl.valueAccessor = this;
464
+ }
465
+ afterNextRender(() => {
466
+ if (isDevMode() && !this.hasAccessibleNameHint()) {
467
+ console.warn('[tw-file-upload] The file-upload has no accessible name. Provide a `label`, project label content, or set `aria-label` / `aria-labelledby`.');
468
+ }
469
+ });
470
+ }
471
+ // ── Public API ──
472
+ /** Programmatically opens the OS file picker. No-op when disabled. */
473
+ open() {
474
+ if (this.disabled())
475
+ return;
476
+ const input = this.hiddenInputRef()?.nativeElement;
477
+ if (input)
478
+ input.click();
479
+ }
480
+ /** Removes the item with the given id. Emits `fileRemoved`. No-op when the id is unknown or the component is disabled. */
481
+ remove(id) {
482
+ if (this.disabled())
483
+ return;
484
+ const current = this.itemsSignal();
485
+ const idx = current.findIndex((i) => i.id === id);
486
+ if (idx === -1)
487
+ return;
488
+ const removed = current[idx];
489
+ const next = current.slice(0, idx).concat(current.slice(idx + 1));
490
+ this.itemsSignal.set(next);
491
+ this.emitChange();
492
+ this.fileRemoved.emit(removed);
493
+ this.liveAnnouncer.announce(`${removed.file.name} removed.`, 'polite');
494
+ // Move focus to the next remove button, or to the trigger button when none remain.
495
+ queueMicrotask(() => {
496
+ const host = this.elementRef.nativeElement;
497
+ const nextButtons = host.querySelectorAll('[data-tw-file-upload-remove]');
498
+ if (nextButtons.length === 0) {
499
+ this.triggerRef()?.nativeElement.focus();
500
+ }
501
+ else {
502
+ const fallback = nextButtons[Math.min(idx, nextButtons.length - 1)];
503
+ fallback?.focus();
504
+ }
505
+ });
506
+ }
507
+ /** Removes all items. Emits `cleared` and calls `onChange([])`. No-op when already empty or disabled. */
508
+ clear() {
509
+ if (this.disabled())
510
+ return;
511
+ if (this.itemsSignal().length === 0)
512
+ return;
513
+ this.itemsSignal.set([]);
514
+ this.emitChange();
515
+ this.cleared.emit();
516
+ this.liveAnnouncer.announce('All files removed.', 'polite');
517
+ }
518
+ /** Updates the per-item progress (0–100, clamped). Does not change status. No-op when the id is unknown. */
519
+ setItemProgress(id, percent) {
520
+ const clamped = clampPercent(percent);
521
+ let found = false;
522
+ const next = this.itemsSignal().map((item) => {
523
+ if (item.id !== id)
524
+ return item;
525
+ found = true;
526
+ if (item.progress === clamped)
527
+ return item;
528
+ return { ...item, progress: clamped };
529
+ });
530
+ if (found)
531
+ this.itemsSignal.set(next);
532
+ }
533
+ /** Updates the per-item status and (optionally) the error message. Announces status transitions via `LiveAnnouncer`. No-op when the id is unknown. */
534
+ setItemStatus(id, status, error) {
535
+ let target;
536
+ let changed = false;
537
+ const next = this.itemsSignal().map((item) => {
538
+ if (item.id !== id)
539
+ return item;
540
+ target = item;
541
+ const nextError = error !== undefined ? error : item.error;
542
+ if (item.status === status && item.error === nextError)
543
+ return item;
544
+ changed = true;
545
+ return { ...item, status, error: nextError };
546
+ });
547
+ if (!target)
548
+ return;
549
+ if (changed)
550
+ this.itemsSignal.set(next);
551
+ const name = target.file.name;
552
+ if (status === 'success') {
553
+ this.liveAnnouncer.announce(`${name} upload complete.`, 'polite');
554
+ }
555
+ else if (status === 'error') {
556
+ this.liveAnnouncer.announce(error ? `${name} upload failed: ${error}.` : `${name} upload failed.`, 'polite');
557
+ }
558
+ else if (status === 'uploading') {
559
+ this.liveAnnouncer.announce(`${name} uploading.`, 'polite');
560
+ }
561
+ }
562
+ // ── Drag-drop handlers ──
563
+ /** @internal */
564
+ onDragEnter(event) {
565
+ if (this.disabled())
566
+ return;
567
+ if (!this.isFileDrag(event))
568
+ return;
569
+ event.preventDefault();
570
+ this.dragCounter += 1;
571
+ if (this.dragCounter === 1) {
572
+ this.dragHasInvalidFilesSignal.set(this.computeDragInvalid(event));
573
+ this.isDraggingSignal.set(true);
574
+ }
575
+ }
576
+ /** @internal */
577
+ onDragOver(event) {
578
+ if (this.disabled())
579
+ return;
580
+ if (!this.isFileDrag(event))
581
+ return;
582
+ // Required to enable `drop` — browsers reject drops by default.
583
+ event.preventDefault();
584
+ }
585
+ /** @internal */
586
+ onDragLeave(event) {
587
+ if (this.disabled())
588
+ return;
589
+ if (!this.isFileDrag(event))
590
+ return;
591
+ if (this.dragCounter > 0)
592
+ this.dragCounter -= 1;
593
+ if (this.dragCounter === 0) {
594
+ this.isDraggingSignal.set(false);
595
+ this.dragHasInvalidFilesSignal.set(false);
596
+ }
597
+ }
598
+ /** @internal */
599
+ onDrop(event) {
600
+ if (this.disabled())
601
+ return;
602
+ event.preventDefault();
603
+ this.dragCounter = 0;
604
+ this.isDraggingSignal.set(false);
605
+ this.dragHasInvalidFilesSignal.set(false);
606
+ const files = event.dataTransfer?.files;
607
+ if (files && files.length) {
608
+ this.addFiles(files);
609
+ }
610
+ }
611
+ /** @internal */
612
+ onTriggerClick(event) {
613
+ event.stopPropagation();
614
+ this.open();
615
+ }
616
+ /** @internal Called by the hidden `<input type="file">` `change` event. */
617
+ onNativeChange(event) {
618
+ const target = event.target;
619
+ const files = target.files;
620
+ if (files && files.length)
621
+ this.addFiles(files);
622
+ // Reset so re-selecting the same file re-fires `change`.
623
+ target.value = '';
624
+ }
625
+ // ── Add files (validation + commit) ──
626
+ addFiles(files) {
627
+ if (this.disabled())
628
+ return;
629
+ const incoming = Array.from(files);
630
+ if (incoming.length === 0)
631
+ return;
632
+ // Single-file mode: replace existing entry with the latest file dropped/selected.
633
+ if (!this.multiple()) {
634
+ const file = incoming[0];
635
+ if (!file)
636
+ return;
637
+ const patterns = parseAcceptList(this.accept());
638
+ if (!matchesAccept(file, patterns)) {
639
+ this.reject(file, 'accept');
640
+ return;
641
+ }
642
+ const maxSize = this.maxSize();
643
+ if (maxSize !== undefined && file.size > maxSize) {
644
+ this.reject(file, 'size');
645
+ return;
646
+ }
647
+ const newItem = this.wrapFile(file);
648
+ this.itemsSignal.set([newItem]);
649
+ this.emitChange();
650
+ this.filesAdded.emit([newItem]);
651
+ this.liveAnnouncer.announce('1 file added.', 'polite');
652
+ return;
653
+ }
654
+ const patterns = parseAcceptList(this.accept());
655
+ const maxSize = this.maxSize();
656
+ const maxFiles = this.maxFiles();
657
+ const accepted = [];
658
+ const currentCount = this.itemsSignal().length;
659
+ for (const file of incoming) {
660
+ if (!matchesAccept(file, patterns)) {
661
+ this.reject(file, 'accept');
662
+ continue;
663
+ }
664
+ if (maxSize !== undefined && file.size > maxSize) {
665
+ this.reject(file, 'size');
666
+ continue;
667
+ }
668
+ if (maxFiles !== undefined && currentCount + accepted.length >= maxFiles) {
669
+ this.reject(file, 'count');
670
+ continue;
671
+ }
672
+ accepted.push(this.wrapFile(file));
673
+ }
674
+ if (accepted.length === 0)
675
+ return;
676
+ this.itemsSignal.set([...this.itemsSignal(), ...accepted]);
677
+ this.emitChange();
678
+ this.filesAdded.emit(accepted);
679
+ this.liveAnnouncer.announce(`${accepted.length} file${accepted.length === 1 ? '' : 's'} added.`, 'polite');
680
+ }
681
+ wrapFile(file) {
682
+ return {
683
+ id: `tw-file-upload-item-${nextItemSeq++}`,
684
+ file,
685
+ progress: 0,
686
+ status: 'pending',
687
+ };
688
+ }
689
+ reject(file, reason) {
690
+ const message = this.rejectionMessage(file, reason);
691
+ // Sequence per-file announcements so a batch of rejections is announced cleanly.
692
+ setTimeout(() => this.liveAnnouncer.announce(message, 'polite'));
693
+ this.fileRejected.emit({ file, reason, message });
694
+ }
695
+ rejectionMessage(file, reason) {
696
+ if (reason === 'accept')
697
+ return `${file.name} was rejected: file type not allowed.`;
698
+ if (reason === 'size') {
699
+ const max = this.maxSize();
700
+ const limit = max !== undefined ? formatBytes(max) : 'allowed size';
701
+ return `${file.name} was rejected: exceeds ${limit}.`;
702
+ }
703
+ return `${file.name} was rejected: maximum file count reached.`;
704
+ }
705
+ // ── Drag inspection ──
706
+ isFileDrag(event) {
707
+ const dt = event.dataTransfer;
708
+ if (!dt)
709
+ return false;
710
+ // `types` includes 'Files' when files are being dragged from the OS.
711
+ return Array.from(dt.types).includes('Files');
712
+ }
713
+ computeDragInvalid(event) {
714
+ const patterns = parseAcceptList(this.accept());
715
+ if (patterns.length === 0)
716
+ return false;
717
+ const items = event.dataTransfer?.items;
718
+ if (!items || items.length === 0)
719
+ return false;
720
+ // During drag, filenames are not exposed — only MIME types via `items[i].type`.
721
+ // Extension-only patterns (`.pdf`) can't be confirmed mid-drag, so we treat
722
+ // the drag as potentially valid if ANY item could match by MIME or if the
723
+ // accept list contains any extension-only pattern.
724
+ const hasExtensionPattern = patterns.some((p) => p.startsWith('.'));
725
+ if (hasExtensionPattern)
726
+ return false;
727
+ for (const item of Array.from(items)) {
728
+ if (item.kind !== 'file')
729
+ continue;
730
+ const type = item.type.toLowerCase();
731
+ for (const pattern of patterns) {
732
+ if (pattern.endsWith('/*')) {
733
+ if (type.startsWith(pattern.slice(0, -1)))
734
+ return false;
735
+ }
736
+ else if (type === pattern) {
737
+ return false;
738
+ }
739
+ }
740
+ }
741
+ return true;
742
+ }
743
+ // ── ControlValueAccessor ──
744
+ writeValue(value) {
745
+ if (value === null || value === undefined) {
746
+ if (this.itemsSignal().length === 0)
747
+ return;
748
+ this.itemsSignal.set([]);
749
+ return;
750
+ }
751
+ const list = Array.isArray(value) ? value : [value];
752
+ if (list.length === 0) {
753
+ if (this.itemsSignal().length === 0)
754
+ return;
755
+ this.itemsSignal.set([]);
756
+ return;
757
+ }
758
+ const current = this.itemsSignal();
759
+ if (current.length === list.length &&
760
+ current.every((item, idx) => item.file === list[idx])) {
761
+ return;
762
+ }
763
+ this.itemsSignal.set(list.map((file) => this.wrapFile(file)));
764
+ }
765
+ registerOnChange(fn) {
766
+ this.onChange = fn;
767
+ }
768
+ registerOnTouched(fn) {
769
+ this.onTouched = fn;
770
+ }
771
+ setDisabledState(isDisabled) {
772
+ this.cvaDisabled.set(isDisabled);
773
+ }
774
+ emitChange() {
775
+ const files = this.itemsSignal().map((i) => i.file);
776
+ this.onChange(files);
777
+ }
778
+ // ── FormFieldControl methods ──
779
+ /** @internal */
780
+ setDescribedByIds(ids) {
781
+ this.describedByIdsSignal.set([...ids]);
782
+ }
783
+ /** @internal */
784
+ setLabelledByIds(ids) {
785
+ this.labelledByIdsSignal.set([...ids]);
786
+ }
787
+ /** @internal */
788
+ onContainerClick(event) {
789
+ if (this.disabled())
790
+ return;
791
+ if (event.defaultPrevented)
792
+ return;
793
+ const dropzone = this.dropzoneRef()?.nativeElement;
794
+ if (!dropzone)
795
+ return;
796
+ const target = event.target;
797
+ if (target && dropzone.contains(target))
798
+ return;
799
+ this.triggerRef()?.nativeElement.focus();
800
+ }
801
+ // ── Rendering helpers ──
802
+ /** @internal */
803
+ iconNameFor(item) {
804
+ return iconNameFor(item.file);
805
+ }
806
+ /** @internal */
807
+ shouldShowProgress(item) {
808
+ if (item.status === 'pending' && item.progress === 0)
809
+ return false;
810
+ return true;
811
+ }
812
+ /** @internal */
813
+ progressColorFor(item) {
814
+ if (item.status === 'error')
815
+ return 'error';
816
+ if (item.status === 'success' || item.progress >= 100)
817
+ return 'success';
818
+ return 'primary';
819
+ }
820
+ /** @internal */
821
+ metaTextFor(item) {
822
+ const size = formatBytes(item.file.size);
823
+ if (item.status === 'pending')
824
+ return size;
825
+ if (item.status === 'uploading')
826
+ return `${size} · Uploading`;
827
+ if (item.status === 'success')
828
+ return `${size} · Done`;
829
+ if (item.error)
830
+ return `${size} · Failed — ${item.error}`;
831
+ return `${size} · Failed`;
832
+ }
833
+ // ── Lifecycle ──
834
+ ngOnInit() {
835
+ const monitor$ = this.focusMonitor.monitor(this.elementRef, true);
836
+ if (isObservable(monitor$)) {
837
+ monitor$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((origin) => {
838
+ const wasFocused = this._focused();
839
+ this._focused.set(!!origin);
840
+ if (wasFocused && !origin) {
841
+ this.onTouched();
842
+ this._ngControlRev.update((n) => n + 1);
843
+ }
844
+ });
845
+ }
846
+ this.destroyRef.onDestroy(() => {
847
+ this.focusMonitor.stopMonitoring(this.elementRef);
848
+ });
849
+ const ctrl = this.ngControl?.control;
850
+ if (ctrl) {
851
+ const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
852
+ if (streams.length) {
853
+ merge(...streams)
854
+ .pipe(takeUntilDestroyed(this.destroyRef))
855
+ .subscribe(() => this._ngControlRev.update((n) => n + 1));
856
+ }
857
+ }
858
+ const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
859
+ if (submit) {
860
+ submit
861
+ .pipe(takeUntilDestroyed(this.destroyRef))
862
+ .subscribe(() => this._formSubmitRev.update((n) => n + 1));
863
+ }
864
+ }
865
+ // ── Helpers ──
866
+ hasAccessibleNameHint() {
867
+ if (this.label() || this.ariaLabel() || this.ariaLabelledby())
868
+ return true;
869
+ const host = this.elementRef.nativeElement;
870
+ return !!host.textContent && host.textContent.trim().length > 0;
871
+ }
872
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: FileUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
873
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: FileUploadComponent, isStandalone: true, selector: "tw-file-upload", 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 }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requiredInput: { classPropertyName: "requiredInput", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, idInput: { classPropertyName: "idInput", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filesAdded: "filesAdded", fileRemoved: "fileRemoved", fileRejected: "fileRejected", cleared: "cleared" }, host: { attributes: { "role": "group" }, properties: { "id": "id()", "class": "rootClasses()", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "effectiveAriaLabelledby() || null", "attr.aria-describedby": "effectiveAriaDescribedby() || null", "attr.aria-disabled": "disabled() || null", "attr.aria-required": "required() || null", "attr.aria-invalid": "errorState() || null" } }, providers: [
874
+ {
875
+ provide: TW_FORM_FIELD_CONTROL,
876
+ useExisting: forwardRef(() => FileUploadComponent),
877
+ },
878
+ ], queries: [{ propertyName: "itemSlot", first: true, predicate: FileUploadItemDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "dropzoneRef", first: true, predicate: ["dropzone"], descendants: true, isSignal: true }, { propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "hiddenInputRef", first: true, predicate: ["hiddenInput"], descendants: true, isSignal: true }], exportAs: ["twFileUpload"], usesInheritance: true, ngImport: i0, template: "<input\n #hiddenInput\n type=\"file\"\n class=\"sr-only\"\n tabindex=\"-1\"\n [id]=\"hiddenInputId\"\n [attr.aria-label]=\"ariaLabel() ?? label() ?? triggerLabel()\"\n [attr.name]=\"name() || null\"\n [attr.accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n [required]=\"required()\"\n (change)=\"onNativeChange($event)\"\n/>\n\n<!-- The dropzone is a mouse click/drop convenience target, intentionally NOT a\n focusable control: the inner trigger <button> owns keyboard operability, so\n adding tabindex/keydown here would create a nested-interactive structure.\n No keyboard user is locked out, so the rules' concern does not apply. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #dropzone\n [class]=\"dropzoneClasses()\"\n (click)=\"open()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div [class]=\"dropzoneInnerClasses()\">\n <div [class]=\"illustrationClasses()\">\n <ng-content select=\"[twFileUploadIcon]\">\n <tw-icon name=\"upload-cloud\" [size]=\"iconSize()\" aria-hidden=\"true\" />\n </ng-content>\n </div>\n\n <span [id]=\"headlineId\" [class]=\"headlineClasses()\">\n <ng-content select=\"[twFileUploadHeadline]\">\n @if (label(); as l) {\n {{ l }}\n } @else {\n Drag files here or click to browse\n }\n </ng-content>\n </span>\n\n <span [id]=\"descriptionId\" [class]=\"descriptionClasses()\">\n <ng-content select=\"[twFileUploadDescription]\">\n @if (description(); as d) {\n {{ d }}\n }\n </ng-content>\n </span>\n\n <button\n #trigger\n type=\"button\"\n twButton\n variant=\"outline\"\n color=\"neutral\"\n [size]=\"size()\"\n [disabled]=\"disabled()\"\n [attr.aria-describedby]=\"effectiveAriaDescribedby() || null\"\n (click)=\"onTriggerClick($event)\"\n >\n {{ triggerLabel() }}\n </button>\n </div>\n</div>\n\n<ul [class]=\"listClasses()\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (itemSlot(); as slot) {\n <ng-container\n [ngTemplateOutlet]=\"slot.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: item, item: item }\"\n />\n } @else {\n <div [class]=\"itemClasses()\">\n <tw-icon\n [name]=\"iconNameFor(item)\"\n size=\"md\"\n [class]=\"itemIconClasses()\"\n aria-hidden=\"true\"\n />\n <div [class]=\"itemBodyClasses()\">\n <span [class]=\"itemNameClasses()\">{{ item.file.name }}</span>\n <span [class]=\"itemMetaClasses()\">{{ metaTextFor(item) }}</span>\n @if (shouldShowProgress(item)) {\n <tw-progress-bar\n [class]=\"itemProgressClasses()\"\n [value]=\"item.progress\"\n size=\"sm\"\n [color]=\"progressColorFor(item)\"\n />\n }\n </div>\n <div [class]=\"itemActionsClasses()\">\n <button\n type=\"button\"\n twButton\n variant=\"ghost\"\n color=\"neutral\"\n size=\"sm\"\n data-tw-file-upload-remove\n [attr.aria-label]=\"'Remove ' + item.file.name\"\n (click)=\"remove(item.id)\"\n >\n <tw-icon twButtonIcon name=\"x\" />\n </button>\n </div>\n </div>\n }\n </li>\n }\n</ul>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ButtonDirective, selector: "[twButton]", inputs: ["variant", "color", "size", "disabled", "loading"], exportAs: ["twButton"] }, { kind: "component", type: IconComponent, selector: "tw-icon", inputs: ["name", "img", "color", "size", "ariaLabel", "svg"] }, { kind: "component", type: ProgressBarComponent, selector: "tw-progress-bar", inputs: ["value", "variant", "color", "size", "label", "options"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
879
+ }
880
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: FileUploadComponent, decorators: [{
881
+ type: Component,
882
+ args: [{ selector: 'tw-file-upload', exportAs: 'twFileUpload', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet, ButtonDirective, IconComponent, ProgressBarComponent], providers: [
883
+ {
884
+ provide: TW_FORM_FIELD_CONTROL,
885
+ useExisting: forwardRef(() => FileUploadComponent),
886
+ },
887
+ ], host: {
888
+ 'role': 'group',
889
+ '[id]': 'id()',
890
+ '[class]': 'rootClasses()',
891
+ '[attr.aria-label]': 'ariaLabel() || null',
892
+ '[attr.aria-labelledby]': 'effectiveAriaLabelledby() || null',
893
+ '[attr.aria-describedby]': 'effectiveAriaDescribedby() || null',
894
+ '[attr.aria-disabled]': 'disabled() || null',
895
+ '[attr.aria-required]': 'required() || null',
896
+ '[attr.aria-invalid]': 'errorState() || null',
897
+ }, template: "<input\n #hiddenInput\n type=\"file\"\n class=\"sr-only\"\n tabindex=\"-1\"\n [id]=\"hiddenInputId\"\n [attr.aria-label]=\"ariaLabel() ?? label() ?? triggerLabel()\"\n [attr.name]=\"name() || null\"\n [attr.accept]=\"accept() || null\"\n [multiple]=\"multiple()\"\n [disabled]=\"disabled()\"\n [required]=\"required()\"\n (change)=\"onNativeChange($event)\"\n/>\n\n<!-- The dropzone is a mouse click/drop convenience target, intentionally NOT a\n focusable control: the inner trigger <button> owns keyboard operability, so\n adding tabindex/keydown here would create a nested-interactive structure.\n No keyboard user is locked out, so the rules' concern does not apply. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<div\n #dropzone\n [class]=\"dropzoneClasses()\"\n (click)=\"open()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div [class]=\"dropzoneInnerClasses()\">\n <div [class]=\"illustrationClasses()\">\n <ng-content select=\"[twFileUploadIcon]\">\n <tw-icon name=\"upload-cloud\" [size]=\"iconSize()\" aria-hidden=\"true\" />\n </ng-content>\n </div>\n\n <span [id]=\"headlineId\" [class]=\"headlineClasses()\">\n <ng-content select=\"[twFileUploadHeadline]\">\n @if (label(); as l) {\n {{ l }}\n } @else {\n Drag files here or click to browse\n }\n </ng-content>\n </span>\n\n <span [id]=\"descriptionId\" [class]=\"descriptionClasses()\">\n <ng-content select=\"[twFileUploadDescription]\">\n @if (description(); as d) {\n {{ d }}\n }\n </ng-content>\n </span>\n\n <button\n #trigger\n type=\"button\"\n twButton\n variant=\"outline\"\n color=\"neutral\"\n [size]=\"size()\"\n [disabled]=\"disabled()\"\n [attr.aria-describedby]=\"effectiveAriaDescribedby() || null\"\n (click)=\"onTriggerClick($event)\"\n >\n {{ triggerLabel() }}\n </button>\n </div>\n</div>\n\n<ul [class]=\"listClasses()\" role=\"list\">\n @for (item of items(); track item.id) {\n <li>\n @if (itemSlot(); as slot) {\n <ng-container\n [ngTemplateOutlet]=\"slot.templateRef\"\n [ngTemplateOutletContext]=\"{ $implicit: item, item: item }\"\n />\n } @else {\n <div [class]=\"itemClasses()\">\n <tw-icon\n [name]=\"iconNameFor(item)\"\n size=\"md\"\n [class]=\"itemIconClasses()\"\n aria-hidden=\"true\"\n />\n <div [class]=\"itemBodyClasses()\">\n <span [class]=\"itemNameClasses()\">{{ item.file.name }}</span>\n <span [class]=\"itemMetaClasses()\">{{ metaTextFor(item) }}</span>\n @if (shouldShowProgress(item)) {\n <tw-progress-bar\n [class]=\"itemProgressClasses()\"\n [value]=\"item.progress\"\n size=\"sm\"\n [color]=\"progressColorFor(item)\"\n />\n }\n </div>\n <div [class]=\"itemActionsClasses()\">\n <button\n type=\"button\"\n twButton\n variant=\"ghost\"\n color=\"neutral\"\n size=\"sm\"\n data-tw-file-upload-remove\n [attr.aria-label]=\"'Remove ' + item.file.name\"\n (click)=\"remove(item.id)\"\n >\n <tw-icon twButtonIcon name=\"x\" />\n </button>\n </div>\n </div>\n }\n </li>\n }\n</ul>\n" }]
898
+ }], ctorParameters: () => [], 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 }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], idInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], filesAdded: [{ type: i0.Output, args: ["filesAdded"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], fileRejected: [{ type: i0.Output, args: ["fileRejected"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], itemSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FileUploadItemDirective), { isSignal: true }] }], dropzoneRef: [{ type: i0.ViewChild, args: ['dropzone', { isSignal: true }] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], hiddenInputRef: [{ type: i0.ViewChild, args: ['hiddenInput', { isSignal: true }] }] } });
899
+
900
+ /**
901
+ * Generated bundle index. Do not edit.
902
+ */
903
+
904
+ export { FileUploadComponent, FileUploadItemDirective };
905
+ //# sourceMappingURL=cdevhub-ngx-tw-file-upload.mjs.map