@bitforgehq/ui 0.3.3 → 0.5.0-next.3

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 (99) hide show
  1. package/CHANGELOG.md +394 -0
  2. package/README.md +14 -14
  3. package/ai/MANIFEST.index.json +101 -2
  4. package/ai/MANIFEST.json +2630 -746
  5. package/ai/REFERENCE.index.json +196 -0
  6. package/ai/REFERENCE.json +1625 -140
  7. package/ai/UI_GUIDE.md +83 -21
  8. package/ai/components/app-frame.md +1 -1
  9. package/ai/components/badge.md +39 -2
  10. package/ai/components/chat-composer.md +357 -0
  11. package/ai/components/chat-message.md +251 -0
  12. package/ai/components/chat-thread.md +521 -0
  13. package/ai/components/checkbox.md +7 -0
  14. package/ai/components/combobox.md +12 -0
  15. package/ai/components/date-input.md +9 -0
  16. package/ai/components/datetime-input.md +2 -0
  17. package/ai/components/field.md +10 -0
  18. package/ai/components/file-input.md +498 -0
  19. package/ai/components/header.md +1 -1
  20. package/ai/components/image-input.md +368 -0
  21. package/ai/components/image-viewer.md +309 -0
  22. package/ai/components/list-item.md +105 -9
  23. package/ai/components/media-card.md +478 -0
  24. package/ai/components/menu-button.md +2 -0
  25. package/ai/components/menu.md +10 -0
  26. package/ai/components/multi-combobox.md +7 -0
  27. package/ai/components/nav-group.md +10 -3
  28. package/ai/components/nav-item.md +11 -2
  29. package/ai/components/number-input.md +2 -7
  30. package/ai/components/popover.md +13 -0
  31. package/ai/components/radio-group.md +6 -0
  32. package/ai/components/segmented.md +14 -2
  33. package/ai/components/select.md +100 -42
  34. package/ai/components/sidebar-toggle.md +16 -9
  35. package/ai/components/sidebar.md +41 -13
  36. package/ai/components/step.md +112 -0
  37. package/ai/components/steps.md +124 -0
  38. package/ai/components/tab-group.md +18 -2
  39. package/ai/components/table.md +115 -4
  40. package/ai/components/theme-menu-button.md +1 -0
  41. package/ai/components/time-input.md +2 -0
  42. package/ai/components/timeline-event.md +144 -0
  43. package/ai/components/timeline.md +201 -0
  44. package/ai/components/toggle.md +11 -1
  45. package/ai/components/toolbar.md +7 -18
  46. package/ai/reference/bf-action-directive.md +3 -1
  47. package/ai/reference/bf-badge-emphasis.md +27 -0
  48. package/ai/reference/bf-chat-composer-harness.md +64 -0
  49. package/ai/reference/bf-chat-message-role.md +27 -0
  50. package/ai/reference/bf-chat-message-status.md +28 -0
  51. package/ai/reference/bf-fade-in-directive.md +18 -0
  52. package/ai/reference/bf-file-input-harness.md +67 -0
  53. package/ai/reference/bf-file-input-variant.md +27 -0
  54. package/ai/reference/bf-image-input-harness.md +67 -0
  55. package/ai/reference/bf-image-input-shape.md +27 -0
  56. package/ai/reference/bf-image-viewer-actions-context.md +29 -0
  57. package/ai/reference/bf-image-viewer-harness.md +87 -0
  58. package/ai/reference/bf-image-viewer-image.md +25 -0
  59. package/ai/reference/bf-image-viewer-options.md +33 -0
  60. package/ai/reference/bf-image-viewer-origin.md +23 -0
  61. package/ai/reference/bf-image-viewer-ref.md +55 -0
  62. package/ai/reference/bf-image-viewer-service.md +32 -0
  63. package/ai/reference/bf-labels.md +105 -74
  64. package/ai/reference/bf-media-card-aspect.md +28 -0
  65. package/ai/reference/bf-media-card-harness.md +66 -0
  66. package/ai/reference/bf-motion-list-directive.md +22 -0
  67. package/ai/reference/bf-prose-directive.md +16 -0
  68. package/ai/reference/bf-rejected-file-reason.md +28 -0
  69. package/ai/reference/bf-rejected-file.md +28 -0
  70. package/ai/reference/bf-select-harness.md +14 -5
  71. package/ai/reference/bf-select-option.md +2 -0
  72. package/ai/reference/bf-size.md +3 -0
  73. package/ai/reference/bf-step-heading-level.md +28 -0
  74. package/ai/reference/bf-table-align.md +1 -1
  75. package/ai/reference/bf-table-column-align.md +33 -0
  76. package/ai/reference/bf-table-column.md +7 -7
  77. package/ai/reference/bf-table-harness.md +12 -0
  78. package/ai/reference/bf-timeline-tone.md +31 -0
  79. package/ai/reference/bf-toggle-harness.md +55 -0
  80. package/ai/reference/bf_table_sort_select.md +23 -0
  81. package/ai/reference/provide-bf-labels.md +10 -3
  82. package/ai/reference/provide-bf-select.md +20 -0
  83. package/bin/bf-ui.js +1 -0
  84. package/dark-ink.css +124 -0
  85. package/fesm2022/bitforgehq-ui-mobile.mjs +2 -1
  86. package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
  87. package/fesm2022/bitforgehq-ui-overlay.mjs +2649 -606
  88. package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
  89. package/fesm2022/bitforgehq-ui-testing.mjs +406 -11
  90. package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
  91. package/fesm2022/bitforgehq-ui.mjs +11149 -6957
  92. package/fesm2022/bitforgehq-ui.mjs.map +1 -1
  93. package/llms-full.txt +5219 -141
  94. package/llms.txt +40 -1
  95. package/package.json +2 -1
  96. package/theme.css +84 -0
  97. package/types/bitforgehq-ui-overlay.d.ts +684 -7
  98. package/types/bitforgehq-ui-testing.d.ts +510 -7
  99. package/types/bitforgehq-ui.d.ts +1757 -186
@@ -0,0 +1,498 @@
1
+ # bf-file-input
2
+
3
+ File picker field that lists picked or dropped files and checks type, size and count
4
+
5
+ `BfFileInputComponent` from `@bitforgehq/ui` · Forms · since 0.5.0
6
+
7
+ ## Example
8
+
9
+ ```ts
10
+ import { Component, computed, signal } from '@angular/core';
11
+ import { BfFileInputComponent } from '@bitforgehq/ui';
12
+
13
+ /**
14
+ * The bf-file-input overview.
15
+ *
16
+ * Live demo of `bf-file-input`: the field look taking several PDFs and images for an invoice, bound
17
+ * to a signal with a line that says what is chosen; then the drop zone look for receipts.
18
+ */
19
+ @Component({
20
+ selector: 'app-example-file-input-overview',
21
+ imports: [BfFileInputComponent],
22
+ template: `
23
+ <div class="flex flex-col gap-4">
24
+ <bf-file-input
25
+ label="Invoice attachments"
26
+ helpText="PDF, PNG or JPEG, up to 10 MB each"
27
+ accept=".pdf,image/png,image/jpeg"
28
+ multiple
29
+ [maxSize]="10 * 1024 * 1024"
30
+ [(value)]="attachments"
31
+ />
32
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
33
+ <bf-file-input variant="dropzone" label="Receipts" accept="image/*,.pdf" multiple />
34
+ </div>
35
+ `,
36
+ })
37
+ export class FileInputOverviewExampleComponent {
38
+ readonly attachments = signal<File[]>([]);
39
+
40
+ readonly status = computed(() => {
41
+ const files = this.attachments();
42
+ return files.length === 0
43
+ ? 'No attachments yet.'
44
+ : `Attached: ${files.map((file) => file.name).join(', ')}.`;
45
+ });
46
+ }
47
+ ```
48
+
49
+ ## Overview
50
+
51
+ File picker field: a tonal "Choose file" button inside a field box that also takes dropped
52
+ files, with the chosen files listed under it (thumbnail, name, size, remove button). `accept`,
53
+ `maxSize` and `maxFiles` decide what is added; a file that breaks one is listed in red with the
54
+ reason until the next pick and emitted through `rejected`, and never enters the value. The value
55
+ is always a `File[]` (one file at most while `multiple` is off). It only picks: uploading the
56
+ files is the app's job. Bind `[(value)]` directly or `[formField]` from a signal form.
57
+
58
+ A file picker field: a tonal "Choose file" button inside a field box that also takes dropped files, or a dashed drop zone with the same API. The chosen files are listed under it with a thumbnail (images) or the file extension, the name, the size and a remove button. `accept`, `maxSize` and `maxFiles` decide what is added; the value is always a `File[]`. It only picks files: uploading them is the app's job.
59
+
60
+ ## When to use
61
+
62
+ Use `bf-file-input` for documents and attachments: a signed contract, the scans for an invoice, a CSV to import. Use [bf-image-input](https://bitforge-ui.web.app/components/image-input) instead for one picture that should be seen while it is chosen (a profile picture, a logo), and [bf-input](https://bitforge-ui.web.app/components/input) for a link to a file that already lives somewhere else.
63
+
64
+ ## Basic usage
65
+
66
+ ```ts
67
+ import { Component, computed, signal } from '@angular/core';
68
+ import { BfFileInputComponent } from '@bitforgehq/ui';
69
+
70
+ /**
71
+ * The bf-file-input overview.
72
+ *
73
+ * Live demo of `bf-file-input`: the field look taking several PDFs and images for an invoice, bound
74
+ * to a signal with a line that says what is chosen; then the drop zone look for receipts.
75
+ */
76
+ @Component({
77
+ selector: 'app-example-file-input-overview',
78
+ imports: [BfFileInputComponent],
79
+ template: `
80
+ <div class="flex flex-col gap-4">
81
+ <bf-file-input
82
+ label="Invoice attachments"
83
+ helpText="PDF, PNG or JPEG, up to 10 MB each"
84
+ accept=".pdf,image/png,image/jpeg"
85
+ multiple
86
+ [maxSize]="10 * 1024 * 1024"
87
+ [(value)]="attachments"
88
+ />
89
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
90
+ <bf-file-input variant="dropzone" label="Receipts" accept="image/*,.pdf" multiple />
91
+ </div>
92
+ `,
93
+ })
94
+ export class FileInputOverviewExampleComponent {
95
+ readonly attachments = signal<File[]>([]);
96
+
97
+ readonly status = computed(() => {
98
+ const files = this.attachments();
99
+ return files.length === 0
100
+ ? 'No attachments yet.'
101
+ : `Attached: ${files.map((file) => file.name).join(', ')}.`;
102
+ });
103
+ }
104
+ ```
105
+
106
+ `value` is a `File[]`, empty while nothing is chosen. `(changed)` emits the new list right after the user adds or removes a file; a programmatic write to `value` does not emit.
107
+
108
+ ## One file or several
109
+
110
+ Without `multiple`, a new pick or drop replaces the chosen file, so the value holds one file at most. With `multiple`, picks and drops are added to the list (a file picked twice is listed twice), and each row has its own remove button.
111
+
112
+ ```ts
113
+ import { Component, signal } from '@angular/core';
114
+ import { BfFileInputComponent } from '@bitforgehq/ui';
115
+
116
+ /**
117
+ * One file or several.
118
+ *
119
+ * Without `multiple` a new pick replaces the chosen file; with it, picks and drops are added to the
120
+ * list, and each file can be removed on its own.
121
+ */
122
+ @Component({
123
+ selector: 'app-example-file-input-multiple',
124
+ imports: [BfFileInputComponent],
125
+ template: `
126
+ <div class="flex flex-col gap-4">
127
+ <bf-file-input label="Signed contract" accept=".pdf" [(value)]="contract" />
128
+ <bf-file-input label="Supporting documents" multiple [(value)]="documents" />
129
+ </div>
130
+ `,
131
+ })
132
+ export class FileInputMultipleExampleComponent {
133
+ protected readonly contract = signal<File[]>([]);
134
+ protected readonly documents = signal<File[]>([]);
135
+ }
136
+ ```
137
+
138
+ ## Drop zone
139
+
140
+ `variant="dropzone"` swaps the field box for a dashed area with an upload badge: the whole area is the control, so a click, <kbd>Enter</kbd> or <kbd>Space</kbd> opens the picker, and files can be dropped on it. Under a coarse pointer it says "Tap to choose" and takes less padding. Use it where picking files is the main task of the page (an import, a claim form); keep the compact field in a longer form.
141
+
142
+ ```ts
143
+ import { Component, signal } from '@angular/core';
144
+ import { BfFileInputComponent } from '@bitforgehq/ui';
145
+
146
+ /**
147
+ * The drop zone look.
148
+ *
149
+ * `variant="dropzone"` makes the whole dashed area the control: click it, press Enter or Space, or
150
+ * drop files on it. Under touch it says "Tap to choose". The list, rules and value are the same as
151
+ * the field look's.
152
+ */
153
+ @Component({
154
+ selector: 'app-example-file-input-dropzone',
155
+ imports: [BfFileInputComponent],
156
+ template: `
157
+ <bf-file-input
158
+ variant="dropzone"
159
+ label="Expense receipts"
160
+ helpText="Photos or PDFs of your receipts"
161
+ accept="image/*,.pdf"
162
+ multiple
163
+ [(value)]="receipts"
164
+ />
165
+ `,
166
+ })
167
+ export class FileInputDropzoneExampleComponent {
168
+ protected readonly receipts = signal<File[]>([]);
169
+ }
170
+ ```
171
+
172
+ ## Rules and rejected files
173
+
174
+ - `accept` takes the native syntax: MIME types (`application/pdf`), wildcards (`image/*`) and extensions (`.pdf`), comma-separated. The native picker filters with it, and dropped files are checked against it too, so a drop cannot get around it. Empty accepts every file.
175
+ - `maxSize` is the largest file in bytes (`5 * 1024 * 1024` for 5 MB); the message shows it formatted ("Too large, max 5 MB").
176
+ - `maxFiles` (with `multiple`) caps the whole list, counting the files already chosen.
177
+
178
+ A file is checked for type, then size, then count. A file that breaks a rule never enters the value and never makes the field invalid: it is listed in red under the chosen files with the reason, until the next pick or drop or until the user dismisses it, and the screen reader hears "contract.exe was not added: Not an accepted type". `(rejected)` emits once per pick or drop with the refused files, each a `BfRejectedFile` with the `file`, the `reason` (`'type'`, `'size'` or `'count'`) and the `message` shown. The messages come from `provideBfLabels()` (`fileRejectedType`, `fileRejectedSize(max)`, `fileRejectedCount(max)`).
179
+
180
+ ```ts
181
+ import { Component, signal } from '@angular/core';
182
+ import { BfFileInputComponent, type BfRejectedFile } from '@bitforgehq/ui';
183
+
184
+ /**
185
+ * Accepted types, size and count.
186
+ *
187
+ * `accept`, `maxSize` (bytes) and `maxFiles` decide what is added. A file that breaks one never
188
+ * enters the value: it is listed in red with the reason until the next pick, and `rejected` emits it
189
+ * with its `reason` (`type`, `size` or `count`) and message.
190
+ */
191
+ @Component({
192
+ selector: 'app-example-file-input-rules',
193
+ imports: [BfFileInputComponent],
194
+ template: `
195
+ <div class="flex flex-col gap-3">
196
+ <bf-file-input
197
+ label="Invoice scans"
198
+ helpText="PDF, PNG or JPEG, up to 5 MB each, at most 3 files"
199
+ accept=".pdf,image/png,image/jpeg"
200
+ multiple
201
+ [maxSize]="5 * 1024 * 1024"
202
+ [maxFiles]="3"
203
+ [(value)]="scans"
204
+ (rejected)="onRejected($event)"
205
+ />
206
+ <p data-status aria-live="polite" class="text-caption text-text-muted">
207
+ {{ lastRejected() }}
208
+ </p>
209
+ </div>
210
+ `,
211
+ })
212
+ export class FileInputRulesExampleComponent {
213
+ protected readonly scans = signal<File[]>([]);
214
+ protected readonly lastRejected = signal('Nothing rejected yet.');
215
+
216
+ protected onRejected(files: BfRejectedFile[]): void {
217
+ this.lastRejected.set(
218
+ `Rejected: ${files.map((item) => `${item.file.name} (${item.reason})`).join(', ')}.`,
219
+ );
220
+ }
221
+ }
222
+ ```
223
+
224
+ ## Uploading
225
+
226
+ The field never uploads: it hands the app the `File` objects the user chose. Send them when the user submits, with the app's own upload code: a data service that posts a `FormData` through `HttpClient`, a signed URL, or a storage SDK. Progress, retries and the server's answer belong to that code. With `name` set, the hidden native input carries the chosen files too, so a plain HTML form post includes them.
227
+
228
+ ```ts
229
+ import { Component, signal } from '@angular/core';
230
+ import { BfButtonComponent, BfFileInputComponent } from '@bitforgehq/ui';
231
+
232
+ /**
233
+ * Handing the files to your upload.
234
+ *
235
+ * The field only picks files. On Send, the app puts the `File[]` into a `FormData` and passes it to
236
+ * its own upload code; here a stand-in reports what it would send.
237
+ */
238
+ @Component({
239
+ selector: 'app-example-file-input-upload',
240
+ imports: [BfButtonComponent, BfFileInputComponent],
241
+ template: `
242
+ <div class="flex flex-col items-start gap-3">
243
+ <bf-file-input
244
+ label="Invoice attachments"
245
+ accept=".pdf,image/*"
246
+ multiple
247
+ class="self-stretch"
248
+ [(value)]="attachments"
249
+ />
250
+ <bf-button [disabled]="attachments().length === 0" (click)="send()">Send</bf-button>
251
+ <p data-status aria-live="polite" class="text-caption text-text-muted">{{ status() }}</p>
252
+ </div>
253
+ `,
254
+ })
255
+ export class FileInputUploadExampleComponent {
256
+ protected readonly attachments = signal<File[]>([]);
257
+ protected readonly status = signal('Nothing sent yet.');
258
+
259
+ protected async send(): Promise<void> {
260
+ const body = new FormData();
261
+ for (const file of this.attachments()) {
262
+ body.append('attachments', file, file.name);
263
+ }
264
+ const sent = await this.upload(body);
265
+ this.status.set(`Sent ${sent} file(s).`);
266
+ this.attachments.set([]);
267
+ }
268
+
269
+ /** Your upload goes here: `HttpClient.post('/api/invoices/114/attachments', body)` in a data
270
+ * service, a signed URL or a storage SDK. This stand-in only counts the files it was given. */
271
+ private upload(body: FormData): Promise<number> {
272
+ return Promise.resolve(body.getAll('attachments').length);
273
+ }
274
+ }
275
+ ```
276
+
277
+ ## With Signal Forms
278
+
279
+ Bind the field with `[formField]` instead of `[(value)]`; touched, invalid, errors, disabled, readonly and required flow in, and the first error shows once the user has left the pick button. `required()` treats an empty list as filled (it checks for `''`, `false` and `null`), so it gives the label its marker only. Add a rule for the missing file:
280
+
281
+ ```ts
282
+ import { Component, signal } from '@angular/core';
283
+ import {
284
+ FormField,
285
+ FormRoot,
286
+ form,
287
+ required,
288
+ requiredError,
289
+ validate,
290
+ } from '@angular/forms/signals';
291
+ import { BfButtonComponent, BfFileInputComponent } from '@bitforgehq/ui';
292
+
293
+ /**
294
+ * A required file on Signal Forms.
295
+ *
296
+ * The value is a `File[]`, and `required()` does not count an empty list as empty: it gives the
297
+ * label its marker, and a `validate()` rule reports the missing file. `[formRoot]` runs the
298
+ * submission on submit.
299
+ */
300
+ @Component({
301
+ selector: 'app-example-file-input-signal-forms',
302
+ imports: [FormField, FormRoot, BfButtonComponent, BfFileInputComponent],
303
+ template: `
304
+ <form [formRoot]="claim" class="flex flex-col items-start gap-3">
305
+ <bf-file-input
306
+ label="Signed contract"
307
+ accept=".pdf"
308
+ class="self-stretch"
309
+ [formField]="claim.contract"
310
+ />
311
+ <bf-button type="submit">Send claim</bf-button>
312
+ </form>
313
+ `,
314
+ })
315
+ export class FileInputSignalFormsExampleComponent {
316
+ private readonly model = signal<{ contract: File[] }>({ contract: [] });
317
+ protected readonly claim = form(
318
+ this.model,
319
+ (p) => {
320
+ required(p.contract);
321
+ validate(p.contract, ({ value }) =>
322
+ value().length === 0 ? requiredError({ message: 'Attach the signed contract' }) : undefined,
323
+ );
324
+ },
325
+ { submission: { action: () => Promise.resolve(undefined) } },
326
+ );
327
+ }
328
+ ```
329
+
330
+ The rule there is `validate(p.contract, ({ value }) => value().length === 0 ? requiredError({ message: 'Attach the signed contract' }) : undefined)` next to `required(p.contract)`. Rejected files are not errors of the field; read them from `(rejected)` when the form needs to know.
331
+
332
+ ## Read-only and disabled
333
+
334
+ `readonly` keeps the chosen files listed with no pick button, drop or remove buttons; with `multiple` the box shows the count ("2 files"), without it the file name. `disabled` greys the field out, ignores drops and takes the pick button out of the tab order.
335
+
336
+ ```ts
337
+ import { Component } from '@angular/core';
338
+ import { BfFileInputComponent } from '@bitforgehq/ui';
339
+
340
+ /**
341
+ * Read-only and disabled.
342
+ *
343
+ * Read-only keeps the files listed with no pick button, drop or remove (with `multiple` the box
344
+ * shows the count); disabled greys the field out and takes it out of the tab order.
345
+ */
346
+ @Component({
347
+ selector: 'app-example-file-input-states',
348
+ imports: [BfFileInputComponent],
349
+ template: `
350
+ <div class="flex flex-col gap-4">
351
+ <bf-file-input label="Submitted documents" multiple readonly [value]="submitted" />
352
+ <bf-file-input label="Contract" disabled helpText="Unlocked once the quote is approved" />
353
+ </div>
354
+ `,
355
+ })
356
+ export class FileInputStatesExampleComponent {
357
+ protected readonly submitted = [
358
+ new File([new Uint8Array(182 * 1024)], 'invoice-2026-114.pdf', { type: 'application/pdf' }),
359
+ new File([new Uint8Array(96 * 1024)], 'delivery-note.pdf', { type: 'application/pdf' }),
360
+ ];
361
+ }
362
+ ```
363
+
364
+ ## Sizes
365
+
366
+ `sm` 32 px, `md` 40 px (default), `lg` 48 px for the field box, the same `BfSize` scale as `bf-input`, so a file field lines up with the text fields of a form; the pick button inside is 24, 28 or 36 px. Unset, `size` follows `provideBfDefaults()`'s `fieldSize`. The drop zone keeps one size.
367
+
368
+ ```ts
369
+ import { Component, signal } from '@angular/core';
370
+ import { BfFileInputComponent } from '@bitforgehq/ui';
371
+
372
+ /** The sm, md and lg file input sizes. */
373
+ @Component({
374
+ selector: 'app-example-file-input-sizes',
375
+ imports: [BfFileInputComponent],
376
+ template: `
377
+ <div data-sizes class="flex flex-col gap-3">
378
+ <bf-file-input size="sm" label="Logo" accept="image/*" [(value)]="logo" />
379
+ <bf-file-input label="Price list" accept=".pdf,.xlsx" [(value)]="priceList" />
380
+ <bf-file-input size="lg" label="Company register extract" [(value)]="extract" />
381
+ </div>
382
+ `,
383
+ })
384
+ export class FileInputSizesExampleComponent {
385
+ protected readonly logo = signal<File[]>([]);
386
+ protected readonly priceList = signal<File[]>([]);
387
+ protected readonly extract = signal<File[]>([]);
388
+ }
389
+ ```
390
+
391
+ ## Phones
392
+
393
+ On a phone the pick button (or a tap on the drop zone) opens the platform's own picker, which on iOS and Android usually offers the photo library, the camera and the files app, filtered by `accept`. Dropping is a desktop gesture; under a coarse pointer the drop zone asks the user to tap instead. Under a coarse pointer the pick button and each remove button reach 44 px through an invisible extension.
394
+
395
+ ## Desktop
396
+
397
+ Files dragged over the field box or the drop zone highlight it (`primary` border, `selected` background) and are added on drop, through the same rules as a pick. The pick button darkens on hover; the drop zone takes `surface-muted` on hover. The field box shows the `primary` border and the focus ring while the pick button has focus. Long file names are truncated with an ellipsis, the full name in a tooltip.
398
+
399
+ ## Theming
400
+
401
+ - Field box: `border-strong` border (`danger-border` on error), `surface` background; while dragging, `primary` border and `selected` background. Read-only and disabled: `surface-muted` background, disabled with a `surface-sunken` border and `text-disabled` text.
402
+ - Pick button: `primary-soft` background with `primary-strong` text (hover `primary-soft-hover`), as the tonal `bf-button`. Hint text: `text-muted`.
403
+ - Drop zone: a 2 px dashed `border-strong` border on `surface`, `rounded-card`; the badge `primary-soft` with a `primary-strong` arrow. Under forced colours its border is `CanvasText` (`Highlight` while dragging, `GrayText` disabled).
404
+ - File rows: `border` border on `surface`, `rounded-tile`; the thumbnail `surface-sunken` with `text-subtle` extension; the size `text-muted`. Rejected rows: the name and reason in `danger`.
405
+ - Label, help and error text come from the `bf-field` wrapper (see its Theming section).
406
+
407
+ ## Accessibility
408
+
409
+ The pick button is a native `<button>` named by the label and its own text ("Contract, Choose file") and described by the text beside it (the file name or the drop hint), the help and the error paragraph; `aria-invalid` and `aria-errormessage` are set once the field is touched and invalid. The drop zone is a `role="button"` element with `tabindex="0"`, named the same way ("Contract, Drag a file here or browse"). The native `<input type="file">` is visually hidden and out of the tab order. `required` gives the label its marker; there is no `aria-required`, which is not allowed on a button. Each remove button is named "Remove invoice.pdf", each rejected row's button "Dismiss setup.exe". A polite live region announces what happened ("2 files added", "invoice.pdf removed", "setup.exe was not added: Not an accepted type"). After a remove, focus moves to the next remove button, else the previous one, else the pick button. Without a visible `label`, give `ariaLabel` or `ariaLabelledby`: it takes the label's place at the start of the name, and the visible text still follows, so a speech-input user can say "Choose file". `focus()` focuses the pick button; Signal Forms calls it from `focusBoundControl()`.
410
+
411
+ ### Keyboard
412
+
413
+ | Key | Where | Action |
414
+ | ------------------------------------------------ | ---------------------- | ------------------------------------------------------------------------- |
415
+ | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | Field | Moves through the pick button (or drop zone) and each row's remove button |
416
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Pick button, drop zone | Opens the native file picker |
417
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Remove button | Removes the file and moves focus to the next row |
418
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Dismiss button | Hides the rejected file and returns focus to the pick button |
419
+
420
+ ## Inputs
421
+
422
+ | Name | Type | Default | Description |
423
+ | ---------------- | ----------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
424
+ | `variant` | `BfFileInputVariant` (`'field' \| 'dropzone'`) | `'field'` | The look: `field` (a compact field with a pick button, the default) or `dropzone`. |
425
+ | `multiple` | `boolean` | `false` | Allows several files: new picks and drops are added to the list. Off, a new file replaces the one chosen. |
426
+ | `accept` | `string` | `''` | The accepted file types, in the native `accept` syntax: MIME types, `type/*` wildcards and `.ext` extensions, comma-separated (`'.pdf,image/*'`). Empty accepts every file. Dropped files are checked against it too. |
427
+ | `maxSize` | `number \| undefined` | `undefined` | Largest file size in bytes; a larger file is rejected. Unset allows any size. |
428
+ | `maxFiles` | `number \| undefined` | `undefined` | Most files in the value with `multiple`, counting those already chosen; files past it are rejected. Unset allows any number. |
429
+ | `size` | `BfSize \| undefined` (`'sm' \| 'md' \| 'lg' \| undefined`) | `undefined` | Height step of the field box: sm 32, md 40, lg 48 px; unset follows `provideBfDefaults()` (`fieldSize`, `md` by default). |
430
+ | `label` | `string` | `''` | Visible label above the control. |
431
+ | `ariaLabel` | `string` | `''` | Accessible name when no visible label is rendered, read before the control's visible text ("Attachments, Choose files"). Ignored when `label` is set. |
432
+ | `ariaLabelledby` | `string` | `''` | Id(s) of the element(s) that name the field when no visible label is rendered, read before the control's visible text. Ignored when `label` is set. |
433
+ | `helpText` | `string` | `''` | Helper text under the control. |
434
+ | `errorMessage` | `string` | `''` | Explicit error text; takes precedence over Signal Forms errors. |
435
+ | `disabled` | `boolean` | `false` | Disables the control: no picking, dropping or removing, and the pick button is not focusable. Set by `[formField]`; do not bind it yourself. |
436
+ | `readonly` | `boolean` | `false` | Makes the control read-only: the files stay listed, with no pick button, drop or remove. Set by `[formField]`; do not bind it yourself. |
437
+ | `required` | `boolean` | `false` | Marks the control required (label marker). Set by `[formField]`; do not bind it yourself. |
438
+ | `invalid` | `boolean` | `false` | Field validity from Signal Forms. Set by `[formField]`; do not bind it yourself. |
439
+ | `touched` | `boolean` | `false` | Whether the user has left the field; errors show only once touched. Set by `[formField]`; do not bind it yourself. |
440
+ | `errors` | `readonly ValidationError.WithOptionalFieldTree[]` | `[]` | Validation errors from Signal Forms; the first message is shown once touched. Set by `[formField]`; do not bind it yourself. |
441
+ | `name` | `string` | `''` | Native name attribute; set, the hidden native file input carries the chosen files, so a plain HTML form post includes them. Set by `[formField]`; do not bind it yourself. |
442
+ | `id` | `string` | `generated` | Control id (the pick button's); generated when not set. |
443
+ | `value` | `File[]` | `[]` | The chosen files; two-way bindable and bound by `[formField]`. Empty while nothing is chosen, at most one file while `multiple` is off. Two-way: `[(value)]`. |
444
+
445
+ ## Outputs
446
+
447
+ | Name | Payload | Description |
448
+ | ---------- | ------------------ | ------------------------------------------------------------------------------------------------------- |
449
+ | `changed` | `File[]` | Emitted with the new files right after the user adds or removes one, not on a model write. |
450
+ | `rejected` | `BfRejectedFile[]` | Emitted once per pick or drop in which at least one file broke a rule, with those files. |
451
+ | `focused` | `FocusEvent` | Emitted when the pick button gains focus. |
452
+ | `blurred` | `FocusEvent` | Emitted when the pick button loses focus. |
453
+ | `touch` | `void` | Emitted on blur so Signal Forms marks the field touched. Set by `[formField]`; do not bind it yourself. |
454
+
455
+ ## Methods
456
+
457
+ ### `focus(options?: FocusOptions): void`
458
+
459
+ Focuses the pick button; Signal Forms calls it from `focusBoundControl()`. Read-only has no
460
+ pick button, so nothing is focused.
461
+
462
+ | Parameter | Type | Description |
463
+ | ---------- | -------------- | --------------------------------------------------------------------- |
464
+ | `options?` | `FocusOptions` | Passed on to the native `focus()`, such as `{ preventScroll: true }`. |
465
+
466
+ ## Notes
467
+
468
+ ### Do
469
+
470
+ - Set `accept` and `maxSize` to what the server takes, so the user hears about a wrong file at once.
471
+ - Upload on submit, from the `File[]` the field holds.
472
+ - Use the drop zone where picking files is the page's main task.
473
+
474
+ ### Don't
475
+
476
+ - Don't rely on `required()` alone; add the `validate()` rule for an empty list.
477
+ - Don't bind both `[(value)]` and `[formField]`.
478
+ - Don't use it for one picture the user should see; use `bf-image-input`.
479
+
480
+ ## Migration notes
481
+
482
+ ### 0.5.0
483
+
484
+ New: `bf-file-input`, a file picker field. The `field` look (the default) is a tonal "Choose
485
+ file" button inside a field box that also takes dropped files; `variant="dropzone"` is a dashed
486
+ area that is the control as a whole (click, Enter, Space or a drop). The chosen files are listed
487
+ under it with a thumbnail (images) or the extension, the name, the size and a remove button. The
488
+ value is a `File[]` (one file at most without `multiple`), bound with `[(value)]` or
489
+ `[formField]`. `accept` (MIME types, `type/*` and `.ext`, checked on drops too), `maxSize`
490
+ (bytes) and `maxFiles` decide what is added; a file that breaks one never enters the value and
491
+ never sets `invalid`: it shows as a red row with the reason until the next pick and is emitted
492
+ through `rejected` (new types `BfRejectedFile`: `file`, `reason`, `message`; and
493
+ `BfRejectedFileReason`: `'type' | 'size' | 'count'`). It only picks; uploading stays with the app. New type `BfFileInputVariant`, new label
494
+ keys `fileInputChoose`, `fileInputDropHint`, `fileInputDropzone`, `fileInputTapToChoose`,
495
+ `fileInputRemove`, `fileInputDismiss`, `fileInputAdded`, `fileInputNotAdded`,
496
+ `fileInputRemoved`, `fileInputCount`, `fileRejectedType`, `fileRejectedSize` and
497
+ `fileRejectedCount`. `@bitforgehq/ui/testing`: new `BfFileInputHarness` (`addFiles()` drops files
498
+ through the same rules, `getFileNames()`, `getRejected()`, `removeFile()`, `getVariant()`).
@@ -380,7 +380,7 @@ whichever `h1` is active (the large title's when `largeTitle` is true, the toolb
380
380
 
381
381
  ```ts
382
382
  import { Component, signal } from '@angular/core';
383
- import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui';
383
+ import { BfSelectComponent, type BfSelectOption } from '@bitforgehq/ui/overlay';
384
384
  import { BfHeaderComponent } from '@bitforgehq/ui/mobile';
385
385
 
386
386
  /** A workspace picker in the title's place. */