feat: add HeatmapCell component and Apple-inspired global design system with dark/light mode support.

This commit is contained in:
Michael committed 2026-02-26 15:54:47 +01:00
1 parent 174b7fa36c
commit 3438d1b35d
3 files changed
+104 -51

No files matched your search

+55 -15
View File
@@ -2,6 +2,42 @@ import { NextResponse } from "next/server";
import { db } from "@/db"; import { db } from "@/db";
import { aiSupplierContext } from "@/db/schema"; import { aiSupplierContext } from "@/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import fs from "fs";
import path from "path";
// File system fallback for environments without PostgreSQL
const DATA_DIR = path.join(process.cwd(), "data");
const FALLBACK_FILE_PATH = path.join(DATA_DIR, "ai-context.json");
function ensureFallbackFileExists() {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
if (!fs.existsSync(FALLBACK_FILE_PATH)) {
fs.writeFileSync(FALLBACK_FILE_PATH, JSON.stringify({}), "utf-8");
}
}
function getFallbackContext(codeFournisseur: string): string {
ensureFallbackFileExists();
try {
const data = JSON.parse(fs.readFileSync(FALLBACK_FILE_PATH, "utf-8"));
return data[codeFournisseur] || "";
} catch {
return "";
}
}
function saveFallbackContext(codeFournisseur: string, contextText: string) {
ensureFallbackFileExists();
try {
const data = JSON.parse(fs.readFileSync(FALLBACK_FILE_PATH, "utf-8"));
data[codeFournisseur] = contextText;
fs.writeFileSync(FALLBACK_FILE_PATH, JSON.stringify(data, null, 2), "utf-8");
} catch (err) {
console.error("Error saving fallback context:", err);
}
}
export async function GET(request: Request) { export async function GET(request: Request) {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
@@ -19,9 +55,8 @@ export async function GET(request: Request) {
return NextResponse.json({ context: result[0]?.context || "" }); return NextResponse.json({ context: result[0]?.context || "" });
} catch (error) { } catch (error) {
console.error("Error fetching AI context:", error); console.warn("DB fetch failed, using fallback JSON for AI context.");
// Fallback en cas d'erreur de BDD ou si la table n'est pas encore créée return NextResponse.json({ context: getFallbackContext(codeFournisseur) });
return NextResponse.json({ context: "" });
} }
} }
@@ -34,23 +69,28 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "Fournisseur manquant" }, { status: 400 }); return NextResponse.json({ error: "Fournisseur manquant" }, { status: 400 });
} }
await db.insert(aiSupplierContext) try {
.values({ await db.insert(aiSupplierContext)
codeFournisseur, .values({
context: context || "", codeFournisseur,
updatedAt: new Date()
})
.onConflictDoUpdate({
target: aiSupplierContext.codeFournisseur,
set: {
context: context || "", context: context || "",
updatedAt: new Date() updatedAt: new Date()
} })
}); .onConflictDoUpdate({
target: aiSupplierContext.codeFournisseur,
set: {
context: context || "",
updatedAt: new Date()
}
});
} catch (dbError) {
console.warn("DB insert failed, saving to fallback JSON.", dbError);
saveFallbackContext(codeFournisseur, context || "");
}
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {
console.error("Error saving AI context:", error); console.error("Error in AI context API:", error);
return NextResponse.json({ error: "Erreur lors de la sauvegarde" }, { status: 500 }); return NextResponse.json({ error: "Erreur lors de la sauvegarde" }, { status: 500 });
} }
} }
+41 -25
View File
@@ -6,17 +6,26 @@
/* ── Dark Mode (default) ── */ /* ── Dark Mode (default) ── */
:root { :root {
/* Apple macOS dark exact values */ /* Apple macOS dark aligned to Slate palette as per UX Spec */
--bg-base: #000000; --bg-base: #020617;
--bg-surface: #111111; /* Slate 950 */
--bg-elevated: #1c1c1e; --bg-surface: #0f172a;
--bg-overlay: rgba(28, 28, 30, 0.85); /* Slate 900 */
--border: rgba(255, 255, 255, 0.08); --bg-elevated: #1e293b;
--border-strong: rgba(255, 255, 255, 0.12); /* Slate 800 */
--bg-overlay: rgba(15, 23, 42, 0.85);
/* Slate 900 with opacity */
--border: #334155;
/* Slate 700 */
--border-strong: #475569;
/* Slate 600 */
--text-primary: #f5f5f7; --text-primary: #e2e8f0;
--text-secondary: #a1a1a6; /* Slate 200 */
--text-muted: #86868b; --text-secondary: #94a3b8;
/* Slate 400 */
--text-muted: #64748b;
/* Slate 500 */
--accent: #2dd4bf; --accent: #2dd4bf;
/* emerald-ish, Apple-tuned */ /* emerald-ish, Apple-tuned */
@@ -127,8 +136,13 @@
} }
/* ── Surfaces & containers ── */ /* ── Surfaces & containers ── */
.surface { background: var(--bg-surface); } .surface {
.elevated { background: var(--bg-elevated); } background: var(--bg-surface);
}
.elevated {
background: var(--bg-elevated);
}
.apple-card { .apple-card {
background: var(--bg-surface); background: var(--bg-surface);
@@ -206,33 +220,35 @@
} }
.heatmap-low { .heatmap-low {
background: rgba(45, 212, 191, 0.08); background: rgba(14, 165, 233, 0.08);
color: rgba(45, 212, 191, 0.70); /* sky-500 */
color: rgba(14, 165, 233, 0.70);
} }
.heatmap-mid { .heatmap-mid {
background: rgba(45, 212, 191, 0.15); background: rgba(14, 165, 233, 0.15);
color: rgba(45, 212, 191, 0.90); color: rgba(14, 165, 233, 0.90);
} }
.heatmap-high { .heatmap-high {
background: rgba(45, 212, 191, 0.25); background: rgba(14, 165, 233, 0.25);
color: #2dd4bf; color: #0ea5e9;
} }
.light .heatmap-low { .light .heatmap-low {
background: rgba(13, 148, 136, 0.08); background: rgba(2, 132, 199, 0.08);
color: #0f766e; /* sky-600 */
color: #0369a1;
} }
.light .heatmap-mid { .light .heatmap-mid {
background: rgba(13, 148, 136, 0.14); background: rgba(2, 132, 199, 0.14);
color: #0d9488; color: #0284c7;
} }
.light .heatmap-high { .light .heatmap-high {
background: rgba(13, 148, 136, 0.22); background: rgba(2, 132, 199, 0.22);
color: #0f766e; color: #0369a1;
} }
/* ── Apple-style input ── */ /* ── Apple-style input ── */
@@ -263,7 +279,7 @@
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 8px 18px; padding: 8px 18px;
background: var(--accent); background: var(--brand-solid);
color: #fff; color: #fff;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
+8 -11
View File
@@ -4,25 +4,22 @@ interface HeatmapCellProps {
value: number | null; value: number | null;
} }
function getHeatmapStyle(value: number | null): { background: string; color: string } { function getHeatmapClass(value: number | null): string {
if (value === null || value === 0) { if (value === null || value === 0) return "heatmap-zero";
return { background: "var(--bg-elevated)", color: "var(--text-muted)" };
}
const abs = Math.abs(value); const abs = Math.abs(value);
if (abs > 100) return { background: "rgba(45,212,191,0.22)", color: "var(--accent)" }; if (abs > 100) return "heatmap-high";
if (abs > 30) return { background: "rgba(45,212,191,0.14)", color: "var(--accent)" }; if (abs > 30) return "heatmap-mid";
if (abs > 10) return { background: "rgba(45,212,191,0.08)", color: "var(--accent)" }; if (abs > 10) return "heatmap-low";
return { background: "var(--bg-elevated)", color: "var(--text-secondary)" }; return "heatmap-zero";
} }
export function HeatmapCell({ value }: HeatmapCellProps) { export function HeatmapCell({ value }: HeatmapCellProps) {
const style = getHeatmapStyle(value); const heatClass = getHeatmapClass(value);
const display = value === null || value === 0 ? "—" : Math.round(Math.abs(value)).toString(); const display = value === null || value === 0 ? "—" : Math.round(Math.abs(value)).toString();
return ( return (
<div <div
className="rounded text-center text-[12px] tabular-nums px-0.5 py-0.5 font-bold tracking-tighter transition-colors min-h-[24px] flex items-center justify-center font-mono-nums" className={`rounded text-center text-[12px] tabular-nums px-0.5 py-0.5 font-bold tracking-tighter transition-colors min-h-[24px] flex items-center justify-center font-mono-nums ${heatClass}`}
style={style}
title={value !== null ? `${Math.abs(value).toLocaleString("fr-FR")} unités` : "Aucune vente"} title={value !== null ? `${Math.abs(value).toLocaleString("fr-FR")} unités` : "Aucune vente"}
> >
{display} {display}