+
-
-
@@ -1768,71 +1845,6 @@ function DesktopBillSplitter() {
- {/* --- Context Menu --- */}
- {contextMenu && (
-
e.stopPropagation()}
- >
-
- Actions
-
- {contextMenu.personId ? (
-
- ) : (
- <>
-
-
-
- >
- )}
-
-
-
- )}
-
{/* --- Person Editor Modal --- */}
{editingPerson && (
e.stopPropagation()}
>
-
Edit Member
+ Edit Member
@@ -1883,14 +1900,14 @@ function DesktopBillSplitter() {
removePerson(editingPerson.id)}
- className="flex-1 py-2.5 rounded-lg border border-red-100 text-red-600 text-xs font-bold uppercase tracking-wide hover:bg-red-50 transition-colors font-inter"
+ onClick={() => openRemovePersonDialog(editingPerson)}
+ className="flex-1 py-2.5 rounded-lg border border-red-100 text-red-600 text-xs font-bold uppercase hover:bg-red-50 transition-colors font-inter"
>
Remove
updatePerson(editingPerson)}
- className="flex-[2] py-2.5 rounded-lg bg-slate-900 text-white text-xs font-bold uppercase tracking-wide hover:bg-slate-800 transition-colors shadow-lg shadow-slate-900/20 font-inter"
+ className="flex-[2] py-2.5 rounded-lg bg-slate-900 text-white text-xs font-bold uppercase hover:bg-slate-800 transition-colors shadow-lg shadow-slate-900/20 font-inter"
>
Save Changes
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..ddf2ebc
--- /dev/null
+++ b/components/ui/alert-dialog.tsx
@@ -0,0 +1,157 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps
) {
+ return
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ AlertDialog,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
From 93ddc342f6fd157d3a6e0f2a7af6fc634a299786 Mon Sep 17 00:00:00 2001
From: Anurag Dhungana <36888347+Aarekaz@users.noreply.github.com>
Date: Sat, 31 Jan 2026 02:12:27 -0500
Subject: [PATCH 02/24] Improve UI accessibility and motion
---
app/admin/page.tsx | 9 ++-
app/globals.css | 14 +++-
components/BillLookup.tsx | 6 ++
components/KeyboardShortcutsHelp.tsx | 1 +
components/LedgerItemsTable.tsx | 3 +-
components/MobileLedgerView.tsx | 1 +
components/MobileSpreadsheetView.tsx | 1 +
components/PeopleBreakdownTable.tsx | 3 +-
components/ProBillSplitter.tsx | 108 +++++++++++++++------------
components/ReceiptScanner.tsx | 33 ++++++--
components/ShareBill.tsx | 2 +
components/TotalsPanel.tsx | 13 +++-
components/mobile/MobileGridView.tsx | 2 +
13 files changed, 139 insertions(+), 57 deletions(-)
diff --git a/app/admin/page.tsx b/app/admin/page.tsx
index cd3c4d2..f58ea65 100644
--- a/app/admin/page.tsx
+++ b/app/admin/page.tsx
@@ -772,6 +772,7 @@ export default function AdminPage() {
placeholder="Search bills by title, ID, or amount..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
+ aria-label="Search bills"
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
/>
@@ -872,6 +873,7 @@ export default function AdminPage() {
setShowBillDialog(true)
}}
title="View details"
+ aria-label="View bill details"
>
@@ -880,6 +882,7 @@ export default function AdminPage() {
size="icon"
onClick={() => window.open(bill.shareUrl, '_blank')}
title="Open in new tab"
+ aria-label="Open share link in new tab"
>
@@ -888,6 +891,7 @@ export default function AdminPage() {
size="icon"
onClick={() => copyToClipboard(bill.shareUrl)}
title="Copy share link"
+ aria-label="Copy share link"
>
@@ -896,6 +900,7 @@ export default function AdminPage() {
size="icon"
onClick={() => handleExtendBill(bill.id)}
title="Extend expiration"
+ aria-label="Extend bill expiration"
>
@@ -907,6 +912,7 @@ export default function AdminPage() {
setShowDeleteDialog(true)
}}
title="Delete bill"
+ aria-label="Delete bill"
>
@@ -996,6 +1002,7 @@ export default function AdminPage() {
variant="outline"
size="icon"
onClick={() => copyToClipboard(selectedBill.shareUrl)}
+ aria-label="Copy share URL"
>
@@ -1119,4 +1126,4 @@ export default function AdminPage() {
)
-}
\ No newline at end of file
+}
diff --git a/app/globals.css b/app/globals.css
index a686a76..9d8c2cd 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -520,7 +520,10 @@
select:focus {
@apply outline-none ring-2 ring-primary ring-offset-2;
background-color: rgba(var(--primary), 0.02);
- transition: all 0.2s var(--ease-out-cubic);
+ transition:
+ box-shadow 0.2s var(--ease-out-cubic),
+ border-color 0.2s var(--ease-out-cubic),
+ background-color 0.2s var(--ease-out-cubic);
}
input::placeholder {
@@ -1142,3 +1145,12 @@
scrollbar-color: #CBD5E1 transparent;
}
}
+
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/components/BillLookup.tsx b/components/BillLookup.tsx
index ccfbd59..2b5c1da 100644
--- a/components/BillLookup.tsx
+++ b/components/BillLookup.tsx
@@ -4,6 +4,7 @@ import React, { useState } from "react"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
+import { Label } from "@/components/ui/label"
import { Search, Loader2 } from "lucide-react"
import { getBillFromCloud } from "@/lib/sharing"
import { useBill } from "@/contexts/BillContext"
@@ -150,7 +151,11 @@ export function BillLookup({ mode = "auto" }: BillLookupProps) {
+
diff --git a/components/LedgerItemsTable.tsx b/components/LedgerItemsTable.tsx
index 3fc7123..e370aec 100644
--- a/components/LedgerItemsTable.tsx
+++ b/components/LedgerItemsTable.tsx
@@ -576,8 +576,9 @@ export function LedgerItemsTable() {
variant="ghost"
size="icon"
onClick={() => handleDeleteItem(item.id)}
- className="h-7 w-7 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-all"
+ className="h-7 w-7 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-all"
title="Delete item"
+ aria-label="Delete item"
>
diff --git a/components/MobileLedgerView.tsx b/components/MobileLedgerView.tsx
index 8ebdace..7ea2d94 100644
--- a/components/MobileLedgerView.tsx
+++ b/components/MobileLedgerView.tsx
@@ -254,6 +254,7 @@ export function MobileLedgerView() {
size="icon"
onClick={() => handleDeleteItem(item.id)}
className="h-8 w-8 text-muted-foreground hover:text-destructive"
+ aria-label="Delete item"
>
diff --git a/components/MobileSpreadsheetView.tsx b/components/MobileSpreadsheetView.tsx
index fcbf6a0..b13c4ec 100644
--- a/components/MobileSpreadsheetView.tsx
+++ b/components/MobileSpreadsheetView.tsx
@@ -67,6 +67,7 @@ export function MobileSpreadsheetView() {
value={state.currentBill.title}
onChange={(e) => dispatch({ type: "SET_BILL_TITLE", payload: e.target.value })}
className="h-9 text-base font-semibold border-none px-0 focus-visible:ring-0 bg-transparent"
+ aria-label="Bill title"
/>
SplitSimple
diff --git a/components/PeopleBreakdownTable.tsx b/components/PeopleBreakdownTable.tsx
index 4896954..0dc0ffd 100644
--- a/components/PeopleBreakdownTable.tsx
+++ b/components/PeopleBreakdownTable.tsx
@@ -181,8 +181,9 @@ export function PeopleBreakdownTable({
variant="ghost"
size="icon"
onClick={() => handleRemovePerson(person.id)}
- className="h-7 w-7 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-all"
+ className="h-7 w-7 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-all"
title={`Remove ${person.name}`}
+ aria-label={`Remove ${person.name}`}
>
diff --git a/components/ProBillSplitter.tsx b/components/ProBillSplitter.tsx
index 4f28aaf..2431b09 100644
--- a/components/ProBillSplitter.tsx
+++ b/components/ProBillSplitter.tsx
@@ -59,6 +59,12 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/ui/context-menu'
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
export type SplitMethod = "even" | "shares" | "percent" | "exact"
@@ -151,10 +157,21 @@ const GridCell = React.memo(({
}
return (
-
onCellClick(row, col)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ onCellClick(row, col)
+ }
+ }}
className={cn(
- "w-full h-full px-4 py-3 flex items-center cursor-text relative",
+ "w-full h-full px-4 py-3 flex items-center cursor-text relative text-left",
isSelected && "ring-inset ring-2 ring-indigo-500 z-10",
className
)}
@@ -162,7 +179,7 @@ const GridCell = React.memo(({
{value ? (field === 'price' ? `$${value}` : value) : placeholder}
-
+
)
})
@@ -860,6 +877,7 @@ function DesktopBillSplitter() {
}}
className="block text-sm font-bold bg-transparent border-none p-0 focus:ring-0 text-slate-900 w-auto min-w-[7ch] max-w-[26ch] hover:text-indigo-600 transition-colors font-inter"
placeholder="Project Name"
+ aria-label="Bill title"
/>
SPLIT SIMPLE
@@ -948,10 +966,13 @@ function DesktopBillSplitter() {
e.stopPropagation()}>
-
+
{
@@ -1090,7 +1111,12 @@ function DesktopBillSplitter() {
-
+
{/* Sticky Header */}
#
@@ -1102,8 +1128,8 @@ function DesktopBillSplitter() {
const colorObj = COLORS[p.colorIdx || 0]
const initials = p.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
return (
-
setHoveredColumn(p.id)}
onMouseLeave={() => setHoveredColumn(null)}
onClick={() => setEditingPerson(p)}
- aria-label={`Edit ${p.name}`}
- type="button"
- >
+ aria-label={`Edit ${p.name}`}
+ type="button"
+ >
-
-
+
+
+
Total
@@ -1315,8 +1341,8 @@ function DesktopBillSplitter() {
duplicateItem(item)}
aria-label="Duplicate row"
- className="text-slate-300 hover:text-indigo-600 opacity-0 group-hover:opacity-100 transition-opacity p-1"
- tabIndex={-1}
+ className="text-slate-300 hover:text-indigo-600 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity p-1"
+ tabIndex={0}
title="Duplicate row"
>
@@ -1324,8 +1350,8 @@ function DesktopBillSplitter() {
openDeleteDialog(item)}
aria-label="Delete row"
- className="text-slate-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity p-1"
- tabIndex={-1}
+ className="text-slate-300 hover:text-red-500 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity p-1"
+ tabIndex={0}
title="Delete row"
>
@@ -1522,8 +1548,9 @@ function DesktopBillSplitter() {
-
+
-
+
-
+
{/* --- Person Editor Modal --- */}
- {editingPerson && (
-
setEditingPerson(null)}
- >
-
e.stopPropagation()}
- >
-
-
Edit Member
- setEditingPerson(null)}
- className="text-slate-400 hover:text-slate-600 bg-slate-50 p-1 rounded-full"
- >
-
-
-
-
+
-
- )}
+ )}
+
+
)
}
diff --git a/components/ReceiptScanner.tsx b/components/ReceiptScanner.tsx
index 51ebfbc..5567bd3 100644
--- a/components/ReceiptScanner.tsx
+++ b/components/ReceiptScanner.tsx
@@ -218,9 +218,10 @@ function UploadView({ onUpload, onPaste }: { onUpload: (file: File) => void, onP
- void, onP
onDragOver={handleDrag}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
+ aria-label="Upload receipt image"
>
void, onP
Click to upload or drag & drop
Supports JPG, PNG, HEIC (Max 5MB) • Preview unavailable for HEIC
-
+
@@ -341,15 +343,33 @@ function ReviewView({
{image && (
-
setZoom(Math.max(0.5, zoom - 0.25))}>
+ setZoom(Math.max(0.5, zoom - 0.25))}
+ aria-label="Zoom out"
+ >
{Math.round(zoom * 100)}%
- setZoom(Math.min(3, zoom + 0.25))}>
+ setZoom(Math.min(3, zoom + 0.25))}
+ aria-label="Zoom in"
+ >
- setRotate((rotation + 90) % 360)}>
+ setRotate((rotation + 90) % 360)}
+ aria-label="Rotate image"
+ >
@@ -424,6 +444,7 @@ function ReviewView({
size="icon"
className="h-8 w-8 text-slate-300 hover:text-red-500 hover:bg-red-50"
onClick={() => handleDelete(idx)}
+ aria-label="Delete item"
>
diff --git a/components/ShareBill.tsx b/components/ShareBill.tsx
index d71c5f3..14e1c3e 100644
--- a/components/ShareBill.tsx
+++ b/components/ShareBill.tsx
@@ -218,6 +218,7 @@ export function ShareBill({ variant = "outline", size = "sm", showText = true, i
disabled={isStoring || !shareUrl}
className="flex-shrink-0"
title="Copy link"
+ aria-label="Copy share link"
>
{copied ? (
@@ -232,6 +233,7 @@ export function ShareBill({ variant = "outline", size = "sm", showText = true, i
disabled={isStoring || !shareUrl}
className="flex-shrink-0"
title="Open in new tab"
+ aria-label="Open share link in new tab"
>
diff --git a/components/TotalsPanel.tsx b/components/TotalsPanel.tsx
index 580cdc4..c383763 100644
--- a/components/TotalsPanel.tsx
+++ b/components/TotalsPanel.tsx
@@ -112,6 +112,16 @@ export function TotalsPanel({
togglePersonExpansion(person.id)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault()
+ togglePersonExpansion(person.id)
+ }
+ }}
+ role="button"
+ tabIndex={0}
+ aria-expanded={isExpanded}
+ aria-label={`Toggle ${person.name} details`}
>
{/* Collapsed Row */}
@@ -135,7 +145,8 @@ export function TotalsPanel({
e.stopPropagation()
handleRemovePerson(person.id)
}}
- className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-all"
+ className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-all"
+ aria-label={`Remove ${person.name}`}
>
diff --git a/components/mobile/MobileGridView.tsx b/components/mobile/MobileGridView.tsx
index 81160d9..0636517 100644
--- a/components/mobile/MobileGridView.tsx
+++ b/components/mobile/MobileGridView.tsx
@@ -440,6 +440,7 @@ export function MobileGridView() {
variant="outline"
onClick={() => handleUpdateItem({ quantity: Math.max(1, editingItem.quantity - 1) })}
className="h-11 w-11"
+ aria-label="Decrease quantity"
>
@@ -455,6 +456,7 @@ export function MobileGridView() {
variant="outline"
onClick={() => handleUpdateItem({ quantity: editingItem.quantity + 1 })}
className="h-11 w-11"
+ aria-label="Increase quantity"
>
From 1ba36ee67bc82e0c2008102e60e65a794b7ae828 Mon Sep 17 00:00:00 2001
From: Anurag Dhungana <36888347+Aarekaz@users.noreply.github.com>
Date: Sat, 31 Jan 2026 10:18:27 -0500
Subject: [PATCH 03/24] Add OG image and metadata
---
app/layout.tsx | 42 +++++++++++++++++++++-
app/og-image/page.tsx | 80 ++++++++++++++++++++++++++++++++++++++++++
public/og-image.png | Bin 0 -> 78692 bytes
public/og-image.svg | 61 ++++++++++++++++++++++++++++++++
4 files changed, 182 insertions(+), 1 deletion(-)
create mode 100644 app/og-image/page.tsx
create mode 100644 public/og-image.png
create mode 100644 public/og-image.svg
diff --git a/app/layout.tsx b/app/layout.tsx
index ba3dd07..4991a26 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -8,9 +8,49 @@ import { Analytics } from "@vercel/analytics/react"
import { PostHogProvider } from "@/components/PostHogProvider"
export const metadata: Metadata = {
+ metadataBase: new URL("https://splitsimple.anuragd.me"),
title: "SplitSimple - Easy Expense Splitting",
description: "Split expenses with friends and colleagues effortlessly",
generator: "v0.app",
+ openGraph: {
+ title: "SplitSimple - Easy Expense Splitting",
+ description: "Split expenses with friends and colleagues effortlessly",
+ url: "https://splitsimple.anuragd.me",
+ siteName: "SplitSimple",
+ locale: "en_US",
+ type: "website",
+ images: [
+ {
+ url: "/og-image.png",
+ width: 1200,
+ height: 630,
+ alt: "SplitSimple bill splitting summary preview",
+ type: "image/png",
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "SplitSimple - Easy Expense Splitting",
+ description: "Split expenses with friends and colleagues effortlessly",
+ images: [
+ {
+ url: "/og-image.png",
+ width: 1200,
+ height: 630,
+ alt: "SplitSimple bill splitting summary preview",
+ },
+ ],
+ },
+ other: {
+ "theme-color": "#1E40AF",
+ "msapplication-TileColor": "#1E40AF",
+ },
+ appleWebApp: {
+ title: "SplitSimple",
+ statusBarStyle: "black-translucent",
+ capable: true,
+ },
}
export default function RootLayout({
@@ -33,4 +73,4 @@ export default function RootLayout({