Cross-Malet Product Comparison & Quick Actions
Two related Buyer-facing features that enhance product discovery across the platform:
- Compare Deck โ Pin up to 4 products from any Malet and evaluate them side-by-side in a unified modal.
- Card Quick Menu โ A universal kebab (โฎ) dropdown on every product and service card with contextual actions.
Both features are entirely client-side โ no backend changes required.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Root Layout โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ ComparisonTray (fixed bottom, persistent across routes) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ ComparisonModal (overlay, opens from tray) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Every Card Surface: โ
โ โโ ProductCard (catalog) โ CardQuickMenu โ
โ โโ ServiceCard (catalog) โ CardQuickMenu โ
โ โโ LobbyTrending (mosaic) โ CardQuickMenu โ
โ โโ Lobby inline cards โ CardQuickMenu โ
โ โโ FeaturedProducts (widget) โ CardQuickMenu โ
โ โโ SearchResultCard โ CardQuickMenu โ
โ โโ Item Detail Page โ CompareButton (expanded) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ comparisonStore (Svelte 5 Runes) โ
โ โ items: ComparisonProduct[] โ
โ โ isOpen: boolean โ
โ โ canAdd / canCompare (derived) โ
โ โ localStorage persistence โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ comparisonUtils.ts โ
โ โ buildComparisonRows() โ
โ โ formatComparisonValue() โ
โ โ getComparisonDiffFlag() โ
โ โ extractComparisonProduct() โ
โ โ ComparisonProductSchema (Zod) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
No backend changes required. The comparison operates entirely on data already loaded by existing GET_ITEM_DETAILS, GET_MALET_PRODUCTS, and GET_PRODUCTS queries.
Card Quick Menu (`CardQuickMenu`)
A universal kebab (โฎ) dropdown that appears on hover over any product or service card. Provides contextual quick actions without navigating away from the current page.
Menu Items
| Action | Status | Description |
|---|---|---|
| Add to Compare Deck | โ Live | Toggle product in/out of the comparison set. Shows "Full" badge when 4 items are pinned, โ checkmark when active. |
| Add to List | ๐ Coming Soon | Disabled with "SOON" badge. Will integrate with the future Buyer Lists/Wishlist feature. |
Technical Details
- Portal rendering: The dropdown is teleported to
document.bodyvia a Svelteuse:portalaction. This is necessary because product cards use CSStransformon hover (for the lift effect), which creates a new containing block that breaksposition: fixedinsideoverflow: hiddencontainers. - Event isolation: All click events within the menu are stopped from propagating to parent
<a>tags, preventing accidental navigation. - Keyboard support:
Escapekey closes the dropdown. - Click-outside dismissal: Global click listener with a 10ms delay to prevent the open-click from immediately closing the menu.
Integration Points
The CardQuickMenu is integrated into every card surface across the platform:
| Surface | Component | Position |
|---|---|---|
| Malet Catalog | ProductCard.svelte |
Top-left, hover reveal |
| Malet Catalog | ServiceCard.svelte |
Top-left, hover reveal |
| Lobby โ Trending Mosaic | LobbyTrending.svelte |
Top-right, hover reveal |
| Lobby โ Featured Products | lobby/+page.svelte |
Top-left, hover reveal |
| Malet Storefront Widget | FeaturedProducts.svelte |
Top-left, hover reveal |
| Search Results | SearchResultCard.svelte |
Top-right, hover reveal |
| Item Detail Page | CompareButton.svelte |
Inline (expanded) |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
product |
ComparisonProduct | undefined |
undefined |
Product data for comparison. When undefined, only the "Add to List" option shows. |
type |
'product' | 'service' |
'product' |
Card type context for future action differentiation. |
Comparison Store API
`comparisonStore` (Singleton)
| Property / Method | Type | Description |
|---|---|---|
items |
ComparisonProduct[] |
Current comparison set (max 4) |
isOpen |
boolean |
Whether the comparison modal is visible |
canAdd |
boolean (derived) |
true if fewer than 4 items |
canCompare |
boolean (derived) |
true if โฅ2 items |
isEmpty |
boolean (derived) |
true if no items |
count |
number (derived) |
Current item count |
addProduct(p) |
โ boolean |
Adds product, returns false if duplicate or full |
removeProduct(id) |
โ void |
Removes by ID, auto-closes modal if <2 remain |
toggleProduct(p) |
โ boolean |
Add or remove, returns true if added |
hasProduct(id) |
โ boolean |
Check if product is in comparison |
clearAll() |
โ void |
Remove all, close modal |
openModal() |
โ void |
Open modal (requires โฅ2 items) |
closeModal() |
โ void |
Close modal |
Persistence: Items are synced to localStorage('ngwenya_compare_items') via a $effect.root() watcher. The $effect.root() wrapper is required because the store is a module-level singleton instantiated outside any component's reactive context.
Visual Components
`ComparisonTray`
Fixed-bottom bar showing current comparison set. Visible when โฅ1 product is pinned.
- Mini product thumbnails with Malet handle
- Empty slot placeholders (dashed borders)
- "Clear All" button
- "Compare Now" CTA (disabled with <2 items)
- Auto-hides when comparison set is empty
`ComparisonModal`
Full-screen overlay with side-by-side comparison table.
- Header: Product image, name, price, Malet link per column
- Body: Attribute rows (Price, Compare-At, Status, SKU, Weight, Dimensions, Inventory, Tags, Variants). Rows with differing values are highlighted.
- Footer: "View Details" link per product
- Escape key and backdrop click close the modal
Utility Functions
`buildComparisonRows(products): ComparisonRow[]`
Generates normalized attribute rows from an array of products. Omits rows where all products have null values.
`formatComparisonValue(value, key): string`
Formats raw product data for display: cents โ currency, dimensions โ LรWรH unit, arrays โ comma-separated, booleans โ Yes/No, nulls โ โ.
`getComparisonDiffFlag(row): boolean`
Returns true if any non-null values in the row differ โ used to highlight divergent attributes.
`extractComparisonProduct(product, maletHandle, maletName): ComparisonProduct`
Bridge function that extracts a ComparisonProduct from a raw ProductNode (as returned by GraphQL queries) plus Malet context.
`ComparisonProductSchema` (Zod)
Validates data before it enters the comparison store. Required fields: id, name, maletHandle, maletName, basePrice.
File Reference
| File | Purpose |
|---|---|
src/stores/comparisonStore.svelte.ts |
Svelte 5 reactive state class with localStorage persistence |
src/lib/utils/comparisonUtils.ts |
Pure utility functions and Zod schema |
src/lib/components/catalog/CardQuickMenu.svelte |
Universal kebab dropdown with portal rendering |
src/lib/components/catalog/CompareButton.svelte |
Toggle button for add/remove (item detail page) |
src/lib/components/catalog/ComparisonTray.svelte |
Fixed-bottom comparison tray |
src/lib/components/catalog/ComparisonModal.svelte |
Full comparison overlay modal |
tests/comparisonUtils.test.ts |
31 unit tests for utilities |
tests/comparisonStore.test.ts |
15 unit tests for store logic |
Design Decisions
- Client-side only: No new GraphQL queries or mutations. Products loaded by existing queries are reused.
- Products only: Services excluded from comparison โ their attributes (duration, booking, location) don't suit tabular comparison. Services still get the quick menu for future "Add to List".
- Max 4 items: Standard comparison limit. Beyond 4, the comparison table becomes unwieldy on mobile.
- localStorage: Simple, session-scoped persistence. No server-side state needed.
- Diff highlighting: Rows where products differ get a subtle accent background to draw the Buyer's eye to distinguishing attributes.
- Portal rendering: The dropdown is teleported to
document.bodybecause card wrappers useoverflow: hidden+transformon hover, which breaksposition: fixedpositioning.
Related
- Storefront Sections โ Layout widgets where product cards with quick menus render
- Tech & Electronics โ Vertical-specific
SPEC_COMPARISONwidget - Community Features โ Future integration point for social lists/wishlists