@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/README.md +4 -0
- package/dist/index.cjs +1046 -192
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +438 -15
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +444 -21
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1029 -196
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
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.
|
|
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,
|
|
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.
|
|
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
|
|
155
|
-
*
|
|
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
|
|
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 (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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: [
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
2743
|
-
|
|
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
|
|
2778
|
-
const
|
|
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 (
|
|
2797
|
-
|
|
2798
|
-
|
|
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
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
3517
|
+
isResolvedNameMatch,
|
|
3518
|
+
resolvedAddress,
|
|
3519
|
+
chainScopeBlocked,
|
|
3520
|
+
fieldOnChange
|
|
2814
3521
|
]);
|
|
2815
|
-
const
|
|
3522
|
+
const matchedName = isResolvedNameMatch ? resolvedName : void 0;
|
|
2816
3523
|
useEffect(() => {
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
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
|
-
|
|
3577
|
+
applySuggestion(resolvedSuggestions[highlightedIndex]);
|
|
3578
|
+
return;
|
|
2835
3579
|
}
|
|
2836
|
-
|
|
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: [
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
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(
|
|
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
|