MCMMCM DocsEngineering (Internal)
LLDUI Design
v1.2 is unreleased — see v1.1 for the current stable release.

UI Design Guidelines

Standard page shell pattern, component conventions, and anti-patterns for the MCM UI frontend.

MCM UI — Design Guidelines

Every page in the MCM UI follows one consistent layout structure. The accounts list page at components/features/enterprise/accounts/accounts-page-client.tsx is the canonical reference implementation.


Standard Page Shell Pattern

Layer Order (outside → inside)

1. getPageContainerClasses()          ← full-height scroll container
2.   getPageContentClasses() + !p-0   ← strips default padding; RouteTabShell owns it
3.     <Tabs>                          ← shadcn Tabs context
4.       <RouteTabShell tabs={…}>      ← sticky top-bar + padded body (px-6 py-5 space-y-6)
5.         <TopLevelTabsNav …/>        ← rendered inside the `tabs` prop
6.         <PageHeader …/>             ← first child in the body
7.         <TabsContent value="…">     ← one per tab; className={ROUTE_FLOW_TAB_CONTENT_CLASSNAME}
8.           {content}

Minimal Template

"use client"

import { SomeIcon } from "lucide-react"
import { Tabs, TabsContent } from "@/components/ui/tabs"
import { RouteTabShell, ROUTE_FLOW_TAB_CONTENT_CLASSNAME } from "@/components/ui/route-tab-shell"
import { TopLevelTabsNav } from "@/components/ui/top-level-tabs-nav"
import { PageHeader } from "@/components/ui/page-header"
import { getPageContainerClasses, getPageContentClasses } from "@/lib/design-tokens"
import { useRouteTabState } from "@/lib/use-route-tab-state"
import { cn } from "@/lib/utils"

const TAB_ITEMS = [
  { value: "all", label: "Label", icon: SomeIcon },
]

export function MyPageClient() {
  const tabState = useRouteTabState({ tabs: ["all"], defaultTab: "all" })

  return (
    <div className={getPageContainerClasses()}>
      <div className={cn(getPageContentClasses(), "!p-0")}>
        <Tabs value={tabState.activeTab} onValueChange={tabState.setActiveTab} className="w-full">
          <RouteTabShell
            tabs={
              <TopLevelTabsNav
                items={TAB_ITEMS}
                activeValue={tabState.activeTab}
                variant="accounts-shell"
              />
            }
          >
            <PageHeader
              title="Page Title"
              description="Short description of this section."
              className="gap-3 pb-4"
              actions={<Button>Primary Action</Button>}
            />
            <TabsContent value="all" className={ROUTE_FLOW_TAB_CONTENT_CLASSNAME}>
              {/* content */}
            </TabsContent>
          </RouteTabShell>
        </Tabs>
      </div>
    </div>
  )
}

Key Rules

1. Always use !p-0 on the content wrapper

getPageContentClasses() adds default padding. The !p-0 override removes it so RouteTabShell controls its own padding (px-6 py-5 space-y-6). Without !p-0 you get double-padding.

// Correct
<div className={cn(getPageContentClasses(), "!p-0")}>

// Wrong — double padding
<div className={getPageContentClasses()}>

2. PageHeader goes inside RouteTabShell, not outside <Tabs>

// Correct
<RouteTabShell tabs={…}>
  <PageHeader title="…" />
  <TabsContent …>

// Wrong — outside Tabs entirely
<div className="px-6 py-5">
  <PageHeader title="…" />
</div>
<Tabs>…</Tabs>

3. Always use variant="accounts-shell" for TopLevelTabsNav

Both "accounts-shell" and "route-shell" render the same visually, but "accounts-shell" is the project standard.

4. Use ROUTE_FLOW_TAB_CONTENT_CLASSNAME on every TabsContent

This exports "mt-0" which prevents extra top margin stacking with RouteTabShell's space-y-6.

5. Single-tab pages still get the full shell

Even if there's only one tab, still use RouteTabShell + TopLevelTabsNav. This keeps layout consistent and makes it trivial to add more tabs later.

6. Page-level tabs must not be managed inside child components

If a child component (e.g., UserManagement) renders its own Tabs + TopLevelTabsNav, those tabs belong in the page-client's RouteTabShell. The component should accept tab context from the parent or render TabsContent blocks directly.

7. KPI stats use InteractiveKpiCard in a 4-column grid

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
  <InteractiveKpiCard title="…" value={…} subtitle="…" icon={<Icon className="h-4 w-4" />} />
</div>

Do not use custom Card components for stats.

8. Use DataTable for data tables

DataTable from components/ui/data-table.tsx provides built-in search, column filters, pagination, and row click handling.

// Correct
<DataTable
  data={items}
  columns={columns}
  searchKey="name"
  onRowClick={(item) => router.push(`/items/${item.id}`)}
/>

// Wrong — manual table + separate search card
<Card><Input /></Card>
<Card><Table>…</Table></Card>

9. No "Open" / "View" action column when rows are already clickable

If onRowClick navigates to a detail page, do not add a redundant "Open" button column.

10. Breadcrumbs are global — do not add per-page

Breadcrumbs is already rendered in ConsoleLayout. Never add it inside individual page components.


Component Reference

ComponentPathPurpose
RouteTabShellcomponents/ui/route-tab-shell.tsxSticky tab-bar + padded body shell
ROUTE_FLOW_TAB_CONTENT_CLASSNAMEsame file"mt-0" — use on every TabsContent
TopLevelTabsNavcomponents/ui/top-level-tabs-nav.tsxHorizontal tab navigation bar
PageHeadercomponents/ui/page-header.tsxTitle + description + optional action button(s)
InteractiveKpiCardcomponents/ui/interactive-surfaces.tsxGradient KPI stat card with hover effect
DataTablecomponents/ui/data-table.tsxFull-featured table with search, filters, pagination
getPageContainerClasseslib/design-tokens.tsOuter scroll container classes
getPageContentClasseslib/design-tokens.tsInner content wrapper classes (always add !p-0)
useRouteTabStatelib/use-route-tab-state.tsTab state synced to URL query params

Detail Pages (with multiple sub-tabs)

Detail pages (e.g., /accounts/[id], /budget/[id]) follow the same shell pattern. Additional metadata (badges, status pills, progress bars) goes inside the RouteTabShell body, before the TabsContent blocks, grouped in a <div className="space-y-3">.

<RouteTabShell tabs={<TopLevelTabsNav />}>
  <div className="space-y-3">
    {/* Metadata badges */}
    <div className="flex items-center gap-2">
      <Badge>…</Badge>
    </div>
    <PageHeader title={item.name} description={item.description} className="gap-3 pb-0" />
    {/* Optional inline status/progress */}
    <div className="flex items-center gap-3">…</div>
  </div>

  <TabsContent value="overview" className={ROUTE_FLOW_TAB_CONTENT_CLASSNAME}>…</TabsContent>
  <TabsContent value="settings" className={ROUTE_FLOW_TAB_CONTENT_CLASSNAME}>…</TabsContent>
</RouteTabShell>

Anti-Patterns

Don't do thisDo this instead
<div className="p-6 space-y-6"> as page rootUse getPageContainerClasses() + !p-0 shell
<h1> or <h2> as page/section titleUse <PageHeader title="…" />
getPageContentClasses() without !p-0Always add cn(getPageContentClasses(), "!p-0")
RouteSubTabsNav as the main page tab barUse TopLevelTabsNav inside RouteTabShell
Custom Card stats gridUse InteractiveKpiCard in a 4-col grid
Raw <Table> or shadcn <Table> for dataUse <DataTable>
Separate search <Input> + filter <Select>Use DataTable's built-in searchKey + filterable columns
"Open" / "View" button column when row is clickableUse onRowClick only, remove the button
TopLevelTabsNav variant "route-shell"Use variant="accounts-shell"
Managing page-level tabs inside a child componentLift tab state to the page-client

Reference Implementations

PageFile
Accounts list (canonical)components/features/enterprise/accounts/accounts-page-client.tsx
Account detail (canonical)app/(main)/(enterprise)/accounts/[id]/page-client.tsx
Budget listapp/(main)/(enterprise)/(manage)/budget/page.tsx
Budget detailapp/(main)/(enterprise)/(manage)/budget/[id]/page-client.tsx
User Management (lifted tabs)components/features/enterprise/user-management/user-management-page-client.tsx
Dashboardscomponents/features/enterprise/dashboards/dashboards-page-client.tsx

On this page