Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Version: 1.6.1 Last Updated: 2026-04-18 Status: Active Target Audience: Developers and AI agents (GitHub Copilot, Cursor, general AI assistants)
Constitution Reference: See constitution.md (current: see docs/VERSIONS.md) §6.7 [UI/UX Guardrails] for non-negotiable requirements. Quick Reference: See root AGENTS.md (current: see docs/VERSIONS.md) §[Quick Reference] for condensed guidelines. Color Utilities: See docs/development/SEMANTIC_COLORS.md (current: see docs/VERSIONS.md) for color mapping patterns. Spacing Patterns: See SPACING_AND_UX_STANDARDS.md (current: see docs/VERSIONS.md) for detailed spacing patterns and responsive design guidelines. Tab Patterns: See .cursor/rules/tab-patterns.md for the canonical tab variant decision table, codemod recipes, and accessibility rules.

AI agents: Use semantic tokens, skeleton loaders (never return null), responsive spacing, and @/shared/ui components. Full pattern list: AGENTS.md, .cursor/rules/quick-reference.mdc.

Design Token Governance

The canonical token source for app/runtime parity is governed by: This policy defines:
  • source-of-truth files and ownership,
  • hard-locked vs app-extensible token families,
  • backward-compat alias expectations,
  • required visual and validation checks before merge.
All token changes in src/index.css must follow this policy.

Quick Reference

Common Patterns

Semantic Color Tokens:
// ✅ CORRECT
<Badge variant="success">Active</Badge>
<div className="bg-warning/10 text-warning-foreground">Warning</div>
<Button variant="destructive">Delete</Button>

// ❌ WRONG
<Badge className="bg-green-500">Active</Badge>
<div className="text-yellow-600">Warning</div>
Component Variants:
  • success - Positive states (active, approved, complete)
  • warning - Caution states (pending, at-risk, expiring)
  • destructive - Error/danger states (rejected, error, critical)
  • info - Informational states (in progress, new)
  • secondary - Neutral/inactive states
Loading States:
// ✅ ALWAYS use skeleton loaders
if (isLoading) return <AppLoadingSkeleton />;

// ❌ NEVER return null
if (isLoading) return null;
Spacing: Use responsive spacing; see SPACING_AND_UX_STANDARDS.md for patterns and breakpoints.

Token Usage

Colors: Always use semantic tokens (success, warning, destructive, primary, muted)
Spacing: Use responsive spacing (space-y-4 md:space-y-6)
Typography: Use font utilities (font-sans, font-serif, font-mono)
Components: Use variant props (variant="success", variant="destructive")

Empty States

For list or content areas with no data (e.g. hub tab content, pipeline stages, credential renewals), use the shared EmptyState component from @/shared/components/EmptyState with a title, optional description, and primary CTA (e.g. “Add credential”, “Create job posting”). Do not leave content areas blank; every empty list or empty tab view must show an explicit empty state. Avoid custom empty layouts (custom Card + icon + text + button); use a thin wrapper around EmptyState when you need domain-specific copy or branching. See constitution §6.7 (widgets handle empty states).

Overlay naming

Use *Dialog or *Sheet in component names for overlay surfaces (e.g. ClaimGenerateDialog, MobileMenuSheet). Do not use Modal in names; the implementation should use Dialog or Sheet from @/shared/ui.

N/A and Zero States

Where a KPI or count is not applicable (e.g. no data yet, metric does not apply), display ”—” or “N/A” with an optional tooltip explaining why, instead of “0” or a blank value, to avoid implying incorrect data.

Hub Entry Copy (Optional)

Hub pages MAY include a one-line “How to use” under the hub title (e.g. “Use the tabs below to switch between Report, Renewals, and My Credentials”) for first-time users. If used, keep the pattern consistent across hubs. For detailed patterns and examples, see sections below.

Table of Contents

  1. Design System
  2. Button Usage
  3. Tabs
  4. Page Templates
  5. Navigation & Information Architecture
  6. Mobile-First Requirements
  7. Performance UX
  8. Accessibility
  9. Related Documentation

Design System

Token System

The platform uses a token-based design system with HSL CSS variables mapped to Tailwind utilities using Tailwind CSS v4 CSS-first configuration. All colors, spacing, typography, and other design tokens are defined in src/index.css. Implementation References:
  • CSS Variables + token mappings: src/index.css (@theme inline, :root, .dark)

Colors

Semantic Colors

Required: Always use semantic color tokens, never hardcoded Tailwind color classes. Available Semantic Colors:
  • primary / primary-foreground - Primary brand color (deep navy chrome accent; not the main CTA — use accent)
  • secondary / secondary-foreground - Secondary actions (note: bg-secondary is aliased to --card-elevated)
  • accent / accent-foreground - Brand CTA color (teal); pair with Button variant="accent" for the main action
  • destructive / destructive-foreground - Errors, destructive actions
  • success / success-foreground - Success states, positive feedback
  • warning / warning-foreground - Warnings, caution states
  • info / info-foreground - Informational states (a distinct blue, not a primary tint); pair bg-info with text-info-foreground for proper dark-mode contrast
  • muted / muted-foreground - Muted text, disabled states
  • card / card-foreground - Card backgrounds
  • popover / popover-foreground - Popover/dropdown backgrounds
Full token catalog: See SEMANTIC_COLORS.md § Token Catalog Reference for the complete inventory, including subtle variants, hover tokens, sidebar slot, special tokens (overlay-scrim, ai-accent, glass, shimmer-highlight), and the Encore brand reserved set.
Usage Examples:
// ✅ CORRECT: Use semantic tokens
<Badge variant="success">Published</Badge>
<div className="bg-warning/10 text-warning-foreground">Warning message</div>
<Button variant="destructive">Delete</Button>

// ❌ WRONG: Hardcoded colors
<Badge className="bg-green-500">Published</Badge>
<div className="text-yellow-600">Warning message</div>
<Button className="bg-red-500">Delete</Button>
Anti-Patterns:
  • ❌ Using text-green-500, bg-red-100, etc. (hardcoded Tailwind colors)
  • ❌ Using text-[#FF0000] (arbitrary color values)
  • ✅ Use semantic tokens: text-success, bg-destructive/10, etc.

Status Colors

Status indicators should use semantic color variants:
  • Success: success token (green) - Completed, approved, published
  • Warning: warning token (yellow/orange) - Pending, needs attention
  • Error: destructive token (red) - Failed, rejected, errors
  • Info: info token (blue) - Informational messages, in-progress states. A distinct semantic state — not a primary tint. Pair bg-info with text-info-foreground (a dedicated, mode-aware token); never with text-primary-foreground (which fails WCAG in dark mode).
  • Neutral: muted token (gray) - Default, inactive states

Module Identity Colors

Each domain core has a dedicated identity color (--module-{short-code} plus -hover and -subtle variants). Use them for navigation accents, dashboard widget headers, and module badges — never for status/state.
// ✅ Module accent on a sidebar nav row
<a className="border-l-4 border-module-cl bg-module-cl-subtle/50">Clinical hub</a>

// ❌ Don't use module colors to convey status
<Badge className="bg-module-fa">Approved</Badge> // → Badge variant="success"
See SEMANTIC_COLORS.md § Module Identity Tokens for the full short-code list (12 active + 4 reserved + 4 deprecated aliases) and recommended usage patterns.

Chart Palette

Chart configs MUST use hsl(var(--chart-N)) (N = 1–8) for series colors — never hex literals or hardcoded HSL strings — so charts adapt to dark mode and respect tenant rebranding.
// ✅ Theme-aware chart series
<Bar dataKey="value" fill="hsl(var(--chart-1))" />

// ❌ Breaks dark mode and tenant theming
<Bar dataKey="value" fill="#3B82F6" />
See SEMANTIC_COLORS.md § Chart Palette for the full 8-color palette, the colorblind-safety note, and ChartContainer integration guidance.

Typography

Font Families:
  • Display/Headings: Space Grotesk - font-heading
  • Body/Sans: Inter (primary UI font) - font-sans
  • Serif alias: Inter fallback contract (matches body token) - font-serif
  • Mono: DM Mono (for code, data) - font-mono
Font Loading:
  • Fonts are loaded asynchronously in index.html (non-blocking); do not add @import for fonts in CSS.
  • Critical inline faces preload Inter and Space Grotesk for first paint parity with design system.
  • System font fallbacks are defined in CSS variables.
Usage:
  • Default: font-sans (Inter) for all body/UI text.
  • Headings: font-heading (Space Grotesk) for page titles and heading hierarchy.
  • Mono: font-mono (DM Mono) for code blocks, data tables, and technical strings.
  • Tenant overrides:
    • --tenant-font-family overrides body typography,
    • --tenant-font-heading overrides heading typography. If unset, the canonical Inter + Space Grotesk contract applies.

Spacing

Spacing scale, container padding, and responsive patterns are defined in SPACING_AND_UX_STANDARDS.md. Use that doc for breakpoints, card padding, and form layout.

Button Usage

Use the shared Button from @/shared/ui/button with semantic variants (default, accent, destructive, outline, secondary, ghost, link, success, warning, soft, glass, ai) and sizes (default, sm, xs, lg, icon). Primary CTA (important): The default variant is a neutral elevated control (card surface + border), not the brand primary. For the main action on a page, form, or dialog footer, use variant="accent" (teal / brand accent) unless a domain pattern explicitly calls for success / destructive / warning. Icon-only buttons must have aria-label. Prefer the loading prop for async actions. Dialog footers: Cancel left, primary right. Full guidance: BUTTON_USAGE.md.

Icons

Source: Use lucide-react only. Do not use heroicons, react-icons, or custom SVG icon sets. Size scale: Use the design token scale for consistency. CSS variables are in src/index.css (--icon-xs--icon-lg); use the equivalent Tailwind classes when applying to icons:
TokenSizeTailwindUse case
icon-xs14pxh-3.5 w-3.5Inline, tight UI (e.g. trend indicators in tables)
icon-sm16pxh-4 w-4List items, buttons, form controls
icon-md20pxh-5 w-5Nav items, section headers, menu items
icon-lg24pxh-6 w-6Empty states, feature icons, page-level visuals
Decorative only: For large hero or empty-state icons use h-8 w-8 (32px) or h-12 w-12 (48px). These are not part of the standard token scale and should only be used for decorative/empty-state contexts. Icon-only buttons: Use size="icon-touch" on Button for icon-only actions so the tap target is at least 44×44px (accessibility). Use size="icon" only when the button is not the primary touch target (e.g. inside a larger clickable area). Nav and lists: Standardize nav item icons to one size (e.g. icon-md / h-5 w-5). Use icon-sm for compact lists and data tables. Color: Use semantic tokens for icon color (e.g. text-muted-foreground, text-primary, text-success). See SEMANTIC_COLORS.md. AppIcon wrapper (available now, preferred for shared code): Use AppIcon from @/shared/ui/icon for shared/reusable components. It bakes in the platform stroke width (1.75), the size scale (xs/sm/md/lg), and the decorative/aria-label accessibility contract:
<AppIcon icon={Check} size="md" className="text-success" />
<AppIcon icon={Trash2} size="sm" decorative={false} aria-label="Delete" />
For accent tones: use tone="accent" on subtle accent surfaces (bg-accent/10, bg-accent/20) and reserve tone="onAccent" for solid accent chips (bg-accent). Tailwind size classes (see table above) remain acceptable for trivial one-off icons inside leaf components. See ICON_GUIDE.md § AppIcon wrapper for the full prop table. For which icon to use, sizing, import patterns, and accessibility, see ICON_GUIDE.md.

Component Variants

Badge Variants

Available Variants:
  • default - Primary actions, highlights
  • secondary - Secondary information
  • destructive - Errors, deletions
  • outline - Subtle indicators
  • success - Success states
  • warning - Warning states
  • info - Informational states (in progress, new)
  • Light variants: success-light, warning-light, destructive-light, primary-light
  • Specialty: accentGlow, glass, ai (non-status use cases)
Usage:
<Badge variant="success">Published</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="destructive">Failed</Badge>
<Badge variant="info">In Progress</Badge>
<Badge variant="outline">Draft</Badge>
See SEMANTIC_COLORS.md for complete variant reference and status mapping patterns.

Button Variants

Available Variants:
  • default - Neutral elevated (card-like surface); use for secondary emphasis, not the single main CTA
  • accent - Brand primary CTA (accent teal); default choice for “Save”, “Submit”, “Continue” when one action dominates
  • destructive - Destructive actions
  • outline - Secondary actions, toolbars
  • secondary - Tertiary / muted actions
  • ghost - Subtle actions
  • link - Link-style buttons
  • success, warning, soft, glass, ai - See JSDoc on src/shared/ui/button.tsx
Sizes:
  • default - h-10 px-4 py-2
  • sm - h-9 px-3
  • lg - h-11 px-8
  • icon / icon-touch - h-11 w-11 min 44×44px (square icon buttons; use icon-touch for icon-only actions to meet touch target size)
Note (2026-04-18 audit): The Button size tokens shipped in code currently differ slightly from the table above (default h-9, sm h-8, lg h-10, icon size-11, plus an undocumented xs h-7). Reconciliation is tracked in reports/ux/ux-implementation-plan-20260418.md § Phase 1.2. Until that PR lands, prefer Button size="default" and trust the variant defaults rather than asserting on the height.

Tabs

Canonical rule file: .cursor/rules/tab-patterns.md. Source primitives: @/shared/ui/tabs (Tabs, TabsList, TabsTrigger, TabsContent, TabBadge), @/shared/components/ScrollableTabsList, @/platform/tabs/useTabUrlState, @/platform/settings/components/SettingsTabs.

Variant decision table

Page classVariantWrapperSizeURL stateIcons
Module hub page (/{core}/dashboard, /{core}/{hub})defaultScrollableTabsList + tabsListClassName="sm:grid sm:grid-cols-N"defaultRequired (useTabUrlState)All-or-none
Sub-module workspace tabsdefaultScrollableTabsListdefaultRequiredAll-or-none
Module/entity settings pagelineSettingsTabs (which uses ScrollableTabsList)defaultOptionalOptional
Detail page section switcher (e.g. employee profile, vendor profile)lineScrollableTabsList variant="line"defaultRequired for shareable deep linksOptional
Form dialog internal sectionsdefaultnonedefaultNoAll-or-none
In-card section switcher (within a <Card>)defaultnonecompactNoNone
Filter / view-mode togglespillnonecompactOptionalNone

What AI must NEVER do (tabs)

  • <TabsList className="grid w-full grid-cols-N"> — equal-width cells break at narrow viewports, no scroll affordance. Wrap with ScrollableTabsList tabsListClassName="sm:grid sm:grid-cols-N".
  • <TabsList className="flex-wrap h-auto"> — wrapping creates uneven heights and breaks the page’s vertical rhythm. Use ScrollableTabsList and let it scroll horizontally instead.
  • ❌ Inline h-N overrides on <TabsList> (h-8, h-10, h-11). Use size="compact" only when you need denser controls (e.g. inside a card).
  • ❌ Inline (${count}) in trigger labels. Use <TabBadge count={n} /> for the canonical pill style and the aria-label="${count} items" accessibility contract.
  • <TabsList> without aria-label (or aria-labelledby). Pass an explicit label when there is no visible heading.
  • ❌ Mobile collapse to icon-only that drops the visible label. At <sm, tabs MUST scroll horizontally OR collapse into a Sheet picker.

Quick recipes

URL-synced hub (deep-link friendly, refresh-stable):
import { useTabUrlState } from '@/platform/tabs';
import { ScrollableTabsList } from '@/shared/components/ScrollableTabsList';
import { Tabs, TabsTrigger } from '@/shared/ui/tabs';

const VALID_TABS = ['accounts', 'statements', 'reconciliations', 'transactions'] as const;
const [tab, setTab] = useTabUrlState('tab', VALID_TABS, 'accounts');

<Tabs value={tab} onValueChange={(v) => setTab(v as (typeof VALID_TABS)[number])}>
  <ScrollableTabsList tabsListClassName="sm:grid sm:grid-cols-4" aria-label="Banking sections">
    {VALID_TABS.map((t) => (
      <TabsTrigger key={t} value={t} className="snap-start">{labelFor(t)}</TabsTrigger>
    ))}
  </ScrollableTabsList>
</Tabs>
Settings page — use SettingsTabs:
import { SettingsTabs } from '@/platform/settings/components/SettingsTabs';

<SettingsTabs
  defaultValue="configuration"
  tabsListClassName="sm:grid sm:grid-cols-5"
  items={[
    { value: 'configuration', label: <><Settings className="size-4" aria-hidden="true" /> Configuration</>, content: <ConfigurationTab /> },
    { value: 'transaction-rules', label: <><Filter className="size-4" aria-hidden="true" /> Transaction Rules</>, content: <TransactionRulesTab /> },
    // ...
  ]}
/>
Tab badges — never inline (${count}):
import { TabsTrigger, TabBadge } from '@/shared/ui/tabs';

<TabsTrigger value="pending">
  Pending <TabBadge count={pendingCount} variant="destructive" />
</TabsTrigger>
Migration guidance: see reports/ux/tabs-consistency-audit-20260418.md (full inventory + before/after recipes) and reports/ux/ux-implementation-plan-20260418.md § Phase 3 for the rollout plan and codemods.

Page Templates

Required Page States

Every page MUST implement these states:

1. Loading State

Requirement: Use skeleton loaders, never return null.
// ✅ CORRECT
if (loading) return <PageSkeleton />;

// ❌ WRONG
if (loading) return null;
Skeleton Patterns by Page Type:
  • List Pages: Grid of skeleton cards/rows
  • Detail Pages: Skeleton header + content sections
  • Dashboards: Skeleton stat cards + chart placeholders
  • Forms: Skeleton form fields
Implementation:
  • Route-level: RouteLoadingSkeleton (already implemented)
  • Component-level: Use Skeleton component from @/shared/ui/skeleton

2. Error State

Requirement: Show user-friendly error messages with recovery actions.
if (error) {
  return (
    <ErrorState
      title="Failed to load data"
      message={error.message}
      action={<Button onClick={retry}>Try Again</Button>}
    />
  );
}
Error State Components:
  • Use Alert component with destructive variant
  • Include retry/refresh action when applicable
  • Show specific error message (not generic “Something went wrong”)

3. Empty State

Requirement: Show helpful empty states for lists and detail pages.
if (items.length === 0) {
  return (
    <EmptyState
      icon={FileText}
      title="No forms yet"
      description="Create your first form to get started"
      action={<Button onClick={createForm}>Create Form</Button>}
    />
  );
}
Empty State Guidelines:
  • Include icon, title, description, and primary action
  • Use friendly, helpful copy
  • Provide clear next steps

Page Template Primitives

Available Components:
  • PageContainer - Responsive page wrapper with padding (src/shared/components/PageContainer.tsx)
  • OverviewPageWrapper - Standardized wrapper for module overview pages with pull-to-refresh (src/shared/components/OverviewPageWrapper.tsx)
  • ResponsiveFormLayout - Form field layout with responsive columns (src/shared/components/ResponsiveFormLayout.tsx)
  • MobileTableWrapper - Responsive table wrapper for mobile (src/shared/components/MobileTableWrapper.tsx)
Page Structure:
<PageContainer spacing="md">
  <PageHeader title="Page Title" description="Page description" />
  <Card>
    <CardContent>
      {/* Page content */}
    </CardContent>
  </Card>
</PageContainer>

Overview Pages

Use OverviewPageWrapper for all module overview/landing pages:
import { OverviewPageWrapper, PageHeader, StatCard } from '@/shared/components';

export default function HROverview() {
  return (
    <OverviewPageWrapper refreshQueryKeys={['hr']} spacing="md">
      <PageHeader
        title="Workforce & HR"
        description="Manage employees and departments"
        icon={Users}
      />
      
      <QuickActionsSection moduleId="hr" ... />
      
      <div className="grid gap-3 md:gap-4 lg:gap-6 md:grid-cols-3">
        {/* Stat cards */}
      </div>
      
      {/* Area navigation cards or widgets */}
    </OverviewPageWrapper>
  );
}
Features:
  • Automatic mobile pull-to-refresh with query invalidation
  • Consistent responsive padding and spacing
  • Standardized page structure
When to Use Which Template:
  • Overview Pages: OverviewPageWrapper for module landing pages
  • List Pages: PageContainer + Card for filters + table/list
  • Detail Pages: PageContainer + Card sections for different data groups
  • Dashboards: PageContainer + grid of stat cards + widgets
  • Forms: PageContainer + ResponsiveFormLayout for form fields

Route Taxonomy

Current Standard: Flat route structure with nested sub-modules where appropriate. Route Patterns:
  • Module Overview: /{module}/dashboard (e.g., /hr/dashboard)
  • Module List: /{module}/{entity} (e.g., /hr/employees)
  • Module Detail: /{module}/{entity}/:id (e.g., /hr/employees/123)
  • Sub-Module: /{module}/{sub-module}/{entity} (e.g., /hr/ats/jobs)
Route Naming:
  • Use kebab-case: /hr/employee-directory not /hr/employeeDirectory
  • Use plural nouns for list routes: /hr/employees not /hr/employee
  • Use singular for detail routes when appropriate: /hr/employees/:id
When to Show:
  • Detail pages (3+ route segments)
  • Nested sub-module pages
  • Settings pages with hierarchy
When NOT to Show:
  • Module overview pages (/hr/dashboard)
  • Top-level list pages (/hr/employees)
  • Home/dashboard (/)
Breadcrumb Standards:
  • Desktop: Full breadcrumb trail in header (via Breadcrumbs.tsx)
  • Mobile: Truncated to last 2 segments (via MobileBreadcrumbs.tsx)
  • Depth Limit: Maximum 5 segments (truncate if deeper)
Implementation:
  • Auto-generated from route (via route-labels.ts)
  • Explicit breadcrumbs via PageContainer breadcrumbs prop for dynamic entity names

Module/Sub-Module Navigation

Navigation Modes:
  1. All Modules: Shows all available modules (home, module switcher)
  2. In Module: Shows module-specific navigation (sidebar with nav items)
  3. In Sub-Module: Shows sub-module navigation with “Back to Parent” button
Sub-Module Pattern:
  • Sub-modules have overviewRoute (e.g., /hr/ats)
  • Sub-modules have navGroups or navItems for sub-navigation
  • Sub-modules show context indicator in mobile header
Permission Filtering:
  • Navigation items filtered by minRole (V1) or permission (V2)
  • Use useFilteredNavItems hook for permission-aware filtering
Related Documentation:
  • docs/architecture/standards/NAVIGATION_STANDARD.md - Navigation patterns and policies
  • docs/development/mobile-navigation-guide.md - Mobile navigation patterns
  • docs/development/breadcrumb-implementation-guide.md - Breadcrumb implementation
  • src/platform/navigation/README.md - Navigation component documentation

Mobile-First Requirements

Touch Targets

Requirement: All interactive elements MUST meet minimum touch target size. Minimum Size: 44x44px (iOS HIG, Material Design) Implementation:
// ✅ CORRECT: Meets minimum
<Button size="icon" className="h-10 w-10"> {/* 40px, but padding makes it 44px+ */}
  <Icon />
</Button>

// ✅ CORRECT: Explicit minimum
<div className="min-h-[44px] min-w-[44px] flex items-center justify-center">
  <Icon />
</div>

// ❌ WRONG: Too small
<button className="h-8 w-8"> {/* 32px - too small */}
  <Icon />
</button>
Common Patterns:
  • Navigation items: min-h-[44px]
  • Icon buttons: h-10 w-10 (40px + padding = 44px+)
  • Bottom nav items: min-w-[56px] min-h-[56px] (generous for thumb reach)

Safe Areas

Requirement: Fixed elements (header, bottom nav) MUST respect safe area insets. Implementation:
// ✅ CORRECT: Use safe area utilities
<header className="fixed top-0 safe-area-top">
  {/* Header content */}
</header>

<nav className="fixed bottom-0 safe-area-bottom">
  {/* Bottom nav */}
</nav>
CSS Variables:
  • --safe-area-inset-top - iOS notch, Android status bar
  • --safe-area-inset-bottom - iOS home indicator, Android nav bar
  • --safe-area-inset-left / --safe-area-inset-right - Landscape safe areas
Utility Classes:
  • .safe-area-top - Adds top padding
  • .safe-area-bottom - Adds bottom padding
  • .safe-area-left / .safe-area-right - Adds side padding

Mobile Header Density

Requirement: Minimize vertical space consumed by mobile header chrome. Current Structure:
  1. Main header (64px) - Logo, menu, actions
  2. Org switcher (48px) - Organization/site selector
  3. Sub-module indicator (~40px, conditional) - Current sub-module context
  4. Breadcrumbs (~40px) - Navigation path
Guidelines:
  • Collapse When Possible: Combine org switcher into header row when space allows
  • Conditional Display: Show breadcrumbs only on 3+ depth routes
  • Sub-Module Indicator: Show only when in sub-module context
Target: Reduce total header height to < 120px on most pages.

Bottom Navigation

Requirement: Bottom nav provides quick access to key modules. Implementation:
  • Fixed bottom position with safe area inset
  • 4-5 primary shortcuts (configurable per user/role)
  • Long-press for module menu (if implemented)
  • Active state indicator
Touch Targets:
  • Minimum 56x56px per nav item (generous for thumb reach)
  • Adequate spacing between items
Related Documentation:
  • docs/development/mobile-navigation-guide.md - Complete mobile navigation guide

Performance UX

Skeleton Loaders

Requirement: All loading states MUST use skeleton loaders, never return null. Skeleton Standards by Page Type:

List Pages

<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
  {Array.from({ length: 6 }).map((_, i) => (
    <Card key={i}>
      <CardHeader>
        <Skeleton className="h-6 w-3/4" />
      </CardHeader>
      <CardContent>
        <Skeleton className="h-4 w-full mb-2" />
        <Skeleton className="h-4 w-2/3" />
      </CardContent>
    </Card>
  ))}
</div>

Detail Pages

<div className="space-y-6">
  <Skeleton className="h-8 w-1/3" /> {/* Title */}
  <Card>
    <CardHeader>
      <Skeleton className="h-6 w-1/4" />
    </CardHeader>
    <CardContent>
      <Skeleton className="h-4 w-full mb-4" />
      <Skeleton className="h-4 w-3/4" />
    </CardContent>
  </Card>
</div>

Dashboards

<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
  {Array.from({ length: 4 }).map((_, i) => (
    <Card key={i}>
      <CardContent className="p-6">
        <Skeleton className="h-4 w-1/2 mb-2" />
        <Skeleton className="h-8 w-3/4" />
      </CardContent>
    </Card>
  ))}
</div>

Route Code Splitting

Requirement: All route components MUST use React.lazy() for code splitting. Implementation:
// ✅ CORRECT: Lazy load route
const Dashboard = lazy(() => import("./platform/dashboard/Dashboard"));

// ❌ WRONG: Direct import
import Dashboard from "./platform/dashboard/Dashboard";
Suspense Boundaries:
<Suspense fallback={<RouteLoadingSkeleton />}>
  <Routes>
    <Route path="/" element={<Dashboard />} />
  </Routes>
</Suspense>

QueryClient Configuration

Requirement: QueryClient MUST have default staleTime and gcTime configured. Configuration:
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,  // 5 minutes
      gcTime: 10 * 60 * 1000,    // 10 minutes (formerly cacheTime)
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
});

Performance Targets

Lighthouse Scores:
  • Performance: 85+ (target: 90+)
  • Accessibility: 95+ (target: 100)
  • Best Practices: 90+ (target: 95+)
  • SEO: 90+ (target: 95+)
Core Web Vitals:
  • First Contentful Paint (FCP): < 2s
  • Largest Contentful Paint (LCP): < 2.5s
  • Cumulative Layout Shift (CLS): < 0.1
  • Time to Interactive (TTI): < 3.5s (on 3G)
Related Documentation:
  • constitution.md §[Performance Requirements] - Non-negotiable performance rules

Form UX Standards

Form Layout

Requirement: Use ResponsiveFormLayout for all multi-field forms. Import:
import { ResponsiveFormLayout } from '@/shared/components';
Usage:
<Form {...form}>
  <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
    {/* Full-width field */}
    <FormField name="title" ... />
    
    {/* Two-column layout */}
    <ResponsiveFormLayout columns={2} spacing="md">
      <FormField name="firstName" ... />
      <FormField name="lastName" ... />
    </ResponsiveFormLayout>
    
    {/* Form actions */}
    <div className="flex justify-end gap-3 pt-4">
      <Button variant="outline">Cancel</Button>
      <Button type="submit">Save</Button>
    </div>
  </form>
</Form>
Column Guidelines:
ColumnsUse Case
1Simple forms, mobile-first, long text inputs
2Most forms (default) - related field pairs
3Dense data entry forms only
Spacing:
ValueUse Case
smCompact forms, modal dialogs
mdStandard forms (default)
lgSpacious layouts, accessibility focus

Shared Field Pilot

Use Field, FieldGroup, FieldLabel, FieldDescription, and FieldError from @/shared/ui/field for simple controlled-field layouts that otherwise repeat label/helper/error markup outside React Hook Form.
<FieldGroup>
  <Field>
    <FieldLabel htmlFor="tenant-id">Tenant ID</FieldLabel>
    <Input id="tenant-id" value={tenantId} onChange={handleChange} />
    <FieldDescription>Used for Microsoft Entra ID single sign-on.</FieldDescription>
  </Field>
</FieldGroup>
Keep FormField / FormItem for React Hook Form validation flows. The field pilot is additive and should not replace mature form abstractions unless it reduces duplicated composition without changing behavior.

Help Text (FormDescription)

Always use for:
  • Sensitive data fields (account numbers, passwords)
  • Masked/transformed displays (“Will be masked as XXX-7890”)
  • Optional fields with non-obvious purpose
  • Fields requiring specific formats
Skip for:
  • Self-explanatory labels (Name, Email, Phone)
  • Required fields with obvious purpose
Example:
<FormField name="accountNumber" render={({ field }) => (
  <FormItem>
    <FormLabel>Account Number *</FormLabel>
    <FormControl><Input {...field} /></FormControl>
    <FormDescription>Will be masked in displays (XXX-7890)</FormDescription>
    <FormMessage />
  </FormItem>
)} />

Error Presentation

Standard: Inline errors only via FormMessage component. Guidelines:
  • Errors appear immediately below the field
  • Use FormMessage component (styled with text-destructive)
  • No error summary at form top (too noisy for most forms)
  • For complex forms with many errors, consider scrolling to first error
Example:
<FormField name="email" render={({ field }) => (
  <FormItem>
    <FormLabel>Email *</FormLabel>
    <FormControl><Input type="email" {...field} /></FormControl>
    <FormMessage /> {/* Shows error inline */}
  </FormItem>
)} />

Progressive Disclosure

Use when: Form has advanced/optional sections that most users don’t need. Pattern: Accordion or Collapsible for “Advanced Options”
<Accordion type="single" collapsible>
  <AccordionItem value="advanced">
    <AccordionTrigger>Advanced Options</AccordionTrigger>
    <AccordionContent>
      <ResponsiveFormLayout columns={2} spacing="md">
        <FormField name="advancedOption1" ... />
        <FormField name="advancedOption2" ... />
      </ResponsiveFormLayout>
    </AccordionContent>
  </AccordionItem>
</Accordion>
Default state: Collapsed (don’t overwhelm users with options)

Form Actions

Standard layout:
<div className="flex justify-end gap-3 pt-4">
  <Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
    Cancel
  </Button>
  <Button type="submit" disabled={isSubmitting}>
    {isSubmitting ? 'Saving...' : 'Save'}
  </Button>
</div>
Guidelines:
  • Right-aligned buttons (LTR layouts)
  • Cancel on left, Submit on right
  • Show loading state during submission
  • Disable buttons during submission
  • Use descriptive labels: “Create Account” vs “Submit”

Accessibility

Tabbed Hub Accessibility Checklist

For pages that use tabbed hubs (e.g. HR Recruiting, Credentialing, Workforce Analytics, Clinical Oversight):
  • Tab panels have correct ARIA: role="tabpanel" and aria-labelledby linking to the corresponding trigger (Radix/shadcn Tabs provide these by default).
  • Focus moves to the active panel on tab change when appropriate (Radix handles this).
  • Keyboard navigation: arrow keys move between tabs (Radix/shadcn support this).
  • Touch targets: tab triggers meet 44px minimum height (see src/shared/ui/tabs.tsx).
  • Hub routes are in scope for accessibility audits (e.g. axe-core); see ACCESSIBILITY_TESTING.md.

Keyboard Navigation

Requirement: All interactive elements MUST be keyboard accessible. Implementation:
  • Use semantic HTML (<button>, <a>, <input>, etc.)
  • Maintain logical tab order
  • Provide skip links for main content
  • Support arrow key navigation for lists/menus
Focus Management:
  • Visible focus indicators on all interactive elements
  • Focus trap in modals/dialogs
  • Return focus to trigger after closing modal

Focus Indicators

Requirement: All interactive elements MUST have visible focus indicators. Implementation:
// ✅ CORRECT: Visible focus ring
<Button className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
  Click me
</Button>

// ✅ CORRECT: Custom focus styles
.nav-focus-ring:focus-visible {
  outline: 2px solid hsl(var(--ring));
  outline-offset: 2px;
}
Standards:
  • 2px solid outline
  • Use --ring color token
  • 2px offset from element
  • Only visible on keyboard focus (:focus-visible)

ARIA Usage

Requirement: Use ARIA attributes when semantic HTML is insufficient. Common Patterns: Labels:
<label htmlFor="email">Email</label>
<input id="email" type="email" aria-describedby="email-help" />
<span id="email-help">We'll never share your email</span>
Roles:
<div role="alert" aria-live="polite">
  {errorMessage}
</div>
Live Regions:
  • aria-live="polite" - Non-urgent updates
  • aria-live="assertive" - Urgent updates
  • aria-atomic="true" - Announce entire region

Contrast Requirements

Requirement: All text MUST meet WCAG AA contrast ratios. Minimum Contrast:
  • Normal Text: 4.5:1 against background
  • Large Text (18pt+): 3:1 against background
  • UI Components: 3:1 for non-text elements (icons, borders)
Testing:
  • Use browser DevTools contrast checker
  • Use tools like WebAIM Contrast Checker
  • Test in both light and dark themes
Related Documentation:

Deep Dives

Implementation References

  • Design Tokens: src/index.css - CSS variable definitions
  • Tailwind token mapping: src/index.css (@theme inline) - Token mappings
  • Shared Components: src/shared/components/ - Page templates and layouts
  • UI Components: src/shared/ui/ - Base UI component library
  • Navigation Components: src/platform/navigation/ - Navigation system

Constitution & Guidelines

  • Constitution: constitution.md - Non-negotiable engineering guardrails
  • AI Guide: AI_GUIDE.md - AI contribution guidelines
  • Quick Reference: AGENTS.md - Condensed cross-platform reference

Shared Component Primitives

Location: src/shared/components/PageHeader.tsx Purpose: Standardized page header with title, description, and actions. Usage:
import { PageHeader } from '@/shared/components/PageHeader';

<PageHeader
  title="Employees"
  description="Manage your organization's workforce"
  actions={<Button>Add Employee</Button>}
/>
Props:
  • title (required) - Page title (h1)
  • description (optional) - Page subtitle
  • actions (optional) - Action buttons/components
  • breadcrumbs (optional) - Custom breadcrumb items

StatCard

Location: src/shared/components/StatCard.tsx Purpose: Standardized metric/stat card for dashboards. Usage:
import { StatCard } from '@/shared/components/StatCard';

<StatCard
  title="Total Employees"
  value={142}
  icon={Users}
  trend={{ value: 12, direction: 'up' }}
/>
Props:
  • title (required) - Metric label
  • value (required) - Metric value (number or string)
  • icon (optional) - Lucide icon component
  • trend (optional) - Trend indicator with value and direction
  • description (optional) - Additional context

EmptyState

Location: src/shared/components/EmptyState.tsx Purpose: Standardized empty state with icon, message, and action. Usage:
import { EmptyState } from '@/shared/components/EmptyState';

<EmptyState
  icon={FileText}
  title="No forms yet"
  description="Create your first form to get started"
  action={<Button onClick={createForm}>Create Form</Button>}
/>
MUST use for all empty lists and states. Never create ad-hoc empty state UIs.

CardActionsMenu

Location: src/shared/components/CardActionsMenu.tsx Purpose: Touch-safe action menu for cards with consistent patterns. Usage:
import { CardActionsMenu } from '@/shared/components/CardActionsMenu';

<CardActionsMenu
  actions={[
    { label: 'Edit', icon: Edit, onClick: handleEdit },
    { label: 'Delete', icon: Trash, onClick: handleDelete, variant: 'destructive' },
  ]}
/>
Requirements:
  • Minimum 44x44px touch target
  • Always visible (not hover-only)
  • Uses DropdownMenu for mobile-friendly interaction

Color Mapping Utilities

Reference: See docs/development/SEMANTIC_COLORS.md for complete documentation.

Import Pattern

import { 
  getStatusColors, 
  getScoreColor,
  semanticColorClasses,
  type SemanticStatus 
} from '@/shared/lib/semantic-colors';

Status Color Mapping

Map business statuses to semantic colors:
const statusMap: Record<string, SemanticStatus> = {
  active: 'success',
  pending: 'warning',
  expired: 'destructive',
  draft: 'muted',
};

const colors = getStatusColors(statusMap[status] || 'muted');
// Returns: { bg, bgSubtle, text, border, full }

Anti-Patterns

❌ WRONG: Hardcoded color maps
const getColor = (status: string) => {
  switch (status) {
    case 'active': return 'bg-green-100 text-green-800';
    case 'pending': return 'bg-yellow-100 text-yellow-800';
    default: return 'bg-gray-100 text-gray-800';
  }
};
✅ CORRECT: Semantic color mapping
import { getStatusColors } from '@/shared/lib/semantic-colors';

const statusMap = { active: 'success', pending: 'warning' } as const;
const colors = getStatusColors(statusMap[status] || 'muted');

Touch-Safe Action Patterns

Requirements

  1. Always visible actions - Never hide actions behind hover-only states
  2. 44x44px minimum - All touch targets must meet minimum size
  3. Menu for multiple actions - Use CardActionsMenu or DropdownMenu for 2+ actions
  4. Single primary action visible - Show most common action directly, others in menu

Hover-Only Anti-Pattern

❌ WRONG: Hover-only actions
<Card className="group">
  <CardActions className="opacity-0 group-hover:opacity-100">
    <Button size="icon" className="h-8 w-8">
      <Edit className="h-4 w-4" />
    </Button>
  </CardActions>
</Card>
✅ CORRECT: Always visible with proper touch targets
<Card>
  <CardActionsMenu
    actions={[
      { label: 'Edit', icon: Edit, onClick: handleEdit },
      { label: 'Delete', icon: Trash, onClick: handleDelete },
    ]}
  />
</Card>

Mobile Considerations

  • Use DropdownMenu instead of hover menus
  • Ensure action buttons are at least 44x44px
  • Place actions in consistent locations (top-right corner)
  • Use swipe gestures sparingly and always provide alternatives

Dialog vs Native Browser Dialogs

CRITICAL: Never Use Native Dialogs

Prohibited: window.prompt(), window.confirm(), window.alert() Native browser dialogs are:
  • Not accessible
  • Not stylable
  • Inconsistent across browsers
  • Not mobile-friendly
  • Cannot be canceled properly

Use Custom Dialogs Instead

For confirmations:
import { AlertDialog } from '@/shared/ui/alert-dialog';

<AlertDialog>
  <AlertDialogTrigger>Delete</AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Are you sure?</AlertDialogTitle>
      <AlertDialogDescription>
        This action cannot be undone.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction onClick={handleDelete}>Delete</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
For text input:
import { Dialog } from '@/shared/ui/dialog';
import { Input } from '@/shared/ui/input';

<Dialog>
  <DialogTrigger>Rename</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Rename Document</DialogTitle>
    </DialogHeader>
    <Input
      value={newName}
      onChange={(e) => setNewName(e.target.value)}
      placeholder="Enter new name"
    />
    <DialogFooter>
      <Button variant="outline" onClick={close}>Cancel</Button>
      <Button onClick={handleRename}>Rename</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
For notifications/alerts:
import { toast } from 'sonner';

// Success
toast.success('Document saved successfully');

// Error
toast.error('Failed to save document');

// Info
toast.info('Processing your request...');

Mobile Gestures

Spec Reference: PF-37 - Mobile Swipe Gestures All mobile interfaces should leverage gesture patterns for improved UX:

Required Gesture Patterns

PatternWhen to UseComponent
Swipe-to-DismissSheets, dialogs, toastsSwipeableSheet
Swipe ActionsList items with quick actionsSwipeableListItem
Pull-to-RefreshRefreshable data listsPullToRefresh
Edge SwipeDetail pages (back navigation)useEdgeSwipe

Implementation

import { SwipeableListItem, PullToRefresh } from '@/platform/gestures';

// List with swipe actions and pull-to-refresh
<PullToRefresh onRefresh={handleRefresh}>
  <ul>
    {items.map(item => (
      <SwipeableListItem
        key={item.id}
        leftActions={[
          { id: 'delete', label: 'Delete', colorClass: 'destructive', onClick: () => onDelete(item.id) }
        ]}
      >
        <ItemContent item={item} />
      </SwipeableListItem>
    ))}
  </ul>
</PullToRefresh>

Accessibility Requirements

  • All gestures MUST have keyboard alternatives
  • Escape closes sheets/dialogs
  • Delete triggers destructive actions
  • Tap/click fallback always available
Full Documentation: See Mobile Gesture Guide

Troubleshooting Common UI/UX Issues

Colors Not Applying Correctly

Problem: Semantic color tokens not working as expected. Solution:
// ❌ WRONG: Hardcoded colors
<div className="text-green-500">Success</div>

// ✅ CORRECT: Use semantic tokens
<div className="text-success">Success</div>
<Badge variant="success">Active</Badge>
Check:
  1. Verify token exists in src/index.css under @theme inline and/or root CSS variables
  2. Check CSS variables in src/index.css
  3. Ensure proper variant usage for components

Loading States Showing Blank Screen

Problem: Page shows blank screen during loading. Solution:
// ❌ WRONG: Return null
if (isLoading) return null;

// ✅ CORRECT: Use skeleton loader
if (isLoading) return <AppLoadingSkeleton />;

Components Not Responsive

Problem: Components don’t adapt to mobile screens. Solution:
  1. Check for fixed widths: Remove w-{fixed} classes
  2. Use responsive utilities: md:, lg: breakpoints
  3. Verify mobile-first approach: Start with mobile, enhance for desktop
  4. Test on actual devices: Use browser dev tools mobile emulation

Accessibility Issues

Problem: Components not accessible to screen readers. Solution:
// ❌ WRONG: Missing labels
<button onClick={handleDelete}>
  <Trash2 />
</button>

// ✅ CORRECT: Include accessibility attributes
<button 
  onClick={handleDelete}
  aria-label="Delete item"
>
  <Trash2 />
</button>

Performance Issues

Problem: Page loads slowly or feels sluggish. Solution:
  1. Check lazy loading: Ensure routes use React.lazy()
  2. Verify code splitting: Check bundle sizes
  3. Optimize images: Use appropriate formats and sizes
  4. Check query optimization: Review TanStack Query configuration
  5. See Performance Optimization Guide for detailed guidance

Design System Tokens Not Found

Problem: TypeScript errors for design system tokens. Solution:
  1. Verify token exists in src/index.css under @theme inline and/or root CSS variables
  2. Check TypeScript types: Ensure Tailwind types are generated
  3. Restart TypeScript server: Reload VS Code window
  4. Check import paths: Verify @/ alias configuration

Mobile Touch Targets Too Small

Problem: Buttons/links hard to tap on mobile. Solution:
// ❌ WRONG: Small touch target
<button className="p-2">Action</button>

// ✅ CORRECT: Minimum 44x44px
<button className="min-w-[44px] min-h-[44px] p-3">Action</button>

Inconsistent Component Variants

Problem: Same component looks different in different places. Solution:
  1. Use standard variants: variant="success", variant="destructive"
  2. Avoid custom styling: Don’t override component styles
  3. Check design system: Verify variant exists in component definition
  4. Use semantic tokens: Don’t hardcode colors

Version: 1.6.1 Maintained By: Platform Foundation Team