diff --git a/apps/dashboard/src/app/(auth)/layout.tsx b/apps/dashboard/src/app/(auth)/layout.tsx
index 3cce9bb..b4e0602 100644
--- a/apps/dashboard/src/app/(auth)/layout.tsx
+++ b/apps/dashboard/src/app/(auth)/layout.tsx
@@ -1,7 +1,7 @@
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
diff --git a/apps/dashboard/src/app/(dashboard)/refunds/page.tsx b/apps/dashboard/src/app/(dashboard)/refunds/page.tsx
new file mode 100644
index 0000000..5dfadb1
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/refunds/page.tsx
@@ -0,0 +1,18 @@
+import { RefundHistoryTable } from "@/components/payments/RefundHistoryTable";
+
+export default function RefundsPage() {
+ return (
+
+
+
+ Refunds
+
+
+ View and manage all refund transactions
+
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/globals.css b/apps/dashboard/src/app/globals.css
index 3c9d68b..4b2ff09 100644
--- a/apps/dashboard/src/app/globals.css
+++ b/apps/dashboard/src/app/globals.css
@@ -70,6 +70,37 @@
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
+
+ /* ── Dark mode page tokens ── */
+ --color-bg-page: var(--bg-page);
+ --color-bg-card: var(--bg-card);
+ --color-bg-sidebar: var(--bg-sidebar);
+ --color-text-primary: var(--text-primary);
+ --color-text-secondary: var(--text-secondary);
+ --color-border-color: var(--border-color);
+ --color-input-bg: var(--input-bg);
+ --color-input-border: var(--input-border);
+ --color-code-bg: var(--code-bg);
+
+ /* ── Chart theming tokens ── */
+ --color-chart-tick: var(--chart-tick);
+ --color-chart-grid: var(--chart-grid);
+ --color-chart-tooltip-bg: var(--chart-tooltip-bg);
+ --color-chart-tooltip-text: var(--chart-tooltip-text);
+
+ /* ── Status tokens ── */
+ --color-status-success-bg: var(--status-success-bg);
+ --color-status-success-text: var(--status-success-text);
+ --color-status-warning-bg: var(--status-warning-bg);
+ --color-status-warning-text: var(--status-warning-text);
+ --color-status-error-bg: var(--status-error-bg);
+ --color-status-error-text: var(--status-error-text);
+ --color-status-info-bg: var(--status-info-bg);
+ --color-status-info-text: var(--status-info-text);
+ --color-status-purple-bg: var(--status-purple-bg);
+ --color-status-purple-text: var(--status-purple-text);
+ --color-status-amber-bg: var(--status-amber-bg);
+ --color-status-amber-text: var(--status-amber-text);
}
@layer base {
@@ -90,6 +121,38 @@
:root {
--header-height: 3.5rem;
+ /* ── Dark mode specific tokens (dark-first) ── */
+ --bg-page: #0B1628;
+ --bg-card: #1A2332;
+ --bg-sidebar: #0F1D2E;
+ --text-primary: #F8F9FA;
+ --text-secondary: #6C757D;
+ --border-color: #2D3748;
+ --input-bg: #1A2332;
+ --input-border: #2D3748;
+ --code-bg: #0B1628;
+
+ /* ── Chart theming tokens ── */
+ --chart-tick: #F8F9FA;
+ --chart-grid: rgba(248, 249, 250, 0.08);
+ --chart-tooltip-bg: #1A2332;
+ --chart-tooltip-text: #F8F9FA;
+ --chart-tooltip-border: #2D3748;
+
+ /* ── Semantic status tokens ── */
+ --status-success-bg: rgba(34, 197, 94, 0.12);
+ --status-success-text: #4ade80;
+ --status-warning-bg: rgba(245, 158, 11, 0.12);
+ --status-warning-text: #fbbf24;
+ --status-error-bg: rgba(239, 68, 68, 0.12);
+ --status-error-text: #f87171;
+ --status-info-bg: rgba(59, 130, 246, 0.12);
+ --status-info-text: #60a5fa;
+ --status-purple-bg: rgba(168, 85, 247, 0.12);
+ --status-purple-text: #c084fc;
+ --status-amber-bg: rgba(245, 158, 11, 0.12);
+ --status-amber-text: #fbbf24;
+
/* Sidebar uses Useroutr ink tiers for dark-first design */
--sidebar: var(--ink2);
--sidebar-foreground: var(--lead);
@@ -102,6 +165,35 @@
}
.light {
+ --bg-page: #FFFFFF;
+ --bg-card: #F8F9FA;
+ --bg-sidebar: #F3F4F6;
+ --text-primary: #1A202C;
+ --text-secondary: #6B7280;
+ --border-color: #E2E8F0;
+ --input-bg: #FFFFFF;
+ --input-border: #D1D5DB;
+ --code-bg: #F1F5F9;
+
+ --chart-tick: #1A202C;
+ --chart-grid: rgba(0, 0, 0, 0.08);
+ --chart-tooltip-bg: #FFFFFF;
+ --chart-tooltip-text: #1A202C;
+ --chart-tooltip-border: #E2E8F0;
+
+ --status-success-bg: rgba(34, 197, 94, 0.1);
+ --status-success-text: #16a34a;
+ --status-warning-bg: rgba(245, 158, 11, 0.1);
+ --status-warning-text: #d97706;
+ --status-error-bg: rgba(239, 68, 68, 0.1);
+ --status-error-text: #dc2626;
+ --status-info-bg: rgba(59, 130, 246, 0.1);
+ --status-info-text: #2563eb;
+ --status-purple-bg: rgba(168, 85, 247, 0.1);
+ --status-purple-text: #7c3aed;
+ --status-amber-bg: rgba(245, 158, 11, 0.1);
+ --status-amber-text: #d97706;
+
--sidebar: oklch(0.975 0.003 265);
--sidebar-foreground: oklch(0.15 0.01 265);
--sidebar-primary: var(--blue);
@@ -113,14 +205,14 @@
}
.dark {
- --sidebar: hsl(240 5.9% 10%);
- --sidebar-foreground: hsl(240 4.8% 95.9%);
- --sidebar-primary: hsl(224.3 76.3% 48%);
- --sidebar-primary-foreground: hsl(0 0% 100%);
- --sidebar-accent: hsl(240 3.7% 15.9%);
- --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
- --sidebar-border: hsl(240 3.7% 15.9%);
- --sidebar-ring: hsl(217.2 91.2% 59.8%);
+ --sidebar: var(--bg-sidebar);
+ --sidebar-foreground: var(--text-primary);
+ --sidebar-primary: var(--blue);
+ --sidebar-primary-foreground: #FFFFFF;
+ --sidebar-accent: #1E293B;
+ --sidebar-accent-foreground: var(--text-primary);
+ --sidebar-border: var(--border-color);
+ --sidebar-ring: var(--blue2);
}
/* ── Modern card surface ── */
diff --git a/apps/dashboard/src/app/layout.tsx b/apps/dashboard/src/app/layout.tsx
index 11fb4c1..ba1fb9f 100644
--- a/apps/dashboard/src/app/layout.tsx
+++ b/apps/dashboard/src/app/layout.tsx
@@ -6,6 +6,7 @@ import { QueryProvider } from "@/providers/QueryProvider";
import { AuthProvider } from "@/providers/AuthProvider";
import "./globals.css";
import { ToastProvider } from "@useroutr/ui";
+import { TooltipProvider } from "@/components/ui/tooltip";
const inter = Inter({
subsets: ["latin"],
@@ -34,12 +35,21 @@ export default function RootLayout({
}) {
return (
+
+
+
- {children}
+
+ {children}
+
diff --git a/apps/dashboard/src/components/analytics/AnalyticsDashboard.tsx b/apps/dashboard/src/components/analytics/AnalyticsDashboard.tsx
index b183228..f4d692f 100644
--- a/apps/dashboard/src/components/analytics/AnalyticsDashboard.tsx
+++ b/apps/dashboard/src/components/analytics/AnalyticsDashboard.tsx
@@ -111,9 +111,9 @@ const PERIOD_OPTIONS: Array<{ value: Period; label: string }> = [
];
const PAYMENT_COLORS: Record = {
- card: "#3b82f6",
- crypto: "#14b8a6",
- bank: "#f59e0b",
+ card: "var(--blue)",
+ crypto: "var(--teal)",
+ bank: "var(--amber)",
};
const WEEKDAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
@@ -782,7 +782,7 @@ function RevenueChart({
x2={width}
y1={y}
y2={y}
- stroke="rgba(148,163,184,0.22)"
+ stroke="var(--chart-grid)"
strokeDasharray="4 6"
/>
);
@@ -806,7 +806,7 @@ function RevenueChart({
width={barWidth}
height={barHeight}
rx={8}
- fill={active ? "#1d4ed8" : "#3b82f6"}
+ fill={active ? "var(--blue)" : "var(--blue2)"}
opacity={active ? 1 : 0.82}
/>
{point.label}
@@ -826,13 +826,13 @@ function RevenueChart({
width={106}
height={30}
rx={10}
- fill="rgba(15,23,42,0.95)"
+ fill="var(--chart-tooltip-bg)"
/>
{formatCompactMoney(point.amount)}
@@ -880,7 +880,7 @@ function PaymentMethodCard({
cy="70"
r={radius}
fill="none"
- stroke="rgba(148,163,184,0.18)"
+ stroke="var(--chart-grid)"
strokeWidth="20"
/>
{segments.map((segment) => {
@@ -983,8 +983,8 @@ function ConversionCard({
>
-
-
+
+
cell.count), 1);
const getColor = (count: number) => {
const alpha = count / maxCount;
- return `rgba(239,68,68,${0.1 + alpha * 0.75})`;
+ return `color-mix(in srgb, var(--red) ${Math.round((0.1 + alpha * 0.75) * 100)}%, transparent)`;
};
return (
diff --git a/apps/dashboard/src/components/app-sidebar.tsx b/apps/dashboard/src/components/app-sidebar.tsx
index 9059916..30b91bc 100644
--- a/apps/dashboard/src/components/app-sidebar.tsx
+++ b/apps/dashboard/src/components/app-sidebar.tsx
@@ -10,6 +10,7 @@ import {
Settings,
LifeBuoy,
Send,
+ RotateCcw,
} from "lucide-react";
import { NavMain } from "@/components/nav-main";
@@ -28,6 +29,7 @@ import {
const navMain = [
{ title: "Overview", url: "/", icon: Home },
{ title: "Payments", url: "/payments", icon: CreditCard },
+ { title: "Refunds", url: "/refunds", icon: RotateCcw },
{ title: "Payment Links", url: "/links", icon: Link2 },
{ title: "Invoices", url: "/invoices", icon: FileText },
{ title: "Payouts", url: "/payouts", icon: ArrowLeftRight },
diff --git a/apps/dashboard/src/components/invoices/InvoiceDetailSheet.tsx b/apps/dashboard/src/components/invoices/InvoiceDetailSheet.tsx
index 3d7e594..060eac1 100644
--- a/apps/dashboard/src/components/invoices/InvoiceDetailSheet.tsx
+++ b/apps/dashboard/src/components/invoices/InvoiceDetailSheet.tsx
@@ -1,4 +1,4 @@
-"use client";
+"use client";
import { useState } from "react";
import {
@@ -34,15 +34,15 @@ import { SendInvoiceModal } from "./SendInvoiceModal";
import { InvoicePdfPreview } from "./InvoicePdfPreview";
import type { Invoice, InvoiceStatus } from "@/hooks/useInvoices";
-// ── Status badge ───────────────────────────────────────────────────────────────
+// ── Status badge ───────────────────────────────────────────────────────────────
const STATUS_CONFIG: Record = {
DRAFT: { label: "Draft", className: "bg-muted text-muted-foreground" },
- SENT: { label: "Sent", className: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300" },
- VIEWED: { label: "Viewed", className: "bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300" },
- PARTIALLY_PAID: { label: "Partially Paid", className: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300" },
- PAID: { label: "Paid", className: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300" },
- OVERDUE: { label: "Overdue", className: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300" },
+ SENT: { label: "Sent", className: "bg-status-info-bg text-status-info-text" },
+ VIEWED: { label: "Viewed", className: "bg-status-purple-bg text-status-purple-text" },
+ PARTIALLY_PAID: { label: "Partially Paid", className: "bg-status-amber-bg text-status-amber-text" },
+ PAID: { label: "Paid", className: "bg-status-success-bg text-status-success-text" },
+ OVERDUE: { label: "Overdue", className: "bg-status-error-bg text-status-error-text" },
CANCELLED: { label: "Cancelled", className: "bg-muted text-muted-foreground" },
};
@@ -55,7 +55,7 @@ function StatusBadge({ status }: { status: InvoiceStatus }) {
);
}
-// ── Activity timeline ─────────────────────────────────────────────────────────
+// ── Activity timeline ─────────────────────────────────────────────────────────
interface TimelineEvent {
key: string;
@@ -239,7 +239,7 @@ function ActivityTimeline({ invoice }: { invoice: Invoice }) {
);
}
-// ── Props ──────────────────────────────────────────────────────────────────────
+// ── Props ──────────────────────────────────────────────────────────────────────
interface InvoiceDetailSheetProps {
invoice: Invoice | null;
@@ -254,7 +254,7 @@ interface InvoiceDetailSheetProps {
isSendPending?: boolean;
}
-// ── Helpers ────────────────────────────────────────────────────────────────────
+// ── Helpers ────────────────────────────────────────────────────────────────────
function fmtCurrency(amount: string | number, currency: string) {
const num = typeof amount === "string" ? parseFloat(amount) : amount;
@@ -263,7 +263,7 @@ function fmtCurrency(amount: string | number, currency: string) {
}
function fmtDate(iso?: string | null) {
- if (!iso) return "—";
+ if (!iso) return "—";
return new Date(iso).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
@@ -271,7 +271,7 @@ function fmtDate(iso?: string | null) {
});
}
-// ── Component ──────────────────────────────────────────────────────────────────
+// ── Component ──────────────────────────────────────────────────────────────────
export function InvoiceDetailSheet({
invoice,
@@ -305,7 +305,7 @@ export function InvoiceDetailSheet({
<>
- {/* ── Header ── */}
+ {/* ── Header ── */}
@@ -334,7 +334,7 @@ export function InvoiceDetailSheet({
- {/* ── Scrollable body ── */}
+ {/* ── Scrollable body ── */}
@@ -402,7 +402,7 @@ export function InvoiceDetailSheet({
{fmtDate(invoice.dueDate)}
@@ -412,7 +412,7 @@ export function InvoiceDetailSheet({
{invoice.paidAt && (
Paid On
-
{fmtDate(invoice.paidAt)}
+
{fmtDate(invoice.paidAt)}
)}
@@ -464,8 +464,8 @@ export function InvoiceDetailSheet({
{discount > 0 && (
Discount
-
- −{fmtCurrency(discount, invoice.currency)}
+
+ −{fmtCurrency(discount, invoice.currency)}
)}
@@ -476,7 +476,7 @@ export function InvoiceDetailSheet({
{amountPaid > 0 && (
<>
-
+
Amount Paid
{fmtCurrency(amountPaid, invoice.currency)}
@@ -494,10 +494,10 @@ export function InvoiceDetailSheet({
Payment Details
-
+
-
-
+
+
{invoice.status === "PAID" ? "Fully paid" : "Partially paid"}
@@ -517,7 +517,7 @@ export function InvoiceDetailSheet({
{amountDue > 0 && (
Balance due
-
+
{fmtCurrency(amountDue, invoice.currency)}
@@ -554,7 +554,7 @@ export function InvoiceDetailSheet({
- {/* ── Footer actions ── */}
+ {/* ── Footer actions ── */}
{/* Primary row: Edit + Send */}
{isDraft && (
@@ -615,7 +615,7 @@ export function InvoiceDetailSheet({
- {/* ── Send modal (rendered outside sheet to avoid z-index nesting issues) ── */}
+ {/* ── Send modal (rendered outside sheet to avoid z-index nesting issues) ── */}
- {/* ── PDF preview dialog ── */}
+ {/* ── PDF preview dialog ── */}
0"),
unitPrice: z
.number({ invalid_type_error: "Must be a number" })
- .nonnegative("Price ≥ 0"),
+ .nonnegative("Price ≥ 0"),
});
const InvoiceSchema = z.object({
@@ -44,7 +44,7 @@ const InvoiceSchema = z.object({
type FormErrors = Partial>;
-// ── Line item helpers ──────────────────────────────────────────────────────────
+// ── Line item helpers ──────────────────────────────────────────────────────────
interface LineItemRow {
id: string;
@@ -80,7 +80,7 @@ function computeTotals(
return { subtotal, taxAmount, total };
}
-// ── Props ──────────────────────────────────────────────────────────────────────
+// ── Props ──────────────────────────────────────────────────────────────────────
interface InvoiceDrawerProps {
open: boolean;
@@ -92,7 +92,7 @@ interface InvoiceDrawerProps {
isLoading?: boolean;
}
-// ── Component ──────────────────────────────────────────────────────────────────
+// ── Component ──────────────────────────────────────────────────────────────────
export function InvoiceDrawer({
open,
@@ -104,12 +104,12 @@ export function InvoiceDrawer({
}: InvoiceDrawerProps) {
const isEditing = !!invoice;
- // ── Customer fields ──────────────────────────────────────────────────────────
+ // ── Customer fields ──────────────────────────────────────────────────────────
const [customerEmail, setCustomerEmail] = useState(invoice?.customerEmail ?? "");
const [customerName, setCustomerName] = useState(invoice?.customerName ?? "");
const [invoiceNumber, setInvoiceNumber] = useState(invoice?.invoiceNumber ?? "");
- // ── Line items ───────────────────────────────────────────────────────────────
+ // ── Line items ───────────────────────────────────────────────────────────────
const [rows, setRows] = useState(() => {
if (invoice?.lineItems?.length) {
return invoice.lineItems.map((li) => ({
@@ -122,7 +122,7 @@ export function InvoiceDrawer({
return [emptyRow()];
});
- // ── Pricing ──────────────────────────────────────────────────────────────────
+ // ── Pricing ──────────────────────────────────────────────────────────────────
const [currency, setCurrency] = useState(invoice?.currency ?? "USD");
const [taxRate, setTaxRate] = useState(
invoice?.taxRate ? String(Number(invoice.taxRate) * 100) : "",
@@ -131,21 +131,21 @@ export function InvoiceDrawer({
invoice?.discount ? String(Number(invoice.discount)) : "",
);
- // ── Meta ─────────────────────────────────────────────────────────────────────
+ // ── Meta ─────────────────────────────────────────────────────────────────────
const [dueDate, setDueDate] = useState(
invoice?.dueDate ? invoice.dueDate.split("T")[0] : "",
);
const [notes, setNotes] = useState(invoice?.notes ?? "");
- // ── Errors ───────────────────────────────────────────────────────────────────
+ // ── Errors ───────────────────────────────────────────────────────────────────
const [errors, setErrors] = useState({});
- // ── Computed totals ───────────────────────────────────────────────────────────
+ // ── Computed totals ───────────────────────────────────────────────────────────
const taxRatePct = parseFloat(taxRate) || 0;
const discountAmt = parseFloat(discount) || 0;
const { subtotal, taxAmount, total } = computeTotals(rows, taxRatePct, discountAmt);
- // ── Row handlers ──────────────────────────────────────────────────────────────
+ // ── Row handlers ──────────────────────────────────────────────────────────────
const addRow = () => setRows((prev) => [...prev, emptyRow()]);
const removeRow = useCallback((id: string) => {
@@ -166,7 +166,7 @@ export function InvoiceDrawer({
[],
);
- // ── Validation ────────────────────────────────────────────────────────────────
+ // ── Validation ────────────────────────────────────────────────────────────────
const validate = (): CreateInvoiceInput | null => {
const lineItemsForValidation = rows.map(rowToItem);
@@ -259,7 +259,7 @@ export function InvoiceDrawer({
- {/* ── Scrollable body ── */}
+ {/* ── Scrollable body ── */}
{/* Client details */}
@@ -466,7 +466,7 @@ export function InvoiceDrawer({
{discountAmt > 0 && (
Discount
- −{fmtCurrency(discountAmt)}
+ −{fmtCurrency(discountAmt)}
)}
@@ -496,7 +496,7 @@ export function InvoiceDrawer({
- {/* ── Footer ── */}
+ {/* ── Footer ── */}