Skip to content

Commit 0d27f72

Browse files
pepakrizclaude
andcommitted
refactor(ui): adopt ReUI Alert, Stepper, and Timeline components
Replace hand-rolled UI in three spots with ReUI's Base UI components: the FIO plugin native-runtime warning now uses Alert with the project's --warning token instead of hardcoded amber colors, the onboarding progress dots are a real Stepper wired to the existing step state, and payment reconciliations render as a Timeline instead of flat cards. Also registers the @ReUI registry in components.json and fixes exhaustive-deps/non-null-assertion issues in the generated stepper.tsx to satisfy the project's Biome lint rules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent cd0fb34 commit 0d27f72

11 files changed

Lines changed: 1001 additions & 143 deletions

File tree

.mcp.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
"chrome-devtools": {
88
"command": "bunx",
99
"args": ["-y", "chrome-devtools-mcp@latest"]
10+
},
11+
"reui": {
12+
"type": "http",
13+
"url": "https://mcp.reui.io/api/mcp"
1014
}
1115
}
1216
}

components.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,9 @@
2121
},
2222
"menuColor": "default",
2323
"menuAccent": "subtle",
24-
"registries": {}
24+
"registries": {
25+
"@reui": {
26+
"url": "https://reui.io/r/{style}/{name}.json"
27+
}
28+
}
2529
}

src/components/payment-detail.tsx

Lines changed: 119 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ import { type KyselyNotNull, sqliteTrue } from "@evolu/common"
22
import { Link } from "@tanstack/react-router"
33
import { ReceiptIcon } from "lucide-react"
44
import { type ReactNode, useMemo } from "react"
5+
import {
6+
Timeline,
7+
TimelineContent,
8+
TimelineDate,
9+
TimelineHeader,
10+
TimelineIndicator,
11+
TimelineItem,
12+
TimelineSeparator,
13+
TimelineTitle,
14+
} from "@/components/reui/timeline.tsx"
515
import { Badge } from "@/components/ui/badge.tsx"
616
import { Button } from "@/components/ui/button.tsx"
717
import {
@@ -335,116 +345,122 @@ function PaymentDetailContent({
335345
<CardHeader>
336346
<CardTitle>{t("paymentDetail.reconciliations")}</CardTitle>
337347
</CardHeader>
338-
<CardContent className="flex flex-col gap-3">
348+
<CardContent>
339349
{reconciliations.length === 0 ? (
340350
<p className="text-sm text-muted-foreground">
341351
{t("paymentDetail.reconciliations.empty")}
342352
</p>
343353
) : (
344-
reconciliations.map((reconciliation) => (
345-
<div
346-
key={reconciliation.id}
347-
className="rounded-lg border bg-muted/20 p-3"
348-
>
349-
<div className="mb-3 flex items-start justify-between gap-4">
350-
<div className="flex flex-col gap-1">
351-
<span className="text-sm font-semibold">
354+
<Timeline value={reconciliations.length}>
355+
{reconciliations.map((reconciliation, index) => (
356+
<TimelineItem key={reconciliation.id} step={index + 1}>
357+
<TimelineHeader>
358+
<TimelineDate>
359+
{formatDateTime(
360+
new Date(reconciliation.claimedAt),
361+
locale
362+
)}
363+
</TimelineDate>
364+
<TimelineTitle>
352365
{t(paymentMethodLabelKey[reconciliation.transactionKind])}
353-
</span>
354-
<span className="break-all text-xs text-muted-foreground">
355-
{reconciliation.id}
356-
</span>
357-
</div>
358-
<Badge variant="secondary">
359-
{t(claimSourceLabelKey[reconciliation.source])}
360-
</Badge>
361-
</div>
362-
<div className="flex flex-col gap-2">
363-
<PaymentDetailRow
364-
label={t("paymentDetail.reconciliation.claimedAt")}
365-
value={formatDateTime(
366-
new Date(reconciliation.claimedAt),
367-
locale
368-
)}
369-
/>
370-
<PaymentDetailRow
371-
label={t("paymentDetail.transaction.id")}
372-
value={
373-
reconciliation.accountTransactionId ??
374-
t("paymentDetail.emptyValue")
375-
}
376-
/>
377-
<PaymentDetailRow
378-
label={t("paymentDetail.transaction.account")}
379-
value={
380-
reconciliation.accountName ??
381-
reconciliation.accountId ??
382-
t("paymentDetail.emptyValue")
383-
}
384-
/>
385-
<PaymentDetailRow
386-
label={t("paymentDetail.transaction.amount")}
387-
value={formatMoney(
388-
{
389-
value: reconciliation.transactionAmount,
390-
currency: reconciliation.transactionCurrency,
391-
},
392-
locale
393-
)}
394-
/>
395-
<PaymentDetailRow
396-
label={t("paymentDetail.transaction.occurredAt")}
397-
value={formatDateTime(
398-
new Date(reconciliation.transactionOccurredAt),
399-
locale
400-
)}
401-
/>
402-
<PaymentDetailRow
403-
label={t("paymentDetail.transaction.recordedAt")}
404-
value={
405-
reconciliation.transactionRecordedAt === null
406-
? t("paymentDetail.emptyValue")
407-
: formatDateTime(
408-
new Date(reconciliation.transactionRecordedAt),
366+
</TimelineTitle>
367+
</TimelineHeader>
368+
<TimelineIndicator />
369+
<TimelineSeparator />
370+
<TimelineContent>
371+
<div className="mt-2 rounded-lg border bg-muted/20 p-3">
372+
<div className="mb-3 flex items-start justify-between gap-4">
373+
<span className="break-all text-xs text-muted-foreground">
374+
{reconciliation.id}
375+
</span>
376+
<Badge variant="secondary">
377+
{t(claimSourceLabelKey[reconciliation.source])}
378+
</Badge>
379+
</div>
380+
<div className="flex flex-col gap-2">
381+
<PaymentDetailRow
382+
label={t("paymentDetail.transaction.id")}
383+
value={
384+
reconciliation.accountTransactionId ??
385+
t("paymentDetail.emptyValue")
386+
}
387+
/>
388+
<PaymentDetailRow
389+
label={t("paymentDetail.transaction.account")}
390+
value={
391+
reconciliation.accountName ??
392+
reconciliation.accountId ??
393+
t("paymentDetail.emptyValue")
394+
}
395+
/>
396+
<PaymentDetailRow
397+
label={t("paymentDetail.transaction.amount")}
398+
value={formatMoney(
399+
{
400+
value: reconciliation.transactionAmount,
401+
currency: reconciliation.transactionCurrency,
402+
},
403+
locale
404+
)}
405+
/>
406+
<PaymentDetailRow
407+
label={t("paymentDetail.transaction.occurredAt")}
408+
value={formatDateTime(
409+
new Date(reconciliation.transactionOccurredAt),
409410
locale
410-
)
411-
}
412-
/>
413-
<PaymentDetailRow
414-
label={t("paymentDetail.transaction.source")}
415-
value={
416-
reconciliation.transactionSource === null
417-
? t("paymentDetail.emptyValue")
418-
: t(
419-
claimSourceLabelKey[
420-
reconciliation.transactionSource
421-
]
422-
)
423-
}
424-
/>
425-
<PaymentDetailOptionalRow
426-
label={t("paymentDetail.transaction.note")}
427-
value={reconciliation.transactionNote}
428-
/>
429-
<PaymentDetailOptionalRow
430-
label={t("paymentDetail.transaction.variableSymbol")}
431-
value={reconciliation.variableSymbol}
432-
/>
433-
<PaymentDetailOptionalRow
434-
label={t("paymentDetail.transaction.bankReference")}
435-
value={reconciliation.bankReference}
436-
/>
437-
<PaymentDetailOptionalRow
438-
label={t("paymentDetail.transaction.sparkTransferId")}
439-
value={reconciliation.sparkTransferId}
440-
/>
441-
<PaymentDetailOptionalRow
442-
label={t("paymentDetail.transaction.paymentHash")}
443-
value={reconciliation.paymentHash}
444-
/>
445-
</div>
446-
</div>
447-
))
411+
)}
412+
/>
413+
<PaymentDetailRow
414+
label={t("paymentDetail.transaction.recordedAt")}
415+
value={
416+
reconciliation.transactionRecordedAt === null
417+
? t("paymentDetail.emptyValue")
418+
: formatDateTime(
419+
new Date(
420+
reconciliation.transactionRecordedAt
421+
),
422+
locale
423+
)
424+
}
425+
/>
426+
<PaymentDetailRow
427+
label={t("paymentDetail.transaction.source")}
428+
value={
429+
reconciliation.transactionSource === null
430+
? t("paymentDetail.emptyValue")
431+
: t(
432+
claimSourceLabelKey[
433+
reconciliation.transactionSource
434+
]
435+
)
436+
}
437+
/>
438+
<PaymentDetailOptionalRow
439+
label={t("paymentDetail.transaction.note")}
440+
value={reconciliation.transactionNote}
441+
/>
442+
<PaymentDetailOptionalRow
443+
label={t("paymentDetail.transaction.variableSymbol")}
444+
value={reconciliation.variableSymbol}
445+
/>
446+
<PaymentDetailOptionalRow
447+
label={t("paymentDetail.transaction.bankReference")}
448+
value={reconciliation.bankReference}
449+
/>
450+
<PaymentDetailOptionalRow
451+
label={t("paymentDetail.transaction.sparkTransferId")}
452+
value={reconciliation.sparkTransferId}
453+
/>
454+
<PaymentDetailOptionalRow
455+
label={t("paymentDetail.transaction.paymentHash")}
456+
value={reconciliation.paymentHash}
457+
/>
458+
</div>
459+
</div>
460+
</TimelineContent>
461+
</TimelineItem>
462+
))}
463+
</Timeline>
448464
)}
449465
</CardContent>
450466
</Card>

src/components/reui/alert.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { cva, type VariantProps } from "class-variance-authority"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
const alertVariants = cva(
6+
[
7+
"relative w-full text-sm border has-[>svg]:grid-cols-[calc(var(--spacing)*3)_1fr] grid-cols-[0_1fr] grid gap-y-0.5 items-center [&>svg:not([class*=size-])]:size-4",
8+
"has-[>[data-slot=alert-title]+[data-slot=alert-description]]:[&_[data-slot=alert-action]]:sm:row-end-3",
9+
"has-[>[data-slot=alert-title]+[data-slot=alert-description]]:items-start",
10+
"has-[>[data-slot=alert-title]+[data-slot=alert-description]]:[&_svg]:translate-y-0.5",
11+
"rounded-lg",
12+
"px-3",
13+
"py-2.5",
14+
"has-[>svg]:gap-x-2.5",
15+
],
16+
{
17+
variants: {
18+
variant: {
19+
default: "bg-card text-card-foreground",
20+
destructive:
21+
"border-destructive/30 bg-destructive/4 [&>svg]:text-destructive",
22+
info: "border-info/30 bg-info/4 [&>svg]:text-info",
23+
success: "border-success/30 bg-success/4 [&>svg]:text-success",
24+
warning: "border-warning/30 bg-warning/4 [&>svg]:text-warning",
25+
invert:
26+
"border-invert bg-invert text-invert-foreground [&_[data-slot=alert-description]]:text-invert-foreground/70",
27+
},
28+
},
29+
defaultVariants: {
30+
variant: "default",
31+
},
32+
}
33+
)
34+
35+
function Alert({
36+
className,
37+
variant,
38+
...props
39+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
40+
return (
41+
<div
42+
data-slot="alert"
43+
role="alert"
44+
className={cn(alertVariants({ variant }), className)}
45+
{...props}
46+
/>
47+
)
48+
}
49+
50+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
51+
return (
52+
<div
53+
data-slot="alert-title"
54+
className={cn(
55+
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
56+
className
57+
)}
58+
{...props}
59+
/>
60+
)
61+
}
62+
63+
function AlertDescription({
64+
className,
65+
...props
66+
}: React.ComponentProps<"div">) {
67+
return (
68+
<div
69+
data-slot="alert-description"
70+
className={cn(
71+
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
72+
className
73+
)}
74+
{...props}
75+
/>
76+
)
77+
}
78+
79+
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
80+
return (
81+
<div
82+
data-slot="alert-action"
83+
className={cn(
84+
"flex gap-1.5 max-sm:col-start-2 max-sm:mt-2 max-sm:justify-start sm:col-start-3 sm:row-start-1 sm:justify-end sm:self-center",
85+
className
86+
)}
87+
{...props}
88+
/>
89+
)
90+
}
91+
92+
export { Alert, AlertAction, AlertDescription, AlertTitle }

0 commit comments

Comments
 (0)