@openzeppelin/ui-components 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/index.cjs +1080 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +349 -12
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +355 -18
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1075 -60
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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
|
-
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import React, { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { cn, getDefaultValueForType, getInvalidUrlMessage, getServiceDisplayName, isValidUrl, truncateMiddle, validateBytesSimple } from "@openzeppelin/ui-utils";
|
|
8
8
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
import { Slot, Slottable } from "@radix-ui/react-slot";
|
|
@@ -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]
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/components/ui/address-display/address-label-context.tsx
|
|
227
|
+
/**
|
|
228
|
+
* Address Label Context
|
|
229
|
+
*
|
|
230
|
+
* Provides a React context for resolving human-readable labels for blockchain
|
|
231
|
+
* addresses. When an `AddressLabelProvider` is mounted, every `AddressDisplay`
|
|
232
|
+
* in the subtree automatically resolves and renders labels without any
|
|
233
|
+
* call-site changes.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```tsx
|
|
237
|
+
* import { AddressLabelProvider } from '@openzeppelin/ui-components';
|
|
238
|
+
*
|
|
239
|
+
* function App() {
|
|
240
|
+
* const resolver = useAliasLabelResolver(db);
|
|
241
|
+
* return (
|
|
242
|
+
* <AddressLabelProvider {...resolver}>
|
|
243
|
+
* <MyApp />
|
|
244
|
+
* </AddressLabelProvider>
|
|
245
|
+
* );
|
|
246
|
+
* }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
/**
|
|
250
|
+
* Provides address label resolution to all `AddressDisplay` instances in the
|
|
251
|
+
* subtree. Wrap your application (or a subsection) with this provider and
|
|
252
|
+
* supply a `resolveLabel` function.
|
|
253
|
+
*
|
|
254
|
+
* @param props - Resolver functions and children
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```tsx
|
|
258
|
+
* <AddressLabelProvider
|
|
259
|
+
* resolveLabel={(addr) => addressBook.get(addr)}
|
|
260
|
+
* onEditLabel={(addr) => openEditor(addr)}
|
|
261
|
+
* >
|
|
262
|
+
* <App />
|
|
263
|
+
* </AddressLabelProvider>
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
function AddressLabelProvider({ children, resolveLabel, onEditLabel }) {
|
|
267
|
+
const value = React$1.useMemo(() => ({
|
|
268
|
+
resolveLabel,
|
|
269
|
+
onEditLabel
|
|
270
|
+
}), [resolveLabel, onEditLabel]);
|
|
271
|
+
return /* @__PURE__ */ jsx(AddressLabelContext.Provider, {
|
|
272
|
+
value,
|
|
273
|
+
children
|
|
152
274
|
});
|
|
153
275
|
}
|
|
154
276
|
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/components/ui/address-display/use-address-label.ts
|
|
279
|
+
/**
|
|
280
|
+
* Convenience hook for resolving an address label from the nearest
|
|
281
|
+
* `AddressLabelProvider`.
|
|
282
|
+
*
|
|
283
|
+
* Kept in its own file so that `address-label-context.tsx` exports only
|
|
284
|
+
* components (required by React Fast Refresh).
|
|
285
|
+
*/
|
|
286
|
+
/**
|
|
287
|
+
* Convenience hook that resolves a label for a specific address using the
|
|
288
|
+
* nearest `AddressLabelProvider`. Returns `undefined` values when no provider
|
|
289
|
+
* is mounted.
|
|
290
|
+
*
|
|
291
|
+
* @param address - The blockchain address to resolve
|
|
292
|
+
* @param networkId - Optional network identifier for network-specific aliases
|
|
293
|
+
* @returns Resolved label and edit handler for the address
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```tsx
|
|
297
|
+
* function MyAddress({ address }: { address: string }) {
|
|
298
|
+
* const { label, onEdit } = useAddressLabel(address, 'ethereum-mainnet');
|
|
299
|
+
* return <span>{label ?? address}</span>;
|
|
300
|
+
* }
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
function useAddressLabel(address, networkId) {
|
|
304
|
+
const resolver = React$1.useContext(AddressLabelContext);
|
|
305
|
+
const label = resolver?.resolveLabel(address, networkId);
|
|
306
|
+
const onEdit = React$1.useCallback(() => {
|
|
307
|
+
resolver?.onEditLabel?.(address, networkId);
|
|
308
|
+
}, [
|
|
309
|
+
resolver,
|
|
310
|
+
address,
|
|
311
|
+
networkId
|
|
312
|
+
]);
|
|
313
|
+
return {
|
|
314
|
+
label,
|
|
315
|
+
onEdit: resolver?.onEditLabel ? onEdit : void 0
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
155
319
|
//#endregion
|
|
156
320
|
//#region src/components/ui/alert.tsx
|
|
157
321
|
const alertVariants = cva("relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", {
|
|
@@ -1085,10 +1249,15 @@ function NetworkIcon({ network, className, size = 16, variant = "branded" }) {
|
|
|
1085
1249
|
|
|
1086
1250
|
//#endregion
|
|
1087
1251
|
//#region src/components/ui/network-selector.tsx
|
|
1088
|
-
/** Searchable dropdown selector for blockchain networks with optional grouping. */
|
|
1089
|
-
function NetworkSelector({ networks,
|
|
1252
|
+
/** Searchable dropdown selector for blockchain networks with optional grouping and multi-select. */
|
|
1253
|
+
function NetworkSelector({ networks, getNetworkLabel, getNetworkIcon, getNetworkType, getNetworkId, groupByEcosystem = false, getEcosystem, filterNetwork, className, placeholder = "Select Network", ...modeProps }) {
|
|
1090
1254
|
const [open, setOpen] = React$1.useState(false);
|
|
1091
1255
|
const [searchQuery, setSearchQuery] = React$1.useState("");
|
|
1256
|
+
const isMultiple = modeProps.multiple === true;
|
|
1257
|
+
const selectedNetworkIds = isMultiple ? modeProps.selectedNetworkIds : void 0;
|
|
1258
|
+
const onSelectionChange = isMultiple ? modeProps.onSelectionChange : void 0;
|
|
1259
|
+
const selectedNetwork = !isMultiple ? modeProps.selectedNetwork : void 0;
|
|
1260
|
+
const onSelectNetwork = !isMultiple ? modeProps.onSelectNetwork : void 0;
|
|
1092
1261
|
const filteredNetworks = React$1.useMemo(() => {
|
|
1093
1262
|
if (!searchQuery) return networks;
|
|
1094
1263
|
if (filterNetwork) return networks.filter((n) => filterNetwork(n, searchQuery));
|
|
@@ -1112,34 +1281,79 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
|
|
|
1112
1281
|
groupByEcosystem,
|
|
1113
1282
|
getEcosystem
|
|
1114
1283
|
]);
|
|
1284
|
+
const isSelected = React$1.useCallback((network) => {
|
|
1285
|
+
if (isMultiple && selectedNetworkIds) return selectedNetworkIds.includes(getNetworkId(network));
|
|
1286
|
+
return selectedNetwork ? getNetworkId(selectedNetwork) === getNetworkId(network) : false;
|
|
1287
|
+
}, [
|
|
1288
|
+
isMultiple,
|
|
1289
|
+
selectedNetworkIds,
|
|
1290
|
+
selectedNetwork,
|
|
1291
|
+
getNetworkId
|
|
1292
|
+
]);
|
|
1293
|
+
const handleSelect = React$1.useCallback((network) => {
|
|
1294
|
+
if (isMultiple && selectedNetworkIds && onSelectionChange) {
|
|
1295
|
+
const id = getNetworkId(network);
|
|
1296
|
+
onSelectionChange(selectedNetworkIds.includes(id) ? selectedNetworkIds.filter((x) => x !== id) : [...selectedNetworkIds, id]);
|
|
1297
|
+
} else if (onSelectNetwork) {
|
|
1298
|
+
onSelectNetwork(network);
|
|
1299
|
+
setOpen(false);
|
|
1300
|
+
}
|
|
1301
|
+
}, [
|
|
1302
|
+
isMultiple,
|
|
1303
|
+
selectedNetworkIds,
|
|
1304
|
+
onSelectionChange,
|
|
1305
|
+
onSelectNetwork,
|
|
1306
|
+
getNetworkId
|
|
1307
|
+
]);
|
|
1308
|
+
const handleClearAll = React$1.useCallback(() => {
|
|
1309
|
+
if (isMultiple && onSelectionChange) onSelectionChange([]);
|
|
1310
|
+
}, [isMultiple, onSelectionChange]);
|
|
1311
|
+
const selectedCount = selectedNetworkIds?.length ?? 0;
|
|
1312
|
+
const renderTrigger = isMultiple ? modeProps.renderTrigger : void 0;
|
|
1115
1313
|
return /* @__PURE__ */ jsxs(DropdownMenu, {
|
|
1116
1314
|
open,
|
|
1117
1315
|
onOpenChange: setOpen,
|
|
1118
1316
|
children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
1119
1317
|
asChild: true,
|
|
1120
|
-
children:
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1318
|
+
children: (() => {
|
|
1319
|
+
if (isMultiple && renderTrigger) return renderTrigger({
|
|
1320
|
+
selectedCount,
|
|
1321
|
+
open
|
|
1322
|
+
});
|
|
1323
|
+
if (isMultiple) return /* @__PURE__ */ jsxs(Button, {
|
|
1324
|
+
variant: "outline",
|
|
1325
|
+
role: "combobox",
|
|
1326
|
+
"aria-expanded": open,
|
|
1327
|
+
className: cn("w-full justify-between", className),
|
|
1328
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1329
|
+
className: "truncate text-muted-foreground",
|
|
1330
|
+
children: selectedCount > 0 ? `${selectedCount} network${selectedCount > 1 ? "s" : ""} selected` : placeholder
|
|
1331
|
+
}), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
|
|
1332
|
+
});
|
|
1333
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
1334
|
+
variant: "outline",
|
|
1335
|
+
role: "combobox",
|
|
1336
|
+
"aria-expanded": open,
|
|
1337
|
+
className: cn("w-full justify-between", className),
|
|
1338
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1339
|
+
className: "flex items-center gap-2 truncate",
|
|
1340
|
+
children: selectedNetwork ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1341
|
+
getNetworkIcon?.(selectedNetwork),
|
|
1342
|
+
/* @__PURE__ */ jsx("span", {
|
|
1343
|
+
className: "truncate",
|
|
1344
|
+
children: getNetworkLabel(selectedNetwork)
|
|
1345
|
+
}),
|
|
1346
|
+
getNetworkType && /* @__PURE__ */ jsx("span", {
|
|
1347
|
+
className: "shrink-0 rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase text-muted-foreground",
|
|
1348
|
+
children: getNetworkType(selectedNetwork)
|
|
1349
|
+
})
|
|
1350
|
+
] }) : /* @__PURE__ */ jsx("span", {
|
|
1351
|
+
className: "text-muted-foreground",
|
|
1352
|
+
children: placeholder
|
|
1136
1353
|
})
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
})
|
|
1141
|
-
}), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
|
|
1142
|
-
})
|
|
1354
|
+
}), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
|
|
1355
|
+
});
|
|
1356
|
+
})()
|
|
1143
1357
|
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
1144
1358
|
className: "w-[--radix-dropdown-menu-trigger-width] min-w-[240px] p-0",
|
|
1145
1359
|
align: "start",
|
|
@@ -1155,9 +1369,19 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
|
|
|
1155
1369
|
className: "h-9 w-full border-0 bg-transparent p-0 placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0",
|
|
1156
1370
|
"aria-label": "Search networks"
|
|
1157
1371
|
})]
|
|
1158
|
-
}), /* @__PURE__ */
|
|
1372
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1159
1373
|
className: "max-h-[300px] overflow-y-auto p-1",
|
|
1160
|
-
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", {
|
|
1161
1385
|
className: "py-6 text-center text-sm text-muted-foreground",
|
|
1162
1386
|
children: "No network found."
|
|
1163
1387
|
}) : Object.entries(groupedNetworks).map(([group, groupNetworks], index) => /* @__PURE__ */ jsxs(React$1.Fragment, { children: [
|
|
@@ -1166,12 +1390,16 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
|
|
|
1166
1390
|
children: group
|
|
1167
1391
|
}),
|
|
1168
1392
|
/* @__PURE__ */ jsx(DropdownMenuGroup, { children: groupNetworks.map((network) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
1169
|
-
onSelect: () => {
|
|
1170
|
-
|
|
1171
|
-
|
|
1393
|
+
onSelect: (e) => {
|
|
1394
|
+
if (isMultiple) e.preventDefault();
|
|
1395
|
+
handleSelect(network);
|
|
1172
1396
|
},
|
|
1173
1397
|
className: "gap-2",
|
|
1174
1398
|
children: [
|
|
1399
|
+
isMultiple ? /* @__PURE__ */ jsx("div", {
|
|
1400
|
+
className: "flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border border-primary",
|
|
1401
|
+
children: isSelected(network) && /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" })
|
|
1402
|
+
}) : null,
|
|
1175
1403
|
getNetworkIcon?.(network),
|
|
1176
1404
|
/* @__PURE__ */ jsxs("div", {
|
|
1177
1405
|
className: "flex flex-1 items-center gap-2 min-w-0",
|
|
@@ -1183,11 +1411,11 @@ function NetworkSelector({ networks, selectedNetwork, onSelectNetwork, getNetwor
|
|
|
1183
1411
|
children: getNetworkType(network)
|
|
1184
1412
|
})]
|
|
1185
1413
|
}),
|
|
1186
|
-
|
|
1414
|
+
!isMultiple && isSelected(network) && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 opacity-100" })
|
|
1187
1415
|
]
|
|
1188
1416
|
}, getNetworkId(network))) }),
|
|
1189
1417
|
index < Object.keys(groupedNetworks).length - 1 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {})
|
|
1190
|
-
] }, group))
|
|
1418
|
+
] }, group))]
|
|
1191
1419
|
})]
|
|
1192
1420
|
})]
|
|
1193
1421
|
});
|
|
@@ -1220,6 +1448,32 @@ function NetworkStatusBadge({ network, className }) {
|
|
|
1220
1448
|
});
|
|
1221
1449
|
}
|
|
1222
1450
|
|
|
1451
|
+
//#endregion
|
|
1452
|
+
//#region src/components/ui/overflow-menu.tsx
|
|
1453
|
+
/** Compact "..." dropdown menu for secondary actions. */
|
|
1454
|
+
function OverflowMenu({ items, align = "end", className, "aria-label": ariaLabel = "More actions" }) {
|
|
1455
|
+
if (items.length === 0) return null;
|
|
1456
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
1457
|
+
asChild: true,
|
|
1458
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
1459
|
+
variant: "ghost",
|
|
1460
|
+
size: "icon",
|
|
1461
|
+
className: cn("h-8 w-8", className),
|
|
1462
|
+
"aria-label": ariaLabel,
|
|
1463
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" })
|
|
1464
|
+
})
|
|
1465
|
+
}), /* @__PURE__ */ jsx(DropdownMenuContent, {
|
|
1466
|
+
align,
|
|
1467
|
+
className: "min-w-[140px]",
|
|
1468
|
+
children: items.map((item, index) => /* @__PURE__ */ jsxs(React$1.Fragment, { children: [item.destructive && index > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}), /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
1469
|
+
onClick: item.onSelect,
|
|
1470
|
+
disabled: item.disabled,
|
|
1471
|
+
className: cn(item.destructive && "text-destructive focus:text-destructive"),
|
|
1472
|
+
children: [item.icon, item.label]
|
|
1473
|
+
})] }, item.id))
|
|
1474
|
+
})] });
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1223
1477
|
//#endregion
|
|
1224
1478
|
//#region src/components/ui/progress.tsx
|
|
1225
1479
|
const Progress = React$1.forwardRef(({ className, value, ...props }, ref) => /* @__PURE__ */ jsx(ProgressPrimitive.Root, {
|
|
@@ -1339,12 +1593,12 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|
|
1339
1593
|
* Can render as a button or anchor element depending on whether href is provided.
|
|
1340
1594
|
*/
|
|
1341
1595
|
function SidebarButton({ icon, children, onClick, size = "default", badge, disabled = false, isSelected = false, href, target, rel, className }) {
|
|
1342
|
-
const commonClass = cn("group relative flex items-center gap-2 px-3 py-2
|
|
1596
|
+
const commonClass = cn("group relative flex flex-wrap items-center gap-x-2 gap-y-0.5 px-3 py-2 rounded-lg font-semibold text-sm transition-colors", badge ? "justify-between" : "justify-start", disabled ? "text-gray-400 cursor-not-allowed" : isSelected ? "text-[#111928] bg-neutral-100" : "text-gray-600 hover:text-gray-700 cursor-pointer hover:before:content-[\"\"] hover:before:absolute hover:before:inset-x-0 hover:before:top-1 hover:before:bottom-1 hover:before:bg-muted/80 hover:before:rounded-lg hover:before:-z-10", size === "small" ? "min-h-10" : "min-h-11", className);
|
|
1343
1597
|
const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
1344
1598
|
className: "flex items-center gap-2",
|
|
1345
1599
|
children: [icon, children]
|
|
1346
1600
|
}), badge && /* @__PURE__ */ jsx("span", {
|
|
1347
|
-
className: "text-xs px-2 py-
|
|
1601
|
+
className: "text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded-full font-medium",
|
|
1348
1602
|
children: badge
|
|
1349
1603
|
})] });
|
|
1350
1604
|
if (href) return /* @__PURE__ */ jsx("a", {
|
|
@@ -1589,6 +1843,555 @@ function ViewContractStateButton({ contractAddress, onToggle }) {
|
|
|
1589
1843
|
});
|
|
1590
1844
|
}
|
|
1591
1845
|
|
|
1846
|
+
//#endregion
|
|
1847
|
+
//#region src/components/ui/wizard/WizardStepper.tsx
|
|
1848
|
+
function resolveState(step, index, currentStepIndex, furthestStepIndex) {
|
|
1849
|
+
if (step.status === "completed" || step.status === "skipped") return "completed";
|
|
1850
|
+
if (index === currentStepIndex) return "current";
|
|
1851
|
+
if (step.isInvalid && (index < currentStepIndex || index <= furthestStepIndex)) return "invalid";
|
|
1852
|
+
if (index < currentStepIndex) return "completed";
|
|
1853
|
+
if (index <= furthestStepIndex) return "visited";
|
|
1854
|
+
return "upcoming";
|
|
1855
|
+
}
|
|
1856
|
+
function canClick(state, freeNavigation = false) {
|
|
1857
|
+
if (freeNavigation) return true;
|
|
1858
|
+
return state !== "upcoming";
|
|
1859
|
+
}
|
|
1860
|
+
function StepCircle({ state, index }) {
|
|
1861
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1862
|
+
className: cn("flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold transition-all", state === "completed" && "bg-blue-600 text-white", state === "current" && "bg-blue-600 text-white ring-2 ring-blue-200", state === "visited" && "bg-blue-100 text-blue-600 ring-1 ring-blue-300", state === "invalid" && "bg-red-100 text-red-600 ring-1 ring-red-300", state === "upcoming" && "bg-zinc-100 text-zinc-400"),
|
|
1863
|
+
children: state === "completed" ? /* @__PURE__ */ jsx(Check, { className: "size-3.5" }) : state === "visited" ? /* @__PURE__ */ jsx(Pencil, { className: "size-3" }) : state === "invalid" ? /* @__PURE__ */ jsx(AlertCircle, { className: "size-3.5" }) : index + 1
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
function StepLabel({ title, state, isSkipped }) {
|
|
1867
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1868
|
+
className: "min-w-0 flex-1",
|
|
1869
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1870
|
+
className: cn("text-sm font-medium transition-colors", state === "current" && "text-blue-700", state === "completed" && "text-zinc-700", state === "visited" && "text-blue-600", state === "invalid" && "text-red-600", state === "upcoming" && "text-zinc-400"),
|
|
1871
|
+
children: title
|
|
1872
|
+
}), isSkipped && /* @__PURE__ */ jsx("span", {
|
|
1873
|
+
className: "mt-0.5 block text-[11px] text-zinc-400",
|
|
1874
|
+
children: "Skipped"
|
|
1875
|
+
})]
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
function VerticalStepper({ steps, currentStepIndex, furthestStepIndex = currentStepIndex, onStepClick, freeNavigation, className }) {
|
|
1879
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
1880
|
+
"aria-label": "Wizard steps",
|
|
1881
|
+
className: cn("rounded-2xl border border-zinc-200 bg-white p-6", className),
|
|
1882
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1883
|
+
className: "flex flex-col gap-1",
|
|
1884
|
+
children: steps.map((step, index) => {
|
|
1885
|
+
const state = resolveState(step, index, currentStepIndex, furthestStepIndex);
|
|
1886
|
+
const clickable = canClick(state, freeNavigation) && !!onStepClick;
|
|
1887
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
1888
|
+
type: "button",
|
|
1889
|
+
onClick: () => clickable && onStepClick?.(index),
|
|
1890
|
+
disabled: !clickable,
|
|
1891
|
+
className: cn("flex items-center gap-3 rounded-xl border border-transparent px-3 py-3 text-left transition-all duration-150", clickable ? "cursor-pointer" : "cursor-not-allowed opacity-50", state === "current" && "border-blue-200 bg-blue-50", state === "completed" && "bg-white hover:bg-gray-50", state === "visited" && "bg-white hover:bg-blue-50/50", state === "invalid" && "border-red-200 bg-red-50 hover:bg-red-100/60", state === "upcoming" && "bg-white"),
|
|
1892
|
+
"aria-current": state === "current" ? "step" : void 0,
|
|
1893
|
+
children: [/* @__PURE__ */ jsx(StepCircle, {
|
|
1894
|
+
state,
|
|
1895
|
+
index
|
|
1896
|
+
}), /* @__PURE__ */ jsx(StepLabel, {
|
|
1897
|
+
title: step.title,
|
|
1898
|
+
state,
|
|
1899
|
+
isSkipped: step.status === "skipped"
|
|
1900
|
+
})]
|
|
1901
|
+
}, step.id);
|
|
1902
|
+
})
|
|
1903
|
+
})
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
function HorizontalStepper({ steps, currentStepIndex, furthestStepIndex = currentStepIndex, onStepClick, freeNavigation, className }) {
|
|
1907
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
1908
|
+
"aria-label": "Wizard steps",
|
|
1909
|
+
className: cn("rounded-2xl border border-zinc-200 bg-white p-6", className),
|
|
1910
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1911
|
+
className: "flex w-full items-center",
|
|
1912
|
+
children: steps.map((step, index) => {
|
|
1913
|
+
const state = resolveState(step, index, currentStepIndex, furthestStepIndex);
|
|
1914
|
+
const clickable = canClick(state, freeNavigation) && !!onStepClick;
|
|
1915
|
+
const isLast = index === steps.length - 1;
|
|
1916
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsxs("button", {
|
|
1917
|
+
type: "button",
|
|
1918
|
+
onClick: () => clickable && onStepClick?.(index),
|
|
1919
|
+
disabled: !clickable,
|
|
1920
|
+
className: cn("flex items-center gap-2 rounded-xl border border-transparent px-3 py-2 text-left transition-all duration-150", clickable ? "cursor-pointer" : "cursor-not-allowed opacity-50", state === "current" && "border-blue-200 bg-blue-50", state === "completed" && "bg-white hover:bg-gray-50", state === "visited" && "bg-white hover:bg-blue-50/50", state === "invalid" && "border-red-200 bg-red-50 hover:bg-red-100/60", state === "upcoming" && "bg-white"),
|
|
1921
|
+
"aria-current": state === "current" ? "step" : void 0,
|
|
1922
|
+
"aria-label": `Step ${index + 1}: ${step.title}`,
|
|
1923
|
+
children: [/* @__PURE__ */ jsx(StepCircle, {
|
|
1924
|
+
state,
|
|
1925
|
+
index
|
|
1926
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1927
|
+
className: "hidden sm:block",
|
|
1928
|
+
children: /* @__PURE__ */ jsx(StepLabel, {
|
|
1929
|
+
title: step.title,
|
|
1930
|
+
state,
|
|
1931
|
+
isSkipped: step.status === "skipped"
|
|
1932
|
+
})
|
|
1933
|
+
})]
|
|
1934
|
+
}), !isLast && /* @__PURE__ */ jsx("div", { className: cn("mx-1 h-px flex-1 transition-colors sm:mx-2", index < currentStepIndex ? "bg-blue-600" : "bg-zinc-200") })] }, step.id);
|
|
1935
|
+
})
|
|
1936
|
+
})
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* A stepper component for navigating through a series of steps.
|
|
1941
|
+
*
|
|
1942
|
+
* @param props - The props for the WizardStepper component.
|
|
1943
|
+
* @returns A React node representing the stepper component.
|
|
1944
|
+
*/
|
|
1945
|
+
function WizardStepper(props) {
|
|
1946
|
+
const { variant = "horizontal", ...rest } = props;
|
|
1947
|
+
return variant === "vertical" ? /* @__PURE__ */ jsx(VerticalStepper, {
|
|
1948
|
+
...rest,
|
|
1949
|
+
variant
|
|
1950
|
+
}) : /* @__PURE__ */ jsx(HorizontalStepper, {
|
|
1951
|
+
...rest,
|
|
1952
|
+
variant
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
//#endregion
|
|
1957
|
+
//#region src/components/ui/wizard/WizardNavigation.tsx
|
|
1958
|
+
/**
|
|
1959
|
+
* A navigation component for the wizard.
|
|
1960
|
+
*
|
|
1961
|
+
* @param props - The props for the WizardNavigation component.
|
|
1962
|
+
* @returns A React node representing the navigation component.
|
|
1963
|
+
*/
|
|
1964
|
+
function WizardNavigation({ isFirstStep, isLastStep, canProceed = true, onPrevious, onNext, onCancel, extraActions, nextLabel = "Next", lastStepLabel = "Finish", className }) {
|
|
1965
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1966
|
+
className: cn("flex items-center justify-between", className),
|
|
1967
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1968
|
+
className: "flex gap-2",
|
|
1969
|
+
children: [onCancel && /* @__PURE__ */ jsxs(Button, {
|
|
1970
|
+
type: "button",
|
|
1971
|
+
variant: "outline",
|
|
1972
|
+
onClick: onCancel,
|
|
1973
|
+
className: "gap-2",
|
|
1974
|
+
children: [/* @__PURE__ */ jsx(X, { className: "size-4" }), "Cancel"]
|
|
1975
|
+
}), !isFirstStep && /* @__PURE__ */ jsxs(Button, {
|
|
1976
|
+
type: "button",
|
|
1977
|
+
variant: "outline",
|
|
1978
|
+
onClick: onPrevious,
|
|
1979
|
+
className: "gap-2",
|
|
1980
|
+
children: [/* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" }), "Previous"]
|
|
1981
|
+
})]
|
|
1982
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1983
|
+
className: "flex gap-2",
|
|
1984
|
+
children: [extraActions, /* @__PURE__ */ jsxs(Button, {
|
|
1985
|
+
type: "button",
|
|
1986
|
+
onClick: onNext,
|
|
1987
|
+
disabled: !canProceed,
|
|
1988
|
+
className: "gap-2",
|
|
1989
|
+
children: [isLastStep ? lastStepLabel : nextLabel, !isLastStep && /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })]
|
|
1990
|
+
})]
|
|
1991
|
+
})]
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
//#endregion
|
|
1996
|
+
//#region src/components/ui/wizard/hooks.ts
|
|
1997
|
+
/**
|
|
1998
|
+
* Clamp a step index into the valid range for the current wizard.
|
|
1999
|
+
*/
|
|
2000
|
+
function getSafeStepIndex(stepCount, currentStepIndex) {
|
|
2001
|
+
if (stepCount === 0) return 0;
|
|
2002
|
+
return Math.max(0, Math.min(currentStepIndex, stepCount - 1));
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Track the highest step reached unless a controlled value is provided.
|
|
2006
|
+
*/
|
|
2007
|
+
function useFurthestStepIndex(currentStepIndex, controlledFurthestStepIndex) {
|
|
2008
|
+
const [internalFurthestStepIndex, setInternalFurthestStepIndex] = useState(currentStepIndex);
|
|
2009
|
+
useEffect(() => {
|
|
2010
|
+
setInternalFurthestStepIndex((prev) => Math.max(prev, currentStepIndex));
|
|
2011
|
+
}, [currentStepIndex]);
|
|
2012
|
+
return controlledFurthestStepIndex ?? internalFurthestStepIndex;
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Keep the scrollable wizard's active and visited step state in sync with scrolling and clicks.
|
|
2016
|
+
*/
|
|
2017
|
+
function useScrollableWizardStepTracking({ steps, currentStepIndex, onStepChange, scrollRef, sectionId, scrollPadding = SCROLL_PADDING_PX }) {
|
|
2018
|
+
const safeIndex = getSafeStepIndex(steps.length, currentStepIndex);
|
|
2019
|
+
const initialIndexRef = useRef(safeIndex);
|
|
2020
|
+
const rafRef = useRef(null);
|
|
2021
|
+
const manualSelectionIndexRef = useRef(null);
|
|
2022
|
+
const stepsRef = useRef(steps);
|
|
2023
|
+
const sectionIdRef = useRef(sectionId);
|
|
2024
|
+
const onStepChangeRef = useRef(onStepChange);
|
|
2025
|
+
const scrollPaddingRef = useRef(scrollPadding);
|
|
2026
|
+
useEffect(() => {
|
|
2027
|
+
stepsRef.current = steps;
|
|
2028
|
+
sectionIdRef.current = sectionId;
|
|
2029
|
+
onStepChangeRef.current = onStepChange;
|
|
2030
|
+
scrollPaddingRef.current = scrollPadding;
|
|
2031
|
+
});
|
|
2032
|
+
const [activeIndex, setActiveIndex] = useState(initialIndexRef.current);
|
|
2033
|
+
const activeIndexRef = useRef(initialIndexRef.current);
|
|
2034
|
+
const [furthestStepIndex, setFurthestStepIndex] = useState(initialIndexRef.current);
|
|
2035
|
+
const isMountedRef = useRef(false);
|
|
2036
|
+
const clearManualSelection = useCallback(() => {
|
|
2037
|
+
manualSelectionIndexRef.current = null;
|
|
2038
|
+
}, []);
|
|
2039
|
+
useEffect(() => {
|
|
2040
|
+
const container = scrollRef.current;
|
|
2041
|
+
if (!container) return;
|
|
2042
|
+
const ownerDocument = container.ownerDocument;
|
|
2043
|
+
isMountedRef.current = false;
|
|
2044
|
+
let didCompleteInitialRaf = false;
|
|
2045
|
+
const releaseManualSelectionOnUserScroll = () => {
|
|
2046
|
+
clearManualSelection();
|
|
2047
|
+
};
|
|
2048
|
+
const handleKeyDown = (event) => {
|
|
2049
|
+
if (isScrollableNavigationKey(event)) clearManualSelection();
|
|
2050
|
+
};
|
|
2051
|
+
const handleScroll = () => {
|
|
2052
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
2053
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
2054
|
+
const currentSteps = stepsRef.current;
|
|
2055
|
+
const currentSectionId = sectionIdRef.current;
|
|
2056
|
+
const currentOnStepChange = onStepChangeRef.current;
|
|
2057
|
+
if (currentSteps.length === 0) return;
|
|
2058
|
+
const manualSelectionIndex = manualSelectionIndexRef.current;
|
|
2059
|
+
const naturalState = resolveScrollableActiveIndex(container, currentSteps, currentSectionId);
|
|
2060
|
+
const naturalActiveIndex = naturalState.activeIndex;
|
|
2061
|
+
const newActiveIndex = manualSelectionIndex ?? naturalActiveIndex;
|
|
2062
|
+
const shouldCommitFurthestStepIndex = manualSelectionIndex !== null ? true : naturalState.commitFurthestStepIndex;
|
|
2063
|
+
if (activeIndexRef.current !== newActiveIndex) {
|
|
2064
|
+
activeIndexRef.current = newActiveIndex;
|
|
2065
|
+
setActiveIndex(newActiveIndex);
|
|
2066
|
+
if (isMountedRef.current) {
|
|
2067
|
+
lastEmittedIndexRef.current = newActiveIndex;
|
|
2068
|
+
currentOnStepChange(newActiveIndex);
|
|
2069
|
+
}
|
|
2070
|
+
} else setActiveIndex(newActiveIndex);
|
|
2071
|
+
if (shouldCommitFurthestStepIndex) setFurthestStepIndex((prev) => Math.max(prev, newActiveIndex));
|
|
2072
|
+
rafRef.current = null;
|
|
2073
|
+
if (!didCompleteInitialRaf) {
|
|
2074
|
+
didCompleteInitialRaf = true;
|
|
2075
|
+
isMountedRef.current = true;
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
};
|
|
2079
|
+
container.addEventListener("wheel", releaseManualSelectionOnUserScroll, { passive: true });
|
|
2080
|
+
container.addEventListener("touchmove", releaseManualSelectionOnUserScroll, { passive: true });
|
|
2081
|
+
container.addEventListener("pointerdown", releaseManualSelectionOnUserScroll);
|
|
2082
|
+
ownerDocument.addEventListener("keydown", handleKeyDown);
|
|
2083
|
+
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
2084
|
+
handleScroll();
|
|
2085
|
+
return () => {
|
|
2086
|
+
isMountedRef.current = false;
|
|
2087
|
+
container.removeEventListener("wheel", releaseManualSelectionOnUserScroll);
|
|
2088
|
+
container.removeEventListener("touchmove", releaseManualSelectionOnUserScroll);
|
|
2089
|
+
container.removeEventListener("pointerdown", releaseManualSelectionOnUserScroll);
|
|
2090
|
+
ownerDocument.removeEventListener("keydown", handleKeyDown);
|
|
2091
|
+
container.removeEventListener("scroll", handleScroll);
|
|
2092
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
2093
|
+
};
|
|
2094
|
+
}, [clearManualSelection, scrollRef]);
|
|
2095
|
+
const lastEmittedIndexRef = useRef(safeIndex);
|
|
2096
|
+
useEffect(() => {
|
|
2097
|
+
const newSafeIndex = getSafeStepIndex(stepsRef.current.length, currentStepIndex);
|
|
2098
|
+
if (newSafeIndex === lastEmittedIndexRef.current) return;
|
|
2099
|
+
lastEmittedIndexRef.current = newSafeIndex;
|
|
2100
|
+
activeIndexRef.current = newSafeIndex;
|
|
2101
|
+
setActiveIndex(newSafeIndex);
|
|
2102
|
+
setFurthestStepIndex((prev) => Math.max(prev, newSafeIndex));
|
|
2103
|
+
const step = stepsRef.current[newSafeIndex];
|
|
2104
|
+
if (!step) return;
|
|
2105
|
+
const sectionElement = scrollRef.current?.querySelector(`#${CSS.escape(sectionIdRef.current(step.id))}`);
|
|
2106
|
+
if (scrollRef.current && sectionElement) scrollSectionIntoView(scrollRef.current, sectionElement, scrollPaddingRef.current);
|
|
2107
|
+
}, [currentStepIndex, scrollRef]);
|
|
2108
|
+
return {
|
|
2109
|
+
activeIndex,
|
|
2110
|
+
furthestStepIndex,
|
|
2111
|
+
scrollToSection: useCallback((index) => {
|
|
2112
|
+
const step = stepsRef.current[index];
|
|
2113
|
+
if (!step) return;
|
|
2114
|
+
manualSelectionIndexRef.current = index;
|
|
2115
|
+
activeIndexRef.current = index;
|
|
2116
|
+
lastEmittedIndexRef.current = index;
|
|
2117
|
+
setActiveIndex(index);
|
|
2118
|
+
setFurthestStepIndex((prev) => Math.max(prev, index));
|
|
2119
|
+
onStepChangeRef.current(index);
|
|
2120
|
+
const container = scrollRef.current;
|
|
2121
|
+
const sectionElement = container?.querySelector(`#${CSS.escape(sectionIdRef.current(step.id))}`);
|
|
2122
|
+
if (container && sectionElement) scrollSectionIntoView(container, sectionElement, scrollPaddingRef.current);
|
|
2123
|
+
}, [scrollRef])
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function resolveScrollableActiveIndex(container, steps, sectionId) {
|
|
2127
|
+
if (steps.length === 0) return {
|
|
2128
|
+
activeIndex: 0,
|
|
2129
|
+
commitFurthestStepIndex: false
|
|
2130
|
+
};
|
|
2131
|
+
const containerRect = container.getBoundingClientRect();
|
|
2132
|
+
const anchorY = containerRect.top + Math.min(containerRect.height * .35, 220);
|
|
2133
|
+
const isScrollable = container.scrollHeight > container.clientHeight + 1;
|
|
2134
|
+
const isAtBottom = isScrollable && container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
|
|
2135
|
+
const isNearBottom = isScrollable && container.scrollTop + container.clientHeight >= container.scrollHeight - 4;
|
|
2136
|
+
if (isAtBottom) return {
|
|
2137
|
+
activeIndex: steps.length - 1,
|
|
2138
|
+
commitFurthestStepIndex: false
|
|
2139
|
+
};
|
|
2140
|
+
let activeIndex = 0;
|
|
2141
|
+
let highestScore = Number.NEGATIVE_INFINITY;
|
|
2142
|
+
for (let i = 0; i < steps.length; i++) {
|
|
2143
|
+
const sectionMetrics = getSectionMetrics(container, steps[i].id, sectionId, containerRect);
|
|
2144
|
+
if (!sectionMetrics) continue;
|
|
2145
|
+
const score = scoreScrollableStep({
|
|
2146
|
+
stepIndex: i,
|
|
2147
|
+
stepCount: steps.length,
|
|
2148
|
+
containerRect,
|
|
2149
|
+
anchorY,
|
|
2150
|
+
isNearBottom,
|
|
2151
|
+
...sectionMetrics
|
|
2152
|
+
});
|
|
2153
|
+
if (score >= highestScore) {
|
|
2154
|
+
highestScore = score;
|
|
2155
|
+
activeIndex = i;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return {
|
|
2159
|
+
activeIndex,
|
|
2160
|
+
commitFurthestStepIndex: true
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
const SCROLL_PADDING_PX = 32;
|
|
2164
|
+
function getSectionElement(container, stepId, sectionId) {
|
|
2165
|
+
return container.querySelector(`#${CSS.escape(sectionId(stepId))}`);
|
|
2166
|
+
}
|
|
2167
|
+
function scrollSectionIntoView(container, sectionElement, padding) {
|
|
2168
|
+
const elementTop = sectionElement.getBoundingClientRect().top;
|
|
2169
|
+
const containerTop = container.getBoundingClientRect().top;
|
|
2170
|
+
const targetScrollTop = container.scrollTop + (elementTop - containerTop) - padding;
|
|
2171
|
+
container.scrollTo({
|
|
2172
|
+
top: targetScrollTop,
|
|
2173
|
+
behavior: "smooth"
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
function getSectionMetrics(container, stepId, sectionId, containerRect) {
|
|
2177
|
+
const sectionElement = getSectionElement(container, stepId, sectionId);
|
|
2178
|
+
if (!sectionElement) return null;
|
|
2179
|
+
const sectionRect = sectionElement.getBoundingClientRect();
|
|
2180
|
+
return {
|
|
2181
|
+
sectionRect,
|
|
2182
|
+
visibleHeight: getVisibleHeight(containerRect, sectionRect)
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
function getVisibleHeight(containerRect, sectionRect) {
|
|
2186
|
+
return Math.max(0, Math.min(sectionRect.bottom, containerRect.bottom) - Math.max(sectionRect.top, containerRect.top));
|
|
2187
|
+
}
|
|
2188
|
+
function scoreScrollableStep({ stepIndex, stepCount, containerRect, sectionRect, visibleHeight, anchorY, isNearBottom }) {
|
|
2189
|
+
const isVisible = visibleHeight > 0;
|
|
2190
|
+
const focusBandTop = containerRect.top + Math.min(containerRect.height * .2, 140);
|
|
2191
|
+
const focusBandBottom = containerRect.top + Math.min(containerRect.height * .55, 360);
|
|
2192
|
+
const focusBandOverlap = getBandOverlapHeight(sectionRect, focusBandTop, focusBandBottom);
|
|
2193
|
+
const distanceToFocusBand = focusBandOverlap > 0 ? 0 : Math.min(Math.abs(sectionRect.top - focusBandBottom), Math.abs(sectionRect.bottom - focusBandTop));
|
|
2194
|
+
const lastStepProminent = stepIndex === stepCount - 1 && visibleHeight >= Math.min(sectionRect.height, containerRect.height) * .25 && sectionRect.top <= containerRect.top + containerRect.height * .65;
|
|
2195
|
+
let score = isVisible ? visibleHeight : Number.NEGATIVE_INFINITY;
|
|
2196
|
+
if (focusBandOverlap > 0) score += 12e3 + focusBandOverlap * 25;
|
|
2197
|
+
if (sectionRect.top <= anchorY) score += 250;
|
|
2198
|
+
score += Math.max(0, 1e3 - distanceToFocusBand);
|
|
2199
|
+
if (isNearBottom && lastStepProminent && isVisible) score += 15e3;
|
|
2200
|
+
return score;
|
|
2201
|
+
}
|
|
2202
|
+
function getBandOverlapHeight(sectionRect, bandTop, bandBottom) {
|
|
2203
|
+
return Math.max(0, Math.min(sectionRect.bottom, bandBottom) - Math.max(sectionRect.top, bandTop));
|
|
2204
|
+
}
|
|
2205
|
+
function isScrollableNavigationKey(event) {
|
|
2206
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return false;
|
|
2207
|
+
return [
|
|
2208
|
+
"ArrowDown",
|
|
2209
|
+
"ArrowUp",
|
|
2210
|
+
"PageDown",
|
|
2211
|
+
"PageUp",
|
|
2212
|
+
"Home",
|
|
2213
|
+
"End",
|
|
2214
|
+
" "
|
|
2215
|
+
].includes(event.key);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
//#endregion
|
|
2219
|
+
//#region src/components/ui/wizard/WizardLayout.tsx
|
|
2220
|
+
function PagedLayout({ steps, currentStepIndex, furthestStepIndex: furthestStepIndexProp, onStepChange, onComplete, onCancel, navActions, header, variant, className }) {
|
|
2221
|
+
const safeIndex = getSafeStepIndex(steps.length, currentStepIndex);
|
|
2222
|
+
const resolvedFurthestStepIndex = useFurthestStepIndex(safeIndex, furthestStepIndexProp);
|
|
2223
|
+
if (steps.length === 0) return null;
|
|
2224
|
+
const isFirstStep = safeIndex === 0;
|
|
2225
|
+
const isLastStep = safeIndex === steps.length - 1;
|
|
2226
|
+
const currentStep = steps[safeIndex];
|
|
2227
|
+
const canProceed = currentStep?.isValid !== false;
|
|
2228
|
+
const handleNext = () => {
|
|
2229
|
+
if (isLastStep) {
|
|
2230
|
+
onComplete?.();
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
onStepChange(safeIndex + 1);
|
|
2234
|
+
};
|
|
2235
|
+
const handlePrevious = () => {
|
|
2236
|
+
if (!isFirstStep) onStepChange(safeIndex - 1);
|
|
2237
|
+
};
|
|
2238
|
+
const stepDefs = toStepDefs(steps, safeIndex);
|
|
2239
|
+
const footer = /* @__PURE__ */ jsx("div", {
|
|
2240
|
+
className: "shrink-0 border-t border-border bg-background px-8 py-4",
|
|
2241
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2242
|
+
className: "mx-auto max-w-5xl",
|
|
2243
|
+
children: /* @__PURE__ */ jsx(WizardNavigation, {
|
|
2244
|
+
isFirstStep,
|
|
2245
|
+
isLastStep,
|
|
2246
|
+
canProceed,
|
|
2247
|
+
onPrevious: handlePrevious,
|
|
2248
|
+
onNext: handleNext,
|
|
2249
|
+
onCancel,
|
|
2250
|
+
extraActions: navActions
|
|
2251
|
+
})
|
|
2252
|
+
})
|
|
2253
|
+
});
|
|
2254
|
+
if (variant === "vertical") return /* @__PURE__ */ jsxs("div", {
|
|
2255
|
+
className: cn("flex h-full gap-6", className),
|
|
2256
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2257
|
+
className: "w-[220px] shrink-0 py-6 pl-6",
|
|
2258
|
+
children: /* @__PURE__ */ jsx(WizardStepper, {
|
|
2259
|
+
variant: "vertical",
|
|
2260
|
+
steps: stepDefs,
|
|
2261
|
+
currentStepIndex: safeIndex,
|
|
2262
|
+
furthestStepIndex: resolvedFurthestStepIndex,
|
|
2263
|
+
onStepClick: onStepChange,
|
|
2264
|
+
className: "h-full"
|
|
2265
|
+
})
|
|
2266
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2267
|
+
className: "flex min-w-0 flex-1 flex-col overflow-hidden",
|
|
2268
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2269
|
+
className: "flex-1 overflow-y-auto p-8",
|
|
2270
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2271
|
+
className: "mx-auto max-w-5xl",
|
|
2272
|
+
children: [header, currentStep?.component]
|
|
2273
|
+
})
|
|
2274
|
+
}), footer]
|
|
2275
|
+
})]
|
|
2276
|
+
});
|
|
2277
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2278
|
+
className: cn("flex h-full flex-col", className),
|
|
2279
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2280
|
+
className: "shrink-0 p-6 pb-0",
|
|
2281
|
+
children: /* @__PURE__ */ jsx(WizardStepper, {
|
|
2282
|
+
variant: "horizontal",
|
|
2283
|
+
steps: stepDefs,
|
|
2284
|
+
currentStepIndex: safeIndex,
|
|
2285
|
+
furthestStepIndex: resolvedFurthestStepIndex,
|
|
2286
|
+
onStepClick: onStepChange
|
|
2287
|
+
})
|
|
2288
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2289
|
+
className: "flex min-w-0 flex-1 flex-col overflow-hidden",
|
|
2290
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2291
|
+
className: "flex-1 overflow-y-auto p-8",
|
|
2292
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2293
|
+
className: "mx-auto max-w-5xl",
|
|
2294
|
+
children: [header, currentStep?.component]
|
|
2295
|
+
})
|
|
2296
|
+
}), footer]
|
|
2297
|
+
})]
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
function ScrollableLayout({ steps, currentStepIndex, onStepChange, header, onComplete, scrollPadding, className }) {
|
|
2301
|
+
const instanceId = useId();
|
|
2302
|
+
const scrollRef = useRef(null);
|
|
2303
|
+
const sectionId = useCallback((stepId) => `wizard-section-${instanceId}-${stepId}`, [instanceId]);
|
|
2304
|
+
const { activeIndex, furthestStepIndex, scrollToSection } = useScrollableWizardStepTracking({
|
|
2305
|
+
steps,
|
|
2306
|
+
currentStepIndex,
|
|
2307
|
+
onStepChange,
|
|
2308
|
+
scrollRef,
|
|
2309
|
+
sectionId,
|
|
2310
|
+
scrollPadding
|
|
2311
|
+
});
|
|
2312
|
+
if (steps.length === 0) return null;
|
|
2313
|
+
const stepDefs = toStepDefs(steps, activeIndex);
|
|
2314
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2315
|
+
className: cn("flex h-full gap-6", className),
|
|
2316
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2317
|
+
className: "w-[220px] shrink-0 py-6 pl-6",
|
|
2318
|
+
children: /* @__PURE__ */ jsx(WizardStepper, {
|
|
2319
|
+
variant: "vertical",
|
|
2320
|
+
steps: stepDefs,
|
|
2321
|
+
currentStepIndex: activeIndex,
|
|
2322
|
+
furthestStepIndex,
|
|
2323
|
+
onStepClick: scrollToSection,
|
|
2324
|
+
freeNavigation: true,
|
|
2325
|
+
className: "h-full"
|
|
2326
|
+
})
|
|
2327
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2328
|
+
ref: scrollRef,
|
|
2329
|
+
className: "flex min-w-0 flex-1 flex-col overflow-y-auto p-8",
|
|
2330
|
+
children: [
|
|
2331
|
+
header,
|
|
2332
|
+
/* @__PURE__ */ jsx("div", {
|
|
2333
|
+
className: "space-y-12",
|
|
2334
|
+
children: steps.map((step) => /* @__PURE__ */ jsx("section", {
|
|
2335
|
+
id: sectionId(step.id),
|
|
2336
|
+
children: step.component
|
|
2337
|
+
}, step.id))
|
|
2338
|
+
}),
|
|
2339
|
+
onComplete && /* @__PURE__ */ jsx("div", {
|
|
2340
|
+
className: "flex justify-end pt-8",
|
|
2341
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
2342
|
+
type: "button",
|
|
2343
|
+
onClick: onComplete,
|
|
2344
|
+
children: "Finish"
|
|
2345
|
+
})
|
|
2346
|
+
})
|
|
2347
|
+
]
|
|
2348
|
+
})]
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
function toStepDefs(steps, currentStepIndex) {
|
|
2352
|
+
return steps.map((s, i) => {
|
|
2353
|
+
const isInvalid = s.isInvalid ?? s.isValid === false;
|
|
2354
|
+
const status = s.status ?? (i < currentStepIndex && !isInvalid ? "completed" : "pending");
|
|
2355
|
+
return {
|
|
2356
|
+
id: s.id,
|
|
2357
|
+
title: s.title,
|
|
2358
|
+
status,
|
|
2359
|
+
isInvalid
|
|
2360
|
+
};
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* A layout component for the wizard.
|
|
2365
|
+
*
|
|
2366
|
+
* @param props - The props for the WizardLayout component.
|
|
2367
|
+
* @returns A React node representing the layout component.
|
|
2368
|
+
*/
|
|
2369
|
+
function WizardLayout(props) {
|
|
2370
|
+
const { variant = "horizontal", ...rest } = props;
|
|
2371
|
+
if (variant === "scrollable") return /* @__PURE__ */ jsx(ScrollableLayout, {
|
|
2372
|
+
steps: rest.steps,
|
|
2373
|
+
currentStepIndex: rest.currentStepIndex,
|
|
2374
|
+
onStepChange: rest.onStepChange,
|
|
2375
|
+
header: rest.header,
|
|
2376
|
+
onComplete: rest.onComplete,
|
|
2377
|
+
scrollPadding: rest.scrollPadding,
|
|
2378
|
+
className: rest.className
|
|
2379
|
+
});
|
|
2380
|
+
return /* @__PURE__ */ jsx(PagedLayout, {
|
|
2381
|
+
...rest,
|
|
2382
|
+
variant
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
//#endregion
|
|
2387
|
+
//#region src/components/fields/address-suggestion/context.ts
|
|
2388
|
+
/**
|
|
2389
|
+
* @internal Shared context instance consumed by both AddressField and
|
|
2390
|
+
* AddressSuggestionProvider. Kept in its own file so component files export
|
|
2391
|
+
* only components (required by React Fast Refresh).
|
|
2392
|
+
*/
|
|
2393
|
+
const AddressSuggestionContext = createContext(null);
|
|
2394
|
+
|
|
1592
2395
|
//#endregion
|
|
1593
2396
|
//#region src/components/fields/utils/accessibility.ts
|
|
1594
2397
|
/**
|
|
@@ -1780,6 +2583,8 @@ function getWidthClasses(width) {
|
|
|
1780
2583
|
|
|
1781
2584
|
//#endregion
|
|
1782
2585
|
//#region src/components/fields/AddressField.tsx
|
|
2586
|
+
const DEBOUNCE_MS = 200;
|
|
2587
|
+
const MAX_SUGGESTIONS = 5;
|
|
1783
2588
|
/**
|
|
1784
2589
|
* Address input field component specifically designed for blockchain addresses via React Hook Form integration.
|
|
1785
2590
|
*
|
|
@@ -1797,11 +2602,82 @@ function getWidthClasses(width) {
|
|
|
1797
2602
|
* - Chain-agnostic design (validation handled by adapters)
|
|
1798
2603
|
* - Full accessibility support with ARIA attributes
|
|
1799
2604
|
* - Keyboard navigation
|
|
2605
|
+
*
|
|
2606
|
+
* Autocomplete suggestions can be provided in two ways:
|
|
2607
|
+
*
|
|
2608
|
+
* 1. **Context-based (zero-config)**: Mount an `AddressSuggestionProvider` in the
|
|
2609
|
+
* component tree. Every `AddressField` below it automatically resolves suggestions.
|
|
2610
|
+
*
|
|
2611
|
+
* 2. **Prop-based (explicit)**: Pass `suggestions` directly. This overrides context.
|
|
2612
|
+
* Pass `suggestions={false}` to opt out when a provider is mounted.
|
|
2613
|
+
*
|
|
2614
|
+
* The suggestion dropdown includes built-in debouncing, keyboard navigation (Arrow keys,
|
|
2615
|
+
* Enter, Escape), click-outside dismissal, and ARIA listbox semantics.
|
|
1800
2616
|
*/
|
|
1801
|
-
function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, adapter, readOnly }) {
|
|
2617
|
+
function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, adapter, readOnly, suggestions: suggestionsProp, onSuggestionSelect }) {
|
|
1802
2618
|
const isRequired = !!validation?.required;
|
|
1803
2619
|
const errorId = `${id}-error`;
|
|
1804
2620
|
const descriptionId = `${id}-description`;
|
|
2621
|
+
const contextResolver = useContext(AddressSuggestionContext);
|
|
2622
|
+
const containerRef = useRef(null);
|
|
2623
|
+
const lastSetValueRef = useRef("");
|
|
2624
|
+
const [inputValue, setInputValue] = useState("");
|
|
2625
|
+
const [debouncedQuery, setDebouncedQuery] = useState("");
|
|
2626
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
2627
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
2628
|
+
const watchedFieldValue = useWatch({
|
|
2629
|
+
control,
|
|
2630
|
+
name
|
|
2631
|
+
});
|
|
2632
|
+
useEffect(() => {
|
|
2633
|
+
const currentFieldValue = watchedFieldValue ?? "";
|
|
2634
|
+
if (currentFieldValue !== lastSetValueRef.current) {
|
|
2635
|
+
lastSetValueRef.current = currentFieldValue;
|
|
2636
|
+
setInputValue(currentFieldValue);
|
|
2637
|
+
}
|
|
2638
|
+
}, [watchedFieldValue]);
|
|
2639
|
+
useEffect(() => {
|
|
2640
|
+
if (!inputValue.trim()) {
|
|
2641
|
+
setDebouncedQuery("");
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
const timer = setTimeout(() => setDebouncedQuery(inputValue), DEBOUNCE_MS);
|
|
2645
|
+
return () => clearTimeout(timer);
|
|
2646
|
+
}, [inputValue]);
|
|
2647
|
+
const suggestionsDisabled = suggestionsProp === false;
|
|
2648
|
+
const resolvedSuggestions = useMemo(() => {
|
|
2649
|
+
if (suggestionsDisabled) return [];
|
|
2650
|
+
if (Array.isArray(suggestionsProp)) return suggestionsProp;
|
|
2651
|
+
if (!contextResolver || !debouncedQuery.trim()) return [];
|
|
2652
|
+
return contextResolver.resolveSuggestions(debouncedQuery).slice(0, MAX_SUGGESTIONS);
|
|
2653
|
+
}, [
|
|
2654
|
+
suggestionsDisabled,
|
|
2655
|
+
suggestionsProp,
|
|
2656
|
+
contextResolver,
|
|
2657
|
+
debouncedQuery
|
|
2658
|
+
]);
|
|
2659
|
+
const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
|
|
2660
|
+
useEffect(() => {
|
|
2661
|
+
let active = true;
|
|
2662
|
+
const handleClickOutside = (e) => {
|
|
2663
|
+
if (active && containerRef.current && !containerRef.current.contains(e.target)) setShowSuggestions(false);
|
|
2664
|
+
};
|
|
2665
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
2666
|
+
return () => {
|
|
2667
|
+
active = false;
|
|
2668
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
2669
|
+
};
|
|
2670
|
+
}, []);
|
|
2671
|
+
const handleSuggestionKeyDown = useCallback((e) => {
|
|
2672
|
+
if (!hasSuggestions) return;
|
|
2673
|
+
if (e.key === "ArrowDown") {
|
|
2674
|
+
e.preventDefault();
|
|
2675
|
+
setHighlightedIndex((prev) => prev < resolvedSuggestions.length - 1 ? prev + 1 : 0);
|
|
2676
|
+
} else if (e.key === "ArrowUp") {
|
|
2677
|
+
e.preventDefault();
|
|
2678
|
+
setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
|
|
2679
|
+
}
|
|
2680
|
+
}, [hasSuggestions, resolvedSuggestions.length]);
|
|
1805
2681
|
return /* @__PURE__ */ jsxs("div", {
|
|
1806
2682
|
className: `flex flex-col gap-2 ${width === "full" ? "w-full" : width === "half" ? "w-1/2" : "w-1/3"}`,
|
|
1807
2683
|
children: [label && /* @__PURE__ */ jsxs(Label, {
|
|
@@ -1835,9 +2711,32 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
|
|
|
1835
2711
|
const handleInputChange = (e) => {
|
|
1836
2712
|
const value = e.target.value;
|
|
1837
2713
|
field.onChange(value);
|
|
2714
|
+
lastSetValueRef.current = value;
|
|
2715
|
+
setInputValue(value);
|
|
2716
|
+
setShowSuggestions(value.length > 0);
|
|
2717
|
+
setHighlightedIndex(-1);
|
|
2718
|
+
};
|
|
2719
|
+
const applySuggestion = (suggestion) => {
|
|
2720
|
+
field.onChange(suggestion.value);
|
|
2721
|
+
onSuggestionSelect?.(suggestion);
|
|
2722
|
+
lastSetValueRef.current = suggestion.value;
|
|
2723
|
+
setInputValue(suggestion.value);
|
|
2724
|
+
setShowSuggestions(false);
|
|
2725
|
+
setHighlightedIndex(-1);
|
|
1838
2726
|
};
|
|
1839
2727
|
const handleKeyDown = (e) => {
|
|
1840
|
-
if (e.key === "
|
|
2728
|
+
if (hasSuggestions && e.key === "Enter" && highlightedIndex >= 0) {
|
|
2729
|
+
e.preventDefault();
|
|
2730
|
+
applySuggestion(resolvedSuggestions[highlightedIndex]);
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
if (e.key === "Escape") {
|
|
2734
|
+
if (hasSuggestions) {
|
|
2735
|
+
setShowSuggestions(false);
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
handleEscapeKey(field.onChange, field.value)(e);
|
|
2739
|
+
}
|
|
1841
2740
|
};
|
|
1842
2741
|
const accessibilityProps = getAccessibilityProps({
|
|
1843
2742
|
id,
|
|
@@ -1846,18 +2745,50 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
|
|
|
1846
2745
|
hasHelperText: !!helperText
|
|
1847
2746
|
});
|
|
1848
2747
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1849
|
-
/* @__PURE__ */
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2748
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2749
|
+
ref: containerRef,
|
|
2750
|
+
className: "relative",
|
|
2751
|
+
onKeyDown: handleSuggestionKeyDown,
|
|
2752
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
2753
|
+
...field,
|
|
2754
|
+
id,
|
|
2755
|
+
placeholder: placeholder || "0x...",
|
|
2756
|
+
className: validationClasses,
|
|
2757
|
+
onChange: handleInputChange,
|
|
2758
|
+
onKeyDown: handleKeyDown,
|
|
2759
|
+
"data-slot": "input",
|
|
2760
|
+
value: field.value ?? "",
|
|
2761
|
+
...accessibilityProps,
|
|
2762
|
+
"aria-describedby": `${helperText ? descriptionId : ""} ${hasError ? errorId : ""}`,
|
|
2763
|
+
"aria-expanded": hasSuggestions,
|
|
2764
|
+
"aria-autocomplete": suggestionsDisabled ? void 0 : "list",
|
|
2765
|
+
"aria-controls": hasSuggestions ? `${id}-suggestions` : void 0,
|
|
2766
|
+
"aria-activedescendant": hasSuggestions && highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : void 0,
|
|
2767
|
+
disabled: readOnly
|
|
2768
|
+
}), hasSuggestions && /* @__PURE__ */ jsx("div", {
|
|
2769
|
+
id: `${id}-suggestions`,
|
|
2770
|
+
className: cn("absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md", "max-h-48 overflow-auto"),
|
|
2771
|
+
role: "listbox",
|
|
2772
|
+
children: resolvedSuggestions.map((s, i) => /* @__PURE__ */ jsxs("button", {
|
|
2773
|
+
id: `${id}-suggestion-${i}`,
|
|
2774
|
+
type: "button",
|
|
2775
|
+
role: "option",
|
|
2776
|
+
"aria-selected": i === highlightedIndex,
|
|
2777
|
+
className: cn("flex w-full flex-col px-3 py-2 text-left text-sm", "hover:bg-accent", i === highlightedIndex && "bg-accent"),
|
|
2778
|
+
onMouseDown: (e) => {
|
|
2779
|
+
e.preventDefault();
|
|
2780
|
+
applySuggestion(s);
|
|
2781
|
+
},
|
|
2782
|
+
onMouseEnter: () => setHighlightedIndex(i),
|
|
2783
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2784
|
+
className: "font-medium",
|
|
2785
|
+
children: s.label
|
|
2786
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2787
|
+
className: "truncate font-mono text-xs text-muted-foreground",
|
|
2788
|
+
children: s.value
|
|
2789
|
+
})]
|
|
2790
|
+
}, `${s.value}-${s.description ?? i}`))
|
|
2791
|
+
})]
|
|
1861
2792
|
}),
|
|
1862
2793
|
helperText && /* @__PURE__ */ jsx("div", {
|
|
1863
2794
|
id: descriptionId,
|
|
@@ -1876,6 +2807,90 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
|
|
|
1876
2807
|
}
|
|
1877
2808
|
AddressField.displayName = "AddressField";
|
|
1878
2809
|
|
|
2810
|
+
//#endregion
|
|
2811
|
+
//#region src/components/fields/address-suggestion/address-suggestion-context.tsx
|
|
2812
|
+
/**
|
|
2813
|
+
* Address Suggestion Context
|
|
2814
|
+
*
|
|
2815
|
+
* Provides a React context for resolving address autocomplete suggestions.
|
|
2816
|
+
* When an `AddressSuggestionProvider` is mounted, every `AddressField`
|
|
2817
|
+
* in the subtree automatically renders a suggestion dropdown as the user types.
|
|
2818
|
+
*
|
|
2819
|
+
* @example
|
|
2820
|
+
* ```tsx
|
|
2821
|
+
* import { AddressSuggestionProvider } from '@openzeppelin/ui-components';
|
|
2822
|
+
* import { useAliasSuggestionResolver } from '@openzeppelin/ui-storage';
|
|
2823
|
+
*
|
|
2824
|
+
* function App() {
|
|
2825
|
+
* const resolver = useAliasSuggestionResolver(db);
|
|
2826
|
+
* return (
|
|
2827
|
+
* <AddressSuggestionProvider {...resolver}>
|
|
2828
|
+
* <MyApp />
|
|
2829
|
+
* </AddressSuggestionProvider>
|
|
2830
|
+
* );
|
|
2831
|
+
* }
|
|
2832
|
+
* ```
|
|
2833
|
+
*/
|
|
2834
|
+
/**
|
|
2835
|
+
* Provides address suggestion resolution to all `AddressField` instances in the
|
|
2836
|
+
* subtree. Wrap your application (or a subsection) with this provider and
|
|
2837
|
+
* supply a `resolveSuggestions` function.
|
|
2838
|
+
*
|
|
2839
|
+
* @param props - Resolver function and children
|
|
2840
|
+
*
|
|
2841
|
+
* @example
|
|
2842
|
+
* ```tsx
|
|
2843
|
+
* <AddressSuggestionProvider
|
|
2844
|
+
* resolveSuggestions={(query, networkId) => filterAliases(query, networkId)}
|
|
2845
|
+
* >
|
|
2846
|
+
* <App />
|
|
2847
|
+
* </AddressSuggestionProvider>
|
|
2848
|
+
* ```
|
|
2849
|
+
*/
|
|
2850
|
+
function AddressSuggestionProvider({ children, resolveSuggestions }) {
|
|
2851
|
+
const value = React$1.useMemo(() => ({ resolveSuggestions }), [resolveSuggestions]);
|
|
2852
|
+
return /* @__PURE__ */ jsx(AddressSuggestionContext.Provider, {
|
|
2853
|
+
value,
|
|
2854
|
+
children
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
//#endregion
|
|
2859
|
+
//#region src/components/fields/address-suggestion/useAddressSuggestions.ts
|
|
2860
|
+
/**
|
|
2861
|
+
* Convenience hook that resolves suggestions for a query string using the
|
|
2862
|
+
* nearest `AddressSuggestionProvider`. Returns an empty array when no provider
|
|
2863
|
+
* is mounted or when the query is empty.
|
|
2864
|
+
*
|
|
2865
|
+
* @param query - Current input value to match against
|
|
2866
|
+
* @param networkId - Optional network identifier for scoping results
|
|
2867
|
+
* @returns Object containing the resolved suggestions array
|
|
2868
|
+
*
|
|
2869
|
+
* @example
|
|
2870
|
+
* ```tsx
|
|
2871
|
+
* function MyField({ query }: { query: string }) {
|
|
2872
|
+
* const { suggestions } = useAddressSuggestions(query, 'ethereum-mainnet');
|
|
2873
|
+
* return (
|
|
2874
|
+
* <ul>
|
|
2875
|
+
* {suggestions.map(s => <li key={s.value}>{s.label}</li>)}
|
|
2876
|
+
* </ul>
|
|
2877
|
+
* );
|
|
2878
|
+
* }
|
|
2879
|
+
* ```
|
|
2880
|
+
*/
|
|
2881
|
+
/** Resolves address suggestions from the nearest `AddressSuggestionProvider`. */
|
|
2882
|
+
function useAddressSuggestions(query, networkId) {
|
|
2883
|
+
const resolver = React$1.useContext(AddressSuggestionContext);
|
|
2884
|
+
return { suggestions: React$1.useMemo(() => {
|
|
2885
|
+
if (!resolver || !query.trim()) return [];
|
|
2886
|
+
return resolver.resolveSuggestions(query, networkId);
|
|
2887
|
+
}, [
|
|
2888
|
+
resolver,
|
|
2889
|
+
query,
|
|
2890
|
+
networkId
|
|
2891
|
+
]) };
|
|
2892
|
+
}
|
|
2893
|
+
|
|
1879
2894
|
//#endregion
|
|
1880
2895
|
//#region src/components/fields/AmountField.tsx
|
|
1881
2896
|
/**
|
|
@@ -5245,5 +6260,5 @@ const Toaster = ({ ...props }) => {
|
|
|
5245
6260
|
};
|
|
5246
6261
|
|
|
5247
6262
|
//#endregion
|
|
5248
|
-
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AddressDisplay, AddressField, Alert, AlertDescription, AlertTitle, AmountField, ArrayField, ArrayObjectField, Banner, BaseField, BigIntField, BooleanField, Button, BytesField, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DateRangePicker, DateTimeField, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EcosystemDropdown, EcosystemIcon, EmptyState, EnumField, ErrorMessage, ExternalLink, FileUploadField, Footer, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Header, INTEGER_HTML_PATTERN, INTEGER_INPUT_PATTERN, INTEGER_PATTERN, Input, Label, LoadingButton, MapEntryRow, MapField, MidnightIcon, NetworkErrorNotificationProvider, NetworkIcon, NetworkSelector, NetworkServiceErrorBanner, NetworkStatusBadge, NumberField, ObjectField, PasswordField, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, RadioField, RadioGroup, RadioGroupItem, RelayerDetailsCard, Select, SelectContent, SelectField, SelectGroup, SelectGroupedField, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SidebarButton, SidebarGroup, SidebarLayout, SidebarSection, Tabs, TabsContent, TabsList, TabsTrigger, TextAreaField, TextField, Textarea, Toaster, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UrlField, ViewContractStateButton, buttonVariants, computeChildTouched, createFocusManager, createValidationResult, formatValidationError, getAccessibilityProps, getDescribedById, getErrorMessage, getValidationStateClasses, getWidthClasses, handleEscapeKey, handleKeyboardEvent, handleNumericKeys, handleToggleKeys, handleValidationError, hasFieldError, isDuplicateMapKey, useDuplicateKeyIndexes, useMapFieldSync, useNetworkErrorAwareAdapter, useNetworkErrorReporter, useNetworkErrors, validateField, validateMapEntries, validateMapStructure };
|
|
6263
|
+
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, WizardLayout, WizardNavigation, WizardStepper, buttonVariants, computeChildTouched, createFocusManager, createValidationResult, formatValidationError, getAccessibilityProps, getDescribedById, getErrorMessage, getValidationStateClasses, getWidthClasses, handleEscapeKey, handleKeyboardEvent, handleNumericKeys, handleToggleKeys, handleValidationError, hasFieldError, isDuplicateMapKey, useAddressLabel, useAddressSuggestions, useDuplicateKeyIndexes, useMapFieldSync, useNetworkErrorAwareAdapter, useNetworkErrorReporter, useNetworkErrors, validateField, validateMapEntries, validateMapStructure };
|
|
5249
6264
|
//# sourceMappingURL=index.mjs.map
|