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,264 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { Plus, Pencil, Trash2 } from 'lucide-react';
|
|
3
|
+
import Table from '../components/Table';
|
|
4
|
+
import Modal from '../components/Modal';
|
|
5
|
+
import Button from '../components/Button';
|
|
6
|
+
import FormField from '../components/FormField';
|
|
7
|
+
import { useToast } from '../components/Toast';
|
|
8
|
+
import { salaryApi, employeeApi } from '../api/ApiClient';
|
|
9
|
+
|
|
10
|
+
const getCurrentMonth = () => {
|
|
11
|
+
const d = new Date();
|
|
12
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function Salary() {
|
|
16
|
+
const toast = useToast();
|
|
17
|
+
const [salaries, setSalaries] = useState([]);
|
|
18
|
+
const [employees, setEmployees] = useState([]);
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
21
|
+
const [editTarget, setEditTarget] = useState(null);
|
|
22
|
+
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
23
|
+
|
|
24
|
+
const loadAll = useCallback(async () => {
|
|
25
|
+
setLoading(true);
|
|
26
|
+
try {
|
|
27
|
+
const [salRes, empRes] = await Promise.allSettled([
|
|
28
|
+
salaryApi.list(),
|
|
29
|
+
employeeApi.list(),
|
|
30
|
+
]);
|
|
31
|
+
if (salRes.status === 'fulfilled') setSalaries(salRes.value?.data?.salaries ?? []);
|
|
32
|
+
if (empRes.status === 'fulfilled') setEmployees(empRes.value?.data?.employees ?? []);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
toast.error('Failed to load data');
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
useEffect(() => { loadAll(); }, [loadAll]);
|
|
41
|
+
|
|
42
|
+
const columns = [
|
|
43
|
+
{
|
|
44
|
+
key: 'employee', label: 'Employee',
|
|
45
|
+
render: (v) => v ? `${v.firstName} ${v.lastName}` : '—',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: 'dept', label: 'Department',
|
|
49
|
+
render: (_, row) => row?.employee?.department?.departmentName || '—',
|
|
50
|
+
},
|
|
51
|
+
{ key: 'grossSalary', label: 'Gross Salary', render: (v) => `${Number(v).toLocaleString()} RWF` },
|
|
52
|
+
{ key: 'totalDeduction', label: 'Deductions', render: (v) => `${Number(v).toLocaleString()} RWF` },
|
|
53
|
+
{ key: 'netSalary', label: 'Net Salary', render: (v) => `${Number(v).toLocaleString()} RWF` },
|
|
54
|
+
{ key: 'month', label: 'Month' },
|
|
55
|
+
{
|
|
56
|
+
key: 'actions', label: '', align: 'right',
|
|
57
|
+
render: (_, row) => (
|
|
58
|
+
<div className="flex items-center justify-end gap-1.5">
|
|
59
|
+
<Button size="xs" variant="outline" icon={<Pencil className="w-3 h-3" />} onClick={() => setEditTarget(row)}>Edit</Button>
|
|
60
|
+
<Button size="xs" variant="danger" icon={<Trash2 className="w-3 h-3" />} onClick={() => setDeleteTarget(row)}>Delete</Button>
|
|
61
|
+
</div>
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="space-y-5">
|
|
68
|
+
<div className="flex items-start justify-between">
|
|
69
|
+
<div>
|
|
70
|
+
<h1 className="text-[18px] font-bold text-gray-800">Salary Management</h1>
|
|
71
|
+
<p className="text-[13px] text-gray-400 mt-0.5">{salaries.length} salary records</p>
|
|
72
|
+
</div>
|
|
73
|
+
<Button size="sm" className='bg-[#008A75]' icon={<Plus className="w-3.5 h-3.5" />} onClick={() => setCreateOpen(true)}>Add Salary Record</Button>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="bg-white border border-gray-200 p-5">
|
|
76
|
+
<Table columns={columns} data={salaries} rowKey="_id" loading={loading} emptyMessage="No salary records yet." maxHeight="480px" />
|
|
77
|
+
</div>
|
|
78
|
+
{createOpen && <CreateModal employees={employees} onClose={() => setCreateOpen(false)} onCreated={() => { setCreateOpen(false); loadAll(); }} />}
|
|
79
|
+
{editTarget && <EditModal salary={editTarget} employees={employees} onClose={() => setEditTarget(null)} onUpdated={() => { setEditTarget(null); loadAll(); }} />}
|
|
80
|
+
{deleteTarget && <DeleteModal salary={deleteTarget} onClose={() => setDeleteTarget(null)} onDeleted={() => { setDeleteTarget(null); loadAll(); }} />}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function CreateModal({ employees, onClose, onCreated }) {
|
|
86
|
+
const toast = useToast();
|
|
87
|
+
const [form, setForm] = useState({ employee: '', grossSalary: '', totalDeduction: '0', month: getCurrentMonth() });
|
|
88
|
+
const [loading, setLoading] = useState(false);
|
|
89
|
+
const [errors, setErrors] = useState({});
|
|
90
|
+
const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
|
|
91
|
+
const gross = Number(form.grossSalary) || 0;
|
|
92
|
+
const deduction = Number(form.totalDeduction) || 0;
|
|
93
|
+
const net = gross - deduction;
|
|
94
|
+
|
|
95
|
+
const empMap = useMemo(() => {
|
|
96
|
+
const m = {};
|
|
97
|
+
employees.forEach((e) => { m[e._id] = e; });
|
|
98
|
+
return m;
|
|
99
|
+
}, [employees]);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (form.employee) {
|
|
103
|
+
const emp = empMap[form.employee];
|
|
104
|
+
if (emp?.department) {
|
|
105
|
+
setForm((p) => ({
|
|
106
|
+
...p,
|
|
107
|
+
grossSalary: emp.department.grossSalary ?? p.grossSalary,
|
|
108
|
+
totalDeduction: emp.department.totalDeduction ?? p.totalDeduction,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}, [form.employee, empMap]);
|
|
113
|
+
|
|
114
|
+
const validate = () => {
|
|
115
|
+
const e = {};
|
|
116
|
+
if (!form.employee) e.employee = 'Required';
|
|
117
|
+
if (!form.grossSalary) e.grossSalary = 'Required';
|
|
118
|
+
if (form.totalDeduction === '') e.totalDeduction = 'Required';
|
|
119
|
+
if (!form.month) e.month = 'Required';
|
|
120
|
+
setErrors(e);
|
|
121
|
+
return Object.keys(e).length === 0;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleSubmit = async () => {
|
|
125
|
+
if (!validate()) return;
|
|
126
|
+
setLoading(true);
|
|
127
|
+
try {
|
|
128
|
+
await salaryApi.create({ employee: form.employee, grossSalary: gross, totalDeduction: deduction, netSalary: net, month: form.month });
|
|
129
|
+
toast.success('Salary record created');
|
|
130
|
+
onCreated();
|
|
131
|
+
} catch (err) {
|
|
132
|
+
toast.error(err.message ?? 'Failed to create salary record');
|
|
133
|
+
} finally {
|
|
134
|
+
setLoading(false);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const empOptions = employees.map((e) => ({ value: e._id, label: `${e.employeeNumber} - ${e.firstName} ${e.lastName}` }));
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Modal open title="Add Salary Record" size="sm" onClose={onClose}>
|
|
142
|
+
<div className="space-y-3">
|
|
143
|
+
<FormField label="Employee" required error={errors.employee}>
|
|
144
|
+
<FormField.Select value={form.employee} onChange={set('employee')} placeholder="Select employee" options={empOptions} />
|
|
145
|
+
</FormField>
|
|
146
|
+
<FormField label="Gross Salary (RWF)" required error={errors.grossSalary}>
|
|
147
|
+
<div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{gross.toLocaleString()} RWF</div>
|
|
148
|
+
</FormField>
|
|
149
|
+
<FormField label="Total Deduction (RWF)" required error={errors.totalDeduction}>
|
|
150
|
+
<div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{deduction.toLocaleString()} RWF</div>
|
|
151
|
+
</FormField>
|
|
152
|
+
<FormField label="Net Salary (RWF)">
|
|
153
|
+
<div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{net.toLocaleString()} RWF</div>
|
|
154
|
+
</FormField>
|
|
155
|
+
<FormField label="Month" required error={errors.month}>
|
|
156
|
+
<FormField.Input type="month" value={form.month} onChange={set('month')} />
|
|
157
|
+
</FormField>
|
|
158
|
+
</div>
|
|
159
|
+
<Modal.Footer>
|
|
160
|
+
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
161
|
+
<Button loading={loading} onClick={handleSubmit}>Create Record</Button>
|
|
162
|
+
</Modal.Footer>
|
|
163
|
+
</Modal>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function EditModal({ salary, employees, onClose, onUpdated }) {
|
|
168
|
+
const toast = useToast();
|
|
169
|
+
const [form, setForm] = useState({ employee: salary.employee?._id || '', grossSalary: salary.grossSalary || '', totalDeduction: salary.totalDeduction || '', month: salary.month || '' });
|
|
170
|
+
const [loading, setLoading] = useState(false);
|
|
171
|
+
const set = (f) => (v) => setForm((p) => ({ ...p, [f]: v }));
|
|
172
|
+
const gross = Number(form.grossSalary) || 0;
|
|
173
|
+
const deduction = Number(form.totalDeduction) || 0;
|
|
174
|
+
const net = gross - deduction;
|
|
175
|
+
const empOptions = employees.map((e) => ({ value: e._id, label: `${e.employeeNumber} - ${e.firstName} ${e.lastName}` }));
|
|
176
|
+
|
|
177
|
+
const empMap = useMemo(() => {
|
|
178
|
+
const m = {};
|
|
179
|
+
employees.forEach((e) => { m[e._id] = e; });
|
|
180
|
+
return m;
|
|
181
|
+
}, [employees]);
|
|
182
|
+
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (form.employee) {
|
|
185
|
+
const emp = empMap[form.employee];
|
|
186
|
+
if (emp?.department) {
|
|
187
|
+
setForm((p) => ({
|
|
188
|
+
...p,
|
|
189
|
+
grossSalary: emp.department.grossSalary ?? p.grossSalary,
|
|
190
|
+
totalDeduction: emp.department.totalDeduction ?? p.totalDeduction,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, [form.employee, empMap]);
|
|
195
|
+
|
|
196
|
+
const handleSave = async () => {
|
|
197
|
+
setLoading(true);
|
|
198
|
+
try {
|
|
199
|
+
await salaryApi.update(salary._id, { employee: form.employee, grossSalary: gross, totalDeduction: deduction, netSalary: net, month: form.month });
|
|
200
|
+
toast.success('Salary record updated');
|
|
201
|
+
onUpdated();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
toast.error(err.message ?? 'Update failed');
|
|
204
|
+
} finally {
|
|
205
|
+
setLoading(false);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Modal open title="Edit Salary Record" size="sm" onClose={onClose}>
|
|
211
|
+
<div className="space-y-3">
|
|
212
|
+
<FormField label="Employee">
|
|
213
|
+
<FormField.Select value={form.employee} onChange={set('employee')} options={empOptions} placeholder="Select employee" />
|
|
214
|
+
</FormField>
|
|
215
|
+
<FormField label="Gross Salary (RWF)">
|
|
216
|
+
<div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{gross.toLocaleString()} RWF</div>
|
|
217
|
+
</FormField>
|
|
218
|
+
<FormField label="Total Deduction (RWF)">
|
|
219
|
+
<div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{deduction.toLocaleString()} RWF</div>
|
|
220
|
+
</FormField>
|
|
221
|
+
<FormField label="Net Salary (RWF)">
|
|
222
|
+
<div className="px-3 py-2 bg-gray-50 text-[14px] font-semibold text-gray-700 border border-gray-200">{net.toLocaleString()} RWF</div>
|
|
223
|
+
</FormField>
|
|
224
|
+
<FormField label="Month">
|
|
225
|
+
<FormField.Input type="month" value={form.month} onChange={set('month')} />
|
|
226
|
+
</FormField>
|
|
227
|
+
</div>
|
|
228
|
+
<Modal.Footer>
|
|
229
|
+
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
230
|
+
<Button loading={loading} onClick={handleSave}>Save Changes</Button>
|
|
231
|
+
</Modal.Footer>
|
|
232
|
+
</Modal>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function DeleteModal({ salary, onClose, onDeleted }) {
|
|
237
|
+
const toast = useToast();
|
|
238
|
+
const [loading, setLoading] = useState(false);
|
|
239
|
+
|
|
240
|
+
const handleDelete = async () => {
|
|
241
|
+
setLoading(true);
|
|
242
|
+
try {
|
|
243
|
+
await salaryApi.remove(salary._id);
|
|
244
|
+
toast.success('Salary record deleted');
|
|
245
|
+
onDeleted();
|
|
246
|
+
} catch (err) {
|
|
247
|
+
toast.error(err.message ?? 'Delete failed');
|
|
248
|
+
} finally {
|
|
249
|
+
setLoading(false);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<Modal open title="Confirm Deletion" size="sm" onClose={onClose}>
|
|
255
|
+
<p className="text-[13px] text-gray-600">
|
|
256
|
+
Delete salary record for <strong>{salary.employee?.firstName} {salary.employee?.lastName}</strong> ({salary.month})? This cannot be undone.
|
|
257
|
+
</p>
|
|
258
|
+
<Modal.Footer>
|
|
259
|
+
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
260
|
+
<Button variant="danger" loading={loading} icon={<Trash2 className="w-3.5 h-3.5" />} onClick={handleDelete}>Delete</Button>
|
|
261
|
+
</Modal.Footer>
|
|
262
|
+
</Modal>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// 🎨 THEMES — Every visual style for the app in one place
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS FILE DOES:
|
|
6
|
+
// Each theme defines colors for every part of the UI.
|
|
7
|
+
// Change `theme: 'forest'` in config.js and everything updates.
|
|
8
|
+
//
|
|
9
|
+
// HOW TO CREATE A NEW THEME:
|
|
10
|
+
// 1. Copy one of the existing themes
|
|
11
|
+
// 2. Give it a name (e.g. 'sunset')
|
|
12
|
+
// 3. Change the hex colors below
|
|
13
|
+
// 4. Add it to this object (e.g. sunset: { ... })
|
|
14
|
+
// 5. Use it: theme: 'sunset' in config.js
|
|
15
|
+
//
|
|
16
|
+
// COLOR GUIDE:
|
|
17
|
+
// primary: main brand color (buttons, links, active nav)
|
|
18
|
+
// surface: page background
|
|
19
|
+
// card: card/panel background
|
|
20
|
+
// border: default border color
|
|
21
|
+
// text: default text color
|
|
22
|
+
// textMuted: secondary/label text
|
|
23
|
+
// navBg: navigation bar background
|
|
24
|
+
// navText: navigation text
|
|
25
|
+
// navActive: navigation active item bg
|
|
26
|
+
// danger: delete/error actions
|
|
27
|
+
// success: success states
|
|
28
|
+
//
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
export const themes = {
|
|
32
|
+
// ── 1. FOREST (green - default) ────────────────────────────────────────
|
|
33
|
+
forest: {
|
|
34
|
+
primary: '#008A75',
|
|
35
|
+
surface: '#F9FAFB',
|
|
36
|
+
card: '#FFFFFF',
|
|
37
|
+
border: '#E5E7EB',
|
|
38
|
+
text: '#1F2937',
|
|
39
|
+
textMuted: '#9CA3AF',
|
|
40
|
+
navBg: '#FFFFFF',
|
|
41
|
+
navText: '#4B5563',
|
|
42
|
+
navActive: '#008A75',
|
|
43
|
+
danger: '#EF4444',
|
|
44
|
+
success: '#10B981',
|
|
45
|
+
warning: '#F59E0B',
|
|
46
|
+
info: '#3B82F6',
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// ── 2. MIDNIGHT (dark) ─────────────────────────────────────────────────
|
|
50
|
+
midnight: {
|
|
51
|
+
primary: '#6B7280',
|
|
52
|
+
surface: '#18181B',
|
|
53
|
+
card: '#27272A',
|
|
54
|
+
border: '#3F3F46',
|
|
55
|
+
text: '#F4F4F5',
|
|
56
|
+
textMuted: '#A1A1AA',
|
|
57
|
+
navBg: '#27272A',
|
|
58
|
+
navText: '#D4D4D8',
|
|
59
|
+
navActive: '#FFFFFF',
|
|
60
|
+
danger: '#F87171',
|
|
61
|
+
success: '#34D399',
|
|
62
|
+
warning: '#FBBF24',
|
|
63
|
+
info: '#60A5FA',
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// ── 3. SLATE (blue-gray professional) ──────────────────────────────────
|
|
67
|
+
slate: {
|
|
68
|
+
primary: '#6366F1',
|
|
69
|
+
surface: '#F1F5F9',
|
|
70
|
+
card: '#FFFFFF',
|
|
71
|
+
border: '#CBD5E1',
|
|
72
|
+
text: '#1E293B',
|
|
73
|
+
textMuted: '#94A3B8',
|
|
74
|
+
navBg: '#1E293B',
|
|
75
|
+
navText: '#CBD5E1',
|
|
76
|
+
navActive: '#818CF8',
|
|
77
|
+
danger: '#EF4444',
|
|
78
|
+
success: '#10B981',
|
|
79
|
+
warning: '#F59E0B',
|
|
80
|
+
info: '#3B82F6',
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// ── 4. ROSE (warm pink/crimson) ────────────────────────────────────────
|
|
84
|
+
rose: {
|
|
85
|
+
primary: '#E11D48',
|
|
86
|
+
surface: '#FFF1F2',
|
|
87
|
+
card: '#FFFFFF',
|
|
88
|
+
border: '#FECDD3',
|
|
89
|
+
text: '#1F2937',
|
|
90
|
+
textMuted: '#FDA4AF',
|
|
91
|
+
navBg: '#FFFFFF',
|
|
92
|
+
navText: '#6B7280',
|
|
93
|
+
navActive: '#E11D48',
|
|
94
|
+
danger: '#DC2626',
|
|
95
|
+
success: '#16A34A',
|
|
96
|
+
warning: '#D97706',
|
|
97
|
+
info: '#2563EB',
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// ── 5. AMBER (orange/gold) ─────────────────────────────────────────────
|
|
101
|
+
amber: {
|
|
102
|
+
primary: '#D97706',
|
|
103
|
+
surface: '#FFFBEB',
|
|
104
|
+
card: '#FFFFFF',
|
|
105
|
+
border: '#FDE68A',
|
|
106
|
+
text: '#1F2937',
|
|
107
|
+
textMuted: '#9CA3AF',
|
|
108
|
+
navBg: '#451A03',
|
|
109
|
+
navText: '#FDE68A',
|
|
110
|
+
navActive: '#F59E0B',
|
|
111
|
+
danger: '#DC2626',
|
|
112
|
+
success: '#16A34A',
|
|
113
|
+
warning: '#D97706',
|
|
114
|
+
info: '#2563EB',
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// ── 6. OCEAN (deep navy + cyan) ────────────────────────────────────────
|
|
118
|
+
ocean: {
|
|
119
|
+
primary: '#06B6D4',
|
|
120
|
+
surface: '#F0F9FF',
|
|
121
|
+
card: '#FFFFFF',
|
|
122
|
+
border: '#BAE6FD',
|
|
123
|
+
text: '#0F172A',
|
|
124
|
+
textMuted: '#94A3B8',
|
|
125
|
+
navBg: '#172554',
|
|
126
|
+
navText: '#BAE6FD',
|
|
127
|
+
navActive: '#22D3EE',
|
|
128
|
+
danger: '#EF4444',
|
|
129
|
+
success: '#10B981',
|
|
130
|
+
warning: '#F59E0B',
|
|
131
|
+
info: '#3B82F6',
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// ── 7. CHALK (black & white minimal) ───────────────────────────────────
|
|
135
|
+
chalk: {
|
|
136
|
+
primary: '#000000',
|
|
137
|
+
surface: '#FAFAFA',
|
|
138
|
+
card: '#FFFFFF',
|
|
139
|
+
border: '#D4D4D4',
|
|
140
|
+
text: '#171717',
|
|
141
|
+
textMuted: '#A3A3A3',
|
|
142
|
+
navBg: '#FFFFFF',
|
|
143
|
+
navText: '#525252',
|
|
144
|
+
navActive: '#000000',
|
|
145
|
+
danger: '#DC2626',
|
|
146
|
+
success: '#16A34A',
|
|
147
|
+
warning: '#D97706',
|
|
148
|
+
info: '#2563EB',
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// ── 8. VIOLET (purple modern) ──────────────────────────────────────────
|
|
152
|
+
violet: {
|
|
153
|
+
primary: '#7C3AED',
|
|
154
|
+
surface: '#F5F3FF',
|
|
155
|
+
card: '#FFFFFF',
|
|
156
|
+
border: '#DDD6FE',
|
|
157
|
+
text: '#1F2937',
|
|
158
|
+
textMuted: '#9CA3AF',
|
|
159
|
+
navBg: '#2E1065',
|
|
160
|
+
navText: '#DDD6FE',
|
|
161
|
+
navActive: '#A78BFA',
|
|
162
|
+
danger: '#EF4444',
|
|
163
|
+
success: '#10B981',
|
|
164
|
+
warning: '#F59E0B',
|
|
165
|
+
info: '#3B82F6',
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// ─── GET RESOLVED THEME (applies config overrides) ───────────────────────────
|
|
170
|
+
// This merges the selected theme with any user overrides from config.js.
|
|
171
|
+
//
|
|
172
|
+
export function getTheme(themeName = 'forest', overrides = {}) {
|
|
173
|
+
const base = themes[themeName] || themes.forest;
|
|
174
|
+
return { ...base, ...overrides };
|
|
175
|
+
}
|