PackageDetail: toggle pattern for Compare button inCart state

Replaces the earlier inert selected-state treatment. Now:
- Button keeps its default soft/secondary chrome in both states — no
  separate brand-tinted visual.
- When `inCart=true`, a leading CheckRoundedIcon is added and the
  label swaps from "Compare" to "Added".
- Button remains clickable; `onCompare` is invoked in both states.
  Caller treats it as a toggle — add when absent, remove when present.
- aria-pressed reflects the state for SR users; aria-label spells
  "Add to comparison" / "Remove from comparison" explicitly.

Demo route swaps `basket.add()` for `basket.toggle()` on the handler
so a second click removes the package from the comparison basket.

Simpler visual (less space, one chrome to maintain) and a clearer
interaction — the user can undo directly from the detail panel
rather than hunting for CompareBar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 11:02:30 +10:00
parent 13bd245872
commit 7ecf309459
2 changed files with 28 additions and 43 deletions

View File

@@ -55,9 +55,9 @@ export interface PackageDetailProps {
/** Whether the compare button is in loading state */
compareLoading?: boolean;
/** Whether this package is already in the comparison basket. When true,
* the Compare button swaps to an "In comparison" selected-state (soft
* brand tint + check icon) and is inert — removal happens via the
* CompareBar, not this button. */
* the Compare button swaps its label to "Added" and adds a leading check
* icon. The button remains clickable — the caller is expected to treat
* `onCompare` as a toggle (add when not in cart, remove when in cart). */
inCart?: boolean;
/** Custom label for the arrange CTA button (default: "Make Arrangement") */
arrangeLabel?: string;
@@ -219,42 +219,25 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
>
{arrangeLabel}
</Button>
{onCompare &&
(inCart ? (
// Selected-state: soft brand tint + check icon. Inert
// (disabled + aria-disabled) — removal happens via the
// CompareBar, not this button. sx override keeps the
// brand tint instead of the default greyed-disabled look.
<Button
variant="outlined"
color="secondary"
size="large"
disabled
startIcon={<CheckRoundedIcon />}
aria-label="Already in comparison"
sx={{
flexShrink: 0,
'&.Mui-disabled': {
bgcolor: 'var(--fa-color-brand-50)',
borderColor: 'var(--fa-color-brand-300)',
color: 'var(--fa-color-text-brand)',
},
}}
>
In comparison
</Button>
) : (
{onCompare && (
// Same soft/secondary chrome in both states; when the package
// is in the basket a leading check icon appears and the label
// changes to "Added". Click is a toggle — caller decides to
// add or remove based on the `inCart` it's passing in.
<Button
variant="soft"
color="secondary"
size="large"
loading={compareLoading}
startIcon={inCart ? <CheckRoundedIcon /> : undefined}
onClick={onCompare}
aria-pressed={inCart}
aria-label={inCart ? 'Remove from comparison' : 'Add to comparison'}
sx={{ flexShrink: 0 }}
>
Compare
{inCart ? 'Added' : 'Compare'}
</Button>
))}
)}
</Box>
</Box>

View File

@@ -21,11 +21,13 @@ export function PackagesRoute() {
if (!provider || !bundle) return <Navigate to="/" replace />;
// Compare CTA on the PackageDetail panel just adds the selection to the
// basket. The floating CompareBar (mounted in App.tsx) handles navigation
// and removal once the user has 2+ packages selected.
// Compare CTA on the PackageDetail panel toggles the selection in the
// basket — adds when absent, removes when present. The button's visible
// state (Compare / Added + ✓) reflects `isSelectedInCart` below. The
// floating CompareBar (mounted in App.tsx) handles navigation once the
// user has 2+ packages selected.
const handleCompare = () => {
if (selectedId) basket.add(makeBasketKey(provider.id, selectedId));
if (selectedId) basket.toggle(makeBasketKey(provider.id, selectedId));
};
// When the selected package is already in the basket, PackageDetail swaps