@openzeppelin/ui-components 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
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, CloudOff, Copy, DollarSign, ExternalLink as ExternalLink$1, ExternalLinkIcon, Eye, EyeOff, File, FileText, GripVertical, Hash, Info, Loader2, Menu, Network, 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, CloudOff, Copy, DollarSign, ExternalLink as ExternalLink$1, ExternalLinkIcon, Eye, EyeOff, File, FileText, GripVertical, Hash, Info, Loader2, Menu, MoreHorizontal, Network, Pencil, Plus, Search, Settings, Timer, Upload, X } from "lucide-react";
5
5
  import * as React$1 from "react";
6
6
  import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
7
7
  import { cn, getDefaultValueForType, getInvalidUrlMessage, getServiceDisplayName, isValidUrl, truncateMiddle, validateBytesSimple } from "@openzeppelin/ui-utils";
@@ -103,11 +103,58 @@ const AccordionContent = React$1.forwardRef(({ className, children, variant: var
103
103
  AccordionContent.displayName = "AccordionContent";
104
104
 
105
105
  //#endregion
106
- //#region src/components/ui/address-display.tsx
107
- /** Displays a blockchain address with optional truncation, copy button, and explorer link. */
108
- function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, className, ...props }) {
106
+ //#region src/components/ui/address-display/context.ts
107
+ /**
108
+ * @internal Shared context instance consumed by both AddressDisplay and
109
+ * AddressLabelProvider. Kept in its own file so component files export
110
+ * only components (required by React Fast Refresh).
111
+ */
112
+ const AddressLabelContext = createContext(null);
113
+
114
+ //#endregion
115
+ //#region src/components/ui/address-display/address-display.tsx
116
+ /**
117
+ * Displays a blockchain address with optional truncation, copy button,
118
+ * explorer link, and human-readable label.
119
+ *
120
+ * Labels are resolved in priority order:
121
+ * 1. Explicit `label` prop
122
+ * 2. `AddressLabelContext` resolver (via `AddressLabelProvider`)
123
+ * 3. No label (renders address only, identical to previous behavior)
124
+ *
125
+ * Pass `disableLabel` to suppress context-based resolution (e.g. when the
126
+ * surrounding UI already shows a name, such as a contract selector).
127
+ *
128
+ * @example
129
+ * ```tsx
130
+ * // Basic usage (unchanged)
131
+ * <AddressDisplay address="0x742d35Cc..." showCopyButton />
132
+ *
133
+ * // Explicit label
134
+ * <AddressDisplay address="0x742d35Cc..." label="Treasury" />
135
+ *
136
+ * // Auto-resolved via context (no changes needed at call site)
137
+ * <AddressLabelProvider resolveLabel={myResolver}>
138
+ * <AddressDisplay address="0x742d35Cc..." />
139
+ * </AddressLabelProvider>
140
+ *
141
+ * // Suppress label resolution for a specific instance
142
+ * <AddressDisplay address="0x742d35Cc..." disableLabel />
143
+ * ```
144
+ */
145
+ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, label: labelProp, onLabelEdit: onLabelEditProp, networkId, disableLabel = false, className, ...props }) {
109
146
  const [copied, setCopied] = React$1.useState(false);
110
147
  const copyTimeoutRef = React$1.useRef(null);
148
+ const resolver = React$1.useContext(AddressLabelContext);
149
+ const resolvedLabel = disableLabel ? void 0 : labelProp ?? resolver?.resolveLabel(address, networkId);
150
+ const contextEditHandler = React$1.useCallback(() => {
151
+ resolver?.onEditLabel?.(address, networkId);
152
+ }, [
153
+ resolver,
154
+ address,
155
+ networkId
156
+ ]);
157
+ const editHandler = disableLabel ? void 0 : onLabelEditProp ?? (resolver?.onEditLabel ? contextEditHandler : void 0);
111
158
  const displayAddress = truncate ? truncateMiddle(address, startChars, endChars) : address;
112
159
  const handleCopy = (e) => {
113
160
  e.stopPropagation();
@@ -124,11 +171,7 @@ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4
124
171
  if (copyTimeoutRef.current) window.clearTimeout(copyTimeoutRef.current);
125
172
  };
126
173
  }, []);
127
- const addressContent = /* @__PURE__ */ jsxs(Fragment, { children: [
128
- /* @__PURE__ */ jsx("span", {
129
- className: cn("truncate", truncate ? "" : "break-all"),
130
- children: displayAddress
131
- }),
174
+ const actionButtons = /* @__PURE__ */ jsxs(Fragment, { children: [
132
175
  showCopyButton && /* @__PURE__ */ jsx("button", {
133
176
  type: "button",
134
177
  onClick: handleCopy,
@@ -143,15 +186,136 @@ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4
143
186
  className: "ml-1.5 shrink-0 text-slate-500 transition-colors hover:text-slate-700",
144
187
  "aria-label": "View in explorer",
145
188
  children: /* @__PURE__ */ jsx(ExternalLink$1, { className: "h-3.5 w-3.5" })
189
+ }),
190
+ editHandler && /* @__PURE__ */ jsx("button", {
191
+ type: "button",
192
+ onClick: (e) => {
193
+ e.stopPropagation();
194
+ editHandler();
195
+ },
196
+ className: "ml-0 w-0 shrink-0 overflow-hidden text-slate-500 opacity-0 transition-all duration-150 hover:text-slate-700 group-hover:ml-1.5 group-hover:w-3.5 group-hover:opacity-100 focus:ml-1.5 focus:w-3.5 focus:opacity-100",
197
+ "aria-label": "Edit label",
198
+ children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" })
146
199
  })
147
200
  ] });
148
- return /* @__PURE__ */ jsx("div", {
201
+ if (resolvedLabel) return /* @__PURE__ */ jsxs("div", {
202
+ className: cn("group inline-flex max-w-full flex-col rounded-md bg-slate-100 px-2 py-1", "text-xs text-slate-700", className),
203
+ ...props,
204
+ children: [/* @__PURE__ */ jsx("span", {
205
+ className: "truncate font-sans font-medium text-slate-900 leading-snug",
206
+ children: resolvedLabel
207
+ }), /* @__PURE__ */ jsxs("div", {
208
+ className: "flex items-center font-mono text-[10px] text-slate-400 leading-snug",
209
+ children: [/* @__PURE__ */ jsx("span", {
210
+ className: cn("truncate", truncate ? "" : "break-all"),
211
+ children: displayAddress
212
+ }), actionButtons]
213
+ })]
214
+ });
215
+ return /* @__PURE__ */ jsxs("div", {
149
216
  className: cn("group inline-flex max-w-full items-center rounded-md bg-slate-100 px-2 py-1", "text-xs font-mono text-slate-700", className),
150
217
  ...props,
151
- children: addressContent
218
+ children: [/* @__PURE__ */ jsx("span", {
219
+ className: cn("truncate", truncate ? "" : "break-all"),
220
+ children: displayAddress
221
+ }), actionButtons]
222
+ });
223
+ }
224
+
225
+ //#endregion
226
+ //#region src/components/ui/address-display/address-label-context.tsx
227
+ /**
228
+ * Address Label Context
229
+ *
230
+ * Provides a React context for resolving human-readable labels for blockchain
231
+ * addresses. When an `AddressLabelProvider` is mounted, every `AddressDisplay`
232
+ * in the subtree automatically resolves and renders labels without any
233
+ * call-site changes.
234
+ *
235
+ * @example
236
+ * ```tsx
237
+ * import { AddressLabelProvider } from '@openzeppelin/ui-components';
238
+ *
239
+ * function App() {
240
+ * const resolver = useAliasLabelResolver(db);
241
+ * return (
242
+ * <AddressLabelProvider {...resolver}>
243
+ * <MyApp />
244
+ * </AddressLabelProvider>
245
+ * );
246
+ * }
247
+ * ```
248
+ */
249
+ /**
250
+ * Provides address label resolution to all `AddressDisplay` instances in the
251
+ * subtree. Wrap your application (or a subsection) with this provider and
252
+ * supply a `resolveLabel` function.
253
+ *
254
+ * @param props - Resolver functions and children
255
+ *
256
+ * @example
257
+ * ```tsx
258
+ * <AddressLabelProvider
259
+ * resolveLabel={(addr) => addressBook.get(addr)}
260
+ * onEditLabel={(addr) => openEditor(addr)}
261
+ * >
262
+ * <App />
263
+ * </AddressLabelProvider>
264
+ * ```
265
+ */
266
+ function AddressLabelProvider({ children, resolveLabel, onEditLabel }) {
267
+ const value = React$1.useMemo(() => ({
268
+ resolveLabel,
269
+ onEditLabel
270
+ }), [resolveLabel, onEditLabel]);
271
+ return /* @__PURE__ */ jsx(AddressLabelContext.Provider, {
272
+ value,
273
+ children
152
274
  });
153
275
  }
154
276
 
277
+ //#endregion
278
+ //#region src/components/ui/address-display/use-address-label.ts
279
+ /**
280
+ * Convenience hook for resolving an address label from the nearest
281
+ * `AddressLabelProvider`.
282
+ *
283
+ * Kept in its own file so that `address-label-context.tsx` exports only
284
+ * components (required by React Fast Refresh).
285
+ */
286
+ /**
287
+ * Convenience hook that resolves a label for a specific address using the
288
+ * nearest `AddressLabelProvider`. Returns `undefined` values when no provider
289
+ * is mounted.
290
+ *
291
+ * @param address - The blockchain address to resolve
292
+ * @param networkId - Optional network identifier for network-specific aliases
293
+ * @returns Resolved label and edit handler for the address
294
+ *
295
+ * @example
296
+ * ```tsx
297
+ * function MyAddress({ address }: { address: string }) {
298
+ * const { label, onEdit } = useAddressLabel(address, 'ethereum-mainnet');
299
+ * return <span>{label ?? address}</span>;
300
+ * }
301
+ * ```
302
+ */
303
+ function useAddressLabel(address, networkId) {
304
+ const resolver = React$1.useContext(AddressLabelContext);
305
+ const label = resolver?.resolveLabel(address, networkId);
306
+ const onEdit = React$1.useCallback(() => {
307
+ resolver?.onEditLabel?.(address, networkId);
308
+ }, [
309
+ resolver,
310
+ address,
311
+ networkId
312
+ ]);
313
+ return {
314
+ label,
315
+ onEdit: resolver?.onEditLabel ? onEdit : void 0
316
+ };
317
+ }
318
+
155
319
  //#endregion
156
320
  //#region src/components/ui/alert.tsx
157
321
  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", {
@@ -1085,10 +1249,15 @@ function NetworkIcon({ network, className, size = 16, variant = "branded" }) {
1085
1249
 
1086
1250
  //#endregion
1087
1251
  //#region src/components/ui/network-selector.tsx
1088
- /** Searchable dropdown selector for blockchain networks with optional grouping. */
1089
- function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetworkLabel, getNetworkIcon, getNetworkType, getNetworkId, groupByEcosystem = false, getEcosystem, filterNetwork, className, placeholder = "Select Network" }) {
1252
+ /** Searchable dropdown selector for blockchain networks with optional grouping and multi-select. */
1253
+ function NetworkSelector({ networks, getNetworkLabel, getNetworkIcon, getNetworkType, getNetworkId, groupByEcosystem = false, getEcosystem, filterNetwork, className, placeholder = "Select Network", ...modeProps }) {
1090
1254
  const [open, setOpen] = React$1.useState(false);
1091
1255
  const [searchQuery, setSearchQuery] = React$1.useState("");
1256
+ const isMultiple = modeProps.multiple === true;
1257
+ const selectedNetworkIds = isMultiple ? modeProps.selectedNetworkIds : void 0;
1258
+ const onSelectionChange = isMultiple ? modeProps.onSelectionChange : void 0;
1259
+ const selectedNetwork = !isMultiple ? modeProps.selectedNetwork : void 0;
1260
+ const onSelectNetwork = !isMultiple ? modeProps.onSelectNetwork : void 0;
1092
1261
  const filteredNetworks = React$1.useMemo(() => {
1093
1262
  if (!searchQuery) return networks;
1094
1263
  if (filterNetwork) return networks.filter((n) => filterNetwork(n, searchQuery));
@@ -1112,34 +1281,79 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1112
1281
  groupByEcosystem,
1113
1282
  getEcosystem
1114
1283
  ]);
1284
+ const isSelected = React$1.useCallback((network) => {
1285
+ if (isMultiple && selectedNetworkIds) return selectedNetworkIds.includes(getNetworkId(network));
1286
+ return selectedNetwork ? getNetworkId(selectedNetwork) === getNetworkId(network) : false;
1287
+ }, [
1288
+ isMultiple,
1289
+ selectedNetworkIds,
1290
+ selectedNetwork,
1291
+ getNetworkId
1292
+ ]);
1293
+ const handleSelect = React$1.useCallback((network) => {
1294
+ if (isMultiple && selectedNetworkIds && onSelectionChange) {
1295
+ const id = getNetworkId(network);
1296
+ onSelectionChange(selectedNetworkIds.includes(id) ? selectedNetworkIds.filter((x) => x !== id) : [...selectedNetworkIds, id]);
1297
+ } else if (onSelectNetwork) {
1298
+ onSelectNetwork(network);
1299
+ setOpen(false);
1300
+ }
1301
+ }, [
1302
+ isMultiple,
1303
+ selectedNetworkIds,
1304
+ onSelectionChange,
1305
+ onSelectNetwork,
1306
+ getNetworkId
1307
+ ]);
1308
+ const handleClearAll = React$1.useCallback(() => {
1309
+ if (isMultiple && onSelectionChange) onSelectionChange([]);
1310
+ }, [isMultiple, onSelectionChange]);
1311
+ const selectedCount = selectedNetworkIds?.length ?? 0;
1312
+ const renderTrigger = isMultiple ? modeProps.renderTrigger : void 0;
1115
1313
  return /* @__PURE__ */ jsxs(DropdownMenu, {
1116
1314
  open,
1117
1315
  onOpenChange: setOpen,
1118
1316
  children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
1119
1317
  asChild: true,
1120
- children: /* @__PURE__ */ jsxs(Button, {
1121
- variant: "outline",
1122
- role: "combobox",
1123
- "aria-expanded": open,
1124
- className: cn("w-full justify-between", className),
1125
- children: [/* @__PURE__ */ jsx("span", {
1126
- className: "flex items-center gap-2 truncate",
1127
- children: selectedNetwork ? /* @__PURE__ */ jsxs(Fragment, { children: [
1128
- getNetworkIcon?.(selectedNetwork),
1129
- /* @__PURE__ */ jsx("span", {
1130
- className: "truncate",
1131
- children: getNetworkLabel(selectedNetwork)
1132
- }),
1133
- getNetworkType && /* @__PURE__ */ jsx("span", {
1134
- className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase text-muted-foreground",
1135
- children: getNetworkType(selectedNetwork)
1318
+ children: (() => {
1319
+ if (isMultiple && renderTrigger) return renderTrigger({
1320
+ selectedCount,
1321
+ open
1322
+ });
1323
+ if (isMultiple) return /* @__PURE__ */ jsxs(Button, {
1324
+ variant: "outline",
1325
+ role: "combobox",
1326
+ "aria-expanded": open,
1327
+ className: cn("w-full justify-between", className),
1328
+ children: [/* @__PURE__ */ jsx("span", {
1329
+ className: "truncate text-muted-foreground",
1330
+ children: selectedCount > 0 ? `${selectedCount} network${selectedCount > 1 ? "s" : ""} selected` : placeholder
1331
+ }), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
1332
+ });
1333
+ return /* @__PURE__ */ jsxs(Button, {
1334
+ variant: "outline",
1335
+ role: "combobox",
1336
+ "aria-expanded": open,
1337
+ className: cn("w-full justify-between", className),
1338
+ children: [/* @__PURE__ */ jsx("span", {
1339
+ className: "flex items-center gap-2 truncate",
1340
+ children: selectedNetwork ? /* @__PURE__ */ jsxs(Fragment, { children: [
1341
+ getNetworkIcon?.(selectedNetwork),
1342
+ /* @__PURE__ */ jsx("span", {
1343
+ className: "truncate",
1344
+ children: getNetworkLabel(selectedNetwork)
1345
+ }),
1346
+ getNetworkType && /* @__PURE__ */ jsx("span", {
1347
+ className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase text-muted-foreground",
1348
+ children: getNetworkType(selectedNetwork)
1349
+ })
1350
+ ] }) : /* @__PURE__ */ jsx("span", {
1351
+ className: "text-muted-foreground",
1352
+ children: placeholder
1136
1353
  })
1137
- ] }) : /* @__PURE__ */ jsx("span", {
1138
- className: "text-muted-foreground",
1139
- children: placeholder
1140
- })
1141
- }), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
1142
- })
1354
+ }), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
1355
+ });
1356
+ })()
1143
1357
  }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
1144
1358
  className: "w-[--radix-dropdown-menu-trigger-width] min-w-[240px] p-0",
1145
1359
  align: "start",
@@ -1155,9 +1369,19 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1155
1369
  className: "h-9 w-full border-0 bg-transparent p-0 placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0",
1156
1370
  "aria-label": "Search networks"
1157
1371
  })]
1158
- }), /* @__PURE__ */ jsx("div", {
1372
+ }), /* @__PURE__ */ jsxs("div", {
1159
1373
  className: "max-h-[300px] overflow-y-auto p-1",
1160
- children: Object.entries(groupedNetworks).length === 0 ? /* @__PURE__ */ jsx("div", {
1374
+ children: [isMultiple && selectedCount > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
1375
+ className: "flex items-center justify-between px-2 py-1.5",
1376
+ children: [/* @__PURE__ */ jsxs("span", {
1377
+ className: "text-xs font-medium text-muted-foreground",
1378
+ children: [selectedCount, " selected"]
1379
+ }), /* @__PURE__ */ jsx("button", {
1380
+ onClick: handleClearAll,
1381
+ className: "text-xs text-muted-foreground hover:text-foreground",
1382
+ children: "Clear all"
1383
+ })]
1384
+ }), /* @__PURE__ */ jsx(DropdownMenuSeparator, {})] }), Object.entries(groupedNetworks).length === 0 ? /* @__PURE__ */ jsx("div", {
1161
1385
  className: "py-6 text-center text-sm text-muted-foreground",
1162
1386
  children: "No network found."
1163
1387
  }) : Object.entries(groupedNetworks).map(([group, groupNetworks], index) => /* @__PURE__ */ jsxs(React$1.Fragment, { children: [
@@ -1166,12 +1390,16 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1166
1390
  children: group
1167
1391
  }),
1168
1392
  /* @__PURE__ */ jsx(DropdownMenuGroup, { children: groupNetworks.map((network) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
1169
- onSelect: () => {
1170
- onSelectNetwork(network);
1171
- setOpen(false);
1393
+ onSelect: (e) => {
1394
+ if (isMultiple) e.preventDefault();
1395
+ handleSelect(network);
1172
1396
  },
1173
1397
  className: "gap-2",
1174
1398
  children: [
1399
+ isMultiple ? /* @__PURE__ */ jsx("div", {
1400
+ className: "flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border border-primary",
1401
+ children: isSelected(network) && /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" })
1402
+ }) : null,
1175
1403
  getNetworkIcon?.(network),
1176
1404
  /* @__PURE__ */ jsxs("div", {
1177
1405
  className: "flex flex-1 items-center gap-2 min-w-0",
@@ -1183,11 +1411,11 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1183
1411
  children: getNetworkType(network)
1184
1412
  })]
1185
1413
  }),
1186
- selectedNetwork && getNetworkId(selectedNetwork) === getNetworkId(network) && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 opacity-100" })
1414
+ !isMultiple && isSelected(network) && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 opacity-100" })
1187
1415
  ]
1188
1416
  }, getNetworkId(network))) }),
1189
1417
  index < Object.keys(groupedNetworks).length - 1 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {})
1190
- ] }, group))
1418
+ ] }, group))]
1191
1419
  })]
1192
1420
  })]
1193
1421
  });
@@ -1220,6 +1448,32 @@ function NetworkStatusBadge({ network, className }) {
1220
1448
  });
1221
1449
  }
1222
1450
 
1451
+ //#endregion
1452
+ //#region src/components/ui/overflow-menu.tsx
1453
+ /** Compact "..." dropdown menu for secondary actions. */
1454
+ function OverflowMenu({ items, align = "end", className, "aria-label": ariaLabel = "More actions" }) {
1455
+ if (items.length === 0) return null;
1456
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
1457
+ asChild: true,
1458
+ children: /* @__PURE__ */ jsx(Button, {
1459
+ variant: "ghost",
1460
+ size: "icon",
1461
+ className: cn("h-8 w-8", className),
1462
+ "aria-label": ariaLabel,
1463
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" })
1464
+ })
1465
+ }), /* @__PURE__ */ jsx(DropdownMenuContent, {
1466
+ align,
1467
+ className: "min-w-[140px]",
1468
+ children: items.map((item, index) => /* @__PURE__ */ jsxs(React$1.Fragment, { children: [item.destructive && index > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}), /* @__PURE__ */ jsxs(DropdownMenuItem, {
1469
+ onClick: item.onSelect,
1470
+ disabled: item.disabled,
1471
+ className: cn(item.destructive && "text-destructive focus:text-destructive"),
1472
+ children: [item.icon, item.label]
1473
+ })] }, item.id))
1474
+ })] });
1475
+ }
1476
+
1223
1477
  //#endregion
1224
1478
  //#region src/components/ui/progress.tsx
1225
1479
  const Progress = React$1.forwardRef(({ className, value, ...props }, ref) => /* @__PURE__ */ jsx(ProgressPrimitive.Root, {
@@ -1589,6 +1843,15 @@ function ViewContractStateButton({ contractAddress, onToggle }) {
1589
1843
  });
1590
1844
  }
1591
1845
 
1846
+ //#endregion
1847
+ //#region src/components/fields/address-suggestion/context.ts
1848
+ /**
1849
+ * @internal Shared context instance consumed by both AddressField and
1850
+ * AddressSuggestionProvider. Kept in its own file so component files export
1851
+ * only components (required by React Fast Refresh).
1852
+ */
1853
+ const AddressSuggestionContext = createContext(null);
1854
+
1592
1855
  //#endregion
1593
1856
  //#region src/components/fields/utils/accessibility.ts
1594
1857
  /**
@@ -1780,6 +2043,8 @@ function getWidthClasses(width) {
1780
2043
 
1781
2044
  //#endregion
1782
2045
  //#region src/components/fields/AddressField.tsx
2046
+ const DEBOUNCE_MS = 200;
2047
+ const MAX_SUGGESTIONS = 5;
1783
2048
  /**
1784
2049
  * Address input field component specifically designed for blockchain addresses via React Hook Form integration.
1785
2050
  *
@@ -1797,11 +2062,82 @@ function getWidthClasses(width) {
1797
2062
  * - Chain-agnostic design (validation handled by adapters)
1798
2063
  * - Full accessibility support with ARIA attributes
1799
2064
  * - Keyboard navigation
2065
+ *
2066
+ * Autocomplete suggestions can be provided in two ways:
2067
+ *
2068
+ * 1. **Context-based (zero-config)**: Mount an `AddressSuggestionProvider` in the
2069
+ * component tree. Every `AddressField` below it automatically resolves suggestions.
2070
+ *
2071
+ * 2. **Prop-based (explicit)**: Pass `suggestions` directly. This overrides context.
2072
+ * Pass `suggestions={false}` to opt out when a provider is mounted.
2073
+ *
2074
+ * The suggestion dropdown includes built-in debouncing, keyboard navigation (Arrow keys,
2075
+ * Enter, Escape), click-outside dismissal, and ARIA listbox semantics.
1800
2076
  */
1801
- function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, adapter, readOnly }) {
2077
+ function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, adapter, readOnly, suggestions: suggestionsProp, onSuggestionSelect }) {
1802
2078
  const isRequired = !!validation?.required;
1803
2079
  const errorId = `${id}-error`;
1804
2080
  const descriptionId = `${id}-description`;
2081
+ const contextResolver = useContext(AddressSuggestionContext);
2082
+ const containerRef = useRef(null);
2083
+ const lastSetValueRef = useRef("");
2084
+ const [inputValue, setInputValue] = useState("");
2085
+ const [debouncedQuery, setDebouncedQuery] = useState("");
2086
+ const [showSuggestions, setShowSuggestions] = useState(false);
2087
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
2088
+ const watchedFieldValue = useWatch({
2089
+ control,
2090
+ name
2091
+ });
2092
+ useEffect(() => {
2093
+ const currentFieldValue = watchedFieldValue ?? "";
2094
+ if (currentFieldValue !== lastSetValueRef.current) {
2095
+ lastSetValueRef.current = currentFieldValue;
2096
+ setInputValue(currentFieldValue);
2097
+ }
2098
+ }, [watchedFieldValue]);
2099
+ useEffect(() => {
2100
+ if (!inputValue.trim()) {
2101
+ setDebouncedQuery("");
2102
+ return;
2103
+ }
2104
+ const timer = setTimeout(() => setDebouncedQuery(inputValue), DEBOUNCE_MS);
2105
+ return () => clearTimeout(timer);
2106
+ }, [inputValue]);
2107
+ const suggestionsDisabled = suggestionsProp === false;
2108
+ const resolvedSuggestions = useMemo(() => {
2109
+ if (suggestionsDisabled) return [];
2110
+ if (Array.isArray(suggestionsProp)) return suggestionsProp;
2111
+ if (!contextResolver || !debouncedQuery.trim()) return [];
2112
+ return contextResolver.resolveSuggestions(debouncedQuery).slice(0, MAX_SUGGESTIONS);
2113
+ }, [
2114
+ suggestionsDisabled,
2115
+ suggestionsProp,
2116
+ contextResolver,
2117
+ debouncedQuery
2118
+ ]);
2119
+ const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
2120
+ useEffect(() => {
2121
+ let active = true;
2122
+ const handleClickOutside = (e) => {
2123
+ if (active && containerRef.current && !containerRef.current.contains(e.target)) setShowSuggestions(false);
2124
+ };
2125
+ document.addEventListener("mousedown", handleClickOutside);
2126
+ return () => {
2127
+ active = false;
2128
+ document.removeEventListener("mousedown", handleClickOutside);
2129
+ };
2130
+ }, []);
2131
+ const handleSuggestionKeyDown = useCallback((e) => {
2132
+ if (!hasSuggestions) return;
2133
+ if (e.key === "ArrowDown") {
2134
+ e.preventDefault();
2135
+ setHighlightedIndex((prev) => prev < resolvedSuggestions.length - 1 ? prev + 1 : 0);
2136
+ } else if (e.key === "ArrowUp") {
2137
+ e.preventDefault();
2138
+ setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
2139
+ }
2140
+ }, [hasSuggestions, resolvedSuggestions.length]);
1805
2141
  return /* @__PURE__ */ jsxs("div", {
1806
2142
  className: `flex flex-col gap-2 ${width === "full" ? "w-full" : width === "half" ? "w-1/2" : "w-1/3"}`,
1807
2143
  children: [label && /* @__PURE__ */ jsxs(Label, {
@@ -1835,9 +2171,32 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
1835
2171
  const handleInputChange = (e) => {
1836
2172
  const value = e.target.value;
1837
2173
  field.onChange(value);
2174
+ lastSetValueRef.current = value;
2175
+ setInputValue(value);
2176
+ setShowSuggestions(value.length > 0);
2177
+ setHighlightedIndex(-1);
2178
+ };
2179
+ const applySuggestion = (suggestion) => {
2180
+ field.onChange(suggestion.value);
2181
+ onSuggestionSelect?.(suggestion);
2182
+ lastSetValueRef.current = suggestion.value;
2183
+ setInputValue(suggestion.value);
2184
+ setShowSuggestions(false);
2185
+ setHighlightedIndex(-1);
1838
2186
  };
1839
2187
  const handleKeyDown = (e) => {
1840
- if (e.key === "Escape") handleEscapeKey(field.onChange, field.value)(e);
2188
+ if (hasSuggestions && e.key === "Enter" && highlightedIndex >= 0) {
2189
+ e.preventDefault();
2190
+ applySuggestion(resolvedSuggestions[highlightedIndex]);
2191
+ return;
2192
+ }
2193
+ if (e.key === "Escape") {
2194
+ if (hasSuggestions) {
2195
+ setShowSuggestions(false);
2196
+ return;
2197
+ }
2198
+ handleEscapeKey(field.onChange, field.value)(e);
2199
+ }
1841
2200
  };
1842
2201
  const accessibilityProps = getAccessibilityProps({
1843
2202
  id,
@@ -1846,18 +2205,50 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
1846
2205
  hasHelperText: !!helperText
1847
2206
  });
1848
2207
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1849
- /* @__PURE__ */ jsx(Input, {
1850
- ...field,
1851
- id,
1852
- placeholder: placeholder || "0x...",
1853
- className: validationClasses,
1854
- onChange: handleInputChange,
1855
- onKeyDown: handleKeyDown,
1856
- "data-slot": "input",
1857
- value: field.value ?? "",
1858
- ...accessibilityProps,
1859
- "aria-describedby": `${helperText ? descriptionId : ""} ${hasError ? errorId : ""}`,
1860
- disabled: readOnly
2208
+ /* @__PURE__ */ jsxs("div", {
2209
+ ref: containerRef,
2210
+ className: "relative",
2211
+ onKeyDown: handleSuggestionKeyDown,
2212
+ children: [/* @__PURE__ */ jsx(Input, {
2213
+ ...field,
2214
+ id,
2215
+ placeholder: placeholder || "0x...",
2216
+ className: validationClasses,
2217
+ onChange: handleInputChange,
2218
+ onKeyDown: handleKeyDown,
2219
+ "data-slot": "input",
2220
+ value: field.value ?? "",
2221
+ ...accessibilityProps,
2222
+ "aria-describedby": `${helperText ? descriptionId : ""} ${hasError ? errorId : ""}`,
2223
+ "aria-expanded": hasSuggestions,
2224
+ "aria-autocomplete": suggestionsDisabled ? void 0 : "list",
2225
+ "aria-controls": hasSuggestions ? `${id}-suggestions` : void 0,
2226
+ "aria-activedescendant": hasSuggestions && highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : void 0,
2227
+ disabled: readOnly
2228
+ }), hasSuggestions && /* @__PURE__ */ jsx("div", {
2229
+ id: `${id}-suggestions`,
2230
+ className: cn("absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md", "max-h-48 overflow-auto"),
2231
+ role: "listbox",
2232
+ children: resolvedSuggestions.map((s, i) => /* @__PURE__ */ jsxs("button", {
2233
+ id: `${id}-suggestion-${i}`,
2234
+ type: "button",
2235
+ role: "option",
2236
+ "aria-selected": i === highlightedIndex,
2237
+ className: cn("flex w-full flex-col px-3 py-2 text-left text-sm", "hover:bg-accent", i === highlightedIndex && "bg-accent"),
2238
+ onMouseDown: (e) => {
2239
+ e.preventDefault();
2240
+ applySuggestion(s);
2241
+ },
2242
+ onMouseEnter: () => setHighlightedIndex(i),
2243
+ children: [/* @__PURE__ */ jsx("span", {
2244
+ className: "font-medium",
2245
+ children: s.label
2246
+ }), /* @__PURE__ */ jsx("span", {
2247
+ className: "truncate font-mono text-xs text-muted-foreground",
2248
+ children: s.value
2249
+ })]
2250
+ }, `${s.value}-${s.description ?? i}`))
2251
+ })]
1861
2252
  }),
1862
2253
  helperText && /* @__PURE__ */ jsx("div", {
1863
2254
  id: descriptionId,
@@ -1876,6 +2267,90 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
1876
2267
  }
1877
2268
  AddressField.displayName = "AddressField";
1878
2269
 
2270
+ //#endregion
2271
+ //#region src/components/fields/address-suggestion/address-suggestion-context.tsx
2272
+ /**
2273
+ * Address Suggestion Context
2274
+ *
2275
+ * Provides a React context for resolving address autocomplete suggestions.
2276
+ * When an `AddressSuggestionProvider` is mounted, every `AddressField`
2277
+ * in the subtree automatically renders a suggestion dropdown as the user types.
2278
+ *
2279
+ * @example
2280
+ * ```tsx
2281
+ * import { AddressSuggestionProvider } from '@openzeppelin/ui-components';
2282
+ * import { useAliasSuggestionResolver } from '@openzeppelin/ui-storage';
2283
+ *
2284
+ * function App() {
2285
+ * const resolver = useAliasSuggestionResolver(db);
2286
+ * return (
2287
+ * <AddressSuggestionProvider {...resolver}>
2288
+ * <MyApp />
2289
+ * </AddressSuggestionProvider>
2290
+ * );
2291
+ * }
2292
+ * ```
2293
+ */
2294
+ /**
2295
+ * Provides address suggestion resolution to all `AddressField` instances in the
2296
+ * subtree. Wrap your application (or a subsection) with this provider and
2297
+ * supply a `resolveSuggestions` function.
2298
+ *
2299
+ * @param props - Resolver function and children
2300
+ *
2301
+ * @example
2302
+ * ```tsx
2303
+ * <AddressSuggestionProvider
2304
+ * resolveSuggestions={(query, networkId) => filterAliases(query, networkId)}
2305
+ * >
2306
+ * <App />
2307
+ * </AddressSuggestionProvider>
2308
+ * ```
2309
+ */
2310
+ function AddressSuggestionProvider({ children, resolveSuggestions }) {
2311
+ const value = React$1.useMemo(() => ({ resolveSuggestions }), [resolveSuggestions]);
2312
+ return /* @__PURE__ */ jsx(AddressSuggestionContext.Provider, {
2313
+ value,
2314
+ children
2315
+ });
2316
+ }
2317
+
2318
+ //#endregion
2319
+ //#region src/components/fields/address-suggestion/useAddressSuggestions.ts
2320
+ /**
2321
+ * Convenience hook that resolves suggestions for a query string using the
2322
+ * nearest `AddressSuggestionProvider`. Returns an empty array when no provider
2323
+ * is mounted or when the query is empty.
2324
+ *
2325
+ * @param query - Current input value to match against
2326
+ * @param networkId - Optional network identifier for scoping results
2327
+ * @returns Object containing the resolved suggestions array
2328
+ *
2329
+ * @example
2330
+ * ```tsx
2331
+ * function MyField({ query }: { query: string }) {
2332
+ * const { suggestions } = useAddressSuggestions(query, 'ethereum-mainnet');
2333
+ * return (
2334
+ * <ul>
2335
+ * {suggestions.map(s => <li key={s.value}>{s.label}</li>)}
2336
+ * </ul>
2337
+ * );
2338
+ * }
2339
+ * ```
2340
+ */
2341
+ /** Resolves address suggestions from the nearest `AddressSuggestionProvider`. */
2342
+ function useAddressSuggestions(query, networkId) {
2343
+ const resolver = React$1.useContext(AddressSuggestionContext);
2344
+ return { suggestions: React$1.useMemo(() => {
2345
+ if (!resolver || !query.trim()) return [];
2346
+ return resolver.resolveSuggestions(query, networkId);
2347
+ }, [
2348
+ resolver,
2349
+ query,
2350
+ networkId
2351
+ ]) };
2352
+ }
2353
+
1879
2354
  //#endregion
1880
2355
  //#region src/components/fields/AmountField.tsx
1881
2356
  /**
@@ -5245,5 +5720,5 @@ const Toaster = ({ ...props }) => {
5245
5720
  };
5246
5721
 
5247
5722
  //#endregion
5248
- export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AddressDisplay, AddressField, Alert, AlertDescription, AlertTitle, AmountField, ArrayField, ArrayObjectField, Banner, BaseField, BigIntField, BooleanField, Button, BytesField, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, 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, NetworkErrorNotificationProvider, NetworkIcon, NetworkSelector, NetworkServiceErrorBanner, NetworkStatusBadge, NumberField, ObjectField, 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, ViewContractStateButton, buttonVariants, computeChildTouched, createFocusManager, createValidationResult, formatValidationError, getAccessibilityProps, getDescribedById, getErrorMessage, getValidationStateClasses, getWidthClasses, handleEscapeKey, handleKeyboardEvent, handleNumericKeys, handleToggleKeys, handleValidationError, hasFieldError, isDuplicateMapKey, useDuplicateKeyIndexes, useMapFieldSync, useNetworkErrorAwareAdapter, useNetworkErrorReporter, useNetworkErrors, validateField, validateMapEntries, validateMapStructure };
5723
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AddressDisplay, AddressField, AddressLabelProvider, AddressSuggestionProvider, Alert, AlertDescription, AlertTitle, AmountField, ArrayField, ArrayObjectField, Banner, BaseField, BigIntField, BooleanField, Button, BytesField, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, 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, 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, ViewContractStateButton, buttonVariants, computeChildTouched, createFocusManager, createValidationResult, formatValidationError, getAccessibilityProps, getDescribedById, getErrorMessage, getValidationStateClasses, getWidthClasses, handleEscapeKey, handleKeyboardEvent, handleNumericKeys, handleToggleKeys, handleValidationError, hasFieldError, isDuplicateMapKey, useAddressLabel, useAddressSuggestions, useDuplicateKeyIndexes, useMapFieldSync, useNetworkErrorAwareAdapter, useNetworkErrorReporter, useNetworkErrors, validateField, validateMapEntries, validateMapStructure };
5249
5724
  //# sourceMappingURL=index.mjs.map