@openzeppelin/ui-components 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -115,11 +115,58 @@ const AccordionContent = react.forwardRef(({ className, children, variant: varia
115
115
  AccordionContent.displayName = "AccordionContent";
116
116
 
117
117
  //#endregion
118
- //#region src/components/ui/address-display.tsx
119
- /** Displays a blockchain address with optional truncation, copy button, and explorer link. */
120
- function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, className, ...props }) {
118
+ //#region src/components/ui/address-display/context.ts
119
+ /**
120
+ * @internal Shared context instance consumed by both AddressDisplay and
121
+ * AddressLabelProvider. Kept in its own file so component files export
122
+ * only components (required by React Fast Refresh).
123
+ */
124
+ const AddressLabelContext = (0, react.createContext)(null);
125
+
126
+ //#endregion
127
+ //#region src/components/ui/address-display/address-display.tsx
128
+ /**
129
+ * Displays a blockchain address with optional truncation, copy button,
130
+ * explorer link, and human-readable label.
131
+ *
132
+ * Labels are resolved in priority order:
133
+ * 1. Explicit `label` prop
134
+ * 2. `AddressLabelContext` resolver (via `AddressLabelProvider`)
135
+ * 3. No label (renders address only, identical to previous behavior)
136
+ *
137
+ * Pass `disableLabel` to suppress context-based resolution (e.g. when the
138
+ * surrounding UI already shows a name, such as a contract selector).
139
+ *
140
+ * @example
141
+ * ```tsx
142
+ * // Basic usage (unchanged)
143
+ * <AddressDisplay address="0x742d35Cc..." showCopyButton />
144
+ *
145
+ * // Explicit label
146
+ * <AddressDisplay address="0x742d35Cc..." label="Treasury" />
147
+ *
148
+ * // Auto-resolved via context (no changes needed at call site)
149
+ * <AddressLabelProvider resolveLabel={myResolver}>
150
+ * <AddressDisplay address="0x742d35Cc..." />
151
+ * </AddressLabelProvider>
152
+ *
153
+ * // Suppress label resolution for a specific instance
154
+ * <AddressDisplay address="0x742d35Cc..." disableLabel />
155
+ * ```
156
+ */
157
+ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, label: labelProp, onLabelEdit: onLabelEditProp, networkId, disableLabel = false, className, ...props }) {
121
158
  const [copied, setCopied] = react.useState(false);
122
159
  const copyTimeoutRef = react.useRef(null);
160
+ const resolver = react.useContext(AddressLabelContext);
161
+ const resolvedLabel = disableLabel ? void 0 : labelProp ?? resolver?.resolveLabel(address, networkId);
162
+ const contextEditHandler = react.useCallback(() => {
163
+ resolver?.onEditLabel?.(address, networkId);
164
+ }, [
165
+ resolver,
166
+ address,
167
+ networkId
168
+ ]);
169
+ const editHandler = disableLabel ? void 0 : onLabelEditProp ?? (resolver?.onEditLabel ? contextEditHandler : void 0);
123
170
  const displayAddress = truncate ? (0, _openzeppelin_ui_utils.truncateMiddle)(address, startChars, endChars) : address;
124
171
  const handleCopy = (e) => {
125
172
  e.stopPropagation();
@@ -136,11 +183,7 @@ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4
136
183
  if (copyTimeoutRef.current) window.clearTimeout(copyTimeoutRef.current);
137
184
  };
138
185
  }, []);
139
- const addressContent = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
140
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
141
- className: (0, _openzeppelin_ui_utils.cn)("truncate", truncate ? "" : "break-all"),
142
- children: displayAddress
143
- }),
186
+ const actionButtons = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
144
187
  showCopyButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
145
188
  type: "button",
146
189
  onClick: handleCopy,
@@ -155,15 +198,136 @@ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4
155
198
  className: "ml-1.5 shrink-0 text-slate-500 transition-colors hover:text-slate-700",
156
199
  "aria-label": "View in explorer",
157
200
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ExternalLink, { className: "h-3.5 w-3.5" })
201
+ }),
202
+ editHandler && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
203
+ type: "button",
204
+ onClick: (e) => {
205
+ e.stopPropagation();
206
+ editHandler();
207
+ },
208
+ 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",
209
+ "aria-label": "Edit label",
210
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Pencil, { className: "h-3.5 w-3.5" })
158
211
  })
159
212
  ] });
160
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
213
+ if (resolvedLabel) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
214
+ className: (0, _openzeppelin_ui_utils.cn)("group inline-flex max-w-full flex-col rounded-md bg-slate-100 px-2 py-1", "text-xs text-slate-700", className),
215
+ ...props,
216
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
217
+ className: "truncate font-sans font-medium text-slate-900 leading-snug",
218
+ children: resolvedLabel
219
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
220
+ className: "flex items-center font-mono text-[10px] text-slate-400 leading-snug",
221
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
222
+ className: (0, _openzeppelin_ui_utils.cn)("truncate", truncate ? "" : "break-all"),
223
+ children: displayAddress
224
+ }), actionButtons]
225
+ })]
226
+ });
227
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
161
228
  className: (0, _openzeppelin_ui_utils.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),
162
229
  ...props,
163
- children: addressContent
230
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
231
+ className: (0, _openzeppelin_ui_utils.cn)("truncate", truncate ? "" : "break-all"),
232
+ children: displayAddress
233
+ }), actionButtons]
234
+ });
235
+ }
236
+
237
+ //#endregion
238
+ //#region src/components/ui/address-display/address-label-context.tsx
239
+ /**
240
+ * Address Label Context
241
+ *
242
+ * Provides a React context for resolving human-readable labels for blockchain
243
+ * addresses. When an `AddressLabelProvider` is mounted, every `AddressDisplay`
244
+ * in the subtree automatically resolves and renders labels without any
245
+ * call-site changes.
246
+ *
247
+ * @example
248
+ * ```tsx
249
+ * import { AddressLabelProvider } from '@openzeppelin/ui-components';
250
+ *
251
+ * function App() {
252
+ * const resolver = useAliasLabelResolver(db);
253
+ * return (
254
+ * <AddressLabelProvider {...resolver}>
255
+ * <MyApp />
256
+ * </AddressLabelProvider>
257
+ * );
258
+ * }
259
+ * ```
260
+ */
261
+ /**
262
+ * Provides address label resolution to all `AddressDisplay` instances in the
263
+ * subtree. Wrap your application (or a subsection) with this provider and
264
+ * supply a `resolveLabel` function.
265
+ *
266
+ * @param props - Resolver functions and children
267
+ *
268
+ * @example
269
+ * ```tsx
270
+ * <AddressLabelProvider
271
+ * resolveLabel={(addr) => addressBook.get(addr)}
272
+ * onEditLabel={(addr) => openEditor(addr)}
273
+ * >
274
+ * <App />
275
+ * </AddressLabelProvider>
276
+ * ```
277
+ */
278
+ function AddressLabelProvider({ children, resolveLabel, onEditLabel }) {
279
+ const value = react.useMemo(() => ({
280
+ resolveLabel,
281
+ onEditLabel
282
+ }), [resolveLabel, onEditLabel]);
283
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressLabelContext.Provider, {
284
+ value,
285
+ children
164
286
  });
165
287
  }
166
288
 
289
+ //#endregion
290
+ //#region src/components/ui/address-display/use-address-label.ts
291
+ /**
292
+ * Convenience hook for resolving an address label from the nearest
293
+ * `AddressLabelProvider`.
294
+ *
295
+ * Kept in its own file so that `address-label-context.tsx` exports only
296
+ * components (required by React Fast Refresh).
297
+ */
298
+ /**
299
+ * Convenience hook that resolves a label for a specific address using the
300
+ * nearest `AddressLabelProvider`. Returns `undefined` values when no provider
301
+ * is mounted.
302
+ *
303
+ * @param address - The blockchain address to resolve
304
+ * @param networkId - Optional network identifier for network-specific aliases
305
+ * @returns Resolved label and edit handler for the address
306
+ *
307
+ * @example
308
+ * ```tsx
309
+ * function MyAddress({ address }: { address: string }) {
310
+ * const { label, onEdit } = useAddressLabel(address, 'ethereum-mainnet');
311
+ * return <span>{label ?? address}</span>;
312
+ * }
313
+ * ```
314
+ */
315
+ function useAddressLabel(address, networkId) {
316
+ const resolver = react.useContext(AddressLabelContext);
317
+ const label = resolver?.resolveLabel(address, networkId);
318
+ const onEdit = react.useCallback(() => {
319
+ resolver?.onEditLabel?.(address, networkId);
320
+ }, [
321
+ resolver,
322
+ address,
323
+ networkId
324
+ ]);
325
+ return {
326
+ label,
327
+ onEdit: resolver?.onEditLabel ? onEdit : void 0
328
+ };
329
+ }
330
+
167
331
  //#endregion
168
332
  //#region src/components/ui/alert.tsx
169
333
  const alertVariants = (0, class_variance_authority.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", {
@@ -1097,10 +1261,15 @@ function NetworkIcon({ network, className, size = 16, variant = "branded" }) {
1097
1261
 
1098
1262
  //#endregion
1099
1263
  //#region src/components/ui/network-selector.tsx
1100
- /** Searchable dropdown selector for blockchain networks with optional grouping. */
1101
- function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetworkLabel, getNetworkIcon, getNetworkType, getNetworkId, groupByEcosystem = false, getEcosystem, filterNetwork, className, placeholder = "Select Network" }) {
1264
+ /** Searchable dropdown selector for blockchain networks with optional grouping and multi-select. */
1265
+ function NetworkSelector({ networks, getNetworkLabel, getNetworkIcon, getNetworkType, getNetworkId, groupByEcosystem = false, getEcosystem, filterNetwork, className, placeholder = "Select Network", ...modeProps }) {
1102
1266
  const [open, setOpen] = react.useState(false);
1103
1267
  const [searchQuery, setSearchQuery] = react.useState("");
1268
+ const isMultiple = modeProps.multiple === true;
1269
+ const selectedNetworkIds = isMultiple ? modeProps.selectedNetworkIds : void 0;
1270
+ const onSelectionChange = isMultiple ? modeProps.onSelectionChange : void 0;
1271
+ const selectedNetwork = !isMultiple ? modeProps.selectedNetwork : void 0;
1272
+ const onSelectNetwork = !isMultiple ? modeProps.onSelectNetwork : void 0;
1104
1273
  const filteredNetworks = react.useMemo(() => {
1105
1274
  if (!searchQuery) return networks;
1106
1275
  if (filterNetwork) return networks.filter((n) => filterNetwork(n, searchQuery));
@@ -1124,34 +1293,79 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1124
1293
  groupByEcosystem,
1125
1294
  getEcosystem
1126
1295
  ]);
1296
+ const isSelected = react.useCallback((network) => {
1297
+ if (isMultiple && selectedNetworkIds) return selectedNetworkIds.includes(getNetworkId(network));
1298
+ return selectedNetwork ? getNetworkId(selectedNetwork) === getNetworkId(network) : false;
1299
+ }, [
1300
+ isMultiple,
1301
+ selectedNetworkIds,
1302
+ selectedNetwork,
1303
+ getNetworkId
1304
+ ]);
1305
+ const handleSelect = react.useCallback((network) => {
1306
+ if (isMultiple && selectedNetworkIds && onSelectionChange) {
1307
+ const id = getNetworkId(network);
1308
+ onSelectionChange(selectedNetworkIds.includes(id) ? selectedNetworkIds.filter((x) => x !== id) : [...selectedNetworkIds, id]);
1309
+ } else if (onSelectNetwork) {
1310
+ onSelectNetwork(network);
1311
+ setOpen(false);
1312
+ }
1313
+ }, [
1314
+ isMultiple,
1315
+ selectedNetworkIds,
1316
+ onSelectionChange,
1317
+ onSelectNetwork,
1318
+ getNetworkId
1319
+ ]);
1320
+ const handleClearAll = react.useCallback(() => {
1321
+ if (isMultiple && onSelectionChange) onSelectionChange([]);
1322
+ }, [isMultiple, onSelectionChange]);
1323
+ const selectedCount = selectedNetworkIds?.length ?? 0;
1324
+ const renderTrigger = isMultiple ? modeProps.renderTrigger : void 0;
1127
1325
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenu, {
1128
1326
  open,
1129
1327
  onOpenChange: setOpen,
1130
1328
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuTrigger, {
1131
1329
  asChild: true,
1132
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
1133
- variant: "outline",
1134
- role: "combobox",
1135
- "aria-expanded": open,
1136
- className: (0, _openzeppelin_ui_utils.cn)("w-full justify-between", className),
1137
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1138
- className: "flex items-center gap-2 truncate",
1139
- children: selectedNetwork ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1140
- getNetworkIcon?.(selectedNetwork),
1141
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1142
- className: "truncate",
1143
- children: getNetworkLabel(selectedNetwork)
1144
- }),
1145
- getNetworkType && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1146
- className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase text-muted-foreground",
1147
- children: getNetworkType(selectedNetwork)
1330
+ children: (() => {
1331
+ if (isMultiple && renderTrigger) return renderTrigger({
1332
+ selectedCount,
1333
+ open
1334
+ });
1335
+ if (isMultiple) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
1336
+ variant: "outline",
1337
+ role: "combobox",
1338
+ "aria-expanded": open,
1339
+ className: (0, _openzeppelin_ui_utils.cn)("w-full justify-between", className),
1340
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1341
+ className: "truncate text-muted-foreground",
1342
+ children: selectedCount > 0 ? `${selectedCount} network${selectedCount > 1 ? "s" : ""} selected` : placeholder
1343
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
1344
+ });
1345
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
1346
+ variant: "outline",
1347
+ role: "combobox",
1348
+ "aria-expanded": open,
1349
+ className: (0, _openzeppelin_ui_utils.cn)("w-full justify-between", className),
1350
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1351
+ className: "flex items-center gap-2 truncate",
1352
+ children: selectedNetwork ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1353
+ getNetworkIcon?.(selectedNetwork),
1354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1355
+ className: "truncate",
1356
+ children: getNetworkLabel(selectedNetwork)
1357
+ }),
1358
+ getNetworkType && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1359
+ className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase text-muted-foreground",
1360
+ children: getNetworkType(selectedNetwork)
1361
+ })
1362
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1363
+ className: "text-muted-foreground",
1364
+ children: placeholder
1148
1365
  })
1149
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1150
- className: "text-muted-foreground",
1151
- children: placeholder
1152
- })
1153
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
1154
- })
1366
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
1367
+ });
1368
+ })()
1155
1369
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenuContent, {
1156
1370
  className: "w-[--radix-dropdown-menu-trigger-width] min-w-[240px] p-0",
1157
1371
  align: "start",
@@ -1167,9 +1381,19 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1167
1381
  className: "h-9 w-full border-0 bg-transparent p-0 placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0",
1168
1382
  "aria-label": "Search networks"
1169
1383
  })]
1170
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1384
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1171
1385
  className: "max-h-[300px] overflow-y-auto p-1",
1172
- children: Object.entries(groupedNetworks).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1386
+ children: [isMultiple && selectedCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1387
+ className: "flex items-center justify-between px-2 py-1.5",
1388
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1389
+ className: "text-xs font-medium text-muted-foreground",
1390
+ children: [selectedCount, " selected"]
1391
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1392
+ onClick: handleClearAll,
1393
+ className: "text-xs text-muted-foreground hover:text-foreground",
1394
+ children: "Clear all"
1395
+ })]
1396
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuSeparator, {})] }), Object.entries(groupedNetworks).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1173
1397
  className: "py-6 text-center text-sm text-muted-foreground",
1174
1398
  children: "No network found."
1175
1399
  }) : Object.entries(groupedNetworks).map(([group, groupNetworks], index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
@@ -1178,12 +1402,16 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1178
1402
  children: group
1179
1403
  }),
1180
1404
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuGroup, { children: groupNetworks.map((network) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenuItem, {
1181
- onSelect: () => {
1182
- onSelectNetwork(network);
1183
- setOpen(false);
1405
+ onSelect: (e) => {
1406
+ if (isMultiple) e.preventDefault();
1407
+ handleSelect(network);
1184
1408
  },
1185
1409
  className: "gap-2",
1186
1410
  children: [
1411
+ isMultiple ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1412
+ className: "flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border border-primary",
1413
+ children: isSelected(network) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Check, { className: "h-3 w-3" })
1414
+ }) : null,
1187
1415
  getNetworkIcon?.(network),
1188
1416
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1189
1417
  className: "flex flex-1 items-center gap-2 min-w-0",
@@ -1195,11 +1423,11 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1195
1423
  children: getNetworkType(network)
1196
1424
  })]
1197
1425
  }),
1198
- selectedNetwork && getNetworkId(selectedNetwork) === getNetworkId(network) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Check, { className: "h-4 w-4 opacity-100" })
1426
+ !isMultiple && isSelected(network) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Check, { className: "h-4 w-4 opacity-100" })
1199
1427
  ]
1200
1428
  }, getNetworkId(network))) }),
1201
1429
  index < Object.keys(groupedNetworks).length - 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuSeparator, {})
1202
- ] }, group))
1430
+ ] }, group))]
1203
1431
  })]
1204
1432
  })]
1205
1433
  });
@@ -1232,6 +1460,32 @@ function NetworkStatusBadge({ network, className }) {
1232
1460
  });
1233
1461
  }
1234
1462
 
1463
+ //#endregion
1464
+ //#region src/components/ui/overflow-menu.tsx
1465
+ /** Compact "..." dropdown menu for secondary actions. */
1466
+ function OverflowMenu({ items, align = "end", className, "aria-label": ariaLabel = "More actions" }) {
1467
+ if (items.length === 0) return null;
1468
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuTrigger, {
1469
+ asChild: true,
1470
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1471
+ variant: "ghost",
1472
+ size: "icon",
1473
+ className: (0, _openzeppelin_ui_utils.cn)("h-8 w-8", className),
1474
+ "aria-label": ariaLabel,
1475
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.MoreHorizontal, { className: "h-4 w-4" })
1476
+ })
1477
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuContent, {
1478
+ align,
1479
+ className: "min-w-[140px]",
1480
+ children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [item.destructive && index > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuSeparator, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenuItem, {
1481
+ onClick: item.onSelect,
1482
+ disabled: item.disabled,
1483
+ className: (0, _openzeppelin_ui_utils.cn)(item.destructive && "text-destructive focus:text-destructive"),
1484
+ children: [item.icon, item.label]
1485
+ })] }, item.id))
1486
+ })] });
1487
+ }
1488
+
1235
1489
  //#endregion
1236
1490
  //#region src/components/ui/progress.tsx
1237
1491
  const Progress = react.forwardRef(({ className, value, ...props }, ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_progress.Root, {
@@ -1351,12 +1605,12 @@ SelectSeparator.displayName = _radix_ui_react_select.Separator.displayName;
1351
1605
  * Can render as a button or anchor element depending on whether href is provided.
1352
1606
  */
1353
1607
  function SidebarButton({ icon, children, onClick, size = "default", badge, disabled = false, isSelected = false, href, target, rel, className }) {
1354
- const commonClass = (0, _openzeppelin_ui_utils.cn)("group relative flex items-center gap-2 px-3 py-2.5 rounded-lg font-semibold text-sm transition-colors", badge ? "justify-between" : "justify-start", disabled ? "text-gray-400 cursor-not-allowed" : isSelected ? "text-[#111928] bg-neutral-100" : "text-gray-600 hover:text-gray-700 cursor-pointer hover:before:content-[\"\"] hover:before:absolute hover:before:inset-x-0 hover:before:top-1 hover:before:bottom-1 hover:before:bg-muted/80 hover:before:rounded-lg hover:before:-z-10", size === "small" ? "h-10" : "h-11", className);
1608
+ const commonClass = (0, _openzeppelin_ui_utils.cn)("group relative flex flex-wrap items-center gap-x-2 gap-y-0.5 px-3 py-2 rounded-lg font-semibold text-sm transition-colors", badge ? "justify-between" : "justify-start", disabled ? "text-gray-400 cursor-not-allowed" : isSelected ? "text-[#111928] bg-neutral-100" : "text-gray-600 hover:text-gray-700 cursor-pointer hover:before:content-[\"\"] hover:before:absolute hover:before:inset-x-0 hover:before:top-1 hover:before:bottom-1 hover:before:bg-muted/80 hover:before:rounded-lg hover:before:-z-10", size === "small" ? "min-h-10" : "min-h-11", className);
1355
1609
  const content = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1356
1610
  className: "flex items-center gap-2",
1357
1611
  children: [icon, children]
1358
1612
  }), badge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1359
- className: "text-xs px-2 py-1 bg-muted text-muted-foreground rounded-full font-medium",
1613
+ className: "text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded-full font-medium",
1360
1614
  children: badge
1361
1615
  })] });
1362
1616
  if (href) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
@@ -1601,6 +1855,555 @@ function ViewContractStateButton({ contractAddress, onToggle }) {
1601
1855
  });
1602
1856
  }
1603
1857
 
1858
+ //#endregion
1859
+ //#region src/components/ui/wizard/WizardStepper.tsx
1860
+ function resolveState(step, index, currentStepIndex, furthestStepIndex) {
1861
+ if (step.status === "completed" || step.status === "skipped") return "completed";
1862
+ if (index === currentStepIndex) return "current";
1863
+ if (step.isInvalid && (index < currentStepIndex || index <= furthestStepIndex)) return "invalid";
1864
+ if (index < currentStepIndex) return "completed";
1865
+ if (index <= furthestStepIndex) return "visited";
1866
+ return "upcoming";
1867
+ }
1868
+ function canClick(state, freeNavigation = false) {
1869
+ if (freeNavigation) return true;
1870
+ return state !== "upcoming";
1871
+ }
1872
+ function StepCircle({ state, index }) {
1873
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1874
+ className: (0, _openzeppelin_ui_utils.cn)("flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold transition-all", state === "completed" && "bg-blue-600 text-white", state === "current" && "bg-blue-600 text-white ring-2 ring-blue-200", state === "visited" && "bg-blue-100 text-blue-600 ring-1 ring-blue-300", state === "invalid" && "bg-red-100 text-red-600 ring-1 ring-red-300", state === "upcoming" && "bg-zinc-100 text-zinc-400"),
1875
+ children: state === "completed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Check, { className: "size-3.5" }) : state === "visited" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Pencil, { className: "size-3" }) : state === "invalid" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertCircle, { className: "size-3.5" }) : index + 1
1876
+ });
1877
+ }
1878
+ function StepLabel({ title, state, isSkipped }) {
1879
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1880
+ className: "min-w-0 flex-1",
1881
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1882
+ className: (0, _openzeppelin_ui_utils.cn)("text-sm font-medium transition-colors", state === "current" && "text-blue-700", state === "completed" && "text-zinc-700", state === "visited" && "text-blue-600", state === "invalid" && "text-red-600", state === "upcoming" && "text-zinc-400"),
1883
+ children: title
1884
+ }), isSkipped && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1885
+ className: "mt-0.5 block text-[11px] text-zinc-400",
1886
+ children: "Skipped"
1887
+ })]
1888
+ });
1889
+ }
1890
+ function VerticalStepper({ steps, currentStepIndex, furthestStepIndex = currentStepIndex, onStepClick, freeNavigation, className }) {
1891
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
1892
+ "aria-label": "Wizard steps",
1893
+ className: (0, _openzeppelin_ui_utils.cn)("rounded-2xl border border-zinc-200 bg-white p-6", className),
1894
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1895
+ className: "flex flex-col gap-1",
1896
+ children: steps.map((step, index) => {
1897
+ const state = resolveState(step, index, currentStepIndex, furthestStepIndex);
1898
+ const clickable = canClick(state, freeNavigation) && !!onStepClick;
1899
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1900
+ type: "button",
1901
+ onClick: () => clickable && onStepClick?.(index),
1902
+ disabled: !clickable,
1903
+ className: (0, _openzeppelin_ui_utils.cn)("flex items-center gap-3 rounded-xl border border-transparent px-3 py-3 text-left transition-all duration-150", clickable ? "cursor-pointer" : "cursor-not-allowed opacity-50", state === "current" && "border-blue-200 bg-blue-50", state === "completed" && "bg-white hover:bg-gray-50", state === "visited" && "bg-white hover:bg-blue-50/50", state === "invalid" && "border-red-200 bg-red-50 hover:bg-red-100/60", state === "upcoming" && "bg-white"),
1904
+ "aria-current": state === "current" ? "step" : void 0,
1905
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepCircle, {
1906
+ state,
1907
+ index
1908
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, {
1909
+ title: step.title,
1910
+ state,
1911
+ isSkipped: step.status === "skipped"
1912
+ })]
1913
+ }, step.id);
1914
+ })
1915
+ })
1916
+ });
1917
+ }
1918
+ function HorizontalStepper({ steps, currentStepIndex, furthestStepIndex = currentStepIndex, onStepClick, freeNavigation, className }) {
1919
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
1920
+ "aria-label": "Wizard steps",
1921
+ className: (0, _openzeppelin_ui_utils.cn)("rounded-2xl border border-zinc-200 bg-white p-6", className),
1922
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1923
+ className: "flex w-full items-center",
1924
+ children: steps.map((step, index) => {
1925
+ const state = resolveState(step, index, currentStepIndex, furthestStepIndex);
1926
+ const clickable = canClick(state, freeNavigation) && !!onStepClick;
1927
+ const isLast = index === steps.length - 1;
1928
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.default.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1929
+ type: "button",
1930
+ onClick: () => clickable && onStepClick?.(index),
1931
+ disabled: !clickable,
1932
+ className: (0, _openzeppelin_ui_utils.cn)("flex items-center gap-2 rounded-xl border border-transparent px-3 py-2 text-left transition-all duration-150", clickable ? "cursor-pointer" : "cursor-not-allowed opacity-50", state === "current" && "border-blue-200 bg-blue-50", state === "completed" && "bg-white hover:bg-gray-50", state === "visited" && "bg-white hover:bg-blue-50/50", state === "invalid" && "border-red-200 bg-red-50 hover:bg-red-100/60", state === "upcoming" && "bg-white"),
1933
+ "aria-current": state === "current" ? "step" : void 0,
1934
+ "aria-label": `Step ${index + 1}: ${step.title}`,
1935
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepCircle, {
1936
+ state,
1937
+ index
1938
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1939
+ className: "hidden sm:block",
1940
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, {
1941
+ title: step.title,
1942
+ state,
1943
+ isSkipped: step.status === "skipped"
1944
+ })
1945
+ })]
1946
+ }), !isLast && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: (0, _openzeppelin_ui_utils.cn)("mx-1 h-px flex-1 transition-colors sm:mx-2", index < currentStepIndex ? "bg-blue-600" : "bg-zinc-200") })] }, step.id);
1947
+ })
1948
+ })
1949
+ });
1950
+ }
1951
+ /**
1952
+ * A stepper component for navigating through a series of steps.
1953
+ *
1954
+ * @param props - The props for the WizardStepper component.
1955
+ * @returns A React node representing the stepper component.
1956
+ */
1957
+ function WizardStepper(props) {
1958
+ const { variant = "horizontal", ...rest } = props;
1959
+ return variant === "vertical" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VerticalStepper, {
1960
+ ...rest,
1961
+ variant
1962
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HorizontalStepper, {
1963
+ ...rest,
1964
+ variant
1965
+ });
1966
+ }
1967
+
1968
+ //#endregion
1969
+ //#region src/components/ui/wizard/WizardNavigation.tsx
1970
+ /**
1971
+ * A navigation component for the wizard.
1972
+ *
1973
+ * @param props - The props for the WizardNavigation component.
1974
+ * @returns A React node representing the navigation component.
1975
+ */
1976
+ function WizardNavigation({ isFirstStep, isLastStep, canProceed = true, onPrevious, onNext, onCancel, extraActions, nextLabel = "Next", lastStepLabel = "Finish", className }) {
1977
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1978
+ className: (0, _openzeppelin_ui_utils.cn)("flex items-center justify-between", className),
1979
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1980
+ className: "flex gap-2",
1981
+ children: [onCancel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
1982
+ type: "button",
1983
+ variant: "outline",
1984
+ onClick: onCancel,
1985
+ className: "gap-2",
1986
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "size-4" }), "Cancel"]
1987
+ }), !isFirstStep && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
1988
+ type: "button",
1989
+ variant: "outline",
1990
+ onClick: onPrevious,
1991
+ className: "gap-2",
1992
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronLeft, { className: "size-4" }), "Previous"]
1993
+ })]
1994
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1995
+ className: "flex gap-2",
1996
+ children: [extraActions, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
1997
+ type: "button",
1998
+ onClick: onNext,
1999
+ disabled: !canProceed,
2000
+ className: "gap-2",
2001
+ children: [isLastStep ? lastStepLabel : nextLabel, !isLastStep && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, { className: "size-4" })]
2002
+ })]
2003
+ })]
2004
+ });
2005
+ }
2006
+
2007
+ //#endregion
2008
+ //#region src/components/ui/wizard/hooks.ts
2009
+ /**
2010
+ * Clamp a step index into the valid range for the current wizard.
2011
+ */
2012
+ function getSafeStepIndex(stepCount, currentStepIndex) {
2013
+ if (stepCount === 0) return 0;
2014
+ return Math.max(0, Math.min(currentStepIndex, stepCount - 1));
2015
+ }
2016
+ /**
2017
+ * Track the highest step reached unless a controlled value is provided.
2018
+ */
2019
+ function useFurthestStepIndex(currentStepIndex, controlledFurthestStepIndex) {
2020
+ const [internalFurthestStepIndex, setInternalFurthestStepIndex] = (0, react.useState)(currentStepIndex);
2021
+ (0, react.useEffect)(() => {
2022
+ setInternalFurthestStepIndex((prev) => Math.max(prev, currentStepIndex));
2023
+ }, [currentStepIndex]);
2024
+ return controlledFurthestStepIndex ?? internalFurthestStepIndex;
2025
+ }
2026
+ /**
2027
+ * Keep the scrollable wizard's active and visited step state in sync with scrolling and clicks.
2028
+ */
2029
+ function useScrollableWizardStepTracking({ steps, currentStepIndex, onStepChange, scrollRef, sectionId, scrollPadding = SCROLL_PADDING_PX }) {
2030
+ const safeIndex = getSafeStepIndex(steps.length, currentStepIndex);
2031
+ const initialIndexRef = (0, react.useRef)(safeIndex);
2032
+ const rafRef = (0, react.useRef)(null);
2033
+ const manualSelectionIndexRef = (0, react.useRef)(null);
2034
+ const stepsRef = (0, react.useRef)(steps);
2035
+ const sectionIdRef = (0, react.useRef)(sectionId);
2036
+ const onStepChangeRef = (0, react.useRef)(onStepChange);
2037
+ const scrollPaddingRef = (0, react.useRef)(scrollPadding);
2038
+ (0, react.useEffect)(() => {
2039
+ stepsRef.current = steps;
2040
+ sectionIdRef.current = sectionId;
2041
+ onStepChangeRef.current = onStepChange;
2042
+ scrollPaddingRef.current = scrollPadding;
2043
+ });
2044
+ const [activeIndex, setActiveIndex] = (0, react.useState)(initialIndexRef.current);
2045
+ const activeIndexRef = (0, react.useRef)(initialIndexRef.current);
2046
+ const [furthestStepIndex, setFurthestStepIndex] = (0, react.useState)(initialIndexRef.current);
2047
+ const isMountedRef = (0, react.useRef)(false);
2048
+ const clearManualSelection = (0, react.useCallback)(() => {
2049
+ manualSelectionIndexRef.current = null;
2050
+ }, []);
2051
+ (0, react.useEffect)(() => {
2052
+ const container = scrollRef.current;
2053
+ if (!container) return;
2054
+ const ownerDocument = container.ownerDocument;
2055
+ isMountedRef.current = false;
2056
+ let didCompleteInitialRaf = false;
2057
+ const releaseManualSelectionOnUserScroll = () => {
2058
+ clearManualSelection();
2059
+ };
2060
+ const handleKeyDown = (event) => {
2061
+ if (isScrollableNavigationKey(event)) clearManualSelection();
2062
+ };
2063
+ const handleScroll = () => {
2064
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
2065
+ rafRef.current = requestAnimationFrame(() => {
2066
+ const currentSteps = stepsRef.current;
2067
+ const currentSectionId = sectionIdRef.current;
2068
+ const currentOnStepChange = onStepChangeRef.current;
2069
+ if (currentSteps.length === 0) return;
2070
+ const manualSelectionIndex = manualSelectionIndexRef.current;
2071
+ const naturalState = resolveScrollableActiveIndex(container, currentSteps, currentSectionId);
2072
+ const naturalActiveIndex = naturalState.activeIndex;
2073
+ const newActiveIndex = manualSelectionIndex ?? naturalActiveIndex;
2074
+ const shouldCommitFurthestStepIndex = manualSelectionIndex !== null ? true : naturalState.commitFurthestStepIndex;
2075
+ if (activeIndexRef.current !== newActiveIndex) {
2076
+ activeIndexRef.current = newActiveIndex;
2077
+ setActiveIndex(newActiveIndex);
2078
+ if (isMountedRef.current) {
2079
+ lastEmittedIndexRef.current = newActiveIndex;
2080
+ currentOnStepChange(newActiveIndex);
2081
+ }
2082
+ } else setActiveIndex(newActiveIndex);
2083
+ if (shouldCommitFurthestStepIndex) setFurthestStepIndex((prev) => Math.max(prev, newActiveIndex));
2084
+ rafRef.current = null;
2085
+ if (!didCompleteInitialRaf) {
2086
+ didCompleteInitialRaf = true;
2087
+ isMountedRef.current = true;
2088
+ }
2089
+ });
2090
+ };
2091
+ container.addEventListener("wheel", releaseManualSelectionOnUserScroll, { passive: true });
2092
+ container.addEventListener("touchmove", releaseManualSelectionOnUserScroll, { passive: true });
2093
+ container.addEventListener("pointerdown", releaseManualSelectionOnUserScroll);
2094
+ ownerDocument.addEventListener("keydown", handleKeyDown);
2095
+ container.addEventListener("scroll", handleScroll, { passive: true });
2096
+ handleScroll();
2097
+ return () => {
2098
+ isMountedRef.current = false;
2099
+ container.removeEventListener("wheel", releaseManualSelectionOnUserScroll);
2100
+ container.removeEventListener("touchmove", releaseManualSelectionOnUserScroll);
2101
+ container.removeEventListener("pointerdown", releaseManualSelectionOnUserScroll);
2102
+ ownerDocument.removeEventListener("keydown", handleKeyDown);
2103
+ container.removeEventListener("scroll", handleScroll);
2104
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
2105
+ };
2106
+ }, [clearManualSelection, scrollRef]);
2107
+ const lastEmittedIndexRef = (0, react.useRef)(safeIndex);
2108
+ (0, react.useEffect)(() => {
2109
+ const newSafeIndex = getSafeStepIndex(stepsRef.current.length, currentStepIndex);
2110
+ if (newSafeIndex === lastEmittedIndexRef.current) return;
2111
+ lastEmittedIndexRef.current = newSafeIndex;
2112
+ activeIndexRef.current = newSafeIndex;
2113
+ setActiveIndex(newSafeIndex);
2114
+ setFurthestStepIndex((prev) => Math.max(prev, newSafeIndex));
2115
+ const step = stepsRef.current[newSafeIndex];
2116
+ if (!step) return;
2117
+ const sectionElement = scrollRef.current?.querySelector(`#${CSS.escape(sectionIdRef.current(step.id))}`);
2118
+ if (scrollRef.current && sectionElement) scrollSectionIntoView(scrollRef.current, sectionElement, scrollPaddingRef.current);
2119
+ }, [currentStepIndex, scrollRef]);
2120
+ return {
2121
+ activeIndex,
2122
+ furthestStepIndex,
2123
+ scrollToSection: (0, react.useCallback)((index) => {
2124
+ const step = stepsRef.current[index];
2125
+ if (!step) return;
2126
+ manualSelectionIndexRef.current = index;
2127
+ activeIndexRef.current = index;
2128
+ lastEmittedIndexRef.current = index;
2129
+ setActiveIndex(index);
2130
+ setFurthestStepIndex((prev) => Math.max(prev, index));
2131
+ onStepChangeRef.current(index);
2132
+ const container = scrollRef.current;
2133
+ const sectionElement = container?.querySelector(`#${CSS.escape(sectionIdRef.current(step.id))}`);
2134
+ if (container && sectionElement) scrollSectionIntoView(container, sectionElement, scrollPaddingRef.current);
2135
+ }, [scrollRef])
2136
+ };
2137
+ }
2138
+ function resolveScrollableActiveIndex(container, steps, sectionId) {
2139
+ if (steps.length === 0) return {
2140
+ activeIndex: 0,
2141
+ commitFurthestStepIndex: false
2142
+ };
2143
+ const containerRect = container.getBoundingClientRect();
2144
+ const anchorY = containerRect.top + Math.min(containerRect.height * .35, 220);
2145
+ const isScrollable = container.scrollHeight > container.clientHeight + 1;
2146
+ const isAtBottom = isScrollable && container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
2147
+ const isNearBottom = isScrollable && container.scrollTop + container.clientHeight >= container.scrollHeight - 4;
2148
+ if (isAtBottom) return {
2149
+ activeIndex: steps.length - 1,
2150
+ commitFurthestStepIndex: false
2151
+ };
2152
+ let activeIndex = 0;
2153
+ let highestScore = Number.NEGATIVE_INFINITY;
2154
+ for (let i = 0; i < steps.length; i++) {
2155
+ const sectionMetrics = getSectionMetrics(container, steps[i].id, sectionId, containerRect);
2156
+ if (!sectionMetrics) continue;
2157
+ const score = scoreScrollableStep({
2158
+ stepIndex: i,
2159
+ stepCount: steps.length,
2160
+ containerRect,
2161
+ anchorY,
2162
+ isNearBottom,
2163
+ ...sectionMetrics
2164
+ });
2165
+ if (score >= highestScore) {
2166
+ highestScore = score;
2167
+ activeIndex = i;
2168
+ }
2169
+ }
2170
+ return {
2171
+ activeIndex,
2172
+ commitFurthestStepIndex: true
2173
+ };
2174
+ }
2175
+ const SCROLL_PADDING_PX = 32;
2176
+ function getSectionElement(container, stepId, sectionId) {
2177
+ return container.querySelector(`#${CSS.escape(sectionId(stepId))}`);
2178
+ }
2179
+ function scrollSectionIntoView(container, sectionElement, padding) {
2180
+ const elementTop = sectionElement.getBoundingClientRect().top;
2181
+ const containerTop = container.getBoundingClientRect().top;
2182
+ const targetScrollTop = container.scrollTop + (elementTop - containerTop) - padding;
2183
+ container.scrollTo({
2184
+ top: targetScrollTop,
2185
+ behavior: "smooth"
2186
+ });
2187
+ }
2188
+ function getSectionMetrics(container, stepId, sectionId, containerRect) {
2189
+ const sectionElement = getSectionElement(container, stepId, sectionId);
2190
+ if (!sectionElement) return null;
2191
+ const sectionRect = sectionElement.getBoundingClientRect();
2192
+ return {
2193
+ sectionRect,
2194
+ visibleHeight: getVisibleHeight(containerRect, sectionRect)
2195
+ };
2196
+ }
2197
+ function getVisibleHeight(containerRect, sectionRect) {
2198
+ return Math.max(0, Math.min(sectionRect.bottom, containerRect.bottom) - Math.max(sectionRect.top, containerRect.top));
2199
+ }
2200
+ function scoreScrollableStep({ stepIndex, stepCount, containerRect, sectionRect, visibleHeight, anchorY, isNearBottom }) {
2201
+ const isVisible = visibleHeight > 0;
2202
+ const focusBandTop = containerRect.top + Math.min(containerRect.height * .2, 140);
2203
+ const focusBandBottom = containerRect.top + Math.min(containerRect.height * .55, 360);
2204
+ const focusBandOverlap = getBandOverlapHeight(sectionRect, focusBandTop, focusBandBottom);
2205
+ const distanceToFocusBand = focusBandOverlap > 0 ? 0 : Math.min(Math.abs(sectionRect.top - focusBandBottom), Math.abs(sectionRect.bottom - focusBandTop));
2206
+ const lastStepProminent = stepIndex === stepCount - 1 && visibleHeight >= Math.min(sectionRect.height, containerRect.height) * .25 && sectionRect.top <= containerRect.top + containerRect.height * .65;
2207
+ let score = isVisible ? visibleHeight : Number.NEGATIVE_INFINITY;
2208
+ if (focusBandOverlap > 0) score += 12e3 + focusBandOverlap * 25;
2209
+ if (sectionRect.top <= anchorY) score += 250;
2210
+ score += Math.max(0, 1e3 - distanceToFocusBand);
2211
+ if (isNearBottom && lastStepProminent && isVisible) score += 15e3;
2212
+ return score;
2213
+ }
2214
+ function getBandOverlapHeight(sectionRect, bandTop, bandBottom) {
2215
+ return Math.max(0, Math.min(sectionRect.bottom, bandBottom) - Math.max(sectionRect.top, bandTop));
2216
+ }
2217
+ function isScrollableNavigationKey(event) {
2218
+ if (event.metaKey || event.ctrlKey || event.altKey) return false;
2219
+ return [
2220
+ "ArrowDown",
2221
+ "ArrowUp",
2222
+ "PageDown",
2223
+ "PageUp",
2224
+ "Home",
2225
+ "End",
2226
+ " "
2227
+ ].includes(event.key);
2228
+ }
2229
+
2230
+ //#endregion
2231
+ //#region src/components/ui/wizard/WizardLayout.tsx
2232
+ function PagedLayout({ steps, currentStepIndex, furthestStepIndex: furthestStepIndexProp, onStepChange, onComplete, onCancel, navActions, header, variant, className }) {
2233
+ const safeIndex = getSafeStepIndex(steps.length, currentStepIndex);
2234
+ const resolvedFurthestStepIndex = useFurthestStepIndex(safeIndex, furthestStepIndexProp);
2235
+ if (steps.length === 0) return null;
2236
+ const isFirstStep = safeIndex === 0;
2237
+ const isLastStep = safeIndex === steps.length - 1;
2238
+ const currentStep = steps[safeIndex];
2239
+ const canProceed = currentStep?.isValid !== false;
2240
+ const handleNext = () => {
2241
+ if (isLastStep) {
2242
+ onComplete?.();
2243
+ return;
2244
+ }
2245
+ onStepChange(safeIndex + 1);
2246
+ };
2247
+ const handlePrevious = () => {
2248
+ if (!isFirstStep) onStepChange(safeIndex - 1);
2249
+ };
2250
+ const stepDefs = toStepDefs(steps, safeIndex);
2251
+ const footer = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2252
+ className: "shrink-0 border-t border-border bg-background px-8 py-4",
2253
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2254
+ className: "mx-auto max-w-5xl",
2255
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WizardNavigation, {
2256
+ isFirstStep,
2257
+ isLastStep,
2258
+ canProceed,
2259
+ onPrevious: handlePrevious,
2260
+ onNext: handleNext,
2261
+ onCancel,
2262
+ extraActions: navActions
2263
+ })
2264
+ })
2265
+ });
2266
+ if (variant === "vertical") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2267
+ className: (0, _openzeppelin_ui_utils.cn)("flex h-full gap-6", className),
2268
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2269
+ className: "w-[220px] shrink-0 py-6 pl-6",
2270
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WizardStepper, {
2271
+ variant: "vertical",
2272
+ steps: stepDefs,
2273
+ currentStepIndex: safeIndex,
2274
+ furthestStepIndex: resolvedFurthestStepIndex,
2275
+ onStepClick: onStepChange,
2276
+ className: "h-full"
2277
+ })
2278
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2279
+ className: "flex min-w-0 flex-1 flex-col overflow-hidden",
2280
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2281
+ className: "flex-1 overflow-y-auto p-8",
2282
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2283
+ className: "mx-auto max-w-5xl",
2284
+ children: [header, currentStep?.component]
2285
+ })
2286
+ }), footer]
2287
+ })]
2288
+ });
2289
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2290
+ className: (0, _openzeppelin_ui_utils.cn)("flex h-full flex-col", className),
2291
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2292
+ className: "shrink-0 p-6 pb-0",
2293
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WizardStepper, {
2294
+ variant: "horizontal",
2295
+ steps: stepDefs,
2296
+ currentStepIndex: safeIndex,
2297
+ furthestStepIndex: resolvedFurthestStepIndex,
2298
+ onStepClick: onStepChange
2299
+ })
2300
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2301
+ className: "flex min-w-0 flex-1 flex-col overflow-hidden",
2302
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2303
+ className: "flex-1 overflow-y-auto p-8",
2304
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2305
+ className: "mx-auto max-w-5xl",
2306
+ children: [header, currentStep?.component]
2307
+ })
2308
+ }), footer]
2309
+ })]
2310
+ });
2311
+ }
2312
+ function ScrollableLayout({ steps, currentStepIndex, onStepChange, header, onComplete, scrollPadding, className }) {
2313
+ const instanceId = (0, react.useId)();
2314
+ const scrollRef = (0, react.useRef)(null);
2315
+ const sectionId = (0, react.useCallback)((stepId) => `wizard-section-${instanceId}-${stepId}`, [instanceId]);
2316
+ const { activeIndex, furthestStepIndex, scrollToSection } = useScrollableWizardStepTracking({
2317
+ steps,
2318
+ currentStepIndex,
2319
+ onStepChange,
2320
+ scrollRef,
2321
+ sectionId,
2322
+ scrollPadding
2323
+ });
2324
+ if (steps.length === 0) return null;
2325
+ const stepDefs = toStepDefs(steps, activeIndex);
2326
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2327
+ className: (0, _openzeppelin_ui_utils.cn)("flex h-full gap-6", className),
2328
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2329
+ className: "w-[220px] shrink-0 py-6 pl-6",
2330
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WizardStepper, {
2331
+ variant: "vertical",
2332
+ steps: stepDefs,
2333
+ currentStepIndex: activeIndex,
2334
+ furthestStepIndex,
2335
+ onStepClick: scrollToSection,
2336
+ freeNavigation: true,
2337
+ className: "h-full"
2338
+ })
2339
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2340
+ ref: scrollRef,
2341
+ className: "flex min-w-0 flex-1 flex-col overflow-y-auto p-8",
2342
+ children: [
2343
+ header,
2344
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2345
+ className: "space-y-12",
2346
+ children: steps.map((step) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
2347
+ id: sectionId(step.id),
2348
+ children: step.component
2349
+ }, step.id))
2350
+ }),
2351
+ onComplete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2352
+ className: "flex justify-end pt-8",
2353
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2354
+ type: "button",
2355
+ onClick: onComplete,
2356
+ children: "Finish"
2357
+ })
2358
+ })
2359
+ ]
2360
+ })]
2361
+ });
2362
+ }
2363
+ function toStepDefs(steps, currentStepIndex) {
2364
+ return steps.map((s, i) => {
2365
+ const isInvalid = s.isInvalid ?? s.isValid === false;
2366
+ const status = s.status ?? (i < currentStepIndex && !isInvalid ? "completed" : "pending");
2367
+ return {
2368
+ id: s.id,
2369
+ title: s.title,
2370
+ status,
2371
+ isInvalid
2372
+ };
2373
+ });
2374
+ }
2375
+ /**
2376
+ * A layout component for the wizard.
2377
+ *
2378
+ * @param props - The props for the WizardLayout component.
2379
+ * @returns A React node representing the layout component.
2380
+ */
2381
+ function WizardLayout(props) {
2382
+ const { variant = "horizontal", ...rest } = props;
2383
+ if (variant === "scrollable") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScrollableLayout, {
2384
+ steps: rest.steps,
2385
+ currentStepIndex: rest.currentStepIndex,
2386
+ onStepChange: rest.onStepChange,
2387
+ header: rest.header,
2388
+ onComplete: rest.onComplete,
2389
+ scrollPadding: rest.scrollPadding,
2390
+ className: rest.className
2391
+ });
2392
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PagedLayout, {
2393
+ ...rest,
2394
+ variant
2395
+ });
2396
+ }
2397
+
2398
+ //#endregion
2399
+ //#region src/components/fields/address-suggestion/context.ts
2400
+ /**
2401
+ * @internal Shared context instance consumed by both AddressField and
2402
+ * AddressSuggestionProvider. Kept in its own file so component files export
2403
+ * only components (required by React Fast Refresh).
2404
+ */
2405
+ const AddressSuggestionContext = (0, react.createContext)(null);
2406
+
1604
2407
  //#endregion
1605
2408
  //#region src/components/fields/utils/accessibility.ts
1606
2409
  /**
@@ -1792,6 +2595,8 @@ function getWidthClasses(width) {
1792
2595
 
1793
2596
  //#endregion
1794
2597
  //#region src/components/fields/AddressField.tsx
2598
+ const DEBOUNCE_MS = 200;
2599
+ const MAX_SUGGESTIONS = 5;
1795
2600
  /**
1796
2601
  * Address input field component specifically designed for blockchain addresses via React Hook Form integration.
1797
2602
  *
@@ -1809,11 +2614,82 @@ function getWidthClasses(width) {
1809
2614
  * - Chain-agnostic design (validation handled by adapters)
1810
2615
  * - Full accessibility support with ARIA attributes
1811
2616
  * - Keyboard navigation
2617
+ *
2618
+ * Autocomplete suggestions can be provided in two ways:
2619
+ *
2620
+ * 1. **Context-based (zero-config)**: Mount an `AddressSuggestionProvider` in the
2621
+ * component tree. Every `AddressField` below it automatically resolves suggestions.
2622
+ *
2623
+ * 2. **Prop-based (explicit)**: Pass `suggestions` directly. This overrides context.
2624
+ * Pass `suggestions={false}` to opt out when a provider is mounted.
2625
+ *
2626
+ * The suggestion dropdown includes built-in debouncing, keyboard navigation (Arrow keys,
2627
+ * Enter, Escape), click-outside dismissal, and ARIA listbox semantics.
1812
2628
  */
1813
- function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, adapter, readOnly }) {
2629
+ function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, adapter, readOnly, suggestions: suggestionsProp, onSuggestionSelect }) {
1814
2630
  const isRequired = !!validation?.required;
1815
2631
  const errorId = `${id}-error`;
1816
2632
  const descriptionId = `${id}-description`;
2633
+ const contextResolver = (0, react.useContext)(AddressSuggestionContext);
2634
+ const containerRef = (0, react.useRef)(null);
2635
+ const lastSetValueRef = (0, react.useRef)("");
2636
+ const [inputValue, setInputValue] = (0, react.useState)("");
2637
+ const [debouncedQuery, setDebouncedQuery] = (0, react.useState)("");
2638
+ const [showSuggestions, setShowSuggestions] = (0, react.useState)(false);
2639
+ const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(-1);
2640
+ const watchedFieldValue = (0, react_hook_form.useWatch)({
2641
+ control,
2642
+ name
2643
+ });
2644
+ (0, react.useEffect)(() => {
2645
+ const currentFieldValue = watchedFieldValue ?? "";
2646
+ if (currentFieldValue !== lastSetValueRef.current) {
2647
+ lastSetValueRef.current = currentFieldValue;
2648
+ setInputValue(currentFieldValue);
2649
+ }
2650
+ }, [watchedFieldValue]);
2651
+ (0, react.useEffect)(() => {
2652
+ if (!inputValue.trim()) {
2653
+ setDebouncedQuery("");
2654
+ return;
2655
+ }
2656
+ const timer = setTimeout(() => setDebouncedQuery(inputValue), DEBOUNCE_MS);
2657
+ return () => clearTimeout(timer);
2658
+ }, [inputValue]);
2659
+ const suggestionsDisabled = suggestionsProp === false;
2660
+ const resolvedSuggestions = (0, react.useMemo)(() => {
2661
+ if (suggestionsDisabled) return [];
2662
+ if (Array.isArray(suggestionsProp)) return suggestionsProp;
2663
+ if (!contextResolver || !debouncedQuery.trim()) return [];
2664
+ return contextResolver.resolveSuggestions(debouncedQuery).slice(0, MAX_SUGGESTIONS);
2665
+ }, [
2666
+ suggestionsDisabled,
2667
+ suggestionsProp,
2668
+ contextResolver,
2669
+ debouncedQuery
2670
+ ]);
2671
+ const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
2672
+ (0, react.useEffect)(() => {
2673
+ let active = true;
2674
+ const handleClickOutside = (e) => {
2675
+ if (active && containerRef.current && !containerRef.current.contains(e.target)) setShowSuggestions(false);
2676
+ };
2677
+ document.addEventListener("mousedown", handleClickOutside);
2678
+ return () => {
2679
+ active = false;
2680
+ document.removeEventListener("mousedown", handleClickOutside);
2681
+ };
2682
+ }, []);
2683
+ const handleSuggestionKeyDown = (0, react.useCallback)((e) => {
2684
+ if (!hasSuggestions) return;
2685
+ if (e.key === "ArrowDown") {
2686
+ e.preventDefault();
2687
+ setHighlightedIndex((prev) => prev < resolvedSuggestions.length - 1 ? prev + 1 : 0);
2688
+ } else if (e.key === "ArrowUp") {
2689
+ e.preventDefault();
2690
+ setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
2691
+ }
2692
+ }, [hasSuggestions, resolvedSuggestions.length]);
1817
2693
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1818
2694
  className: `flex flex-col gap-2 ${width === "full" ? "w-full" : width === "half" ? "w-1/2" : "w-1/3"}`,
1819
2695
  children: [label && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Label, {
@@ -1847,9 +2723,32 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
1847
2723
  const handleInputChange = (e) => {
1848
2724
  const value = e.target.value;
1849
2725
  field.onChange(value);
2726
+ lastSetValueRef.current = value;
2727
+ setInputValue(value);
2728
+ setShowSuggestions(value.length > 0);
2729
+ setHighlightedIndex(-1);
2730
+ };
2731
+ const applySuggestion = (suggestion) => {
2732
+ field.onChange(suggestion.value);
2733
+ onSuggestionSelect?.(suggestion);
2734
+ lastSetValueRef.current = suggestion.value;
2735
+ setInputValue(suggestion.value);
2736
+ setShowSuggestions(false);
2737
+ setHighlightedIndex(-1);
1850
2738
  };
1851
2739
  const handleKeyDown = (e) => {
1852
- if (e.key === "Escape") handleEscapeKey(field.onChange, field.value)(e);
2740
+ if (hasSuggestions && e.key === "Enter" && highlightedIndex >= 0) {
2741
+ e.preventDefault();
2742
+ applySuggestion(resolvedSuggestions[highlightedIndex]);
2743
+ return;
2744
+ }
2745
+ if (e.key === "Escape") {
2746
+ if (hasSuggestions) {
2747
+ setShowSuggestions(false);
2748
+ return;
2749
+ }
2750
+ handleEscapeKey(field.onChange, field.value)(e);
2751
+ }
1853
2752
  };
1854
2753
  const accessibilityProps = getAccessibilityProps({
1855
2754
  id,
@@ -1858,18 +2757,50 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
1858
2757
  hasHelperText: !!helperText
1859
2758
  });
1860
2759
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1861
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
1862
- ...field,
1863
- id,
1864
- placeholder: placeholder || "0x...",
1865
- className: validationClasses,
1866
- onChange: handleInputChange,
1867
- onKeyDown: handleKeyDown,
1868
- "data-slot": "input",
1869
- value: field.value ?? "",
1870
- ...accessibilityProps,
1871
- "aria-describedby": `${helperText ? descriptionId : ""} ${hasError ? errorId : ""}`,
1872
- disabled: readOnly
2760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2761
+ ref: containerRef,
2762
+ className: "relative",
2763
+ onKeyDown: handleSuggestionKeyDown,
2764
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
2765
+ ...field,
2766
+ id,
2767
+ placeholder: placeholder || "0x...",
2768
+ className: validationClasses,
2769
+ onChange: handleInputChange,
2770
+ onKeyDown: handleKeyDown,
2771
+ "data-slot": "input",
2772
+ value: field.value ?? "",
2773
+ ...accessibilityProps,
2774
+ "aria-describedby": `${helperText ? descriptionId : ""} ${hasError ? errorId : ""}`,
2775
+ "aria-expanded": hasSuggestions,
2776
+ "aria-autocomplete": suggestionsDisabled ? void 0 : "list",
2777
+ "aria-controls": hasSuggestions ? `${id}-suggestions` : void 0,
2778
+ "aria-activedescendant": hasSuggestions && highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : void 0,
2779
+ disabled: readOnly
2780
+ }), hasSuggestions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2781
+ id: `${id}-suggestions`,
2782
+ className: (0, _openzeppelin_ui_utils.cn)("absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md", "max-h-48 overflow-auto"),
2783
+ role: "listbox",
2784
+ children: resolvedSuggestions.map((s, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2785
+ id: `${id}-suggestion-${i}`,
2786
+ type: "button",
2787
+ role: "option",
2788
+ "aria-selected": i === highlightedIndex,
2789
+ className: (0, _openzeppelin_ui_utils.cn)("flex w-full flex-col px-3 py-2 text-left text-sm", "hover:bg-accent", i === highlightedIndex && "bg-accent"),
2790
+ onMouseDown: (e) => {
2791
+ e.preventDefault();
2792
+ applySuggestion(s);
2793
+ },
2794
+ onMouseEnter: () => setHighlightedIndex(i),
2795
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2796
+ className: "font-medium",
2797
+ children: s.label
2798
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2799
+ className: "truncate font-mono text-xs text-muted-foreground",
2800
+ children: s.value
2801
+ })]
2802
+ }, `${s.value}-${s.description ?? i}`))
2803
+ })]
1873
2804
  }),
1874
2805
  helperText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1875
2806
  id: descriptionId,
@@ -1888,6 +2819,90 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
1888
2819
  }
1889
2820
  AddressField.displayName = "AddressField";
1890
2821
 
2822
+ //#endregion
2823
+ //#region src/components/fields/address-suggestion/address-suggestion-context.tsx
2824
+ /**
2825
+ * Address Suggestion Context
2826
+ *
2827
+ * Provides a React context for resolving address autocomplete suggestions.
2828
+ * When an `AddressSuggestionProvider` is mounted, every `AddressField`
2829
+ * in the subtree automatically renders a suggestion dropdown as the user types.
2830
+ *
2831
+ * @example
2832
+ * ```tsx
2833
+ * import { AddressSuggestionProvider } from '@openzeppelin/ui-components';
2834
+ * import { useAliasSuggestionResolver } from '@openzeppelin/ui-storage';
2835
+ *
2836
+ * function App() {
2837
+ * const resolver = useAliasSuggestionResolver(db);
2838
+ * return (
2839
+ * <AddressSuggestionProvider {...resolver}>
2840
+ * <MyApp />
2841
+ * </AddressSuggestionProvider>
2842
+ * );
2843
+ * }
2844
+ * ```
2845
+ */
2846
+ /**
2847
+ * Provides address suggestion resolution to all `AddressField` instances in the
2848
+ * subtree. Wrap your application (or a subsection) with this provider and
2849
+ * supply a `resolveSuggestions` function.
2850
+ *
2851
+ * @param props - Resolver function and children
2852
+ *
2853
+ * @example
2854
+ * ```tsx
2855
+ * <AddressSuggestionProvider
2856
+ * resolveSuggestions={(query, networkId) => filterAliases(query, networkId)}
2857
+ * >
2858
+ * <App />
2859
+ * </AddressSuggestionProvider>
2860
+ * ```
2861
+ */
2862
+ function AddressSuggestionProvider({ children, resolveSuggestions }) {
2863
+ const value = react.useMemo(() => ({ resolveSuggestions }), [resolveSuggestions]);
2864
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressSuggestionContext.Provider, {
2865
+ value,
2866
+ children
2867
+ });
2868
+ }
2869
+
2870
+ //#endregion
2871
+ //#region src/components/fields/address-suggestion/useAddressSuggestions.ts
2872
+ /**
2873
+ * Convenience hook that resolves suggestions for a query string using the
2874
+ * nearest `AddressSuggestionProvider`. Returns an empty array when no provider
2875
+ * is mounted or when the query is empty.
2876
+ *
2877
+ * @param query - Current input value to match against
2878
+ * @param networkId - Optional network identifier for scoping results
2879
+ * @returns Object containing the resolved suggestions array
2880
+ *
2881
+ * @example
2882
+ * ```tsx
2883
+ * function MyField({ query }: { query: string }) {
2884
+ * const { suggestions } = useAddressSuggestions(query, 'ethereum-mainnet');
2885
+ * return (
2886
+ * <ul>
2887
+ * {suggestions.map(s => <li key={s.value}>{s.label}</li>)}
2888
+ * </ul>
2889
+ * );
2890
+ * }
2891
+ * ```
2892
+ */
2893
+ /** Resolves address suggestions from the nearest `AddressSuggestionProvider`. */
2894
+ function useAddressSuggestions(query, networkId) {
2895
+ const resolver = react.useContext(AddressSuggestionContext);
2896
+ return { suggestions: react.useMemo(() => {
2897
+ if (!resolver || !query.trim()) return [];
2898
+ return resolver.resolveSuggestions(query, networkId);
2899
+ }, [
2900
+ resolver,
2901
+ query,
2902
+ networkId
2903
+ ]) };
2904
+ }
2905
+
1891
2906
  //#endregion
1892
2907
  //#region src/components/fields/AmountField.tsx
1893
2908
  /**
@@ -5263,6 +6278,8 @@ exports.AccordionItem = AccordionItem;
5263
6278
  exports.AccordionTrigger = AccordionTrigger;
5264
6279
  exports.AddressDisplay = AddressDisplay;
5265
6280
  exports.AddressField = AddressField;
6281
+ exports.AddressLabelProvider = AddressLabelProvider;
6282
+ exports.AddressSuggestionProvider = AddressSuggestionProvider;
5266
6283
  exports.Alert = Alert;
5267
6284
  exports.AlertDescription = AlertDescription;
5268
6285
  exports.AlertTitle = AlertTitle;
@@ -5342,6 +6359,7 @@ exports.NetworkServiceErrorBanner = NetworkServiceErrorBanner;
5342
6359
  exports.NetworkStatusBadge = NetworkStatusBadge;
5343
6360
  exports.NumberField = NumberField;
5344
6361
  exports.ObjectField = ObjectField;
6362
+ exports.OverflowMenu = OverflowMenu;
5345
6363
  exports.PasswordField = PasswordField;
5346
6364
  exports.Popover = Popover;
5347
6365
  exports.PopoverAnchor = PopoverAnchor;
@@ -5382,6 +6400,9 @@ exports.TooltipProvider = TooltipProvider;
5382
6400
  exports.TooltipTrigger = TooltipTrigger;
5383
6401
  exports.UrlField = UrlField;
5384
6402
  exports.ViewContractStateButton = ViewContractStateButton;
6403
+ exports.WizardLayout = WizardLayout;
6404
+ exports.WizardNavigation = WizardNavigation;
6405
+ exports.WizardStepper = WizardStepper;
5385
6406
  exports.buttonVariants = buttonVariants;
5386
6407
  exports.computeChildTouched = computeChildTouched;
5387
6408
  exports.createFocusManager = createFocusManager;
@@ -5399,6 +6420,8 @@ exports.handleToggleKeys = handleToggleKeys;
5399
6420
  exports.handleValidationError = require_ErrorMessage.handleValidationError;
5400
6421
  exports.hasFieldError = require_ErrorMessage.hasFieldError;
5401
6422
  exports.isDuplicateMapKey = require_ErrorMessage.isDuplicateMapKey;
6423
+ exports.useAddressLabel = useAddressLabel;
6424
+ exports.useAddressSuggestions = useAddressSuggestions;
5402
6425
  exports.useDuplicateKeyIndexes = useDuplicateKeyIndexes;
5403
6426
  exports.useMapFieldSync = useMapFieldSync;
5404
6427
  exports.useNetworkErrorAwareAdapter = useNetworkErrorAwareAdapter;