@zitadel/react theme wrapper, component

This commit is contained in:
Max Peintner
2023-04-20 17:07:26 +02:00
parent 787d177eb9
commit 238a73d923
10 changed files with 406 additions and 37 deletions

View File

@@ -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",

View File

@@ -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
View 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>
);
};

View File

@@ -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>
);
},
}
);

View File

@@ -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>
);
}

View 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;

View File

@@ -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>
);

View File

@@ -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;
}

View File

@@ -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>;
}

View File

@@ -9,3 +9,8 @@ export {
SignInWithGitlab,
type SignInWithGitlabProps,
} from "./components/SignInWithGitlab";
export {
ZitadelUIProvider,
type ZitadelUIProps,
} from "./components/ZitadelUIProvider";