Merge pull request 'core/page-provider' (#29) from core/page-provider into main
Reviewed-on: eigen/fe-monorepo-template#29
This commit is contained in:
@@ -18,7 +18,8 @@
|
||||
// 4173: VitePress Docs (Preview)
|
||||
// 5173: Web App (Must be strictly 5173 for Electron IPC compatibility)
|
||||
// 3000: Landing App (Isolated from Vite's default 517x blast radius)
|
||||
"forwardPorts": [6060, 4173, 5173, 3000],
|
||||
// 3005: Showcase App
|
||||
"forwardPorts": [6060, 4173, 5173, 3000, 3005],
|
||||
|
||||
"portsAttributes": {
|
||||
"6060": {
|
||||
@@ -33,6 +34,10 @@
|
||||
"label": "Web App (Electron Target)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"3005": {
|
||||
"label": "Showcase App",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"3000": {
|
||||
"label": "Landing App (Public)",
|
||||
"onAutoForward": "notify"
|
||||
|
||||
@@ -230,7 +230,7 @@ In the target web app's entry point (e.g., `apps/web/src/apps/index.tsx`):
|
||||
}
|
||||
```
|
||||
|
||||
All routes transition to hash-based addressing: `#/app/dashboard`, `#/auth/login`, `#/showcase`.
|
||||
All routes transition to hash-based addressing: `#/app/dashboard`, `#/auth/login`.
|
||||
|
||||
### Step 2: Decommission the Custom Protocol — Main Process
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ A full working example is available in the showcase booking feature:
|
||||
|
||||
| File | Description |
|
||||
| ---- | ----------- |
|
||||
| `apps/web/.../booking/data/booking.transformer.ts` | Basic transformer with snake_case ↔ camelCase mapping |
|
||||
| `apps/web/.../booking/data/booking.data-services.ts` | Data service with injected transformer |
|
||||
| `apps/web/.../booking/data/advanced-booking.transformer.ts` | Extended transformer with custom chart method |
|
||||
| `apps/web/.../booking/data/advanced-booking.data-services.ts` | Extended service with custom `getAvailabilityChart()` |
|
||||
| `apps/showcase/.../booking/data/booking.transformer.ts` | Basic transformer with snake_case ↔ camelCase mapping |
|
||||
| `apps/showcase/.../booking/data/booking.data-services.ts` | Data service with injected transformer |
|
||||
| `apps/showcase/.../booking/data/advanced-booking.transformer.ts` | Extended transformer with custom chart method |
|
||||
| `apps/showcase/.../booking/data/advanced-booking.data-services.ts` | Extended service with custom `getAvailabilityChart()` |
|
||||
|
||||
@@ -418,7 +418,7 @@ function App() {
|
||||
|
||||
### Interactive Config Builder
|
||||
|
||||
The showcase demo at `apps/web/src/apps/showcase/shell-demo/` demonstrates a live, interactive config builder where every feature toggle and variant switch updates the layout in real-time. The key pattern is managing `config` state externally and passing it as a prop:
|
||||
The showcase demo at `apps/showcase/src/pages/showcase-original/shell-demo/` demonstrates a live, interactive config builder where every feature toggle and variant switch updates the layout in real-time. The key pattern is managing `config` state externally and passing it as a prop:
|
||||
|
||||
```tsx
|
||||
import { useState, useMemo } from 'react';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['@repo/eslint-config/react.js'],
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Showcase</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "showcase",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --clearScreen false",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest --watch",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@repo/core-api": "workspace:*",
|
||||
"@repo/core-events": "workspace:*",
|
||||
"@repo/core-i18n": "workspace:*",
|
||||
"@repo/core-storage": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"@repo/utils": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"dayjs": "^1.11.19",
|
||||
"events": "^3.3.0",
|
||||
"i18next": "^24.2.2",
|
||||
"lucide-react": "^1.22.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-router-dom": "^7.11.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"zod": "^3.25.36",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^8.57.1",
|
||||
"typescript": "5.5.4",
|
||||
"vite": "^5.1.4",
|
||||
"vitest": "^4.0.17"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useState, Suspense } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { ThemeProvider, DensityType } from '@repo/ui/provider';
|
||||
import { AgGridProvider } from '@repo/ui/components';
|
||||
import { useThemeStore } from './core/stores/theme.store';
|
||||
import ShowcaseLayout from './layouts/ShowcaseLayout';
|
||||
|
||||
// Pages
|
||||
import UiComponentsPage from './pages/ui-components';
|
||||
import FormsPage from './pages/forms';
|
||||
import StoragePage from './pages/storage';
|
||||
import EventsPage from './pages/events';
|
||||
import HardwarePage from './pages/hardware';
|
||||
import RbacPage from './pages/rbac';
|
||||
import AuthPage from './pages/auth';
|
||||
import ShellDemoPage from './pages/shell-demo';
|
||||
import ActionToolsPage from './pages/action-tools';
|
||||
import AgGridPage from './pages/ag-grid';
|
||||
|
||||
export default function App() {
|
||||
const colorScheme = useThemeStore((s) => s.colorScheme);
|
||||
const [density, setDensity] = useState<DensityType>('compact');
|
||||
|
||||
return (
|
||||
<ThemeProvider colorScheme={colorScheme} density={density}>
|
||||
<AgGridProvider bypassLicense>
|
||||
<BrowserRouter>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Routes>
|
||||
<Route path="/" element={<ShowcaseLayout density={density} setDensity={setDensity} />}>
|
||||
<Route index element={<Navigate to="/ui-components" replace />} />
|
||||
<Route path="ui-components" element={<UiComponentsPage />} />
|
||||
<Route path="forms" element={<FormsPage />} />
|
||||
<Route path="storage" element={<StoragePage />} />
|
||||
<Route path="events" element={<EventsPage />} />
|
||||
<Route path="hardware" element={<HardwarePage />} />
|
||||
<Route path="rbac" element={<RbacPage />} />
|
||||
<Route path="auth" element={<AuthPage />} />
|
||||
<Route path="action-tools" element={<ActionToolsPage />} />
|
||||
<Route path="ag-grid" element={<AgGridPage />} />
|
||||
</Route>
|
||||
<Route path="/shell-demo" element={<ShellDemoPage />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
</AgGridProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export const LAYOUT_EVENTS = {
|
||||
TOGGLE_HISTORY_DRAWER: 'LAYOUT:TOGGLE_HISTORY_DRAWER',
|
||||
TOGGLE_BOOKMARK_DRAWER: 'LAYOUT:TOGGLE_BOOKMARK_DRAWER',
|
||||
} as const;
|
||||
|
||||
export const DEVICE_EVENTS = {
|
||||
PRINT_RECEIPT: 'DEVICE:PRINT_RECEIPT',
|
||||
} as const;
|
||||
|
||||
export const WS_EVENTS = {
|
||||
STOCK_UPDATE: 'WS:STOCK_UPDATE',
|
||||
} as const;
|
||||
|
||||
export const AUTH_EVENTS = {
|
||||
PROFILE_UPDATED: 'AUTH:PROFILE_UPDATED',
|
||||
} as const;
|
||||
|
||||
export const APP_EVENTS = {
|
||||
INITIALIZED: 'APP:INITIALIZED',
|
||||
ERROR: 'APP:ERROR',
|
||||
} as const;
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useIsElectron } from './use-is-electron';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────
|
||||
|
||||
export interface UseElectronPrinterReturn {
|
||||
/** List of available printers (populated after calling `refreshPrinters`) */
|
||||
printers: ElectronPrinterInfo[];
|
||||
/** Whether a printer operation is in progress */
|
||||
loading: boolean;
|
||||
/** Last error message, if any */
|
||||
error: string | null;
|
||||
/** Whether the app is running inside Electron */
|
||||
isElectron: boolean;
|
||||
/** Fetch the current list of available printers */
|
||||
refreshPrinters: () => Promise<ElectronPrinterInfo[]>;
|
||||
/** Print with the given options. Returns success/failure. */
|
||||
print: (options?: ElectronPrintOptions) => Promise<ElectronPrintResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for Electron printer integration.
|
||||
*
|
||||
* Provides methods to list available printers and trigger print jobs
|
||||
* via the secure `window.electronAPI` bridge.
|
||||
*
|
||||
* Safe to use in both Electron and browser environments.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function PrintButton() {
|
||||
* const { printers, refreshPrinters, print, loading } = useElectronPrinter();
|
||||
*
|
||||
* useEffect(() => { refreshPrinters(); }, []);
|
||||
*
|
||||
* const handlePrint = async () => {
|
||||
* const result = await print({ silent: true, deviceName: printers[0]?.name });
|
||||
* if (!result.success) alert(`Print failed: ${result.failureReason}`);
|
||||
* };
|
||||
*
|
||||
* return (
|
||||
* <button onClick={handlePrint} disabled={loading || printers.length === 0}>
|
||||
* Print
|
||||
* </button>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useElectronPrinter(): UseElectronPrinterReturn {
|
||||
const [printers, setPrinters] = useState<ElectronPrinterInfo[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const isElectron = useIsElectron();
|
||||
|
||||
const refreshPrinters = useCallback(async (): Promise<ElectronPrinterInfo[]> => {
|
||||
if (!window.electronAPI) {
|
||||
setError('Not running in Electron');
|
||||
return [];
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await window.electronAPI.getPrinters();
|
||||
setPrinters(result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to get printers';
|
||||
setError(message);
|
||||
return [];
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const print = useCallback(
|
||||
async (options?: ElectronPrintOptions): Promise<ElectronPrintResult> => {
|
||||
if (!window.electronAPI) {
|
||||
return { success: false, failureReason: 'Not running in Electron' };
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await window.electronAPI.print(options);
|
||||
if (!result.success && result.failureReason) {
|
||||
setError(result.failureReason);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Print failed';
|
||||
setError(message);
|
||||
return { success: false, failureReason: message };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
printers,
|
||||
loading,
|
||||
error,
|
||||
isElectron,
|
||||
refreshPrinters,
|
||||
print,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useIsElectron } from './use-is-electron';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────
|
||||
|
||||
export type UpdateStatus =
|
||||
| 'idle'
|
||||
| 'checking'
|
||||
| 'available'
|
||||
| 'not-available'
|
||||
| 'downloading'
|
||||
| 'ready'
|
||||
| 'error';
|
||||
|
||||
export interface UseElectronUpdaterReturn {
|
||||
/** Current status of the auto-updater lifecycle */
|
||||
status: UpdateStatus;
|
||||
/** Download progress percentage (0–100) */
|
||||
progress: number;
|
||||
/** Download speed in bytes per second */
|
||||
bytesPerSecond: number;
|
||||
/** Information about the available/downloaded update */
|
||||
updateInfo: ElectronUpdateInfo | null;
|
||||
/** Error message if the updater encountered an issue */
|
||||
errorMessage: string | null;
|
||||
/** Whether the app is running inside Electron */
|
||||
isElectron: boolean;
|
||||
/** Trigger a manual update check */
|
||||
checkForUpdates: () => void;
|
||||
/** Quit the app and install the downloaded update */
|
||||
installUpdate: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for the Electron auto-updater.
|
||||
*
|
||||
* Subscribes to all update lifecycle events via `window.electronAPI`
|
||||
* and provides reactive state for building an update notification UI.
|
||||
*
|
||||
* Safe to use in both Electron and browser environments — all
|
||||
* Electron-specific calls are gated behind `window.electronAPI` checks.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function UpdateBanner() {
|
||||
* const { status, progress, updateInfo, checkForUpdates, installUpdate } = useElectronUpdater();
|
||||
*
|
||||
* if (status === 'available') {
|
||||
* return <div>Update {updateInfo?.version} available! Downloading...</div>;
|
||||
* }
|
||||
* if (status === 'downloading') {
|
||||
* return <div>Downloading... {progress.toFixed(0)}%</div>;
|
||||
* }
|
||||
* if (status === 'ready') {
|
||||
* return <button onClick={installUpdate}>Restart to update</button>;
|
||||
* }
|
||||
* return <button onClick={checkForUpdates}>Check for updates</button>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useElectronUpdater(): UseElectronUpdaterReturn {
|
||||
const [status, setStatus] = useState<UpdateStatus>('idle');
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [bytesPerSecond, setBytesPerSecond] = useState(0);
|
||||
const [updateInfo, setUpdateInfo] = useState<ElectronUpdateInfo | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const isElectron = useIsElectron();
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI) return;
|
||||
|
||||
const api = window.electronAPI;
|
||||
|
||||
const unsubChecking = api.onUpdateChecking(() => {
|
||||
setStatus('checking');
|
||||
setErrorMessage(null);
|
||||
});
|
||||
|
||||
const unsubAvailable = api.onUpdateAvailable((info) => {
|
||||
setStatus('available');
|
||||
setUpdateInfo(info);
|
||||
});
|
||||
|
||||
const unsubNotAvailable = api.onUpdateNotAvailable((info) => {
|
||||
setStatus('not-available');
|
||||
setUpdateInfo(info);
|
||||
});
|
||||
|
||||
const unsubProgress = api.onDownloadProgress((progressInfo) => {
|
||||
setStatus('downloading');
|
||||
setProgress(progressInfo.percent);
|
||||
setBytesPerSecond(progressInfo.bytesPerSecond);
|
||||
});
|
||||
|
||||
const unsubDownloaded = api.onUpdateDownloaded((info) => {
|
||||
setStatus('ready');
|
||||
setProgress(100);
|
||||
setUpdateInfo(info);
|
||||
});
|
||||
|
||||
const unsubError = api.onUpdateError((error) => {
|
||||
setStatus('error');
|
||||
setErrorMessage(error);
|
||||
});
|
||||
|
||||
// Cleanup all listeners on unmount
|
||||
return () => {
|
||||
unsubChecking();
|
||||
unsubAvailable();
|
||||
unsubNotAvailable();
|
||||
unsubProgress();
|
||||
unsubDownloaded();
|
||||
unsubError();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const checkForUpdates = useCallback(() => {
|
||||
if (!window.electronAPI) return;
|
||||
setStatus('checking');
|
||||
setErrorMessage(null);
|
||||
window.electronAPI.checkForUpdates();
|
||||
}, []);
|
||||
|
||||
const installUpdate = useCallback(() => {
|
||||
if (!window.electronAPI) return;
|
||||
window.electronAPI.installUpdate();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
status,
|
||||
progress,
|
||||
bytesPerSecond,
|
||||
updateInfo,
|
||||
errorMessage,
|
||||
isElectron,
|
||||
checkForUpdates,
|
||||
installUpdate,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* A utility hook to determine if the React application is running
|
||||
* inside the Electron desktop wrapper or a standard web browser.
|
||||
*/
|
||||
export function useIsElectron(): boolean {
|
||||
return typeof window !== 'undefined' && !!window.electronAPI;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createHttpClient } from '@repo/core-api/http-client';
|
||||
import { faroAdapter } from '@repo/core-api/observability';
|
||||
import { AppStorageKey, secureStorage } from '../storage/local';
|
||||
|
||||
/**
|
||||
* Enterprise HTTP client for showcase.
|
||||
*/
|
||||
export const apiClient = createHttpClient(
|
||||
{
|
||||
baseURL: 'http://localhost:8000/api',
|
||||
timeout: 15000,
|
||||
observability: faroAdapter,
|
||||
},
|
||||
{
|
||||
// ── Auth Interceptor ──────────────────────────────────────────
|
||||
onRequest: async (config) => {
|
||||
config.headers['ex-app-name'] = 'showcase';
|
||||
config.headers['ex-app-version'] = '1.0.0';
|
||||
config.headers['ex-timezone-offset-minutes'] = new Date().getTimezoneOffset();
|
||||
config.headers['ex-timezone-offset-hours'] = Math.floor(new Date().getTimezoneOffset() / 60);
|
||||
|
||||
const language = await secureStorage.getItem<string>(AppStorageKey.LANGUAGE);
|
||||
config.headers['ex-language'] = language ?? 'en';
|
||||
|
||||
// const token = localStorage.getItem('access_token');
|
||||
const token =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImM1OWY4MTFlLTg3M2MtNDQ3Mi1iZDU4LTIxYzExMTkwMjExNCIsIm5hbWUiOiJzdXBlcmFkbWluIiwidXNlcm5hbWUiOiJzdXBlcmFkbWluIiwicm9sZSI6InN1cGVyYWRtaW4iLCJ1c2VyX3ByaXZpbGVnZV9pZCI6bnVsbCwic291cmNlIjoiUE9TX0FETUlOIiwiaWF0IjoxNzg0Nzc4MDM5LCJleHAiOjE3ODQ4MjEyMzl9.5Wa0Ypz4mM8jYFkdGHxXi1YQzRN3JWcmAl3kCvcLDTo';
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
// ── Error Interceptor ─────────────────────────────────────────
|
||||
onResponseError: async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Clear stale token and redirect to login
|
||||
localStorage.removeItem('access_token');
|
||||
window.location.href = '/auth';
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createLocalStorage, createIndexedDB } from '@repo/core-storage';
|
||||
|
||||
export const AppStorageKey = {
|
||||
USER_PROFILE: 'user_profile',
|
||||
LANGUAGE: 'app_language',
|
||||
THEME: 'app_theme',
|
||||
ACCESS_TOKEN: 'access_token',
|
||||
REFRESH_TOKEN: 'refresh_token',
|
||||
MOCK_DB_COMPANY_A: 'mock_db_company_a',
|
||||
OFFLINE_DRAFT: 'offline_draft',
|
||||
SYSTEM_SETTINGS: 'system_settings',
|
||||
HISTORY_PAGE: 'history_page',
|
||||
BOOKMARK_PAGE: 'bookmark_page',
|
||||
} as const;
|
||||
|
||||
export type AppStorageKeyValue = (typeof AppStorageKey)[keyof typeof AppStorageKey];
|
||||
|
||||
export const ENCRYPTED_KEYS = new Set<AppStorageKeyValue>([
|
||||
AppStorageKey.USER_PROFILE,
|
||||
AppStorageKey.ACCESS_TOKEN,
|
||||
AppStorageKey.REFRESH_TOKEN,
|
||||
]);
|
||||
|
||||
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([
|
||||
AppStorageKey.LANGUAGE,
|
||||
AppStorageKey.THEME,
|
||||
AppStorageKey.MOCK_DB_COMPANY_A,
|
||||
AppStorageKey.OFFLINE_DRAFT,
|
||||
AppStorageKey.SYSTEM_SETTINGS,
|
||||
AppStorageKey.HISTORY_PAGE,
|
||||
AppStorageKey.BOOKMARK_PAGE,
|
||||
]);
|
||||
|
||||
export const secureStorage = createLocalStorage<AppStorageKeyValue>({
|
||||
encryptedKeys: ENCRYPTED_KEYS,
|
||||
plainTextKeys: PLAIN_KEYS,
|
||||
});
|
||||
|
||||
export const secureIndexedDB = createIndexedDB<AppStorageKeyValue>({
|
||||
dbName: 'e_apps_db',
|
||||
storeName: 'web_store',
|
||||
encryptedKeys: ENCRYPTED_KEYS,
|
||||
plainTextKeys: PLAIN_KEYS,
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './item.pouchdb.entity';
|
||||
export * from './pos-configuration.pouchdb.entity';
|
||||
export * from './new-item.pouchdb.entity';
|
||||
@@ -0,0 +1,27 @@
|
||||
interface ItemRateEntity {
|
||||
season_period?: string | null;
|
||||
price: string | number;
|
||||
}
|
||||
|
||||
interface ItemCategoryEntity {
|
||||
_id?: string;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a sellable product or service.
|
||||
*/
|
||||
export interface ItemEntity {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
name: string;
|
||||
base_price: string | number;
|
||||
item_type: string;
|
||||
usage_type?: string;
|
||||
item_category?: ItemCategoryEntity[] | ItemCategoryEntity | string;
|
||||
item_rates?: ItemRateEntity[];
|
||||
|
||||
// Allow for other ERP-specific fields
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export interface SeasonType {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SeasonPeriod {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
season_type: SeasonType;
|
||||
}
|
||||
|
||||
export interface ItemRate {
|
||||
id: string;
|
||||
price: string;
|
||||
season_period: SeasonPeriod;
|
||||
}
|
||||
|
||||
export interface ItemCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed interface for an Item entity, based on the provided JSON sample.
|
||||
* This represents the business data payload (`data` property in the envelope).
|
||||
*/
|
||||
export interface NewItemData {
|
||||
id: string;
|
||||
creator_id: string;
|
||||
creator_name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
status: 'active' | 'inactive' | string;
|
||||
item_type: string;
|
||||
hpp: string;
|
||||
sales_margin: string;
|
||||
share_profit: string;
|
||||
total_price: number;
|
||||
base_price: string;
|
||||
play_estimation: number;
|
||||
use_queue: boolean;
|
||||
show_to_booking: boolean;
|
||||
breakdown_bundling: boolean;
|
||||
limit_type: string;
|
||||
limit_value: number;
|
||||
item_category_id: string;
|
||||
item_category: ItemCategory;
|
||||
item_rates: ItemRate[];
|
||||
price: string;
|
||||
qty: number;
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ItemEntity } from './item.pouchdb.entity';
|
||||
|
||||
/**
|
||||
* Represents the configuration and assigned data for a specific Point of Sale terminal.
|
||||
*/
|
||||
export interface POSConfigurationEntity {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
pos_number: string;
|
||||
pos_name: string;
|
||||
items: ItemEntity[];
|
||||
payment_methods?: any[];
|
||||
|
||||
// Allow for other ERP-specific fields
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Multi-Database PouchDB Configuration for apps/web.
|
||||
*
|
||||
* This module demonstrates the IoC pattern: the consuming app decides
|
||||
* which databases to create and where they sync to. The core engine
|
||||
* (`PouchDBManager` and `PouchEnvelopeManager`) has zero knowledge of business domains.
|
||||
*/
|
||||
import { PouchDBManager, PouchEnvelopeDBManager } from '@repo/core-storage';
|
||||
import { ItemEntity, POSConfigurationEntity } from './entities';
|
||||
|
||||
// ─── Manager Singletons ──────────────────────────────────────────
|
||||
|
||||
export const dbManager = new PouchDBManager();
|
||||
export const envelopeDbManager = new PouchEnvelopeDBManager();
|
||||
|
||||
// ─── Helper: Build Secure Remote URL ────────────────────────────
|
||||
|
||||
function buildRemoteUrl(dbName: string): string | undefined {
|
||||
// In Showcase, we default to offline-only mode since there's no ENV
|
||||
console.info(`[DB Config] Showcase running "${dbName}" in offline-only mode.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ─── Register Application Databases ─────────────────────────────
|
||||
|
||||
/** POS Configuration database — stores device settings, theme, etc. */
|
||||
export const posConfigDB = dbManager.register<POSConfigurationEntity>({
|
||||
localName: 'pos_configuration',
|
||||
remoteUrl: buildRemoteUrl('pos_configuration'),
|
||||
});
|
||||
|
||||
/** Items database — products available for sale in POS. */
|
||||
export const itemDB = dbManager.register<ItemEntity>({
|
||||
localName: 'item',
|
||||
remoteUrl: buildRemoteUrl('item'),
|
||||
});
|
||||
|
||||
/** New Items database using the Envelope Service pattern. */
|
||||
export const newItemDB = envelopeDbManager.register(
|
||||
{
|
||||
localName: 'master_data',
|
||||
remoteUrl: buildRemoteUrl('master_data'),
|
||||
},
|
||||
'item',
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import type { ColorSchemeType } from '@repo/ui/provider';
|
||||
import { AppStorageKey } from '../storage/local';
|
||||
|
||||
interface ThemeState {
|
||||
colorScheme: ColorSchemeType;
|
||||
setColorScheme: (scheme: ColorSchemeType) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global theme store with localStorage persistence.
|
||||
*
|
||||
* Uses the Zustand `persist` middleware so the chosen color scheme
|
||||
* survives page refreshes. The storage key (`app_theme`) is
|
||||
* intentionally kept in sync with `AppStorageKey.THEME` — both
|
||||
* write to the same localStorage entry.
|
||||
*/
|
||||
export const useThemeStore = create<ThemeState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
colorScheme: 'light',
|
||||
setColorScheme: (scheme) => set({ colorScheme: scheme }),
|
||||
}),
|
||||
{
|
||||
name: AppStorageKey.THEME, // matches AppStorageKey.THEME
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,131 @@
|
||||
import { AppShell, NavLink, Group, Title, Box, Select, Text, Paper } from '@repo/ui/components';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from '@repo/core-i18n';
|
||||
import { Layout, FileText, Database, ShieldCheck, Lock, Activity, Printer, Globe, Layers, Table2 } from 'lucide-react';
|
||||
import { DensityType } from '@repo/ui/provider';
|
||||
import { useThemeStore } from '../core/stores/theme.store';
|
||||
import { ColorSchemeType } from '@repo/ui/provider';
|
||||
|
||||
interface ShowcaseLayoutProps {
|
||||
density: DensityType;
|
||||
setDensity: (val: DensityType) => void;
|
||||
}
|
||||
|
||||
export default function ShowcaseLayout({ density, setDensity }: ShowcaseLayoutProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { i18n } = useTranslation();
|
||||
const { colorScheme, setColorScheme } = useThemeStore();
|
||||
|
||||
const navItems = [
|
||||
{ label: 'UI Components', path: '/ui-components', icon: Layout },
|
||||
{ label: 'Layout Engine', path: '/shell-demo', icon: Layout },
|
||||
{ label: 'Form Engine', path: '/forms', icon: FileText },
|
||||
{ label: 'Offline Storage', path: '/storage', icon: Database },
|
||||
{ label: 'RBAC Engine', path: '/rbac', icon: ShieldCheck },
|
||||
{ label: 'Auth & Security', path: '/auth', icon: Lock },
|
||||
{ label: 'Events', path: '/events', icon: Activity },
|
||||
{ label: 'Hardware', path: '/hardware', icon: Printer },
|
||||
{ label: 'Action Tools', path: '/action-tools', icon: Layers },
|
||||
{ label: 'AG Grid', path: '/ag-grid', icon: Table2 },
|
||||
];
|
||||
|
||||
const getSubtitle = () => {
|
||||
const currentPath = location.pathname.replace('/', '');
|
||||
|
||||
switch (currentPath) {
|
||||
case 'rbac': return 'Role-Based Access Control and Permissions';
|
||||
case 'storage': return 'Offline-First PouchDB Synchronization';
|
||||
case 'auth': return 'Authentication & Security Layers';
|
||||
case 'ui-components': return 'Theme, Typography, Forms & Data Grids';
|
||||
case 'forms': return 'Enterprise Form Engine & Zod Validation';
|
||||
case 'events': return 'Global Event Bus Synchronization';
|
||||
case 'hardware': return 'Hardware Integration & Printers';
|
||||
case 'layout-engine': return 'Core Layout Engine & Variants';
|
||||
case 'action-tools': return 'Showcase for PageActions and RowActions components';
|
||||
case 'ag-grid': return 'Enterprise Data Grid with Mantine Theme Integration';
|
||||
default: return 'Architecture Showcase';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
navbar={{
|
||||
width: 260,
|
||||
breakpoint: 'sm',
|
||||
}}
|
||||
padding="0"
|
||||
>
|
||||
<AppShell.Navbar p="md">
|
||||
<Box mb="xl">
|
||||
<Title order={3} c="brand.7">Eigen ERP</Title>
|
||||
<Text size="xs" c="dimmed">Architecture Showcase</Text>
|
||||
</Box>
|
||||
<Box style={{ flex: 1 }}>
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
active={location.pathname === item.path}
|
||||
label={item.label}
|
||||
leftSection={<item.icon size={18} />}
|
||||
onClick={() => navigate(item.path)}
|
||||
variant="filled"
|
||||
style={{ borderRadius: 'var(--mantine-radius-md)', marginBottom: 4 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box mt="auto" pt="md" style={{ borderTop: '1px solid var(--mantine-color-default-border)' }}>
|
||||
<Select
|
||||
label="Color Scheme"
|
||||
size="xs"
|
||||
mb="xs"
|
||||
value={colorScheme}
|
||||
onChange={(val) => setColorScheme((val as ColorSchemeType) || 'light')}
|
||||
data={[
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Density"
|
||||
size="xs"
|
||||
value={density}
|
||||
onChange={(val) => setDensity((val as DensityType) || 'standard')}
|
||||
data={[
|
||||
{ value: 'compact', label: 'Compact' },
|
||||
{ value: 'standard', label: 'Standard' },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main style={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' }}>
|
||||
<Paper p="md" radius={0} withBorder style={{ borderTop: 0, borderLeft: 0, borderRight: 0, zIndex: 10, flexShrink: 0 }}>
|
||||
<Group justify="space-between">
|
||||
<Box>
|
||||
<Title order={3}>Architecture Showcase</Title>
|
||||
<Text size="sm" c="dimmed">{getSubtitle()}</Text>
|
||||
</Box>
|
||||
<Select
|
||||
w={180}
|
||||
size="sm"
|
||||
variant="filled"
|
||||
leftSection={<Globe size={16} />}
|
||||
data={[
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'id', label: 'Bahasa Indonesia' },
|
||||
]}
|
||||
value={i18n.resolvedLanguage || i18n.language}
|
||||
onChange={(val) => val && i18n.changeLanguage(val)}
|
||||
/>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Box className="flex-1 overflow-y-auto p-6" style={{ height: 'calc(100vh - 80px)' }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@repo/ui/theme.css';
|
||||
@source "../../../packages/ui/src";
|
||||
@@ -0,0 +1,17 @@
|
||||
import './main.css';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { setupI18n } from '@repo/core-i18n';
|
||||
import App from './App';
|
||||
|
||||
async function bootstrap() {
|
||||
await setupI18n({}, 'en'); // Minimal i18n setup
|
||||
|
||||
createRoot(document.getElementById('app')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Container, Card, Title, Text } from '@repo/ui/components';
|
||||
|
||||
export default function AuthPage() {
|
||||
return (
|
||||
<Container size="xl" m={0} p={0}>
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Auth & Security</Title>
|
||||
<Text c="dimmed">Auth Demo Component Coming Soon...</Text>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { usePublishEvent } from '@repo/core-events';
|
||||
import { Button, Group, Stack, TextInput, Badge } from '@repo/ui/components';
|
||||
import { AUTH_EVENTS } from '../../../../core/constants/events';
|
||||
import { AUTH_EVENTS } from '../../../../../core/constants/events';
|
||||
|
||||
// ─── ProfileSettingsUI ──────────────────────────────────────────
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { useAppEvent } from '@repo/core-events';
|
||||
import { AUTH_EVENTS } from '../../../../core/constants/events';
|
||||
import { AUTH_EVENTS } from '../../../../../core/constants/events';
|
||||
import type { ProfileUpdatedPayload } from '@repo/core-events';
|
||||
import { secureIndexedDB, AppStorageKey } from '../../../../core/storage/local';
|
||||
import { secureIndexedDB, AppStorageKey } from '../../../../../core/storage/local';
|
||||
|
||||
// ─── Props ──────────────────────────────────────────────────────
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { PrinterListener } from './printer/printer.listener';
|
||||
import { LiveStockGrid } from './stock-grid/live-stock-grid.ui';
|
||||
import { ProfileSettingsUI } from './auth-sync/profile-settings.ui';
|
||||
import { StorageSyncListener } from './auth-sync/storage-sync.listener';
|
||||
import { DEVICE_EVENTS, AUTH_EVENTS } from '../../../core/constants/events';
|
||||
import { DEVICE_EVENTS, AUTH_EVENTS } from '../../../../core/constants/events';
|
||||
|
||||
// ─── Events Demo Page ───────────────────────────────────────────
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
||||
Table,
|
||||
Badge,
|
||||
} from '@repo/ui/components';
|
||||
import { DEVICE_EVENTS } from '../../../../core/constants/events';
|
||||
import { DEVICE_EVENTS } from '../../../../../core/constants/events';
|
||||
|
||||
// ─── Mock Receipt Data ──────────────────────────────────────────
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { useAppEvent } from '@repo/core-events';
|
||||
import { DEVICE_EVENTS } from '../../../../core/constants/events';
|
||||
import { DEVICE_EVENTS } from '../../../../../core/constants/events';
|
||||
import type { PrintReceiptPayload } from '@repo/core-events';
|
||||
|
||||
// ─── Props ──────────────────────────────────────────────────────
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { publish } from '@repo/core-events';
|
||||
import { WS_EVENTS } from '../../../../core/constants/events';
|
||||
import { WS_EVENTS } from '../../../../../core/constants/events';
|
||||
|
||||
// ─── Stock Tickers ──────────────────────────────────────────────
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { memo, useState, useRef } from 'react';
|
||||
import { useAppEvent } from '@repo/core-events';
|
||||
import { WS_EVENTS } from '../../../../core/constants/events';
|
||||
import { WS_EVENTS } from '../../../../../core/constants/events';
|
||||
|
||||
// ─── Props ──────────────────────────────────────────────────────
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { BaseRemoteDataServices } from '@repo/core-api/data-services';
|
||||
import type { ApiResponse } from '@repo/core-api/http-client';
|
||||
import { apiClient } from '../../../../../../core/lib/api-client';
|
||||
import { apiClient } from '../../../../../../../core/lib/api-client';
|
||||
import type { BookingEntity } from './booking.data-services';
|
||||
import type { BookingDTO } from './booking.transformer';
|
||||
import {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||
import { apiClient } from '../../../../../../core/lib/api-client';
|
||||
import { apiClient } from '../../../../../../../core/lib/api-client';
|
||||
import { BookingTransformer } from './booking.transformer';
|
||||
import type { BookingDTO } from './booking.transformer';
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation, changeLanguage, applyTenantOverrides, i18n } from '@repo/core-i18n';
|
||||
import { secureIndexedDB, AppStorageKey } from '../../../../../../core/storage/local';
|
||||
import { secureIndexedDB, AppStorageKey } from '../../../../../../../core/storage/local';
|
||||
|
||||
// Decentralized languages imports
|
||||
import bookingId from '../languages/id/booking.json';
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { secureStorage, secureIndexedDB, AppStorageKey } from '../../../../../../core/storage/local';
|
||||
import { secureStorage, secureIndexedDB, AppStorageKey } from '../../../../../../../core/storage/local';
|
||||
|
||||
// ─── Demo Data ──────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Stack, Container, Card, Title, Text } from '@repo/ui/components';
|
||||
import ExamplePage from './components/example/example.page';
|
||||
import EventsDemoPage from './components/events-demo';
|
||||
|
||||
export default function EventsPage() {
|
||||
return (
|
||||
<Container size="xl" m={0} p={0}>
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Nested Showcase Example</Title>
|
||||
<Text mb="md">This is an example of a nested showcase component.</Text>
|
||||
<ExamplePage />
|
||||
</Card>
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<EventsDemoPage />
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Stack, Container } from '@repo/ui/components';
|
||||
import FormDemoView from './components/form-demo';
|
||||
|
||||
export default function FormsPage() {
|
||||
return (
|
||||
<Container size="xl" m={0} p={0}>
|
||||
<Stack gap="xl">
|
||||
<FormDemoView />
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
// 1. Import hooks yang baru saja dibuat Opus
|
||||
import { Button } from '@repo/ui/components';
|
||||
import { useElectronPrinter } from '../../core/hooks/use-electron-printer';
|
||||
import { useElectronUpdater } from '../../core/hooks/use-electron-updater';
|
||||
import { useElectronPrinter } from '../../../core/hooks/use-electron-printer';
|
||||
import { useElectronUpdater } from '../../../core/hooks/use-electron-updater';
|
||||
|
||||
export default function App() {
|
||||
// 2. Panggil hooks-nya
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Container, Card } from '@repo/ui/components';
|
||||
import PrinterList from './components/printer-list';
|
||||
|
||||
export default function HardwarePage() {
|
||||
return (
|
||||
<Container size="xl" m={0} p={0}>
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<PrinterList />
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Container, Card, Title, Text } from '@repo/ui/components';
|
||||
|
||||
export default function RbacPage() {
|
||||
return (
|
||||
<Container size="xl" m={0} p={0}>
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">RBAC Engine</Title>
|
||||
<Text c="dimmed">RBAC Demo Component Coming Soon...</Text>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -246,7 +246,7 @@ export default function ShellDemo() {
|
||||
Layout Engine Interactive Demo
|
||||
</Text>
|
||||
<Group>
|
||||
<Button component={Link} to="/showcase" leftSection={<ArrowLeft size={16} />} variant="default">
|
||||
<Button component={Link} to="/" leftSection={<ArrowLeft size={16} />} variant="default">
|
||||
Back to Showcase
|
||||
</Button>
|
||||
</Group>
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { Button, Card, Group, Stack, Title, Text, Table, Badge } from '@repo/ui/components';
|
||||
|
||||
import { itemDB, posConfigDB, newItemDB } from '../../core/storage/pouch-db';
|
||||
import type { ItemEntity, POSConfigurationEntity } from '../../core/storage/pouch-db/entities';
|
||||
import { itemDB, posConfigDB, newItemDB } from '../../../core/storage/pouch-db';
|
||||
import type { ItemEntity, POSConfigurationEntity } from '../../../core/storage/pouch-db/entities';
|
||||
|
||||
export default function PouchSample() {
|
||||
const [configs, setConfigs] = useState<POSConfigurationEntity[]>([]);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Stack, Container } from '@repo/ui/components';
|
||||
import PouchSample from './components/pouch-sample';
|
||||
|
||||
export default function StoragePage() {
|
||||
return (
|
||||
<Container size="xl" m={0} p={0}>
|
||||
<Stack gap="xl">
|
||||
<PouchSample />
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Stack, Card, Title, Group, Select, Text, Badge, Divider, Button, TextInput, NumberInput, PasswordInput, Textarea, Checkbox, Switch, Table, StatusBadge, STATUS_DATA, Container } from '@repo/ui/components';
|
||||
import { useThemeStore } from '../../core/stores/theme.store';
|
||||
import { ColorSchemeType } from '@repo/ui/provider';
|
||||
|
||||
const tableData = [
|
||||
{ id: 'ORD-001', customer: 'John Doe', status: 'Shipped', total: '$120.00' },
|
||||
{ id: 'ORD-002', customer: 'Jane Smith', status: 'Pending', total: '$85.50' },
|
||||
{ id: 'ORD-003', customer: 'Acme Corp', status: 'Delivered', total: '$1,250.00' },
|
||||
];
|
||||
|
||||
export default function UiComponentsPage() {
|
||||
const { colorScheme, setColorScheme } = useThemeStore();
|
||||
|
||||
return (
|
||||
<Container size="xl" m={0} p={0}>
|
||||
<Stack gap="xl">
|
||||
{/* Control Panel */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Theme Controls</Title>
|
||||
<Group grow align="flex-end">
|
||||
<Select
|
||||
label="Color Scheme"
|
||||
value={colorScheme}
|
||||
onChange={(val) => setColorScheme((val as ColorSchemeType) || 'light')}
|
||||
data={[
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
{/* Typography & Buttons */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Title order={4} mb="xs">Typography & Badges</Title>
|
||||
<Text size="sm" c="dimmed">This is dimmed small text indicating a subtitle.</Text>
|
||||
<Group mt="md">
|
||||
<Badge color="brand">Brand Badge</Badge>
|
||||
<Badge color="success" variant="light">Success Status</Badge>
|
||||
<Badge color="error" variant="outline">Error State</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
<Divider />
|
||||
<div>
|
||||
<Title order={4} mb="xs">Enterprise Status Badges</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">Pre-configured status badges for transaction and master data.</Text>
|
||||
<Group>
|
||||
<StatusBadge status={STATUS_DATA.DRAFT} />
|
||||
<StatusBadge status={STATUS_DATA.PENDING} />
|
||||
<StatusBadge status={STATUS_DATA.PROCESS} />
|
||||
<StatusBadge status={STATUS_DATA.APPROVED} />
|
||||
<StatusBadge status={STATUS_DATA.CANCELLED} />
|
||||
<StatusBadge status={STATUS_DATA.WAITING} />
|
||||
<StatusBadge status={STATUS_DATA.ON_HOLD} />
|
||||
<StatusBadge status={STATUS_DATA.REFUNDED} />
|
||||
</Group>
|
||||
</div>
|
||||
<Divider />
|
||||
<div>
|
||||
<Title order={4} mb="md">Buttons</Title>
|
||||
<Group>
|
||||
<Button variant="filled" color="brand">Filled Button</Button>
|
||||
<Button variant="outline" color="brand">Outline Button</Button>
|
||||
<Button variant="light" color="info">Light Info</Button>
|
||||
<Button variant="subtle" color="error">Cancel</Button>
|
||||
</Group>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Forms */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Form Elements</Title>
|
||||
<Stack gap="md">
|
||||
<Group grow align="flex-start">
|
||||
<TextInput label="First Name" placeholder="Enter your first name" withAsterisk />
|
||||
<TextInput label="Last Name" placeholder="Enter your last name" />
|
||||
</Group>
|
||||
<Group grow align="flex-start">
|
||||
<NumberInput label="Age" placeholder="25" min={0} max={100} />
|
||||
<PasswordInput label="Password" placeholder="Your secret password" withAsterisk />
|
||||
</Group>
|
||||
<Textarea label="Bio" placeholder="Tell us about yourself" minRows={3} />
|
||||
<Group mt="sm">
|
||||
<Checkbox label="I agree to the terms and conditions" defaultChecked />
|
||||
<Switch label="Enable notifications" defaultChecked />
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Data Grid */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">Data Grid</Title>
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Order ID</Table.Th>
|
||||
<Table.Th>Customer</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Total</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{tableData.map((row) => (
|
||||
<Table.Tr key={row.id}>
|
||||
<Table.Td>{row.id}</Table.Td>
|
||||
<Table.Td>{row.customer}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" color={row.status === 'Delivered' ? 'success' : row.status === 'Shipped' ? 'info' : 'warning'}>
|
||||
{row.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{row.total}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Type declarations for the Electron preload API.
|
||||
*
|
||||
* When running inside Electron, `window.electronAPI` is defined.
|
||||
* When running in a regular browser, it is `undefined`.
|
||||
*
|
||||
* Usage:
|
||||
* if (window.electronAPI) {
|
||||
* const printers = await window.electronAPI.getPrinters();
|
||||
* }
|
||||
*/
|
||||
|
||||
// ─── Printer types ──────────────────────────────────────────────
|
||||
|
||||
interface ElectronPrinterInfo {
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
status: number;
|
||||
isDefault: boolean;
|
||||
options?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ElectronPrintOptions {
|
||||
silent?: boolean;
|
||||
printBackground?: boolean;
|
||||
deviceName?: string;
|
||||
color?: boolean;
|
||||
margins?: {
|
||||
marginType?: 'default' | 'none' | 'printableArea' | 'custom';
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
};
|
||||
landscape?: boolean;
|
||||
scaleFactor?: number;
|
||||
pagesPerSheet?: number;
|
||||
collate?: boolean;
|
||||
copies?: number;
|
||||
pageRanges?: Array<{ from: number; to: number }>;
|
||||
duplexMode?: 'simplex' | 'shortEdge' | 'longEdge';
|
||||
header?: string;
|
||||
footer?: string;
|
||||
}
|
||||
|
||||
interface ElectronPrintResult {
|
||||
success: boolean;
|
||||
failureReason?: string;
|
||||
}
|
||||
|
||||
// ─── Auto-Update types ──────────────────────────────────────────
|
||||
|
||||
interface ElectronUpdateInfo {
|
||||
version: string;
|
||||
releaseDate: string;
|
||||
releaseName?: string | null;
|
||||
releaseNotes?: string | null;
|
||||
}
|
||||
|
||||
interface ElectronProgressInfo {
|
||||
total: number;
|
||||
delta: number;
|
||||
transferred: number;
|
||||
percent: number;
|
||||
bytesPerSecond: number;
|
||||
}
|
||||
|
||||
// ─── ElectronAPI interface ──────────────────────────────────────
|
||||
|
||||
interface ElectronAPI {
|
||||
// Printing
|
||||
getPrinters: () => Promise<ElectronPrinterInfo[]>;
|
||||
print: (options?: ElectronPrintOptions) => Promise<ElectronPrintResult>;
|
||||
|
||||
// Auto-Update: Commands
|
||||
checkForUpdates: () => void;
|
||||
installUpdate: () => void;
|
||||
|
||||
// Auto-Update: Event Subscriptions
|
||||
// Each returns an unsubscribe function.
|
||||
onUpdateChecking: (callback: () => void) => () => void;
|
||||
onUpdateAvailable: (callback: (info: ElectronUpdateInfo) => void) => () => void;
|
||||
onUpdateNotAvailable: (callback: (info: ElectronUpdateInfo) => void) => () => void;
|
||||
onDownloadProgress: (callback: (progress: ElectronProgressInfo) => void) => () => void;
|
||||
onUpdateDownloaded: (callback: (info: ElectronUpdateInfo) => void) => () => void;
|
||||
onUpdateError: (callback: (error: string) => void) => () => void;
|
||||
}
|
||||
|
||||
// ─── Augment the global Window interface ────────────────────────
|
||||
|
||||
interface Window {
|
||||
/**
|
||||
* Available only when running inside Electron.
|
||||
* Always check `if (window.electronAPI)` before use.
|
||||
*/
|
||||
electronAPI?: ElectronAPI;
|
||||
}
|
||||
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* App-level event registry for `apps/web`.
|
||||
*
|
||||
* This file uses TypeScript Declaration Merging (Module Augmentation)
|
||||
* to extend the open `AppEventRegistry` interface exported by
|
||||
* `@repo/core-events`. This is the IoC pattern in action:
|
||||
*
|
||||
* - `@repo/core-events` provides the bus, hooks, and helpers (the tool).
|
||||
* - `apps/web` defines which events exist and their payload shapes (the contract).
|
||||
*
|
||||
* The core package has zero knowledge of these events. If `apps/web`
|
||||
* is removed from the monorepo, the core package remains unchanged.
|
||||
*
|
||||
* **Adding new events**: Simply add new entries to `AppEventRegistry`
|
||||
* below. TypeScript will automatically provide autocomplete and
|
||||
* type safety across every `publish()` / `useAppEvent()` call in
|
||||
* the web app.
|
||||
*
|
||||
* @see packages/core-events/src/events.registry.ts
|
||||
*/
|
||||
|
||||
// This import turns this file from an ambient declaration into a
|
||||
// module augmentation. Without it, `declare module` would REPLACE
|
||||
// the module signature instead of merging into it.
|
||||
import type {} from '@repo/core-events';
|
||||
|
||||
declare module '@repo/core-events' {
|
||||
|
||||
// ─── Payload Types ──────────────────────────────────────────
|
||||
|
||||
interface ReceiptItem {
|
||||
name: string;
|
||||
qty: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
interface PrintReceiptPayload {
|
||||
receiptId: string;
|
||||
items: ReceiptItem[];
|
||||
total: number;
|
||||
cashierName: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface StockUpdatePayload {
|
||||
id: string;
|
||||
price: number;
|
||||
change: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
interface ProfileUpdatedPayload {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// ─── Event Registry ─────────────────────────────────────────
|
||||
|
||||
interface AppEventRegistry {
|
||||
// ── Device / Hardware ─────────────────────────────────────
|
||||
'DEVICE:PRINT_RECEIPT': PrintReceiptPayload;
|
||||
|
||||
// ── WebSocket / Real-Time ────────────────────────────────
|
||||
'WS:STOCK_UPDATE': StockUpdatePayload;
|
||||
|
||||
// ── Auth / User ──────────────────────────────────────────
|
||||
'AUTH:PROFILE_UPDATED': ProfileUpdatedPayload;
|
||||
|
||||
// ── App Lifecycle ────────────────────────────────────────
|
||||
'APP:INITIALIZED': undefined;
|
||||
'APP:ERROR': { message: string; code?: string };
|
||||
|
||||
// ── Layout ───────────────────────────────────────────────
|
||||
'LAYOUT:TOGGLE_HISTORY_DRAWER': undefined;
|
||||
'LAYOUT:TOGGLE_BOOKMARK_DRAWER': undefined;
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/react-app.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
port: 3005,
|
||||
strictPort: true,
|
||||
},
|
||||
define: {
|
||||
global: 'window',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
events: 'events',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { lazy, Suspense, useEffect, useState } from 'react';
|
||||
import { lazy, Suspense, useEffect } from 'react';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { ThemeProvider, DensityType } from '@repo/ui/provider';
|
||||
import { ThemeProvider } from '@repo/ui/provider';
|
||||
import { NotFound, Forbidden, Maintenance, ComingSoon, AgGridProvider } from '@repo/ui/components';
|
||||
import { LoadingScreen } from '../core/components/loading-screen';
|
||||
import { useThemeStore } from '../core/stores/theme.store';
|
||||
@@ -8,12 +8,9 @@ import { initializeAndPurgeHistoryBackground } from './modules/layouts/hooks/use
|
||||
|
||||
const AuthModule = lazy(() => import('./auth'));
|
||||
const AppModule = lazy(() => import('./modules'));
|
||||
const ShowcaseView = lazy(() => import('./showcase/showcase-view'));
|
||||
const ShellDemo = lazy(() => import('./showcase/shell-demo'));
|
||||
|
||||
export default function App() {
|
||||
const colorScheme = useThemeStore((s) => s.colorScheme);
|
||||
const [density, setDensity] = useState<DensityType>('compact');
|
||||
|
||||
useEffect(() => {
|
||||
// Execution runs purely in the background (fire and forget)
|
||||
@@ -22,20 +19,18 @@ export default function App() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider colorScheme={colorScheme} density={density}>
|
||||
<ThemeProvider colorScheme={colorScheme} density={'compact'}>
|
||||
<AgGridProvider bypassLicense>
|
||||
<BrowserRouter>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<Routes>
|
||||
<Route path="/auth/*" element={<AuthModule />} />
|
||||
<Route path="/app/*" element={<AppModule />} />
|
||||
<Route path="/showcase" element={<ShowcaseView density={density} setDensity={setDensity} />} />
|
||||
<Route path="/shell-demo" element={<ShellDemo />} />
|
||||
<Route path="/404" element={<NotFound homeUrl="/app" />} />
|
||||
<Route path="/403" element={<Forbidden homeUrl="/app" />} />
|
||||
<Route path="/maintenance" element={<Maintenance />} />
|
||||
<Route path="/coming-soon" element={<ComingSoon />} />
|
||||
<Route path="/" element={<Navigate to="/showcase" />} />
|
||||
<Route path="/" element={<Navigate to="/app" />} />
|
||||
<Route path="*" element={<Navigate to="/404" />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { lazy } from 'react';
|
||||
import { Layers, Table2 } from 'lucide-react';
|
||||
|
||||
export const COMPONENTS_REGISTRY = {
|
||||
actionTools: {
|
||||
id: 'action-tools',
|
||||
name: 'Action Tools',
|
||||
description: 'Showcase for PageActions and RowActions components',
|
||||
icon: Layers,
|
||||
component: lazy(() => import('./components/ActionToolsShowcase')),
|
||||
},
|
||||
agGrid: {
|
||||
id: 'ag-grid',
|
||||
name: 'AG Grid',
|
||||
description: 'Enterprise Data Grid with Mantine Theme Integration',
|
||||
icon: Table2,
|
||||
component: lazy(() => import('./components/AgGridShowcase')),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,437 +0,0 @@
|
||||
import { useState, Suspense } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ColorSchemeType, DensityType } from '@repo/ui/provider';
|
||||
import { useThemeStore } from '../../core/stores/theme.store';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Select,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
TextInput,
|
||||
Checkbox,
|
||||
PasswordInput,
|
||||
NumberInput,
|
||||
Textarea,
|
||||
Switch,
|
||||
Table,
|
||||
Badge,
|
||||
Divider,
|
||||
Tabs,
|
||||
Box,
|
||||
Paper,
|
||||
StatusBadge,
|
||||
STATUS_DATA,
|
||||
} from '@repo/ui/components';
|
||||
import { ShieldCheck, Database, Lock, Layout, Activity, Printer, FileText, LayoutDashboard } from 'lucide-react';
|
||||
import { Globe } from 'lucide-react';
|
||||
import { useTranslation } from '@repo/core-i18n';
|
||||
import PrinterList from './printer-list';
|
||||
import ExamplePage from './example/example.page';
|
||||
import EventsDemoPage from './events-demo';
|
||||
import PouchSample from './pouch-sample';
|
||||
import FormDemoView from './example/features/form-demo';
|
||||
import { COMPONENTS_REGISTRY } from './registry';
|
||||
|
||||
interface ShowcaseViewProps {
|
||||
density: DensityType;
|
||||
setDensity: (val: DensityType) => void;
|
||||
}
|
||||
|
||||
export default function ShowcaseView({ density, setDensity }: ShowcaseViewProps) {
|
||||
const { colorScheme, setColorScheme } = useThemeStore();
|
||||
const [activeTab, setActiveTab] = useState<string | null>('ui-components');
|
||||
const { i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Mock data for the table
|
||||
const tableData = [
|
||||
{ id: 'ORD-001', customer: 'John Doe', status: 'Shipped', total: '$120.00' },
|
||||
{ id: 'ORD-002', customer: 'Jane Smith', status: 'Pending', total: '$85.50' },
|
||||
{ id: 'ORD-003', customer: 'Acme Corp', status: 'Delivered', total: '$1,250.00' },
|
||||
];
|
||||
|
||||
const getSubtitle = () => {
|
||||
const registryItem = Object.values(COMPONENTS_REGISTRY).find((item) => item.id === activeTab);
|
||||
if (registryItem) {
|
||||
return registryItem.description;
|
||||
}
|
||||
|
||||
switch (activeTab) {
|
||||
case 'rbac':
|
||||
return 'Role-Based Access Control and Permissions';
|
||||
case 'storage':
|
||||
return 'Offline-First PouchDB Synchronization';
|
||||
case 'auth':
|
||||
return 'Authentication & Security Layers';
|
||||
case 'ui-components':
|
||||
return 'Theme, Typography, Forms & Data Grids';
|
||||
case 'forms':
|
||||
return 'Enterprise Form Engine & Zod Validation';
|
||||
case 'events':
|
||||
return 'Global Event Bus Synchronization';
|
||||
case 'hardware':
|
||||
return 'Hardware Integration & Printers';
|
||||
case 'layout-engine':
|
||||
return 'Core Layout Engine & Variants';
|
||||
default:
|
||||
return 'Architecture Showcase';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="min-h-screen" style={{ backgroundColor: 'var(--mantine-color-body)' }}>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
placement="left"
|
||||
value={activeTab}
|
||||
onChange={(val) => {
|
||||
if (val === 'layout-engine') {
|
||||
navigate('/shell-demo');
|
||||
} else if (val === 'apps') {
|
||||
navigate('/app');
|
||||
} else {
|
||||
setActiveTab(val);
|
||||
}
|
||||
}}
|
||||
variant="pills"
|
||||
radius="md"
|
||||
className="h-screen"
|
||||
styles={{
|
||||
root: { display: 'flex', height: '100vh', overflow: 'hidden' },
|
||||
list: {
|
||||
minWidth: 260,
|
||||
padding: '1rem',
|
||||
borderRight: '1px solid var(--app-shell-border-color)',
|
||||
backgroundColor: 'var(--mantine-color-default-element-bg)',
|
||||
},
|
||||
panel: { flex: 1, display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' },
|
||||
tab: { justifyContent: 'flex-start' }, // Ensure the entire tab content aligns left
|
||||
tabLabel: { textAlign: 'left', flex: 1 }, // Ensure the text pushes to fill and aligns left
|
||||
}}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Box mb="xl" px="sm">
|
||||
<Title order={3} c="brand.7">
|
||||
Eigen ERP
|
||||
</Title>
|
||||
<Text size="xs" c="dimmed">
|
||||
Architecture Showcase
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Tabs.Tab value="ui-components" leftSection={<Layout size={18} />}>
|
||||
UI Components
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="layout-engine" leftSection={<Layout size={18} />}>
|
||||
Layout Engine
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="forms" leftSection={<FileText size={18} />}>
|
||||
Form Engine
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="storage" leftSection={<Database size={18} />}>
|
||||
Offline Storage
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="rbac" leftSection={<ShieldCheck size={18} />}>
|
||||
RBAC Engine
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="auth" leftSection={<Lock size={18} />}>
|
||||
Auth & Security
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="events" leftSection={<Activity size={18} />}>
|
||||
Events
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="hardware" leftSection={<Printer size={18} />}>
|
||||
Hardware
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="apps" leftSection={<LayoutDashboard size={18} />}>
|
||||
Apps
|
||||
</Tabs.Tab>
|
||||
|
||||
{Object.values(COMPONENTS_REGISTRY).map((registryItem) => {
|
||||
const Icon = registryItem.icon;
|
||||
return (
|
||||
<Tabs.Tab key={registryItem.id} value={registryItem.id} leftSection={<Icon size={18} />}>
|
||||
{registryItem.name}
|
||||
</Tabs.Tab>
|
||||
);
|
||||
})}
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value={activeTab as string}>
|
||||
{/* Header */}
|
||||
<Paper
|
||||
p="md"
|
||||
radius={0}
|
||||
withBorder
|
||||
style={{
|
||||
borderTop: 0,
|
||||
borderLeft: 0,
|
||||
borderRight: 0,
|
||||
zIndex: 10,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Title order={3}>Architecture Showcase</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{getSubtitle()}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Select
|
||||
w={180}
|
||||
size="sm"
|
||||
variant="filled"
|
||||
leftSection={<Globe size={16} />}
|
||||
data={[
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'id', label: 'Bahasa Indonesia' },
|
||||
]}
|
||||
value={i18n.resolvedLanguage || i18n.language}
|
||||
onChange={(val) => val && i18n.changeLanguage(val)}
|
||||
/>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{/* Scrollable Content Area */}
|
||||
<Box className="flex-1 overflow-y-auto p-6" style={{ height: 'calc(100vh - 80px)' }}>
|
||||
<Container size="xl" m={0} p={0}>
|
||||
{/* --- UI COMPONENTS TAB --- */}
|
||||
{activeTab === 'ui-components' && (
|
||||
<Stack gap="xl">
|
||||
{/* Control Panel */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Theme Controls
|
||||
</Title>
|
||||
<Group grow align="flex-end">
|
||||
<Select
|
||||
label="Color Scheme"
|
||||
value={colorScheme}
|
||||
onChange={(val) => setColorScheme((val as ColorSchemeType) || 'light')}
|
||||
data={[
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Density (Spacing & Sizing)"
|
||||
value={density}
|
||||
onChange={(val) => setDensity((val as DensityType) || 'standard')}
|
||||
data={[
|
||||
{ value: 'compact', label: 'Compact (ERP Mode)' },
|
||||
{ value: 'standard', label: 'Standard (UI Mode)' },
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
{/* Typography & Buttons */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Title order={4} mb="xs">
|
||||
Typography & Badges
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
This is dimmed small text indicating a subtitle.
|
||||
</Text>
|
||||
<Group mt="md">
|
||||
<Badge color="brand">Brand Badge</Badge>
|
||||
<Badge color="success" variant="light">
|
||||
Success Status
|
||||
</Badge>
|
||||
<Badge color="error" variant="outline">
|
||||
Error State
|
||||
</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
<Divider />
|
||||
<div>
|
||||
<Title order={4} mb="xs">
|
||||
Enterprise Status Badges
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Pre-configured status badges for transaction and master data.
|
||||
</Text>
|
||||
<Group>
|
||||
<StatusBadge status={STATUS_DATA.DRAFT} />
|
||||
<StatusBadge status={STATUS_DATA.PENDING} />
|
||||
<StatusBadge status={STATUS_DATA.PROCESS} />
|
||||
<StatusBadge status={STATUS_DATA.APPROVED} />
|
||||
<StatusBadge status={STATUS_DATA.CANCELLED} />
|
||||
<StatusBadge status={STATUS_DATA.WAITING} />
|
||||
<StatusBadge status={STATUS_DATA.ON_HOLD} />
|
||||
<StatusBadge status={STATUS_DATA.REFUNDED} />
|
||||
</Group>
|
||||
</div>
|
||||
<Divider />
|
||||
<div>
|
||||
<Title order={4} mb="md">
|
||||
Buttons
|
||||
</Title>
|
||||
<Group>
|
||||
<Button variant="filled" color="brand">
|
||||
Filled Button
|
||||
</Button>
|
||||
<Button variant="outline" color="brand">
|
||||
Outline Button
|
||||
</Button>
|
||||
<Button variant="light" color="info">
|
||||
Light Info
|
||||
</Button>
|
||||
<Button variant="subtle" color="error">
|
||||
Cancel
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Forms */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Form Elements
|
||||
</Title>
|
||||
<Stack gap="md">
|
||||
<Group grow align="flex-start">
|
||||
<TextInput label="First Name" placeholder="Enter your first name" withAsterisk />
|
||||
<TextInput label="Last Name" placeholder="Enter your last name" />
|
||||
</Group>
|
||||
<Group grow align="flex-start">
|
||||
<NumberInput label="Age" placeholder="25" min={0} max={100} />
|
||||
<PasswordInput label="Password" placeholder="Your secret password" withAsterisk />
|
||||
</Group>
|
||||
<Textarea label="Bio" placeholder="Tell us about yourself" minRows={3} />
|
||||
<Group mt="sm">
|
||||
<Checkbox label="I agree to the terms and conditions" defaultChecked />
|
||||
<Switch label="Enable notifications" defaultChecked />
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Data Grid */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Data Grid
|
||||
</Title>
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Order ID</Table.Th>
|
||||
<Table.Th>Customer</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Total</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{tableData.map((row) => (
|
||||
<Table.Tr key={row.id}>
|
||||
<Table.Td>{row.id}</Table.Td>
|
||||
<Table.Td>{row.customer}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
size="sm"
|
||||
color={
|
||||
row.status === 'Delivered' ? 'success' : row.status === 'Shipped' ? 'info' : 'warning'
|
||||
}
|
||||
>
|
||||
{row.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{row.total}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- FORMS TAB --- */}
|
||||
{activeTab === 'forms' && (
|
||||
<Stack gap="xl">
|
||||
<FormDemoView />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- STORAGE TAB --- */}
|
||||
{activeTab === 'storage' && (
|
||||
<Stack gap="xl">
|
||||
<PouchSample />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- RBAC TAB --- */}
|
||||
{activeTab === 'rbac' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
RBAC Engine
|
||||
</Title>
|
||||
<Text c="dimmed">RBAC Demo Component Coming Soon...</Text>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- AUTH TAB --- */}
|
||||
{activeTab === 'auth' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Auth & Security
|
||||
</Title>
|
||||
<Text c="dimmed">Auth Demo Component Coming Soon...</Text>
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- EVENTS TAB --- */}
|
||||
{activeTab === 'events' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Title order={4} mb="md">
|
||||
Nested Showcase Example
|
||||
</Title>
|
||||
<Text mb="md">This is an example of a nested showcase component.</Text>
|
||||
<ExamplePage />
|
||||
</Card>
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<EventsDemoPage />
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- HARDWARE TAB --- */}
|
||||
{activeTab === 'hardware' && (
|
||||
<Stack gap="xl">
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<PrinterList />
|
||||
</Card>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* --- DYNAMIC COMPONENTS FROM REGISTRY --- */}
|
||||
{Object.values(COMPONENTS_REGISTRY).map((registryItem) => {
|
||||
const Component = registryItem.component;
|
||||
return (
|
||||
activeTab === registryItem.id && (
|
||||
<Stack gap="xl" key={registryItem.id}>
|
||||
<Suspense fallback={<Text>Loading {registryItem.name}...</Text>}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
</Box>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -8,11 +8,13 @@
|
||||
"build:landing": "turbo run build --filter=landing",
|
||||
"build:docs-dev": "turbo run build --filter=docs-dev",
|
||||
"build:desktop": "turbo run build --filter=desktop",
|
||||
"build:showcase": "turbo run build --filter=showcase",
|
||||
"dev": "turbo run dev",
|
||||
"dev:web": "turbo run dev --filter=web",
|
||||
"dev:landing": "turbo run dev --filter=landing",
|
||||
"dev:docs-dev": "turbo run dev --filter=docs-dev",
|
||||
"dev:desktop": "turbo run dev --filter=web --filter=desktop --parallel",
|
||||
"dev:showcase": "turbo run dev --filter=showcase",
|
||||
"package:desktop": "pnpm run build:desktop && pnpm --filter desktop run package",
|
||||
"package:mac": "pnpm run build:desktop && pnpm --filter desktop run package:mac",
|
||||
"package:win": "pnpm run build:desktop && pnpm --filter desktop run package:win",
|
||||
@@ -24,6 +26,7 @@
|
||||
"typecheck:web": "turbo run typecheck --filter=web",
|
||||
"typecheck:landing": "turbo run typecheck --filter=landing",
|
||||
"typecheck:desktop": "turbo run typecheck --filter=desktop",
|
||||
"typecheck:showcase": "turbo run typecheck --filter=showcase",
|
||||
"check:all": "turbo run lint typecheck test",
|
||||
"clean:workspaces": "rm -rf node_modules **/*/node_modules .turbo **/*/.turbo dist **/*/dist out **/*/out web-dist **/*/web-dist release **/*/release",
|
||||
"nuke": "pnpm run clean:workspaces && pnpm install && pnpm build"
|
||||
|
||||
@@ -44,7 +44,7 @@ import { BulkActionMenu } from './components/bulk-actions';
|
||||
import { ActionConfirmationModal, ACTION_TRANSLATION_MAP } from '../action-confirmation-modal';
|
||||
import { BulkActionConfirmationModal } from '../bulk-action-confirmation';
|
||||
import { TableFilterDrawer, TableFilterConfig } from './components/table-filter-drawer';
|
||||
import { TableSettingDrawer } from './components/table-setting-drawer';
|
||||
// import { TableSettingDrawer } from './components/table-setting-drawer';
|
||||
import { EntityId } from '../../../../../../core-api/src/data-services/types';
|
||||
|
||||
export * from 'ag-grid-community';
|
||||
|
||||
Generated
+98
-8
@@ -147,6 +147,97 @@ importers:
|
||||
specifier: ^5.1.4
|
||||
version: 5.4.17(@types/node@22.19.3)
|
||||
|
||||
apps/showcase:
|
||||
dependencies:
|
||||
'@hookform/resolvers':
|
||||
specifier: ^5.0.1
|
||||
version: 5.4.0(react-hook-form@7.79.0)
|
||||
'@repo/core-api':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core-api
|
||||
'@repo/core-events':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core-events
|
||||
'@repo/core-i18n':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core-i18n
|
||||
'@repo/core-storage':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core-storage
|
||||
'@repo/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
'@repo/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/utils
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18(vite@5.4.17)
|
||||
dayjs:
|
||||
specifier: ^1.11.19
|
||||
version: 1.11.19
|
||||
events:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
i18next:
|
||||
specifier: ^24.2.2
|
||||
version: 24.2.3(typescript@5.5.4)
|
||||
lucide-react:
|
||||
specifier: ^1.22.0
|
||||
version: 1.22.0(react@19.2.3)
|
||||
react:
|
||||
specifier: ^19.2.3
|
||||
version: 19.2.3
|
||||
react-dom:
|
||||
specifier: ^19.2.3
|
||||
version: 19.2.3(react@19.2.3)
|
||||
react-hook-form:
|
||||
specifier: ^7.56.4
|
||||
version: 7.79.0(react@19.2.3)
|
||||
react-i18next:
|
||||
specifier: ^15.4.0
|
||||
version: 15.7.4(i18next@24.2.3)(react-dom@19.2.3)(react@19.2.3)(typescript@5.5.4)
|
||||
react-router-dom:
|
||||
specifier: ^7.11.0
|
||||
version: 7.11.0(react-dom@19.2.3)(react@19.2.3)
|
||||
tailwindcss:
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18
|
||||
zod:
|
||||
specifier: ^3.25.36
|
||||
version: 3.25.76
|
||||
zustand:
|
||||
specifier: ^5.0.14
|
||||
version: 5.0.14(@types/react@19.2.7)(react@19.2.3)
|
||||
devDependencies:
|
||||
'@repo/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/configs/eslint
|
||||
'@repo/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/configs/typescript
|
||||
'@types/react':
|
||||
specifier: ^19.2.7
|
||||
version: 19.2.7
|
||||
'@types/react-dom':
|
||||
specifier: ^19.2.3
|
||||
version: 19.2.3(@types/react@19.2.7)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2(vite@5.4.17)
|
||||
eslint:
|
||||
specifier: ^8.57.1
|
||||
version: 8.57.1
|
||||
typescript:
|
||||
specifier: 5.5.4
|
||||
version: 5.5.4
|
||||
vite:
|
||||
specifier: ^5.1.4
|
||||
version: 5.4.17(@types/node@22.19.3)
|
||||
vitest:
|
||||
specifier: ^4.0.17
|
||||
version: 4.0.17(jsdom@26.1.0)
|
||||
|
||||
apps/web:
|
||||
dependencies:
|
||||
'@hookform/resolvers':
|
||||
@@ -236,7 +327,7 @@ importers:
|
||||
version: 5.4.17(@types/node@22.19.3)
|
||||
vitest:
|
||||
specifier: ^4.0.17
|
||||
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||
version: 4.0.17(jsdom@26.1.0)
|
||||
|
||||
packages/configs/eslint:
|
||||
dependencies:
|
||||
@@ -433,7 +524,7 @@ importers:
|
||||
version: 5.5.4
|
||||
vitest:
|
||||
specifier: ^4.0.17
|
||||
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||
version: 4.0.17(jsdom@26.1.0)
|
||||
|
||||
packages/ui:
|
||||
dependencies:
|
||||
@@ -597,7 +688,7 @@ importers:
|
||||
version: 5.5.4
|
||||
vitest:
|
||||
specifier: ^4.0.17
|
||||
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||
version: 4.0.17(jsdom@26.1.0)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -2103,7 +2194,7 @@ packages:
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@gar/promisify': 1.1.3
|
||||
semver: 7.7.1
|
||||
semver: 7.7.4
|
||||
dev: true
|
||||
|
||||
/@npmcli/git@5.0.8:
|
||||
@@ -9557,7 +9648,7 @@ packages:
|
||||
resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
dependencies:
|
||||
semver: 7.7.1
|
||||
semver: 7.7.4
|
||||
dev: false
|
||||
|
||||
/npm-normalize-package-bin@3.0.1:
|
||||
@@ -9571,7 +9662,7 @@ packages:
|
||||
dependencies:
|
||||
hosted-git-info: 7.0.2
|
||||
proc-log: 4.2.0
|
||||
semver: 7.7.1
|
||||
semver: 7.7.4
|
||||
validate-npm-package-name: 5.0.1
|
||||
dev: false
|
||||
|
||||
@@ -9582,7 +9673,7 @@ packages:
|
||||
npm-install-checks: 6.3.0
|
||||
npm-normalize-package-bin: 3.0.1
|
||||
npm-package-arg: 11.0.3
|
||||
semver: 7.7.1
|
||||
semver: 7.7.4
|
||||
dev: false
|
||||
|
||||
/npmlog@6.0.2:
|
||||
@@ -10962,7 +11053,6 @@ packages:
|
||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/serialize-error@7.0.1:
|
||||
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
|
||||
|
||||
Reference in New Issue
Block a user