Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create PersonListItem component #1071

Merged
merged 2 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/experimental/Information/Tags/DotTag/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ type NewColor = Extract<
| "camel"
>

interface Props {
export interface DotTagProps {
text: string
color: NewColor
}

export const DotTag = forwardRef<HTMLDivElement, Props>(
export const DotTag = forwardRef<HTMLDivElement, DotTagProps>(
({ text, color }, ref) => {
useTextFormatEnforcer(text, { disallowEmpty: true })

Expand Down
11 changes: 7 additions & 4 deletions lib/experimental/Information/Tags/RawTag/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ import { cn } from "@/lib/utils"
import { forwardRef } from "react"
import { BaseTag } from "../BaseTag"

type Props = {
export type RawTagProps = {
text?: string
additionalAccesibleText?: string
icon?: IconType
noBorder?: boolean
}

export const RawTag = forwardRef<HTMLDivElement, Props>(
({ text, additionalAccesibleText, icon }, ref) => {
export const RawTag = forwardRef<HTMLDivElement, RawTagProps>(
({ text, additionalAccesibleText, icon, noBorder }, ref) => {
useTextFormatEnforcer(text, { disallowEmpty: true })

return (
<BaseTag
ref={ref}
className={cn("border-[1px] border-solid border-f1-border-secondary")}
className={cn(
!noBorder && "border-[1px] border-solid border-f1-border-secondary"
)}
left={
icon ? (
<Icon icon={icon} size="sm" className="text-f1-icon" aria-hidden />
Expand Down
4 changes: 2 additions & 2 deletions lib/experimental/Information/Tags/StatusTag/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type Variant = "neutral" | "info" | "positive" | "warning" | "critical"

export type StatusVariant = Variant

interface Props {
export interface StatusTagProps {
text: string
variant: Variant
/**
Expand All @@ -17,7 +17,7 @@ interface Props {
additionalAccesibleText?: string
}

export const StatusTag = forwardRef<HTMLDivElement, Props>(
export const StatusTag = forwardRef<HTMLDivElement, StatusTagProps>(
({ text, additionalAccesibleText, variant }, ref) => {
useTextFormatEnforcer(text, { disallowEmpty: true })

Expand Down
80 changes: 80 additions & 0 deletions lib/experimental/Lists/PersonListItem/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Check, Placeholder } from "@/icons/app"
import type { Meta, StoryObj } from "@storybook/react"
import { PersonListItem } from "./index"

const meta = {
title: "Experimental/Lists/PersonListItem",
component: PersonListItem,
tags: ["autodocs"],
} satisfies Meta<typeof PersonListItem>

export default meta
type Story = StoryObj<typeof PersonListItem>

export const Default: Story = {
args: {
person: {
firstName: "John",
lastName: "Smith",
avatarUrl: "https://i.pravatar.cc/300",
avatarBadge: {
icon: Check,
type: "positive",
},
},
description: "Software Engineer",
rightTag: {
text: "Parental leave",
color: "army",
},
},
}

export const WithActions: Story = {
args: {
person: {
firstName: "Sarah",
lastName: "Johnson",
avatarUrl: "https://i.pravatar.cc/300",
},
description: "Product Designer",
actions: {
primary: {
icon: Placeholder,
label: "Message",
onClick: () => console.log("Primary action clicked"),
},
secondary: {
icon: Placeholder,
onClick: () => console.log("Secondary action clicked"),
},
},
},
}

export const WithTags: Story = {
args: {
person: {
firstName: "Emma",
lastName: "Wilson",
avatarUrl: "https://i.pravatar.cc/300",
},
bottomTags: [
{ text: "Label", icon: Placeholder },
{ text: "Label", icon: Placeholder },
{ text: "Label", icon: Placeholder },
{ text: "Label", icon: Placeholder },
],
},
}

export const Minimal: Story = {
args: {
person: {
firstName: "Emma",
lastName: "Wilson",
avatarUrl: "https://i.pravatar.cc/300",
},
description: "Software Engineer",
},
}
112 changes: 112 additions & 0 deletions lib/experimental/Lists/PersonListItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Button } from "@/components/Actions/Button"
import { Icon, IconType } from "@/components/Utilities/Icon"
import { BadgeProps } from "@/experimental/exports"
import { PersonAvatar } from "@/experimental/Information/Avatars/PersonAvatar"
import { DotTag, DotTagProps } from "@/experimental/Information/Tags/DotTag"
import { RawTag, RawTagProps } from "@/experimental/Information/Tags/RawTag"
import { InfoCircle } from "@/icons/app"
import React from "react"

export type PersonListItemProps = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we simplify the type? I am lost with logical AND and OR props, it's quite puzzling

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sure, I'll try to simplify it

person: {
firstName: string
lastName: string
avatarUrl?: string
avatarBadge?: Omit<BadgeProps, "size">
}
description?: string
bottomTags: Omit<RawTagProps, "noBorder">[]
rightTag?: DotTagProps
actions?: {
primary?: {
icon?: IconType
label: string
onClick: () => void
}
secondary?: {
icon: IconType
onClick: () => void
}
}
onClick: () => void
}

export const PersonListItem = React.forwardRef<
HTMLDivElement,
PersonListItemProps
>(({ person, onClick, ...props }, ref) => {
const handleClick = () => {
onClick()
}

return (
<div
ref={ref}
className="flex w-full flex-row flex-wrap items-center gap-2 rounded-md border p-2 hover:bg-f1-background-hover focus:outline focus:outline-1 focus:outline-offset-1 focus:outline-f1-border-selected-bold"
onClick={handleClick}
>
<PersonAvatar
firstName={person.firstName}
lastName={person.lastName}
src={person.avatarUrl}
badge={person.avatarBadge}
/>

<div className="flex flex-1 flex-col">
<div className="flex flex-1 flex-row items-center gap-1">
<span className="truncate font-medium">{`${person.firstName} ${person.lastName}`}</span>
<Icon
icon={InfoCircle}
size="sm"
className="text-f1-icon-secondary"
/>
</div>
{"bottomTags" in props && (
<div className="-ml-1.5 flex flex-row items-center text-f1-foreground-secondary [&>div]:-mr-1">
{props.bottomTags.map((tag, i) => (
<>
<RawTag key={tag.text} {...tag} noBorder />
{i < props.bottomTags.length - 1 && <span>·</span>}
</>
))}
</div>
)}
{"description" in props && props.description && (
<p className="truncate text-f1-foreground-secondary">
{props.description}
</p>
)}
</div>

<div className="flex flex-row items-center justify-between gap-2">
{"rightTag" in props && props.rightTag && (
<DotTag {...props.rightTag} />
)}
{"actions" in props && (
<div className="flex flex-1 flex-row items-center justify-end gap-2">
{props.actions?.primary && (
<Button
variant="outline"
onClick={props.actions.primary.onClick}
label={props.actions.primary.label}
icon={props.actions.primary.icon}
/>
)}

{props.actions?.secondary && (
<Button
variant="outline"
onClick={props.actions.secondary.onClick}
label="Secondary"
icon={props.actions.secondary.icon}
hideLabel
/>
)}
</div>
)}
</div>
</div>
)
})

PersonListItem.displayName = "PersonListItem"
Loading