The PDP bundle widget (“Buy together, save 10%”) - renders the bundles the Storesynk Shopify
app synced for the current product: one PUBLIC_READ $app:bundle metaobject per live
bundle (title, percentage pricing, members + per-set quantities, date window), referenced from
the product’s bundles metafield and read with the browser-safe public token. Like the
collection module, it ships as its own on-demand chunk (bundle.js, loaded only when a
<storesynk-bundle> is on the page - nothing to configure, the elements bundle handles it).
A bundle renders only while its date window is open and every member resolves
(deleted/unpublished members drop the whole bundle). Blueprint: build-patterns §2.10.
Namespace is required configuration: Storefront queries cannot use the $app: shorthand,
so the app’s concrete namespace (app--<numeric-app-id>) must reach the widget - via
app-namespace on <storesynk-store> (recommended; one setting serves every widget) or a
per-widget namespace attribute. Neither → the widget stays [ss-empty] (SSK-211).
storesynk-bundle
Section titled “storesynk-bundle”- Purpose: the widget root. Queries the current product’s live bundles and clones its template once per bundle.
- Attributes:
namespace : string = ""- overrides the store-levelapp-namespacefor this widget alone.
- Required ancestor / context:
storesynk-product(consumesproductContextfor the product id - SSK-102) +storeContext(client, locale,appNamespace). - Children / template rule: TEMPLATE-CLONE - its first element child (a
bundle-offer, or a wrapper containing one) is the per-bundle template (SSK-107). - Events / state: sets
[ss-loading]during the fetch and[ss-empty]when there are no live bundles (or no namespace) - hide it viastoresynk-bundle[ss-empty] { display: none }. A market/locale switch re-fetches (prices are market-aware).
bundle-offer
Section titled “bundle-offer”- Purpose: one bundle. Provides
bundleContext(entry,members,pricing) to the displayers, member list, and add button inside; fed viaprovideBundle()by the widget. - Attributes: none authored (
ss-bundle-keyis SSR-set - never author it). - Required ancestor:
storesynk-bundle(SSK-106 - only the widget clones and feeds it). - State it owns: the bundle’s variant selection. Each member card is a full
storesynk-product, so a shopper’s pick bubbles the standardsf:variant-changeup here; the offer records the selection per member and re-provides the bundle with re-keyed members + recomputed pricing - every displayer, the ×N subtotals, and the add button’s variant ids track the selection. Selection state is per clone: two bundles sharing a product select independently. A shopper can only select among the member’s merchant-allowed variants (the card is fed only those), so the add button never sends a disallowed id.
bundle-member-list
Section titled “bundle-member-list”- Purpose: the bundle’s member cards. Clones its member-card template once per member and
feeds each clone’s
storesynk-productviaprovideProduct()- the standard product displayers (show-title,show-price,show-image,product-link,change-option, …) all work inside. When the merchant restricted a member to specific variants, the card is fed only those variants -change-optionoffers only the allowed values (option values are derived from the allowed variants), exactly one allowed variant renders no selector, and unknown ids fall back to all variants. Also attaches a per-clonebundleMemberContextprovider (the resolved member with its bundle quantity) read by theshow-member-*displayers. - Attributes: none.
- Required ancestor / context:
bundle-offer(consumesbundleContext- SSK-105). - Children / template rule: TEMPLATE-CLONE - its first element child (a
storesynk-product, or a wrapper containing one) is the member-card template (SSK-107). The member-cardstoresynk-productcarries nohandle/product-idand is the one sanctionedstoresynk-productnesting (exempt from SSK-103). Selection updates refresh cards in place (re-cloning would wipe the shopper’s picks). - Variant pickers in the card: use
change-optionin native-<select>mode with the positionalgroup="1"/"2"/"3"idiom (members are arbitrary products; extras self-hide). Single-variant members render no selector (the synthetic Title option is stripped from the member’s data). Sold-out combos come pre-disabled in the<select>. A member with more variants than the query cap (100) degrades to default-variant mode - no selectors - and its card gets[ss-variants-truncated]: style a “choose options on the product page”product-linkaffordance off it.
show-bundle-title / show-bundle-total / show-bundle-original-total / show-bundle-savings / show-bundle-percentage
Section titled “show-bundle-title / show-bundle-total / show-bundle-original-total / show-bundle-savings / show-bundle-percentage”- Purpose: bundle displayers - the customer-facing title; the discounted total (what the
Cart Transform merge will charge); the pre-discount component sum (struck-through “was”
price); the money saved; the discount percentage rendered as
N%(add suffixes like ” off” via CSS::after). - Attributes: none. Money displayers format via the store’s
formatMoney. - Required ancestor / context:
bundle-offer(consumebundleContext- SSK-105);storeContextfor money formatting.
- State:
show-bundle-original-total/show-bundle-savings/show-bundle-percentageset[ss-empty]when there are no savings (0%) - hide them via[ss-empty] { display: none }.
show-member-quantity / show-member-subtotal
Section titled “show-member-quantity / show-member-subtotal”- Purpose: member-card displayers - the member’s per-set quantity as “×N” (e.g.
×2), and the member’s line contribution (unit price × quantity, cent-exact with the bundle total). - Attributes: none.
- Required ancestor / context: inside the member-card template within
bundle-member-list(consume the per-clonebundleMemberContext- SSK-105); +storeContextfor money. - State:
show-member-quantityhides (nativehidden+[ss-empty]) at quantity 1 - a single unit needs no multiplier;show-member-subtotalhides the same way when no subtotal is computable.
add-bundle-to-cart
Section titled “add-bundle-to-cart”- Purpose: adds every member to the cart in ONE multi-line
window.Shopify.actions.updateCart({ lines })call (each member’s selected variant - default until the shopper picks - with its bundle quantity); the app’s Cart Transform Function merges those lines into the single discounted bundle line server-side, so the cart drawer renders it natively with no extra work. - Attributes: none. Recommended: nest a native
<button>(else the host upgrades itself torole="button"+tabindex+ Enter/Space, per the action-control conventions). - Required ancestor / context:
bundle-offer(consumesbundleContext- SSK-105). - Events: emits
sf:add-to-cart(bubbles, composed;detail = { bundle }) on success - the cart drawer’sopen-on-addworks for bundle adds too. A failed add never fires it. - State it sets (VariantButton conventions):
[ss-unavailable]on itself when some member’s selection resolved to no variant,[ss-out-of-stock]when every member has a variant but at least one is unpurchasable (mutually exclusive; the bundle stays visible either way);[ss-loading]during the add;aria-disabled="true"on the focusable control whenever blocked (clicks are ignored).
Imported from the Storesynk skill v0.1.0 (references/component-reference.md). To change this page, change it there.