diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f759ae2..5d1677a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -18,7 +18,8 @@ // 4173: VitePress Docs (Preview) // 5173: Web App (Must be strictly 5173 for Electron IPC compatibility) // 3000: Landing App (Isolated from Vite's default 517x blast radius) - "forwardPorts": [6060, 4173, 5173, 3000], + // 3005: Showcase App + "forwardPorts": [6060, 4173, 5173, 3000, 3005], "portsAttributes": { "6060": { @@ -33,6 +34,10 @@ "label": "Web App (Electron Target)", "onAutoForward": "notify" }, + "3005": { + "label": "Showcase App", + "onAutoForward": "notify" + }, "3000": { "label": "Landing App (Public)", "onAutoForward": "notify" diff --git a/apps/docs-dev/src/apps/desktop/CONFIGURATION.md b/apps/docs-dev/src/apps/desktop/CONFIGURATION.md index ba63405..2bb648a 100644 --- a/apps/docs-dev/src/apps/desktop/CONFIGURATION.md +++ b/apps/docs-dev/src/apps/desktop/CONFIGURATION.md @@ -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 diff --git a/apps/docs-dev/src/packages/core-api/transformers.md b/apps/docs-dev/src/packages/core-api/transformers.md index 614eb39..9a1b1a7 100644 --- a/apps/docs-dev/src/packages/core-api/transformers.md +++ b/apps/docs-dev/src/packages/core-api/transformers.md @@ -317,7 +317,7 @@ A full working example is available in the showcase booking feature: | File | Description | | ---- | ----------- | -| `apps/web/.../booking/data/booking.transformer.ts` | Basic transformer with snake_case ↔ camelCase mapping | -| `apps/web/.../booking/data/booking.data-services.ts` | Data service with injected transformer | -| `apps/web/.../booking/data/advanced-booking.transformer.ts` | Extended transformer with custom chart method | -| `apps/web/.../booking/data/advanced-booking.data-services.ts` | Extended service with custom `getAvailabilityChart()` | +| `apps/showcase/.../booking/data/booking.transformer.ts` | Basic transformer with snake_case ↔ camelCase mapping | +| `apps/showcase/.../booking/data/booking.data-services.ts` | Data service with injected transformer | +| `apps/showcase/.../booking/data/advanced-booking.transformer.ts` | Extended transformer with custom chart method | +| `apps/showcase/.../booking/data/advanced-booking.data-services.ts` | Extended service with custom `getAvailabilityChart()` | diff --git a/apps/docs-dev/src/packages/ui/CORE-APP-SHELL.md b/apps/docs-dev/src/packages/ui/CORE-APP-SHELL.md index 98d34da..ca9c16b 100644 --- a/apps/docs-dev/src/packages/ui/CORE-APP-SHELL.md +++ b/apps/docs-dev/src/packages/ui/CORE-APP-SHELL.md @@ -418,7 +418,7 @@ function App() { ### Interactive Config Builder -The showcase demo at `apps/web/src/apps/showcase/shell-demo/` demonstrates a live, interactive config builder where every feature toggle and variant switch updates the layout in real-time. The key pattern is managing `config` state externally and passing it as a prop: +The showcase demo at `apps/showcase/src/pages/showcase-original/shell-demo/` demonstrates a live, interactive config builder where every feature toggle and variant switch updates the layout in real-time. The key pattern is managing `config` state externally and passing it as a prop: ```tsx import { useState, useMemo } from 'react'; diff --git a/apps/showcase/.eslintrc.cjs b/apps/showcase/.eslintrc.cjs new file mode 100644 index 0000000..a779c57 --- /dev/null +++ b/apps/showcase/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ['@repo/eslint-config/react.js'], +}; diff --git a/apps/showcase/index.html b/apps/showcase/index.html new file mode 100644 index 0000000..e09f575 --- /dev/null +++ b/apps/showcase/index.html @@ -0,0 +1,13 @@ + + + + + + + Showcase + + +
+ + + diff --git a/apps/showcase/package.json b/apps/showcase/package.json new file mode 100644 index 0000000..def6253 --- /dev/null +++ b/apps/showcase/package.json @@ -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" + } +} diff --git a/apps/showcase/src/App.tsx b/apps/showcase/src/App.tsx new file mode 100644 index 0000000..e145187 --- /dev/null +++ b/apps/showcase/src/App.tsx @@ -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('compact'); + + return ( + + + + Loading...}> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + + + + + ); +} diff --git a/apps/showcase/src/core/constants/events.ts b/apps/showcase/src/core/constants/events.ts new file mode 100644 index 0000000..0cdd2f4 --- /dev/null +++ b/apps/showcase/src/core/constants/events.ts @@ -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; diff --git a/apps/showcase/src/core/hooks/use-electron-printer.ts b/apps/showcase/src/core/hooks/use-electron-printer.ts new file mode 100644 index 0000000..bf732b4 --- /dev/null +++ b/apps/showcase/src/core/hooks/use-electron-printer.ts @@ -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; + /** Print with the given options. Returns success/failure. */ + print: (options?: ElectronPrintOptions) => Promise; +} + +/** + * 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 ( + * + * ); + * } + * ``` + */ +export function useElectronPrinter(): UseElectronPrinterReturn { + const [printers, setPrinters] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const isElectron = useIsElectron(); + + const refreshPrinters = useCallback(async (): Promise => { + 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 => { + 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, + }; +} diff --git a/apps/showcase/src/core/hooks/use-electron-updater.ts b/apps/showcase/src/core/hooks/use-electron-updater.ts new file mode 100644 index 0000000..1fa8cf5 --- /dev/null +++ b/apps/showcase/src/core/hooks/use-electron-updater.ts @@ -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
Update {updateInfo?.version} available! Downloading...
; + * } + * if (status === 'downloading') { + * return
Downloading... {progress.toFixed(0)}%
; + * } + * if (status === 'ready') { + * return ; + * } + * return ; + * } + * ``` + */ +export function useElectronUpdater(): UseElectronUpdaterReturn { + const [status, setStatus] = useState('idle'); + const [progress, setProgress] = useState(0); + const [bytesPerSecond, setBytesPerSecond] = useState(0); + const [updateInfo, setUpdateInfo] = useState(null); + const [errorMessage, setErrorMessage] = useState(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, + }; +} diff --git a/apps/showcase/src/core/hooks/use-is-electron.ts b/apps/showcase/src/core/hooks/use-is-electron.ts new file mode 100644 index 0000000..7f8bbb9 --- /dev/null +++ b/apps/showcase/src/core/hooks/use-is-electron.ts @@ -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; +} diff --git a/apps/showcase/src/core/lib/api-client.ts b/apps/showcase/src/core/lib/api-client.ts new file mode 100644 index 0000000..0fea5e4 --- /dev/null +++ b/apps/showcase/src/core/lib/api-client.ts @@ -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(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; + }, + }, +); diff --git a/apps/showcase/src/core/storage/local/index.ts b/apps/showcase/src/core/storage/local/index.ts new file mode 100644 index 0000000..6f38f55 --- /dev/null +++ b/apps/showcase/src/core/storage/local/index.ts @@ -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([ + AppStorageKey.USER_PROFILE, + AppStorageKey.ACCESS_TOKEN, + AppStorageKey.REFRESH_TOKEN, +]); + +export const PLAIN_KEYS = new Set([ + 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({ + encryptedKeys: ENCRYPTED_KEYS, + plainTextKeys: PLAIN_KEYS, +}); + +export const secureIndexedDB = createIndexedDB({ + dbName: 'e_apps_db', + storeName: 'web_store', + encryptedKeys: ENCRYPTED_KEYS, + plainTextKeys: PLAIN_KEYS, +}); diff --git a/apps/showcase/src/core/storage/pouch-db/entities/index.ts b/apps/showcase/src/core/storage/pouch-db/entities/index.ts new file mode 100644 index 0000000..686679f --- /dev/null +++ b/apps/showcase/src/core/storage/pouch-db/entities/index.ts @@ -0,0 +1,3 @@ +export * from './item.pouchdb.entity'; +export * from './pos-configuration.pouchdb.entity'; +export * from './new-item.pouchdb.entity'; diff --git a/apps/showcase/src/core/storage/pouch-db/entities/item.pouchdb.entity.ts b/apps/showcase/src/core/storage/pouch-db/entities/item.pouchdb.entity.ts new file mode 100644 index 0000000..feefa14 --- /dev/null +++ b/apps/showcase/src/core/storage/pouch-db/entities/item.pouchdb.entity.ts @@ -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; +} diff --git a/apps/showcase/src/core/storage/pouch-db/entities/new-item.pouchdb.entity.ts b/apps/showcase/src/core/storage/pouch-db/entities/new-item.pouchdb.entity.ts new file mode 100644 index 0000000..c03b4a4 --- /dev/null +++ b/apps/showcase/src/core/storage/pouch-db/entities/new-item.pouchdb.entity.ts @@ -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; +} diff --git a/apps/showcase/src/core/storage/pouch-db/entities/pos-configuration.pouchdb.entity.ts b/apps/showcase/src/core/storage/pouch-db/entities/pos-configuration.pouchdb.entity.ts new file mode 100644 index 0000000..3bed0c3 --- /dev/null +++ b/apps/showcase/src/core/storage/pouch-db/entities/pos-configuration.pouchdb.entity.ts @@ -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; +} diff --git a/apps/showcase/src/core/storage/pouch-db/index.ts b/apps/showcase/src/core/storage/pouch-db/index.ts new file mode 100644 index 0000000..8e3ff4b --- /dev/null +++ b/apps/showcase/src/core/storage/pouch-db/index.ts @@ -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({ + localName: 'pos_configuration', + remoteUrl: buildRemoteUrl('pos_configuration'), +}); + +/** Items database — products available for sale in POS. */ +export const itemDB = dbManager.register({ + 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', +); diff --git a/apps/showcase/src/core/stores/theme.store.ts b/apps/showcase/src/core/stores/theme.store.ts new file mode 100644 index 0000000..d780321 --- /dev/null +++ b/apps/showcase/src/core/stores/theme.store.ts @@ -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()( + persist( + (set) => ({ + colorScheme: 'light', + setColorScheme: (scheme) => set({ colorScheme: scheme }), + }), + { + name: AppStorageKey.THEME, // matches AppStorageKey.THEME + storage: createJSONStorage(() => localStorage), + }, + ), +); diff --git a/apps/showcase/src/layouts/ShowcaseLayout.tsx b/apps/showcase/src/layouts/ShowcaseLayout.tsx new file mode 100644 index 0000000..6a96b27 --- /dev/null +++ b/apps/showcase/src/layouts/ShowcaseLayout.tsx @@ -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 ( + + + + Eigen ERP + Architecture Showcase + + + {navItems.map((item) => ( + } + onClick={() => navigate(item.path)} + variant="filled" + style={{ borderRadius: 'var(--mantine-radius-md)', marginBottom: 4 }} + /> + ))} + + + + setDensity((val as DensityType) || 'standard')} + data={[ + { value: 'compact', label: 'Compact' }, + { value: 'standard', label: 'Standard' }, + ]} + /> + + + + + + + + Architecture Showcase + {getSubtitle()} + + setColorScheme((val as ColorSchemeType) || 'light')} + data={[ + { value: 'light', label: 'Light' }, + { value: 'dark', label: 'Dark' }, + ]} + /> + + + + {/* Typography & Buttons */} + + +
+ Typography & Badges + This is dimmed small text indicating a subtitle. + + Brand Badge + Success Status + Error State + +
+ +
+ Enterprise Status Badges + Pre-configured status badges for transaction and master data. + + + + + + + + + + +
+ +
+ Buttons + + + + + + +
+
+
+ + {/* Forms */} + + Form Elements + + + + + + + + + +