diff --git a/.cursor/rules/create-flow.mdc b/.cursor/rules/create-flow.mdc
index cf8d0a2..0d872bb 100644
--- a/.cursor/rules/create-flow.mdc
+++ b/.cursor/rules/create-flow.mdc
@@ -33,7 +33,7 @@ Reach for these before writing new markup:
| `[– value +]` numeric stepper (± label) | `app/components/controls/Incrementer` / `IncrementerBlock` |
| Mid-paragraph "expand / see all" link button | `app/components/buttons/InlineTextButton` |
| Help-icon + label above a control | `app/components/type/InputLabel` (`helpIcon` prop) |
-| Toggle chip (dim-but-clickable) | `Chip` with `state="Disabled" disabled={false}` |
+| Toggle chip | `Chip` with `state="selected"` / `"unselected"` (Chip sets `aria-pressed`; selected includes a check mark) |
| Card-click → structured creation modal | `Create` with `backdropVariant="blurredYellow"` |
If a screen grows a 2nd inline copy of any pattern above, **extract a shared
diff --git a/app/(app)/create/components/ApplicableScopeField.tsx b/app/(app)/create/components/ApplicableScopeField.tsx
index bdb0635..61d9467 100644
--- a/app/(app)/create/components/ApplicableScopeField.tsx
+++ b/app/(app)/create/components/ApplicableScopeField.tsx
@@ -74,11 +74,16 @@ function ApplicableScopeFieldComponent({
{scopes.map((scope) => {
const isSelected = selectedScopes.includes(scope);
+ const chipState = isSelected
+ ? "selected"
+ : readOnly
+ ? "disabled"
+ : "unselected";
return (
coreValueOptions.filter((o) => o.state === "selected").length,
+ [coreValueOptions],
+ );
+ const atSelectionLimit = selectedCount >= MAX_CORE_VALUES;
+ const selectionCountText = cv.multiSelect.selectionCount
+ .replace("{count}", String(selectedCount))
+ .replace("{max}", String(MAX_CORE_VALUES));
+
const kebabMenuItems = useMemo(() => {
if (!modalSession || !activeModalChipId) return [];
- const selectedCount = coreValueOptions.filter(
- (o) => o.state === "selected",
- ).length;
return buildCustomRuleModalKebabMenu(modalKebabMenu, {
showCustomize: true,
onCustomize: handleCustomize,
onDuplicate:
(state.editingPublishedRuleId?.trim() ?? "") !== "" ||
- selectedCount >= MAX_CORE_VALUES
+ atSelectionLimit
? undefined
: handleDuplicateCoreChip,
showRemove: modalSession === "editing",
@@ -605,7 +611,7 @@ export function CoreValuesSelectScreen() {
});
}, [
activeModalChipId,
- coreValueOptions,
+ atSelectionLimit,
handleCustomize,
handleDuplicateCoreChip,
handleRemoveFromKebab,
@@ -695,7 +701,8 @@ export function CoreValuesSelectScreen() {
@@ -730,6 +737,9 @@ export function CoreValuesSelectScreen() {
onCustomChipClose={addHandlers.onCustomChipClose}
addButton
addButtonText={cv.multiSelect.addButtonText}
+ maxSelections={MAX_CORE_VALUES}
+ selectionCountText={selectionCountText}
+ limitReachedAnnouncement={cv.multiSelect.limitReached}
/>
{detailModal && (
diff --git a/app/components/controls/Chip/Chip.types.ts b/app/components/controls/Chip/Chip.types.ts
index 2a082a6..f0d2974 100644
--- a/app/components/controls/Chip/Chip.types.ts
+++ b/app/components/controls/Chip/Chip.types.ts
@@ -29,10 +29,9 @@ export interface ChipProps {
className?: string;
/**
* Whether the chip should be non-interactive. Defaults to `true` when
- * `state === "disabled"` to preserve historical behavior. Pass
- * `disabled={false}` alongside `state="disabled"` to render the dimmed
- * "disabled" visual while keeping the chip clickable — useful for toggle
- * groups where the unselected state is the disabled visual.
+ * `state === "disabled"`. Toggle groups use `selected` / `unselected`
+ * (Chip sets `aria-pressed`); pass `disabled` only when the chip cannot
+ * be activated.
*/
disabled?: boolean;
onClick?: (event: React.MouseEvent) => void;
diff --git a/app/components/controls/Chip/Chip.view.tsx b/app/components/controls/Chip/Chip.view.tsx
index 5658f59..378ad54 100644
--- a/app/components/controls/Chip/Chip.view.tsx
+++ b/app/components/controls/Chip/Chip.view.tsx
@@ -23,12 +23,12 @@ function ChipView({
typeToAddPlaceholder,
closeAriaLabel,
}: ChipViewProps) {
- // The container is the source of truth for `disabled`. This allows
- // `state="disabled"` to be used purely as a visual (for toggle-group chips
- // that look dimmed while remaining clickable) by passing `disabled={false}`.
+ // The container is the source of truth for `disabled`. `state="disabled"`
+ // is the non-interactive visual only — toggle groups use selected/unselected.
const isDisabled = disabled ?? false;
const isSelected = state === "selected";
const isCustom = state === "custom";
+ const isToggle = !isCustom;
const isInverse = palette === "inverse";
const isDefault = palette === "default";
@@ -145,6 +145,7 @@ function ChipView({
const sharedA11y = {
"aria-label": ariaLabel,
+ ...(isToggle ? { "aria-pressed": isSelected } : {}),
};
// Custom state rendering with check/close buttons
@@ -265,6 +266,23 @@ function ChipView({
onClick={handleClick}
{...sharedA11y}
>
+ {isSelected ? (
+
+ ) : null}
{label}
{onRemove && !isDisabled && (