@blamejs/blamejs-shop 0.0.44 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.0.x
10
10
 
11
+ - v0.0.45 (2026-05-22) — **Cart line items — thumbnail + product title + slug-linked anchor alongside the SKU chip.** Cart rows previously rendered only the SKU code, qty, unit, and total — the visitor had to remember what they actually added. Each line now leads with a 3.5rem rounded thumbnail (from the product's first `catalog.media` row), the product title in a bold inline label, and the SKU as a small monospace chip below. The whole product cell is an anchor back to the PDP so a visitor can re-enter the product page directly from the cart. Lines whose variant is missing media render the placeholder tile (dashed-border + the diagonal-stripe pattern the catalog empty-state uses). **Changed:** *`CART_LINE_EDITABLE` template — product cell replaces SKU-only cell* — First cell is now `<a class="cart-line__product-link" href="/products/<slug>">` wrapping a thumbnail + a `<span class="cart-line__product-meta">` that stacks the product title (bold) and SKU chip (monospace, on a `--bg-2` background). Hover on the link tints both the title and the link affordance to the accent color. The qty / unit / total / action cells are unchanged. · *`storefront.renderCart({ ..., product_lookup })` accepts a variant→product lookup* — Routes pass a `{ variant_id: { product, hero_media } }` map; lines render with the matching product's title + slug-linked URL + hero-media thumbnail. Lines without a lookup match (variant deleted, media not attached yet) fall through to a SKU-as-title display with the placeholder tile. The cart-table column header is now 'Product' instead of 'SKU'. · *`GET /cart` builds the product lookup* — After listing the cart's lines, the route walks each unique `variant_id` (cached by id so a cart with the same variant twice only hits the catalog once), pulls `catalog.variants.get` → `catalog.products.get` + `catalog.media.listForProduct`, and bundles the result for the renderer.
12
+
11
13
  - v0.0.44 (2026-05-22) — **Package rename — `@blamejs/blamejs-shop` (scoped under the org alongside `@blamejs/core` + `@blamejs/exceptd-skills`).** The npm package is now published under the `blamejs` organization scope as `@blamejs/blamejs-shop`. Installation switches from `npm install blamejs-shop` to `npm install @blamejs/blamejs-shop`; the import surface is unchanged — `require("@blamejs/blamejs-shop")` returns the same shop primitive set the unscoped name did. The legacy unscoped package will be deprecated on the registry with a pointer to the new name so existing operators see the upgrade path on their next install. **Changed:** *`package.json#name` → `@blamejs/blamejs-shop`* — Matches the existing org-scoped packages (`@blamejs/core`, `@blamejs/exceptd-skills`). The publish workflow already passes `--access public` to `npm publish`, which is the access mode scoped packages need to land as publicly-installable. · *Install command* — `npm install @blamejs/blamejs-shop` replaces `npm install blamejs-shop`. The `require` path follows: `var bShop = require("@blamejs/blamejs-shop");`. The entry point + export shape are byte-for-byte identical to v0.0.43 — only the resolution path changes. **Migration:** *Operator upgrade path* — Operators on the unscoped `blamejs-shop` change one line in their `package.json` dependencies: `"blamejs-shop": "^0.0.43"` → `"@blamejs/blamejs-shop": "^0.0.44"`. Every `require("blamejs-shop")` call site needs the matching update (`require("@blamejs/blamejs-shop")`). The unscoped name will be deprecated on the registry with a pointer to the new scoped path so a fresh `npm install` on the old name surfaces the migration message in npm's stderr.
12
14
 
13
15
  - v0.0.43 (2026-05-22) — **Search results — image-bearing product cards (consistent with the home grid).** The search results page rendered text-only `PRODUCT_CARD` tiles — title + price + 'View product →' link — while the home catalog grid (v0.0.42) shifted to clickable image-bearing `product-card` tiles. This release unifies the two surfaces: search results now go through the same `_buildProductCard(p)` picker so a hit lands a real hero-image tile when the product carries media, and the text-only fallback when it doesn't. The same `asset_prefix` override the home renderer accepts also works on the search renderer. **Changed:** *`storefront.renderSearch({ products, asset_prefix? })` uses `_buildProductCard`* — Same render-time picker the home grid uses — when a result product carries `hero_media`, the card renders as the image-bearing tile; without media, the text-only fallback. `asset_prefix` defaults to `/assets/` and can be overridden for operators on a different R2 binding or CDN remap. · *Search route fetches `catalog.media.listForProduct` per hit* — `GET /search` now calls the media lookup alongside the existing variants + prices loop. The first attached media row becomes `product.hero_media` on the data shape `renderSearch` consumes — same shape the home route v0.0.42 emits.
package/lib/storefront.js CHANGED
@@ -711,9 +711,22 @@ var CART_LINE =
711
711
  // /cart/lines/:id/remove). HTML forms don't natively support
712
712
  // PATCH/DELETE so the framework routes use POST with verb-suffix
713
713
  // paths.
714
+ // Editable cart line. The first cell carries a small media tile +
715
+ // the product title + the SKU code chip below it; without media,
716
+ // the tile drops to a dashed-border placeholder so the row's grid
717
+ // doesn't collapse. `product_url` is the slug-linked anchor so the
718
+ // visitor can re-enter the PDP from the cart without retyping.
714
719
  var CART_LINE_EDITABLE =
715
720
  "<tr>\n" +
716
- " <td class=\"cart-line__sku\"><code>{{sku}}</code></td>\n" +
721
+ " <td class=\"cart-line__product\">\n" +
722
+ " <a class=\"cart-line__product-link\" href=\"{{product_url}}\">\n" +
723
+ " RAW_CART_LINE_THUMB\n" +
724
+ " <span class=\"cart-line__product-meta\">\n" +
725
+ " <span class=\"cart-line__product-title\">{{product_title}}</span>\n" +
726
+ " <code class=\"cart-line__sku-chip\">{{sku}}</code>\n" +
727
+ " </span>\n" +
728
+ " </a>\n" +
729
+ " </td>\n" +
717
730
  " <td class=\"cart-line__qty\">\n" +
718
731
  " <form method=\"post\" action=\"/cart/lines/{{line_id}}/update\" class=\"cart-line__update\">\n" +
719
732
  " <input type=\"number\" name=\"qty\" value=\"{{qty}}\" min=\"1\" max=\"99\" class=\"cart-line__qty-input\" aria-label=\"Quantity\">\n" +
@@ -952,7 +965,7 @@ var CART_PAGE =
952
965
  " <div class=\"cart-page__items\">\n" +
953
966
  " <div class=\"table-scroll\">\n" +
954
967
  " <table class=\"cart-table\">\n" +
955
- " <thead><tr><th>SKU</th><th>Quantity</th><th>Unit</th><th>Total</th><th class=\"variant-table__action-h\">Action</th></tr></thead>\n" +
968
+ " <thead><tr><th>Product</th><th>Quantity</th><th>Unit</th><th>Total</th><th class=\"variant-table__action-h\">Action</th></tr></thead>\n" +
956
969
  " <tbody>{{line_rows}}</tbody>\n" +
957
970
  " </table>\n" +
958
971
  " </div>\n" +
@@ -974,13 +987,27 @@ function renderCart(opts) {
974
987
  var lines = opts.lines || [];
975
988
  var totals = opts.totals || { subtotal_minor: 0, grand_total_minor: 0, currency: "USD" };
976
989
  var shopName = opts.shop_name || "blamejs.shop";
990
+ var assetPrefix = opts.asset_prefix || "/assets/";
991
+ // `product_lookup` is { variant_id: { product, hero_media } } — the
992
+ // route handler bundles it in. Lines without an entry render with
993
+ // a dashed-placeholder tile + the SKU as the fallback title.
994
+ var lookup = opts.product_lookup || {};
977
995
  var rendered = lines.map(function (l) {
996
+ var match = lookup[l.variant_id] || null;
997
+ var prod = match && match.product;
998
+ var hero = match && match.hero_media;
999
+ var imageUrl = hero ? assetPrefix + hero.r2_key : null;
1000
+ var imageAlt = hero ? (hero.alt_text || (prod && prod.title) || l.sku) : null;
978
1001
  return {
979
- id: l.id,
980
- sku: l.sku,
981
- qty: String(l.qty),
982
- unit: pricing.format(l.unit_amount_minor, l.unit_currency),
983
- total: pricing.format(l.qty * l.unit_amount_minor, l.unit_currency),
1002
+ id: l.id,
1003
+ sku: l.sku,
1004
+ qty: String(l.qty),
1005
+ unit: pricing.format(l.unit_amount_minor, l.unit_currency),
1006
+ total: pricing.format(l.qty * l.unit_amount_minor, l.unit_currency),
1007
+ product_title: (prod && prod.title) || l.sku,
1008
+ product_url: prod ? ("/products/" + prod.slug) : "#",
1009
+ image_url: imageUrl,
1010
+ image_alt: imageAlt,
984
1011
  };
985
1012
  });
986
1013
  var subtotal = pricing.format(totals.subtotal_minor, totals.currency);
@@ -997,10 +1024,23 @@ function renderCart(opts) {
997
1024
  asset_css_main: opts.theme.assetUrl("css/main.css"),
998
1025
  });
999
1026
  }
1027
+ function _escAttr(s) {
1028
+ return String(s == null ? "" : s)
1029
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1030
+ }
1000
1031
  var rows = rendered.map(function (l) {
1032
+ var thumb = l.image_url
1033
+ ? "<span class=\"cart-line__thumb\"><img src=\"" + _escAttr(l.image_url) + "\" alt=\"" + _escAttr(l.image_alt) + "\" loading=\"lazy\"></span>"
1034
+ : "<span class=\"cart-line__thumb cart-line__thumb--empty\" aria-hidden=\"true\"></span>";
1001
1035
  return _render(CART_LINE_EDITABLE, {
1002
- sku: l.sku, qty: l.qty, unit: l.unit, total: l.total, line_id: l.id,
1003
- });
1036
+ sku: l.sku,
1037
+ qty: l.qty,
1038
+ unit: l.unit,
1039
+ total: l.total,
1040
+ line_id: l.id,
1041
+ product_title: l.product_title,
1042
+ product_url: l.product_url,
1043
+ }).replace("RAW_CART_LINE_THUMB", thumb);
1004
1044
  }).join("");
1005
1045
  if (!rows) rows = "<tr><td colspan=\"5\" class=\"empty\">Your cart is empty.</td></tr>";
1006
1046
  var body = _render(CART_PAGE, {
@@ -1578,7 +1618,30 @@ function mount(router, deps) {
1578
1618
  }
1579
1619
  var lines = await deps.cart.listLines(c.id);
1580
1620
  var totals = pricing.totals(c, lines, {});
1581
- _send(res, 200, renderCart({ lines: lines, totals: totals, shop_name: shopName, theme: theme }));
1621
+ // Build the variant_id { product, hero_media } lookup the
1622
+ // renderer uses to decorate each line with a thumbnail + title.
1623
+ // Cache by variant_id so a cart with the same variant twice
1624
+ // only hits the catalog once.
1625
+ var productLookup = {};
1626
+ for (var i = 0; i < lines.length; i += 1) {
1627
+ var vId = lines[i].variant_id;
1628
+ if (productLookup[vId]) continue;
1629
+ var v = await deps.catalog.variants.get(vId);
1630
+ if (!v) { productLookup[vId] = null; continue; }
1631
+ var prod = await deps.catalog.products.get(v.product_id);
1632
+ var media = await deps.catalog.media.listForProduct(v.product_id);
1633
+ productLookup[vId] = {
1634
+ product: prod,
1635
+ hero_media: media.length ? media[0] : null,
1636
+ };
1637
+ }
1638
+ _send(res, 200, renderCart({
1639
+ lines: lines,
1640
+ totals: totals,
1641
+ product_lookup: productLookup,
1642
+ shop_name: shopName,
1643
+ theme: theme,
1644
+ }));
1582
1645
  });
1583
1646
 
1584
1647
  // ---- checkout flow -------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.0.44",
3
+ "version": "0.0.45",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {