Extend the standard wishlist page with per-item quantity controls and buyer notes for B2B purchasing workflows.
Wishlist Page — B2B Extensions
Prerequisite: Complete the Wishlist Page guide first. This guide only covers what changes for B2B — do not repeat the base setup.
B2B buyers need to specify quantities, attach notes, and bulk-convert their list to an order. This guide adds four things to the base implementation:
- Quantity controls (
+/−buttons) on each product card - Item notes (a textarea per card) for buyer instructions, sizing notes, or special requests
- Add All to Cart button that adds every in-stock item at its current quantity in a single cart request
- Dual-view layout — a structured table (default) and a card grid the buyer can toggle between; both views stay in sync when qty or notes change
Quantity and note changes are persisted back to the Swym list via swat.updateListItem().
What Changes
| Base guide | B2B extension | |
|---|---|---|
| Liquid | Standard header row | Adds #swym-add-all-btn, list switcher (#swym-list-switcher-wrapper), view toggle (#swym-view-toggle), table structure (#swym-wishlist-table / #swym-wishlist-tbody), and card grid (#swym-wishlist-grid, hidden by default) |
| Module-level vars | productJSONCache, shareButtonHandler | Adds currentView (persists table/grid choice across renders) |
renderProducts | Async; fetches Shopify product JSON; renders cards into a grid | Also accepts lid; caches product JSON by variant ID (productJSONCache); sets data-epi on each card; renders qty controls and a note textarea per card; simultaneously builds a structured table row in #swym-wishlist-tbody; calls setupViewToggle when done |
initWishlistPage | Fetches lists; renders the first (or previously-active) list; calls setupListSwitcher for multi-list support | On load, reads SS_PRODUCTS_KEY from sessionStorage; if a cached list is found, renders immediately before swat.fetchLists resolves (no loading flicker). swat.fetchLists then always re-renders from the authoritative API response. Calls setupListSwitcher for multi-list support after the live render. |
(new) setupViewToggle | — | Shows or hides the Table / Grid toggle; wires button clicks to flip currentView and swap visibility between table and grid |
(new) syncQtyAcrossViews | — | Keeps qty displays in sync when the buyer changes qty in one view, preventing table and grid from diverging |
(new) syncNoteAcrossViews | — | Same for note textareas |
(new) scheduleItemUpdate | — | Debounced call to swat.updateListItem() to persist qty + note changes (600 ms) |
(new) setupAddAllButton | — | Shows/hides the Add All to Cart button; hidden when all items are out of stock |
(new) addAllToCart | — | Reads data-epi + .swym-wishlist-qty-value from every card and POSTs all in-stock items to /cart/add.js in one request |
The share modal, save nudge, login nudge, and openShareModal are unchanged from the base guide.
Cache is a UX optimisation, not a data source.
initWishlistPagerenders immediately fromsessionStorageif a cached list is found, thenswat.fetchListsalways re-renders from the authoritative API response. The cached render does not callsetupListSwitcher-- the list switcher appears once the live render completes.
Wiring setupAddAllButton into initWishlistPage
setupAddAllButton into initWishlistPageIf you are copying the Complete Implementation directly, setupAddAllButton is already wired at every call site — this section is a reference for integrating piecemeal into an existing custom build.
Call setupAddAllButton immediately after renderProducts in each place renderProducts is called — the initial fetchLists callback, the sw:removedfromwishlist listener, and the list-switcher change handler:
// In the fetchLists callbackFn, after renderProducts:
renderProducts(swat, lid, products, grid, countEl, empty);
setupShareButton(swat, lid, products.length);
setupAddAllButton(products); // ← add this line
// In the sw:removedfromwishlist handler, after renderProducts:
renderProducts(swat, lid, updated, grid, countEl, empty);
setupShareButton(swat, lid, count);
setupAddAllButton(updated); // ← add this lineModified renderProducts
renderProductsReplace renderProducts in assets/swym-wishlist-page.js with the version in the Complete Implementation below. The B2B version differs from the base guide in four ways:
-
Product JSON cache — fetches are keyed by variant ID (
product.epi) and stored inproductJSONCache. Switching between wishlists re-uses cached data without hitting Shopify's product endpoint again. -
Dual-view output — each product renders into two places simultaneously: a card appended to
#swym-wishlist-grid(same structure as the base guide, plus qty controls and a note textarea), and a structured table row appended to#swym-wishlist-tbody(image, product name, variant title, qty stepper, live line price, note textarea, Add to Cart, Remove). Both elements carrydata-episo the sync helpers can find them by variant ID. -
Cross-view sync — qty and note change handlers call
syncQtyAcrossViewsandsyncNoteAcrossViewsso both the card and the table row always reflect the same value. The table row also updates the displayed line price (unit price × qty) as the buyer changes qty. -
View toggle —
setupViewToggle(true)is called after all rows are built to show the Table / Grid buttons and apply the currentcurrentViewstate. When the wishlist is empty,setupViewToggle(false)hides the toggle.
Note on quantity: each per-card Add to Cart call reads the current qty display value rather than a hardcoded
1, so the quantity the buyer set carries through to the cart.addAllToCartdoes the same — reading from the DOM at click time.
New scheduleItemUpdate
scheduleItemUpdateAdd this function anywhere in the IIFE, alongside the existing helpers. It debounces writes so rapid +/− taps or keystroke-by-keystroke note edits do not fire one API call per interaction. The complete function is in the Complete Implementation below.
Debounce interval: 600 ms works well for both quantity buttons and textarea input. Lower values increase API call volume; higher values make saves feel sluggish.
New setupViewToggle
setupViewToggleAdd this function alongside the existing helpers. renderProducts calls it at the end of every render pass — with true when products exist and false when the wishlist is empty.
When hasProducts is false, the toggle wrapper is hidden. When true, it reads currentView and applies visibility to either #swym-wishlist-table-wrapper or #swym-wishlist-grid, then re-wires the Table and Grid buttons using cloneNode to clear any listener attached in a previous render pass.
currentViewis module-level — it persists across list switches and re-renders within the same page session, so the buyer's view choice is preserved when they switch wishlists.
New syncQtyAcrossViews and syncNoteAcrossViews
syncQtyAcrossViews and syncNoteAcrossViewsAdd both functions alongside the existing helpers. They are called from the qty stepper and note input handlers in both the grid card and the table row.
Both functions query [data-epi] elements so they work regardless of which view is currently visible. The sourceEl guard prevents the triggering input from being overwritten with its own value, which would reset the cursor position in a textarea.
Liquid addition
Add #swym-add-all-btn to the header row in sections/swym-wishlist-page.liquid, alongside the existing Share button:
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
<h1>My Wishlist <span id="swym-wishlist-count"></span></h1>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button id="swym-add-all-btn" style="display:none;" aria-label="Add all wishlist items to cart">Add All to Cart</button>
<button id="swym-share-btn" style="display:none;" aria-label="Share wishlist">Share Wishlist</button>
</div>
</div>New setupAddAllButton and addAllToCart
setupAddAllButton and addAllToCartAdd both functions to assets/swym-wishlist-page.js alongside the existing helpers. The complete functions are in the Complete Implementation below.
setupAddAllButton shows or hides #swym-add-all-btn based on whether at least one in-stock product exists. It uses cloneNode to replace the button and clear any listener from a previous render pass before attaching a fresh click handler.
addAllToCart reads data-epi and the qty display from every .swym-wishlist-card[data-epi] inside #swym-wishlist-grid at click time (not from the original products array), so any qty changes the buyer made are respected. It checks each card's cart button's disabled state before including the item, skipping out-of-stock products silently.
Out-of-stock items are skipped.
addAllToCartchecks the per-card cart button's disabled state before including an item — so a wishlist with a mix of in-stock and out-of-stock products adds only the in-stock ones, without erroring on the rest.
Styling additions
Add these selectors to assets/swym-wishlist-page.css alongside the existing card styles.
| Element | Purpose |
|---|---|
#swym-add-all-btn | Add All to Cart button — hidden until 1+ in-stock items exist; sits in the header row alongside #swym-share-btn |
#swym-view-toggle | Wrapper for the Table / Grid toggle buttons; hidden when wishlist is empty |
.swym-view-btn | Table and Grid toggle buttons |
.swym-view-btn.is-active | Active view button — indicates the selected view |
#swym-wishlist-table-wrapper | Outer wrapper for the table view; hidden when grid view is active |
.swym-table-header | Column header row (Product, Qty, Price, Note, actions) |
.swym-table-row | One row per product in the table |
.swym-table-col-* | Individual column cells: img, product, qty, price, note, actions |
.swym-table-variant-title | Variant name shown below the product title in the table |
.swym-table-line-price | Per-row price that updates live as qty changes (unit price × qty) |
.swym-table-actions-inner | Flex wrapper for the cart and remove buttons inside .swym-table-col-actions |
.swym-table-note | Modifier on .swym-wishlist-note for compact table-row sizing |
.swym-wishlist-qty | Quantity stepper wrapper (− / count / +) — shared between grid cards and table rows |
.swym-wishlist-qty-value | Quantity number display — set a fixed min-width to prevent layout shift |
.swym-wishlist-note-label | "Note" label above the card textarea (grid view only) |
.swym-wishlist-note | Note textarea — resize:vertical; shared class between grid and table |
.swym-wishlist-cart-btn | Add to Cart button; [disabled] = out of stock |
.swym-wishlist-remove-btn | Remove button |
As with all card styles, pull border colour, border radius, and font values from your theme's CSS variables rather than hardcoding them.
Product Fields Used (additions)
These fields are added on top of the ones documented in the base guide.
| Field | Usage |
|---|---|
qty | Quantity stored on the wishlist item (defaults to 1 if not set). Read on render, written back via updateListItem |
note | Free-text note stored on the wishlist item. Read on render, written back via updateListItem |
Complete Implementation
Copy the files for your setup into the theme. The Liquid goes in sections/, the JS in assets/. Choose the variant that matches your Swym dashboard configuration.
sections/swym-wishlist-page.liquid
sections/swym-wishlist-page.liquid{% comment %} Swym Wishlist Page — B2B, Multiple Wishlists {% endcomment %}
<div id="swym-wishlist-page" class="page-width">
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
<h1><span id="swym-wishlist-title">My Wishlist</span> <span id="swym-wishlist-count"></span></h1>
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<div id="swym-list-switcher-wrapper" style="display:none;">
<label for="swym-list-switcher" style="font-size:0.875rem;">List:</label>
<select id="swym-list-switcher" aria-label="Switch between your wishlists"></select>
</div>
<div id="swym-view-toggle" style="display:none;">
<button id="swym-view-table-btn" class="swym-view-btn is-active" aria-pressed="true" aria-label="Switch to table view">Table</button>
<button id="swym-view-grid-btn" class="swym-view-btn" aria-pressed="false" aria-label="Switch to grid view">Grid</button>
</div>
<button id="swym-add-all-btn" style="display:none;" aria-label="Add all wishlist items to cart">Add All to Cart</button>
<button id="swym-share-btn" style="display:none;" aria-label="Share wishlist">Share Wishlist</button>
</div>
</div>
{%- comment -%} Share modal — rendered hidden; JS controls visibility {%- endcomment -%}
<div id="swym-share-modal" role="dialog" aria-modal="true" aria-labelledby="swym-share-modal-title" style="display:none; position:fixed; inset:0; z-index:9999;">
<div id="swym-share-modal-overlay" style="display:block; position:absolute; inset:0; background:rgba(0,0,0,0.5);"></div>
<div id="swym-share-modal-content" style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; padding:2rem; max-width:480px; width:90%; border-radius:8px; max-height:90vh; overflow-y:auto;">
<div id="swym-share-modal-header">
<h2 id="swym-share-modal-title">Share Your Wishlist</h2>
<button id="swym-share-modal-close" aria-label="Close share dialog">×</button>
</div>
{%- comment -%} Email share form {%- endcomment -%}
<div class="swym-share-section">
<label for="swym-share-name-input">Name</label>
<input id="swym-share-name-input" type="text" placeholder="Your name" aria-label="Your name">
<label for="swym-share-email-input">Email</label>
<input id="swym-share-email-input" type="email" placeholder="[email protected]" aria-label="Recipient email address">
<label for="swym-share-note-input">Message</label>
<textarea id="swym-share-note-input" placeholder="Check out my wishlist!" aria-label="Message"></textarea>
<button id="swym-share-email-btn" aria-label="Send wishlist by email">Share Via Email</button>
<span id="swym-share-email-feedback" style="display:none;" aria-live="polite">Email sent!</span>
</div>
{%- comment -%} Social / copy {%- endcomment -%}
<div class="swym-share-section swym-share-social-section">
<p class="swym-share-divider-label">or Share via</p>
<div class="swym-share-social-row">
<button class="swym-share-social-btn" data-platform="facebook" aria-label="Share on Facebook">Facebook</button>
<button class="swym-share-social-btn" data-platform="twitter" aria-label="Share on Twitter / X">X</button>
<button id="swym-share-copy-btn" aria-label="Copy link">Copy Link</button>
</div>
<span id="swym-share-copy-feedback" style="display:none;" aria-live="polite">Copied!</span>
</div>
</div>
</div>
{%- comment -%} Login nudge for anonymous users {%- endcomment -%}
<div id="swym-login-nudge" style="display:none;">
<p>Sign in to save your wishlist across devices</p>
<a href="/account/login?return_url=/pages/wishlist">Sign in</a>
</div>
{%- comment -%} Save nudge — shown after 3+ items for anonymous users {%- endcomment -%}
<div id="swym-save-nudge" style="display:none;" aria-live="polite" aria-atomic="true">
<p><strong>Don't lose your list!</strong> You've saved <span id="swym-save-nudge-count"></span> items. <a href="/account/login?return_url=/pages/wishlist">Sign in to keep them.</a></p>
<button id="swym-save-nudge-dismiss" aria-label="Dismiss">×</button>
</div>
<div id="swym-wishlist-loading" aria-live="polite" aria-atomic="true">Loading your wishlist...</div>
<div id="swym-wishlist-table-wrapper" style="min-height:600px;">
<div id="swym-wishlist-table" role="region" aria-label="Wishlist products">
<div class="swym-table-header">
<div class="swym-table-col-img"> </div>
<div class="swym-table-col-product">Product</div>
<div class="swym-table-col-qty">Qty</div>
<div class="swym-table-col-price">Price</div>
<div class="swym-table-col-note">Note</div>
<div class="swym-table-col-actions"> </div>
</div>
<div id="swym-wishlist-tbody"></div>
</div>
</div>
<div id="swym-wishlist-grid" aria-busy="true" aria-label="Wishlist products" style="display:none;"></div>
<div id="swym-wishlist-empty" style="display:none;">
<p>Your wishlist is empty</p>
<p>Start adding items you love</p>
<a href="/collections">Browse products</a>
</div>
</div>
<script src="{{ 'swym-wishlist-page.js' | asset_url }}" defer></script>assets/swym-wishlist-page.js
assets/swym-wishlist-page.js(function() {
'use strict';
var SAVE_NUDGE_THRESHOLD = 3;
var SS_PRODUCTS_KEY = 'swym-wishlist-products';
var SS_SHARE_URL_KEY = 'swym-share-url';
var productJSONCache = {};
var shareButtonHandler = null;
var currentView = 'table';
function initWishlistPage(swat) {
var isLoggedIn = swat.platform.isLoggedIn();
var grid = document.getElementById('swym-wishlist-grid');
var loading = document.getElementById('swym-wishlist-loading');
var empty = document.getElementById('swym-wishlist-empty');
var countEl = document.getElementById('swym-wishlist-count');
if (!grid) return;
if (!isLoggedIn) {
var loginNudge = document.getElementById('swym-login-nudge');
if (loginNudge) loginNudge.style.display = '';
}
var cachedRaw = sessionStorage.getItem(SS_PRODUCTS_KEY);
var cachedData = null;
if (cachedRaw) {
try {
cachedData = JSON.parse(cachedRaw);
if (loading) loading.style.display = 'none';
renderProducts(swat, cachedData.lid, cachedData.products, grid, countEl, empty);
if (countEl) countEl.textContent = '(' + cachedData.products.length + ' items)';
setupShareButton(swat, cachedData.lid, cachedData.products.length);
setupAddAllButton(cachedData.products);
} catch (e) {
sessionStorage.removeItem(SS_PRODUCTS_KEY);
cachedData = null;
}
}
swat.fetchLists({
callbackFn: function(lists) {
if (loading) loading.style.display = 'none';
var cachedLid = cachedData ? cachedData.lid : null;
var activeList = lists && lists.length
? (cachedLid ? (lists.find(function(l) { return l.lid === cachedLid; }) || lists[0]) : lists[0])
: null;
var lid = activeList ? activeList.lid : null;
var products = activeList ? (activeList.listcontents || []) : [];
// Always update cache and re-render from the authoritative API response
sessionStorage.setItem(SS_PRODUCTS_KEY, JSON.stringify({ lid: lid, products: products }));
if (products.length === 0) {
if (empty) empty.style.display = 'block';
if (countEl) countEl.textContent = '(0 items)';
} else {
if (countEl) countEl.textContent = '(' + products.length + ' items)';
showSaveNudge(swat, products.length);
renderProducts(swat, lid, products, grid, countEl, empty);
setupShareButton(swat, lid, products.length);
setupAddAllButton(products);
sessionStorage.removeItem(SS_SHARE_URL_KEY);
}
setupListSwitcher(swat, lists, lid, grid, countEl, empty);
if (swat.evtLayer) {
swat.evtLayer.addEventListener('sw:removedfromwishlist', function() {
swat.fetchLists({
callbackFn: function(freshLists) {
var activeLid = sessionStorage.getItem(SS_PRODUCTS_KEY)
? (JSON.parse(sessionStorage.getItem(SS_PRODUCTS_KEY)) || {}).lid
: lid;
var activeList = freshLists && freshLists.length
? (freshLists.find(function(l) { return l.lid === activeLid; }) || freshLists[0])
: null;
var currentLid = activeList ? activeList.lid : null;
var updated = activeList ? (activeList.listcontents || []) : [];
var count = updated.length;
sessionStorage.setItem(SS_PRODUCTS_KEY, JSON.stringify({ lid: currentLid, products: updated }));
sessionStorage.removeItem(SS_SHARE_URL_KEY);
renderProducts(swat, currentLid, updated, grid, countEl, empty);
if (countEl) countEl.textContent = '(' + count + ' items)';
setupShareButton(swat, currentLid, count);
setupAddAllButton(updated);
setupListSwitcher(swat, freshLists || [], currentLid, grid, countEl, empty);
}
});
});
}
},
errorFn: function() {
if (loading) loading.style.display = 'none';
}
});
}
function showSaveNudge(swat, count) {
if (swat.platform.isLoggedIn()) return;
if (count < SAVE_NUDGE_THRESHOLD) return;
if (sessionStorage.getItem('swym-save-nudge-dismissed')) return;
var nudge = document.getElementById('swym-save-nudge');
if (!nudge) return;
var countSpan = document.getElementById('swym-save-nudge-count');
if (countSpan) countSpan.textContent = count;
nudge.style.display = '';
var dismiss = document.getElementById('swym-save-nudge-dismiss');
if (dismiss) {
dismiss.addEventListener('click', function() {
nudge.style.display = 'none';
sessionStorage.setItem('swym-save-nudge-dismissed', 'true');
});
}
}
async function renderProducts(swat, lid, products, container, countEl, emptyEl) {
container.setAttribute('aria-busy', 'true');
container.innerHTML = '';
var tableBody = document.getElementById('swym-wishlist-tbody');
if (tableBody) tableBody.innerHTML = '';
if (!products || products.length === 0) {
if (emptyEl) emptyEl.style.display = 'block';
container.setAttribute('aria-busy', 'false');
setupViewToggle(false);
return;
}
if (emptyEl) emptyEl.style.display = 'none';
var shopifyData = await Promise.all(products.map(function(product) {
var key = String(product.epi);
if (productJSONCache[key]) return Promise.resolve(productJSONCache[key]);
return fetchProductJSON(product.du)
.then(function(data) { productJSONCache[key] = data; return data; })
.catch(function() { return null; });
}));
products.forEach(function(product, index) {
var shopifyProduct = shopifyData[index];
var allVariants = shopifyProduct ? shopifyProduct.variants : null;
var variant = allVariants
? allVariants.find(function(v) { return String(v.id) === String(product.epi); })
: null;
var inStock = variant ? variant.available : product.stk !== 0;
var displayPrice = variant ? variant.price : product.pr;
var title = esc(shopifyProduct && shopifyProduct.title || product.dt || 'Product');
var href = esc(shopifyProduct && shopifyProduct.url || product.du || '#');
var imgSrc = esc(shopifyProduct && (shopifyProduct.featured_image || (shopifyProduct.images && shopifyProduct.images[0])) || product.iu || '');
var noteId = 'swym-note-' + esc(String(product.epi));
var card = document.createElement('div');
card.className = 'swym-wishlist-card';
card.dataset.epi = product.epi;
card.innerHTML =
'<a href="' + href + '">' +
'<img src="' + imgSrc + '" alt="' + title + '" loading="lazy">' +
'</a>' +
'<a href="' + href + '">' + title + '</a>' +
(displayPrice ? '<span>' + esc(formatPrice(displayPrice)) + '</span>' : '') +
'<div class="swym-wishlist-card-actions">' +
'<div class="swym-wishlist-qty">' +
'<button type="button" aria-label="Decrease quantity of ' + title + '">−</button>' +
'<span class="swym-wishlist-qty-value" aria-live="polite">' + (product.qty || 1) + '</span>' +
'<button type="button" aria-label="Increase quantity of ' + title + '">+</button>' +
'</div>' +
'<label class="swym-wishlist-note-label" for="' + noteId + '">Note</label>' +
'<textarea id="' + noteId + '" class="swym-wishlist-note" rows="2"' +
' placeholder="Add a note (size, colour, special instructions…)"' +
' aria-label="Note for ' + title + '">' + esc(product.note || '') + '</textarea>' +
'<button type="button" class="swym-wishlist-cart-btn"' + (!inStock ? ' disabled' : '') +
' aria-label="' + (inStock ? 'Add ' + title + ' to cart' : title + ' is out of stock') + '">' +
(inStock ? 'Add to Cart' : 'Out of Stock') +
'</button>' +
'<button type="button" class="swym-wishlist-remove-btn" aria-label="Remove ' + title + ' from wishlist">×</button>' +
'</div>';
var decrementBtn = card.querySelector('.swym-wishlist-qty > button:first-of-type');
var qtyDisplay = card.querySelector('.swym-wishlist-qty-value');
var incrementBtn = card.querySelector('.swym-wishlist-qty > button:last-of-type');
var noteInput = card.querySelector('.swym-wishlist-note');
var cartBtn = card.querySelector('.swym-wishlist-cart-btn');
var removeBtn = card.querySelector('.swym-wishlist-remove-btn');
decrementBtn.addEventListener('click', function() {
var qty = parseInt(qtyDisplay.textContent, 10);
if (qty <= 1) return;
qty -= 1;
qtyDisplay.textContent = qty;
product.qty = qty;
syncQtyAcrossViews(product.epi, qty, qtyDisplay);
scheduleItemUpdate(swat, lid, product, qty, noteInput.value.trim());
});
incrementBtn.addEventListener('click', function() {
var qty = parseInt(qtyDisplay.textContent, 10) + 1;
qtyDisplay.textContent = qty;
product.qty = qty;
syncQtyAcrossViews(product.epi, qty, qtyDisplay);
scheduleItemUpdate(swat, lid, product, qty, noteInput.value.trim());
});
noteInput.addEventListener('input', function() {
var qty = parseInt(qtyDisplay.textContent, 10);
product.note = noteInput.value.trim();
syncNoteAcrossViews(product.epi, noteInput.value, noteInput);
scheduleItemUpdate(swat, lid, product, qty, noteInput.value.trim());
});
cartBtn.addEventListener('click', function() {
if (cartBtn.disabled) return;
var qty = parseInt(qtyDisplay.textContent, 10);
cartBtn.textContent = 'Adding...';
cartBtn.setAttribute('aria-label', 'Adding ' + title + ' to cart');
cartBtn.disabled = true;
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: [{ id: Number(product.epi), quantity: qty }] })
})
.then(function(response) {
if (response.ok) {
cartBtn.textContent = 'Added!';
cartBtn.setAttribute('aria-label', title + ' added to cart');
setTimeout(function() {
cartBtn.textContent = 'Add to Cart';
cartBtn.setAttribute('aria-label', 'Add ' + title + ' to cart');
cartBtn.disabled = false;
}, 2000);
} else {
cartBtn.textContent = 'Error';
cartBtn.setAttribute('aria-label', 'Failed to add ' + title + ' to cart');
cartBtn.disabled = false;
}
})
.catch(function() {
cartBtn.textContent = 'Error';
cartBtn.setAttribute('aria-label', 'Failed to add ' + title + ' to cart');
cartBtn.disabled = false;
});
});
removeBtn.addEventListener('click', function() {
swat.deleteFromList(
lid,
{ epi: product.epi, empi: product.empi, du: product.du },
function() {
card.remove();
var tableRow = document.querySelector('#swym-wishlist-table [data-epi="' + String(product.epi) + '"]');
if (tableRow) tableRow.remove();
var remaining = container.querySelectorAll('.swym-wishlist-card');
if (countEl) countEl.textContent = '(' + remaining.length + ' items)';
if (remaining.length === 0 && emptyEl) emptyEl.style.display = 'block';
},
function() {}
);
});
container.appendChild(card);
if (tableBody) {
var tableNoteId = 'swym-tnote-' + esc(String(product.epi));
var variantTitle = variant ? esc(variant.title) : '';
var initialQty = product.qty || 1;
var initialLinePrice = displayPrice ? formatPrice(displayPrice * initialQty) : '';
var row = document.createElement('div');
row.className = 'swym-table-row';
row.dataset.epi = product.epi;
row.innerHTML =
'<div class="swym-table-col-img">' +
'<a href="' + href + '"><img src="' + imgSrc + '" alt="' + title + '" loading="lazy"></a>' +
'</div>' +
'<div class="swym-table-col-product">' +
'<a href="' + href + '">' + title + '</a>' +
(variantTitle ? '<span class="swym-table-variant-title">' + variantTitle + '</span>' : '') +
'</div>' +
'<div class="swym-table-col-qty">' +
'<div class="swym-wishlist-qty">' +
'<button type="button" aria-label="Decrease quantity of ' + title + '">−</button>' +
'<span class="swym-wishlist-qty-value" aria-live="polite">' + initialQty + '</span>' +
'<button type="button" aria-label="Increase quantity of ' + title + '">+</button>' +
'</div>' +
'</div>' +
'<div class="swym-table-col-price">' +
(displayPrice ? '<span class="swym-table-line-price">' + esc(initialLinePrice) + '</span>' : '') +
'</div>' +
'<div class="swym-table-col-note">' +
'<textarea id="' + tableNoteId + '" class="swym-wishlist-note swym-table-note" rows="1"' +
' placeholder="Note…"' +
' aria-label="Note for ' + title + '">' + esc(product.note || '') + '</textarea>' +
'</div>' +
'<div class="swym-table-col-actions">' +
'<div class="swym-table-actions-inner">' +
'<button type="button" class="swym-wishlist-cart-btn"' + (!inStock ? ' disabled' : '') +
' aria-label="' + (inStock ? 'Add ' + title + ' to cart' : title + ' is out of stock') + '">' +
(inStock ? 'Add to Cart' : 'Out of Stock') +
'</button>' +
'<button type="button" class="swym-wishlist-remove-btn" aria-label="Remove ' + title + ' from wishlist">\xd7</button>' +
'</div>' +
'</div>';
var tDecrBtn = row.querySelector('.swym-table-col-qty .swym-wishlist-qty > button:first-of-type');
var tQtyDisplay = row.querySelector('.swym-wishlist-qty-value');
var tIncrBtn = row.querySelector('.swym-table-col-qty .swym-wishlist-qty > button:last-of-type');
var tNoteInput = row.querySelector('.swym-wishlist-note');
var tCartBtn = row.querySelector('.swym-wishlist-cart-btn');
var tRemoveBtn = row.querySelector('.swym-wishlist-remove-btn');
var tPriceEl = row.querySelector('.swym-table-line-price');
tDecrBtn.addEventListener('click', function() {
var qty = parseInt(tQtyDisplay.textContent, 10);
if (qty <= 1) return;
qty -= 1;
tQtyDisplay.textContent = qty;
product.qty = qty;
syncQtyAcrossViews(product.epi, qty, tQtyDisplay);
if (tPriceEl && displayPrice) tPriceEl.textContent = formatPrice(displayPrice * qty);
scheduleItemUpdate(swat, lid, product, qty, tNoteInput.value.trim());
});
tIncrBtn.addEventListener('click', function() {
var qty = parseInt(tQtyDisplay.textContent, 10) + 1;
tQtyDisplay.textContent = qty;
product.qty = qty;
syncQtyAcrossViews(product.epi, qty, tQtyDisplay);
if (tPriceEl && displayPrice) tPriceEl.textContent = formatPrice(displayPrice * qty);
scheduleItemUpdate(swat, lid, product, qty, tNoteInput.value.trim());
});
tNoteInput.addEventListener('input', function() {
var qty = parseInt(tQtyDisplay.textContent, 10);
product.note = tNoteInput.value.trim();
syncNoteAcrossViews(product.epi, tNoteInput.value, tNoteInput);
scheduleItemUpdate(swat, lid, product, qty, tNoteInput.value.trim());
});
tCartBtn.addEventListener('click', function() {
if (tCartBtn.disabled) return;
var qty = parseInt(tQtyDisplay.textContent, 10);
tCartBtn.textContent = 'Adding...';
tCartBtn.disabled = true;
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: [{ id: Number(product.epi), quantity: qty }] })
})
.then(function(response) {
if (response.ok) {
tCartBtn.textContent = 'Added!';
setTimeout(function() {
tCartBtn.textContent = 'Add to Cart';
tCartBtn.disabled = false;
}, 2000);
} else {
tCartBtn.textContent = 'Error';
tCartBtn.disabled = false;
}
})
.catch(function() {
tCartBtn.textContent = 'Error';
tCartBtn.disabled = false;
});
});
tRemoveBtn.addEventListener('click', function() {
swat.deleteFromList(
lid,
{ epi: product.epi, empi: product.empi, du: product.du },
function() {
row.remove();
var cardEl = container.querySelector('[data-epi="' + String(product.epi) + '"]');
if (cardEl) cardEl.remove();
var remaining = container.querySelectorAll('.swym-wishlist-card');
if (countEl) countEl.textContent = '(' + remaining.length + ' items)';
if (remaining.length === 0 && emptyEl) emptyEl.style.display = 'block';
},
function() {}
);
});
tableBody.appendChild(row);
}
});
container.setAttribute('aria-busy', 'false');
setupViewToggle(true);
}
var _updateTimers = {};
function scheduleItemUpdate(swat, lid, product, qty, note) {
var key = String(product.epi);
clearTimeout(_updateTimers[key]);
_updateTimers[key] = setTimeout(function() {
swat.updateListItem(
lid,
{
epi: product.epi,
empi: product.empi,
du: product.du,
qty: qty,
note: note
},
function() {},
function() {}
);
}, 600);
}
function syncQtyAcrossViews(epi, qty, sourceEl) {
document.querySelectorAll('[data-epi="' + String(epi) + '"] .swym-wishlist-qty-value').forEach(function(el) {
if (el !== sourceEl) el.textContent = qty;
});
}
function syncNoteAcrossViews(epi, value, sourceEl) {
document.querySelectorAll('[data-epi="' + String(epi) + '"] .swym-wishlist-note').forEach(function(el) {
if (el !== sourceEl) el.value = value;
});
}
function setupViewToggle(hasProducts) {
var toggle = document.getElementById('swym-view-toggle');
var tableWrapper = document.getElementById('swym-wishlist-table-wrapper');
var gridEl = document.getElementById('swym-wishlist-grid');
if (!toggle) return;
toggle.style.display = hasProducts ? '' : 'none';
if (!hasProducts) {
if (tableWrapper) tableWrapper.style.display = 'none';
return;
}
var tableBtn = document.getElementById('swym-view-table-btn');
var gridBtn = document.getElementById('swym-view-grid-btn');
if (!tableBtn || !gridBtn || !tableWrapper || !gridEl) return;
if (currentView === 'table') {
tableWrapper.style.display = '';
gridEl.style.display = 'none';
} else {
tableWrapper.style.display = 'none';
gridEl.style.display = '';
}
var freshTable = tableBtn.cloneNode(true);
tableBtn.parentNode.replaceChild(freshTable, tableBtn);
var freshGrid = gridBtn.cloneNode(true);
gridBtn.parentNode.replaceChild(freshGrid, gridBtn);
if (currentView === 'table') {
freshTable.classList.add('is-active');
freshTable.setAttribute('aria-pressed', 'true');
freshGrid.classList.remove('is-active');
freshGrid.setAttribute('aria-pressed', 'false');
} else {
freshGrid.classList.add('is-active');
freshGrid.setAttribute('aria-pressed', 'true');
freshTable.classList.remove('is-active');
freshTable.setAttribute('aria-pressed', 'false');
}
freshTable.addEventListener('click', function() {
currentView = 'table';
tableWrapper.style.display = '';
gridEl.style.display = 'none';
freshTable.classList.add('is-active');
freshTable.setAttribute('aria-pressed', 'true');
freshGrid.classList.remove('is-active');
freshGrid.setAttribute('aria-pressed', 'false');
});
freshGrid.addEventListener('click', function() {
currentView = 'grid';
gridEl.style.display = '';
tableWrapper.style.display = 'none';
freshGrid.classList.add('is-active');
freshGrid.setAttribute('aria-pressed', 'true');
freshTable.classList.remove('is-active');
freshTable.setAttribute('aria-pressed', 'false');
});
}
function setupAddAllButton(products) {
var btn = document.getElementById('swym-add-all-btn');
if (!btn) return;
var hasInStock = products && products.some(function(p) { return p.stk !== 0; });
if (!hasInStock) {
btn.style.display = 'none';
return;
}
btn.style.display = '';
var fresh = btn.cloneNode(true);
btn.parentNode.replaceChild(fresh, btn);
fresh.addEventListener('click', function() { addAllToCart(fresh); });
}
function addAllToCart(btn) {
var cards = document.querySelectorAll('#swym-wishlist-grid .swym-wishlist-card[data-epi]');
var items = [];
cards.forEach(function(card) {
var epi = card.dataset.epi;
var qtyEl = card.querySelector('.swym-wishlist-qty-value');
var qty = qtyEl ? parseInt(qtyEl.textContent, 10) : 1;
var cartBtn = card.querySelector('.swym-wishlist-cart-btn');
if (epi && qty > 0 && cartBtn && !cartBtn.disabled) {
items.push({ id: Number(epi), quantity: qty });
}
});
if (!items.length) return;
btn.disabled = true;
btn.textContent = 'Adding...';
btn.setAttribute('aria-label', 'Adding all items to cart');
fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: items })
})
.then(function(response) {
if (response.ok) {
btn.textContent = 'Added!';
btn.setAttribute('aria-label', 'All items added to cart');
setTimeout(function() {
btn.textContent = 'Add All to Cart';
btn.setAttribute('aria-label', 'Add all wishlist items to cart');
btn.disabled = false;
}, 2000);
} else {
btn.textContent = 'Error';
btn.setAttribute('aria-label', 'Failed to add items to cart');
btn.disabled = false;
}
})
.catch(function() {
btn.textContent = 'Error';
btn.setAttribute('aria-label', 'Failed to add items to cart');
btn.disabled = false;
});
}
function bindShareUrlButtons(modal, copyBtn, url, swat, lid) {
if (copyBtn) {
copyBtn.disabled = false;
var freshCopy = copyBtn.cloneNode(true);
copyBtn.parentNode.replaceChild(freshCopy, copyBtn);
freshCopy.addEventListener('click', function() { handleCopyLink(url); });
}
modal.querySelectorAll('.swym-share-social-btn').forEach(function(socialBtn) {
var freshSocial = socialBtn.cloneNode(true);
socialBtn.parentNode.replaceChild(freshSocial, socialBtn);
freshSocial.addEventListener('click', function() {
handleSocialShare(swat, lid, freshSocial.dataset.platform, url);
});
});
}
function setupShareButton(swat, lid, productCount) {
var btn = document.getElementById('swym-share-btn');
if (!btn || !lid) return;
if (productCount === 0) {
btn.style.display = 'none';
return;
}
btn.style.display = '';
if (shareButtonHandler) btn.removeEventListener('click', shareButtonHandler);
shareButtonHandler = function() { openShareModal(swat, lid); };
btn.addEventListener('click', shareButtonHandler);
}
function openShareModal(swat, lid) {
var modal = document.getElementById('swym-share-modal');
if (!modal) return;
var nameInput = document.getElementById('swym-share-name-input');
var emailInput = document.getElementById('swym-share-email-input');
var noteInput = document.getElementById('swym-share-note-input');
var copyBtn = document.getElementById('swym-share-copy-btn');
if (nameInput) nameInput.value = '';
if (emailInput) emailInput.value = '';
if (noteInput) noteInput.value = '';
if (copyBtn) copyBtn.disabled = true;
modal.querySelectorAll('[id$="-feedback"]').forEach(function(el) { el.style.display = 'none'; });
modal.style.display = '';
document.body.style.overflow = 'hidden';
if (nameInput) nameInput.focus();
var cachedShareUrl = sessionStorage.getItem(SS_SHARE_URL_KEY);
if (cachedShareUrl) {
bindShareUrlButtons(modal, copyBtn, cachedShareUrl, swat, lid);
} else {
swat.markListPublic(lid, function() {
swat.generateSharedListURL(lid, function(url) {
sessionStorage.setItem(SS_SHARE_URL_KEY, url);
bindShareUrlButtons(modal, copyBtn, url, swat, lid);
});
}, function() {
if (copyBtn) copyBtn.disabled = true;
});
}
var emailBtn = document.getElementById('swym-share-email-btn');
if (emailBtn) {
var freshEmail = emailBtn.cloneNode(true);
emailBtn.parentNode.replaceChild(freshEmail, emailBtn);
freshEmail.addEventListener('click', function() { handleEmailShare(swat, lid); });
}
var closeBtn = document.getElementById('swym-share-modal-close');
var overlay = document.getElementById('swym-share-modal-overlay');
if (closeBtn) {
var freshClose = closeBtn.cloneNode(true);
closeBtn.parentNode.replaceChild(freshClose, closeBtn);
freshClose.addEventListener('click', closeShareModal);
}
if (overlay) {
var freshOverlay = overlay.cloneNode(true);
overlay.parentNode.replaceChild(freshOverlay, overlay);
freshOverlay.addEventListener('click', closeShareModal);
}
document.addEventListener('keydown', handleShareModalKeydown);
}
function handleShareModalKeydown(e) {
if (e.key === 'Escape') {
closeShareModal();
return;
}
if (e.key !== 'Tab') return;
var modal = document.getElementById('swym-share-modal-content');
if (!modal) return;
var focusable = Array.prototype.slice.call(
modal.querySelectorAll('a[href],button:not([disabled]),input,textarea,select,[tabindex]:not([tabindex="-1"])')
).filter(function(el) { return !el.disabled && el.offsetParent !== null; });
if (!focusable.length) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
} else {
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
}
}
function closeShareModal() {
var modal = document.getElementById('swym-share-modal');
if (modal) modal.style.display = 'none';
document.body.style.overflow = '';
document.removeEventListener('keydown', handleShareModalKeydown);
var trigger = document.getElementById('swym-share-btn');
if (trigger) trigger.focus();
}
function handleCopyLink(url) {
var feedback = document.getElementById('swym-share-copy-feedback');
var done = function() {
showModalFeedback(feedback);
setTimeout(closeShareModal, 800);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(done).catch(function() {
fallbackCopy(url, feedback);
setTimeout(closeShareModal, 800);
});
} else {
fallbackCopy(url, feedback);
setTimeout(closeShareModal, 800);
}
}
function handleEmailShare(swat, lid) {
var nameInput = document.getElementById('swym-share-name-input');
var emailInput = document.getElementById('swym-share-email-input');
var noteInput = document.getElementById('swym-share-note-input');
var emailBtn = document.getElementById('swym-share-email-btn');
var feedback = document.getElementById('swym-share-email-feedback');
if (!emailInput) return;
var email = emailInput.value.trim();
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
emailInput.focus();
return;
}
if (emailBtn) emailBtn.disabled = true;
swat.sendListViaEmail({
toEmailId: email,
note: noteInput ? noteInput.value.trim() : '',
fromName: nameInput ? nameInput.value.trim() : '',
lid: lid
}, function() {
if (emailBtn) emailBtn.disabled = false;
if (nameInput) nameInput.value = '';
emailInput.value = '';
if (noteInput) noteInput.value = '';
showModalFeedback(feedback);
setTimeout(closeShareModal, 1000);
}, function() {
if (emailBtn) emailBtn.disabled = false;
});
}
function handleSocialShare(swat, lid, platform, shareUrl) {
var platformUrlTemplate = '';
if (platform === 'facebook') platformUrlTemplate = 'https://www.facebook.com/sharer.php?u=' + encodeURIComponent(shareUrl) + '&t=';
else if (platform === 'twitter') platformUrlTemplate = 'https://twitter.com/share?text=&url=' + encodeURIComponent(shareUrl);
swat.shareListSocial(lid, '', platformUrlTemplate, platform, '', function() {});
if (platformUrlTemplate) window.open(platformUrlTemplate, '_blank', 'noopener,noreferrer');
closeShareModal();
}
function showModalFeedback(el) {
if (!el) return;
el.style.display = '';
setTimeout(function() { el.style.display = 'none'; }, 2500);
}
function fallbackCopy(url, feedbackEl) {
var ta = document.createElement('textarea');
ta.value = url;
ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none;';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); showModalFeedback(feedbackEl); } catch (e) {}
document.body.removeChild(ta);
}
function formatPrice(priceInCents) {
return '$' + (priceInCents / 100).toFixed(2);
}
function esc(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function fetchProductJSON(du) {
var parts = du.split('/products/');
if (parts.length < 2) return Promise.reject(new Error('invalid product URL'));
var url = window.location.origin + window.Shopify.routes.root + 'products/' + parts[1].split('?')[0] + '.js';
return fetch(url)
.then(function(response) {
if (!response.ok) throw new Error('product fetch failed');
return response.json();
});
}
function setWishlistTitle(name) {
var titleEl = document.getElementById('swym-wishlist-title');
if (titleEl) titleEl.textContent = name || 'My Wishlist';
}
function setupListSwitcher(swat, lists, activeLid, grid, countEl, emptyEl) {
var wrapper = document.getElementById('swym-list-switcher-wrapper');
var switcher = document.getElementById('swym-list-switcher');
if (!wrapper || !switcher) return;
if (!swat.isCollectionsEnabled() || lists.length < 2) {
wrapper.style.display = 'none';
return;
}
wrapper.style.display = '';
var fresh = switcher.cloneNode(false);
switcher.parentNode.replaceChild(fresh, switcher);
lists.forEach(function(list) {
var option = document.createElement('option');
option.value = list.lid;
option.textContent = list.lname + ' (' + list.listcontents.length + ' items)';
if (list.lid === activeLid) option.selected = true;
fresh.appendChild(option);
});
var activeList = lists.find(function(l) { return l.lid === activeLid; });
if (activeList) setWishlistTitle(activeList.lname);
fresh.addEventListener('change', function() {
var selected = lists.find(function(l) { return l.lid === fresh.value; });
if (!selected) return;
var products = selected.listcontents || [];
sessionStorage.setItem(SS_PRODUCTS_KEY, JSON.stringify({ lid: selected.lid, products: products }));
sessionStorage.removeItem(SS_SHARE_URL_KEY);
var count = products.length;
if (countEl) countEl.textContent = '(' + count + ' items)';
setWishlistTitle(selected.lname);
setupShareButton(swat, selected.lid, count);
setupAddAllButton(products);
renderProducts(swat, selected.lid, products, grid, countEl, emptyEl);
});
}
window.SwymCallbacks = window.SwymCallbacks || [];
window.SwymCallbacks.push(initWishlistPage);
})();assets/swym-wishlist-page.css
assets/swym-wishlist-page.css/* ============================================================
Swym Wishlist Page
============================================================ */
/* --- Page layout ------------------------------------------ */
#swym-wishlist-page {
padding: 2rem 1rem 4rem;
}
/* Desktop header: flex row, title pushes controls to the right */
.swym-page-header {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.swym-page-header h1 {
margin: 0 auto 0 0;
font-size: 2rem;
font-weight: 700;
line-height: 1.2;
}
.swym-header-actions {
display: flex;
gap: 8px;
align-items: center;
}
/* Mobile header: 2-col grid
[Title (count)] [Toggle]
[List switcher] [Add All] [Share] */
@media (max-width: 768px) {
.swym-page-header {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px 12px;
align-items: center;
margin-bottom: 0.75rem;
}
.swym-page-header h1 {
grid-column: 1;
font-size: 1.375rem;
margin: 0;
}
#swym-view-toggle {
grid-column: 2;
justify-self: end; /* prevent stretching to fill the auto column */
}
/* switcher auto-places to col 1, row 2 */
#swym-list-switcher-wrapper {
grid-column: 1;
}
/* actions sit beside the switcher in col 2, row 2 */
.swym-header-actions {
grid-column: 2;
justify-self: end;
}
}
#swym-wishlist-count {
font-size: 1.125rem;
font-weight: 400;
color: #6b7280;
}
/* --- List switcher ---------------------------------------- */
#swym-list-switcher-wrapper {
display: flex;
align-items: center;
gap: 6px;
}
#swym-list-switcher-wrapper label {
font-size: 1rem;
color: #374151;
white-space: nowrap;
}
#swym-list-switcher {
padding: 7px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
background: #fff;
cursor: pointer;
}
#swym-list-switcher:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* --- View toggle ------------------------------------------ */
#swym-view-toggle {
display: flex;
border: 1px solid #d1d5db;
border-radius: 6px;
overflow: hidden;
}
.swym-view-btn {
padding: 7px 16px;
background: #fff;
border: none;
border-left: 1px solid #d1d5db;
font-size: 0.9375rem;
font-weight: 500;
cursor: pointer;
color: #6b7280;
transition: background 0.15s, color 0.15s;
}
.swym-view-btn:first-child {
border-left: none;
}
.swym-view-btn.is-active {
background: #111827;
color: #fff;
}
.swym-view-btn:hover:not(.is-active) {
background: #f3f4f6;
color: #374151;
}
/* --- Share button ----------------------------------------- */
#swym-share-btn {
padding: 9px 18px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
#swym-share-btn:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
/* --- Loading / empty states ------------------------------- */
#swym-wishlist-loading {
padding: 3rem 0;
text-align: center;
color: #6b7280;
font-size: 1.0625rem;
}
#swym-wishlist-empty {
padding: 4rem 0;
text-align: center;
}
#swym-wishlist-empty p {
margin: 0 0 0.5rem;
color: #374151;
}
#swym-wishlist-empty p:first-child {
font-size: 1.25rem;
font-weight: 600;
}
#swym-wishlist-empty p:last-of-type {
color: #6b7280;
font-size: 1rem;
margin-bottom: 1.5rem;
}
#swym-wishlist-empty a {
display: inline-block;
padding: 11px 22px;
background: #111827;
color: #fff;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
text-decoration: none;
transition: background 0.15s;
}
#swym-wishlist-empty a:hover {
background: #374151;
}
/* --- Nudges ----------------------------------------------- */
#swym-login-nudge {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 18px;
margin-bottom: 1.5rem;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
font-size: 1rem;
color: #0369a1;
}
#swym-login-nudge p {
margin: 0;
flex: 1;
}
#swym-login-nudge a {
font-weight: 600;
color: #0369a1;
text-decoration: underline;
}
#swym-save-nudge {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 13px 18px;
margin-bottom: 1.5rem;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
font-size: 1rem;
color: #92400e;
}
#swym-save-nudge p {
margin: 0;
flex: 1;
line-height: 1.5;
}
#swym-save-nudge a {
color: #92400e;
font-weight: 600;
}
#swym-save-nudge-dismiss {
flex-shrink: 0;
padding: 0 4px;
background: none;
border: none;
font-size: 1.375rem;
line-height: 1;
cursor: pointer;
color: #92400e;
opacity: 0.7;
}
#swym-save-nudge-dismiss:hover {
opacity: 1;
}
/* --- Product grid ----------------------------------------- */
#swym-wishlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
align-items: start;
gap: 1.5rem;
margin-top: 2rem;
}
#swym-wishlist-grid[aria-busy="true"] {
min-height: 600px;
}
@media (max-width: 640px) {
#swym-wishlist-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
}
/* --- Product card ----------------------------------------- */
.swym-wishlist-card {
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
border-radius: 10px;
overflow: hidden;
background: #fff;
transition: box-shadow 0.15s;
}
.swym-wishlist-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.swym-wishlist-card > a:first-child {
display: block;
aspect-ratio: 1 / 1;
overflow: hidden;
background: #f9fafb;
}
.swym-wishlist-card > a:first-child img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.2s;
}
.swym-wishlist-card:hover > a:first-child img {
transform: scale(1.03);
}
.swym-wishlist-card > a:nth-child(2) {
display: block;
padding: 14px 14px 4px;
font-size: 1rem;
font-weight: 500;
color: #111827;
text-decoration: none;
line-height: 1.4;
}
.swym-wishlist-card > a:nth-child(2):hover {
text-decoration: underline;
}
.swym-wishlist-card > span {
padding: 4px 14px 0;
font-size: 1.0625rem;
font-weight: 600;
color: #111827;
}
/* --- Card actions ----------------------------------------- */
.swym-wishlist-card-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 14px 14px;
margin-top: auto;
}
/* --- Variant selector ------------------------------------- */
.swym-wishlist-variant-label {
flex-basis: 100%;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: -2px;
}
.swym-wishlist-variant-select {
flex: 1 0 100%;
padding: 7px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.9375rem;
background: #fff;
color: #111827;
cursor: pointer;
appearance: auto;
}
.swym-wishlist-variant-select:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
.swym-wishlist-cart-btn {
flex: 1;
padding: 9px 12px;
background: #111827;
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.9375rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
white-space: nowrap;
}
.swym-wishlist-cart-btn:hover:not(:disabled) {
background: #374151;
}
.swym-wishlist-cart-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.swym-wishlist-remove-btn {
flex-shrink: 0;
width: 38px;
height: 38px;
padding: 0;
background: none;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-size: 1.25rem;
line-height: 1;
cursor: pointer;
color: #6b7280;
transition: color 0.15s, border-color 0.15s;
}
.swym-wishlist-remove-btn:hover {
color: #ef4444;
border-color: #fca5a5;
}
/* --- Add All to Cart button -------------------------------- */
#swym-add-all-btn {
padding: 9px 18px;
background: #111827;
color: #fff;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s;
}
#swym-add-all-btn:hover:not(:disabled) {
background: #374151;
}
#swym-add-all-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
/* --- Quantity stepper ------------------------------------- */
.swym-wishlist-qty {
display: flex;
align-items: center;
flex-basis: 100%;
gap: 0;
border: 1px solid #d1d5db;
border-radius: 6px;
overflow: hidden;
height: 36px;
}
.swym-wishlist-qty button {
flex-shrink: 0;
width: 36px;
height: 100%;
padding: 0;
background: #f9fafb;
border: none;
font-size: 1.125rem;
line-height: 1;
cursor: pointer;
color: #374151;
transition: background 0.15s;
}
.swym-wishlist-qty button:hover {
background: #f3f4f6;
}
.swym-wishlist-qty-value {
flex: 1;
text-align: center;
font-size: 0.9375rem;
font-weight: 500;
color: #111827;
border-left: 1px solid #d1d5db;
border-right: 1px solid #d1d5db;
line-height: 36px;
min-width: 32px;
user-select: none;
}
/* --- Item note -------------------------------------------- */
.swym-wishlist-note-label {
flex-basis: 100%;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: -2px;
}
.swym-wishlist-note {
flex: 1 0 100%;
padding: 8px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.9375rem;
color: #111827;
background: #fff;
resize: vertical;
min-height: 60px;
font-family: inherit;
line-height: 1.4;
}
.swym-wishlist-note:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
.swym-wishlist-note::placeholder {
color: #9ca3af;
}
/* --- Wishlist table --------------------------------------- */
#swym-wishlist-table-wrapper {
margin-top: 2rem;
}
/* Columns: img | product | qty | price | note | actions */
.swym-table-header,
.swym-table-row {
display: grid;
grid-template-columns: 72px 2fr 120px 90px 1fr 160px;
align-items: center;
}
.swym-table-header {
border-bottom: 2px solid #e5e7eb;
}
.swym-table-header > div {
display: block; /* prevent theme :empty rules from collapsing empty cells */
padding: 8px 12px;
font-size: 0.8125rem;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
}
.swym-table-row {
border-bottom: 1px solid #f3f4f6;
transition: background 0.1s;
}
.swym-table-row:hover {
background: #f9fafb;
}
.swym-table-col-img,
.swym-table-col-product,
.swym-table-col-qty,
.swym-table-col-price,
.swym-table-col-note,
.swym-table-col-actions {
padding: 12px;
}
.swym-table-col-img img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 6px;
display: block;
}
.swym-table-col-product a {
font-weight: 500;
color: #111827;
text-decoration: none;
display: block;
}
.swym-table-col-product a:hover {
text-decoration: underline;
}
.swym-table-variant-title {
display: block;
font-size: 0.8125rem;
color: #6b7280;
margin-top: 3px;
}
.swym-table-line-price {
font-size: 0.9375rem;
font-weight: 600;
color: #111827;
white-space: nowrap;
}
.swym-table-note {
min-height: 38px;
}
.swym-table-actions-inner {
display: flex;
gap: 6px;
align-items: center;
}
.swym-table-actions-inner .swym-wishlist-cart-btn {
flex: 1;
padding: 7px 10px;
font-size: 0.875rem;
}
@media (max-width: 768px) {
.swym-table-header {
display: none;
}
/* Layout per card:
[img] Title [×]
[img] Variant
[img] [-] qty [+] $price
[Add to Cart ──────] */
.swym-table-row {
grid-template-columns: 56px 1fr auto;
grid-template-rows: auto auto auto auto;
gap: 6px 12px;
padding: 12px 0;
align-items: start;
}
.swym-table-col-img {
grid-column: 1;
grid-row: 1 / 4;
align-self: start;
padding: 0;
}
.swym-table-col-img img {
width: 52px;
height: 52px;
}
/* title + variant stacked (display:block on both) fill rows 1-2 */
.swym-table-col-product {
grid-column: 2;
grid-row: 1 / 3;
padding: 0;
}
.swym-table-col-qty {
grid-column: 2;
grid-row: 3;
padding: 0;
}
.swym-table-col-price {
grid-column: 3;
grid-row: 3;
padding: 0;
text-align: right;
align-self: center;
}
.swym-table-col-note {
display: none;
}
/* Dissolve the actions wrapper so each button becomes a direct grid item */
.swym-table-col-actions,
.swym-table-col-actions .swym-table-actions-inner {
display: contents;
}
.swym-table-col-actions .swym-wishlist-remove-btn {
grid-column: 3;
grid-row: 1;
align-self: start;
padding: 2px 6px;
font-size: 1rem;
line-height: 1;
}
.swym-table-col-actions .swym-wishlist-cart-btn {
grid-column: 2 / 4;
grid-row: 4;
width: 100%;
}
}
/* --- Share modal ------------------------------------------ */
#swym-share-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.25rem;
}
#swym-share-modal-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #111827;
}
#swym-share-modal-close {
padding: 4px 8px;
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: #6b7280;
border-radius: 4px;
transition: background 0.15s, color 0.15s;
}
#swym-share-modal-close:hover {
background: #f3f4f6;
color: #111827;
}
.swym-share-section {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 1.25rem;
}
.swym-share-section label {
font-size: 0.9375rem;
font-weight: 500;
color: #374151;
}
.swym-share-section input,
.swym-share-section textarea {
padding: 9px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
color: #111827;
background: #fff;
resize: vertical;
}
.swym-share-section input:focus,
.swym-share-section textarea:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
.swym-share-section textarea {
min-height: 72px;
}
#swym-share-email-btn {
align-self: flex-start;
padding: 10px 20px;
background: #111827;
color: #fff;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
}
#swym-share-email-btn:hover:not(:disabled) {
background: #374151;
}
#swym-share-email-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
#swym-share-email-feedback,
#swym-share-copy-feedback {
font-size: 0.9375rem;
color: #16a34a;
font-weight: 500;
}
.swym-share-divider-label {
margin: 0 0 8px;
font-size: 0.9375rem;
color: #9ca3af;
text-align: center;
position: relative;
}
.swym-share-divider-label::before,
.swym-share-divider-label::after {
content: '';
position: absolute;
top: 50%;
width: 38%;
height: 1px;
background: #e5e7eb;
}
.swym-share-divider-label::before { left: 0; }
.swym-share-divider-label::after { right: 0; }
.swym-share-social-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.swym-share-social-btn,
#swym-share-copy-btn {
flex: 1;
padding: 9px 14px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
font-size: 0.9375rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
white-space: nowrap;
}
.swym-share-social-btn:hover,
#swym-share-copy-btn:hover:not(:disabled) {
background: #f3f4f6;
border-color: #9ca3af;
}
#swym-share-copy-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.swym-wishlist-collections-v2-container{
display: none !important;
}The selectors used across both setups are:
| Selector | Purpose |
|---|---|
#swym-add-all-btn | Add All to Cart button in the header row |
#swym-list-switcher-wrapper | Wrapper for the list <select> (Multiple Wishlists only) |
#swym-list-switcher | The <select> element for switching between lists (Multiple Wishlists only) |
.swym-wishlist-card | Individual product card |
.swym-wishlist-card-actions | Actions container inside each card |
.swym-wishlist-qty | Wrapper for decrease button, quantity display, increase button |
.swym-wishlist-qty-value | Quantity number display |
.swym-wishlist-note-label | "Note" label above the textarea |
.swym-wishlist-note | Note textarea |
.swym-wishlist-cart-btn | Add to Cart button; [disabled] state = out of stock |
.swym-wishlist-remove-btn | Remove from wishlist button |
#swym-share-btn | Share Wishlist button in the header row |
#swym-share-modal | Share modal overlay wrapper |
#swym-share-modal-content | Share modal panel |
#swym-share-modal-header | Share modal title row |
.swym-share-section | Share modal content section (email form, social row) |
.swym-share-social-row | Row of social share buttons |
.swym-share-social-btn | Individual social share button |
.swym-share-divider-label | "or Share via" label between sections |
#swym-login-nudge | Sign-in prompt for anonymous users |
#swym-save-nudge | "Don't lose your list" banner for anonymous users with 3+ items |