Skip to content

Commit

Permalink
Add Blurb component
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrkulpinski committed Jan 15, 2024
1 parent ee8597c commit 7ad6843
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 35 deletions.
Binary file modified bun.lockb
Binary file not shown.
35 changes: 1 addition & 34 deletions src/shared/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactNode} from "react";
import type { ReactNode } from "react"
import { Children, isValidElement } from "react"

/**
Expand All @@ -20,36 +20,3 @@ export const isChildrenEmpty = (children: ReactNode) => {
export const isReactElement = (element: React.ReactNode): element is React.ReactElement => {
return isValidElement(element)
}

/**
* Check if a value is truthy
* @param value - The value to check
* @returns A boolean indicating if the value is truthy
*/
export function isTruthy<T>(value?: T | undefined | null | false): value is T {
return !!value
}

/**
* Get the initials from a string
* @param value A string to get the initials from
* @param limit The maximum number of initials to return
* @returns The initials from the string
*/
export const getInitials = (value?: string | null, limit = 0) => {
const val = (value || "").trim()

// If the value is empty, a single character, or two characters (already initials)
if (val.length === 0 || val.length === 1 || val.length === 2) {
return val.toUpperCase()
}

const values = val.split(" ").filter(isTruthy)
const initials = values.map((name) => name.charAt(0).toUpperCase()).join("")

if (limit > 0) {
return initials.slice(0, limit)
}

return initials
}
3 changes: 2 additions & 1 deletion src/ui/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getInitials } from "@curiousleaf/utils"
import * as Primitive from "@radix-ui/react-avatar"
import { Slot } from "@radix-ui/react-slot"
import { IconUser } from "@tabler/icons-react"
Expand All @@ -6,7 +7,7 @@ import type { ComponentPropsWithoutRef, ElementRef, ReactElement, RefObject } fr

import { useTheme } from "~/providers"
import { type VariantProps, cx } from "~/shared/cva"
import { getInitials, isReactElement } from "~/shared/helpers"
import { isReactElement } from "~/shared/helpers"
import { Loader } from "~/ui/Loader"

import {
Expand Down
50 changes: 50 additions & 0 deletions src/ui/Blurb/Blurb.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Meta, StoryObj } from "@storybook/react"
import { IconUserBolt } from "@tabler/icons-react"

import { Blurb } from "./Blurb"

type Story = StoryObj<typeof Blurb>

// Meta
export default {
title: "UI/Blurb",
component: Blurb,
args: {
...Blurb.defaultProps,
avatar: {
src: "https://images.unsplash.com/photo-1517841905240-472988babdf9?q=80&w=250&h=250&auto=format&fit=crop",
size: "lg",
},
title: "John Doe",
description: "Software Engineer",
},
} satisfies Meta

// Stories
export const Default = {
args: {},
} satisfies Story

export const WithInitials = {
args: {
avatar: {
initials: "John Doe",
size: "lg",
},
},
} satisfies Story

export const WithCustomMarkup = {
render: ({ avatar, title, description }) => (
<Blurb.Root className="rounded-md border p-3">
<IconUserBolt className="text-xs" />

<Blurb.Content>
<Blurb.Description>{description}</Blurb.Description>
<Blurb.Title>{title}</Blurb.Title>
</Blurb.Content>

<Blurb.Avatar {...avatar} />
</Blurb.Root>
),
} satisfies Story
132 changes: 132 additions & 0 deletions src/ui/Blurb/Blurb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Slot } from "@radix-ui/react-slot"
import { forwardRef, isValidElement } from "react"
import type { ComponentPropsWithoutRef } from "react"

import { type VariantProps, cx } from "~/shared/cva"
import { Avatar, type AvatarElement, type AvatarProps } from "~/ui/Avatar"

import {
blurbContentVariants,
blurbVariants,
blurbDescriptionVariants,
blurbTitleVariants,
} from "./Blurb.variants"

export type BlurbElement = HTMLDivElement

type BlurbRootProps = ComponentPropsWithoutRef<"div"> &
VariantProps<typeof blurbVariants> & {
/**
* If set to `true`, the button will be rendered as a child within the component.
* This child component must be a valid React component.
*/
asChild?: boolean
}

export type BlurbProps = BlurbRootProps & {
/**
* Represents the avatar displayed on the Blurb.
*/
avatar?: AvatarProps

/**
* Represents the title displayed on the Blurb.
*/
title?: string

/**
* Represents the description displayed on the Blurb.
*/
description?: string
}

const BlurbRoot = forwardRef<BlurbElement, BlurbRootProps>(
({ className, asChild, ...props }, ref) => {
const useAsChild = asChild && isValidElement(props.children)
const Component = useAsChild ? Slot : "div"

return <Component ref={ref} className={cx(blurbVariants({ className }))} {...props} />
},
)

const BlurbAvatar = forwardRef<AvatarElement, AvatarProps>(({ ...props }, ref) => {
return <Avatar ref={ref} {...props} />
})

const BlurbContent = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<"div"> & VariantProps<typeof blurbContentVariants>
>(({ className, ...props }, ref) => {
return <div ref={ref} className={cx(blurbContentVariants({ className }))} {...props} />
})

const BlurbTitle = forwardRef<
HTMLSpanElement,
ComponentPropsWithoutRef<"span"> & VariantProps<typeof blurbTitleVariants>
>(({ children, className, ...rest }, ref) => {
if (!children) {
return null
}

return (
<span ref={ref} className={cx(blurbTitleVariants({ className }))} {...rest}>
{children}
</span>
)
})

const BlurbDescription = forwardRef<
HTMLSpanElement,
ComponentPropsWithoutRef<"span"> & VariantProps<typeof blurbDescriptionVariants>
>(({ children, className, ...rest }, ref) => {
if (!children) {
return null
}

return (
<span ref={ref} className={cx(blurbDescriptionVariants({ className }))} {...rest}>
{children}
</span>
)
})

const BlurbBase = forwardRef<BlurbElement, BlurbProps>((props, ref) => {
const { children, avatar, title, description, ...rest } = props

return (
<BlurbRoot ref={ref} {...rest}>
{avatar && <BlurbAvatar {...avatar} />}

{(title || description) && (
<BlurbContent>
<BlurbTitle>{title}</BlurbTitle>
<BlurbDescription>{description}</BlurbDescription>
</BlurbContent>
)}

{children}
</BlurbRoot>
)
})

BlurbBase.displayName = "Blurb"
BlurbRoot.displayName = "BlurbRoot"
BlurbAvatar.displayName = "BlurbAvatar"
BlurbContent.displayName = "BlurbContent"
BlurbTitle.displayName = "BlurbTitle"
BlurbDescription.displayName = "BlurbDescription"

export const Blurb = Object.assign(BlurbBase, {
Root: BlurbRoot,
Avatar: BlurbAvatar,
Content: BlurbContent,
Title: BlurbTitle,
Description: BlurbDescription,
})

Blurb.defaultProps = {
avatar: Avatar.defaultProps,
title: "",
description: "",
asChild: false,
}
17 changes: 17 additions & 0 deletions src/ui/Blurb/Blurb.variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { cva } from "~/shared/cva"

export const blurbVariants = cva({
base: "flex items-center gap-3 text-start",
})

export const blurbContentVariants = cva({
base: "flex min-w-0 flex-1 flex-col gap-0.5",
})

export const blurbTitleVariants = cva({
base: "text-sm font-medium truncate",
})

export const blurbDescriptionVariants = cva({
base: "text-xs leading-tight opacity-60 truncate",
})
2 changes: 2 additions & 0 deletions src/ui/Blurb/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Blurb } from "./Blurb"
export type { BlurbProps, BlurbElement } from "./Blurb"

0 comments on commit 7ad6843

Please sign in to comment.