From f7503d03d0e72a4704c1faeff4e97a5209d4bea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Tue, 25 Aug 2026 16:43:28 +0200 Subject: [PATCH] Restyle UI with a channel-strip console look and brand icon Adds a pink/purple/blue SVG brand mark (favicon + header) and reworks the visual language around the app's actual interaction - fader-driven device control - via LED-style indicators, mono instrument readouts, hairline-framed cards, console-tab nav, and an oscilloscope-style session timeline. Leaves components/ui/* untouched, restyling only via design tokens and the app's own composed components. Also fixes a proxy.ts matcher gap that was gating /icon.svg behind auth. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01W8WkFF5ppURBAB918593Eb --- README.md | 2 +- app/(app)/control/page.tsx | 2 +- app/(app)/devices/page.tsx | 2 +- app/(app)/page.tsx | 46 +++-- app/(app)/recordings/[id]/page.tsx | 2 +- app/(app)/recordings/[id]/replay/page.tsx | 2 +- app/(app)/recordings/page.tsx | 2 +- app/(app)/sessions/[id]/page.tsx | 2 +- app/(app)/sessions/page.tsx | 2 +- app/(app)/stats/page.tsx | 2 +- app/globals.css | 200 ++++++++++--------- app/icon.svg | 22 ++ app/layout.tsx | 9 +- app/login/page.tsx | 6 +- components/control/ActuatorSlider.tsx | 17 +- components/control/DeviceCard.tsx | 9 +- components/devices/DevicesTable.tsx | 4 +- components/layout/BrandMark.tsx | 42 ++++ components/layout/ConnectionStatus.tsx | 13 +- components/layout/NavBar.tsx | 12 +- components/recordings/RecordingsTable.tsx | 8 +- components/sessions/SessionTimelineChart.tsx | 23 ++- components/sessions/SessionsTable.tsx | 6 +- components/stats/DeviceUsageTable.tsx | 8 +- components/stats/RecordingLibraryStats.tsx | 10 +- components/stats/SessionsSummaryCards.tsx | 6 +- proxy.ts | 2 +- 27 files changed, 278 insertions(+), 183 deletions(-) create mode 100644 app/icon.svg create mode 100644 components/layout/BrandMark.tsx diff --git a/README.md b/README.md index bbde0c5..2e1222b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# sexy +# SEXY A self-hosted web console for scanning, connecting to, and controlling Bluetooth LE sex toys over the [Buttplug](https://buttplug.io) protocol - the same open-source protocol behind Intiface Central and most diff --git a/app/(app)/control/page.tsx b/app/(app)/control/page.tsx index 5bb4f7a..ad783a7 100644 --- a/app/(app)/control/page.tsx +++ b/app/(app)/control/page.tsx @@ -4,7 +4,7 @@ export default function ControlPage() { return (
-

Control

+

Control

Scan for devices, connect, and control them live. Every command runs directly from your browser to the device over Web Bluetooth - the server never sees it in real time. diff --git a/app/(app)/devices/page.tsx b/app/(app)/devices/page.tsx index 94cc530..e6e9b47 100644 --- a/app/(app)/devices/page.tsx +++ b/app/(app)/devices/page.tsx @@ -10,7 +10,7 @@ export default async function DevicesPage() { return (

-

Devices

+

Devices

Devices seen across past sessions. Give them friendlier names.

diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index ef45e04..be41b30 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { BrandMark } from "@/components/layout/BrandMark"; import { listRecordings } from "@/lib/db/queries/recordings"; import { listPlaySessions } from "@/lib/db/queries/play-sessions"; import { getSessionsSummary } from "@/lib/db/queries/stats"; @@ -23,7 +24,8 @@ export default async function DashboardPage() {
-

Welcome back

+ +

Welcome back

Scan for nearby devices, take control, and record sessions to replay later - all running directly from your browser over Web Bluetooth. @@ -39,13 +41,13 @@ export default async function DashboardPage() { Completed sessions - {summary.count} + {summary.count} Total play time - + {(summary.totalDurationMs / 3_600_000).toFixed(1)}h @@ -53,7 +55,7 @@ export default async function DashboardPage() { Saved recordings - {recordings.length} + {recordings.length}

@@ -62,18 +64,17 @@ export default async function DashboardPage() { Recent recordings - + {recentRecordings.length === 0 && (

Nothing saved yet.

)} - {recentRecordings.map((r) => ( - - {r.name} - {r.playCount} plays + {recentRecordings.map((r, i) => ( + + {i > 0 &&
} +
+ {r.name} + {r.playCount} plays +
))} @@ -83,16 +84,17 @@ export default async function DashboardPage() { Recent sessions - + {recentSessions.length === 0 &&

No sessions yet.

} - {recentSessions.map((s) => ( - - {s.name ?? `Session #${s.id}`} - {new Date(s.startedAt).toLocaleDateString()} + {recentSessions.map((s, i) => ( + + {i > 0 &&
} +
+ {s.name ?? `Session #${s.id}`} + + {new Date(s.startedAt).toLocaleDateString()} + +
))} diff --git a/app/(app)/recordings/[id]/page.tsx b/app/(app)/recordings/[id]/page.tsx index fdbc1ee..46d9e9e 100644 --- a/app/(app)/recordings/[id]/page.tsx +++ b/app/(app)/recordings/[id]/page.tsx @@ -23,7 +23,7 @@ export default async function RecordingDetailPage({ params }: { params: Promise<
-

{recording.name}

+

{recording.name}

{recording.description &&

{recording.description}

}
- +
+ {device.actuators.length === 0 ? (

No controllable actuators reported.

) : ( diff --git a/components/devices/DevicesTable.tsx b/components/devices/DevicesTable.tsx index 0764bff..1a82ba0 100644 --- a/components/devices/DevicesTable.tsx +++ b/components/devices/DevicesTable.tsx @@ -66,8 +66,8 @@ export function DevicesTable({ devices }: { devices: DeviceRow[] }) { - {d.bleName} - + {d.bleName} + {d.lastConnectedAt ? new Date(d.lastConnectedAt).toLocaleString() : "Never"} diff --git a/components/layout/BrandMark.tsx b/components/layout/BrandMark.tsx new file mode 100644 index 0000000..41b4c31 --- /dev/null +++ b/components/layout/BrandMark.tsx @@ -0,0 +1,42 @@ +import { cn } from "@/lib/utils"; + +interface BrandMarkProps { + size?: number; + className?: string; +} + +/** Inline twin of app/icon.svg (the favicon) - kept as markup, not an , so it can be sized/animated with CSS. */ +export function BrandMark({ size = 28, className }: BrandMarkProps) { + return ( + + ); +} diff --git a/components/layout/ConnectionStatus.tsx b/components/layout/ConnectionStatus.tsx index b1f9702..545cf5d 100644 --- a/components/layout/ConnectionStatus.tsx +++ b/components/layout/ConnectionStatus.tsx @@ -8,15 +8,10 @@ export function ConnectionStatus() { const deviceCount = useButtplugStore((s) => Object.keys(s.devices).length); return ( -
- - - {connected ? `${deviceCount} device${deviceCount === 1 ? "" : "s"} connected` : "Not connected"} +
+ + + {connected ? `${deviceCount} device${deviceCount === 1 ? "" : "s"} connected` : "not connected"}
); diff --git a/components/layout/NavBar.tsx b/components/layout/NavBar.tsx index 49e3b3d..431d9d2 100644 --- a/components/layout/NavBar.tsx +++ b/components/layout/NavBar.tsx @@ -7,6 +7,7 @@ import { Menu, Moon, Sun, LogOut } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; import { ConnectionStatus } from "./ConnectionStatus"; +import { BrandMark } from "./BrandMark"; import { cn } from "@/lib/utils"; const LINKS = [ @@ -28,10 +29,10 @@ function NavLinks({ onNavigate }: { onNavigate?: () => void }) { href={link.href} onClick={onNavigate} className={cn( - "rounded-lg px-3 py-1.5 text-sm font-medium transition-colors", + "border-b-2 px-2.5 py-1.5 text-xs font-medium tracking-wide uppercase transition-colors", pathname === link.href - ? "bg-primary/15 text-primary" - : "text-muted-foreground hover:bg-muted hover:text-foreground", + ? "border-primary text-foreground" + : "border-transparent text-muted-foreground hover:text-foreground", )} > {link.label} @@ -54,8 +55,9 @@ export function NavBar() { return (
- - sexy + + + SEXY
@@ -47,8 +47,8 @@ export function RecordingLibraryStats({ stats }: { stats: RecordingLibrarySummar {stats.list.map((r) => ( {r.name} - {r.playCount} - + {r.playCount} + {r.lastPlayedAt ? new Date(r.lastPlayedAt).toLocaleString() : "Never"} diff --git a/components/stats/SessionsSummaryCards.tsx b/components/stats/SessionsSummaryCards.tsx index 38da728..4573254 100644 --- a/components/stats/SessionsSummaryCards.tsx +++ b/components/stats/SessionsSummaryCards.tsx @@ -23,19 +23,19 @@ export function SessionsSummaryCards({ summary }: { summary: SessionsSummary }) Completed sessions - {summary.count} + {summary.count} Total time - {formatHours(summary.totalDurationMs)} + {formatHours(summary.totalDurationMs)} Avg session length - + {Math.round(summary.avgDurationMs / 60_000)}m diff --git a/proxy.ts b/proxy.ts index 2540e04..e33c208 100644 --- a/proxy.ts +++ b/proxy.ts @@ -22,5 +22,5 @@ export default async function proxy(req: NextRequest) { } export const config = { - matcher: ["/((?!_next/static|_next/image|favicon.ico|login|api/auth/login|api/health).*)"], + matcher: ["/((?!_next/static|_next/image|favicon.ico|icon.svg|login|api/auth/login|api/health).*)"], };