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