diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..17f55e5 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,226 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* ─── Base ─────────────────────────────────────────────────────────────────── */ + +:root { + --color-positive: #059669; + --color-negative: #dc2626; + --color-interest: #f59e0b; + --color-capital: #2563eb; + --color-insurance: #8b5cf6; + --color-charges: #6b7280; + --color-rent: #ef4444; + --color-projected: #2563eb; +} + +html { + scroll-behavior: smooth; +} + +body { + @apply bg-slate-50 text-slate-900 antialiased; + font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; +} + +.dark body, +html.dark body { + @apply bg-slate-950 text-slate-100; +} + +/* ─── Form Elements ────────────────────────────────────────────────────────── */ + +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type='number'] { + -moz-appearance: textfield; +} + +/* ─── Cards ────────────────────────────────────────────────────────────────── */ + +@layer components { + .card { + @apply bg-white rounded-xl border border-slate-200 shadow-sm + dark:bg-slate-900 dark:border-slate-800; + } + + .card-header { + @apply flex items-center justify-between px-5 py-4 cursor-pointer select-none + border-b border-slate-100 hover:bg-slate-50/50 transition-colors + dark:border-slate-800 dark:hover:bg-slate-800/50; + } + + .card-body { + @apply px-5 py-4; + } + + /* Form field */ + .form-field { + @apply flex flex-col gap-1; + } + + .form-label { + @apply text-xs font-medium text-slate-500 uppercase tracking-wide + dark:text-slate-400; + } + + .form-input { + @apply block w-full rounded-lg border-slate-300 text-sm + focus:border-blue-500 focus:ring-blue-500 + placeholder:text-slate-400 transition-colors + dark:bg-slate-800 dark:border-slate-700 dark:text-slate-100 + dark:placeholder:text-slate-500 dark:focus:border-blue-400 dark:focus:ring-blue-400; + } + + .form-input-sm { + @apply form-input py-1.5 px-2.5 text-sm; + } + + /* Metric cards */ + .metric-card { + @apply bg-white rounded-xl border border-slate-200 p-4 shadow-sm + dark:bg-slate-900 dark:border-slate-800; + } + + .metric-value { + @apply text-2xl font-bold text-slate-900 tabular-nums + dark:text-slate-100; + } + + .metric-label { + @apply text-xs font-medium text-slate-500 mt-1 + dark:text-slate-400; + } + + /* Table */ + .data-table { + @apply w-full text-sm; + } + + .data-table thead th { + @apply px-3 py-2.5 text-left text-xs font-semibold text-slate-500 + uppercase tracking-wider bg-slate-50 border-b border-slate-200 + whitespace-nowrap + dark:bg-slate-800/50 dark:text-slate-400 dark:border-slate-700; + } + + .data-table tbody td { + @apply px-3 py-2 tabular-nums border-b border-slate-100 + dark:border-slate-800; + } + + .data-table tbody tr:hover { + @apply bg-blue-50/40 dark:bg-blue-950/30; + } + + /* Tab styles */ + .tab-btn { + @apply px-4 py-2.5 text-sm font-medium rounded-lg transition-all + text-slate-500 hover:text-slate-700 hover:bg-slate-100 + dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800; + } + + .tab-btn-active { + @apply bg-white text-blue-700 shadow-sm border border-slate-200 + dark:bg-slate-800 dark:text-blue-400 dark:border-slate-700; + } + + /* Badge */ + .badge { + @apply inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full; + } + + .badge-blue { + @apply badge bg-blue-50 text-blue-700 + dark:bg-blue-950/50 dark:text-blue-400; + } + + .badge-green { + @apply badge bg-emerald-50 text-emerald-700 + dark:bg-emerald-950/50 dark:text-emerald-400; + } + + .badge-amber { + @apply badge bg-amber-50 text-amber-700 + dark:bg-amber-950/50 dark:text-amber-400; + } + + .badge-red { + @apply badge bg-red-50 text-red-700 + dark:bg-red-950/50 dark:text-red-400; + } +} + +/* ─── Animations ───────────────────────────────────────────────────────────── */ + +@layer utilities { + .animate-collapsible-open { + animation: collapsible-open 0.25s ease-out; + } + + .animate-collapsible-close { + animation: collapsible-close 0.2s ease-in forwards; + } +} + +@keyframes collapsible-open { + from { + opacity: 0; + max-height: 0; + } + to { + opacity: 1; + max-height: 1000px; + } +} + +@keyframes collapsible-close { + from { + opacity: 1; + max-height: 1000px; + } + to { + opacity: 0; + max-height: 0; + } +} + +/* ─── Print ────────────────────────────────────────────────────────────────── */ + +@media print { + .no-print { + display: none !important; + } + + body { + background: white; + color: black; + } + + html.dark body { + background: white; + color: black; + } + + .card { + break-inside: avoid; + box-shadow: none; + border: 1px solid #e2e8f0; + background: white; + } +} + +/* ─── Dark mode utility overrides ──────────────────────────────────────────── */ + +html.dark select { + @apply bg-slate-800 border-slate-700 text-slate-100; +} + +html.dark input[type='checkbox'] { + @apply bg-slate-800 border-slate-600; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..998c42c --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,43 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import './globals.css'; + +const inter = Inter({ + subsets: ['latin'], + display: 'swap', + variable: '--font-inter', +}); + +export const metadata: Metadata = { + title: 'SimO — Simulateur de Prêt Immobilier', + description: + 'Simulez votre prêt immobilier gratuitement : PTZ, Action Logement, Primo Boost, amortissement détaillé, comparaison loyer/crédit et projection de revente.', + keywords: 'simulateur, prêt immobilier, PTZ, Action Logement, amortissement, crédit immobilier', + openGraph: { + title: 'SimO — Simulateur de Prêt Immobilier', + description: 'Simulez votre prêt immobilier gratuitement avec tous les dispositifs aidés.', + type: 'website', + }, +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + // Inline script to prevent FOUC (flash of unstyled content) on dark mode + const themeScript = ` + (function() { + try { + var t = localStorage.getItem('simo-theme') || 'system'; + var dark = t === 'dark' || (t === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); + if (dark) document.documentElement.classList.add('dark'); + } catch(e) {} + })(); + `; + + return ( + +
+ + + {children} + + ); +} diff --git a/src/components/ui.tsx b/src/components/ui.tsx new file mode 100644 index 0000000..4316c4d --- /dev/null +++ b/src/components/ui.tsx @@ -0,0 +1,241 @@ +'use client'; + +import React, { useState, type ReactNode } from 'react'; + +// ─── Collapsible Section ──────────────────────────────────────────────────── + +interface CollapsibleSectionProps { + title: string; + icon?: ReactNode; + defaultOpen?: boolean; + badge?: ReactNode; + children: ReactNode; +} + +export function CollapsibleSection({ + title, + icon, + defaultOpen = true, + badge, + children, +}: CollapsibleSectionProps) { + const [open, setOpen] = useState(defaultOpen); + + return ( +{hint}
} +{hint}
} +{title}
+{description}
+ {action &&