mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-27 01:32:15 +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" />
107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
|
import { clsx } from "clsx";
|
|
import { ChangeEvent, DetailedHTMLProps, forwardRef, InputHTMLAttributes, ReactNode } from "react";
|
|
import { getComponentRoundness } from "@/lib/theme";
|
|
|
|
export type TextInputProps = DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {
|
|
label: string;
|
|
suffix?: string;
|
|
placeholder?: string;
|
|
defaultValue?: string;
|
|
error?: string | ReactNode;
|
|
success?: string | ReactNode;
|
|
disabled?: boolean;
|
|
onChange?: (value: ChangeEvent<HTMLInputElement>) => void;
|
|
onBlur?: (value: ChangeEvent<HTMLInputElement>) => void;
|
|
roundness?: string; // Allow override via props
|
|
};
|
|
|
|
const styles = (error: boolean, disabled: boolean, roundnessClasses: string = "rounded-md") =>
|
|
clsx(
|
|
{
|
|
"h-[40px] mb-[2px] p-[7px] bg-input-light-background dark:bg-input-dark-background transition-colors duration-300 grow": true,
|
|
"border border-input-light-border dark:border-input-dark-border hover:border-black hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500": true,
|
|
"focus:outline-none focus:ring-0 text-base text-black dark:text-white placeholder:italic placeholder-gray-700 dark:placeholder-gray-700": true,
|
|
"border border-warn-light-500 dark:border-warn-dark-500 hover:border-warn-light-500 hover:dark:border-warn-dark-500 focus:border-warn-light-500 focus:dark:border-warn-dark-500":
|
|
error,
|
|
"pointer-events-none text-gray-500 dark:text-gray-800 border border-input-light-border dark:border-input-dark-border hover:border-light-hoverborder hover:dark:border-hoverborder cursor-default":
|
|
disabled,
|
|
},
|
|
roundnessClasses, // Apply the full roundness classes directly
|
|
);
|
|
|
|
// Helper function to get default input roundness from theme
|
|
function getDefaultInputRoundness(): string {
|
|
return getComponentRoundness("input");
|
|
}
|
|
|
|
// eslint-disable-next-line react/display-name
|
|
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
|
(
|
|
{
|
|
label,
|
|
placeholder,
|
|
defaultValue,
|
|
suffix,
|
|
required = false,
|
|
error,
|
|
disabled,
|
|
success,
|
|
onChange,
|
|
onBlur,
|
|
roundness,
|
|
...props
|
|
},
|
|
ref,
|
|
) => {
|
|
// Use theme-based roundness if not explicitly provided
|
|
const actualRoundness = roundness || getDefaultInputRoundness();
|
|
|
|
return (
|
|
<label className="relative flex flex-col text-12px text-input-light-label dark:text-input-dark-label">
|
|
<span className={`mb-1 leading-3 ${error ? "text-warn-light-500 dark:text-warn-dark-500" : ""}`}>
|
|
{label} {required && "*"}
|
|
</span>
|
|
<input
|
|
suppressHydrationWarning
|
|
ref={ref}
|
|
className={styles(!!error, !!disabled, actualRoundness)}
|
|
defaultValue={defaultValue}
|
|
required={required}
|
|
disabled={disabled}
|
|
placeholder={placeholder}
|
|
autoComplete={props.autoComplete ?? "off"}
|
|
onChange={(e) => onChange && onChange(e)}
|
|
onBlur={(e) => onBlur && onBlur(e)}
|
|
{...props}
|
|
/>
|
|
|
|
{suffix && (
|
|
<span
|
|
className={clsx(
|
|
"absolute bottom-[22px] right-[3px] z-30 translate-y-1/2 transform bg-background-light-500 p-2 dark:bg-background-dark-500",
|
|
// Extract just the roundness part for the suffix (no padding)
|
|
actualRoundness.split(" ")[0], // Take only the first part (rounded-full, rounded-md, etc.)
|
|
)}
|
|
>
|
|
@{suffix}
|
|
</span>
|
|
)}
|
|
|
|
<div className="leading-14.5px h-14.5px flex flex-row items-center text-12px text-warn-light-500 dark:text-warn-dark-500">
|
|
<span>{error ? error : " "}</span>
|
|
</div>
|
|
|
|
{success && (
|
|
<div className="text-md mt-1 flex flex-row items-center text-green-500">
|
|
<CheckCircleIcon className="h-4 w-4" />
|
|
<span className="ml-1">{success}</span>
|
|
</div>
|
|
)}
|
|
</label>
|
|
);
|
|
},
|
|
);
|