Files
twenty/packages/twenty-website/src/ui/CategoryCardSelect.tsx
T
Abdullah. 569d887d1e [Website] Cut over to the rebuilt site (#21825)
Renaming the package so any further PRs directed to the website are
targeted to the reworked code instead of diverging. Once merged, I will
start preparing this for deployment to dev to test before releasing to
prod. Any improvements will also be applied to this package.

I avoided making significant changes to API routes so nothing breaks,
but will test it thoroughly today to confirm. That said, everything is
ported - double checked.

Big diff PR, impossible to review, but last one! No more rebuilds.
2026-06-19 10:22:46 +02:00

146 lines
3.1 KiB
TypeScript

'use client';
import { styled } from '@linaria/react';
import { CheckMark } from '@/icons';
import {
color,
FONT_WEIGHT,
fontFamily,
radius,
semanticColor,
spacing,
typeRampDeclarations,
} from '@/tokens';
export type CategoryOption<TValue extends string> = {
description: string;
examples: string;
label: string;
value: TValue;
};
const CardGroup = styled.div`
display: flex;
flex-direction: column;
& > * + * {
margin-top: ${spacing(2)};
}
`;
const Card = styled.button`
align-items: flex-start;
background: none;
border: 1px solid ${semanticColor.lineStrong};
border-radius: ${radius(2)};
column-gap: ${spacing(2)};
cursor: pointer;
display: flex;
padding: ${spacing(2.5)} ${spacing(3)};
text-align: left;
width: 100%;
&[data-selected] {
background: ${color('blue-10')};
border-color: ${color('blue')};
}
&[data-invalid] {
border-color: ${color('error')};
}
&:focus-visible {
outline: 2px solid ${color('blue')};
outline-offset: 2px;
}
`;
const Indicator = styled.span`
align-items: center;
border: 1.5px solid ${semanticColor.divider};
border-radius: ${radius(1)};
color: ${color('black')};
display: flex;
flex: none;
height: 18px;
justify-content: center;
margin-top: 2px;
width: 18px;
[data-selected] & {
background: ${color('blue')};
border-color: ${color('blue')};
}
`;
const Texts = styled.span`
display: flex;
flex-direction: column;
min-width: 0;
& > * + * {
margin-top: ${spacing(0.5)};
}
`;
const Title = styled.span`
${typeRampDeclarations('bodySm')}
color: ${semanticColor.ink};
font-family: ${fontFamily('sans')};
font-weight: ${FONT_WEIGHT.medium};
`;
const Description = styled.span`
${typeRampDeclarations('bodyXs')}
color: ${semanticColor.inkMuted};
font-family: ${fontFamily('sans')};
`;
const Examples = styled.span`
${typeRampDeclarations('bodyXs')}
color: ${semanticColor.inkSubtle};
font-family: ${fontFamily('sans')};
`;
export function CategoryCardSelect<TValue extends string>({
ariaLabel,
invalid = false,
onToggle,
options,
values,
}: {
ariaLabel: string;
invalid?: boolean;
onToggle: (value: TValue) => void;
options: readonly CategoryOption<TValue>[];
values: readonly TValue[];
}) {
return (
<CardGroup aria-label={ariaLabel} role="group">
{options.map((option) => {
const selected = values.includes(option.value);
return (
<Card
aria-pressed={selected}
data-invalid={invalid && !selected ? '' : undefined}
data-selected={selected ? '' : undefined}
key={option.value}
onClick={() => onToggle(option.value)}
type="button"
>
<Indicator aria-hidden>
{selected ? <CheckMark sizePx={12} /> : null}
</Indicator>
<Texts>
<Title>{option.label}</Title>
<Description>{option.description}</Description>
<Examples>ex. {option.examples}</Examples>
</Texts>
</Card>
);
})}
</CardGroup>
);
}