npm package: @omnifyjp/ui
Description: A comprehensive UI component library for React apps. Contains 67 Shadcn/Radix primitives and 14 domain-specific components with a complete design token system.
Latest version: 2.3.3

# @omnifyjp/ui

A comprehensive UI component library for React apps. Contains 67 Shadcn/Radix primitives and 14 domain-specific components with a complete design token system.

## Installation

```bash
npm install @omnifyjp/ui
```

### Peer Dependencies

```bash
npm install react react-dom react-router
```

---

## Quick Start

```tsx
import { createBrowserRouter, RouterProvider } from 'react-router';
import { AppShell, initOmnifyI18n } from '@omnifyjp/ui';
import type { ShellConfig } from '@omnifyjp/ui';
import { LayoutDashboard, Users, Settings } from 'lucide-react';

// 1. Initialize i18n with your service translations
import enLocale from './locales/en.json';
import viLocale from './locales/vi.json';

initOmnifyI18n({
  namespaces: {
    myapp: { en: enLocale, vi: viLocale },
  },
});

// 2. Define shell configuration
const config: ShellConfig = {
  app: {
    name: 'My App',
    key: 'myapp',
    logo: { icon: LayoutDashboard, text: 'My App', color: 'text-blue-600' },
  },
  sidebar: {
    menuItems: [
      { icon: LayoutDashboard, label: 'Dashboard', path: '/' },
      { icon: Users, label: 'Users', path: '/users' },
      { icon: Settings, label: 'Settings', path: '/settings' },
    ],
  },
  header: {
    user: {
      name: 'John Doe',
      email: 'john@example.com',
      onLogout: () => console.log('logout'),
    },
  },
  organization: {
    organizations: [
      { id: '1', name: 'Acme Corp', shortName: 'AC' },
    ],
    branches: [
      { id: '1', name: 'Tokyo Office', organizationId: '1', location: 'Tokyo' },
      { id: '2', name: 'Hanoi Office', organizationId: '1', location: 'Hanoi' },
    ],
  },
};

// 3. Set up routes with AppShell as the layout
const router = createBrowserRouter([
  {
    path: '/',
    element: <AppShell config={config} />,
    children: [
      { index: true, element: <Dashboard /> },
      { path: 'users', element: <Users /> },
      { path: 'settings', element: <Settings /> },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}
```

---

## Core Components

### AppShell

The main application wrapper. Combines `ThemeProvider` + `I18nextProvider` + `OrganizationProvider`, then renders the sidebar, header, and a React Router `<Outlet />`.

```tsx
import { AppShell } from '@omnifyjp/ui';

<AppShell config={config} extra={<ChatWidget />} />
```

| Prop | Type | Description |
|------|------|-------------|
| `config` | `ShellConfig` | Full shell configuration (see below) |
| `extra` | `ReactNode` | Optional floating elements (e.g., chat widget) |

### Sidebar

Collapsible left navigation with context switching, expandable groups, and tooltips when collapsed.

Features:
- Logo area with app icon
- Main menu items with nested groups
- Context switching popover (e.g., project switcher with search)
- Recent items section
- Collapse/expand toggle
- Organization selector badge

### Header

Top navigation bar with:
- Organization/branch selector
- Service menu (grid of available apps)
- Global search input
- Notification dropdown with count badge
- Custom action buttons
- User menu (profile, settings, logout)

### PageContainer

Flexible page layout wrapper with three variants:

```tsx
import {
  StandardPageContainer,
  SplitPageContainer,
  FullWidthPageContainer,
} from '@omnifyjp/ui';

// Standard — padded page with title bar
<StandardPageContainer title="Users" subtitle="Manage team members" extra={<Button>Add User</Button>}>
  <UserTable />
</StandardPageContainer>

// Split — two-column layout with sidebar
<SplitPageContainer title="Settings" sidebar={<SettingsNav />} sidebarPosition="left">
  <SettingsForm />
</SplitPageContainer>

// Full width — no padding (for Kanban boards, Gantt charts)
<FullWidthPageContainer>
  <KanbanBoard />
</FullWidthPageContainer>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `string` | — | Page heading |
| `subtitle` | `string` | — | Description text below title |
| `extra` | `ReactNode` | — | Header actions (buttons, etc.) |
| `children` | `ReactNode` | — | Main content |
| `footer` | `ReactNode` | — | Footer content |
| `variant` | `'standard' \| 'split' \| 'full'` | `'standard'` | Layout mode |
| `sidebar` | `ReactNode` | — | Sidebar content (split mode) |
| `sidebarPosition` | `'left' \| 'right'` | `'right'` | Sidebar placement |
| `sidebarWidth` | `string` | `'w-80'` | Sidebar Tailwind width class |

### OrganizationSelector

Two-step modal for switching organizations and branches:

1. **Step 1**: Select organization (shows description + branch count)
2. **Step 2**: Select branch (searchable, shows location)

```tsx
import { OrganizationSelector } from '@omnifyjp/ui';

<OrganizationSelector variant="header" />  // in header
<OrganizationSelector variant="sidebar" /> // in sidebar
```

### ServiceMenu

Searchable dropdown grid listing all available services/apps:

```tsx
const config: ShellConfig = {
  services: [
    {
      category: 'HR',
      items: [
        { icon: Users, label: 'Attendance', url: '/attendance', color: 'bg-blue-600' },
        { icon: Calendar, label: 'Leave', url: '/leave', color: 'bg-green-600' },
      ],
    },
  ],
};
```

---

## Configuration: `ShellConfig`

```typescript
interface ShellConfig {
  app: {
    name: string;                    // App display name
    key: string;                     // App identifier
    logo: {
      icon: LucideIcon;             // Logo icon
      text: string;                  // Logo text
      color?: string;                // Icon color class
    };
  };

  sidebar: {
    menuItems: SidebarMenuItem[];    // Navigation items
    contextMenu?: {                  // Context switcher (e.g., project selector)
      paramName: string;             // URL param name
      getItems: () => ContextItem[];
      getHeader?: () => ReactNode;
      getFooterItems?: () => ContextItem[];
    };
    recentItems?: {                  // Recent items section
      title: string;
      items: { label: string; path: string; badge?: string; color?: string }[];
    };
    footer?: ReactNode;              // Custom sidebar footer
  };

  services?: ServiceCategory[];      // Service menu grid

  header?: {
    searchPlaceholder?: string;
    showSearch?: boolean;            // Default: true
    actions?: ReactNode;             // Custom header buttons
    notifications?: {
      count: number;
      content: ReactNode;
    };
    user?: {
      name: string;
      email?: string;
      avatar?: string;
      onLogout?: () => void;
    };
  };

  organization?: {
    organizations: Organization[];
    branches: Branch[];
    onOrganizationChange?: (org: Organization) => void;
    onBranchChange?: (branch: Branch) => void;
  };
}
```

### SidebarMenuItem

```typescript
interface SidebarMenuItem {
  icon: LucideIcon;
  label: string;
  path?: string;                     // Omit for collapsible groups
  badge?: number;                    // Notification count
  children?: SidebarMenuItem[];      // Nested items
}
```

---

## Context Providers

### ThemeProvider

Dark mode management with system preference detection:

```tsx
import { useTheme } from '@omnifyjp/ui';

function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  // theme: 'light' | 'dark' | 'system'

  return (
    <select value={theme} onChange={(e) => setTheme(e.target.value)}>
      <option value="light">Light</option>
      <option value="dark">Dark</option>
      <option value="system">System</option>
    </select>
  );
}
```

Persists selection to `localStorage.omnify_theme`.

### OrganizationProvider

Multi-tenant organization/branch management:

```tsx
import { useOrganization } from '@omnifyjp/ui';

function OrgInfo() {
  const {
    selectedOrganization,   // Organization | null
    selectedBranch,         // Branch | null
    setSelectedOrganization,
    setSelectedBranch,
    getBranchesByOrg,       // (orgId: string) => Branch[]
    isSetupComplete,        // true when both selected
  } = useOrganization();

  return <p>{selectedOrganization?.name} — {selectedBranch?.name}</p>;
}
```

Persists selections to `localStorage.selectedOrganizationId` and `localStorage.selectedBranchId`.

---

## i18n (Internationalization)

### Supported Languages

| Code | Language |
|------|----------|
| `vi` | Tieng Viet (default, fallback) |
| `en` | English |
| `ja` | Japanese |

### Shell Namespace

The shell comes pre-loaded with translations for common UI strings: sidebar labels, header actions, status/priority names, settings pages, login forms, organization selection, date picker, etc.

### Adding Service Translations

```tsx
import { initOmnifyI18n } from '@omnifyjp/ui';

// Add your service namespace
initOmnifyI18n({
  namespaces: {
    myapp: {
      en: { dashboard: { title: 'Dashboard' } },
      vi: { dashboard: { title: 'Bang dieu khien' } },
      ja: { dashboard: { title: 'dasshubodo' } },
    },
  },
});

// Use in components
import { useTranslation } from 'react-i18next';

function Dashboard() {
  const { t } = useTranslation(['myapp', 'shell']);
  return <h1>{t('dashboard.title')}</h1>;
  // Falls back to 'shell' namespace for common keys
}
```

### Changing Language

```tsx
import { changeLanguage } from '@omnifyjp/ui';

await changeLanguage('en'); // Persists to localStorage
```

---

## Hooks

### useDateFormat

Locale-aware date formatting with timezone support:

```tsx
import { useDateFormat } from '@omnifyjp/ui';

function EventDate({ date }: { date: Date }) {
  const {
    formatDate,           // '01/15/2026' (locale-dependent)
    formatDateTime,       // '01/15/2026 14:30'
    formatRelativeTime,   // '5 minutes ago'
    formatShortDate,      // '01/15'
    formatMonthYear,      // 'January 2026'
    formatDayOfWeek,      // 'Mon'
    formatDateLong,       // 'January 15, 2026'
    timezone,             // 'Asia/Ho_Chi_Minh'
    setTimezone,          // Change timezone
    dateFnsLocale,        // date-fns Locale object
  } = useDateFormat();

  return <time>{formatDateTime(date)}</time>;
}
```

Supported timezones: `Asia/Ho_Chi_Minh`, `Asia/Tokyo`, `America/New_York`, `America/Los_Angeles`, `Europe/London`, `UTC`.

---

## Package Exports

```
@omnifyjp/ui                             → all exports (barrel)
@omnifyjp/ui/components/AppShell      → AppShell
@omnifyjp/ui/components/Sidebar       → Sidebar
@omnifyjp/ui/components/Header        → Header
@omnifyjp/ui/components/PageContainer → PageContainer, Standard/Split/FullWidth variants
@omnifyjp/ui/components/ServiceMenu   → ServiceMenu
@omnifyjp/ui/components/OrganizationSelector → OrganizationSelector
@omnifyjp/ui/contexts/ThemeContext    → ThemeProvider, useTheme
@omnifyjp/ui/contexts/OrganizationContext → OrganizationProvider, useOrganization
@omnifyjp/ui/hooks/useDateFormat      → useDateFormat, timezoneLabels
@omnifyjp/ui/i18n                     → i18n, initOmnifyI18n, changeLanguage
@omnifyjp/ui/types                    → ShellConfig, Organization, Branch, etc.
```

---

## TypeScript

Full type definitions are included. Key types:

```typescript
import type {
  ShellConfig,
  SidebarMenuItem,
  ServiceCategory,
  ServiceItem,
  Organization,
  Branch,
  PageContainerProps,
} from '@omnifyjp/ui';
```

---

## Dependencies

This package depends on:

| Package | Purpose |
|---------|---------|
|  `@omnifyjp/ui` | UI component primitives (auto-installed) |
| `i18next` | Internationalization core |
| `react-i18next` | React i18n bindings |
| `date-fns` | Date formatting |
| `lucide-react` | Icons |
| `sonner` | Toast notifications |

Peer dependencies (you provide):

| Package | Version |
|---------|---------|
| `react` | >=18 |
| `react-dom` | >=18 |
| `react-router` | >=7 |

---

## Related Packages

| Package | Description |
|---------|-------------|
| [`@omnifyjp/ui`](https://www.npmjs.com/package/@omnifyjp/ui) | 53 Shadcn primitives + 14 domain components |
| [`@omnifyjp/editor`](https://www.npmjs.com/package/@omnifyjp/editor) | Rich text editors (Tiptap + BlockNote) |

---

## License

MIT