Styling and theming
The app is styled with Chakra UI v3 — a token-driven system, not the v2
extendTheme API. Everything themed flows from one file:
frontend/src/ui/theme.ts.
The Chakra v3 system
theme.ts builds the system with createSystem(defaultConfig, defineConfig({...}))
and main.tsx mounts it via <ChakraProvider value={system}>. What it defines:
- Brand palette — a 10-step navy ramp with the MotorPH corporate blue
#1A3470atbrand.800(headers, primary buttons, active nav) and#122550atbrand.900; accent tokensaccent.red/yellow/green. Full brand rationale and logo usage live in ../reference/logo.md. - Fonts —
Interfor body and headings,Outfit Variableas thedisplayfont (marketing headlines, theeyebrowtext style). Both are self-hosted via@fontsourceimports inmain.tsx— no external font CDN. - Semantic tokens with
_light/_darkconditionals —fg,fg.muted,border, andfocusRingresolve per color mode, so components using them adapt to dark mode with no extra code. - Recipes — a
buttonrecipe restyling the four variants (solid/subtle/outline/ghost) onto the brand ramp, and aninputrecipe (radiusmd). This is why a bare<Button>looks on-brand without props. - Custom radii scale,
card/cardHovershadows plus a deeperfloatingshadow for marketing surfaces (see the comment in the file), and theeyebrowtext style.
A minimal global reset plus a WCAG touch-target rule (44×44 px minimum on
coarse pointers) lives in index.css.
Dark mode
Color mode is handled by next-themes:
ui/components/color-mode.tsx
wraps ThemeProvider with attribute="class" and defaultTheme="light", and
color-mode-toggle.tsx
flips it with setTheme. Components adapt three ways, in order of preference:
- use semantic tokens (
fg,fg.muted,border,bg) that already resolve per mode; _darkstyle conditionals, e.g.color={{ base: 'brand.800', _dark: 'brand.100' }}(seeapp-layout.tsx);- imperative checks via
useTheme()where whole style clusters diverge (const isDark = theme === 'dark', same file).
Chakra v3 gotchas as practiced here
Chakra v3 components are compositional (explicit Root/Trigger/
Positioner/Content parts). Two hard-won rules from this codebase:
- Menus and tooltips must be composed with
Portal+Positioner, or they render clipped. The canonical example is the grid row-actions kebab inServerDataGrid.tsx(RowActionsCell):MenuRoot → MenuTrigger → Portal → MenuPositioner → MenuContent. The trigger sits inside AG Grid's overflow-clipped cell containers; without thePortal, the popup is cut off at the row edge. The same composition is used everywhere a popup lives near clipping/stacking contexts — the profile menu, pagination page-size menu, grid View/Table menus. - Keep the same
Tooltip.Roottree mounted; don't swap it in and out.NavTooltipinapp-layout.tsxalways rendersTooltip.Rootand disables it with thedisabledprop when the sidebar is expanded. As its comment notes, conditionally rendering a bare fragment vs. the tooltip would remount every nav item on each sidebar toggle — visible flicker and lost state.
AG Grid theming
Grids don't use Chakra styling; they use AG Grid's theming API, parameterized to
match the brand:
ui/ag-grid/theme.ts exports
motorphGridTheme = themeQuartz.withParams({...}) with accentColor: '#1A3470',
header background #E9EEF8 (= brand.50), Inter at 14 px, and 44 px
row/header heights (the density system overrides heights per page —
ag-grid.md). The grid PDF exporter's header fill
([26, 52, 112] in pdf.ts) is the
same brand navy, so exports visually match the grids.
Theming the ALTCHA widget
The login CAPTCHA is a web component that themes itself via CSS custom
properties.
ui/components/altcha-captcha.tsx
maps those onto Chakra's CSS variables —
'--altcha-color-base': 'var(--chakra-colors-bg-panel)',
'--altcha-color-base-content': 'var(--chakra-colors-fg)',
'--altcha-border-color': 'var(--chakra-colors-border)',
'--altcha-color-primary': 'var(--chakra-colors-brand-600)',
— so, as the inline comment puts it, the widget stays correct in both color modes for free: the Chakra variables it points at are the semantic tokens that already flip with the theme.