diff --git a/packages/components/src/components/Avatar/Avatar.module.css b/packages/components/src/components/Avatar/Avatar.module.css new file mode 100644 index 000000000..41acd3464 --- /dev/null +++ b/packages/components/src/components/Avatar/Avatar.module.css @@ -0,0 +1,34 @@ +@import "@/styles"; +.root { + border-radius: var(--border-radius--round); + width: var(--avatar-size); + height: var(--avatar-size); + overflow: hidden; +} + +.initials { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: calc(var(--avatar-size) * 0.4); +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.size-s { + --avatar-size: var(--avatar--size--s); +} + +.size-m { + --avatar-size: var(--avatar--size--m); +} + +.size-l { + --avatar-size: var(--avatar--size--l); +} diff --git a/packages/components/src/components/Avatar/Avatar.tsx b/packages/components/src/components/Avatar/Avatar.tsx new file mode 100644 index 000000000..04ddc0048 --- /dev/null +++ b/packages/components/src/components/Avatar/Avatar.tsx @@ -0,0 +1,35 @@ +import React, { FC, PropsWithChildren } from "react"; +import styles from "./Avatar.module.css"; +import clsx from "clsx"; +import { PropsContext, PropsContextProvider } from "@/lib/propsContext"; + +interface AvatarProps extends PropsWithChildren { + className?: string; + /** @default "s" */ + size?: "s" | "m" | "l"; +} + +export const Avatar: FC = (props) => { + const { children, className, size = "s" } = props; + + const rootClassName = clsx(className, styles.root, styles[`size-${size}`]); + + const propsContext: PropsContext = { + Initials: { + className: styles.initials, + }, + Image: { + className: styles.image, + }, + }; + + return ( +
+ + {children} + +
+ ); +}; + +export default Avatar; diff --git a/packages/components/src/components/Avatar/index.ts b/packages/components/src/components/Avatar/index.ts new file mode 100644 index 000000000..b8693f5f3 --- /dev/null +++ b/packages/components/src/components/Avatar/index.ts @@ -0,0 +1,3 @@ +import { Avatar } from "./Avatar"; +export { Avatar } from "./Avatar"; +export default Avatar; diff --git a/packages/components/src/components/Avatar/stories/Default.stories.tsx b/packages/components/src/components/Avatar/stories/Default.stories.tsx new file mode 100644 index 000000000..fafe5b188 --- /dev/null +++ b/packages/components/src/components/Avatar/stories/Default.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Avatar from "../Avatar"; +import React from "react"; +import { Initials } from "@/components/Initials"; +import { Image } from "@/components/Image"; +import { dummyText } from "@/lib/dev/dummyText"; + +const meta: Meta = { + title: "Avatar", + component: Avatar, + render: (props) => ( + + Gopher + + ), + parameters: { + controls: { exclude: ["className"] }, + }, + argTypes: { + size: { + control: "inline-radio", + }, + }, +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithInitials: Story = { + render: (props) => ( + + Max Mustermann + + ), +}; diff --git a/packages/components/src/components/Avatar/stories/Variants.stories.tsx b/packages/components/src/components/Avatar/stories/Variants.stories.tsx new file mode 100644 index 000000000..b225757e1 --- /dev/null +++ b/packages/components/src/components/Avatar/stories/Variants.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Avatar from "../Avatar"; +import defaultMeta from "./Default.stories"; + +const meta: Meta = { + ...defaultMeta, + title: "Avatar/Variants", +}; +export default meta; + +type Story = StoryObj; + +export const SizeS: Story = {}; + +export const SizeM: Story = { args: { size: "m" } }; + +export const SizeL: Story = { args: { size: "l" } }; diff --git a/packages/components/src/components/Content/Content.tsx b/packages/components/src/components/Content/Content.tsx index 915e034a1..922dd4aaa 100644 --- a/packages/components/src/components/Content/Content.tsx +++ b/packages/components/src/components/Content/Content.tsx @@ -1,15 +1,14 @@ import React, { + ComponentProps, createElement, FC, - HTMLAttributes, PropsWithChildren, } from "react"; import styles from "./Content.module.css"; import clsx from "clsx"; import { ClearPropsContext, useProps } from "@/lib/propsContext"; -export interface ContentProps - extends PropsWithChildren> { +export interface ContentProps extends PropsWithChildren> { elementType?: string; } diff --git a/packages/components/src/components/Heading/Heading.tsx b/packages/components/src/components/Heading/Heading.tsx index 5d5fd999d..27e459353 100644 --- a/packages/components/src/components/Heading/Heading.tsx +++ b/packages/components/src/components/Heading/Heading.tsx @@ -1,10 +1,12 @@ -import { createElement, FC, HTMLAttributes, PropsWithChildren } from "react"; +import { ComponentProps, createElement, FC, PropsWithChildren } from "react"; import styles from "./Heading.module.css"; import clsx from "clsx"; import { useProps } from "@/lib/propsContext"; export interface HeadingProps - extends PropsWithChildren> { + extends PropsWithChildren< + ComponentProps<"h1" | "h2" | "h3" | "h4" | "h5" | "h6"> + > { level?: 1 | 2 | 3 | 4 | 5 | 6; } diff --git a/packages/components/src/components/Icon/Icon.tsx b/packages/components/src/components/Icon/Icon.tsx index 8d2d89270..99936d710 100644 --- a/packages/components/src/components/Icon/Icon.tsx +++ b/packages/components/src/components/Icon/Icon.tsx @@ -1,6 +1,6 @@ import React, { + ComponentProps, FC, - HTMLAttributes, PropsWithChildren, SVGAttributes, useMemo, @@ -46,7 +46,7 @@ export const Icon: FC = (props) => { * Icon is wrapped inside span, so it always behaves as an inline element * (line-height is applied), even if used in flex/grid layouts. */ - const spanProps: HTMLAttributes = { + const spanProps: ComponentProps<"span"> = { className: clsx(classNameFromProps, styles.root), }; diff --git a/packages/components/src/components/Image/Image.module.css b/packages/components/src/components/Image/Image.module.css new file mode 100644 index 000000000..696149055 --- /dev/null +++ b/packages/components/src/components/Image/Image.module.css @@ -0,0 +1,4 @@ +@import "@/styles"; + +.root { +} diff --git a/packages/components/src/components/Image/Image.tsx b/packages/components/src/components/Image/Image.tsx new file mode 100644 index 000000000..27b3c3fdf --- /dev/null +++ b/packages/components/src/components/Image/Image.tsx @@ -0,0 +1,16 @@ +import React, { ComponentProps, FC } from "react"; +import styles from "./Image.module.css"; +import clsx from "clsx"; +import { useProps } from "@/lib/propsContext"; + +export interface ImageProps extends ComponentProps<"img"> {} + +export const Image: FC = (props) => { + const { className, ...rest } = useProps("Image", props); + + const rootClassName = clsx(className, styles.root); + + return ; +}; + +export default Image; diff --git a/packages/components/src/components/Image/index.ts b/packages/components/src/components/Image/index.ts new file mode 100644 index 000000000..32fb85881 --- /dev/null +++ b/packages/components/src/components/Image/index.ts @@ -0,0 +1,3 @@ +import { Image } from "./Image"; +export { type ImageProps, Image } from "./Image"; +export default Image; diff --git a/packages/components/src/components/Image/stories/Default.stories.tsx b/packages/components/src/components/Image/stories/Default.stories.tsx new file mode 100644 index 000000000..f831fcdda --- /dev/null +++ b/packages/components/src/components/Image/stories/Default.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Image from "../Image"; +import React from "react"; +import { dummyText } from "@/lib/dev/dummyText"; + +const meta: Meta = { + title: "Image", + component: Image, + render: (props) => Gopher, +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/components/src/components/Initials/Initials.module.css b/packages/components/src/components/Initials/Initials.module.css new file mode 100644 index 000000000..4bbdbc971 --- /dev/null +++ b/packages/components/src/components/Initials/Initials.module.css @@ -0,0 +1,43 @@ +@import "@/styles"; + +.root { + display: flex; + font-weight: bold; + background-color: var(--background-color); +} + +.root > *:nth-child(odd) { + color: var(--char-1-color); +} + +.root > *:nth-child(even) { + color: var(--char-2-color); +} + +.root > *:only-child { + color: var(--char-2-color); +} + +.variant-1 { + --background-color: var(--initials--variant-1-background-color); + --char-1-color: var(--initials--variant-1-first-initial-color); + --char-2-color: var(--initials--variant-1-second-initial-color); +} + +.variant-2 { + --background-color: var(--initials--variant-2-background-color); + --char-1-color: var(--initials--variant-2-first-initial-color); + --char-2-color: var(--initials--variant-2-second-initial-color); +} + +.variant-3 { + --background-color: var(--initials--variant-3-background-color); + --char-1-color: var(--initials--variant-3-first-initial-color); + --char-2-color: var(--initials--variant-3-second-initial-color); +} + +.variant-4 { + --background-color: var(--initials--variant-4-background-color); + --char-1-color: var(--initials--variant-4-first-initial-color); + --char-2-color: var(--initials--variant-4-second-initial-color); +} diff --git a/packages/components/src/components/Initials/Initials.tsx b/packages/components/src/components/Initials/Initials.tsx new file mode 100644 index 000000000..143b70b0f --- /dev/null +++ b/packages/components/src/components/Initials/Initials.tsx @@ -0,0 +1,34 @@ +import React, { FC, PropsWithChildren } from "react"; +import { getVariantFromInitials } from "./lib/getVariantFromInitials"; +import { getInitialsFromString } from "./lib/getInitialsFromString"; +import styles from "./Initials.module.css"; +import clsx from "clsx"; +import { useProps } from "@/lib/propsContext"; + +export interface InitialsProps extends PropsWithChildren<{ children: string }> { + className?: string; +} + +export const Initials: FC = (props) => { + const { children, className } = useProps("Initials", props); + + const initials = getInitialsFromString(children); + + const rootClassName = clsx( + className, + styles.root, + styles[`variant-${getVariantFromInitials(initials)}`], + ); + + const initialsElements = initials.map((initial, index) => ( + {initial} + )); + + return ( +
+ {initialsElements} +
+ ); +}; + +export default Initials; diff --git a/packages/components/src/components/Initials/index.ts b/packages/components/src/components/Initials/index.ts new file mode 100644 index 000000000..e05242fef --- /dev/null +++ b/packages/components/src/components/Initials/index.ts @@ -0,0 +1,3 @@ +import { Initials } from "./Initials"; +export { type InitialsProps, Initials } from "./Initials"; +export default Initials; diff --git a/packages/components/src/components/Initials/lib/getInitialsFromString.test.ts b/packages/components/src/components/Initials/lib/getInitialsFromString.test.ts new file mode 100644 index 000000000..18c59f6e9 --- /dev/null +++ b/packages/components/src/components/Initials/lib/getInitialsFromString.test.ts @@ -0,0 +1,16 @@ +import { getInitialsFromString } from "./getInitialsFromString"; + +describe('"getInitialsFromString()', () => { + test("does return empty array if string is empty", () => { + expect(getInitialsFromString("")).toStrictEqual([]); + }); + + test.each([ + ["Max Mustermann", "MM"], + ["Max & Mustermann", "MM"], + ["Max (Mustermann)", "MM"], + ["Max", "M"], + ])("builds correct initials for %o", (item, expectedResult) => { + expect(getInitialsFromString(item).join("")).toBe(expectedResult); + }); +}); diff --git a/packages/components/src/components/Initials/lib/getInitialsFromString.ts b/packages/components/src/components/Initials/lib/getInitialsFromString.ts new file mode 100644 index 000000000..52439ad67 --- /dev/null +++ b/packages/components/src/components/Initials/lib/getInitialsFromString.ts @@ -0,0 +1,9 @@ +export const getInitialsFromString = (initials: string): string[] => { + return initials + .replace(/[^\p{L}\s]/giu, "") + .split(" ") + .map((part) => part.trim()[0]) + .filter((p) => p !== undefined) + .map((char) => char.toUpperCase()) + .slice(0, 2); +}; diff --git a/packages/components/src/components/Initials/lib/getVariantFromInitials.test.ts b/packages/components/src/components/Initials/lib/getVariantFromInitials.test.ts new file mode 100644 index 000000000..18e1c4954 --- /dev/null +++ b/packages/components/src/components/Initials/lib/getVariantFromInitials.test.ts @@ -0,0 +1,18 @@ +import { getVariantFromInitials } from "./getVariantFromInitials"; + +describe('"getVariantFromInitials()', () => { + test("does return 1 if array is empty", () => { + expect(getVariantFromInitials([])).toStrictEqual(1); + }); + + test.each([ + [["A"], 2], + [["B"], 3], + [["C", "D"], 4], + [["Z"], 3], + [["Ä"], 1], + [["1"], 2], + ])("does get correct variant for given initial", (item, expectedVariant) => { + expect(getVariantFromInitials(item)).toBe(expectedVariant); + }); +}); diff --git a/packages/components/src/components/Initials/lib/getVariantFromInitials.ts b/packages/components/src/components/Initials/lib/getVariantFromInitials.ts new file mode 100644 index 000000000..717161871 --- /dev/null +++ b/packages/components/src/components/Initials/lib/getVariantFromInitials.ts @@ -0,0 +1,6 @@ +export const getVariantFromInitials = (initials: string[]): number => { + if (initials.length < 1) { + return 1; + } + return (initials[0].charCodeAt(0) % 4) + 1; +}; diff --git a/packages/components/src/components/Initials/stories/Default.stories.tsx b/packages/components/src/components/Initials/stories/Default.stories.tsx new file mode 100644 index 000000000..118bf671a --- /dev/null +++ b/packages/components/src/components/Initials/stories/Default.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Initials from "../Initials"; +import React from "react"; + +const meta: Meta = { + title: "Initials", + component: Initials, + render: (props) => Max Mustermann, + parameters: { + controls: { exclude: ["className"] }, + }, +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const OneLetter: Story = { + render: (props) => Max , +}; diff --git a/packages/components/src/components/Initials/stories/Variants.stories.tsx b/packages/components/src/components/Initials/stories/Variants.stories.tsx new file mode 100644 index 000000000..c930326d1 --- /dev/null +++ b/packages/components/src/components/Initials/stories/Variants.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Initials from "../Initials"; +import React from "react"; +import defaultMeta from "./Default.stories"; + +const meta: Meta = { + ...defaultMeta, + title: "Initials/Variants", +}; +export default meta; + +type Story = StoryObj; + +export const Variant1: Story = { + render: (props) => Max Mustermann, +}; + +export const Variant2: Story = { + render: (props) => Bettina Mustermann, +}; + +export const Variant3: Story = { + render: (props) => Daniel Mustermann, +}; + +export const Variant4: Story = { + render: (props) => Karla Mustermann, +}; diff --git a/packages/components/src/components/Note/Note.tsx b/packages/components/src/components/Note/Note.tsx index f2149c0f1..229d1309a 100644 --- a/packages/components/src/components/Note/Note.tsx +++ b/packages/components/src/components/Note/Note.tsx @@ -1,4 +1,4 @@ -import React, { FC, HTMLAttributes, PropsWithChildren } from "react"; +import React, { ComponentProps, FC, PropsWithChildren } from "react"; import { PropsContext, PropsContextProvider, @@ -10,7 +10,7 @@ import { StatusIcon } from "@/components/StatusIcon"; import { StatusVariantProps } from "@/lib/types/props"; export interface NoteProps - extends PropsWithChildren>, + extends PropsWithChildren>, StatusVariantProps<"success"> {} export const Note: FC = (props) => { diff --git a/packages/components/src/components/propTypes/index.ts b/packages/components/src/components/propTypes/index.ts index 457dc027e..4a76db0a1 100644 --- a/packages/components/src/components/propTypes/index.ts +++ b/packages/components/src/components/propTypes/index.ts @@ -12,6 +12,8 @@ import { FieldErrorProps } from "@/components/FieldError"; import { FieldDescriptionProps } from "@/components/FieldDescription"; import { NoteProps } from "@/components/Note"; import { HeadingProps } from "@/components/Heading"; +import { InitialsProps } from "@/components/Initials"; +import { ImageProps } from "@/components/Image"; export * from "./types"; @@ -26,6 +28,8 @@ export interface FlowComponentPropsTypes { Heading: HeadingProps; Note: NoteProps; Link: LinkProps; + Initials: InitialsProps; + Image: ImageProps; FieldError: FieldErrorProps; FieldDescription: FieldDescriptionProps; } diff --git a/packages/components/src/lib/dev/dummyText.ts b/packages/components/src/lib/dev/dummyText.ts index 0ef592a8a..e18a21dfe 100755 --- a/packages/components/src/lib/dev/dummyText.ts +++ b/packages/components/src/lib/dev/dummyText.ts @@ -3,4 +3,6 @@ export const dummyText = { medium: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque eius quam quas vel voluptas, ullam aliquid fugit. Voluptate harum accusantium rerum ullam modi blanditiis vitae.", long: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque eius quam quas vel voluptas, ullam aliquid fugit. Voluptate harum accusantium rerum ullam modi blanditiis vitae, laborum ea tempore, dolore voluptas. Earum pariatur, similique corrupti id officia perferendis. Labore, similique. Earum, quas in. At dolorem corrupti blanditiis nulla deserunt laborum! Corrupti delectus aspernatur nihil nulla obcaecati ipsam porro sequi rem? Quam.", + imageSrc: + "https://cdn.shopify.com/s/files/1/2022/6883/products/IMG_2002_250x250@2x.JPG?v=1538235544", }; diff --git a/packages/design-tokens/src/border.yml b/packages/design-tokens/src/border.yml index 36c5c65a2..07f541c92 100644 --- a/packages/design-tokens/src/border.yml +++ b/packages/design-tokens/src/border.yml @@ -2,7 +2,7 @@ border-radius: default: value: "{size-px.xs}" round: - value: "50%" + value: 50% border-width: default: value: 1px diff --git a/packages/design-tokens/src/color-palette.yml b/packages/design-tokens/src/color-palette.yml index e19b6bab3..52bc001a0 100644 --- a/packages/design-tokens/src/color-palette.yml +++ b/packages/design-tokens/src/color-palette.yml @@ -143,6 +143,30 @@ color: 1100: value: "#BF393C" + soft-contrast-violet: + 100: + value: "#eeecff" + 200: + value: "#eeecff" + 300: + value: "#eeecff" + 400: + value: "#dbd7f8" + 500: + value: "#b8aef7" + 600: + value: "#9485f2" + 700: + value: "#715dee" + 800: + value: "#5c50ca" + 900: + value: "#4743a5" + 1000: + value: "#4743a5" + 1100: + value: "#4743a5" + white: value: rgb(255,255,255) diff --git a/packages/design-tokens/src/components/avatar.yml b/packages/design-tokens/src/components/avatar.yml new file mode 100644 index 000000000..3c6f2f698 --- /dev/null +++ b/packages/design-tokens/src/components/avatar.yml @@ -0,0 +1,8 @@ +avatar: + size: + s: + value: 2.5rem + m: + value: 6rem + l: + value: 9.75rem diff --git a/packages/design-tokens/src/components/icon.yml b/packages/design-tokens/src/components/icon.yml index c99eff1b2..ec3c7a3d8 100644 --- a/packages/design-tokens/src/components/icon.yml +++ b/packages/design-tokens/src/components/icon.yml @@ -4,5 +4,5 @@ icon: value: 1rem m: value: 1.25rem - xl: + l: value: 4rem diff --git a/packages/design-tokens/src/components/initials.yml b/packages/design-tokens/src/components/initials.yml new file mode 100644 index 000000000..ef4ea1261 --- /dev/null +++ b/packages/design-tokens/src/components/initials.yml @@ -0,0 +1,35 @@ +initials: + font-weight: + value: "{font-weight.bold}" + + # Variant 1 + variant-1-background-color: + value: "{color.soft-contrast-violet.100}" + variant-1-first-initial-color: + value: "{color.soft-contrast-violet.600}" + variant-1-second-initial-color: + value: "{color.soft-contrast-violet.900}" + + # Variant 2 + variant-2-background-color: + value: "{color.royal-orange.200}" + variant-2-first-initial-color: + value: "{color.royal-orange.600}" + variant-2-second-initial-color: + value: "{color.royal-orange.900}" + + # Variant 3 + variant-3-background-color: + value: "{color.espelkamp-green.100}" + variant-3-first-initial-color: + value: "{color.espelkamp-green.600}" + variant-3-second-initial-color: + value: "{color.espelkamp-green.900}" + + # Variant 4 + variant-4-background-color: + value: "{color.hosting-blue.100}" + variant-4-first-initial-color: + value: "{color.hosting-blue.600}" + variant-4-second-initial-color: + value: "{color.hosting-blue.900}" diff --git a/packages/design-tokens/src/components/radio-group.yml b/packages/design-tokens/src/components/radio-group.yml index 306866b07..3feafc6bc 100644 --- a/packages/design-tokens/src/components/radio-group.yml +++ b/packages/design-tokens/src/components/radio-group.yml @@ -52,4 +52,4 @@ radio-group: label-font-weight-with-content-or-icon: value: "{font-weight.bold}" icon-size-without-content: - value: "{icon.size.xl}" + value: "{icon.size.l}"