@openzeppelin/ui-components 3.3.0 → 3.4.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.mjs CHANGED
@@ -4,7 +4,7 @@ import { cva } from "class-variance-authority";
4
4
  import { AlertCircle, Calendar as Calendar$1, Check, CheckCircle, CheckCircle2, CheckIcon, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, ClipboardPaste, CloudOff, Copy, DollarSign, ExternalLink as ExternalLink$1, ExternalLinkIcon, Eye, EyeOff, File, FileText, GripVertical, Hash, Info, Loader2, Menu, MoreHorizontal, Network, Pencil, PencilLine, Plus, Search, Settings, Timer, Upload, X } from "lucide-react";
5
5
  import * as React$1 from "react";
6
6
  import React, { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
7
- import { classifyAddressCandidates, cn, getDefaultValueForType, getHostedNetworkAvailabilityNoticeCopy, getInvalidUrlMessage, getServiceDisplayName, isNetworkAvailabilityPolicyActive, isValidUrl, parseDelimitedAddressInput, truncateMiddle, validateBytesSimple } from "@openzeppelin/ui-utils";
7
+ import { classifyAddressCandidates, classifyAddressInput, cn, getDefaultValueForType, getHostedNetworkAvailabilityNoticeCopy, getInvalidUrlMessage, getServiceDisplayName, isChainScopeMismatch, isNetworkAvailabilityPolicyActive, isValidUrl, logger, nameResolutionChainScopeMismatchMessage, nameResolutionMessageForCode, parseDelimitedAddressInput, truncateMiddle, validateBytesSimple } from "@openzeppelin/ui-utils";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
10
10
  import { Slot, Slottable } from "@radix-ui/react-slot";
@@ -14,7 +14,7 @@ import { format } from "date-fns";
14
14
  import * as PopoverPrimitive from "@radix-ui/react-popover";
15
15
  import * as DialogPrimitive from "@radix-ui/react-dialog";
16
16
  import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
17
- import { Controller, FormProvider, useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form";
17
+ import { Controller, FormProvider, useController, useFieldArray, useForm, useFormContext, useFormState, useWatch } from "react-hook-form";
18
18
  import * as LabelPrimitive from "@radix-ui/react-label";
19
19
  import * as ProgressPrimitive from "@radix-ui/react-progress";
20
20
  import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
@@ -26,7 +26,7 @@ import { Toaster as Toaster$1, toast } from "sonner";
26
26
  import { useTheme } from "next-themes";
27
27
 
28
28
  //#region src/version.ts
29
- const VERSION = "3.3.0";
29
+ const VERSION = "3.4.0";
30
30
 
31
31
  //#endregion
32
32
  //#region src/components/ui/accordion.tsx
@@ -119,6 +119,50 @@ const TooltipContent = React$1.forwardRef(({ className, sideOffset = 4, ...props
119
119
  }) }));
120
120
  TooltipContent.displayName = TooltipPrimitive.Content.displayName;
121
121
 
122
+ //#endregion
123
+ //#region src/components/ui/address-display/address-avatar.tsx
124
+ /**
125
+ * Allowlist for name-owner-controlled avatar URLs at the render site.
126
+ * Only `https:` and `data:image/*` pass; `http:` (mixed content), `javascript:`,
127
+ * and other schemes are dropped. Does not rely on adapter sanitization.
128
+ */
129
+ function isAllowedAvatarSrc(src) {
130
+ try {
131
+ const url = new URL(src);
132
+ if (url.protocol === "https:") return true;
133
+ if (url.protocol === "data:") return /^data:image\/[a-z0-9.+-]+/i.test(src);
134
+ return false;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+ /**
140
+ * @internal Avatar image for a resolved name, hiding itself on load error
141
+ * (never a broken-image icon) — the name renders as a complete, avatar-less
142
+ * row. Purely presentational; its only state is the per-instance load-error
143
+ * flag (INV-121).
144
+ *
145
+ * The error state is keyed to the current `src` rather than a bare boolean,
146
+ * so it resets automatically when `src` changes — a virtualized/reused list
147
+ * row that scrolls to a new address re-attempts its avatar instead of staying
148
+ * permanently hidden by a prior URL's failure (INV-59).
149
+ *
150
+ * @param props - {@link AddressAvatarProps}.
151
+ * @returns The avatar `<img>`, or `null` when the load failed for this `src`.
152
+ */
153
+ function AddressAvatar({ src, alt = "", variant = "fill", sizeClassName }) {
154
+ const [failedSrc, setFailedSrc] = React$1.useState(null);
155
+ if (!isAllowedAvatarSrc(src)) return null;
156
+ if (failedSrc === src) return null;
157
+ return /* @__PURE__ */ jsx("img", {
158
+ src,
159
+ alt,
160
+ className: cn("object-cover", sizeClassName ?? (variant === "fill" ? "absolute inset-0 h-full w-full rounded-none" : "h-4 w-4 shrink-0 rounded-full")),
161
+ referrerPolicy: "no-referrer",
162
+ onError: () => setFailedSrc(src)
163
+ });
164
+ }
165
+
122
166
  //#endregion
123
167
  //#region src/components/ui/address-display/context.ts
124
168
  /**
@@ -127,6 +171,14 @@ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
127
171
  * only components (required by React Fast Refresh).
128
172
  */
129
173
  const AddressLabelContext = createContext(null);
174
+ /**
175
+ * @internal Shared context instance consumed by AddressDisplay,
176
+ * AddressNameProvider, and useAddressName. Sibling of AddressLabelContext
177
+ * (SF-4 INV-122): carries a synchronous, value-only reverse-name resolver.
178
+ * Defaults to `null` — no provider means no resolution and a byte-identical
179
+ * zero-injection render (INV-54).
180
+ */
181
+ const AddressNameContext = createContext(null);
130
182
 
131
183
  //#endregion
132
184
  //#region src/components/ui/address-display/address-display.tsx
@@ -143,16 +195,37 @@ function usePrefersHover() {
143
195
  }, []), () => typeof window !== "undefined" && typeof window.matchMedia !== "undefined" ? window.matchMedia("(hover: hover)").matches : true, () => true);
144
196
  }
145
197
  /**
198
+ * Strip C0/C1 controls, bidi embeds/isolates, and zero-width characters from a
199
+ * verified name at the display seam. ENSIP-15 normalization remains the
200
+ * adapter's job; this only removes invisible directionality/control glyphs so
201
+ * a raw render cannot spoof adjacent UI (homoglyph confusables are out of scope).
202
+ */
203
+ function sanitizeVerifiedNameForDisplay(name) {
204
+ return name.replace(/[\u0000-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF]/g, "");
205
+ }
206
+ /**
146
207
  * Displays a blockchain address with optional truncation, copy button,
147
- * explorer link, tooltip, and human-readable label.
208
+ * explorer link, tooltip, human-readable label, and (when an already-resolved
209
+ * record is injected) a forward-verified name + avatar.
148
210
  *
149
- * Labels are resolved in priority order:
211
+ * Labels are resolved in priority order (INV-56):
150
212
  * 1. Explicit `label` prop
151
213
  * 2. `AddressLabelContext` resolver (via `AddressLabelProvider`)
152
- * 3. No label (renders address only, identical to previous behavior)
214
+ * 3. Forward-verified resolved name (`resolvedName` prop, else
215
+ * `AddressNameContext` via `AddressNameProvider`) — a record with
216
+ * `forwardVerified: false` is suppressed to hex (INV-52, LOCKED)
217
+ * 4. No label (renders address only, identical to previous behavior)
218
+ *
219
+ * The component is synchronous and capability-free (INV-121): it never
220
+ * resolves anything itself — it reads injected values. Hex renders on first
221
+ * commit; the name+avatar swap in when the react layer feeds an updated
222
+ * record (progressive enhancement, INV-61). With no `resolvedName` and no
223
+ * `AddressNameProvider`, output is byte-identical to the pre-enhancement
224
+ * component (INV-54, LOCKED).
153
225
  *
154
- * Pass `disableLabel` to suppress context-based resolution (e.g. when the
155
- * surrounding UI already shows a name, such as a contract selector).
226
+ * Pass `disableLabel` to render fully raw it suppresses both the context
227
+ * alias and any injected resolved name (e.g. when the surrounding UI already
228
+ * shows a name, such as a contract selector).
156
229
  *
157
230
  * @example
158
231
  * ```tsx
@@ -179,18 +252,30 @@ function usePrefersHover() {
179
252
  * // Inline variant (no chip background) — useful inside wallet bars
180
253
  * <AddressDisplay address="0x742d35Cc..." variant="inline" showTooltip showCopyButton />
181
254
  *
255
+ * // Reverse-resolved name + avatar via an injected value (react layer owns the async)
256
+ * <AddressDisplay address="0x742d35Cc..." resolvedName={record} showCopyButton />
257
+ *
258
+ * // Or subtree-wide via context (rows stay plain <AddressDisplay/>)
259
+ * <AddressNameProvider resolveAddressName={readFromResolutionCache}>
260
+ * <AddressDisplay address="0x742d35Cc..." />
261
+ * </AddressNameProvider>
262
+ *
182
263
  * // Truncate only when an alias/label is present (full address when unlabeled)
183
264
  * <AddressDisplay address="G..." truncateWhenLabeled />
184
265
  * ```
185
266
  */
186
- function AddressDisplay({ address, truncate: truncateProp, truncateWhenLabeled = false, untruncateOnHover = false, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, label: labelProp, onLabelEdit: onLabelEditProp, networkId, disableLabel = false, showTooltip = false, variant = "chip", className, onPointerEnter, onPointerLeave, onMouseEnter, onMouseLeave, onClick, ...props }) {
267
+ function AddressDisplay({ address, truncate: truncateProp, truncateWhenLabeled = false, untruncateOnHover = false, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, label: labelProp, onLabelEdit: onLabelEditProp, networkId, disableLabel = false, showTooltip = false, variant = "chip", avatar, avatarVariant = "fill", resolvedName, className, onPointerEnter, onPointerLeave, onMouseEnter, onMouseLeave, onClick, ...props }) {
187
268
  const [copied, setCopied] = React$1.useState(false);
188
269
  const [isHovered, setIsHovered] = React$1.useState(false);
189
270
  const copyTimeoutRef = React$1.useRef(null);
190
271
  const prefersHover = usePrefersHover();
191
272
  const resolver = React$1.useContext(AddressLabelContext);
273
+ const nameResolver = React$1.useContext(AddressNameContext);
192
274
  const resolvedLabel = disableLabel ? void 0 : labelProp ?? resolver?.resolveLabel(address, networkId);
193
- const effectiveTruncate = truncateProp !== void 0 ? truncateProp : truncateWhenLabeled ? Boolean(resolvedLabel) : true;
275
+ const record = disableLabel ? void 0 : resolvedName ?? nameResolver?.resolveAddressName(address, networkId);
276
+ const ensName = record?.forwardVerified === true ? sanitizeVerifiedNameForDisplay(record.name) : void 0;
277
+ const effectiveLabel = resolvedLabel ?? ensName;
278
+ const effectiveTruncate = truncateProp !== void 0 ? truncateProp : truncateWhenLabeled ? Boolean(effectiveLabel) : true;
194
279
  const contextEditHandler = React$1.useCallback(() => {
195
280
  resolver?.onEditLabel?.(address, networkId);
196
281
  }, [
@@ -264,6 +349,17 @@ function AddressDisplay({ address, truncate: truncateProp, truncateWhenLabeled =
264
349
  children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" })
265
350
  })
266
351
  ] });
352
+ const ensAvatarUrl = ensName !== void 0 && resolvedLabel === void 0 ? record?.avatarUrl : void 0;
353
+ const effectiveAvatar = avatar ?? (ensAvatarUrl ? /* @__PURE__ */ jsx(AddressAvatar, {
354
+ src: ensAvatarUrl,
355
+ variant: avatarVariant
356
+ }) : void 0);
357
+ const isFillAvatar = avatarVariant === "fill" && Boolean(effectiveAvatar);
358
+ const fillAvatarChipClassName = isFillAvatar && isChip ? "overflow-hidden" : void 0;
359
+ const avatarSlot = effectiveAvatar ? /* @__PURE__ */ jsx("span", {
360
+ className: cn("flex shrink-0", isFillAvatar ? cn("relative w-9 self-stretch overflow-hidden mr-2", isChip && "-my-1 -ml-2") : "mr-1.5 items-center"),
361
+ children: effectiveAvatar
362
+ }) : null;
267
363
  const shouldShowTooltip = showTooltip && effectiveTruncate;
268
364
  const wrapWithTooltip = (content) => {
269
365
  if (!shouldShowTooltip) return content;
@@ -278,33 +374,43 @@ function AddressDisplay({ address, truncate: truncateProp, truncateWhenLabeled =
278
374
  })] })
279
375
  });
280
376
  };
281
- if (resolvedLabel) return wrapWithTooltip(/* @__PURE__ */ jsxs("div", {
282
- className: cn("group inline-flex max-w-full min-w-0 flex-col", isChip && "rounded-md bg-slate-100 px-2 py-1", "text-xs text-slate-700", expandInteractionClassName, className),
283
- onPointerEnter: handlePointerEnter,
284
- onPointerLeave: handlePointerLeave,
285
- onClick: handleUntruncateClick,
286
- ...props,
287
- children: [/* @__PURE__ */ jsx("span", {
377
+ if (effectiveLabel) {
378
+ const labelColumn = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
288
379
  className: "truncate font-sans font-medium text-slate-900 leading-snug",
289
- children: resolvedLabel
380
+ children: effectiveLabel
290
381
  }), /* @__PURE__ */ jsxs("div", {
291
382
  className: "flex min-w-0 items-center font-mono text-[10px] text-slate-400 leading-snug",
292
383
  children: [/* @__PURE__ */ jsx("span", {
293
384
  className: addressTextClassName,
294
385
  children: displayAddress
295
386
  }), actionButtons]
296
- })]
297
- }));
387
+ })] });
388
+ return wrapWithTooltip(/* @__PURE__ */ jsx("div", {
389
+ className: cn("group inline-flex max-w-full min-w-0", effectiveAvatar ? "items-center" : "flex-col", isChip && "rounded-md bg-slate-100 px-2 py-1", fillAvatarChipClassName, "text-xs text-slate-700", expandInteractionClassName, className),
390
+ onPointerEnter: handlePointerEnter,
391
+ onPointerLeave: handlePointerLeave,
392
+ onClick: handleUntruncateClick,
393
+ ...props,
394
+ children: effectiveAvatar ? /* @__PURE__ */ jsxs(Fragment, { children: [avatarSlot, /* @__PURE__ */ jsx("div", {
395
+ className: "inline-flex min-w-0 flex-col",
396
+ children: labelColumn
397
+ })] }) : labelColumn
398
+ }));
399
+ }
298
400
  return wrapWithTooltip(/* @__PURE__ */ jsxs("div", {
299
- className: cn("group inline-flex max-w-full min-w-0 items-center", isChip && "rounded-md bg-slate-100 px-2 py-1", "text-xs font-mono text-slate-700", expandInteractionClassName, className),
401
+ className: cn("group inline-flex max-w-full min-w-0 items-center", isChip && "rounded-md bg-slate-100 px-2 py-1", fillAvatarChipClassName, "text-xs font-mono text-slate-700", expandInteractionClassName, className),
300
402
  onPointerEnter: handlePointerEnter,
301
403
  onPointerLeave: handlePointerLeave,
302
404
  onClick: handleUntruncateClick,
303
405
  ...props,
304
- children: [/* @__PURE__ */ jsx("span", {
305
- className: addressTextClassName,
306
- children: displayAddress
307
- }), actionButtons]
406
+ children: [
407
+ avatarSlot,
408
+ /* @__PURE__ */ jsx("span", {
409
+ className: addressTextClassName,
410
+ children: displayAddress
411
+ }),
412
+ actionButtons
413
+ ]
308
414
  }));
309
415
  }
310
416
 
@@ -402,6 +508,79 @@ function useAddressLabel(address, networkId) {
402
508
  };
403
509
  }
404
510
 
511
+ //#endregion
512
+ //#region src/components/ui/address-display/address-name-context.tsx
513
+ /**
514
+ * Address Name Context
515
+ *
516
+ * Provides a React context for surfacing already-resolved reverse name
517
+ * records (`ResolvedName`) for blockchain addresses. When an
518
+ * `AddressNameProvider` is mounted, every `AddressDisplay` in the subtree
519
+ * reads its record synchronously — no call-site changes — and renders the
520
+ * name/avatar only when the record is forward-verified (INV-52).
521
+ *
522
+ * Structural mirror of `AddressLabelProvider` (INV-122): synchronous,
523
+ * value-only, no-op by default. The resolver implementation lives in the
524
+ * react/renderer layer, which bridges async resolution state into this
525
+ * synchronous read; this file stays capability-free (INV-121).
526
+ *
527
+ * @example
528
+ * ```tsx
529
+ * import { AddressNameProvider } from '@openzeppelin/ui-components';
530
+ *
531
+ * <AddressNameProvider resolveAddressName={readFromResolutionCache}>
532
+ * <TxHistory /> {// rows are plain <AddressDisplay/> — verified names fill in }
533
+ * </AddressNameProvider>
534
+ * ```
535
+ */
536
+ /**
537
+ * Provides reverse-name resolution to all `AddressDisplay` instances in the
538
+ * subtree. `resolveAddressName` MUST be synchronous — it is called during
539
+ * render (INV-122). A resolver that returns `undefined` for an address is
540
+ * behaviorally identical to mounting no provider (INV-54).
541
+ *
542
+ * @param props - Resolver function and children
543
+ */
544
+ function AddressNameProvider({ children, resolveAddressName }) {
545
+ const value = React$1.useMemo(() => ({ resolveAddressName }), [resolveAddressName]);
546
+ return /* @__PURE__ */ jsx(AddressNameContext.Provider, {
547
+ value,
548
+ children
549
+ });
550
+ }
551
+
552
+ //#endregion
553
+ //#region src/components/ui/address-display/use-address-name.ts
554
+ /**
555
+ * Convenience hook for reading a reverse name record from the nearest
556
+ * `AddressNameProvider`.
557
+ *
558
+ * Kept in its own file so that `address-name-context.tsx` exports only
559
+ * components (required by React Fast Refresh) — mirrors `use-address-label.ts`.
560
+ */
561
+ /**
562
+ * Reads the `AddressNameContext` for one address, synchronously (INV-122).
563
+ * Returns `{ record: undefined }` when no provider is mounted — never throws,
564
+ * never suspends. Note the record is returned verbatim: callers rendering a
565
+ * name from it must apply the same `forwardVerified === true` gate that
566
+ * `AddressDisplay` applies (INV-52).
567
+ *
568
+ * @param address - The blockchain address to reverse-resolve
569
+ * @param networkId - Optional network identifier scoping the lookup
570
+ * @returns The current best-known record for the address
571
+ *
572
+ * @example
573
+ * ```tsx
574
+ * function EnsBadge({ address }: { address: string }) {
575
+ * const { record } = useAddressName(address);
576
+ * return record?.forwardVerified ? <Badge>{record.name}</Badge> : null;
577
+ * }
578
+ * ```
579
+ */
580
+ function useAddressName(address, networkId) {
581
+ return { record: React$1.useContext(AddressNameContext)?.resolveAddressName(address, networkId) };
582
+ }
583
+
405
584
  //#endregion
406
585
  //#region src/components/ui/alert.tsx
407
586
  const alertVariants = cva("relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", {
@@ -2539,6 +2718,43 @@ function WizardLayout(props) {
2539
2718
  });
2540
2719
  }
2541
2720
 
2721
+ //#endregion
2722
+ //#region src/components/fields/address-suggestion/AddressSuggestionList.tsx
2723
+ /**
2724
+ * Render the suggestion dropdown. Returns `null` when there are no suggestions
2725
+ * (the consuming field also gates on `hasSuggestions`, so this is a safety net).
2726
+ *
2727
+ * @param props - {@link AddressSuggestionListProps}.
2728
+ */
2729
+ function AddressSuggestionList({ id, suggestions, highlightedIndex, onSelect, onHighlight }) {
2730
+ if (suggestions.length === 0) return null;
2731
+ return /* @__PURE__ */ jsx("div", {
2732
+ id: `${id}-suggestions`,
2733
+ className: cn("absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md", "max-h-48 overflow-auto"),
2734
+ role: "listbox",
2735
+ children: suggestions.map((s, i) => /* @__PURE__ */ jsxs("button", {
2736
+ id: `${id}-suggestion-${i}`,
2737
+ type: "button",
2738
+ role: "option",
2739
+ "aria-selected": i === highlightedIndex,
2740
+ className: cn("flex w-full flex-col px-3 py-2 text-left text-sm", "hover:bg-selected/10", i === highlightedIndex && "bg-selected/10"),
2741
+ onMouseDown: (e) => {
2742
+ e.preventDefault();
2743
+ onSelect(s);
2744
+ },
2745
+ onMouseEnter: () => onHighlight(i),
2746
+ children: [/* @__PURE__ */ jsx("span", {
2747
+ className: "font-medium",
2748
+ children: s.label
2749
+ }), /* @__PURE__ */ jsx("span", {
2750
+ className: "truncate font-mono text-xs text-muted-foreground",
2751
+ children: s.value
2752
+ })]
2753
+ }, `${s.value}-${s.description ?? i}`))
2754
+ });
2755
+ }
2756
+ AddressSuggestionList.displayName = "AddressSuggestionList";
2757
+
2542
2758
  //#endregion
2543
2759
  //#region src/components/fields/address-suggestion/context.ts
2544
2760
  /**
@@ -2548,6 +2764,372 @@ function WizardLayout(props) {
2548
2764
  */
2549
2765
  const AddressSuggestionContext = createContext(null);
2550
2766
 
2767
+ //#endregion
2768
+ //#region src/components/fields/address-suggestion/useAddressSuggestionField.ts
2769
+ /**
2770
+ * Headless suggestion-dropdown behavior for address fields.
2771
+ *
2772
+ * Extracted from `AddressField`'s inline dropdown logic (SF-3, D9) into one
2773
+ * reusable headless hook the base `AddressField` consumes — no divergence, no
2774
+ * regression (INV-72). It owns:
2775
+ * - `AddressSuggestionContext` resolution (explicit `suggestions` prop overrides),
2776
+ * - the `MAX_SUGGESTIONS` (5) cap on context-resolved lists,
2777
+ * - the 200 ms suggestion debounce,
2778
+ * - keyboard highlight state with wrapping ArrowUp / ArrowDown navigation,
2779
+ * - click-outside-to-close.
2780
+ *
2781
+ * It is capability-free and chain-agnostic — nothing about ENS or any chain
2782
+ * leaks into `@openzeppelin/ui-components`. The consuming field owns the RHF
2783
+ * value write on selection and the Enter / Escape input handlers; this hook
2784
+ * exposes the state and primitives they need.
2785
+ *
2786
+ * Distinct from the pre-existing convenience hook `useAddressSuggestions(query,
2787
+ * networkId)`, which resolves a raw suggestion list from context with no
2788
+ * debounce / cap / keyboard state — that hook is unchanged and still exported.
2789
+ *
2790
+ * @see INV-72 (suggestion-dropdown behavior preserved with zero regression; SF-5 depends)
2791
+ * @see INV-94 (legacy a11y + suggestion keyboard contract preserved verbatim)
2792
+ */
2793
+ /** Suggestion debounce window (ms). Matches the legacy `AddressField`. */
2794
+ const DEBOUNCE_MS = 200;
2795
+ /** Cap on context-resolved suggestions. Explicit arrays are rendered as-is. */
2796
+ const MAX_SUGGESTIONS = 5;
2797
+ /**
2798
+ * Headless address-suggestion dropdown behavior. See the module docstring for
2799
+ * the full behavior contract.
2800
+ *
2801
+ * @param args - {@link UseAddressSuggestionFieldArgs}.
2802
+ * @returns {@link UseAddressSuggestionFieldResult}.
2803
+ */
2804
+ function useAddressSuggestionField({ inputValue, suggestions: suggestionsProp, networkId }) {
2805
+ const contextResolver = useContext(AddressSuggestionContext);
2806
+ const containerRef = useRef(null);
2807
+ const [debouncedQuery, setDebouncedQuery] = useState("");
2808
+ const [showSuggestions, setShowSuggestions] = useState(false);
2809
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
2810
+ useEffect(() => {
2811
+ if (!inputValue.trim()) {
2812
+ setDebouncedQuery("");
2813
+ return;
2814
+ }
2815
+ const timer = setTimeout(() => setDebouncedQuery(inputValue), DEBOUNCE_MS);
2816
+ return () => clearTimeout(timer);
2817
+ }, [inputValue]);
2818
+ const suggestionsDisabled = suggestionsProp === false;
2819
+ const resolvedSuggestions = useMemo(() => {
2820
+ if (suggestionsDisabled) return [];
2821
+ if (Array.isArray(suggestionsProp)) return suggestionsProp;
2822
+ if (!contextResolver || !debouncedQuery.trim()) return [];
2823
+ return contextResolver.resolveSuggestions(debouncedQuery, networkId).slice(0, MAX_SUGGESTIONS);
2824
+ }, [
2825
+ suggestionsDisabled,
2826
+ suggestionsProp,
2827
+ contextResolver,
2828
+ debouncedQuery,
2829
+ networkId
2830
+ ]);
2831
+ const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
2832
+ useEffect(() => {
2833
+ let active = true;
2834
+ const handleClickOutside = (e) => {
2835
+ if (active && containerRef.current && !containerRef.current.contains(e.target)) setShowSuggestions(false);
2836
+ };
2837
+ document.addEventListener("mousedown", handleClickOutside);
2838
+ return () => {
2839
+ active = false;
2840
+ document.removeEventListener("mousedown", handleClickOutside);
2841
+ };
2842
+ }, []);
2843
+ const onContainerKeyDown = useCallback((e) => {
2844
+ if (!hasSuggestions) return;
2845
+ if (e.key === "ArrowDown") {
2846
+ e.preventDefault();
2847
+ setHighlightedIndex((prev) => prev < resolvedSuggestions.length - 1 ? prev + 1 : 0);
2848
+ } else if (e.key === "ArrowUp") {
2849
+ e.preventDefault();
2850
+ setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
2851
+ }
2852
+ }, [hasSuggestions, resolvedSuggestions.length]);
2853
+ const onInputChange = useCallback((value) => {
2854
+ setShowSuggestions(value.length > 0);
2855
+ setHighlightedIndex(-1);
2856
+ }, []);
2857
+ return {
2858
+ containerRef,
2859
+ resolvedSuggestions,
2860
+ hasSuggestions,
2861
+ highlightedIndex,
2862
+ suggestionsDisabled,
2863
+ setHighlightedIndex,
2864
+ closeSuggestions: useCallback(() => {
2865
+ setShowSuggestions(false);
2866
+ setHighlightedIndex(-1);
2867
+ }, []),
2868
+ onInputChange,
2869
+ onContainerKeyDown
2870
+ };
2871
+ }
2872
+
2873
+ //#endregion
2874
+ //#region src/components/fields/name-resolution/useInjectedNameResolution.ts
2875
+ /**
2876
+ * Injected name-resolution machine (SF-3).
2877
+ *
2878
+ * A thin, capability-free async machine that `AddressField` consumes: given a
2879
+ * typed input and the injected `resolveName`, it debounces, dispatches, tracks
2880
+ * status, and applies the out-of-order name-drop guard (INV-117). It is the
2881
+ * component-boundary analogue of SF-2's `useResolveName`, with all hard
2882
+ * mechanics (cache, dedupe, retries, UNSUPPORTED synthesis) deliberately left
2883
+ * behind the injected function — this hook does ONLY debounce + status
2884
+ * derivation + the funds guards.
2885
+ *
2886
+ * The machine holds no resolved-hex cache of its own (INV-85): its single
2887
+ * `settled` record is valid only while it matches the current debounced input,
2888
+ * the current resolver function identity, and the current retry attempt — any
2889
+ * mismatch derives `loading`, never a stale `resolved`.
2890
+ *
2891
+ * Resolver-identity-churn backstop (INV-123/124/125). The dispatch effect keys
2892
+ * on the injected `resolveName` function identity, so an unstable (non-memoized)
2893
+ * integrator resolver re-fires it on every render. To keep that from becoming an
2894
+ * unbounded RPC loop (rate-ban + cost + funds-path DoS), the machine caps
2895
+ * dispatches per resolution intent — the pair (normalized debounced input × retry
2896
+ * attempt) — at {@link MAX_DISPATCHES_PER_INTENT}, regardless of render count and
2897
+ * regardless of how many times `resolveName` identity changes under that intent.
2898
+ * The budget resets only on intent change (a new typed name, or `retry()`) or when
2899
+ * the field leaves the active name-resolution path (clear / disable). It does
2900
+ * *not* reset on `settled.source` mismatch: that would let two settling sources
2901
+ * flap and refill the counter (settling churn), silently defeating INV-123. A
2902
+ * genuine network swap still re-dispatches within the shared intent budget
2903
+ * (INV-119 / acceptance #5) because a single identity change costs one unit.
2904
+ * Clear→retype of the same name gets a fresh budget via the leave-path reset, so
2905
+ * it cannot accumulate toward the cap across cycles and then strand a later swap.
2906
+ * A churning identity that exhausts the budget emits a one-shot, development-only
2907
+ * warning (INV-124) and degrades to the safe gated state (bounded RPC, RHF value
2908
+ * `''`, submit gated, never a wrong hex, never a throw — INV-125). This backstop
2909
+ * is entirely internal: no signature, param, or return-type change; `AddressField`
2910
+ * consumes the machine unchanged.
2911
+ */
2912
+ /**
2913
+ * Default debounce window (ms) for typed names — mirrors SF-2's
2914
+ * `forwardDebounceMs` default so the field and the bare hook feel identical.
2915
+ */
2916
+ const NAME_RESOLUTION_DEBOUNCE_MS = 300;
2917
+ /**
2918
+ * Cap on resolver dispatches per resolution intent — the pair (normalized
2919
+ * debounced input × retry attempt) — regardless of how many times the field
2920
+ * re-renders or how many times `resolveName` identity changes under that intent
2921
+ * (INV-123). Chosen to sit comfortably above the dispatches a single displayed
2922
+ * input legitimately incurs (1 initial call + any human-initiated genuine network
2923
+ * swaps of that same name, each ≤1 unit — INV-119) and far below per-render
2924
+ * identity churn (hundreds/sec): a user does not switch networks eight times
2925
+ * while one unedited name sits in the field, whereas churn blows past eight
2926
+ * within milliseconds. The budget resets when the intent changes or when the
2927
+ * field leaves the name-resolution path (clear / disable) — never on
2928
+ * `settled.source` mismatch, so settling-source oscillation cannot refill it.
2929
+ */
2930
+ const MAX_DISPATCHES_PER_INTENT = 8;
2931
+ /**
2932
+ * Debounce + dispatch + status derivation over an injected `resolveName`.
2933
+ *
2934
+ * Guarantees:
2935
+ * - INV-83: no call unless `enabled` (name-candidate + injected method), and
2936
+ * never for a debounced copy that lags the current normalized input.
2937
+ * - INV-117: a settled result whose requested name ≠ the current debounced
2938
+ * input is discarded (last-write-wins at the component boundary).
2939
+ * - INV-87: a rejecting resolver (contract violation) is mapped to a typed
2940
+ * `ADAPTER_ERROR` — nothing throws into the render tree.
2941
+ * - INV-123: resolver dispatches are bounded per resolution intent (normalized
2942
+ * debounced input × retry attempt) at {@link MAX_DISPATCHES_PER_INTENT},
2943
+ * regardless of render count or resolver-identity oscillation; the budget
2944
+ * resets on intent change or when leaving the name-resolution path — never on
2945
+ * `settled.source` mismatch; `retry()` never consumes an existing budget.
2946
+ * - INV-124: an identity-churning resolver that exhausts the budget triggers a
2947
+ * one-shot, development-only `logger.warn` (silent in production).
2948
+ * - INV-125: under sustained churn the field stays in the safe gated state — the
2949
+ * perpetually-changing identity keeps failing the `settled.source` check, so
2950
+ * the machine derives `loading`, never `resolved`; combined with INV-87 it
2951
+ * never throws and never writes a hex.
2952
+ * - No `setState` after unmount: every dispatch is cancelled on cleanup.
2953
+ *
2954
+ * @param params - {@link UseInjectedNameResolutionParams}
2955
+ * @returns The current {@link InjectedNameResolutionResult}.
2956
+ */
2957
+ function useInjectedNameResolution({ input, enabled, resolveName, debounceMs = NAME_RESOLUTION_DEBOUNCE_MS }) {
2958
+ const normalized = input.trim().toLowerCase();
2959
+ const [debounced, setDebounced] = useState(normalized);
2960
+ const seededRef = useRef(false);
2961
+ useEffect(() => {
2962
+ if (!seededRef.current) {
2963
+ seededRef.current = true;
2964
+ return;
2965
+ }
2966
+ if (debounceMs <= 0) {
2967
+ setDebounced(normalized);
2968
+ return;
2969
+ }
2970
+ const timer = setTimeout(() => setDebounced(normalized), debounceMs);
2971
+ return () => clearTimeout(timer);
2972
+ }, [normalized, debounceMs]);
2973
+ const [attempt, setAttempt] = useState(0);
2974
+ const [settled, setSettled] = useState(null);
2975
+ const currentTargetRef = useRef("");
2976
+ currentTargetRef.current = enabled && resolveName ? debounced : "";
2977
+ const intentKeyRef = useRef("");
2978
+ const dispatchCountRef = useRef(0);
2979
+ const churnWarnedRef = useRef(false);
2980
+ const warnOnceOnChurn = useCallback(() => {
2981
+ if (process.env.NODE_ENV === "production" || churnWarnedRef.current) return;
2982
+ churnWarnedRef.current = true;
2983
+ logger.warn("AddressField", "The injected name resolver changes identity on every render, so inline name resolution was bounded to prevent an unbounded RPC loop. Memoize the resolver (e.g. wrap it in useMemo, or use useRuntimeNameResolver from @openzeppelin/ui-react) so its function identity is stable across renders. See the ens-address-input integration guide.");
2984
+ }, []);
2985
+ useEffect(() => {
2986
+ if (!enabled || !resolveName || debounced === "") {
2987
+ intentKeyRef.current = "";
2988
+ dispatchCountRef.current = 0;
2989
+ return;
2990
+ }
2991
+ if (debounced !== normalized) return;
2992
+ const intentKey = `${debounced}\u0000${attempt}`;
2993
+ if (intentKeyRef.current !== intentKey) {
2994
+ intentKeyRef.current = intentKey;
2995
+ dispatchCountRef.current = 0;
2996
+ }
2997
+ if (dispatchCountRef.current >= MAX_DISPATCHES_PER_INTENT) {
2998
+ warnOnceOnChurn();
2999
+ return;
3000
+ }
3001
+ dispatchCountRef.current += 1;
3002
+ let cancelled = false;
3003
+ const requestedName = debounced;
3004
+ const requestedAttempt = attempt;
3005
+ const source = resolveName;
3006
+ const settle = (result) => {
3007
+ if (cancelled) return;
3008
+ if (requestedName !== currentTargetRef.current) return;
3009
+ setSettled({
3010
+ name: requestedName,
3011
+ result,
3012
+ source,
3013
+ attempt: requestedAttempt
3014
+ });
3015
+ };
3016
+ source(requestedName).then(settle, (cause) => {
3017
+ settle({
3018
+ ok: false,
3019
+ error: {
3020
+ code: "ADAPTER_ERROR",
3021
+ message: cause instanceof Error ? cause.message : String(cause),
3022
+ cause
3023
+ }
3024
+ });
3025
+ });
3026
+ return () => {
3027
+ cancelled = true;
3028
+ };
3029
+ }, [
3030
+ enabled,
3031
+ resolveName,
3032
+ debounced,
3033
+ normalized,
3034
+ attempt,
3035
+ warnOnceOnChurn
3036
+ ]);
3037
+ const retry = useCallback(() => {
3038
+ setAttempt((current) => current + 1);
3039
+ }, []);
3040
+ if (!enabled || !resolveName || normalized === "") return { status: "idle" };
3041
+ if (debounced !== normalized) return {
3042
+ status: "debouncing",
3043
+ name: normalized
3044
+ };
3045
+ if (settled !== null && settled.name === debounced && settled.source === resolveName && settled.attempt === attempt) {
3046
+ if (settled.result.ok) return {
3047
+ status: "resolved",
3048
+ name: settled.name,
3049
+ data: settled.result.value
3050
+ };
3051
+ return {
3052
+ status: "error",
3053
+ name: settled.name,
3054
+ error: settled.result.error,
3055
+ retry
3056
+ };
3057
+ }
3058
+ return {
3059
+ status: "loading",
3060
+ name: normalized
3061
+ };
3062
+ }
3063
+
3064
+ //#endregion
3065
+ //#region src/components/fields/name-resolution/context.ts
3066
+ /**
3067
+ * Process-global key for the context object. Uses `Symbol.for` so all duplicated
3068
+ * module instances share ONE React context — the same cross-bundle-singleton
3069
+ * pattern as `@openzeppelin/ui-react`'s `NameResolutionContext` /
3070
+ * `WalletStateContext` (bundler pre-bundling / npm-installed consumers).
3071
+ */
3072
+ const CONTEXT_KEY = Symbol.for("@openzeppelin/ui-components/NameResolverContext");
3073
+ function getOrCreateSharedContext() {
3074
+ const globalScope = globalThis;
3075
+ if (!globalScope[CONTEXT_KEY]) globalScope[CONTEXT_KEY] = createContext(null);
3076
+ return globalScope[CONTEXT_KEY];
3077
+ }
3078
+ /**
3079
+ * @internal Shared context instance consumed by both AddressField and
3080
+ * NameResolverProvider (INV-118). Kept in its own file so component files
3081
+ * export only components (required by React Fast Refresh).
3082
+ *
3083
+ * `null` means no provider is mounted — every ENS branch in `AddressField` is
3084
+ * then dead code and the field is byte-identical to its pre-ENS behavior
3085
+ * (INV-82).
3086
+ */
3087
+ const NameResolverContext = getOrCreateSharedContext();
3088
+
3089
+ //#endregion
3090
+ //#region src/components/fields/name-resolution/useNameResolver.ts
3091
+ /**
3092
+ * Read the injected resolver context. Returns `null` when no
3093
+ * `NameResolverProvider` is mounted — the consuming field must then treat
3094
+ * every ENS branch as dead code (INV-82).
3095
+ *
3096
+ * @returns The extended context value, or `null` when no provider is mounted.
3097
+ */
3098
+ function useNameResolver() {
3099
+ return useContext(NameResolverContext);
3100
+ }
3101
+
3102
+ //#endregion
3103
+ //#region src/components/fields/name-resolution/useResolvingAnnouncerCopy.ts
3104
+ /** Default escalation threshold for long-latency resolves (INV-129/130). */
3105
+ const RESOLVING_ANNOUNCER_ESCALATE_AFTER_MS = 3e3;
3106
+ const PHASE_1_COPY = "Resolving…";
3107
+ const PHASE_2_COPY = "Still resolving…";
3108
+ /**
3109
+ * Returns the announcer string for debouncing/loading arms.
3110
+ * Phase 1: "Resolving…" (SF-3 default, unchanged for first N ms).
3111
+ * Phase 2: "Still resolving…" after threshold — reduces frozen perception
3112
+ * during CCIP-scale latency without naming gateway/CCIP/v2.
3113
+ */
3114
+ function useResolvingAnnouncerCopy({ isPending, escalateAfterMs = RESOLVING_ANNOUNCER_ESCALATE_AFTER_MS }) {
3115
+ const [escalated, setEscalated] = useState(false);
3116
+ useEffect(() => {
3117
+ if (!isPending) {
3118
+ setEscalated(false);
3119
+ return;
3120
+ }
3121
+ setEscalated(false);
3122
+ const timeoutId = window.setTimeout(() => {
3123
+ setEscalated(true);
3124
+ }, escalateAfterMs);
3125
+ return () => {
3126
+ window.clearTimeout(timeoutId);
3127
+ };
3128
+ }, [isPending, escalateAfterMs]);
3129
+ if (!isPending) return PHASE_1_COPY;
3130
+ return escalated ? PHASE_2_COPY : PHASE_1_COPY;
3131
+ }
3132
+
2551
3133
  //#endregion
2552
3134
  //#region src/components/fields/utils/accessibility.ts
2553
3135
  /**
@@ -2739,8 +3321,26 @@ function getWidthClasses(width) {
2739
3321
 
2740
3322
  //#endregion
2741
3323
  //#region src/components/fields/AddressField.tsx
2742
- const DEBOUNCE_MS = 200;
2743
- const MAX_SUGGESTIONS = 5;
3324
+ /**
3325
+ * Non-user-facing gate string returned by the sync validator while a typed name
3326
+ * is resolving, so `formState.isValid` is `false` even for an optional field
3327
+ * (INV-84). The visible message comes from the aria-live region, never this string.
3328
+ */
3329
+ const NAME_RESOLUTION_PENDING_GATE = "__ens_name_resolution_pending__";
3330
+ /** Codes for which a `retry()` affordance is offered — transient only (INV-90). */
3331
+ const TRANSIENT_ERROR_CODES = new Set([
3332
+ "RESOLUTION_TIMEOUT",
3333
+ "EXTERNAL_GATEWAY_ERROR",
3334
+ "ADAPTER_ERROR"
3335
+ ]);
3336
+ /**
3337
+ * Normalize a typed name for the resolved-write match guard: trim then lowercase,
3338
+ * mirroring the SF-2 engine (whose echoed `result.name` is already normalized).
3339
+ * Comparing the raw typed value would spuriously fail on case/whitespace (INV-79).
3340
+ */
3341
+ function normalizeResolvedName(value) {
3342
+ return value.trim().toLowerCase();
3343
+ }
2744
3344
  /**
2745
3345
  * Address input field component specifically designed for blockchain addresses via React Hook Form integration.
2746
3346
  *
@@ -2768,19 +3368,35 @@ const MAX_SUGGESTIONS = 5;
2768
3368
  * Pass `suggestions={false}` to opt out when a provider is mounted.
2769
3369
  *
2770
3370
  * The suggestion dropdown includes built-in debouncing, keyboard navigation (Arrow keys,
2771
- * Enter, Escape), click-outside dismissal, and ARIA listbox semantics.
3371
+ * Enter, Escape), click-outside dismissal, and ARIA listbox semantics — all provided by
3372
+ * the shared headless `useAddressSuggestionField` hook + `AddressSuggestionList`.
3373
+ *
3374
+ * **Inline name resolution (SF-3, opt-in via context).** When a
3375
+ * `NameResolverProvider` is mounted, the field also accepts a name (e.g.
3376
+ * `alice.eth`): the name is resolved inline through the injected `resolveName`
3377
+ * and the RHF form value becomes the resolved hex — never the name, never a
3378
+ * silently-coerced value. The correctness spine is a shadow-state model:
3379
+ * the `<input>` always shows the typed string (`inputValue`, INV-69); the RHF
3380
+ * value is `''` for every unresolved name state so submit stays gated with no
3381
+ * async validator (INV-75); the single name→hex write fires only on
3382
+ * `resolved` + normalized name-match (INV-79/80). With **no** provider mounted
3383
+ * every resolution branch below is dead code and the field is byte-identical
3384
+ * to its pre-ENS behavior (INV-82 — the LOCKED backward-compat guarantee).
3385
+ * The component stays capability-free: it never reads a runtime or wallet
3386
+ * state; everything arrives through the injected context (INV-118).
2772
3387
  */
2773
- function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, addressing, readOnly, suggestions: suggestionsProp, onSuggestionSelect }) {
3388
+ function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, addressing, readOnly, suggestions: suggestionsProp, onSuggestionSelect, onResolvedNameChange }) {
2774
3389
  const isRequired = !!validation?.required;
2775
3390
  const errorId = `${id}-error`;
2776
3391
  const descriptionId = `${id}-description`;
2777
- const contextResolver = useContext(AddressSuggestionContext);
2778
- const containerRef = useRef(null);
3392
+ const resolutionRegionId = `${id}-resolution`;
3393
+ const resolver = useNameResolver();
3394
+ const resolveName = resolver?.resolveName;
3395
+ const isValidName = resolver?.isValidName;
3396
+ const activeNetworkId = resolver?.activeNetworkId;
3397
+ const activeNetworkName = resolver?.activeNetworkName;
2779
3398
  const lastSetValueRef = useRef("");
2780
3399
  const [inputValue, setInputValue] = useState("");
2781
- const [debouncedQuery, setDebouncedQuery] = useState("");
2782
- const [showSuggestions, setShowSuggestions] = useState(false);
2783
- const [highlightedIndex, setHighlightedIndex] = useState(-1);
2784
3400
  const watchedFieldValue = useWatch({
2785
3401
  control,
2786
3402
  name
@@ -2792,173 +3408,271 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
2792
3408
  setInputValue(currentFieldValue);
2793
3409
  }
2794
3410
  }, [watchedFieldValue]);
3411
+ const isValidAddress = useMemo(() => addressing ? (v) => addressing.isValidAddress(v) : void 0, [addressing]);
3412
+ const classification = useMemo(() => classifyAddressInput(inputValue, {
3413
+ isValidAddress,
3414
+ isValidName
3415
+ }), [
3416
+ inputValue,
3417
+ isValidAddress,
3418
+ isValidName
3419
+ ]);
3420
+ const resolverActive = resolver !== null && classification === "name-candidate";
3421
+ const nameEnabled = resolverActive && resolveName != null;
3422
+ const forwardUnsupported = resolverActive && resolveName == null;
3423
+ const resolution = useInjectedNameResolution({
3424
+ input: inputValue,
3425
+ enabled: nameEnabled,
3426
+ resolveName
3427
+ });
3428
+ const chainScopeBlocked = resolution.status === "resolved" && isChainScopeMismatch(resolution.data.provenance, activeNetworkId);
3429
+ const resolvingAnnouncerCopy = useResolvingAnnouncerCopy({ isPending: !forwardUnsupported && (resolution.status === "debouncing" || resolution.status === "loading") });
3430
+ const resolverPresentRef = useRef(false);
3431
+ const classificationRef = useRef("empty");
3432
+ const statusRef = useRef("idle");
3433
+ const resolvedNameRef = useRef(void 0);
3434
+ const chainScopeBlockedRef = useRef(false);
3435
+ const validationRef = useRef(validation);
3436
+ const addressingRef = useRef(addressing);
3437
+ resolverPresentRef.current = resolver !== null;
3438
+ classificationRef.current = classification;
3439
+ statusRef.current = resolution.status;
3440
+ resolvedNameRef.current = resolution.status === "resolved" ? resolution.name : void 0;
3441
+ chainScopeBlockedRef.current = chainScopeBlocked;
3442
+ validationRef.current = validation;
3443
+ addressingRef.current = addressing;
3444
+ const { field, fieldState } = useController({
3445
+ control,
3446
+ name,
3447
+ rules: { validate: useCallback((value) => {
3448
+ if (resolverPresentRef.current && classificationRef.current === "name-candidate" && (statusRef.current !== "resolved" || chainScopeBlockedRef.current)) return NAME_RESOLUTION_PENDING_GATE;
3449
+ if (value === void 0 || value === null || value === "") return validationRef.current?.required ? "This field is required" : true;
3450
+ const standardValidationResult = validateField(value, validationRef.current);
3451
+ if (standardValidationResult !== true) return standardValidationResult;
3452
+ if (addressingRef.current && typeof value === "string") {
3453
+ if (!addressingRef.current.isValidAddress(value)) return "Invalid address format for the selected chain";
3454
+ }
3455
+ return true;
3456
+ }, []) },
3457
+ disabled: readOnly
3458
+ });
3459
+ const fieldOnChange = field.onChange;
3460
+ const gateFormState = useFormState({
3461
+ control,
3462
+ disabled: resolver === null
3463
+ });
3464
+ const observedIsValid = resolver !== null && gateFormState.isValid;
3465
+ const pendingNameGate = resolver !== null && classification === "name-candidate" && (resolution.status !== "resolved" || chainScopeBlocked);
2795
3466
  useEffect(() => {
2796
- if (!inputValue.trim()) {
2797
- setDebouncedQuery("");
2798
- return;
3467
+ if (pendingNameGate && observedIsValid) control.setError(name, {
3468
+ type: "validate",
3469
+ message: NAME_RESOLUTION_PENDING_GATE
3470
+ });
3471
+ }, [
3472
+ pendingNameGate,
3473
+ observedIsValid,
3474
+ control,
3475
+ name
3476
+ ]);
3477
+ const resolvedAddress = resolution.status === "resolved" ? resolution.data.address : void 0;
3478
+ const resolvedName = resolution.status === "resolved" ? resolution.name : void 0;
3479
+ const isResolvedNameMatch = resolvedName !== void 0 && normalizeResolvedName(inputValue) === resolvedName;
3480
+ useEffect(() => {
3481
+ if (isResolvedNameMatch && resolvedAddress !== void 0 && !chainScopeBlocked) {
3482
+ lastSetValueRef.current = resolvedAddress;
3483
+ fieldOnChange(resolvedAddress);
2799
3484
  }
2800
- const timer = setTimeout(() => setDebouncedQuery(inputValue), DEBOUNCE_MS);
2801
- return () => clearTimeout(timer);
2802
- }, [inputValue]);
2803
- const suggestionsDisabled = suggestionsProp === false;
2804
- const resolvedSuggestions = useMemo(() => {
2805
- if (suggestionsDisabled) return [];
2806
- if (Array.isArray(suggestionsProp)) return suggestionsProp;
2807
- if (!contextResolver || !debouncedQuery.trim()) return [];
2808
- return contextResolver.resolveSuggestions(debouncedQuery).slice(0, MAX_SUGGESTIONS);
2809
3485
  }, [
2810
- suggestionsDisabled,
2811
- suggestionsProp,
2812
- contextResolver,
2813
- debouncedQuery
3486
+ isResolvedNameMatch,
3487
+ resolvedAddress,
3488
+ chainScopeBlocked,
3489
+ fieldOnChange
2814
3490
  ]);
2815
- const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
3491
+ const matchedName = isResolvedNameMatch ? resolvedName : void 0;
2816
3492
  useEffect(() => {
2817
- let active = true;
2818
- const handleClickOutside = (e) => {
2819
- if (active && containerRef.current && !containerRef.current.contains(e.target)) setShowSuggestions(false);
2820
- };
2821
- document.addEventListener("mousedown", handleClickOutside);
2822
- return () => {
2823
- active = false;
2824
- document.removeEventListener("mousedown", handleClickOutside);
2825
- };
2826
- }, []);
2827
- const handleSuggestionKeyDown = useCallback((e) => {
2828
- if (!hasSuggestions) return;
2829
- if (e.key === "ArrowDown") {
2830
- e.preventDefault();
2831
- setHighlightedIndex((prev) => prev < resolvedSuggestions.length - 1 ? prev + 1 : 0);
2832
- } else if (e.key === "ArrowUp") {
3493
+ onResolvedNameChange?.(matchedName);
3494
+ }, [matchedName, onResolvedNameChange]);
3495
+ const { containerRef, resolvedSuggestions, hasSuggestions, highlightedIndex, suggestionsDisabled, setHighlightedIndex, closeSuggestions, onInputChange, onContainerKeyDown } = useAddressSuggestionField({
3496
+ inputValue,
3497
+ suggestions: suggestionsProp
3498
+ });
3499
+ /**
3500
+ * The single site for the typed/raw write (the name→hex write lives in the
3501
+ * effect above). With no resolver this is the legacy write, verbatim (INV-82);
3502
+ * with a resolver, only the `name-candidate` classification diverges:
3503
+ * - hex / malformed / empty → the raw value (legacy sync validate applies)
3504
+ * - name-candidate → `''` — gates submit and synchronously invalidates any
3505
+ * prior resolved hex before re-resolution (INV-75 / INV-81)
3506
+ */
3507
+ const commitTypedValue = useCallback((value) => {
3508
+ setInputValue(value);
3509
+ if (!resolverPresentRef.current) {
3510
+ lastSetValueRef.current = value;
3511
+ fieldOnChange(value);
3512
+ return;
3513
+ }
3514
+ const c = classifyAddressInput(value, {
3515
+ isValidAddress,
3516
+ isValidName
3517
+ });
3518
+ classificationRef.current = c;
3519
+ if (c === "name-candidate") {
3520
+ const normalized = normalizeResolvedName(value);
3521
+ if (statusRef.current === "resolved" && resolvedNameRef.current === normalized && !chainScopeBlockedRef.current) return;
3522
+ statusRef.current = "debouncing";
3523
+ lastSetValueRef.current = "";
3524
+ fieldOnChange("");
3525
+ } else {
3526
+ lastSetValueRef.current = value;
3527
+ fieldOnChange(value);
3528
+ }
3529
+ }, [
3530
+ fieldOnChange,
3531
+ isValidAddress,
3532
+ isValidName
3533
+ ]);
3534
+ const handleInputChange = (e) => {
3535
+ commitTypedValue(e.target.value);
3536
+ onInputChange(e.target.value);
3537
+ };
3538
+ const applySuggestion = (suggestion) => {
3539
+ commitTypedValue(suggestion.value);
3540
+ onSuggestionSelect?.(suggestion);
3541
+ closeSuggestions();
3542
+ };
3543
+ const handleKeyDown = (e) => {
3544
+ if (hasSuggestions && e.key === "Enter" && highlightedIndex >= 0) {
2833
3545
  e.preventDefault();
2834
- setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
3546
+ applySuggestion(resolvedSuggestions[highlightedIndex]);
3547
+ return;
2835
3548
  }
2836
- }, [hasSuggestions, resolvedSuggestions.length]);
3549
+ if (e.key === "Escape") {
3550
+ if (hasSuggestions) {
3551
+ closeSuggestions();
3552
+ return;
3553
+ }
3554
+ handleEscapeKey(commitTypedValue, inputValue)(e);
3555
+ }
3556
+ };
3557
+ const hasError = !!fieldState.error;
3558
+ const isPendingGate = fieldState.error?.message === NAME_RESOLUTION_PENDING_GATE;
3559
+ const hasRealError = hasError && !isPendingGate;
3560
+ const shouldShowError = hasRealError && fieldState.isTouched;
3561
+ const validationClasses = getValidationStateClasses(hasRealError ? fieldState.error : void 0, isPendingGate ? false : fieldState.isTouched);
3562
+ const patternErrorMessage = validation?.pattern && typeof validation.pattern === "object" && "message" in validation.pattern ? validation.pattern.message : void 0;
3563
+ const accessibilityProps = getAccessibilityProps({
3564
+ id,
3565
+ hasError: hasRealError,
3566
+ isRequired,
3567
+ hasHelperText: !!helperText
3568
+ });
3569
+ const renderOutcome = () => {
3570
+ if (forwardUnsupported) return /* @__PURE__ */ jsx("span", {
3571
+ role: "alert",
3572
+ className: "text-destructive text-sm",
3573
+ children: nameResolutionMessageForCode("UNSUPPORTED_NETWORK")
3574
+ });
3575
+ switch (resolution.status) {
3576
+ case "idle": return null;
3577
+ case "debouncing":
3578
+ case "loading": return /* @__PURE__ */ jsx("span", {
3579
+ className: "text-muted-foreground text-sm",
3580
+ children: resolvingAnnouncerCopy
3581
+ });
3582
+ case "resolved":
3583
+ if (chainScopeBlocked) return /* @__PURE__ */ jsx("span", {
3584
+ role: "alert",
3585
+ className: "text-destructive text-sm",
3586
+ children: nameResolutionChainScopeMismatchMessage({ activeNetworkName })
3587
+ });
3588
+ return /* @__PURE__ */ jsxs("span", {
3589
+ className: "text-sm",
3590
+ children: ["Resolved to ", /* @__PURE__ */ jsx("code", {
3591
+ className: "font-mono",
3592
+ children: resolution.data.address
3593
+ })]
3594
+ });
3595
+ case "error": {
3596
+ const networkName = resolution.error.code === "UNSUPPORTED_NETWORK" ? resolution.error.networkId || void 0 : void 0;
3597
+ const message = nameResolutionMessageForCode(resolution.error.code, { networkName });
3598
+ const isTransient = TRANSIENT_ERROR_CODES.has(resolution.error.code);
3599
+ const retry = resolution.retry;
3600
+ return /* @__PURE__ */ jsxs("span", {
3601
+ role: "alert",
3602
+ className: "text-destructive text-sm",
3603
+ children: [message, isTransient ? /* @__PURE__ */ jsx("button", {
3604
+ type: "button",
3605
+ className: "ml-2 underline",
3606
+ onClick: retry,
3607
+ children: "Retry"
3608
+ }) : null]
3609
+ });
3610
+ }
3611
+ }
3612
+ };
2837
3613
  return /* @__PURE__ */ jsxs("div", {
2838
3614
  className: `flex flex-col gap-2 ${width === "full" ? "w-full" : width === "half" ? "w-1/2" : "w-1/3"}`,
2839
- children: [label && /* @__PURE__ */ jsxs(Label, {
2840
- htmlFor: id,
2841
- children: [
2842
- label,
2843
- " ",
2844
- isRequired && /* @__PURE__ */ jsx("span", {
2845
- className: "text-destructive",
2846
- children: "*"
2847
- })
2848
- ]
2849
- }), /* @__PURE__ */ jsx(Controller, {
2850
- control,
2851
- name,
2852
- rules: { validate: (value) => {
2853
- if (value === void 0 || value === null || value === "") return validation?.required ? "This field is required" : true;
2854
- const standardValidationResult = validateField(value, validation);
2855
- if (standardValidationResult !== true) return standardValidationResult;
2856
- if (addressing && typeof value === "string") {
2857
- if (!addressing.isValidAddress(value)) return "Invalid address format for the selected chain";
2858
- }
2859
- return true;
2860
- } },
2861
- disabled: readOnly,
2862
- render: ({ fieldState: { error, isTouched }, field }) => {
2863
- const hasError = !!error;
2864
- const shouldShowError = hasError && isTouched;
2865
- const validationClasses = getValidationStateClasses(error, isTouched);
2866
- const patternErrorMessage = validation?.pattern && typeof validation.pattern === "object" && "message" in validation.pattern ? validation.pattern.message : void 0;
2867
- const handleInputChange = (e) => {
2868
- const value = e.target.value;
2869
- field.onChange(value);
2870
- lastSetValueRef.current = value;
2871
- setInputValue(value);
2872
- setShowSuggestions(value.length > 0);
2873
- setHighlightedIndex(-1);
2874
- };
2875
- const applySuggestion = (suggestion) => {
2876
- field.onChange(suggestion.value);
2877
- onSuggestionSelect?.(suggestion);
2878
- lastSetValueRef.current = suggestion.value;
2879
- setInputValue(suggestion.value);
2880
- setShowSuggestions(false);
2881
- setHighlightedIndex(-1);
2882
- };
2883
- const handleKeyDown = (e) => {
2884
- if (hasSuggestions && e.key === "Enter" && highlightedIndex >= 0) {
2885
- e.preventDefault();
2886
- applySuggestion(resolvedSuggestions[highlightedIndex]);
2887
- return;
2888
- }
2889
- if (e.key === "Escape") {
2890
- if (hasSuggestions) {
2891
- setShowSuggestions(false);
2892
- return;
2893
- }
2894
- handleEscapeKey(field.onChange, field.value)(e);
2895
- }
2896
- };
2897
- const accessibilityProps = getAccessibilityProps({
2898
- id,
2899
- hasError,
2900
- isRequired,
2901
- hasHelperText: !!helperText
2902
- });
2903
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2904
- /* @__PURE__ */ jsxs("div", {
2905
- ref: containerRef,
2906
- className: "relative",
2907
- onKeyDown: handleSuggestionKeyDown,
2908
- children: [/* @__PURE__ */ jsx(Input, {
2909
- ...field,
2910
- id,
2911
- placeholder: placeholder || "0x...",
2912
- className: validationClasses,
2913
- onChange: handleInputChange,
2914
- onKeyDown: handleKeyDown,
2915
- "data-slot": "input",
2916
- value: field.value ?? "",
2917
- ...accessibilityProps,
2918
- "aria-describedby": `${helperText ? descriptionId : ""} ${hasError ? errorId : ""}`,
2919
- "aria-expanded": hasSuggestions,
2920
- "aria-autocomplete": suggestionsDisabled ? void 0 : "list",
2921
- "aria-controls": hasSuggestions ? `${id}-suggestions` : void 0,
2922
- "aria-activedescendant": hasSuggestions && highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : void 0,
2923
- disabled: readOnly
2924
- }), hasSuggestions && /* @__PURE__ */ jsx("div", {
2925
- id: `${id}-suggestions`,
2926
- className: cn("absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md", "max-h-48 overflow-auto"),
2927
- role: "listbox",
2928
- children: resolvedSuggestions.map((s, i) => /* @__PURE__ */ jsxs("button", {
2929
- id: `${id}-suggestion-${i}`,
2930
- type: "button",
2931
- role: "option",
2932
- "aria-selected": i === highlightedIndex,
2933
- className: cn("flex w-full flex-col px-3 py-2 text-left text-sm", "hover:bg-selected/10", i === highlightedIndex && "bg-selected/10"),
2934
- onMouseDown: (e) => {
2935
- e.preventDefault();
2936
- applySuggestion(s);
2937
- },
2938
- onMouseEnter: () => setHighlightedIndex(i),
2939
- children: [/* @__PURE__ */ jsx("span", {
2940
- className: "font-medium",
2941
- children: s.label
2942
- }), /* @__PURE__ */ jsx("span", {
2943
- className: "truncate font-mono text-xs text-muted-foreground",
2944
- children: s.value
2945
- })]
2946
- }, `${s.value}-${s.description ?? i}`))
2947
- })]
2948
- }),
2949
- helperText && /* @__PURE__ */ jsx("div", {
2950
- id: descriptionId,
2951
- className: "text-muted-foreground text-sm",
2952
- children: helperText
2953
- }),
2954
- /* @__PURE__ */ jsx(ErrorMessage, {
2955
- error,
2956
- id: errorId,
2957
- message: shouldShowError ? error?.message || patternErrorMessage : void 0
3615
+ children: [
3616
+ label && /* @__PURE__ */ jsxs(Label, {
3617
+ htmlFor: id,
3618
+ children: [
3619
+ label,
3620
+ " ",
3621
+ isRequired && /* @__PURE__ */ jsx("span", {
3622
+ className: "text-destructive",
3623
+ children: "*"
2958
3624
  })
2959
- ] });
2960
- }
2961
- })]
3625
+ ]
3626
+ }),
3627
+ /* @__PURE__ */ jsxs("div", {
3628
+ ref: containerRef,
3629
+ className: "relative",
3630
+ onKeyDown: onContainerKeyDown,
3631
+ children: [/* @__PURE__ */ jsx(Input, {
3632
+ ...field,
3633
+ id,
3634
+ placeholder: placeholder || (resolver !== null ? "0x... or name" : "0x..."),
3635
+ className: validationClasses,
3636
+ onChange: handleInputChange,
3637
+ onKeyDown: handleKeyDown,
3638
+ "data-slot": "input",
3639
+ value: inputValue,
3640
+ ...accessibilityProps,
3641
+ "aria-describedby": resolver === null ? `${helperText ? descriptionId : ""} ${hasRealError ? errorId : ""}` : [
3642
+ helperText ? descriptionId : "",
3643
+ hasRealError ? errorId : "",
3644
+ resolutionRegionId
3645
+ ].filter(Boolean).join(" ") || void 0,
3646
+ "aria-expanded": hasSuggestions,
3647
+ "aria-autocomplete": suggestionsDisabled ? void 0 : "list",
3648
+ "aria-controls": hasSuggestions ? `${id}-suggestions` : void 0,
3649
+ "aria-activedescendant": hasSuggestions && highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : void 0,
3650
+ disabled: readOnly
3651
+ }), hasSuggestions && /* @__PURE__ */ jsx(AddressSuggestionList, {
3652
+ id,
3653
+ suggestions: resolvedSuggestions,
3654
+ highlightedIndex,
3655
+ onSelect: applySuggestion,
3656
+ onHighlight: setHighlightedIndex
3657
+ })]
3658
+ }),
3659
+ helperText && /* @__PURE__ */ jsx("div", {
3660
+ id: descriptionId,
3661
+ className: "text-muted-foreground text-sm",
3662
+ children: helperText
3663
+ }),
3664
+ /* @__PURE__ */ jsx(ErrorMessage, {
3665
+ error: hasRealError ? fieldState.error : void 0,
3666
+ id: errorId,
3667
+ message: shouldShowError ? fieldState.error?.message || patternErrorMessage : void 0
3668
+ }),
3669
+ resolver !== null && /* @__PURE__ */ jsx("div", {
3670
+ id: resolutionRegionId,
3671
+ "aria-live": "polite",
3672
+ className: "min-h-5",
3673
+ children: renderOutcome()
3674
+ })
3675
+ ]
2962
3676
  });
2963
3677
  }
2964
3678
  AddressField.displayName = "AddressField";
@@ -3498,6 +4212,82 @@ function useAddressSuggestions(query, networkId) {
3498
4212
  ]) };
3499
4213
  }
3500
4214
 
4215
+ //#endregion
4216
+ //#region src/components/fields/name-resolution/name-resolver-context.tsx
4217
+ /**
4218
+ * Name Resolver Context (SF-3)
4219
+ *
4220
+ * Provides a React context for forward name resolution (name → address).
4221
+ * When a `NameResolverProvider` is mounted, every `AddressField` in the
4222
+ * subtree resolves typed names inline through the injected `resolveName`
4223
+ * and submits the resolved hex — never the name (SC-004).
4224
+ *
4225
+ * The provider is deliberately **dumb** (INV-118): it memoizes the injected
4226
+ * functions into the context value and nothing else — no hook state, no
4227
+ * capability, no chain dependency. The smart runtime wiring lives upstream in
4228
+ * `@openzeppelin/ui-react` (`useRuntimeNameResolver`), which an app or the
4229
+ * renderer mounts ambiently:
4230
+ *
4231
+ * @example
4232
+ * ```tsx
4233
+ * import { NameResolverProvider } from '@openzeppelin/ui-components';
4234
+ * import { useRuntimeNameResolver } from '@openzeppelin/ui-react';
4235
+ *
4236
+ * function FormRoot() {
4237
+ * const resolver = useRuntimeNameResolver();
4238
+ * return (
4239
+ * <NameResolverProvider {...resolver}>
4240
+ * <MyForm />
4241
+ * </NameResolverProvider>
4242
+ * );
4243
+ * }
4244
+ * ```
4245
+ */
4246
+ /**
4247
+ * Provides forward name resolution to all `AddressField` instances in the
4248
+ * subtree (zero call-site wiring, SC-001). Both functions are optional: an
4249
+ * absent `resolveName` means forward resolution is unsupported and a typed
4250
+ * name surfaces `UNSUPPORTED_NETWORK` (INV-119 / SC-006).
4251
+ *
4252
+ * ## Stable resolver identity (integrator contract)
4253
+ *
4254
+ * The injected resolver **must be referentially stable across renders** —
4255
+ * memoize it (e.g. `useMemo`) or use `useRuntimeNameResolver` from
4256
+ * `@openzeppelin/ui-react`, which is stable by construction. A resolver whose
4257
+ * function identity changes on every render (e.g. a fresh inline function) is a
4258
+ * misconfiguration: the field keys inline resolution on that identity, so a
4259
+ * churning identity would otherwise re-dispatch on every render — an unbounded
4260
+ * RPC loop on a funds path.
4261
+ *
4262
+ * The base component **detects and withstands** this (it is not merely a caveat):
4263
+ * dispatch is bounded per resolution intent so a churning resolver can never drive
4264
+ * an unbounded loop (INV-123), a one-shot development-only warning names the
4265
+ * misconfiguration (INV-124), and the field degrades to a **safe gated state**
4266
+ * (empty value, submit blocked — never a wrong address, never a throw; INV-125)
4267
+ * until the resolver is memoized. A **genuine** resolver/network swap — a single
4268
+ * identity change for a given input — is still honored and re-resolves within
4269
+ * budget (INV-119). The bound is a backstop; the contract is a stable identity.
4270
+ *
4271
+ * @param props - Injected resolver functions and children
4272
+ */
4273
+ function NameResolverProvider({ children, isValidName, resolveName, activeNetworkId, activeNetworkName }) {
4274
+ const value = React$1.useMemo(() => ({
4275
+ isValidName,
4276
+ resolveName,
4277
+ activeNetworkId,
4278
+ activeNetworkName
4279
+ }), [
4280
+ isValidName,
4281
+ resolveName,
4282
+ activeNetworkId,
4283
+ activeNetworkName
4284
+ ]);
4285
+ return /* @__PURE__ */ jsx(NameResolverContext.Provider, {
4286
+ value,
4287
+ children
4288
+ });
4289
+ }
4290
+
3501
4291
  //#endregion
3502
4292
  //#region src/components/fields/AmountField.tsx
3503
4293
  /**
@@ -6226,7 +7016,7 @@ SelectGroupedField.displayName = "SelectGroupedField";
6226
7016
  * - Full accessibility support with ARIA attributes
6227
7017
  * - Keyboard navigation
6228
7018
  */
6229
- function TextField({ id, label, placeholder, helperText, control, name, width = "full", validation, readOnly }) {
7019
+ function TextField({ id, label, placeholder, helperText, control, name, width = "full", validation, readOnly, onUserEdit }) {
6230
7020
  const isRequired = !!validation?.required;
6231
7021
  const errorId = `${id}-error`;
6232
7022
  const descriptionId = `${id}-description`;
@@ -6262,9 +7052,13 @@ function TextField({ id, label, placeholder, helperText, control, name, width =
6262
7052
  const handleInputChange = (e) => {
6263
7053
  const value = e.target.value;
6264
7054
  field.onChange(value);
7055
+ onUserEdit?.(value);
6265
7056
  };
6266
7057
  const handleKeyDown = (e) => {
6267
- if (e.key === "Escape") handleEscapeKey(field.onChange, field.value)(e);
7058
+ if (e.key === "Escape") handleEscapeKey((value) => {
7059
+ field.onChange(value);
7060
+ onUserEdit?.(value);
7061
+ }, field.value)(e);
6268
7062
  };
6269
7063
  const accessibilityProps = getAccessibilityProps({
6270
7064
  id,
@@ -6797,5 +7591,5 @@ const Toaster = ({ ...props }) => {
6797
7591
  };
6798
7592
 
6799
7593
  //#endregion
6800
- export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AddressDisplay, AddressField, AddressLabelProvider, AddressListField, AddressSuggestionProvider, Alert, AlertDescription, AlertTitle, AmountField, ArrayField, ArrayObjectField, Banner, BaseField, BigIntField, BooleanField, Button, BytesField, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DEFAULT_ADDRESS_LIST_FIELD_LABELS, DateRangePicker, DateTimeField, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EcosystemDropdown, EcosystemIcon, EmptyState, EnumField, ErrorMessage, ExternalLink, FileUploadField, Footer, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Header, INTEGER_HTML_PATTERN, INTEGER_INPUT_PATTERN, INTEGER_PATTERN, Input, Label, LoadingButton, MapEntryRow, MapField, MidnightIcon, NetworkAvailabilityNotice, NetworkErrorNotificationProvider, NetworkIcon, NetworkSelector, NetworkServiceErrorBanner, NetworkStatusBadge, NumberField, ObjectField, OverflowMenu, PasswordField, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, RadioField, RadioGroup, RadioGroupItem, RelayerDetailsCard, Select, SelectContent, SelectField, SelectGroup, SelectGroupedField, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SidebarButton, SidebarGroup, SidebarLayout, SidebarSection, Tabs, TabsContent, TabsList, TabsTrigger, TextAreaField, TextField, Textarea, Toaster, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UrlField, VERSION, ViewContractStateButton, WizardLayout, WizardNavigation, WizardStepper, buildAddressListPreviewSummary, buttonVariants, computeChildTouched, createFocusManager, createValidationResult, formatAddressBulkSummary, formatValidationError, getAccessibilityProps, getDescribedById, getErrorMessage, getValidationStateClasses, getWidthClasses, handleEscapeKey, handleKeyboardEvent, handleNumericKeys, handleToggleKeys, handleValidationError, hasFieldError, isDuplicateMapKey, resolveAddressListFieldLabels, useAddressLabel, useAddressSuggestions, useDuplicateKeyIndexes, useMapFieldSync, useNetworkErrorAwareAdapter, useNetworkErrorReporter, useNetworkErrors, validateField, validateMapEntries, validateMapStructure };
7594
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AddressDisplay, AddressField, AddressLabelProvider, AddressListField, AddressNameProvider, AddressSuggestionList, AddressSuggestionProvider, Alert, AlertDescription, AlertTitle, AmountField, ArrayField, ArrayObjectField, Banner, BaseField, BigIntField, BooleanField, Button, BytesField, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DEFAULT_ADDRESS_LIST_FIELD_LABELS, DateRangePicker, DateTimeField, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EcosystemDropdown, EcosystemIcon, EmptyState, EnumField, ErrorMessage, ExternalLink, FileUploadField, Footer, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Header, INTEGER_HTML_PATTERN, INTEGER_INPUT_PATTERN, INTEGER_PATTERN, Input, Label, LoadingButton, MapEntryRow, MapField, MidnightIcon, NAME_RESOLUTION_DEBOUNCE_MS, NameResolverContext, NameResolverProvider, NetworkAvailabilityNotice, NetworkErrorNotificationProvider, NetworkIcon, NetworkSelector, NetworkServiceErrorBanner, NetworkStatusBadge, NumberField, ObjectField, OverflowMenu, PasswordField, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, RadioField, RadioGroup, RadioGroupItem, RelayerDetailsCard, Select, SelectContent, SelectField, SelectGroup, SelectGroupedField, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SidebarButton, SidebarGroup, SidebarLayout, SidebarSection, Tabs, TabsContent, TabsList, TabsTrigger, TextAreaField, TextField, Textarea, Toaster, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UrlField, VERSION, ViewContractStateButton, WizardLayout, WizardNavigation, WizardStepper, buildAddressListPreviewSummary, buttonVariants, computeChildTouched, createFocusManager, createValidationResult, formatAddressBulkSummary, formatValidationError, getAccessibilityProps, getDescribedById, getErrorMessage, getValidationStateClasses, getWidthClasses, handleEscapeKey, handleKeyboardEvent, handleNumericKeys, handleToggleKeys, handleValidationError, hasFieldError, isDuplicateMapKey, resolveAddressListFieldLabels, useAddressLabel, useAddressName, useAddressSuggestionField, useAddressSuggestions, useDuplicateKeyIndexes, useInjectedNameResolution, useMapFieldSync, useNameResolver, useNetworkErrorAwareAdapter, useNetworkErrorReporter, useNetworkErrors, validateField, validateMapEntries, validateMapStructure };
6801
7595
  //# sourceMappingURL=index.mjs.map