Expose builder chip selection beyond color, raise unselected contrast, and make the five-value limit visible.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-10 09:04:06 -06:00
co-authored by Cursor
parent 95e1937419
commit 64a2ef5a00
16 changed files with 266 additions and 51 deletions
@@ -19,6 +19,9 @@ function MultiSelectView({
formHeader = true,
onCustomChipConfirm,
onCustomChipClose,
maxSelections,
selectionCountText,
limitReachedAnnouncement,
className,
}: MultiSelectViewProps) {
const isSmall = size === "s";
@@ -30,6 +33,11 @@ function MultiSelectView({
: "gap-[var(--measures-spacing-300,12px)]";
const chipSize = size;
const selectedCount = options.filter((o) => o.state === "selected").length;
const atLimit =
maxSelections != null && selectedCount >= maxSelections;
const countCaption = selectionCountText?.trim() ?? "";
const helperText = countCaption.length > 0 ? countCaption : false;
return (
<div
@@ -41,58 +49,83 @@ function MultiSelectView({
label={label}
helpIcon={showHelpIcon}
asterisk={false}
helperText={false}
helperText={helperText}
size={size}
palette={palette}
/>
)}
{!formHeader || !label ? (
countCaption.length > 0 ? (
<p className="w-full text-x-small-paragraph text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
{countCaption}
</p>
) : null
) : null}
{limitReachedAnnouncement ? (
<p className="sr-only" aria-live="polite">
{atLimit ? limitReachedAnnouncement : ""}
</p>
) : null}
{/* Chips container */}
<div
className={`flex flex-wrap ${gapClass} items-center relative shrink-0 w-full`}
>
{options.map((option) => (
<Chip
key={option.id}
label={option.state === "custom" ? "" : option.label}
state={option.state || "unselected"}
palette={palette}
size={chipSize}
onClick={() => {
if (option.state !== "custom" && onChipClick) {
onChipClick(option.id);
}
}}
onCheck={(value, e) => {
e.stopPropagation();
if (onCustomChipConfirm) {
onCustomChipConfirm(option.id, value);
}
}}
onClose={(e) => {
e.stopPropagation();
if (onCustomChipClose) {
onCustomChipClose(option.id);
}
}}
/>
))}
{options.map((option) => {
const isCustom = option.state === "custom";
const isSelected = option.state === "selected";
const chipDisabled = !isCustom && !isSelected && atLimit;
const chipState = chipDisabled
? "disabled"
: option.state || "unselected";
return (
<Chip
key={option.id}
label={isCustom ? "" : option.label}
state={chipState}
palette={palette}
size={chipSize}
disabled={chipDisabled}
onClick={() => {
if (!isCustom && !chipDisabled && onChipClick) {
onChipClick(option.id);
}
}}
onCheck={(value, e) => {
e.stopPropagation();
if (onCustomChipConfirm) {
onCustomChipConfirm(option.id, value);
}
}}
onClose={(e) => {
e.stopPropagation();
if (onCustomChipClose) {
onCustomChipClose(option.id);
}
}}
/>
);
})}
{/* Add button — icon-only: bordered circle + brand icon (chips stay yellow). With label: Figma 19688:38288 — brand + icon, primary label text, no fill/border. */}
{addButton && (
<button
type="button"
aria-label={addButtonAriaLabel}
disabled={atLimit}
onClick={(e) => {
e.stopPropagation();
if (atLimit) return;
onAddClick?.();
}}
className={
!addButtonText
? // Circular button with border (Rule style)
`cursor-pointer bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
`${atLimit ? "cursor-not-allowed opacity-60" : "cursor-pointer hover:opacity-80"} bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 transition-opacity`
: // Text add control (default palette: white label + brand “+”; inverse: inverse primary for both)
`cursor-pointer flex items-center justify-center overflow-hidden rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity ${
`${atLimit ? "cursor-not-allowed opacity-60" : "cursor-pointer hover:opacity-80"} flex items-center justify-center overflow-hidden rounded-[var(--measures-radius-full,9999px)] shrink-0 transition-opacity ${
isSmall
? "gap-[var(--measures-spacing-100,4px)] px-[var(--measures-spacing-300,12px)] py-[var(--measures-spacing-200,8px)]"
: "gap-[var(--measures-spacing-150,6px)] px-[var(--space-400,16px)] py-[var(--measures-spacing-300,12px)]"