@openzeppelin/ui-components 1.2.1 → 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.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]
164
234
  });
165
235
  }
166
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
286
+ });
287
+ }
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", {
@@ -618,6 +782,134 @@ const DropdownMenuShortcut = ({ className, ...props }) => {
618
782
  };
619
783
  DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
620
784
 
785
+ //#endregion
786
+ //#region src/components/ui/ecosystem-dropdown.tsx
787
+ /** Simple dropdown selector for choosing a blockchain ecosystem. */
788
+ function EcosystemDropdown({ options, value, onValueChange, getEcosystemIcon, disabled = false, className, placeholder = "Select blockchain...", "aria-labelledby": ariaLabelledby }) {
789
+ const [open, setOpen] = react.useState(false);
790
+ const selectedOption = options.find((o) => o.value === value);
791
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenu, {
792
+ open,
793
+ onOpenChange: setOpen,
794
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuTrigger, {
795
+ asChild: true,
796
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
797
+ variant: "outline",
798
+ role: "combobox",
799
+ "aria-expanded": open,
800
+ "aria-labelledby": ariaLabelledby,
801
+ disabled,
802
+ className: (0, _openzeppelin_ui_utils.cn)("w-full justify-between", className),
803
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
804
+ className: "flex items-center gap-2 truncate",
805
+ children: selectedOption ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [getEcosystemIcon?.(selectedOption.value), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
806
+ className: "truncate",
807
+ children: selectedOption.label
808
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
809
+ className: "text-muted-foreground",
810
+ children: placeholder
811
+ })
812
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
813
+ })
814
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuContent, {
815
+ className: "w-[--radix-dropdown-menu-trigger-width] min-w-[200px]",
816
+ align: "start",
817
+ children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenuItem, {
818
+ disabled: !option.enabled,
819
+ onSelect: () => {
820
+ onValueChange(option.value);
821
+ setOpen(false);
822
+ },
823
+ className: "gap-2",
824
+ children: [
825
+ getEcosystemIcon?.(option.value),
826
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
827
+ className: "flex-1 truncate",
828
+ children: option.label
829
+ }),
830
+ !option.enabled && option.disabledLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
831
+ className: "shrink-0 text-xs text-muted-foreground",
832
+ children: option.disabledLabel
833
+ }),
834
+ value === option.value && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Check, { className: "h-4 w-4 shrink-0 opacity-100" })
835
+ ]
836
+ }, option.value))
837
+ })]
838
+ });
839
+ }
840
+
841
+ //#endregion
842
+ //#region src/components/icons/MidnightIcon.tsx
843
+ /**
844
+ * MidnightIcon - SVG icon for the Midnight blockchain
845
+ * Inline SVG to ensure it renders correctly when this package is consumed as a library
846
+ */
847
+ function MidnightIcon({ size = 16, className = "", variant: _variant }) {
848
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
849
+ xmlns: "http://www.w3.org/2000/svg",
850
+ viewBox: "0 0 789.37 789.37",
851
+ width: size,
852
+ height: size,
853
+ className,
854
+ "aria-hidden": "true",
855
+ children: [
856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
857
+ d: "m394.69,0C176.71,0,0,176.71,0,394.69s176.71,394.69,394.69,394.69,394.69-176.71,394.69-394.69S612.67,0,394.69,0Zm0,716.6c-177.5,0-321.91-144.41-321.91-321.91S217.18,72.78,394.69,72.78s321.91,144.41,321.91,321.91-144.41,321.91-321.91,321.91Z",
858
+ fill: "currentColor"
859
+ }),
860
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
861
+ x: "357.64",
862
+ y: "357.64",
863
+ width: "74.09",
864
+ height: "74.09",
865
+ fill: "currentColor"
866
+ }),
867
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
868
+ x: "357.64",
869
+ y: "240.66",
870
+ width: "74.09",
871
+ height: "74.09",
872
+ fill: "currentColor"
873
+ }),
874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
875
+ x: "357.64",
876
+ y: "123.69",
877
+ width: "74.09",
878
+ height: "74.09",
879
+ fill: "currentColor"
880
+ })
881
+ ]
882
+ });
883
+ }
884
+
885
+ //#endregion
886
+ //#region src/components/ui/ecosystem-icon.tsx
887
+ /** Displays the appropriate icon for a blockchain ecosystem. */
888
+ function EcosystemIcon({ ecosystem, fallbackLabel, className, size = 16, variant = "branded" }) {
889
+ if (ecosystem.id === "midnight") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MidnightIcon, {
890
+ size,
891
+ variant,
892
+ className: (0, _openzeppelin_ui_utils.cn)("shrink-0", className)
893
+ });
894
+ if (ecosystem.iconComponent) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ecosystem.iconComponent, {
895
+ size,
896
+ variant,
897
+ className: (0, _openzeppelin_ui_utils.cn)("shrink-0", className)
898
+ });
899
+ const initial = (fallbackLabel ?? ecosystem.id).charAt(0).toUpperCase();
900
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
901
+ className: (0, _openzeppelin_ui_utils.cn)("bg-muted text-muted-foreground shrink-0 rounded-full flex items-center justify-center font-medium", className),
902
+ style: {
903
+ width: size,
904
+ height: size,
905
+ fontSize: size * .5
906
+ },
907
+ role: "img",
908
+ "aria-label": fallbackLabel ?? ecosystem.id,
909
+ children: initial
910
+ });
911
+ }
912
+
621
913
  //#endregion
622
914
  //#region src/components/ui/empty-state.tsx
623
915
  /**
@@ -943,50 +1235,6 @@ const LoadingButton = react.forwardRef(({ className, loading = false, children,
943
1235
  });
944
1236
  LoadingButton.displayName = "LoadingButton";
945
1237
 
946
- //#endregion
947
- //#region src/components/icons/MidnightIcon.tsx
948
- /**
949
- * MidnightIcon - SVG icon for the Midnight blockchain
950
- * Inline SVG to ensure it renders correctly when this package is consumed as a library
951
- */
952
- function MidnightIcon({ size = 16, className = "", variant: _variant }) {
953
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
954
- xmlns: "http://www.w3.org/2000/svg",
955
- viewBox: "0 0 789.37 789.37",
956
- width: size,
957
- height: size,
958
- className,
959
- "aria-hidden": "true",
960
- children: [
961
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
962
- d: "m394.69,0C176.71,0,0,176.71,0,394.69s176.71,394.69,394.69,394.69,394.69-176.71,394.69-394.69S612.67,0,394.69,0Zm0,716.6c-177.5,0-321.91-144.41-321.91-321.91S217.18,72.78,394.69,72.78s321.91,144.41,321.91,321.91-144.41,321.91-321.91,321.91Z",
963
- fill: "currentColor"
964
- }),
965
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
966
- x: "357.64",
967
- y: "357.64",
968
- width: "74.09",
969
- height: "74.09",
970
- fill: "currentColor"
971
- }),
972
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
973
- x: "357.64",
974
- y: "240.66",
975
- width: "74.09",
976
- height: "74.09",
977
- fill: "currentColor"
978
- }),
979
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
980
- x: "357.64",
981
- y: "123.69",
982
- width: "74.09",
983
- height: "74.09",
984
- fill: "currentColor"
985
- })
986
- ]
987
- });
988
- }
989
-
990
1238
  //#endregion
991
1239
  //#region src/components/ui/network-icon.tsx
992
1240
  /** Displays the appropriate icon for a blockchain network. */
@@ -1013,10 +1261,15 @@ function NetworkIcon({ network, className, size = 16, variant = "branded" }) {
1013
1261
 
1014
1262
  //#endregion
1015
1263
  //#region src/components/ui/network-selector.tsx
1016
- /** Searchable dropdown selector for blockchain networks with optional grouping. */
1017
- 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 }) {
1018
1266
  const [open, setOpen] = react.useState(false);
1019
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;
1020
1273
  const filteredNetworks = react.useMemo(() => {
1021
1274
  if (!searchQuery) return networks;
1022
1275
  if (filterNetwork) return networks.filter((n) => filterNetwork(n, searchQuery));
@@ -1040,34 +1293,79 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1040
1293
  groupByEcosystem,
1041
1294
  getEcosystem
1042
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;
1043
1325
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenu, {
1044
1326
  open,
1045
1327
  onOpenChange: setOpen,
1046
1328
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuTrigger, {
1047
1329
  asChild: true,
1048
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
1049
- variant: "outline",
1050
- role: "combobox",
1051
- "aria-expanded": open,
1052
- className: (0, _openzeppelin_ui_utils.cn)("w-full justify-between", className),
1053
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1054
- className: "flex items-center gap-2 truncate",
1055
- children: selectedNetwork ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1056
- getNetworkIcon?.(selectedNetwork),
1057
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1058
- className: "truncate",
1059
- children: getNetworkLabel(selectedNetwork)
1060
- }),
1061
- getNetworkType && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1062
- className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase text-muted-foreground",
1063
- 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
1064
1365
  })
1065
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1066
- className: "text-muted-foreground",
1067
- children: placeholder
1068
- })
1069
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
1070
- })
1366
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
1367
+ });
1368
+ })()
1071
1369
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenuContent, {
1072
1370
  className: "w-[--radix-dropdown-menu-trigger-width] min-w-[240px] p-0",
1073
1371
  align: "start",
@@ -1083,9 +1381,19 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1083
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",
1084
1382
  "aria-label": "Search networks"
1085
1383
  })]
1086
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1384
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1087
1385
  className: "max-h-[300px] overflow-y-auto p-1",
1088
- 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", {
1089
1397
  className: "py-6 text-center text-sm text-muted-foreground",
1090
1398
  children: "No network found."
1091
1399
  }) : Object.entries(groupedNetworks).map(([group, groupNetworks], index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
@@ -1094,12 +1402,16 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1094
1402
  children: group
1095
1403
  }),
1096
1404
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuGroup, { children: groupNetworks.map((network) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenuItem, {
1097
- onSelect: () => {
1098
- onSelectNetwork(network);
1099
- setOpen(false);
1405
+ onSelect: (e) => {
1406
+ if (isMultiple) e.preventDefault();
1407
+ handleSelect(network);
1100
1408
  },
1101
1409
  className: "gap-2",
1102
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,
1103
1415
  getNetworkIcon?.(network),
1104
1416
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1105
1417
  className: "flex flex-1 items-center gap-2 min-w-0",
@@ -1111,11 +1423,11 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
1111
1423
  children: getNetworkType(network)
1112
1424
  })]
1113
1425
  }),
1114
- 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" })
1115
1427
  ]
1116
1428
  }, getNetworkId(network))) }),
1117
1429
  index < Object.keys(groupedNetworks).length - 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuSeparator, {})
1118
- ] }, group))
1430
+ ] }, group))]
1119
1431
  })]
1120
1432
  })]
1121
1433
  });
@@ -1148,6 +1460,32 @@ function NetworkStatusBadge({ network, className }) {
1148
1460
  });
1149
1461
  }
1150
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
+
1151
1489
  //#endregion
1152
1490
  //#region src/components/ui/progress.tsx
1153
1491
  const Progress = react.forwardRef(({ className, value, ...props }, ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_progress.Root, {
@@ -1517,6 +1855,15 @@ function ViewContractStateButton({ contractAddress, onToggle }) {
1517
1855
  });
1518
1856
  }
1519
1857
 
1858
+ //#endregion
1859
+ //#region src/components/fields/address-suggestion/context.ts
1860
+ /**
1861
+ * @internal Shared context instance consumed by both AddressField and
1862
+ * AddressSuggestionProvider. Kept in its own file so component files export
1863
+ * only components (required by React Fast Refresh).
1864
+ */
1865
+ const AddressSuggestionContext = (0, react.createContext)(null);
1866
+
1520
1867
  //#endregion
1521
1868
  //#region src/components/fields/utils/accessibility.ts
1522
1869
  /**
@@ -1708,6 +2055,8 @@ function getWidthClasses(width) {
1708
2055
 
1709
2056
  //#endregion
1710
2057
  //#region src/components/fields/AddressField.tsx
2058
+ const DEBOUNCE_MS = 200;
2059
+ const MAX_SUGGESTIONS = 5;
1711
2060
  /**
1712
2061
  * Address input field component specifically designed for blockchain addresses via React Hook Form integration.
1713
2062
  *
@@ -1725,11 +2074,82 @@ function getWidthClasses(width) {
1725
2074
  * - Chain-agnostic design (validation handled by adapters)
1726
2075
  * - Full accessibility support with ARIA attributes
1727
2076
  * - Keyboard navigation
2077
+ *
2078
+ * Autocomplete suggestions can be provided in two ways:
2079
+ *
2080
+ * 1. **Context-based (zero-config)**: Mount an `AddressSuggestionProvider` in the
2081
+ * component tree. Every `AddressField` below it automatically resolves suggestions.
2082
+ *
2083
+ * 2. **Prop-based (explicit)**: Pass `suggestions` directly. This overrides context.
2084
+ * Pass `suggestions={false}` to opt out when a provider is mounted.
2085
+ *
2086
+ * The suggestion dropdown includes built-in debouncing, keyboard navigation (Arrow keys,
2087
+ * Enter, Escape), click-outside dismissal, and ARIA listbox semantics.
1728
2088
  */
1729
- function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, adapter, readOnly }) {
2089
+ function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, adapter, readOnly, suggestions: suggestionsProp, onSuggestionSelect }) {
1730
2090
  const isRequired = !!validation?.required;
1731
2091
  const errorId = `${id}-error`;
1732
2092
  const descriptionId = `${id}-description`;
2093
+ const contextResolver = (0, react.useContext)(AddressSuggestionContext);
2094
+ const containerRef = (0, react.useRef)(null);
2095
+ const lastSetValueRef = (0, react.useRef)("");
2096
+ const [inputValue, setInputValue] = (0, react.useState)("");
2097
+ const [debouncedQuery, setDebouncedQuery] = (0, react.useState)("");
2098
+ const [showSuggestions, setShowSuggestions] = (0, react.useState)(false);
2099
+ const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(-1);
2100
+ const watchedFieldValue = (0, react_hook_form.useWatch)({
2101
+ control,
2102
+ name
2103
+ });
2104
+ (0, react.useEffect)(() => {
2105
+ const currentFieldValue = watchedFieldValue ?? "";
2106
+ if (currentFieldValue !== lastSetValueRef.current) {
2107
+ lastSetValueRef.current = currentFieldValue;
2108
+ setInputValue(currentFieldValue);
2109
+ }
2110
+ }, [watchedFieldValue]);
2111
+ (0, react.useEffect)(() => {
2112
+ if (!inputValue.trim()) {
2113
+ setDebouncedQuery("");
2114
+ return;
2115
+ }
2116
+ const timer = setTimeout(() => setDebouncedQuery(inputValue), DEBOUNCE_MS);
2117
+ return () => clearTimeout(timer);
2118
+ }, [inputValue]);
2119
+ const suggestionsDisabled = suggestionsProp === false;
2120
+ const resolvedSuggestions = (0, react.useMemo)(() => {
2121
+ if (suggestionsDisabled) return [];
2122
+ if (Array.isArray(suggestionsProp)) return suggestionsProp;
2123
+ if (!contextResolver || !debouncedQuery.trim()) return [];
2124
+ return contextResolver.resolveSuggestions(debouncedQuery).slice(0, MAX_SUGGESTIONS);
2125
+ }, [
2126
+ suggestionsDisabled,
2127
+ suggestionsProp,
2128
+ contextResolver,
2129
+ debouncedQuery
2130
+ ]);
2131
+ const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
2132
+ (0, react.useEffect)(() => {
2133
+ let active = true;
2134
+ const handleClickOutside = (e) => {
2135
+ if (active && containerRef.current && !containerRef.current.contains(e.target)) setShowSuggestions(false);
2136
+ };
2137
+ document.addEventListener("mousedown", handleClickOutside);
2138
+ return () => {
2139
+ active = false;
2140
+ document.removeEventListener("mousedown", handleClickOutside);
2141
+ };
2142
+ }, []);
2143
+ const handleSuggestionKeyDown = (0, react.useCallback)((e) => {
2144
+ if (!hasSuggestions) return;
2145
+ if (e.key === "ArrowDown") {
2146
+ e.preventDefault();
2147
+ setHighlightedIndex((prev) => prev < resolvedSuggestions.length - 1 ? prev + 1 : 0);
2148
+ } else if (e.key === "ArrowUp") {
2149
+ e.preventDefault();
2150
+ setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
2151
+ }
2152
+ }, [hasSuggestions, resolvedSuggestions.length]);
1733
2153
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1734
2154
  className: `flex flex-col gap-2 ${width === "full" ? "w-full" : width === "half" ? "w-1/2" : "w-1/3"}`,
1735
2155
  children: [label && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Label, {
@@ -1763,9 +2183,32 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
1763
2183
  const handleInputChange = (e) => {
1764
2184
  const value = e.target.value;
1765
2185
  field.onChange(value);
2186
+ lastSetValueRef.current = value;
2187
+ setInputValue(value);
2188
+ setShowSuggestions(value.length > 0);
2189
+ setHighlightedIndex(-1);
2190
+ };
2191
+ const applySuggestion = (suggestion) => {
2192
+ field.onChange(suggestion.value);
2193
+ onSuggestionSelect?.(suggestion);
2194
+ lastSetValueRef.current = suggestion.value;
2195
+ setInputValue(suggestion.value);
2196
+ setShowSuggestions(false);
2197
+ setHighlightedIndex(-1);
1766
2198
  };
1767
2199
  const handleKeyDown = (e) => {
1768
- if (e.key === "Escape") handleEscapeKey(field.onChange, field.value)(e);
2200
+ if (hasSuggestions && e.key === "Enter" && highlightedIndex >= 0) {
2201
+ e.preventDefault();
2202
+ applySuggestion(resolvedSuggestions[highlightedIndex]);
2203
+ return;
2204
+ }
2205
+ if (e.key === "Escape") {
2206
+ if (hasSuggestions) {
2207
+ setShowSuggestions(false);
2208
+ return;
2209
+ }
2210
+ handleEscapeKey(field.onChange, field.value)(e);
2211
+ }
1769
2212
  };
1770
2213
  const accessibilityProps = getAccessibilityProps({
1771
2214
  id,
@@ -1774,18 +2217,50 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
1774
2217
  hasHelperText: !!helperText
1775
2218
  });
1776
2219
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1777
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
1778
- ...field,
1779
- id,
1780
- placeholder: placeholder || "0x...",
1781
- className: validationClasses,
1782
- onChange: handleInputChange,
1783
- onKeyDown: handleKeyDown,
1784
- "data-slot": "input",
1785
- value: field.value ?? "",
1786
- ...accessibilityProps,
1787
- "aria-describedby": `${helperText ? descriptionId : ""} ${hasError ? errorId : ""}`,
1788
- disabled: readOnly
2220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2221
+ ref: containerRef,
2222
+ className: "relative",
2223
+ onKeyDown: handleSuggestionKeyDown,
2224
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
2225
+ ...field,
2226
+ id,
2227
+ placeholder: placeholder || "0x...",
2228
+ className: validationClasses,
2229
+ onChange: handleInputChange,
2230
+ onKeyDown: handleKeyDown,
2231
+ "data-slot": "input",
2232
+ value: field.value ?? "",
2233
+ ...accessibilityProps,
2234
+ "aria-describedby": `${helperText ? descriptionId : ""} ${hasError ? errorId : ""}`,
2235
+ "aria-expanded": hasSuggestions,
2236
+ "aria-autocomplete": suggestionsDisabled ? void 0 : "list",
2237
+ "aria-controls": hasSuggestions ? `${id}-suggestions` : void 0,
2238
+ "aria-activedescendant": hasSuggestions && highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : void 0,
2239
+ disabled: readOnly
2240
+ }), hasSuggestions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2241
+ id: `${id}-suggestions`,
2242
+ 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"),
2243
+ role: "listbox",
2244
+ children: resolvedSuggestions.map((s, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2245
+ id: `${id}-suggestion-${i}`,
2246
+ type: "button",
2247
+ role: "option",
2248
+ "aria-selected": i === highlightedIndex,
2249
+ 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"),
2250
+ onMouseDown: (e) => {
2251
+ e.preventDefault();
2252
+ applySuggestion(s);
2253
+ },
2254
+ onMouseEnter: () => setHighlightedIndex(i),
2255
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2256
+ className: "font-medium",
2257
+ children: s.label
2258
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2259
+ className: "truncate font-mono text-xs text-muted-foreground",
2260
+ children: s.value
2261
+ })]
2262
+ }, `${s.value}-${s.description ?? i}`))
2263
+ })]
1789
2264
  }),
1790
2265
  helperText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1791
2266
  id: descriptionId,
@@ -1804,6 +2279,90 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
1804
2279
  }
1805
2280
  AddressField.displayName = "AddressField";
1806
2281
 
2282
+ //#endregion
2283
+ //#region src/components/fields/address-suggestion/address-suggestion-context.tsx
2284
+ /**
2285
+ * Address Suggestion Context
2286
+ *
2287
+ * Provides a React context for resolving address autocomplete suggestions.
2288
+ * When an `AddressSuggestionProvider` is mounted, every `AddressField`
2289
+ * in the subtree automatically renders a suggestion dropdown as the user types.
2290
+ *
2291
+ * @example
2292
+ * ```tsx
2293
+ * import { AddressSuggestionProvider } from '@openzeppelin/ui-components';
2294
+ * import { useAliasSuggestionResolver } from '@openzeppelin/ui-storage';
2295
+ *
2296
+ * function App() {
2297
+ * const resolver = useAliasSuggestionResolver(db);
2298
+ * return (
2299
+ * <AddressSuggestionProvider {...resolver}>
2300
+ * <MyApp />
2301
+ * </AddressSuggestionProvider>
2302
+ * );
2303
+ * }
2304
+ * ```
2305
+ */
2306
+ /**
2307
+ * Provides address suggestion resolution to all `AddressField` instances in the
2308
+ * subtree. Wrap your application (or a subsection) with this provider and
2309
+ * supply a `resolveSuggestions` function.
2310
+ *
2311
+ * @param props - Resolver function and children
2312
+ *
2313
+ * @example
2314
+ * ```tsx
2315
+ * <AddressSuggestionProvider
2316
+ * resolveSuggestions={(query, networkId) => filterAliases(query, networkId)}
2317
+ * >
2318
+ * <App />
2319
+ * </AddressSuggestionProvider>
2320
+ * ```
2321
+ */
2322
+ function AddressSuggestionProvider({ children, resolveSuggestions }) {
2323
+ const value = react.useMemo(() => ({ resolveSuggestions }), [resolveSuggestions]);
2324
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressSuggestionContext.Provider, {
2325
+ value,
2326
+ children
2327
+ });
2328
+ }
2329
+
2330
+ //#endregion
2331
+ //#region src/components/fields/address-suggestion/useAddressSuggestions.ts
2332
+ /**
2333
+ * Convenience hook that resolves suggestions for a query string using the
2334
+ * nearest `AddressSuggestionProvider`. Returns an empty array when no provider
2335
+ * is mounted or when the query is empty.
2336
+ *
2337
+ * @param query - Current input value to match against
2338
+ * @param networkId - Optional network identifier for scoping results
2339
+ * @returns Object containing the resolved suggestions array
2340
+ *
2341
+ * @example
2342
+ * ```tsx
2343
+ * function MyField({ query }: { query: string }) {
2344
+ * const { suggestions } = useAddressSuggestions(query, 'ethereum-mainnet');
2345
+ * return (
2346
+ * <ul>
2347
+ * {suggestions.map(s => <li key={s.value}>{s.label}</li>)}
2348
+ * </ul>
2349
+ * );
2350
+ * }
2351
+ * ```
2352
+ */
2353
+ /** Resolves address suggestions from the nearest `AddressSuggestionProvider`. */
2354
+ function useAddressSuggestions(query, networkId) {
2355
+ const resolver = react.useContext(AddressSuggestionContext);
2356
+ return { suggestions: react.useMemo(() => {
2357
+ if (!resolver || !query.trim()) return [];
2358
+ return resolver.resolveSuggestions(query, networkId);
2359
+ }, [
2360
+ resolver,
2361
+ query,
2362
+ networkId
2363
+ ]) };
2364
+ }
2365
+
1807
2366
  //#endregion
1808
2367
  //#region src/components/fields/AmountField.tsx
1809
2368
  /**
@@ -5179,6 +5738,8 @@ exports.AccordionItem = AccordionItem;
5179
5738
  exports.AccordionTrigger = AccordionTrigger;
5180
5739
  exports.AddressDisplay = AddressDisplay;
5181
5740
  exports.AddressField = AddressField;
5741
+ exports.AddressLabelProvider = AddressLabelProvider;
5742
+ exports.AddressSuggestionProvider = AddressSuggestionProvider;
5182
5743
  exports.Alert = Alert;
5183
5744
  exports.AlertDescription = AlertDescription;
5184
5745
  exports.AlertTitle = AlertTitle;
@@ -5226,6 +5787,8 @@ exports.DropdownMenuSub = DropdownMenuSub;
5226
5787
  exports.DropdownMenuSubContent = DropdownMenuSubContent;
5227
5788
  exports.DropdownMenuSubTrigger = DropdownMenuSubTrigger;
5228
5789
  exports.DropdownMenuTrigger = DropdownMenuTrigger;
5790
+ exports.EcosystemDropdown = EcosystemDropdown;
5791
+ exports.EcosystemIcon = EcosystemIcon;
5229
5792
  exports.EmptyState = EmptyState;
5230
5793
  exports.EnumField = EnumField;
5231
5794
  exports.ErrorMessage = require_ErrorMessage.ErrorMessage;
@@ -5256,6 +5819,7 @@ exports.NetworkServiceErrorBanner = NetworkServiceErrorBanner;
5256
5819
  exports.NetworkStatusBadge = NetworkStatusBadge;
5257
5820
  exports.NumberField = NumberField;
5258
5821
  exports.ObjectField = ObjectField;
5822
+ exports.OverflowMenu = OverflowMenu;
5259
5823
  exports.PasswordField = PasswordField;
5260
5824
  exports.Popover = Popover;
5261
5825
  exports.PopoverAnchor = PopoverAnchor;
@@ -5313,6 +5877,8 @@ exports.handleToggleKeys = handleToggleKeys;
5313
5877
  exports.handleValidationError = require_ErrorMessage.handleValidationError;
5314
5878
  exports.hasFieldError = require_ErrorMessage.hasFieldError;
5315
5879
  exports.isDuplicateMapKey = require_ErrorMessage.isDuplicateMapKey;
5880
+ exports.useAddressLabel = useAddressLabel;
5881
+ exports.useAddressSuggestions = useAddressSuggestions;
5316
5882
  exports.useDuplicateKeyIndexes = useDuplicateKeyIndexes;
5317
5883
  exports.useMapFieldSync = useMapFieldSync;
5318
5884
  exports.useNetworkErrorAwareAdapter = useNetworkErrorAwareAdapter;