feat: add per-page contextual help via CircleHelp icon

Each page now shows a ? icon next to its title that toggles a collapsible
help card with page-specific tips. Supports EN/FR via i18n, closes on
outside click or X button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Le-King-Fu 2026-02-11 12:17:28 +00:00
parent 0adfa5fe5e
commit 3351601ff5
11 changed files with 250 additions and 23 deletions

View file

@ -0,0 +1,59 @@
import { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { CircleHelp, X } from "lucide-react";
export function PageHelp({ helpKey }: { helpKey: string }) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
// Close on outside click (same pattern as CategoryCombobox)
useEffect(() => {
if (!isOpen) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [isOpen]);
const tips = t(`${helpKey}.help.tips`, { returnObjects: true }) as string[];
return (
<div ref={ref} className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
aria-label={t(`${helpKey}.help.title`)}
>
<CircleHelp size={20} />
</button>
{isOpen && (
<div className="absolute left-0 top-full mt-2 z-40 w-[calc(100vw-var(--sidebar-width,16rem)-6rem)] max-w-3xl bg-[var(--card)] border border-[var(--border)] rounded-xl p-5 shadow-lg">
<div className="flex items-start justify-between gap-4 mb-3">
<h3 className="font-semibold text-[var(--foreground)]">
{t(`${helpKey}.help.title`)}
</h3>
<button
onClick={() => setIsOpen(false)}
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors shrink-0"
>
<X size={18} />
</button>
</div>
<ul className="space-y-1.5 text-sm text-[var(--muted-foreground)]">
{Array.isArray(tips) &&
tips.map((tip, i) => (
<li key={i} className="flex gap-2">
<span className="shrink-0"></span>
<span>{tip}</span>
</li>
))}
</ul>
</div>
)}
</div>
);
}

View file

@ -26,6 +26,15 @@
"6months": "6 months", "6months": "6 months",
"12months": "12 months", "12months": "12 months",
"all": "All" "all": "All"
},
"help": {
"title": "How to use the Dashboard",
"tips": [
"Use the period selector (top right) to view different time ranges",
"Summary cards show your balance, income, and expenses for the selected period",
"The pie chart breaks down your expenses by category",
"Recent transactions are listed at the bottom"
]
} }
}, },
"import": { "import": {
@ -145,6 +154,15 @@
"checkDuplicates": "Check duplicates", "checkDuplicates": "Check duplicates",
"confirm": "Confirm", "confirm": "Confirm",
"import": "Import" "import": "Import"
},
"help": {
"title": "How to import bank statements",
"tips": [
"Set your import folder, then create one subfolder per bank/source with CSV files inside",
"Click a source to configure column mapping, delimiter, and date format",
"Preview your data before importing to catch formatting issues",
"Duplicate detection prevents the same transactions from being imported twice"
]
} }
}, },
"transactions": { "transactions": {
@ -184,7 +202,16 @@
}, },
"autoCategorize": "Auto-categorize", "autoCategorize": "Auto-categorize",
"autoCategorizeResult": "{{count}} transaction(s) categorized", "autoCategorizeResult": "{{count}} transaction(s) categorized",
"autoCategorizeNone": "No new matches found" "autoCategorizeNone": "No new matches found",
"help": {
"title": "How to use Transactions",
"tips": [
"Use the filters to search by description, category, source, or date range",
"Click a column header to sort transactions",
"Assign categories by clicking the category dropdown on each row",
"Auto-categorize uses your keyword rules to categorize transactions in bulk"
]
}
}, },
"categories": { "categories": {
"title": "Categories", "title": "Categories",
@ -207,7 +234,16 @@
"keywordCount": "Keywords", "keywordCount": "Keywords",
"keywordText": "Keyword...", "keywordText": "Keyword...",
"priority": "Priority", "priority": "Priority",
"customColor": "Custom color" "customColor": "Custom color",
"help": {
"title": "How to manage Categories",
"tips": [
"Create top-level categories and subcategories to organize your expenses and income",
"Add keywords to a category so transactions matching those words are auto-categorized",
"Set a priority on keywords to resolve conflicts when multiple categories match",
"Click a category in the tree to view its details, edit it, or manage keywords"
]
}
}, },
"adjustments": { "adjustments": {
"title": "Adjustments", "title": "Adjustments",
@ -215,7 +251,15 @@
"date": "Date", "date": "Date",
"description": "Description", "description": "Description",
"amount": "Amount", "amount": "Amount",
"recurring": "Recurring" "recurring": "Recurring",
"help": {
"title": "How to use Adjustments",
"tips": [
"Adjustments let you add manual entries that don't come from bank imports",
"Use them for expected expenses or income not yet reflected in your statements",
"Recurring adjustments repeat automatically each period"
]
}
}, },
"budget": { "budget": {
"title": "Budget", "title": "Budget",
@ -224,7 +268,15 @@
"planned": "Planned", "planned": "Planned",
"actual": "Actual", "actual": "Actual",
"difference": "Difference", "difference": "Difference",
"template": "Template" "template": "Template",
"help": {
"title": "How to use Budget",
"tips": [
"Set planned amounts for each category to track your spending goals",
"Compare planned vs. actual spending to see where you're over or under budget",
"Use templates to quickly apply the same budget across multiple months"
]
}
}, },
"reports": { "reports": {
"title": "Reports", "title": "Reports",
@ -232,7 +284,16 @@
"byCategory": "Expenses by Category", "byCategory": "Expenses by Category",
"overTime": "Category Over Time", "overTime": "Category Over Time",
"trends": "Monthly Trends", "trends": "Monthly Trends",
"export": "Export" "export": "Export",
"help": {
"title": "How to use Reports",
"tips": [
"Switch between Trends, By Category, and Over Time views using the tabs",
"Use the period selector to adjust the time range for all charts",
"Monthly Trends shows your income and expenses over time",
"Category Over Time tracks how spending in each category evolves"
]
}
}, },
"settings": { "settings": {
"title": "Settings", "title": "Settings",
@ -251,7 +312,15 @@
"error": "Update failed", "error": "Update failed",
"retryButton": "Retry" "retryButton": "Retry"
}, },
"dataSafeNotice": "Your data is safe — only the app binary is replaced, your database is not modified." "dataSafeNotice": "Your data is safe — only the app binary is replaced, your database is not modified.",
"help": {
"title": "About Settings",
"tips": [
"Check for app updates and install them directly from this page",
"Your data is stored locally and is never affected by updates",
"Change the app language using the language selector in the sidebar"
]
}
}, },
"common": { "common": {
"save": "Save", "save": "Save",

View file

@ -26,6 +26,15 @@
"6months": "6 mois", "6months": "6 mois",
"12months": "12 mois", "12months": "12 mois",
"all": "Tout" "all": "Tout"
},
"help": {
"title": "Comment utiliser le tableau de bord",
"tips": [
"Utilisez le sélecteur de période (en haut à droite) pour changer la plage de dates",
"Les cartes résumées affichent votre solde, revenus et dépenses pour la période sélectionnée",
"Le graphique circulaire détaille vos dépenses par catégorie",
"Les transactions récentes sont listées en bas de page"
]
} }
}, },
"import": { "import": {
@ -145,6 +154,15 @@
"checkDuplicates": "Vérifier les doublons", "checkDuplicates": "Vérifier les doublons",
"confirm": "Confirmer", "confirm": "Confirmer",
"import": "Importer" "import": "Importer"
},
"help": {
"title": "Comment importer des relevés bancaires",
"tips": [
"Configurez votre dossier d'import, puis créez un sous-dossier par banque/source avec des fichiers CSV",
"Cliquez sur une source pour configurer le mapping des colonnes, le délimiteur et le format de date",
"Prévisualisez vos données avant l'import pour détecter les problèmes de formatage",
"La détection des doublons empêche d'importer les mêmes transactions deux fois"
]
} }
}, },
"transactions": { "transactions": {
@ -184,7 +202,16 @@
}, },
"autoCategorize": "Auto-catégoriser", "autoCategorize": "Auto-catégoriser",
"autoCategorizeResult": "{{count}} transaction(s) catégorisée(s)", "autoCategorizeResult": "{{count}} transaction(s) catégorisée(s)",
"autoCategorizeNone": "Aucune correspondance trouvée" "autoCategorizeNone": "Aucune correspondance trouvée",
"help": {
"title": "Comment utiliser les Transactions",
"tips": [
"Utilisez les filtres pour rechercher par description, catégorie, source ou plage de dates",
"Cliquez sur un en-tête de colonne pour trier les transactions",
"Assignez une catégorie via le menu déroulant sur chaque ligne",
"L'auto-catégorisation utilise vos règles de mots-clés pour catégoriser en masse"
]
}
}, },
"categories": { "categories": {
"title": "Catégories", "title": "Catégories",
@ -207,7 +234,16 @@
"keywordCount": "Mots-clés", "keywordCount": "Mots-clés",
"keywordText": "Mot-clé...", "keywordText": "Mot-clé...",
"priority": "Priorité", "priority": "Priorité",
"customColor": "Couleur personnalisée" "customColor": "Couleur personnalisée",
"help": {
"title": "Comment gérer les Catégories",
"tips": [
"Créez des catégories et sous-catégories pour organiser vos dépenses et revenus",
"Ajoutez des mots-clés à une catégorie pour que les transactions correspondantes soient auto-catégorisées",
"Définissez une priorité sur les mots-clés pour résoudre les conflits entre catégories",
"Cliquez sur une catégorie dans l'arbre pour voir ses détails, la modifier ou gérer ses mots-clés"
]
}
}, },
"adjustments": { "adjustments": {
"title": "Ajustements", "title": "Ajustements",
@ -215,7 +251,15 @@
"date": "Date", "date": "Date",
"description": "Description", "description": "Description",
"amount": "Montant", "amount": "Montant",
"recurring": "Récurrent" "recurring": "Récurrent",
"help": {
"title": "Comment utiliser les Ajustements",
"tips": [
"Les ajustements permettent d'ajouter des entrées manuelles non issues de vos relevés bancaires",
"Utilisez-les pour des dépenses ou revenus prévus non encore reflétés dans vos relevés",
"Les ajustements récurrents se répètent automatiquement à chaque période"
]
}
}, },
"budget": { "budget": {
"title": "Budget", "title": "Budget",
@ -224,7 +268,15 @@
"planned": "Prévu", "planned": "Prévu",
"actual": "Réel", "actual": "Réel",
"difference": "Écart", "difference": "Écart",
"template": "Modèle" "template": "Modèle",
"help": {
"title": "Comment utiliser le Budget",
"tips": [
"Définissez des montants prévus par catégorie pour suivre vos objectifs de dépenses",
"Comparez le prévu et le réel pour voir où vous dépassez ou êtes en dessous du budget",
"Utilisez les modèles pour appliquer rapidement le même budget sur plusieurs mois"
]
}
}, },
"reports": { "reports": {
"title": "Rapports", "title": "Rapports",
@ -232,7 +284,16 @@
"byCategory": "Dépenses par catégorie", "byCategory": "Dépenses par catégorie",
"overTime": "Catégories dans le temps", "overTime": "Catégories dans le temps",
"trends": "Tendances mensuelles", "trends": "Tendances mensuelles",
"export": "Exporter" "export": "Exporter",
"help": {
"title": "Comment utiliser les Rapports",
"tips": [
"Basculez entre les vues Tendances, Par catégorie et Dans le temps via les onglets",
"Utilisez le sélecteur de période pour ajuster la plage de dates de tous les graphiques",
"Les tendances mensuelles montrent vos revenus et dépenses au fil du temps",
"Catégories dans le temps suit l'évolution des dépenses par catégorie"
]
}
}, },
"settings": { "settings": {
"title": "Paramètres", "title": "Paramètres",
@ -251,7 +312,15 @@
"error": "Erreur lors de la mise à jour", "error": "Erreur lors de la mise à jour",
"retryButton": "Réessayer" "retryButton": "Réessayer"
}, },
"dataSafeNotice": "Vos données sont en sécurité — seul le programme est remplacé, votre base de données n'est pas modifiée." "dataSafeNotice": "Vos données sont en sécurité — seul le programme est remplacé, votre base de données n'est pas modifiée.",
"help": {
"title": "À propos des Paramètres",
"tips": [
"Vérifiez les mises à jour de l'application et installez-les directement depuis cette page",
"Vos données sont stockées localement et ne sont jamais affectées par les mises à jour",
"Changez la langue de l'application via le sélecteur de langue dans la barre latérale"
]
}
}, },
"common": { "common": {
"save": "Enregistrer", "save": "Enregistrer",

View file

@ -1,11 +1,15 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PageHelp } from "../components/shared/PageHelp";
export default function AdjustmentsPage() { export default function AdjustmentsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div> <div>
<h1 className="text-2xl font-bold mb-6">{t("adjustments.title")}</h1> <div className="relative flex items-center gap-3 mb-6">
<h1 className="text-2xl font-bold">{t("adjustments.title")}</h1>
<PageHelp helpKey="adjustments" />
</div>
<div className="bg-[var(--card)] rounded-xl p-8 border border-[var(--border)] text-center text-[var(--muted-foreground)]"> <div className="bg-[var(--card)] rounded-xl p-8 border border-[var(--border)] text-center text-[var(--muted-foreground)]">
<p>{t("common.noResults")}</p> <p>{t("common.noResults")}</p>
</div> </div>

View file

@ -1,11 +1,15 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PageHelp } from "../components/shared/PageHelp";
export default function BudgetPage() { export default function BudgetPage() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div> <div>
<h1 className="text-2xl font-bold mb-6">{t("budget.title")}</h1> <div className="relative flex items-center gap-3 mb-6">
<h1 className="text-2xl font-bold">{t("budget.title")}</h1>
<PageHelp helpKey="budget" />
</div>
<div className="bg-[var(--card)] rounded-xl p-8 border border-[var(--border)] text-center text-[var(--muted-foreground)]"> <div className="bg-[var(--card)] rounded-xl p-8 border border-[var(--border)] text-center text-[var(--muted-foreground)]">
<p>{t("common.noResults")}</p> <p>{t("common.noResults")}</p>
</div> </div>

View file

@ -1,5 +1,6 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { PageHelp } from "../components/shared/PageHelp";
import { useCategories } from "../hooks/useCategories"; import { useCategories } from "../hooks/useCategories";
import CategoryTree from "../components/categories/CategoryTree"; import CategoryTree from "../components/categories/CategoryTree";
import CategoryDetailPanel from "../components/categories/CategoryDetailPanel"; import CategoryDetailPanel from "../components/categories/CategoryDetailPanel";
@ -26,8 +27,11 @@ export default function CategoriesPage() {
return ( return (
<div> <div>
<div className="flex items-center justify-between mb-6"> <div className="relative flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold">{t("categories.title")}</h1> <div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">{t("categories.title")}</h1>
<PageHelp helpKey="categories" />
</div>
<button <button
onClick={startCreating} onClick={startCreating}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[var(--primary)] text-white text-sm font-medium hover:opacity-90" className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[var(--primary)] text-white text-sm font-medium hover:opacity-90"

View file

@ -1,6 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Wallet, TrendingUp, TrendingDown } from "lucide-react"; import { Wallet, TrendingUp, TrendingDown } from "lucide-react";
import { useDashboard } from "../hooks/useDashboard"; import { useDashboard } from "../hooks/useDashboard";
import { PageHelp } from "../components/shared/PageHelp";
import PeriodSelector from "../components/dashboard/PeriodSelector"; import PeriodSelector from "../components/dashboard/PeriodSelector";
import CategoryPieChart from "../components/dashboard/CategoryPieChart"; import CategoryPieChart from "../components/dashboard/CategoryPieChart";
import RecentTransactionsList from "../components/dashboard/RecentTransactionsList"; import RecentTransactionsList from "../components/dashboard/RecentTransactionsList";
@ -43,8 +44,11 @@ export default function DashboardPage() {
return ( return (
<div className={isLoading ? "opacity-50 pointer-events-none" : ""}> <div className={isLoading ? "opacity-50 pointer-events-none" : ""}>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6"> <div className="relative flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<h1 className="text-2xl font-bold">{t("dashboard.title")}</h1> <div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">{t("dashboard.title")}</h1>
<PageHelp helpKey="dashboard" />
</div>
<PeriodSelector value={period} onChange={setPeriod} /> <PeriodSelector value={period} onChange={setPeriod} />
</div> </div>

View file

@ -11,6 +11,7 @@ import ImportReportPanel from "../components/import/ImportReportPanel";
import WizardNavigation from "../components/import/WizardNavigation"; import WizardNavigation from "../components/import/WizardNavigation";
import ImportHistoryPanel from "../components/import/ImportHistoryPanel"; import ImportHistoryPanel from "../components/import/ImportHistoryPanel";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { PageHelp } from "../components/shared/PageHelp";
export default function ImportPage() { export default function ImportPage() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -33,7 +34,10 @@ export default function ImportPage() {
return ( return (
<div> <div>
<h1 className="text-2xl font-bold mb-6">{t("import.title")}</h1> <div className="relative flex items-center gap-3 mb-6">
<h1 className="text-2xl font-bold">{t("import.title")}</h1>
<PageHelp helpKey="import" />
</div>
{/* Error banner */} {/* Error banner */}
{state.error && ( {state.error && (

View file

@ -1,5 +1,6 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useReports } from "../hooks/useReports"; import { useReports } from "../hooks/useReports";
import { PageHelp } from "../components/shared/PageHelp";
import type { ReportTab } from "../shared/types"; import type { ReportTab } from "../shared/types";
import PeriodSelector from "../components/dashboard/PeriodSelector"; import PeriodSelector from "../components/dashboard/PeriodSelector";
import MonthlyTrendsChart from "../components/reports/MonthlyTrendsChart"; import MonthlyTrendsChart from "../components/reports/MonthlyTrendsChart";
@ -14,8 +15,11 @@ export default function ReportsPage() {
return ( return (
<div className={state.isLoading ? "opacity-50 pointer-events-none" : ""}> <div className={state.isLoading ? "opacity-50 pointer-events-none" : ""}>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6"> <div className="relative flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<h1 className="text-2xl font-bold">{t("reports.title")}</h1> <div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">{t("reports.title")}</h1>
<PageHelp helpKey="reports" />
</div>
<PeriodSelector value={state.period} onChange={setPeriod} /> <PeriodSelector value={state.period} onChange={setPeriod} />
</div> </div>

View file

@ -13,6 +13,7 @@ import {
import { getVersion } from "@tauri-apps/api/app"; import { getVersion } from "@tauri-apps/api/app";
import { useUpdater } from "../hooks/useUpdater"; import { useUpdater } from "../hooks/useUpdater";
import { APP_NAME } from "../shared/constants"; import { APP_NAME } from "../shared/constants";
import { PageHelp } from "../components/shared/PageHelp";
export default function SettingsPage() { export default function SettingsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -31,7 +32,10 @@ export default function SettingsPage() {
return ( return (
<div className="p-6 max-w-2xl mx-auto space-y-6"> <div className="p-6 max-w-2xl mx-auto space-y-6">
<h1 className="text-2xl font-bold">{t("settings.title")}</h1> <div className="relative flex items-center gap-3">
<h1 className="text-2xl font-bold">{t("settings.title")}</h1>
<PageHelp helpKey="settings" />
</div>
{/* About card */} {/* About card */}
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-6"> <div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-6">

View file

@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Wand2 } from "lucide-react"; import { Wand2 } from "lucide-react";
import { PageHelp } from "../components/shared/PageHelp";
import { useTransactions } from "../hooks/useTransactions"; import { useTransactions } from "../hooks/useTransactions";
import TransactionFilterBar from "../components/transactions/TransactionFilterBar"; import TransactionFilterBar from "../components/transactions/TransactionFilterBar";
import TransactionSummaryBar from "../components/transactions/TransactionSummaryBar"; import TransactionSummaryBar from "../components/transactions/TransactionSummaryBar";
@ -26,8 +27,9 @@ export default function TransactionsPage() {
return ( return (
<div> <div>
<div className="flex items-center gap-3 mb-6"> <div className="relative flex items-center gap-3 mb-6">
<h1 className="text-2xl font-bold">{t("transactions.title")}</h1> <h1 className="text-2xl font-bold">{t("transactions.title")}</h1>
<PageHelp helpKey="transactions" />
<button <button
onClick={handleAutoCategorize} onClick={handleAutoCategorize}
disabled={state.isAutoCategorizing} disabled={state.isAutoCategorizing}