Imported from doubleG7/nirvana (
.agents/skills/design-system/SKILL.md). Install upstream withnpx skills add doubleG7/nirvana --skill design-system. Copyright stays with the author.
Nirvana Design System
This skill defines the visual language for the Nirvana Construction Software Suite. All UI work must follow these patterns. The design is derived from the Trimble Modus 2.0 design system with a glassmorphic, professional enterprise aesthetic.
1. Design principles
- Dense but scannable — Construction professionals work with large datasets. Pack information efficiently but maintain clear visual hierarchy.
- AI-native — AI insights are first-class UI citizens, not sidebars or afterthoughts. Every module surface should accommodate insight cards.
- Glassmorphic depth — Use blur, transparency, and layered surfaces to create depth without heavy shadows.
- Professional enterprise — Clean, restrained, high-contrast. No playful illustrations or casual tones. This is software for managing millions of dollars in construction projects.
- Trimble brand — Trimble Blue is the primary accent. Trimble Yellow is for warnings and highlights. Follow Modus 2.0 guidelines.
2. Trimble brand tokens
Colors (CSS variables defined in index.css)
/* Primary brand */
--color-primary-50: #e6f0f7;
--color-primary-100: #b3d4e8;
--color-primary-200: #80b8d9;
--color-primary-300: #4d9cca;
--color-primary-400: #2680bb;
--color-primary-500: #0063a3; /* Trimble Blue — primary actions, active states */
--color-primary-600: #005892;
--color-primary-700: #004d82;
--color-primary-800: #004271;
--color-primary-900: #003761;
/* Accent */
--color-accent-500: #fbad26; /* Trimble Yellow — warnings, highlights */
/* Secondary */
--color-secondary-500: #3d444b; /* Dark charcoal — secondary text, borders */
/* Semantic status */
--color-success: #22c55e; /* Green — completed, healthy, on-budget */
--color-warning: #fbad26; /* Amber — at risk, approaching limit */
--color-danger: #ef4444; /* Red — over budget, critical, failed */
--color-info: #3b82f6; /* Blue — informational */
Surface system (glassmorphic layers)
/* Page background */
--app-bg: #eef2f5; /* Light cool gray */
--app-bg-elevated: #f8fafc;
--app-bg-muted: #e7ecf1;
/* Card / panel surfaces — increasing opacity = higher elevation */
--app-surface-1: rgba(255, 255, 255, 0.72); /* Background cards */
--app-surface-2: rgba(255, 255, 255, 0.88); /* Elevated cards */
--app-surface-3: rgba(255, 255, 255, 1.0); /* Modal / dialog */
/* Input fields */
--app-input-bg: #f7f9fb;
/* Borders */
--app-border: rgba(15, 23, 42, 0.10);
/* Text hierarchy */
--app-text-strong: #111827; /* Headings, primary values */
--app-text: #253142; /* Body text */
--app-text-sub: #526172; /* Secondary text, labels */
/* Shadows */
--app-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06);
--app-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.04), 0 2px 4px rgba(0, 0, 0, 0.06);
Typography
- Font family: Inter (loaded via Google Fonts or local asset)
- Base size: 14px (
text-smin Tailwind)
| Token | Tailwind | Use |
|---|---|---|
| Page title | text-xl font-bold text-[--app-text-strong] |
Page headings |
| Section title | text-base font-semibold text-[--app-text-strong] |
Card headers, section labels |
| Body | text-sm text-[--app-text] |
Default body text |
| Subtitle | text-sm text-[--app-text-sub] |
Descriptions, secondary info |
| Label | text-xs font-medium uppercase tracking-wider text-[--app-text-sub] |
Form labels, nav group headers |
| Caption | text-xs text-[--app-text-sub] |
Timestamps, footnotes |
| Metric value | text-2xl font-bold tabular-nums |
KPI numbers, stat cards |
3. AppShell layout
The application uses a fixed shell layout matching the Trimble enterprise pattern:
┌─────────────────────────────────────────────────┐
│ TopBar (branding, search, status, user menu) │
├────────┬────────────────────────────────────────┤
│ │ │
│ Sidebar│ Main Content Area │
│ (nav) │ (scrollable) │
│ w-56 │ │
│ fixed │ ┌──────────────────────────────┐ │
│ │ │ Page content with │ │
│ │ │ gradient bg, grid overlay, │ │
│ │ │ and decorative orbs │ │
│ │ └──────────────────────────────┘ │
│ │ │
├────────┤────────────────────────────────────────┤
│ Footer │ │
└────────┴────────────────────────────────────────┘
Sidebar (SidebarNav.tsx)
- Width:
w-56(224px) fixed - Background:
bg-white/70 backdrop-blur-md - Border:
border-r border-[--app-border] - Contains collapsible
NavGroupsections
TopBar (TopBar.tsx)
- Brand: "NIRVANA" logo + "AI-Native Construction Suite" tagline
- Center: Status pills (AI Active, Models Online) with animated pulse dots
- Right: Notifications, user avatar with dropdown menu
Main content area
- Background: gradient from
--app-bgwith subtle grid overlay (60x60px lines) - Three decorative blur orbs (blur-3xl) at low opacity for depth
- Content is scrollable, sidebar and topbar are fixed
4. Navigation pattern
NavGroup (collapsible section)
// Label is uppercase, includes item count badge and chevron toggle
<div className="flex items-center justify-between px-3 py-2">
<span className="text-xs font-medium uppercase tracking-wider text-[--app-text-sub]">
{label}
</span>
<div className="flex items-center gap-1.5">
<span className="text-[10px] text-[--app-text-sub]">{count}</span>
<ChevronDown className={cn("h-3.5 w-3.5 transition-transform", open && "rotate-180")} />
</div>
</div>
NavItem (link)
// Active state uses primary color treatment
<NavLink
to={route}
className={({ isActive }) => cn(
"flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm transition-colors",
isActive
? "bg-primary-50 text-primary-700 border border-primary-100 font-medium"
: "text-[--app-text] hover:bg-[--app-bg-muted]"
)}
>
<Icon className="h-[18px] w-[18px]" />
{label}
</NavLink>
Navigation groups
| Group | Modules |
|---|---|
| Overview | Dashboard, AI Insights |
| Operations | Project Management, Estimating, Service Dispatch |
| Finance | Accounting, Payroll, Payment |
| People | HR |
| Tools | Custom Forms, Analytics |
| Admin | Tenant Config, Access Control, Extensions |
Each NavItem must include a feature gate — items are only rendered if the user has access via useAuth().canFeature(featureKey).
5. Component patterns
StatCard (KPI metric)
<div className="ui-card flex items-center gap-4 p-4">
<div className={cn("rounded-xl p-2.5", iconBgColor)}>
<Icon className={cn("h-5 w-5", iconColor)} />
</div>
<div>
<p className="text-xs text-[--app-text-sub]">{label}</p>
<p className="text-2xl font-bold tabular-nums text-[--app-text-strong]">{value}</p>
<p className="text-xs text-[--app-text-sub]">{subtitle}</p>
</div>
</div>
Grid: grid md:grid-cols-2 xl:grid-cols-4 gap-4
DashboardModuleCard (module quick-access)
- Color-coded by theme: blue, emerald, amber, violet, rose, cyan
- Each color defines:
iconBg,iconText,border,glow,pillvariants - Shows AI health indicator dot with severity color (red/amber/primary/emerald)
- Staggered entrance animation
Data table
- Use Modus table components as base
- Header:
text-xs font-medium uppercase tracking-wider text-[--app-text-sub] bg-[--app-bg-muted] - Rows:
border-b border-[--app-border] hover:bg-[--app-bg-muted]/50 - Pagination: bottom-right, showing page X of Y with prev/next buttons
- Sorting: click column header, show chevron indicator
- Filtering: filter bar above table with Zod-validated inputs
Forms
- Use Modus form controls as base components
- Labels:
text-xs font-medium text-[--app-text-sub]above field - Inputs:
bg-[--app-input-bg] border border-[--app-border] rounded-lg px-3 py-2 - Focus:
focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 - Validation errors:
text-xs text-danger mt-1below field - Form sections: group related fields with section title and subtle divider
- Multi-step wizards: progress indicator at top, prev/next buttons at bottom
Charts (Recharts)
- Wrap in
<ResponsiveContainer width="100%" height={300}> - Color palette: use CSS variables (
--color-primary-500,--color-success, etc.) - Custom tooltip: glassmorphic dark bg (
bg-slate-900/95 backdrop-blur-sm border border-white/10 rounded-lg) - Grid:
stroke="var(--app-border)"with dashed pattern - Axis labels:
fontSize={12} fill="var(--app-text-sub)"
AI Insight card
<div className={cn("ui-card border-l-4 p-4", severityBorderColor)}>
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<SeverityIcon className="h-4 w-4" />
<span className="text-xs font-medium uppercase tracking-wider">{category}</span>
</div>
<span className="text-xs text-[--app-text-sub]">{timestamp}</span>
</div>
<p className="mt-2 text-sm text-[--app-text]">{message}</p>
<p className="mt-1 text-xs text-primary-500 cursor-pointer hover:underline">{actionLabel}</p>
</div>
Severity border colors:
- Critical:
border-l-danger - Warning:
border-l-warning - Info:
border-l-info - Success:
border-l-success
StatusBadge
<span className={cn(
"inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider",
statusStyles[status] // e.g., "bg-primary-50 text-primary-700"
)}>
{label}
</span>
Status color mapping:
- Open / Active / In Progress →
bg-primary-50 text-primary-700 - Complete / Approved / Paid →
bg-emerald-50 text-emerald-700 - Pending / Review →
bg-amber-50 text-amber-700 - Rejected / Failed / Overdue →
bg-red-50 text-red-700 - Draft / Inactive →
bg-slate-100 text-slate-600
Modal / Dialog
- Overlay:
bg-black/40 backdrop-blur-sm - Panel:
bg-[--app-surface-3] rounded-2xl shadow-xl max-w-lg mx-auto - Header: title + close button
- Footer: cancel (secondary) + confirm (primary) buttons
6. UI utility classes
Define these in index.css:
.ui-card {
background: var(--app-surface-2);
border: 1px solid var(--app-border);
border-radius: 1rem; /* rounded-2xl */
box-shadow: var(--app-shadow-sm);
}
.ui-input {
background: var(--app-input-bg);
border: 1px solid var(--app-border);
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
transition: border-color 150ms, box-shadow 150ms;
}
.ui-input:focus {
border-color: var(--color-primary-500);
box-shadow: 0 0 0 3px rgba(0, 99, 163, 0.15);
outline: none;
}
.ui-btn-primary {
background: var(--color-primary-500);
color: white;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
transition: background 150ms;
}
.ui-btn-primary:hover { background: var(--color-primary-600); }
.ui-btn-primary:active { background: var(--color-primary-700); }
.ui-btn-secondary {
background: white;
color: var(--app-text);
border: 1px solid var(--app-border);
border-radius: 0.5rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
transition: background 150ms;
}
.ui-btn-secondary:hover { background: var(--app-bg-muted); }
.ui-btn-danger {
background: var(--color-danger);
color: white;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
}
.ui-btn-danger:hover { background: #dc2626; }
7. Animation patterns (Framer Motion)
Staggered list entrance
const containerVariants = {
hidden: {},
visible: { transition: { staggerChildren: 0.07 } },
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.45, ease: "easeOut" } },
};
<motion.div variants={containerVariants} initial="hidden" animate="visible">
{items.map(item => (
<motion.div key={item.id} variants={itemVariants}>
<Card {...item} />
</motion.div>
))}
</motion.div>
Hover scale on cards
<motion.div whileHover={{ scale: 1.02 }} transition={{ duration: 0.2 }}>
<Card />
</motion.div>
Dropdown menu entrance
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.96 }}
transition={{ duration: 0.15 }}
>
{children}
</motion.div>
)}
</AnimatePresence>
Page transitions
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.25 }}
>
<Outlet />
</motion.div>
</AnimatePresence>
Loading skeleton pulse
Use Tailwind's animate-pulse on placeholder blocks:
<div className="ui-card p-4 space-y-3">
<div className="h-4 w-1/3 rounded bg-[--app-bg-muted] animate-pulse" />
<div className="h-3 w-2/3 rounded bg-[--app-bg-muted] animate-pulse" />
<div className="h-3 w-1/2 rounded bg-[--app-bg-muted] animate-pulse" />
</div>
8. Responsive design
Breakpoints (Tailwind defaults)
sm: 640px — mobile landscapemd: 768px — tabletlg: 1024px — small desktopxl: 1280px — standard desktop2xl: 1536px — wide desktop
Grid patterns
- KPI cards:
grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 - Module cards:
grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 - Two-column layout:
grid grid-cols-1 lg:grid-cols-[1fr_minmax(0,380px)] gap-6 - Dashboard:
grid grid-cols-1 lg:grid-cols-[1fr_minmax(0,868px)] gap-6
Sidebar behavior
- Desktop (
lg+): Fixed sidebar, always visible - Tablet (
md): Collapsible sidebar with hamburger toggle - Mobile (
< md): Full-screen overlay sidebar
9. Common mistakes — do NOT do these
- No raw hex values — Always use CSS variables or Tailwind tokens. Never
color: #0063a3inline. - No ad-hoc typography — Use the defined text hierarchy tokens. Never random
text-lg font-normal. - No heavy drop shadows — Use
--app-shadow-smor--app-shadow-mdonly. The glassmorphic style relies on transparency and blur, not shadow. - No inconsistent border radius — Cards use
rounded-2xl, buttons and inputs userounded-lg, badges userounded-full. - No color without semantic meaning — Status colors must map to defined states. Don't use green for decoration.
- No animations without purpose — Every animation should communicate state change. Don't animate for novelty.
- No feature without gate — Every nav item, route, and module-level action must check RBAC via
useAuth().canFeature(). - No Modus override without comment — If you must override a Modus component's default styling, add a comment explaining why.
- No inline styles — Use Tailwind classes or CSS variables. Exception: dynamic values computed at runtime.
- No icon inconsistency — All icons from Lucide React at 18px default. Never mix icon libraries.