| name | project-mtp-ui-decisions | ||
|---|---|---|---|
| description | Consolidated frontend/UI/CSS/layout decisions for mtp-web — stack, monorepo, design-system, state, fidelity, conventions. Read before any frontend work. | ||
| metadata |
|
All locked frontend decisions in one place. Authoritative ADRs in parentheses; this is the fast index.
Stack (ADR-0007): React 19 + TypeScript only (no JS, strict) + Vite. Radix Primitives (headless @radix-ui/react-*, NOT @radix-ui/themes). CSS Modules + PostCSS + CSS custom properties — compile-time only, no runtime CSS-in-JS (no Emotion/styled-components), no utility framework (no Tailwind), no styled component kits. Native CSS Grid + container queries. TanStack Table + Virtual (headless). lucide-react. Zod + React Hook Form. i18next.
State (ADR-0019): server state = TanStack Query (sole owner, never copied into a store). Client/UI state = Zustand. Forms = React Hook Form + Zod. Shareable view state = URL params. No Redux/RTK. Rule: no server data in Zustand. SignalR pushes into the query cache. render=useQuery, action=useMutation+invalidateQueries; query keys [domain,module,resource,params].
Component authoring (ADR-0008 + [[project-mtp-component-authoring]]): native HTML default (<button>/<input>/<table>/<form>); Radix only for non-trivial behaviour (focus trap, portal, popover, type-ahead, dismiss). Radix wrapped in ui/<Name>/ via asChild + forwardRef, never raw in feature code. Inputs uncontrolled (RHF register). Tables headless — we render the DOM, TanStack owns logic. One .module.css per component; no global classes except tokens. JSX rename rules (className/htmlFor/etc.).
Layout primitives (ADR-0007): in-house Box, Stack, Cluster, Grid, SplitPane, Center, Container, Sidebar — ~30 lines CSS each. No grid/layout library.
Navigation depth (ADR + [[project-mtp-nav-depth]]): 4-level — module (rail) → group (submenu header) → page (item) → view (in-page tab). Collapsible groups, Ctrl-K palette, never 5-deep. Entity-360 hubs add a vtab rail (still level-4).
Mockup shell (ADR-0010): all mockups use the shared _shell.js + _mockup.css + _nav-manifest.js; never inline chrome. Zoho-style 2-stage dark-rail layout.
Design system (ADR-0020): the React UI is a faithful 1:1 PORT of the finalized pure-HTML shell + Zoho theme (_mockup.css/_shell.js) — not a redesign; a rendered React screen must be visually indistinguishable from its mockup. Two-tier tokens: primitive scale → semantic (components use semantic only); light-first, theme-ready (dark/tenant = token remap, no component edits). Build order: tokens → layout primitives → native+CSS components → Radix wrappers → DataGrid → AppShell → composites → pages. Reuse audit first (in-repo only): PORT from _mockup.css/mockups, BUILD only genuine gaps; never re-author an existing class.
Monorepo (ADR-0021): frontend is a pnpm-workspace monorepo (+ Turborepo). packages/{tokens,ui,icons,hooks,config} + apps/web. Internal scope @mtp/*, workspace:* deps. NO npm publishing — every package private:true, internal consumption only. Single Vite bundle (ADR-0006, no module federation); Vite transpiles workspace packages directly. "No monorepo" in ADR-0005 means across stacks (.NET/Node/Flutter separate repos) — the frontend repo is internally a workspace monorepo; not a contradiction.
Hosting: all repos under github.com/paramhq, always private (ADR-0005 amendment). Repos: mtp-docs, mtp-api (.NET), mtp-web (frontend monorepo).
Responsive (ADR-0020): every screen responsive, container-query-first (adapts to container, not just viewport). AppShell collapses rail→icons + submenu→drawer narrow; DataGrid → stacked cards on mobile; multi-col forms → one col; self-service mobile-first. No fixed-pixel layouts; breakpoints are tokens.
Hard exclusions: ❌ @gnwebsoft/ui (not used in this project — ignore it). ❌ Redux/RTK. ❌ runtime CSS-in-JS / Tailwind / MUI/Chakra/AntD/Mantine / Radix Themes. ❌ npm publishing. ❌ JS (TypeScript only). ❌ module federation.
Reports/PDF/Excel: native libs (QuestPDF / ClosedXML), never HTML→PDF — see [[feedback-reports-native-libs]].
Build status / specifics (mtp-web exists at D:\Projects\mtp-web):
- Scaffolded (pnpm + Turbo + Vite + React 19 + TS strict).
@mtp/tokens(two-tier),@mtp/ui(Stack/Cluster/Grid/Box, Button/StatusPill/KpiCard/DataTable, AppShell),@mtp/icons(inline set ported from _shell.js). AppShell verified pixel-ditto tomockups/_shell-demo.html. - Tree-shaking:
sideEffects=["**/*.css"]for tokens/ui,falsefor icons/hooks. ESM + barrel exports OK because only CSS is side-effectful → unused component JS is dropped. @/alias →apps/web/srcin BOTHvite.config.ts(resolve.alias) andapps/web/tsconfig.json(paths + baseUrl).- Fonts must load in
apps/web/index.html(Inter + Fraunces + JetBrains Mono via Google Fonts) — else design breaks (system-font fallback). Same<link>as the mockups. @types/nodein apps/web devDeps +"node"in tsconfig types (for vite.config). pnpmonlyBuiltDependencies: ["esbuild"]in root.- TS strict extras on: noUncheckedIndexedAccess, verbatimModuleSyntax, isolatedModules.
- Component library complete (2026-05-22): layout (Box/Stack/Cluster/Grid/Center/Container/SplitPane/Sidebar); Button/IconButton/StatusPill/Badge/Card/KpiCard/EmptyState/Tabs/FilterBar+Chip; Field (Label/TextField/Textarea/FormField); DataTable (hand DOM) + DataGrid (TanStack, sortable); Radix wrappers Dialog/Popover/Tooltip/DropdownMenu/Select/Switch/Toast (Toast = useToast() over Zustand store). All exported from
@mtp/uibarrel; typechecks clean. Next page-specific components built as 5b/5c needs them.
Pipeline: see docs/module-prep-prompt.md (Layer 5: identify pages → design HTML → design-system foundations → React pages → endpoints → wire).