alpe-temp 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/bin/epms.js +149 -60
  2. package/frontend-project/src/App.css +1 -0
  3. package/frontend-project/src/Auth/Login.jsx +85 -0
  4. package/frontend-project/src/Auth/Register.jsx +183 -0
  5. package/frontend-project/src/Intro.jsx +33 -0
  6. package/frontend-project/src/LayOut.jsx +38 -0
  7. package/frontend-project/src/api/ApiClient.js +92 -0
  8. package/frontend-project/src/assets/hero.png +0 -0
  9. package/frontend-project/src/assets/react.svg +1 -0
  10. package/frontend-project/src/assets/vite.svg +1 -0
  11. package/frontend-project/src/components/Aside.jsx +9 -0
  12. package/frontend-project/src/components/Button.jsx +100 -0
  13. package/frontend-project/src/components/Card.jsx +104 -0
  14. package/frontend-project/src/components/FormField.jsx +129 -0
  15. package/frontend-project/src/components/Modal.jsx +106 -0
  16. package/frontend-project/src/components/Table.jsx +127 -0
  17. package/frontend-project/src/components/Toast.jsx +64 -0
  18. package/frontend-project/src/components/index.js +14 -0
  19. package/frontend-project/src/config.js +66 -0
  20. package/frontend-project/src/design.js +115 -0
  21. package/frontend-project/src/index.css +60 -0
  22. package/frontend-project/src/layouts/BottomNav.jsx +156 -0
  23. package/frontend-project/src/layouts/TopNav.jsx +150 -0
  24. package/frontend-project/src/layouts/useShell.js +44 -0
  25. package/frontend-project/src/main.jsx +41 -0
  26. package/frontend-project/src/pages/Department.jsx +188 -0
  27. package/frontend-project/src/pages/Employee.jsx +274 -0
  28. package/frontend-project/src/pages/Home.jsx +79 -0
  29. package/frontend-project/src/pages/Profile.jsx +9 -0
  30. package/frontend-project/src/pages/Register.jsx +57 -0
  31. package/frontend-project/src/pages/Reports.jsx +91 -0
  32. package/frontend-project/src/pages/Salary.jsx +264 -0
  33. package/frontend-project/src/themes.js +175 -0
  34. package/package.json +1 -1
@@ -0,0 +1,127 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // 📊 TABLE — Reusable data table with loading & empty states
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+ //
5
+ // HOW TO USE:
6
+ // <Table
7
+ // columns={[
8
+ // { key: 'name', label: 'Name' },
9
+ // { key: 'email', label: 'Email', align: 'center' },
10
+ // { key: 'status', label: 'Status', render: (val) => <Badge>{val}</Badge> },
11
+ // ]}
12
+ // data={users}
13
+ // rowKey="id"
14
+ // />
15
+ //
16
+ // PROPS:
17
+ // columns: array of { key, label, align?, render? }
18
+ // data: array of row objects
19
+ // rowKey: unique key per row (default: 'id')
20
+ // loading: show loading skeleton
21
+ // emptyMessage: text when no data
22
+ // className: additional CSS classes
23
+ // maxHeight: max scrollable height (default: '360px')
24
+ //
25
+ // CONFIG INTEGRATION:
26
+ // - Rounded corners from config.js
27
+ // - Colors from CSS variables
28
+ //
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+
31
+ import React from 'react';
32
+ import { config } from '../config';
33
+ import { getRoundedClass } from '../design';
34
+
35
+ export default function Table({
36
+ columns,
37
+ data = [],
38
+ rowKey = 'id',
39
+ loading = false,
40
+ emptyMessage = 'No data found',
41
+ className = '',
42
+ maxHeight = '360px',
43
+ }) {
44
+ const ALIGN = { left: 'text-left', center: 'text-center', right: 'text-right' };
45
+ const roundClass = getRoundedClass(config.rounded);
46
+
47
+ return (
48
+ <div
49
+ className={`w-full overflow-auto border ${roundClass} ${className}`}
50
+ style={{ maxHeight, borderColor: 'var(--color-border)' }}
51
+ >
52
+ <table className="w-full min-w-max">
53
+ {/* ─── HEADER ──────────────────────────────────────────────────── */}
54
+ <thead className="sticky top-0 z-10">
55
+ <tr
56
+ className="border-b"
57
+ style={{
58
+ backgroundColor: 'var(--color-surface)',
59
+ borderColor: 'var(--color-border)',
60
+ }}
61
+ >
62
+ {columns.map((col) => (
63
+ <th
64
+ key={col.key}
65
+ className={`px-4 py-3 text-[11px] font-semibold uppercase tracking-wider whitespace-nowrap ${ALIGN[col.align ?? 'left']}`}
66
+ style={{ color: 'var(--color-text-muted)' }}
67
+ >
68
+ {col.label}
69
+ </th>
70
+ ))}
71
+ </tr>
72
+ </thead>
73
+
74
+ {/* ─── BODY ────────────────────────────────────────────────────── */}
75
+ <tbody className="divide-y" style={{ backgroundColor: 'var(--color-card)', borderColor: 'var(--color-border)' }}>
76
+ {/* Loading skeleton */}
77
+ {loading ? (
78
+ Array.from({ length: 5 }).map((_, i) => (
79
+ <tr key={i}>
80
+ {columns.map((col) => (
81
+ <td key={col.key} className="px-4 py-3">
82
+ <div
83
+ className="h-3 animate-pulse"
84
+ style={{
85
+ width: `${60 + (i * 13 + col.key.length * 7) % 30}%`,
86
+ backgroundColor: 'var(--color-border)',
87
+ }}
88
+ />
89
+ </td>
90
+ ))}
91
+ </tr>
92
+ ))
93
+ ) : data.length === 0 ? (
94
+ // Empty state
95
+ <tr>
96
+ <td
97
+ colSpan={columns.length}
98
+ className="px-4 py-10 text-center text-[13px]"
99
+ style={{ color: 'var(--color-text-muted)' }}
100
+ >
101
+ {emptyMessage}
102
+ </td>
103
+ </tr>
104
+ ) : (
105
+ // Data rows
106
+ data.map((row, rowIdx) => (
107
+ <tr
108
+ key={row[rowKey] ?? rowIdx}
109
+ className="transition-colors duration-100 hover:bg-gray-50"
110
+ >
111
+ {columns.map((col) => (
112
+ <td
113
+ key={col.key}
114
+ className={`px-4 py-3 text-[12px] font-medium whitespace-nowrap ${ALIGN[col.align ?? 'left']}`}
115
+ style={{ color: 'var(--color-text)' }}
116
+ >
117
+ {col.render ? col.render(row[col.key], row) : (row[col.key] ?? '—')}
118
+ </td>
119
+ ))}
120
+ </tr>
121
+ ))
122
+ )}
123
+ </tbody>
124
+ </table>
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,64 @@
1
+ import React, { createContext, useCallback, useContext, useState } from 'react';
2
+ import { CheckCircle, XCircle, Info, X } from 'lucide-react';
3
+
4
+ const ToastContext = createContext(null);
5
+
6
+ const ICONS = {
7
+ success: <CheckCircle className="w-4 h-4 text-emerald-500 flex-shrink-0" />,
8
+ error: <XCircle className="w-4 h-4 text-red-500 flex-shrink-0" />,
9
+ info: <Info className="w-4 h-4 text-blue-500 flex-shrink-0" />,
10
+ };
11
+
12
+ const BG = {
13
+ success: 'bg-white border-emerald-200',
14
+ error: 'bg-white border-red-200',
15
+ info: 'bg-white border-blue-200',
16
+ };
17
+
18
+ let _id = 0;
19
+
20
+ export function ToastProvider({ children }) {
21
+ const [toasts, setToasts] = useState([]);
22
+
23
+ const add = useCallback((message, type = 'info', duration = 3500) => {
24
+ const id = ++_id;
25
+ setToasts((prev) => [...prev, { id, message, type }]);
26
+ setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), duration);
27
+ return id;
28
+ }, []);
29
+
30
+ const remove = useCallback((id) => setToasts((prev) => prev.filter((t) => t.id !== id)), []);
31
+
32
+ const api = {
33
+ success: (msg, dur) => add(msg, 'success', dur),
34
+ error: (msg, dur) => add(msg, 'error', dur),
35
+ info: (msg, dur) => add(msg, 'info', dur),
36
+ remove,
37
+ };
38
+
39
+ return (
40
+ <ToastContext.Provider value={api}>
41
+ {children}
42
+ <div className="fixed top-5 right-5 z-[9999] flex flex-col gap-2 pointer-events-none">
43
+ {toasts.map((t) => (
44
+ <div
45
+ key={t.id}
46
+ className={`pointer-events-auto flex items-center gap-3 px-4 py-3 border shadow-lg text-[13px] font-medium text-gray-700 min-w-[240px] max-w-xs ${BG[t.type]}`}
47
+ >
48
+ {ICONS[t.type]}
49
+ <span className="flex-1">{t.message}</span>
50
+ <button onClick={() => remove(t.id)} className="text-gray-300 hover:text-gray-500 transition-colors">
51
+ <X className="w-3.5 h-3.5" />
52
+ </button>
53
+ </div>
54
+ ))}
55
+ </div>
56
+ </ToastContext.Provider>
57
+ );
58
+ }
59
+
60
+ export function useToast() {
61
+ const ctx = useContext(ToastContext);
62
+ if (!ctx) throw new Error('useToast must be used inside <ToastProvider>');
63
+ return ctx;
64
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * src/components/ui/index.js
3
+ * Barrel export for all reusable UI primitives.
4
+ *
5
+ * Usage:
6
+ * import { Button, Table, Modal, FormField, Card, StatCard, ToastProvider, useToast } from '../components/ui';
7
+ */
8
+
9
+ export { default as Button } from './Button';
10
+ export { default as Table } from './Table';
11
+ export { default as Modal } from './Modal';
12
+ export { default as FormField } from './FormField';
13
+ export { Card, StatCard } from './Card';
14
+ export { ToastProvider, useToast } from './Toast';
@@ -0,0 +1,66 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // 🎨 APP CONFIGURATION — Change ONE value to transform the whole app!
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+ //
5
+ // 📖 BEGINNER GUIDE:
6
+ //
7
+ // This is the ONLY file you need to touch to change the entire look
8
+ // and behavior of the app. Every page, component, and layout reads
9
+ // from this file.
10
+ //
11
+ // 🚀 QUICK EXAMPLES:
12
+ //
13
+ // 1. navigation: 'bottomnav' → navbar moves to the bottom (mobile style)
14
+ // 2. theme: 'midnight' → dark mode, instantly
15
+ // 3. rounded: 'none' → all buttons/cards become square
16
+ // 4. fontSize: 'large' → bigger text everywhere
17
+ // 5. colors.primary: '#FF0000' → brand color changes to red
18
+ //
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+
21
+ export const config = {
22
+ // ─── NAVIGATION ───────────────────────────────────────────────────────
23
+ // 'topnav' → horizontal bar at the TOP of the screen
24
+ // 'bottomnav' → bar at the BOTTOM (great for mobile / app-like feel)
25
+ navigation: 'bottomnav',
26
+
27
+ // ─── THEME ────────────────────────────────────────────────────────────
28
+ // Pick any theme name from themes.js. Available themes:
29
+ // 'forest' (green - default)
30
+ // 'midnight' (dark)
31
+ // 'slate' (blue-gray)
32
+ // 'rose' (pink/crimson)
33
+ // 'amber' (orange/gold)
34
+ // 'ocean' (navy/cyan)
35
+ // 'chalk' (black & white)
36
+ // 'violet' (purple)
37
+ theme: 'forest',
38
+
39
+ // ─── BORDER RADIUS ────────────────────────────────────────────────────
40
+ // Controls how rounded buttons, cards, inputs, and modals are.
41
+ // 'none' → completely square (sharp, modern)
42
+ // 'sm' → slightly rounded
43
+ // 'md' → moderately rounded (default feel)
44
+ // 'lg' → very rounded (pill-like)
45
+ // 'xl' → extra rounded
46
+ // 'full' → fully pill-shaped
47
+ rounded: 'md',
48
+
49
+ // ─── FONT SIZE ────────────────────────────────────────────────────────
50
+ // 'normal' → default text size
51
+ // 'large' → bigger text (easier to read)
52
+ fontSize: 'normal',
53
+
54
+ // ─── COLORS ───────────────────────────────────────────────────────────
55
+ // Leave empty ('') to use the theme's default colors.
56
+ // Set a value to override that specific color.
57
+ // Format: any valid CSS color (#hex, rgb, name)
58
+ colors: {
59
+ primary: '', // main brand color (buttons, links, active nav)
60
+ // secondary: '', // secondary accent
61
+ // success: '', // success states (green)
62
+ // danger: '', // error/danger states (red)
63
+ // warning: '', // warning states (yellow/amber)
64
+ // info: '', // info states (blue)
65
+ },
66
+ };
@@ -0,0 +1,115 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // 📐 DESIGN TOKENS — Turns config.js into CSS variables and utilities
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+ //
5
+ // WHAT THIS FILE DOES:
6
+ // Takes your config.js settings and turns them into:
7
+ // 1. CSS custom properties (variables) for global styling
8
+ // 2. Tailwind-compatible class names for components
9
+ //
10
+ // HOW IT WORKS:
11
+ // The Layout component calls `getDesignTokens(config)` to get an object
12
+ // with CSS variable values and utility class names. These are applied
13
+ // to the root div so every child component can use them.
14
+ //
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+
17
+ import { getTheme } from './themes';
18
+
19
+ // ─── RADIUS VALUES ───────────────────────────────────────────────────────────
20
+ const RADIUS = {
21
+ none: '0px',
22
+ sm: '0.125rem', // 2px
23
+ md: '0.375rem', // 6px
24
+ lg: '0.5rem', // 8px
25
+ xl: '0.75rem', // 12px
26
+ full: '9999px',
27
+ };
28
+
29
+ // ─── FONT SIZE SCALES ────────────────────────────────────────────────────────
30
+ const FONT_SCALE = {
31
+ normal: {
32
+ xs: '0.75rem', // 12px
33
+ sm: '0.8125rem', // 13px
34
+ base:'0.875rem', // 14px
35
+ lg: '1rem', // 16px
36
+ xl: '1.25rem', // 20px
37
+ '2xl':'1.5rem', // 24px
38
+ '3xl':'2rem', // 32px
39
+ },
40
+ large: {
41
+ xs: '0.8125rem', // 13px
42
+ sm: '0.875rem', // 14px
43
+ base:'1rem', // 16px
44
+ lg: '1.125rem', // 18px
45
+ xl: '1.375rem', // 22px
46
+ '2xl':'1.75rem', // 28px
47
+ '3xl':'2.25rem', // 36px
48
+ },
49
+ };
50
+
51
+ // ─── ROUNDED CLASS HELPER ────────────────────────────────────────────────────
52
+ // Use this in components to get the Tailwind rounded class for a given level.
53
+ // Example: getRoundedClass('md') → 'rounded-md'
54
+ //
55
+ export function getRoundedClass(level = 'md') {
56
+ const map = {
57
+ none: 'rounded-none',
58
+ sm: 'rounded-sm',
59
+ md: 'rounded-md',
60
+ lg: 'rounded-lg',
61
+ xl: 'rounded-xl',
62
+ full: 'rounded-full',
63
+ };
64
+ return map[level] || map.md;
65
+ }
66
+
67
+ // ─── GENERATE DESIGN TOKENS ──────────────────────────────────────────────────
68
+ // This is called once by the Layout component to produce:
69
+ // {
70
+ // cssVars: { '--color-primary': '#008A75', ... },
71
+ // roundedClass: 'rounded-md',
72
+ // theme: { ... resolved theme colors },
73
+ // }
74
+ //
75
+ export function getDesignTokens(config) {
76
+ const theme = getTheme(config.theme, config.colors);
77
+
78
+ // Merge user color overrides
79
+ const colors = { ...theme };
80
+ if (config.colors) {
81
+ Object.entries(config.colors).forEach(([key, val]) => {
82
+ if (val) colors[key] = val;
83
+ });
84
+ }
85
+
86
+ // Build CSS variable map
87
+ const cssVars = {
88
+ '--color-primary': colors.primary,
89
+ '--color-surface': colors.surface,
90
+ '--color-card': colors.card,
91
+ '--color-border': colors.border,
92
+ '--color-text': colors.text,
93
+ '--color-text-muted': colors.textMuted,
94
+ '--color-nav-bg': colors.navBg,
95
+ '--color-nav-text': colors.navText,
96
+ '--color-nav-active': colors.navActive,
97
+ '--color-danger': colors.danger,
98
+ '--color-success': colors.success,
99
+ '--color-warning': colors.warning,
100
+ '--color-info': colors.info,
101
+ '--radius': RADIUS[config.rounded] || RADIUS.md,
102
+ };
103
+
104
+ // Add font size variables
105
+ const fontSizes = FONT_SCALE[config.fontSize] || FONT_SCALE.normal;
106
+ Object.entries(fontSizes).forEach(([key, val]) => {
107
+ cssVars[`--text-${key}`] = val;
108
+ });
109
+
110
+ return {
111
+ cssVars,
112
+ roundedClass: getRoundedClass(config.rounded),
113
+ theme: colors,
114
+ };
115
+ }
@@ -0,0 +1,60 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════════
2
+ 🎨 GLOBAL STYLES — Tailwind + CSS custom properties
3
+ ═══════════════════════════════════════════════════════════════════════════
4
+
5
+ CSS VARIABLES (--color-*, --radius, --text-*):
6
+ These are set dynamically by design.js based on your config.js.
7
+ They are applied to the :root element by the Layout component.
8
+
9
+ HOW TO USE IN COMPONENTS:
10
+ style={{ color: 'var(--color-primary)' }}
11
+ or Tailwind: className="bg-[var(--color-primary)]"
12
+
13
+ ═══════════════════════════════════════════════════════════════════════════ */
14
+
15
+ @tailwind base;
16
+ @tailwind components;
17
+ @tailwind utilities;
18
+
19
+ /* ─── Custom CSS Variables (applied by Layout component) ───────────────── */
20
+ :root {
21
+ --color-primary: #008A75;
22
+ --color-surface: #F9FAFB;
23
+ --color-card: #FFFFFF;
24
+ --color-border: #E5E7EB;
25
+ --color-text: #1F2937;
26
+ --color-text-muted: #9CA3AF;
27
+ --color-nav-bg: #FFFFFF;
28
+ --color-nav-text: #4B5563;
29
+ --color-nav-active: #008A75;
30
+ --color-danger: #EF4444;
31
+ --color-success: #10B981;
32
+ --color-warning: #F59E0B;
33
+ --color-info: #3B82F6;
34
+ --radius: 0.375rem;
35
+ --text-xs: 0.75rem;
36
+ --text-sm: 0.8125rem;
37
+ --text-base: 0.875rem;
38
+ --text-lg: 1rem;
39
+ --text-xl: 1.25rem;
40
+ --text-2xl: 1.5rem;
41
+ --text-3xl: 2rem;
42
+ }
43
+
44
+ /* ─── Tailwind-like utilities using CSS variables ──────────────────────── */
45
+ @layer utilities {
46
+ .bg-primary { background-color: var(--color-primary); }
47
+ .text-primary { color: var(--color-primary); }
48
+ .border-primary { border-color: var(--color-primary); }
49
+ .bg-surface { background-color: var(--color-surface); }
50
+ .bg-card { background-color: var(--color-card); }
51
+ .text-muted { color: var(--color-text-muted); }
52
+ }
53
+
54
+ /* ─── Animations ───────────────────────────────────────────────────────── */
55
+ @keyframes spin {
56
+ to { transform: rotate(360deg); }
57
+ }
58
+ .animate-spin {
59
+ animation: spin 1s linear infinite;
60
+ }
@@ -0,0 +1,156 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // 📱 BOTTOM NAV LAYOUT — Navbar at the bottom (mobile-first style)
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+ //
5
+ // WHAT THIS FILE DOES:
6
+ // Renders the app shell with a bottom navigation bar.
7
+ // Switch to this by setting navigation: 'bottomnav' in config.js
8
+ //
9
+ // HOW IT LOOKS:
10
+ // ┌────────────────────────────────┐
11
+ // │ │
12
+ // │ PAGE CONTENT │ ← scrollable
13
+ // │ │
14
+ // ├────────────────────────────────┤
15
+ // │ 📊 👥 💰 🏢 📄 │ ← bottom nav (fixed)
16
+ // └────────────────────────────────┘
17
+ //
18
+ // ═══════════════════════════════════════════════════════════════════════════
19
+
20
+ import React, { useState } from 'react';
21
+ import { NavLink, Outlet } from 'react-router-dom';
22
+ import {
23
+ LayoutDashboard, Users, DollarSign, Building2,
24
+ FileText, User, LogOut, ChevronDown,
25
+ } from 'lucide-react';
26
+ import { useShell } from './useShell';
27
+
28
+ // ─── NAVIGATION ITEMS ────────────────────────────────────────────────────────
29
+ // Add new pages here. The icon + label + path = one nav item.
30
+ //
31
+ const NAV_ITEMS = [
32
+ { label: 'Home', path: '/dashboard/overview', Icon: LayoutDashboard },
33
+ { label: 'Employees',path: '/dashboard/employees', Icon: Users },
34
+ { label: 'Salary', path: '/dashboard/salary', Icon: DollarSign },
35
+ { label: 'Depts', path: '/dashboard/departments', Icon: Building2 },
36
+ { label: 'Reports', path: '/dashboard/reports', Icon: FileText },
37
+ ];
38
+
39
+ export default function BottomNav() {
40
+ const { user, handleLogout } = useShell();
41
+ const [menuOpen, setMenuOpen] = useState(false);
42
+
43
+ return (
44
+ <div className="flex flex-col h-screen overflow-hidden">
45
+ {/* ─── TOP BAR (user menu, logo) ─────────────────────────────────── */}
46
+ <header
47
+ className="flex-shrink-0 flex items-center h-[52px] px-4 gap-1 z-20"
48
+ style={{
49
+ backgroundColor: 'var(--color-nav-bg)',
50
+ color: 'var(--color-nav-text)',
51
+ borderBottom: '1px solid var(--color-border)',
52
+ }}
53
+ >
54
+ <div className="flex items-center gap-2 mr-4">
55
+ <img src="/logo.png" alt="logo" className="w-7 h-9" />
56
+ <div className="flex flex-col mt-4">
57
+ <span className="text-[15px] font-bold tracking-tight leading-tight">
58
+ EP<span style={{ color: 'var(--color-primary)' }}>MS</span>
59
+ </span>
60
+ <span className="text-[7px] font-medium uppercase tracking-wider leading-tight opacity-60">
61
+ Employee Payroll
62
+ </span>
63
+ </div>
64
+ </div>
65
+
66
+ {/* Spacer */}
67
+ <div className="flex-1" />
68
+
69
+ {/* User dropdown */}
70
+ <div className="relative">
71
+ <button
72
+ onClick={() => setMenuOpen((v) => !v)}
73
+ className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-medium transition-colors hover:opacity-80"
74
+ >
75
+ <div
76
+ className="w-6 h-6 flex items-center justify-center"
77
+ style={{ backgroundColor: 'var(--color-primary)', color: '#fff' }}
78
+ >
79
+ <User className="w-3 h-3" />
80
+ </div>
81
+ <span className="hidden sm:inline">{user?.name ?? 'Admin'}</span>
82
+ <ChevronDown className={`w-3 h-3 transition-transform ${menuOpen ? 'rotate-180' : ''}`} />
83
+ </button>
84
+ {menuOpen && (
85
+ <>
86
+ <div className="fixed inset-0 z-10" onClick={() => setMenuOpen(false)} />
87
+ <div
88
+ className="absolute right-0 top-full mt-1 z-20 shadow-lg py-1 min-w-[150px]"
89
+ style={{ backgroundColor: 'var(--color-card)', border: '1px solid var(--color-border)' }}
90
+ >
91
+ <div className="px-3 py-2 border-b" style={{ borderColor: 'var(--color-border)' }}>
92
+ <p className="text-[12px] font-semibold" style={{ color: 'var(--color-text)' }}>
93
+ {user?.name ?? 'Admin'}
94
+ </p>
95
+ <p className="text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
96
+ {user?.email ?? ''}
97
+ </p>
98
+ </div>
99
+ <button
100
+ onClick={() => { setMenuOpen(false); handleLogout(); }}
101
+ className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-medium transition-colors"
102
+ style={{ color: 'var(--color-danger)' }}
103
+ onMouseEnter={(e) => e.target.style.backgroundColor = 'var(--color-danger)'}
104
+ onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
105
+ >
106
+ <LogOut className="w-3.5 h-3.5" />
107
+ Logout
108
+ </button>
109
+ </div>
110
+ </>
111
+ )}
112
+ </div>
113
+ </header>
114
+
115
+ {/* ─── MAIN CONTENT ──────────────────────────────────────────────── */}
116
+ <main
117
+ className="flex-1 overflow-y-auto"
118
+ style={{
119
+ backgroundColor: 'var(--color-surface)',
120
+ padding: 'var(--text-base)',
121
+ }}
122
+ >
123
+ <div className="max-w-6xl mx-auto">
124
+ <Outlet />
125
+ </div>
126
+ </main>
127
+
128
+ {/* ─── BOTTOM NAV ──────────────────────────────────────────────────── */}
129
+ <nav
130
+ className="flex-shrink-0 flex items-center justify-around h-[64px] px-2 z-20"
131
+ style={{
132
+ backgroundColor: 'var(--color-nav-bg)',
133
+ borderTop: '1px solid var(--color-border)',
134
+ }}
135
+ >
136
+ {NAV_ITEMS.map(({ label, path, Icon }) => (
137
+ <NavLink
138
+ key={path}
139
+ to={path}
140
+ className="flex flex-col items-center gap-0.5 px-2 py-1 text-[10px] font-medium transition-colors"
141
+ style={({ isActive }) => ({
142
+ color: isActive ? 'var(--color-nav-active)' : 'var(--color-nav-text)',
143
+ })}
144
+ >
145
+ {({ isActive }) => (
146
+ <>
147
+ <Icon className="w-5 h-5" />
148
+ <span>{label}</span>
149
+ </>
150
+ )}
151
+ </NavLink>
152
+ ))}
153
+ </nav>
154
+ </div>
155
+ );
156
+ }