@dloizides/ui-forms 1.7.0 → 1.9.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.ts CHANGED
@@ -1,5 +1,47 @@
1
1
  import React from 'react';
2
- import { StyleProp, TextStyle, ViewStyle, TextInputProps, TextInput } from 'react-native';
2
+ import { StyleProp, TextStyle, ViewStyle, TextInputProps, TextInput, View } from 'react-native';
3
+
4
+ /**
5
+ * `Field`'s TWO spacing models — the F3 fix, shipped as an OPT-IN rather than a breaking change.
6
+ *
7
+ * ## The defect
8
+ *
9
+ * `Field` hard-coded a 16px bottom margin on its container and 4px margins on its label / hint /
10
+ * error. That is the pre-flexbox model: the CHILD owns the space after itself. In a `gap`-laid-out
11
+ * parent — which is how every modern form row in this fleet is written — the child's margin STACKS
12
+ * on the parent's gap, so the consumer has to cancel it by hand. aml-v2 alone carries 13
13
+ * `marginBottom: 0` cancels, each with an apologetic comment; the fleet carries 16.
14
+ *
15
+ * AML v1 got this right in CSS: `.ui-field { display: flex; flex-direction: column; gap: 6px; }` —
16
+ * internal gap only, and the parent grid owns the space BETWEEN fields. That contract is written up
17
+ * in `PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md` ("The gap is owned by the container,
18
+ * never by margins on the children").
19
+ *
20
+ * ## Why opt-in and not a 2.0.0
21
+ *
22
+ * Switching every `Field` to the gap model at once moves pixels on every form in seven portals in a
23
+ * single publish — which is exactly the change you cannot visually QA, because there is no app you
24
+ * can look at that is still on the old behaviour to compare against. The two models are therefore
25
+ * both implemented, selected per field:
26
+ *
27
+ * - `stack` (DEFAULT) — the historical margins, byte-identical to 1.8.0. Every existing consumer
28
+ * is untouched, including the 16 that currently cancel the margin.
29
+ * - `gap` — the FORMS.md contract: no bottom margin at all, uniform 6px internal gap. The parent
30
+ * owns the rhythm.
31
+ *
32
+ * A `Field` inside a `FormGrid` resolves to `gap` automatically, so adopting the shared grid IS the
33
+ * opt-in — the consumer deletes its hand-rolled `grid`/`field` styles and its margin cancel in the
34
+ * same edit, and the fleet migrates one app at a time behind visual QA.
35
+ */
36
+
37
+ /**
38
+ * Which spacing model a field speaks.
39
+ *
40
+ * A plain string union rather than a `const enum` for the same reason as `FieldLabelVariant`: this
41
+ * type crosses the package boundary into consumers' `.d.ts`, and a cross-boundary `const enum`
42
+ * breaks `isolatedModules` / Babel-only builds.
43
+ */
44
+ type FieldSpacing = 'stack' | 'gap';
3
45
 
4
46
  /**
5
47
  * The TWO legitimate label voices, per the shared forms contract
@@ -102,9 +144,18 @@ interface FieldProps {
102
144
  * plus a `grow` modifier — instead of having to restate a COMPLETE style per variant.
103
145
  */
104
146
  containerStyle?: StyleProp<ViewStyle>;
147
+ /**
148
+ * Which of the two spacing models this field speaks — see `fieldSpacing.ts`.
149
+ *
150
+ * Leave it unset. Inside a `FormGrid` or `FormSection` it resolves to `gap` (the FORMS.md
151
+ * contract: no bottom margin, the parent's `gap` owns the rhythm); anywhere else it resolves to
152
+ * `stack` (the historical 16px margin, byte-identical to 1.8.0). Set it explicitly only to
153
+ * override that — e.g. a cell inside a grid that genuinely stacks two fields wants `stack`.
154
+ */
155
+ spacing?: FieldSpacing;
105
156
  testID?: string;
106
157
  }
107
- declare const Field: ({ label, children, required, optional, optionalLabel, hint, error, labelVariant, labelStyle, containerStyle, testID, }: FieldProps) => React.ReactElement;
158
+ declare const Field: ({ label, children, required, optional, optionalLabel, hint, error, labelVariant, labelStyle, containerStyle, spacing, testID, }: FieldProps) => React.ReactElement;
108
159
 
109
160
  /**
110
161
  * Required / optional markers, per the shared forms contract
@@ -137,6 +188,117 @@ type FieldMarker = 'none' | 'required' | 'optional';
137
188
  */
138
189
  declare const DEFAULT_OPTIONAL_LABEL = "(optional)";
139
190
 
191
+ /**
192
+ * `FormGrid` — the wrapping form row, extracted from ~177 hand-rolled copies.
193
+ *
194
+ * Verbatim in every portal, differing only in one constant:
195
+ *
196
+ * ```ts
197
+ * grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 },
198
+ * field: { minWidth: 220, flexGrow: 1, flexBasis: 260 }, // or 200/240/260
199
+ * ```
200
+ *
201
+ * Extracting it is worth more than the deleted lines: because the grid declares itself through
202
+ * `FormGridContext`, a `Field` inside it knows its vertical rhythm is owned by the grid's `gap` and
203
+ * drops its own bottom margin. That kills the fleet's 16 `marginBottom: 0` cancel hacks at the same
204
+ * time — the grid and the cancel were always two halves of one bug.
205
+ *
206
+ * The cell metrics live on the GRID, not on each cell, because that is how the real call sites are
207
+ * written: one `styles.field` reused by every cell in the row, with the occasional narrow one-off.
208
+ * `<FormCell>` with no props is therefore the common case, and `<FormCell basis={160} grow={false}>`
209
+ * the exception — which is exactly the ratio in aml-v2's `ScreeningForm`.
210
+ */
211
+
212
+ interface FormGridProps {
213
+ children: React.ReactNode;
214
+ /**
215
+ * Space between cells, horizontally AND between wrapped rows. This is the value that makes the
216
+ * grid own the vertical rhythm, so it is also what a descendant `Field` is dropping its own
217
+ * margin in favour of.
218
+ */
219
+ gap?: number;
220
+ /** Default `minWidth` for every `FormCell` in this grid. */
221
+ minWidth?: number;
222
+ /** Default `flexBasis` for every `FormCell` in this grid. */
223
+ basis?: number;
224
+ style?: StyleProp<ViewStyle>;
225
+ testID?: string;
226
+ }
227
+ declare const FormGrid: ({ children, gap, minWidth, basis, style, testID, }: FormGridProps) => React.ReactElement;
228
+
229
+ /**
230
+ * `FormCell` — one cell of a `FormGrid`.
231
+ *
232
+ * Replaces the `<View style={styles.field}>` wrapper every real call site already puts around each
233
+ * control. It inherits `minWidth` / `flexBasis` from the enclosing grid so the common case is a bare
234
+ * `<FormCell>`, and takes per-cell overrides for the genuine one-offs (aml-v2's `fieldNarrow`:
235
+ * `minWidth: 140, flexBasis: 160, flexGrow: 0`).
236
+ *
237
+ * Used OUTSIDE a `FormGrid` it still renders a sensible cell from the package defaults rather than
238
+ * throwing — a cell is a layout hint, and a form that renders slightly wide is a better failure than
239
+ * a form that does not render. The grid-owned SPACING behaviour, by contrast, is deliberately NOT
240
+ * available this way: that one has to come from a real grid, because a `Field` dropping its margin
241
+ * with nothing to replace it would collapse the form.
242
+ */
243
+
244
+ interface FormCellProps {
245
+ children: React.ReactNode;
246
+ /** Overrides the grid's `minWidth` for this cell only. */
247
+ minWidth?: number;
248
+ /** Overrides the grid's `basis` for this cell only. */
249
+ basis?: number;
250
+ /** `false` pins the cell to its basis instead of letting it absorb the row's slack. */
251
+ grow?: boolean;
252
+ style?: StyleProp<ViewStyle>;
253
+ testID?: string;
254
+ }
255
+ declare const FormCell: ({ children, minWidth, basis, grow, style, testID, }: FormCellProps) => React.ReactElement;
256
+
257
+ /**
258
+ * `FormSection` — a titled group of fields. The React answer to v1's `<fieldset>` / `<legend>`.
259
+ *
260
+ * v1 has a real `<fieldset>` whose `<legend>` takes the identical label metric as a field label, so
261
+ * a group heading reads as part of the form rather than as page furniture. Nothing in the React
262
+ * fleet has this: zygos fakes it with a generic `Heading`, and everybody else emits an untitled
263
+ * `<View>` — which means the grouping exists visually but not semantically, so a screen-reader user
264
+ * tabbing into "Threshold" is never told it belongs to "Advanced matching".
265
+ *
266
+ * ## Why not a literal `<fieldset>`
267
+ *
268
+ * react-native-web has no `fieldset` mapping — a `View` is a `div`, and there is no escape hatch
269
+ * that turns one into a `<fieldset>` with a real `<legend>`. What IS available is the pattern the
270
+ * ARIA spec defines as the equivalent: `role="group"` + `aria-labelledby` pointing at the heading.
271
+ * Assistive tech announces "Advanced matching, group" exactly as it would for a fieldset. This is
272
+ * the honest ceiling of the platform, not a shortcut — and it is why the legend is a `Text` whose
273
+ * `nativeID` is generated the same way `Field`'s hint/error ids are.
274
+ *
275
+ * The legend deliberately reuses `resolveLabelVariantStyle` — the SAME function `Field` uses for its
276
+ * label — so the two cannot drift. A section titled in one metric above fields labelled in another
277
+ * is precisely the "third voice is drift" failure FORMS.md warns about.
278
+ */
279
+
280
+ interface FormSectionProps {
281
+ /**
282
+ * The legend — pre-localized, like every string in this package. Optional: a section with no
283
+ * title still groups its children for layout, but renders NO legend row and NO `aria-labelledby`,
284
+ * because a group labelled by an empty element is worse for assistive tech than an unlabelled one.
285
+ */
286
+ title?: string;
287
+ /** Help line for the whole group, under the legend. The group-level sibling of `Field`'s hint. */
288
+ description?: string;
289
+ children: React.ReactNode;
290
+ /**
291
+ * Which label voice the legend speaks. Defaults to `field` — a section heading on a form the user
292
+ * fills in. Pass `control` on a dense surface so the legend matches the controls beneath it.
293
+ */
294
+ labelVariant?: FieldLabelVariant;
295
+ /** Escape hatch applied AFTER `labelVariant`, for the genuine one-off a variant does not cover. */
296
+ titleStyle?: StyleProp<TextStyle>;
297
+ style?: StyleProp<ViewStyle>;
298
+ testID?: string;
299
+ }
300
+ declare const FormSection: ({ title, description, children, labelVariant, titleStyle, style, testID, }: FormSectionProps) => React.ReactElement;
301
+
140
302
  /**
141
303
  * `FormActions` — the submit/cancel button row every form ends with.
142
304
  *
@@ -289,7 +451,17 @@ interface ChipSelectorProps<T> {
289
451
  disabled?: boolean;
290
452
  /** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
291
453
  variant?: ChipVariant;
292
- containerStyle?: ViewStyle;
454
+ /**
455
+ * `StyleProp` (not a bare `ViewStyle`) so it composes the same way `Field`'s does — a caller can
456
+ * pass an array instead of restating a complete style per variant.
457
+ */
458
+ containerStyle?: StyleProp<ViewStyle>;
459
+ /**
460
+ * Forwarded verbatim to the underlying `Field`. `ChipSelector` deliberately holds NO spacing
461
+ * opinion of its own: it delegates, so the two cannot drift into disagreeing about how much room
462
+ * a chip block occupies. The guard for that is `chipSelectorFieldLockstep` in the tests.
463
+ */
464
+ spacing?: FieldSpacing;
293
465
  /** Marks the selection mandatory — renders `Field`'s decorative `*` next to the label. */
294
466
  required?: boolean;
295
467
  /** Validation message under the chips, wired to the chip group via `aria-describedby`. */
@@ -308,7 +480,7 @@ interface ChipSelectorProps<T> {
308
480
  */
309
481
  optionAccessibilityHint?: string;
310
482
  }
311
- declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, required, error, testID, chipTestIDPrefix, optionAccessibilityHint, }: ChipSelectorProps<T>) => React.ReactElement;
483
+ declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, spacing, required, error, testID, chipTestIDPrefix, optionAccessibilityHint, }: ChipSelectorProps<T>) => React.ReactElement;
312
484
 
313
485
  /**
314
486
  * Reusable form switch/toggle with an optional description.
@@ -351,4 +523,277 @@ interface FormCheckboxProps {
351
523
  }
352
524
  declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
353
525
 
354
- export { type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, DEFAULT_OPTIONAL_LABEL, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, FormActions, type FormActionsProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
526
+ /**
527
+ * Shared value types for the dense CONTROLS (select / typeahead / date-range) promoted out of
528
+ * `@dloizides/ui-tables`' private filter bar.
529
+ *
530
+ * They live here, not in `ui-tables`, because a control's option list and a date range are form
531
+ * concepts — a filter bar is only one surface that happens to compose them. `ui-tables` aliases
532
+ * its long-standing public names (`FilterOption`, `DateRangeValue`) onto these, so there is ONE
533
+ * definition and the two can never drift into structurally-compatible-but-separate twins.
534
+ */
535
+ /** A single choice in a select / typeahead. `label` is PRE-LOCALIZED by the caller. */
536
+ interface ControlOption {
537
+ readonly label: string;
538
+ readonly value: string;
539
+ }
540
+ /** The inclusive from/to value of a date-range control (each an ISO `YYYY-MM-DD` string, or ''). */
541
+ interface DateRangeValue {
542
+ readonly from: string;
543
+ readonly to: string;
544
+ }
545
+
546
+ /**
547
+ * `SelectControl` — a single-select enum/status dropdown. A bordered trigger shows the selected
548
+ * option's label (or the placeholder when unset) plus a chevron; pressing it opens the shared
549
+ * {@link AnchoredMenu}. Selecting fires `onChange` and closes.
550
+ *
551
+ * Promoted out of `@dloizides/ui-tables`' private `Filters/fields/SelectField`, where six portals
552
+ * could see it working and none could import it — which is why the fleet grew SIX independent
553
+ * select implementations.
554
+ *
555
+ * The control is LABEL-FREE by design: wrap it in a `Field` (with `labelVariant="control"` on a
556
+ * dense surface) to get the label, hint and error slots. That keeps one label implementation for
557
+ * every control instead of one per control.
558
+ *
559
+ * i18n: this package never calls `t`. `accessibilityLabel`, `placeholder`, `accessibilityHint`
560
+ * and `optionHint` are PRE-LOCALIZED strings supplied by the caller — the only shape that can
561
+ * serve both aml-v2's positional `{0}` `@dloizides/i18n` and the other six portals' i18next
562
+ * `{{p1}}` from one component.
563
+ */
564
+
565
+ interface SelectControlProps {
566
+ options: readonly ControlOption[];
567
+ /** The selected option's `value`. `''` (or any unmatched value) renders the placeholder. */
568
+ value: string;
569
+ onChange: (value: string) => void;
570
+ /** PRE-LOCALIZED text shown when the value matches no option. */
571
+ placeholder?: string;
572
+ /**
573
+ * PRE-LOCALIZED accessible NAME of the trigger. It REPLACES the trigger's visible text for a
574
+ * screen reader, so it must carry the SELECTION too — "Status: Active", not just "Status",
575
+ * which announces the control but silently hides its value.
576
+ */
577
+ accessibilityLabel: string;
578
+ accessibilityHint?: string;
579
+ /** PRE-LOCALIZED accessibility hint applied to every option in the menu. */
580
+ optionHint?: string;
581
+ /** Merged LAST onto the anchor wrapper — the consumer always wins over the shared style. */
582
+ style?: StyleProp<ViewStyle>;
583
+ /** Trigger is `${testID}-trigger`, menu `${testID}-menu`, options `${testID}-option-${value}`. */
584
+ testID: string;
585
+ }
586
+ declare function SelectControl({ options, value, onChange, placeholder, accessibilityLabel, accessibilityHint, optionHint, style, testID, }: SelectControlProps): React.ReactElement;
587
+
588
+ /**
589
+ * `TypeaheadControl` — a search-as-you-type combobox (aml-v2's CountryPicker, generalized). A
590
+ * text input plus a floating {@link AnchoredMenu} of ranked suggestions ({@link suggestOptions});
591
+ * picking one fills the canonical label.
592
+ *
593
+ * Free typing is PRESERVED: the value is the raw text, so a caller can accept "CY", "cyprus" or
594
+ * "Cyprus (CY)" and normalise it themselves at query time, exactly as aml does. An optional
595
+ * PRE-LOCALIZED `error` shows a persistent banner and tints the border.
596
+ *
597
+ * Promoted out of `@dloizides/ui-tables`' private `Filters/fields/TypeaheadField`.
598
+ *
599
+ * Label-free by design — wrap in a `Field` for the label/hint slots.
600
+ */
601
+
602
+ /** Default min characters before suggestions surface. */
603
+ declare const DEFAULT_TYPEAHEAD_MIN_CHARS = 1;
604
+ /** Default cap on rendered suggestions. */
605
+ declare const DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS = 8;
606
+ interface TypeaheadControlProps {
607
+ options: readonly ControlOption[];
608
+ /** The raw text in the box — NOT necessarily an option value. */
609
+ value: string;
610
+ onChange: (value: string) => void;
611
+ /** Called on Enter/submit (after the menu closes). Omit if the surface has nothing to commit. */
612
+ onSubmit?: () => void;
613
+ placeholder?: string;
614
+ /** PRE-LOCALIZED accessible name of the input. */
615
+ accessibilityLabel: string;
616
+ accessibilityHint?: string;
617
+ /** PRE-LOCALIZED accessibility hint applied to every suggestion. */
618
+ optionHint?: string;
619
+ /** PRE-LOCALIZED inline error (e.g. "Unrecognised country"). Shows a persistent banner. */
620
+ error?: string;
621
+ /** Minimum characters before suggestions surface. Default 1. */
622
+ minChars?: number;
623
+ /** Cap on rendered suggestions. Default 8. */
624
+ maxSuggestions?: number;
625
+ /** Merged LAST onto the anchor wrapper. */
626
+ style?: StyleProp<ViewStyle>;
627
+ /** Input is `${testID}-input`, menu `${testID}-menu`, error `${testID}-error`. */
628
+ testID: string;
629
+ }
630
+ declare function TypeaheadControl({ options, value, onChange, onSubmit, placeholder, accessibilityLabel, accessibilityHint, optionHint, error, minChars, maxSuggestions, style, testID, }: TypeaheadControlProps): React.ReactElement;
631
+
632
+ /**
633
+ * `DateRangeControl` — an inclusive from/to pair (aml cases `from`/`to`, kefi audit `from`/`to`).
634
+ * Each side is a `YYYY-MM-DD` text input; on web the browser's native date UI is offered via
635
+ * `type="date"` (a react-native-web `TextInput` passes unknown props to the DOM input). Editing
636
+ * either side patches ONLY that side, so the other survives the edit.
637
+ *
638
+ * Promoted out of `@dloizides/ui-tables`' private `Filters/fields/DateRangeField`, which is why
639
+ * the fleet grew FIVE independent date fields.
640
+ *
641
+ * Label-free by design — the From/To sub-labels name the two HALVES; wrap in a `Field` for the
642
+ * range's own label.
643
+ */
644
+
645
+ interface DateRangeControlProps {
646
+ value: DateRangeValue;
647
+ onChange: (value: DateRangeValue) => void;
648
+ /** Called on Enter/submit in either side. */
649
+ onSubmit?: () => void;
650
+ /** PRE-LOCALIZED sub-label for the FROM input (e.g. "From"). Also its accessible name. */
651
+ fromLabel: string;
652
+ /** PRE-LOCALIZED sub-label for the TO input (e.g. "To"). Also its accessible name. */
653
+ toLabel: string;
654
+ fromPlaceholder?: string;
655
+ toPlaceholder?: string;
656
+ /** PRE-LOCALIZED hint applied to BOTH inputs. */
657
+ accessibilityHint?: string;
658
+ /** Merged LAST onto the row wrapper. */
659
+ style?: StyleProp<ViewStyle>;
660
+ /** Row is `${testID}-range`, inputs `${testID}-from` / `${testID}-to`. */
661
+ testID: string;
662
+ }
663
+ declare function DateRangeControl({ value, onChange, onSubmit, fromLabel, toLabel, fromPlaceholder, toPlaceholder, accessibilityHint, style, testID, }: DateRangeControlProps): React.ReactElement;
664
+
665
+ /**
666
+ * `AnchoredMenu` — the floating option list shared by `SelectControl` and `TypeaheadControl`.
667
+ *
668
+ * Renders an in-tree, absolutely-positioned popover under its anchor (the pattern aml-v2's
669
+ * CountryPicker proved: `position:absolute; top:100%; zIndex; elevation`), so it floats over
670
+ * sibling fields without a portal. On web it dismisses on outside-click / Escape; on native the
671
+ * caller closes it on blur/select. Purely presentational + theme-flat, so it stays reusable.
672
+ *
673
+ * Promoted verbatim out of `@dloizides/ui-tables`' private `Filters/components/AnchoredMenu`.
674
+ *
675
+ * Deliberately in-tree (no `ui-layout` dependency). That was a considered choice in `ui-tables`
676
+ * and it still holds here: `ui-forms` is a low-level primitive and must not take a runtime
677
+ * dependency on the higher-level `ui-layout` `ModalDropdown` — that would invert the graph for
678
+ * every consumer. Apps needing the responsive modal/bottom-sheet variant inject it instead.
679
+ */
680
+
681
+ /** The four theme colours the menu paints with — passed in, so the menu stays theme-flat. */
682
+ interface AnchoredMenuColors {
683
+ text: string;
684
+ border: string;
685
+ surface: string;
686
+ brand: string;
687
+ }
688
+ interface AnchoredMenuProps {
689
+ options: readonly ControlOption[];
690
+ /** The currently-selected value, highlighted in the list ('' = none). */
691
+ selectedValue: string;
692
+ onSelect: (value: string) => void;
693
+ onDismiss: () => void;
694
+ colors: AnchoredMenuColors;
695
+ /** PRE-LOCALIZED accessibility hint applied to every option. */
696
+ optionHint: string;
697
+ /** Base testID: the menu is `${testID}-menu`, each option `${testID}-option-${value}`. */
698
+ testID: string;
699
+ /** Node whose clicks count as "inside" (the anchor) so its own press doesn't self-dismiss. */
700
+ anchorRef: React.RefObject<View | null>;
701
+ }
702
+ declare function AnchoredMenu({ options, selectedValue, onSelect, onDismiss, colors, optionHint, testID, anchorRef, }: AnchoredMenuProps): React.ReactElement;
703
+
704
+ declare const controlStyles: {
705
+ /** The shared bordered box every dense text control renders in. */
706
+ input: {
707
+ borderWidth: number;
708
+ borderRadius: number;
709
+ paddingHorizontal: number;
710
+ paddingVertical: number;
711
+ fontSize: number;
712
+ };
713
+ /** The select trigger: a bordered field box matching the text inputs + a chevron. */
714
+ selectTrigger: {
715
+ flexDirection: "row";
716
+ alignItems: "center";
717
+ justifyContent: "space-between";
718
+ gap: number;
719
+ borderWidth: number;
720
+ borderRadius: number;
721
+ paddingHorizontal: number;
722
+ paddingVertical: number;
723
+ };
724
+ selectTriggerText: {
725
+ fontSize: number;
726
+ flexShrink: number;
727
+ };
728
+ chevron: {
729
+ fontSize: number;
730
+ };
731
+ dateRow: {
732
+ flexDirection: "row";
733
+ gap: number;
734
+ };
735
+ dateCol: {
736
+ gap: number;
737
+ flex: number;
738
+ };
739
+ /**
740
+ * The micro-caption above a sub-control (a date range's From/To) and beside a switch. Note it
741
+ * is deliberately NOT the `control` label voice: it carries no `700`/uppercase, because it
742
+ * names a HALF of a control that already has a `control`-voice label above it. Uppercasing it
743
+ * too would make one field shout twice.
744
+ */
745
+ subLabel: {
746
+ fontSize: number;
747
+ letterSpacing: number;
748
+ };
749
+ /** Positioning context for a floating `AnchoredMenu` (`position:absolute; top:100%`). */
750
+ anchor: {
751
+ position: "relative";
752
+ };
753
+ menu: {
754
+ position: "absolute";
755
+ top: "100%";
756
+ left: number;
757
+ marginTop: number;
758
+ minWidth: number;
759
+ maxHeight: number;
760
+ borderWidth: number;
761
+ borderRadius: number;
762
+ paddingVertical: number;
763
+ zIndex: number;
764
+ boxShadow: string;
765
+ elevation: number;
766
+ overflow: "hidden";
767
+ };
768
+ option: {
769
+ paddingHorizontal: number;
770
+ paddingVertical: number;
771
+ };
772
+ optionText: {
773
+ fontSize: number;
774
+ };
775
+ error: {
776
+ fontSize: number;
777
+ marginTop: number;
778
+ };
779
+ };
780
+
781
+ /**
782
+ * `suggestOptions` — the ranked substring matcher behind `TypeaheadControl`, generalized from
783
+ * aml-v2's `suggestCountries` and promoted verbatim out of `@dloizides/ui-tables`' private
784
+ * `Filters/utils/suggest.ts`.
785
+ *
786
+ * Ranks: value exact (0) > value prefix (1) > label prefix (2) > label contains (3); ties broken
787
+ * by `label.localeCompare`. Empty/short queries return `[]` — nothing surfaces until `minChars`
788
+ * is typed, mirroring a native `<datalist>`.
789
+ */
790
+
791
+ /**
792
+ * Rank + cap the options matching `query`. Returns `[]` until `query` (trimmed) is at least
793
+ * `minChars` long, then the best `limit` matches, ranked as documented above.
794
+ */
795
+ declare function suggestOptions(options: readonly ControlOption[], query: string, minChars: number, limit: number): readonly ControlOption[];
796
+ /** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
797
+ declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
798
+
799
+ export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useThemedInput };