@openzeppelin/ui-components 3.3.1 → 3.5.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
@@ -1,10 +1,10 @@
1
1
  import { a as getValidationStateClasses, c as isDuplicateMapKey, d as INTEGER_HTML_PATTERN, f as INTEGER_INPUT_PATTERN, i as getErrorMessage, l as validateField, n as createValidationResult, o as handleValidationError, p as INTEGER_PATTERN, r as formatValidationError, s as hasFieldError, t as ErrorMessage, u as validateMapEntries } from "./ErrorMessage-BqOEJm84.mjs";
2
2
  import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
3
  import { cva } from "class-variance-authority";
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";
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, TriangleAlert, 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, crossNetworkFallbackMessageNames, getDefaultValueForType, getFallbackNetworks, getHostedNetworkAvailabilityNoticeCopy, getInvalidUrlMessage, getServiceDisplayName, isChainScopeMismatch, isCrossNetworkFallback, isNetworkAvailabilityPolicyActive, isValidUrl, logger, nameResolutionChainScopeMismatchMessage, nameResolutionCrossNetworkFallbackMessage, 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.1";
29
+ const VERSION = "3.5.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,37 @@ 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, showCrossNetworkFallbackDisclaimer = true, 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 showFallbackNote = showCrossNetworkFallbackDisclaimer && ensName !== void 0 && effectiveLabel === ensName && record !== void 0 && isCrossNetworkFallback(record.provenance);
279
+ const fallbackNote = (() => {
280
+ if (!showFallbackNote) return void 0;
281
+ const networks = getFallbackNetworks(record.provenance);
282
+ if (networks === void 0) return void 0;
283
+ return nameResolutionCrossNetworkFallbackMessage(networks, crossNetworkFallbackMessageNames(networks, nameResolver?.resolveNetworkLabel));
284
+ })();
285
+ const effectiveTruncate = truncateProp !== void 0 ? truncateProp : truncateWhenLabeled ? Boolean(effectiveLabel) : true;
194
286
  const contextEditHandler = React$1.useCallback(() => {
195
287
  resolver?.onEditLabel?.(address, networkId);
196
288
  }, [
@@ -264,6 +356,17 @@ function AddressDisplay({ address, truncate: truncateProp, truncateWhenLabeled =
264
356
  children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" })
265
357
  })
266
358
  ] });
359
+ const ensAvatarUrl = ensName !== void 0 && resolvedLabel === void 0 ? record?.avatarUrl : void 0;
360
+ const effectiveAvatar = avatar ?? (ensAvatarUrl ? /* @__PURE__ */ jsx(AddressAvatar, {
361
+ src: ensAvatarUrl,
362
+ variant: avatarVariant
363
+ }) : void 0);
364
+ const isFillAvatar = avatarVariant === "fill" && Boolean(effectiveAvatar);
365
+ const fillAvatarChipClassName = isFillAvatar && isChip ? "overflow-hidden" : void 0;
366
+ const avatarSlot = effectiveAvatar ? /* @__PURE__ */ jsx("span", {
367
+ 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"),
368
+ children: effectiveAvatar
369
+ }) : null;
267
370
  const shouldShowTooltip = showTooltip && effectiveTruncate;
268
371
  const wrapWithTooltip = (content) => {
269
372
  if (!shouldShowTooltip) return content;
@@ -278,33 +381,63 @@ function AddressDisplay({ address, truncate: truncateProp, truncateWhenLabeled =
278
381
  })] })
279
382
  });
280
383
  };
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", {
288
- className: "truncate font-sans font-medium text-slate-900 leading-snug",
289
- children: resolvedLabel
384
+ if (effectiveLabel) {
385
+ const labelColumn = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
386
+ className: "inline-flex min-w-0 max-w-full items-center gap-1",
387
+ children: [/* @__PURE__ */ jsx("span", {
388
+ className: "truncate font-sans font-medium text-slate-900 leading-snug",
389
+ children: effectiveLabel
390
+ }), fallbackNote && showCrossNetworkFallbackDisclaimer ? /* @__PURE__ */ jsx(TooltipProvider, {
391
+ delayDuration: 300,
392
+ children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
393
+ asChild: true,
394
+ children: /* @__PURE__ */ jsx("button", {
395
+ type: "button",
396
+ className: "inline-flex shrink-0 rounded-sm text-amber-500 outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50",
397
+ "aria-label": fallbackNote,
398
+ children: /* @__PURE__ */ jsx(TriangleAlert, {
399
+ className: "h-3 w-3",
400
+ "aria-hidden": true
401
+ })
402
+ })
403
+ }), /* @__PURE__ */ jsx(TooltipContent, {
404
+ className: "max-w-xs font-sans text-xs font-normal normal-case",
405
+ children: fallbackNote
406
+ })] })
407
+ }) : null]
290
408
  }), /* @__PURE__ */ jsxs("div", {
291
409
  className: "flex min-w-0 items-center font-mono text-[10px] text-slate-400 leading-snug",
292
410
  children: [/* @__PURE__ */ jsx("span", {
293
411
  className: addressTextClassName,
294
412
  children: displayAddress
295
413
  }), actionButtons]
296
- })]
297
- }));
414
+ })] });
415
+ return wrapWithTooltip(/* @__PURE__ */ jsx("div", {
416
+ 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),
417
+ onPointerEnter: handlePointerEnter,
418
+ onPointerLeave: handlePointerLeave,
419
+ onClick: handleUntruncateClick,
420
+ ...props,
421
+ children: effectiveAvatar ? /* @__PURE__ */ jsxs(Fragment, { children: [avatarSlot, /* @__PURE__ */ jsx("div", {
422
+ className: "inline-flex min-w-0 flex-col",
423
+ children: labelColumn
424
+ })] }) : labelColumn
425
+ }));
426
+ }
298
427
  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),
428
+ 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
429
  onPointerEnter: handlePointerEnter,
301
430
  onPointerLeave: handlePointerLeave,
302
431
  onClick: handleUntruncateClick,
303
432
  ...props,
304
- children: [/* @__PURE__ */ jsx("span", {
305
- className: addressTextClassName,
306
- children: displayAddress
307
- }), actionButtons]
433
+ children: [
434
+ avatarSlot,
435
+ /* @__PURE__ */ jsx("span", {
436
+ className: addressTextClassName,
437
+ children: displayAddress
438
+ }),
439
+ actionButtons
440
+ ]
308
441
  }));
309
442
  }
310
443
 
@@ -402,6 +535,82 @@ function useAddressLabel(address, networkId) {
402
535
  };
403
536
  }
404
537
 
538
+ //#endregion
539
+ //#region src/components/ui/address-display/address-name-context.tsx
540
+ /**
541
+ * Address Name Context
542
+ *
543
+ * Provides a React context for surfacing already-resolved reverse name
544
+ * records (`ResolvedName`) for blockchain addresses. When an
545
+ * `AddressNameProvider` is mounted, every `AddressDisplay` in the subtree
546
+ * reads its record synchronously — no call-site changes — and renders the
547
+ * name/avatar only when the record is forward-verified (INV-52).
548
+ *
549
+ * Structural mirror of `AddressLabelProvider` (INV-122): synchronous,
550
+ * value-only, no-op by default. The resolver implementation lives in the
551
+ * react/renderer layer, which bridges async resolution state into this
552
+ * synchronous read; this file stays capability-free (INV-121).
553
+ *
554
+ * @example
555
+ * ```tsx
556
+ * import { AddressNameProvider } from '@openzeppelin/ui-components';
557
+ *
558
+ * <AddressNameProvider resolveAddressName={readFromResolutionCache}>
559
+ * <TxHistory /> {// rows are plain <AddressDisplay/> — verified names fill in }
560
+ * </AddressNameProvider>
561
+ * ```
562
+ */
563
+ /**
564
+ * Provides reverse-name resolution to all `AddressDisplay` instances in the
565
+ * subtree. `resolveAddressName` MUST be synchronous — it is called during
566
+ * render (INV-122). A resolver that returns `undefined` for an address is
567
+ * behaviorally identical to mounting no provider (INV-54).
568
+ *
569
+ * @param props - Resolver function and children
570
+ */
571
+ function AddressNameProvider({ children, resolveAddressName, resolveNetworkLabel }) {
572
+ const value = React$1.useMemo(() => ({
573
+ resolveAddressName,
574
+ resolveNetworkLabel
575
+ }), [resolveAddressName, resolveNetworkLabel]);
576
+ return /* @__PURE__ */ jsx(AddressNameContext.Provider, {
577
+ value,
578
+ children
579
+ });
580
+ }
581
+
582
+ //#endregion
583
+ //#region src/components/ui/address-display/use-address-name.ts
584
+ /**
585
+ * Convenience hook for reading a reverse name record from the nearest
586
+ * `AddressNameProvider`.
587
+ *
588
+ * Kept in its own file so that `address-name-context.tsx` exports only
589
+ * components (required by React Fast Refresh) — mirrors `use-address-label.ts`.
590
+ */
591
+ /**
592
+ * Reads the `AddressNameContext` for one address, synchronously (INV-122).
593
+ * Returns `{ record: undefined }` when no provider is mounted — never throws,
594
+ * never suspends. Note the record is returned verbatim: callers rendering a
595
+ * name from it must apply the same `forwardVerified === true` gate that
596
+ * `AddressDisplay` applies (INV-52).
597
+ *
598
+ * @param address - The blockchain address to reverse-resolve
599
+ * @param networkId - Optional network identifier scoping the lookup
600
+ * @returns The current best-known record for the address
601
+ *
602
+ * @example
603
+ * ```tsx
604
+ * function EnsBadge({ address }: { address: string }) {
605
+ * const { record } = useAddressName(address);
606
+ * return record?.forwardVerified ? <Badge>{record.name}</Badge> : null;
607
+ * }
608
+ * ```
609
+ */
610
+ function useAddressName(address, networkId) {
611
+ return { record: React$1.useContext(AddressNameContext)?.resolveAddressName(address, networkId) };
612
+ }
613
+
405
614
  //#endregion
406
615
  //#region src/components/ui/alert.tsx
407
616
  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 +2748,43 @@ function WizardLayout(props) {
2539
2748
  });
2540
2749
  }
2541
2750
 
2751
+ //#endregion
2752
+ //#region src/components/fields/address-suggestion/AddressSuggestionList.tsx
2753
+ /**
2754
+ * Render the suggestion dropdown. Returns `null` when there are no suggestions
2755
+ * (the consuming field also gates on `hasSuggestions`, so this is a safety net).
2756
+ *
2757
+ * @param props - {@link AddressSuggestionListProps}.
2758
+ */
2759
+ function AddressSuggestionList({ id, suggestions, highlightedIndex, onSelect, onHighlight }) {
2760
+ if (suggestions.length === 0) return null;
2761
+ return /* @__PURE__ */ jsx("div", {
2762
+ id: `${id}-suggestions`,
2763
+ className: cn("absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md", "max-h-48 overflow-auto"),
2764
+ role: "listbox",
2765
+ children: suggestions.map((s, i) => /* @__PURE__ */ jsxs("button", {
2766
+ id: `${id}-suggestion-${i}`,
2767
+ type: "button",
2768
+ role: "option",
2769
+ "aria-selected": i === highlightedIndex,
2770
+ className: cn("flex w-full flex-col px-3 py-2 text-left text-sm", "hover:bg-selected/10", i === highlightedIndex && "bg-selected/10"),
2771
+ onMouseDown: (e) => {
2772
+ e.preventDefault();
2773
+ onSelect(s);
2774
+ },
2775
+ onMouseEnter: () => onHighlight(i),
2776
+ children: [/* @__PURE__ */ jsx("span", {
2777
+ className: "font-medium",
2778
+ children: s.label
2779
+ }), /* @__PURE__ */ jsx("span", {
2780
+ className: "truncate font-mono text-xs text-muted-foreground",
2781
+ children: s.value
2782
+ })]
2783
+ }, `${s.value}-${s.description ?? i}`))
2784
+ });
2785
+ }
2786
+ AddressSuggestionList.displayName = "AddressSuggestionList";
2787
+
2542
2788
  //#endregion
2543
2789
  //#region src/components/fields/address-suggestion/context.ts
2544
2790
  /**
@@ -2548,6 +2794,372 @@ function WizardLayout(props) {
2548
2794
  */
2549
2795
  const AddressSuggestionContext = createContext(null);
2550
2796
 
2797
+ //#endregion
2798
+ //#region src/components/fields/address-suggestion/useAddressSuggestionField.ts
2799
+ /**
2800
+ * Headless suggestion-dropdown behavior for address fields.
2801
+ *
2802
+ * Extracted from `AddressField`'s inline dropdown logic (SF-3, D9) into one
2803
+ * reusable headless hook the base `AddressField` consumes — no divergence, no
2804
+ * regression (INV-72). It owns:
2805
+ * - `AddressSuggestionContext` resolution (explicit `suggestions` prop overrides),
2806
+ * - the `MAX_SUGGESTIONS` (5) cap on context-resolved lists,
2807
+ * - the 200 ms suggestion debounce,
2808
+ * - keyboard highlight state with wrapping ArrowUp / ArrowDown navigation,
2809
+ * - click-outside-to-close.
2810
+ *
2811
+ * It is capability-free and chain-agnostic — nothing about ENS or any chain
2812
+ * leaks into `@openzeppelin/ui-components`. The consuming field owns the RHF
2813
+ * value write on selection and the Enter / Escape input handlers; this hook
2814
+ * exposes the state and primitives they need.
2815
+ *
2816
+ * Distinct from the pre-existing convenience hook `useAddressSuggestions(query,
2817
+ * networkId)`, which resolves a raw suggestion list from context with no
2818
+ * debounce / cap / keyboard state — that hook is unchanged and still exported.
2819
+ *
2820
+ * @see INV-72 (suggestion-dropdown behavior preserved with zero regression; SF-5 depends)
2821
+ * @see INV-94 (legacy a11y + suggestion keyboard contract preserved verbatim)
2822
+ */
2823
+ /** Suggestion debounce window (ms). Matches the legacy `AddressField`. */
2824
+ const DEBOUNCE_MS = 200;
2825
+ /** Cap on context-resolved suggestions. Explicit arrays are rendered as-is. */
2826
+ const MAX_SUGGESTIONS = 5;
2827
+ /**
2828
+ * Headless address-suggestion dropdown behavior. See the module docstring for
2829
+ * the full behavior contract.
2830
+ *
2831
+ * @param args - {@link UseAddressSuggestionFieldArgs}.
2832
+ * @returns {@link UseAddressSuggestionFieldResult}.
2833
+ */
2834
+ function useAddressSuggestionField({ inputValue, suggestions: suggestionsProp, networkId }) {
2835
+ const contextResolver = useContext(AddressSuggestionContext);
2836
+ const containerRef = useRef(null);
2837
+ const [debouncedQuery, setDebouncedQuery] = useState("");
2838
+ const [showSuggestions, setShowSuggestions] = useState(false);
2839
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
2840
+ useEffect(() => {
2841
+ if (!inputValue.trim()) {
2842
+ setDebouncedQuery("");
2843
+ return;
2844
+ }
2845
+ const timer = setTimeout(() => setDebouncedQuery(inputValue), DEBOUNCE_MS);
2846
+ return () => clearTimeout(timer);
2847
+ }, [inputValue]);
2848
+ const suggestionsDisabled = suggestionsProp === false;
2849
+ const resolvedSuggestions = useMemo(() => {
2850
+ if (suggestionsDisabled) return [];
2851
+ if (Array.isArray(suggestionsProp)) return suggestionsProp;
2852
+ if (!contextResolver || !debouncedQuery.trim()) return [];
2853
+ return contextResolver.resolveSuggestions(debouncedQuery, networkId).slice(0, MAX_SUGGESTIONS);
2854
+ }, [
2855
+ suggestionsDisabled,
2856
+ suggestionsProp,
2857
+ contextResolver,
2858
+ debouncedQuery,
2859
+ networkId
2860
+ ]);
2861
+ const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
2862
+ useEffect(() => {
2863
+ let active = true;
2864
+ const handleClickOutside = (e) => {
2865
+ if (active && containerRef.current && !containerRef.current.contains(e.target)) setShowSuggestions(false);
2866
+ };
2867
+ document.addEventListener("mousedown", handleClickOutside);
2868
+ return () => {
2869
+ active = false;
2870
+ document.removeEventListener("mousedown", handleClickOutside);
2871
+ };
2872
+ }, []);
2873
+ const onContainerKeyDown = useCallback((e) => {
2874
+ if (!hasSuggestions) return;
2875
+ if (e.key === "ArrowDown") {
2876
+ e.preventDefault();
2877
+ setHighlightedIndex((prev) => prev < resolvedSuggestions.length - 1 ? prev + 1 : 0);
2878
+ } else if (e.key === "ArrowUp") {
2879
+ e.preventDefault();
2880
+ setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
2881
+ }
2882
+ }, [hasSuggestions, resolvedSuggestions.length]);
2883
+ const onInputChange = useCallback((value) => {
2884
+ setShowSuggestions(value.length > 0);
2885
+ setHighlightedIndex(-1);
2886
+ }, []);
2887
+ return {
2888
+ containerRef,
2889
+ resolvedSuggestions,
2890
+ hasSuggestions,
2891
+ highlightedIndex,
2892
+ suggestionsDisabled,
2893
+ setHighlightedIndex,
2894
+ closeSuggestions: useCallback(() => {
2895
+ setShowSuggestions(false);
2896
+ setHighlightedIndex(-1);
2897
+ }, []),
2898
+ onInputChange,
2899
+ onContainerKeyDown
2900
+ };
2901
+ }
2902
+
2903
+ //#endregion
2904
+ //#region src/components/fields/name-resolution/useInjectedNameResolution.ts
2905
+ /**
2906
+ * Injected name-resolution machine (SF-3).
2907
+ *
2908
+ * A thin, capability-free async machine that `AddressField` consumes: given a
2909
+ * typed input and the injected `resolveName`, it debounces, dispatches, tracks
2910
+ * status, and applies the out-of-order name-drop guard (INV-117). It is the
2911
+ * component-boundary analogue of SF-2's `useResolveName`, with all hard
2912
+ * mechanics (cache, dedupe, retries, UNSUPPORTED synthesis) deliberately left
2913
+ * behind the injected function — this hook does ONLY debounce + status
2914
+ * derivation + the funds guards.
2915
+ *
2916
+ * The machine holds no resolved-hex cache of its own (INV-85): its single
2917
+ * `settled` record is valid only while it matches the current debounced input,
2918
+ * the current resolver function identity, and the current retry attempt — any
2919
+ * mismatch derives `loading`, never a stale `resolved`.
2920
+ *
2921
+ * Resolver-identity-churn backstop (INV-123/124/125). The dispatch effect keys
2922
+ * on the injected `resolveName` function identity, so an unstable (non-memoized)
2923
+ * integrator resolver re-fires it on every render. To keep that from becoming an
2924
+ * unbounded RPC loop (rate-ban + cost + funds-path DoS), the machine caps
2925
+ * dispatches per resolution intent — the pair (normalized debounced input × retry
2926
+ * attempt) — at {@link MAX_DISPATCHES_PER_INTENT}, regardless of render count and
2927
+ * regardless of how many times `resolveName` identity changes under that intent.
2928
+ * The budget resets only on intent change (a new typed name, or `retry()`) or when
2929
+ * the field leaves the active name-resolution path (clear / disable). It does
2930
+ * *not* reset on `settled.source` mismatch: that would let two settling sources
2931
+ * flap and refill the counter (settling churn), silently defeating INV-123. A
2932
+ * genuine network swap still re-dispatches within the shared intent budget
2933
+ * (INV-119 / acceptance #5) because a single identity change costs one unit.
2934
+ * Clear→retype of the same name gets a fresh budget via the leave-path reset, so
2935
+ * it cannot accumulate toward the cap across cycles and then strand a later swap.
2936
+ * A churning identity that exhausts the budget emits a one-shot, development-only
2937
+ * warning (INV-124) and degrades to the safe gated state (bounded RPC, RHF value
2938
+ * `''`, submit gated, never a wrong hex, never a throw — INV-125). This backstop
2939
+ * is entirely internal: no signature, param, or return-type change; `AddressField`
2940
+ * consumes the machine unchanged.
2941
+ */
2942
+ /**
2943
+ * Default debounce window (ms) for typed names — mirrors SF-2's
2944
+ * `forwardDebounceMs` default so the field and the bare hook feel identical.
2945
+ */
2946
+ const NAME_RESOLUTION_DEBOUNCE_MS = 300;
2947
+ /**
2948
+ * Cap on resolver dispatches per resolution intent — the pair (normalized
2949
+ * debounced input × retry attempt) — regardless of how many times the field
2950
+ * re-renders or how many times `resolveName` identity changes under that intent
2951
+ * (INV-123). Chosen to sit comfortably above the dispatches a single displayed
2952
+ * input legitimately incurs (1 initial call + any human-initiated genuine network
2953
+ * swaps of that same name, each ≤1 unit — INV-119) and far below per-render
2954
+ * identity churn (hundreds/sec): a user does not switch networks eight times
2955
+ * while one unedited name sits in the field, whereas churn blows past eight
2956
+ * within milliseconds. The budget resets when the intent changes or when the
2957
+ * field leaves the name-resolution path (clear / disable) — never on
2958
+ * `settled.source` mismatch, so settling-source oscillation cannot refill it.
2959
+ */
2960
+ const MAX_DISPATCHES_PER_INTENT = 8;
2961
+ /**
2962
+ * Debounce + dispatch + status derivation over an injected `resolveName`.
2963
+ *
2964
+ * Guarantees:
2965
+ * - INV-83: no call unless `enabled` (name-candidate + injected method), and
2966
+ * never for a debounced copy that lags the current normalized input.
2967
+ * - INV-117: a settled result whose requested name ≠ the current debounced
2968
+ * input is discarded (last-write-wins at the component boundary).
2969
+ * - INV-87: a rejecting resolver (contract violation) is mapped to a typed
2970
+ * `ADAPTER_ERROR` — nothing throws into the render tree.
2971
+ * - INV-123: resolver dispatches are bounded per resolution intent (normalized
2972
+ * debounced input × retry attempt) at {@link MAX_DISPATCHES_PER_INTENT},
2973
+ * regardless of render count or resolver-identity oscillation; the budget
2974
+ * resets on intent change or when leaving the name-resolution path — never on
2975
+ * `settled.source` mismatch; `retry()` never consumes an existing budget.
2976
+ * - INV-124: an identity-churning resolver that exhausts the budget triggers a
2977
+ * one-shot, development-only `logger.warn` (silent in production).
2978
+ * - INV-125: under sustained churn the field stays in the safe gated state — the
2979
+ * perpetually-changing identity keeps failing the `settled.source` check, so
2980
+ * the machine derives `loading`, never `resolved`; combined with INV-87 it
2981
+ * never throws and never writes a hex.
2982
+ * - No `setState` after unmount: every dispatch is cancelled on cleanup.
2983
+ *
2984
+ * @param params - {@link UseInjectedNameResolutionParams}
2985
+ * @returns The current {@link InjectedNameResolutionResult}.
2986
+ */
2987
+ function useInjectedNameResolution({ input, enabled, resolveName, debounceMs = NAME_RESOLUTION_DEBOUNCE_MS }) {
2988
+ const normalized = input.trim().toLowerCase();
2989
+ const [debounced, setDebounced] = useState(normalized);
2990
+ const seededRef = useRef(false);
2991
+ useEffect(() => {
2992
+ if (!seededRef.current) {
2993
+ seededRef.current = true;
2994
+ return;
2995
+ }
2996
+ if (debounceMs <= 0) {
2997
+ setDebounced(normalized);
2998
+ return;
2999
+ }
3000
+ const timer = setTimeout(() => setDebounced(normalized), debounceMs);
3001
+ return () => clearTimeout(timer);
3002
+ }, [normalized, debounceMs]);
3003
+ const [attempt, setAttempt] = useState(0);
3004
+ const [settled, setSettled] = useState(null);
3005
+ const currentTargetRef = useRef("");
3006
+ currentTargetRef.current = enabled && resolveName ? debounced : "";
3007
+ const intentKeyRef = useRef("");
3008
+ const dispatchCountRef = useRef(0);
3009
+ const churnWarnedRef = useRef(false);
3010
+ const warnOnceOnChurn = useCallback(() => {
3011
+ if (process.env.NODE_ENV === "production" || churnWarnedRef.current) return;
3012
+ churnWarnedRef.current = true;
3013
+ 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.");
3014
+ }, []);
3015
+ useEffect(() => {
3016
+ if (!enabled || !resolveName || debounced === "") {
3017
+ intentKeyRef.current = "";
3018
+ dispatchCountRef.current = 0;
3019
+ return;
3020
+ }
3021
+ if (debounced !== normalized) return;
3022
+ const intentKey = `${debounced}\u0000${attempt}`;
3023
+ if (intentKeyRef.current !== intentKey) {
3024
+ intentKeyRef.current = intentKey;
3025
+ dispatchCountRef.current = 0;
3026
+ }
3027
+ if (dispatchCountRef.current >= MAX_DISPATCHES_PER_INTENT) {
3028
+ warnOnceOnChurn();
3029
+ return;
3030
+ }
3031
+ dispatchCountRef.current += 1;
3032
+ let cancelled = false;
3033
+ const requestedName = debounced;
3034
+ const requestedAttempt = attempt;
3035
+ const source = resolveName;
3036
+ const settle = (result) => {
3037
+ if (cancelled) return;
3038
+ if (requestedName !== currentTargetRef.current) return;
3039
+ setSettled({
3040
+ name: requestedName,
3041
+ result,
3042
+ source,
3043
+ attempt: requestedAttempt
3044
+ });
3045
+ };
3046
+ source(requestedName).then(settle, (cause) => {
3047
+ settle({
3048
+ ok: false,
3049
+ error: {
3050
+ code: "ADAPTER_ERROR",
3051
+ message: cause instanceof Error ? cause.message : String(cause),
3052
+ cause
3053
+ }
3054
+ });
3055
+ });
3056
+ return () => {
3057
+ cancelled = true;
3058
+ };
3059
+ }, [
3060
+ enabled,
3061
+ resolveName,
3062
+ debounced,
3063
+ normalized,
3064
+ attempt,
3065
+ warnOnceOnChurn
3066
+ ]);
3067
+ const retry = useCallback(() => {
3068
+ setAttempt((current) => current + 1);
3069
+ }, []);
3070
+ if (!enabled || !resolveName || normalized === "") return { status: "idle" };
3071
+ if (debounced !== normalized) return {
3072
+ status: "debouncing",
3073
+ name: normalized
3074
+ };
3075
+ if (settled !== null && settled.name === debounced && settled.source === resolveName && settled.attempt === attempt) {
3076
+ if (settled.result.ok) return {
3077
+ status: "resolved",
3078
+ name: settled.name,
3079
+ data: settled.result.value
3080
+ };
3081
+ return {
3082
+ status: "error",
3083
+ name: settled.name,
3084
+ error: settled.result.error,
3085
+ retry
3086
+ };
3087
+ }
3088
+ return {
3089
+ status: "loading",
3090
+ name: normalized
3091
+ };
3092
+ }
3093
+
3094
+ //#endregion
3095
+ //#region src/components/fields/name-resolution/context.ts
3096
+ /**
3097
+ * Process-global key for the context object. Uses `Symbol.for` so all duplicated
3098
+ * module instances share ONE React context — the same cross-bundle-singleton
3099
+ * pattern as `@openzeppelin/ui-react`'s `NameResolutionContext` /
3100
+ * `WalletStateContext` (bundler pre-bundling / npm-installed consumers).
3101
+ */
3102
+ const CONTEXT_KEY = Symbol.for("@openzeppelin/ui-components/NameResolverContext");
3103
+ function getOrCreateSharedContext() {
3104
+ const globalScope = globalThis;
3105
+ if (!globalScope[CONTEXT_KEY]) globalScope[CONTEXT_KEY] = createContext(null);
3106
+ return globalScope[CONTEXT_KEY];
3107
+ }
3108
+ /**
3109
+ * @internal Shared context instance consumed by both AddressField and
3110
+ * NameResolverProvider (INV-118). Kept in its own file so component files
3111
+ * export only components (required by React Fast Refresh).
3112
+ *
3113
+ * `null` means no provider is mounted — every ENS branch in `AddressField` is
3114
+ * then dead code and the field is byte-identical to its pre-ENS behavior
3115
+ * (INV-82).
3116
+ */
3117
+ const NameResolverContext = getOrCreateSharedContext();
3118
+
3119
+ //#endregion
3120
+ //#region src/components/fields/name-resolution/useNameResolver.ts
3121
+ /**
3122
+ * Read the injected resolver context. Returns `null` when no
3123
+ * `NameResolverProvider` is mounted — the consuming field must then treat
3124
+ * every ENS branch as dead code (INV-82).
3125
+ *
3126
+ * @returns The extended context value, or `null` when no provider is mounted.
3127
+ */
3128
+ function useNameResolver() {
3129
+ return useContext(NameResolverContext);
3130
+ }
3131
+
3132
+ //#endregion
3133
+ //#region src/components/fields/name-resolution/useResolvingAnnouncerCopy.ts
3134
+ /** Default escalation threshold for long-latency resolves (INV-129/130). */
3135
+ const RESOLVING_ANNOUNCER_ESCALATE_AFTER_MS = 3e3;
3136
+ const PHASE_1_COPY = "Resolving…";
3137
+ const PHASE_2_COPY = "Still resolving…";
3138
+ /**
3139
+ * Returns the announcer string for debouncing/loading arms.
3140
+ * Phase 1: "Resolving…" (SF-3 default, unchanged for first N ms).
3141
+ * Phase 2: "Still resolving…" after threshold — reduces frozen perception
3142
+ * during CCIP-scale latency without naming gateway/CCIP/v2.
3143
+ */
3144
+ function useResolvingAnnouncerCopy({ isPending, escalateAfterMs = RESOLVING_ANNOUNCER_ESCALATE_AFTER_MS }) {
3145
+ const [escalated, setEscalated] = useState(false);
3146
+ useEffect(() => {
3147
+ if (!isPending) {
3148
+ setEscalated(false);
3149
+ return;
3150
+ }
3151
+ setEscalated(false);
3152
+ const timeoutId = window.setTimeout(() => {
3153
+ setEscalated(true);
3154
+ }, escalateAfterMs);
3155
+ return () => {
3156
+ window.clearTimeout(timeoutId);
3157
+ };
3158
+ }, [isPending, escalateAfterMs]);
3159
+ if (!isPending) return PHASE_1_COPY;
3160
+ return escalated ? PHASE_2_COPY : PHASE_1_COPY;
3161
+ }
3162
+
2551
3163
  //#endregion
2552
3164
  //#region src/components/fields/utils/accessibility.ts
2553
3165
  /**
@@ -2739,8 +3351,26 @@ function getWidthClasses(width) {
2739
3351
 
2740
3352
  //#endregion
2741
3353
  //#region src/components/fields/AddressField.tsx
2742
- const DEBOUNCE_MS = 200;
2743
- const MAX_SUGGESTIONS = 5;
3354
+ /**
3355
+ * Non-user-facing gate string returned by the sync validator while a typed name
3356
+ * is resolving, so `formState.isValid` is `false` even for an optional field
3357
+ * (INV-84). The visible message comes from the aria-live region, never this string.
3358
+ */
3359
+ const NAME_RESOLUTION_PENDING_GATE = "__ens_name_resolution_pending__";
3360
+ /** Codes for which a `retry()` affordance is offered — transient only (INV-90). */
3361
+ const TRANSIENT_ERROR_CODES = new Set([
3362
+ "RESOLUTION_TIMEOUT",
3363
+ "EXTERNAL_GATEWAY_ERROR",
3364
+ "ADAPTER_ERROR"
3365
+ ]);
3366
+ /**
3367
+ * Normalize a typed name for the resolved-write match guard: trim then lowercase,
3368
+ * mirroring the SF-2 engine (whose echoed `result.name` is already normalized).
3369
+ * Comparing the raw typed value would spuriously fail on case/whitespace (INV-79).
3370
+ */
3371
+ function normalizeResolvedName(value) {
3372
+ return value.trim().toLowerCase();
3373
+ }
2744
3374
  /**
2745
3375
  * Address input field component specifically designed for blockchain addresses via React Hook Form integration.
2746
3376
  *
@@ -2768,19 +3398,36 @@ const MAX_SUGGESTIONS = 5;
2768
3398
  * Pass `suggestions={false}` to opt out when a provider is mounted.
2769
3399
  *
2770
3400
  * The suggestion dropdown includes built-in debouncing, keyboard navigation (Arrow keys,
2771
- * Enter, Escape), click-outside dismissal, and ARIA listbox semantics.
3401
+ * Enter, Escape), click-outside dismissal, and ARIA listbox semantics — all provided by
3402
+ * the shared headless `useAddressSuggestionField` hook + `AddressSuggestionList`.
3403
+ *
3404
+ * **Inline name resolution (SF-3, opt-in via context).** When a
3405
+ * `NameResolverProvider` is mounted, the field also accepts a name (e.g.
3406
+ * `alice.eth`): the name is resolved inline through the injected `resolveName`
3407
+ * and the RHF form value becomes the resolved hex — never the name, never a
3408
+ * silently-coerced value. The correctness spine is a shadow-state model:
3409
+ * the `<input>` always shows the typed string (`inputValue`, INV-69); the RHF
3410
+ * value is `''` for every unresolved name state so submit stays gated with no
3411
+ * async validator (INV-75); the single name→hex write fires only on
3412
+ * `resolved` + normalized name-match (INV-79/80). With **no** provider mounted
3413
+ * every resolution branch below is dead code and the field is byte-identical
3414
+ * to its pre-ENS behavior (INV-82 — the LOCKED backward-compat guarantee).
3415
+ * The component stays capability-free: it never reads a runtime or wallet
3416
+ * state; everything arrives through the injected context (INV-118).
2772
3417
  */
2773
- function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, addressing, readOnly, suggestions: suggestionsProp, onSuggestionSelect }) {
3418
+ function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, addressing, readOnly, suggestions: suggestionsProp, onSuggestionSelect, onResolvedNameChange, showCrossNetworkFallbackDisclaimer = true }) {
2774
3419
  const isRequired = !!validation?.required;
2775
3420
  const errorId = `${id}-error`;
2776
3421
  const descriptionId = `${id}-description`;
2777
- const contextResolver = useContext(AddressSuggestionContext);
2778
- const containerRef = useRef(null);
3422
+ const resolutionRegionId = `${id}-resolution`;
3423
+ const resolver = useNameResolver();
3424
+ const resolveName = resolver?.resolveName;
3425
+ const isValidName = resolver?.isValidName;
3426
+ const activeNetworkId = resolver?.activeNetworkId;
3427
+ const activeNetworkName = resolver?.activeNetworkName;
3428
+ const resolveNetworkLabel = resolver?.resolveNetworkLabel;
2779
3429
  const lastSetValueRef = useRef("");
2780
3430
  const [inputValue, setInputValue] = useState("");
2781
- const [debouncedQuery, setDebouncedQuery] = useState("");
2782
- const [showSuggestions, setShowSuggestions] = useState(false);
2783
- const [highlightedIndex, setHighlightedIndex] = useState(-1);
2784
3431
  const watchedFieldValue = useWatch({
2785
3432
  control,
2786
3433
  name
@@ -2792,173 +3439,277 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
2792
3439
  setInputValue(currentFieldValue);
2793
3440
  }
2794
3441
  }, [watchedFieldValue]);
3442
+ const isValidAddress = useMemo(() => addressing ? (v) => addressing.isValidAddress(v) : void 0, [addressing]);
3443
+ const classification = useMemo(() => classifyAddressInput(inputValue, {
3444
+ isValidAddress,
3445
+ isValidName
3446
+ }), [
3447
+ inputValue,
3448
+ isValidAddress,
3449
+ isValidName
3450
+ ]);
3451
+ const resolverActive = resolver !== null && classification === "name-candidate";
3452
+ const nameEnabled = resolverActive && resolveName != null;
3453
+ const forwardUnsupported = resolverActive && resolveName == null;
3454
+ const resolution = useInjectedNameResolution({
3455
+ input: inputValue,
3456
+ enabled: nameEnabled,
3457
+ resolveName
3458
+ });
3459
+ const chainScopeBlocked = resolution.status === "resolved" && isChainScopeMismatch(resolution.data.provenance, activeNetworkId);
3460
+ const resolvingAnnouncerCopy = useResolvingAnnouncerCopy({ isPending: !forwardUnsupported && (resolution.status === "debouncing" || resolution.status === "loading") });
3461
+ const resolverPresentRef = useRef(false);
3462
+ const classificationRef = useRef("empty");
3463
+ const statusRef = useRef("idle");
3464
+ const resolvedNameRef = useRef(void 0);
3465
+ const chainScopeBlockedRef = useRef(false);
3466
+ const validationRef = useRef(validation);
3467
+ const addressingRef = useRef(addressing);
3468
+ resolverPresentRef.current = resolver !== null;
3469
+ classificationRef.current = classification;
3470
+ statusRef.current = resolution.status;
3471
+ resolvedNameRef.current = resolution.status === "resolved" ? resolution.name : void 0;
3472
+ chainScopeBlockedRef.current = chainScopeBlocked;
3473
+ validationRef.current = validation;
3474
+ addressingRef.current = addressing;
3475
+ const { field, fieldState } = useController({
3476
+ control,
3477
+ name,
3478
+ rules: { validate: useCallback((value) => {
3479
+ if (resolverPresentRef.current && classificationRef.current === "name-candidate" && (statusRef.current !== "resolved" || chainScopeBlockedRef.current)) return NAME_RESOLUTION_PENDING_GATE;
3480
+ if (value === void 0 || value === null || value === "") return validationRef.current?.required ? "This field is required" : true;
3481
+ const standardValidationResult = validateField(value, validationRef.current);
3482
+ if (standardValidationResult !== true) return standardValidationResult;
3483
+ if (addressingRef.current && typeof value === "string") {
3484
+ if (!addressingRef.current.isValidAddress(value)) return "Invalid address format for the selected chain";
3485
+ }
3486
+ return true;
3487
+ }, []) },
3488
+ disabled: readOnly
3489
+ });
3490
+ const fieldOnChange = field.onChange;
3491
+ const gateFormState = useFormState({
3492
+ control,
3493
+ disabled: resolver === null
3494
+ });
3495
+ const observedIsValid = resolver !== null && gateFormState.isValid;
3496
+ const pendingNameGate = resolver !== null && classification === "name-candidate" && (resolution.status !== "resolved" || chainScopeBlocked);
2795
3497
  useEffect(() => {
2796
- if (!inputValue.trim()) {
2797
- setDebouncedQuery("");
2798
- return;
3498
+ if (pendingNameGate && observedIsValid) control.setError(name, {
3499
+ type: "validate",
3500
+ message: NAME_RESOLUTION_PENDING_GATE
3501
+ });
3502
+ }, [
3503
+ pendingNameGate,
3504
+ observedIsValid,
3505
+ control,
3506
+ name
3507
+ ]);
3508
+ const resolvedAddress = resolution.status === "resolved" ? resolution.data.address : void 0;
3509
+ const resolvedName = resolution.status === "resolved" ? resolution.name : void 0;
3510
+ const isResolvedNameMatch = resolvedName !== void 0 && normalizeResolvedName(inputValue) === resolvedName;
3511
+ useEffect(() => {
3512
+ if (isResolvedNameMatch && resolvedAddress !== void 0 && !chainScopeBlocked) {
3513
+ lastSetValueRef.current = resolvedAddress;
3514
+ fieldOnChange(resolvedAddress);
2799
3515
  }
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
3516
  }, [
2810
- suggestionsDisabled,
2811
- suggestionsProp,
2812
- contextResolver,
2813
- debouncedQuery
3517
+ isResolvedNameMatch,
3518
+ resolvedAddress,
3519
+ chainScopeBlocked,
3520
+ fieldOnChange
2814
3521
  ]);
2815
- const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
3522
+ const matchedName = isResolvedNameMatch ? resolvedName : void 0;
2816
3523
  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") {
3524
+ onResolvedNameChange?.(matchedName);
3525
+ }, [matchedName, onResolvedNameChange]);
3526
+ const { containerRef, resolvedSuggestions, hasSuggestions, highlightedIndex, suggestionsDisabled, setHighlightedIndex, closeSuggestions, onInputChange, onContainerKeyDown } = useAddressSuggestionField({
3527
+ inputValue,
3528
+ suggestions: suggestionsProp
3529
+ });
3530
+ /**
3531
+ * The single site for the typed/raw write (the name→hex write lives in the
3532
+ * effect above). With no resolver this is the legacy write, verbatim (INV-82);
3533
+ * with a resolver, only the `name-candidate` classification diverges:
3534
+ * - hex / malformed / empty → the raw value (legacy sync validate applies)
3535
+ * - name-candidate → `''` — gates submit and synchronously invalidates any
3536
+ * prior resolved hex before re-resolution (INV-75 / INV-81)
3537
+ */
3538
+ const commitTypedValue = useCallback((value) => {
3539
+ setInputValue(value);
3540
+ if (!resolverPresentRef.current) {
3541
+ lastSetValueRef.current = value;
3542
+ fieldOnChange(value);
3543
+ return;
3544
+ }
3545
+ const c = classifyAddressInput(value, {
3546
+ isValidAddress,
3547
+ isValidName
3548
+ });
3549
+ classificationRef.current = c;
3550
+ if (c === "name-candidate") {
3551
+ const normalized = normalizeResolvedName(value);
3552
+ if (statusRef.current === "resolved" && resolvedNameRef.current === normalized && !chainScopeBlockedRef.current) return;
3553
+ statusRef.current = "debouncing";
3554
+ lastSetValueRef.current = "";
3555
+ fieldOnChange("");
3556
+ } else {
3557
+ lastSetValueRef.current = value;
3558
+ fieldOnChange(value);
3559
+ }
3560
+ }, [
3561
+ fieldOnChange,
3562
+ isValidAddress,
3563
+ isValidName
3564
+ ]);
3565
+ const handleInputChange = (e) => {
3566
+ commitTypedValue(e.target.value);
3567
+ onInputChange(e.target.value);
3568
+ };
3569
+ const applySuggestion = (suggestion) => {
3570
+ commitTypedValue(suggestion.value);
3571
+ onSuggestionSelect?.(suggestion);
3572
+ closeSuggestions();
3573
+ };
3574
+ const handleKeyDown = (e) => {
3575
+ if (hasSuggestions && e.key === "Enter" && highlightedIndex >= 0) {
2833
3576
  e.preventDefault();
2834
- setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
3577
+ applySuggestion(resolvedSuggestions[highlightedIndex]);
3578
+ return;
2835
3579
  }
2836
- }, [hasSuggestions, resolvedSuggestions.length]);
3580
+ if (e.key === "Escape") {
3581
+ if (hasSuggestions) {
3582
+ closeSuggestions();
3583
+ return;
3584
+ }
3585
+ handleEscapeKey(commitTypedValue, inputValue)(e);
3586
+ }
3587
+ };
3588
+ const hasError = !!fieldState.error;
3589
+ const isPendingGate = fieldState.error?.message === NAME_RESOLUTION_PENDING_GATE;
3590
+ const hasRealError = hasError && !isPendingGate;
3591
+ const shouldShowError = hasRealError && fieldState.isTouched;
3592
+ const validationClasses = getValidationStateClasses(hasRealError ? fieldState.error : void 0, isPendingGate ? false : fieldState.isTouched);
3593
+ const patternErrorMessage = validation?.pattern && typeof validation.pattern === "object" && "message" in validation.pattern ? validation.pattern.message : void 0;
3594
+ const accessibilityProps = getAccessibilityProps({
3595
+ id,
3596
+ hasError: hasRealError,
3597
+ isRequired,
3598
+ hasHelperText: !!helperText
3599
+ });
3600
+ const renderOutcome = () => {
3601
+ if (forwardUnsupported) return /* @__PURE__ */ jsx("span", {
3602
+ role: "alert",
3603
+ className: "text-destructive text-sm",
3604
+ children: nameResolutionMessageForCode("UNSUPPORTED_NETWORK")
3605
+ });
3606
+ switch (resolution.status) {
3607
+ case "idle": return null;
3608
+ case "debouncing":
3609
+ case "loading": return /* @__PURE__ */ jsx("span", {
3610
+ className: "text-muted-foreground text-sm",
3611
+ children: resolvingAnnouncerCopy
3612
+ });
3613
+ case "resolved":
3614
+ if (chainScopeBlocked) return /* @__PURE__ */ jsx("span", {
3615
+ role: "alert",
3616
+ className: "text-destructive text-sm",
3617
+ children: nameResolutionChainScopeMismatchMessage({ activeNetworkName })
3618
+ });
3619
+ const fallbackNetworks = showCrossNetworkFallbackDisclaimer && isCrossNetworkFallback(resolution.data.provenance) ? getFallbackNetworks(resolution.data.provenance) : void 0;
3620
+ const fallbackMessage = fallbackNetworks && nameResolutionCrossNetworkFallbackMessage(fallbackNetworks, crossNetworkFallbackMessageNames(fallbackNetworks, resolveNetworkLabel));
3621
+ return /* @__PURE__ */ jsxs("span", {
3622
+ className: "text-sm",
3623
+ children: [/* @__PURE__ */ jsxs("span", { children: ["Resolved to ", /* @__PURE__ */ jsx("code", {
3624
+ className: "font-mono",
3625
+ children: resolution.data.address
3626
+ })] }), fallbackMessage ? /* @__PURE__ */ jsx("span", {
3627
+ className: "mt-1 block text-muted-foreground text-xs",
3628
+ role: "note",
3629
+ children: fallbackMessage
3630
+ }) : null]
3631
+ });
3632
+ case "error": {
3633
+ const networkName = resolution.error.code === "UNSUPPORTED_NETWORK" ? resolution.error.networkId || void 0 : void 0;
3634
+ const message = nameResolutionMessageForCode(resolution.error.code, { networkName });
3635
+ const isTransient = TRANSIENT_ERROR_CODES.has(resolution.error.code);
3636
+ const retry = resolution.retry;
3637
+ return /* @__PURE__ */ jsxs("span", {
3638
+ role: "alert",
3639
+ className: "text-destructive text-sm",
3640
+ children: [message, isTransient ? /* @__PURE__ */ jsx("button", {
3641
+ type: "button",
3642
+ className: "ml-2 underline",
3643
+ onClick: retry,
3644
+ children: "Retry"
3645
+ }) : null]
3646
+ });
3647
+ }
3648
+ }
3649
+ };
2837
3650
  return /* @__PURE__ */ jsxs("div", {
2838
3651
  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
3652
+ children: [
3653
+ label && /* @__PURE__ */ jsxs(Label, {
3654
+ htmlFor: id,
3655
+ children: [
3656
+ label,
3657
+ " ",
3658
+ isRequired && /* @__PURE__ */ jsx("span", {
3659
+ className: "text-destructive",
3660
+ children: "*"
2958
3661
  })
2959
- ] });
2960
- }
2961
- })]
3662
+ ]
3663
+ }),
3664
+ /* @__PURE__ */ jsxs("div", {
3665
+ ref: containerRef,
3666
+ className: "relative",
3667
+ onKeyDown: onContainerKeyDown,
3668
+ children: [/* @__PURE__ */ jsx(Input, {
3669
+ ...field,
3670
+ id,
3671
+ placeholder: placeholder || (resolver !== null ? "0x... or name" : "0x..."),
3672
+ className: validationClasses,
3673
+ onChange: handleInputChange,
3674
+ onKeyDown: handleKeyDown,
3675
+ "data-slot": "input",
3676
+ value: inputValue,
3677
+ ...accessibilityProps,
3678
+ "aria-describedby": resolver === null ? `${helperText ? descriptionId : ""} ${hasRealError ? errorId : ""}` : [
3679
+ helperText ? descriptionId : "",
3680
+ hasRealError ? errorId : "",
3681
+ resolutionRegionId
3682
+ ].filter(Boolean).join(" ") || void 0,
3683
+ "aria-expanded": hasSuggestions,
3684
+ "aria-autocomplete": suggestionsDisabled ? void 0 : "list",
3685
+ "aria-controls": hasSuggestions ? `${id}-suggestions` : void 0,
3686
+ "aria-activedescendant": hasSuggestions && highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : void 0,
3687
+ disabled: readOnly
3688
+ }), hasSuggestions && /* @__PURE__ */ jsx(AddressSuggestionList, {
3689
+ id,
3690
+ suggestions: resolvedSuggestions,
3691
+ highlightedIndex,
3692
+ onSelect: applySuggestion,
3693
+ onHighlight: setHighlightedIndex
3694
+ })]
3695
+ }),
3696
+ helperText && /* @__PURE__ */ jsx("div", {
3697
+ id: descriptionId,
3698
+ className: "text-muted-foreground text-sm",
3699
+ children: helperText
3700
+ }),
3701
+ /* @__PURE__ */ jsx(ErrorMessage, {
3702
+ error: hasRealError ? fieldState.error : void 0,
3703
+ id: errorId,
3704
+ message: shouldShowError ? fieldState.error?.message || patternErrorMessage : void 0
3705
+ }),
3706
+ resolver !== null && /* @__PURE__ */ jsx("div", {
3707
+ id: resolutionRegionId,
3708
+ "aria-live": "polite",
3709
+ className: "min-h-5",
3710
+ children: renderOutcome()
3711
+ })
3712
+ ]
2962
3713
  });
2963
3714
  }
2964
3715
  AddressField.displayName = "AddressField";
@@ -3498,6 +4249,84 @@ function useAddressSuggestions(query, networkId) {
3498
4249
  ]) };
3499
4250
  }
3500
4251
 
4252
+ //#endregion
4253
+ //#region src/components/fields/name-resolution/name-resolver-context.tsx
4254
+ /**
4255
+ * Name Resolver Context (SF-3)
4256
+ *
4257
+ * Provides a React context for forward name resolution (name → address).
4258
+ * When a `NameResolverProvider` is mounted, every `AddressField` in the
4259
+ * subtree resolves typed names inline through the injected `resolveName`
4260
+ * and submits the resolved hex — never the name (SC-004).
4261
+ *
4262
+ * The provider is deliberately **dumb** (INV-118): it memoizes the injected
4263
+ * functions into the context value and nothing else — no hook state, no
4264
+ * capability, no chain dependency. The smart runtime wiring lives upstream in
4265
+ * `@openzeppelin/ui-react` (`useRuntimeNameResolver`), which an app or the
4266
+ * renderer mounts ambiently:
4267
+ *
4268
+ * @example
4269
+ * ```tsx
4270
+ * import { NameResolverProvider } from '@openzeppelin/ui-components';
4271
+ * import { useRuntimeNameResolver } from '@openzeppelin/ui-react';
4272
+ *
4273
+ * function FormRoot() {
4274
+ * const resolver = useRuntimeNameResolver();
4275
+ * return (
4276
+ * <NameResolverProvider {...resolver}>
4277
+ * <MyForm />
4278
+ * </NameResolverProvider>
4279
+ * );
4280
+ * }
4281
+ * ```
4282
+ */
4283
+ /**
4284
+ * Provides forward name resolution to all `AddressField` instances in the
4285
+ * subtree (zero call-site wiring, SC-001). Both functions are optional: an
4286
+ * absent `resolveName` means forward resolution is unsupported and a typed
4287
+ * name surfaces `UNSUPPORTED_NETWORK` (INV-119 / SC-006).
4288
+ *
4289
+ * ## Stable resolver identity (integrator contract)
4290
+ *
4291
+ * The injected resolver **must be referentially stable across renders** —
4292
+ * memoize it (e.g. `useMemo`) or use `useRuntimeNameResolver` from
4293
+ * `@openzeppelin/ui-react`, which is stable by construction. A resolver whose
4294
+ * function identity changes on every render (e.g. a fresh inline function) is a
4295
+ * misconfiguration: the field keys inline resolution on that identity, so a
4296
+ * churning identity would otherwise re-dispatch on every render — an unbounded
4297
+ * RPC loop on a funds path.
4298
+ *
4299
+ * The base component **detects and withstands** this (it is not merely a caveat):
4300
+ * dispatch is bounded per resolution intent so a churning resolver can never drive
4301
+ * an unbounded loop (INV-123), a one-shot development-only warning names the
4302
+ * misconfiguration (INV-124), and the field degrades to a **safe gated state**
4303
+ * (empty value, submit blocked — never a wrong address, never a throw; INV-125)
4304
+ * until the resolver is memoized. A **genuine** resolver/network swap — a single
4305
+ * identity change for a given input — is still honored and re-resolves within
4306
+ * budget (INV-119). The bound is a backstop; the contract is a stable identity.
4307
+ *
4308
+ * @param props - Injected resolver functions and children
4309
+ */
4310
+ function NameResolverProvider({ children, isValidName, resolveName, activeNetworkId, activeNetworkName, resolveNetworkLabel }) {
4311
+ const value = React$1.useMemo(() => ({
4312
+ isValidName,
4313
+ resolveName,
4314
+ activeNetworkId,
4315
+ activeNetworkName,
4316
+ resolveNetworkLabel
4317
+ }), [
4318
+ isValidName,
4319
+ resolveName,
4320
+ activeNetworkId,
4321
+ activeNetworkName,
4322
+ resolveNetworkLabel
4323
+ ]);
4324
+ return /* @__PURE__ */ jsx(NameResolverContext.Provider, {
4325
+ value,
4326
+ children
4327
+ });
4328
+ }
4329
+
3501
4330
  //#endregion
3502
4331
  //#region src/components/fields/AmountField.tsx
3503
4332
  /**
@@ -6226,7 +7055,7 @@ SelectGroupedField.displayName = "SelectGroupedField";
6226
7055
  * - Full accessibility support with ARIA attributes
6227
7056
  * - Keyboard navigation
6228
7057
  */
6229
- function TextField({ id, label, placeholder, helperText, control, name, width = "full", validation, readOnly }) {
7058
+ function TextField({ id, label, placeholder, helperText, control, name, width = "full", validation, readOnly, onUserEdit }) {
6230
7059
  const isRequired = !!validation?.required;
6231
7060
  const errorId = `${id}-error`;
6232
7061
  const descriptionId = `${id}-description`;
@@ -6262,9 +7091,13 @@ function TextField({ id, label, placeholder, helperText, control, name, width =
6262
7091
  const handleInputChange = (e) => {
6263
7092
  const value = e.target.value;
6264
7093
  field.onChange(value);
7094
+ onUserEdit?.(value);
6265
7095
  };
6266
7096
  const handleKeyDown = (e) => {
6267
- if (e.key === "Escape") handleEscapeKey(field.onChange, field.value)(e);
7097
+ if (e.key === "Escape") handleEscapeKey((value) => {
7098
+ field.onChange(value);
7099
+ onUserEdit?.(value);
7100
+ }, field.value)(e);
6268
7101
  };
6269
7102
  const accessibilityProps = getAccessibilityProps({
6270
7103
  id,
@@ -6797,5 +7630,5 @@ const Toaster = ({ ...props }) => {
6797
7630
  };
6798
7631
 
6799
7632
  //#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 };
7633
+ 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
7634
  //# sourceMappingURL=index.mjs.map