UI Conventions (apps/web)
i18n — every user-facing string, both locales
- Framework:
next-intl, localeszh(default) anden. Message files:apps/web/messages/{zh,en}.json— a key added to one file MUST be added to the other in the same commit. - Components use
useTranslations('namespace'). Never hardcode user-facing strings — including chart tooltips,titleattributes, and aria-labels (historically the most-missed spots). - Utility functions that produce display text (
formatDurationstyle) must not embed English words; return structured data and translate in the component.
Dark mode — check on every visual change
This is the project's highest-frequency rework source. Rules:
- Never a lone light class. Every
bg-*-50/100,text-*-500/600,border-*-200needs adark:counterpart. Grep your diff forbg-white|bg-.*-50|text-gray-before committing. - Prefer theme tokens over palette classes where one exists:
bg-background,bg-popover,text-muted-foreground,border-border(defined inapp/globals.cssfor both themes). - Inline SVG / chart colors can't use Tailwind — define both palettes and switch on
resolvedTheme:
const { resolvedTheme } = useTheme(); // next-themes
const palette = resolvedTheme === "dark" ? DARK_COLORS : LIGHT_COLORS;
Precedents: components/features/countries/world-traffic-map.tsx (MAP_THEME), components/features/rules/rule-chain-flow.tsx. Recharts axis ticks should use fill: "currentColor" or CSS variables, not hex grays.
- Badge/status color sets: copy an existing complete set (e.g. the health badge classes in
app/[locale]/dashboard/components/header/index.tsx) rather than writing light-only classes.
Three states, always
Every data view needs loading (skeleton), empty, and error states. Error must be visible and actionable (retry button) — return null on error is a known anti-pattern here (see the chain-flow retry card in rule-chain-flow.tsx for the reference implementation). Route-level failures are caught by app/[locale]/error.tsx / app/global-error.tsx; heavy visualizations should still fail locally, not blank the page.
Data layer
- Server state via React Query; query keys are centralized in
lib/stats-query-keys.ts— never inline ad-hoc keys. - The stats WebSocket (
lib/websocket.ts) tags pushes withbackendIdand the hook drops mismatched messages — preserve this when touching the message path (prevents cross-backend cache bleed on backend switch). - Custom
memocomparators must compare every prop the component renders from (a missed prop froze the Active Policy whitelist once — include new props in the comparator when you add them).
Components
- shadcn/ui (
new-york) + Tailwind v4 + lucide-react. Base primitives incomponents/ui/— reuse before creating; use the RadixDialogfor modals (no hand-rolled fixed overlays — they lose Esc/focus-trap behavior). - Aesthetic bar: modern SaaS quality; avoid flat "admin panel" layouts. Search and other high-frequency entries should be prominent (icon inside input, clear affordances).