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)
|
// 4173: VitePress Docs (Preview)
|
||||||
// 5173: Web App (Must be strictly 5173 for Electron IPC compatibility)
|
// 5173: Web App (Must be strictly 5173 for Electron IPC compatibility)
|
||||||
// 3000: Landing App (Isolated from Vite's default 517x blast radius)
|
// 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": {
|
"portsAttributes": {
|
||||||
"6060": {
|
"6060": {
|
||||||
@@ -33,6 +34,10 @@
|
|||||||
"label": "Web App (Electron Target)",
|
"label": "Web App (Electron Target)",
|
||||||
"onAutoForward": "notify"
|
"onAutoForward": "notify"
|
||||||
},
|
},
|
||||||
|
"3005": {
|
||||||
|
"label": "Showcase App",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
},
|
||||||
"3000": {
|
"3000": {
|
||||||
"label": "Landing App (Public)",
|
"label": "Landing App (Public)",
|
||||||
"onAutoForward": "notify"
|
"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
|
### 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 |
|
| File | Description |
|
||||||
| ---- | ----------- |
|
| ---- | ----------- |
|
||||||
| `apps/web/.../booking/data/booking.transformer.ts` | Basic transformer with snake_case ↔ camelCase mapping |
|
| `apps/showcase/.../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/showcase/.../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/showcase/.../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/advanced-booking.data-services.ts` | Extended service with custom `getAvailabilityChart()` |
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ function App() {
|
|||||||
|
|
||||||
### Interactive Config Builder
|
### 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
|
```tsx
|
||||||
import { useState, useMemo } from 'react';
|
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 { useState } from 'react';
|
||||||
import { usePublishEvent } from '@repo/core-events';
|
import { usePublishEvent } from '@repo/core-events';
|
||||||
import { Button, Group, Stack, TextInput, Badge } from '@repo/ui/components';
|
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 ──────────────────────────────────────────
|
// ─── ProfileSettingsUI ──────────────────────────────────────────
|
||||||
|
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { useAppEvent } from '@repo/core-events';
|
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 type { ProfileUpdatedPayload } from '@repo/core-events';
|
||||||
import { secureIndexedDB, AppStorageKey } from '../../../../core/storage/local';
|
import { secureIndexedDB, AppStorageKey } from '../../../../../core/storage/local';
|
||||||
|
|
||||||
// ─── Props ──────────────────────────────────────────────────────
|
// ─── Props ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
+1
-1
@@ -14,7 +14,7 @@ import { PrinterListener } from './printer/printer.listener';
|
|||||||
import { LiveStockGrid } from './stock-grid/live-stock-grid.ui';
|
import { LiveStockGrid } from './stock-grid/live-stock-grid.ui';
|
||||||
import { ProfileSettingsUI } from './auth-sync/profile-settings.ui';
|
import { ProfileSettingsUI } from './auth-sync/profile-settings.ui';
|
||||||
import { StorageSyncListener } from './auth-sync/storage-sync.listener';
|
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 ───────────────────────────────────────────
|
// ─── Events Demo Page ───────────────────────────────────────────
|
||||||
|
|
||||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Badge,
|
Badge,
|
||||||
} from '@repo/ui/components';
|
} from '@repo/ui/components';
|
||||||
import { DEVICE_EVENTS } from '../../../../core/constants/events';
|
import { DEVICE_EVENTS } from '../../../../../core/constants/events';
|
||||||
|
|
||||||
// ─── Mock Receipt Data ──────────────────────────────────────────
|
// ─── Mock Receipt Data ──────────────────────────────────────────
|
||||||
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { useAppEvent } from '@repo/core-events';
|
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';
|
import type { PrintReceiptPayload } from '@repo/core-events';
|
||||||
|
|
||||||
// ─── Props ──────────────────────────────────────────────────────
|
// ─── Props ──────────────────────────────────────────────────────
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { publish } from '@repo/core-events';
|
import { publish } from '@repo/core-events';
|
||||||
import { WS_EVENTS } from '../../../../core/constants/events';
|
import { WS_EVENTS } from '../../../../../core/constants/events';
|
||||||
|
|
||||||
// ─── Stock Tickers ──────────────────────────────────────────────
|
// ─── Stock Tickers ──────────────────────────────────────────────
|
||||||
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { memo, useState, useRef } from 'react';
|
import { memo, useState, useRef } from 'react';
|
||||||
import { useAppEvent } from '@repo/core-events';
|
import { useAppEvent } from '@repo/core-events';
|
||||||
import { WS_EVENTS } from '../../../../core/constants/events';
|
import { WS_EVENTS } from '../../../../../core/constants/events';
|
||||||
|
|
||||||
// ─── Props ──────────────────────────────────────────────────────
|
// ─── Props ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { BaseRemoteDataServices } from '@repo/core-api/data-services';
|
import { BaseRemoteDataServices } from '@repo/core-api/data-services';
|
||||||
import type { ApiResponse } from '@repo/core-api/http-client';
|
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 { BookingEntity } from './booking.data-services';
|
||||||
import type { BookingDTO } from './booking.transformer';
|
import type { BookingDTO } from './booking.transformer';
|
||||||
import {
|
import {
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||||
import type { BaseEntity } 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 { BookingTransformer } from './booking.transformer';
|
||||||
import type { BookingDTO } from './booking.transformer';
|
import type { BookingDTO } from './booking.transformer';
|
||||||
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useTranslation, changeLanguage, applyTenantOverrides, i18n } from '@repo/core-i18n';
|
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
|
// Decentralized languages imports
|
||||||
import bookingId from '../languages/id/booking.json';
|
import bookingId from '../languages/id/booking.json';
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { secureStorage, secureIndexedDB, AppStorageKey } from '../../../../../../core/storage/local';
|
import { secureStorage, secureIndexedDB, AppStorageKey } from '../../../../../../../core/storage/local';
|
||||||
|
|
||||||
// ─── Demo Data ──────────────────────────────────────────────────
|
// ─── 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
|
// 1. Import hooks yang baru saja dibuat Opus
|
||||||
import { Button } from '@repo/ui/components';
|
import { Button } from '@repo/ui/components';
|
||||||
import { useElectronPrinter } from '../../core/hooks/use-electron-printer';
|
import { useElectronPrinter } from '../../../core/hooks/use-electron-printer';
|
||||||
import { useElectronUpdater } from '../../core/hooks/use-electron-updater';
|
import { useElectronUpdater } from '../../../core/hooks/use-electron-updater';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
// 2. Panggil hooks-nya
|
// 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
|
Layout Engine Interactive Demo
|
||||||
</Text>
|
</Text>
|
||||||
<Group>
|
<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
|
Back to Showcase
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { Button, Card, Group, Stack, Title, Text, Table, Badge } from '@repo/ui/components';
|
import { Button, Card, Group, Stack, Title, Text, Table, Badge } from '@repo/ui/components';
|
||||||
|
|
||||||
import { itemDB, posConfigDB, newItemDB } from '../../core/storage/pouch-db';
|
import { itemDB, posConfigDB, newItemDB } from '../../../core/storage/pouch-db';
|
||||||
import type { ItemEntity, POSConfigurationEntity } from '../../core/storage/pouch-db/entities';
|
import type { ItemEntity, POSConfigurationEntity } from '../../../core/storage/pouch-db/entities';
|
||||||
|
|
||||||
export default function PouchSample() {
|
export default function PouchSample() {
|
||||||
const [configs, setConfigs] = useState<POSConfigurationEntity[]>([]);
|
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 { 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 { NotFound, Forbidden, Maintenance, ComingSoon, AgGridProvider } from '@repo/ui/components';
|
||||||
import { LoadingScreen } from '../core/components/loading-screen';
|
import { LoadingScreen } from '../core/components/loading-screen';
|
||||||
import { useThemeStore } from '../core/stores/theme.store';
|
import { useThemeStore } from '../core/stores/theme.store';
|
||||||
@@ -8,12 +8,9 @@ import { initializeAndPurgeHistoryBackground } from './modules/layouts/hooks/use
|
|||||||
|
|
||||||
const AuthModule = lazy(() => import('./auth'));
|
const AuthModule = lazy(() => import('./auth'));
|
||||||
const AppModule = lazy(() => import('./modules'));
|
const AppModule = lazy(() => import('./modules'));
|
||||||
const ShowcaseView = lazy(() => import('./showcase/showcase-view'));
|
|
||||||
const ShellDemo = lazy(() => import('./showcase/shell-demo'));
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const colorScheme = useThemeStore((s) => s.colorScheme);
|
const colorScheme = useThemeStore((s) => s.colorScheme);
|
||||||
const [density, setDensity] = useState<DensityType>('compact');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Execution runs purely in the background (fire and forget)
|
// Execution runs purely in the background (fire and forget)
|
||||||
@@ -22,20 +19,18 @@ export default function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider colorScheme={colorScheme} density={density}>
|
<ThemeProvider colorScheme={colorScheme} density={'compact'}>
|
||||||
<AgGridProvider bypassLicense>
|
<AgGridProvider bypassLicense>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Suspense fallback={<LoadingScreen />}>
|
<Suspense fallback={<LoadingScreen />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/auth/*" element={<AuthModule />} />
|
<Route path="/auth/*" element={<AuthModule />} />
|
||||||
<Route path="/app/*" element={<AppModule />} />
|
<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="/404" element={<NotFound homeUrl="/app" />} />
|
||||||
<Route path="/403" element={<Forbidden homeUrl="/app" />} />
|
<Route path="/403" element={<Forbidden homeUrl="/app" />} />
|
||||||
<Route path="/maintenance" element={<Maintenance />} />
|
<Route path="/maintenance" element={<Maintenance />} />
|
||||||
<Route path="/coming-soon" element={<ComingSoon />} />
|
<Route path="/coming-soon" element={<ComingSoon />} />
|
||||||
<Route path="/" element={<Navigate to="/showcase" />} />
|
<Route path="/" element={<Navigate to="/app" />} />
|
||||||
<Route path="*" element={<Navigate to="/404" />} />
|
<Route path="*" element={<Navigate to="/404" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</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:landing": "turbo run build --filter=landing",
|
||||||
"build:docs-dev": "turbo run build --filter=docs-dev",
|
"build:docs-dev": "turbo run build --filter=docs-dev",
|
||||||
"build:desktop": "turbo run build --filter=desktop",
|
"build:desktop": "turbo run build --filter=desktop",
|
||||||
|
"build:showcase": "turbo run build --filter=showcase",
|
||||||
"dev": "turbo run dev",
|
"dev": "turbo run dev",
|
||||||
"dev:web": "turbo run dev --filter=web",
|
"dev:web": "turbo run dev --filter=web",
|
||||||
"dev:landing": "turbo run dev --filter=landing",
|
"dev:landing": "turbo run dev --filter=landing",
|
||||||
"dev:docs-dev": "turbo run dev --filter=docs-dev",
|
"dev:docs-dev": "turbo run dev --filter=docs-dev",
|
||||||
"dev:desktop": "turbo run dev --filter=web --filter=desktop --parallel",
|
"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:desktop": "pnpm run build:desktop && pnpm --filter desktop run package",
|
||||||
"package:mac": "pnpm run build:desktop && pnpm --filter desktop run package:mac",
|
"package:mac": "pnpm run build:desktop && pnpm --filter desktop run package:mac",
|
||||||
"package:win": "pnpm run build:desktop && pnpm --filter desktop run package:win",
|
"package:win": "pnpm run build:desktop && pnpm --filter desktop run package:win",
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
"typecheck:web": "turbo run typecheck --filter=web",
|
"typecheck:web": "turbo run typecheck --filter=web",
|
||||||
"typecheck:landing": "turbo run typecheck --filter=landing",
|
"typecheck:landing": "turbo run typecheck --filter=landing",
|
||||||
"typecheck:desktop": "turbo run typecheck --filter=desktop",
|
"typecheck:desktop": "turbo run typecheck --filter=desktop",
|
||||||
|
"typecheck:showcase": "turbo run typecheck --filter=showcase",
|
||||||
"check:all": "turbo run lint typecheck test",
|
"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",
|
"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"
|
"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 { ActionConfirmationModal, ACTION_TRANSLATION_MAP } from '../action-confirmation-modal';
|
||||||
import { BulkActionConfirmationModal } from '../bulk-action-confirmation';
|
import { BulkActionConfirmationModal } from '../bulk-action-confirmation';
|
||||||
import { TableFilterDrawer, TableFilterConfig } from './components/table-filter-drawer';
|
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';
|
import { EntityId } from '../../../../../../core-api/src/data-services/types';
|
||||||
|
|
||||||
export * from 'ag-grid-community';
|
export * from 'ag-grid-community';
|
||||||
|
|||||||
Generated
+98
-8
@@ -147,6 +147,97 @@ importers:
|
|||||||
specifier: ^5.1.4
|
specifier: ^5.1.4
|
||||||
version: 5.4.17(@types/node@22.19.3)
|
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:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
@@ -236,7 +327,7 @@ importers:
|
|||||||
version: 5.4.17(@types/node@22.19.3)
|
version: 5.4.17(@types/node@22.19.3)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.17
|
specifier: ^4.0.17
|
||||||
version: 4.0.17(@opentelemetry/api@1.9.1)
|
version: 4.0.17(jsdom@26.1.0)
|
||||||
|
|
||||||
packages/configs/eslint:
|
packages/configs/eslint:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -433,7 +524,7 @@ importers:
|
|||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.17
|
specifier: ^4.0.17
|
||||||
version: 4.0.17(@opentelemetry/api@1.9.1)
|
version: 4.0.17(jsdom@26.1.0)
|
||||||
|
|
||||||
packages/ui:
|
packages/ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -597,7 +688,7 @@ importers:
|
|||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.17
|
specifier: ^4.0.17
|
||||||
version: 4.0.17(@opentelemetry/api@1.9.1)
|
version: 4.0.17(jsdom@26.1.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -2103,7 +2194,7 @@ packages:
|
|||||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@gar/promisify': 1.1.3
|
'@gar/promisify': 1.1.3
|
||||||
semver: 7.7.1
|
semver: 7.7.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@npmcli/git@5.0.8:
|
/@npmcli/git@5.0.8:
|
||||||
@@ -9557,7 +9648,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==}
|
resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.1
|
semver: 7.7.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/npm-normalize-package-bin@3.0.1:
|
/npm-normalize-package-bin@3.0.1:
|
||||||
@@ -9571,7 +9662,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hosted-git-info: 7.0.2
|
hosted-git-info: 7.0.2
|
||||||
proc-log: 4.2.0
|
proc-log: 4.2.0
|
||||||
semver: 7.7.1
|
semver: 7.7.4
|
||||||
validate-npm-package-name: 5.0.1
|
validate-npm-package-name: 5.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -9582,7 +9673,7 @@ packages:
|
|||||||
npm-install-checks: 6.3.0
|
npm-install-checks: 6.3.0
|
||||||
npm-normalize-package-bin: 3.0.1
|
npm-normalize-package-bin: 3.0.1
|
||||||
npm-package-arg: 11.0.3
|
npm-package-arg: 11.0.3
|
||||||
semver: 7.7.1
|
semver: 7.7.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/npmlog@6.0.2:
|
/npmlog@6.0.2:
|
||||||
@@ -10962,7 +11053,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
|
||||||
|
|
||||||
/serialize-error@7.0.1:
|
/serialize-error@7.0.1:
|
||||||
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
|
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
|
||||||
|
|||||||
Reference in New Issue
Block a user