UX Standards — 25: Shared Components and Monorepo Architecture
Governs: How shared UI components, hooks, styles, and utilities are structured and consumed across multiple Anshin frontend applications.
Parent rules: See 00-OVERVIEW-AND-CSS-RULES.md first.
Overview
All Anshin Health frontend applications (Orchestrate, Accelerate, Admin portal, etc.) share a common component library via the @anshin/shared package. This ensures visual and behavioral consistency across products without duplicating code.
Package Structure
orchestrate-frontend/
├── packages/
│ └── shared/ ← @anshin/shared package
│ ├── package.json name: "@anshin/shared"
│ ├── index.ts root barrel export
│ ├── components/
│ │ ├── layouts/ header, sidebar, footer, nav
│ │ ├── data-table/ universal data table components
│ │ ├── shared/ universal UI components
│ │ ├── forms/ form field wrappers
│ │ └── ui/ shadcn/ui re-exports
│ ├── hooks/ shared React hooks
│ ├── lib/
│ │ ├── frappe/ Frappe API client
│ │ ├── oauth2/ OAuth2 authentication
│ │ ├── i18n/ i18n utilities
│ │ ├── export/ CSV/Excel export
│ │ └── popup/ Persistent popup utilities
│ ├── styles/ shared CSS (see doc 24)
│ └── tsconfig.json
├── src/ ← Orchestrate app
├── admin/ ← Admin portal app
└── package.json workspaces config
Package Identity
{
"name": "@anshin/shared",
"private": true,
"exports": {
"./components/*": "./components/*",
"./lib/*": "./lib/*",
"./styles/*": "./styles/*"
},
"peerDependencies": {
"react": ">=19.0.0",
"react-dom": ">=19.0.0",
"next": ">=15.0.0"
}
}
The package is private: true — it's a workspace package, never published to npm.
Import Patterns
There are three ways to import from shared, in order of preference:
1. Path Alias (Most Common)
Apps configure a @/ alias that resolves to their src/ directory. Shared components are re-exported or referenced from within the app's own source tree:
// Import a shared layout component
import { Header } from '@/components/layouts/header';
// Import a shared data table component
import { DataTable } from '@/components/data-table';
2. Shared Package Path Export
For direct imports from the shared package (avoids re-exporting in app):
import { StatusBadge } from '@anshin/shared/components/shared';
import { openPersistentPopup } from '@anshin/shared/lib/popup';
3. Barrel Import (Less Specific)
import { StatusBadge, DataTable } from '@anshin/shared';
Recommendation: Use pattern 1 (path alias) within an app. Use pattern 2 for cross-app utilities. Avoid pattern 3 for tree-shaking efficiency.
Component Categories
1. components/layouts/ — Application Shell
These components form the outer shell of every page. All Anshin apps MUST use these.
| File | Component | Purpose |
|---|---|---|
header.tsx | AppHeader | Top header bar (contains HeaderLeft + HeaderRight) |
header-sections.tsx | HeaderRight | Right zone: search, language, notifications, helpdesk, Anna, user menu |
header-user-menu.tsx | UserMenu | User avatar dropdown (Profile, Settings, Log out) |
header-anna-button.tsx | AnnaButton | Anna AI popup trigger |
header-helpdesk-sheet.tsx | HelpdeskSheet | Frappe HelpDesk slide-out panel |
app-sidebar.tsx | AppSidebar | Left sidebar with logo, navigation, user widget |
sidebar-nav.tsx | SidebarNav | Nav item rendering with HoverCard tooltips |
sidebar-user.tsx | SidebarUser | User widget at sidebar bottom |
footer.tsx | AppFooter | Page footer |
language-selector.tsx | LanguageSelector | Language dropdown |
mode-toggle.tsx | ModeToggle | Light/Dark/System theme toggle |
Layout Usage in App:
// app/(dashboard)/layout.tsx
import { AppSidebar } from '@/components/layouts/app-sidebar';
import { AppHeader } from '@/components/layouts/header';
export default function DashboardLayout({ children }) {
return (
<SidebarProvider>
<AppSidebar />
<div className="flex flex-1 flex-col">
<AppHeader />
<main>{children}</main>
</div>
</SidebarProvider>
);
}
2. components/data-table/ — Universal Data Table
All list pages use these components (see 07-UNIVERSAL-DATA-TABLE.md):
| Component | Purpose |
|---|---|
DataTable (via config.ts) | Full UDT with TanStack Table |
data-table-toolbar.tsx | Search, filters, column selector, export |
data-table-column-selector.tsx | Column visibility toggle (ResizableDialog) |
data-table-column-header.tsx | Sortable column header with sort indicators |
data-table-pagination.tsx | Page size + page navigation |
data-table-row-actions.tsx | ... dropdown for row-level actions |
data-table-icon-button.tsx | Icon-only toolbar button |
data-table-sortable.tsx | Drag-to-reorder row support |
resizable-dialog.tsx | ResizableDialogContent — draggable+resizable dialog |
delete-dialog.tsx | Confirm delete alert dialog |
hooks/ | useDocTypeTable, useAnnaTableContext, etc. |
types.ts | Column definition types |
3. components/shared/ — Universal UI Components
General-purpose components used across pages:
| Component | Purpose |
|---|---|
StatusBadge | Color-coded status pill (wraps shadcn Badge) |
DetailRow | Label + value row for detail pages |
MetadataCard | Card showing record metadata (created, modified, etc.) |
InfoBanner | Dismissible information banner |
ErrorState | Full-page or inline error display |
LoadingState | Skeleton or spinner loading display |
4. components/forms/ — Form Components
form-field.tsx → FormField wrapper with label, error, and help text
Used with React Hook Form + Zod. Wraps the shadcn FormField pattern with consistent label placement and error display.
5. components/ui/ — shadcn/ui Re-exports
The shared package re-exports shadcn/ui components to ensure all apps use the same version and configuration. Never import shadcn components directly from @radix-ui/* — always use the @/components/ui/ path which maps to these shared components.
Nav Configuration System
Navigation is defined declaratively, not hardcoded in components:
components/layouts/
├── nav-config.ts ← App-specific nav config type
├── nav-main-config.ts ← Main sidebar nav items
├── nav-admin-config.ts ← Admin-specific nav items
├── nav-types.ts ← NavItem, NavGroup types
├── page-titles.ts ← Page title registry (auto page titles)
├── page-titles-main.ts ← Main app page titles
├── page-titles-admin.ts ← Admin app page titles
├── page-title-types.ts ← PageTitle type
└── page-title-override-context.tsx ← Context for dynamic page title overrides
Nav Item Type
interface NavItem {
title: string; // Display label
href: string; // Route path
icon: LucideIcon; // Lucide icon component
badge?: string; // Optional badge text (e.g., "New")
children?: NavItem[]; // Sub-items for collapsible groups
}
Dynamic Page Titles
The page-title-override-context.tsx allows pages to override their header title dynamically (e.g., showing the record name instead of "Connector Detail"):
import { usePageTitle } from '@/components/layouts/page-title-override-context';
// In a detail page:
const { setPageTitle } = usePageTitle();
useEffect(() => {
setPageTitle(connector.name);
return () => setPageTitle(null);
}, [connector.name]);
Shared Library Utilities
lib/frappe/ — Frappe BFF API Client
Provides typed API functions for all BFF endpoints:
import { frappeClient } from '@/lib/frappe';
// Generic DocType fetch
const records = await frappeClient.getList('Booking', { filters, orderBy });
const record = await frappeClient.getDoc('Booking', name);
lib/oauth2/ — Authentication
OAuth2 + JWT token management. Used by useAuth() hook.
lib/i18n/ — Internationalization
i18n utilities. Used by useTranslation() and useLocalization() hooks.
lib/export/ — Data Export
CSV/Excel export utilities used by data table toolbar:
import { exportToCSV, exportToExcel } from '@/lib/export';
exportToCSV(rows, columns, 'filename.csv');
lib/popup/ — Persistent Popup Windows
import { openPersistentPopup } from '@/lib/popup';
openPersistentPopup('policy', '/policies/privacy', 'policy-window');
See 12-POPOUT-AND-SLIDE-OVER-WINDOWS.md for full popup documentation.
Extending vs Overriding Shared Components
When to EXTEND
Add app-specific behavior in the app's own source tree:
// src/components/layouts/header.tsx (app-level wrapper)
import { AppHeader as SharedHeader } from '@anshin/shared/components/layouts/header';
export function AppHeader() {
const anna = useAnna();
const t = useTranslation();
return (
<SharedHeader
anna={anna}
t={t}
notificationSlot={<NotificationPopover />} // App-specific
tenantFilterSlot={<TenantFilter />} // App-specific
/>
);
}
Use slot props to inject app-specific content without modifying the shared component.
When to FORK (Copy to App)
Fork when:
- The component needs fundamentally different behavior for one specific app
- The changes would break other apps using the shared version
Forked components live in src/components/ (not packages/shared/) and are NOT re-exported.
When to ADD to Shared
Add to packages/shared when:
- The component is used by 2+ apps
- The component implements a UX Standard documented here
- The component has no app-specific dependencies
Adding a New Shared Component
- Create file:
packages/shared/components/{category}/my-component.tsx - Add export to
packages/shared/components/{category}/index.ts - If it should be in the root barrel: add to
packages/shared/index.ts - Document it in the relevant UX Standards doc (or create a new one)
// packages/shared/components/shared/my-component.tsx
'use client';
export interface MyComponentProps {
// ...
}
export function MyComponent({ ...props }: MyComponentProps) {
// ...
}
Multiple Apps in the Monorepo
The monorepo currently contains two frontend apps that both consume @anshin/shared:
| App | Directory | Description |
|---|---|---|
| Orchestrate Main | src/ | Full dashboard at app-*.orchestrate.anshin.us |
| Admin Portal | admin/ | Admin-only portal |
Both apps:
- Have their own
app/globals.css(with@tailwinddirectives) - Import shared styles via
@importfrompackages/shared/styles/globals.css - Import shared components via path aliases
- Use the same design tokens and shadcn/ui components
- Maintain their own nav configuration (from
nav-main-config.tsvsnav-admin-config.ts)
Source Reference Folder
Key source files that are frequently referenced when creating new components:
packages/shared/components/layouts/
├── header-sections.tsx ← Canonical header right zone implementation
├── header-user-menu.tsx ← User menu spec
├── sidebar-user.tsx ← Sidebar user widget spec
├── language-selector.tsx ← Language selector spec
packages/shared/components/data-table/
├── config.ts ← DataTable configuration (useDocTypeTable)
├── data-table-column-selector.tsx ← ResizableDialogContent pattern
├── resizable-dialog.tsx ← Draggable resizable dialog
packages/shared/lib/popup/
├── persistent-popup.ts ← openPersistentPopup() utility
packages/shared/styles/
├── _variables.css ← All design tokens
├── _typography.css ← Semantic typography classes
├── _layout.css ← Layout component classes
Violation Checklist
- All Anshin apps use layout components from
packages/shared/components/layouts/ - Never duplicate shared components in app source — extend via slot props instead
- Import shadcn/ui components via
@/components/ui/(NOT directly from@radix-ui/*) - App-level
globals.cssdeclares@tailwinddirectives (NOT in shared package) - App-level
globals.cssimports shared styles via@import - Navigation defined in
nav-*-config.ts(NOT hardcoded in sidebar components) - New components used by 2+ apps go in
packages/shared/(NOT duplicated) -
@anshin/sharedpackage isprivate: true— never publish to npm - Peer dependencies: React 19+, Next.js 15+
- All exports go through barrel
index.tsfiles at each level