From c5fc43a17563a63456673b13d11a684e7e827b52 Mon Sep 17 00:00:00 2001 From: Mahakisore7 Date: Sat, 19 Sep 2026 19:58:35 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20visual=20polish=20=E2=80=94=20dark=20mo?= =?UTF-8?q?de,=20toast=20notifications,=20loading=20skeletons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the three items closing out the "complete product" roadmap's final phase: - Dark mode: Tailwind's class-based dark mode (tailwind.config.js), ThemeContext (persists to localStorage, falls back to the OS preference, degrades gracefully — same try/catch pattern as tokenStore.ts — if storage is blocked), and a ThemeToggle in the top bar. Every page and component gets dark: variants; nothing was left light-only. - Toast notifications: a dependency-free ToastContext/ToastContainer (no library pulled in for a handful of confirmation messages, same call this codebase already made for icons.tsx) wired into checkout completion, staff invites, product create/delete, and conflict resolution — success and failure both surface a toast, stacked, auto-dismissing after 4s or on click. - Loading skeletons: Skeleton.tsx's layout-shaped placeholders replace spinner/text loading states on CounterPage's item grid, DashboardPage's summary tiles, and AuditLogView's history list, so the page doesn't visually jump once real content arrives. Tests: 9 new (ThemeContext, ToastContext/ToastContainer), 1 new App.test.tsx case for the theme toggle; HeroPage.test.tsx and StaffPage.test.tsx updated to wrap the providers their pages now depend on. Full suite verified green (66/66 web), typecheck and lint clean, production build succeeds. Co-Authored-By: Claude Sonnet 5 --- apps/web/src/App.tsx | 28 ++++-- apps/web/src/components/AttributionBadge.tsx | 10 +- apps/web/src/components/AuditLogView.tsx | 33 ++++--- apps/web/src/components/AuthPanel.tsx | 42 +++++---- apps/web/src/components/BarcodeScanButton.tsx | 10 +- apps/web/src/components/CartDrawer.tsx | 17 +++- .../src/components/ConflictReviewPanel.tsx | 22 ++--- apps/web/src/components/CounterPicker.tsx | 16 ++-- apps/web/src/components/EditableField.tsx | 4 +- apps/web/src/components/ItemCard.tsx | 32 ++++--- apps/web/src/components/ProductForm.tsx | 17 ++-- apps/web/src/components/QueueDrawer.tsx | 12 +-- apps/web/src/components/Skeleton.tsx | 43 +++++++++ apps/web/src/components/ThemeToggle.tsx | 20 ++++ apps/web/src/components/ToastContainer.tsx | 49 ++++++++++ .../src/components/VectorClockExplainer.tsx | 9 +- apps/web/src/components/icons.tsx | 34 +++++++ apps/web/src/index.css | 13 ++- apps/web/src/pages/CheckoutPage.tsx | 23 +++-- apps/web/src/pages/CounterPage.tsx | 68 +++++++------- apps/web/src/pages/DashboardPage.tsx | 50 +++++----- apps/web/src/pages/HeroPage.tsx | 54 ++++++----- apps/web/src/pages/ProductsPage.tsx | 54 +++++++---- apps/web/src/pages/StaffPage.tsx | 29 +++--- apps/web/src/state/ThemeContext.tsx | 57 ++++++++++++ apps/web/src/state/ToastContext.tsx | 66 ++++++++++++++ apps/web/tailwind.config.js | 1 + apps/web/test/App.test.tsx | 14 +++ apps/web/test/pages/HeroPage.test.tsx | 22 +++-- apps/web/test/pages/StaffPage.test.tsx | 15 ++- apps/web/test/state/ThemeContext.test.tsx | 78 ++++++++++++++++ apps/web/test/state/ToastContext.test.tsx | 91 +++++++++++++++++++ 32 files changed, 805 insertions(+), 228 deletions(-) create mode 100644 apps/web/src/components/Skeleton.tsx create mode 100644 apps/web/src/components/ThemeToggle.tsx create mode 100644 apps/web/src/components/ToastContainer.tsx create mode 100644 apps/web/src/state/ThemeContext.tsx create mode 100644 apps/web/src/state/ToastContext.tsx create mode 100644 apps/web/test/state/ThemeContext.test.tsx create mode 100644 apps/web/test/state/ToastContext.test.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index f260320..9edae7b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; import { AuthProvider, useAuth, type AuthUser } from "./state/AuthContext"; import { ShopProvider } from "./state/ShopContext"; +import { ThemeProvider } from "./state/ThemeContext"; +import { ToastProvider } from "./state/ToastContext"; import { CounterPage } from "./pages/CounterPage"; import { HeroPage } from "./pages/HeroPage"; import { ProductsPage } from "./pages/ProductsPage"; @@ -8,6 +10,8 @@ import { CheckoutPage } from "./pages/CheckoutPage"; import { StaffPage } from "./pages/StaffPage"; import { DashboardPage } from "./pages/DashboardPage"; import { CounterPicker } from "./components/CounterPicker"; +import { ThemeToggle } from "./components/ThemeToggle"; +import { ToastContainer } from "./components/ToastContainer"; function useQueryParam(name: string, fallback: string): string { return new URLSearchParams(window.location.search).get(name) ?? fallback; @@ -65,7 +69,11 @@ function AuthedApp({ shopId, role }: AuthedAppProps) { {link.label} @@ -84,9 +92,10 @@ function TopBar({ user, onSignOut }: { user: AuthUser; onSignOut: () => void }) return (
{user.email} - +
); } @@ -95,7 +104,9 @@ function AppShell() { const { status, user, signOut } = useAuth(); if (status === "loading") { - return
Loading…
; + return ( +
Loading…
+ ); } if (status === "signed_out" || !user?.shopId) { @@ -112,8 +123,13 @@ function AppShell() { export function App() { return ( - - - + + + + + + + + ); } diff --git a/apps/web/src/components/AttributionBadge.tsx b/apps/web/src/components/AttributionBadge.tsx index 6046304..9b00d78 100644 --- a/apps/web/src/components/AttributionBadge.tsx +++ b/apps/web/src/components/AttributionBadge.tsx @@ -1,9 +1,9 @@ const COLORS = [ - "bg-sky-100 text-sky-800", - "bg-emerald-100 text-emerald-800", - "bg-amber-100 text-amber-800", - "bg-violet-100 text-violet-800", - "bg-rose-100 text-rose-800", + "bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300", + "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300", + "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300", + "bg-violet-100 text-violet-800 dark:bg-violet-950 dark:text-violet-300", + "bg-rose-100 text-rose-800 dark:bg-rose-950 dark:text-rose-300", ]; /** Deterministic so the same counter always renders the same color, without a shared color registry. */ diff --git a/apps/web/src/components/AuditLogView.tsx b/apps/web/src/components/AuditLogView.tsx index ab9e0a8..33ffff3 100644 --- a/apps/web/src/components/AuditLogView.tsx +++ b/apps/web/src/components/AuditLogView.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import type { AuditHistoryEntry } from "@stocksync/core"; import { getAudit } from "../api/client"; import { VectorClockExplainer } from "./VectorClockExplainer"; +import { Skeleton } from "./Skeleton"; import { AlertTriangleIcon } from "./icons"; export interface AuditLogViewProps { @@ -38,32 +39,40 @@ export function AuditLogView({ itemId, shopId }: AuditLogViewProps) { if (error) return ( -

+

Couldn't load history for {itemId}.

); if (!history) return ( -

- - Loading history… -

+
+ {[0, 1, 2].map((i) => ( +
+ + +
+ ))} +
); - if (history.length === 0) return

No history yet for {itemId}.

; + if (history.length === 0) return

No history yet for {itemId}.

; return ( -
    +
      {history.map((entry, index) => (
    1. - {entry.action} - {entry.counter_id} - {new Date(entry.timestamp).toLocaleTimeString()} + {entry.action} + + {entry.counter_id} + + {new Date(entry.timestamp).toLocaleTimeString()}
      -

      {formatDetails(entry.details)}

      +

      {formatDetails(entry.details)}

      - Why? + + Why? +
    2. diff --git a/apps/web/src/components/AuthPanel.tsx b/apps/web/src/components/AuthPanel.tsx index 80848d0..d72f186 100644 --- a/apps/web/src/components/AuthPanel.tsx +++ b/apps/web/src/components/AuthPanel.tsx @@ -4,9 +4,11 @@ import { ArrowRightIcon } from "./icons"; type Mode = "login" | "signup" | "confirm" | "new_password"; -const INPUT_CLASS = "w-full rounded-md border border-slate-300 px-3 py-2 text-sm"; +const INPUT_CLASS = + "w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:placeholder:text-slate-500"; const SUBMIT_CLASS = "flex w-full items-center justify-center gap-1.5 rounded-md bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-indigo-700 disabled:opacity-50"; +const LINK_CLASS = "mt-4 text-sm text-slate-500 underline decoration-dotted hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"; /** * The app's real front door for identity — replaces the old shop_id/ @@ -89,12 +91,16 @@ export function AuthPanel() { } return ( -
      +
      {mode === "login" && ( <> -

      Sign in

      -

      Enter your shop's account to get to your counter.

      - {info &&

      {info}

      } +

      Sign in

      +

      Enter your shop's account to get to your counter.

      + {info && ( +

      + {info} +

      + )}
      - {error &&

      {error}

      } + {error &&

      {error}

      } @@ -125,7 +131,7 @@ export function AuthPanel() { setMode("signup"); setError(""); }} - className="mt-4 text-sm text-slate-500 underline decoration-dotted hover:text-slate-700" + className={LINK_CLASS} > New shop? Create an account instead @@ -134,8 +140,8 @@ export function AuthPanel() { {mode === "signup" && ( <> -

      Set up your shop

      -

      You'll be the owner — invite your staff once you're in.

      +

      Set up your shop

      +

      You'll be the owner — invite your staff once you're in.

      - {error &&

      {error}

      } + {error &&

      {error}

      } @@ -174,7 +180,7 @@ export function AuthPanel() { setMode("login"); setError(""); }} - className="mt-4 text-sm text-slate-500 underline decoration-dotted hover:text-slate-700" + className={LINK_CLASS} > Already have an account? Sign in @@ -183,8 +189,8 @@ export function AuthPanel() { {mode === "confirm" && ( <> -

      Check your email

      -

      Enter the confirmation code we sent to {email}.

      +

      Check your email

      +

      Enter the confirmation code we sent to {email}.

      - {error &&

      {error}

      } + {error &&

      {error}

      } @@ -202,7 +208,7 @@ export function AuthPanel() { @@ -211,8 +217,8 @@ export function AuthPanel() { {mode === "new_password" && ( <> -

      Choose a password

      -

      First sign-in on an invited account — set your own password to continue.

      +

      Choose a password

      +

      First sign-in on an invited account — set your own password to continue.

      - {error &&

      {error}

      } + {error &&

      {error}

      } diff --git a/apps/web/src/components/BarcodeScanButton.tsx b/apps/web/src/components/BarcodeScanButton.tsx index c6822f7..987c1d3 100644 --- a/apps/web/src/components/BarcodeScanButton.tsx +++ b/apps/web/src/components/BarcodeScanButton.tsx @@ -66,12 +66,12 @@ export function BarcodeScanButton({ items, onResolved }: BarcodeScanButtonProps) if (state === "scanning") { return ( -
      +