@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.
- package/CHANGELOG.md +394 -0
- package/README.md +14 -14
- package/ai/MANIFEST.index.json +101 -2
- package/ai/MANIFEST.json +2630 -746
- package/ai/REFERENCE.index.json +196 -0
- package/ai/REFERENCE.json +1625 -140
- package/ai/UI_GUIDE.md +83 -21
- package/ai/components/app-frame.md +1 -1
- package/ai/components/badge.md +39 -2
- package/ai/components/chat-composer.md +357 -0
- package/ai/components/chat-message.md +251 -0
- package/ai/components/chat-thread.md +521 -0
- package/ai/components/checkbox.md +7 -0
- package/ai/components/combobox.md +12 -0
- package/ai/components/date-input.md +9 -0
- package/ai/components/datetime-input.md +2 -0
- package/ai/components/field.md +10 -0
- package/ai/components/file-input.md +498 -0
- package/ai/components/header.md +1 -1
- package/ai/components/image-input.md +368 -0
- package/ai/components/image-viewer.md +309 -0
- package/ai/components/list-item.md +105 -9
- package/ai/components/media-card.md +478 -0
- package/ai/components/menu-button.md +2 -0
- package/ai/components/menu.md +10 -0
- package/ai/components/multi-combobox.md +7 -0
- package/ai/components/nav-group.md +10 -3
- package/ai/components/nav-item.md +11 -2
- package/ai/components/number-input.md +2 -7
- package/ai/components/popover.md +13 -0
- package/ai/components/radio-group.md +6 -0
- package/ai/components/segmented.md +14 -2
- package/ai/components/select.md +100 -42
- package/ai/components/sidebar-toggle.md +16 -9
- package/ai/components/sidebar.md +41 -13
- package/ai/components/step.md +112 -0
- package/ai/components/steps.md +124 -0
- package/ai/components/tab-group.md +18 -2
- package/ai/components/table.md +115 -4
- package/ai/components/theme-menu-button.md +1 -0
- package/ai/components/time-input.md +2 -0
- package/ai/components/timeline-event.md +144 -0
- package/ai/components/timeline.md +201 -0
- package/ai/components/toggle.md +11 -1
- package/ai/components/toolbar.md +7 -18
- package/ai/reference/bf-action-directive.md +3 -1
- package/ai/reference/bf-badge-emphasis.md +27 -0
- package/ai/reference/bf-chat-composer-harness.md +64 -0
- package/ai/reference/bf-chat-message-role.md +27 -0
- package/ai/reference/bf-chat-message-status.md +28 -0
- package/ai/reference/bf-fade-in-directive.md +18 -0
- package/ai/reference/bf-file-input-harness.md +67 -0
- package/ai/reference/bf-file-input-variant.md +27 -0
- package/ai/reference/bf-image-input-harness.md +67 -0
- package/ai/reference/bf-image-input-shape.md +27 -0
- package/ai/reference/bf-image-viewer-actions-context.md +29 -0
- package/ai/reference/bf-image-viewer-harness.md +87 -0
- package/ai/reference/bf-image-viewer-image.md +25 -0
- package/ai/reference/bf-image-viewer-options.md +33 -0
- package/ai/reference/bf-image-viewer-origin.md +23 -0
- package/ai/reference/bf-image-viewer-ref.md +55 -0
- package/ai/reference/bf-image-viewer-service.md +32 -0
- package/ai/reference/bf-labels.md +105 -74
- package/ai/reference/bf-media-card-aspect.md +28 -0
- package/ai/reference/bf-media-card-harness.md +66 -0
- package/ai/reference/bf-motion-list-directive.md +22 -0
- package/ai/reference/bf-prose-directive.md +16 -0
- package/ai/reference/bf-rejected-file-reason.md +28 -0
- package/ai/reference/bf-rejected-file.md +28 -0
- package/ai/reference/bf-select-harness.md +14 -5
- package/ai/reference/bf-select-option.md +2 -0
- package/ai/reference/bf-size.md +3 -0
- package/ai/reference/bf-step-heading-level.md +28 -0
- package/ai/reference/bf-table-align.md +1 -1
- package/ai/reference/bf-table-column-align.md +33 -0
- package/ai/reference/bf-table-column.md +7 -7
- package/ai/reference/bf-table-harness.md +12 -0
- package/ai/reference/bf-timeline-tone.md +31 -0
- package/ai/reference/bf-toggle-harness.md +55 -0
- package/ai/reference/bf_table_sort_select.md +23 -0
- package/ai/reference/provide-bf-labels.md +10 -3
- package/ai/reference/provide-bf-select.md +20 -0
- package/bin/bf-ui.js +1 -0
- package/dark-ink.css +124 -0
- package/fesm2022/bitforgehq-ui-mobile.mjs +2 -1
- package/fesm2022/bitforgehq-ui-mobile.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-overlay.mjs +2649 -606
- package/fesm2022/bitforgehq-ui-overlay.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui-testing.mjs +406 -11
- package/fesm2022/bitforgehq-ui-testing.mjs.map +1 -1
- package/fesm2022/bitforgehq-ui.mjs +11149 -6957
- package/fesm2022/bitforgehq-ui.mjs.map +1 -1
- package/llms-full.txt +5219 -141
- package/llms.txt +40 -1
- package/package.json +2 -1
- package/theme.css +84 -0
- package/types/bitforgehq-ui-overlay.d.ts +684 -7
- package/types/bitforgehq-ui-testing.d.ts +510 -7
- 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()`).
|
package/ai/components/header.md
CHANGED
|
@@ -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. */
|