localStorage.getItem("vf_token");
@@ -56,11 +52,11 @@ function RolSelect({ value, onChange, opties, T, darkMode }) {
{opties.map((o) => (
{ onChange(o); setOpen(false); }} style={{
padding: "9px 12px", fontSize: 13, cursor: "pointer",
- color: o === value ? TEAL_COLOR : T.text,
- background: o === value ? TEAL_BG : "transparent",
+ color: o === value ? T.accentTekst : T.text,
+ background: o === value ? T.inputBg : "transparent",
}}
onMouseEnter={(e) => e.currentTarget.style.background = "rgba(14,221,212,0.10)"}
- onMouseLeave={(e) => e.currentTarget.style.background = o === value ? TEAL_BG : "transparent"}
+ onMouseLeave={(e) => e.currentTarget.style.background = o === value ? T.inputBg : "transparent"}
>
{o}
@@ -197,8 +193,8 @@ export default function GebruikersBeheer({ onClose }) {
}}>
Gebruikersbeheer
-
{users.length} gebruiker{users.length !== 1 ? "s" : ""}
@@ -236,18 +232,18 @@ export default function GebruikersBeheer({ onClose }) {
{initials}
{u.naam}
{isSelf && (
jij
)}
@@ -263,9 +259,9 @@ export default function GebruikersBeheer({ onClose }) {
openEdit(u)} style={{
- padding: "3px 8px", background: TEAL_BG,
- border: `1px solid ${TEAL_BORDER}`, borderRadius: 6,
- color: TEAL_COLOR, cursor: "pointer", fontSize: 12,
+ padding: "3px 8px", background: T.inputBg,
+ border: `1px solid ${T.inputBorder}`, borderRadius: 6,
+ color: T.accentTekst, cursor: "pointer", fontSize: 12,
}}>✏️
{!isSelf && (
<>
@@ -303,10 +299,10 @@ export default function GebruikersBeheer({ onClose }) {
{/* Bewerken modal */}
{editUser && (
-
+
{editUser.naam} bewerken
@@ -322,7 +318,7 @@ export default function GebruikersBeheer({ onClose }) {
}}>Annuleren
{saving ? "Opslaan..." : "Opslaan"}
@@ -333,7 +329,7 @@ export default function GebruikersBeheer({ onClose }) {
{/* Nieuwe gebruiker */}
{showAdd ? (
@@ -353,7 +349,7 @@ export default function GebruikersBeheer({ onClose }) {
}}>Annuleren
@@ -364,7 +360,7 @@ export default function GebruikersBeheer({ onClose }) {
) : (
setShowAdd(true)} style={{
padding: "8px 18px",
- background: TEAL,
+ background: T.accentGradient,
border: "none", borderRadius: 8, color: "#fff",
fontWeight: 700, fontSize: 13, cursor: "pointer",
}}>+ Nieuwe gebruiker
diff --git a/frontend/src/components/InstellingenModal.jsx b/frontend/src/components/InstellingenModal.jsx
index e1423ab..c3c75f5 100644
--- a/frontend/src/components/InstellingenModal.jsx
+++ b/frontend/src/components/InstellingenModal.jsx
@@ -1,6 +1,5 @@
import { useApp } from "../context/AppContext.jsx";
-
-const TEAL = "linear-gradient(135deg, #7c3aed, #a855f7)";
+import { THEMAS } from "../constants/themas.js";
const BACKUP_ICON = (
@@ -35,7 +34,7 @@ function ActionRow({ icon, title, description, onClick }) {
}
export default function InstellingenModal({ onClose }) {
- const { T, darkMode, setDarkMode, doBackup, doRestore } = useApp();
+ const { T, darkMode, setDarkMode, thema, setThema, doBackup, doRestore } = useApp();
return (
Dark mode
-
setDarkMode((d) => !d)}
style={{
width: 44, height: 24, borderRadius: 12, cursor: "pointer",
- background: darkMode ? TEAL : "#334155",
+ background: darkMode ? T.accent : "#334155",
+ border: "none", padding: 0,
position: "relative", transition: "background 0.2s", flexShrink: 0,
}}
>
-
+
+
+
+ {/* Kleurthema */}
+
+
+ Kleurthema
+ {T.themaNaam}
+
+ {/* Drie kolommen: negen thema's vullen zo precies drie rijen, zonder
+ een halve rij onderaan. */}
+
+ {THEMAS.map((t) => {
+ const gekozen = t.id === thema;
+ return (
+ setThema(t.id)}
+ title={t.naam}
+ aria-label={`Thema ${t.naam}`}
+ aria-pressed={gekozen}
+ style={{
+ height: 30, borderRadius: 9, cursor: "pointer", padding: 0,
+ background: `linear-gradient(135deg, ${t.hoofd}, ${t.licht})`,
+ // De gekozen kleur krijgt een ring in de kaartkleur plus een
+ // rand, zodat hij ook opvalt als het thema zelf licht is.
+ border: `2px solid ${gekozen ? T.text : "transparent"}`,
+ boxShadow: gekozen ? `0 0 0 2px ${T.accentLicht}` : "none",
+ transition: "box-shadow 0.15s, border-color 0.15s",
+ }}
+ />
+ );
+ })}
diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx
index 47b95e7..16103d3 100644
--- a/frontend/src/components/NavBar.jsx
+++ b/frontend/src/components/NavBar.jsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { TABS } from "../constants/index.js";
+import { TABS, RED } from "../constants/index.js";
import { version } from "../version.js";
import { useUpdateCheck } from "../hooks/useUpdateCheck.js";
import { useApp } from "../context/AppContext.jsx";
@@ -82,9 +82,6 @@ function SectionLabel({ children }) {
);
}
-const ACTIVE_BG = "#7c3aed";
-const ACTIVE_SHADOW = "0 4px 14px rgba(124,58,237,0.4)";
-
function NavItem({ active, onClick, icon, label, T, danger }) {
const [hovered, setHovered] = useState(false);
const isLit = active || hovered;
@@ -98,12 +95,12 @@ function NavItem({ active, onClick, icon, label, T, danger }) {
display: "flex", alignItems: "center", gap: 10,
padding: "11px 12px", borderRadius: 9, cursor: "pointer", marginBottom: 4,
background: active
- ? ACTIVE_BG
- : hovered ? "rgba(139,92,246,0.08)" : "transparent",
- color: active ? "#fff" : hovered ? T.text : (danger ? "#ef4444" : T.muted),
+ ? T.accent
+ : hovered ? T.inputBg : "transparent",
+ color: active ? "#fff" : hovered ? T.text : (danger ? RED : T.muted),
fontWeight: active ? 600 : 400,
fontSize: 15,
- boxShadow: active ? ACTIVE_SHADOW : "none",
+ boxShadow: active ? T.cardShadow : "none",
transition: "all 0.15s",
}}
>
@@ -141,12 +138,12 @@ function OverPopup({ onClose, nieuweVersie, T }) {
@@ -158,7 +155,7 @@ function OverPopup({ onClose, nieuweVersie, T }) {
Versie
-
+
v{version}
@@ -198,7 +195,7 @@ function DarkModeToggle({ darkMode, setDarkMode, T }) {
{/* Toggle pill */}
+ stroke={T.accentTekst} strokeOpacity="0.35" strokeWidth="1.4"/>
VibeFinance
diff --git a/frontend/src/components/ProfielPopup.jsx b/frontend/src/components/ProfielPopup.jsx
index 6c0b5c1..a4d010f 100644
--- a/frontend/src/components/ProfielPopup.jsx
+++ b/frontend/src/components/ProfielPopup.jsx
@@ -2,8 +2,6 @@ import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { RED } from "../constants/index.js";
-const TEAL = "linear-gradient(135deg, #7c3aed, #a855f7)";
-const TEAL_SHADOW = "0 4px 14px rgba(124,58,237,0.4)";
function initials(naam = "") {
const parts = naam.trim().split(/\s+/);
@@ -149,7 +147,7 @@ export default function ProfielPopup({ onClose }) {
{currentUser?.email}
setView("profiel")} style={{
marginTop: 8, padding: "9px 24px",
- background: TEAL,
+ background: T.accentGradient,
border: "none", borderRadius: 10, color: "#fff",
fontWeight: 700, fontSize: 13, cursor: "pointer",
- boxShadow: TEAL_SHADOW,
+ boxShadow: T.cardShadow,
}}>Profiel bewerken
@@ -189,7 +187,7 @@ export default function ProfielPopup({ onClose }) {
@@ -229,7 +227,7 @@ export default function ProfielPopup({ onClose }) {
{profielErr &&
{profielErr}
}
{profielOk &&
Profiel opgeslagen.
}
{profielBusy ? "Opslaan…" : "Opslaan"}
@@ -252,7 +250,7 @@ export default function ProfielPopup({ onClose }) {
{pwErr &&
{pwErr}
}
{pwOk &&
Wachtwoord gewijzigd.
}
{pwBusy ? "Opslaan…" : "Opslaan"}
diff --git a/frontend/src/components/ui/index.jsx b/frontend/src/components/ui/index.jsx
index 7bfb767..25b1be9 100644
--- a/frontend/src/components/ui/index.jsx
+++ b/frontend/src/components/ui/index.jsx
@@ -1,5 +1,5 @@
import { useRef, useState, useCallback, useContext, createContext } from "react";
-import { PURPLE, PURPLE_LIGHT, EMOJI_OPTIONS } from "../../constants/index.js";
+import { EMOJI_OPTIONS } from "../../constants/index.js";
import { useApp } from "../../context/AppContext.jsx";
const DragContext = createContext(null);
@@ -18,11 +18,13 @@ export function VCard({ children, style = {} }) {
}
// ── VBadge ────────────────────────────────────────────────────────────────────
-export function VBadge({ label, color = PURPLE }) {
+export function VBadge({ label, color }) {
+ const { T } = useApp();
+ const kleur = color ?? T.accentTekst;
return (
@@ -32,16 +34,17 @@ export function VBadge({ label, color = PURPLE }) {
}
// ── TabBtn ────────────────────────────────────────────────────────────────────
-export function TabBtn({ active, onClick, children, darkMode = true }) {
+export function TabBtn({ active, onClick, children }) {
+ const { T } = useApp();
return (
@@ -51,12 +54,14 @@ export function TabBtn({ active, onClick, children, darkMode = true }) {
}
// ── VProgressBar ──────────────────────────────────────────────────────────────
-export function VProgressBar({ value, max, color = PURPLE }) {
+export function VProgressBar({ value, max, color }) {
+ const { T } = useApp();
+ const kleur = color ?? T.accent;
const p = Math.min((value / (max || 1)) * 100, 100);
return (
@@ -527,7 +532,7 @@ export function EmptyState({ icon, text, hint, onClick, locked, T }) {
{icon}
{text}
{!locked && hint && (
- {hint}
+ {hint}
)}
);
diff --git a/frontend/src/constants/themas.js b/frontend/src/constants/themas.js
new file mode 100644
index 0000000..83d06f1
--- /dev/null
+++ b/frontend/src/constants/themas.js
@@ -0,0 +1,92 @@
+/**
+ * Kleurthema's.
+ *
+ * Een thema bepaalt het **accent**, de dark-mode schakelaar bepaalt de
+ * **ondergrond**. Dat scheelt een compleet tweede palet per thema, en de
+ * bestaande schakelaar blijft doen wat hij deed.
+ *
+ * Per thema:
+ * hoofd de donkere stap van het accent — draagt tekst op een lichte ondergrond
+ * licht de lichte stap — draagt tekst op een donkere ondergrond
+ * chart startkleur van de gradient in de vermogensgrafiek (loopt naar `licht`)
+ * donker / helder: de ondergronden, licht naar het accent toe getint
+ *
+ * Eén stap voor beide modi haalt aan één kant altijd te weinig contrast; daarom
+ * kiest `useTheme` per modus de passende stap als `accentTekst`.
+ *
+ * Wat hier NIET in staat en dat ook niet hoort: GREEN, RED en GOLD uit
+ * `constants/index.js`. Die betekenen iets — winst, verlies en schuld, en de
+ * inleglijn in de grafiek. Betekenis mag niet van een themakeuze afhangen.
+ * Hetzelfde geldt voor de kleuren die de gebruiker zelf aan categorieën en
+ * schuldblokken geeft.
+ */
+
+export const THEMAS = [
+ {
+ id: "violet", naam: "Violet",
+ hoofd: "#7c3aed", licht: "#a855f7", chart: "#0eddd4",
+ donker: { bg: "#241A39", diep: "#1b1330", card: "#201930", tekst: "#ffffff", sub: "#a0aec0", muted: "#8b8fa8" },
+ helder: { bg: "#eef1f8", diep: "#e6eaf5", card: "#ffffff", tekst: "#0f172a", sub: "#475569", muted: "#7c8399" },
+ },
+ {
+ id: "oceaan", naam: "Oceaan",
+ hoofd: "#0369a1", licht: "#38bdf8", chart: "#22d3ee",
+ donker: { bg: "#11202f", diep: "#0c1826", card: "#16283a", tekst: "#ffffff", sub: "#9fb4c7", muted: "#7e93a6" },
+ helder: { bg: "#eef3f9", diep: "#e3ebf4", card: "#ffffff", tekst: "#0c1a26", sub: "#41576b", muted: "#748699" },
+ },
+ {
+ id: "indigo", naam: "Indigo",
+ hoofd: "#4338ca", licht: "#818cf8", chart: "#67e8f9",
+ donker: { bg: "#1a1c35", diep: "#13152a", card: "#1f2240", tekst: "#ffffff", sub: "#a5abcd", muted: "#868cb0" },
+ helder: { bg: "#eef0fa", diep: "#e5e8f6", card: "#ffffff", tekst: "#12142b", sub: "#474d75", muted: "#787ea3" },
+ },
+ {
+ id: "azuur", naam: "Azuur",
+ hoofd: "#1d4ed8", licht: "#60a5fa", chart: "#7dd3fc",
+ donker: { bg: "#131c33", diep: "#0e1628", card: "#1a2440", tekst: "#ffffff", sub: "#a3b2d4", muted: "#838fb3" },
+ helder: { bg: "#eef1fb", diep: "#e4e9f7", card: "#ffffff", tekst: "#0d1426", sub: "#42506e", muted: "#737e9c" },
+ },
+ {
+ id: "jade", naam: "Jade",
+ // Ligt dicht bij de GREEN die winst betekent: in de KPI-rij staan dan twee
+ // soorten groen naast elkaar.
+ hoofd: "#0f766e", licht: "#2dd4bf", chart: "#5eead4",
+ donker: { bg: "#0f2629", diep: "#0a1c1f", card: "#143035", tekst: "#ffffff", sub: "#9dbdbd", muted: "#7d9d9d" },
+ helder: { bg: "#edf5f4", diep: "#e2eeed", card: "#ffffff", tekst: "#0b1f21", sub: "#3c5c5c", muted: "#6e8b8b" },
+ },
+ {
+ id: "magenta", naam: "Magenta",
+ hoofd: "#a21caf", licht: "#e879f9", chart: "#f5d0fe",
+ donker: { bg: "#2a1130", diep: "#200b26", card: "#331639", tekst: "#ffffff", sub: "#c3a3c9", muted: "#a084a6" },
+ helder: { bg: "#faeefb", diep: "#f3e2f5", card: "#ffffff", tekst: "#25092a", sub: "#6a4370", muted: "#94709a" },
+ },
+ // Roos (#be123c) en Koper (#9a3412) stonden hier ook. Verwijderd: ze botsten
+ // met kleuren die al betekenis dragen — roos met de RED van verlies en schuld,
+ // koper met de gouden inleglijn en het oranje Belastingdienst-blok.
+ {
+ id: "grafiet", naam: "Grafiet",
+ hoofd: "#475569", licht: "#94a3b8", chart: "#cbd5e1",
+ donker: { bg: "#17191f", diep: "#111318", card: "#1e2129", tekst: "#ffffff", sub: "#a8b0bd", muted: "#868e9c" },
+ helder: { bg: "#f1f2f5", diep: "#e7e9ee", card: "#ffffff", tekst: "#14161a", sub: "#414751", muted: "#737a86" },
+ },
+ {
+ id: "papier", naam: "Papier",
+ // Warm van toon en bijna monochroom: het accent doet een stap terug.
+ hoofd: "#3f5570", licht: "#8fa5c0", chart: "#b6c9dc",
+ donker: { bg: "#1c1a17", diep: "#161412", card: "#24211d", tekst: "#f6f2eb", sub: "#b8b0a4", muted: "#948c80" },
+ helder: { bg: "#f5f1e8", diep: "#ece7db", card: "#fffdf8", tekst: "#1f1c17", sub: "#4d4a43", muted: "#7c766a" },
+ },
+ {
+ id: "puur", naam: "Puur",
+ // Zwart of wit, niets ertussenin. Op OLED scheelt de zwarte variant stroom.
+ hoofd: "#6d28d9", licht: "#c4b5fd", chart: "#22d3ee",
+ donker: { bg: "#000000", diep: "#000000", card: "#0e0e12", tekst: "#ffffff", sub: "#d4d4d8", muted: "#a1a1aa" },
+ helder: { bg: "#f7f7f8", diep: "#efeff1", card: "#ffffff", tekst: "#000000", sub: "#27272a", muted: "#52525b" },
+ },
+];
+
+export const STANDAARD_THEMA = "violet";
+
+/** Zoekt een thema op id; valt terug op het standaardthema. */
+export const zoekThema = (id) =>
+ THEMAS.find((t) => t.id === id) ?? THEMAS.find((t) => t.id === STANDAARD_THEMA);
diff --git a/frontend/src/context/AppContext.jsx b/frontend/src/context/AppContext.jsx
index 0c49b88..b8818d3 100644
--- a/frontend/src/context/AppContext.jsx
+++ b/frontend/src/context/AppContext.jsx
@@ -1,7 +1,8 @@
import { createContext, useContext, useState, useMemo, useRef } from "react";
import JSZip from "jszip";
import { INITIAL_DATA } from "../constants/index.js";
-import { migrateData } from "../utils/storage.js";
+import { STANDAARD_THEMA } from "../constants/themas.js";
+import { migrateData, loadFromStorage, saveToStorage } from "../utils/storage.js";
import { useTheme } from "../hooks/useTheme.js";
const AppContext = createContext(null);
@@ -34,7 +35,22 @@ export function AppProvider({ children }) {
};
// ── UI ────────────────────────────────────────────────────────
- const [darkMode, setDarkMode] = useState(true);
+ // Weergavevoorkeuren overleven een herlaad. Ze stonden eerder alleen in
+ // state, dus na een refresh stond je weer in het donker.
+ const [darkMode, setDarkModeRaw] = useState(() => loadFromStorage("darkMode") ?? true);
+ const [thema, setThemaRaw] = useState(() => loadFromStorage("thema") ?? STANDAARD_THEMA);
+
+ const setDarkMode = (v) => {
+ const n = typeof v === "function" ? v(darkMode) : v;
+ saveToStorage("darkMode", n);
+ setDarkModeRaw(n);
+ };
+
+ const setThema = (id) => {
+ saveToStorage("thema", id);
+ setThemaRaw(id);
+ };
+
const [locked, setLocked] = useState(false);
const [tab, setTab] = useState(0);
const [menuOpen, setMenuOpen] = useState(false);
@@ -87,7 +103,7 @@ export function AppProvider({ children }) {
const setUsers = (u) => { localStorage.setItem("wbjw_users", JSON.stringify(u)); setUsersRaw(u); };
// ── Thema ─────────────────────────────────────────────────────
- const T = useTheme(darkMode);
+ const T = useTheme(thema, darkMode);
// ── Afgeleide waarden ─────────────────────────────────────────
const ev = data.eigenVermogen;
@@ -199,6 +215,7 @@ export function AppProvider({ children }) {
loggedIn, currentUser, login, logout, loading, updateProfile, changePassword,
avatar, setAvatar,
darkMode, setDarkMode,
+ thema, setThema,
locked, setLocked,
tab, setTab,
menuOpen, setMenuOpen,
diff --git a/frontend/src/hooks/useTheme.js b/frontend/src/hooks/useTheme.js
index 2e55de9..e9b4528 100644
--- a/frontend/src/hooks/useTheme.js
+++ b/frontend/src/hooks/useTheme.js
@@ -1,27 +1,68 @@
+import { zoekThema } from "../constants/themas.js";
+
+/** `#rrggbb` (of `#rgb`) → `rgba(r, g, b, alpha)` */
+function metAlpha(hex, alpha) {
+ const h = hex.replace("#", "");
+ const vol = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
+ const n = parseInt(vol, 16);
+ return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
+}
+
/**
- * Geeft een thema-object (T) terug op basis van darkMode.
- * Wordt gebruikt door alle componenten om consistent kleuren te krijgen.
+ * Geeft een thema-object (T) terug op basis van het gekozen kleurthema en
+ * licht/donker. Wordt gebruikt door alle componenten om consistent kleuren te
+ * krijgen.
+ *
+ * Thema en modus staan los van elkaar: het thema levert het accent, `darkMode`
+ * de ondergrond. De randen, invoervelden en schaduwen worden uit het accent
+ * afgeleid, zodat ze vanzelf meekleuren.
+ *
+ * @param {string} themaId id uit `constants/themas.js`
+ * @param {boolean} darkMode
*/
-export function useTheme(darkMode) {
- const bg = darkMode ? "#241A39" : "#eef1f8";
- const card = darkMode ? "rgba(22,23,27,0.30)" : "#ffffff";
+export function useTheme(themaId, darkMode) {
+ const thema = zoekThema(themaId);
+ const p = darkMode ? thema.donker : thema.helder;
+
+ // Het accent dat tekst draagt is per ondergrond een andere stap van dezelfde
+ // kleur: de lichte op donker, de donkere op licht. Eén stap voor beide haalt
+ // aan één kant altijd te weinig contrast.
+ const accentTekst = darkMode ? thema.licht : thema.hoofd;
+
return {
- // Exacte Finex kleuren (dark) / licht alternatief
- bg,
- card,
+ bg: p.bg,
+ card: p.card,
// Ondoorzichtige achtergrond voor popups/modals: kaartkleur over de pagina-bg.
- popupBg: `linear-gradient(${card}, ${card}), ${bg}`,
- sidebar: darkMode ? "#16171B" : "#f8faff",
- border: darkMode ? "rgba(139,92,246,0.15)" : "rgba(0,0,0,0.07)",
- muted: darkMode ? "#8b8fa8" : "#94a3b8",
- text: darkMode ? "#ffffff" : "#0f172a",
- subtext: darkMode ? "#a0aec0" : "#64748b",
- inputBg: darkMode ? "rgba(139,92,246,0.08)" : "#f1f5ff",
- inputBorder:darkMode ? "rgba(139,92,246,0.2)" : "#e2e8f0",
- contentBg: darkMode ? "#241A39" : "#eef1f8",
- shadow: darkMode ? "0 4px 24px rgba(124,58,237,0.25)" : "0 4px 20px rgba(0,0,0,0.07)",
- cardShadow: darkMode ? "0 4px 20px rgba(124,58,237,0.2)" : "0 2px 8px rgba(0,0,0,0.06)",
- radius: "14px",
- font: "'Poppins', system-ui, sans-serif",
+ popupBg: `linear-gradient(${p.card}, ${p.card}), ${p.bg}`,
+ sidebar: p.diep,
+ border: darkMode ? metAlpha(thema.licht, 0.15) : "rgba(0,0,0,0.07)",
+ muted: p.muted,
+ text: p.tekst,
+ subtext: p.sub,
+ inputBg: darkMode ? metAlpha(thema.licht, 0.08) : metAlpha(thema.hoofd, 0.05),
+ inputBorder: darkMode ? metAlpha(thema.licht, 0.2) : "#e2e8f0",
+ contentBg: p.bg,
+ shadow: darkMode ? `0 4px 24px ${metAlpha(thema.hoofd, 0.25)}` : "0 4px 20px rgba(0,0,0,0.07)",
+ cardShadow: darkMode ? `0 4px 20px ${metAlpha(thema.hoofd, 0.2)}` : "0 2px 8px rgba(0,0,0,0.06)",
+ radius: "14px",
+ font: "'Poppins', system-ui, sans-serif",
+
+ // ── Accenten ──────────────────────────────────────────────────────────────
+ // Componenten die nu nog PURPLE / PURPLE_LIGHT uit constants/index.js
+ // importeren horen hier naartoe: een const op moduleniveau is niet
+ // reactief en kan dus niet met een themakeuze meeveranderen.
+ accent: thema.hoofd,
+ accentLicht: thema.licht,
+ accentTekst,
+ accentGradient: `linear-gradient(135deg, ${thema.hoofd}, ${thema.licht})`,
+ // Start- en eindkleur van de vermogenslijn in de grafiek.
+ chartVan: thema.chart,
+ chartTot: thema.licht,
+
+ // Voor componenten die op de modus moeten beslissen. Eerder werd dat gedaan
+ // door T.bg met een vaste hex te vergelijken; met elf thema's kan dat niet meer.
+ darkMode,
+ themaId: thema.id,
+ themaNaam: thema.naam,
};
}
diff --git a/frontend/src/pages/DashboardTab.jsx b/frontend/src/pages/DashboardTab.jsx
index 14db405..bf28d23 100644
--- a/frontend/src/pages/DashboardTab.jsx
+++ b/frontend/src/pages/DashboardTab.jsx
@@ -3,7 +3,7 @@ import { useApp } from "../context/AppContext.jsx";
import { fmt, pct, fmtDatum } from "../utils/format.js";
import { GREEN, RED, GOLD } from "../constants/index.js";
-const TEAL = "#a855f7";
+
import { VCard, VBadge } from "../components/ui/index.jsx";
export default function DashboardTab() {
@@ -27,9 +27,9 @@ export default function DashboardTab() {
const progressPct = Math.min(totEV / doel, 1);
const milestones = [0, 0.25, 0.5, 0.75, 1].map((f) => f * doel);
const kpis = [
- { label: "Start eigen vermogen", val: fmt(startEV), accent: TEAL},
- { label: "Extra geïnvesteerde fiat", val: fmt(extraFiat), accent: TEAL},
- { label: "Totaal geïnvesteerde fiat", val: fmt(totaalFiat), accent: TEAL},
+ { label: "Start eigen vermogen", val: fmt(startEV), accent: T.accentTekst},
+ { label: "Extra geïnvesteerde fiat", val: fmt(extraFiat), accent: T.accentTekst},
+ { label: "Totaal geïnvesteerde fiat", val: fmt(totaalFiat), accent: T.accentTekst},
{ label: "Totaal eigen vermogen", val: fmt(totEV), accent: GREEN },
{ label: "Winst op investering", val: fmt(winst), accent: winst >= 0 ? GREEN : RED },
{ label: "ROI %", val: totaalFiat > 0 ? pct(roi, 2) : "—", accent: roi >= 0 ? GREEN : RED },
@@ -91,19 +91,19 @@ export default function DashboardTab() {
🎯
Doel & Bijbehorende Progressie
-
+
{pct(progressPct)} behaald
{[0.25, 0.5, 0.75].map((f) => (
@@ -130,7 +130,7 @@ export default function DashboardTab() {
Ontwikkeling van Eigen Vermogen
{vData.length >= 2 && (
-
+
) : (
-
+
)}
@@ -183,7 +183,7 @@ function crPath(xs, ys, t = 0.4) {
}
// ── Mini inline SVG grafiek ────────────────────────────────────────────────────
-function MiniLineChart({ vData, fiatData, darkMode }) {
+function MiniLineChart({ vData, fiatData, darkMode, T }) {
const [hovered, setHovered] = useState(null);
const W = 700, H = 230, pl = 62, pr = 16, pt = 16, pb = 52;
const vals = vData.map((d) => d.nettoWaarde || 0);
@@ -203,7 +203,7 @@ function MiniLineChart({ vData, fiatData, darkMode }) {
// afronding zou tussen twee stortingen een dip tekenen die er niet is.
const fiatPath = xs.map((x, i) => `${i ? "L" : "M"} ${x},${fYs[i]}`).join(" ");
const gc = darkMode ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.06)";
- const lc = darkMode ? "#6b7280" : "#94a3b8";
+ const lc = T.muted;
const handleMouseMove = (e) => {
const rect = e.currentTarget.getBoundingClientRect();
@@ -236,8 +236,8 @@ function MiniLineChart({ vData, fiatData, darkMode }) {
>
-
-
+
+
@@ -277,16 +277,16 @@ function MiniLineChart({ vData, fiatData, darkMode }) {
{/* Verticale hover-lijn */}
{h !== null && (
+ stroke={T.muted} strokeOpacity="0.6" strokeWidth="1" strokeDasharray="4,3" />
)}
{/* Hover punten */}
{h !== null && (
<>
+ fill={GOLD} stroke={T.card} strokeWidth="2.5" />
+ fill={T.chartVan} stroke={T.card} strokeWidth="2.5" />
>
)}
@@ -294,13 +294,13 @@ function MiniLineChart({ vData, fiatData, darkMode }) {
{h !== null && (
+ fill={T.card}
+ stroke={T.border} strokeWidth="1" />
{fmtDatum(vData[h].datum)}
Vermogen
-
{fmt(vals[h])}
diff --git a/frontend/src/pages/EigenVermogenTab.jsx b/frontend/src/pages/EigenVermogenTab.jsx
index 3d4a706..5e0b0ce 100644
--- a/frontend/src/pages/EigenVermogenTab.jsx
+++ b/frontend/src/pages/EigenVermogenTab.jsx
@@ -2,7 +2,7 @@ import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { fmt, fmtDatum } from "../utils/format.js";
import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
-import { PURPLE, PURPLE_LIGHT, GREEN, RED, CAT_COLORS } from "../constants/index.js";
+import { GREEN, RED, CAT_COLORS } from "../constants/index.js";
import {
VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter,
EmojiPicker, AppIcon, ConfirmDeleteDialog, EmptyState,
@@ -130,14 +130,14 @@ function AanpassenPopup({ cat, onClose }) {
{ setNieuwNaam(e.target.value); setErr(""); }}
placeholder="Naam" style={{ ...is, flex: 1 }} />
- + Voeg toe
+ + Voeg toe
{err && }
@@ -275,12 +275,12 @@ function EVVoortgangTabel() {
@@ -325,7 +325,7 @@ function EVVoortgangTabel() {
Annuleren
- {editDatum ? "Opslaan" : "Toevoegen"}
+ {editDatum ? "Opslaan" : "Toevoegen"}
@@ -334,7 +334,7 @@ function EVVoortgangTabel() {
Geïnvesteerde Fiat
{!locked && (
-
+ Toevoegen
+
+ Toevoegen
)}
@@ -349,7 +349,9 @@ function EVVoortgangTabel() {
Datum
{cats.map((c) => (
-
+ // title: een lange categorienaam ("Crowdfunding") wordt bij
+ // de standaardbreedte afgekapt, dus hover geeft de volle naam.
+
{c.naam}
))}
@@ -369,14 +371,14 @@ function EVVoortgangTabel() {
{fmtDatum(row.datum)}
{cats.map((c) => {fmt(row[c.id] || 0)} )}
- {fmt(row.nettoWaarde || 0)}
+ {fmt(row.nettoWaarde || 0)}
{vLabel}
{/* title: bij een smalle kolom is de tekst afgekapt */}
{row.opmerkingen || "—"}
{!locked && (
- openEdit(row)} style={editBtn}>✏️
+ openEdit(row)} style={editBtn(T)}>✏️
removeRow(row.datum)} style={deleteSmBtn}>×
@@ -422,7 +424,7 @@ function DoelProgressie({ ev, data, setData, totVerdeling, locked, T }) {
Gewenst doel EV
-
{fmt(totVerdeling)}
+
{fmt(totVerdeling)}
{editing ? (
/
@@ -432,10 +434,10 @@ function DoelProgressie({ ev, data, setData, totVerdeling, locked, T }) {
onKeyDown={(e) => { if (e.key === "Enter") save(); if (e.key === "Escape") setEditing(false); }}
style={{
width: 130, padding: "4px 8px", borderRadius: 8, fontSize: 13,
- background: T.inputBg, border: `1px solid ${PURPLE}`, color: T.text, outline: "none",
+ background: T.inputBg, border: `1px solid ${T.accent}`, color: T.text, outline: "none",
}}
/>
- OK
+ OK
) : (
<>
@@ -445,13 +447,13 @@ function DoelProgressie({ ev, data, setData, totVerdeling, locked, T }) {
)}
>
)}
-
{Math.round(progressPct * 100)}%
+
{Math.round(progressPct * 100)}%
@@ -494,9 +496,9 @@ export default function EigenVermogenTab() {
total: fmt(totVerdeling), totalColor: GREEN,
},
{
- title: "Geïnvesteerde Fiat", accent: PURPLE_LIGHT,
+ title: "Geïnvesteerde Fiat", accent: T.accentTekst,
rows: cats.map((c) => ({ label: c.naam, value: evRows.length > 0 ? fmt(sumCat(c.id)) : "—", color: c.color })),
- total: evRows.length > 0 ? fmt(totGeinv) : "—", totalColor: PURPLE_LIGHT,
+ total: evRows.length > 0 ? fmt(totGeinv) : "—", totalColor: T.accentTekst,
},
{
title: "Rendement", accent: "#f59e0b",
@@ -579,16 +581,16 @@ export default function EigenVermogenTab() {
{!locked && (
setShowAddCat(true)} style={{
- padding: "10px 20px", background: `${PURPLE}22`,
- border: `1px solid ${PURPLE}44`, borderRadius: 10,
- color: PURPLE_LIGHT, cursor: "pointer", fontWeight: 700, fontSize: 13,
+ padding: "10px 20px", background: `${T.accent}22`,
+ border: `1px solid ${T.accent}44`, borderRadius: 10,
+ color: T.accentTekst, cursor: "pointer", fontWeight: 700, fontSize: 13,
}}>+ Nieuwe categorie
)}
{showAddCat && (
setShowAddCat(false)}>
- setShowAddCat(false)} headerBg={darkMode ? `${PURPLE}08` : "#f8f5ff"} T={T} />
+ setShowAddCat(false)} headerBg={darkMode ? `${T.accent}08` : T.inputBg} T={T} />
Naam *
setShowAddCat(false)} style={{ flex: 1, padding: "10px", background: "transparent", border: `1px solid ${T.border}`, borderRadius: 10, color: T.muted, cursor: "pointer", fontWeight: 600, fontSize: 13 }}>Annuleren
- Aanmaken
+ Aanmaken
@@ -668,9 +670,9 @@ function ErrMsg({ msg }) {
}}>{msg}
);
}
-const primaryBtn = { padding: "10px 20px", background: PURPLE, border: "none", borderRadius: 8, color: "#fff", fontWeight: 700, cursor: "pointer", fontSize: 13 };
-const editBtn = { padding: "2px 10px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 6, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, fontWeight: 600 };
+const primaryBtn = (T) => ({ padding: "10px 20px", background: T.accent, border: "none", borderRadius: 8, color: "#fff", fontWeight: 700, cursor: "pointer", fontSize: 13 });
+const editBtn = (T) => ({ padding: "2px 10px", background: `${T.accent}22`, border: `1px solid ${T.accent}44`, borderRadius: 6, color: T.accentTekst, cursor: "pointer", fontSize: 12, fontWeight: 600 });
const deleteSmBtn = { padding: "2px 8px", background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 6, color: RED, cursor: "pointer", fontSize: 12 };
const trashBtn = { padding: "10px 14px", background: "rgba(239,68,68,0.08)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 10, color: RED, cursor: "pointer", fontWeight: 600, fontSize: 13 };
-const addBtn = { padding: "6px 14px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 8, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, fontWeight: 700, flexShrink: 0 };
+const addBtn = (T) => ({ padding: "6px 14px", background: `${T.accent}22`, border: `1px solid ${T.accent}44`, borderRadius: 8, color: T.accentTekst, cursor: "pointer", fontSize: 12, fontWeight: 700, flexShrink: 0 });
const cancelBtn = (T) => ({ padding: "10px", background: "transparent", border: `1px solid ${T.border}`, borderRadius: 10, color: T.muted, cursor: "pointer", fontWeight: 600, fontSize: 13 });
diff --git a/frontend/src/pages/SchuldTab.jsx b/frontend/src/pages/SchuldTab.jsx
index 962c518..324ef49 100644
--- a/frontend/src/pages/SchuldTab.jsx
+++ b/frontend/src/pages/SchuldTab.jsx
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { fmt, pct } from "../utils/format.js";
-import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js";
+import { RED, GREEN } from "../constants/index.js";
import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, EmojiPicker, AppIcon, ConfirmDeleteDialog } from "../components/ui/index.jsx";
// ── SchuldAanpassenPopup ──────────────────────────────────────────────────────
@@ -108,14 +108,14 @@ function SchuldAanpassenPopup({ blok, onClose }) {
{ setNieuwNaam(e.target.value); setNieuwErr(""); }}
placeholder="Naam" style={{ ...is, flex: 1 }} />
- + Voeg toe
+ + Voeg toe
{nieuwErr && }
@@ -448,5 +448,5 @@ function ErrMsg({ msg }) {
}
const deleteSmBtn = { padding: "4px 8px", background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 6, color: RED, cursor: "pointer", fontSize: 13, flexShrink: 0 };
const trashBtn = { padding: "10px 14px", background: "rgba(239,68,68,0.08)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 10, color: RED, cursor: "pointer", fontWeight: 600, fontSize: 13 };
-const addBtn = { padding: "6px 14px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 8, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, fontWeight: 700, flexShrink: 0 };
+const addBtn = (T) => ({ padding: "6px 14px", background: `${T.accent}22`, border: `1px solid ${T.accent}44`, borderRadius: 8, color: T.accentTekst, cursor: "pointer", fontSize: 12, fontWeight: 700, flexShrink: 0 });
const cancelBtn = (T) => ({ padding: "10px", background: "transparent", border: `1px solid ${T.border}`, borderRadius: 10, color: T.muted, cursor: "pointer", fontWeight: 600, fontSize: 13 });
diff --git a/frontend/src/pages/VoortgangTab.jsx b/frontend/src/pages/VoortgangTab.jsx
index 5001975..118e7eb 100644
--- a/frontend/src/pages/VoortgangTab.jsx
+++ b/frontend/src/pages/VoortgangTab.jsx
@@ -2,7 +2,7 @@ import { useState } from "react";
import { useApp } from "../context/AppContext.jsx";
import { fmt, fmtDatum } from "../utils/format.js";
import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
-import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js";
+import { RED, GREEN } from "../constants/index.js";
import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter } from "../components/ui/index.jsx";
// ── Inline SVG line chart ─────────────────────────────────────────────────────
@@ -27,8 +27,11 @@ function SvgLineChart({ pts, lines, T, H = 200 }) {
const rng = mx - mn || 1;
const xp = (i) => pl + (i / (pts.length - 1)) * iW;
const yp = (v) => pt2 + (1 - ((v ?? 0) - mn) / rng) * iH;
- const gc = T.bg === "#0f1117" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)";
- const darkMode = T.bg === "#0f1117";
+ // Was een vergelijking met een vaste hex (`T.bg === "#0f1117"`). Die kleur is
+ // de achtergrond allang niet meer, dus beide stonden altijd op licht — en met
+ // instelbare thema's valt er sowieso niets meer aan de bg af te lezen.
+ const darkMode = T.darkMode;
+ const gc = darkMode ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)";
return (
@@ -82,7 +85,7 @@ function SvgLineChart({ pts, lines, T, H = 200 }) {
{isHov && (
@@ -145,12 +148,12 @@ function VoortgangModal({ onSave, onClose, T, darkMode, initialData }) {
@@ -189,7 +192,7 @@ function VoortgangModal({ onSave, onClose, T, darkMode, initialData }) {
Annuleren
-
+
{isEdit ? "Opslaan" : "Toevoegen"}
@@ -286,8 +289,8 @@ export default function VoortgangTab() {
{/* Hero netto */}
@@ -397,7 +400,7 @@ export default function VoortgangTab() {
Voortgang overzicht
{!locked && (
setShowAdd(true)} style={{
- padding: "6px 14px", background: PURPLE, border: `1px solid ${PURPLE}`,
+ padding: "6px 14px", background: T.accent, border: `1px solid ${T.accent}`,
borderRadius: 8, color: "#fff", fontWeight: 700, fontSize: 12, cursor: "pointer",
}}>+ Toevoegen
)}
@@ -419,7 +422,7 @@ export default function VoortgangTab() {
Datum
Eigen Vermogen
Schuld
-
Nettowaarde
+
Nettowaarde
Opmerkingen
{!locked &&
}
@@ -435,7 +438,7 @@ export default function VoortgangTab() {
{row.opmerkingen || "—"}
{!locked && (
- setEditRow(row)} style={{ padding: "2px 8px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 6, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, marginRight: 4 }}>✏️
+ setEditRow(row)} style={{ padding: "2px 8px", background: `${T.accent}22`, border: `1px solid ${T.accent}44`, borderRadius: 6, color: T.accentTekst, cursor: "pointer", fontSize: 12, marginRight: 4 }}>✏️
removeVoortgang(row.datum)} style={{ padding: "2px 8px", background: "rgba(239,68,68,0.1)", border: "1px solid rgba(239,68,68,0.25)", borderRadius: 6, color: RED, cursor: "pointer", fontSize: 12 }}>×
)}
diff --git a/frontend/src/utils/storage.js b/frontend/src/utils/storage.js
index c0c6536..f60d446 100644
--- a/frontend/src/utils/storage.js
+++ b/frontend/src/utils/storage.js
@@ -3,6 +3,10 @@ const KEYS = {
evRows: "ev_voortgang_rows",
users: "wbjw_users",
exitplan: "exitplan_data",
+ // Weergavevoorkeuren: per browser, net als de kolombreedtes. Ze staan bewust
+ // los van de financiële data, dus ze gaan niet mee in de backup-ZIP.
+ thema: "vibefinance_thema",
+ darkMode: "vibefinance_darkmode",
};
export function loadFromStorage(key) {
diff --git a/package.json b/package.json
index 5e05c69..ce20d32 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "vibefinance",
- "version": "1.0.7",
+ "version": "1.0.8",
"description": "VibeFinance — persoonlijk vermogensbeheer",
"private": true,
"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",