npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@togenar/embed

v1.7.0

Published

The <togenar-embed> web component: drop a live 3D product configurator and native AR into your own storefront, and drive it from your cart.

Readme

<togenar-embed> — live 3D product configurator & AR for any storefront

@togenar/embed is the official Togenar web component. One script tag — or one import — puts a live 3D product configurator with native iOS / Android AR on any product page: shoppers pick colors, materials and options, see the product in their own space, and hand your cart the exact configured SKUs. 3.4 KB gzipped, zero dependencies, TypeScript definitions included.

  • Docs: docs.togenar.com — quick start, attributes, full SDK reference
  • Live tester: Test your embed — paste a project ID, verify end to end in the browser
  • Demo: demo.html — self-contained reference implementation for any published project

The rest of this page documents the add-to-cart integration: passing the configured product into your own cart, without the shopper leaving your site.

Division of responsibility. Togenar delivers the configuration and presentation layer end to end: the embed, the customization interface, AR, snapshots, share links, and live stock and price display. Your commerce platform remains the system of record for cart, checkout, inventory, tax and payment. The embed returns the configured SKU; a short integration posts it to your existing cart. This page specifies that integration.


1. Add the embed

<script type="module" src="https://model.togenar.com/embed/togenar-embed.js"></script>

<togenar-embed
  id="tg"
  project="YOUR_PROJECT_ID"
  configurator
  aspect="1/1">
</togenar-embed>

Common attributes: configurator (enable option picking), aspect="16/9" | "1/1" or height="520px", lang, picker="off" (hide the built-in panel and drive it from your own UI — see §5), mode="launcher" (device-adaptive AR launcher page instead of the inline viewer).


2. Add to cart in three steps

The embed emits togenar:configurator:selection_change on load and on every option change. Its detail is:

{
  "parts": [
    { "partId": "…", "partLabel": "Body", "variantId": "…", "label": "Walnut",
      "sku": "OSLO-BODY-WALNUT", "modelName": "…" }
  ],
  "shareUrl": "https://model.togenar.com/…"   // deep-link to this exact configuration
}

So the pattern is: wait for a valid selection → enable your button → on click, read the SKUs and POST them to your cart.

<togenar-embed id="tg" project="YOUR_PROJECT_ID" configurator aspect="1/1"></togenar-embed>
<button id="add" disabled>Add to cart</button>

<script>
  const tg  = document.getElementById('tg');
  const btn = document.getElementById('add');

  // 1) Enable the button once the shopper has a complete configuration.
  tg.addEventListener('togenar:configurator:selection_change', (e) => {
    const skus = (e.detail.parts || []).map(p => p.sku).filter(Boolean);
    btn.disabled = skus.length === 0;
  });

  // 2) On click, read the current configuration…
  btn.addEventListener('click', async () => {
    const skus     = tg.getSkus();        // ["OSLO-BODY-WALNUT", "OSLO-LEGS-CHROME"]
    const shareUrl = tg.getShareUrl();    // permalink to rebuild this exact config later

    // 3) …and hand it to YOUR store. Replace this with your real cart call (see §4).
    await fetch('/cart/add', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ skus, quantity: 1, configuration_url: shareUrl }),
    });
  });
</script>

getSkus() / getSelection() / getShareUrl() are synchronous reads of the last selection_change — no round-trip. They return null/[] until the first event arrives, so always gate your button on the event as shown above.

Persist shareUrl on the order line. It rebuilds the shopper's exact configuration for order review, fulfillment, or re-order — the one field worth storing beyond the SKUs.


3. View in their room (AR)

document.getElementById('view-in-ar').addEventListener('click', () => {
  tg.enterAR();   // iOS Quick Look / Android Scene Viewer / WebXR
});

Call enterAR() from your own click handler — starting a WebXR session may require an in-page user gesture per the browser's activation policy. AR must be enabled on the workspace plan; on desktop, use getQr() for a "scan to view in AR" code.

When AR finishes on a different device

A desktop shopper scans that QR code and ends the journey on a phone that never loaded your site — no page of yours to receive togenar:ar-add-to-cart, and none to push a price for Apple's AR banner. The embed hands your product-page URL to the viewer so the hop keeps both: the desktop session's price travels with the QR link, and an AR add-to-cart tap returns the shopper to your page with the configuration and togenar_ar_cart=1.

const q = new URLSearchParams(location.search);
if (q.get('togenar_ar_cart') === '1') {
  addToCart(tg.getSkus(), tg.getShareUrl());   // same handler as your button

  q.delete('togenar_ar_cart');                 // a refresh must not add twice
  history.replaceState(null, '', location.pathname + (q.toString() ? `?${q}` : '') + location.hash);
}

| Attribute | Purpose | | --- | --- | | page-url | Product-page URL for that hand-off. Defaults to the embedding page's own URL — set it only when the embed lives somewhere else (an iframe of your own, a preview shell). |

The URL's domain must be on your workspace embed allowlist; otherwise the hand-off stays off and AR behaves as before. A return address that anyone could put in a QR code would make the viewer an open redirect, so it has to be registered in advance — domain allowlist.


4. Concrete: Shopify & WooCommerce

Both platforms add to cart by their internal variant/product ID, not by SKU. So the one real integration task is mapping the Togenar SKU → your platform's ID. That mapping is what the generic /cart/add above stands in for.

Shopify — the turnkey connector

On Shopify you don't build that map by hand. Load the connector next to the embed and it reads your storefront's own product JSON, keys everything by SKU, pushes prices + stock, and wires any data-togenar-add-to-cart button to /cart/add.js:

<script type="module" src="https://model.togenar.com/embed/togenar-shopify.js"></script>

<togenar-embed project="YOUR_PROJECT_ID" configurator shopify
  shopify-handles="{{ product.handle }}"
  shopify-currency="{{ cart.currency.iso_code }}"></togenar-embed>

<button data-togenar-add-to-cart disabled>Add to cart</button>

Pasteable into a Custom Liquid block as-is — full attribute reference in the Shopify connector docs. Also importable as @togenar/embed/shopify. The manual pattern below is what the connector does under the hood, kept for custom setups.

Shopify (AJAX Cart API)

// Map each configured SKU to its Shopify variant ID. Build this map from your product data
// (e.g. window.meta / a metafield / the product JSON) — Shopify's cart API takes IDs, not SKUs.
const SKU_TO_VARIANT = {
  'OSLO-BODY-WALNUT': 40123456789012,
  'OSLO-LEGS-CHROME': 40123456789013,
};

btn.addEventListener('click', async () => {
  const items = tg.getSkus()
    .map(sku => SKU_TO_VARIANT[sku])
    .filter(Boolean)
    .map(id => ({ id, quantity: 1, properties: { _config: tg.getShareUrl() } }));

  await fetch('/cart/add.js', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ items }),
  });
  // then refresh your cart drawer / redirect to /cart
});

For a configured bundle you typically sell one parent SKU (map to a single variant) and stash the full part list + shareUrl in line-item properties.

WooCommerce (Store API)

await fetch('/wp-json/wc/store/v1/cart/add-item', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'Nonce': wcStoreApiNonce },
  body: JSON.stringify({ id: WC_PRODUCT_ID, quantity: 1 }),
});

Resolve WC_PRODUCT_ID from the Togenar SKU the same way (SKU → product/variation ID map).

The Shopify side of this mapping ships today as the turnkey connector above. A WooCommerce plugin that auto-wires the same contract is on the roadmap — until then, this ~15-line snippet is the whole WooCommerce integration.


5. Two-way sync (optional but recommended)

Keep the embed in step with your catalogue — all async, all keyed by the same SKUs/handles the events report:

| Method | Purpose | | --- | --- | | setAvailability({ body: { walnut: false } }) | Push live stock. Out-of-stock swatches dim + become unselectable. | | setPrices({ currency: 'EUR', locale: 'de-DE', items: { 'OSLO-BODY-WALNUT': 129.9 }, total: 259.8 }) | Show your prices in the summary. The viewer never computes price — your page stays the pricing source of truth. | | getOptions() | Enumerate every part + variant (with select()-ready handles + sku) to build your own option panel. Each entry carries inPickerfalse for parts a link group drives — so filter(o => o.inPicker) matches the published panel. Every variant carries available, and while it is false also disabledReason ('rule' — the current configuration rules it out; 'stock' — you reported it out of stock) and blockedBy, the selections to change first ([{ partKey, handle, … }]), so your panel can say "not available with Body: Oak" instead of only greying the swatch. | | select(partKey, variantKey) | Drive a swap from your own UI. Resolves after the model settles. An option with its own recorded camera angle eases the camera there; one sharing its part's angle leaves it still. An option that cannot be picked right now resolves { ok: false, error: 'blocked', reason, blockedBy, selection } and changes nothing. | | setAnimationState(partKey, open) | Open or close an option that moves (an oven door, a drawer). Separate from select() — the choice and its state are two different things. Idempotent; getOptions() reports which variants carry one (variants[].animation) and where the selected one stands (animationState.open). | | getGroups() | Enumerate the product's module slots — visibility groups, where one member part is shown at a time (e.g. Shelf 1 with 22 alternative modules). Each member carries a showPart()-ready handle, plus available / disabledReason / blockedBy while a rule blocks that module. getOptions() reports membership but cannot change it. | | showPart(partKey) | Show one member of a module slot. The group is resolved from the member itself and membership is re-checked, so an unknown handle resolves to { ok: false } and changes nothing; a module a rule blocks resolves { ok: false, error: 'blocked', reason: 'rule', blockedBy }. | | getProducts() | The products a configurator offers (a two-seater and a one-seater of the same sofa) — [{ productId, key, label, active }], key being what ?product= carries. Empty when the project has none. | | selectProduct(productKey) | Switch the product from your own UI, e.g. selectProduct('one-seater') — the same switch as the built-in panel's product header. Resolves after the new product has loaded; matching choices carry over and getOptions() then lists its parts. An unknown key resolves { ok: false } and changes nothing. | | focusPart(partKey) | Apply the camera angle recorded for a part — call it when your panel changes section, not when a swatch is picked. null for a section that is not a part, and a part with no recorded angle, ease back to the opening framing. moved: false means the project does not move the camera on part change. | | setDimensions(on?) | Show or hide the product's measurement overlay; omit the argument to flip it, and render the reply's on on your button. Measurements have to be on for the project. | | getControls() | What the viewer's own controls would be offering right now — { ar, photo, dimensions: { enabled, on }, resetView }. With controls="off" this is how your buttons know which ones are live; the togenar:controls event fires on every change. | | resetCamera() | Ease the camera back to the opening framing. Show your own button while getControls().resetView is true — that is when the viewer would have shown its own. | | reset() | Back to the published default. | | addedToCart(detail?) | Confirms that your cart call succeeded — call it from your own Add-to-cart handler. On Android this is what credits an AR-driven sale: Google allows no button inside AR, and Togenar does not render a second one over yours, so an add that lands immediately after the shopper leaves AR is attributed to that session. | | getShareLink() / getQr() / getSnapshot() | Short share link, "scan for AR" QR, PNG hero of the current config. |

Headless mode (picker="off") hides the built-in panel so your getOptions() + select() UI is the only chrome.


6. Events reference

Listen with tg.addEventListener('togenar:<name>', e => …):

| Event | When | | --- | --- | | togenar:ready | Viewer loaded and interactive. | | togenar:configurator:selection_change | On load + every option change, including a module slot changing (whether the shopper used the built-in picker or your own showPart() call). detail = { parts, shareUrl }, plus activeMembers on products that have module slots. | | togenar:configurator:product_change | The product changed — from the built-in panel's product header or your selectProduct() call. detail = { product, productId, label }. A selection_change follows with the new product's parts; redraw your panel from that. | | togenar:configurator:enquire | Shopper tapped the summary panel's enquire button (needs the enquire attribute). detail = { parts, skus, shareUrl }. | | togenar:ar-add-to-cart | Shopper added the configured product to the cart from inside iOS AR (enable AR Add to Cart per project in WebAR settings). detail = { surface, arMode, label, projectId }. Wire it to your cart. Android has no in-AR button — call addedToCart() from your own button instead, and the add is credited to the AR session. | | togenar:commerce:add_to_cart_success / …_fail | Emitted by the viewer's own commerce flows. To report your cart call, use addedToCart() above. | | togenar:error | Load/config failure. |

Conversion attributes

| Attribute | Purpose | | --- | --- | | enquire | Show the summary panel's enquire button. Off by default — a button with no listener is a dead end. | | enquire-label | Rename it ("Request a quote", "Add to cart", …). | | consent="analytics" | Opt in to configurator funnel analytics (option picks, enquiries). Off by default; you are the data controller for it. |

<togenar-embed project="…" configurator enquire enquire-label="Request a quote" consent="analytics"></togenar-embed>
tg.addEventListener('togenar:configurator:enquire', (e) => openQuoteForm(e.detail));

Because the tap happens inside the configurator, it can be attributed — a buy button that lives outside the 3D player cannot be.

Presentation attributes

| Attribute | Purpose | | --- | --- | | reflection | Studio-style ground reflection under the product. Off by default; skipped automatically on heavy or animated models and inside AR. |


7. Reference implementation

demo.html in this folder is a live, self-contained reference build — including a working Add to cart card that shows the exact payload a real store would POST. Open it against a published project to see the full customize → AR → add-to-cart flow end to end.