Files
twenty/packages/twenty-website/src/ui/ChipMultiSelect.tsx
T
Rashad Karanouh 6b99bcea7f feat(partners): require Twenty experience on apply, drop Cal success (#23223)
## Summary
- Add a dedicated **Experience** step to the partner apply wizard
(milestones, ≥200-char narrative, proof URL) before commercials
- Stop collecting `applicationNotes`; rename Expertise chrome away from
“experience”
- Replace the post-submit Cal.com embed with a review-and-reach-out
thank-you so unqualified inbound no longer books intros automatically

**Companion PR (app):** #23224 — Partner schema, submit persistence,
triage views, Tally CSV mapper (`twenty-partners` v1.4.0). Land the app
PR with or before this one.

## Test plan
- [ ] Open apply modal: wizard order is identity → profile → expertise →
experience → commercials
- [ ] Experience step blocks continue without ≥1 milestone, narrative
≥200 chars, and a valid https URL
- [ ] Submit creates/updates Partner with the three experience fields
(with #23224 deployed)
- [ ] Success screen has no Cal embed / book-later CTA
- [ ] `npx jest --config=jest.config.mjs partner-application` passes
locally (71 tests)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-07-24 09:40:22 +05:00

93 lines
2.0 KiB
TypeScript

'use client';
import { styled } from '@linaria/react';
import {
color,
FONT_WEIGHT,
fontFamily,
fontSize,
radius,
semanticColor,
spacing,
} from '@/tokens';
export type ChipOption<TValue extends string> = {
label: string;
value: TValue;
};
const PillGroup = styled.div`
display: flex;
flex-wrap: wrap;
gap: ${spacing(2)};
`;
// Selected = the scheme inverted (ink fill / surface text): on the dark form a
// white pill with black text, the old site's selected state expressed as a rule.
const Pill = styled.button`
background: none;
border: 1px solid ${semanticColor.lineStrong};
border-radius: ${radius(8)};
color: ${semanticColor.ink};
cursor: pointer;
font-family: ${fontFamily('sans')};
font-size: ${fontSize(3.5)};
font-weight: ${FONT_WEIGHT.regular};
line-height: 1.2;
padding: ${spacing(1.5)} ${spacing(3)};
&[data-selected] {
background: ${semanticColor.ink};
border-color: ${semanticColor.ink};
color: ${semanticColor.surface};
}
&[data-invalid] {
border-color: ${color('error')};
}
&:focus-visible {
outline: 2px solid ${color('blue')};
outline-offset: 2px;
}
`;
export function ChipMultiSelect<TValue extends string>({
ariaLabel,
invalid = false,
onToggle,
options,
values,
}: {
ariaLabel: string;
invalid?: boolean;
onToggle: (value: TValue) => void;
options: readonly ChipOption<TValue>[];
values: readonly TValue[];
}) {
return (
<PillGroup
aria-invalid={invalid ? true : undefined}
aria-label={ariaLabel}
role="group"
>
{options.map((option) => {
const selected = values.includes(option.value);
return (
<Pill
aria-pressed={selected}
data-invalid={invalid && !selected ? '' : undefined}
data-selected={selected ? '' : undefined}
key={option.value}
onClick={() => onToggle(option.value)}
type="button"
>
{option.label}
</Pill>
);
})}
</PillGroup>
);
}