feat(domain,application,infrastructure,backend,web-server,app-tauri,frontend): lot utilisateur #325 #326 — icônes projet avec watermark CLI custom et localisation FR/EN (QA verte)
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.
This commit is contained in:
@ -17,7 +17,7 @@ describe("DockRegion", () => {
|
||||
</DockRegion>,
|
||||
);
|
||||
expect(screen.getByText("docked body")).toBeTruthy();
|
||||
const handle = screen.getByRole("separator", { name: "resize left dock" });
|
||||
const handle = screen.getByRole("separator", { name: "redimensionner la zone ancrée à gauche" });
|
||||
expect(handle.getAttribute("aria-orientation")).toBe("vertical");
|
||||
});
|
||||
|
||||
@ -28,7 +28,7 @@ describe("DockRegion", () => {
|
||||
<p>body</p>
|
||||
</DockRegion>,
|
||||
);
|
||||
const handle = screen.getByRole("separator", { name: "resize left dock" });
|
||||
const handle = screen.getByRole("separator", { name: "redimensionner la zone ancrée à gauche" });
|
||||
fireEvent.pointerDown(handle, { pointerId: 1, clientX: 100 });
|
||||
fireEvent.pointerMove(handle, { pointerId: 1, clientX: 160 });
|
||||
expect(onResize).toHaveBeenLastCalledWith(360);
|
||||
@ -47,7 +47,7 @@ describe("DockRegion", () => {
|
||||
<p>body</p>
|
||||
</DockRegion>,
|
||||
);
|
||||
const handle = screen.getByRole("separator", { name: "resize right dock" });
|
||||
const handle = screen.getByRole("separator", { name: "redimensionner la zone ancrée à droite" });
|
||||
// Dragging left (negative delta) grows a right dock.
|
||||
fireEvent.pointerDown(handle, { pointerId: 1, clientX: 400 });
|
||||
fireEvent.pointerMove(handle, { pointerId: 1, clientX: 320 });
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
import { useRef, type ReactNode } from "react";
|
||||
|
||||
import { cn } from "../lib/cn";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
||||
/** Which side of `<main>` the column is docked to. */
|
||||
export type DockSide = "left" | "right";
|
||||
@ -59,6 +60,7 @@ export function DockRegion({
|
||||
"aria-label": ariaLabel,
|
||||
children,
|
||||
}: DockRegionProps) {
|
||||
const { tr } = useI18n();
|
||||
// Anchor the drag on pointer-down: remember where the pointer and the column
|
||||
// width started, then map the signed pointer delta to a width delta. A
|
||||
// left-docked column grows as the pointer moves right; a right-docked one
|
||||
@ -94,7 +96,7 @@ export function DockRegion({
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label={`resize ${side} dock`}
|
||||
aria-label={tr(`redimensionner la zone ancrée à ${side === "left" ? "gauche" : "droite"}`, `resize ${side} dock`)}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
@ -105,7 +107,7 @@ export function DockRegion({
|
||||
|
||||
return (
|
||||
<aside
|
||||
aria-label={ariaLabel ?? `${side} dock`}
|
||||
aria-label={ariaLabel ?? tr(`zone ancrée à ${side === "left" ? "gauche" : "droite"}`, `${side} dock`)}
|
||||
className={cn(
|
||||
"flex h-full shrink-0 bg-surface",
|
||||
// Children come first in the DOM; the flex direction places the handle
|
||||
|
||||
@ -73,7 +73,7 @@ describe("FloatingWindow (#16, G1)", () => {
|
||||
);
|
||||
// The first focusable element (the Close button in the header) receives focus.
|
||||
expect(document.activeElement).toBe(
|
||||
screen.getByRole("button", { name: "close Panel" }),
|
||||
screen.getByRole("button", { name: "fermer Panel" }),
|
||||
);
|
||||
});
|
||||
|
||||
@ -83,7 +83,7 @@ describe("FloatingWindow (#16, G1)", () => {
|
||||
<button>only</button>
|
||||
</FloatingWindow>,
|
||||
);
|
||||
const closeBtn = screen.getByRole("button", { name: "close Panel" });
|
||||
const closeBtn = screen.getByRole("button", { name: "fermer Panel" });
|
||||
const onlyBtn = screen.getByRole("button", { name: "only" });
|
||||
|
||||
// Focus the last focusable, press Tab ⇒ wraps to the first (close button).
|
||||
@ -112,7 +112,7 @@ describe("FloatingWindow (#16, G1)", () => {
|
||||
fireEvent.mouseDown(dialog.parentElement as HTMLElement); // backdrop
|
||||
expect(onClose).toHaveBeenCalledTimes(2);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "close Panel" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "fermer Panel" }));
|
||||
expect(onClose).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import { useEffect, useRef, type ReactNode } from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
import { Button } from "./Button";
|
||||
import { zIndex } from "./zIndex";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
||||
/** Width presets; height is capped at 80vh with an internal scroll region. */
|
||||
export type FloatingWindowSize = "sm" | "md" | "lg" | "xl";
|
||||
@ -79,6 +80,7 @@ function FloatingWindowBody({
|
||||
children,
|
||||
}: Omit<FloatingWindowProps, "open">) {
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const { tr } = useI18n();
|
||||
|
||||
// Keep the latest `onClose` in a ref so the mount-only effect below never
|
||||
// re-runs when the caller passes an unstable `onClose` closure (a new
|
||||
@ -168,10 +170,10 @@ function FloatingWindowBody({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={`close ${title}`}
|
||||
aria-label={tr(`fermer ${title}`, `close ${title}`)}
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
{tr("Fermer", "Close")}
|
||||
</Button>
|
||||
</header>
|
||||
<div className="min-h-0 flex-1 overflow-auto p-4">{children}</div>
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
import { cn } from "../lib/cn";
|
||||
import { IconButton } from "./IconButton";
|
||||
import type { ReactNode } from "react";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
||||
export interface TabItem {
|
||||
/** Stable id, returned by `onSelect`/`onClose`. */
|
||||
@ -35,6 +36,7 @@ export interface TabsProps {
|
||||
|
||||
/** A themed tab strip with selection and optional per-tab close. */
|
||||
export function Tabs({ items, value, onSelect, onClose, className }: TabsProps) {
|
||||
const { tr } = useI18n();
|
||||
return (
|
||||
<div
|
||||
role="tablist"
|
||||
@ -76,7 +78,7 @@ export function Tabs({ items, value, onSelect, onClose, className }: TabsProps)
|
||||
{onClose && (
|
||||
<IconButton
|
||||
size="sm"
|
||||
aria-label={`close ${tab.label}`}
|
||||
aria-label={tr(`fermer ${tab.label}`, `close ${tab.label}`)}
|
||||
onClick={() => onClose(tab.id)}
|
||||
className="opacity-60 group-hover:opacity-100"
|
||||
>
|
||||
|
||||
@ -109,10 +109,10 @@ describe("Tabs", () => {
|
||||
const { rerender } = render(
|
||||
<Tabs items={items} value="a" onSelect={() => {}} />,
|
||||
);
|
||||
expect(screen.queryByRole("button", { name: "close alpha" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "fermer alpha" })).toBeNull();
|
||||
|
||||
rerender(<Tabs items={items} value="a" onSelect={() => {}} onClose={onClose} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "close beta" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "fermer beta" }));
|
||||
expect(onClose).toHaveBeenCalledWith("b");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user