Gestion des Thèmes

This commit is contained in:
ocroguennec committed 2026-09-06 18:59:09 +02:00
1 parent bab7a79b63
commit be3c93897b
24 files changed
+1041 -141

No files matched your search

+5
View File
@@ -56,6 +56,11 @@ function FraisOnly({ children }) {
export default function App() {
const [allowRegistration, setAllowRegistration] = useState(true);
// Note (06/09/26) : le thème de couleur (Admin > Général, ou choix
// personnel de l'utilisateur) n'est plus géré ici — voir UiContext.jsx
// (colorTheme / adminColorTheme), qui applique l'attribut data-color-theme
// sur <html> et fonctionne aussi avant authentification (UiProvider
// enveloppe App dans main.jsx, y compris pour la page de connexion).
useEffect(() => {
fetch('/api/app-info').then(r => r.json()).then(d => {
if (d.allowRegistration === false) setAllowRegistration(false);
+9 -6
View File
@@ -10,10 +10,13 @@ import { useMemo, useState, useRef } from 'react';
* montant par frais).
*/
const COLOR = '#e8547a';
const BG = '#070c15';
const GRID = 'rgba(255,255,255,0.055)';
const LABEL = '#4a5568';
/* Couleurs alignées sur le thème actif (Admin > Général, cf.
* project_theme_interface.md) plutôt que fixées en dur — même mécanisme
* que InvChart.jsx / InteretsDonutChart.jsx. */
const COLOR = 'var(--primary)';
const BG = 'var(--surface)';
const GRID = 'var(--border)';
const LABEL = 'var(--text-muted)';
const RANGES = ['1J', '7J', '1M', '3M', 'YTD', '1A', 'TOUT'];
const MOIS_COURT = ['jan.','fév.','mar.','avr.','mai','juin','juil.','août','sep.','oct.','nov.','déc.'];
@@ -247,7 +250,7 @@ export default function FraisCumulChart({ rows }) {
{/* Ligne zéro */}
{yZero && yZero > PAD.top && yZero < PAD.top + plotH && (
<line x1={PAD.left} y1={yZero} x2={W - PAD.right} y2={yZero}
stroke="rgba(255,255,255,0.18)" strokeWidth="1" />
stroke={GRID} strokeWidth="1" />
)}
{/* Remplissage dégradé */}
@@ -273,7 +276,7 @@ export default function FraisCumulChart({ rows }) {
{hover && (
<g>
<line x1={hover.x} y1={PAD.top} x2={hover.x} y2={PAD.top + plotH}
stroke="rgba(255,255,255,0.12)" strokeWidth="1" strokeDasharray="3 4" />
stroke={GRID} strokeWidth="1" strokeDasharray="3 4" />
<circle cx={hover.x} cy={hover.y} r="4.5"
fill={COLOR} stroke={BG} strokeWidth="2" />
</g>
+9 -6
View File
@@ -1,10 +1,13 @@
import { useMemo, useState, useRef } from 'react';
/* ── Constantes ─────────────────────────────────────────────── */
const COLOR = '#4fa8e8';
const BG = '#070c15';
const GRID = 'rgba(255,255,255,0.055)';
const LABEL = '#4a5568';
/* Couleurs alignées sur le thème actif (Admin > Général, cf.
* project_theme_interface.md) plutôt que fixées en dur — même mécanisme
* que InvChart.jsx / InteretsDonutChart.jsx. */
const COLOR = 'var(--primary)';
const BG = 'var(--surface)';
const GRID = 'var(--border)';
const LABEL = 'var(--text-muted)';
const RANGES = ['1J', '7J', '1M', '3M', 'YTD', '1A', 'TOUT'];
const MOIS_COURT = ['jan.','fév.','mar.','avr.','mai','juin','juil.','août','sep.','oct.','nov.','déc.'];
@@ -237,7 +240,7 @@ export default function InteretsChart({ rows, netMode }) {
{/* Ligne zéro */}
{yZero && yZero > PAD.top && yZero < PAD.top + plotH && (
<line x1={PAD.left} y1={yZero} x2={W - PAD.right} y2={yZero}
stroke="rgba(255,255,255,0.18)" strokeWidth="1" />
stroke={GRID} strokeWidth="1" />
)}
{/* Remplissage dégradé */}
@@ -263,7 +266,7 @@ export default function InteretsChart({ rows, netMode }) {
{hover && (
<g>
<line x1={hover.x} y1={PAD.top} x2={hover.x} y2={PAD.top + plotH}
stroke="rgba(255,255,255,0.12)" strokeWidth="1" strokeDasharray="3 4" />
stroke={GRID} strokeWidth="1" strokeDasharray="3 4" />
<circle cx={hover.x} cy={hover.y} r="4.5"
fill={COLOR} stroke={BG} strokeWidth="2" />
</g>
@@ -1,11 +1,13 @@
import { useState, useRef, useEffect } from 'react';
import { api } from '../api.js';
import { useInteretsChart } from '../context/InteretsChartContext.jsx';
import { useTheme } from '../context/ThemeContext.jsx';
const ICONS_BASE = '/api/icons-files/';
const GRID = 'rgba(255,255,255,0.055)';
const LABEL = '#4a5568';
const LABEL_LIGHT = '#4a5568';
const LABEL_DARK = 'rgba(255,255,255,.65)';
const MOIS = ['Jan','Fév','Mar','Avr','Mai','Jun','Jul','Aoû','Sep','Oct','Nov','Déc'];
function fmtShort(v) {
@@ -40,6 +42,8 @@ export default function InteretsMensuelsChart() {
netMode,
chartInterets, chartCapital, chartCashback,
} = useInteretsChart();
const { resolved } = useTheme();
const LABEL = resolved === 'dark' ? LABEL_DARK : LABEL_LIGHT;
// État local uniquement
const [hovered, setHovered] = useState(null);
+10 -6
View File
@@ -1,10 +1,14 @@
import { useMemo, useState, useRef } from 'react';
/* ── Constantes ─────────────────────────────────────────────── */
const BLUE = '#4fa8e8';
const BG = '#070c15';
const GRID = 'rgba(255,255,255,0.055)';
const LABEL = '#4a5568';
/* Couleurs alignées sur le thème actif (Admin > Général, cf.
* project_theme_interface.md) plutôt que fixées en dur — même mécanisme
* que InteretsDonutChart.jsx (attributs de présentation SVG fill/stroke
* acceptant var(--...), supporté nativement par les navigateurs modernes). */
const BLUE = 'var(--primary)';
const BG = 'var(--surface)';
const GRID = 'var(--border)';
const LABEL = 'var(--text-muted)';
const RANGES = ['1J', '7J', '1M', '3M', 'YTD', '1A', 'TOUT'];
const MOIS_COURT = ['jan.','fév.','mar.','avr.','mai','juin','juil.','août','sep.','oct.','nov.','déc.'];
@@ -292,7 +296,7 @@ export default function InvChart({ rows, remboursements, reinvestissements, plat
{/* Ligne zéro */}
{yZero && yZero > PAD.top && yZero < PAD.top + plotH && (
<line x1={PAD.left} y1={yZero} x2={W - PAD.right} y2={yZero}
stroke="rgba(255,255,255,0.18)" strokeWidth="1" />
stroke={GRID} strokeWidth="1" />
)}
{/* Aire dégradée + courbe */}
@@ -316,7 +320,7 @@ export default function InvChart({ rows, remboursements, reinvestissements, plat
{hover && (
<g>
<line x1={hover.x} y1={PAD.top} x2={hover.x} y2={PAD.top + plotH}
stroke="rgba(255,255,255,0.12)" strokeWidth="1" strokeDasharray="3 4" />
stroke={GRID} strokeWidth="1" strokeDasharray="3 4" />
<circle cx={hover.x} cy={hover.y} r="4.5"
fill={BLUE} stroke={BG} strokeWidth="2" />
</g>
+13 -4
View File
@@ -1,3 +1,12 @@
/* Logo Crowdlending — carré aligné sur le thème d'interface actif
* (Admin > Général, cf. project_theme_interface.md) : le fond suit
* var(--primary), les 3 barres restent blanches à opacités variées
* pour la profondeur, et la flèche (accent "croissance") garde un
* vert dédié via var(--sidebar-accent) — stable quel que soit le
* thème de couleur choisi (non surchargé par les 9 thèmes) — plutôt
* que fixé en dur. Même mécanisme que InvChart.jsx /
* InteretsDonutChart.jsx (attributs de présentation SVG fill
* acceptant var(--...), supporté nativement). */
export default function Logo({ size = 32 }) {
return (
<svg
@@ -8,11 +17,11 @@ export default function Logo({ size = 32 }) {
style={{ display: 'block', flexShrink: 0 }}
aria-hidden="true"
>
<rect width="48" height="48" rx="10" fill="#1e3a8a" />
<rect x="7" y="31" width="9" height="11" rx="2" fill="#93c5fd" />
<rect x="20" y="21" width="9" height="21" rx="2" fill="#60a5fa" />
<rect width="48" height="48" rx="10" fill="var(--primary)" />
<rect x="7" y="31" width="9" height="11" rx="2" fill="white" fillOpacity="0.55" />
<rect x="20" y="21" width="9" height="21" rx="2" fill="white" fillOpacity="0.78" />
<rect x="33" y="11" width="9" height="31" rx="2" fill="white" />
<polygon points="37.5,4 44,12 31,12" fill="#4ade80" />
<polygon points="37.5,4 44,12 31,12" fill="var(--sidebar-accent)" />
</svg>
);
}
+2 -1
View File
@@ -28,6 +28,7 @@ export default function PageIcon({ name, size = 40, style }) {
return (
<img
src={`${ICONS_BASE}${filename}`}
className="app-lib-icon"
width={size}
height={size}
alt=""
@@ -37,7 +38,7 @@ export default function PageIcon({ name, size = 40, style }) {
verticalAlign: 'middle',
marginRight: 10,
objectFit: 'contain',
opacity: 0.85,
opacity: 1,
...style,
}}
/>
+9 -6
View File
@@ -1,10 +1,13 @@
import { useMemo, useState, useRef, useEffect } from 'react';
/* ── Constantes ─────────────────────────────────────────────── */
const GOLD = '#4fa8e8'; // bleu ciel — accord avec le thème navy du site
const BG = '#070c15';
const GRID = 'rgba(255,255,255,0.055)';
const LABEL = '#4a5568';
/* Couleurs alignées sur le thème actif (Admin > Général, cf.
* project_theme_interface.md) plutôt que fixées en dur — même mécanisme
* que InvChart.jsx / InteretsDonutChart.jsx. */
const GOLD = 'var(--primary)';
const BG = 'var(--surface)';
const GRID = 'var(--border)';
const LABEL = 'var(--text-muted)';
const RANGES = ['1J', '7J', '1M', '3M', 'YTD', '1A', 'TOUT'];
@@ -254,7 +257,7 @@ export default function SoldeChart({ rows, range: rangeProp, onRangeChange }) {
{/* Ligne zéro */}
{yZero && yZero > PAD.top && yZero < PAD.top + plotH && (
<line x1={PAD.left} y1={yZero} x2={W - PAD.right} y2={yZero}
stroke="rgba(255,255,255,0.18)" strokeWidth="1" />
stroke={GRID} strokeWidth="1" />
)}
{/* Remplissage dégradé */}
@@ -280,7 +283,7 @@ export default function SoldeChart({ rows, range: rangeProp, onRangeChange }) {
{hover && (
<g>
<line x1={hover.x} y1={PAD.top} x2={hover.x} y2={PAD.top + plotH}
stroke="rgba(255,255,255,0.12)" strokeWidth="1" strokeDasharray="3 4" />
stroke={GRID} strokeWidth="1" strokeDasharray="3 4" />
<circle cx={hover.x} cy={hover.y} r="4.5"
fill={GOLD} stroke={BG} strokeWidth="2" />
</g>
+80
View File
@@ -0,0 +1,80 @@
/**
* ThemeSelect — sélecteur déroulant pour un choix unique avec pastille de
* couleur (ex : thème de couleur de l'interface, Admin > Général).
* Même schéma d'interaction que CategorySelect (bouton déclencheur +
* dropdown, fermeture au clic extérieur / scroll / resize), en position
* absolue (pas de portail fixed : utilisé dans une page classique, pas
* dans une modale au overflow contraint).
*
* Props :
* value : string — value sélectionnée
* onChange : (value: string) => void
* options : { value, label, swatch }[] — liste des choix
*/
import { useEffect, useRef, useState } from 'react';
export default function ThemeSelect({ value, onChange, options = [] }) {
const [open, setOpen] = useState(false);
const wrapRef = useRef(null);
useEffect(() => {
if (!open) return;
const close = (e) => {
if (wrapRef.current?.contains(e.target)) return;
setOpen(false);
};
document.addEventListener('mousedown', close);
return () => document.removeEventListener('mousedown', close);
}, [open]);
const current = options.find(o => o.value === value) || options[0];
return (
<div ref={wrapRef} className="theme-select-wrap">
<button
type="button"
className={`theme-select-trigger${open ? ' open' : ''}`}
onClick={() => setOpen(o => !o)}
aria-haspopup="listbox"
aria-expanded={open}
>
<span className="theme-select-swatch" style={{ background: current?.swatch }} aria-hidden="true" />
<span className="theme-select-label">{current?.label || 'Choisir…'}</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
style={{ flexShrink: 0, marginLeft: 'auto', transition: 'transform .15s', transform: open ? 'rotate(180deg)' : 'rotate(0deg)' }}
aria-hidden="true">
<path d="M6 9l6 6 6-6" />
</svg>
</button>
{open && (
<div className="theme-select-dropdown" role="listbox">
{options.map(({ value: v, label, swatch }) => {
const active = v === value;
return (
<button
key={v}
type="button"
role="option"
aria-selected={active}
className={`theme-select-item${active ? ' active' : ''}`}
onClick={() => { onChange(v); setOpen(false); }}
>
<span className="theme-select-swatch" style={{ background: swatch }} aria-hidden="true" />
<span className="theme-select-item-label">{label}</span>
{active && (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
style={{ marginLeft: 'auto', color: 'var(--accent)' }} aria-hidden="true">
<polyline points="20 6 9 17 4 12" />
</svg>
)}
</button>
);
})}
</div>
)}
</div>
);
}
+4 -4
View File
@@ -56,7 +56,7 @@ function UserAvatar({ user, size = 36 }) {
return (
<div style={{
width: size, height: size, borderRadius: '50%',
background: 'linear-gradient(135deg, #1e40af 0%, #1e3a8a 100%)',
background: 'linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%)',
border: '2px solid rgba(74,222,128,.5)',
color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontWeight: 700, fontSize: Math.round(size * 0.38), flexShrink: 0,
@@ -70,8 +70,8 @@ function UserAvatar({ user, size = 36 }) {
function MemberAvatar({ member, size = 28 }) {
const isEntreprise = member?.type === 'entreprise';
const bg = isEntreprise
? 'linear-gradient(135deg, #3730a3, #4338ca)'
: 'linear-gradient(135deg, #1e3a8a, #1e40af)';
? 'linear-gradient(135deg, var(--avatar-entreprise-start), var(--avatar-entreprise-end))'
: 'linear-gradient(135deg, var(--primary-hover), var(--primary))';
return (
<div style={{
width: size, height: size, borderRadius: '50%',
@@ -89,7 +89,7 @@ function TeamBadge({ size = 28 }) {
return (
<div className="team-badge" style={{
width: size, height: size, borderRadius: '50%',
background: '#1e3a8a',
background: 'var(--primary-hover)',
border: '1.5px solid rgba(255,255,255,.25)',
color: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
+78 -1
View File
@@ -1,10 +1,19 @@
import { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react';
import { api } from '../api.js';
import { THEME_TO_PALETTE_NAME } from '../utils/colorThemes.js';
import { getSuggestedPalettes } from '../utils/chartPalettes.js';
/**
* UI state that persists across sessions:
* - sidebarCollapsed : sidebar visibility
* - fontScale : 'large' (max/accessibilité) | 'medium' | 'compact'
* - colorTheme : thème de couleur personnel (06/09/26) — un des 10
* thèmes de colorThemes.js, ou null ("Automatique" :
* suit le thème par défaut défini par l'administrateur
* dans Admin > Général, cf. adminColorTheme). Stocké
* en base (table user_preferences, clé color_theme)
* pour suivre l'utilisateur d'un appareil à l'autre —
* voir project_theme_interface.md.
*/
const UiCtx = createContext(null);
@@ -19,6 +28,8 @@ const KEY_CHART_CAPITAL = 'cl_chart_capital';
const KEY_CHART_CASHBACK = 'cl_chart_cashback';
const KEY_PFO_ASSUJETTI = 'cl_pfo_assujetti';
const KEY_OBJECTIFS_ACTIFS = 'cl_objectifs_actifs';
const KEY_COLOR_THEME = 'cl_color_theme';
const KEY_ADMIN_COLOR_THEME = 'cl_admin_color_theme';
const FONT_SIZES = ['large', 'medium', 'compact'];
const LANGUES = ['fr', 'en'];
@@ -79,6 +90,37 @@ export function UiProvider({ children }) {
return saved === null ? false : saved === '1';
});
// Thème de couleur personnel (06/09/26) : null = "Automatique" (suit
// adminColorTheme). Une valeur explicite ici prime toujours sur le thème
// par défaut de l'administrateur.
const [colorTheme, setColorThemeState] = useState(() =>
localStorage.getItem(KEY_COLOR_THEME) || null
);
// Thème par défaut défini par l'administrateur (Admin > Général), utilisé
// en repli quand colorTheme est null. Mis en cache en localStorage pour
// éviter un flash au chargement le temps que /api/app-info réponde.
const [adminColorTheme, setAdminColorThemeState] = useState(() =>
localStorage.getItem(KEY_ADMIN_COLOR_THEME) || 'classique'
);
// ── Thème par défaut (admin) : chargé une fois au montage ───────
// Endpoint public (pas d'auth requise) : reste valable sur la page de
// connexion, comme dans App.jsx.
useEffect(() => {
fetch('/api/app-info').then(r => r.json()).then(d => {
const t = d.colorTheme || 'classique';
localStorage.setItem(KEY_ADMIN_COLOR_THEME, t);
setAdminColorThemeState(t);
}).catch(() => {});
}, []);
// ── Applique le thème de couleur effectif sur <html> ────────────
// Un choix personnel (colorTheme) prime toujours sur le défaut admin.
useEffect(() => {
document.documentElement.setAttribute('data-color-theme', colorTheme || adminColorTheme);
}, [colorTheme, adminColorTheme]);
// ── Sync DB → localStorage au montage ──────────────────────────
// La DB fait foi : ses valeurs écrasent le localStorage si présentes.
useEffect(() => {
@@ -105,6 +147,10 @@ export function UiProvider({ children }) {
localStorage.setItem(KEY_OBJECTIFS_ACTIFS, val ? '1' : '0');
setObjectifsActifsState(val);
}
if (prefs.color_theme) {
localStorage.setItem(KEY_COLOR_THEME, prefs.color_theme);
setColorThemeState(prefs.color_theme);
}
}).catch(() => {
// Silencieux : token absent au premier rendu, on garde le localStorage
});
@@ -163,6 +209,37 @@ export function UiProvider({ children }) {
persistPref('chart_cashback', hex);
}, [persistPref]);
// Adapte automatiquement la palette de couleurs de graphiques (section
// ci-dessus) sur celle associée au thème donné (même logique que le
// reverse-coupling admin de GeneralSection.jsx) — appelé uniquement lors
// d'un changement explicite de thème, jamais lors de la resynchronisation
// silencieuse au montage (sinon un choix de couleurs personnalisé serait
// écrasé à chaque rechargement de page).
const applyPaletteForTheme = useCallback((themeValue) => {
const paletteName = THEME_TO_PALETTE_NAME[themeValue];
if (!paletteName) return;
const palette = getSuggestedPalettes().find(p => p.name === paletteName);
if (!palette) return;
setChartInterets(palette.interets);
setChartCapital(palette.capital);
setChartCashback(palette.cashback);
}, [setChartInterets, setChartCapital, setChartCashback]);
// value = un des thèmes de colorThemes.js pour un choix personnel explicite,
// ou null/undefined pour revenir à "Automatique" (suit le thème par défaut
// de l'administrateur — cf. adminColorTheme).
const setColorTheme = useCallback((value) => {
setColorThemeState(value || null);
if (value) {
localStorage.setItem(KEY_COLOR_THEME, value);
persistPref('color_theme', value);
} else {
localStorage.removeItem(KEY_COLOR_THEME);
api.del('/preferences/color_theme').catch(() => {});
}
applyPaletteForTheme(value || adminColorTheme);
}, [persistPref, applyPaletteForTheme, adminColorTheme]);
const setPfoAssujetti = useCallback((val) => {
setPfoAssujettiState(val);
localStorage.setItem(KEY_PFO_ASSUJETTI, val ? '1' : '0');
@@ -176,7 +253,7 @@ export function UiProvider({ children }) {
}, [persistPref]);
return (
<UiCtx.Provider value={{ sidebarCollapsed, setSidebarCollapsed, toggleSidebar, fontScale, setFontScale, langue, setLangue, devise, setDevise, displayMode, setDisplayMode, chartInterets, setChartInterets, chartCapital, setChartCapital, chartCashback, setChartCashback, pfoAssujetti, setPfoAssujetti, objectifsActifs, setObjectifsActifs }}>
<UiCtx.Provider value={{ sidebarCollapsed, setSidebarCollapsed, toggleSidebar, fontScale, setFontScale, langue, setLangue, devise, setDevise, displayMode, setDisplayMode, chartInterets, setChartInterets, chartCapital, setChartCapital, chartCashback, setChartCashback, pfoAssujetti, setPfoAssujetti, objectifsActifs, setObjectifsActifs, colorTheme, adminColorTheme, setColorTheme }}>
{children}
</UiCtx.Provider>
);
+6 -6
View File
@@ -48,12 +48,12 @@ function YearSelectorKpi() {
onClick={() => setOpen(v => !v)}
style={{
height: '100%', boxSizing: 'border-box',
background: 'linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%)',
background: 'linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%)',
borderRadius: 10,
padding: '16px 20px',
boxShadow: open
? '0 6px 28px rgba(109,40,217,0.45)'
: '0 4px 20px rgba(109,40,217,0.30)',
? '0 6px 28px color-mix(in srgb, var(--primary) 45%, transparent)'
: '0 4px 20px color-mix(in srgb, var(--primary) 30%, transparent)',
cursor: 'pointer',
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
userSelect: 'none',
@@ -101,8 +101,8 @@ function YearSelectorKpi() {
style={{
padding: '10px 16px',
cursor: 'pointer',
background: isActive ? 'rgba(109,40,217,0.08)' : 'transparent',
color: isActive ? '#7c3aed' : 'var(--text)',
background: isActive ? 'color-mix(in srgb, var(--primary) 8%, transparent)' : 'transparent',
color: isActive ? 'var(--primary)' : 'var(--text)',
fontWeight: isActive ? 700 : 400,
fontSize: 'var(--fs-sm)',
borderBottom: i < options.length - 1 ? '1px solid var(--border)' : 'none',
@@ -115,7 +115,7 @@ function YearSelectorKpi() {
<span>{opt.label}</span>
{isActive && (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="#7c3aed" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
stroke="var(--primary)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
)}
+2 -2
View File
@@ -9,8 +9,8 @@ import ConfirmModal from '../components/ConfirmModal.jsx';
function MemberAvatar({ membre, size = 40 }) {
const initials = memberInitials(membre);
const bg = membre.type === 'entreprise'
? 'linear-gradient(135deg, #4f46e5 0%, #3730a3 100%)'
: 'linear-gradient(135deg, #1e40af 0%, #1e3a8a 100%)';
? 'linear-gradient(135deg, var(--avatar-entreprise-start) 0%, var(--avatar-entreprise-end) 100%)'
: 'linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%)';
return (
<div style={{
width: size, height: size, borderRadius: '50%',
+13 -7
View File
@@ -69,9 +69,11 @@ function PlatSelector({ platOptions, selectedPlatName, onSelect }) {
<div
onClick={() => setOpen(v => !v)}
style={{
background: 'linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%)',
background: 'linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%)',
borderRadius: 10, padding: '12px 16px',
boxShadow: open ? '0 6px 28px rgba(109,40,217,0.45)' : '0 4px 20px rgba(109,40,217,0.30)',
boxShadow: open
? '0 6px 28px color-mix(in srgb, var(--primary) 45%, transparent)'
: '0 4px 20px color-mix(in srgb, var(--primary) 30%, transparent)',
cursor: 'pointer', userSelect: 'none', transition: 'box-shadow .15s',
}}
>
@@ -89,7 +91,7 @@ function PlatSelector({ platOptions, selectedPlatName, onSelect }) {
{selected?.icon_filename && (
<img src={`${LOGOS_BASE}${selected.icon_filename}`}
width={28} height={28}
style={{ objectFit: 'contain', borderRadius: 4, background: 'rgba(255,255,255,0.12)', padding: 2, flexShrink: 0 }}
style={{ objectFit: 'contain', borderRadius: 4, background: '#fff', padding: 2, flexShrink: 0 }}
alt="" />
)}
<span style={{ color: '#fff', fontSize: '1.1rem', fontWeight: 700, lineHeight: 1.2 }}>
@@ -156,9 +158,11 @@ function DetenteurSelector({ detenteurOptions, selectedId, onSelect }) {
<div
onClick={() => setOpen(v => !v)}
style={{
background: 'linear-gradient(135deg, #0d9488 0%, #0891b2 100%)',
background: 'linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%)',
borderRadius: 10, padding: '12px 16px',
boxShadow: open ? '0 6px 22px rgba(13,148,136,0.45)' : '0 4px 16px rgba(13,148,136,0.3)',
boxShadow: open
? '0 6px 22px color-mix(in srgb, var(--primary) 45%, transparent)'
: '0 4px 16px color-mix(in srgb, var(--primary) 30%, transparent)',
cursor: 'pointer', userSelect: 'none', transition: 'box-shadow .15s',
}}
>
@@ -230,9 +234,11 @@ function AnneeSelector({ availableYears, selectedYear, onSelect }) {
<div
onClick={() => setOpen(v => !v)}
style={{
background: 'linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%)',
background: 'linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%)',
borderRadius: 10, padding: '12px 16px',
boxShadow: open ? '0 6px 28px rgba(109,40,217,0.45)' : '0 4px 20px rgba(109,40,217,0.30)',
boxShadow: open
? '0 6px 28px color-mix(in srgb, var(--primary) 45%, transparent)'
: '0 4px 20px color-mix(in srgb, var(--primary) 30%, transparent)',
cursor: 'pointer', userSelect: 'none', transition: 'box-shadow .15s',
}}
>
+6 -6
View File
@@ -32,12 +32,12 @@ function YearSelector({ annee, setAnnee, availableYears }) {
onClick={() => setOpen(v => !v)}
style={{
height: '100%', boxSizing: 'border-box',
background: 'linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%)',
background: 'linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%)',
borderRadius: 10,
padding: '12px 16px',
boxShadow: open
? '0 6px 28px rgba(109,40,217,0.45)'
: '0 4px 20px rgba(109,40,217,0.30)',
? '0 6px 28px color-mix(in srgb, var(--primary) 45%, transparent)'
: '0 4px 20px color-mix(in srgb, var(--primary) 30%, transparent)',
cursor: 'pointer',
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
userSelect: 'none',
@@ -76,8 +76,8 @@ function YearSelector({ annee, setAnnee, availableYears }) {
onClick={() => { setAnnee(String(yr)); setOpen(false); }}
style={{
padding: '10px 16px', cursor: 'pointer',
background: isActive ? 'rgba(109,40,217,0.08)' : 'transparent',
color: isActive ? '#7c3aed' : 'var(--text)',
background: isActive ? 'color-mix(in srgb, var(--primary) 8%, transparent)' : 'transparent',
color: isActive ? 'var(--primary)' : 'var(--text)',
fontWeight: isActive ? 700 : 400,
fontSize: 'var(--fs-sm)',
borderBottom: i < availableYears.length - 1 ? '1px solid var(--border)' : 'none',
@@ -90,7 +90,7 @@ function YearSelector({ annee, setAnnee, availableYears }) {
<span>{yr}</span>
{isActive && (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="#7c3aed" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
stroke="var(--primary)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
)}
@@ -6,6 +6,10 @@
import { useState, useEffect } from 'react';
import { api } from '../../api.js';
import { withDevOverrides } from '../../utils/devOverrides.js';
import { getSuggestedPalettes } from '../../utils/chartPalettes.js';
import { COLOR_THEMES, THEME_TO_PALETTE_NAME } from '../../utils/colorThemes.js';
import { useUi } from '../../context/UiContext.jsx';
import ThemeSelect from '../../components/ThemeSelect.jsx';
function SettingRow({ label, description, children }) {
return (
@@ -36,6 +40,7 @@ const DEFAULT = {
appName: 'Crowdlending Tracker',
appUrl: '',
environnement: 'developpement',
colorTheme: 'classique',
mcpUrl: '',
allowRegistration: true,
minPasswordLength: 8,
@@ -50,6 +55,7 @@ const ENVIRONNEMENTS = [
];
export default function GeneralSection() {
const { setChartInterets, setChartCapital, setChartCashback } = useUi();
const [form, setForm] = useState(DEFAULT);
const [saved, setSaved] = useState(null); // 'ok' | 'err' | null
const [busy, setBusy] = useState(false);
@@ -66,6 +72,7 @@ export default function GeneralSection() {
appName: dd.appName || 'Crowdlending Tracker',
appUrl: dd.appUrl || '',
environnement: dd.environnement || 'developpement',
colorTheme: dd.colorTheme || 'classique',
mcpUrl: dd.mcpUrl || '',
allowRegistration: dd.allowRegistration !== false,
minPasswordLength: dd.minPasswordLength || 8,
@@ -86,12 +93,29 @@ export default function GeneralSection() {
appName: form.appName.trim(),
appUrl: form.appUrl.trim(),
environnement: form.environnement,
colorTheme: form.colorTheme,
mcpUrl: form.mcpUrl.trim(),
allowRegistration: form.allowRegistration,
minPasswordLength: form.minPasswordLength,
documentsQuotaGo: form.documentsQuotaGo,
documentsQuotaCount: form.documentsQuotaCount,
});
// Applique immédiatement la nouvelle teinte à cette session (sans
// attendre un rechargement) — mêmes attribut/valeurs que App.jsx au
// boot ('classique' = no-op, aucun bloc CSS [data-color-theme="classique"]).
document.documentElement.setAttribute('data-color-theme', form.colorTheme);
// Adapte automatiquement, sur ce compte, la palette de couleurs de
// graphiques (Settings > Apparence) sur celle du thème qui vient
// d'être choisi (même nom) — cf. project_theme_interface.md.
const paletteName = THEME_TO_PALETTE_NAME[form.colorTheme];
if (paletteName) {
const palette = getSuggestedPalettes().find(p => p.name === paletteName);
if (palette) {
setChartInterets(palette.interets);
setChartCapital(palette.capital);
setChartCashback(palette.cashback);
}
}
setSaved('ok');
setTimeout(() => setSaved(null), 3000);
} catch {
@@ -165,6 +189,28 @@ export default function GeneralSection() {
</SettingRow>
</div>
{/* Apparence */}
<div className="card" style={{ marginBottom: 24 }}>
<h3 style={{ margin: '0 0 4px', fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>Apparence</h3>
<p style={{ margin: '0 0 20px', fontSize: 13, color: 'var(--text-muted)' }}>
Thème de couleur par défaut de l'interface, appliqué à tout utilisateur
n'ayant pas choisi son propre thème (Réglages &gt; Apparence).
</p>
<SettingRow
label="Thème de couleur"
description="Teinte des menus et boutons appliquée par défaut à tout compte n'ayant pas défini son propre thème dans Réglages > Apparence (chacun peut y choisir une couleur différente, stockée sur son compte). Utile pour repérer visuellement cet environnement au premier coup d'œil, par exemple en gardant Production dans le bleu par défaut et en donnant une autre couleur à Qualification/Développement. Conservé tel quel lors d'une restauration de base copiée depuis un autre environnement (comme Environnement ci-dessus)."
>
<div style={{ width: 260 }}>
<ThemeSelect
value={form.colorTheme}
onChange={(v) => set('colorTheme', v)}
options={COLOR_THEMES}
/>
</div>
</SettingRow>
</div>
{/* Accès */}
<div className="card" style={{ marginBottom: 24 }}>
<h3 style={{ margin: '0 0 4px', fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>Accès</h3>
@@ -1,7 +1,10 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import { useTheme } from '../../context/ThemeContext.jsx';
import { useUi } from '../../context/UiContext.jsx';
import { api } from '../../api.js';
import { getSuggestedPalettes } from '../../utils/chartPalettes.js';
import { COLOR_THEMES, colorThemeSwatch } from '../../utils/colorThemes.js';
import ThemeSelect from '../../components/ThemeSelect.jsx';
/* ── Apparence — données ─────────────────────────────────────── */
@@ -109,7 +112,8 @@ function isLightColor(hex) {
return (r * 299 + g * 587 + b * 114) / 1000 > 155;
}
/* Retrouve la famille et la teinte d'un hex donné */
/* Retrouve la famille et la teinte d'un hex donné (correspondance EXACTE
* à une nuance MD_PALETTE) */
function findInPalette(hex) {
const h = hex.toLowerCase();
for (const [family, { shades }] of Object.entries(MD_PALETTE)) {
@@ -120,6 +124,51 @@ function findInPalette(hex) {
return null;
}
/* Teinte (0-360) d'un hex, sans dépendance externe */
function hexHue(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
if (max === min) return 0;
const d = max - min;
let h;
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0));
else if (max === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
return h * 60;
}
/* Famille MD_PALETTE la plus proche en teinte pour un hex qui ne
* correspond à AUCUNE nuance exacte (ex. couleur de thème dynamique pour
* "Classique", ou couleurs des 9 autres palettes suggérées, calculées par
* rotation HSL et non alignées sur les nuances MD_PALETTE) — évite
* d'afficher une famille sans rapport avec la couleur réelle (bug
* rencontré le 05/09/26 : famille "Vert" restée affichée, avec son point
* indicateur vert, après avoir choisi la palette "Coucher de soleil"
* (orange), car l'ancien code laissait `selectedFamily` inchangé — donc
* périmée — dès que `findInPalette` ne trouvait aucune correspondance
* exacte). Ne présuppose jamais une correspondance exacte de nuance : le
* point indicateur affiche toujours la vraie couleur (`value`), jamais
* `familyData.base`. */
function findNearestFamily(hex) {
const targetHue = hexHue(hex);
let best = 'Bleu', bestDist = Infinity;
for (const [family, { base }] of Object.entries(MD_PALETTE)) {
// "Gris" est achromatique (R=G=B) : sa teinte n'a mathématiquement pas
// de sens (0 par convention), ce qui le fait à tort "gagner" pour les
// couleurs proches du rouge (teinte elle aussi proche de 0). L'exclure
// du calcul de plus proche teinte.
if (family === 'Gris') continue;
const dist = Math.min(
Math.abs(hexHue(base) - targetHue),
360 - Math.abs(hexHue(base) - targetHue)
);
if (dist < bestDist) { bestDist = dist; best = family; }
}
return best;
}
/* Mélange une couleur hex avec du blanc (factor 0=original, 1=blanc) */
function lightenColor(hex, factor = 0.72) {
const r = parseInt(hex.slice(1,3),16);
@@ -131,21 +180,7 @@ function lightenColor(hex, factor = 0.72) {
return '#' + [lr,lg,lb].map(v => v.toString(16).padStart(2,'0')).join('');
}
/* ── Palettes suggérées ──────────────────────────────────────── */
const SUGGESTED_PALETTES = [
{ name: 'Classique', interets: '#2196f3', capital: '#4caf50', cashback: '#ffc107' },
{ name: 'Indigo & Teal', interets: '#3949ab', capital: '#009688', cashback: '#fb8c00' },
{ name: 'Nuit', interets: '#5c6bc0', capital: '#4dd0e1', cashback: '#ffca28' },
{ name: 'Nature', interets: '#43a047', capital: '#039be5', cashback: '#ff9800' },
{ name: 'Coucher de soleil', interets: '#ff5722', capital: '#1976d2', cashback: '#ffa000' },
{ name: 'Violet', interets: '#7e57c2', capital: '#26a69a', cashback: '#ffb300' },
{ name: 'Frais', interets: '#00acc1', capital: '#7cb342', cashback: '#ec407a' },
{ name: 'Contraste', interets: '#e53935', capital: '#1e88e5', cashback: '#43a047' },
{ name: 'Pastel', interets: '#29b6f6', capital: '#66bb6a', cashback: '#ffb74d' },
{ name: 'Moderne', interets: '#9c27b0', capital: '#00bcd4', cashback: '#ff9800' },
];
function PaletteSelector({ interets, capital, cashback, onSelect }) {
function PaletteSelector({ interets, capital, cashback, onSelect, palettes }) {
const isActive = (p) =>
p.interets === interets && p.capital === capital && p.cashback === cashback;
@@ -155,7 +190,7 @@ function PaletteSelector({ interets, capital, cashback, onSelect }) {
Palettes suggérées
</p>
<div className="palette-grid">
{SUGGESTED_PALETTES.map((p) => (
{palettes.map((p) => (
<button
key={p.name}
type="button"
@@ -178,12 +213,15 @@ function PaletteSelector({ interets, capital, cashback, onSelect }) {
function ChartColorPicker({ value, onChange }) {
const found = findInPalette(value);
const [selectedFamily, setSelectedFamily] = useState(found?.family || 'Bleu');
const [selectedFamily, setSelectedFamily] = useState(found?.family || findNearestFamily(value));
// Sync la famille quand value change depuis l'extérieur (sélection palette)
// Sync la famille quand value change depuis l'extérieur (sélection
// palette) : toujours retomber sur la famille la plus proche en teinte
// à défaut de correspondance exacte, jamais laisser `selectedFamily`
// périmé (cf. commentaire de findNearestFamily ci-dessus).
useEffect(() => {
const f = findInPalette(value);
if (f) setSelectedFamily(f.family);
setSelectedFamily(f ? f.family : findNearestFamily(value));
}, [value]);
const familyData = MD_PALETTE[selectedFamily];
@@ -203,7 +241,7 @@ function ChartColorPicker({ value, onChange }) {
return (
<div className="chart-color-picker">
<div className="color-family-select-row">
<span className="color-family-indicator" style={{ backgroundColor: familyData?.base || value }} />
<span className="color-family-indicator" style={{ backgroundColor: value }} />
<select value={selectedFamily} onChange={handleFamilyChange}>
{Object.keys(MD_PALETTE).map(name => (
<option key={name} value={name}>{name}</option>
@@ -242,8 +280,15 @@ function ChartColorPicker({ value, onChange }) {
export default function AppearanceSection() {
const { mode, setMode } = useTheme();
const { fontScale, setFontScale, chartInterets, setChartInterets, chartCapital, setChartCapital, chartCashback, setChartCashback } = useUi();
const { fontScale, setFontScale, chartInterets, setChartInterets, chartCapital, setChartCapital, chartCashback, setChartCashback, colorTheme, adminColorTheme, setColorTheme } = useUi();
const [libIcons, setLibIcons] = useState({});
const suggestedPalettes = useMemo(() => getSuggestedPalettes(), []);
// "Automatique" (value '') suit le thème par défaut de l'administrateur —
// sa pastille reflète donc la couleur réellement appliquée dans ce cas.
const colorThemeOptions = useMemo(() => [
{ value: '', label: 'Automatique (thème par défaut)', swatch: colorThemeSwatch(adminColorTheme) },
...COLOR_THEMES,
], [adminColorTheme]);
useEffect(() => {
api.get('/icons').then(rows => {
@@ -271,6 +316,25 @@ export default function AppearanceSection() {
</button>
))}
</div>
{/* Thème de couleur personnel (06/09/26) : indépendant du défaut admin
(Admin > Général), stocké en base sur ce compte (cf. UiContext.jsx) */}
<div style={{ marginTop: 20, paddingTop: 20, borderTop: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
<div>
<div style={{ fontWeight: 700, fontSize: 'var(--fs-sm)' }}>Couleur de l'application</div>
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginTop: 2, maxWidth: 420 }}>
Teinte des menus et boutons, propre à votre compte et conservée d'un appareil à l'autre.
« Automatique » suit le thème par défaut choisi par l'administrateur.
</div>
</div>
<div style={{ width: 260, flexShrink: 0 }}>
<ThemeSelect
value={colorTheme || ''}
onChange={(v) => setColorTheme(v || null)}
options={colorThemeOptions}
/>
</div>
</div>
</div>
<div className="card">
@@ -310,6 +374,7 @@ export default function AppearanceSection() {
interets={chartInterets}
capital={chartCapital}
cashback={chartCashback}
palettes={suggestedPalettes}
onSelect={(p) => { setChartInterets(p.interets); setChartCapital(p.capital); setChartCashback(p.cashback); }}
/>
<div className="chart-color-row">
+443 -48
View File
@@ -38,8 +38,11 @@
--sidebar-hover: #163080;
--sidebar-border: #2a4a9e;
--sidebar-accent: #4ade80;
--avatar-entreprise-start: #3730a3;
--avatar-entreprise-end: #4338ca;
--primary: #1e40af;
--primary-hover: #1e3a8a;
--primary-rgb: 30,64,175;
--accent: #4ade80;
--accent-hover: #22c55e;
--success: #16a34a;
@@ -78,8 +81,11 @@
--sidebar-hover: #0d1a40;
--sidebar-border: #1e3a6a;
--sidebar-accent: #4ade80;
--primary: #3b82f6;
--primary-hover: #60a5fa;
--avatar-entreprise-start: #3730a3;
--avatar-entreprise-end: #4338ca;
--primary: #3659ca;
--primary-hover: #5b77d3;
--primary-rgb: 54,89,202;
--accent: #4ade80;
--accent-hover: #86efac;
--success: #4ade80;
@@ -103,6 +109,361 @@
--success-bg: #14532d; --success-fg: #bbf7d0;
}
/* ================= Thème d'interface (teinte) ================= */
/* Indépendant du mode clair/sombre (data-theme) ci-dessus : rotation de
* teinte (même saturation/luminosité par jeton) appliquée au bleu par
* défaut, y compris --sidebar-text (labels du menu) et le dégradé
* "entreprise" des avatars (--avatar-entreprise-*, distinct de --primary
* par un écart de teinte fixe, préservé par la rotation dans tous les
* thèmes). "classique" = valeurs par défaut ci-dessus, aucun bloc nécessaire.
* Réglage global (Admin > Général), cf. project_theme_interface.md.
*
* 05/09/26 : chacun des 9 thèmes ci-dessous reprend la teinte de la couleur
* "Intérêts" d'une des 10 palettes de graphiques suggérées dans Settings >
* Apparence (getSuggestedPalettes, cf. utils/chartPalettes.js et
* project_chart_colors.md).
*
* 05/09/26 (suite, même jour) : les couleurs Intérêts d'indigo_teal/nuit/
* frais/pastel ont été retravaillées — elles dérivaient toutes du bleu et
* étaient peu distinguables entre elles (deltas de quelques degrés à
* peine). Les 9 thèmes sont maintenant répartis sur tout le cercle
* chromatique (deltas recalculés depuis l'ancre #1e40af, écart minimal
* ~22°, la plupart ≥30°) : rouge (contraste), orange (coucher_de_soleil),
* vert (nature), vert-teal (indigo_teal), cyan (frais), bleu (classique,
* ancre), indigo (nuit), violet (violet), magenta (moderne), rose
* (pastel). Contraste et nature sont restés inchangés (déjà bien
* distincts) ; coucher_de_soleil/violet/moderne n'ont eu qu'un léger
* réajustement de teinte pour dégager de la place. */
/* 06/09/26 : sur demande d'Olivier, --primary (clair) de chacun des 9 thèmes
* est désormais EXACTEMENT la couleur "Intérêts" de sa palette de graphiques
* jumelle (plus seulement une teinte approchante à même S/L que l'ancre
* #1e40af) — un seul écart HSL (teinte/saturation/luminosité) est calculé
* entre l'ancien --primary calculé et cette couleur cible, puis appliqué
* uniquement à la "famille primaire" du thème (--primary, --primary-hover,
* --sidebar-bg/-hover en clair, --primary/-hover en sombre, --shadow) ; les
* teintes de fond très pâles (--bg, --surface-2, --border, --input-border,
* --text-muted, --sidebar-border, --sidebar-text, --avatar-entreprise-*)
* restent inchangées (l'écart de teinte pur était de toute façon proche de
* 0°, ces teintes étaient déjà correctes) — appliquer le même écart S/L à
* ces tokens quasi blancs/noirs les aurait saturés à #fff/#000 (piège
* rencontré et écarté, cf. project_theme_interface.md). */
[data-theme="light"][data-color-theme="indigo_teal"] {
--bg: #f0fff9;
--surface-2: #e8f8f0;
--border: #c7e8d8;
--input-border: #c7e8d8;
--text-muted: #4a806a;
--sidebar-bg: #1e7452;
--sidebar-hover: #166a49;
--sidebar-border: #2a9e6d;
--sidebar-text: #bfeed7;
--primary: #20976a;
--primary-hover: #1e7452;
--primary-rgb: 32,151,106;
--avatar-entreprise-start: #30a399;
--avatar-entreprise-end: #38cabf;
--shadow: 0 1px 3px rgba(30,116,82,.10), 0 1px 2px rgba(30,116,82,.06);
}
[data-theme="dark"][data-color-theme="indigo_teal"] {
--bg: #061f13;
--surface: #113524;
--surface-2: #0d291c;
--border: #1e6a43;
--input-bg: #0d291c;
--input-border: #1e6a43;
--text-muted: #7ab899;
--sidebar-bg: #061f13;
--sidebar-hover: #0d402b;
--sidebar-border: #1e6a43;
--sidebar-text: #8adcaf;
--primary: #37b384;
--primary-hover: #4fc99b;
--primary-rgb: 55,179,132;
--avatar-entreprise-start: #30a399;
--avatar-entreprise-end: #38cabf;
}
[data-theme="light"][data-color-theme="frais"] {
--bg: #f0fdff;
--surface-2: #e8f7f8;
--border: #c7e5e8;
--input-border: #c7e5e8;
--text-muted: #4a7780;
--sidebar-bg: #057f97;
--sidebar-hover: #00718a;
--sidebar-border: #2a8d9e;
--sidebar-text: #bfeaee;
--primary: #009cc1;
--primary-hover: #057f97;
--primary-rgb: 0,156,193;
--avatar-entreprise-start: #306ba3;
--avatar-entreprise-end: #3881ca;
--shadow: 0 1px 3px rgba(5,127,151,.10), 0 1px 2px rgba(5,127,151,.06);
}
[data-theme="dark"][data-color-theme="frais"] {
--bg: #061c1f;
--surface: #113035;
--surface-2: #0d2629;
--border: #1e666a;
--input-bg: #0d2629;
--input-border: #1e666a;
--text-muted: #7ab4b8;
--sidebar-bg: #061c1f;
--sidebar-hover: #0d3740;
--sidebar-border: #1e666a;
--sidebar-text: #8adadc;
--primary: #1bb5d9;
--primary-hover: #3bc6e7;
--primary-rgb: 27,181,217;
--avatar-entreprise-start: #306ba3;
--avatar-entreprise-end: #3881ca;
}
[data-theme="light"][data-color-theme="nuit"] {
--bg: #f1f0ff;
--surface-2: #e8e8f8;
--border: #c8c7e8;
--input-border: #c8c7e8;
--text-muted: #504a80;
--sidebar-bg: #3f368a;
--sidebar-hover: #362b83;
--sidebar-border: #342a9e;
--sidebar-text: #c0bfee;
--primary: #4839ac;
--primary-hover: #3f368a;
--primary-rgb: 72,57,172;
--avatar-entreprise-start: #6130a3;
--avatar-entreprise-end: #7838ca;
--shadow: 0 1px 3px rgba(63,54,138,.10), 0 1px 2px rgba(63,54,138,.06);
}
[data-theme="dark"][data-color-theme="nuit"] {
--bg: #07061f;
--surface: #131135;
--surface-2: #0e0d29;
--border: #1e1e6a;
--input-bg: #0e0d29;
--input-border: #1e1e6a;
--text-muted: #7b7ab8;
--sidebar-bg: #07061f;
--sidebar-hover: #130d40;
--sidebar-border: #1e1e6a;
--sidebar-text: #8a8ddc;
--primary: #695dbb;
--primary-hover: #877dc9;
--primary-rgb: 105,93,187;
--avatar-entreprise-start: #6130a3;
--avatar-entreprise-end: #7838ca;
}
[data-theme="light"][data-color-theme="nature"] {
--bg: #f0fff0;
--surface-2: #eaf8e8;
--border: #c9e8c7;
--input-border: #c9e8c7;
--text-muted: #4a804b;
--sidebar-bg: #3e8040;
--sidebar-hover: #337935;
--sidebar-border: #2a9e2a;
--sidebar-text: #c2eebf;
--primary: #43a047;
--primary-hover: #3e8040;
--primary-rgb: 67,160,71;
--avatar-entreprise-start: #30a357;
--avatar-entreprise-end: #38ca6c;
--shadow: 0 1px 3px rgba(62,128,64,.10), 0 1px 2px rgba(62,128,64,.06);
}
[data-theme="dark"][data-color-theme="nature"] {
--bg: #071f06;
--surface: #123511;
--surface-2: #0e290d;
--border: #256a1e;
--input-bg: #0e290d;
--input-border: #256a1e;
--text-muted: #7fb87a;
--sidebar-bg: #071f06;
--sidebar-hover: #0d400e;
--sidebar-border: #256a1e;
--sidebar-text: #94dc8a;
--primary: #64b267;
--primary-hover: #83c185;
--primary-rgb: 100,178,103;
--avatar-entreprise-start: #30a357;
--avatar-entreprise-end: #38ca6c;
}
[data-theme="light"][data-color-theme="coucher_de_soleil"] {
--bg: #fff9f0;
--surface-2: #f8efe8;
--border: #e8d8c7;
--input-border: #e8d8c7;
--text-muted: #80694a;
--sidebar-bg: #f49008;
--sidebar-hover: #ea8900;
--sidebar-border: #9e6b2a;
--sidebar-text: #eed6bf;
--primary: #ffa722;
--primary-hover: #f49008;
--primary-rgb: 255,167,34;
--avatar-entreprise-start: #a39730;
--avatar-entreprise-end: #cabd38;
--shadow: 0 1px 3px rgba(244,144,8,.10), 0 1px 2px rgba(244,144,8,.06);
}
[data-theme="dark"][data-color-theme="coucher_de_soleil"] {
--bg: #1f1306;
--surface: #352411;
--surface-2: #291b0d;
--border: #6a411e;
--input-bg: #291b0d;
--input-border: #6a411e;
--text-muted: #b8987a;
--sidebar-bg: #1f1306;
--sidebar-hover: #402b0d;
--sidebar-border: #6a411e;
--sidebar-text: #dcad8a;
--primary: #ecb665;
--primary-hover: #f0c789;
--primary-rgb: 236,182,101;
--avatar-entreprise-start: #a39730;
--avatar-entreprise-end: #cabd38;
}
[data-theme="light"][data-color-theme="violet"] {
--bg: #f7f0ff;
--surface-2: #eee8f8;
--border: #d4c7e8;
--input-border: #d4c7e8;
--text-muted: #644a80;
--sidebar-bg: #7749ab;
--sidebar-hover: #703ca6;
--sidebar-border: #5f2a9e;
--sidebar-text: #d2bfee;
--primary: #8d57c2;
--primary-hover: #7749ab;
--primary-rgb: 141,87,194;
--avatar-entreprise-start: #8b30a3;
--avatar-entreprise-end: #ae38ca;
--shadow: 0 1px 3px rgba(119,73,171,.10), 0 1px 2px rgba(119,73,171,.06);
}
[data-theme="dark"][data-color-theme="violet"] {
--bg: #10061f;
--surface: #201135;
--surface-2: #190d29;
--border: #3a1e6a;
--input-bg: #190d29;
--input-border: #3a1e6a;
--text-muted: #927ab8;
--sidebar-bg: #10061f;
--sidebar-hover: #250d40;
--sidebar-border: #3a1e6a;
--sidebar-text: #a58adc;
--primary: #a684c8;
--primary-hover: #bda3d6;
--primary-rgb: 166,132,200;
--avatar-entreprise-start: #8b30a3;
--avatar-entreprise-end: #ae38ca;
}
[data-theme="light"][data-color-theme="contraste"] {
--bg: #fff0f0;
--surface-2: #f8e8ea;
--border: #e8c7ca;
--input-border: #e8c7ca;
--text-muted: #804a4a;
--sidebar-bg: #d12424;
--sidebar-hover: #c91c1a;
--sidebar-border: #9e2a2c;
--sidebar-text: #eebfc3;
--primary: #e53935;
--primary-hover: #d12424;
--primary-rgb: 229,57,53;
--avatar-entreprise-start: #a35530;
--avatar-entreprise-end: #ca6938;
--shadow: 0 1px 3px rgba(209,36,36,.10), 0 1px 2px rgba(209,36,36,.06);
}
[data-theme="dark"][data-color-theme="contraste"] {
--bg: #1f0608;
--surface: #351113;
--surface-2: #290d0f;
--border: #6a1e26;
--input-bg: #290d0f;
--input-border: #6a1e26;
--text-muted: #b87a80;
--sidebar-bg: #1f0608;
--sidebar-hover: #400d0d;
--sidebar-border: #6a1e26;
--sidebar-text: #dc8a96;
--primary: #df716e;
--primary-hover: #e79593;
--primary-rgb: 223,113,110;
--avatar-entreprise-start: #a35530;
--avatar-entreprise-end: #ca6938;
}
[data-theme="light"][data-color-theme="moderne"] {
--bg: #fff0ff;
--surface-2: #f6e8f8;
--border: #e5c7e8;
--input-border: #e5c7e8;
--text-muted: #7f4a80;
--sidebar-bg: #89268c;
--sidebar-hover: #821d83;
--sidebar-border: #992a9e;
--sidebar-text: #e9bfee;
--primary: #b027b0;
--primary-hover: #89268c;
--primary-rgb: 176,39,176;
--avatar-entreprise-start: #a33081;
--avatar-entreprise-end: #ca389d;
--shadow: 0 1px 3px rgba(137,38,140,.10), 0 1px 2px rgba(137,38,140,.06);
}
[data-theme="dark"][data-color-theme="moderne"] {
--bg: #1d061f;
--surface: #331135;
--surface-2: #270d29;
--border: #601e6a;
--input-bg: #270d29;
--input-border: #601e6a;
--text-muted: #b17ab8;
--sidebar-bg: #1d061f;
--sidebar-hover: #3f0d40;
--sidebar-border: #601e6a;
--sidebar-text: #ce8adc;
--primary: #c545c5;
--primary-hover: #d068d0;
--primary-rgb: 197,69,197;
--avatar-entreprise-start: #a33081;
--avatar-entreprise-end: #ca389d;
}
[data-theme="light"][data-color-theme="pastel"] {
--bg: #fff0f8;
--surface-2: #f8e8f2;
--border: #e8c7da;
--input-border: #e8c7da;
--text-muted: #804a66;
--sidebar-bg: #eb2288;
--sidebar-hover: #ef0c7d;
--sidebar-border: #9e2a67;
--sidebar-text: #eebfdb;
--primary: #f53d97;
--primary-hover: #eb2288;
--primary-rgb: 245,61,151;
--avatar-entreprise-start: #a33046;
--avatar-entreprise-end: #ca3852;
--shadow: 0 1px 3px rgba(235,34,136,.10), 0 1px 2px rgba(235,34,136,.06);
}
[data-theme="dark"][data-color-theme="pastel"] {
--bg: #1f0614;
--surface: #351125;
--surface-2: #290d1d;
--border: #6a1e4d;
--input-bg: #290d1d;
--input-border: #6a1e4d;
--text-muted: #b87aa0;
--sidebar-bg: #1f0614;
--sidebar-hover: #400d27;
--sidebar-border: #6a1e4d;
--sidebar-text: #dc8ac0;
--primary: #e868a7;
--primary-hover: #ee8cbc;
--primary-rgb: 232,104,167;
--avatar-entreprise-start: #a33046;
--avatar-entreprise-end: #ca3852;
}
/* ================= Base ================= */
* { box-sizing: border-box; }
html, body, #root { height: 100%; margin: 0; }
@@ -250,10 +611,10 @@ a { color: var(--primary); }
border: none; border-radius: 20px;
font-size: var(--fs-base); font-weight: 600;
cursor: pointer;
box-shadow: 0 2px 8px rgba(59,130,246,.35);
box-shadow: 0 2px 8px rgba(var(--primary-rgb),.35);
transition: background .15s, box-shadow .15s;
}
.btn-add-invest:hover { background: var(--primary-hover); box-shadow: 0 3px 12px rgba(59,130,246,.5); }
.btn-add-invest:hover { background: var(--primary-hover); box-shadow: 0 3px 12px rgba(var(--primary-rgb),.5); }
/* ── Boutons génériques ──────────────────────────────────────── */
.btn {
@@ -373,7 +734,7 @@ a { color: var(--primary); }
border-radius: 20px;
padding: 3px;
gap: 1px;
box-shadow: 0 2px 8px rgba(59,130,246,.35);
box-shadow: 0 2px 8px rgba(var(--primary-rgb),.35);
flex-shrink: 0;
}
.display-toggle-btn {
@@ -420,7 +781,7 @@ a { color: var(--primary); }
/* Sidebar toujours sombre → icônes toujours inversées */
.sidebar-nav .nav-lib-icon {
filter: invert(1);
opacity: 0.85;
opacity: 1;
}
.sidebar-nav a.active .nav-lib-icon {
filter: invert(1);
@@ -431,16 +792,16 @@ a { color: var(--primary); }
/* Thème sombre : fond du toggle plus discret */
[data-theme="dark"] .display-toggle {
background: rgba(59,130,246,.25);
background: rgba(var(--primary-rgb),.25);
box-shadow: 0 2px 8px rgba(0,0,0,.4);
border: 1px solid rgba(59,130,246,.4);
border: 1px solid rgba(var(--primary-rgb),.4);
}
[data-theme="dark"] .display-toggle-btn.active {
background: var(--primary);
color: #fff;
}
[data-theme="dark"] .display-toggle-btn:hover:not(.active) {
background: rgba(59,130,246,.2);
background: rgba(var(--primary-rgb),.2);
color: rgba(255,255,255,.9);
}
@@ -594,7 +955,7 @@ input, select, textarea {
}
input:focus, select:focus, textarea:focus {
outline: none; border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(59,130,246,.2);
box-shadow: 0 0 0 2px rgba(var(--primary-rgb),.2);
}
label {
display: block; font-size: var(--fs-sm); font-weight: 500;
@@ -704,13 +1065,13 @@ tr:hover td { background: var(--surface-2); }
.pref-option:hover {
border-color: var(--primary);
background: var(--surface-hover, var(--surface));
box-shadow: 0 0 0 3px rgba(59,130,246,.12);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb),.12);
}
.pref-option.active {
border-color: var(--primary);
background: rgba(59,130,246,.07);
box-shadow: 0 0 0 3px rgba(59,130,246,.18);
background: rgba(var(--primary-rgb),.07);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb),.18);
}
.pref-option svg,
@@ -745,7 +1106,7 @@ tr:hover td { background: var(--surface-2); }
.import-module-note {
display: flex; align-items: flex-start; gap: 6px;
margin-top: 10px; padding: 8px 12px;
background: rgba(59,130,246,.07); border: 1px solid rgba(59,130,246,.2);
background: rgba(var(--primary-rgb),.07); border: 1px solid rgba(var(--primary-rgb),.2);
border-radius: 6px; font-size: var(--fs-sm); color: var(--text-muted);
line-height: 1.5;
}
@@ -850,7 +1211,7 @@ tr:hover td { background: var(--surface-2); }
.sg-tooltip-value {
font-size: var(--fs-md);
font-weight: 700;
color: #4fa8e8; /* même bleu que la courbe */
color: var(--primary); /* même couleur que la courbe (thème actif) */
letter-spacing: -.01em;
}
@@ -1106,6 +1467,41 @@ tr:hover td { background: var(--surface-2); }
.cat-select-empty { padding: 10px 12px; font-size: var(--fs-xs); color: var(--text-muted); }
.cat-select-err { padding: 4px 12px; font-size: var(--fs-xs); color: var(--danger); }
/* ThemeSelect (Admin > Général > Apparence : sélecteur de thème) */
.theme-select-wrap { position: relative; width: 100%; }
.theme-select-trigger {
display: flex; align-items: center; gap: 10px;
width: 100%; padding: 0 10px;
border: 1px solid var(--input-border); border-radius: 6px;
min-height: 38px; background: var(--input-bg);
font-size: var(--fs-sm); color: var(--text);
cursor: pointer; text-align: left;
transition: border-color .12s;
}
.theme-select-trigger:hover { border-color: var(--primary); }
.theme-select-trigger.open { border-color: var(--primary); }
.theme-select-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.theme-select-swatch {
display: block; flex-shrink: 0; width: 16px; height: 16px; border-radius: 50%;
box-shadow: 0 0 0 1px var(--border);
}
.theme-select-dropdown {
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
background: var(--surface); border: 1px solid var(--border);
border-radius: 8px; box-shadow: var(--shadow); z-index: 40;
max-height: 260px; overflow-y: auto; padding: 4px;
}
.theme-select-item {
display: flex; align-items: center; gap: 10px;
width: 100%; padding: 7px 8px; border-radius: 6px;
border: none; background: none; cursor: pointer; text-align: left;
font-size: var(--fs-sm); color: var(--text);
transition: background .1s;
}
.theme-select-item:hover { background: var(--surface-2); }
.theme-select-item.active { font-weight: 600; }
.theme-select-item-label { flex: 1; }
/* ================= Depots/Retraits Tabs + Layout ================= */
.dr-tabs {
display: flex;
@@ -1137,7 +1533,7 @@ tr:hover td { background: var(--surface-2); }
.dr-row { transition: background .12s; }
.dr-row:hover td { background: var(--surface-2) !important; }
.dr-row-selected td { background: rgba(59,130,246,.08) !important; }
.dr-row-selected td { background: rgba(var(--primary-rgb),.08) !important; }
.dr-detail {
background: var(--surface); border: 1px solid var(--border);
@@ -1215,7 +1611,7 @@ tr:hover td { background: var(--surface-2); }
}
.dr-kpi-clickable:hover {
border-color: var(--border);
box-shadow: 0 0 0 3px rgba(59,130,246,.12);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb),.12);
}
.dr-kpi-active {
border-width: 1.5px !important;
@@ -1232,8 +1628,8 @@ tr:hover td { background: var(--surface-2); }
}
.dr-kpi-active-neutral {
border-color: var(--primary) !important;
box-shadow: 0 0 0 3px rgba(59,130,246,.14) !important;
background: rgba(59,130,246,.05);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb),.14) !important;
background: rgba(var(--primary-rgb),.05);
}
/* Bouton "Effacer les filtres" dans la topbar */
@@ -1370,7 +1766,7 @@ tr:hover td { background: var(--surface-2); }
color: var(--text);
}
.account-nav-item.active {
background: rgba(59,130,246,.12);
background: rgba(var(--primary-rgb),.12);
color: var(--primary);
font-weight: 600;
}
@@ -1620,12 +2016,12 @@ tr:hover td { background: var(--surface-2); }
.palette-card:hover {
border-color: var(--primary);
background: var(--surface-hover, var(--surface));
box-shadow: 0 0 0 3px rgba(59,130,246,.12);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb),.12);
}
.palette-card.active {
border-color: var(--primary);
background: rgba(59,130,246,.07);
box-shadow: 0 0 0 3px rgba(59,130,246,.18);
background: rgba(var(--primary-rgb),.07);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb),.18);
}
.palette-swatches {
display: flex;
@@ -1768,7 +2164,7 @@ tr:hover td { background: var(--surface-2); }
/* En-têtes */
.tip-th-year {
background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%);
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%);
color: #fff;
text-align: center;
font-weight: 700;
@@ -1778,7 +2174,7 @@ tr:hover td { background: var(--surface-2); }
}
.tip-th-total,
.tip-th-avg {
background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%);
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%);
color: #fff;
text-align: right;
font-weight: 700;
@@ -1786,7 +2182,7 @@ tr:hover td { background: var(--surface-2); }
padding: 6px 10px;
}
.tip-th-name {
background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%);
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%);
color: #fff;
text-align: left;
padding: 6px 12px;
@@ -1839,8 +2235,8 @@ tr:hover td { background: var(--surface-2); }
text-align: right;
font-weight: 700;
white-space: nowrap;
background: rgba(109,40,217,.06);
border-left: 1px solid rgba(109,40,217,.15);
background: rgba(var(--primary-rgb), .06);
border-left: 1px solid rgba(var(--primary-rgb), .15);
}
.tip-td-avg {
padding: 7px 10px;
@@ -1848,8 +2244,8 @@ tr:hover td { background: var(--surface-2); }
white-space: nowrap;
color: var(--text-muted);
font-size: var(--fs-xs);
background: rgba(109,40,217,.04);
border-left: 1px solid rgba(109,40,217,.1);
background: rgba(var(--primary-rgb), .04);
border-left: 1px solid rgba(var(--primary-rgb), .1);
}
/* Valeurs projetées */
@@ -1860,7 +2256,7 @@ tr:hover td { background: var(--surface-2); }
.tip-td-clickable { cursor: pointer; }
.tip-row-plat:hover .tip-td-clickable { text-decoration: underline dotted; text-underline-offset: 2px; }
.tip-td-active {
background: rgba(var(--primary-rgb, 99,102,241), 0.14) !important;
background: rgba(var(--primary-rgb), 0.14) !important;
font-weight: 700;
color: var(--primary);
outline: 2px solid var(--primary);
@@ -1872,17 +2268,17 @@ tr:hover td { background: var(--surface-2); }
background: var(--surface-2) !important;
}
.tip-td-closed.tip-col-current {
background: rgba(109,40,217,0.045) !important;
background: rgba(var(--primary-rgb), 0.045) !important;
}
[data-theme="dark"] .tip-td-closed {
background: rgba(255,255,255,0.04) !important;
}
[data-theme="dark"] .tip-td-closed.tip-col-current {
background: rgba(109,40,217,0.08) !important;
background: rgba(var(--primary-rgb), 0.08) !important;
}
/* Cellule cliquable (lien vers mouvements) */
.tip-td-link:hover { background: var(--primary-light, rgba(109,40,217,.1)) !important; color: var(--primary); }
.tip-td-link:hover { background: rgba(var(--primary-rgb), .1) !important; color: var(--primary); }
/* Lignes corps */
.tip-row-plat { border-bottom: 1px solid var(--border); }
@@ -1915,20 +2311,19 @@ tr:hover td { background: var(--surface-2); }
border-right: 1px solid var(--border);
}
.tip-footer-total {
background: rgba(109,40,217,.08);
background: rgba(var(--primary-rgb), .08);
font-weight: 700;
}
.tip-footer-total td { border-top: 2px solid rgba(109,40,217,.3); }
.tip-footer-total td { border-top: 2px solid rgba(var(--primary-rgb), .3); }
.tip-footer-capital { color: var(--text-muted); font-style: italic; }
.tip-footer-capital td { border-top: 1px solid var(--border); }
.tip-footer-perf td { border-top: 1px solid var(--border); }
.tip-footer-total td.tip-td-total { background: rgba(109,40,217,.12); border-left: 1px solid rgba(109,40,217,.2); }
.tip-footer-total td.tip-td-total { background: rgba(var(--primary-rgb), .12); border-left: 1px solid rgba(var(--primary-rgb), .2); }
/* Dark mode ajustements */
[data-theme="dark"] .tip-footer-total { background: rgba(217,119,6,.12); }
[data-theme="dark"] .tip-td-total { background: rgba(217,119,6,.08); }
[data-theme="dark"] .tip-td-avg { background: rgba(217,119,6,.04); border-left-color: rgba(217,119,6,.15); }
[data-theme="dark"] .tip-th-name { background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%); }
/* Dark mode : plus d'overrides nécessaires ici — --primary-rgb et
--primary/--primary-hover sont déjà redéfinis par [data-theme="dark"]
(et par chaque thème de couleur), donc les règles de base ci-dessus
résolvent automatiquement la bonne teinte dans les deux modes. */
/* Colonne mois courant */
.tip-th-month-current {
@@ -1941,25 +2336,25 @@ tr:hover td { background: var(--surface-2); }
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
background: linear-gradient(90deg, #7c3aed, #4f46e5);
background: linear-gradient(90deg, var(--primary), var(--primary-hover));
border-radius: 0 0 2px 2px;
}
.tip-col-current {
background: rgba(109,40,217,0.045) !important;
background: rgba(var(--primary-rgb), 0.045) !important;
font-weight: 600;
}
.tip-footer-total .tip-col-current {
background: rgba(109,40,217,0.13) !important;
background: rgba(var(--primary-rgb), 0.13) !important;
}
.tip-footer-capital .tip-col-current,
.tip-footer-perf .tip-col-current {
background: rgba(109,40,217,0.045) !important;
background: rgba(var(--primary-rgb), 0.045) !important;
}
[data-theme="dark"] .tip-col-current {
background: rgba(109,40,217,0.08) !important;
background: rgba(var(--primary-rgb), 0.08) !important;
}
[data-theme="dark"] .tip-footer-total .tip-col-current {
background: rgba(109,40,217,0.18) !important;
background: rgba(var(--primary-rgb), 0.18) !important;
}
.tip-th-empty {
+35
View File
@@ -0,0 +1,35 @@
/**
* chartPalettes — les 10 palettes suggérées pour les couleurs de graphiques
* (Settings > Apparence), partagées avec :
* - AppearanceSection.jsx (grille de sélection + personnalisation)
* - GeneralSection.jsx (adaptation automatique quand l'admin change le
* thème d'interface global, cf. project_theme_interface.md)
*
* 06/09/26 : "Classique" a une couleur "Intérêts" FIXE (#1e40af, la couleur
* --primary par défaut du thème "Classique" en Admin > Général), comme les
* 9 autres palettes — ce n'était pas le cas avant (elle suivait en direct
* --primary du thème d'interface actif, quel qu'il soit) : redevenu inutile
* maintenant que chaque thème admin a sa propre palette jumelle qui
* s'active automatiquement à sa sélection (cf. project_theme_interface.md
* V4/V6) — Classique n'a plus besoin de "suivre" le thème actif, elle est
* elle-même l'un des 10 choix fixes. Les 9 autres couleurs "Intérêts" sont
* fixes depuis le 05/09/26, choisies pour bien se distinguer les unes des
* autres sur tout le cercle chromatique (les anciennes valeurs de
* indigo_teal/nuit/frais/pastel dérivaient toutes du bleu et étaient peu
* distinguables entre elles) — cf. project_chart_colors.md. Les couleurs
* Capital/Cashback de chaque palette restent inchangées.
*/
export function getSuggestedPalettes() {
return [
{ name: 'Classique', interets: '#1e40af', capital: '#4caf50', cashback: '#ffc107' },
{ name: 'Indigo & Teal', interets: '#20976a', capital: '#009688', cashback: '#fb8c00' },
{ name: 'Nuit', interets: '#4839ac', capital: '#4dd0e1', cashback: '#ffca28' },
{ name: 'Nature', interets: '#43a047', capital: '#039be5', cashback: '#ff9800' },
{ name: 'Coucher de soleil', interets: '#ffa722', capital: '#1976d2', cashback: '#e91e63' },
{ name: 'Violet', interets: '#8d57c2', capital: '#26a69a', cashback: '#ffb300' },
{ name: 'Frais', interets: '#009cc1', capital: '#7cb342', cashback: '#ec407a' },
{ name: 'Contraste', interets: '#e53935', capital: '#1e88e5', cashback: '#43a047' },
{ name: 'Pastel', interets: '#f53d97', capital: '#66bb6a', cashback: '#ffb74d' },
{ name: 'Moderne', interets: '#b027b0', capital: '#00bcd4', cashback: '#ff9800' },
];
}
+53
View File
@@ -0,0 +1,53 @@
/**
* colorThemes.js — liste partagée des 10 thèmes de couleur d'interface
* (Admin > Général, cf. project_theme_interface.md), et de leur association
* avec la palette de graphiques correspondante (Settings > Apparence, cf.
* chartPalettes.js).
*
* Utilisé par GeneralSection.jsx (thème par défaut de l'application,
* Admin > Général) ET AppearanceSection.jsx (thème personnel de
* l'utilisateur, Settings > Apparence, 06/09/26) — auparavant dupliqué
* uniquement côté admin ; extrait ici pour que les deux sélecteurs restent
* strictement synchronisés (mêmes valeurs, mêmes libellés, même swatch).
*/
// 05/09/26 (suite, même jour) : les couleurs "Intérêts" des palettes
// indigo_teal/nuit/frais/pastel ont été retravaillées (trop proches du bleu
// classique et peu distinguables entre elles) pour une meilleure diversité
// sur tout le cercle chromatique — cf. utils/chartPalettes.js et
// project_theme_interface.md. Le swatch de chaque thème reprend exactement
// la couleur --primary (mode clair) de ce thème dans styles.css.
export const COLOR_THEMES = [
{ value: 'classique', label: 'Classique (défaut)', swatch: '#1e40af' },
{ value: 'indigo_teal', label: 'Indigo & Teal', swatch: '#20976a' },
{ value: 'frais', label: 'Frais', swatch: '#009cc1' },
{ value: 'nuit', label: 'Nuit', swatch: '#4839ac' },
{ value: 'nature', label: 'Nature', swatch: '#43a047' },
{ value: 'coucher_de_soleil', label: 'Coucher de soleil', swatch: '#ffa722' },
{ value: 'violet', label: 'Violet', swatch: '#8d57c2' },
{ value: 'contraste', label: 'Contraste', swatch: '#e53935' },
{ value: 'moderne', label: 'Moderne', swatch: '#b027b0' },
{ value: 'pastel', label: 'Pastel', swatch: '#f53d97' },
];
// Nom de la palette (Settings > Apparence, cf. utils/chartPalettes.js)
// correspondant à chaque thème : sert à adapter automatiquement les
// couleurs de graphiques d'un compte quand ce thème est choisi (admin par
// défaut, ou personnel — cf. reverse-coupling dans GeneralSection.jsx et
// AppearanceSection.jsx).
export const THEME_TO_PALETTE_NAME = {
classique: 'Classique',
indigo_teal: 'Indigo & Teal',
nuit: 'Nuit',
nature: 'Nature',
coucher_de_soleil: 'Coucher de soleil',
violet: 'Violet',
frais: 'Frais',
contraste: 'Contraste',
pastel: 'Pastel',
moderne: 'Moderne',
};
export function colorThemeSwatch(themeValue) {
return COLOR_THEMES.find(t => t.value === themeValue)?.swatch || COLOR_THEMES[0].swatch;
}