@openzeppelin/ui-components 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +284 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -42
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +43 -48
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +285 -117
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -38,7 +38,7 @@ let sonner = require("sonner");
|
|
|
38
38
|
let next_themes = require("next-themes");
|
|
39
39
|
|
|
40
40
|
//#region src/version.ts
|
|
41
|
-
const VERSION = "3.
|
|
41
|
+
const VERSION = "3.3.0";
|
|
42
42
|
|
|
43
43
|
//#endregion
|
|
44
44
|
//#region src/components/ui/accordion.tsx
|
|
@@ -470,32 +470,55 @@ const variantStyles = {
|
|
|
470
470
|
icon: "text-red-600",
|
|
471
471
|
title: "text-red-900",
|
|
472
472
|
body: "text-red-800"
|
|
473
|
+
},
|
|
474
|
+
neutral: {
|
|
475
|
+
container: "border-border bg-muted/30",
|
|
476
|
+
icon: "text-muted-foreground",
|
|
477
|
+
title: "text-foreground",
|
|
478
|
+
body: "text-muted-foreground"
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
const sizeStyles = {
|
|
482
|
+
default: {
|
|
483
|
+
container: "p-4",
|
|
484
|
+
gap: "gap-3",
|
|
485
|
+
title: "mb-2 text-sm font-semibold",
|
|
486
|
+
body: "text-sm whitespace-pre-line",
|
|
487
|
+
dismissIcon: "h-5 w-5"
|
|
488
|
+
},
|
|
489
|
+
compact: {
|
|
490
|
+
container: "px-4 py-3",
|
|
491
|
+
gap: "gap-2",
|
|
492
|
+
title: "mb-1 text-xs font-medium",
|
|
493
|
+
body: "text-xs leading-relaxed whitespace-pre-line",
|
|
494
|
+
dismissIcon: "h-4 w-4"
|
|
473
495
|
}
|
|
474
496
|
};
|
|
475
497
|
/**
|
|
476
498
|
* Dismissible banner component for notifications and alerts
|
|
477
|
-
* Can be used with various variants (info, success, warning, error)
|
|
499
|
+
* Can be used with various variants (info, success, warning, error, neutral)
|
|
478
500
|
*/
|
|
479
|
-
const Banner = react.forwardRef(({ className, variant = "info", title, children, dismissible = true, onDismiss, icon }, ref) => {
|
|
480
|
-
const styles = variantStyles[variant]
|
|
501
|
+
const Banner = react.forwardRef(({ className, variant = "info", size = "default", title, children, dismissible = true, onDismiss, icon }, ref) => {
|
|
502
|
+
const styles = variantStyles[variant];
|
|
503
|
+
const sizing = sizeStyles[size];
|
|
481
504
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
482
505
|
ref,
|
|
483
506
|
role: "alert",
|
|
484
|
-
className: (0, _openzeppelin_ui_utils.cn)("rounded-md border
|
|
507
|
+
className: (0, _openzeppelin_ui_utils.cn)("rounded-md border", sizing.container, styles.container, className),
|
|
485
508
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
486
|
-
className: "flex gap
|
|
509
|
+
className: (0, _openzeppelin_ui_utils.cn)("flex", sizing.gap),
|
|
487
510
|
children: [
|
|
488
511
|
icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
489
512
|
className: (0, _openzeppelin_ui_utils.cn)("mt-0.5 shrink-0", styles.icon),
|
|
490
513
|
children: icon
|
|
491
514
|
}),
|
|
492
515
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
493
|
-
className: "flex-1",
|
|
516
|
+
className: "min-w-0 flex-1",
|
|
494
517
|
children: [title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
495
|
-
className: (0, _openzeppelin_ui_utils.cn)(
|
|
518
|
+
className: (0, _openzeppelin_ui_utils.cn)(sizing.title, styles.title),
|
|
496
519
|
children: title
|
|
497
520
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
498
|
-
className: (0, _openzeppelin_ui_utils.cn)(
|
|
521
|
+
className: (0, _openzeppelin_ui_utils.cn)(sizing.body, styles.body),
|
|
499
522
|
children
|
|
500
523
|
})]
|
|
501
524
|
}),
|
|
@@ -504,7 +527,7 @@ const Banner = react.forwardRef(({ className, variant = "info", title, children,
|
|
|
504
527
|
onClick: onDismiss,
|
|
505
528
|
className: (0, _openzeppelin_ui_utils.cn)("self-start shrink-0 transition-colors hover:opacity-70", styles.icon),
|
|
506
529
|
"aria-label": "Dismiss banner",
|
|
507
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className:
|
|
530
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: sizing.dismissIcon })
|
|
508
531
|
})
|
|
509
532
|
]
|
|
510
533
|
})
|
|
@@ -3057,6 +3080,10 @@ TextAreaField.displayName = "TextAreaField";
|
|
|
3057
3080
|
//#region src/components/fields/AddressListField/labels.ts
|
|
3058
3081
|
/** Default English labels for {@link AddressListField}. */
|
|
3059
3082
|
const DEFAULT_ADDRESS_LIST_FIELD_LABELS = {
|
|
3083
|
+
addSingleAddress: "Add",
|
|
3084
|
+
invalidSingleAddress: "Enter a valid address",
|
|
3085
|
+
enableBulkEntry: "Bulk paste",
|
|
3086
|
+
enableSingleEntry: "Single entry",
|
|
3060
3087
|
addAddresses: "Add addresses",
|
|
3061
3088
|
addAddressCount: (count) => `Add ${count} address${count === 1 ? "" : "es"}`,
|
|
3062
3089
|
invalidPrefix: "Invalid:",
|
|
@@ -3109,41 +3136,9 @@ function formatAddressBulkSummary(classification, addedCount, labels) {
|
|
|
3109
3136
|
}
|
|
3110
3137
|
|
|
3111
3138
|
//#endregion
|
|
3112
|
-
//#region src/components/fields/AddressListField/
|
|
3113
|
-
/**
|
|
3114
|
-
|
|
3115
|
-
*
|
|
3116
|
-
* Architecture flow:
|
|
3117
|
-
* 1. App or wizard steps hold the committed address list in local or form state
|
|
3118
|
-
* 2. AddressListField renders a draft textarea plus the committed list rows
|
|
3119
|
-
* 3. Parsing and classification run via `@openzeppelin/ui-utils` helpers
|
|
3120
|
-
* 4. Optional {@link AddressingCapability} validates candidates before add
|
|
3121
|
-
* 5. {@link AddressDisplay} renders each committed row with copy and explorer links
|
|
3122
|
-
*
|
|
3123
|
-
* The component includes:
|
|
3124
|
-
* - Delimiter-aware bulk paste (newlines, commas, semicolons)
|
|
3125
|
-
* - Live preview of accepted, invalid, duplicate, and already-listed candidates
|
|
3126
|
-
* - Optional `maxItems` cap with disabled input at the limit
|
|
3127
|
-
* - Removable list rows with accessible remove buttons
|
|
3128
|
-
* - Caller-controlled copy for placeholders and format hints
|
|
3129
|
-
* - Optional `labels` overrides for built-in button and status strings
|
|
3130
|
-
*
|
|
3131
|
-
* @example
|
|
3132
|
-
* ```tsx
|
|
3133
|
-
* const [addresses, setAddresses] = useState<string[]>([]);
|
|
3134
|
-
*
|
|
3135
|
-
* <AddressListField
|
|
3136
|
-
* value={addresses}
|
|
3137
|
-
* onChange={setAddresses}
|
|
3138
|
-
* placeholder="Paste addresses (one per line or comma-separated)"
|
|
3139
|
-
* formatHint="Each entry is validated before it is added."
|
|
3140
|
-
* addressing={capabilities}
|
|
3141
|
-
* />
|
|
3142
|
-
* ```
|
|
3143
|
-
*/
|
|
3144
|
-
function AddressListField({ value, onChange, placeholder, formatHint, addressing, getExplorerUrl, label, helperText, maxItems, disabled = false, maxInvalidPreview = 3, className, labels: labelOverrides }) {
|
|
3145
|
-
const fieldId = (0, react.useId)();
|
|
3146
|
-
const labels = (0, react.useMemo)(() => resolveAddressListFieldLabels(labelOverrides), [labelOverrides]);
|
|
3139
|
+
//#region src/components/fields/AddressListField/AddressListBulkEntry.tsx
|
|
3140
|
+
/** Bulk paste entry — info callout, textarea, and add action in a compact stack. */
|
|
3141
|
+
function AddressListBulkEntry({ fieldId, value, onAdd, placeholder, formatHint, addressing, maxItems, disabled = false, maxInvalidPreview = 3, labels, modeToggle }) {
|
|
3147
3142
|
const atLimit = maxItems != null && value.length >= maxItems;
|
|
3148
3143
|
const inputDisabled = disabled || atLimit;
|
|
3149
3144
|
const [feedback, setFeedback] = (0, react.useState)(null);
|
|
@@ -3162,97 +3157,270 @@ function AddressListField({ value, onChange, placeholder, formatHint, addressing
|
|
|
3162
3157
|
maxItems
|
|
3163
3158
|
]);
|
|
3164
3159
|
const previewSummary = (0, react.useMemo)(() => buildAddressListPreviewSummary(classification, labels), [classification, labels]);
|
|
3165
|
-
const
|
|
3160
|
+
const statusMessage = rawInput.trim() ? previewSummary ?? void 0 : feedback ?? void 0;
|
|
3166
3161
|
const handleAdd = (0, react.useCallback)(() => {
|
|
3167
3162
|
if (!classification || classification.accepted.length === 0 || atLimit) return;
|
|
3168
|
-
|
|
3163
|
+
onAdd(classification.accepted);
|
|
3169
3164
|
reset({ input: "" });
|
|
3170
3165
|
setFeedback(formatAddressBulkSummary(classification, classification.accepted.length, labels));
|
|
3171
3166
|
}, [
|
|
3172
3167
|
classification,
|
|
3173
3168
|
atLimit,
|
|
3174
|
-
|
|
3175
|
-
value,
|
|
3169
|
+
onAdd,
|
|
3176
3170
|
reset,
|
|
3177
3171
|
labels
|
|
3178
3172
|
]);
|
|
3179
|
-
const handleRemove = (0, react.useCallback)((index) => {
|
|
3180
|
-
onChange(value.filter((_, currentIndex) => currentIndex !== index));
|
|
3181
|
-
setFeedback(null);
|
|
3182
|
-
}, [onChange, value]);
|
|
3183
3173
|
const acceptedCount = classification?.accepted.length ?? 0;
|
|
3184
3174
|
const addButtonLabel = acceptedCount > 0 ? labels.addAddressCount(acceptedCount) : labels.addAddresses;
|
|
3185
3175
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3186
|
-
className:
|
|
3176
|
+
className: "space-y-3",
|
|
3187
3177
|
children: [
|
|
3188
3178
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3189
|
-
className: "
|
|
3190
|
-
children: [
|
|
3191
|
-
className: "text-xs text-muted-foreground",
|
|
3192
|
-
children: helperText
|
|
3193
|
-
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3194
|
-
className: "text-xs text-muted-foreground",
|
|
3179
|
+
className: "flex items-start gap-2 rounded-lg border border-border/70 bg-muted/30 px-3 py-2.5",
|
|
3180
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Info, { className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3181
|
+
className: "text-xs leading-relaxed text-muted-foreground",
|
|
3195
3182
|
children: formatHint
|
|
3196
3183
|
})]
|
|
3197
3184
|
}),
|
|
3198
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3185
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3186
|
+
className: "relative",
|
|
3187
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextAreaField, {
|
|
3188
|
+
id: `address-list-bulk-${fieldId}`,
|
|
3189
|
+
name: "input",
|
|
3190
|
+
label: "",
|
|
3191
|
+
placeholder,
|
|
3192
|
+
helperText: statusMessage,
|
|
3193
|
+
control,
|
|
3194
|
+
rows: 4,
|
|
3195
|
+
validation: { required: false },
|
|
3196
|
+
readOnly: inputDisabled
|
|
3197
|
+
})
|
|
3198
|
+
}), modeToggle ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3199
|
+
className: "flex justify-end pr-2",
|
|
3200
|
+
children: modeToggle
|
|
3201
|
+
}) : null] }),
|
|
3202
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3203
|
+
className: "flex flex-wrap items-center justify-between gap-2",
|
|
3204
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3205
|
+
className: "min-w-0 flex-1",
|
|
3206
|
+
children: [classification && classification.invalid.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3207
|
+
className: "text-xs text-destructive",
|
|
3208
|
+
children: [
|
|
3209
|
+
labels.invalidPrefix,
|
|
3210
|
+
" ",
|
|
3211
|
+
classification.invalid.slice(0, maxInvalidPreview).join(", "),
|
|
3212
|
+
classification.invalid.length > maxInvalidPreview ? ` ${labels.invalidMore(classification.invalid.length - maxInvalidPreview)}` : ""
|
|
3213
|
+
]
|
|
3214
|
+
}) : null, atLimit && maxItems != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3215
|
+
className: "text-xs text-muted-foreground",
|
|
3216
|
+
children: labels.maxItemsReached(maxItems)
|
|
3217
|
+
}) : null]
|
|
3218
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
3219
|
+
type: "button",
|
|
3220
|
+
size: "sm",
|
|
3221
|
+
className: "shrink-0",
|
|
3222
|
+
disabled: inputDisabled || acceptedCount === 0,
|
|
3223
|
+
onClick: () => handleAdd(),
|
|
3224
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "mr-1 size-4" }), addButtonLabel]
|
|
3225
|
+
})]
|
|
3226
|
+
})
|
|
3227
|
+
]
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
//#endregion
|
|
3232
|
+
//#region src/components/fields/AddressListField/AddressListEntries.tsx
|
|
3233
|
+
/** Renders committed addresses with remove actions. Shared by single and bulk entry modes. */
|
|
3234
|
+
function AddressListEntries({ value, getExplorerUrl, disabled = false, labels, onRemove }) {
|
|
3235
|
+
if (value.length === 0) return null;
|
|
3236
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3237
|
+
className: "space-y-1",
|
|
3238
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3239
|
+
className: "text-xs text-muted-foreground",
|
|
3240
|
+
children: labels.addressesAdded(value.length)
|
|
3241
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3242
|
+
className: "max-h-44 space-y-1 overflow-y-auto rounded-md border border-border/70 p-1",
|
|
3243
|
+
children: value.map((address, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3244
|
+
className: "flex items-center justify-between rounded bg-muted p-2",
|
|
3245
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressDisplay, {
|
|
3246
|
+
address,
|
|
3247
|
+
variant: "inline",
|
|
3248
|
+
truncateWhenLabeled: true,
|
|
3249
|
+
showCopyButton: true,
|
|
3250
|
+
explorerUrl: getExplorerUrl?.(address) ?? void 0
|
|
3251
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
3252
|
+
type: "button",
|
|
3253
|
+
variant: "ghost",
|
|
3254
|
+
size: "sm",
|
|
3255
|
+
className: "size-7 shrink-0 p-0 text-muted-foreground hover:text-destructive",
|
|
3256
|
+
onClick: () => onRemove(index),
|
|
3257
|
+
disabled,
|
|
3258
|
+
"aria-label": labels.removeAddress(index),
|
|
3259
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "size-3.5" })
|
|
3260
|
+
})]
|
|
3261
|
+
}, `${address}-${index}`))
|
|
3262
|
+
})]
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
//#endregion
|
|
3267
|
+
//#region src/components/fields/AddressListField/AddressListSingleEntry.tsx
|
|
3268
|
+
/** Single-address entry — inline {@link AddressField} with an Add button (address-book aware). */
|
|
3269
|
+
function AddressListSingleEntry({ fieldId, value, onAdd, placeholder, addressing, maxItems, disabled = false, labels, modeToggle }) {
|
|
3270
|
+
const atLimit = maxItems != null && value.length >= maxItems;
|
|
3271
|
+
const inputDisabled = disabled || atLimit;
|
|
3272
|
+
const { control, reset, getValues, handleSubmit } = (0, react_hook_form.useForm)({
|
|
3273
|
+
defaultValues: { draft: "" },
|
|
3274
|
+
mode: "onChange"
|
|
3275
|
+
});
|
|
3276
|
+
const trimmedDraft = ((0, react_hook_form.useWatch)({
|
|
3277
|
+
control,
|
|
3278
|
+
name: "draft"
|
|
3279
|
+
}) ?? "").trim();
|
|
3280
|
+
const validationState = (0, react.useMemo)(() => {
|
|
3281
|
+
if (!trimmedDraft) return {
|
|
3282
|
+
canAdd: false,
|
|
3283
|
+
showInvalid: false
|
|
3284
|
+
};
|
|
3285
|
+
if (value.includes(trimmedDraft)) return {
|
|
3286
|
+
canAdd: false,
|
|
3287
|
+
showInvalid: false
|
|
3288
|
+
};
|
|
3289
|
+
if (atLimit) return {
|
|
3290
|
+
canAdd: false,
|
|
3291
|
+
showInvalid: false
|
|
3292
|
+
};
|
|
3293
|
+
if (addressing && !addressing.isValidAddress(trimmedDraft)) return {
|
|
3294
|
+
canAdd: false,
|
|
3295
|
+
showInvalid: true
|
|
3296
|
+
};
|
|
3297
|
+
return {
|
|
3298
|
+
canAdd: true,
|
|
3299
|
+
showInvalid: false
|
|
3300
|
+
};
|
|
3301
|
+
}, [
|
|
3302
|
+
trimmedDraft,
|
|
3303
|
+
value,
|
|
3304
|
+
atLimit,
|
|
3305
|
+
addressing
|
|
3306
|
+
]);
|
|
3307
|
+
const submitAdd = (0, react.useCallback)(() => {
|
|
3308
|
+
if (!validationState.canAdd) return;
|
|
3309
|
+
onAdd(getValues("draft").trim());
|
|
3310
|
+
reset({ draft: "" });
|
|
3311
|
+
}, [
|
|
3312
|
+
validationState.canAdd,
|
|
3313
|
+
onAdd,
|
|
3314
|
+
getValues,
|
|
3315
|
+
reset
|
|
3316
|
+
]);
|
|
3317
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3318
|
+
className: "space-y-1",
|
|
3319
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3320
|
+
className: "flex items-start gap-2",
|
|
3321
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3322
|
+
className: "min-w-0 flex-1",
|
|
3323
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressField, {
|
|
3324
|
+
id: `address-list-single-${fieldId}`,
|
|
3325
|
+
name: "draft",
|
|
3326
|
+
label: "",
|
|
3327
|
+
placeholder,
|
|
3328
|
+
helperText: validationState.showInvalid ? labels.invalidSingleAddress : void 0,
|
|
3329
|
+
control,
|
|
3330
|
+
addressing,
|
|
3331
|
+
validation: { required: false },
|
|
3332
|
+
readOnly: inputDisabled
|
|
3333
|
+
}), modeToggle ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3334
|
+
className: "-mt-px flex justify-end pr-2",
|
|
3335
|
+
children: modeToggle
|
|
3336
|
+
}) : null]
|
|
3337
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
3219
3338
|
type: "button",
|
|
3220
|
-
|
|
3221
|
-
disabled: inputDisabled ||
|
|
3222
|
-
onClick: ()
|
|
3223
|
-
children:
|
|
3224
|
-
})
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3339
|
+
className: "h-10 shrink-0",
|
|
3340
|
+
disabled: inputDisabled || !validationState.canAdd,
|
|
3341
|
+
onClick: handleSubmit(submitAdd),
|
|
3342
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "mr-1 size-4" }), labels.addSingleAddress]
|
|
3343
|
+
})]
|
|
3344
|
+
}), atLimit && maxItems != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3345
|
+
className: "text-xs text-muted-foreground",
|
|
3346
|
+
children: labels.maxItemsReached(maxItems)
|
|
3347
|
+
})]
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
//#endregion
|
|
3352
|
+
//#region src/components/fields/AddressListField/AddressListField.tsx
|
|
3353
|
+
/**
|
|
3354
|
+
* Multi-address list field with single-entry default and optional bulk paste.
|
|
3355
|
+
*/
|
|
3356
|
+
function AddressListField({ value, onChange, placeholder, bulkPlaceholder, formatHint, addressing, getExplorerUrl, label, helperText, maxItems, disabled = false, maxInvalidPreview = 3, className, defaultEntryMode = "single", allowModeToggle = true, labels: labelOverrides }) {
|
|
3357
|
+
const fieldId = (0, react.useId)();
|
|
3358
|
+
const labels = (0, react.useMemo)(() => resolveAddressListFieldLabels(labelOverrides), [labelOverrides]);
|
|
3359
|
+
const [entryMode, setEntryMode] = (0, react.useState)(defaultEntryMode);
|
|
3360
|
+
const resolvedBulkPlaceholder = bulkPlaceholder ?? placeholder;
|
|
3361
|
+
const handleRemove = (0, react.useCallback)((index) => {
|
|
3362
|
+
onChange(value.filter((_, currentIndex) => currentIndex !== index));
|
|
3363
|
+
}, [onChange, value]);
|
|
3364
|
+
const handleAddSingle = (0, react.useCallback)((address) => {
|
|
3365
|
+
onChange([...value, address]);
|
|
3366
|
+
}, [onChange, value]);
|
|
3367
|
+
const handleAddBulk = (0, react.useCallback)((addresses) => {
|
|
3368
|
+
onChange([...value, ...addresses]);
|
|
3369
|
+
}, [onChange, value]);
|
|
3370
|
+
const switchToBulk = (0, react.useCallback)(() => setEntryMode("bulk"), []);
|
|
3371
|
+
const switchToSingle = (0, react.useCallback)(() => setEntryMode("single"), []);
|
|
3372
|
+
const toggleClassName = "inline-flex items-center gap-1 rounded-b-md rounded-t-none border border-t-0 border-input bg-muted/40 px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground";
|
|
3373
|
+
const modeToggle = !allowModeToggle ? void 0 : entryMode === "single" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3374
|
+
type: "button",
|
|
3375
|
+
onClick: switchToBulk,
|
|
3376
|
+
className: toggleClassName,
|
|
3377
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ClipboardPaste, { className: "size-3" }), labels.enableBulkEntry]
|
|
3378
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3379
|
+
type: "button",
|
|
3380
|
+
onClick: switchToSingle,
|
|
3381
|
+
className: toggleClassName,
|
|
3382
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.PencilLine, { className: "size-3" }), labels.enableSingleEntry]
|
|
3383
|
+
});
|
|
3384
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3385
|
+
className: (0, _openzeppelin_ui_utils.cn)("space-y-3", className),
|
|
3386
|
+
children: [
|
|
3387
|
+
label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Label, {
|
|
3388
|
+
className: "text-sm font-medium leading-none",
|
|
3389
|
+
children: label
|
|
3390
|
+
}) : null,
|
|
3391
|
+
helperText && entryMode === "single" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3392
|
+
className: "text-sm leading-relaxed text-muted-foreground",
|
|
3393
|
+
children: helperText
|
|
3394
|
+
}) : null,
|
|
3395
|
+
entryMode === "single" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressListSingleEntry, {
|
|
3396
|
+
fieldId,
|
|
3397
|
+
value,
|
|
3398
|
+
onAdd: handleAddSingle,
|
|
3399
|
+
placeholder,
|
|
3400
|
+
addressing,
|
|
3401
|
+
maxItems,
|
|
3402
|
+
disabled,
|
|
3403
|
+
labels,
|
|
3404
|
+
modeToggle
|
|
3405
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressListBulkEntry, {
|
|
3406
|
+
fieldId,
|
|
3407
|
+
value,
|
|
3408
|
+
onAdd: handleAddBulk,
|
|
3409
|
+
placeholder: resolvedBulkPlaceholder,
|
|
3410
|
+
formatHint,
|
|
3411
|
+
addressing,
|
|
3412
|
+
maxItems,
|
|
3413
|
+
disabled,
|
|
3414
|
+
maxInvalidPreview,
|
|
3415
|
+
labels,
|
|
3416
|
+
modeToggle
|
|
3228
3417
|
}),
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
className: "max-h-44 space-y-1 overflow-y-auto rounded-md border border-border/70 p-1",
|
|
3236
|
-
children: value.map((address, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3237
|
-
className: "flex items-center justify-between rounded bg-muted p-2",
|
|
3238
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressDisplay, {
|
|
3239
|
-
address,
|
|
3240
|
-
variant: "inline",
|
|
3241
|
-
truncateWhenLabeled: true,
|
|
3242
|
-
showCopyButton: true,
|
|
3243
|
-
explorerUrl: getExplorerUrl?.(address) ?? void 0
|
|
3244
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
3245
|
-
type: "button",
|
|
3246
|
-
variant: "ghost",
|
|
3247
|
-
size: "sm",
|
|
3248
|
-
className: "size-7 shrink-0 p-0 text-muted-foreground hover:text-destructive",
|
|
3249
|
-
onClick: () => handleRemove(index),
|
|
3250
|
-
disabled,
|
|
3251
|
-
"aria-label": labels.removeAddress(index),
|
|
3252
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "size-3.5" })
|
|
3253
|
-
})]
|
|
3254
|
-
}, `${address}-${index}`))
|
|
3255
|
-
})]
|
|
3418
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressListEntries, {
|
|
3419
|
+
value,
|
|
3420
|
+
getExplorerUrl,
|
|
3421
|
+
disabled,
|
|
3422
|
+
labels,
|
|
3423
|
+
onRemove: handleRemove
|
|
3256
3424
|
})
|
|
3257
3425
|
]
|
|
3258
3426
|
});
|