@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.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as getValidationStateClasses, c as isDuplicateMapKey, d as INTEGER_HTML_PATTERN, f as INTEGER_INPUT_PATTERN, i as getErrorMessage, l as validateField, n as createValidationResult, o as handleValidationError, p as INTEGER_PATTERN, r as formatValidationError, s as hasFieldError, t as ErrorMessage, u as validateMapEntries } from "./ErrorMessage-BqOEJm84.mjs";
|
|
2
2
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
|
-
import { AlertCircle, Calendar as Calendar$1, Check, CheckCircle, CheckCircle2, CheckIcon, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, CloudOff, Copy, DollarSign, ExternalLink as ExternalLink$1, ExternalLinkIcon, Eye, EyeOff, File, FileText, GripVertical, Hash, Info, Loader2, Menu, Network, Plus, Search, Settings, Timer, Upload, X } from "lucide-react";
|
|
4
|
+
import { AlertCircle, Calendar as Calendar$1, Check, CheckCircle, CheckCircle2, CheckIcon, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, CloudOff, Copy, DollarSign, ExternalLink as ExternalLink$1, ExternalLinkIcon, Eye, EyeOff, File, FileText, GripVertical, Hash, Info, Loader2, Menu, MoreHorizontal, Network, Pencil, Plus, Search, Settings, Timer, Upload, X } from "lucide-react";
|
|
5
5
|
import * as React$1 from "react";
|
|
6
6
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { cn, getDefaultValueForType, getInvalidUrlMessage, getServiceDisplayName, isValidUrl, truncateMiddle, validateBytesSimple } from "@openzeppelin/ui-utils";
|
|
@@ -103,11 +103,58 @@ const AccordionContent = React$1.forwardRef(({ className, children, variant: var
|
|
|
103
103
|
AccordionContent.displayName = "AccordionContent";
|
|
104
104
|
|
|
105
105
|
//#endregion
|
|
106
|
-
//#region src/components/ui/address-display.
|
|
107
|
-
/**
|
|
108
|
-
|
|
106
|
+
//#region src/components/ui/address-display/context.ts
|
|
107
|
+
/**
|
|
108
|
+
* @internal Shared context instance consumed by both AddressDisplay and
|
|
109
|
+
* AddressLabelProvider. Kept in its own file so component files export
|
|
110
|
+
* only components (required by React Fast Refresh).
|
|
111
|
+
*/
|
|
112
|
+
const AddressLabelContext = createContext(null);
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/components/ui/address-display/address-display.tsx
|
|
116
|
+
/**
|
|
117
|
+
* Displays a blockchain address with optional truncation, copy button,
|
|
118
|
+
* explorer link, and human-readable label.
|
|
119
|
+
*
|
|
120
|
+
* Labels are resolved in priority order:
|
|
121
|
+
* 1. Explicit `label` prop
|
|
122
|
+
* 2. `AddressLabelContext` resolver (via `AddressLabelProvider`)
|
|
123
|
+
* 3. No label (renders address only, identical to previous behavior)
|
|
124
|
+
*
|
|
125
|
+
* Pass `disableLabel` to suppress context-based resolution (e.g. when the
|
|
126
|
+
* surrounding UI already shows a name, such as a contract selector).
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```tsx
|
|
130
|
+
* // Basic usage (unchanged)
|
|
131
|
+
* <AddressDisplay address="0x742d35Cc..." showCopyButton />
|
|
132
|
+
*
|
|
133
|
+
* // Explicit label
|
|
134
|
+
* <AddressDisplay address="0x742d35Cc..." label="Treasury" />
|
|
135
|
+
*
|
|
136
|
+
* // Auto-resolved via context (no changes needed at call site)
|
|
137
|
+
* <AddressLabelProvider resolveLabel={myResolver}>
|
|
138
|
+
* <AddressDisplay address="0x742d35Cc..." />
|
|
139
|
+
* </AddressLabelProvider>
|
|
140
|
+
*
|
|
141
|
+
* // Suppress label resolution for a specific instance
|
|
142
|
+
* <AddressDisplay address="0x742d35Cc..." disableLabel />
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, label: labelProp, onLabelEdit: onLabelEditProp, networkId, disableLabel = false, className, ...props }) {
|
|
109
146
|
const [copied, setCopied] = React$1.useState(false);
|
|
110
147
|
const copyTimeoutRef = React$1.useRef(null);
|
|
148
|
+
const resolver = React$1.useContext(AddressLabelContext);
|
|
149
|
+
const resolvedLabel = disableLabel ? void 0 : labelProp ?? resolver?.resolveLabel(address, networkId);
|
|
150
|
+
const contextEditHandler = React$1.useCallback(() => {
|
|
151
|
+
resolver?.onEditLabel?.(address, networkId);
|
|
152
|
+
}, [
|
|
153
|
+
resolver,
|
|
154
|
+
address,
|
|
155
|
+
networkId
|
|
156
|
+
]);
|
|
157
|
+
const editHandler = disableLabel ? void 0 : onLabelEditProp ?? (resolver?.onEditLabel ? contextEditHandler : void 0);
|
|
111
158
|
const displayAddress = truncate ? truncateMiddle(address, startChars, endChars) : address;
|
|
112
159
|
const handleCopy = (e) => {
|
|
113
160
|
e.stopPropagation();
|
|
@@ -124,11 +171,7 @@ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4
|
|
|
124
171
|
if (copyTimeoutRef.current) window.clearTimeout(copyTimeoutRef.current);
|
|
125
172
|
};
|
|
126
173
|
}, []);
|
|
127
|
-
const
|
|
128
|
-
/* @__PURE__ */ jsx("span", {
|
|
129
|
-
className: cn("truncate", truncate ? "" : "break-all"),
|
|
130
|
-
children: displayAddress
|
|
131
|
-
}),
|
|
174
|
+
const actionButtons = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
132
175
|
showCopyButton && /* @__PURE__ */ jsx("button", {
|
|
133
176
|
type: "button",
|
|
134
177
|
onClick: handleCopy,
|
|
@@ -143,15 +186,136 @@ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4
|
|
|
143
186
|
className: "ml-1.5 shrink-0 text-slate-500 transition-colors hover:text-slate-700",
|
|
144
187
|
"aria-label": "View in explorer",
|
|
145
188
|
children: /* @__PURE__ */ jsx(ExternalLink$1, { className: "h-3.5 w-3.5" })
|
|
189
|
+
}),
|
|
190
|
+
editHandler && /* @__PURE__ */ jsx("button", {
|
|
191
|
+
type: "button",
|
|
192
|
+
onClick: (e) => {
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
editHandler();
|
|
195
|
+
},
|
|
196
|
+
className: "ml-0 w-0 shrink-0 overflow-hidden text-slate-500 opacity-0 transition-all duration-150 hover:text-slate-700 group-hover:ml-1.5 group-hover:w-3.5 group-hover:opacity-100 focus:ml-1.5 focus:w-3.5 focus:opacity-100",
|
|
197
|
+
"aria-label": "Edit label",
|
|
198
|
+
children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" })
|
|
146
199
|
})
|
|
147
200
|
] });
|
|
148
|
-
return /* @__PURE__ */
|
|
201
|
+
if (resolvedLabel) return /* @__PURE__ */ jsxs("div", {
|
|
202
|
+
className: cn("group inline-flex max-w-full flex-col rounded-md bg-slate-100 px-2 py-1", "text-xs text-slate-700", className),
|
|
203
|
+
...props,
|
|
204
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
205
|
+
className: "truncate font-sans font-medium text-slate-900 leading-snug",
|
|
206
|
+
children: resolvedLabel
|
|
207
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
208
|
+
className: "flex items-center font-mono text-[10px] text-slate-400 leading-snug",
|
|
209
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
210
|
+
className: cn("truncate", truncate ? "" : "break-all"),
|
|
211
|
+
children: displayAddress
|
|
212
|
+
}), actionButtons]
|
|
213
|
+
})]
|
|
214
|
+
});
|
|
215
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
149
216
|
className: cn("group inline-flex max-w-full items-center rounded-md bg-slate-100 px-2 py-1", "text-xs font-mono text-slate-700", className),
|
|
150
217
|
...props,
|
|
151
|
-
children:
|
|
218
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
219
|
+
className: cn("truncate", truncate ? "" : "break-all"),
|
|
220
|
+
children: displayAddress
|
|
221
|
+
}), actionButtons]
|
|
152
222
|
});
|
|
153
223
|
}
|
|
154
224
|
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/components/ui/address-display/address-label-context.tsx
|
|
227
|
+
/**
|
|
228
|
+
* Address Label Context
|
|
229
|
+
*
|
|
230
|
+
* Provides a React context for resolving human-readable labels for blockchain
|
|
231
|
+
* addresses. When an `AddressLabelProvider` is mounted, every `AddressDisplay`
|
|
232
|
+
* in the subtree automatically resolves and renders labels without any
|
|
233
|
+
* call-site changes.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```tsx
|
|
237
|
+
* import { AddressLabelProvider } from '@openzeppelin/ui-components';
|
|
238
|
+
*
|
|
239
|
+
* function App() {
|
|
240
|
+
* const resolver = useAliasLabelResolver(db);
|
|
241
|
+
* return (
|
|
242
|
+
* <AddressLabelProvider {...resolver}>
|
|
243
|
+
* <MyApp />
|
|
244
|
+
* </AddressLabelProvider>
|
|
245
|
+
* );
|
|
246
|
+
* }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
/**
|
|
250
|
+
* Provides address label resolution to all `AddressDisplay` instances in the
|
|
251
|
+
* subtree. Wrap your application (or a subsection) with this provider and
|
|
252
|
+
* supply a `resolveLabel` function.
|
|
253
|
+
*
|
|
254
|
+
* @param props - Resolver functions and children
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```tsx
|
|
258
|
+
* <AddressLabelProvider
|
|
259
|
+
* resolveLabel={(addr) => addressBook.get(addr)}
|
|
260
|
+
* onEditLabel={(addr) => openEditor(addr)}
|
|
261
|
+
* >
|
|
262
|
+
* <App />
|
|
263
|
+
* </AddressLabelProvider>
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
function AddressLabelProvider({ children, resolveLabel, onEditLabel }) {
|
|
267
|
+
const value = React$1.useMemo(() => ({
|
|
268
|
+
resolveLabel,
|
|
269
|
+
onEditLabel
|
|
270
|
+
}), [resolveLabel, onEditLabel]);
|
|
271
|
+
return /* @__PURE__ */ jsx(AddressLabelContext.Provider, {
|
|
272
|
+
value,
|
|
273
|
+
children
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/components/ui/address-display/use-address-label.ts
|
|
279
|
+
/**
|
|
280
|
+
* Convenience hook for resolving an address label from the nearest
|
|
281
|
+
* `AddressLabelProvider`.
|
|
282
|
+
*
|
|
283
|
+
* Kept in its own file so that `address-label-context.tsx` exports only
|
|
284
|
+
* components (required by React Fast Refresh).
|
|
285
|
+
*/
|
|
286
|
+
/**
|
|
287
|
+
* Convenience hook that resolves a label for a specific address using the
|
|
288
|
+
* nearest `AddressLabelProvider`. Returns `undefined` values when no provider
|
|
289
|
+
* is mounted.
|
|
290
|
+
*
|
|
291
|
+
* @param address - The blockchain address to resolve
|
|
292
|
+
* @param networkId - Optional network identifier for network-specific aliases
|
|
293
|
+
* @returns Resolved label and edit handler for the address
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```tsx
|
|
297
|
+
* function MyAddress({ address }: { address: string }) {
|
|
298
|
+
* const { label, onEdit } = useAddressLabel(address, 'ethereum-mainnet');
|
|
299
|
+
* return <span>{label ?? address}</span>;
|
|
300
|
+
* }
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
function useAddressLabel(address, networkId) {
|
|
304
|
+
const resolver = React$1.useContext(AddressLabelContext);
|
|
305
|
+
const label = resolver?.resolveLabel(address, networkId);
|
|
306
|
+
const onEdit = React$1.useCallback(() => {
|
|
307
|
+
resolver?.onEditLabel?.(address, networkId);
|
|
308
|
+
}, [
|
|
309
|
+
resolver,
|
|
310
|
+
address,
|
|
311
|
+
networkId
|
|
312
|
+
]);
|
|
313
|
+
return {
|
|
314
|
+
label,
|
|
315
|
+
onEdit: resolver?.onEditLabel ? onEdit : void 0
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
155
319
|
//#endregion
|
|
156
320
|
//#region src/components/ui/alert.tsx
|
|
157
321
|
const alertVariants = cva("relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", {
|
|
@@ -606,6 +770,134 @@ const DropdownMenuShortcut = ({ className, ...props }) => {
|
|
|
606
770
|
};
|
|
607
771
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
|
608
772
|
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/components/ui/ecosystem-dropdown.tsx
|
|
775
|
+
/** Simple dropdown selector for choosing a blockchain ecosystem. */
|
|
776
|
+
function EcosystemDropdown({ options, value, onValueChange, getEcosystemIcon, disabled = false, className, placeholder = "Select blockchain...", "aria-labelledby": ariaLabelledby }) {
|
|
777
|
+
const [open, setOpen] = React$1.useState(false);
|
|
778
|
+
const selectedOption = options.find((o) => o.value === value);
|
|
779
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, {
|
|
780
|
+
open,
|
|
781
|
+
onOpenChange: setOpen,
|
|
782
|
+
children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
783
|
+
asChild: true,
|
|
784
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
785
|
+
variant: "outline",
|
|
786
|
+
role: "combobox",
|
|
787
|
+
"aria-expanded": open,
|
|
788
|
+
"aria-labelledby": ariaLabelledby,
|
|
789
|
+
disabled,
|
|
790
|
+
className: cn("w-full justify-between", className),
|
|
791
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
792
|
+
className: "flex items-center gap-2 truncate",
|
|
793
|
+
children: selectedOption ? /* @__PURE__ */ jsxs(Fragment, { children: [getEcosystemIcon?.(selectedOption.value), /* @__PURE__ */ jsx("span", {
|
|
794
|
+
className: "truncate",
|
|
795
|
+
children: selectedOption.label
|
|
796
|
+
})] }) : /* @__PURE__ */ jsx("span", {
|
|
797
|
+
className: "text-muted-foreground",
|
|
798
|
+
children: placeholder
|
|
799
|
+
})
|
|
800
|
+
}), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
|
|
801
|
+
})
|
|
802
|
+
}), /* @__PURE__ */ jsx(DropdownMenuContent, {
|
|
803
|
+
className: "w-[--radix-dropdown-menu-trigger-width] min-w-[200px]",
|
|
804
|
+
align: "start",
|
|
805
|
+
children: options.map((option) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
806
|
+
disabled: !option.enabled,
|
|
807
|
+
onSelect: () => {
|
|
808
|
+
onValueChange(option.value);
|
|
809
|
+
setOpen(false);
|
|
810
|
+
},
|
|
811
|
+
className: "gap-2",
|
|
812
|
+
children: [
|
|
813
|
+
getEcosystemIcon?.(option.value),
|
|
814
|
+
/* @__PURE__ */ jsx("span", {
|
|
815
|
+
className: "flex-1 truncate",
|
|
816
|
+
children: option.label
|
|
817
|
+
}),
|
|
818
|
+
!option.enabled && option.disabledLabel && /* @__PURE__ */ jsx("span", {
|
|
819
|
+
className: "shrink-0 text-xs text-muted-foreground",
|
|
820
|
+
children: option.disabledLabel
|
|
821
|
+
}),
|
|
822
|
+
value === option.value && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 shrink-0 opacity-100" })
|
|
823
|
+
]
|
|
824
|
+
}, option.value))
|
|
825
|
+
})]
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region src/components/icons/MidnightIcon.tsx
|
|
831
|
+
/**
|
|
832
|
+
* MidnightIcon - SVG icon for the Midnight blockchain
|
|
833
|
+
* Inline SVG to ensure it renders correctly when this package is consumed as a library
|
|
834
|
+
*/
|
|
835
|
+
function MidnightIcon({ size = 16, className = "", variant: _variant }) {
|
|
836
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
837
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
838
|
+
viewBox: "0 0 789.37 789.37",
|
|
839
|
+
width: size,
|
|
840
|
+
height: size,
|
|
841
|
+
className,
|
|
842
|
+
"aria-hidden": "true",
|
|
843
|
+
children: [
|
|
844
|
+
/* @__PURE__ */ jsx("path", {
|
|
845
|
+
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",
|
|
846
|
+
fill: "currentColor"
|
|
847
|
+
}),
|
|
848
|
+
/* @__PURE__ */ jsx("rect", {
|
|
849
|
+
x: "357.64",
|
|
850
|
+
y: "357.64",
|
|
851
|
+
width: "74.09",
|
|
852
|
+
height: "74.09",
|
|
853
|
+
fill: "currentColor"
|
|
854
|
+
}),
|
|
855
|
+
/* @__PURE__ */ jsx("rect", {
|
|
856
|
+
x: "357.64",
|
|
857
|
+
y: "240.66",
|
|
858
|
+
width: "74.09",
|
|
859
|
+
height: "74.09",
|
|
860
|
+
fill: "currentColor"
|
|
861
|
+
}),
|
|
862
|
+
/* @__PURE__ */ jsx("rect", {
|
|
863
|
+
x: "357.64",
|
|
864
|
+
y: "123.69",
|
|
865
|
+
width: "74.09",
|
|
866
|
+
height: "74.09",
|
|
867
|
+
fill: "currentColor"
|
|
868
|
+
})
|
|
869
|
+
]
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/components/ui/ecosystem-icon.tsx
|
|
875
|
+
/** Displays the appropriate icon for a blockchain ecosystem. */
|
|
876
|
+
function EcosystemIcon({ ecosystem, fallbackLabel, className, size = 16, variant = "branded" }) {
|
|
877
|
+
if (ecosystem.id === "midnight") return /* @__PURE__ */ jsx(MidnightIcon, {
|
|
878
|
+
size,
|
|
879
|
+
variant,
|
|
880
|
+
className: cn("shrink-0", className)
|
|
881
|
+
});
|
|
882
|
+
if (ecosystem.iconComponent) return /* @__PURE__ */ jsx(ecosystem.iconComponent, {
|
|
883
|
+
size,
|
|
884
|
+
variant,
|
|
885
|
+
className: cn("shrink-0", className)
|
|
886
|
+
});
|
|
887
|
+
const initial = (fallbackLabel ?? ecosystem.id).charAt(0).toUpperCase();
|
|
888
|
+
return /* @__PURE__ */ jsx("div", {
|
|
889
|
+
className: cn("bg-muted text-muted-foreground shrink-0 rounded-full flex items-center justify-center font-medium", className),
|
|
890
|
+
style: {
|
|
891
|
+
width: size,
|
|
892
|
+
height: size,
|
|
893
|
+
fontSize: size * .5
|
|
894
|
+
},
|
|
895
|
+
role: "img",
|
|
896
|
+
"aria-label": fallbackLabel ?? ecosystem.id,
|
|
897
|
+
children: initial
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
609
901
|
//#endregion
|
|
610
902
|
//#region src/components/ui/empty-state.tsx
|
|
611
903
|
/**
|
|
@@ -931,50 +1223,6 @@ const LoadingButton = React$1.forwardRef(({ className, loading = false, children
|
|
|
931
1223
|
});
|
|
932
1224
|
LoadingButton.displayName = "LoadingButton";
|
|
933
1225
|
|
|
934
|
-
//#endregion
|
|
935
|
-
//#region src/components/icons/MidnightIcon.tsx
|
|
936
|
-
/**
|
|
937
|
-
* MidnightIcon - SVG icon for the Midnight blockchain
|
|
938
|
-
* Inline SVG to ensure it renders correctly when this package is consumed as a library
|
|
939
|
-
*/
|
|
940
|
-
function MidnightIcon({ size = 16, className = "", variant: _variant }) {
|
|
941
|
-
return /* @__PURE__ */ jsxs("svg", {
|
|
942
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
943
|
-
viewBox: "0 0 789.37 789.37",
|
|
944
|
-
width: size,
|
|
945
|
-
height: size,
|
|
946
|
-
className,
|
|
947
|
-
"aria-hidden": "true",
|
|
948
|
-
children: [
|
|
949
|
-
/* @__PURE__ */ jsx("path", {
|
|
950
|
-
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",
|
|
951
|
-
fill: "currentColor"
|
|
952
|
-
}),
|
|
953
|
-
/* @__PURE__ */ jsx("rect", {
|
|
954
|
-
x: "357.64",
|
|
955
|
-
y: "357.64",
|
|
956
|
-
width: "74.09",
|
|
957
|
-
height: "74.09",
|
|
958
|
-
fill: "currentColor"
|
|
959
|
-
}),
|
|
960
|
-
/* @__PURE__ */ jsx("rect", {
|
|
961
|
-
x: "357.64",
|
|
962
|
-
y: "240.66",
|
|
963
|
-
width: "74.09",
|
|
964
|
-
height: "74.09",
|
|
965
|
-
fill: "currentColor"
|
|
966
|
-
}),
|
|
967
|
-
/* @__PURE__ */ jsx("rect", {
|
|
968
|
-
x: "357.64",
|
|
969
|
-
y: "123.69",
|
|
970
|
-
width: "74.09",
|
|
971
|
-
height: "74.09",
|
|
972
|
-
fill: "currentColor"
|
|
973
|
-
})
|
|
974
|
-
]
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
|
|
978
1226
|
//#endregion
|
|
979
1227
|
//#region src/components/ui/network-icon.tsx
|
|
980
1228
|
/** Displays the appropriate icon for a blockchain network. */
|
|
@@ -1001,10 +1249,15 @@ function NetworkIcon({ network, className, size = 16, variant = "branded" }) {
|
|
|
1001
1249
|
|
|
1002
1250
|
//#endregion
|
|
1003
1251
|
//#region src/components/ui/network-selector.tsx
|
|
1004
|
-
/** Searchable dropdown selector for blockchain networks with optional grouping. */
|
|
1005
|
-
function NetworkSelector({ networks,
|
|
1252
|
+
/** Searchable dropdown selector for blockchain networks with optional grouping and multi-select. */
|
|
1253
|
+
function NetworkSelector({ networks, getNetworkLabel, getNetworkIcon, getNetworkType, getNetworkId, groupByEcosystem = false, getEcosystem, filterNetwork, className, placeholder = "Select Network", ...modeProps }) {
|
|
1006
1254
|
const [open, setOpen] = React$1.useState(false);
|
|
1007
1255
|
const [searchQuery, setSearchQuery] = React$1.useState("");
|
|
1256
|
+
const isMultiple = modeProps.multiple === true;
|
|
1257
|
+
const selectedNetworkIds = isMultiple ? modeProps.selectedNetworkIds : void 0;
|
|
1258
|
+
const onSelectionChange = isMultiple ? modeProps.onSelectionChange : void 0;
|
|
1259
|
+
const selectedNetwork = !isMultiple ? modeProps.selectedNetwork : void 0;
|
|
1260
|
+
const onSelectNetwork = !isMultiple ? modeProps.onSelectNetwork : void 0;
|
|
1008
1261
|
const filteredNetworks = React$1.useMemo(() => {
|
|
1009
1262
|
if (!searchQuery) return networks;
|
|
1010
1263
|
if (filterNetwork) return networks.filter((n) => filterNetwork(n, searchQuery));
|
|
@@ -1028,34 +1281,79 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
|
|
|
1028
1281
|
groupByEcosystem,
|
|
1029
1282
|
getEcosystem
|
|
1030
1283
|
]);
|
|
1284
|
+
const isSelected = React$1.useCallback((network) => {
|
|
1285
|
+
if (isMultiple && selectedNetworkIds) return selectedNetworkIds.includes(getNetworkId(network));
|
|
1286
|
+
return selectedNetwork ? getNetworkId(selectedNetwork) === getNetworkId(network) : false;
|
|
1287
|
+
}, [
|
|
1288
|
+
isMultiple,
|
|
1289
|
+
selectedNetworkIds,
|
|
1290
|
+
selectedNetwork,
|
|
1291
|
+
getNetworkId
|
|
1292
|
+
]);
|
|
1293
|
+
const handleSelect = React$1.useCallback((network) => {
|
|
1294
|
+
if (isMultiple && selectedNetworkIds && onSelectionChange) {
|
|
1295
|
+
const id = getNetworkId(network);
|
|
1296
|
+
onSelectionChange(selectedNetworkIds.includes(id) ? selectedNetworkIds.filter((x) => x !== id) : [...selectedNetworkIds, id]);
|
|
1297
|
+
} else if (onSelectNetwork) {
|
|
1298
|
+
onSelectNetwork(network);
|
|
1299
|
+
setOpen(false);
|
|
1300
|
+
}
|
|
1301
|
+
}, [
|
|
1302
|
+
isMultiple,
|
|
1303
|
+
selectedNetworkIds,
|
|
1304
|
+
onSelectionChange,
|
|
1305
|
+
onSelectNetwork,
|
|
1306
|
+
getNetworkId
|
|
1307
|
+
]);
|
|
1308
|
+
const handleClearAll = React$1.useCallback(() => {
|
|
1309
|
+
if (isMultiple && onSelectionChange) onSelectionChange([]);
|
|
1310
|
+
}, [isMultiple, onSelectionChange]);
|
|
1311
|
+
const selectedCount = selectedNetworkIds?.length ?? 0;
|
|
1312
|
+
const renderTrigger = isMultiple ? modeProps.renderTrigger : void 0;
|
|
1031
1313
|
return /* @__PURE__ */ jsxs(DropdownMenu, {
|
|
1032
1314
|
open,
|
|
1033
1315
|
onOpenChange: setOpen,
|
|
1034
1316
|
children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
1035
1317
|
asChild: true,
|
|
1036
|
-
children:
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1318
|
+
children: (() => {
|
|
1319
|
+
if (isMultiple && renderTrigger) return renderTrigger({
|
|
1320
|
+
selectedCount,
|
|
1321
|
+
open
|
|
1322
|
+
});
|
|
1323
|
+
if (isMultiple) return /* @__PURE__ */ jsxs(Button, {
|
|
1324
|
+
variant: "outline",
|
|
1325
|
+
role: "combobox",
|
|
1326
|
+
"aria-expanded": open,
|
|
1327
|
+
className: cn("w-full justify-between", className),
|
|
1328
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1329
|
+
className: "truncate text-muted-foreground",
|
|
1330
|
+
children: selectedCount > 0 ? `${selectedCount} network${selectedCount > 1 ? "s" : ""} selected` : placeholder
|
|
1331
|
+
}), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
|
|
1332
|
+
});
|
|
1333
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
1334
|
+
variant: "outline",
|
|
1335
|
+
role: "combobox",
|
|
1336
|
+
"aria-expanded": open,
|
|
1337
|
+
className: cn("w-full justify-between", className),
|
|
1338
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1339
|
+
className: "flex items-center gap-2 truncate",
|
|
1340
|
+
children: selectedNetwork ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1341
|
+
getNetworkIcon?.(selectedNetwork),
|
|
1342
|
+
/* @__PURE__ */ jsx("span", {
|
|
1343
|
+
className: "truncate",
|
|
1344
|
+
children: getNetworkLabel(selectedNetwork)
|
|
1345
|
+
}),
|
|
1346
|
+
getNetworkType && /* @__PURE__ */ jsx("span", {
|
|
1347
|
+
className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase text-muted-foreground",
|
|
1348
|
+
children: getNetworkType(selectedNetwork)
|
|
1349
|
+
})
|
|
1350
|
+
] }) : /* @__PURE__ */ jsx("span", {
|
|
1351
|
+
className: "text-muted-foreground",
|
|
1352
|
+
children: placeholder
|
|
1052
1353
|
})
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
})
|
|
1057
|
-
}), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
|
|
1058
|
-
})
|
|
1354
|
+
}), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
|
|
1355
|
+
});
|
|
1356
|
+
})()
|
|
1059
1357
|
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
1060
1358
|
className: "w-[--radix-dropdown-menu-trigger-width] min-w-[240px] p-0",
|
|
1061
1359
|
align: "start",
|
|
@@ -1071,9 +1369,19 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
|
|
|
1071
1369
|
className: "h-9 w-full border-0 bg-transparent p-0 placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0",
|
|
1072
1370
|
"aria-label": "Search networks"
|
|
1073
1371
|
})]
|
|
1074
|
-
}), /* @__PURE__ */
|
|
1372
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1075
1373
|
className: "max-h-[300px] overflow-y-auto p-1",
|
|
1076
|
-
children:
|
|
1374
|
+
children: [isMultiple && selectedCount > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
1375
|
+
className: "flex items-center justify-between px-2 py-1.5",
|
|
1376
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
1377
|
+
className: "text-xs font-medium text-muted-foreground",
|
|
1378
|
+
children: [selectedCount, " selected"]
|
|
1379
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1380
|
+
onClick: handleClearAll,
|
|
1381
|
+
className: "text-xs text-muted-foreground hover:text-foreground",
|
|
1382
|
+
children: "Clear all"
|
|
1383
|
+
})]
|
|
1384
|
+
}), /* @__PURE__ */ jsx(DropdownMenuSeparator, {})] }), Object.entries(groupedNetworks).length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
1077
1385
|
className: "py-6 text-center text-sm text-muted-foreground",
|
|
1078
1386
|
children: "No network found."
|
|
1079
1387
|
}) : Object.entries(groupedNetworks).map(([group, groupNetworks], index) => /* @__PURE__ */ jsxs(React$1.Fragment, { children: [
|
|
@@ -1082,12 +1390,16 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
|
|
|
1082
1390
|
children: group
|
|
1083
1391
|
}),
|
|
1084
1392
|
/* @__PURE__ */ jsx(DropdownMenuGroup, { children: groupNetworks.map((network) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
1085
|
-
onSelect: () => {
|
|
1086
|
-
|
|
1087
|
-
|
|
1393
|
+
onSelect: (e) => {
|
|
1394
|
+
if (isMultiple) e.preventDefault();
|
|
1395
|
+
handleSelect(network);
|
|
1088
1396
|
},
|
|
1089
1397
|
className: "gap-2",
|
|
1090
1398
|
children: [
|
|
1399
|
+
isMultiple ? /* @__PURE__ */ jsx("div", {
|
|
1400
|
+
className: "flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border border-primary",
|
|
1401
|
+
children: isSelected(network) && /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" })
|
|
1402
|
+
}) : null,
|
|
1091
1403
|
getNetworkIcon?.(network),
|
|
1092
1404
|
/* @__PURE__ */ jsxs("div", {
|
|
1093
1405
|
className: "flex flex-1 items-center gap-2 min-w-0",
|
|
@@ -1099,11 +1411,11 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
|
|
|
1099
1411
|
children: getNetworkType(network)
|
|
1100
1412
|
})]
|
|
1101
1413
|
}),
|
|
1102
|
-
|
|
1414
|
+
!isMultiple && isSelected(network) && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 opacity-100" })
|
|
1103
1415
|
]
|
|
1104
1416
|
}, getNetworkId(network))) }),
|
|
1105
1417
|
index < Object.keys(groupedNetworks).length - 1 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {})
|
|
1106
|
-
] }, group))
|
|
1418
|
+
] }, group))]
|
|
1107
1419
|
})]
|
|
1108
1420
|
})]
|
|
1109
1421
|
});
|
|
@@ -1136,6 +1448,32 @@ function NetworkStatusBadge({ network, className }) {
|
|
|
1136
1448
|
});
|
|
1137
1449
|
}
|
|
1138
1450
|
|
|
1451
|
+
//#endregion
|
|
1452
|
+
//#region src/components/ui/overflow-menu.tsx
|
|
1453
|
+
/** Compact "..." dropdown menu for secondary actions. */
|
|
1454
|
+
function OverflowMenu({ items, align = "end", className, "aria-label": ariaLabel = "More actions" }) {
|
|
1455
|
+
if (items.length === 0) return null;
|
|
1456
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
1457
|
+
asChild: true,
|
|
1458
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
1459
|
+
variant: "ghost",
|
|
1460
|
+
size: "icon",
|
|
1461
|
+
className: cn("h-8 w-8", className),
|
|
1462
|
+
"aria-label": ariaLabel,
|
|
1463
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" })
|
|
1464
|
+
})
|
|
1465
|
+
}), /* @__PURE__ */ jsx(DropdownMenuContent, {
|
|
1466
|
+
align,
|
|
1467
|
+
className: "min-w-[140px]",
|
|
1468
|
+
children: items.map((item, index) => /* @__PURE__ */ jsxs(React$1.Fragment, { children: [item.destructive && index > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}), /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
1469
|
+
onClick: item.onSelect,
|
|
1470
|
+
disabled: item.disabled,
|
|
1471
|
+
className: cn(item.destructive && "text-destructive focus:text-destructive"),
|
|
1472
|
+
children: [item.icon, item.label]
|
|
1473
|
+
})] }, item.id))
|
|
1474
|
+
})] });
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1139
1477
|
//#endregion
|
|
1140
1478
|
//#region src/components/ui/progress.tsx
|
|
1141
1479
|
const Progress = React$1.forwardRef(({ className, value, ...props }, ref) => /* @__PURE__ */ jsx(ProgressPrimitive.Root, {
|
|
@@ -1505,6 +1843,15 @@ function ViewContractStateButton({ contractAddress, onToggle }) {
|
|
|
1505
1843
|
});
|
|
1506
1844
|
}
|
|
1507
1845
|
|
|
1846
|
+
//#endregion
|
|
1847
|
+
//#region src/components/fields/address-suggestion/context.ts
|
|
1848
|
+
/**
|
|
1849
|
+
* @internal Shared context instance consumed by both AddressField and
|
|
1850
|
+
* AddressSuggestionProvider. Kept in its own file so component files export
|
|
1851
|
+
* only components (required by React Fast Refresh).
|
|
1852
|
+
*/
|
|
1853
|
+
const AddressSuggestionContext = createContext(null);
|
|
1854
|
+
|
|
1508
1855
|
//#endregion
|
|
1509
1856
|
//#region src/components/fields/utils/accessibility.ts
|
|
1510
1857
|
/**
|
|
@@ -1696,6 +2043,8 @@ function getWidthClasses(width) {
|
|
|
1696
2043
|
|
|
1697
2044
|
//#endregion
|
|
1698
2045
|
//#region src/components/fields/AddressField.tsx
|
|
2046
|
+
const DEBOUNCE_MS = 200;
|
|
2047
|
+
const MAX_SUGGESTIONS = 5;
|
|
1699
2048
|
/**
|
|
1700
2049
|
* Address input field component specifically designed for blockchain addresses via React Hook Form integration.
|
|
1701
2050
|
*
|
|
@@ -1713,11 +2062,82 @@ function getWidthClasses(width) {
|
|
|
1713
2062
|
* - Chain-agnostic design (validation handled by adapters)
|
|
1714
2063
|
* - Full accessibility support with ARIA attributes
|
|
1715
2064
|
* - Keyboard navigation
|
|
2065
|
+
*
|
|
2066
|
+
* Autocomplete suggestions can be provided in two ways:
|
|
2067
|
+
*
|
|
2068
|
+
* 1. **Context-based (zero-config)**: Mount an `AddressSuggestionProvider` in the
|
|
2069
|
+
* component tree. Every `AddressField` below it automatically resolves suggestions.
|
|
2070
|
+
*
|
|
2071
|
+
* 2. **Prop-based (explicit)**: Pass `suggestions` directly. This overrides context.
|
|
2072
|
+
* Pass `suggestions={false}` to opt out when a provider is mounted.
|
|
2073
|
+
*
|
|
2074
|
+
* The suggestion dropdown includes built-in debouncing, keyboard navigation (Arrow keys,
|
|
2075
|
+
* Enter, Escape), click-outside dismissal, and ARIA listbox semantics.
|
|
1716
2076
|
*/
|
|
1717
|
-
function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, adapter, readOnly }) {
|
|
2077
|
+
function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, adapter, readOnly, suggestions: suggestionsProp, onSuggestionSelect }) {
|
|
1718
2078
|
const isRequired = !!validation?.required;
|
|
1719
2079
|
const errorId = `${id}-error`;
|
|
1720
2080
|
const descriptionId = `${id}-description`;
|
|
2081
|
+
const contextResolver = useContext(AddressSuggestionContext);
|
|
2082
|
+
const containerRef = useRef(null);
|
|
2083
|
+
const lastSetValueRef = useRef("");
|
|
2084
|
+
const [inputValue, setInputValue] = useState("");
|
|
2085
|
+
const [debouncedQuery, setDebouncedQuery] = useState("");
|
|
2086
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
2087
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
2088
|
+
const watchedFieldValue = useWatch({
|
|
2089
|
+
control,
|
|
2090
|
+
name
|
|
2091
|
+
});
|
|
2092
|
+
useEffect(() => {
|
|
2093
|
+
const currentFieldValue = watchedFieldValue ?? "";
|
|
2094
|
+
if (currentFieldValue !== lastSetValueRef.current) {
|
|
2095
|
+
lastSetValueRef.current = currentFieldValue;
|
|
2096
|
+
setInputValue(currentFieldValue);
|
|
2097
|
+
}
|
|
2098
|
+
}, [watchedFieldValue]);
|
|
2099
|
+
useEffect(() => {
|
|
2100
|
+
if (!inputValue.trim()) {
|
|
2101
|
+
setDebouncedQuery("");
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
const timer = setTimeout(() => setDebouncedQuery(inputValue), DEBOUNCE_MS);
|
|
2105
|
+
return () => clearTimeout(timer);
|
|
2106
|
+
}, [inputValue]);
|
|
2107
|
+
const suggestionsDisabled = suggestionsProp === false;
|
|
2108
|
+
const resolvedSuggestions = useMemo(() => {
|
|
2109
|
+
if (suggestionsDisabled) return [];
|
|
2110
|
+
if (Array.isArray(suggestionsProp)) return suggestionsProp;
|
|
2111
|
+
if (!contextResolver || !debouncedQuery.trim()) return [];
|
|
2112
|
+
return contextResolver.resolveSuggestions(debouncedQuery).slice(0, MAX_SUGGESTIONS);
|
|
2113
|
+
}, [
|
|
2114
|
+
suggestionsDisabled,
|
|
2115
|
+
suggestionsProp,
|
|
2116
|
+
contextResolver,
|
|
2117
|
+
debouncedQuery
|
|
2118
|
+
]);
|
|
2119
|
+
const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
|
|
2120
|
+
useEffect(() => {
|
|
2121
|
+
let active = true;
|
|
2122
|
+
const handleClickOutside = (e) => {
|
|
2123
|
+
if (active && containerRef.current && !containerRef.current.contains(e.target)) setShowSuggestions(false);
|
|
2124
|
+
};
|
|
2125
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
2126
|
+
return () => {
|
|
2127
|
+
active = false;
|
|
2128
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
2129
|
+
};
|
|
2130
|
+
}, []);
|
|
2131
|
+
const handleSuggestionKeyDown = useCallback((e) => {
|
|
2132
|
+
if (!hasSuggestions) return;
|
|
2133
|
+
if (e.key === "ArrowDown") {
|
|
2134
|
+
e.preventDefault();
|
|
2135
|
+
setHighlightedIndex((prev) => prev < resolvedSuggestions.length - 1 ? prev + 1 : 0);
|
|
2136
|
+
} else if (e.key === "ArrowUp") {
|
|
2137
|
+
e.preventDefault();
|
|
2138
|
+
setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
|
|
2139
|
+
}
|
|
2140
|
+
}, [hasSuggestions, resolvedSuggestions.length]);
|
|
1721
2141
|
return /* @__PURE__ */ jsxs("div", {
|
|
1722
2142
|
className: `flex flex-col gap-2 ${width === "full" ? "w-full" : width === "half" ? "w-1/2" : "w-1/3"}`,
|
|
1723
2143
|
children: [label && /* @__PURE__ */ jsxs(Label, {
|
|
@@ -1751,9 +2171,32 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
|
|
|
1751
2171
|
const handleInputChange = (e) => {
|
|
1752
2172
|
const value = e.target.value;
|
|
1753
2173
|
field.onChange(value);
|
|
2174
|
+
lastSetValueRef.current = value;
|
|
2175
|
+
setInputValue(value);
|
|
2176
|
+
setShowSuggestions(value.length > 0);
|
|
2177
|
+
setHighlightedIndex(-1);
|
|
2178
|
+
};
|
|
2179
|
+
const applySuggestion = (suggestion) => {
|
|
2180
|
+
field.onChange(suggestion.value);
|
|
2181
|
+
onSuggestionSelect?.(suggestion);
|
|
2182
|
+
lastSetValueRef.current = suggestion.value;
|
|
2183
|
+
setInputValue(suggestion.value);
|
|
2184
|
+
setShowSuggestions(false);
|
|
2185
|
+
setHighlightedIndex(-1);
|
|
1754
2186
|
};
|
|
1755
2187
|
const handleKeyDown = (e) => {
|
|
1756
|
-
if (e.key === "
|
|
2188
|
+
if (hasSuggestions && e.key === "Enter" && highlightedIndex >= 0) {
|
|
2189
|
+
e.preventDefault();
|
|
2190
|
+
applySuggestion(resolvedSuggestions[highlightedIndex]);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
if (e.key === "Escape") {
|
|
2194
|
+
if (hasSuggestions) {
|
|
2195
|
+
setShowSuggestions(false);
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
handleEscapeKey(field.onChange, field.value)(e);
|
|
2199
|
+
}
|
|
1757
2200
|
};
|
|
1758
2201
|
const accessibilityProps = getAccessibilityProps({
|
|
1759
2202
|
id,
|
|
@@ -1762,18 +2205,50 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
|
|
|
1762
2205
|
hasHelperText: !!helperText
|
|
1763
2206
|
});
|
|
1764
2207
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1765
|
-
/* @__PURE__ */
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
2208
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2209
|
+
ref: containerRef,
|
|
2210
|
+
className: "relative",
|
|
2211
|
+
onKeyDown: handleSuggestionKeyDown,
|
|
2212
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
2213
|
+
...field,
|
|
2214
|
+
id,
|
|
2215
|
+
placeholder: placeholder || "0x...",
|
|
2216
|
+
className: validationClasses,
|
|
2217
|
+
onChange: handleInputChange,
|
|
2218
|
+
onKeyDown: handleKeyDown,
|
|
2219
|
+
"data-slot": "input",
|
|
2220
|
+
value: field.value ?? "",
|
|
2221
|
+
...accessibilityProps,
|
|
2222
|
+
"aria-describedby": `${helperText ? descriptionId : ""} ${hasError ? errorId : ""}`,
|
|
2223
|
+
"aria-expanded": hasSuggestions,
|
|
2224
|
+
"aria-autocomplete": suggestionsDisabled ? void 0 : "list",
|
|
2225
|
+
"aria-controls": hasSuggestions ? `${id}-suggestions` : void 0,
|
|
2226
|
+
"aria-activedescendant": hasSuggestions && highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : void 0,
|
|
2227
|
+
disabled: readOnly
|
|
2228
|
+
}), hasSuggestions && /* @__PURE__ */ jsx("div", {
|
|
2229
|
+
id: `${id}-suggestions`,
|
|
2230
|
+
className: cn("absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md", "max-h-48 overflow-auto"),
|
|
2231
|
+
role: "listbox",
|
|
2232
|
+
children: resolvedSuggestions.map((s, i) => /* @__PURE__ */ jsxs("button", {
|
|
2233
|
+
id: `${id}-suggestion-${i}`,
|
|
2234
|
+
type: "button",
|
|
2235
|
+
role: "option",
|
|
2236
|
+
"aria-selected": i === highlightedIndex,
|
|
2237
|
+
className: cn("flex w-full flex-col px-3 py-2 text-left text-sm", "hover:bg-accent", i === highlightedIndex && "bg-accent"),
|
|
2238
|
+
onMouseDown: (e) => {
|
|
2239
|
+
e.preventDefault();
|
|
2240
|
+
applySuggestion(s);
|
|
2241
|
+
},
|
|
2242
|
+
onMouseEnter: () => setHighlightedIndex(i),
|
|
2243
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2244
|
+
className: "font-medium",
|
|
2245
|
+
children: s.label
|
|
2246
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2247
|
+
className: "truncate font-mono text-xs text-muted-foreground",
|
|
2248
|
+
children: s.value
|
|
2249
|
+
})]
|
|
2250
|
+
}, `${s.value}-${s.description ?? i}`))
|
|
2251
|
+
})]
|
|
1777
2252
|
}),
|
|
1778
2253
|
helperText && /* @__PURE__ */ jsx("div", {
|
|
1779
2254
|
id: descriptionId,
|
|
@@ -1792,6 +2267,90 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
|
|
|
1792
2267
|
}
|
|
1793
2268
|
AddressField.displayName = "AddressField";
|
|
1794
2269
|
|
|
2270
|
+
//#endregion
|
|
2271
|
+
//#region src/components/fields/address-suggestion/address-suggestion-context.tsx
|
|
2272
|
+
/**
|
|
2273
|
+
* Address Suggestion Context
|
|
2274
|
+
*
|
|
2275
|
+
* Provides a React context for resolving address autocomplete suggestions.
|
|
2276
|
+
* When an `AddressSuggestionProvider` is mounted, every `AddressField`
|
|
2277
|
+
* in the subtree automatically renders a suggestion dropdown as the user types.
|
|
2278
|
+
*
|
|
2279
|
+
* @example
|
|
2280
|
+
* ```tsx
|
|
2281
|
+
* import { AddressSuggestionProvider } from '@openzeppelin/ui-components';
|
|
2282
|
+
* import { useAliasSuggestionResolver } from '@openzeppelin/ui-storage';
|
|
2283
|
+
*
|
|
2284
|
+
* function App() {
|
|
2285
|
+
* const resolver = useAliasSuggestionResolver(db);
|
|
2286
|
+
* return (
|
|
2287
|
+
* <AddressSuggestionProvider {...resolver}>
|
|
2288
|
+
* <MyApp />
|
|
2289
|
+
* </AddressSuggestionProvider>
|
|
2290
|
+
* );
|
|
2291
|
+
* }
|
|
2292
|
+
* ```
|
|
2293
|
+
*/
|
|
2294
|
+
/**
|
|
2295
|
+
* Provides address suggestion resolution to all `AddressField` instances in the
|
|
2296
|
+
* subtree. Wrap your application (or a subsection) with this provider and
|
|
2297
|
+
* supply a `resolveSuggestions` function.
|
|
2298
|
+
*
|
|
2299
|
+
* @param props - Resolver function and children
|
|
2300
|
+
*
|
|
2301
|
+
* @example
|
|
2302
|
+
* ```tsx
|
|
2303
|
+
* <AddressSuggestionProvider
|
|
2304
|
+
* resolveSuggestions={(query, networkId) => filterAliases(query, networkId)}
|
|
2305
|
+
* >
|
|
2306
|
+
* <App />
|
|
2307
|
+
* </AddressSuggestionProvider>
|
|
2308
|
+
* ```
|
|
2309
|
+
*/
|
|
2310
|
+
function AddressSuggestionProvider({ children, resolveSuggestions }) {
|
|
2311
|
+
const value = React$1.useMemo(() => ({ resolveSuggestions }), [resolveSuggestions]);
|
|
2312
|
+
return /* @__PURE__ */ jsx(AddressSuggestionContext.Provider, {
|
|
2313
|
+
value,
|
|
2314
|
+
children
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
//#endregion
|
|
2319
|
+
//#region src/components/fields/address-suggestion/useAddressSuggestions.ts
|
|
2320
|
+
/**
|
|
2321
|
+
* Convenience hook that resolves suggestions for a query string using the
|
|
2322
|
+
* nearest `AddressSuggestionProvider`. Returns an empty array when no provider
|
|
2323
|
+
* is mounted or when the query is empty.
|
|
2324
|
+
*
|
|
2325
|
+
* @param query - Current input value to match against
|
|
2326
|
+
* @param networkId - Optional network identifier for scoping results
|
|
2327
|
+
* @returns Object containing the resolved suggestions array
|
|
2328
|
+
*
|
|
2329
|
+
* @example
|
|
2330
|
+
* ```tsx
|
|
2331
|
+
* function MyField({ query }: { query: string }) {
|
|
2332
|
+
* const { suggestions } = useAddressSuggestions(query, 'ethereum-mainnet');
|
|
2333
|
+
* return (
|
|
2334
|
+
* <ul>
|
|
2335
|
+
* {suggestions.map(s => <li key={s.value}>{s.label}</li>)}
|
|
2336
|
+
* </ul>
|
|
2337
|
+
* );
|
|
2338
|
+
* }
|
|
2339
|
+
* ```
|
|
2340
|
+
*/
|
|
2341
|
+
/** Resolves address suggestions from the nearest `AddressSuggestionProvider`. */
|
|
2342
|
+
function useAddressSuggestions(query, networkId) {
|
|
2343
|
+
const resolver = React$1.useContext(AddressSuggestionContext);
|
|
2344
|
+
return { suggestions: React$1.useMemo(() => {
|
|
2345
|
+
if (!resolver || !query.trim()) return [];
|
|
2346
|
+
return resolver.resolveSuggestions(query, networkId);
|
|
2347
|
+
}, [
|
|
2348
|
+
resolver,
|
|
2349
|
+
query,
|
|
2350
|
+
networkId
|
|
2351
|
+
]) };
|
|
2352
|
+
}
|
|
2353
|
+
|
|
1795
2354
|
//#endregion
|
|
1796
2355
|
//#region src/components/fields/AmountField.tsx
|
|
1797
2356
|
/**
|
|
@@ -5161,5 +5720,5 @@ const Toaster = ({ ...props }) => {
|
|
|
5161
5720
|
};
|
|
5162
5721
|
|
|
5163
5722
|
//#endregion
|
|
5164
|
-
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AddressDisplay, AddressField, Alert, AlertDescription, AlertTitle, AmountField, ArrayField, ArrayObjectField, Banner, BaseField, BigIntField, BooleanField, Button, BytesField, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DateRangePicker, DateTimeField, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, EnumField, ErrorMessage, ExternalLink, FileUploadField, Footer, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Header, INTEGER_HTML_PATTERN, INTEGER_INPUT_PATTERN, INTEGER_PATTERN, Input, Label, LoadingButton, MapEntryRow, MapField, MidnightIcon, NetworkErrorNotificationProvider, NetworkIcon, NetworkSelector, NetworkServiceErrorBanner, NetworkStatusBadge, NumberField, ObjectField, PasswordField, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, RadioField, RadioGroup, RadioGroupItem, RelayerDetailsCard, Select, SelectContent, SelectField, SelectGroup, SelectGroupedField, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SidebarButton, SidebarGroup, SidebarLayout, SidebarSection, Tabs, TabsContent, TabsList, TabsTrigger, TextAreaField, TextField, Textarea, Toaster, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UrlField, ViewContractStateButton, buttonVariants, computeChildTouched, createFocusManager, createValidationResult, formatValidationError, getAccessibilityProps, getDescribedById, getErrorMessage, getValidationStateClasses, getWidthClasses, handleEscapeKey, handleKeyboardEvent, handleNumericKeys, handleToggleKeys, handleValidationError, hasFieldError, isDuplicateMapKey, useDuplicateKeyIndexes, useMapFieldSync, useNetworkErrorAwareAdapter, useNetworkErrorReporter, useNetworkErrors, validateField, validateMapEntries, validateMapStructure };
|
|
5723
|
+
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AddressDisplay, AddressField, AddressLabelProvider, AddressSuggestionProvider, Alert, AlertDescription, AlertTitle, AmountField, ArrayField, ArrayObjectField, Banner, BaseField, BigIntField, BooleanField, Button, BytesField, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DateRangePicker, DateTimeField, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EcosystemDropdown, EcosystemIcon, EmptyState, EnumField, ErrorMessage, ExternalLink, FileUploadField, Footer, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Header, INTEGER_HTML_PATTERN, INTEGER_INPUT_PATTERN, INTEGER_PATTERN, Input, Label, LoadingButton, MapEntryRow, MapField, MidnightIcon, NetworkErrorNotificationProvider, NetworkIcon, NetworkSelector, NetworkServiceErrorBanner, NetworkStatusBadge, NumberField, ObjectField, OverflowMenu, PasswordField, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, RadioField, RadioGroup, RadioGroupItem, RelayerDetailsCard, Select, SelectContent, SelectField, SelectGroup, SelectGroupedField, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SidebarButton, SidebarGroup, SidebarLayout, SidebarSection, Tabs, TabsContent, TabsList, TabsTrigger, TextAreaField, TextField, Textarea, Toaster, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UrlField, ViewContractStateButton, buttonVariants, computeChildTouched, createFocusManager, createValidationResult, formatValidationError, getAccessibilityProps, getDescribedById, getErrorMessage, getValidationStateClasses, getWidthClasses, handleEscapeKey, handleKeyboardEvent, handleNumericKeys, handleToggleKeys, handleValidationError, hasFieldError, isDuplicateMapKey, useAddressLabel, useAddressSuggestions, useDuplicateKeyIndexes, useMapFieldSync, useNetworkErrorAwareAdapter, useNetworkErrorReporter, useNetworkErrors, validateField, validateMapEntries, validateMapStructure };
|
|
5165
5724
|
//# sourceMappingURL=index.mjs.map
|