@formancy/react 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { ComponentType, ReactNode } from "react";
1
+ import { ComponentType, ReactElement, ReactNode } from "react";
2
2
  import { ControlProps, FieldSnapshot, FormEngine, Path } from "@formancy/core";
3
- import { FieldType } from "@formancy/spec";
3
+ import { FieldType, RemoteOption, RichCommand } from "@formancy/spec";
4
4
  //#region src/context.d.ts
5
5
  export declare function FormancyProvider({ engine, children }: {
6
6
  engine: FormEngine;
@@ -43,6 +43,8 @@ interface RepeaterBinding {
43
43
  rowIds: readonly string[];
44
44
  addRow(): void;
45
45
  removeRow(index: number): void;
46
+ /** Move a row. The keyboard route to reordering; a drag is a second route to this. */
47
+ moveRow(from: number, to: number): void;
46
48
  }
47
49
  /**
48
50
  * A repeater's live row list. Renders exactly when rows are added, removed or
@@ -55,6 +57,10 @@ export declare function useRepeater(path: string | Path): RepeaterBinding;
55
57
  interface WizardBinding {
56
58
  page: number;
57
59
  pageCount: number;
60
+ /** Whether there is a live page after this one. Not `page < pageCount - 1`
61
+ * once a page can be walked past: the last live page is not always the last. */
62
+ canGoNext: boolean;
63
+ canGoBack: boolean;
58
64
  /** Resolves true iff the page advanced; a failed validation keeps the page. */
59
65
  next(): Promise<boolean>;
60
66
  back(): void;
@@ -82,7 +88,13 @@ export declare function useSubmit(): () => {
82
88
  errors: Record<string, string[]>;
83
89
  };
84
90
  //#endregion
85
- //#region src/form.d.ts
91
+ //#region src/fields/internals.d.ts
92
+ /**
93
+ * The pieces every control shares: the shell it renders inside, the prop shapes, and the few helpers more than one of them needs.
94
+ *
95
+ * One file per control, because a control changes for its own reasons.
96
+ * These were one 2,154-line file — the largest in the repository.
97
+ */
86
98
  /**
87
99
  * The component registry — theming mechanism number two. The schema decides
88
100
  * WHAT a field is; the registry decides what RENDERS it, with per-path entries
@@ -114,9 +126,30 @@ interface FormancyFormProps {
114
126
  */
115
127
  labels?: Record<string, string>;
116
128
  registry?: Registry;
129
+ /**
130
+ * Render a named entry from the schema's `layouts` instead of model order —
131
+ * `"web"`, `"print"`, whatever the document defines. Unknown or absent, the
132
+ * form falls back to model order, because a mistyped layout name should not
133
+ * produce an empty form.
134
+ */
135
+ layout?: string;
117
136
  submitLabel?: string;
118
137
  onSubmit?: (outcome: SubmitOutcome) => void;
119
138
  }
139
+ //#endregion
140
+ //#region src/form.d.ts
141
+ /**
142
+ * The form, the list that walks it, and which component renders which type.
143
+ *
144
+ * What is left after the controls moved into `fields/`: 2,154 lines that had
145
+ * become the place things went. Three subjects remain here and they are one —
146
+ * a form renders a list, a list renders a slot, a slot may render a repeater,
147
+ * and a repeater renders a list. Mutual recursion is not a seam.
148
+ *
149
+ * `null` in the map is not an omission. `hidden` renders nothing by design, and
150
+ * `group`, `page` and `repeater` are containers the list itself walks rather
151
+ * than controls.
152
+ */
120
153
  /**
121
154
  * Renders the whole form from the engine: one slot per field, resolved through
122
155
  * the registry. Slots subscribe individually, so a keystroke re-renders one
@@ -143,5 +176,299 @@ interface ErrorSummaryProps {
143
176
  */
144
177
  export declare function ErrorSummary({ labels }: ErrorSummaryProps): import("react").JSX.Element | null;
145
178
  //#endregion
146
- export type { ErrorSummaryProps, FieldBinding, FieldComponent, FieldComponentProps, FormancyFormProps, Registry, RepeaterBinding, SubmitOutcome, WizardBinding };
179
+ //#region src/rich-text.d.ts
180
+ /**
181
+ * Showing a `richtext` answer.
182
+ *
183
+ * Every element here is created by React from a typed tree. The stored answer
184
+ * never reaches `dangerouslySetInnerHTML`, and there is no sanitiser, because
185
+ * there is nothing to sanitise: the parser in `@formancy/spec` has already
186
+ * turned the characters somebody typed into `{ kind: 'text' }` nodes, and a
187
+ * text node cannot be an element however it is spelled.
188
+ *
189
+ * That is the whole reason the grammar exists rather than storing HTML. A form
190
+ * answer is written by anyone who can reach the form and read later by an
191
+ * administrator, which is the exact shape of a stored cross-site scripting
192
+ * bug ([0052](../../../docs/decisions/0052-richtext-is-not-html.md)).
193
+ *
194
+ * The Angular renderer builds the same tree into the same elements, from the
195
+ * same parser, so the two cannot disagree about what an answer says.
196
+ */
197
+ export declare function RichText({ source }: {
198
+ source: string;
199
+ }): ReactElement | null;
200
+ //#endregion
201
+ //#region src/options-source.d.ts
202
+ /**
203
+ * What a source is asked for.
204
+ *
205
+ * `kind` is a flat discriminator rather than a union of two interfaces, on the same
206
+ * reasoning the scanner uses: a host written in plain JavaScript must be harmless, and
207
+ * a shape it can read with one `if` is a shape it gets right.
208
+ */
209
+ interface OptionsRequest {
210
+ /**
211
+ * `search` — somebody is typing and wants matching rows.
212
+ * `labels` — the form holds these values already and needs their names.
213
+ */
214
+ kind: 'search' | 'labels';
215
+ /** The name the document gave, which the deployment resolves. */
216
+ source: string;
217
+ /** The field's data path, e.g. `canton` or `people[1].canton`. */
218
+ path: string;
219
+ /** What was typed. Empty on a `labels` request. */
220
+ query: string;
221
+ /** The stored values to name. Empty on a `search`. */
222
+ values: readonly string[];
223
+ /**
224
+ * The locale the form is being resolved in — the engine's, which is the host's own
225
+ * `locale` when it passed one and the document's default otherwise.
226
+ *
227
+ * A remote label is a plain string, never a `{$t}` reference: nothing can check a
228
+ * reference that arrives at runtime, and an unchecked one resolves to nothing and
229
+ * shows an opaque identifier. So the source answers in the right language instead.
230
+ */
231
+ locale: string;
232
+ /** How many rows the control can show. A hint: the control caps what arrives anyway. */
233
+ limit: number;
234
+ /** Aborted when the answer stops being wanted — a newer keystroke, or an unmount. */
235
+ signal: AbortSignal;
236
+ }
237
+ /**
238
+ * Where one named list of options comes from.
239
+ *
240
+ * The third instance of the inversion the uploader and the scanner already are: the
241
+ * document names a list, the deployment says what that name means, and nothing in
242
+ * formancy ever makes a request of its own. A URL in a form document would be a
243
+ * deployment detail in a portable format, unfixable once published, and an SSRF
244
+ * surface on a self-hosted instance.
245
+ */
246
+ interface OptionsSource {
247
+ resolve(request: OptionsRequest): Promise<readonly RemoteOption[]>;
248
+ /** How long to wait after a keystroke before asking. The control has a default. */
249
+ debounceMs?: number;
250
+ /** Below this many characters, do not ask at all. The control says so on screen. */
251
+ minQueryLength?: number;
252
+ /** How many rows to show at once. */
253
+ maxRows?: number;
254
+ }
255
+ /**
256
+ * Every source this deployment has, by the name a document would use.
257
+ *
258
+ * A MAP rather than one resolver function, and that is the one deliberate difference
259
+ * from `Scanner`. The control has to know **synchronously** whether a name resolves,
260
+ * because absence here is the *file field's* branch and not the scanner's: a text
261
+ * field with no scanner still collects the answer by typing, but a select whose
262
+ * options come only from a source collects nothing at all, so it must say so instead
263
+ * of rendering an empty chooser. With a single resolver, absence would only be
264
+ * discoverable by calling it and failing.
265
+ */
266
+ type OptionsSources = Readonly<Record<string, OptionsSource>>;
267
+ export declare const OptionsSourcesProvider: import("react").Provider<Readonly<Record<string, OptionsSource>> | undefined>;
268
+ /**
269
+ * The host's sources, or undefined when there are none.
270
+ *
271
+ * Undefined is a supported state and not a misconfiguration — a form with no sourced
272
+ * field never needs one. What it costs, when a document DOES name a source, is the
273
+ * whole field: it renders a message where the chooser would be, exactly as the file
274
+ * field does without an uploader.
275
+ */
276
+ export declare function useOptionsSources(): OptionsSources | undefined;
277
+ //#endregion
278
+ //#region src/scanning.d.ts
279
+ /**
280
+ * What a scanner is asked to read.
281
+ *
282
+ * The field's resolved label and its data path, and nothing else. A host's camera sheet
283
+ * needs to say what it is looking for, and the path lets a host meter one field's scans.
284
+ *
285
+ * **Nothing about the format is here.** `pattern` is the field's and the engine checks
286
+ * it; a scanner told to pre-filter would be a second validator drifting from the first.
287
+ */
288
+ interface ScanRequest {
289
+ /** The field's label, resolved through the message catalogue. */
290
+ label: string;
291
+ /** The field's data path, e.g. `serial` or `items[1].serial`. */
292
+ path: string;
293
+ }
294
+ /**
295
+ * Reads a code and reports the text on it.
296
+ *
297
+ * The camera, the permission prompt, the viewfinder and the decoding all belong to
298
+ * whoever mounted the form ([0071](../../../docs/decisions/0071-a-scanner-is-supplied-not-built.md)).
299
+ *
300
+ * **Resolving with `null` means nobody scanned anything** — the sheet was closed. That
301
+ * is not a failure and the field says nothing about it.
302
+ *
303
+ * **Rejecting means the device did not work**: permission refused, no camera, a stream
304
+ * that died. The field says so in its status region rather than its error region,
305
+ * because a hardware problem is not a wrong answer.
306
+ *
307
+ * What comes back is stored exactly as typing it would be. **A scanner may not
308
+ * pre-validate**: a value the field's `pattern` refuses is still what the camera read,
309
+ * and dropping it would leave the field looking empty with no record of why.
310
+ */
311
+ type Scanner = (request: ScanRequest) => Promise<string | null>;
312
+ export declare const ScannerProvider: import("react").Provider<Scanner | undefined>;
313
+ /**
314
+ * The host's scanner, or undefined when there is none.
315
+ *
316
+ * Undefined is a supported state. A `scanner` widget with no scanner behind it renders
317
+ * the ordinary text input and no button — typing was always the field's primary route,
318
+ * so there is nothing to disable and a Scan button that opened nothing would be worse.
319
+ */
320
+ export declare function useScanner(): Scanner | undefined;
321
+ //#endregion
322
+ //#region src/uploads.d.ts
323
+ /**
324
+ * Where a file goes, and what the submission remembers about it.
325
+ *
326
+ * This package has no opinion about the destination: the same field works against local
327
+ * disk, S3 or a customer's own service, and none of them is a dependency of a renderer.
328
+ *
329
+ * **The bytes never pass through the submission.** What is stored is what the file is
330
+ * and where it went, so a submission read back years later is small, readable on its
331
+ * own, and says what was attached even if the object store has since been emptied.
332
+ */
333
+ interface StoredFile {
334
+ /** Stable within the submission; how a row is keyed and removed. */
335
+ id: string;
336
+ name: string;
337
+ size: number;
338
+ contentType: string;
339
+ /** Where the bytes are, in whatever the host's storage calls a location. */
340
+ storageKey: string;
341
+ }
342
+ /**
343
+ * Uploads one file and reports what was stored.
344
+ *
345
+ * **Rejecting is a real answer**: the field says so out loud rather than dropping the
346
+ * file, because a submission somebody believes carries their evidence and does not is
347
+ * the worst outcome available here.
348
+ */
349
+ type Uploader = (file: File) => Promise<StoredFile>;
350
+ export declare const UploaderProvider: import("react").Provider<Uploader | undefined>;
351
+ /**
352
+ * The host's uploader, or undefined when there is none.
353
+ *
354
+ * Undefined is a supported state, not a misconfiguration: a form with no file
355
+ * fields needs no uploader, and a file field without one renders read-only and
356
+ * says why. The alternative — throwing — would turn a form that mostly works
357
+ * into a blank page.
358
+ */
359
+ export declare function useUploader(): Uploader | undefined;
360
+ //#endregion
361
+ //#region src/rich-text-editor.d.ts
362
+ /**
363
+ * A rich-text editing surface, supplied by the host.
364
+ *
365
+ * The same split as the uploader, for the same reason. A contenteditable editor
366
+ * means ProseMirror, and ProseMirror is larger than this entire package — so a
367
+ * form with no rich-text field, which is most of them, must not pay for one.
368
+ * The host passes a factory; without one the field is a textarea with a toolbar,
369
+ * which is a working editor and not a degraded mode
370
+ * ([0061](../../../docs/decisions/0061-tiptap-over-the-closed-grammar.md)).
371
+ *
372
+ * What crosses this boundary is **the stored grammar in both directions, never
373
+ * markup**. That is the whole reason an editor is admissible: nothing on either
374
+ * side of this interface holds a string of HTML, so no consumer of an answer
375
+ * becomes a sanitiser ([0052](../../../docs/decisions/0052-richtext-is-not-html.md)).
376
+ * A factory that returned HTML here would be the bug this design exists to
377
+ * prevent, which is why the type says `string` and the documentation says which
378
+ * string.
379
+ */
380
+ interface RichTextEditorHandle {
381
+ /** The stored answer, in the grammar. */
382
+ value: () => string;
383
+ /** Replace the content when the form's value changes underneath the editor. */
384
+ setValue: (value: string) => void;
385
+ /**
386
+ * Run a formatting command, so the field's own toolbar keeps working.
387
+ *
388
+ * Needed because an editor library brings keyboard shortcuts and no toolbar
389
+ * UI. Leaving the field's toolbar out made Bold reachable by Ctrl+B and by no
390
+ * visible control, which is a regression against the `<textarea>` it replaced
391
+ * and unusable for anybody who does not know the shortcut.
392
+ *
393
+ * The commands are the grammar's whole surface — the same `RichCommand`
394
+ * values the textarea's toolbar uses — so one toolbar drives either surface
395
+ * and the two cannot offer different things.
396
+ */
397
+ run: (command: RichCommand, href?: string) => void;
398
+ /** Whether the command is on at the caret, for the toolbar's pressed state. */
399
+ isActive: (command: RichCommand) => boolean;
400
+ destroy: () => void;
401
+ }
402
+ interface RichTextEditorMount {
403
+ /** Where to mount. The host's factory owns what it puts inside. */
404
+ readonly element: Element;
405
+ /** The stored answer to open with, in the grammar. */
406
+ readonly value: string;
407
+ /** Called with the new stored answer, in the grammar. */
408
+ readonly onChange: (value: string) => void;
409
+ readonly editable: boolean;
410
+ /**
411
+ * Attributes for the editing surface itself, so the ENGINE keeps owning the
412
+ * accessibility wiring.
413
+ *
414
+ * The ids in here are minted by the engine and composed centrally, which is
415
+ * what makes `aria-describedby` correct in both renderers rather than in
416
+ * three separate implementations. An editor package that invented its own
417
+ * labelling would be the fourth implementation, and the one nobody tests.
418
+ */
419
+ readonly attributes: Readonly<Record<string, string>>;
420
+ }
421
+ type RichTextEditorFactory = (mount: RichTextEditorMount) => RichTextEditorHandle;
422
+ export declare const RichTextEditorProvider: import("react").Provider<RichTextEditorFactory | undefined>;
423
+ /**
424
+ * The host's editor factory, or undefined when there is none.
425
+ *
426
+ * Undefined is a supported state and the default one. Unlike the uploader — where
427
+ * its absence makes a file field read-only, because there is nowhere to put the
428
+ * bytes — its absence here costs nothing but the WYSIWYG surface: the answer is
429
+ * still editable, still valid, and still the same grammar.
430
+ */
431
+ export declare function useRichTextEditorFactory(): RichTextEditorFactory | undefined;
432
+ //#endregion
433
+ //#region src/resume-notice.d.ts
434
+ /**
435
+ * Telling somebody their draft came back changed.
436
+ *
437
+ * The server already does the careful half. A republished form migrates a draft
438
+ * lazily on resume, and answers whose field no longer exists move to
439
+ * `data.__orphaned` rather than being deleted
440
+ * ([0027](../../../docs/decisions/0027-lazy-draft-migration.md)). It reports what
441
+ * happened as a severity and a list of changes.
442
+ *
443
+ * This is what shows it. Without it somebody resumes a draft, some of their answers are
444
+ * no longer on the form, and they submit believing everything they typed is in it — the
445
+ * answers are not lost from storage, they are lost from the submission.
446
+ *
447
+ * It is built like the error summary, and for the same reason: something important
448
+ * happened and the person may be looking at the middle of a long form. The container
449
+ * takes focus through `tabindex="-1"`, and is **not** `role="alert"` and carries no
450
+ * `aria-live` — focusing a container already makes a screen reader announce it, and
451
+ * doing both announces it twice.
452
+ *
453
+ * **A library cannot make a host render it.** Resuming a draft is the host's call, so
454
+ * this component exists and is documented, and showing it is the deployment's
455
+ * responsibility. `SAFETY-ANALYSIS.md` says so rather than claiming a guarantee.
456
+ */
457
+ /** What `resumeDraft` reports. Structural, so a host need not import a type. */
458
+ interface ResumeMigration {
459
+ readonly severity: 'lossy' | 'breaking';
460
+ readonly changes: ReadonlyArray<{
461
+ readonly kind: string;
462
+ readonly path?: string;
463
+ }>;
464
+ }
465
+ interface ResumeNoticeProps {
466
+ /** Omitted or undefined when the draft came back unchanged. */
467
+ readonly migration?: ResumeMigration | undefined;
468
+ /** Question wording for a field key, since a key is not what the form asked. */
469
+ readonly labels?: Readonly<Record<string, string>>;
470
+ }
471
+ export declare function ResumeNotice({ migration, labels }: ResumeNoticeProps): ReactElement | null;
472
+ //#endregion
473
+ export type { ErrorSummaryProps, FieldBinding, FieldComponent, FieldComponentProps, FormancyFormProps, OptionsRequest, OptionsSource, OptionsSources, Registry, RepeaterBinding, ResumeMigration, ResumeNoticeProps, RichTextEditorFactory, RichTextEditorHandle, RichTextEditorMount, ScanRequest, Scanner, StoredFile, SubmitOutcome, Uploader, WizardBinding };
147
474
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/context.tsx","../src/use-field.ts","../src/use-repeater.ts","../src/use-wizard.ts","../src/use-submit.ts","../src/form.tsx","../src/error-summary.tsx"],"mappings":";;;;wBAYgB,mBAAmB,QAAQ;EAAc,QAAQ;EAAY,WAAW;oBAAW,IAAA;wBAInF,iBAAiB;;;UCXhB,qBAAqB;EACpC,SAAS;EACT;;EAEA,cAAc;;EAEd;IAAc;IAAY;;;EAE1B;IAAc;;;;;;;;;;;;;wBAaA,SAAS,eAAe,OAAO;;;UCrB9B;EACf;;EAEA;EACA;EACA,UAAU;;;;;;;wBAQI,YAAY,eAAe,OAAO;;;UCfjC;EACf;EACA;;EAEA,QAAQ;EACR;;EAEA,KAAK;;;;;;;wBAQS,aAAa;;;;;;;;;;;wBCNb;EAAqB;EAAa,QAAQ;;;;;;;;;;;UCKzC;EACf;EACA;;KAGU,iBAAiB,cAAc;UAE1B;EACf,SAAS,QAAQ,OAAO,WAAW;EACnC,SAAS,eAAe;;UAGT;EACf;EACA,QAAQ;;EAER;;UAGe;;;;;;;EAOf,SAAS;EACT,WAAW;EACX;EACA,YAAY,SAAS;;;;;;;;;;;wBAYP,aAAa,OAAO,oCAAiB,IAAA;;;UCtDpC;EACf,SAAS;;;;;;;;;;;wBAYK,eAAe,UAAU,oCAAiB,IAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/context.tsx","../src/use-field.ts","../src/use-repeater.ts","../src/use-wizard.ts","../src/use-submit.ts","../src/fields/internals.tsx","../src/form.tsx","../src/error-summary.tsx","../src/rich-text.tsx","../src/options-source.ts","../src/scanning.ts","../src/uploads.ts","../src/rich-text-editor.ts","../src/resume-notice.tsx"],"mappings":";;;;wBAYgB,mBAAmB,QAAQ;EAAc,QAAQ;EAAY,WAAW;oBAAW,IAAA;wBAInF,iBAAiB;;;UCXhB,qBAAqB;EACpC,SAAS;EACT;;EAEA,cAAc;;EAEd;IAAc;IAAY;;;EAE1B;IAAc;;;;;;;;;;;;;wBAaA,SAAS,eAAe,OAAO;;;UCrB9B;EACf;;EAEA;EACA;EACA,UAAU;;EAEV,QAAQ,cAAc;;;;;;;wBAQR,YAAY,eAAe,OAAO;;;UCjBjC;EACf;EACA;;;EAGA;EACA;;EAEA,QAAQ;EACR;;EAEA,KAAK;;;;;;;wBAQS,aAAa;;;;;;;;;;;wBCTb;EAAqB;EAAa,QAAQ;;;;;;;;;;;;;;;;;UCOzC;EACf;EACA;;KAGU,iBAAiB,cAAc;UAE1B;EACf,SAAS,QAAQ,OAAO,WAAW;EACnC,SAAS,eAAe;;UAGT;EACf;EACA,QAAQ;;EAER;;UAGe;;;;;;;EAOf,SAAS;EACT,WAAW;;;;;;;EAOX;EACA;EACA,YAAY,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;wBCHP,aAAa,OAAO,oCAAiB,IAAA;;;UChDpC;EACf,SAAS;;;;;;;;;;;wBAYK,eAAe,UAAU,oCAAiB,IAAA;;;;;;;;;;;;;;;;;;;;wBCI1C,WAAW;EAAY;IAAmB;;;;;;;;;;UCZzC;;;;;EAKf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;EASA;;EAEA;;EAEA,QAAQ;;;;;;;;;;;UAYO;EACf,QAAQ,SAAS,iBAAiB,iBAAiB;;EAEnD;;EAEA;;EAEA;;;;;;;;;;;;;KAcU,iBAAiB,SAAS,eAAe;qBAIxC,wCAAsB,SAAA,SAAA,eAAA;;;;;;;;;wBAUnB,qBAAqB;;;;;;;;;;;;UCxEpB;;EAEf;;EAEA;;;;;;;;;;;;;;;;;;;KAoBU,WAAW,SAAS,gBAAgB;qBAInC,iCAAe,SAAA;;;;;;;;wBASZ,cAAc;;;;;;;;;;;;;UCpCb;;EAEf;EACA;EACA;EACA;;EAEA;;;;;;;;;KAUU,YAAY,MAAM,SAAS,QAAQ;qBAIlC,kCAAgB,SAAA;;;;;;;;;wBAUb,eAAe;;;;;;;;;;;;;;;;;;;;;UCtBd;;EAEf;;EAEA,WAAW;;;;;;;;;;;;;EAaX,MAAM,SAAS,aAAa;;EAE5B,WAAW,SAAS;EACpB;;UAGe;;WAEN,SAAS;;WAET;;WAEA,WAAW;WACX;;;;;;;;;;WAUA,YAAY,SAAS;;KAGpB,yBAAyB,OAAO,wBAAwB;qBAIvD,wCAAsB,SAAA;;;;;;;;;wBAUnB,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;UClD3B;WACN;WACA,SAAS;aAAyB;aAAuB;;;UAGnD;;WAEN,YAAY;;WAEZ,SAAS,SAAS;;wBAGb,eAAe,WAAW,UAAU,oBAAoB"}