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.
- package/bin/epms.js +149 -60
- package/frontend-project/src/App.css +1 -0
- package/frontend-project/src/Auth/Login.jsx +85 -0
- package/frontend-project/src/Auth/Register.jsx +183 -0
- package/frontend-project/src/Intro.jsx +33 -0
- package/frontend-project/src/LayOut.jsx +38 -0
- package/frontend-project/src/api/ApiClient.js +92 -0
- package/frontend-project/src/assets/hero.png +0 -0
- package/frontend-project/src/assets/react.svg +1 -0
- package/frontend-project/src/assets/vite.svg +1 -0
- package/frontend-project/src/components/Aside.jsx +9 -0
- package/frontend-project/src/components/Button.jsx +100 -0
- package/frontend-project/src/components/Card.jsx +104 -0
- package/frontend-project/src/components/FormField.jsx +129 -0
- package/frontend-project/src/components/Modal.jsx +106 -0
- package/frontend-project/src/components/Table.jsx +127 -0
- package/frontend-project/src/components/Toast.jsx +64 -0
- package/frontend-project/src/components/index.js +14 -0
- package/frontend-project/src/config.js +66 -0
- package/frontend-project/src/design.js +115 -0
- package/frontend-project/src/index.css +60 -0
- package/frontend-project/src/layouts/BottomNav.jsx +156 -0
- package/frontend-project/src/layouts/TopNav.jsx +150 -0
- package/frontend-project/src/layouts/useShell.js +44 -0
- package/frontend-project/src/main.jsx +41 -0
- package/frontend-project/src/pages/Department.jsx +188 -0
- package/frontend-project/src/pages/Employee.jsx +274 -0
- package/frontend-project/src/pages/Home.jsx +79 -0
- package/frontend-project/src/pages/Profile.jsx +9 -0
- package/frontend-project/src/pages/Register.jsx +57 -0
- package/frontend-project/src/pages/Reports.jsx +91 -0
- package/frontend-project/src/pages/Salary.jsx +264 -0
- package/frontend-project/src/themes.js +175 -0
- 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
|
+
}
|