@zerodev/react-ui 0.0.3 → 0.0.4
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/_types/components/ArrowCardPair/index.d.ts +2 -2
- package/dist/_types/components/ArrowCardPair/index.d.ts.map +1 -1
- package/dist/_types/components/Badge/index.d.ts +1 -1
- package/dist/_types/components/Badge/index.d.ts.map +1 -1
- package/dist/_types/components/BottomSheet/index.d.ts +17 -0
- package/dist/_types/components/BottomSheet/index.d.ts.map +1 -0
- package/dist/_types/components/Button/index.d.ts +1 -1
- package/dist/_types/components/Button/index.d.ts.map +1 -1
- package/dist/_types/components/Callout/index.d.ts +1 -1
- package/dist/_types/components/Callout/index.d.ts.map +1 -1
- package/dist/_types/components/DataRow/index.d.ts +2 -2
- package/dist/_types/components/DataRow/index.d.ts.map +1 -1
- package/dist/_types/components/Icon/index.d.ts +1 -1
- package/dist/_types/components/Icon/index.d.ts.map +1 -1
- package/dist/_types/components/IconButton/index.d.ts +1 -1
- package/dist/_types/components/IconButton/index.d.ts.map +1 -1
- package/dist/_types/components/Input/index.d.ts +1 -1
- package/dist/_types/components/Input/index.d.ts.map +1 -1
- package/dist/_types/components/ListItem/index.d.ts +32 -15
- package/dist/_types/components/ListItem/index.d.ts.map +1 -1
- package/dist/_types/components/Pill/index.d.ts +25 -0
- package/dist/_types/components/Pill/index.d.ts.map +1 -0
- package/dist/_types/components/PoweredBy/index.d.ts +6 -0
- package/dist/_types/components/PoweredBy/index.d.ts.map +1 -0
- package/dist/_types/components/Screen/EllipticalRadial.d.ts +23 -0
- package/dist/_types/components/Screen/EllipticalRadial.d.ts.map +1 -0
- package/dist/_types/components/Screen/MultiRadialBackground.d.ts +4 -0
- package/dist/_types/components/Screen/MultiRadialBackground.d.ts.map +1 -0
- package/dist/_types/components/Screen/index.d.ts +15 -0
- package/dist/_types/components/Screen/index.d.ts.map +1 -0
- package/dist/_types/components/Select/index.d.ts +28 -0
- package/dist/_types/components/Select/index.d.ts.map +1 -0
- package/dist/_types/components/Switch/index.d.ts +1 -1
- package/dist/_types/components/Switch/index.d.ts.map +1 -1
- package/dist/_types/components/Text/index.d.ts +1 -1
- package/dist/_types/components/Text/index.d.ts.map +1 -1
- package/dist/_types/components/TokenListItem/index.d.ts +33 -0
- package/dist/_types/components/TokenListItem/index.d.ts.map +1 -0
- package/dist/_types/components/TopNav/index.d.ts +20 -0
- package/dist/_types/components/TopNav/index.d.ts.map +1 -0
- package/dist/_types/components/WrappedPressable/index.d.ts +1 -1
- package/dist/_types/components/WrappedPressable/index.d.ts.map +1 -1
- package/dist/_types/components/Wrapper/index.d.ts +3 -2
- package/dist/_types/components/Wrapper/index.d.ts.map +1 -1
- package/dist/_types/components/ZeroDevLogo/index.d.ts +1 -1
- package/dist/_types/components/ZeroDevLogo/index.d.ts.map +1 -1
- package/dist/_types/index.d.ts +8 -1
- package/dist/_types/index.d.ts.map +1 -1
- package/dist/index.cjs +51 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +6035 -1344
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +2 -2
- package/package.json +7 -5
- package/src/components/ArrowCardPair/index.tsx +25 -5
- package/src/components/BottomSheet/index.tsx +57 -0
- package/src/components/DataRow/index.tsx +2 -2
- package/src/components/ListItem/index.tsx +74 -79
- package/src/components/Pill/index.tsx +167 -0
- package/src/components/PoweredBy/index.tsx +29 -0
- package/src/components/Screen/EllipticalRadial.tsx +65 -0
- package/src/components/Screen/MultiRadialBackground.tsx +323 -0
- package/src/components/Screen/index.tsx +121 -0
- package/src/components/Select/index.tsx +153 -0
- package/src/components/TokenListItem/index.tsx +192 -0
- package/src/components/TopNav/index.tsx +67 -0
- package/src/components/Wrapper/index.tsx +4 -1
- package/src/index.ts +33 -0
- package/tailwind.config.ts +39 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
interface RadialStop {
|
|
2
|
+
offset: number
|
|
3
|
+
color: string
|
|
4
|
+
opacity: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RadialLayer {
|
|
8
|
+
id: string
|
|
9
|
+
// Figma-exported gradientTransform matrix: the unit circle
|
|
10
|
+
// (center 0,0 radius 1) mapped to a rotated/scaled/translated ellipse.
|
|
11
|
+
matrix: [number, number, number, number, number, number]
|
|
12
|
+
// Opacity of the rect the gradient fills (Figma exports this on the <rect>).
|
|
13
|
+
fillOpacity: number
|
|
14
|
+
stops: RadialStop[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The rect each gradient fills, in the parent SVG's viewBox coordinates.
|
|
18
|
+
interface RadialRect {
|
|
19
|
+
x: number
|
|
20
|
+
y: number
|
|
21
|
+
width: number
|
|
22
|
+
height: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function EllipticalRadial({
|
|
26
|
+
layer,
|
|
27
|
+
rect,
|
|
28
|
+
}: {
|
|
29
|
+
layer: RadialLayer
|
|
30
|
+
rect: RadialRect
|
|
31
|
+
}) {
|
|
32
|
+
const { id, matrix, fillOpacity, stops } = layer
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<defs>
|
|
37
|
+
<radialGradient
|
|
38
|
+
id={id}
|
|
39
|
+
cx="0"
|
|
40
|
+
cy="0"
|
|
41
|
+
r="1"
|
|
42
|
+
gradientUnits="userSpaceOnUse"
|
|
43
|
+
gradientTransform={`matrix(${matrix.join(' ')})`}
|
|
44
|
+
>
|
|
45
|
+
{stops.map((s, i) => (
|
|
46
|
+
<stop
|
|
47
|
+
key={s.color + i.toString()}
|
|
48
|
+
offset={s.offset}
|
|
49
|
+
stopColor={s.color}
|
|
50
|
+
stopOpacity={s.opacity}
|
|
51
|
+
/>
|
|
52
|
+
))}
|
|
53
|
+
</radialGradient>
|
|
54
|
+
</defs>
|
|
55
|
+
<rect
|
|
56
|
+
x={rect.x}
|
|
57
|
+
y={rect.y}
|
|
58
|
+
width={rect.width}
|
|
59
|
+
height={rect.height}
|
|
60
|
+
fill={`url(#${id})`}
|
|
61
|
+
fillOpacity={fillOpacity}
|
|
62
|
+
/>
|
|
63
|
+
</>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -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,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
|
+
}
|