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

+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',