mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-30 11:02:16 +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" />
216 lines
7.9 KiB
TypeScript
216 lines
7.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import { render } from "@testing-library/react";
|
|
import { Button, ButtonSizes, ButtonVariants, ButtonColors, getButtonClasses } from "./button";
|
|
|
|
describe("Button Component", () => {
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...originalEnv };
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
describe("Component Rendering", () => {
|
|
it("should render button with children", () => {
|
|
const { getByText } = render(<Button>Click me</Button>);
|
|
expect(getByText("Click me")).toBeTruthy();
|
|
});
|
|
|
|
it("should apply custom className", () => {
|
|
const { container } = render(<Button className="custom-class">Test</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button?.className).toContain("custom-class");
|
|
});
|
|
|
|
it("should pass through native button props", () => {
|
|
const { container } = render(<Button disabled>Test</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button?.disabled).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("Button Variants", () => {
|
|
it("should render primary variant by default", () => {
|
|
const { container } = render(<Button>Primary</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button).toBeTruthy();
|
|
// Primary should have background color
|
|
expect(button?.className).toMatch(/bg-/);
|
|
});
|
|
|
|
it("should render secondary variant", () => {
|
|
const { container } = render(<Button variant={ButtonVariants.Secondary}>Secondary</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button).toBeTruthy();
|
|
// Secondary should have border
|
|
expect(button?.className).toMatch(/border/);
|
|
});
|
|
|
|
it("should render all variant types", () => {
|
|
const variants = [ButtonVariants.Primary, ButtonVariants.Secondary, ButtonVariants.Destructive];
|
|
|
|
variants.forEach((variant) => {
|
|
const { container } = render(<Button variant={variant}>Test</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button).toBeTruthy();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Button Sizes", () => {
|
|
it("should render small size by default", () => {
|
|
const { container } = render(<Button>Small</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button).toBeTruthy();
|
|
// Should have padding styles
|
|
expect(button?.className).toMatch(/p[xy]?-/);
|
|
});
|
|
|
|
it("should render large size", () => {
|
|
const { container } = render(<Button size={ButtonSizes.Large}>Large</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button).toBeTruthy();
|
|
// Should have padding styles
|
|
expect(button?.className).toMatch(/p[xy]?-/);
|
|
});
|
|
|
|
it("should render all size types", () => {
|
|
const sizes = [ButtonSizes.Small, ButtonSizes.Large];
|
|
|
|
sizes.forEach((size) => {
|
|
const { container } = render(<Button size={size}>Test</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button).toBeTruthy();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Button Colors", () => {
|
|
it("should render primary color by default", () => {
|
|
const { container } = render(<Button>Primary Color</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button).toBeTruthy();
|
|
// Should have background color
|
|
expect(button?.className).toMatch(/bg-/);
|
|
});
|
|
|
|
it("should render warn color", () => {
|
|
const { container } = render(<Button color={ButtonColors.Warn}>Warn</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button).toBeTruthy();
|
|
// Should have background color
|
|
expect(button?.className).toMatch(/bg-/);
|
|
});
|
|
|
|
it("should render all color types", () => {
|
|
const colors = [ButtonColors.Neutral, ButtonColors.Primary, ButtonColors.Warn];
|
|
|
|
colors.forEach((color) => {
|
|
const { container } = render(<Button color={color}>Test</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button).toBeTruthy();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Theme Integration", () => {
|
|
it("should apply theme-based roundness", () => {
|
|
const { container } = render(<Button>Themed</Button>);
|
|
const button = container.querySelector("button");
|
|
// Should have some roundness class applied
|
|
expect(button?.className).toBeTruthy();
|
|
expect(button?.className).toMatch(/rounded/);
|
|
});
|
|
|
|
it("should completely override theme roundness with custom roundness prop", () => {
|
|
// Set theme to have full roundness
|
|
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "full";
|
|
const customRoundness = "custom-round-test";
|
|
|
|
const { container } = render(<Button roundness={customRoundness}>Custom</Button>);
|
|
const button = container.querySelector("button");
|
|
|
|
// Should contain the custom roundness
|
|
expect(button?.className).toContain(customRoundness);
|
|
|
|
// The custom prop completely replaces theme roundness
|
|
expect(button).toBeTruthy();
|
|
});
|
|
|
|
it("should change appearance when theme changes", () => {
|
|
// Default theme
|
|
const { container: container1 } = render(<Button>Default</Button>);
|
|
const button1 = container1.querySelector("button");
|
|
const defaultClasses = button1?.className;
|
|
|
|
// Change theme
|
|
process.env.NEXT_PUBLIC_THEME_ROUNDNESS = "full";
|
|
const { container: container2 } = render(<Button>Full</Button>);
|
|
const button2 = container2.querySelector("button");
|
|
|
|
// Classes should be present (we don't check exact values)
|
|
expect(defaultClasses).toBeTruthy();
|
|
expect(button2?.className).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("getButtonClasses", () => {
|
|
it("should return valid class string", () => {
|
|
const classes = getButtonClasses(ButtonSizes.Small, ButtonVariants.Primary, ButtonColors.Primary);
|
|
expect(typeof classes).toBe("string");
|
|
expect(classes.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should include custom roundness classes", () => {
|
|
const customRoundness = "rounded-2xl";
|
|
const classes = getButtonClasses(ButtonSizes.Small, ButtonVariants.Primary, ButtonColors.Primary, customRoundness);
|
|
expect(classes).toContain(customRoundness);
|
|
});
|
|
|
|
it("should include appearance classes", () => {
|
|
const appearance = "shadow-lg";
|
|
const classes = getButtonClasses(
|
|
ButtonSizes.Small,
|
|
ButtonVariants.Primary,
|
|
ButtonColors.Primary,
|
|
"rounded-md",
|
|
appearance,
|
|
);
|
|
expect(classes).toContain(appearance);
|
|
});
|
|
|
|
it("should generate different classes for different variants", () => {
|
|
const primaryClasses = getButtonClasses(ButtonSizes.Small, ButtonVariants.Primary, ButtonColors.Primary);
|
|
const secondaryClasses = getButtonClasses(ButtonSizes.Small, ButtonVariants.Secondary, ButtonColors.Primary);
|
|
|
|
expect(primaryClasses).not.toBe(secondaryClasses);
|
|
});
|
|
|
|
it("should generate different classes for different sizes", () => {
|
|
const smallClasses = getButtonClasses(ButtonSizes.Small, ButtonVariants.Primary, ButtonColors.Primary);
|
|
const largeClasses = getButtonClasses(ButtonSizes.Large, ButtonVariants.Primary, ButtonColors.Primary);
|
|
|
|
expect(smallClasses).not.toBe(largeClasses);
|
|
});
|
|
});
|
|
|
|
describe("Accessibility", () => {
|
|
it("should have type='button' by default", () => {
|
|
const { container } = render(<Button>Test</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button?.type).toBe("button");
|
|
});
|
|
|
|
it("should support disabled state", () => {
|
|
const { container } = render(<Button disabled>Disabled</Button>);
|
|
const button = container.querySelector("button");
|
|
expect(button?.disabled).toBe(true);
|
|
// Should have disabled styles
|
|
expect(button?.className).toMatch(/disabled:/);
|
|
});
|
|
});
|
|
});
|