From 238a73d9236dff581c4438ced9b657321ea9308f Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Thu, 20 Apr 2023 17:07:26 +0200 Subject: [PATCH] @zitadel/react theme wrapper, component --- apps/login/app/layout.tsx | 1 + apps/login/tailwind.config.js | 19 ++ apps/login/ui/Avatar.tsx | 90 +++++++ apps/login/ui/Button.tsx | 44 ++-- apps/login/ui/LayoutProviders.tsx | 4 +- apps/login/ui/ThemeWrapper.tsx | 15 ++ apps/login/ui/UserAvatar.tsx | 15 +- apps/login/utils/colors.ts | 242 +++++++++++++++++- .../src/components/ZitadelUIProvider.tsx | 8 + packages/zitadel-react/src/index.tsx | 5 + 10 files changed, 406 insertions(+), 37 deletions(-) create mode 100644 apps/login/ui/Avatar.tsx create mode 100644 apps/login/ui/ThemeWrapper.tsx create mode 100644 packages/zitadel-react/src/components/ZitadelUIProvider.tsx diff --git a/apps/login/app/layout.tsx b/apps/login/app/layout.tsx index 7a8722f03b0..74becaec203 100644 --- a/apps/login/app/layout.tsx +++ b/apps/login/app/layout.tsx @@ -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", diff --git a/apps/login/tailwind.config.js b/apps/login/tailwind.config.js index fa538e7f8cb..cceb0dd40b3 100644 --- a/apps/login/tailwind.config.js +++ b/apps/login/tailwind.config.js @@ -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", diff --git a/apps/login/ui/Avatar.tsx b/apps/login/ui/Avatar.tsx new file mode 100644 index 00000000000..c76eb3b9f09 --- /dev/null +++ b/apps/login/ui/Avatar.tsx @@ -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 = ({ + 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 ( +
+ {imageUrl ? ( + + ) : ( + + {credentials} + + )} +
+ ); +}; diff --git a/apps/login/ui/Button.tsx b/apps/login/ui/Button.tsx index 588a030d277..ea6dd2e5dcc 100644 --- a/apps/login/ui/Button.tsx +++ b/apps/login/ui/Button.tsx @@ -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( ( { children, - className = '', + className = "", variant = ButtonVariants.Primary, size = ButtonSizes.Small, color = ButtonColors.Primary, ...props }, - ref, + ref ) => { return ( ); - }, + } ); diff --git a/apps/login/ui/LayoutProviders.tsx b/apps/login/ui/LayoutProviders.tsx index 15e65b31a73..4d97861c12a 100644 --- a/apps/login/ui/LayoutProviders.tsx +++ b/apps/login/ui/LayoutProviders.tsx @@ -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} + {children} ); } diff --git a/apps/login/ui/ThemeWrapper.tsx b/apps/login/ui/ThemeWrapper.tsx new file mode 100644 index 00000000000..81a85625951 --- /dev/null +++ b/apps/login/ui/ThemeWrapper.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { useTheme } from "next-themes"; + +const ThemeWrapper = ({ children }: any) => { + const { resolvedTheme } = useTheme(); + + const isDark = resolvedTheme && resolvedTheme === "dark"; + + return ( +
{children}
+ ); +}; + +export default ThemeWrapper; diff --git a/apps/login/ui/UserAvatar.tsx b/apps/login/ui/UserAvatar.tsx index 80278f56165..fbf3b3886e5 100644 --- a/apps/login/ui/UserAvatar.tsx +++ b/apps/login/ui/UserAvatar.tsx @@ -1,18 +1,15 @@ +import { Avatar, AvatarSize } from "#/ui/Avatar"; + type Props = { name: string; }; export default function UserAvatar({ name }: Props) { return ( -
- {/* user-avatar */} -
+
+
+ +
{name}
); diff --git a/apps/login/utils/colors.ts b/apps/login/utils/colors.ts index 87f5e9dd0c8..97550947605 100644 --- a/apps/login/utils/colors.ts +++ b/apps/login/utils/colors.ts @@ -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; +} diff --git a/packages/zitadel-react/src/components/ZitadelUIProvider.tsx b/packages/zitadel-react/src/components/ZitadelUIProvider.tsx new file mode 100644 index 00000000000..2caa5a64474 --- /dev/null +++ b/packages/zitadel-react/src/components/ZitadelUIProvider.tsx @@ -0,0 +1,8 @@ +export type ZitadelUIProps = { + dark: boolean; + children: React.ReactNode; +}; + +export function ZitadelUIProvider({ dark, children }: ZitadelUIProps) { + return
{children}
; +} diff --git a/packages/zitadel-react/src/index.tsx b/packages/zitadel-react/src/index.tsx index 6bb124c0635..eec85c858c3 100644 --- a/packages/zitadel-react/src/index.tsx +++ b/packages/zitadel-react/src/index.tsx @@ -9,3 +9,8 @@ export { SignInWithGitlab, type SignInWithGitlabProps, } from "./components/SignInWithGitlab"; + +export { + ZitadelUIProvider, + type ZitadelUIProps, +} from "./components/ZitadelUIProvider";