@zerodev/smart-routing-address-react-ui 0.0.1 → 0.0.3

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.
@@ -12,6 +12,7 @@ import {
12
12
  Text,
13
13
  TokenListItem,
14
14
  Tooltip,
15
+ WrappedPressable,
15
16
  Wrapper,
16
17
  } from '@zerodev/react-ui'
17
18
  import type { DepositedToken, TOKEN_TYPE } from '@zerodev/smart-routing-address'
@@ -221,10 +222,9 @@ export function Deposit({
221
222
  return out
222
223
  }, [srcTokens, selectedTokenType])
223
224
 
224
- // Only formatted when the consumer explicitly sets a slippage otherwise
225
- // the server picks its own default and there is no meaningful value to show.
226
- const slippage =
227
- typeof config.slippage === 'number' ? formatSlippage(config.slippage) : null
225
+ // Always present: config.slippage is required now that the SRA server no
226
+ // longer supplies a default.
227
+ const slippage = formatSlippage(config.slippage)
228
228
 
229
229
  const minDepositAmount =
230
230
  feeData && tokenSymbol
@@ -279,7 +279,12 @@ export function Deposit({
279
279
  ])
280
280
 
281
281
  return (
282
- <div className="zd:flex zd:h-full zd:w-full zd:flex-col zd:items-center zd:gap-4 zd:pt-4 zd:pb-6">
282
+ // min-h-full, not h-full: with a fixed height, overflowing content
283
+ // scrolls past the padding box and pb-6 never lands after the last row
284
+ // (the "Past deposits" row hits the bottom edge). min-h keeps short
285
+ // content filling the viewport while letting tall content grow so the
286
+ // bottom padding is honoured.
287
+ <div className="zd:flex zd:min-h-full zd:w-full zd:flex-col zd:items-center zd:gap-4 zd:pt-4 zd:pb-6">
283
288
  <Text className="zd:w-full zd:text-center">{SUBTITLE}</Text>
284
289
 
285
290
  <div className="zd:relative zd:flex zd:w-full zd:flex-1 zd:flex-col zd:gap-2">
@@ -378,38 +383,36 @@ export function Deposit({
378
383
  }
379
384
  />
380
385
  <div className="zd:flex zd:w-full zd:flex-col zd:items-start zd:gap-2 zd:px-2 zd:py-4">
381
- {slippage && (
382
- <DataRow
383
- label="Max slippage"
384
- value={slippage}
385
- info
386
- infoTooltip={FEE_INFO.maxSlippage}
387
- trailing={
388
- breakdown?.provider &&
389
- PROVIDER_ICONS[breakdown.provider] ? (
390
- <LiveValue
391
- loading={providerFees.loading}
392
- flashKey={breakdown.provider}
393
- >
394
- <Tooltip content={`Quoted via ${breakdown.provider}`}>
395
- <button
396
- type="button"
397
- aria-label={`Quoted via ${breakdown.provider}`}
398
- className="zd:inline-flex zd:items-center zd:justify-center zd:cursor-help zd:outline-none zd:bg-transparent"
399
- >
400
- <img
401
- src={PROVIDER_ICONS[breakdown.provider]}
402
- alt=""
403
- aria-hidden
404
- className="zd:size-4 zd:shrink-0 zd:rounded-[4px] zd:object-cover"
405
- />
406
- </button>
407
- </Tooltip>
408
- </LiveValue>
409
- ) : null
410
- }
411
- />
412
- )}
386
+ <DataRow
387
+ label="Max slippage"
388
+ value={slippage}
389
+ info
390
+ infoTooltip={FEE_INFO.maxSlippage}
391
+ trailing={
392
+ breakdown?.provider &&
393
+ PROVIDER_ICONS[breakdown.provider] ? (
394
+ <LiveValue
395
+ loading={providerFees.loading}
396
+ flashKey={breakdown.provider}
397
+ >
398
+ <Tooltip content={`Quoted via ${breakdown.provider}`}>
399
+ <button
400
+ type="button"
401
+ aria-label={`Quoted via ${breakdown.provider}`}
402
+ className="zd:inline-flex zd:items-center zd:justify-center zd:cursor-help zd:outline-none zd:bg-transparent"
403
+ >
404
+ <img
405
+ src={PROVIDER_ICONS[breakdown.provider]}
406
+ alt=""
407
+ aria-hidden
408
+ className="zd:size-4 zd:shrink-0 zd:rounded-[4px] zd:object-cover"
409
+ />
410
+ </button>
411
+ </Tooltip>
412
+ </LiveValue>
413
+ ) : null
414
+ }
415
+ />
413
416
  <DataRow
414
417
  label="Estimated fee"
415
418
  value={
@@ -489,6 +492,7 @@ export function Deposit({
489
492
  address={address}
490
493
  onQrClick={onQrClick}
491
494
  />
495
+ {address && <CopyAddressButton address={address} />}
492
496
  {errorMessage && (
493
497
  <ErrorRetryCard
494
498
  message={errorMessage}
@@ -521,66 +525,124 @@ export function Deposit({
521
525
  }
522
526
  />
523
527
 
524
- {newDeposits.length > 0 ? (
525
- <PendingDeposits
526
- deposits={newDeposits}
527
- estimatedFees={estimatedFees}
528
- config={config}
529
- {...(onSelectDeposit && { onSelectDeposit })}
530
- />
531
- ) : (
532
- <LoadingCard
533
- text={
534
- source
535
- ? `Watching for your deposit on ${source.chain.name}…`
536
- : 'Watching for your deposit…'
537
- }
538
- />
539
- )}
528
+ {/* Deposit watching only makes sense once the address exists —
529
+ while creation is loading (or failed) there is nothing to watch,
530
+ so neither card renders. */}
531
+ {address &&
532
+ (newDeposits.length > 0 ? (
533
+ <PendingDeposits
534
+ deposits={newDeposits}
535
+ estimatedFees={estimatedFees}
536
+ config={config}
537
+ {...(onSelectDeposit && { onSelectDeposit })}
538
+ />
539
+ ) : (
540
+ <LoadingCard
541
+ text={
542
+ source
543
+ ? `Watching for your deposit on ${source.chain.name}…`
544
+ : 'Watching for your deposit…'
545
+ }
546
+ />
547
+ ))}
540
548
 
541
549
  {pastDepositsCount > 0 &&
542
550
  (onViewPastDeposits ? (
543
- <button
544
- type="button"
551
+ <WrappedPressable
545
552
  onClick={onViewPastDeposits}
546
- className="zd:flex zd:w-full zd:items-center zd:gap-2 zd:px-4 zd:py-4 zd:cursor-pointer"
553
+ className={cn('zd:w-full', PAST_DEPOSITS_CARD)}
547
554
  >
548
- <Icon
549
- name="clock"
550
- className="zd:size-4 zd:text-greyScale/50"
551
- aria-hidden
552
- />
553
- <Text className="zd:flex-1 zd:text-left zd:text-body1">
554
- Past deposits ({pastDepositsCount})
555
- </Text>
556
- <Icon
557
- name="chevronRight"
558
- className="zd:size-4 zd:text-greyScale/50"
559
- aria-hidden
560
- />
561
- </button>
555
+ <PastDepositsRow count={pastDepositsCount} />
556
+ </WrappedPressable>
562
557
  ) : (
563
- <div className="zd:flex zd:w-full zd:items-center zd:gap-2 zd:px-4 zd:py-4">
564
- <Icon
565
- name="clock"
566
- className="zd:size-4 zd:text-greyScale/50"
567
- aria-hidden
568
- />
569
- <Text className="zd:flex-1 zd:text-left zd:text-body1">
570
- Past deposits ({pastDepositsCount})
571
- </Text>
572
- <Icon
573
- name="chevronRight"
574
- className="zd:size-4 zd:text-greyScale/50"
575
- aria-hidden
576
- />
577
- </div>
558
+ <Wrapper
559
+ variant="ghost"
560
+ className={cn(
561
+ 'zd:flex zd:w-full zd:items-center',
562
+ PAST_DEPOSITS_CARD,
563
+ )}
564
+ >
565
+ <PastDepositsRow count={pastDepositsCount} />
566
+ </Wrapper>
578
567
  ))}
579
568
  </div>
580
569
  </div>
581
570
  )
582
571
  }
583
572
 
573
+ /**
574
+ * Full-width dark "Copy Address" pill under the address row (Figma
575
+ * `20002:36049`). Not react-ui's `Button`: that primitive is locked to the
576
+ * 64px/24px-radius spec, while this is the design system's smaller
577
+ * 48px/14px button — worth promoting as a Button size variant if it
578
+ * recurs. Label flips to "Copied!" for 2s as tap feedback (the design has
579
+ * no pressed state).
580
+ */
581
+ function CopyAddressButton({ address }: { address: string }) {
582
+ const [copied, setCopied] = useState(false)
583
+
584
+ const handleCopy = async () => {
585
+ try {
586
+ await navigator.clipboard.writeText(address)
587
+ setCopied(true)
588
+ setTimeout(() => setCopied(false), 2000)
589
+ } catch {
590
+ // Clipboard can reject on insecure contexts / denied permission —
591
+ // keep the label unchanged rather than claim a copy happened.
592
+ }
593
+ }
594
+
595
+ return (
596
+ <button
597
+ type="button"
598
+ onClick={handleCopy}
599
+ className={cn(
600
+ 'zd:relative zd:flex zd:h-12 zd:w-full zd:shrink-0 zd:cursor-pointer zd:items-center zd:justify-center zd:gap-2',
601
+ 'zd:rounded-[14px] zd:backdrop-blur-[15px] zd:transition-colors zd:hover:bg-greyScale',
602
+ 'zd:shadow-[inset_0_-4px_4px_0_rgba(255,255,255,0.1),inset_0_3px_4px_0_rgba(0,0,0,0.02)]',
603
+ )}
604
+ style={{ backgroundColor: 'rgba(19, 14, 11, 0.9)' }}
605
+ >
606
+ <Text className="zd:text-body1 zd:text-offWhite">
607
+ {copied ? 'Copied!' : 'Copy Address'}
608
+ </Text>
609
+ <Icon
610
+ name="copy"
611
+ className="zd:size-4 zd:shrink-0 zd:text-offWhite"
612
+ aria-hidden
613
+ />
614
+ </button>
615
+ )
616
+ }
617
+
618
+ // Past-deposits card treatment (Figma 20002:36111): 16px radius plus the
619
+ // universal inner shadow; border/blur/tint come from the ghost Wrapper.
620
+ const PAST_DEPOSITS_CARD =
621
+ 'zd:rounded-2xl zd:shadow-[inset_0_-4px_4px_0_rgba(255,255,255,0.1),inset_0_3px_4px_0_rgba(0,0,0,0.02)]'
622
+
623
+ /** Row content shared by the tappable and inert past-deposits variants. */
624
+ function PastDepositsRow({ count }: { count: number }) {
625
+ return (
626
+ <div className="zd:flex zd:w-full zd:items-center zd:gap-3 zd:p-4">
627
+ <div className="zd:flex zd:min-w-0 zd:flex-1 zd:items-center zd:gap-2">
628
+ <Icon
629
+ name="clockFill"
630
+ className="zd:size-4 zd:shrink-0 zd:text-solarOrange"
631
+ aria-hidden
632
+ />
633
+ <Text className="zd:flex-1 zd:text-left zd:text-h3">
634
+ Past deposits ({count})
635
+ </Text>
636
+ </div>
637
+ <Icon
638
+ name="chevronRight"
639
+ className="zd:size-4.5 zd:shrink-0 zd:text-greyScale"
640
+ aria-hidden
641
+ />
642
+ </div>
643
+ )
644
+ }
645
+
584
646
  function CardTitle({ children }: { children: string }) {
585
647
  return (
586
648
  <div className={cn('zd:flex zd:items-center zd:px-2 zd:py-3')}>
@@ -24,7 +24,9 @@ import {
24
24
  const STAGE_TO_STATUS: Record<DepositStage, TxnStatus> = {
25
25
  pending: 'Detected',
26
26
  bridging: 'Routing',
27
- completed: 'Received',
27
+ // Past deposits use the terminal label — "Received" (plain ink) is the
28
+ // active list's arrival state; "Delivered" renders green.
29
+ completed: 'Delivered',
28
30
  failed: 'Failed',
29
31
  }
30
32
 
@@ -179,14 +181,29 @@ export function PastDeposits({ onSelectDeposit }: PastDepositsProps) {
179
181
  // whether the deposit's token matched the current fees.
180
182
  const href = getTxUrl(chainId, transactionHash)
181
183
  const status = STAGE_TO_STATUS[getDepositStage(deposit)]
184
+ // Delivered amount for the detailed row headline
185
+ // (`amount → destAmount`). `'nearest'` rounding — the
186
+ // server reports the actual post-fee on-chain amount and
187
+ // users read this as "what I received". Falls back to the
188
+ // deposited amount while execution details are absent so
189
+ // the detailed layout stays stable across stages.
190
+ const destAmountLabel =
191
+ deposit.execution?.outputAmount && feeData
192
+ ? `${formatDisplayAmount(deposit.execution.outputAmount, feeData.decimal, 'nearest')} ${sourceSymbol}`
193
+ : amountLabel
182
194
 
183
195
  const row = (
184
196
  <TxnItem
185
197
  amount={amountLabel}
198
+ destAmount={destAmountLabel}
186
199
  address={truncateAddress(transactionHash)}
187
200
  {...(href && { href })}
188
201
  timestamp={timestamp}
189
202
  status={status}
203
+ {...(source?.chain.name && {
204
+ sourceChainName: source.chain.name,
205
+ })}
206
+ destChainName={destChain.name}
190
207
  {...(sourceTokenLogo && {
191
208
  sourceTokenIconUrl: sourceTokenLogo,
192
209
  })}
@@ -204,14 +221,14 @@ export function PastDeposits({ onSelectDeposit }: PastDepositsProps) {
204
221
  return (
205
222
  <li key={transactionHash}>
206
223
  {onSelectDeposit ? (
207
- // -mx-1 + px-1 + a wider explicit width extend the
208
- // hover surface 4px past the row content on each
209
- // side, so the highlight has visible breathing
210
- // room without shifting the TxnItem's position.
224
+ // No padding of its own: the TxnItem's built-in
225
+ // p-2 (the Figma Txn Row inset) doubles as the
226
+ // hover highlight's breathing room. Radius matches
227
+ // the row's rounded-2xl.
211
228
  <button
212
229
  type="button"
213
230
  onClick={() => onSelectDeposit(deposit)}
214
- className="zd:w-full zd:cursor-pointer zd:rounded-xl zd:px-1 zd:text-left zd:hover:bg-white/30"
231
+ className="zd:w-full zd:cursor-pointer zd:rounded-2xl zd:text-left zd:hover:bg-white/30"
215
232
  >
216
233
  {row}
217
234
  </button>
@@ -4,11 +4,11 @@ import {
4
4
  DataRow,
5
5
  Icon,
6
6
  InfoCard,
7
+ ProgressStep,
7
8
  Section,
8
9
  Text,
9
- Tooltip,
10
10
  } from '@zerodev/react-ui'
11
- import { type ReactNode, useState } from 'react'
11
+ import { useState } from 'react'
12
12
  import { FeeBreakdownRows, FeeSummary } from '../components/FeeBreakdown'
13
13
  import { FEE_INFO } from '../components/FeeBreakdown/feeInfo'
14
14
  import { useSmartRoutingAddressContext } from '../context/SmartRoutingAddressContext'
@@ -235,7 +235,7 @@ export function TransactionDetails({
235
235
  <ProgressStep
236
236
  label="Deposit detected"
237
237
  info={FEE_INFO.detected}
238
- done
238
+ status="done"
239
239
  right={
240
240
  <TxLink
241
241
  label={truncateAddress(transactionHash)}
@@ -250,7 +250,11 @@ export function TransactionDetails({
250
250
  // detected stage — including the failed path (failure implies the
251
251
  // route was tried), so mark it done there too rather than showing
252
252
  // an inert pending circle above the "Failed" row.
253
- done={stage === 'bridging' || stage === 'completed' || failed}
253
+ status={
254
+ stage === 'bridging' || stage === 'completed' || failed
255
+ ? 'done'
256
+ : 'pending'
257
+ }
254
258
  right={
255
259
  providerFees.loading ? (
256
260
  <ProviderChipSkeleton />
@@ -265,8 +269,7 @@ export function TransactionDetails({
265
269
  <ProgressStep
266
270
  label="Failed"
267
271
  info={FEE_INFO.failed}
268
- done={false}
269
- failed
272
+ status="failed"
270
273
  isLast
271
274
  right={<StatusText tone="error">Error</StatusText>}
272
275
  />
@@ -274,7 +277,7 @@ export function TransactionDetails({
274
277
  <ProgressStep
275
278
  label="Completed"
276
279
  info={FEE_INFO.completed}
277
- done={stage === 'completed'}
280
+ status={stage === 'completed' ? 'done' : 'pending'}
278
281
  isLast
279
282
  right={
280
283
  execution ? (
@@ -304,89 +307,6 @@ function NetworkValue({ name, logoUrl }: { name: string; logoUrl?: string }) {
304
307
  )
305
308
  }
306
309
 
307
- function ProgressStep({
308
- label,
309
- info,
310
- done,
311
- failed,
312
- isLast,
313
- right,
314
- }: {
315
- label: string
316
- info?: string | undefined
317
- done: boolean
318
- failed?: boolean
319
- isLast?: boolean
320
- right?: ReactNode
321
- }) {
322
- return (
323
- <div className="zd:relative zd:flex zd:w-full zd:items-start zd:gap-3">
324
- <div className="zd:flex zd:flex-col zd:items-center zd:pt-0.5">
325
- <StatusMark done={done} failed={!!failed} />
326
- {!isLast && (
327
- <div
328
- className={cn(
329
- 'zd:mt-1 zd:h-4 zd:w-px',
330
- done ? 'zd:bg-solarOrange/60' : 'zd:bg-greyScale/20',
331
- )}
332
- />
333
- )}
334
- </div>
335
- <div className="zd:flex zd:min-w-0 zd:flex-1 zd:items-center zd:gap-1">
336
- <Text
337
- className={cn(
338
- 'zd:whitespace-nowrap',
339
- failed
340
- ? 'zd:text-negative'
341
- : // Mute the label until the step is done so a "Completed" row
342
- // with an inert open circle doesn't read at full intensity
343
- // while the flow is still bridging.
344
- done
345
- ? 'zd:text-greyScale'
346
- : 'zd:text-greyScale/50',
347
- )}
348
- >
349
- {label}
350
- </Text>
351
- {info && (
352
- <Tooltip content={info}>
353
- <button
354
- type="button"
355
- aria-label="More info"
356
- className="zd:inline-flex zd:items-center zd:justify-center zd:cursor-help zd:outline-none zd:bg-transparent"
357
- >
358
- <Icon
359
- name="info"
360
- className="zd:w-3 zd:h-3 zd:text-greyScale/50"
361
- aria-hidden
362
- />
363
- </button>
364
- </Tooltip>
365
- )}
366
- </div>
367
- <div className="zd:flex zd:shrink-0 zd:items-center">{right}</div>
368
- </div>
369
- )
370
- }
371
-
372
- function StatusMark({ done, failed }: { done: boolean; failed: boolean }) {
373
- if (failed) {
374
- return (
375
- <Icon name="warning" className="zd:size-4 zd:text-negative" aria-hidden />
376
- )
377
- }
378
- if (done) {
379
- return (
380
- <span className="zd:inline-flex zd:size-4 zd:items-center zd:justify-center zd:rounded-full zd:bg-solarOrange">
381
- <Icon name="check" className="zd:size-3 zd:text-white" aria-hidden />
382
- </span>
383
- )
384
- }
385
- return (
386
- <span className="zd:inline-block zd:size-4 zd:rounded-full zd:border zd:border-greyScale/30" />
387
- )
388
- }
389
-
390
310
  function TxLink({ label, href }: { label: string; href?: string | undefined }) {
391
311
  if (!href) {
392
312
  return (
package/src/types.ts CHANGED
@@ -40,8 +40,10 @@ export type SmartRoutingAddressConfig = {
40
40
  * transferred to the owner.
41
41
  */
42
42
  actions?: CreateSmartRoutingAddressParams['actions']
43
- /** Max slippage in basis points (50 = 0.5%) */
44
- slippage?: number
43
+ /**
44
+ * Max slippage in basis points (50 = 0.5%).
45
+ */
46
+ slippage: number
45
47
  /**
46
48
  * Override the smart routing address server root URL; the projectId is
47
49
  * appended to it