Files
zitadel/apps/login/src/components/language-switcher.tsx
T
Max PeintnerandGitHub 434aeb275a feat(login): comprehensive theme system (#10848)
# Which Problems Are Solved

This PR introduces a comprehensive theme customization system for the
login application with responsive behavior and enhanced visual options.

<img width="1122" height="578" alt="Screenshot 2025-08-19 at 09 55 24"
src="https://github.com/user-attachments/assets/cdcc8948-533d-4e13-bf45-fdcc24acfb2b"
/>

# How the Problems Are Solved

## ✨ Features Added

- **🔄 Responsive Layout System**: Automatic switching between
side-by-side and top-to-bottom layouts based on screen size
- **🖼️ Background Image Support**: Custom background images configurable
via environment variables
- **⚙️ Theme Configuration**: Complete theme system with roundness,
spacing, appearance, and layout options
- **📱 Mobile-First Design**: Intelligent layout adaptation for different
screen sizes
- **🎯 Enhanced Typography**: Improved visual hierarchy with larger
titles in side-by-side mode

## 🏗️ Architecture

- **Server-Safe Theme Functions**: Theme configuration accessible on
both server and client
- **SSR-Safe Hooks**: Proper hydration handling for responsive layouts
- **Component Separation**: Clear boundaries between server and client
components
- **Two-Section Layout**: Consistent content structure across all login
pages

## 🔧 Configuration Options

All theme options are configurable via environment variables:

- `NEXT_PUBLIC_THEME_ROUNDNESS`: `edgy` | `mid` | `full`
- `NEXT_PUBLIC_THEME_LAYOUT`: `side-by-side` | `top-to-bottom`
- `NEXT_PUBLIC_THEME_APPEARANCE`: `flat` | `material`
- `NEXT_PUBLIC_THEME_SPACING`: `regular` | `compact`
- `NEXT_PUBLIC_THEME_BACKGROUND_IMAGE`: Custom background image URL

## 📄 Pages Updated

Updated all major login pages to use the new two-section responsive
layout:
- Login name entry
- Password verification
- MFA verification
- User registration
- Account selection
- Device authorization
- Logout confirmation

## 📚 Documentation

- **THEME_ARCHITECTURE.md**: Complete technical documentation of the
theme system
- **THEME_CUSTOMIZATION.md**: User-friendly guide with examples and
troubleshooting

## 🚀 Benefits

- **Better UX**: Responsive design that works seamlessly across all
devices
- **Brand Flexibility**: Easy customization to match any brand identity
- **Maintainable Code**: Clean separation of concerns and
well-documented architecture
- **Future-Proof**: Extensible system for additional theme options


<img width="580" height="680" alt="Screenshot 2025-08-19 at 09 22 23"
src="https://github.com/user-attachments/assets/9de8da37-6d56-4fe9-b337-5d8ad2a3ba59"
/>
<img width="599" height="689" alt="Screenshot 2025-08-19 at 09 23 45"
src="https://github.com/user-attachments/assets/26a30cc7-4017-4f4b-8b87-a49466c42b94"
/>
<img width="595" height="681" alt="Screenshot 2025-08-19 at 09 23 17"
src="https://github.com/user-attachments/assets/a3d31088-4545-4f36-aafe-1aae1253d677"
/>
2025-10-10 10:26:06 +02:00

79 lines
3.0 KiB
TypeScript

"use client";
import { setLanguageCookie } from "@/lib/cookies";
import { Lang, LANGS } from "@/lib/i18n";
import { getThemeConfig, getComponentRoundness, APPEARANCE_STYLES } from "@/lib/theme";
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/react";
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import { useLocale } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
// Helper function to get language switcher roundness from theme
function getLanguageSwitcherRoundness(): string {
return getComponentRoundness("button");
}
// Helper function to get card appearance styles for the language switcher
function getLanguageSwitcherCardAppearance(): string {
const themeConfig = getThemeConfig();
const appearance = APPEARANCE_STYLES[themeConfig.appearance];
return appearance?.card || "bg-black/5 dark:bg-white/5"; // Fallback to current styling
}
export function LanguageSwitcher() {
const currentLocale = useLocale();
const switcherRoundness = getLanguageSwitcherRoundness();
const cardAppearance = getLanguageSwitcherCardAppearance();
const [selected, setSelected] = useState(LANGS.find((l) => l.code === currentLocale) || LANGS[0]);
const router = useRouter();
const handleChange = async (language: Lang) => {
setSelected(language);
const newLocale = language.code;
await setLanguageCookie(newLocale);
router.refresh();
};
return (
<div className="w-32">
<Listbox value={selected} onChange={handleChange}>
<ListboxButton
className={clsx(
`relative block w-full py-1.5 pl-3 pr-8 text-left text-sm/6 text-black dark:text-white ${switcherRoundness}`,
cardAppearance,
"focus:outline-none data-[focus]:outline-2 data-[focus]:-outline-offset-2 data-[focus]:outline-white/25",
)}
>
{selected.name}
<ChevronDownIcon className="group pointer-events-none absolute right-2.5 top-2.5 size-4" aria-hidden="true" />
</ListboxButton>
<ListboxOptions
anchor="bottom"
transition
className={clsx(
`w-[var(--button-width)] border border-black/5 bg-background-light-500 p-1 [--anchor-gap:var(--spacing-1)] focus:outline-none dark:border-white/5 dark:bg-background-dark-500 rounded-md`,
"transition duration-100 ease-in data-[leave]:data-[closed]:opacity-0",
)}
>
{LANGS.map((lang) => (
<ListboxOption
key={lang.code}
value={lang}
className={`group flex cursor-default select-none items-center gap-2 px-3 py-1.5 data-[focus]:bg-black/10 dark:data-[focus]:bg-white/10 ${switcherRoundness}`}
>
<CheckIcon className="invisible size-4 group-data-[selected]:visible" />
<div className="text-sm/6 text-black dark:text-white">{lang.name}</div>
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
</div>
);
}