From 3c1099efdf2703803fd0328a4d52636bc03932ec Mon Sep 17 00:00:00 2001 From: Matiss Jurevics Date: Thu, 16 Apr 2026 14:45:00 +0100 Subject: [PATCH] feat(web): refresh simulator dashboard UI --- WebApp/src/app.css | 37 +--- WebApp/src/lib/app/controller.js | 33 --- .../src/lib/components/ui/alert/alert.svelte | 1 + .../lib/components/ui/button/button.svelte | 1 + WebApp/src/lib/components/ui/card/card.svelte | 38 +++- .../ui/dialog/dialog-content.svelte | 10 +- .../src/lib/components/ui/skeleton/index.ts | 7 + .../components/ui/skeleton/skeleton.svelte | 16 ++ WebApp/src/lib/components/ui/switch/index.ts | 7 + .../lib/components/ui/switch/switch.svelte | 25 +++ WebApp/src/lib/sim/ui/AppChrome.svelte | 123 +++++------- WebApp/src/routes/+page.svelte | 15 +- WebApp/src/routes/activity/+page.svelte | 30 ++- WebApp/src/routes/camera/+page.svelte | 190 +++++++++--------- WebApp/src/routes/client/+page.svelte | 177 ++++++++-------- WebApp/src/routes/onboarding/+page.svelte | 16 +- WebApp/src/routes/settings/+page.svelte | 88 ++++---- 17 files changed, 407 insertions(+), 407 deletions(-) create mode 100644 WebApp/src/lib/components/ui/skeleton/index.ts create mode 100644 WebApp/src/lib/components/ui/skeleton/skeleton.svelte create mode 100644 WebApp/src/lib/components/ui/switch/index.ts create mode 100644 WebApp/src/lib/components/ui/switch/switch.svelte diff --git a/WebApp/src/app.css b/WebApp/src/app.css index 65201fc..60ddff0 100644 --- a/WebApp/src/app.css +++ b/WebApp/src/app.css @@ -38,7 +38,6 @@ --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); } - .dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); @@ -109,6 +108,13 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + + /* Custom brand colors and glassmorphism tokens */ + --color-premium: #2563eb; + --color-premium-foreground: #ffffff; + --color-glass-background: rgb(25 25 30 / 60%); + --color-glass-border: rgb(255 255 255 / 8%); + --color-glass-panel: rgb(15 15 20 / 70%); } @layer base { @@ -123,7 +129,7 @@ } } -/* Preserve existing simulator utility styles alongside shadcn tokens. */ +/* Scrollbar customizations */ ::-webkit-scrollbar { width: 6px; height: 6px; @@ -142,29 +148,6 @@ background: rgb(255 255 255 / 20%); } -.glass-panel { - background: rgb(15 15 20 / 70%); - backdrop-filter: blur(16px); - border: 1px solid rgb(255 255 255 / 5%); -} - -.glass-card { - background: rgb(25 25 30 / 60%); - border: 1px solid rgb(255 255 255 / 8%); -} - -.btn-premium { - background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); - border: none; - color: white; - transition: all 0.2s ease; -} - -.btn-premium:hover { - transform: translateY(-1px); - box-shadow: 0 4px 12px rgb(37 99 235 / 30%); -} - .status-dot { height: 8px; width: 8px; @@ -193,7 +176,3 @@ opacity: 1; } } - -.toast-enter { - animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; -} diff --git a/WebApp/src/lib/app/controller.js b/WebApp/src/lib/app/controller.js index d2c431d..44a0c2e 100644 --- a/WebApp/src/lib/app/controller.js +++ b/WebApp/src/lib/app/controller.js @@ -805,39 +805,6 @@ const stopPolling = () => { } }; -const pollClientData = async () => { - const { device } = getAppState(); - if (!device || device.role !== 'client') return; - - const [recs, links, deviceList, notifications] = await Promise.all([ - api.ops.listRecordings().catch(() => ({ recordings: [] })), - api.devices.listLinks().catch(() => ({ links: [] })), - api.devices.list().catch(() => ({ devices: [] })), - api.ops.listNotifications().catch(() => ({ notifications: [] })) - ]); - - const cameraById = new Map( - (deviceList.devices || []) - .filter((entry) => entry.role === 'camera') - .map((entry) => [entry.id, entry]) - ); - - const linkedCameras = (links.links || []).map((link) => { - const camera = cameraById.get(link.cameraDeviceId); - return { - ...link, - cameraName: camera?.name ?? null, - cameraStatus: camera?.status ?? 'offline' - }; - }); - - setAppState({ - recordings: recs.recordings || [], - linkedCameras - }); - syncMotionNotificationsFromDeliveries(notifications.notifications); -}; - const startPolling = () => { stopPolling(); void pollClientData(); diff --git a/WebApp/src/lib/components/ui/alert/alert.svelte b/WebApp/src/lib/components/ui/alert/alert.svelte index abf7487..d7bc03c 100644 --- a/WebApp/src/lib/components/ui/alert/alert.svelte +++ b/WebApp/src/lib/components/ui/alert/alert.svelte @@ -7,6 +7,7 @@ variant: { default: "bg-card text-card-foreground", destructive: "text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", + glass: "bg-glass-background border-glass-border backdrop-blur-xl shadow-lg", }, }, defaultVariants: { diff --git a/WebApp/src/lib/components/ui/button/button.svelte b/WebApp/src/lib/components/ui/button/button.svelte index 89cedcb..1ae7f3a 100644 --- a/WebApp/src/lib/components/ui/button/button.svelte +++ b/WebApp/src/lib/components/ui/button/button.svelte @@ -13,6 +13,7 @@ ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", link: "text-primary underline-offset-4 hover:underline", + premium: "bg-premium text-premium-foreground shadow-lg shadow-premium/20 transition-all hover:-translate-y-0.5 hover:shadow-xl hover:shadow-premium/30 active:translate-y-0", }, size: { default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", diff --git a/WebApp/src/lib/components/ui/card/card.svelte b/WebApp/src/lib/components/ui/card/card.svelte index 2b4f81a..de42e90 100644 --- a/WebApp/src/lib/components/ui/card/card.svelte +++ b/WebApp/src/lib/components/ui/card/card.svelte @@ -1,21 +1,51 @@ - + +
img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)} + class={cn(cardVariants({ variant, size }), className)} {...restProps} > {@render children?.()} diff --git a/WebApp/src/lib/components/ui/dialog/dialog-content.svelte b/WebApp/src/lib/components/ui/dialog/dialog-content.svelte index c0551ee..f035598 100644 --- a/WebApp/src/lib/components/ui/dialog/dialog-content.svelte +++ b/WebApp/src/lib/components/ui/dialog/dialog-content.svelte @@ -14,12 +14,19 @@ portalProps, children, showCloseButton = true, + variant = "default", ...restProps }: WithoutChildrenOrChild & { portalProps?: WithoutChildrenOrChild>; children: Snippet; showCloseButton?: boolean; + variant?: "default" | "glass"; } = $props(); + + const variantClasses = { + default: "bg-popover text-popover-foreground ring-1 ring-foreground/10", + glass: "bg-glass-background border-glass-border border backdrop-blur-xl shadow-2xl text-foreground" + }; @@ -28,7 +35,8 @@ bind:ref data-slot="dialog-content" class={cn( - "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none", + "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none", + variantClasses[variant], className )} {...restProps} diff --git a/WebApp/src/lib/components/ui/skeleton/index.ts b/WebApp/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..186db21 --- /dev/null +++ b/WebApp/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/WebApp/src/lib/components/ui/skeleton/skeleton.svelte b/WebApp/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..187e4ff --- /dev/null +++ b/WebApp/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,16 @@ + + +
diff --git a/WebApp/src/lib/components/ui/switch/index.ts b/WebApp/src/lib/components/ui/switch/index.ts new file mode 100644 index 0000000..f5533db --- /dev/null +++ b/WebApp/src/lib/components/ui/switch/index.ts @@ -0,0 +1,7 @@ +import Root from "./switch.svelte"; + +export { + Root, + // + Root as Switch, +}; diff --git a/WebApp/src/lib/components/ui/switch/switch.svelte b/WebApp/src/lib/components/ui/switch/switch.svelte new file mode 100644 index 0000000..874dbe8 --- /dev/null +++ b/WebApp/src/lib/components/ui/switch/switch.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/WebApp/src/lib/sim/ui/AppChrome.svelte b/WebApp/src/lib/sim/ui/AppChrome.svelte index c2589ad..20d0e86 100644 --- a/WebApp/src/lib/sim/ui/AppChrome.svelte +++ b/WebApp/src/lib/sim/ui/AppChrome.svelte @@ -9,6 +9,18 @@ import { Button } from '$lib/components/ui/button/index.js'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '$lib/components/ui/dialog/index.js'; import { Separator } from '$lib/components/ui/separator/index.js'; + import { Skeleton } from '$lib/components/ui/skeleton/index.js'; + import { + LayoutDashboard, + Bell, + Settings, + X, + Download, + Info, + AlertTriangle, + RefreshCw, + CheckCircle2 + } from '@lucide/svelte'; let { children, pageKey } = $props<{ children: () => unknown; pageKey: string }>(); let recordingDialogOpen = $state(false); @@ -51,11 +63,22 @@
{#each $appState.toasts as toast (toast.id)} - {toastTitle(toast.type)} - {toast.message} +
+ {#if toast.type === 'error'} + + {:else if toast.type === 'success'} + + {:else} + + {/if} +
+ {toastTitle(toast.type)} + {toast.message} +
+
@@ -73,7 +94,8 @@ {/each} {#if $pwaState.installAvailable} - + + Install App Add PhoneCam to your home screen for a more native dashboard experience. @@ -93,7 +115,8 @@ {/if} {#if $pwaState.showIosInstallHint} - + + Install on iPhone In Safari, tap the Share button, then choose “Add to Home Screen” to install PhoneCam. @@ -108,6 +131,7 @@ {#if $pwaState.updateAvailable} + Update Ready A newer version of PhoneCam is ready. Reload to use the latest app shell. @@ -121,7 +145,8 @@ {/if} {#if $pwaState.offlineReady} - + + Offline Shell Ready The app shell is now cached for quicker relaunches, but live camera features still need connectivity. @@ -142,20 +167,20 @@ {#if showSidebarSkeleton()}
-
+
-
+
{:else} @@ -187,21 +212,7 @@ class="nav-btn h-12 w-full justify-center gap-3 rounded-xl border border-transparent text-gray-400 hover:text-white lg:justify-start" onclick={() => appController.navigate('home')} > - - - + @@ -210,21 +221,7 @@ class="nav-btn relative h-12 w-full justify-center gap-3 rounded-xl border border-transparent text-gray-400 hover:text-white lg:justify-start" onclick={() => appController.navigate('activity')} > - - - + {#if $unreadNotificationsCount > 0} appController.navigate('settings')} > - - - - + @@ -276,8 +253,11 @@
{#if $pwaState.isOffline}
- - {offlineTitle()} + +
+ + {offlineTitle()} +
{offlineDescription()} @@ -294,7 +274,8 @@
@@ -313,9 +294,7 @@ title="Close recording modal" onclick={() => appController.closeRecordingModal()} > - - - +
diff --git a/WebApp/src/routes/+page.svelte b/WebApp/src/routes/+page.svelte index a475af1..eae7b4e 100644 --- a/WebApp/src/routes/+page.svelte +++ b/WebApp/src/routes/+page.svelte @@ -8,23 +8,17 @@ import { Input } from '$lib/components/ui/input/index.js'; import { Label } from '$lib/components/ui/label/index.js'; import { Separator } from '$lib/components/ui/separator/index.js'; + import { ShieldCheck } from '@lucide/svelte';
- +
- - - +
PhoneCam Web @@ -71,7 +65,8 @@
- + Motion Alerts @@ -31,17 +33,10 @@
{#if $appState.motionNotifications.length === 0} - - - - - All quiet - + + + All quiet + No notifications yet. @@ -50,11 +45,14 @@ {/each} {/if} diff --git a/WebApp/src/routes/camera/+page.svelte b/WebApp/src/routes/camera/+page.svelte index 245164a..0e97469 100644 --- a/WebApp/src/routes/camera/+page.svelte +++ b/WebApp/src/routes/camera/+page.svelte @@ -10,6 +10,17 @@ import { Label } from '$lib/components/ui/label/index.js'; import { ScrollArea } from '$lib/components/ui/scroll-area/index.js'; import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/components/ui/select/index.js'; + import { Switch } from '$lib/components/ui/switch/index.js'; + import { + VideoOff, + RefreshCw, + Zap, + Square, + Activity, + Settings2, + Terminal, + Camera + } from '@lucide/svelte'; let cameraVideoElement: HTMLVideoElement | null = null; @@ -40,7 +51,7 @@
- +
- - REC + + REC
@@ -63,27 +74,20 @@ id="cameraOfflineOverlay" class="absolute inset-0 bg-[#0a0a0c]/80 backdrop-blur-sm z-10 flex flex-col items-center justify-center gap-4 {$appState.socketConnected ? 'hidden' : ''}" > - - - - - Camera Offline - + + + Camera Offline + Reconnect this device to resume live monitoring. -
+
@@ -91,9 +95,10 @@
- - - Logs + + + + Activity Logs Awaiting connection...
{:else} {#each $appState.activityLog as log (log.id)} -
[{new Date(log.createdAt).toLocaleTimeString()}] {log.type}: {log.message}
+
+ [{new Date(log.createdAt).toLocaleTimeString()}] + {log.type}: + {log.message} +
{/each} {/if} - - - Actions / Options + + + + Control Plane
-
-
+
-

Automatic Detection

-

- {$appState.motionDetection.enabled ? 'Armed for automatic motion events.' : 'Paused until manually armed.'} +

Automatic Detection

+

+ {$appState.motionDetection.enabled ? 'Armed' : 'Disarmed'}

-

- Designed for a plugged-in browser kept in the foreground. +

+ Foreground browser monitoring.