diff --git a/app/login/actions.ts b/app/login/actions.ts index 78adebe..e9b8cc0 100644 --- a/app/login/actions.ts +++ b/app/login/actions.ts @@ -99,5 +99,5 @@ export async function signOut() { await callAuthAction<{ success: boolean }>('/sign-out') revalidatePath('/', 'layout') - redirect('/login') + return { redirectTo: '/login' } as const } diff --git a/components/layout/app-shell.tsx b/components/layout/app-shell.tsx index 9a68dca..2e5cd23 100644 --- a/components/layout/app-shell.tsx +++ b/components/layout/app-shell.tsx @@ -20,6 +20,7 @@ import { TooltipContent, TooltipProvider, TooltipTrigger, + toast, } from "poyraz-ui/molecules"; import { SidebarContent, @@ -39,7 +40,8 @@ import { ChevronUp, LogOut, Settings } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; +import { useTransition } from "react"; export type AppShellNavItem = { title: string; @@ -306,6 +308,21 @@ function ProgressSummary({ progress }: { progress: number }) { } function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: string }) { + const router = useRouter(); + const [isSigningOut, startSignOutTransition] = useTransition(); + + function handleSignOut() { + startSignOutTransition(async () => { + try { + const result = await signOut(); + router.replace(result.redirectTo); + router.refresh(); + } catch { + toast.error("Çıkış yapılamadı. Lütfen tekrar deneyin."); + } + }); + } + return ( @@ -352,17 +369,25 @@ function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: st -
- + - -
+
); diff --git a/docs/self-hosted-redesign/phase-4-poyraz-ui-foundation.md b/docs/self-hosted-redesign/phase-4-poyraz-ui-foundation.md index 27c1168..7d0445a 100644 --- a/docs/self-hosted-redesign/phase-4-poyraz-ui-foundation.md +++ b/docs/self-hosted-redesign/phase-4-poyraz-ui-foundation.md @@ -102,11 +102,10 @@ Renk tek başına anlam taşımaz; badge her zaman Türkçe durum metni içerir. Branding servisi eski `--primary` benzeri uygulama tokenları yerine Poyraz v3 sözleşmesini server-side üretir: - `--poyraz-primary` ve foreground/hover/active/scale rolleri -- `--poyraz-accent` ve foreground/hover rolleri - `--poyraz-ring` ve `--poyraz-focus-ring` - `--poyraz-radius-xs` … `--poyraz-radius-xl` -Değerler root `` içine SSR edilir; bu nedenle ilk render'da varsayılan renkten instance rengine geçiş parlaması oluşmaz. `system` renk modu ilk paint öncesi media query ile `.dark` sınıfına çevrilir ve işletim sistemi değişikliği dinlenir. Poyraz preset kalan surface, status, elevation, density ve motion rollerinin tek kaynağıdır. +Değerler root `` içine SSR edilir; bu nedenle ilk render'da varsayılan renkten instance rengine geçiş parlaması oluşmaz. `system` renk modu ilk paint öncesi media query ile `.dark` sınıfına çevrilir ve işletim sistemi değişikliği dinlenir. `--poyraz-accent`, secondary, surface, status, elevation, density ve motion gibi semantik etkileşim rolleri inline branding katmanında ezilmez; light/dark karşılıklarının tek kaynağı Poyraz presetidir. Branding API'sindeki türetilmiş `accentColor` alanı mobil ve harici istemciler için korunur, web tema köprüsüne enjekte edilmez. Recharts içindeki eski `hsl(var(--...))` ifadeleri geçerli Poyraz CSS renk tokenlarına taşındı. diff --git a/scripts/phase4-ui-boundary.mjs b/scripts/phase4-ui-boundary.mjs index 01e5a9b..b476bd3 100644 --- a/scripts/phase4-ui-boundary.mjs +++ b/scripts/phase4-ui-boundary.mjs @@ -7,7 +7,12 @@ const repoRoot = process.cwd(); const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")); const globalsCss = fs.readFileSync(path.join(repoRoot, "app/globals.css"), "utf8"); const appShell = fs.readFileSync(path.join(repoRoot, "components/layout/app-shell.tsx"), "utf8"); +const loginActions = fs.readFileSync(path.join(repoRoot, "app/login/actions.ts"), "utf8"); const rootLayout = fs.readFileSync(path.join(repoRoot, "app/layout.tsx"), "utf8"); +const brandingService = fs.readFileSync( + path.join(repoRoot, "server/branding/service.ts"), + "utf8", +); const settingsPage = fs.readFileSync( path.join(repoRoot, "app/(dashboard)/settings/page.tsx"), "utf8", @@ -63,6 +68,26 @@ assert.doesNotMatch( /]*\binteractiveMotion=/s, "Poyraz UI 3.0.2 leaks DropdownMenuItem interactiveMotion to the DOM", ); +assert.doesNotMatch( + appShell, + //, + "Dropdown menu selection must not rely on a cancelable native form submit for logout", +); +for (const logoutMarker of [ + "startSignOutTransition", + "await signOut()", + "router.replace(result.redirectTo)", + "loading={isSigningOut}", +]) { + assert.ok( + appShell.includes(logoutMarker), + `Sidebar logout flow is missing ${logoutMarker}`, + ); +} +assert.ok( + loginActions.includes("return { redirectTo: '/login' } as const"), + "Logout server action must return an explicit client navigation target after clearing cookies", +); assert.match(settingsPage, /\bRadioGroup\b/, "Settings must use the Poyraz RadioGroup for theme selection"); for (const workspaceControl of [ @@ -95,6 +120,17 @@ assert.ok( rootLayout.includes("COLOR_MODE_COOKIE"), "Root layout must resolve the persisted color mode before rendering", ); +for (const semanticAccentToken of [ + "--poyraz-accent", + "--poyraz-accent-foreground", + "--poyraz-accent-hover", +]) { + assert.doesNotMatch( + brandingService, + new RegExp(`"${semanticAccentToken}"\\s*:`), + `Branding must not override Poyraz's light/dark ${semanticAccentToken} token`, + ); +} assert.ok( appShell.includes("ColorModeSync"), "Authenticated shells must synchronize the database-backed color mode",