Repository navigation
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 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.
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).
| 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_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.
@serwist/next compiles app/sw.ts to public/sw.js, disabled in development.
Two rules matter:
- Same-origin
/api/requests areNetworkOnly. 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.htmland the logo are precached explicitly, because theforce-dynamiclayout 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
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.
AGPL-3.0 · reportmate.app