@formancy/react 0.5.0 → 0.6.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,7 +1,7 @@
1
1
  import { ComponentType, ReactElement, ReactNode } from "react";
2
2
  import { ControlProps, FieldSnapshot, FormEngine, Path } from "@formancy/core";
3
3
  import { FormText, FormText as FormText$1, FormWordId, FormWordValues, FormWords, FormWordsByLocale, FormWordsByLocale as FormWordsByLocale$1 } from "@formancy/core/words";
4
- import { FieldType, RemoteOption, RichCommand } from "@formancy/spec";
4
+ import { FieldType, RemoteOption, RemoteOptionsAnswer, RemoteOptionsAnswer as RemoteOptionsAnswer$1, RemoteOptionsPage, RichCommand } from "@formancy/spec";
5
5
  //#region src/context.d.ts
6
6
  export declare function FormancyProvider({ engine, words, children }: {
7
7
  engine: FormEngine;
@@ -58,6 +58,10 @@ interface RepeaterBinding {
58
58
  rowCount: number;
59
59
  /** Stable per-row keys, in row order. Never key a row by its index. */
60
60
  rowIds: readonly string[];
61
+ /** Whether the engine would add a row: under the repeater's `maxItems`, or unbounded. */
62
+ canAdd: boolean;
63
+ /** Whether the engine would remove a row: above the repeater's `minItems`. */
64
+ canRemove: boolean;
61
65
  addRow(): void;
62
66
  removeRow(index: number): void;
63
67
  /** Move a row. The keyboard route to reordering; a drag is a second route to this. */
@@ -246,7 +250,13 @@ interface OptionsRequest {
246
250
  * shows an opaque identifier. So the source answers in the right language instead.
247
251
  */
248
252
  locale: string;
249
- /** How many rows the control can show. A hint: the control caps what arrives anyway. */
253
+ /**
254
+ * How many rows the control can show. A hint: the control caps what arrives anyway.
255
+ *
256
+ * A source that pages reads `limit + 1` rows to know whether there are more, answers a
257
+ * page of `limit`, and never counts the rest: a count reads every matching row on every
258
+ * keystroke, which is the scan paging exists to avoid.
259
+ */
250
260
  limit: number;
251
261
  /** Aborted when the answer stops being wanted — a newer keystroke, or an unmount. */
252
262
  signal: AbortSignal;
@@ -261,10 +271,26 @@ interface OptionsRequest {
261
271
  * surface on a self-hosted instance.
262
272
  */
263
273
  interface OptionsSource {
264
- resolve(request: OptionsRequest): Promise<readonly RemoteOption[]>;
274
+ /**
275
+ * Every matching row, as a list — exactly as before a page could be answered, its true
276
+ * total included — or one page of them: `{ options, more, withheld? }`.
277
+ *
278
+ * `more` says rows match beyond these, and the control says so without a number, since
279
+ * nobody counted one. `withheld` counts rows the source matched and cannot hand over — a
280
+ * value a select could not store — because a source withholds and counts rather than
281
+ * filtering silently. A page with any other key, a `more` that is not a boolean or a
282
+ * `withheld` that is not a whole count is refused whole, and the field says its source
283
+ * failed rather than showing a list that looks complete
284
+ * ([0177](../../../docs/decisions/0177-a-source-may-say-its-list-goes-on.md)).
285
+ */
286
+ resolve(request: OptionsRequest): Promise<RemoteOptionsAnswer$1>;
265
287
  /** How long to wait after a keystroke before asking. The control has a default. */
266
288
  debounceMs?: number;
267
- /** Below this many characters, do not ask at all. The control says so on screen. */
289
+ /**
290
+ * Below this many characters, do not ask at all. The control says so on screen: a
291
+ * typeahead how many to type, and a plain select — whose query is always empty, so it
292
+ * never asks — that its answers are found by searching.
293
+ */
268
294
  minQueryLength?: number;
269
295
  /** How many rows to show at once. */
270
296
  maxRows?: number;
@@ -509,5 +535,5 @@ interface ResumeNoticeProps {
509
535
  }
510
536
  export declare function ResumeNotice({ migration, labels }: ResumeNoticeProps): ReactElement | null;
511
537
  //#endregion
512
- export type { ErrorSummaryProps, FieldBinding, FieldComponent, FieldComponentProps, FormText, FormWordId, FormWordValues, FormWords, FormWordsByLocale, FormancyFormProps, OptionsRequest, OptionsSource, OptionsSources, Registry, RepeaterBinding, ResumeMigration, ResumeNoticeProps, RichTextEditorFactory, RichTextEditorHandle, RichTextEditorMount, ScanRequest, Scanner, StoredFile, SubmitOutcome, UploadOptions, Uploader, WizardBinding };
538
+ export type { ErrorSummaryProps, FieldBinding, FieldComponent, FieldComponentProps, FormText, FormWordId, FormWordValues, FormWords, FormWordsByLocale, FormancyFormProps, OptionsRequest, OptionsSource, OptionsSources, Registry, RemoteOption, RemoteOptionsAnswer, RemoteOptionsPage, RepeaterBinding, ResumeMigration, ResumeNoticeProps, RichTextEditorFactory, RichTextEditorHandle, RichTextEditorMount, ScanRequest, Scanner, StoredFile, SubmitOutcome, UploadOptions, Uploader, WizardBinding };
513
539
  //# 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/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":";;;;;wBAsBgB,mBACd,QACA,OACA;EAEA,QAAQ;;;;;;;EAOR,QAAQ;EACR,WAAW;oBACZ,IAAA;wBAsBe,iBAAiB;;;;;;;;;wBAkBjB,eAAe;;;UCvEd,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;;;;;;;;;;;;;;;;;UCQzC;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;;;;;;;;;;;;;;;;;;;;;;;;;wBCAP,aAAa,OAAO,oCAAiB,IAAA;;;UCpDpC;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;;;;;;;;;;UAWe;;;;;EAKf;;EAEA,QAAQ;;EAER,WAAW,cAAc;;;;;;;;;KAUf,YAAY,MAAM,MAAM,SAAS,kBAAkB,QAAQ;qBAI1D,kCAAgB,SAAA;;;;;;;;;wBAUb,eAAe;;;;;;;;;;;;;;;;;;;;;UC1Cd;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UC9C3B;WACN;WACA,SAAS;aAAyB;aAAuB;;;UAGnD;;WAEN,YAAY;;WAEZ,SAAS,SAAS;;wBAGb,eAAe,WAAW,UAAU,oBAAoB"}
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":";;;;;wBAsBgB,mBACd,QACA,OACA;EAEA,QAAQ;;;;;;;EAOR,QAAQ;EACR,WAAW;oBACZ,IAAA;wBAsBe,iBAAiB;;;;;;;;;wBAkBjB,eAAe;;;UCvEd,qBAAqB;EACpC,SAAS;EACT;;EAEA,cAAc;;EAEd;IAAc;IAAY;;;EAE1B;IAAc;;;;;;;;;;;;;wBAaA,SAAS,eAAe,OAAO;;;UCrB9B;EACf;;EAEA;;EAEA;;EAEA;EACA;EACA,UAAU;;EAEV,QAAQ,cAAc;;;;;;;wBAQR,YAAY,eAAe,OAAO;;;UCrBjC;EACf;EACA;;;EAGA;EACA;;EAEA,QAAQ;EACR;;EAEA,KAAK;;;;;;;wBAQS,aAAa;;;;;;;;;;;wBCTb;EAAqB;EAAa,QAAQ;;;;;;;;;;;;;;;;;UCQzC;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;;;;;;;;;;;;;;;;;;;;;;;;;wBCCP,aAAa,OAAO,oCAAiB,IAAA;;;UCrDpC;EACf,SAAS;;;;;;;;;;;wBAYK,eAAe,UAAU,oCAAiB,IAAA;;;;;;;;;;;;;;;;;;;;wBCI1C,WAAW;EAAY;IAAmB;;;;;;;;;;UCZzC;;;;;EAKf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;EASA;;;;;;;;EAQA;;EAEA,QAAQ;;;;;;;;;;;UAYO;;;;;;;;;;;;;EAaf,QAAQ,SAAS,iBAAiB,QAAQ;;EAE1C;;;;;;EAMA;;EAEA;;;;;;;;;;;;;KAcU,iBAAiB,SAAS,eAAe;qBAIxC,wCAAsB,SAAA,SAAA,eAAA;;;;;;;;;wBAUnB,qBAAqB;;;;;;;;;;;;UC9FpB;;EAEf;;EAEA;;;;;;;;;;;;;;;;;;;KAoBU,WAAW,SAAS,gBAAgB;qBAInC,iCAAe,SAAA;;;;;;;;wBASZ,cAAc;;;;;;;;;;;;;UCpCb;;EAEf;EACA;EACA;EACA;;EAEA;;;;;;;;;;UAWe;;;;;EAKf;;EAEA,QAAQ;;EAER,WAAW,cAAc;;;;;;;;;KAUf,YAAY,MAAM,MAAM,SAAS,kBAAkB,QAAQ;qBAI1D,kCAAgB,SAAA;;;;;;;;;wBAUb,eAAe;;;;;;;;;;;;;;;;;;;;;UC1Cd;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UC9C3B;WACN;WACA,SAAS;aAAyB;aAAuB;;;UAGnD;;WAEN,YAAY;;WAEZ,SAAS,SAAS;;wBAGb,eAAe,WAAW,UAAU,oBAAoB"}
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Fragment, createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
2
  import { cancelUploads, fieldUploads, formatPath, parsePath, placedGroup, placedPage, thumbnailSize } from "@formancy/core";
3
- import { createFormText } from "@formancy/core/words";
4
- import { LAYOUT_LEAF_KINDS, acceptRemoteOptions, answerFromText, applyRichCommand, capRemoteOptions, datagridColumns, editMasked, formatMasked, isImageSource, layoutChildren, layoutNodeShows, maskIsNumeric, maskPlaceholder, narrowOptionsByLabel, parseRichText, resolveText, resolvedLocale } from "@formancy/spec";
3
+ import { createFormText, optionsStatus } from "@formancy/core/words";
4
+ import { LAYOUT_LEAF_KINDS, acceptRemoteAnswer, answerFromText, applyRichCommand, capRemoteAnswer, datagridColumns, editMasked, formatMasked, isImageSource, layoutChildren, layoutNodeShows, maskIsNumeric, maskPlaceholder, narrowOptionsByLabel, parseRichText, resolveText, resolvedLocale } from "@formancy/spec";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { flushSync } from "react-dom";
7
7
  import { encode } from "uqr";
@@ -103,6 +103,10 @@ function useRepeater(path) {
103
103
  const subscribe = useCallback((onChange) => engine.subscribeField(parsed, onChange), [engine, parsed]);
104
104
  const getRowCount = useCallback(() => engine.rowCount(parsed), [engine, parsed]);
105
105
  const rowCount = useSyncExternalStore(subscribe, getRowCount, getRowCount);
106
+ const getCanAdd = useCallback(() => engine.canAddRow(parsed), [engine, parsed]);
107
+ const canAdd = useSyncExternalStore(subscribe, getCanAdd, getCanAdd);
108
+ const getCanRemove = useCallback(() => engine.canRemoveRow(parsed), [engine, parsed]);
109
+ const canRemove = useSyncExternalStore(subscribe, getCanRemove, getCanRemove);
106
110
  const addRow = useCallback(() => engine.addRow(parsed), [engine, parsed]);
107
111
  const removeRow = useCallback((index) => engine.removeRow(parsed, index), [engine, parsed]);
108
112
  const moveRow = useCallback((from, to) => engine.moveRow(parsed, from, to), [engine, parsed]);
@@ -112,12 +116,16 @@ function useRepeater(path) {
112
116
  return useMemo(() => ({
113
117
  rowCount,
114
118
  rowIds,
119
+ canAdd,
120
+ canRemove,
115
121
  addRow,
116
122
  removeRow,
117
123
  moveRow
118
124
  }), [
119
125
  rowCount,
120
126
  rowIds,
127
+ canAdd,
128
+ canRemove,
121
129
  addRow,
122
130
  removeRow,
123
131
  moveRow
@@ -547,9 +555,13 @@ function useOptionsSources() {
547
555
  const DEBOUNCE_MS = 250;
548
556
  const MIN_QUERY = 0;
549
557
  const MAX_ROWS = 50;
558
+ /**
559
+ * Nothing left out, as one value: a reset that stores it again is the same state, so
560
+ * React bails out of the render rather than repeating one.
561
+ */
562
+ const COMPLETE = { kind: "complete" };
550
563
  function useSourcedOptions(field, query, enabled = true) {
551
564
  const engine = useFormEngine();
552
- const text = useFormText();
553
565
  const sources = useOptionsSources();
554
566
  const name = field.def.optionsSource;
555
567
  const source = name === void 0 ? void 0 : sources?.[name];
@@ -557,19 +569,28 @@ function useSourcedOptions(field, query, enabled = true) {
557
569
  const [named, setNamed] = useState(/* @__PURE__ */ new Map());
558
570
  const [busy, setBusy] = useState(false);
559
571
  const [failed, setFailed] = useState(false);
560
- const [capped, setCapped] = useState(null);
572
+ const [completeness, setCompleteness] = useState(COMPLETE);
573
+ const [attempt, setAttempt] = useState(0);
574
+ const retry = useCallback(() => setAttempt((was) => was + 1), []);
561
575
  const debounceMs = source?.debounceMs ?? DEBOUNCE_MS;
562
576
  const minQueryLength = source?.minQueryLength ?? MIN_QUERY;
563
577
  const maxRows = source?.maxRows ?? MAX_ROWS;
564
578
  const stored = typeof field.value === "string" && field.value !== "" ? field.value : void 0;
565
579
  const path = field.path;
580
+ const disabled = field.disabled;
566
581
  const asked = useRef(0);
567
582
  useEffect(() => {
568
- if (!enabled || source === void 0) return;
569
583
  const generation = asked.current += 1;
584
+ if (!enabled || source === void 0) return;
585
+ if (disabled) {
586
+ setBusy(false);
587
+ setFailed(false);
588
+ setCompleteness(COMPLETE);
589
+ return;
590
+ }
570
591
  if (query.trim().length < minQueryLength) {
571
592
  setRows([]);
572
- setCapped(null);
593
+ setCompleteness(COMPLETE);
573
594
  setBusy(false);
574
595
  return;
575
596
  }
@@ -588,23 +609,21 @@ function useSourcedOptions(field, query, enabled = true) {
588
609
  signal: controller.signal
589
610
  }).then((answer) => {
590
611
  if (generation !== asked.current) return;
591
- const accepted = acceptRemoteOptions(answer);
612
+ const accepted = acceptRemoteAnswer(answer);
592
613
  if (accepted === void 0) {
593
614
  setFailed(true);
594
615
  setRows([]);
595
- setCapped(null);
616
+ setCompleteness(COMPLETE);
596
617
  return;
597
618
  }
598
- const { shown, total, capped: wasCapped } = capRemoteOptions(accepted, maxRows);
599
- setRows(shown);
600
- setCapped(wasCapped ? {
601
- shown: shown.length,
602
- total
603
- } : null);
619
+ const settled = capRemoteAnswer(accepted, maxRows);
620
+ setRows(settled.shown);
621
+ setCompleteness(settled.completeness);
604
622
  }).catch(() => {
605
623
  if (generation !== asked.current) return;
606
624
  setFailed(true);
607
625
  setRows([]);
626
+ setCompleteness(COMPLETE);
608
627
  }).finally(() => {
609
628
  if (generation === asked.current) setBusy(false);
610
629
  });
@@ -615,6 +634,7 @@ function useSourcedOptions(field, query, enabled = true) {
615
634
  };
616
635
  }, [
617
636
  enabled,
637
+ disabled,
618
638
  source,
619
639
  name,
620
640
  path,
@@ -622,7 +642,8 @@ function useSourcedOptions(field, query, enabled = true) {
622
642
  minQueryLength,
623
643
  maxRows,
624
644
  debounceMs,
625
- engine
645
+ engine,
646
+ attempt
626
647
  ]);
627
648
  const askedFor = useRef(/* @__PURE__ */ new Set());
628
649
  useEffect(() => {
@@ -640,11 +661,11 @@ function useSourcedOptions(field, query, enabled = true) {
640
661
  limit: 1,
641
662
  signal: controller.signal
642
663
  }).then((answer) => {
643
- const accepted = acceptRemoteOptions(answer);
664
+ const accepted = acceptRemoteAnswer(answer);
644
665
  if (accepted === void 0) return;
645
666
  setNamed((was) => {
646
667
  const next = new Map(was);
647
- for (const row of accepted) next.set(row.value, row.label);
668
+ for (const row of accepted.options) next.set(row.value, row.label);
648
669
  return next;
649
670
  });
650
671
  }).catch(() => void 0);
@@ -677,33 +698,11 @@ function useSourcedOptions(field, query, enabled = true) {
677
698
  busy,
678
699
  failed,
679
700
  needs: minQueryLength,
680
- status: statusFor(text, {
681
- failed,
682
- busy,
683
- needs: minQueryLength,
684
- query,
685
- capped
686
- })
701
+ completeness,
702
+ retry
687
703
  }
688
704
  };
689
705
  }
690
- /**
691
- * The one thing the field says out loud, and never more than one.
692
- *
693
- * Success announces nothing: the listbox and `aria-activedescendant` are the
694
- * feedback, and a region that spoke on every successful keystroke would talk over
695
- * them. A failure is a sentence somebody can act on rather than a code.
696
- */
697
- function statusFor(text, { failed, busy, needs, query, capped }) {
698
- if (failed) return text("options.failed");
699
- if (query.trim().length < needs) return text("options.tooShort", { count: needs });
700
- if (busy) return text("options.searching");
701
- if (capped !== null) return text("options.capped", {
702
- shown: capped.shown,
703
- total: capped.total
704
- });
705
- return "";
706
- }
707
706
  //#endregion
708
707
  //#region src/uploads.ts
709
708
  const UploaderContext = createContext(void 0);
@@ -784,11 +783,11 @@ function FieldShell({ path, field, label, children }) {
784
783
  * Not the text: a theme that keyed off English prose would break in every other
785
784
  * language the form is offered in.
786
785
  */
787
- function statusState({ sourced, open, matches }) {
786
+ function statusState({ sourced, said, open, matches }) {
788
787
  if (sourced.remote === null) return open && matches === 0 ? "empty" : void 0;
789
788
  if (sourced.remote.busy) return "busy";
790
789
  if (sourced.remote.failed) return "failed";
791
- if (sourced.remote.status !== "") return "hint";
790
+ if (said !== "") return "hint";
792
791
  return open && matches === 0 ? "empty" : void 0;
793
792
  }
794
793
  /**
@@ -867,7 +866,11 @@ function TypeaheadSelectField({ path, label, field }) {
867
866
  * first clears the answer on the way out.
868
867
  */
869
868
  const [query, setQuery] = useState(null);
870
- const [open, setOpen] = useState(false);
869
+ const [opened, setOpen] = useState(false);
870
+ /** A field disabled by a rule while its list was showing has no list: nobody can
871
+ * choose from it, and its region would say "no options match" under a read-only
872
+ * answer. */
873
+ const open = opened && !field.disabled;
871
874
  /** The arrowed-over option by VALUE, not by index: the filtered list changes on
872
875
  * every keystroke and an index would point at a different row after one. */
873
876
  const [activeValue, setActiveValue] = useState(null);
@@ -876,6 +879,16 @@ function TypeaheadSelectField({ path, label, field }) {
876
879
  path
877
880
  }, query ?? "");
878
881
  const options = sourced.options;
882
+ const said = sourced.remote === null ? "" : optionsStatus(text, {
883
+ control: "typeahead",
884
+ open,
885
+ disabled: field.disabled,
886
+ failed: sourced.remote.failed,
887
+ busy: sourced.remote.busy,
888
+ needs: sourced.remote.needs,
889
+ query: query ?? "",
890
+ completeness: sourced.remote.completeness
891
+ });
879
892
  const chosen = options.find((option) => option.value === field.value);
880
893
  const matches = sourced.remote === null ? narrowOptionsByLabel(options, query ?? "") : options;
881
894
  /** Collapsed whenever there is nothing on the screen, so `aria-expanded` never
@@ -988,14 +1001,16 @@ function TypeaheadSelectField({ path, label, field }) {
988
1001
  "data-formancy-part": "typeahead-status",
989
1002
  ...statusState({
990
1003
  sourced,
1004
+ said,
991
1005
  open,
992
1006
  matches: matches.length
993
1007
  }) === void 0 ? {} : { "data-state": statusState({
994
1008
  sourced,
1009
+ said,
995
1010
  open,
996
1011
  matches: matches.length
997
1012
  }) },
998
- children: sourced.remote !== null && sourced.remote.status !== "" ? sourced.remote.status : open && matches.length === 0 && sourced.remote?.busy !== true ? text("options.noMatch") : ""
1013
+ children: said !== "" ? said : open && matches.length === 0 && sourced.remote?.busy !== true ? text("options.noMatch") : ""
999
1014
  })]
1000
1015
  });
1001
1016
  }
@@ -1031,6 +1046,17 @@ function SelectField({ path, label }) {
1031
1046
  path
1032
1047
  }, "", field.def.widget !== "typeahead");
1033
1048
  const options = sourced.options;
1049
+ const remote = sourced.remote;
1050
+ const said = remote === null ? "" : optionsStatus(text, {
1051
+ control: "select",
1052
+ open: true,
1053
+ disabled: field.disabled,
1054
+ failed: remote.failed,
1055
+ busy: remote.busy,
1056
+ needs: remote.needs,
1057
+ query: "",
1058
+ completeness: remote.completeness
1059
+ });
1034
1060
  if (sourced.remote?.unavailable === true) return /* @__PURE__ */ jsx(FieldShell, {
1035
1061
  path,
1036
1062
  field,
@@ -1053,15 +1079,18 @@ function SelectField({ path, label }) {
1053
1079
  ...field.controlProps,
1054
1080
  value: typeof field.value === "string" ? field.value : "",
1055
1081
  onChange: (event) => field.setValue(event.target.value === "" ? null : event.target.value),
1082
+ onFocus: () => {
1083
+ if (remote?.failed === true) remote.retry();
1084
+ },
1056
1085
  onBlur: () => field.touch(),
1057
1086
  children: [/* @__PURE__ */ jsx("option", { value: "" }), options.map((option) => /* @__PURE__ */ jsx("option", {
1058
1087
  value: option.value,
1059
1088
  children: option.label
1060
1089
  }, option.value))]
1061
- }), sourced.remote === null ? null : /* @__PURE__ */ jsx("p", {
1090
+ }), remote === null ? null : /* @__PURE__ */ jsx("p", {
1062
1091
  role: "status",
1063
1092
  "data-formancy-part": "select-status",
1064
- children: sourced.remote.status
1093
+ children: said
1065
1094
  })]
1066
1095
  });
1067
1096
  }
@@ -1700,7 +1729,7 @@ function TagPickerField({ path, label }) {
1700
1729
  const listboxId = `${field.ids.control}:listbox`;
1701
1730
  const remaining = options.filter((option) => !chosen.includes(option.value));
1702
1731
  const matches = narrowOptionsByLabel(remaining, query);
1703
- const expanded = open && matches.length > 0;
1732
+ const expanded = open && !field.disabled && matches.length > 0;
1704
1733
  const add = (value) => {
1705
1734
  const next = options.map((option) => option.value).filter((candidate) => candidate === value || chosen.includes(candidate));
1706
1735
  field.setValue(next);
@@ -1726,6 +1755,7 @@ function TagPickerField({ path, label }) {
1726
1755
  type: "button",
1727
1756
  "data-formancy-part": "tagpicker-remove",
1728
1757
  "aria-label": text("tagpicker.remove", { option: labelFor(value) }),
1758
+ disabled: field.disabled,
1729
1759
  onClick: () => remove(value),
1730
1760
  children: "×"
1731
1761
  })]
@@ -2179,6 +2209,7 @@ function NumberFallback({ path, label }) {
2179
2209
  children: /* @__PURE__ */ jsx("input", {
2180
2210
  type: "number",
2181
2211
  ...field.controlProps,
2212
+ readOnly: field.readOnly,
2182
2213
  value: typeof field.value === "number" ? field.value : "",
2183
2214
  onChange: (event) => field.setValue(event.target.value === "" ? null : event.target.valueAsNumber),
2184
2215
  onBlur: () => field.touch()
@@ -2372,6 +2403,7 @@ function DateField({ path, label }) {
2372
2403
  children: /* @__PURE__ */ jsx("input", {
2373
2404
  type: "date",
2374
2405
  ...field.controlProps,
2406
+ readOnly: field.readOnly,
2375
2407
  value: typeof field.value === "string" ? field.value : "",
2376
2408
  onChange: (event) => field.setValue(event.target.value === "" ? null : event.target.value),
2377
2409
  onBlur: () => field.touch()
@@ -2400,6 +2432,7 @@ function TimeField({ path, label }) {
2400
2432
  children: /* @__PURE__ */ jsx("input", {
2401
2433
  type: "time",
2402
2434
  ...field.controlProps,
2435
+ readOnly: field.readOnly,
2403
2436
  ...typeof field.def.earliest === "string" ? { min: field.def.earliest } : {},
2404
2437
  ...typeof field.def.latest === "string" ? { max: field.def.latest } : {},
2405
2438
  value: typeof field.value === "string" ? field.value : "",
@@ -2446,6 +2479,7 @@ function DateTimeField({ path, label }) {
2446
2479
  children: /* @__PURE__ */ jsx("input", {
2447
2480
  type: "datetime-local",
2448
2481
  ...field.controlProps,
2482
+ readOnly: field.readOnly,
2449
2483
  value: asLocal(),
2450
2484
  onChange: (event) => {
2451
2485
  const local = event.target.value;
@@ -2529,6 +2563,7 @@ function TextField({ path, label }) {
2529
2563
  children: [/* @__PURE__ */ jsx("input", {
2530
2564
  type: "text",
2531
2565
  ...field.controlProps,
2566
+ readOnly: field.readOnly,
2532
2567
  value: shown,
2533
2568
  placeholder: mask === void 0 ? void 0 : maskPlaceholder(mask),
2534
2569
  inputMode: mask !== void 0 && maskIsNumeric(mask) ? "numeric" : void 0,
@@ -2579,6 +2614,7 @@ function TextareaField({ path, label }) {
2579
2614
  label,
2580
2615
  children: /* @__PURE__ */ jsx("textarea", {
2581
2616
  ...field.controlProps,
2617
+ readOnly: field.readOnly,
2582
2618
  value: typeof field.value === "string" ? field.value : "",
2583
2619
  onChange: (event) => field.setValue(event.target.value),
2584
2620
  onBlur: () => field.touch()
@@ -2772,6 +2808,7 @@ function RepeaterSection({ wire, labels, registry }) {
2772
2808
  const engine = useFormEngine();
2773
2809
  const text = useFormText();
2774
2810
  const repeater = useRepeater(wire);
2811
+ const field = useField(wire);
2775
2812
  const def = engine.repeaters().find((candidate) => candidate.wire === wire)?.def;
2776
2813
  const label = engine.text(def?.label) ?? labels?.[wire] ?? wire;
2777
2814
  const addLabel = def?.addLabel ?? text("repeater.add", { label });
@@ -2823,9 +2860,10 @@ function RepeaterSection({ wire, labels, registry }) {
2823
2860
  * the consumer ([0004](../../../docs/decisions/0004-headless-core.md)), and a renderer
2824
2861
  * that shipped a glyph would be choosing one for every design system at once. */
2825
2862
  const rowButtons = (index) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
2826
- /* @__PURE__ */ jsx("button", {
2863
+ repeater.canRemove ? /* @__PURE__ */ jsx("button", {
2827
2864
  type: "button",
2828
2865
  "data-formancy-part": "row-remove",
2866
+ disabled: field.disabled,
2829
2867
  onClick: () => repeater.removeRow(index),
2830
2868
  children: /* @__PURE__ */ jsx("span", {
2831
2869
  "data-formancy-part": "row-action-text",
@@ -2835,10 +2873,11 @@ function RepeaterSection({ wire, labels, registry }) {
2835
2873
  count: repeater.rowCount
2836
2874
  })
2837
2875
  })
2838
- }),
2876
+ }) : null,
2839
2877
  index > 0 ? /* @__PURE__ */ jsx("button", {
2840
2878
  type: "button",
2841
2879
  "data-formancy-part": "row-up",
2880
+ disabled: field.disabled,
2842
2881
  onClick: () => repeater.moveRow(index, index - 1),
2843
2882
  children: /* @__PURE__ */ jsx("span", {
2844
2883
  "data-formancy-part": "row-action-text",
@@ -2852,6 +2891,7 @@ function RepeaterSection({ wire, labels, registry }) {
2852
2891
  index < repeater.rowCount - 1 ? /* @__PURE__ */ jsx("button", {
2853
2892
  type: "button",
2854
2893
  "data-formancy-part": "row-down",
2894
+ disabled: field.disabled,
2855
2895
  onClick: () => repeater.moveRow(index, index + 1),
2856
2896
  children: /* @__PURE__ */ jsx("span", {
2857
2897
  "data-formancy-part": "row-action-text",
@@ -2892,14 +2932,20 @@ function RepeaterSection({ wire, labels, registry }) {
2892
2932
  "data-formancy-part": "row",
2893
2933
  children: [engine.fieldPaths().filter((candidate) => candidate.startsWith(`${wire}[${String(index)}]`)).map(slot), rowButtons(index)]
2894
2934
  }, rowId));
2935
+ if (!field.visible) return null;
2936
+ const showError = field.touched && field.errors.length > 0;
2895
2937
  return /* @__PURE__ */ jsxs("fieldset", {
2896
2938
  "data-formancy-part": "repeater",
2897
2939
  "data-formancy-field-path": wire,
2940
+ "data-state": showError ? "invalid" : "valid",
2941
+ id: field.controlProps.id,
2942
+ "aria-describedby": field.controlProps["aria-describedby"],
2898
2943
  children: [
2899
2944
  /* @__PURE__ */ jsx("legend", {
2900
2945
  "data-formancy-part": "repeater-legend",
2901
2946
  children: label
2902
2947
  }),
2948
+ /* @__PURE__ */ jsx(RequiredHint, { field }),
2903
2949
  grid ? /* @__PURE__ */ jsxs("div", {
2904
2950
  "data-formancy-part": "datagrid",
2905
2951
  "data-columns": String(plan.length),
@@ -2913,11 +2959,17 @@ function RepeaterSection({ wire, labels, registry }) {
2913
2959
  }, entry.key))
2914
2960
  }) : null, rows]
2915
2961
  }) : rows,
2916
- /* @__PURE__ */ jsx("button", {
2962
+ showError ? /* @__PURE__ */ jsx("p", {
2963
+ "data-formancy-part": "error",
2964
+ ...field.errorProps,
2965
+ children: field.errors.join(", ")
2966
+ }) : null,
2967
+ repeater.canAdd ? /* @__PURE__ */ jsx("button", {
2917
2968
  type: "button",
2969
+ disabled: field.disabled,
2918
2970
  onClick: () => repeater.addRow(),
2919
2971
  children: addLabel
2920
- })
2972
+ }) : null
2921
2973
  ]
2922
2974
  });
2923
2975
  }