@openzeppelin/ui-components 1.5.0 → 1.6.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.mjs CHANGED
@@ -6,6 +6,7 @@ import * as React$1 from "react";
6
6
  import React, { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
7
7
  import { cn, getDefaultValueForType, getInvalidUrlMessage, getServiceDisplayName, isValidUrl, truncateMiddle, validateBytesSimple } from "@openzeppelin/ui-utils";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
9
10
  import { Slot, Slottable } from "@radix-ui/react-slot";
10
11
  import { DayPicker } from "react-day-picker";
11
12
  import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
@@ -20,7 +21,6 @@ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
20
21
  import * as SelectPrimitive from "@radix-ui/react-select";
21
22
  import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
22
23
  import * as TabsPrimitive from "@radix-ui/react-tabs";
23
- import * as TooltipPrimitive from "@radix-ui/react-tooltip";
24
24
  import { isMapEntryArray } from "@openzeppelin/ui-types";
25
25
  import { Toaster as Toaster$1, toast } from "sonner";
26
26
  import { useTheme } from "next-themes";
@@ -102,6 +102,19 @@ const AccordionContent = React$1.forwardRef(({ className, children, variant: var
102
102
  });
103
103
  AccordionContent.displayName = "AccordionContent";
104
104
 
105
+ //#endregion
106
+ //#region src/components/ui/tooltip.tsx
107
+ const TooltipProvider = TooltipPrimitive.Provider;
108
+ const Tooltip = TooltipPrimitive.Root;
109
+ const TooltipTrigger = TooltipPrimitive.Trigger;
110
+ const TooltipContent = React$1.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsx(TooltipPrimitive.Content, {
111
+ ref,
112
+ sideOffset,
113
+ className: cn("bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md px-3 py-1.5 text-xs", className),
114
+ ...props
115
+ }) }));
116
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
117
+
105
118
  //#endregion
106
119
  //#region src/components/ui/address-display/context.ts
107
120
  /**
@@ -114,8 +127,20 @@ const AddressLabelContext = createContext(null);
114
127
  //#endregion
115
128
  //#region src/components/ui/address-display/address-display.tsx
116
129
  /**
130
+ * True when the primary input can hover (e.g. desktop with mouse).
131
+ * Touch-first phones typically report false. SSR assumes hover-capable.
132
+ */
133
+ function usePrefersHover() {
134
+ return React$1.useSyncExternalStore(React$1.useCallback((onStoreChange) => {
135
+ if (typeof window === "undefined" || typeof window.matchMedia === "undefined") return () => {};
136
+ const mq = window.matchMedia("(hover: hover)");
137
+ mq.addEventListener("change", onStoreChange);
138
+ return () => mq.removeEventListener("change", onStoreChange);
139
+ }, []), () => typeof window !== "undefined" && typeof window.matchMedia !== "undefined" ? window.matchMedia("(hover: hover)").matches : true, () => true);
140
+ }
141
+ /**
117
142
  * Displays a blockchain address with optional truncation, copy button,
118
- * explorer link, and human-readable label.
143
+ * explorer link, tooltip, and human-readable label.
119
144
  *
120
145
  * Labels are resolved in priority order:
121
146
  * 1. Explicit `label` prop
@@ -140,11 +165,22 @@ const AddressLabelContext = createContext(null);
140
165
  *
141
166
  * // Suppress label resolution for a specific instance
142
167
  * <AddressDisplay address="0x742d35Cc..." disableLabel />
168
+ *
169
+ * // Reveal full address on hover (still truncated when idle)
170
+ * <AddressDisplay address="0x742d35Cc..." untruncateOnHover />
171
+ *
172
+ * // Tooltip with full address on hover + copy icon on hover
173
+ * <AddressDisplay address="0x742d35Cc..." showTooltip showCopyButton showCopyButtonOnHover />
174
+ *
175
+ * // Inline variant (no chip background) — useful inside wallet bars
176
+ * <AddressDisplay address="0x742d35Cc..." variant="inline" showTooltip showCopyButton />
143
177
  * ```
144
178
  */
145
- function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, label: labelProp, onLabelEdit: onLabelEditProp, networkId, disableLabel = false, className, ...props }) {
179
+ function AddressDisplay({ address, truncate = true, untruncateOnHover = false, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, label: labelProp, onLabelEdit: onLabelEditProp, networkId, disableLabel = false, showTooltip = false, variant = "chip", className, onPointerEnter, onPointerLeave, onMouseEnter, onMouseLeave, onClick, ...props }) {
146
180
  const [copied, setCopied] = React$1.useState(false);
181
+ const [isHovered, setIsHovered] = React$1.useState(false);
147
182
  const copyTimeoutRef = React$1.useRef(null);
183
+ const prefersHover = usePrefersHover();
148
184
  const resolver = React$1.useContext(AddressLabelContext);
149
185
  const resolvedLabel = disableLabel ? void 0 : labelProp ?? resolver?.resolveLabel(address, networkId);
150
186
  const contextEditHandler = React$1.useCallback(() => {
@@ -155,7 +191,25 @@ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4
155
191
  networkId
156
192
  ]);
157
193
  const editHandler = disableLabel ? void 0 : onLabelEditProp ?? (resolver?.onEditLabel ? contextEditHandler : void 0);
158
- const displayAddress = truncate ? truncateMiddle(address, startChars, endChars) : address;
194
+ const canUntruncate = untruncateOnHover && truncate && !showTooltip;
195
+ const showFullAddress = !truncate || canUntruncate && isHovered;
196
+ const displayAddress = showFullAddress ? address : truncateMiddle(address, startChars, endChars);
197
+ const addressTextClassName = cn(!showFullAddress && "truncate", (showFullAddress || !truncate) && "break-all");
198
+ const expandInteractionClassName = canUntruncate && !prefersHover ? "cursor-pointer" : void 0;
199
+ const handlePointerEnter = (e) => {
200
+ if (canUntruncate && prefersHover) setIsHovered(true);
201
+ onPointerEnter?.(e);
202
+ onMouseEnter?.(e);
203
+ };
204
+ const handlePointerLeave = (e) => {
205
+ if (canUntruncate && prefersHover) setIsHovered(false);
206
+ onPointerLeave?.(e);
207
+ onMouseLeave?.(e);
208
+ };
209
+ const handleUntruncateClick = (e) => {
210
+ if (canUntruncate && !prefersHover) setIsHovered((open) => !open);
211
+ onClick?.(e);
212
+ };
159
213
  const handleCopy = (e) => {
160
214
  e.stopPropagation();
161
215
  navigator.clipboard.writeText(address);
@@ -171,6 +225,7 @@ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4
171
225
  if (copyTimeoutRef.current) window.clearTimeout(copyTimeoutRef.current);
172
226
  };
173
227
  }, []);
228
+ const isChip = variant === "chip";
174
229
  const actionButtons = /* @__PURE__ */ jsxs(Fragment, { children: [
175
230
  showCopyButton && /* @__PURE__ */ jsx("button", {
176
231
  type: "button",
@@ -183,6 +238,9 @@ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4
183
238
  href: explorerUrl,
184
239
  target: "_blank",
185
240
  rel: "noopener noreferrer",
241
+ onClick: (e) => {
242
+ e.stopPropagation();
243
+ },
186
244
  className: "ml-1.5 shrink-0 text-slate-500 transition-colors hover:text-slate-700",
187
245
  "aria-label": "View in explorer",
188
246
  children: /* @__PURE__ */ jsx(ExternalLink$1, { className: "h-3.5 w-3.5" })
@@ -198,28 +256,48 @@ function AddressDisplay({ address, truncate = true, startChars = 6, endChars = 4
198
256
  children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" })
199
257
  })
200
258
  ] });
201
- if (resolvedLabel) return /* @__PURE__ */ jsxs("div", {
202
- className: cn("group inline-flex max-w-full flex-col rounded-md bg-slate-100 px-2 py-1", "text-xs text-slate-700", className),
259
+ const shouldShowTooltip = showTooltip && truncate;
260
+ const wrapWithTooltip = (content) => {
261
+ if (!shouldShowTooltip) return content;
262
+ return /* @__PURE__ */ jsx(TooltipProvider, {
263
+ delayDuration: 300,
264
+ children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
265
+ asChild: true,
266
+ children: content
267
+ }), /* @__PURE__ */ jsx(TooltipContent, {
268
+ className: "font-mono text-xs",
269
+ children: address
270
+ })] })
271
+ });
272
+ };
273
+ if (resolvedLabel) return wrapWithTooltip(/* @__PURE__ */ jsxs("div", {
274
+ className: cn("group inline-flex max-w-full min-w-0 flex-col", isChip && "rounded-md bg-slate-100 px-2 py-1", "text-xs text-slate-700", expandInteractionClassName, className),
275
+ onPointerEnter: handlePointerEnter,
276
+ onPointerLeave: handlePointerLeave,
277
+ onClick: handleUntruncateClick,
203
278
  ...props,
204
279
  children: [/* @__PURE__ */ jsx("span", {
205
280
  className: "truncate font-sans font-medium text-slate-900 leading-snug",
206
281
  children: resolvedLabel
207
282
  }), /* @__PURE__ */ jsxs("div", {
208
- className: "flex items-center font-mono text-[10px] text-slate-400 leading-snug",
283
+ className: "flex min-w-0 items-center font-mono text-[10px] text-slate-400 leading-snug",
209
284
  children: [/* @__PURE__ */ jsx("span", {
210
- className: cn("truncate", truncate ? "" : "break-all"),
285
+ className: addressTextClassName,
211
286
  children: displayAddress
212
287
  }), actionButtons]
213
288
  })]
214
- });
215
- return /* @__PURE__ */ jsxs("div", {
216
- className: cn("group inline-flex max-w-full items-center rounded-md bg-slate-100 px-2 py-1", "text-xs font-mono text-slate-700", className),
289
+ }));
290
+ return wrapWithTooltip(/* @__PURE__ */ jsxs("div", {
291
+ className: cn("group inline-flex max-w-full min-w-0 items-center", isChip && "rounded-md bg-slate-100 px-2 py-1", "text-xs font-mono text-slate-700", expandInteractionClassName, className),
292
+ onPointerEnter: handlePointerEnter,
293
+ onPointerLeave: handlePointerLeave,
294
+ onClick: handleUntruncateClick,
217
295
  ...props,
218
296
  children: [/* @__PURE__ */ jsx("span", {
219
- className: cn("truncate", truncate ? "" : "break-all"),
297
+ className: addressTextClassName,
220
298
  children: displayAddress
221
299
  }), actionButtons]
222
- });
300
+ }));
223
301
  }
224
302
 
225
303
  //#endregion
@@ -1796,19 +1874,6 @@ const Textarea = React$1.forwardRef(({ className, ...props }, ref) => {
1796
1874
  });
1797
1875
  Textarea.displayName = "Textarea";
1798
1876
 
1799
- //#endregion
1800
- //#region src/components/ui/tooltip.tsx
1801
- const TooltipProvider = TooltipPrimitive.Provider;
1802
- const Tooltip = TooltipPrimitive.Root;
1803
- const TooltipTrigger = TooltipPrimitive.Trigger;
1804
- const TooltipContent = React$1.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(TooltipPrimitive.Content, {
1805
- ref,
1806
- sideOffset,
1807
- className: cn("bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md px-3 py-1.5 text-xs", className),
1808
- ...props
1809
- }));
1810
- TooltipContent.displayName = TooltipPrimitive.Content.displayName;
1811
-
1812
1877
  //#endregion
1813
1878
  //#region src/components/ui/view-contract-state-button.tsx
1814
1879
  /**