@omnifyjp/shell 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +476 -0
- package/dist/chunk-6JYWZJEY.js +123 -0
- package/dist/chunk-6JYWZJEY.js.map +1 -0
- package/dist/chunk-ACCHC3AM.js +57 -0
- package/dist/chunk-ACCHC3AM.js.map +1 -0
- package/dist/chunk-EJEVW4RO.js +49 -0
- package/dist/chunk-EJEVW4RO.js.map +1 -0
- package/dist/chunk-OHORC3F5.js +72 -0
- package/dist/chunk-OHORC3F5.js.map +1 -0
- package/dist/chunk-OMIE3Z5N.js +661 -0
- package/dist/chunk-OMIE3Z5N.js.map +1 -0
- package/dist/chunk-OYE3TXTK.js +37 -0
- package/dist/chunk-OYE3TXTK.js.map +1 -0
- package/dist/chunk-Q3QWQG6P.js +91 -0
- package/dist/chunk-Q3QWQG6P.js.map +1 -0
- package/dist/chunk-QNCYBLHC.js +189 -0
- package/dist/chunk-QNCYBLHC.js.map +1 -0
- package/dist/chunk-SHHZRZMM.js +83 -0
- package/dist/chunk-SHHZRZMM.js.map +1 -0
- package/dist/chunk-WCRLQ5M5.js +235 -0
- package/dist/chunk-WCRLQ5M5.js.map +1 -0
- package/dist/chunk-YVUVYTVZ.js +224 -0
- package/dist/chunk-YVUVYTVZ.js.map +1 -0
- package/dist/components/AppShell.d.ts +27 -0
- package/dist/components/AppShell.js +11 -0
- package/dist/components/AppShell.js.map +1 -0
- package/dist/components/Header.d.ts +11 -0
- package/dist/components/Header.js +6 -0
- package/dist/components/Header.js.map +1 -0
- package/dist/components/OrganizationSelector.d.ts +8 -0
- package/dist/components/OrganizationSelector.js +4 -0
- package/dist/components/OrganizationSelector.js.map +1 -0
- package/dist/components/OrganizationSetupModal.d.ts +5 -0
- package/dist/components/OrganizationSetupModal.js +4 -0
- package/dist/components/OrganizationSetupModal.js.map +1 -0
- package/dist/components/PageContainer.d.ts +105 -0
- package/dist/components/PageContainer.js +3 -0
- package/dist/components/PageContainer.js.map +1 -0
- package/dist/components/ServiceMenu.d.ts +11 -0
- package/dist/components/ServiceMenu.js +3 -0
- package/dist/components/ServiceMenu.js.map +1 -0
- package/dist/components/Sidebar.d.ts +11 -0
- package/dist/components/Sidebar.js +5 -0
- package/dist/components/Sidebar.js.map +1 -0
- package/dist/contexts/OrganizationContext.d.ts +26 -0
- package/dist/contexts/OrganizationContext.js +3 -0
- package/dist/contexts/OrganizationContext.js.map +1 -0
- package/dist/contexts/ThemeContext.d.ts +14 -0
- package/dist/contexts/ThemeContext.js +3 -0
- package/dist/contexts/ThemeContext.js.map +1 -0
- package/dist/hooks/useDateFormat.d.ts +28 -0
- package/dist/hooks/useDateFormat.js +4 -0
- package/dist/hooks/useDateFormat.js.map +1 -0
- package/dist/i18n.d.ts +38 -0
- package/dist/i18n.js +3 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# @omnifyjp/shell
|
|
2
|
+
|
|
3
|
+
A production-ready application shell framework for multi-tenant React apps. Provides a complete layout system with collapsible sidebar, header with organization switching, theme management, internationalization (vi/en/ja), and flexible page containers.
|
|
4
|
+
|
|
5
|
+
Built on top of [`@omnifyjp/ui`](https://www.npmjs.com/package/@omnifyjp/ui) components.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @omnifyjp/shell
|
|
11
|
+
# This automatically installs @omnifyjp/ui as a dependency
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Peer Dependencies
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install react react-dom react-router
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { createBrowserRouter, RouterProvider } from 'react-router';
|
|
26
|
+
import { AppShell, initOmnifyI18n } from '@omnifyjp/shell';
|
|
27
|
+
import type { ShellConfig } from '@omnifyjp/shell';
|
|
28
|
+
import { LayoutDashboard, Users, Settings } from 'lucide-react';
|
|
29
|
+
|
|
30
|
+
// 1. Initialize i18n with your service translations
|
|
31
|
+
import enLocale from './locales/en.json';
|
|
32
|
+
import viLocale from './locales/vi.json';
|
|
33
|
+
|
|
34
|
+
initOmnifyI18n({
|
|
35
|
+
namespaces: {
|
|
36
|
+
myapp: { en: enLocale, vi: viLocale },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 2. Define shell configuration
|
|
41
|
+
const config: ShellConfig = {
|
|
42
|
+
app: {
|
|
43
|
+
name: 'My App',
|
|
44
|
+
key: 'myapp',
|
|
45
|
+
logo: { icon: LayoutDashboard, text: 'My App', color: 'text-blue-600' },
|
|
46
|
+
},
|
|
47
|
+
sidebar: {
|
|
48
|
+
menuItems: [
|
|
49
|
+
{ icon: LayoutDashboard, label: 'Dashboard', path: '/' },
|
|
50
|
+
{ icon: Users, label: 'Users', path: '/users' },
|
|
51
|
+
{ icon: Settings, label: 'Settings', path: '/settings' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
header: {
|
|
55
|
+
user: {
|
|
56
|
+
name: 'John Doe',
|
|
57
|
+
email: 'john@example.com',
|
|
58
|
+
onLogout: () => console.log('logout'),
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
organization: {
|
|
62
|
+
organizations: [
|
|
63
|
+
{ id: '1', name: 'Acme Corp', shortName: 'AC' },
|
|
64
|
+
],
|
|
65
|
+
branches: [
|
|
66
|
+
{ id: '1', name: 'Tokyo Office', organizationId: '1', location: 'Tokyo' },
|
|
67
|
+
{ id: '2', name: 'Hanoi Office', organizationId: '1', location: 'Hanoi' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// 3. Set up routes with AppShell as the layout
|
|
73
|
+
const router = createBrowserRouter([
|
|
74
|
+
{
|
|
75
|
+
path: '/',
|
|
76
|
+
element: <AppShell config={config} />,
|
|
77
|
+
children: [
|
|
78
|
+
{ index: true, element: <Dashboard /> },
|
|
79
|
+
{ path: 'users', element: <Users /> },
|
|
80
|
+
{ path: 'settings', element: <Settings /> },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
function App() {
|
|
86
|
+
return <RouterProvider router={router} />;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Core Components
|
|
93
|
+
|
|
94
|
+
### AppShell
|
|
95
|
+
|
|
96
|
+
The main application wrapper. Combines `ThemeProvider` + `I18nextProvider` + `OrganizationProvider`, then renders the sidebar, header, and a React Router `<Outlet />`.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { AppShell } from '@omnifyjp/shell';
|
|
100
|
+
|
|
101
|
+
<AppShell config={config} extra={<ChatWidget />} />
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Prop | Type | Description |
|
|
105
|
+
|------|------|-------------|
|
|
106
|
+
| `config` | `ShellConfig` | Full shell configuration (see below) |
|
|
107
|
+
| `extra` | `ReactNode` | Optional floating elements (e.g., chat widget) |
|
|
108
|
+
|
|
109
|
+
### Sidebar
|
|
110
|
+
|
|
111
|
+
Collapsible left navigation with context switching, expandable groups, and tooltips when collapsed.
|
|
112
|
+
|
|
113
|
+
Features:
|
|
114
|
+
- Logo area with app icon
|
|
115
|
+
- Main menu items with nested groups
|
|
116
|
+
- Context switching popover (e.g., project switcher with search)
|
|
117
|
+
- Recent items section
|
|
118
|
+
- Collapse/expand toggle
|
|
119
|
+
- Organization selector badge
|
|
120
|
+
|
|
121
|
+
### Header
|
|
122
|
+
|
|
123
|
+
Top navigation bar with:
|
|
124
|
+
- Organization/branch selector
|
|
125
|
+
- Service menu (grid of available apps)
|
|
126
|
+
- Global search input
|
|
127
|
+
- Notification dropdown with count badge
|
|
128
|
+
- Custom action buttons
|
|
129
|
+
- User menu (profile, settings, logout)
|
|
130
|
+
|
|
131
|
+
### PageContainer
|
|
132
|
+
|
|
133
|
+
Flexible page layout wrapper with three variants:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import {
|
|
137
|
+
StandardPageContainer,
|
|
138
|
+
SplitPageContainer,
|
|
139
|
+
FullWidthPageContainer,
|
|
140
|
+
} from '@omnifyjp/shell';
|
|
141
|
+
|
|
142
|
+
// Standard — padded page with title bar
|
|
143
|
+
<StandardPageContainer title="Users" subtitle="Manage team members" extra={<Button>Add User</Button>}>
|
|
144
|
+
<UserTable />
|
|
145
|
+
</StandardPageContainer>
|
|
146
|
+
|
|
147
|
+
// Split — two-column layout with sidebar
|
|
148
|
+
<SplitPageContainer title="Settings" sidebar={<SettingsNav />} sidebarPosition="left">
|
|
149
|
+
<SettingsForm />
|
|
150
|
+
</SplitPageContainer>
|
|
151
|
+
|
|
152
|
+
// Full width — no padding (for Kanban boards, Gantt charts)
|
|
153
|
+
<FullWidthPageContainer>
|
|
154
|
+
<KanbanBoard />
|
|
155
|
+
</FullWidthPageContainer>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| Prop | Type | Default | Description |
|
|
159
|
+
|------|------|---------|-------------|
|
|
160
|
+
| `title` | `string` | — | Page heading |
|
|
161
|
+
| `subtitle` | `string` | — | Description text below title |
|
|
162
|
+
| `extra` | `ReactNode` | — | Header actions (buttons, etc.) |
|
|
163
|
+
| `children` | `ReactNode` | — | Main content |
|
|
164
|
+
| `footer` | `ReactNode` | — | Footer content |
|
|
165
|
+
| `variant` | `'standard' \| 'split' \| 'full'` | `'standard'` | Layout mode |
|
|
166
|
+
| `sidebar` | `ReactNode` | — | Sidebar content (split mode) |
|
|
167
|
+
| `sidebarPosition` | `'left' \| 'right'` | `'right'` | Sidebar placement |
|
|
168
|
+
| `sidebarWidth` | `string` | `'w-80'` | Sidebar Tailwind width class |
|
|
169
|
+
|
|
170
|
+
### OrganizationSelector
|
|
171
|
+
|
|
172
|
+
Two-step modal for switching organizations and branches:
|
|
173
|
+
|
|
174
|
+
1. **Step 1**: Select organization (shows description + branch count)
|
|
175
|
+
2. **Step 2**: Select branch (searchable, shows location)
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import { OrganizationSelector } from '@omnifyjp/shell';
|
|
179
|
+
|
|
180
|
+
<OrganizationSelector variant="header" /> // in header
|
|
181
|
+
<OrganizationSelector variant="sidebar" /> // in sidebar
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### ServiceMenu
|
|
185
|
+
|
|
186
|
+
Searchable dropdown grid listing all available services/apps:
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
const config: ShellConfig = {
|
|
190
|
+
services: [
|
|
191
|
+
{
|
|
192
|
+
category: 'HR',
|
|
193
|
+
items: [
|
|
194
|
+
{ icon: Users, label: 'Attendance', url: '/attendance', color: 'bg-blue-600' },
|
|
195
|
+
{ icon: Calendar, label: 'Leave', url: '/leave', color: 'bg-green-600' },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Configuration: `ShellConfig`
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
interface ShellConfig {
|
|
208
|
+
app: {
|
|
209
|
+
name: string; // App display name
|
|
210
|
+
key: string; // App identifier
|
|
211
|
+
logo: {
|
|
212
|
+
icon: LucideIcon; // Logo icon
|
|
213
|
+
text: string; // Logo text
|
|
214
|
+
color?: string; // Icon color class
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
sidebar: {
|
|
219
|
+
menuItems: SidebarMenuItem[]; // Navigation items
|
|
220
|
+
contextMenu?: { // Context switcher (e.g., project selector)
|
|
221
|
+
paramName: string; // URL param name
|
|
222
|
+
getItems: () => ContextItem[];
|
|
223
|
+
getHeader?: () => ReactNode;
|
|
224
|
+
getFooterItems?: () => ContextItem[];
|
|
225
|
+
};
|
|
226
|
+
recentItems?: { // Recent items section
|
|
227
|
+
title: string;
|
|
228
|
+
items: { label: string; path: string; badge?: string; color?: string }[];
|
|
229
|
+
};
|
|
230
|
+
footer?: ReactNode; // Custom sidebar footer
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
services?: ServiceCategory[]; // Service menu grid
|
|
234
|
+
|
|
235
|
+
header?: {
|
|
236
|
+
searchPlaceholder?: string;
|
|
237
|
+
showSearch?: boolean; // Default: true
|
|
238
|
+
actions?: ReactNode; // Custom header buttons
|
|
239
|
+
notifications?: {
|
|
240
|
+
count: number;
|
|
241
|
+
content: ReactNode;
|
|
242
|
+
};
|
|
243
|
+
user?: {
|
|
244
|
+
name: string;
|
|
245
|
+
email?: string;
|
|
246
|
+
avatar?: string;
|
|
247
|
+
onLogout?: () => void;
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
organization?: {
|
|
252
|
+
organizations: Organization[];
|
|
253
|
+
branches: Branch[];
|
|
254
|
+
onOrganizationChange?: (org: Organization) => void;
|
|
255
|
+
onBranchChange?: (branch: Branch) => void;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### SidebarMenuItem
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
interface SidebarMenuItem {
|
|
264
|
+
icon: LucideIcon;
|
|
265
|
+
label: string;
|
|
266
|
+
path?: string; // Omit for collapsible groups
|
|
267
|
+
badge?: number; // Notification count
|
|
268
|
+
children?: SidebarMenuItem[]; // Nested items
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Context Providers
|
|
275
|
+
|
|
276
|
+
### ThemeProvider
|
|
277
|
+
|
|
278
|
+
Dark mode management with system preference detection:
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
import { useTheme } from '@omnifyjp/shell';
|
|
282
|
+
|
|
283
|
+
function ThemeToggle() {
|
|
284
|
+
const { theme, setTheme } = useTheme();
|
|
285
|
+
// theme: 'light' | 'dark' | 'system'
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
|
|
289
|
+
<option value="light">Light</option>
|
|
290
|
+
<option value="dark">Dark</option>
|
|
291
|
+
<option value="system">System</option>
|
|
292
|
+
</select>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Persists selection to `localStorage.omnify_theme`.
|
|
298
|
+
|
|
299
|
+
### OrganizationProvider
|
|
300
|
+
|
|
301
|
+
Multi-tenant organization/branch management:
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
import { useOrganization } from '@omnifyjp/shell';
|
|
305
|
+
|
|
306
|
+
function OrgInfo() {
|
|
307
|
+
const {
|
|
308
|
+
selectedOrganization, // Organization | null
|
|
309
|
+
selectedBranch, // Branch | null
|
|
310
|
+
setSelectedOrganization,
|
|
311
|
+
setSelectedBranch,
|
|
312
|
+
getBranchesByOrg, // (orgId: string) => Branch[]
|
|
313
|
+
isSetupComplete, // true when both selected
|
|
314
|
+
} = useOrganization();
|
|
315
|
+
|
|
316
|
+
return <p>{selectedOrganization?.name} — {selectedBranch?.name}</p>;
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Persists selections to `localStorage.selectedOrganizationId` and `localStorage.selectedBranchId`.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## i18n (Internationalization)
|
|
325
|
+
|
|
326
|
+
### Supported Languages
|
|
327
|
+
|
|
328
|
+
| Code | Language |
|
|
329
|
+
|------|----------|
|
|
330
|
+
| `vi` | Tieng Viet (default, fallback) |
|
|
331
|
+
| `en` | English |
|
|
332
|
+
| `ja` | Japanese |
|
|
333
|
+
|
|
334
|
+
### Shell Namespace
|
|
335
|
+
|
|
336
|
+
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.
|
|
337
|
+
|
|
338
|
+
### Adding Service Translations
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
import { initOmnifyI18n } from '@omnifyjp/shell';
|
|
342
|
+
|
|
343
|
+
// Add your service namespace
|
|
344
|
+
initOmnifyI18n({
|
|
345
|
+
namespaces: {
|
|
346
|
+
myapp: {
|
|
347
|
+
en: { dashboard: { title: 'Dashboard' } },
|
|
348
|
+
vi: { dashboard: { title: 'Bang dieu khien' } },
|
|
349
|
+
ja: { dashboard: { title: 'dasshubodo' } },
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Use in components
|
|
355
|
+
import { useTranslation } from 'react-i18next';
|
|
356
|
+
|
|
357
|
+
function Dashboard() {
|
|
358
|
+
const { t } = useTranslation(['myapp', 'shell']);
|
|
359
|
+
return <h1>{t('dashboard.title')}</h1>;
|
|
360
|
+
// Falls back to 'shell' namespace for common keys
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Changing Language
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
import { changeLanguage } from '@omnifyjp/shell';
|
|
368
|
+
|
|
369
|
+
await changeLanguage('en'); // Persists to localStorage
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Hooks
|
|
375
|
+
|
|
376
|
+
### useDateFormat
|
|
377
|
+
|
|
378
|
+
Locale-aware date formatting with timezone support:
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
import { useDateFormat } from '@omnifyjp/shell';
|
|
382
|
+
|
|
383
|
+
function EventDate({ date }: { date: Date }) {
|
|
384
|
+
const {
|
|
385
|
+
formatDate, // '01/15/2026' (locale-dependent)
|
|
386
|
+
formatDateTime, // '01/15/2026 14:30'
|
|
387
|
+
formatRelativeTime, // '5 minutes ago'
|
|
388
|
+
formatShortDate, // '01/15'
|
|
389
|
+
formatMonthYear, // 'January 2026'
|
|
390
|
+
formatDayOfWeek, // 'Mon'
|
|
391
|
+
formatDateLong, // 'January 15, 2026'
|
|
392
|
+
timezone, // 'Asia/Ho_Chi_Minh'
|
|
393
|
+
setTimezone, // Change timezone
|
|
394
|
+
dateFnsLocale, // date-fns Locale object
|
|
395
|
+
} = useDateFormat();
|
|
396
|
+
|
|
397
|
+
return <time>{formatDateTime(date)}</time>;
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Supported timezones: `Asia/Ho_Chi_Minh`, `Asia/Tokyo`, `America/New_York`, `America/Los_Angeles`, `Europe/London`, `UTC`.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Package Exports
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
@omnifyjp/shell → all exports (barrel)
|
|
409
|
+
@omnifyjp/shell/components/AppShell → AppShell
|
|
410
|
+
@omnifyjp/shell/components/Sidebar → Sidebar
|
|
411
|
+
@omnifyjp/shell/components/Header → Header
|
|
412
|
+
@omnifyjp/shell/components/PageContainer → PageContainer, Standard/Split/FullWidth variants
|
|
413
|
+
@omnifyjp/shell/components/ServiceMenu → ServiceMenu
|
|
414
|
+
@omnifyjp/shell/components/OrganizationSelector → OrganizationSelector
|
|
415
|
+
@omnifyjp/shell/contexts/ThemeContext → ThemeProvider, useTheme
|
|
416
|
+
@omnifyjp/shell/contexts/OrganizationContext → OrganizationProvider, useOrganization
|
|
417
|
+
@omnifyjp/shell/hooks/useDateFormat → useDateFormat, timezoneLabels
|
|
418
|
+
@omnifyjp/shell/i18n → i18n, initOmnifyI18n, changeLanguage
|
|
419
|
+
@omnifyjp/shell/types → ShellConfig, Organization, Branch, etc.
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## TypeScript
|
|
425
|
+
|
|
426
|
+
Full type definitions are included. Key types:
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
import type {
|
|
430
|
+
ShellConfig,
|
|
431
|
+
SidebarMenuItem,
|
|
432
|
+
ServiceCategory,
|
|
433
|
+
ServiceItem,
|
|
434
|
+
Organization,
|
|
435
|
+
Branch,
|
|
436
|
+
PageContainerProps,
|
|
437
|
+
} from '@omnifyjp/shell';
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Dependencies
|
|
443
|
+
|
|
444
|
+
This package depends on:
|
|
445
|
+
|
|
446
|
+
| Package | Purpose |
|
|
447
|
+
|---------|---------|
|
|
448
|
+
| `@omnifyjp/ui` | UI component primitives (auto-installed) |
|
|
449
|
+
| `i18next` | Internationalization core |
|
|
450
|
+
| `react-i18next` | React i18n bindings |
|
|
451
|
+
| `date-fns` | Date formatting |
|
|
452
|
+
| `lucide-react` | Icons |
|
|
453
|
+
| `sonner` | Toast notifications |
|
|
454
|
+
|
|
455
|
+
Peer dependencies (you provide):
|
|
456
|
+
|
|
457
|
+
| Package | Version |
|
|
458
|
+
|---------|---------|
|
|
459
|
+
| `react` | >=18 |
|
|
460
|
+
| `react-dom` | >=18 |
|
|
461
|
+
| `react-router` | >=7 |
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Related Packages
|
|
466
|
+
|
|
467
|
+
| Package | Description |
|
|
468
|
+
|---------|-------------|
|
|
469
|
+
| [`@omnifyjp/ui`](https://www.npmjs.com/package/@omnifyjp/ui) | 53 Shadcn primitives + 14 domain components |
|
|
470
|
+
| [`@omnifyjp/editor`](https://www.npmjs.com/package/@omnifyjp/editor) | Rich text editors (Tiptap + BlockNote) |
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## License
|
|
475
|
+
|
|
476
|
+
MIT
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { changeLanguage } from './chunk-OMIE3Z5N.js';
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { format, formatDistanceToNow } from 'date-fns';
|
|
5
|
+
import { ja, enUS, vi } from 'date-fns/locale';
|
|
6
|
+
|
|
7
|
+
var timezoneLabels = {
|
|
8
|
+
"Asia/Ho_Chi_Minh": "(UTC+7) Ho Chi Minh",
|
|
9
|
+
"Asia/Tokyo": "(UTC+9) Tokyo",
|
|
10
|
+
"America/New_York": "(UTC-5) New York",
|
|
11
|
+
"America/Los_Angeles": "(UTC-8) Los Angeles",
|
|
12
|
+
"Europe/London": "(UTC+0) London",
|
|
13
|
+
"UTC": "(UTC+0) UTC"
|
|
14
|
+
};
|
|
15
|
+
var dateFnsLocales = {
|
|
16
|
+
vi: vi,
|
|
17
|
+
en: enUS,
|
|
18
|
+
ja: ja
|
|
19
|
+
};
|
|
20
|
+
var dateFormatPatterns = {
|
|
21
|
+
vi: "dd/MM/yyyy",
|
|
22
|
+
en: "MM/dd/yyyy",
|
|
23
|
+
ja: "yyyy/MM/dd"
|
|
24
|
+
};
|
|
25
|
+
var dateTimeFormatPatterns = {
|
|
26
|
+
vi: "dd/MM/yyyy HH:mm",
|
|
27
|
+
en: "MM/dd/yyyy HH:mm",
|
|
28
|
+
ja: "yyyy/MM/dd HH:mm"
|
|
29
|
+
};
|
|
30
|
+
var shortDatePatterns = {
|
|
31
|
+
vi: "dd/MM",
|
|
32
|
+
en: "MM/dd",
|
|
33
|
+
ja: "MM/dd"
|
|
34
|
+
};
|
|
35
|
+
var localeTags = {
|
|
36
|
+
vi: "vi-VN",
|
|
37
|
+
en: "en-US",
|
|
38
|
+
ja: "ja-JP"
|
|
39
|
+
};
|
|
40
|
+
function toDate(d) {
|
|
41
|
+
return typeof d === "string" ? new Date(d) : d;
|
|
42
|
+
}
|
|
43
|
+
function loadSavedTimezone() {
|
|
44
|
+
if (typeof window === "undefined") return "Asia/Ho_Chi_Minh";
|
|
45
|
+
const saved = localStorage.getItem("omnify_timezone");
|
|
46
|
+
if (saved && saved in timezoneLabels) return saved;
|
|
47
|
+
return "Asia/Ho_Chi_Minh";
|
|
48
|
+
}
|
|
49
|
+
function useDateFormat() {
|
|
50
|
+
const { i18n } = useTranslation();
|
|
51
|
+
const language = i18n.language || "vi";
|
|
52
|
+
const locale = dateFnsLocales[language] || vi;
|
|
53
|
+
const [timezone, setTimezoneState] = useState(() => loadSavedTimezone());
|
|
54
|
+
const setTimezone = useCallback((tz) => {
|
|
55
|
+
localStorage.setItem("omnify_timezone", tz);
|
|
56
|
+
setTimezoneState(tz);
|
|
57
|
+
}, []);
|
|
58
|
+
const setLanguage = useCallback((lang) => {
|
|
59
|
+
changeLanguage(lang);
|
|
60
|
+
}, []);
|
|
61
|
+
const formatDate = useCallback(
|
|
62
|
+
(date) => {
|
|
63
|
+
return format(toDate(date), dateFormatPatterns[language] || "dd/MM/yyyy", { locale });
|
|
64
|
+
},
|
|
65
|
+
[language, locale]
|
|
66
|
+
);
|
|
67
|
+
const formatDateTime = useCallback(
|
|
68
|
+
(date) => {
|
|
69
|
+
return format(toDate(date), dateTimeFormatPatterns[language] || "dd/MM/yyyy HH:mm", { locale });
|
|
70
|
+
},
|
|
71
|
+
[language, locale]
|
|
72
|
+
);
|
|
73
|
+
const formatRelativeTime = useCallback(
|
|
74
|
+
(date) => {
|
|
75
|
+
return formatDistanceToNow(toDate(date), { addSuffix: true, locale });
|
|
76
|
+
},
|
|
77
|
+
[locale]
|
|
78
|
+
);
|
|
79
|
+
const formatShortDate = useCallback(
|
|
80
|
+
(date) => {
|
|
81
|
+
return format(toDate(date), shortDatePatterns[language] || "dd/MM", { locale });
|
|
82
|
+
},
|
|
83
|
+
[language, locale]
|
|
84
|
+
);
|
|
85
|
+
const formatMonthYear = useCallback(
|
|
86
|
+
(date) => {
|
|
87
|
+
return format(toDate(date), "MMMM yyyy", { locale });
|
|
88
|
+
},
|
|
89
|
+
[locale]
|
|
90
|
+
);
|
|
91
|
+
const formatDayOfWeek = useCallback(
|
|
92
|
+
(date) => {
|
|
93
|
+
return format(toDate(date), "EEE", { locale });
|
|
94
|
+
},
|
|
95
|
+
[locale]
|
|
96
|
+
);
|
|
97
|
+
const formatDateLong = useCallback(
|
|
98
|
+
(date) => {
|
|
99
|
+
return format(toDate(date), "PPP", { locale });
|
|
100
|
+
},
|
|
101
|
+
[locale]
|
|
102
|
+
);
|
|
103
|
+
return {
|
|
104
|
+
language,
|
|
105
|
+
timezone,
|
|
106
|
+
setLanguage,
|
|
107
|
+
setTimezone,
|
|
108
|
+
formatDate,
|
|
109
|
+
formatDateTime,
|
|
110
|
+
formatRelativeTime,
|
|
111
|
+
formatShortDate,
|
|
112
|
+
formatMonthYear,
|
|
113
|
+
formatDayOfWeek,
|
|
114
|
+
formatDateLong,
|
|
115
|
+
dateFnsLocale: locale,
|
|
116
|
+
localeTag: localeTags[language] || "vi-VN",
|
|
117
|
+
languageNames: { vi: "Ti\u1EBFng Vi\u1EC7t", en: "English", ja: "\u65E5\u672C\u8A9E" }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { timezoneLabels, useDateFormat };
|
|
122
|
+
//# sourceMappingURL=chunk-6JYWZJEY.js.map
|
|
123
|
+
//# sourceMappingURL=chunk-6JYWZJEY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useDateFormat.ts"],"names":["viLocale","jaLocale"],"mappings":"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","file":"chunk-6JYWZJEY.js","sourcesContent":["import { useCallback, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { format, formatDistanceToNow } from 'date-fns';\nimport { vi as viLocale } from 'date-fns/locale';\nimport { enUS } from 'date-fns/locale';\nimport { ja as jaLocale } from 'date-fns/locale';\nimport type { Locale } from 'date-fns';\nimport { changeLanguage } from '../i18n';\nimport type { SupportedLanguage } from '../i18n';\n\nexport type SupportedTimezone =\n | 'Asia/Ho_Chi_Minh'\n | 'Asia/Tokyo'\n | 'America/New_York'\n | 'America/Los_Angeles'\n | 'Europe/London'\n | 'UTC';\n\nexport const timezoneLabels: Record<SupportedTimezone, string> = {\n 'Asia/Ho_Chi_Minh': '(UTC+7) Ho Chi Minh',\n 'Asia/Tokyo': '(UTC+9) Tokyo',\n 'America/New_York': '(UTC-5) New York',\n 'America/Los_Angeles': '(UTC-8) Los Angeles',\n 'Europe/London': '(UTC+0) London',\n 'UTC': '(UTC+0) UTC',\n};\n\nconst dateFnsLocales: Record<SupportedLanguage, Locale> = {\n vi: viLocale,\n en: enUS,\n ja: jaLocale,\n};\n\nconst dateFormatPatterns: Record<SupportedLanguage, string> = {\n vi: 'dd/MM/yyyy',\n en: 'MM/dd/yyyy',\n ja: 'yyyy/MM/dd',\n};\n\nconst dateTimeFormatPatterns: Record<SupportedLanguage, string> = {\n vi: 'dd/MM/yyyy HH:mm',\n en: 'MM/dd/yyyy HH:mm',\n ja: 'yyyy/MM/dd HH:mm',\n};\n\nconst shortDatePatterns: Record<SupportedLanguage, string> = {\n vi: 'dd/MM',\n en: 'MM/dd',\n ja: 'MM/dd',\n};\n\nconst localeTags: Record<SupportedLanguage, string> = {\n vi: 'vi-VN',\n en: 'en-US',\n ja: 'ja-JP',\n};\n\nfunction toDate(d: Date | string): Date {\n return typeof d === 'string' ? new Date(d) : d;\n}\n\nfunction loadSavedTimezone(): SupportedTimezone {\n if (typeof window === 'undefined') return 'Asia/Ho_Chi_Minh';\n const saved = localStorage.getItem('omnify_timezone');\n if (saved && saved in timezoneLabels) return saved as SupportedTimezone;\n return 'Asia/Ho_Chi_Minh';\n}\n\n/**\n * Hook that provides date formatting utilities.\n * Reads the current language from i18next automatically.\n */\nexport function useDateFormat() {\n const { i18n } = useTranslation();\n const language = (i18n.language || 'vi') as SupportedLanguage;\n const locale = dateFnsLocales[language] || viLocale;\n\n const [timezone, setTimezoneState] = useState<SupportedTimezone>(() => loadSavedTimezone());\n\n const setTimezone = useCallback((tz: SupportedTimezone) => {\n localStorage.setItem('omnify_timezone', tz);\n setTimezoneState(tz);\n }, []);\n\n const setLanguage = useCallback((lang: SupportedLanguage) => {\n changeLanguage(lang);\n }, []);\n\n const formatDate = useCallback(\n (date: Date | string): string => {\n return format(toDate(date), dateFormatPatterns[language] || 'dd/MM/yyyy', { locale });\n },\n [language, locale]\n );\n\n const formatDateTime = useCallback(\n (date: Date | string): string => {\n return format(toDate(date), dateTimeFormatPatterns[language] || 'dd/MM/yyyy HH:mm', { locale });\n },\n [language, locale]\n );\n\n const formatRelativeTime = useCallback(\n (date: Date | string): string => {\n return formatDistanceToNow(toDate(date), { addSuffix: true, locale });\n },\n [locale]\n );\n\n const formatShortDate = useCallback(\n (date: Date | string): string => {\n return format(toDate(date), shortDatePatterns[language] || 'dd/MM', { locale });\n },\n [language, locale]\n );\n\n const formatMonthYear = useCallback(\n (date: Date | string): string => {\n return format(toDate(date), 'MMMM yyyy', { locale });\n },\n [locale]\n );\n\n const formatDayOfWeek = useCallback(\n (date: Date | string): string => {\n return format(toDate(date), 'EEE', { locale });\n },\n [locale]\n );\n\n const formatDateLong = useCallback(\n (date: Date | string): string => {\n return format(toDate(date), 'PPP', { locale });\n },\n [locale]\n );\n\n return {\n language,\n timezone,\n setLanguage,\n setTimezone,\n formatDate,\n formatDateTime,\n formatRelativeTime,\n formatShortDate,\n formatMonthYear,\n formatDayOfWeek,\n formatDateLong,\n dateFnsLocale: locale,\n localeTag: localeTags[language] || 'vi-VN',\n languageNames: { vi: 'Tiếng Việt', en: 'English', ja: '日本語' } as Record<SupportedLanguage, string>,\n };\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button } from '@omnifyjp/ui/components/button';
|
|
4
|
+
import { Input } from '@omnifyjp/ui/components/input';
|
|
5
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuItem } from '@omnifyjp/ui/components/dropdown-menu';
|
|
6
|
+
import { Grid3X3, Search } from 'lucide-react';
|
|
7
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
8
|
+
|
|
9
|
+
// src/components/ServiceMenu.tsx
|
|
10
|
+
function ServiceMenu({ categories }) {
|
|
11
|
+
const [search, setSearch] = useState("");
|
|
12
|
+
const { t } = useTranslation("shell");
|
|
13
|
+
const filteredCategories = categories.map((cat) => ({
|
|
14
|
+
...cat,
|
|
15
|
+
items: cat.items.filter(
|
|
16
|
+
(s) => s.label.toLowerCase().includes(search.toLowerCase())
|
|
17
|
+
)
|
|
18
|
+
})).filter((cat) => cat.items.length > 0);
|
|
19
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
20
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", className: "gap-2", children: [
|
|
21
|
+
/* @__PURE__ */ jsx(Grid3X3, { className: "w-4 h-4" }),
|
|
22
|
+
t("header.services")
|
|
23
|
+
] }) }),
|
|
24
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", className: "w-[340px] p-0", children: [
|
|
25
|
+
/* @__PURE__ */ jsx("div", { className: "p-2 border-b", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
26
|
+
/* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" }),
|
|
27
|
+
/* @__PURE__ */ jsx(
|
|
28
|
+
Input,
|
|
29
|
+
{
|
|
30
|
+
placeholder: t("header.searchServices"),
|
|
31
|
+
value: search,
|
|
32
|
+
onChange: (e) => setSearch(e.target.value),
|
|
33
|
+
className: "h-8 pl-8 text-sm"
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
] }) }),
|
|
37
|
+
/* @__PURE__ */ jsx("div", { className: "max-h-[420px] overflow-y-auto p-1", children: filteredCategories.length === 0 ? /* @__PURE__ */ jsx("div", { className: "py-6 text-center text-sm text-muted-foreground", children: t("header.noServiceFound") }) : filteredCategories.map((category) => /* @__PURE__ */ jsxs("div", { children: [
|
|
38
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-medium px-2 py-1.5", children: category.category }),
|
|
39
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-0.5 px-1 mb-1", children: category.items.map((service) => /* @__PURE__ */ jsxs(
|
|
40
|
+
DropdownMenuItem,
|
|
41
|
+
{
|
|
42
|
+
className: "flex items-center gap-2.5 px-2 py-2 rounded-md cursor-pointer",
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ jsx("div", { className: `w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${service.color}`, children: /* @__PURE__ */ jsx(service.icon, { className: "w-4 h-4" }) }),
|
|
45
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm truncate", children: service.label })
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
service.label
|
|
49
|
+
)) })
|
|
50
|
+
] }, category.category)) })
|
|
51
|
+
] })
|
|
52
|
+
] });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { ServiceMenu };
|
|
56
|
+
//# sourceMappingURL=chunk-ACCHC3AM.js.map
|
|
57
|
+
//# sourceMappingURL=chunk-ACCHC3AM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/ServiceMenu.tsx"],"names":[],"mappings":"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","file":"chunk-ACCHC3AM.js","sourcesContent":["import { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { Button } from '@omnifyjp/ui/components/button';\nimport { Input } from '@omnifyjp/ui/components/input';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuTrigger,\n} from '@omnifyjp/ui/components/dropdown-menu';\nimport { Grid3X3, Search } from 'lucide-react';\nimport type { ServiceCategory } from '../types';\n\ninterface ServiceMenuProps {\n categories: ServiceCategory[];\n}\n\nexport function ServiceMenu({ categories }: ServiceMenuProps) {\n const [search, setSearch] = useState('');\n const { t } = useTranslation('shell');\n\n const filteredCategories = categories\n .map((cat) => ({\n ...cat,\n items: cat.items.filter((s) =>\n s.label.toLowerCase().includes(search.toLowerCase())\n ),\n }))\n .filter((cat) => cat.items.length > 0);\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" className=\"gap-2\">\n <Grid3X3 className=\"w-4 h-4\" />\n {t('header.services')}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[340px] p-0\">\n <div className=\"p-2 border-b\">\n <div className=\"relative\">\n <Search className=\"absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground\" />\n <Input\n placeholder={t('header.searchServices')}\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"h-8 pl-8 text-sm\"\n />\n </div>\n </div>\n <div className=\"max-h-[420px] overflow-y-auto p-1\">\n {filteredCategories.length === 0 ? (\n <div className=\"py-6 text-center text-sm text-muted-foreground\">\n {t('header.noServiceFound')}\n </div>\n ) : (\n filteredCategories.map((category) => (\n <div key={category.category}>\n <DropdownMenuLabel className=\"text-xs text-muted-foreground font-medium px-2 py-1.5\">\n {category.category}\n </DropdownMenuLabel>\n <div className=\"grid grid-cols-2 gap-0.5 px-1 mb-1\">\n {category.items.map((service) => (\n <DropdownMenuItem\n key={service.label}\n className=\"flex items-center gap-2.5 px-2 py-2 rounded-md cursor-pointer\"\n >\n <div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${service.color}`}>\n <service.icon className=\"w-4 h-4\" />\n </div>\n <span className=\"text-sm truncate\">{service.label}</span>\n </DropdownMenuItem>\n ))}\n </div>\n </div>\n ))\n )}\n </div>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"]}
|