mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-12 00:55:12 +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" />
169 lines
6.0 KiB
TypeScript
169 lines
6.0 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
import { render } from "@testing-library/react";
|
|
import { Avatar, getInitials } from "./avatar";
|
|
|
|
// Mock next-themes
|
|
vi.mock("next-themes", () => ({
|
|
useTheme: () => ({
|
|
resolvedTheme: "light",
|
|
}),
|
|
}));
|
|
|
|
// Mock next/image
|
|
vi.mock("next/image", () => ({
|
|
default: ({ src, alt }: { src: string; alt: string }) => <img src={src} alt={alt} />,
|
|
}));
|
|
|
|
describe("Avatar Component", () => {
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...originalEnv };
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
describe("getInitials", () => {
|
|
it("should get initials from full name", () => {
|
|
const initials = getInitials("John Doe", "[email protected]");
|
|
expect(initials).toBe("JD");
|
|
});
|
|
|
|
it("should get single initial from single name", () => {
|
|
const initials = getInitials("John", "[email protected]");
|
|
expect(initials).toBe("J");
|
|
});
|
|
|
|
it("should get initials from loginName when name is empty", () => {
|
|
const initials = getInitials("", "[email protected]");
|
|
expect(initials.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should handle loginName with underscore separator", () => {
|
|
const initials = getInitials("", "[email protected]");
|
|
expect(initials).toBe("jd");
|
|
});
|
|
|
|
it("should handle loginName with dash separator", () => {
|
|
const initials = getInitials("", "[email protected]");
|
|
expect(initials).toBe("jd");
|
|
});
|
|
|
|
it("should handle loginName with dot separator", () => {
|
|
const initials = getInitials("", "[email protected]");
|
|
expect(initials).toBe("jd");
|
|
});
|
|
|
|
it("should get initials from username part of email", () => {
|
|
const initials = getInitials("", "[email protected]");
|
|
expect(initials.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("Component Rendering", () => {
|
|
it("should render avatar with initials", () => {
|
|
const { container } = render(<Avatar name="John Doe" loginName="[email protected]" />);
|
|
expect(container.querySelector(".avatar, [class*='avatar'], div")).toBeTruthy();
|
|
});
|
|
|
|
it("should render with different sizes", () => {
|
|
const sizes: Array<"small" | "base" | "large"> = ["small", "base", "large"];
|
|
|
|
sizes.forEach((size) => {
|
|
const { container } = render(<Avatar size={size} name="Test User" loginName="[email protected]" />);
|
|
expect(container.firstChild).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it("should render with shadow prop", () => {
|
|
const { container } = render(<Avatar name="Test User" loginName="[email protected]" shadow={true} />);
|
|
expect(container.firstChild).toBeTruthy();
|
|
});
|
|
|
|
it("should render without shadow prop", () => {
|
|
const { container } = render(<Avatar name="Test User" loginName="[email protected]" shadow={false} />);
|
|
expect(container.firstChild).toBeTruthy();
|
|
});
|
|
|
|
it("should render with image URL", () => {
|
|
const { container } = render(
|
|
<Avatar name="Test User" loginName="[email protected]" imageUrl="https://example.com/avatar.jpg" />,
|
|
);
|
|
expect(container.firstChild).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("Theme Integration", () => {
|
|
it("should apply theme-based roundness", () => {
|
|
const { container } = render(<Avatar name="Test User" loginName="[email protected]" />);
|
|
const avatar = container.firstChild as HTMLElement;
|
|
// Should render with some styling
|
|
expect(avatar).toBeTruthy();
|
|
});
|
|
|
|
it("should respect theme roundness changes", () => {
|
|
// Default theme
|
|
const { container: container1 } = render(<Avatar name="Test User" loginName="[email protected]" />);
|
|
const avatar1 = container1.firstChild as HTMLElement;
|
|
|
|
// Change theme
|
|
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "edgy";
|
|
const { container: container2 } = render(<Avatar name="Test User" loginName="[email protected]" />);
|
|
const avatar2 = container2.firstChild as HTMLElement;
|
|
|
|
// Both should render
|
|
expect(avatar1).toBeTruthy();
|
|
expect(avatar2).toBeTruthy();
|
|
});
|
|
|
|
it("should render with full roundness", () => {
|
|
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "full";
|
|
const { container } = render(<Avatar name="Test User" loginName="[email protected]" />);
|
|
expect(container.firstChild).toBeTruthy();
|
|
});
|
|
|
|
it("should render with mid roundness", () => {
|
|
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "mid";
|
|
const { container } = render(<Avatar name="Test User" loginName="[email protected]" />);
|
|
expect(container.firstChild).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("Color Generation", () => {
|
|
it("should generate consistent colors for same loginName", () => {
|
|
const { container: container1 } = render(<Avatar name="Test" loginName="[email protected]" />);
|
|
const { container: container2 } = render(<Avatar name="Test" loginName="[email protected]" />);
|
|
|
|
expect(container1.firstChild).toBeTruthy();
|
|
expect(container2.firstChild).toBeTruthy();
|
|
});
|
|
|
|
it("should render for different loginNames", () => {
|
|
const { container: container1 } = render(<Avatar name="User 1" loginName="[email protected]" />);
|
|
const { container: container2 } = render(<Avatar name="User 2" loginName="[email protected]" />);
|
|
|
|
expect(container1.firstChild).toBeTruthy();
|
|
expect(container2.firstChild).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("Props Validation", () => {
|
|
it("should handle null name", () => {
|
|
const { container } = render(<Avatar name={null} loginName="[email protected]" />);
|
|
expect(container.firstChild).toBeTruthy();
|
|
});
|
|
|
|
it("should handle undefined name", () => {
|
|
const { container } = render(<Avatar name={undefined} loginName="[email protected]" />);
|
|
expect(container.firstChild).toBeTruthy();
|
|
});
|
|
|
|
it("should require loginName", () => {
|
|
const { container } = render(<Avatar name="Test" loginName="[email protected]" />);
|
|
expect(container.firstChild).toBeTruthy();
|
|
});
|
|
});
|
|
});
|