@formancy/angular 0.1.0 → 0.2.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.
@@ -1,8 +1,8 @@
1
- import * as i0 from '@angular/core';
1
+ import * as _angular_core from '@angular/core';
2
2
  import { InjectionToken, Provider, Signal, Type, OnChanges, OnDestroy, Injector, OnInit } from '@angular/core';
3
3
  import * as _formancy_core from '@formancy/core';
4
4
  import { FormEngine, FieldSnapshot, Path } from '@formancy/core';
5
- import { FieldType, FieldDef } from '@formancy/spec';
5
+ import { FieldType, RemoteOption, FieldDef, LayoutNode, RichInline, RichBlock, RichCommand } from '@formancy/spec';
6
6
  import * as _formancy_angular from '@formancy/angular';
7
7
 
8
8
  /**
@@ -39,6 +39,8 @@ interface RepeaterBinding {
39
39
  rowIds: Signal<readonly string[]>;
40
40
  addRow(): void;
41
41
  removeRow(index: number): void;
42
+ /** Move a row. The keyboard route to reordering; a drag is a second route to this. */
43
+ moveRow(from: number, to: number): void;
42
44
  }
43
45
  /**
44
46
  * A repeater's live row count as a signal — the Angular half of React's
@@ -110,6 +112,75 @@ interface FormancyRegistry {
110
112
  declare const FORMANCY_REGISTRY: InjectionToken<FormancyRegistry>;
111
113
  declare function provideFormancyRegistry(registry: FormancyRegistry): Provider[];
112
114
 
115
+ /**
116
+ * A field's options, from the document or from the deployment.
117
+ *
118
+ * The same contract the React binding states in `use-sourced-options.ts`, written
119
+ * out here rather than shared: the async lifecycle is expressed in this framework's
120
+ * own primitives, and a shared abstraction would report that both bindings
121
+ * implemented it when one had not.
122
+ *
123
+ * `optionsSource` decides which answers EXIST rather than how they are shown, so the
124
+ * plain `<select>` honours it as well as the typeahead — one ignoring it would render
125
+ * an empty chooser over a field that collects something.
126
+ */
127
+ interface SourcedOptionsState {
128
+ /** The rows a source returned, empty until one does. */
129
+ rows: Signal<readonly RemoteOption[]>;
130
+ /** Names for values the form already holds, so a resumed draft is not blank. */
131
+ named: Signal<ReadonlyMap<string, string>>;
132
+ /** The document names a source this deployment does not have. */
133
+ unavailable: Signal<boolean>;
134
+ /** A request is in flight. The control says `aria-busy`; it never disables itself. */
135
+ busy: Signal<boolean>;
136
+ /** The one thing the field says out loud, or the empty string. */
137
+ status: Signal<string>;
138
+ /** Whether this field is sourced at all. False is the ordinary case. */
139
+ sourced: Signal<boolean>;
140
+ }
141
+
142
+ /**
143
+ * Where a file goes, and what the submission remembers about it.
144
+ *
145
+ * The same contract the React binding uses, with the same reasoning: this
146
+ * package has no opinion about the destination, so the one field works against
147
+ * local disk, S3 or a customer's own service without any of them becoming a
148
+ * dependency of a renderer.
149
+ *
150
+ * The bytes never pass through the submission. What is stored is what the file
151
+ * *is* and where it went, so a submission read back years later is small and
152
+ * says what was attached even if the object store has since been emptied.
153
+ */
154
+ interface StoredFile {
155
+ /** Stable within the submission; how a row is keyed and removed. */
156
+ id: string;
157
+ name: string;
158
+ size: number;
159
+ contentType: string;
160
+ /** Where the bytes are, in whatever the host's storage calls a location. */
161
+ storageKey: string;
162
+ }
163
+ /**
164
+ * Uploads one file and reports what was stored.
165
+ *
166
+ * Rejecting is a real answer: the field says so out loud rather than dropping
167
+ * the file, because a submission somebody believes carries their evidence and
168
+ * does not is the worst outcome available here.
169
+ */
170
+ type Uploader = (file: File) => Promise<StoredFile>;
171
+ /**
172
+ * Optional by design. A form with no file fields needs no uploader, and a file
173
+ * field without one renders read-only and says why — which beats turning a
174
+ * form that mostly works into a failed injection.
175
+ */
176
+ declare const FORMANCY_UPLOADER: InjectionToken<Uploader>;
177
+ declare function injectUploader(): Uploader | null;
178
+ /** Providing one, for a host that has somewhere to put bytes. */
179
+ declare function provideFormancyUploader(upload: Uploader): {
180
+ provide: InjectionToken<Uploader>;
181
+ useValue: Uploader;
182
+ };
183
+
113
184
  /**
114
185
  * The built-in unstyled field components — the Angular rendering of the same
115
186
  * decisions React's defaults made. Zero CSS; `data-formancy-part` is the
@@ -123,80 +194,158 @@ declare function provideFormancyRegistry(registry: FormancyRegistry): Provider[]
123
194
  /** Shared unstyled shell: real label, projected control, error text as the
124
195
  * describedby target. */
125
196
  declare class FormancyFieldShell {
126
- readonly field: i0.InputSignal<FieldBinding>;
127
- readonly label: i0.InputSignal<string>;
128
- protected readonly showError: i0.Signal<boolean>;
129
- protected readonly errorText: i0.Signal<string>;
130
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyFieldShell, never>;
131
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyFieldShell, "formancy-field-shell", never, { "field": { "alias": "field"; "required": true; "isSignal": true; }; "label": { "alias": "label"; "required": true; "isSignal": true; }; }, {}, never, ["*"], true, never>;
197
+ readonly field: _angular_core.InputSignal<FieldBinding>;
198
+ readonly label: _angular_core.InputSignal<string>;
199
+ /** Inert here; read by tools outside the renderer. See FormancyLayout. */
200
+ readonly path: _angular_core.InputSignal<string>;
201
+ protected readonly showError: _angular_core.Signal<boolean>;
202
+ protected readonly errorText: _angular_core.Signal<string>;
203
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyFieldShell, never>;
204
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyFieldShell, "formancy-field-shell", never, { "field": { "alias": "field"; "required": true; "isSignal": true; }; "label": { "alias": "label"; "required": true; "isSignal": true; }; "path": { "alias": "path"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
132
205
  }
133
206
  /** The state every leaf control shares; components extend it so the templates
134
207
  * stay the only per-type code. Context comes through DI (see registry.ts). */
135
208
  declare abstract class FieldComponentBase {
136
209
  protected readonly context: _formancy_angular.FormancyFieldContext;
137
210
  protected readonly field: FieldBinding;
138
- protected readonly control: i0.Signal<_formancy_core.ControlProps>;
139
- private readonly engine;
211
+ protected readonly control: _angular_core.Signal<_formancy_core.ControlProps>;
212
+ protected readonly engine: _formancy_core.FormEngine;
140
213
  /**
141
214
  * Option labels resolved to strings, since a label may be a reference into
142
215
  * the message catalogue. Falling back to the stored value keeps an
143
216
  * untranslated option selectable rather than blank.
144
217
  */
145
- protected readonly options: i0.Signal<readonly {
218
+ protected readonly options: _angular_core.Signal<readonly {
219
+ value: string;
220
+ label: string;
221
+ }[]>;
222
+ /**
223
+ * What the control types into, when it has something to type into.
224
+ *
225
+ * A plain `<select>` never writes to it, so its source is asked for everything and
226
+ * shows what fits; the typeahead writes every keystroke.
227
+ */
228
+ protected readonly sourceQuery: _angular_core.WritableSignal<string>;
229
+ /** The remote half, or an inert one for the ordinary field that lists its options. */
230
+ protected readonly remote: SourcedOptionsState;
231
+ /**
232
+ * Whether THIS component is the one that will render the field.
233
+ *
234
+ * True for every control but the select that hands over to the typeahead, which
235
+ * overrides it. Both extend this base, so without the distinction a sourced
236
+ * typeahead asked its source twice and read one set of answers.
237
+ */
238
+ protected sourceEnabled(): boolean;
239
+ /**
240
+ * The options to offer: the document's, or the deployment's.
241
+ *
242
+ * The stored answer is always offerable even when the current query does not match
243
+ * it — a control that dropped it would show an empty box over an answer the form
244
+ * holds, and the next blur would look like the person cleared it.
245
+ */
246
+ protected readonly offered: _angular_core.Signal<readonly {
146
247
  value: string;
147
248
  label: string;
148
249
  }[]>;
149
250
  }
251
+ /**
252
+ * A single-line answer, and — with `widget: "scanner"` — a camera route to the same
253
+ * string.
254
+ *
255
+ * The input is the control in both cases, never a second one beside it: typing is the
256
+ * accessibility floor and the fallback at once, so it is what is always there and the
257
+ * scan button is what is sometimes added. With no scanner supplied the markup is the
258
+ * default control exactly, because a Scan button that opens nothing is worse than no
259
+ * button ([0071](../../../docs/decisions/0071-a-scanner-is-supplied-not-built.md)).
260
+ *
261
+ * The React binding renders the same three elements for the same reasons.
262
+ */
150
263
  declare class FormancyTextField extends FieldComponentBase {
151
- protected readonly text: i0.Signal<string>;
264
+ private readonly scan;
265
+ protected readonly scanning: _angular_core.WritableSignal<boolean>;
266
+ /** A device failure, held here rather than in the field's errors. See above. */
267
+ private readonly trouble;
268
+ protected readonly text: _angular_core.Signal<string>;
269
+ /**
270
+ * A computed over the field's SIGNAL, not over `engine.getFieldSnapshot` — the
271
+ * widget can arrive with a new document, and a plain method call is not a
272
+ * dependency an OnPush component re-runs for.
273
+ */
274
+ protected readonly scannable: _angular_core.Signal<boolean>;
275
+ protected readonly status: _angular_core.Signal<string>;
152
276
  protected onInput(event: Event): void;
153
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyTextField, never>;
154
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyTextField, "formancy-text-field", never, {}, {}, never, never, true, never>;
277
+ /**
278
+ * The ONE place a text field's answer is written, typed or scanned.
279
+ *
280
+ * Structural rather than careful: the parameter is a `string`, so there is no path
281
+ * from the camera to `setValue` that could store something typing could not — the
282
+ * line a widget may never cross
283
+ * ([0065](../../../docs/decisions/0065-a-widget-is-authored-not-registered.md)).
284
+ */
285
+ private commit;
286
+ protected read(): Promise<void>;
287
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyTextField, never>;
288
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyTextField, "formancy-text-field", never, {}, {}, never, never, true, never>;
155
289
  }
156
290
  declare class FormancyTextareaField extends FieldComponentBase {
157
- protected readonly text: i0.Signal<string>;
291
+ protected readonly text: _angular_core.Signal<string>;
158
292
  protected onInput(event: Event): void;
159
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyTextareaField, never>;
160
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyTextareaField, "formancy-textarea-field", never, {}, {}, never, never, true, never>;
293
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyTextareaField, never>;
294
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyTextareaField, "formancy-textarea-field", never, {}, {}, never, never, true, never>;
161
295
  }
162
296
  declare class FormancyNumberField extends FieldComponentBase {
163
- protected readonly text: i0.Signal<string>;
297
+ protected readonly text: _angular_core.Signal<string>;
164
298
  protected onInput(event: Event): void;
165
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyNumberField, never>;
166
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyNumberField, "formancy-number-field", never, {}, {}, never, never, true, never>;
299
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyNumberField, never>;
300
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyNumberField, "formancy-number-field", never, {}, {}, never, never, true, never>;
167
301
  }
168
302
  declare class FormancyCheckboxField extends FieldComponentBase {
169
- protected readonly checked: i0.Signal<boolean>;
303
+ protected readonly checked: _angular_core.Signal<boolean>;
304
+ /**
305
+ * `widget: "toggle"` is a part name and NOT `role="switch"`.
306
+ *
307
+ * ARIA's switch means a control that takes effect when you operate it, and a form
308
+ * field sets a value submitted later or never — so announcing "switch" describes
309
+ * it incorrectly to the people who rely on the description. A role is also not
310
+ * paint: changing it would make this the first widget to change what a control
311
+ * claims to be, which is the line the widget mechanism exists to hold. The switch
312
+ * is CSS, and conformance keeps finding this by role `checkbox` either way.
313
+ *
314
+ * Null rather than absent when there is no widget, because Angular omits an
315
+ * attribute bound to null — which is what keeps an ordinary checkbox's markup
316
+ * exactly as it was.
317
+ */
318
+ protected readonly part: _angular_core.Signal<"toggle" | null>;
170
319
  protected onChange(event: Event): void;
171
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyCheckboxField, never>;
172
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyCheckboxField, "formancy-checkbox-field", never, {}, {}, never, never, true, never>;
320
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyCheckboxField, never>;
321
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyCheckboxField, "formancy-checkbox-field", never, {}, {}, never, never, true, never>;
173
322
  }
174
323
  declare class FormancyDateField extends FieldComponentBase {
175
- protected readonly text: i0.Signal<string>;
324
+ protected readonly text: _angular_core.Signal<string>;
176
325
  protected onInput(event: Event): void;
177
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyDateField, never>;
178
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyDateField, "formancy-date-field", never, {}, {}, never, never, true, never>;
326
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyDateField, never>;
327
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyDateField, "formancy-date-field", never, {}, {}, never, never, true, never>;
179
328
  }
180
329
  declare class FormancySelectField extends FieldComponentBase {
181
- protected readonly selected: i0.Signal<string>;
330
+ protected readonly typeahead: _angular_core.Signal<boolean>;
331
+ /** The name the document gave, for the message when this deployment has no such source. */
332
+ protected readonly sourceName: _angular_core.Signal<string>;
333
+ /** This one hands over to the typeahead, which does its own asking. */
334
+ protected sourceEnabled(): boolean;
335
+ protected readonly selected: _angular_core.Signal<string>;
182
336
  protected onChange(event: Event): void;
183
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancySelectField, never>;
184
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancySelectField, "formancy-select-field", never, {}, {}, never, never, true, never>;
337
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancySelectField, never>;
338
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancySelectField, "formancy-select-field", never, {}, {}, never, never, true, never>;
185
339
  }
186
340
  declare class FormancyRadioGroupField extends FieldComponentBase {
187
- protected readonly showError: i0.Signal<boolean>;
188
- protected readonly errorText: i0.Signal<string>;
341
+ protected readonly showError: _angular_core.Signal<boolean>;
342
+ protected readonly errorText: _angular_core.Signal<string>;
189
343
  protected optionId(option: {
190
344
  value: string;
191
345
  }): string;
192
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyRadioGroupField, never>;
193
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyRadioGroupField, "formancy-radio-group-field", never, {}, {}, never, never, true, never>;
346
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyRadioGroupField, never>;
347
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyRadioGroupField, "formancy-radio-group-field", never, {}, {}, never, never, true, never>;
194
348
  }
195
- /**
196
- * The built-in unstyled components. `null` means the type renders nothing here:
197
- * hidden and static are non-inputs, and the container types are laid out by
198
- * their own machinery, not by a leaf slot.
199
- */
200
349
  declare const DEFAULT_FIELD_COMPONENTS: Record<FieldType, Type<unknown> | null>;
201
350
 
202
351
  interface SubmitOutcome {
@@ -214,13 +363,13 @@ interface SubmitOutcome {
214
363
  */
215
364
  declare class FormancyComponentOutlet implements OnChanges, OnDestroy {
216
365
  private readonly container;
217
- readonly formancyOutlet: i0.InputSignal<Type<unknown>>;
218
- readonly formancyOutletInjector: i0.InputSignal<Injector>;
366
+ readonly formancyOutlet: _angular_core.InputSignal<Type<unknown>>;
367
+ readonly formancyOutletInjector: _angular_core.InputSignal<Injector>;
219
368
  private ref;
220
369
  ngOnChanges(): void;
221
370
  ngOnDestroy(): void;
222
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyComponentOutlet, never>;
223
- static ɵdir: i0.ɵɵDirectiveDeclaration<FormancyComponentOutlet, "[formancyOutlet]", never, { "formancyOutlet": { "alias": "formancyOutlet"; "required": true; "isSignal": true; }; "formancyOutletInjector": { "alias": "formancyOutletInjector"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
371
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyComponentOutlet, never>;
372
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<FormancyComponentOutlet, "[formancyOutlet]", never, { "formancyOutlet": { "alias": "formancyOutlet"; "required": true; "isSignal": true; }; "formancyOutletInjector": { "alias": "formancyOutletInjector"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
224
373
  }
225
374
  /**
226
375
  * One field's slot: resolve the component through the registry (per-path beats
@@ -236,22 +385,27 @@ declare class FormancyComponentOutlet implements OnChanges, OnDestroy {
236
385
  declare class FormancyFieldSlot implements OnInit {
237
386
  private readonly registry;
238
387
  private readonly injector;
239
- readonly path: i0.InputSignal<string>;
240
- readonly fallbackLabel: i0.InputSignal<string | undefined>;
388
+ readonly path: _angular_core.InputSignal<string>;
389
+ readonly fallbackLabel: _angular_core.InputSignal<string | undefined>;
241
390
  protected state?: {
242
391
  snapshot: Signal<FieldSnapshot>;
243
392
  component: Type<unknown> | null;
244
393
  injector: Injector;
245
394
  };
246
395
  ngOnInit(): void;
247
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyFieldSlot, never>;
248
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyFieldSlot, "formancy-field", never, { "path": { "alias": "path"; "required": true; "isSignal": true; }; "fallbackLabel": { "alias": "fallbackLabel"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
396
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyFieldSlot, never>;
397
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyFieldSlot, "formancy-field", never, { "path": { "alias": "path"; "required": true; "isSignal": true; }; "fallbackLabel": { "alias": "fallbackLabel"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
249
398
  }
250
399
  interface RepeaterRow {
251
400
  index: number;
252
401
  /** The row's stable identity, which `@for` tracks instead of its position. */
253
402
  id: string;
254
- wires: readonly string[];
403
+ /** Each field of the row: its key within the row, and its positional wire.
404
+ * Tracked by `key`, bound by `wire`. */
405
+ children: ReadonlyArray<{
406
+ key: string;
407
+ wire: string;
408
+ }>;
255
409
  }
256
410
  /**
257
411
  * A repeater's own chrome: a named fieldset, one row div per item with the row
@@ -262,8 +416,11 @@ interface RepeaterRow {
262
416
  declare class FormancyRepeaterSection implements OnInit {
263
417
  private readonly engine;
264
418
  private readonly injector;
265
- readonly wire: i0.InputSignal<string>;
266
- readonly labels: i0.InputSignal<Record<string, string> | undefined>;
419
+ readonly wire: _angular_core.InputSignal<string>;
420
+ readonly labels: _angular_core.InputSignal<Record<string, string> | undefined>;
421
+ /** The repeater's own definition, kept because the column plan reads `widget`,
422
+ * `columns` and the child labels off it on every render. */
423
+ protected definition: FieldDef | undefined;
267
424
  protected state?: {
268
425
  repeater: RepeaterBinding;
269
426
  label: string;
@@ -271,10 +428,83 @@ declare class FormancyRepeaterSection implements OnInit {
271
428
  removeLabel: string;
272
429
  rows: Signal<readonly RepeaterRow[]>;
273
430
  };
431
+ /**
432
+ * The columns the grid shows, in the order it shows them, or an empty list when the
433
+ * widget was not asked for.
434
+ *
435
+ * `columns` without the widget stays inert on purpose: 0066 lets an author write the
436
+ * arrangement before a renderer honours it, and a repeater that silently became a grid
437
+ * because somebody sized its columns would be the opposite of that.
438
+ *
439
+ * The React binding computes the same list with the same helper, for the same reasons.
440
+ */
441
+ protected readonly plan: Signal<readonly {
442
+ key: string;
443
+ heading: string;
444
+ align: string | null;
445
+ width?: number;
446
+ }[]>;
447
+ /**
448
+ * The authored ratios, as ONE custom property rather than as `grid-template-columns`.
449
+ *
450
+ * A property lays nothing out by itself, so a theme's narrow-screen media query
451
+ * replaces its own declaration and wins rather than losing to an inline one it cannot
452
+ * outrank. It also carries a value space no attribute could enumerate: `width` is a
453
+ * number with `exclusiveMinimum: 0`, so 1.5 is legal and nothing bounds it from above.
454
+ *
455
+ * Nothing at all when no column was sized, so the theme's fallback is live code.
456
+ */
457
+ protected trackStyle(): Record<string, string>;
458
+ /**
459
+ * The controls in one cell: every leaf inside the row that belongs to that column's
460
+ * child field.
461
+ *
462
+ * One child, because a grid's rows are FLAT: a child holding fields of its own is
463
+ * refused when the document is saved (0078). This walked the whole subtree under the
464
+ * child while a group could be a column, and every clause that made that walk safe is
465
+ * gone with the arrangement it served.
466
+ *
467
+ * Still a filter over the children that EXIST rather than the wire the column implies,
468
+ * so a document nobody validated renders an empty cell rather than a field the engine
469
+ * does not have.
470
+ */
471
+ protected cellChildren(row: RepeaterRow, key: string): readonly RepeaterRow['children'][number][];
274
472
  ngOnInit(): void;
275
473
  protected fallbackFor(instanceWire: string): string | undefined;
276
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyRepeaterSection, never>;
277
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyRepeaterSection, "formancy-repeater", never, { "wire": { "alias": "wire"; "required": true; "isSignal": true; }; "labels": { "alias": "labels"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
474
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyRepeaterSection, never>;
475
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyRepeaterSection, "formancy-repeater", never, { "wire": { "alias": "wire"; "required": true; "isSignal": true; }; "labels": { "alias": "labels"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
476
+ }
477
+ declare class FormancyLayout {
478
+ readonly nodes: _angular_core.InputSignal<readonly LayoutNode[]>;
479
+ readonly labels: _angular_core.InputSignal<Record<string, string> | undefined>;
480
+ /** Index path of the container these nodes are the children of. */
481
+ readonly at: _angular_core.InputSignal<string>;
482
+ private readonly engine;
483
+ constructor();
484
+ /** The span as a number CSS can count with, and nothing at all for `all`.
485
+ *
486
+ * A style OBJECT rather than `[style.--fm-span]`: both set a custom property --
487
+ * measured, both work -- and the object form lets this return nothing for `all`
488
+ * without binding an empty string. */
489
+ protected spanStyle(node: LayoutNode): Record<string, string>;
490
+ /** This node's index path, as the dotted string the attribute carries. */
491
+ protected pathOf(index: number): string;
492
+ /**
493
+ * Headings are cached per node. Minting an id inside the template would give
494
+ * a different one on every change-detection pass, leaving aria-labelledby
495
+ * pointing at an element that no longer exists.
496
+ */
497
+ private readonly headings;
498
+ private static counter;
499
+ /** A tabs node's own name, for the tab strip. Null when it has none. */
500
+ protected stripLabelFor(node: LayoutNode): string | null;
501
+ protected isRepeater(path: string): boolean;
502
+ protected headingFor(node: LayoutNode): {
503
+ id: string;
504
+ text: string;
505
+ } | null;
506
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyLayout, never>;
507
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyLayout, "formancy-layout", never, { "nodes": { "alias": "nodes"; "required": true; "isSignal": true; }; "labels": { "alias": "labels"; "required": false; "isSignal": true; }; "at": { "alias": "at"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
278
508
  }
279
509
  /**
280
510
  * Renders the whole form from the engine: one slot per field, resolved through
@@ -294,9 +524,14 @@ declare class FormancyForm {
294
524
  * `items[].name`). A `label` on the model definition wins — that is how
295
525
  * fixture schemas carry text until the spec's i18n section lands.
296
526
  */
297
- readonly labels: i0.InputSignal<Record<string, string> | undefined>;
298
- readonly submitLabel: i0.InputSignal<string | undefined>;
299
- readonly submitted: i0.OutputEmitterRef<SubmitOutcome>;
527
+ readonly labels: _angular_core.InputSignal<Record<string, string> | undefined>;
528
+ /**
529
+ * Render a named entry from the schema's `layouts` instead of model order.
530
+ * Unknown or absent, the form falls back to model order.
531
+ */
532
+ readonly layout: _angular_core.InputSignal<string | undefined>;
533
+ readonly submitLabel: _angular_core.InputSignal<string | undefined>;
534
+ readonly submitted: _angular_core.OutputEmitterRef<SubmitOutcome>;
300
535
  protected readonly pages: readonly {
301
536
  key: string;
302
537
  def: FieldDef;
@@ -305,6 +540,11 @@ declare class FormancyForm {
305
540
  protected readonly staticWires: string[];
306
541
  protected readonly staticOnPage: Signal<string[]>;
307
542
  protected readonly repeatersOnPage: Signal<string[]>;
543
+ /**
544
+ * The nodes of the named layout, or undefined to fall back to model order —
545
+ * a mistyped layout name should not produce an empty form.
546
+ */
547
+ protected readonly arrangement: Signal<readonly LayoutNode[] | undefined>;
308
548
  protected fallbackFor(wire: string): string | undefined;
309
549
  protected pageLabel(page: {
310
550
  key: string;
@@ -312,8 +552,8 @@ declare class FormancyForm {
312
552
  }): string;
313
553
  protected onNext(): void;
314
554
  protected onSubmit(): void;
315
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyForm, never>;
316
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyForm, "formancy-form", never, { "labels": { "alias": "labels"; "required": false; "isSignal": true; }; "submitLabel": { "alias": "submitLabel"; "required": false; "isSignal": true; }; }, { "submitted": "submitted"; }, never, never, true, never>;
555
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyForm, never>;
556
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyForm, "formancy-form", never, { "labels": { "alias": "labels"; "required": false; "isSignal": true; }; "layout": { "alias": "layout"; "required": false; "isSignal": true; }; "submitLabel": { "alias": "submitLabel"; "required": false; "isSignal": true; }; }, { "submitted": "submitted"; }, never, never, true, never>;
317
557
  }
318
558
 
319
559
  /**
@@ -326,7 +566,7 @@ declare class FormancyForm {
326
566
  * problem" already means to assistive tech.
327
567
  */
328
568
  declare class FormancyErrorSummary {
329
- readonly labels: i0.InputSignal<Record<string, string> | undefined>;
569
+ readonly labels: _angular_core.InputSignal<Record<string, string> | undefined>;
330
570
  private readonly engine;
331
571
  private readonly region;
332
572
  protected readonly errors: Signal<ReadonlyArray<{
@@ -338,9 +578,288 @@ declare class FormancyErrorSummary {
338
578
  protected labelFor(path: string): string;
339
579
  protected controlIdOf(path: string): string;
340
580
  protected focusControl(event: Event, path: string): void;
341
- static ɵfac: i0.ɵɵFactoryDeclaration<FormancyErrorSummary, never>;
342
- static ɵcmp: i0.ɵɵComponentDeclaration<FormancyErrorSummary, "formancy-error-summary", never, { "labels": { "alias": "labels"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
581
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyErrorSummary, never>;
582
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyErrorSummary, "formancy-error-summary", never, { "labels": { "alias": "labels"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
583
+ }
584
+
585
+ /**
586
+ * One run of inline nodes, recursing through its own selector.
587
+ *
588
+ * Angular has no fragment component, so emphasis inside emphasis is expressed
589
+ * by the component referring to itself — which is why it is its own
590
+ * declaration rather than part of the block template below.
591
+ */
592
+ declare class FormancyRichInline {
593
+ readonly nodes: _angular_core.InputSignal<readonly RichInline[]>;
594
+ protected asText(node: RichInline): string;
595
+ protected childrenOf(node: RichInline): readonly RichInline[];
596
+ protected hrefOf(node: RichInline): string;
597
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyRichInline, never>;
598
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyRichInline, "formancy-rich-inline", never, { "nodes": { "alias": "nodes"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
599
+ }
600
+ /**
601
+ * Showing a `richtext` answer, from the same parser the React renderer uses.
602
+ *
603
+ * No `[innerHTML]`, and no `DomSanitizer` — there is nothing to sanitise. The
604
+ * stored answer is parsed into a typed tree in `@formancy/spec`, and every
605
+ * element here is created by Angular from that tree, so the characters
606
+ * somebody typed arrive as interpolated text and cannot become markup however
607
+ * they are spelled ([0052](../../../docs/decisions/0052-richtext-is-not-html.md)).
608
+ *
609
+ * Both renderers building the same tree into the same elements is the promise
610
+ * the engine makes across browser and server, applied to presentation: two
611
+ * implementations, one meaning.
612
+ */
613
+ declare class FormancyRichText {
614
+ readonly source: _angular_core.InputSignal<string>;
615
+ protected readonly blocks: _angular_core.Signal<RichBlock[]>;
616
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyRichText, never>;
617
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyRichText, "formancy-rich-text", never, { "source": { "alias": "source"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
343
618
  }
344
619
 
345
- export { DEFAULT_FIELD_COMPONENTS, FORMANCY_ENGINE, FORMANCY_FIELD_CONTEXT, FORMANCY_REGISTRY, FormancyCheckboxField, FormancyComponentOutlet, FormancyDateField, FormancyErrorSummary, FormancyFieldShell, FormancyFieldSlot, FormancyForm, FormancyNumberField, FormancyRadioGroupField, FormancyRepeaterSection, FormancySelectField, FormancyTextField, FormancyTextareaField, injectEngine, injectField, injectFieldContext, injectRepeater, injectSubmit, injectWizard, provideFormancy, provideFormancyRegistry };
346
- export type { FieldBinding, FormancyFieldContext, FormancyRegistry, RepeaterBinding, SubmitOutcome, WizardBinding };
620
+ /**
621
+ * Telling somebody their draft came back changed.
622
+ *
623
+ * The same component the React binding has, word for word in what it says,
624
+ * because the two renderers agreeing about what a form TELLS somebody matters as
625
+ * much as them agreeing about what it collects.
626
+ *
627
+ * The server already does the careful half: a republished form migrates a draft
628
+ * lazily on resume, and answers whose field no longer exists move to
629
+ * `data.__orphaned` rather than being deleted
630
+ * ([0027](../../../docs/decisions/0027-lazy-draft-migration.md)).
631
+ *
632
+ * **Nothing showed that to the person.** They resumed a draft, some of their
633
+ * answers were no longer on the form, and they submitted believing everything
634
+ * they had typed was in it. The answers are not lost from storage — they are
635
+ * lost from the submission, and nobody was told.
636
+ *
637
+ * The semantics follow `formancy-error-summary`, which solved the same shape of
638
+ * problem: the container takes focus through `tabindex="-1"` and is deliberately
639
+ * NOT `role="alert"`, because focusing it already makes a screen reader announce
640
+ * it and doing both announces it twice.
641
+ */
642
+ interface ResumeMigration {
643
+ readonly severity: 'lossy' | 'breaking';
644
+ readonly changes: ReadonlyArray<{
645
+ readonly kind: string;
646
+ readonly path?: string;
647
+ }>;
648
+ }
649
+ declare class FormancyResumeNotice {
650
+ /** Omitted when the draft came back unchanged. */
651
+ readonly migration: _angular_core.InputSignal<ResumeMigration | undefined>;
652
+ /** Question wording for a field key, since a key is not what the form asked. */
653
+ readonly labels: _angular_core.InputSignal<Readonly<Record<string, string>>>;
654
+ protected readonly setAside: _angular_core.Signal<string[]>;
655
+ protected readonly summary: _angular_core.Signal<string>;
656
+ private readonly region;
657
+ private readonly focused;
658
+ protected labelFor(path: string): string;
659
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormancyResumeNotice, never>;
660
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormancyResumeNotice, "formancy-resume-notice", never, { "migration": { "alias": "migration"; "required": false; "isSignal": true; }; "labels": { "alias": "labels"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
661
+ }
662
+
663
+ /**
664
+ * A rich-text editing surface, supplied by the host.
665
+ *
666
+ * The same contract the React binding uses, deliberately to the letter: one
667
+ * interface, one factory, one set of attributes. A renderer that invented its own
668
+ * shape here would be the drift the conformance suite exists to prevent, except
669
+ * in the one place the suite cannot see — what the HOST passes in.
670
+ *
671
+ * Why the host and not this package: a contenteditable editor means ProseMirror,
672
+ * which is larger than this entire package, and most forms have no rich-text
673
+ * field. Without a factory the field is a textarea with a toolbar, which is a
674
+ * working editor and not a degraded mode
675
+ * ([0061](../../../docs/decisions/0061-tiptap-over-the-closed-grammar.md)).
676
+ *
677
+ * What crosses this boundary is **the stored grammar in both directions, never
678
+ * markup**. That is the whole reason an editor is admissible: nothing on either
679
+ * side holds a string of HTML, so no consumer of an answer becomes a sanitiser
680
+ * ([0052](../../../docs/decisions/0052-richtext-is-not-html.md)).
681
+ */
682
+ interface RichTextEditorHandle {
683
+ /** The stored answer, in the grammar. */
684
+ value: () => string;
685
+ /** Replace the content when the form's value changes underneath the editor. */
686
+ setValue: (value: string) => void;
687
+ /**
688
+ * Run a formatting command, so the field's own toolbar keeps working.
689
+ *
690
+ * Needed because an editor library brings keyboard shortcuts and no toolbar
691
+ * UI. Leaving the field's toolbar out made Bold reachable by Ctrl+B and by no
692
+ * visible control, which is a regression against the `<textarea>` it replaced
693
+ * and unusable for anybody who does not know the shortcut.
694
+ *
695
+ * The commands are the grammar's whole surface — the same `RichCommand`
696
+ * values the textarea's toolbar uses — so one toolbar drives either surface
697
+ * and the two cannot offer different things.
698
+ */
699
+ run: (command: RichCommand, href?: string) => void;
700
+ /** Whether the command is on at the caret, for the toolbar's pressed state. */
701
+ isActive: (command: RichCommand) => boolean;
702
+ destroy: () => void;
703
+ }
704
+ interface RichTextEditorMount {
705
+ /** Where to mount. The host's factory owns what it puts inside. */
706
+ readonly element: Element;
707
+ /** The stored answer to open with, in the grammar. */
708
+ readonly value: string;
709
+ /** Called with the new stored answer, in the grammar. */
710
+ readonly onChange: (value: string) => void;
711
+ readonly editable: boolean;
712
+ /**
713
+ * Attributes for the editing surface itself, so the ENGINE keeps owning the
714
+ * accessibility wiring.
715
+ *
716
+ * These ids are minted by `@formancy/core` and composed centrally, which is
717
+ * what makes `aria-describedby` identical in both renderers rather than
718
+ * correct in one of them. An editor package that labelled its own surface
719
+ * would be a third implementation, and the one nobody tests.
720
+ */
721
+ readonly attributes: Readonly<Record<string, string>>;
722
+ }
723
+ type RichTextEditorFactory = (mount: RichTextEditorMount) => RichTextEditorHandle;
724
+ /**
725
+ * Optional by design, and its absence costs less than the uploader's.
726
+ *
727
+ * A missing uploader makes a file field read-only, because there is nowhere to
728
+ * put the bytes. A missing editor costs only the WYSIWYG surface: the answer is
729
+ * still editable, still valid, and still the same grammar.
730
+ */
731
+ declare const FORMANCY_RICH_TEXT_EDITOR: InjectionToken<RichTextEditorFactory>;
732
+ declare function injectRichTextEditorFactory(): RichTextEditorFactory | null;
733
+ /** Providing one, for a host that wants the contenteditable surface. */
734
+ declare function provideFormancyRichTextEditor(make: RichTextEditorFactory): {
735
+ provide: InjectionToken<RichTextEditorFactory>;
736
+ useValue: RichTextEditorFactory;
737
+ };
738
+
739
+ /**
740
+ * What a source is asked for.
741
+ *
742
+ * Declared here as well as in `@formancy/react` and deliberately not shared, exactly
743
+ * as `ScanRequest` and `Scanner` are declared twice. A host contract each binding
744
+ * states in its own words is a contract each binding can be read on its own.
745
+ *
746
+ * `kind` is a flat discriminator rather than a union of two interfaces: a host
747
+ * written in plain JavaScript must be harmless, and a shape it can read with one
748
+ * `if` is a shape it gets right.
749
+ */
750
+ interface OptionsRequest {
751
+ /**
752
+ * `search` — somebody is typing and wants matching rows.
753
+ * `labels` — the form holds these values already and needs their names.
754
+ */
755
+ kind: 'search' | 'labels';
756
+ /** The name the document gave, which the deployment resolves. */
757
+ source: string;
758
+ /** The field's data path, e.g. `canton` or `people[1].canton`. */
759
+ path: string;
760
+ /** What was typed. Empty on a `labels` request. */
761
+ query: string;
762
+ /** The stored values to name. Empty on a `search`. */
763
+ values: readonly string[];
764
+ /**
765
+ * The locale the form is being resolved in — the engine's, which is the host's own
766
+ * `locale` when it passed one and the document's default otherwise.
767
+ *
768
+ * A remote label is a plain string, never a `{$t}` reference: nothing can check a
769
+ * reference that arrives at runtime, and an unchecked one resolves to nothing and
770
+ * shows an opaque identifier. So the source answers in the right language instead.
771
+ */
772
+ locale: string;
773
+ /** How many rows the control can show. A hint: the control caps what arrives anyway. */
774
+ limit: number;
775
+ /** Aborted when the answer stops being wanted — a newer keystroke, or a destroy. */
776
+ signal: AbortSignal;
777
+ }
778
+ /**
779
+ * Where one named list of options comes from.
780
+ *
781
+ * The third instance of the inversion the uploader and the scanner already are: the
782
+ * document names a list, the deployment says what that name means, and nothing in
783
+ * formancy ever makes a request of its own. A URL in a form document would be a
784
+ * deployment detail in a portable format, unfixable once published, and an SSRF
785
+ * surface on a self-hosted instance.
786
+ */
787
+ interface OptionsSource {
788
+ resolve(request: OptionsRequest): Promise<readonly RemoteOption[]>;
789
+ /** How long to wait after a keystroke before asking. The control has a default. */
790
+ debounceMs?: number;
791
+ /** Below this many characters, do not ask at all. The control says so on screen. */
792
+ minQueryLength?: number;
793
+ /** How many rows to show at once. */
794
+ maxRows?: number;
795
+ }
796
+ /**
797
+ * Every source this deployment has, by the name a document would use.
798
+ *
799
+ * A MAP rather than one resolver function, and that is the one deliberate difference
800
+ * from `Scanner`. The control has to know **synchronously** whether a name resolves,
801
+ * because absence here is the *file field's* branch and not the scanner's: a text
802
+ * field with no scanner still collects the answer by typing, but a select whose
803
+ * options come only from a source collects nothing at all, so it must say so instead
804
+ * of rendering an empty chooser.
805
+ */
806
+ type OptionsSources = Readonly<Record<string, OptionsSource>>;
807
+ /**
808
+ * Optional, and what its absence costs depends on the document. A form with no
809
+ * sourced field never needs one; a field that names a source renders a message where
810
+ * its chooser would be, exactly as the file field does without an uploader.
811
+ */
812
+ declare const FORMANCY_OPTIONS_SOURCES: InjectionToken<Readonly<Record<string, OptionsSource>>>;
813
+ declare function injectOptionsSources(): OptionsSources | null;
814
+ /** Providing them, for a host that has the lists a document names. */
815
+ declare function provideFormancyOptionsSources(sources: OptionsSources): {
816
+ provide: InjectionToken<Readonly<Record<string, OptionsSource>>>;
817
+ useValue: Readonly<Record<string, OptionsSource>>;
818
+ };
819
+
820
+ /**
821
+ * What a scanner is asked to read.
822
+ *
823
+ * The same contract the React binding declares: the field's resolved label, so a host's
824
+ * camera sheet can say what it is looking for, and its data path, so a host can meter
825
+ * one field's scans.
826
+ *
827
+ * **Nothing about the format is here.** `pattern` is the field's and the engine checks
828
+ * it; a scanner told to pre-filter would be a second validator drifting from the first.
829
+ */
830
+ interface ScanRequest {
831
+ /** The field's label, resolved through the message catalogue. */
832
+ label: string;
833
+ /** The field's data path, e.g. `serial` or `items[1].serial`. */
834
+ path: string;
835
+ }
836
+ /**
837
+ * Reads a code and reports the text on it.
838
+ *
839
+ * The camera, the permission prompt, the viewfinder and the decoding all belong to
840
+ * whoever mounted the form ([0071](../../../docs/decisions/0071-a-scanner-is-supplied-not-built.md)).
841
+ *
842
+ * **Resolving with `null` means nobody scanned anything** — the sheet was closed. Not a
843
+ * failure, and the field says nothing about it. **Rejecting means the device did not
844
+ * work**, and the field says so in its status region rather than its error region,
845
+ * because a hardware problem is not a wrong answer.
846
+ *
847
+ * **A scanner may not pre-validate**: a value the field's `pattern` refuses is still
848
+ * what the camera read, and dropping it would leave the field looking empty.
849
+ */
850
+ type Scanner = (request: ScanRequest) => Promise<string | null>;
851
+ /**
852
+ * Optional by design. A `scanner` widget with no scanner behind it renders the ordinary
853
+ * text input and no button — typing was always the field's primary route, so there is
854
+ * nothing to disable and a Scan button that opened nothing would be worse.
855
+ */
856
+ declare const FORMANCY_SCANNER: InjectionToken<Scanner>;
857
+ declare function injectScanner(): Scanner | null;
858
+ /** Providing one, for a host that has a camera and a decoder. */
859
+ declare function provideFormancyScanner(scan: Scanner): {
860
+ provide: InjectionToken<Scanner>;
861
+ useValue: Scanner;
862
+ };
863
+
864
+ export { DEFAULT_FIELD_COMPONENTS, FORMANCY_ENGINE, FORMANCY_FIELD_CONTEXT, FORMANCY_OPTIONS_SOURCES, FORMANCY_REGISTRY, FORMANCY_RICH_TEXT_EDITOR, FORMANCY_SCANNER, FORMANCY_UPLOADER, FormancyCheckboxField, FormancyComponentOutlet, FormancyDateField, FormancyErrorSummary, FormancyFieldShell, FormancyFieldSlot, FormancyForm, FormancyLayout, FormancyNumberField, FormancyRadioGroupField, FormancyRepeaterSection, FormancyResumeNotice, FormancyRichInline, FormancyRichText, FormancySelectField, FormancyTextField, FormancyTextareaField, injectEngine, injectField, injectFieldContext, injectOptionsSources, injectRepeater, injectRichTextEditorFactory, injectScanner, injectSubmit, injectUploader, injectWizard, provideFormancy, provideFormancyOptionsSources, provideFormancyRegistry, provideFormancyRichTextEditor, provideFormancyScanner, provideFormancyUploader };
865
+ export type { FieldBinding, FormancyFieldContext, FormancyRegistry, OptionsRequest, OptionsSource, OptionsSources, RepeaterBinding, ResumeMigration, RichTextEditorFactory, RichTextEditorHandle, RichTextEditorMount, ScanRequest, Scanner, StoredFile, SubmitOutcome, Uploader, WizardBinding };