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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ export default defineConfig([
])
```

<!-- 27 31 33 34 35 -->
<!-- 27 33 -->
105 changes: 105 additions & 0 deletions src/Chip/Chip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { Meta, StoryObj } from '@storybook/react-vite'

import { Chip, ChipGroup } from '.'

import { useState } from 'react'
import { fn } from 'storybook/test'

const meta = {
title: 'Shared/Primitives/Chip',

component: Chip,

tags: ['autodocs'],
} satisfies Meta<typeof Chip>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
label: 'Escalated',
onSelect: fn(),
},
}

export const Selected: Story = {
args: {
label: 'Escalated',
onSelect: fn(),

isSelected: true,
},
}

export const WithCount: Story = {
args: {
label: 'Spam',
onSelect: fn(),

count: 12,
},
}

export const Disabled: Story = {
args: {
label: 'Disabled',
onSelect: fn(),

isDisabled: true,
},
}

export const Dismissible: Story = {
args: {
label: 'Active Filter',
onSelect: fn(),

onDismiss: () => {
console.log('Dismiss clicked')
},
},
}

export const Group: StoryObj = {
render: () => {
const [selected, setSelected] = useState('all')

return (
<div className="max-w-[500px]">
<ChipGroup
selected={selected}
onChange={setSelected}
options={[
{
label: 'All',
value: 'all',
},

{
label: 'Escalated',
value: 'escalated',
count: 1,
},

{
label: 'Spam',
value: 'spam',
},

{
label: 'Hate speech',
value: 'hate',
},

{
label: 'Misinformation',
value: 'misinformation',
},
]}
/>
</div>
)
},
}
147 changes: 147 additions & 0 deletions src/Chip/Chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react'

import { cva } from 'class-variance-authority'

import { XMarkIcon } from '@heroicons/react/24/solid'

const chipStyles = cva(
`
inline-flex
items-center
gap-2
rounded-full
border
px-4
py-2
text-sm
transition-colors
whitespace-nowrap
select-none
focus:outline-none
`,
{
variants: {
isSelected: {
true: `
bg-text-tertiary/20
text-text-primary
border-border-secondary
font-medium
`,

false: `
bg-transparent
text-text-secondary
border-border-tertiary
font-normal
hover:bg-bg-secondary
hover:text-text-primary
`,
},

isDisabled: {
true: `
opacity-50
pointer-events-none
`,

false: `
cursor-pointer
`,
},
},

defaultVariants: {
isSelected: false,
isDisabled: false,
},
}
)

export interface ChipProps {
label: string

isSelected?: boolean

isDisabled?: boolean

onSelect?: () => void

onDismiss?: () => void

count?: number

className?: string
}

export function Chip({
label,

isSelected = false,

isDisabled = false,

onSelect,

onDismiss,

count,

className = '',
}: ChipProps) {
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()

onSelect?.()
}
}

return (
<button
type="button"
onClick={onSelect}
onKeyDown={handleKeyDown}
disabled={isDisabled}
className={chipStyles({
isSelected,
isDisabled,
className,
})}
>
<span>{label}</span>

{typeof count === 'number' && (
<span className="bg-bg-tertiary text-text-secondary rounded-full px-2 py-0.5 text-xs">
{count}
</span>
)}

{onDismiss && (
<span
role="button"
tabIndex={0}
onClick={(e) => {
e.stopPropagation()

onDismiss()
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()

e.stopPropagation()

onDismiss()
}
}}
className="text-text-secondary hover:text-text-primary flex items-center justify-center"
>
<XMarkIcon className="size-4" />
</span>
)}
</button>
)
}

export default Chip
45 changes: 45 additions & 0 deletions src/Chip/ChipGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'

import { Chip } from './Chip'

export interface ChipGroupOption {
label: string

value: string

count?: number
}

export interface ChipGroupProps {
options: ChipGroupOption[]

selected: string

onChange: (value: string) => void
}

export function ChipGroup({
options,

selected,

onChange,
}: ChipGroupProps) {
return (
<div className="scrollbar-hide flex gap-3 overflow-x-auto whitespace-nowrap">
{options.map((option) => (
<Chip
key={option.value}
label={option.label}
count={option.count}
isSelected={selected === option.value}
onSelect={() => {
onChange(option.value)
}}
/>
))}
</div>
)
}

export default ChipGroup
7 changes: 7 additions & 0 deletions src/Chip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { Chip } from './Chip'

export type { ChipProps } from './Chip'

export { ChipGroup } from './ChipGroup'

export type { ChipGroupProps, ChipGroupOption } from './ChipGroup'
42 changes: 40 additions & 2 deletions src/LoginCard.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,52 @@
import { ProgressBar } from './shared/primitives/Progressbar'
import { useState } from 'react'
import { ChipGroup } from './Chip'
// import { ProgressBar } from './shared/primitives/Progressbar'

function LoginCard() {
const handleGoogleLogin = () => {
// Replace with your actual backend OAuth endpoint
window.location.href = 'http://localhost:8080/oauth2/authorization/google'
}
const [selected, setSelected] = useState('all')

return (
<div className="bg-bg-primary fixed inset-0 flex items-center justify-center">
<div className="bg-bg-secondary border-border-primary w-full max-w-sm space-y-6 rounded-lg border p-6 shadow-md">
<ChipGroup
selected={selected}
onChange={(value) => {
setSelected(value)

console.log('Selected:', value)
}}
options={[
{
label: 'All',
value: 'all',
},

{
label: 'Escalated',
value: 'escalated',
count: 1,
},

{
label: 'Spam',
value: 'spam',
},

{
label: 'Hate speech',
value: 'hate-speech',
},

{
label: 'Misinformation',
value: 'misinformation',
},
]}
/>
{/* Heading */}
<div className="space-y-1">
<h2 className="text-text-primary text-center text-2xl font-semibold">Welcome</h2>
Expand Down Expand Up @@ -56,7 +94,7 @@ function LoginCard() {
Privacy Policy
</a>
</p>
<ProgressBar value={0.5} variant="danger" scoreLabel="AI conf." />
{/* <ProgressBar value={0.5} variant="danger" scoreLabel="AI conf." /> */}
</div>
</div>
)
Expand Down
10 changes: 10 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ html[data-theme-loaded='true'] * {
box-shadow 0.25s ease;
}

/* Hide scrollbar but preserve scrolling */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}

.scrollbar-hide::-webkit-scrollbar {
display: none;
}

@keyframes shimmer {
0% {
background-position: 200% 0;
Expand Down
Loading