Skip to content
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
137 changes: 137 additions & 0 deletions src/components/pill/Pill.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { Pill } from "./Pill";

const meta = {
title: "Components/Pill",
component: Pill,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
state: {
control: "select",
options: ["default", "hover", "selected"],
description: "Visual state of the pill",
},
children: {
control: "text",
description: "Pill label text",
},
onClick: {
action: "clicked",
description: "Callback function when pill is clicked",
},
},
} satisfies Meta<typeof Pill>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
state: "default",
children: "Learn",
},
};

export const Hover: Story = {
args: {
state: "hover",
children: "Learn",
},
};

export const Selected: Story = {
args: {
state: "selected",
children: "Learn",
},
};

export const AllStates: Story = {
render: () => (
<div className="flex flex-col gap-4 p-8">
<div className="flex flex-col gap-2">
<h3 className="text-sm font-semibold">Pill : default</h3>
<Pill state="default">Learn</Pill>
</div>
<div className="flex flex-col gap-2">
<h3 className="text-sm font-semibold">Pill : hover</h3>
<Pill state="hover">Learn</Pill>
</div>
<div className="flex flex-col gap-2">
<h3 className="text-sm font-semibold">Pill : Selected</h3>
<Pill state="selected">Learn</Pill>
</div>
</div>
),
parameters: {
docs: {
description: {
story:
"All pill states: default (light background), hover (medium background), and selected (dark background with white text).",
},
},
},
};

export const AllStatesInline: Story = {
render: () => (
<div className="flex gap-4 items-center p-8">
<Pill state="default">Learn</Pill>
<Pill state="hover">Learn</Pill>
<Pill state="selected">Learn</Pill>
</div>
),
parameters: {
docs: {
description: {
story: "All pill states displayed in a row for comparison.",
},
},
},
};

export const InteractiveDemo: Story = {
render: () => (
<div className="flex flex-col gap-4">
<p className="text-sm text-gray-600">Click to toggle the state:</p>
<Pill onClick={() => console.log("Pill clicked!")}>Clickable Pill</Pill>

Check warning on line 101 in src/components/pill/Pill.stories.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
</div>
),
parameters: {
docs: {
description: {
story:
"Interactive pill that changes state when clicked. Try hovering and clicking!",
},
},
},
};

export const MultiplePills: Story = {
render: () => (
<div className="flex gap-2 flex-wrap">
<Pill>Learn</Pill>
<Pill>Discover</Pill>
<Pill>Explore</Pill>
<Pill>Build</Pill>
<Pill>Create</Pill>
</div>
),
parameters: {
docs: {
description: {
story: "Multiple pills with different text labels.",
},
},
},
};

export const WithCustomText: Story = {
args: {
children: "Custom Text",
},
};
113 changes: 113 additions & 0 deletions src/components/pill/Pill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { cn } from "../../utils/cn";

export type PillState = "default" | "hover" | "selected";

export interface PillProps {
state?: PillState;
onClick?: () => void;
className?: string;
children?: React.ReactNode;
selected?: boolean;
onSelectChange?: (selected: boolean) => void;
}

// Figma design colors
const pillColors = {
default: {
backgroundColor: "transparent",
textColor: "rgba(32, 30, 30, 0.6)", // #201E1E at 60% opacity
borderColor: "rgba(32, 30, 30, 0.6)", // #201E1E at 60% opacity
},
hover: {
backgroundColor: "rgba(169, 164, 155, 0.3)", // #A9A49B at 30% opacity
textColor: "rgba(32, 30, 30, 0.6)", // #201E1E at 60% opacity
borderColor: "rgba(32, 30, 30, 0.6)", // #201E1E at 60% opacity
},
selected: {
backgroundColor: "#201E1E", // Solid dark color
textColor: "#F6F0E6", // Light beige text
borderColor: "#201E1E", // Solid dark border
},
} as const;

export const Pill: React.FC<PillProps> = ({
state,
onClick,
className,
children,
selected: controlledSelected,
onSelectChange,
}) => {
const [internalSelected, setInternalSelected] = useState(false);

const isSelected =
controlledSelected !== undefined ? controlledSelected : internalSelected;

const effectiveState = state ?? (isSelected ? "selected" : "default");
const isForcedState =
effectiveState === "selected" || effectiveState === "hover";

const handleClick = () => {
if (state === undefined) {
const newSelected = !isSelected;
setInternalSelected(newSelected);
onSelectChange?.(newSelected);
}
onClick?.();
};

const colors = pillColors[effectiveState];

return (
<div
onClick={handleClick}
className={cn(
"inline-flex items-center justify-center px-2 py-1 transition-all duration-200 cursor-pointer border text-sm font-medium",
className,
)}
style={{
backgroundColor: colors.backgroundColor,
borderColor: colors.borderColor,
color: colors.textColor,
borderWidth: "1px",
borderRadius: "12px",
}}
data-state={effectiveState}
onMouseEnter={(e) => {
if (!isForcedState && !isSelected) {
const target = e.currentTarget;
target.style.backgroundColor = pillColors.hover.backgroundColor;
target.style.borderColor = pillColors.hover.borderColor;
}
}}
onMouseLeave={(e) => {
if (!isForcedState && !isSelected) {
const target = e.currentTarget;
target.style.backgroundColor = colors.backgroundColor;
target.style.borderColor = colors.borderColor;
}
}}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick();
}
}}
>
<span>{children}</span>
</div>
);
};

Pill.propTypes = {
state: PropTypes.oneOf(["default", "hover", "selected"]),
onClick: PropTypes.func,
className: PropTypes.string,
children: PropTypes.node,
selected: PropTypes.bool,
onSelectChange: PropTypes.func,
};
2 changes: 2 additions & 0 deletions src/components/pill/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Pill } from "./Pill";
export type { PillProps, PillState } from "./Pill";
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./components/select";
export * from "./components/banner";
export * from "./components/search";
export * from "./components/tag";
export * from "./components/pill";