Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: npm

- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-node-${{ matrix.node-version }}-

- name: Install dependencies
run: npm ci
run: npm install

- name: Verify contract bindings are up-to-date
run: npm run codegen:validate
Expand Down
142 changes: 142 additions & 0 deletions components/atoms/escrow-state-badge/escrow-state-badge.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.025em;
text-transform: uppercase;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}

/* ── Status Colors ──────────────────────────────────────────── */

.Pending {
background-color: rgb(243 244 246);
color: rgb(75 85 99);
}

.Funded {
background-color: rgb(219 234 254);
color: rgb(29 78 216);
}

.Completed {
background-color: rgb(220 252 231);
color: rgb(21 128 61);
}

.Released {
background-color: rgb(187 247 208);
color: rgb(22 101 52);
}

.Disputed {
background-color: rgb(254 226 226);
color: rgb(185 28 28);
}

.Refunded {
background-color: rgb(254 243 199);
color: rgb(146 64 14);
}

/* ── Dark Mode ──────────────────────────────────────────────── */

:global(.dark) .Pending {
background-color: rgb(31 41 55);
color: rgb(156 163 175);
}

:global(.dark) .Funded {
background-color: rgb(30 58 138 / 0.3);
color: rgb(147 197 253);
}

:global(.dark) .Completed {
background-color: rgb(20 83 45 / 0.3);
color: rgb(134 239 172);
}

:global(.dark) .Released {
background-color: rgb(20 83 45 / 0.3);
color: rgb(74 222 128);
}

:global(.dark) .Disputed {
background-color: rgb(127 29 29 / 0.3);
color: rgb(252 165 165);
}

:global(.dark) .Refunded {
background-color: rgb(120 53 15 / 0.3);
color: rgb(253 224 71);
}

/* ── Optimistic Pulse ───────────────────────────────────────── */

.optimistic {
animation: optimisticPulse 1.5s ease-in-out infinite;
}

@keyframes optimisticPulse {
0%, 100% {
opacity: 1;
box-shadow: 0 0 0 0 currentColor;
}
50% {
opacity: 0.75;
box-shadow: 0 0 0 4px transparent;
}
}

/* ── Pulse Dot ──────────────────────────────────────────────── */

.pulseDot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: currentColor;
animation: dotPulse 1.5s ease-in-out infinite;
}

@keyframes dotPulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.4;
transform: scale(1.4);
}
}

/* ── Rollback Flash ─────────────────────────────────────────── */

.rollbackFlash {
animation: rollbackShake 0.5s ease-in-out;
}

@keyframes rollbackShake {
0%, 100% {
transform: translateX(0);
background-color: rgb(254 226 226);
color: rgb(185 28 28);
}
20% {
transform: translateX(-3px);
}
40% {
transform: translateX(3px);
}
60% {
transform: translateX(-2px);
}
80% {
transform: translateX(2px);
}
}
55 changes: 55 additions & 0 deletions components/atoms/escrow-state-badge/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react'
import type { MilestoneStatus } from '../../../shared/contracts-gen/escrow'
import styles from './escrow-state-badge.module.css'

export interface EscrowStateBadgeProps {
/** The current (or optimistic) milestone status. */
status: MilestoneStatus
/** Whether this status is an unconfirmed optimistic update. */
isOptimistic?: boolean
/** If true, play the rollback flash animation (automatically clears after animation ends). */
showRollback?: boolean
/** Optional extra CSS class names. */
className?: string
}

/**
* Displays the current escrow milestone status as a color-coded badge.
*
* - Pulses when `isOptimistic` is true (pending on-chain confirmation)
* - Flashes/shakes when `showRollback` fires (transaction was rejected)
*/
export function EscrowStateBadge({
status,
isOptimistic = false,
showRollback = false,
className,
}: EscrowStateBadgeProps) {
const [flashing, setFlashing] = useState(false)

useEffect(() => {
if (showRollback) {
setFlashing(true)
const timer = setTimeout(() => setFlashing(false), 600)
return () => clearTimeout(timer)
}
}, [showRollback])

const classes = [
styles.badge,
styles[status],
isOptimistic ? styles.optimistic : '',
flashing ? styles.rollbackFlash : '',
className ?? '',
]
.filter(Boolean)
.join(' ')

return (
<span className={classes} data-testid={`escrow-badge-${status.toLowerCase()}`}>
{isOptimistic && <span className={styles.pulseDot} aria-hidden="true" />}
{status}
{isOptimistic && <span className="sr-only"> (pending confirmation)</span>}
</span>
)
}
1 change: 1 addition & 0 deletions components/atoms/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './theme-toggle'
export * from './spacer'
export * from './markdown-renderer'
export * from './error-boundary'
export * from './escrow-state-badge'
Loading
Loading