Lot en un seul commit: les deux features partagent les mêmes contrats (domain/settings.rs, DTO, commandes Tauri/HTTP, web-server) et les mêmes surfaces UI — la séparation par fichiers était impossible sans chirurgie de hunks risquant des états intermédiaires non compilables (convention lot des batches #310-#313 et #315). #325 — icône configurable par projet, watermark CLI custom: - domaine: ProjectIconSet (devicon, simple-icons, lucide, idea), ProjectIconRef validé (nom ASCII 1..=64, -_. internes), champ icon optionnel sur ProjectSettings; sérialisation camelCase et try_from strict - application: UpdateProjectSettings.execute accepte un patch theme_color OU icon (absent = inchangé, null = défaut produit), tests in-dependants avec store mémoire - backend/app-tauri/web-server: ProjectIconRefDto, UpdateProjectSettingsRequestDto avec icon optionnel (désérialisation additive), commandes update_project_settings alignées Tauri/HTTP, tests DTO et HTTP (défaut null, set, clear, nom invalide rejeté) - frontend: catalogue projectIcons (recherche EN), ProjectIconPicker en grille multi-lignes dans Paramètres projet, affichage de l'icône dans ProjectTabs et watermark grand format à faible opacité en fond de CLI custom; ports ProjectSettings.icon, Partial<ProjectSettings>, adapters tauri/mock/http et tests alignés #326 — localisation FR/EN et traduction des textes IdeA: - domaine: IdeaLanguage (fr | en), champ language optionnel sur IdeaUserSettings (absence = défaut produit, série additive validée) - backend/web-server: UserSettingsDto.language, update_user_settings accepte language, tests HTTP alignés; fixtures structured_launch_d3 mises à jour - frontend: i18n/ (catalogue FR/EN, I18nProvider, scan anti-régression des sources visibles), GeneralSettings avec sélecteur de langue dans les paramètres, setupI18n dans vitest; migration de l'ensemble des textes hardcodés vers le catalogue sur toutes les surfaces (agents, layout, projets, tickets, sprints, web, plugins, permissions, git, templates, skills, mémoire, model servers, embedder, first-run, devices, scheduled tasks, workstate, dialogs), FR conservé par défaut; recherche d'icônes #325 en anglais conformément au ticket QA verte sur feature/batch-325-326-project-icons-localisation. Aucun push.
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
/**
|
||
* Root component. Smoke-tests the full hexagonal pipeline by calling `health`
|
||
* through the {@link SystemGateway} (never `invoke()` directly) and listening
|
||
* for relayed domain events.
|
||
*/
|
||
|
||
import { useEffect, useState } from "react";
|
||
|
||
import type { DomainEvent, HealthReport } from "@/domain";
|
||
import { ProjectsView } from "@/features/projects";
|
||
import { FirstRunWizard } from "@/features/first-run";
|
||
import { AppExitConfirmDialog } from "@/features/appExit";
|
||
import { PluginRuntimeProvider } from "@/features/plugins";
|
||
import { Panel, Spinner, Toolbar } from "@/shared";
|
||
import { useGateways, shouldUseMock } from "./di";
|
||
import { useI18n } from "@/i18n";
|
||
|
||
export function App() {
|
||
const { tr } = useI18n();
|
||
const { system, profile } = useGateways();
|
||
const [health, setHealth] = useState<HealthReport | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [events, setEvents] = useState<DomainEvent[]>([]);
|
||
// First-run gating: null while loading, then true (wizard) / false (normal).
|
||
const [firstRun, setFirstRun] = useState<boolean | null>(null);
|
||
|
||
useEffect(() => {
|
||
let unsub: (() => void) | undefined;
|
||
let cancelled = false;
|
||
|
||
system
|
||
.onDomainEvent((e) => setEvents((prev) => [...prev, e]))
|
||
.then((u) => {
|
||
if (cancelled) u();
|
||
else unsub = u;
|
||
})
|
||
.catch(() => {
|
||
/* event relay unavailable in this environment; ignore for smoke test */
|
||
});
|
||
|
||
system
|
||
.health("ui-boot")
|
||
.then((report) => setHealth(report))
|
||
.catch((e: unknown) => setError(describeError(e)));
|
||
|
||
return () => {
|
||
cancelled = true;
|
||
unsub?.();
|
||
};
|
||
}, [system]);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
profile
|
||
.firstRunState()
|
||
.then((s) => {
|
||
if (!cancelled) setFirstRun(s.isFirstRun);
|
||
})
|
||
.catch(() => {
|
||
// First-run state unavailable: behave as a normal (non-first) run.
|
||
if (!cancelled) setFirstRun(false);
|
||
});
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [profile]);
|
||
|
||
return (
|
||
<PluginRuntimeProvider>
|
||
<div className="flex h-full flex-col bg-canvas text-content">
|
||
{/* ── Header ── */}
|
||
<header className="flex shrink-0 items-center justify-between border-b border-border px-6 py-3">
|
||
<div className="flex items-baseline gap-2">
|
||
<h1 className="text-lg font-semibold tracking-tight">IdeA</h1>
|
||
<span className="rounded-md bg-raised px-1.5 py-0.5 text-[0.65rem] font-medium uppercase text-muted">
|
||
{shouldUseMock() ? "mock" : "tauri"}
|
||
</span>
|
||
</div>
|
||
<Toolbar justify="end">
|
||
<span className="text-xs text-faint">
|
||
{health ? (
|
||
<>
|
||
{tr("backend", "backend")} <span className="text-success">●</span> v{health.version} ·{" "}
|
||
{events.length} {tr("événements", "events")}
|
||
</>
|
||
) : error ? (
|
||
<span className="text-danger">{tr("backend indisponible", "backend unavailable")}</span>
|
||
) : (
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<Spinner size={12} /> {tr("vérification…", "checking…")}
|
||
</span>
|
||
)}
|
||
</span>
|
||
</Toolbar>
|
||
</header>
|
||
|
||
{/* ── Body ── */}
|
||
<div className="flex flex-1 flex-col overflow-hidden">
|
||
{error && (
|
||
<Panel className="mx-4 mt-4 border-danger/40">
|
||
<p className="text-sm text-danger">{tr("Erreur :", "Error:")} {error}</p>
|
||
</Panel>
|
||
)}
|
||
|
||
{firstRun ? (
|
||
// First launch: the wizard takes over the area, in a scrollable column
|
||
// (it can be taller than the viewport).
|
||
<div className="flex flex-1 justify-center overflow-y-auto p-6">
|
||
<div className="w-full max-w-xl">
|
||
<FirstRunWizard onDone={() => setFirstRun(false)} />
|
||
</div>
|
||
</div>
|
||
) : (
|
||
// ProjectsView owns the full remaining area (its own IDE layout,
|
||
// single menu bar, and the AI Profiles view switch — ticket #16).
|
||
<ProjectsView />
|
||
)}
|
||
</div>
|
||
</div>
|
||
<AppExitConfirmDialog />
|
||
</PluginRuntimeProvider>
|
||
);
|
||
}
|
||
|
||
function describeError(e: unknown): string {
|
||
if (e && typeof e === "object" && "message" in e) {
|
||
return String((e as { message: unknown }).message);
|
||
}
|
||
return String(e);
|
||
}
|