From 8c89af05cf761461d3134772668cb6d9f2667482 Mon Sep 17 00:00:00 2001 From: jrakibi Date: Sun, 26 Oct 2025 22:42:25 +0100 Subject: [PATCH 1/2] add pills --- src/components/pill/Pill.stories.tsx | 138 +++++++++++++++++++++++++++ src/components/pill/Pill.tsx | 114 ++++++++++++++++++++++ src/components/pill/index.tsx | 3 + src/index.ts | 1 + 4 files changed, 256 insertions(+) create mode 100644 src/components/pill/Pill.stories.tsx create mode 100644 src/components/pill/Pill.tsx create mode 100644 src/components/pill/index.tsx diff --git a/src/components/pill/Pill.stories.tsx b/src/components/pill/Pill.stories.tsx new file mode 100644 index 0000000..e069cde --- /dev/null +++ b/src/components/pill/Pill.stories.tsx @@ -0,0 +1,138 @@ +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; + +export default meta; +type Story = StoryObj; + +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: () => ( +
+
+

Pill : default

+ Learn +
+
+

Pill : hover

+ Learn +
+
+

Pill : Selected

+ Learn +
+
+ ), + 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: () => ( +
+ Learn + Learn + Learn +
+ ), + parameters: { + docs: { + description: { + story: "All pill states displayed in a row for comparison.", + }, + }, + }, +}; + +export const InteractiveDemo: Story = { + render: () => ( +
+

Click to toggle the state:

+ console.log("Pill clicked!")}>Clickable Pill +
+ ), + parameters: { + docs: { + description: { + story: + "Interactive pill that changes state when clicked. Try hovering and clicking!", + }, + }, + }, +}; + +export const MultiplePills: Story = { + render: () => ( +
+ Learn + Discover + Explore + Build + Create +
+ ), + parameters: { + docs: { + description: { + story: "Multiple pills with different text labels.", + }, + }, + }, +}; + +export const WithCustomText: Story = { + args: { + children: "Custom Text", + }, +}; + diff --git a/src/components/pill/Pill.tsx b/src/components/pill/Pill.tsx new file mode 100644 index 0000000..e2e9a01 --- /dev/null +++ b/src/components/pill/Pill.tsx @@ -0,0 +1,114 @@ +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 = ({ + 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 ( +
{ + 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(); + } + }} + > + {children} +
+ ); +}; + +Pill.propTypes = { + state: PropTypes.oneOf(["default", "hover", "selected"]), + onClick: PropTypes.func, + className: PropTypes.string, + children: PropTypes.node, + selected: PropTypes.bool, + onSelectChange: PropTypes.func, +}; + diff --git a/src/components/pill/index.tsx b/src/components/pill/index.tsx new file mode 100644 index 0000000..420ed0a --- /dev/null +++ b/src/components/pill/index.tsx @@ -0,0 +1,3 @@ +export { Pill } from "./Pill"; +export type { PillProps, PillState } from "./Pill"; + diff --git a/src/index.ts b/src/index.ts index 3eabadb..073afeb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ export * from "./components/select"; export * from "./components/banner"; export * from "./components/search"; export * from "./components/tag"; +export * from "./components/pill"; From bebffb182433d76d7aa84f8f695a65c200ba89db Mon Sep 17 00:00:00 2001 From: jrakibi Date: Sun, 26 Oct 2025 22:49:23 +0100 Subject: [PATCH 2/2] format code --- src/components/pill/Pill.stories.tsx | 1 - src/components/pill/Pill.tsx | 1 - src/components/pill/index.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/src/components/pill/Pill.stories.tsx b/src/components/pill/Pill.stories.tsx index e069cde..4fff699 100644 --- a/src/components/pill/Pill.stories.tsx +++ b/src/components/pill/Pill.stories.tsx @@ -135,4 +135,3 @@ export const WithCustomText: Story = { children: "Custom Text", }, }; - diff --git a/src/components/pill/Pill.tsx b/src/components/pill/Pill.tsx index e2e9a01..216c7cb 100644 --- a/src/components/pill/Pill.tsx +++ b/src/components/pill/Pill.tsx @@ -111,4 +111,3 @@ Pill.propTypes = { selected: PropTypes.bool, onSelectChange: PropTypes.func, }; - diff --git a/src/components/pill/index.tsx b/src/components/pill/index.tsx index 420ed0a..d14bcf0 100644 --- a/src/components/pill/index.tsx +++ b/src/components/pill/index.tsx @@ -1,3 +1,2 @@ export { Pill } from "./Pill"; export type { PillProps, PillState } from "./Pill"; -