@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/README.md +14 -0
- package/dist/index.cjs +665 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +293 -12
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +293 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +660 -101
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
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.
|
|
119
|
-
/**
|
|
120
|
-
|
|
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
|
|
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.
|
|
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:
|
|
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,
|
|
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:
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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.
|
|
1384
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1087
1385
|
className: "max-h-[300px] overflow-y-auto p-1",
|
|
1088
|
-
children:
|
|
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
|
-
|
|
1099
|
-
|
|
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
|
-
|
|
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 === "
|
|
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.
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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;
|