@formancy/react 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.
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
@@ -114,6 +116,13 @@ interface FormancyFormProps {
114
116
  */
115
117
  labels?: Record<string, string>;
116
118
  registry?: Registry;
119
+ /**
120
+ * Render a named entry from the schema's `layouts` instead of model order —
121
+ * `"web"`, `"print"`, whatever the document defines. Unknown or absent, the
122
+ * form falls back to model order, because a mistyped layout name should not
123
+ * produce an empty form.
124
+ */
125
+ layout?: string;
117
126
  submitLabel?: string;
118
127
  onSubmit?: (outcome: SubmitOutcome) => void;
119
128
  }
@@ -143,5 +152,299 @@ interface ErrorSummaryProps {
143
152
  */
144
153
  export declare function ErrorSummary({ labels }: ErrorSummaryProps): import("react").JSX.Element | null;
145
154
  //#endregion
146
- export type { ErrorSummaryProps, FieldBinding, FieldComponent, FieldComponentProps, FormancyFormProps, Registry, RepeaterBinding, SubmitOutcome, WizardBinding };
155
+ //#region src/rich-text.d.ts
156
+ /**
157
+ * Showing a `richtext` answer.
158
+ *
159
+ * Every element here is created by React from a typed tree. The stored answer
160
+ * never reaches `dangerouslySetInnerHTML`, and there is no sanitiser, because
161
+ * there is nothing to sanitise: the parser in `@formancy/spec` has already
162
+ * turned the characters somebody typed into `{ kind: 'text' }` nodes, and a
163
+ * text node cannot be an element however it is spelled.
164
+ *
165
+ * That is the whole reason the grammar exists rather than storing HTML. A form
166
+ * answer is written by anyone who can reach the form and read later by an
167
+ * administrator, which is the exact shape of a stored cross-site scripting
168
+ * bug ([0052](../../../docs/decisions/0052-richtext-is-not-html.md)).
169
+ *
170
+ * The Angular renderer builds the same tree into the same elements, from the
171
+ * same parser, so the two cannot disagree about what an answer says.
172
+ */
173
+ export declare function RichText({ source }: {
174
+ source: string;
175
+ }): ReactElement | null;
176
+ //#endregion
177
+ //#region src/options-source.d.ts
178
+ /**
179
+ * What a source is asked for.
180
+ *
181
+ * `kind` is a flat discriminator rather than a union of two interfaces, on the same
182
+ * reasoning the scanner uses: a host written in plain JavaScript must be harmless, and
183
+ * a shape it can read with one `if` is a shape it gets right.
184
+ */
185
+ interface OptionsRequest {
186
+ /**
187
+ * `search` — somebody is typing and wants matching rows.
188
+ * `labels` — the form holds these values already and needs their names.
189
+ */
190
+ kind: 'search' | 'labels';
191
+ /** The name the document gave, which the deployment resolves. */
192
+ source: string;
193
+ /** The field's data path, e.g. `canton` or `people[1].canton`. */
194
+ path: string;
195
+ /** What was typed. Empty on a `labels` request. */
196
+ query: string;
197
+ /** The stored values to name. Empty on a `search`. */
198
+ values: readonly string[];
199
+ /**
200
+ * The locale the form is being resolved in — the engine's, which is the host's own
201
+ * `locale` when it passed one and the document's default otherwise.
202
+ *
203
+ * A remote label is a plain string, never a `{$t}` reference: nothing can check a
204
+ * reference that arrives at runtime, and an unchecked one resolves to nothing and
205
+ * shows an opaque identifier. So the source answers in the right language instead.
206
+ */
207
+ locale: string;
208
+ /** How many rows the control can show. A hint: the control caps what arrives anyway. */
209
+ limit: number;
210
+ /** Aborted when the answer stops being wanted — a newer keystroke, or an unmount. */
211
+ signal: AbortSignal;
212
+ }
213
+ /**
214
+ * Where one named list of options comes from.
215
+ *
216
+ * The third instance of the inversion the uploader and the scanner already are: the
217
+ * document names a list, the deployment says what that name means, and nothing in
218
+ * formancy ever makes a request of its own. A URL in a form document would be a
219
+ * deployment detail in a portable format, unfixable once published, and an SSRF
220
+ * surface on a self-hosted instance.
221
+ */
222
+ interface OptionsSource {
223
+ resolve(request: OptionsRequest): Promise<readonly RemoteOption[]>;
224
+ /** How long to wait after a keystroke before asking. The control has a default. */
225
+ debounceMs?: number;
226
+ /** Below this many characters, do not ask at all. The control says so on screen. */
227
+ minQueryLength?: number;
228
+ /** How many rows to show at once. */
229
+ maxRows?: number;
230
+ }
231
+ /**
232
+ * Every source this deployment has, by the name a document would use.
233
+ *
234
+ * A MAP rather than one resolver function, and that is the one deliberate difference
235
+ * from `Scanner`. The control has to know **synchronously** whether a name resolves,
236
+ * because absence here is the *file field's* branch and not the scanner's: a text
237
+ * field with no scanner still collects the answer by typing, but a select whose
238
+ * options come only from a source collects nothing at all, so it must say so instead
239
+ * of rendering an empty chooser. With a single resolver, absence would only be
240
+ * discoverable by calling it and failing.
241
+ */
242
+ type OptionsSources = Readonly<Record<string, OptionsSource>>;
243
+ export declare const OptionsSourcesProvider: import("react").Provider<Readonly<Record<string, OptionsSource>> | undefined>;
244
+ /**
245
+ * The host's sources, or undefined when there are none.
246
+ *
247
+ * Undefined is a supported state and not a misconfiguration — a form with no sourced
248
+ * field never needs one. What it costs, when a document DOES name a source, is the
249
+ * whole field: it renders a message where the chooser would be, exactly as the file
250
+ * field does without an uploader.
251
+ */
252
+ export declare function useOptionsSources(): OptionsSources | undefined;
253
+ //#endregion
254
+ //#region src/scanning.d.ts
255
+ /**
256
+ * What a scanner is asked to read.
257
+ *
258
+ * The field's resolved label and its data path, and nothing else. A host's camera sheet
259
+ * needs to say what it is looking for, and the path lets a host meter one field's scans.
260
+ *
261
+ * **Nothing about the format is here.** `pattern` is the field's and the engine checks
262
+ * it; a scanner told to pre-filter would be a second validator drifting from the first.
263
+ */
264
+ interface ScanRequest {
265
+ /** The field's label, resolved through the message catalogue. */
266
+ label: string;
267
+ /** The field's data path, e.g. `serial` or `items[1].serial`. */
268
+ path: string;
269
+ }
270
+ /**
271
+ * Reads a code and reports the text on it.
272
+ *
273
+ * The camera, the permission prompt, the viewfinder and the decoding all belong to
274
+ * whoever mounted the form ([0071](../../../docs/decisions/0071-a-scanner-is-supplied-not-built.md)).
275
+ *
276
+ * **Resolving with `null` means nobody scanned anything** — the sheet was closed. That
277
+ * is not a failure and the field says nothing about it.
278
+ *
279
+ * **Rejecting means the device did not work**: permission refused, no camera, a stream
280
+ * that died. The field says so in its status region rather than its error region,
281
+ * because a hardware problem is not a wrong answer.
282
+ *
283
+ * What comes back is stored exactly as typing it would be. **A scanner may not
284
+ * pre-validate**: a value the field's `pattern` refuses is still what the camera read,
285
+ * and dropping it would leave the field looking empty with no record of why.
286
+ */
287
+ type Scanner = (request: ScanRequest) => Promise<string | null>;
288
+ export declare const ScannerProvider: import("react").Provider<Scanner | undefined>;
289
+ /**
290
+ * The host's scanner, or undefined when there is none.
291
+ *
292
+ * Undefined is a supported state. A `scanner` widget with no scanner behind it renders
293
+ * the ordinary text input and no button — typing was always the field's primary route,
294
+ * so there is nothing to disable and a Scan button that opened nothing would be worse.
295
+ */
296
+ export declare function useScanner(): Scanner | undefined;
297
+ //#endregion
298
+ //#region src/uploads.d.ts
299
+ /**
300
+ * Where a file goes, and what the submission remembers about it.
301
+ *
302
+ * This package has no opinion about the destination: the same field works against local
303
+ * disk, S3 or a customer's own service, and none of them is a dependency of a renderer.
304
+ *
305
+ * **The bytes never pass through the submission.** What is stored is what the file is
306
+ * and where it went, so a submission read back years later is small, readable on its
307
+ * own, and says what was attached even if the object store has since been emptied.
308
+ */
309
+ interface StoredFile {
310
+ /** Stable within the submission; how a row is keyed and removed. */
311
+ id: string;
312
+ name: string;
313
+ size: number;
314
+ contentType: string;
315
+ /** Where the bytes are, in whatever the host's storage calls a location. */
316
+ storageKey: string;
317
+ }
318
+ /**
319
+ * Uploads one file and reports what was stored.
320
+ *
321
+ * **Rejecting is a real answer**: the field says so out loud rather than dropping the
322
+ * file, because a submission somebody believes carries their evidence and does not is
323
+ * the worst outcome available here.
324
+ */
325
+ type Uploader = (file: File) => Promise<StoredFile>;
326
+ export declare const UploaderProvider: import("react").Provider<Uploader | undefined>;
327
+ /**
328
+ * The host's uploader, or undefined when there is none.
329
+ *
330
+ * Undefined is a supported state, not a misconfiguration: a form with no file
331
+ * fields needs no uploader, and a file field without one renders read-only and
332
+ * says why. The alternative — throwing — would turn a form that mostly works
333
+ * into a blank page.
334
+ */
335
+ export declare function useUploader(): Uploader | undefined;
336
+ //#endregion
337
+ //#region src/rich-text-editor.d.ts
338
+ /**
339
+ * A rich-text editing surface, supplied by the host.
340
+ *
341
+ * The same split as the uploader, for the same reason. A contenteditable editor
342
+ * means ProseMirror, and ProseMirror is larger than this entire package — so a
343
+ * form with no rich-text field, which is most of them, must not pay for one.
344
+ * The host passes a factory; without one the field is a textarea with a toolbar,
345
+ * which is a working editor and not a degraded mode
346
+ * ([0061](../../../docs/decisions/0061-tiptap-over-the-closed-grammar.md)).
347
+ *
348
+ * What crosses this boundary is **the stored grammar in both directions, never
349
+ * markup**. That is the whole reason an editor is admissible: nothing on either
350
+ * side of this interface holds a string of HTML, so no consumer of an answer
351
+ * becomes a sanitiser ([0052](../../../docs/decisions/0052-richtext-is-not-html.md)).
352
+ * A factory that returned HTML here would be the bug this design exists to
353
+ * prevent, which is why the type says `string` and the documentation says which
354
+ * string.
355
+ */
356
+ interface RichTextEditorHandle {
357
+ /** The stored answer, in the grammar. */
358
+ value: () => string;
359
+ /** Replace the content when the form's value changes underneath the editor. */
360
+ setValue: (value: string) => void;
361
+ /**
362
+ * Run a formatting command, so the field's own toolbar keeps working.
363
+ *
364
+ * Needed because an editor library brings keyboard shortcuts and no toolbar
365
+ * UI. Leaving the field's toolbar out made Bold reachable by Ctrl+B and by no
366
+ * visible control, which is a regression against the `<textarea>` it replaced
367
+ * and unusable for anybody who does not know the shortcut.
368
+ *
369
+ * The commands are the grammar's whole surface — the same `RichCommand`
370
+ * values the textarea's toolbar uses — so one toolbar drives either surface
371
+ * and the two cannot offer different things.
372
+ */
373
+ run: (command: RichCommand, href?: string) => void;
374
+ /** Whether the command is on at the caret, for the toolbar's pressed state. */
375
+ isActive: (command: RichCommand) => boolean;
376
+ destroy: () => void;
377
+ }
378
+ interface RichTextEditorMount {
379
+ /** Where to mount. The host's factory owns what it puts inside. */
380
+ readonly element: Element;
381
+ /** The stored answer to open with, in the grammar. */
382
+ readonly value: string;
383
+ /** Called with the new stored answer, in the grammar. */
384
+ readonly onChange: (value: string) => void;
385
+ readonly editable: boolean;
386
+ /**
387
+ * Attributes for the editing surface itself, so the ENGINE keeps owning the
388
+ * accessibility wiring.
389
+ *
390
+ * The ids in here are minted by the engine and composed centrally, which is
391
+ * what makes `aria-describedby` correct in both renderers rather than in
392
+ * three separate implementations. An editor package that invented its own
393
+ * labelling would be the fourth implementation, and the one nobody tests.
394
+ */
395
+ readonly attributes: Readonly<Record<string, string>>;
396
+ }
397
+ type RichTextEditorFactory = (mount: RichTextEditorMount) => RichTextEditorHandle;
398
+ export declare const RichTextEditorProvider: import("react").Provider<RichTextEditorFactory | undefined>;
399
+ /**
400
+ * The host's editor factory, or undefined when there is none.
401
+ *
402
+ * Undefined is a supported state and the default one. Unlike the uploader — where
403
+ * its absence makes a file field read-only, because there is nowhere to put the
404
+ * bytes — its absence here costs nothing but the WYSIWYG surface: the answer is
405
+ * still editable, still valid, and still the same grammar.
406
+ */
407
+ export declare function useRichTextEditorFactory(): RichTextEditorFactory | undefined;
408
+ //#endregion
409
+ //#region src/resume-notice.d.ts
410
+ /**
411
+ * Telling somebody their draft came back changed.
412
+ *
413
+ * The server already does the careful half. A republished form migrates a draft
414
+ * lazily on resume, and answers whose field no longer exists move to
415
+ * `data.__orphaned` rather than being deleted
416
+ * ([0027](../../../docs/decisions/0027-lazy-draft-migration.md)). It reports what
417
+ * happened as a severity and a list of changes.
418
+ *
419
+ * This is what shows it. Without it somebody resumes a draft, some of their answers are
420
+ * no longer on the form, and they submit believing everything they typed is in it — the
421
+ * answers are not lost from storage, they are lost from the submission.
422
+ *
423
+ * It is built like the error summary, and for the same reason: something important
424
+ * happened and the person may be looking at the middle of a long form. The container
425
+ * takes focus through `tabindex="-1"`, and is **not** `role="alert"` and carries no
426
+ * `aria-live` — focusing a container already makes a screen reader announce it, and
427
+ * doing both announces it twice.
428
+ *
429
+ * **A library cannot make a host render it.** Resuming a draft is the host's call, so
430
+ * this component exists and is documented, and showing it is the deployment's
431
+ * responsibility. `SAFETY-ANALYSIS.md` says so rather than claiming a guarantee.
432
+ */
433
+ /** What `resumeDraft` reports. Structural, so a host need not import a type. */
434
+ interface ResumeMigration {
435
+ readonly severity: 'lossy' | 'breaking';
436
+ readonly changes: ReadonlyArray<{
437
+ readonly kind: string;
438
+ readonly path?: string;
439
+ }>;
440
+ }
441
+ interface ResumeNoticeProps {
442
+ /** Omitted or undefined when the draft came back unchanged. */
443
+ readonly migration?: ResumeMigration | undefined;
444
+ /** Question wording for a field key, since a key is not what the form asked. */
445
+ readonly labels?: Readonly<Record<string, string>>;
446
+ }
447
+ export declare function ResumeNotice({ migration, labels }: ResumeNoticeProps): ReactElement | null;
448
+ //#endregion
449
+ export type { ErrorSummaryProps, FieldBinding, FieldComponent, FieldComponentProps, FormancyFormProps, OptionsRequest, OptionsSource, OptionsSources, Registry, RepeaterBinding, ResumeMigration, ResumeNoticeProps, RichTextEditorFactory, RichTextEditorHandle, RichTextEditorMount, ScanRequest, Scanner, StoredFile, SubmitOutcome, Uploader, WizardBinding };
147
450
  //# 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/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;;EAEA,QAAQ;EACR;;EAEA,KAAK;;;;;;;wBAQS,aAAa;;;;;;;;;;;wBCLb;EAAqB;EAAa,QAAQ;;;;;;;;;;;UCgBzC;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;;;;;;;;;;;wBAYP,aAAa,OAAO,oCAAiB,IAAA;;;UCxEpC;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"}