mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 10:25:58 +00:00
@zitadel/react theme wrapper, component
This commit is contained in:
@@ -7,6 +7,7 @@ import { Lato } from "next/font/google";
|
||||
import Byline from "#/ui/Byline";
|
||||
import { LayoutProviders } from "#/ui/LayoutProviders";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { ZitadelUIProvider } from "#/../../packages/zitadel-react/dist";
|
||||
|
||||
const lato = Lato({
|
||||
weight: "400",
|
||||
|
||||
@@ -30,6 +30,14 @@ module.exports = {
|
||||
violet: "#7928CA",
|
||||
},
|
||||
primary: {
|
||||
light: {
|
||||
200: "#bec6ef",
|
||||
300: "#8594e0",
|
||||
400: "#6c7eda",
|
||||
500: "#5469d4",
|
||||
600: "#3c54ce",
|
||||
contrast: "#ffffff",
|
||||
},
|
||||
dark: {
|
||||
100: "#afd1f2",
|
||||
200: "#7fb5ea",
|
||||
@@ -42,6 +50,17 @@ module.exports = {
|
||||
900: "#0f355b",
|
||||
},
|
||||
},
|
||||
accent: {
|
||||
light: {
|
||||
400: "#9142d5",
|
||||
500: "#7e21ce",
|
||||
},
|
||||
dark: {
|
||||
300: "#ff6396",
|
||||
400: "#ff4180",
|
||||
500: "#ff2069",
|
||||
},
|
||||
},
|
||||
background: {
|
||||
dark: {
|
||||
100: "#4a69aa",
|
||||
|
||||
90
apps/login/ui/Avatar.tsx
Normal file
90
apps/login/ui/Avatar.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Color, getColorHash } from "#/utils/colors";
|
||||
import { useTheme } from "next-themes";
|
||||
import { FC } from "react";
|
||||
|
||||
export enum AvatarSize {
|
||||
SMALL = "small",
|
||||
BASE = "base",
|
||||
LARGE = "large",
|
||||
}
|
||||
|
||||
interface AvatarProps {
|
||||
name: string | null | undefined;
|
||||
loginName: string;
|
||||
imageUrl?: string;
|
||||
size?: AvatarSize;
|
||||
shadow?: boolean;
|
||||
}
|
||||
|
||||
export const Avatar: FC<AvatarProps> = ({
|
||||
size = AvatarSize.BASE,
|
||||
name,
|
||||
loginName,
|
||||
imageUrl,
|
||||
shadow,
|
||||
}) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
let credentials = "";
|
||||
|
||||
if (name) {
|
||||
const split = name.split(" ");
|
||||
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : "");
|
||||
credentials = initials;
|
||||
} else {
|
||||
const username = loginName.split("@")[0];
|
||||
let separator = "_";
|
||||
if (username.includes("-")) {
|
||||
separator = "-";
|
||||
}
|
||||
if (username.includes(".")) {
|
||||
separator = ".";
|
||||
}
|
||||
const split = username.split(separator);
|
||||
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : "");
|
||||
credentials = initials;
|
||||
}
|
||||
|
||||
const color: Color = getColorHash(loginName);
|
||||
|
||||
const avatarStyleDark = {
|
||||
backgroundColor: color[900],
|
||||
color: color[200],
|
||||
};
|
||||
|
||||
const avatarStyleLight = {
|
||||
backgroundColor: color[200],
|
||||
color: color[900],
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full h-full flex-shrink-0 flex justify-center items-center cursor-default pointer-events-none group-focus:outline-none group-focus:ring-2 transition-colors duration-200 dark:group-focus:ring-offset-blue bg-primary-light-500 hover:bg-primary-light-400 hover:dark:bg-primary-dark-500 group-focus:ring-primary-light-200 dark:group-focus:ring-primary-dark-400 dark:bg-primary-dark-300 text-white dark:text-blue rounded-full ${
|
||||
shadow ? "shadow" : ""
|
||||
} ${
|
||||
size === AvatarSize.LARGE
|
||||
? "h-20 w-20 font-normal"
|
||||
: size === AvatarSize.BASE
|
||||
? "w-38px h-38px font-bold"
|
||||
: size === AvatarSize.SMALL
|
||||
? "w-32px h-32px font-bold"
|
||||
: ""
|
||||
}`}
|
||||
style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark}
|
||||
>
|
||||
{imageUrl ? (
|
||||
<img
|
||||
className="border border-gray-500 rounded-full w-12 h-12"
|
||||
src={imageUrl}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={`uppercase ${
|
||||
size === AvatarSize.LARGE ? "text-xl" : "text-13px"
|
||||
}`}
|
||||
>
|
||||
{credentials}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +1,25 @@
|
||||
import clsx from 'clsx';
|
||||
import clsx from "clsx";
|
||||
import React, {
|
||||
ButtonHTMLAttributes,
|
||||
DetailedHTMLProps,
|
||||
forwardRef,
|
||||
} from 'react';
|
||||
} from "react";
|
||||
|
||||
export enum ButtonSizes {
|
||||
Small = 'Small',
|
||||
Large = 'Large',
|
||||
Small = "Small",
|
||||
Large = "Large",
|
||||
}
|
||||
|
||||
export enum ButtonVariants {
|
||||
Primary = 'Primary',
|
||||
Secondary = 'Secondary',
|
||||
Destructive = 'Destructive',
|
||||
Primary = "Primary",
|
||||
Secondary = "Secondary",
|
||||
Destructive = "Destructive",
|
||||
}
|
||||
|
||||
export enum ButtonColors {
|
||||
Neutral = 'Neutral',
|
||||
Primary = 'Primary',
|
||||
Warn = 'Warn',
|
||||
Neutral = "Neutral",
|
||||
Primary = "Primary",
|
||||
Warn = "Warn",
|
||||
}
|
||||
|
||||
export type ButtonProps = DetailedHTMLProps<
|
||||
@@ -34,23 +34,23 @@ export type ButtonProps = DetailedHTMLProps<
|
||||
export const getButtonClasses = (
|
||||
size: ButtonSizes,
|
||||
variant: ButtonVariants,
|
||||
color: ButtonColors,
|
||||
color: ButtonColors
|
||||
) =>
|
||||
clsx({
|
||||
'box-border font-normal leading-36px text-14px inline-flex items-center rounded-md focus:outline-none transition-colors transition-shadow duration-300':
|
||||
"box-border font-normal leading-36px text-14px inline-flex items-center rounded-md focus:outline-none transition-colors transition-shadow duration-300":
|
||||
true,
|
||||
'shadow hover:shadow-xl active:shadow-xl disabled:border-none disabled:bg-gray-300 disabled:text-gray-600 disabled:cursor-not-allowed disabled:dark:bg-gray-800 disabled:dark:text-gray-900':
|
||||
"shadow hover:shadow-xl active:shadow-xl disabled:border-none disabled:bg-gray-300 disabled:text-gray-600 disabled:cursor-not-allowed disabled:dark:bg-gray-800 disabled:dark:text-gray-900":
|
||||
variant === ButtonVariants.Primary,
|
||||
'bg-primary-light-500 dark:bg-primary-dark-500 hover:bg-primary-light-400 hover:dark:bg-primary-dark-400 text-primary-light-contrast dark:text-primary-dark-contrast':
|
||||
"!bg-primary-light-500 !dark:bg-primary-dark-500 hover:bg-primary-light-400 hover:dark:bg-primary-dark-400 text-primary-light-contrast dark:text-primary-dark-contrast":
|
||||
variant === ButtonVariants.Primary && color !== ButtonColors.Warn,
|
||||
'bg-warn-light-500 dark:bg-warn-dark-500 hover:bg-warn-light-400 hover:dark:bg-warn-dark-400 text-white dark:text-white':
|
||||
"bg-warn-light-500 dark:bg-warn-dark-500 hover:bg-warn-light-400 hover:dark:bg-warn-dark-400 text-white dark:text-white":
|
||||
variant === ButtonVariants.Primary && color === ButtonColors.Warn,
|
||||
'border border-button-light-border dark:border-button-dark-border text-gray-950 hover:bg-gray-500 hover:bg-opacity-20 hover:dark:bg-white hover:dark:bg-opacity-10 focus:bg-gray-500 focus:bg-opacity-20 focus:dark:bg-white focus:dark:bg-opacity-10 dark:text-white disabled:text-gray-600 disabled:hover:bg-transparent disabled:dark:hover:bg-transparent disabled:cursor-not-allowed disabled:dark:text-gray-900':
|
||||
"border border-button-light-border dark:border-button-dark-border text-gray-950 hover:bg-gray-500 hover:bg-opacity-20 hover:dark:bg-white hover:dark:bg-opacity-10 focus:bg-gray-500 focus:bg-opacity-20 focus:dark:bg-white focus:dark:bg-opacity-10 dark:text-white disabled:text-gray-600 disabled:hover:bg-transparent disabled:dark:hover:bg-transparent disabled:cursor-not-allowed disabled:dark:text-gray-900":
|
||||
variant === ButtonVariants.Secondary,
|
||||
'border border-button-light-border dark:border-button-dark-border text-warn-light-500 dark:text-warn-dark-500 hover:bg-warn-light-500 hover:bg-opacity-10 dark:hover:bg-warn-light-500 dark:hover:bg-opacity-10 focus:bg-warn-light-500 focus:bg-opacity-20 dark:focus:bg-warn-light-500 dark:focus:bg-opacity-20':
|
||||
"border border-button-light-border dark:border-button-dark-border text-warn-light-500 dark:text-warn-dark-500 hover:bg-warn-light-500 hover:bg-opacity-10 dark:hover:bg-warn-light-500 dark:hover:bg-opacity-10 focus:bg-warn-light-500 focus:bg-opacity-20 dark:focus:bg-warn-light-500 dark:focus:bg-opacity-20":
|
||||
color === ButtonColors.Warn && variant !== ButtonVariants.Primary,
|
||||
'px-16 py-2': size === ButtonSizes.Large,
|
||||
'px-4 h-[36px]': size === ButtonSizes.Small,
|
||||
"px-16 py-2": size === ButtonSizes.Large,
|
||||
"px-4 h-[36px]": size === ButtonSizes.Small,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
@@ -58,13 +58,13 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className = '',
|
||||
className = "",
|
||||
variant = ButtonVariants.Primary,
|
||||
size = ButtonSizes.Small,
|
||||
color = ButtonColors.Primary,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
@@ -76,5 +76,5 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import ThemeWrapper from "./ThemeWrapper";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -14,7 +14,7 @@ export function LayoutProviders({ children }: Props) {
|
||||
storageKey="cp-theme"
|
||||
value={{ dark: "dark" }}
|
||||
>
|
||||
{children}
|
||||
<ThemeWrapper>{children}</ThemeWrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
15
apps/login/ui/ThemeWrapper.tsx
Normal file
15
apps/login/ui/ThemeWrapper.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
const ThemeWrapper = ({ children }: any) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const isDark = resolvedTheme && resolvedTheme === "dark";
|
||||
|
||||
return (
|
||||
<div className={`${isDark ? "ui-dark" : "ui-light"} `}>{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeWrapper;
|
||||
@@ -1,18 +1,15 @@
|
||||
import { Avatar, AvatarSize } from "#/ui/Avatar";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default function UserAvatar({ name }: Props) {
|
||||
return (
|
||||
<div className="flex w-full flex-row items-center rounded-full border p-[1px] dark:border-white/20">
|
||||
{/* <Image
|
||||
height={20}
|
||||
width={20}
|
||||
className="avatar-img"
|
||||
src=""
|
||||
alt="user-avatar"
|
||||
/> */}
|
||||
<div className="h-8 w-8 rounded-full bg-primary-light-700 dark:bg-primary-dark-800"></div>
|
||||
<div className="flex h-full w-full flex-row items-center rounded-full border p-[1px] dark:border-white/20">
|
||||
<div>
|
||||
<Avatar size={AvatarSize.SMALL} name={name} loginName={name} />
|
||||
</div>
|
||||
<span className="ml-4 text-14px">{name}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import tinycolor from "tinycolor2";
|
||||
|
||||
export interface Color {
|
||||
export interface ColorForTiny {
|
||||
name: string;
|
||||
hex: string;
|
||||
rgb: string;
|
||||
@@ -34,7 +34,7 @@ export type ColorName =
|
||||
// }
|
||||
|
||||
export type ColorMap = {
|
||||
[key in MapName]: { [key in ColorName]: Color[] };
|
||||
[key in MapName]: { [key in ColorName]: ColorForTiny[] };
|
||||
};
|
||||
|
||||
export const DARK_PRIMARY = "#2073c4";
|
||||
@@ -71,7 +71,7 @@ export class ColorService {
|
||||
}
|
||||
}
|
||||
|
||||
function computeColors(hex: string): Color[] {
|
||||
function computeColors(hex: string): ColorForTiny[] {
|
||||
return [
|
||||
getColorObject(tinycolor(hex).lighten(52), "50"),
|
||||
getColorObject(tinycolor(hex).lighten(37), "100"),
|
||||
@@ -90,7 +90,7 @@ function computeColors(hex: string): Color[] {
|
||||
];
|
||||
}
|
||||
|
||||
function getColorObject(value: any, name: string): Color {
|
||||
function getColorObject(value: any, name: string): ColorForTiny {
|
||||
const c = tinycolor(value);
|
||||
return {
|
||||
name: name,
|
||||
@@ -150,3 +150,237 @@ export function computeMap(labelpolicy: any, dark: boolean): ColorMap {
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
export interface Color {
|
||||
200: string;
|
||||
300: string;
|
||||
500: string;
|
||||
600: string;
|
||||
700: string;
|
||||
900: string;
|
||||
}
|
||||
|
||||
export const COLORS = [
|
||||
{
|
||||
500: "#ef4444",
|
||||
200: "#fecaca",
|
||||
300: "#fca5a5",
|
||||
600: "#dc2626",
|
||||
700: "#b91c1c",
|
||||
900: "#7f1d1d",
|
||||
},
|
||||
{
|
||||
500: "#f97316",
|
||||
200: "#fed7aa",
|
||||
300: "#fdba74",
|
||||
600: "#ea580c",
|
||||
700: "#c2410c",
|
||||
900: "#7c2d12",
|
||||
},
|
||||
{
|
||||
500: "#f59e0b",
|
||||
200: "#fde68a",
|
||||
300: "#fcd34d",
|
||||
600: "#d97706",
|
||||
700: "#b45309",
|
||||
900: "#78350f",
|
||||
},
|
||||
{
|
||||
500: "#eab308",
|
||||
200: "#fef08a",
|
||||
300: "#fde047",
|
||||
600: "#ca8a04",
|
||||
700: "#a16207",
|
||||
900: "#713f12",
|
||||
},
|
||||
{
|
||||
500: "#84cc16",
|
||||
200: "#d9f99d",
|
||||
300: "#bef264",
|
||||
600: "#65a30d",
|
||||
700: "#4d7c0f",
|
||||
900: "#365314",
|
||||
},
|
||||
{
|
||||
500: "#22c55e",
|
||||
200: "#bbf7d0",
|
||||
300: "#86efac",
|
||||
600: "#16a34a",
|
||||
700: "#15803d",
|
||||
900: "#14532d",
|
||||
},
|
||||
{
|
||||
500: "#10b981",
|
||||
200: "#a7f3d0",
|
||||
300: "#6ee7b7",
|
||||
600: "#059669",
|
||||
700: "#047857",
|
||||
900: "#064e3b",
|
||||
},
|
||||
{
|
||||
500: "#14b8a6",
|
||||
200: "#99f6e4",
|
||||
300: "#5eead4",
|
||||
600: "#0d9488",
|
||||
700: "#0f766e",
|
||||
900: "#134e4a",
|
||||
},
|
||||
{
|
||||
500: "#06b6d4",
|
||||
200: "#a5f3fc",
|
||||
300: "#67e8f9",
|
||||
600: "#0891b2",
|
||||
700: "#0e7490",
|
||||
900: "#164e63",
|
||||
},
|
||||
{
|
||||
500: "#0ea5e9",
|
||||
200: "#bae6fd",
|
||||
300: "#7dd3fc",
|
||||
600: "#0284c7",
|
||||
700: "#0369a1",
|
||||
900: "#0c4a6e",
|
||||
},
|
||||
{
|
||||
500: "#3b82f6",
|
||||
200: "#bfdbfe",
|
||||
300: "#93c5fd",
|
||||
600: "#2563eb",
|
||||
700: "#1d4ed8",
|
||||
900: "#1e3a8a",
|
||||
},
|
||||
{
|
||||
500: "#6366f1",
|
||||
200: "#c7d2fe",
|
||||
300: "#a5b4fc",
|
||||
600: "#4f46e5",
|
||||
700: "#4338ca",
|
||||
900: "#312e81",
|
||||
},
|
||||
{
|
||||
500: "#8b5cf6",
|
||||
200: "#ddd6fe",
|
||||
300: "#c4b5fd",
|
||||
600: "#7c3aed",
|
||||
700: "#6d28d9",
|
||||
900: "#4c1d95",
|
||||
},
|
||||
{
|
||||
500: "#a855f7",
|
||||
200: "#e9d5ff",
|
||||
300: "#d8b4fe",
|
||||
600: "#9333ea",
|
||||
700: "#7e22ce",
|
||||
900: "#581c87",
|
||||
},
|
||||
{
|
||||
500: "#d946ef",
|
||||
200: "#f5d0fe",
|
||||
300: "#f0abfc",
|
||||
600: "#c026d3",
|
||||
700: "#a21caf",
|
||||
900: "#701a75",
|
||||
},
|
||||
{
|
||||
500: "#ec4899",
|
||||
200: "#fbcfe8",
|
||||
300: "#f9a8d4",
|
||||
600: "#db2777",
|
||||
700: "#be185d",
|
||||
900: "#831843",
|
||||
},
|
||||
{
|
||||
500: "#f43f5e",
|
||||
200: "#fecdd3",
|
||||
300: "#fda4af",
|
||||
600: "#e11d48",
|
||||
700: "#be123c",
|
||||
900: "#881337",
|
||||
},
|
||||
];
|
||||
|
||||
export function getColorHash(value: string): Color {
|
||||
let hash = 0;
|
||||
|
||||
if (value.length === 0) {
|
||||
return COLORS[hash];
|
||||
}
|
||||
|
||||
hash = hashCode(value);
|
||||
return COLORS[hash % COLORS.length];
|
||||
}
|
||||
|
||||
export function hashCode(str: string, seed = 0): number {
|
||||
let h1 = 0xdeadbeef ^ seed,
|
||||
h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 =
|
||||
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
||||
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 =
|
||||
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
||||
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
}
|
||||
|
||||
export function getMembershipColor(role: string): Color {
|
||||
const hash = hashCode(role);
|
||||
let color = COLORS[hash % COLORS.length];
|
||||
|
||||
switch (role) {
|
||||
case "IAM_OWNER":
|
||||
color = COLORS[0];
|
||||
break;
|
||||
case "IAM_OWNER_VIEWER":
|
||||
color = COLORS[14];
|
||||
break;
|
||||
case "IAM_ORG_MANAGER":
|
||||
color = COLORS[11];
|
||||
break;
|
||||
case "IAM_USER_MANAGER":
|
||||
color = COLORS[8];
|
||||
break;
|
||||
|
||||
case "ORG_OWNER":
|
||||
color = COLORS[16];
|
||||
break;
|
||||
case "ORG_USER_MANAGER":
|
||||
color = COLORS[8];
|
||||
break;
|
||||
case "ORG_OWNER_VIEWER":
|
||||
color = COLORS[14];
|
||||
break;
|
||||
case "ORG_USER_PERMISSION_EDITOR":
|
||||
color = COLORS[7];
|
||||
break;
|
||||
case "ORG_PROJECT_PERMISSION_EDITOR":
|
||||
color = COLORS[11];
|
||||
break;
|
||||
case "ORG_PROJECT_CREATOR":
|
||||
color = COLORS[12];
|
||||
break;
|
||||
|
||||
case "PROJECT_OWNER":
|
||||
color = COLORS[9];
|
||||
break;
|
||||
case "PROJECT_OWNER_VIEWER":
|
||||
color = COLORS[10];
|
||||
break;
|
||||
case "PROJECT_OWNER_GLOBAL":
|
||||
color = COLORS[11];
|
||||
break;
|
||||
case "PROJECT_OWNER_VIEWER_GLOBAL":
|
||||
color = COLORS[12];
|
||||
break;
|
||||
|
||||
default:
|
||||
color = COLORS[hash % COLORS.length];
|
||||
break;
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export type ZitadelUIProps = {
|
||||
dark: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function ZitadelUIProvider({ dark, children }: ZitadelUIProps) {
|
||||
return <div className={`${dark ? "ui-dark" : "ui-light"} `}>{children}</div>;
|
||||
}
|
||||
@@ -9,3 +9,8 @@ export {
|
||||
SignInWithGitlab,
|
||||
type SignInWithGitlabProps,
|
||||
} from "./components/SignInWithGitlab";
|
||||
|
||||
export {
|
||||
ZitadelUIProvider,
|
||||
type ZitadelUIProps,
|
||||
} from "./components/ZitadelUIProvider";
|
||||
|
||||
Reference in New Issue
Block a user