mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-01 03:52:14 +00:00
# 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" />
79 lines
3.0 KiB
TypeScript
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>
|
|
);
|
|
}
|