fix(ui): stabilize logout and theme boundaries

This commit is contained in:
poyrazavsever
2026-07-17 22:51:55 +03:00
parent 5afb04c580
commit ec824c0eec
4 changed files with 74 additions and 14 deletions
+1 -1
View File
@@ -99,5 +99,5 @@ export async function signOut() {
await callAuthAction<{ success: boolean }>('/sign-out') await callAuthAction<{ success: boolean }>('/sign-out')
revalidatePath('/', 'layout') revalidatePath('/', 'layout')
redirect('/login') return { redirectTo: '/login' } as const
} }
+30 -5
View File
@@ -20,6 +20,7 @@ import {
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
toast,
} from "poyraz-ui/molecules"; } from "poyraz-ui/molecules";
import { import {
SidebarContent, SidebarContent,
@@ -39,7 +40,8 @@ import { ChevronUp, LogOut, Settings } from "lucide-react";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useTransition } from "react";
export type AppShellNavItem = { export type AppShellNavItem = {
title: string; title: string;
@@ -306,6 +308,21 @@ function ProgressSummary({ progress }: { progress: number }) {
} }
function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: string }) { 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 ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -352,17 +369,25 @@ function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: st
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<form action={signOut}>
<DropdownMenuItem <DropdownMenuItem
asChild asChild
disabled={isSigningOut}
className="text-destructive focus:text-destructive data-[highlighted]:text-destructive" className="text-destructive focus:text-destructive data-[highlighted]:text-destructive"
> >
<Button effect="shine" type="submit" variant="secondary" size="sm" className="w-full justify-start gap-2 text-left text-destructive"> <Button
effect="shine"
type="button"
variant="secondary"
size="sm"
loading={isSigningOut}
aria-busy={isSigningOut}
onClick={handleSignOut}
className="w-full justify-start gap-2 text-left text-destructive"
>
<LogOut className="h-4 w-4 shrink-0" aria-hidden="true" /> <LogOut className="h-4 w-4 shrink-0" aria-hidden="true" />
<span>Çıkış yap</span> <span>{isSigningOut ? "Çıkış yapılıyor" : "Çıkış yap"}</span>
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
</form>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );
@@ -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: 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-primary` ve foreground/hover/active/scale rolleri
- `--poyraz-accent` ve foreground/hover rolleri
- `--poyraz-ring` ve `--poyraz-focus-ring` - `--poyraz-ring` ve `--poyraz-focus-ring`
- `--poyraz-radius-xs``--poyraz-radius-xl` - `--poyraz-radius-xs``--poyraz-radius-xl`
Değerler root `<html style>` 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 `<html style>` 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ı. Recharts içindeki eski `hsl(var(--...))` ifadeleri geçerli Poyraz CSS renk tokenlarına taşındı.
+36
View File
@@ -7,7 +7,12 @@ const repoRoot = process.cwd();
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")); const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"));
const globalsCss = fs.readFileSync(path.join(repoRoot, "app/globals.css"), "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 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 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( const settingsPage = fs.readFileSync(
path.join(repoRoot, "app/(dashboard)/settings/page.tsx"), path.join(repoRoot, "app/(dashboard)/settings/page.tsx"),
"utf8", "utf8",
@@ -63,6 +68,26 @@ assert.doesNotMatch(
/<DropdownMenuItem[^>]*\binteractiveMotion=/s, /<DropdownMenuItem[^>]*\binteractiveMotion=/s,
"Poyraz UI 3.0.2 leaks DropdownMenuItem interactiveMotion to the DOM", "Poyraz UI 3.0.2 leaks DropdownMenuItem interactiveMotion to the DOM",
); );
assert.doesNotMatch(
appShell,
/<form\s+action=\{signOut\}>/,
"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"); assert.match(settingsPage, /\bRadioGroup\b/, "Settings must use the Poyraz RadioGroup for theme selection");
for (const workspaceControl of [ for (const workspaceControl of [
@@ -95,6 +120,17 @@ assert.ok(
rootLayout.includes("COLOR_MODE_COOKIE"), rootLayout.includes("COLOR_MODE_COOKIE"),
"Root layout must resolve the persisted color mode before rendering", "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( assert.ok(
appShell.includes("ColorModeSync"), appShell.includes("ColorModeSync"),
"Authenticated shells must synchronize the database-backed color mode", "Authenticated shells must synchronize the database-backed color mode",