Skip to content

Theming and UI

Rod Christiansen edited this page Oct 8, 2026 · 3 revisions

Theming and UI

Conventions you need before changing anything visual, plus the two behaviours that are easy to break by accident: theme flash and URL state.

Tailwind and dark mode

Tailwind CSS with darkMode: 'class', scanning ./app/**/*.{ts,tsx} and ./src/**/*.{ts,tsx}. The theme extends a primary blue scale and three named shadows (soft, medium, large); everything else is stock Tailwind. Dark styling is written inline as dark: variants, not as a second token set.

Theme state is light, dark or system, held by ThemeProvider (src/components/theme-provider.tsx) and persisted in localStorage under reportmate-theme. system follows prefers-color-scheme live via a matchMedia listener.

Because the provider is a client component, an inline script in the root layout applies the stored class to <html> before React hydrates. That script is what prevents a light flash on a dark-themed load — if you move theme handling, keep an equivalent pre-hydration step. EdgeThemeFix exists alongside it and, in Microsoft Edge only, re-applies the theme class to force a style recalculation.

Layout and providers

app/layout.tsx wraps every page in this order: AuthProvider (NextAuth session) → SWRProvider → SettingsProvider → PlatformFilterProvider → ThemeProvider → DebugModeProvider → DemoModeProvider, which renders EdgeThemeFix and KioskBehaviour beside ErrorBoundary → ToolbarWrapper. In production AutoAuth sits between the error boundary and the toolbar; in development and demo mode it is omitted.

The root layout is force-dynamic with revalidate = 0, and deliberately fetches no data, so no page waits on the device list before rendering.

PlatformFilterProvider supplies the global Windows/macOS filter, normalizing the platform strings the agents report. DebugModeProvider toggles verbose in-page diagnostics from localStorage (reportmate-debug-mode).

Component layout

Directory Contents
src/components/tabs/ The per-module panes on the device detail page
src/components/widgets/ Dashboard tiles and per-module summary cards
src/components/tables/ Module-specific tables
src/components/ui/ Shared primitives: VirtualTable, FilterPills, CollapsibleSection, CopyButton, PlatformToggle, PlatformBadge, FitText, ScalableTabNavigation, DemoBanner
src/components/navigation/ AppToolbar, ToolbarWrapper, DevicePageNavigation
src/components/settings/ Inventory mapping, security rules, kiosk settings, onboarding wizard
src/components/skeleton/, src/components/lazy/ Loading placeholders and deferred imports

Long fleet tables render through VirtualTable (react-window); charts use recharts; icons come from lucide-react and @heroicons/react.

URL as state

URL_STATE_CONVENTIONS.md in the repo is the contract for viewable state: the URL is canonical, defaults are omitted from the query string, param names are short (q, apps, period, type, mode, sort, dir, tab, from), list values are comma-separated, and filter changes use router.replace so the back button leaves the report instead of undoing one filter at a time. Drill-downs carry the originating URL in ?from= so the back link returns to a populated report.

The applications report and its usage drill-down implement the convention, including the legacy ?application=, ?usage=, ?catalog=, ?room= and ?search= params. Other report pages keep most filter state in components (the System report reads ?search= and ?osVersion=, and the platform filter is in the URL), and there is no shared useUrlState hook.

Progressive web app

@serwist/next compiles app/sw.ts to public/sw.js, disabled in development. Two rules matter:

  • Same-origin /api/ requests are NetworkOnly. Fleet data is never served from cache, so an offline dashboard shows its own empty state rather than a stale snapshot presented as current.
  • offline.html and the logo are precached explicitly, because the force-dynamic layout means no page HTML is prerendered for Serwist to precache.

The service worker, the manifest and the icon files are all in the middleware's public-prefix list, so they load before a session exists.

Icons are generated from a single source image:

pnpm generate-icons

Kiosk presentation

A viewer session picks up the kiosk block from Settings: page zoom applied to <html>, a forced theme, and a return to the configured home page after idleMinutes without input. The return is a full navigation, which also renews the kiosk session through the middleware. Signed-in users never see any of it.

Clone this wiki locally