@zerodev/react-ui 0.0.3 → 0.0.5

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.
Files changed (84) hide show
  1. package/dist/_types/components/ArrowCardPair/index.d.ts +2 -2
  2. package/dist/_types/components/ArrowCardPair/index.d.ts.map +1 -1
  3. package/dist/_types/components/Badge/index.d.ts +1 -1
  4. package/dist/_types/components/Badge/index.d.ts.map +1 -1
  5. package/dist/_types/components/BottomSheet/index.d.ts +17 -0
  6. package/dist/_types/components/BottomSheet/index.d.ts.map +1 -0
  7. package/dist/_types/components/Button/index.d.ts +1 -1
  8. package/dist/_types/components/Button/index.d.ts.map +1 -1
  9. package/dist/_types/components/Callout/index.d.ts +1 -1
  10. package/dist/_types/components/Callout/index.d.ts.map +1 -1
  11. package/dist/_types/components/DataRow/index.d.ts +6 -2
  12. package/dist/_types/components/DataRow/index.d.ts.map +1 -1
  13. package/dist/_types/components/Icon/index.d.ts +1 -1
  14. package/dist/_types/components/Icon/index.d.ts.map +1 -1
  15. package/dist/_types/components/IconButton/index.d.ts +1 -1
  16. package/dist/_types/components/IconButton/index.d.ts.map +1 -1
  17. package/dist/_types/components/InfoCard/index.d.ts +17 -0
  18. package/dist/_types/components/InfoCard/index.d.ts.map +1 -0
  19. package/dist/_types/components/Input/index.d.ts +1 -1
  20. package/dist/_types/components/Input/index.d.ts.map +1 -1
  21. package/dist/_types/components/ListItem/index.d.ts +32 -15
  22. package/dist/_types/components/ListItem/index.d.ts.map +1 -1
  23. package/dist/_types/components/Pill/index.d.ts +25 -0
  24. package/dist/_types/components/Pill/index.d.ts.map +1 -0
  25. package/dist/_types/components/PoweredBy/index.d.ts +6 -0
  26. package/dist/_types/components/PoweredBy/index.d.ts.map +1 -0
  27. package/dist/_types/components/Screen/EllipticalRadial.d.ts +23 -0
  28. package/dist/_types/components/Screen/EllipticalRadial.d.ts.map +1 -0
  29. package/dist/_types/components/Screen/MultiRadialBackground.d.ts +4 -0
  30. package/dist/_types/components/Screen/MultiRadialBackground.d.ts.map +1 -0
  31. package/dist/_types/components/Screen/index.d.ts +15 -0
  32. package/dist/_types/components/Screen/index.d.ts.map +1 -0
  33. package/dist/_types/components/Section/MessageDetails/index.d.ts +5 -0
  34. package/dist/_types/components/Section/MessageDetails/index.d.ts.map +1 -0
  35. package/dist/_types/components/Section/index.d.ts +11 -0
  36. package/dist/_types/components/Section/index.d.ts.map +1 -0
  37. package/dist/_types/components/Select/index.d.ts +28 -0
  38. package/dist/_types/components/Select/index.d.ts.map +1 -0
  39. package/dist/_types/components/Switch/index.d.ts +1 -1
  40. package/dist/_types/components/Switch/index.d.ts.map +1 -1
  41. package/dist/_types/components/Text/index.d.ts +1 -1
  42. package/dist/_types/components/Text/index.d.ts.map +1 -1
  43. package/dist/_types/components/TokenListItem/index.d.ts +33 -0
  44. package/dist/_types/components/TokenListItem/index.d.ts.map +1 -0
  45. package/dist/_types/components/Tooltip/index.d.ts +43 -0
  46. package/dist/_types/components/Tooltip/index.d.ts.map +1 -0
  47. package/dist/_types/components/TopNav/index.d.ts +20 -0
  48. package/dist/_types/components/TopNav/index.d.ts.map +1 -0
  49. package/dist/_types/components/WrappedPressable/index.d.ts +1 -1
  50. package/dist/_types/components/WrappedPressable/index.d.ts.map +1 -1
  51. package/dist/_types/components/Wrapper/index.d.ts +3 -2
  52. package/dist/_types/components/Wrapper/index.d.ts.map +1 -1
  53. package/dist/_types/components/ZeroDevLogo/index.d.ts +1 -1
  54. package/dist/_types/components/ZeroDevLogo/index.d.ts.map +1 -1
  55. package/dist/_types/index.d.ts +13 -2
  56. package/dist/_types/index.d.ts.map +1 -1
  57. package/dist/_types/utils/common.d.ts +4 -0
  58. package/dist/_types/utils/common.d.ts.map +1 -1
  59. package/dist/index.cjs +51 -7
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.mjs +6538 -1341
  62. package/dist/index.mjs.map +1 -1
  63. package/dist/styles.css +2 -2
  64. package/package.json +7 -5
  65. package/src/components/ArrowCardPair/index.tsx +25 -5
  66. package/src/components/BottomSheet/index.tsx +57 -0
  67. package/src/components/DataRow/index.tsx +55 -29
  68. package/src/components/InfoCard/index.tsx +104 -0
  69. package/src/components/ListItem/index.tsx +74 -79
  70. package/src/components/Pill/index.tsx +167 -0
  71. package/src/components/PoweredBy/index.tsx +29 -0
  72. package/src/components/Screen/EllipticalRadial.tsx +65 -0
  73. package/src/components/Screen/MultiRadialBackground.tsx +323 -0
  74. package/src/components/Screen/index.tsx +121 -0
  75. package/src/components/Section/MessageDetails/index.tsx +19 -0
  76. package/src/components/Section/index.tsx +50 -0
  77. package/src/components/Select/index.tsx +153 -0
  78. package/src/components/TokenListItem/index.tsx +192 -0
  79. package/src/components/Tooltip/index.tsx +94 -0
  80. package/src/components/TopNav/index.tsx +67 -0
  81. package/src/components/Wrapper/index.tsx +4 -1
  82. package/src/index.ts +54 -1
  83. package/src/utils/common.ts +15 -0
  84. package/tailwind.config.ts +47 -0
@@ -0,0 +1,323 @@
1
+ import { useId } from 'react'
2
+
3
+ import { EllipticalRadial, type RadialLayer } from './EllipticalRadial'
4
+
5
+ // The design frame: the gradient layers live on a 412×812 rect offset to
6
+ // (-6,-6) so they bleed 6px past the 400×800 card on every side — that 6px
7
+ // reveal is the wrapper's gradient border. The matrices below are the
8
+ // Figma-exported gradientTransform values verbatim, so rendering the SVG with
9
+ // a matching viewBox + preserveAspectRatio="none" reproduces the design 1:1
10
+ // while still scaling to any container size.
11
+ const FRAME_VIEWBOX = '-6 -6 412 812'
12
+
13
+ export function MultiRadialBackground() {
14
+ // The colorful gradient now lives entirely in CardGlow. This base layer is
15
+ // just a flat, soft warm-white fill behind it so CardGlow's colors read
16
+ // bright and clean, with no gray muddiers underneath.
17
+ return (
18
+ <div className="zd:absolute zd:inset-0 zd:z-0 zd:rounded-4xl zd:pointer-events-none">
19
+ <svg
20
+ width="100%"
21
+ height="100%"
22
+ viewBox={FRAME_VIEWBOX}
23
+ preserveAspectRatio="none"
24
+ role="img"
25
+ aria-label="Decorative gradient background"
26
+ >
27
+ <rect x="-6" y="-6" width="412" height="812" fill="#FBF7F2" />
28
+ </svg>
29
+ </div>
30
+ )
31
+ }
32
+
33
+ // On-card overlay (400×800), sitting ON TOP of the translucent card body so its
34
+ // color reads at full strength (the card at 0.8 opacity would otherwise wash a
35
+ // behind-card gradient to ~20%). This reproduces the vivid orange + blue that in
36
+ // the source design came from the decorative "Amorphic" blob raster overlaid on
37
+ // the lower card, plus the design's own white card glow (Figma paint6).
38
+ //
39
+ // The orange + blue lobes are NOT verbatim Figma layers — they reconstruct the
40
+ // blob's measured color footprint (orange across the lower card; blue lobe
41
+ // bottom-left ~x 0-150 / y 600-800) as gradients, per the decision to treat the
42
+ // blob as a background effect rather than ship the raster.
43
+ export function CardGlow() {
44
+ const baseId = useId()
45
+ // VERBATIM from the Figma `--Background-Brand-Gradient` token (6 stacked
46
+ // radial-gradients). CSS lists topmost-first; SVG paints last-on-top, so the
47
+ // array is the CSS list REVERSED. Each matrix is the CSS `rx% ry% at cx% cy%`
48
+ // converted over the 400×800 frame. An upward linear fade is applied on top
49
+ // (see the <rect> below) to soften the gradient toward the top.
50
+ const layers: RadialLayer[] = [
51
+ {
52
+ // (CSS #6) blue → top-right. x-radius widened so the lobe reaches further
53
+ // left; y-radius kept short so it stays up top and doesn't bleed into the
54
+ // orange band.
55
+ id: `${baseId}-blue`,
56
+ matrix: [1850, 0, 0, 645, 381.44, 0],
57
+ fillOpacity: 1,
58
+ stops: [
59
+ { offset: 0, color: '#45ABFB', opacity: 0.66 },
60
+ { offset: 0.4615, color: '#1E9AFB', opacity: 0.35 },
61
+ { offset: 1, color: '#45ABFB', opacity: 0 },
62
+ ],
63
+ },
64
+ {
65
+ // (CSS #5) offWhite → blue, very faint
66
+ id: `${baseId}-offwhite-tr`,
67
+ matrix: [55.28, 0, 0, 773.36, 386.68, 13.28],
68
+ fillOpacity: 1,
69
+ stops: [
70
+ { offset: 0, color: '#F7F5F0', opacity: 0.1 },
71
+ { offset: 1, color: '#45ABFB', opacity: 0 },
72
+ ],
73
+ },
74
+ {
75
+ // (CSS #4) warmGrey
76
+ id: `${baseId}-warmgrey`,
77
+ matrix: [326.72, 0, 0, 855.52, 0, 50.32],
78
+ fillOpacity: 1,
79
+ stops: [
80
+ { offset: 0, color: '#E3CFC3', opacity: 1 },
81
+ { offset: 1, color: '#E3CFC3', opacity: 0 },
82
+ ],
83
+ },
84
+ {
85
+ // (CSS #3) orange — spans the full width across the bottom ~30%, uniform.
86
+ id: `${baseId}-orange`,
87
+ matrix: [580, 0, 0, 380, 200, 835],
88
+ fillOpacity: 1,
89
+ stops: [
90
+ { offset: 0, color: '#EE6C2E', opacity: 0.8 },
91
+ { offset: 0.55, color: '#EE6C2E', opacity: 0.6 },
92
+ { offset: 1, color: '#EE6C2E', opacity: 0.15 },
93
+ ],
94
+ },
95
+ {
96
+ // (CSS #2) peach — spread further up (~30% of the bottom), a bit more vivid
97
+ id: `${baseId}-peach`,
98
+ matrix: [520, 0, 0, 560, 180, 830],
99
+ fillOpacity: 1,
100
+ stops: [
101
+ { offset: 0, color: '#FAC8AC', opacity: 1 },
102
+ { offset: 1, color: '#F27B3E', opacity: 0 },
103
+ ],
104
+ },
105
+ {
106
+ // (CSS #1) white glow (topmost of the colour stack)
107
+ id: `${baseId}-white-bl`,
108
+ matrix: [90.72, 0, 0, 550.48, 38.04, 702.64],
109
+ fillOpacity: 1,
110
+ stops: [
111
+ { offset: 0, color: 'white', opacity: 0.58 },
112
+ { offset: 1, color: 'white', opacity: 0 },
113
+ ],
114
+ },
115
+ ]
116
+
117
+ return (
118
+ <div className="zd:absolute zd:inset-0 zd:z-0 zd:rounded-4xl zd:pointer-events-none zd:overflow-hidden">
119
+ <svg
120
+ width="100%"
121
+ height="100%"
122
+ viewBox="0 0 400 800"
123
+ preserveAspectRatio="none"
124
+ role="presentation"
125
+ >
126
+ {layers.map((layer) => (
127
+ <EllipticalRadial
128
+ key={layer.id}
129
+ layer={layer}
130
+ rect={{ x: 0, y: 0, width: 400, height: 800 }}
131
+ />
132
+ ))}
133
+ {/* Vertical white fade — strongest at the bottom, easing linearly to
134
+ nothing at the top. Softens the lower (most vivid) part of the
135
+ gradient. A gradient layer (not an opaque card) so the frosted
136
+ buttons composite over it cleanly. */}
137
+ <defs>
138
+ <linearGradient
139
+ id={`${baseId}-vfade`}
140
+ x1="0"
141
+ y1="0"
142
+ x2="0"
143
+ y2="800"
144
+ gradientUnits="userSpaceOnUse"
145
+ >
146
+ <stop offset="0" stopColor="white" stopOpacity="0.55" />
147
+ <stop offset="0.45" stopColor="white" stopOpacity="0.52" />
148
+ <stop offset="0.65" stopColor="white" stopOpacity="0.5" />
149
+ <stop offset="0.85" stopColor="white" stopOpacity="0.5" />
150
+ <stop offset="1" stopColor="white" stopOpacity="0.5" />
151
+ </linearGradient>
152
+ </defs>
153
+ <rect
154
+ x="0"
155
+ y="0"
156
+ width="400"
157
+ height="800"
158
+ fill={`url(#${baseId}-vfade)`}
159
+ />
160
+ {/* Blue — a diagonal linear gradient from the bottom-left corner fading
161
+ to transparent toward the top-right, so it blends smoothly into the
162
+ orange with no hard edge. On top of the fade so #A7B8E2 reads true. */}
163
+ <defs>
164
+ <linearGradient
165
+ id={`${baseId}-bluefade`}
166
+ x1="0"
167
+ y1="800"
168
+ x2="120"
169
+ y2="640"
170
+ gradientUnits="userSpaceOnUse"
171
+ >
172
+ <stop offset="0" stopColor="#A7B8E2" stopOpacity="1" />
173
+ <stop offset="0.33" stopColor="#A7B8E2" stopOpacity="0.7" />
174
+ <stop offset="0.66" stopColor="#A7B8E2" stopOpacity="0.35" />
175
+ <stop offset="1" stopColor="#A7B8E2" stopOpacity="0" />
176
+ </linearGradient>
177
+ </defs>
178
+ <rect
179
+ x="0"
180
+ y="0"
181
+ width="400"
182
+ height="800"
183
+ fill={`url(#${baseId}-bluefade)`}
184
+ />
185
+ </svg>
186
+ </div>
187
+ )
188
+ }
189
+
190
+ // The wrapper's gradient BORDER. Renders full-frame behind the inner card; the
191
+ // card sits on top with a 6px margin so this gradient shows as the border ring.
192
+ // Uses normalized cx/cy/rx/ry radial gradients (scale-transform) over a #130E0B
193
+ // base — the original border look.
194
+ type BorderLayer = {
195
+ id: string
196
+ cx: number
197
+ cy: number
198
+ rx: number
199
+ ry: number
200
+ stops: { offset: number; color: string; opacity: number }[]
201
+ }
202
+
203
+ export function WrapperBorder() {
204
+ const baseId = useId()
205
+ // VERBATIM from the Figma `--Background-Brand-Gradient` token (6 stacked
206
+ // radial-gradients). CSS lists topmost-first, SVG paints last-on-top, so the
207
+ // array is the CSS list REVERSED. Each CSS `rx% ry% at cx% cy%` maps to
208
+ // normalized cx/cy/rx/ry (÷100); the inline <radialGradient> below applies
209
+ // the cx=(cx/rx)*100% + scale(rx,ry) convention.
210
+ const layers: BorderLayer[] = [
211
+ {
212
+ id: `${baseId}-g6`, // (CSS #6) blue, top-right
213
+ cx: 0.9536,
214
+ cy: 0.0,
215
+ rx: 0.9385,
216
+ ry: 1.2256,
217
+ stops: [
218
+ { offset: 0, color: '#45ABFB', opacity: 0.6 },
219
+ { offset: 0.4615, color: '#1E9AFB', opacity: 0.32 },
220
+ { offset: 1, color: '#45ABFB', opacity: 0 },
221
+ ],
222
+ },
223
+ {
224
+ id: `${baseId}-g5`, // (CSS #5) faint light, top-right
225
+ cx: 0.9667,
226
+ cy: 0.0166,
227
+ rx: 0.1382,
228
+ ry: 0.9667,
229
+ stops: [
230
+ { offset: 0, color: '#F7F5F0', opacity: 0.1 },
231
+ { offset: 1, color: '#45ABFB', opacity: 0 },
232
+ ],
233
+ },
234
+ {
235
+ id: `${baseId}-g4`, // (CSS #4) warm grey, top-left
236
+ cx: 0.0,
237
+ cy: 0.0629,
238
+ rx: 0.8168,
239
+ ry: 1.0694,
240
+ stops: [
241
+ { offset: 0, color: '#E3CFC3', opacity: 1 },
242
+ { offset: 1, color: '#E3CFC3', opacity: 0 },
243
+ ],
244
+ },
245
+ {
246
+ id: `${baseId}-g3`, // (CSS #3) orange, bottom-left
247
+ cx: 0.0393,
248
+ cy: 1.0,
249
+ rx: 1.0628,
250
+ ry: 1.0303,
251
+ stops: [
252
+ { offset: 0, color: '#FF6B1F', opacity: 1 },
253
+ { offset: 0.476, color: '#E76000', opacity: 0.72 },
254
+ { offset: 1, color: '#E76000', opacity: 0 },
255
+ ],
256
+ },
257
+ {
258
+ id: `${baseId}-g2`, // (CSS #2) peach, bottom-center
259
+ cx: 0.3476,
260
+ cy: 1.0,
261
+ rx: 0.9614,
262
+ ry: 0.338,
263
+ stops: [
264
+ { offset: 0, color: '#FAC8AC', opacity: 0.7 },
265
+ { offset: 1, color: '#F27B3E', opacity: 0 },
266
+ ],
267
+ },
268
+ {
269
+ id: `${baseId}-g1`, // (CSS #1) white glow, bottom-left
270
+ cx: 0.0951,
271
+ cy: 0.8783,
272
+ rx: 0.2268,
273
+ ry: 0.6881,
274
+ stops: [
275
+ { offset: 0, color: '#FFFFFF', opacity: 0.58 },
276
+ { offset: 1, color: '#FFFFFF', opacity: 0 },
277
+ ],
278
+ },
279
+ ]
280
+
281
+ return (
282
+ <div className="zd:absolute zd:inset-0 zd:z-0 zd:rounded-4xl zd:pointer-events-none zd:overflow-hidden">
283
+ <svg
284
+ width="100%"
285
+ height="100%"
286
+ preserveAspectRatio="none"
287
+ role="presentation"
288
+ >
289
+ <rect width="100%" height="100%" fill="#130E0B" />
290
+ <defs>
291
+ {layers.map((layer) => (
292
+ <radialGradient
293
+ key={layer.id}
294
+ id={layer.id}
295
+ cx={`${(layer.cx / layer.rx) * 100}%`}
296
+ cy={`${(layer.cy / layer.ry) * 100}%`}
297
+ r="100%"
298
+ gradientUnits="objectBoundingBox"
299
+ gradientTransform={`scale(${layer.rx}, ${layer.ry})`}
300
+ >
301
+ {layer.stops.map((s, i) => (
302
+ <stop
303
+ key={s.color + i.toString()}
304
+ offset={s.offset}
305
+ stopColor={s.color}
306
+ stopOpacity={s.opacity}
307
+ />
308
+ ))}
309
+ </radialGradient>
310
+ ))}
311
+ </defs>
312
+ {layers.map((layer) => (
313
+ <rect
314
+ key={`${layer.id}-rect`}
315
+ width="100%"
316
+ height="100%"
317
+ fill={`url(#${layer.id})`}
318
+ />
319
+ ))}
320
+ </svg>
321
+ </div>
322
+ )
323
+ }
@@ -0,0 +1,121 @@
1
+ import {
2
+ type CSSProperties,
3
+ createContext,
4
+ type ReactNode,
5
+ useContext,
6
+ useState,
7
+ } from 'react'
8
+ import { cn } from '../../utils/common'
9
+ import { TOP_NAV_HEIGHT } from '../TopNav'
10
+ import {
11
+ CardGlow,
12
+ MultiRadialBackground,
13
+ WrapperBorder,
14
+ } from './MultiRadialBackground'
15
+
16
+ const CONTENT_PADDING_TOP = TOP_NAV_HEIGHT + 16
17
+
18
+ // Container for card-scoped overlays (bottom sheets, dialogs, etc.). Consumers
19
+ // portal into this element via `Radix Dialog.Portal container={...}` so the
20
+ // overlay is clipped inside the card's rounded frame instead of covering the
21
+ // whole page. Populated once Screen's inner card mounts.
22
+ const ScreenOverlayContext = createContext<HTMLDivElement | null>(null)
23
+
24
+ /** Element inside `Screen` that overlay/portal-based components (e.g. the QR
25
+ * sheet) should render into. Returns `null` outside a `Screen` — callers
26
+ * should guard on that and skip rendering. */
27
+ export function useScreenOverlayContainer() {
28
+ return useContext(ScreenOverlayContext)
29
+ }
30
+
31
+ export function Screen({
32
+ children,
33
+ className,
34
+ contentClassName,
35
+ size = 'lg',
36
+ style,
37
+ topNav,
38
+ overlay,
39
+ }: {
40
+ children: ReactNode
41
+ className?: string | undefined
42
+ contentClassName?: string | undefined
43
+ size?: 'sm' | 'md' | 'lg' | undefined
44
+ style?: CSSProperties | undefined
45
+ topNav?: ReactNode
46
+ overlay?: ReactNode
47
+ }) {
48
+ // Overlay container ref goes into state so children re-render once the DOM
49
+ // node is available — that lets deeply nested components own their own
50
+ // overlay state and portal into the card without hoisting it up here.
51
+ const [overlayContainer, setOverlayContainer] =
52
+ useState<HTMLDivElement | null>(null)
53
+
54
+ return (
55
+ <div
56
+ data-zd-size={size}
57
+ className={cn(
58
+ // 400×810 by default, but never taller/wider than the parent — a
59
+ // consumer wrapper with its own size caps the Screen (max-w/h-full =
60
+ // 100% of the parent). overflow-hidden clips the frame to its rounded
61
+ // border ring; content scrolls in the inner div below.
62
+ 'zd:relative zd:flex zd:flex-col zd:text-left',
63
+ // w-100 = 400px, h-202.5 = 810px at density 1; both compile to
64
+ // calc(var(--zd-spacing) * N), so they scale with the size variants.
65
+ // rounded-[38px] = inner card radius (32px) + the 6px border-ring gap,
66
+ // so the ring has a uniform width — equal inner/outer radii would make
67
+ // the corners read thicker than the sides.
68
+ 'zd:w-100 zd:h-202.5 zd:max-w-full zd:max-h-full zd:overflow-hidden zd:rounded-[38px]',
69
+ className,
70
+ )}
71
+ style={style}
72
+ >
73
+ {/* Gradient border layer — fills the frame; the card on top has a 6px
74
+ margin so this shows as the border ring. */}
75
+ <WrapperBorder />
76
+ {/* Inner card sits on top with a 6px margin, exposing the border ring.
77
+ Its own gradient (base + CardGlow) fills the card so the colour stays
78
+ vivid and distinct from the border. */}
79
+ <div
80
+ ref={setOverlayContainer}
81
+ className={cn(
82
+ // m-1.5 = calc(var(--zd-spacing) * 1.5) = 6px at density 1, so the
83
+ // gradient border ring thins with the size variants. (Corner radii
84
+ // stay fixed, so at smaller sizes the ring is marginally non-uniform
85
+ // at the corners — a minor cosmetic trade-off.)
86
+ 'zd:flex zd:flex-1 zd:flex-col zd:m-1.5 zd:px-4 zd:overflow-hidden zd:rounded-4xl zd:relative',
87
+ contentClassName,
88
+ )}
89
+ // clip-path clips backdrop-filter to the rounded corners; plain
90
+ // overflow-hidden does not (WebKit/Chromium leaves scrolled blurred
91
+ // children bleeding past the radius). Must match rounded-4xl (32px).
92
+ style={{ clipPath: 'inset(0 round 32px)' }}
93
+ >
94
+ <MultiRadialBackground />
95
+ <CardGlow />
96
+ <div className="zd:relative zd:z-10 zd:flex zd:flex-1 zd:flex-col zd:min-h-0">
97
+ {topNav}
98
+ <div
99
+ className="zd:flex zd:flex-1 zd:flex-col zd:min-h-0 zd:overflow-y-auto zd:overflow-x-hidden zd:-mr-4 zd:pr-4"
100
+ // Scale via --zd-spacing (matches TopNav's scaled height) so the
101
+ // top padding shrinks with the frame — otherwise the fixed 68px
102
+ // eats a disproportionate share at smaller sizes and overflows.
103
+ style={{
104
+ paddingTop: `calc(${CONTENT_PADDING_TOP / 4} * var(--zd-spacing))`,
105
+ }}
106
+ >
107
+ <ScreenOverlayContext.Provider value={overlayContainer}>
108
+ {children}
109
+ </ScreenOverlayContext.Provider>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ {/* Overlay is a sibling of the inner card, not a child — so it escapes
114
+ the card's clip-path + horizontal padding, and its own absolute
115
+ `inset-0` fills the outer 400×810 frame. That lets the backdrop dim
116
+ the TopNav band too and lets a bottom sheet anchor to the outer
117
+ frame's bottom, not the padded inner card. */}
118
+ {overlay}
119
+ </div>
120
+ )
121
+ }
@@ -0,0 +1,19 @@
1
+ import { camelCaseToTitle } from '../../../utils/common'
2
+ import { DataRow } from '../../DataRow'
3
+ import { Section } from '..'
4
+
5
+ export interface MessageDetailsProps {
6
+ details: Record<string, string>
7
+ }
8
+
9
+ export function MessageDetails({ details }: MessageDetailsProps) {
10
+ return (
11
+ <Section title="Message Details" iconName="message">
12
+ {Object.entries(details).map(([label, value]) => (
13
+ // Object keys are raw camelCase (e.g. "fromAddress"); the primitive
14
+ // DataRow renders labels verbatim, so title-case them here.
15
+ <DataRow key={label} label={camelCaseToTitle(label)} value={value} />
16
+ ))}
17
+ </Section>
18
+ )
19
+ }
@@ -0,0 +1,50 @@
1
+ import { type ReactNode, useState } from 'react'
2
+ import { Icon, type IconName } from '../Icon'
3
+ import { Text } from '../Text'
4
+ import { Wrapper } from '../Wrapper'
5
+
6
+ export interface SectionProps {
7
+ title: string
8
+ /** Optional leading icon (solarOrange tint). Omit for a plain title row. */
9
+ iconName?: IconName
10
+ children: ReactNode
11
+ collapsible?: boolean
12
+ }
13
+
14
+ export function Section({
15
+ title,
16
+ iconName,
17
+ collapsible,
18
+ children,
19
+ }: SectionProps) {
20
+ const [collapsed, setCollapsed] = useState(collapsible === true)
21
+
22
+ return (
23
+ <Wrapper className="zd:p-4 zd:flex zd:flex-col zd:gap-3 zd:rounded-xl zd:w-full">
24
+ <div className="zd:flex zd:flex-row zd:justify-between zd:items-center">
25
+ <div className="zd:flex zd:flex-row zd:items-center zd:gap-2">
26
+ {iconName && (
27
+ <Icon
28
+ name={iconName}
29
+ className="zd:h-4 zd:w-4 zd:text-solarOrange"
30
+ />
31
+ )}
32
+ <Text className="zd:text-h3">{title}</Text>
33
+ </div>
34
+ {collapsible !== undefined && (
35
+ <button
36
+ type="button"
37
+ onClick={() => setCollapsed((c) => !c)}
38
+ className="zd:flex zd:items-center zd:justify-center zd:cursor-pointer"
39
+ >
40
+ <Icon
41
+ name={collapsed ? 'chevronDown' : 'chevronUp'}
42
+ className="zd:w-4 zd:h-4 zd:text-greyScale"
43
+ />
44
+ </button>
45
+ )}
46
+ </div>
47
+ {!collapsed && children}
48
+ </Wrapper>
49
+ )
50
+ }
@@ -0,0 +1,153 @@
1
+ import { Select as SelectPrimitive } from 'radix-ui'
2
+ import type { ComponentProps, Ref } from 'react'
3
+
4
+ import { cn } from '../../utils/common'
5
+ import { Icon } from '../Icon'
6
+ import { Wrapper } from '../Wrapper'
7
+
8
+ export const Select = SelectPrimitive.Root
9
+ export const SelectGroup = SelectPrimitive.Group
10
+ export const SelectValue = SelectPrimitive.Value
11
+ export const SelectItemText = SelectPrimitive.ItemText
12
+
13
+ /** Bare styled trigger. Use `asChild` to wrap a custom trigger like `Pill`. */
14
+ export function SelectTrigger({
15
+ ref,
16
+ className,
17
+ children,
18
+ ...props
19
+ }: ComponentProps<typeof SelectPrimitive.Trigger> & {
20
+ ref?: Ref<HTMLButtonElement>
21
+ }) {
22
+ return (
23
+ <SelectPrimitive.Trigger
24
+ ref={ref}
25
+ className={cn(
26
+ 'zd:inline-flex zd:items-center zd:justify-between zd:gap-2',
27
+ 'zd:outline-none zd:cursor-pointer',
28
+ // Counters Radix's `pointer-events: none` on the trigger while the
29
+ // panel is open — without this the trigger goes inert and can't be
30
+ // clicked to toggle closed.
31
+ 'zd:data-[state=open]:pointer-events-auto',
32
+ 'zd:disabled:cursor-not-allowed zd:disabled:opacity-50',
33
+ className,
34
+ )}
35
+ {...props}
36
+ >
37
+ {children}
38
+ </SelectPrimitive.Trigger>
39
+ )
40
+ }
41
+
42
+ /** Trigger affordance slot. Defaults to a chevronDown icon. */
43
+ export function SelectIcon({
44
+ ref,
45
+ className,
46
+ children,
47
+ ...props
48
+ }: ComponentProps<typeof SelectPrimitive.Icon> & {
49
+ ref?: Ref<HTMLSpanElement>
50
+ }) {
51
+ return (
52
+ <SelectPrimitive.Icon
53
+ ref={ref}
54
+ className={cn('zd:shrink-0', className)}
55
+ {...props}
56
+ >
57
+ {children ?? (
58
+ <Icon name="chevronDown" className="zd:size-4 zd:text-greyScale" />
59
+ )}
60
+ </SelectPrimitive.Icon>
61
+ )
62
+ }
63
+
64
+ /** Portaled popper panel. Width defaults to `--radix-select-trigger-width`;
65
+ * override via inline `style={{ width }}`. */
66
+ export function SelectContent({
67
+ ref,
68
+ className,
69
+ children,
70
+ position = 'popper',
71
+ sideOffset = 8,
72
+ style,
73
+ ...props
74
+ }: ComponentProps<typeof SelectPrimitive.Content> & {
75
+ ref?: Ref<HTMLDivElement>
76
+ }) {
77
+ return (
78
+ <SelectPrimitive.Portal>
79
+ <SelectPrimitive.Content
80
+ ref={ref}
81
+ position={position}
82
+ sideOffset={sideOffset}
83
+ className={cn(
84
+ 'zd:z-50 zd:max-h-80 zd:outline-none',
85
+ 'zd:data-[state=open]:animate-popper-in',
86
+ className,
87
+ )}
88
+ {...props}
89
+ style={{
90
+ width: 'var(--radix-select-trigger-width)',
91
+ transformOrigin: 'var(--radix-select-content-transform-origin)',
92
+ ...style,
93
+ }}
94
+ >
95
+ <Wrapper
96
+ variant="solid"
97
+ // Override Wrapper's 80% white translucency — a dropdown over
98
+ // content shouldn't let the content bleed through.
99
+ style={{ backgroundColor: '#fff' }}
100
+ className="zd:flex zd:flex-col zd:rounded-2xl zd:overflow-hidden"
101
+ >
102
+ <SelectPrimitive.Viewport className="zd:overflow-y-auto zd:max-h-80">
103
+ {children}
104
+ </SelectPrimitive.Viewport>
105
+ </Wrapper>
106
+ </SelectPrimitive.Content>
107
+ </SelectPrimitive.Portal>
108
+ )
109
+ }
110
+
111
+ /** Option row. Children render as-is; set `textValue` for typeahead when
112
+ * children aren't plain text. */
113
+ export function SelectItem({
114
+ ref,
115
+ className,
116
+ children,
117
+ ...props
118
+ }: ComponentProps<typeof SelectPrimitive.Item> & {
119
+ ref?: Ref<HTMLDivElement>
120
+ }) {
121
+ return (
122
+ <SelectPrimitive.Item
123
+ ref={ref}
124
+ className={cn(
125
+ // Default padding for plain-text items. Complex children with their
126
+ // own padding (e.g. `<TokenListItem>`) opt out via `className="zd:p-0"`.
127
+ 'zd:relative zd:outline-none zd:cursor-pointer zd:px-3 zd:py-2',
128
+ 'zd:data-[highlighted]:bg-offWhite/40',
129
+ 'zd:data-[state=checked]:bg-offWhite/60',
130
+ className,
131
+ )}
132
+ {...props}
133
+ >
134
+ {children}
135
+ </SelectPrimitive.Item>
136
+ )
137
+ }
138
+
139
+ export function SelectSeparator({
140
+ ref,
141
+ className,
142
+ ...props
143
+ }: ComponentProps<typeof SelectPrimitive.Separator> & {
144
+ ref?: Ref<HTMLDivElement>
145
+ }) {
146
+ return (
147
+ <SelectPrimitive.Separator
148
+ ref={ref}
149
+ className={cn('zd:h-px zd:bg-offWhite/50 zd:mx-2 zd:my-1', className)}
150
+ {...props}
151
+ />
152
+ )
153
+ }