Skip to main content

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.

FileComponentPurpose
header.tsxAppHeaderTop header bar (contains HeaderLeft + HeaderRight)
header-sections.tsxHeaderRightRight zone: search, language, notifications, helpdesk, Anna, user menu
header-user-menu.tsxUserMenuUser avatar dropdown (Profile, Settings, Log out)
header-anna-button.tsxAnnaButtonAnna AI popup trigger
header-helpdesk-sheet.tsxHelpdeskSheetFrappe HelpDesk slide-out panel
app-sidebar.tsxAppSidebarLeft sidebar with logo, navigation, user widget
sidebar-nav.tsxSidebarNavNav item rendering with HoverCard tooltips
sidebar-user.tsxSidebarUserUser widget at sidebar bottom
footer.tsxAppFooterPage footer
language-selector.tsxLanguageSelectorLanguage dropdown
mode-toggle.tsxModeToggleLight/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):

ComponentPurpose
DataTable (via config.ts)Full UDT with TanStack Table
data-table-toolbar.tsxSearch, filters, column selector, export
data-table-column-selector.tsxColumn visibility toggle (ResizableDialog)
data-table-column-header.tsxSortable column header with sort indicators
data-table-pagination.tsxPage size + page navigation
data-table-row-actions.tsx... dropdown for row-level actions
data-table-icon-button.tsxIcon-only toolbar button
data-table-sortable.tsxDrag-to-reorder row support
resizable-dialog.tsxResizableDialogContent — draggable+resizable dialog
delete-dialog.tsxConfirm delete alert dialog
hooks/useDocTypeTable, useAnnaTableContext, etc.
types.tsColumn definition types

3. components/shared/ — Universal UI Components

General-purpose components used across pages:

ComponentPurpose
StatusBadgeColor-coded status pill (wraps shadcn Badge)
DetailRowLabel + value row for detail pages
MetadataCardCard showing record metadata (created, modified, etc.)
InfoBannerDismissible information banner
ErrorStateFull-page or inline error display
LoadingStateSkeleton 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.


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
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

  1. Create file: packages/shared/components/{category}/my-component.tsx
  2. Add export to packages/shared/components/{category}/index.ts
  3. If it should be in the root barrel: add to packages/shared/index.ts
  4. 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:

AppDirectoryDescription
Orchestrate Mainsrc/Full dashboard at app-*.orchestrate.anshin.us
Admin Portaladmin/Admin-only portal

Both apps:

  • Have their own app/globals.css (with @tailwind directives)
  • Import shared styles via @import from packages/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.ts vs nav-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.css declares @tailwind directives (NOT in shared package)
  • App-level globals.css imports 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/shared package is private: true — never publish to npm
  • Peer dependencies: React 19+, Next.js 15+
  • All exports go through barrel index.ts files at each level