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:
2026-07-23 10:31:36 +00:00
72 changed files with 1353 additions and 499 deletions
+6 -1
View File
@@ -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';
+5
View File
@@ -0,0 +1,5 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ['@repo/eslint-config/react.js'],
};
+13
View File
@@ -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>
+48
View File
@@ -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"
}
}
+49
View File
@@ -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 (0100) */
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;
}
+43
View File
@@ -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>
);
}
+3
View File
@@ -0,0 +1,3 @@
@import 'tailwindcss';
@import '@repo/ui/theme.css';
@source "../../../packages/ui/src";
+17
View File
@@ -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();
+14
View File
@@ -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,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 ──────────────────────────────────────────
@@ -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 ──────────────────────────────────────────────────────
@@ -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 ───────────────────────────────────────────
@@ -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,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,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,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,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,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,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,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 ──────────────────────────────────────────────────
+20
View File
@@ -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>
);
}
+12
View File
@@ -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>
);
}
@@ -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>
);
}
+14
View File
@@ -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>
);
}
@@ -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>
@@ -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[]>([]);
+12
View File
@@ -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
View File
@@ -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;
}
+80
View File
@@ -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;
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "@repo/typescript-config/react-app.json",
"include": ["src"],
"compilerOptions": {
"jsx": "react-jsx"
}
}
+19
View File
@@ -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',
},
},
});
+4 -9
View File
@@ -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>
-20
View File
@@ -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>
);
}
+3
View File
@@ -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';
+98 -8
View File
@@ -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==}