@blamejs/blamejs-shop 0.3.54 → 0.3.55

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.3.x
10
10
 
11
+ - v0.3.55 (2026-06-02) — **Accessibility: field-level form-error messages and horizontally scrollable admin tables.** Two accessibility improvements. When a storefront form is rejected, the specific field at fault is now marked for assistive technology with an inline error message tied to that input, in addition to the existing summary at the top of the form. And wide tables in the admin console now scroll horizontally inside their own container instead of overflowing the page on a narrow or zoomed viewport. **Fixed:** *Field-level error messages on storefront forms* — When the address, review, product-question, or support-ticket form is rejected, the offending field is now marked aria-invalid with an inline error message associated to it (aria-describedby), so a screen-reader user is taken to the exact field to fix rather than only hearing a top-of-form summary. The server remains the only validator; this only changes how a rejected field is presented. Forms that fail on a whole-form condition continue to show the summary message. · *Admin tables reflow on narrow screens* — Wide data tables in the admin console (orders, products, inventory, and the rest) are now wrapped in a horizontal-scroll container, so columns stay reachable on a smaller laptop window, a split screen, or at high zoom instead of overflowing the layout. Column header semantics are preserved.
12
+
11
13
  - v0.3.54 (2026-06-02) — **In-store pickup, saved payment methods, and gift wrapping at checkout.** Three more built-but-unreachable capabilities are now wired end to end. Operators can offer in-store pickup with managed pickup locations and a fulfilment queue; signed-in customers can save a card and reuse it at checkout; and operators can offer gift wrapping with a per-order gift message. The gift-wrap fee is added as a real cart line so it is included in the charged total. **Added:** *In-store pickup (click and collect)* — Operators can define pickup locations and work a pickup queue in the admin console (filtered by location), and shoppers can choose 'Pick up in store' at checkout. The order page and a new account pickups view show the pickup status. A not-found pickup or location returns a clear not-found rather than a generic error. · *Saved payment methods* — When Stripe is configured, a signed-in customer can save a card via a secure setup flow (loaded through a route-scoped policy and a same-origin script, with no inline script), manage their saved cards under their account, set a default, and reuse a saved card at checkout. A customer can only ever see or change their own saved cards. · *Gift wrapping at checkout* — Operators can define gift-wrap options (with a fee) in the admin console, and shoppers can add a wrap plus a gift message and recipient at checkout. The wrap fee is added as a real cart line, so it is included in the order total and the amount charged — never applied after the charge.
12
14
 
13
15
  - v0.3.53 (2026-06-02) — **Back-in-stock notifications, operator-tunable search ranking, and trust badges.** Three capabilities that were built into the framework but reachable from no route are now wired end to end. A shopper looking at an out-of-stock variant can ask to be emailed when it returns; operators can tune how search results are ordered and pin products for specific queries; and operators can author trust badges that render at checkout and on the order-confirmation page. **Added:** *Back-in-stock notifications* — When a variant is out of stock, the product page now shows a 'Notify me when it's back' form. The shopper confirms by email (double opt-in), and a scheduled sweep emails everyone waiting when stock returns, with a one-click unsubscribe. The notify form is served on the cached product page and is protected by same-site and fetch-metadata checks. · *Operator-tunable search ranking* — A new Search ranking screen in the admin console lets an operator define weighted ranking signals, activate a weight set, and pin specific products to the top for a given query. Ranking is applied to the search results; if a ranking configuration is missing or invalid, search falls back to its default order rather than erroring. · *Trust badges* — A new Trust badges screen lets an operator author badges (with scheduling windows) that render at checkout and on the order-confirmation page, with impression tracking. Badge content — including any SVG — is sanitized when saved.
package/lib/admin.js CHANGED
@@ -181,6 +181,16 @@ function _htmlEscape(s) {
181
181
  return b.template.escapeHtml(String(s));
182
182
  }
183
183
 
184
+ // Wrap a built data-table string in a horizontal-scroll container so a
185
+ // narrow viewport scrolls the table rather than the whole page (.table-wrap
186
+ // is defined in admin.css — WCAG 1.4.10 reflow). Layout-only tables
187
+ // (order-totals) are NOT wrapped — they never overflow. `tableHtml` is
188
+ // already-escaped markup assembled by the caller; this helper interpolates
189
+ // no untrusted data, so it is XSS-neutral.
190
+ function _tableWrap(tableHtml) {
191
+ return "<div class=\"table-wrap\">" + tableHtml + "</div>";
192
+ }
193
+
184
194
  // Strict integer coercion for money / count form fields. Refuses
185
195
  // "50abc" / "" / floats, unlike parseInt's loose prefix match (which
186
196
  // silently turns "50abc" into 50). Accepts an already-numeric value
@@ -10508,7 +10518,7 @@ function renderDashboard(opts) {
10508
10518
  }).join("");
10509
10519
  otherCurrencies =
10510
10520
  "<section><h2>Other currencies in window</h2><div class=\"panel\">" +
10511
- "<table><thead><tr><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + rows + "</tbody></table>" +
10521
+ _tableWrap("<table><thead><tr><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + rows + "</tbody></table>") +
10512
10522
  "</div></section>";
10513
10523
  }
10514
10524
 
@@ -10542,11 +10552,11 @@ function renderDashboard(opts) {
10542
10552
  "<section><h2>Catalog + activity</h2><div class=\"two-col\">" +
10543
10553
  " <div class=\"panel\">" +
10544
10554
  " <h3 class=\"subhead\">Top SKUs by units sold</h3>" +
10545
- " <table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Units</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + topRows + "</tbody></table>" +
10555
+ " " + _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Units</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + topRows + "</tbody></table>") +
10546
10556
  " </div>" +
10547
10557
  " <div class=\"panel\">" +
10548
10558
  " <h3 class=\"subhead\">Recent orders</h3>" +
10549
- " <table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Total</th></tr></thead><tbody>" + recentRows + "</tbody></table>" +
10559
+ " " + _tableWrap("<table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Total</th></tr></thead><tbody>" + recentRows + "</tbody></table>") +
10550
10560
  " </div>" +
10551
10561
  "</div></section>";
10552
10562
 
@@ -10807,10 +10817,10 @@ function renderAdminIntegrations(opts) {
10807
10817
  "<section>" +
10808
10818
  "<h2>Integrations</h2>" +
10809
10819
  "<p class=\"meta\">Every integration is off until you supply its credentials — set them as deployment secrets, then redeploy. Nothing is enabled without your keys.</p>" +
10810
- "<div class=\"panel\"><table>" +
10820
+ "<div class=\"panel\">" + _tableWrap("<table>" +
10811
10821
  "<thead><tr><th scope=\"col\">Integration</th><th scope=\"col\">Status</th><th scope=\"col\">To enable</th></tr></thead>" +
10812
10822
  "<tbody>" + rows + "</tbody>" +
10813
- "</table></div>" +
10823
+ "</table>") + "</div>" +
10814
10824
  "<p class=\"meta mt-125\">Sign in with Apple and PayPal are planned. “Sign in with Shop” / Shop Pay isn't available to a self-hosted store. See the README “Optional integrations” section for full setup steps.</p>" +
10815
10825
  "<div class=\"actions-row\"><a class=\"btn btn--ghost\" href=\"/admin\">Back</a></div>" +
10816
10826
  "</section>";
@@ -10836,7 +10846,7 @@ function renderAdminProducts(opts) {
10836
10846
  "</div></td></tr>";
10837
10847
  }).join("");
10838
10848
  var table = products.length
10839
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
10849
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
10840
10850
  : "<p class=\"empty\">No products yet — create your first one below.</p>";
10841
10851
  var body =
10842
10852
  "<section><h2>Products</h2>" + created + notice + table +
@@ -10892,7 +10902,7 @@ function renderAdminOrders(opts) {
10892
10902
  }).join("");
10893
10903
 
10894
10904
  var table = orders.length
10895
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Items</th><th scope=\"col\" class=\"num\">Total</th><th scope=\"col\">Placed</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
10905
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Items</th><th scope=\"col\" class=\"num\">Total</th><th scope=\"col\">Placed</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
10896
10906
  : "<p class=\"empty\">No orders" + (active ? " with status “" + _htmlEscape(active) + "”" : " yet") + ".</p>";
10897
10907
 
10898
10908
  var body = "<section><h2>Orders</h2>" + notice + chips + table + "</section>";
@@ -10964,7 +10974,7 @@ function renderAdminAudit(opts) {
10964
10974
  }).join("");
10965
10975
 
10966
10976
  var table = rows.length
10967
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Time</th><th scope=\"col\">Action</th><th scope=\"col\">Outcome</th><th scope=\"col\">Actor</th><th scope=\"col\">Resource</th><th scope=\"col\">Details</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
10977
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Time</th><th scope=\"col\">Action</th><th scope=\"col\">Outcome</th><th scope=\"col\">Actor</th><th scope=\"col\">Resource</th><th scope=\"col\">Details</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
10968
10978
  : "<p class=\"empty\">No audit events match.</p>";
10969
10979
 
10970
10980
  // Pager: Newer steps back one page (disabled at offset 0); Older steps
@@ -11055,7 +11065,7 @@ function renderAdminReports(opts) {
11055
11065
  }).join("");
11056
11066
  var funnelBlock =
11057
11067
  "<section><h2>Order status</h2><div class=\"panel\">" +
11058
- "<table><thead><tr><th scope=\"col\">Stage</th><th scope=\"col\" class=\"num\">Orders</th></tr></thead><tbody>" + funnelRows + "</tbody></table>" +
11068
+ _tableWrap("<table><thead><tr><th scope=\"col\">Stage</th><th scope=\"col\" class=\"num\">Orders</th></tr></thead><tbody>" + funnelRows + "</tbody></table>") +
11059
11069
  "</div></section>";
11060
11070
 
11061
11071
  // Top products by gross revenue across the window.
@@ -11069,7 +11079,7 @@ function renderAdminReports(opts) {
11069
11079
  : "<tr><td colspan=\"3\" class=\"empty\">No sales in this window.</td></tr>";
11070
11080
  var topBlock =
11071
11081
  "<section><h2>Top products</h2><div class=\"panel\">" +
11072
- "<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Units</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + topRows + "</tbody></table>" +
11082
+ _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Units</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + topRows + "</tbody></table>") +
11073
11083
  "</div></section>";
11074
11084
 
11075
11085
  // By-day revenue series — one row per (day, currency) bucket.
@@ -11088,7 +11098,7 @@ function renderAdminReports(opts) {
11088
11098
  : "<tr><td colspan=\"6\" class=\"empty\">No sales in this window.</td></tr>";
11089
11099
  var dayBlock =
11090
11100
  "<section><h2>By day</h2><div class=\"panel\">" +
11091
- "<table><thead><tr><th scope=\"col\">Date</th><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Gross</th><th scope=\"col\" class=\"num\">Net</th><th scope=\"col\" class=\"num\">Refunds</th></tr></thead><tbody>" + dayRows + "</tbody></table>" +
11101
+ _tableWrap("<table><thead><tr><th scope=\"col\">Date</th><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Gross</th><th scope=\"col\" class=\"num\">Net</th><th scope=\"col\" class=\"num\">Refunds</th></tr></thead><tbody>" + dayRows + "</tbody></table>") +
11092
11102
  "</div></section>";
11093
11103
 
11094
11104
  var body =
@@ -11180,7 +11190,7 @@ function renderAdminExports(opts) {
11180
11190
  "</tr>";
11181
11191
  }).join("");
11182
11192
  var jobTable = exportsRows.length
11183
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Format</th><th scope=\"col\">Window</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Rows</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + jobRows + "</tbody></table></div>"
11193
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Format</th><th scope=\"col\">Window</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Rows</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + jobRows + "</tbody></table>") + "</div>"
11184
11194
  : "<p class=\"empty\">No scheduled exports yet.</p>";
11185
11195
 
11186
11196
  var scheduleFmtOptions = formats.map(function (f) {
@@ -11283,7 +11293,7 @@ function renderAdminShippingLabels(opts) {
11283
11293
 
11284
11294
  var rows = labels.map(_shippingLabelRow).join("");
11285
11295
  var table = labels.length
11286
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Broker</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\">Created</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
11296
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Broker</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\">Created</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
11287
11297
  : "<p class=\"empty\">No " + _htmlEscape(status) + " labels" + (status === "voided" ? " in this window" : "") + ".</p>";
11288
11298
 
11289
11299
  var body =
@@ -11305,7 +11315,7 @@ function renderAdminShippingLabelsPending(opts) {
11305
11315
  var notice = opts.notice ? "<div class=\"banner banner--warn\">" + _htmlEscape(opts.notice) + "</div>" : "";
11306
11316
  var rows = labels.map(_shippingLabelRow).join("");
11307
11317
  var table = labels.length
11308
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Broker</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\">Created</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
11318
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Broker</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\">Created</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
11309
11319
  : "<p class=\"empty\">No labels awaiting a broker mint.</p>";
11310
11320
  var body =
11311
11321
  "<section><h2>Shipping labels</h2>" + notice +
@@ -11361,7 +11371,7 @@ function renderAdminShippingLabelCosts(opts) {
11361
11371
  : "<tr><td colspan=\"4\" class=\"empty\">No labels purchased in this window.</td></tr>";
11362
11372
  var brokerBlock =
11363
11373
  "<section><h2>By broker</h2><div class=\"panel\">" +
11364
- "<table><thead><tr><th scope=\"col\">Broker</th><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Labels</th><th scope=\"col\" class=\"num\">Spend</th></tr></thead><tbody>" + brokerRows + "</tbody></table>" +
11374
+ _tableWrap("<table><thead><tr><th scope=\"col\">Broker</th><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Labels</th><th scope=\"col\" class=\"num\">Spend</th></tr></thead><tbody>" + brokerRows + "</tbody></table>") +
11365
11375
  "</div></section>";
11366
11376
 
11367
11377
  var body =
@@ -11394,7 +11404,7 @@ function renderAdminOrder(opts) {
11394
11404
  "</tr>";
11395
11405
  }).join("");
11396
11406
  var linesTable = (o.lines && o.lines.length)
11397
- ? "<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Qty</th><th scope=\"col\" class=\"num\">Unit</th><th scope=\"col\" class=\"num\">Line</th></tr></thead><tbody>" + lineRows + "</tbody></table>"
11407
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Qty</th><th scope=\"col\" class=\"num\">Unit</th><th scope=\"col\" class=\"num\">Line</th></tr></thead><tbody>" + lineRows + "</tbody></table>")
11398
11408
  : "<p class=\"empty\">No line items recorded.</p>";
11399
11409
 
11400
11410
  function _total(label, minor, strong) {
@@ -11482,7 +11492,7 @@ function renderAdminOrder(opts) {
11482
11492
  "<td>" + _htmlEscape(_fmtDate(e.occurred_at)) + "</td></tr>";
11483
11493
  }).join("");
11484
11494
  var eventsTable = (s.events && s.events.length)
11485
- ? "<table><thead><tr><th scope=\"col\">Status</th><th scope=\"col\">Location</th><th scope=\"col\">When</th></tr></thead><tbody>" + eventRows + "</tbody></table>"
11495
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Status</th><th scope=\"col\">Location</th><th scope=\"col\">When</th></tr></thead><tbody>" + eventRows + "</tbody></table>")
11486
11496
  : "<p class=\"empty\">No carrier events yet.</p>";
11487
11497
  var statusOpts = statuses.map(function (st) {
11488
11498
  return "<option value=\"" + _htmlEscape(st) + "\">" + _htmlEscape(st) + "</option>";
@@ -11608,7 +11618,7 @@ function _orderLabelPanel(orderId, shipment, carriers, packageTypes, purchasedVi
11608
11618
  "</tr>";
11609
11619
  }).join("");
11610
11620
  var labelsTable = labels.length
11611
- ? "<table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\"></th></tr></thead><tbody>" + labelRows + "</tbody></table>"
11621
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\"></th></tr></thead><tbody>" + labelRows + "</tbody></table>")
11612
11622
  : "<p class=\"empty\">No labels recorded for this shipment.</p>";
11613
11623
  var carrierOpts = carriers.map(function (c) {
11614
11624
  return "<option value=\"" + _htmlEscape(c) + "\">" + _htmlEscape(c) + "</option>";
@@ -11687,7 +11697,7 @@ function _orderSplitPanel(o, plans) {
11687
11697
  ? "<form method=\"post\" action=\"/admin/orders/" + _htmlEscape(o.id) + "/split/plan\" class=\"return-action\">" +
11688
11698
  "<h4>Plan a manual split</h4>" +
11689
11699
  "<p class=\"meta\">Assign each line to a parcel number (lines sharing a number ship together). Every unit must land in a parcel.</p>" +
11690
- "<table><thead><tr><th scope=\"col\">Line (order qty)</th><th scope=\"col\">Parcel</th><th scope=\"col\">Qty</th></tr></thead><tbody>" + lineRows + "</tbody></table>" +
11700
+ _tableWrap("<table><thead><tr><th scope=\"col\">Line (order qty)</th><th scope=\"col\">Parcel</th><th scope=\"col\">Qty</th></tr></thead><tbody>" + lineRows + "</tbody></table>") +
11691
11701
  "<button class=\"btn\" type=\"submit\">Propose split</button>" +
11692
11702
  "</form>"
11693
11703
  : "<p class=\"empty\">No order lines to split.</p>";
@@ -11720,7 +11730,7 @@ function renderAdminPickLists(opts) {
11720
11730
  "</tr>";
11721
11731
  }).join("");
11722
11732
  var table = lists.length
11723
- ? "<table><thead><tr><th scope=\"col\">List</th><th scope=\"col\">Location</th><th scope=\"col\">Status</th><th scope=\"col\">Sort</th><th scope=\"col\">Generated</th></tr></thead><tbody>" + rows + "</tbody></table>"
11733
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">List</th><th scope=\"col\">Location</th><th scope=\"col\">Status</th><th scope=\"col\">Sort</th><th scope=\"col\">Generated</th></tr></thead><tbody>" + rows + "</tbody></table>")
11724
11734
  : "<p class=\"empty\">No pick lists yet. Generate one from the open orders at a location.</p>";
11725
11735
 
11726
11736
  var statusFilter = "<form method=\"get\" action=\"/admin/pick-lists\" class=\"form-inline\">" +
@@ -11807,7 +11817,7 @@ function renderAdminPickList(opts) {
11807
11817
  "</tr>";
11808
11818
  }).join("");
11809
11819
  var linesTable = (l.lines && l.lines.length)
11810
- ? "<table><thead><tr><th scope=\"col\">Aisle/bin</th><th scope=\"col\">SKU</th><th scope=\"col\">Order</th><th scope=\"col\" class=\"num\">Expected</th><th scope=\"col\">Picked</th><th scope=\"col\"></th></tr></thead><tbody>" + lineRows + "</tbody></table>"
11820
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Aisle/bin</th><th scope=\"col\">SKU</th><th scope=\"col\">Order</th><th scope=\"col\" class=\"num\">Expected</th><th scope=\"col\">Picked</th><th scope=\"col\"></th></tr></thead><tbody>" + lineRows + "</tbody></table>")
11811
11821
  : "<p class=\"empty\">This worksheet has no lines.</p>";
11812
11822
 
11813
11823
  // Variance summary — short / over picks only (the discrepancies feed
@@ -11822,7 +11832,7 @@ function renderAdminPickList(opts) {
11822
11832
  }).join("");
11823
11833
  var variancePanel = discRows
11824
11834
  ? "<div class=\"panel mt\"><h3 class=\"subhead\">Variances</h3>" +
11825
- "<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Expected</th><th scope=\"col\" class=\"num\">Picked</th><th scope=\"col\" class=\"num\">Diff</th></tr></thead><tbody>" + discRows + "</tbody></table></div>"
11835
+ _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Expected</th><th scope=\"col\" class=\"num\">Picked</th><th scope=\"col\" class=\"num\">Diff</th></tr></thead><tbody>" + discRows + "</tbody></table>") + "</div>"
11826
11836
  : "";
11827
11837
 
11828
11838
  // Complete / cancel actions — only on a non-terminal worksheet.
@@ -11946,7 +11956,7 @@ function renderAdminCustomers(opts) {
11946
11956
  var notice = opts.notice ? "<div class=\"banner banner--warn\">" + _htmlEscape(opts.notice) + "</div>" : "";
11947
11957
 
11948
11958
  var table = customers.length
11949
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Name</th><th scope=\"col\">ID</th><th scope=\"col\">Joined</th><th scope=\"col\">Sign-in method</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\"><span class=\"sr-only\">Manage</span></th></tr></thead><tbody>" + rows + "</tbody></table></div>"
11959
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Name</th><th scope=\"col\">ID</th><th scope=\"col\">Joined</th><th scope=\"col\">Sign-in method</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\"><span class=\"sr-only\">Manage</span></th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
11950
11960
  : "<p class=\"empty\">No customers yet.</p>";
11951
11961
 
11952
11962
  // Cursor pager — a Next link when the page filled and more rows remain.
@@ -11997,7 +12007,7 @@ function renderAdminCustomerDetail(opts) {
11997
12007
  }).join("");
11998
12008
  var ordersPanel = "<div class=\"panel\"><h3 class=\"subhead\">Recent orders</h3>" +
11999
12009
  ((opts.recent_orders || []).length
12000
- ? "<table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Total</th><th scope=\"col\">Placed</th></tr></thead><tbody>" + orderRows + "</tbody></table>"
12010
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Total</th><th scope=\"col\">Placed</th></tr></thead><tbody>" + orderRows + "</tbody></table>")
12001
12011
  : "<p class=\"empty\">No orders yet.</p>") +
12002
12012
  "</div>";
12003
12013
 
@@ -12021,7 +12031,7 @@ function renderAdminCustomerDetail(opts) {
12021
12031
  "</tr>";
12022
12032
  }).join("");
12023
12033
  var histTable = (opts.store_credit_history || []).length
12024
- ? "<table><thead><tr><th scope=\"col\">Kind</th><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\" class=\"num\">Balance after</th><th scope=\"col\">Reason</th><th scope=\"col\">When</th></tr></thead><tbody>" + histRows + "</tbody></table>"
12034
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Kind</th><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\" class=\"num\">Balance after</th><th scope=\"col\">Reason</th><th scope=\"col\">When</th></tr></thead><tbody>" + histRows + "</tbody></table>")
12025
12035
  : "<p class=\"empty\">No store-credit activity yet.</p>";
12026
12036
  creditPanel = "<div class=\"panel\"><h3 class=\"subhead\">Store credit</h3>" +
12027
12037
  creditNotice +
@@ -12321,7 +12331,7 @@ function renderAdminReturns(opts) {
12321
12331
  }).join("");
12322
12332
 
12323
12333
  var table = rmas.length
12324
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">RMA</th><th scope=\"col\">Order</th><th scope=\"col\">Reason</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Items</th><th scope=\"col\" class=\"num\">Refund</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
12334
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">RMA</th><th scope=\"col\">Order</th><th scope=\"col\">Reason</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Items</th><th scope=\"col\" class=\"num\">Refund</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
12325
12335
  : "<p class=\"empty\">No “" + _htmlEscape(active) + "” returns.</p>";
12326
12336
 
12327
12337
  var body = "<section><h2>Returns</h2>" + notice + chips + table + "</section>";
@@ -12341,7 +12351,7 @@ function renderAdminReturn(opts) {
12341
12351
  "<td>" + _htmlEscape(l.reason || "—") + "</td></tr>";
12342
12352
  }).join("");
12343
12353
  var linesTable = (r.lines && r.lines.length)
12344
- ? "<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Qty</th><th scope=\"col\">Reason</th></tr></thead><tbody>" + lineRows + "</tbody></table>"
12354
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Qty</th><th scope=\"col\">Reason</th></tr></thead><tbody>" + lineRows + "</tbody></table>")
12345
12355
  : "<p class=\"empty\">No line items recorded.</p>";
12346
12356
 
12347
12357
  function _field(label, value) {
@@ -12483,7 +12493,7 @@ function renderAdminSupport(opts) {
12483
12493
  }).join("");
12484
12494
 
12485
12495
  var table = tickets.length
12486
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Subject</th><th scope=\"col\">Category</th><th scope=\"col\">Status</th><th scope=\"col\">Priority</th><th scope=\"col\">Assignee</th><th scope=\"col\">Opened</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
12496
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Subject</th><th scope=\"col\">Category</th><th scope=\"col\">Status</th><th scope=\"col\">Priority</th><th scope=\"col\">Assignee</th><th scope=\"col\">Opened</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
12487
12497
  : "<p class=\"empty\">No “" + _htmlEscape(active) + "” tickets.</p>";
12488
12498
 
12489
12499
  var body = "<section><h2>Support</h2>" + notice + chips + table + "</section>";
@@ -12641,7 +12651,7 @@ function renderAdminDsr(opts) {
12641
12651
  }).join("");
12642
12652
 
12643
12653
  var table = requests.length
12644
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Customer</th><th scope=\"col\">Kind</th><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Scope</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
12654
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Customer</th><th scope=\"col\">Kind</th><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Scope</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
12645
12655
  : "<p class=\"empty\">No “" + _htmlEscape(active) + "” privacy requests.</p>";
12646
12656
 
12647
12657
  var body = "<section><h2>Privacy requests</h2>" +
@@ -12724,7 +12734,7 @@ function renderAdminDsrDetail(opts) {
12724
12734
  previewHtml =
12725
12735
  "<div class=\"panel mt\"><h3 class=\"subhead\">Erasure preview (dry run — nothing was changed)</h3>" +
12726
12736
  (dRows
12727
- ? "<table><thead><tr><th scope=\"col\">Table</th><th scope=\"col\">Affected</th></tr></thead><tbody>" + dRows + "</tbody></table>"
12737
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Table</th><th scope=\"col\">Affected</th></tr></thead><tbody>" + dRows + "</tbody></table>")
12728
12738
  : "<p class=\"empty\">No per-domain handlers wired.</p>") +
12729
12739
  (absent ? "<p class=\"meta\">Domains without a deletion handler: " + absent + "</p>" : "") +
12730
12740
  "<p class=\"meta\">Total affected: " + _htmlEscape(String(preview.total_affected)) + "</p>" +
@@ -12740,7 +12750,7 @@ function renderAdminDsrDetail(opts) {
12740
12750
  "</tr>";
12741
12751
  }).join("");
12742
12752
  var historyHtml = history.length
12743
- ? "<table><thead><tr><th scope=\"col\">Request</th><th scope=\"col\">Kind</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + histRows + "</tbody></table>"
12753
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Request</th><th scope=\"col\">Kind</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + histRows + "</tbody></table>")
12744
12754
  : "<p class=\"empty\">No prior requests.</p>";
12745
12755
 
12746
12756
  var body =
@@ -12814,7 +12824,7 @@ function renderAdminExchanges(opts) {
12814
12824
  }).join("");
12815
12825
 
12816
12826
  var table = list.length
12817
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Exchange</th><th scope=\"col\">Order</th><th scope=\"col\">Swap</th><th scope=\"col\">Reason</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
12827
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Exchange</th><th scope=\"col\">Order</th><th scope=\"col\">Swap</th><th scope=\"col\">Reason</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
12818
12828
  : "<p class=\"empty\">No “" + _htmlEscape(active) + "” exchanges.</p>";
12819
12829
 
12820
12830
  var body = "<section><h2>Exchanges</h2>" + notice + chips + table + "</section>";
@@ -12962,7 +12972,7 @@ function renderAdminPickupLocations(opts) {
12962
12972
  }).join("");
12963
12973
 
12964
12974
  var table = list.length
12965
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Code</th><th scope=\"col\">Name</th><th scope=\"col\">Address</th><th scope=\"col\">Capacity</th><th scope=\"col\">Lead time</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
12975
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Code</th><th scope=\"col\">Name</th><th scope=\"col\">Address</th><th scope=\"col\">Capacity</th><th scope=\"col\">Lead time</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
12966
12976
  : "<p class=\"empty\">No active pickup locations yet. Add one below.</p>";
12967
12977
 
12968
12978
  var form =
@@ -13058,7 +13068,7 @@ function renderAdminPickups(opts) {
13058
13068
  var table = !selected
13059
13069
  ? "<p class=\"empty\">Add a pickup location first.</p>"
13060
13070
  : (list.length
13061
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\">Window</th><th scope=\"col\">Picked up</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
13071
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\">Window</th><th scope=\"col\">Picked up</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
13062
13072
  : "<p class=\"empty\">No pickups in this window for this location.</p>");
13063
13073
 
13064
13074
  var body = "<section><h2>Pickups</h2>" + moved + notice + selector + chips + table + "</section>";
@@ -13104,7 +13114,7 @@ function renderAdminGiftWraps(opts) {
13104
13114
  }).join("");
13105
13115
 
13106
13116
  var table = list.length
13107
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\">Image</th><th scope=\"col\">Title</th><th scope=\"col\">Fee</th><th scope=\"col\">State</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
13117
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\">Image</th><th scope=\"col\">Title</th><th scope=\"col\">Fee</th><th scope=\"col\">State</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
13108
13118
  : "<p class=\"empty\">No gift wraps yet. Define one below — the SKU must be a real catalog variant.</p>";
13109
13119
 
13110
13120
  var form =
@@ -13443,7 +13453,7 @@ function renderAdminInventory(opts) {
13443
13453
  }).join("");
13444
13454
 
13445
13455
  var table = rows.length
13446
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">On hand</th><th scope=\"col\" class=\"num\">Held</th><th scope=\"col\" class=\"num\">Available</th><th scope=\"col\">Restock / threshold</th></tr></thead><tbody>" + body + "</tbody></table></div>"
13456
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">On hand</th><th scope=\"col\" class=\"num\">Held</th><th scope=\"col\" class=\"num\">Available</th><th scope=\"col\">Restock / threshold</th></tr></thead><tbody>" + body + "</tbody></table>") + "</div>"
13447
13457
  : "<p class=\"empty\">No inventory rows" + (opts.low ? " below threshold" : " yet") + ".</p>";
13448
13458
 
13449
13459
  var createForm =
@@ -13550,7 +13560,7 @@ function renderAdminSubscriptionPlans(opts) {
13550
13560
  }).join("");
13551
13561
 
13552
13562
  var table = rows.length
13553
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Price / interval</th><th scope=\"col\">Stripe price</th><th scope=\"col\">Variant</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
13563
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Price / interval</th><th scope=\"col\">Stripe price</th><th scope=\"col\">Variant</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
13554
13564
  : "<p class=\"empty\">No subscription plans" + (af === "0" ? " archived" : af === "1" ? " active" : " yet") + ".</p>";
13555
13565
 
13556
13566
  var intervalOpts = ["month", "year", "week", "day"].map(function (iv) {
@@ -13651,7 +13661,7 @@ function renderAdminTaxRates(opts) {
13651
13661
  "</tr>";
13652
13662
  }).join("");
13653
13663
  table = rows.length
13654
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Category</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">From</th><th scope=\"col\">Until</th><th scope=\"col\">Source</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
13664
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Category</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">From</th><th scope=\"col\">Until</th><th scope=\"col\">Source</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
13655
13665
  : "<p class=\"empty\">No rates for " + _htmlEscape(jurisdiction) + " yet.</p>";
13656
13666
  }
13657
13667
 
@@ -13734,7 +13744,7 @@ function renderAdminTaxFilings(opts) {
13734
13744
  }).join("");
13735
13745
  var dueBlock = upcoming.length
13736
13746
  ? "<section><h2>Due soon or overdue</h2><div class=\"panel\">" +
13737
- "<table><thead><tr><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Period</th><th scope=\"col\">Status</th><th scope=\"col\">Due</th></tr></thead><tbody>" + dueRows + "</tbody></table>" +
13747
+ _tableWrap("<table><thead><tr><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Period</th><th scope=\"col\">Status</th><th scope=\"col\">Due</th></tr></thead><tbody>" + dueRows + "</tbody></table>") +
13738
13748
  "</div></section>"
13739
13749
  : "";
13740
13750
 
@@ -13751,7 +13761,7 @@ function renderAdminTaxFilings(opts) {
13751
13761
  "</tr>";
13752
13762
  }).join("");
13753
13763
  var table = filings.length
13754
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Kind</th><th scope=\"col\">Period</th><th scope=\"col\">Due</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Collected</th><th scope=\"col\" class=\"num\">Owed</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
13764
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Kind</th><th scope=\"col\">Period</th><th scope=\"col\">Due</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Collected</th><th scope=\"col\" class=\"num\">Owed</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
13755
13765
  : "<p class=\"empty\">No filings" + (jurisdiction ? " for " + _htmlEscape(jurisdiction) : "") + " yet. Open a period below.</p>";
13756
13766
 
13757
13767
  // Open-a-period form.
@@ -13841,7 +13851,7 @@ function renderAdminTaxFiling(opts) {
13841
13851
  "</tr>";
13842
13852
  }).join("");
13843
13853
  var breakdownBlock = bkeys.length
13844
- ? "<section><h2>By rate</h2><div class=\"panel\"><table><thead><tr><th scope=\"col\">Rate</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Taxable</th><th scope=\"col\" class=\"num\">Tax</th></tr></thead><tbody>" + breakdownRows + "</tbody></table></div></section>"
13854
+ ? "<section><h2>By rate</h2><div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Rate</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Taxable</th><th scope=\"col\" class=\"num\">Tax</th></tr></thead><tbody>" + breakdownRows + "</tbody></table>") + "</div></section>"
13845
13855
  : "";
13846
13856
 
13847
13857
  // Audit trail — submission + payment + amendment columns once recorded.
@@ -13862,7 +13872,7 @@ function renderAdminTaxFiling(opts) {
13862
13872
  trailRows += "<tr><td>Amended</td><td>" + _htmlEscape(f.amended_reason) +
13863
13873
  (f.amended_at != null ? " <span class=\"meta\">" + _htmlEscape(_fmtDate(f.amended_at)) + "</span>" : "") + "</td></tr>";
13864
13874
  }
13865
- trail = "<section><h2>Audit trail</h2><div class=\"panel\"><table><tbody>" + trailRows + "</tbody></table></div></section>";
13875
+ trail = "<section><h2>Audit trail</h2><div class=\"panel\">" + _tableWrap("<table><tbody>" + trailRows + "</tbody></table>") + "</div></section>";
13866
13876
  }
13867
13877
 
13868
13878
  // Lifecycle actions — gated on the FSM. draft → compute; computed →
@@ -13937,7 +13947,7 @@ function renderAdminTaxFilingReport(opts) {
13937
13947
  "</tr>";
13938
13948
  }).join("");
13939
13949
  var table = (report.filings || []).length
13940
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Period</th><th scope=\"col\">Window</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Collected</th><th scope=\"col\" class=\"num\">Owed</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
13950
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Period</th><th scope=\"col\">Window</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Collected</th><th scope=\"col\" class=\"num\">Owed</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
13941
13951
  : "<p class=\"empty\">No filings intersect this window.</p>";
13942
13952
 
13943
13953
  var body =
@@ -13981,7 +13991,7 @@ function renderAdminShipping(opts) {
13981
13991
  }).join("");
13982
13992
 
13983
13993
  var table = rows.length
13984
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Regions</th><th scope=\"col\" class=\"num\">Rates</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
13994
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Regions</th><th scope=\"col\" class=\"num\">Rates</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
13985
13995
  : "<p class=\"empty\">No shipping zones yet.</p>";
13986
13996
 
13987
13997
  var createForm =
@@ -14031,7 +14041,7 @@ function renderAdminShippingZone(opts) {
14031
14041
  "</tr>";
14032
14042
  }).join("");
14033
14043
  var rateTable = (z.rates || []).length
14034
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Service</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">Bucket</th></tr></thead><tbody>" + rateRows + "</tbody></table></div>"
14044
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Service</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">Bucket</th></tr></thead><tbody>" + rateRows + "</tbody></table>") + "</div>"
14035
14045
  : "<p class=\"empty\">No rate rows.</p>";
14036
14046
 
14037
14047
  var head = "<p class=\"meta\"><a href=\"/admin/shipping\">&larr; Shipping</a> · <code class=\"order-id\">" + _htmlEscape(z.slug) + "</code> · " +
@@ -14193,7 +14203,7 @@ function renderAdminDiscounts(opts) {
14193
14203
  "</tr>";
14194
14204
  }).join("");
14195
14205
  var ruleTable = rules.length
14196
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Rule</th><th scope=\"col\">Trigger</th><th scope=\"col\">Value</th><th scope=\"col\" class=\"num\">Priority</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + ruleRows + "</tbody></table></div>"
14206
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Rule</th><th scope=\"col\">Trigger</th><th scope=\"col\">Value</th><th scope=\"col\" class=\"num\">Priority</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + ruleRows + "</tbody></table>") + "</div>"
14197
14207
  : "<p class=\"empty\">No automatic discount rules yet.</p>";
14198
14208
 
14199
14209
  var TRIGGERS = [
@@ -14256,7 +14266,7 @@ function renderAdminDiscounts(opts) {
14256
14266
  "</tr>";
14257
14267
  }).join("");
14258
14268
  var polTable = policies.length
14259
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Policy</th><th scope=\"col\" class=\"num\">Max codes</th><th scope=\"col\">Combines with</th><th scope=\"col\" class=\"num\">Order min</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + polRows + "</tbody></table></div>"
14269
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Policy</th><th scope=\"col\" class=\"num\">Max codes</th><th scope=\"col\">Combines with</th><th scope=\"col\" class=\"num\">Order min</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + polRows + "</tbody></table>") + "</div>"
14260
14270
  : "<p class=\"empty\">No stacking policies yet — without one, only one code applies per order.</p>";
14261
14271
  var polForm =
14262
14272
  "<div class=\"panel mt mw-40\">" +
@@ -14362,7 +14372,7 @@ function renderAdminSegments(opts) {
14362
14372
  }).join("");
14363
14373
 
14364
14374
  var table = segments.length
14365
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Segment</th><th scope=\"col\">Rules</th><th scope=\"col\" class=\"num\">Members</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
14375
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Segment</th><th scope=\"col\">Rules</th><th scope=\"col\" class=\"num\">Members</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
14366
14376
  : "<p class=\"empty\">No " + (filter === "archived" ? "archived " : (filter === "all" ? "" : "active ")) + "segments yet. Define one to start grouping customers for campaigns.</p>";
14367
14377
 
14368
14378
  var body = "<section><h2>Customer segments</h2>" + created + updated + archived + unarchived + recomputed + notice +
@@ -14504,7 +14514,7 @@ function renderAdminGiftCards(opts) {
14504
14514
  }).join("");
14505
14515
 
14506
14516
  var table = rows.length
14507
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Code</th><th scope=\"col\" class=\"num\">Issued</th><th scope=\"col\" class=\"num\">Remaining</th><th scope=\"col\">Status</th><th scope=\"col\">Issued on</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
14517
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Code</th><th scope=\"col\" class=\"num\">Issued</th><th scope=\"col\" class=\"num\">Remaining</th><th scope=\"col\">Status</th><th scope=\"col\">Issued on</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
14508
14518
  : "<p class=\"empty\">No gift cards" + (sf ? " " + _htmlEscape(sf) : " yet") + ".</p>";
14509
14519
 
14510
14520
  // The issue form composes the giftcards primitive's issue() — the
@@ -14571,7 +14581,7 @@ function renderAdminGiftCard(opts) {
14571
14581
  "</tr>";
14572
14582
  }).join("");
14573
14583
  var ledgerTable = ledger.length
14574
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Type</th><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\" class=\"num\">Balance after</th><th scope=\"col\">Detail</th><th scope=\"col\">When</th></tr></thead><tbody>" + ledgerRows + "</tbody></table></div>"
14584
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Type</th><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\" class=\"num\">Balance after</th><th scope=\"col\">Detail</th><th scope=\"col\">When</th></tr></thead><tbody>" + ledgerRows + "</tbody></table>") + "</div>"
14575
14585
  : "<p class=\"empty\">No ledger transactions recorded for this card yet.</p>";
14576
14586
 
14577
14587
  // Void is offered only while the card is active — a redeemed card has
@@ -14613,7 +14623,7 @@ function renderAdminWebhooks(opts) {
14613
14623
  }).join("");
14614
14624
 
14615
14625
  var table = rows.length
14616
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">URL</th><th scope=\"col\">Events</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
14626
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">URL</th><th scope=\"col\">Events</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
14617
14627
  : "<p class=\"empty\">No webhook endpoints yet.</p>";
14618
14628
 
14619
14629
  var eventChecks = known.map(function (ev) {
@@ -14690,7 +14700,7 @@ function renderAdminWebhookDeliveries(opts) {
14690
14700
  }).join("");
14691
14701
 
14692
14702
  var table = rows.length
14693
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Event</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Code</th><th scope=\"col\" class=\"num\">Attempts</th><th scope=\"col\">Last error</th><th scope=\"col\">Created</th><th scope=\"col\"></th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
14703
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Event</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Code</th><th scope=\"col\" class=\"num\">Attempts</th><th scope=\"col\">Last error</th><th scope=\"col\">Created</th><th scope=\"col\"></th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
14694
14704
  : "<p class=\"empty\">No deliveries recorded for this endpoint yet.</p>";
14695
14705
 
14696
14706
  var head = "<p class=\"meta\">Endpoint <code class=\"order-id\">" + _htmlEscape(e.url) + "</code></p>";
@@ -14734,7 +14744,7 @@ function renderAdminCollections(opts) {
14734
14744
  }).join("");
14735
14745
 
14736
14746
  var table = rows.length
14737
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Type</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Products</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
14747
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Type</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Products</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
14738
14748
  : "<p class=\"empty\">No collections" + (af === "0" ? " archived" : af === "1" ? " active" : " yet") + ".</p>";
14739
14749
 
14740
14750
  // The create form toggles between a manual and a smart shape. Manual
@@ -14813,7 +14823,7 @@ function renderAdminDiscountAllocation(opts) {
14813
14823
  "</tr>";
14814
14824
  }).join("");
14815
14825
  var lineTable = breakdown.length
14816
- ? "<table><thead><tr><th scope=\"col\">Line</th><th scope=\"col\" class=\"num\">Allocated (minor)</th><th scope=\"col\" class=\"num\">Remaining (minor)</th></tr></thead><tbody>" + lineRows + "</tbody></table>"
14826
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Line</th><th scope=\"col\" class=\"num\">Allocated (minor)</th><th scope=\"col\" class=\"num\">Remaining (minor)</th></tr></thead><tbody>" + lineRows + "</tbody></table>")
14817
14827
  : "<p class=\"empty\">This allocation has no recorded lines.</p>";
14818
14828
  return "<div class=\"panel mt\">" +
14819
14829
  "<h3 class=\"subhead\">" + _htmlEscape(a.source || "—") + "</h3>" +
@@ -15125,7 +15135,7 @@ function renderAdminAnnouncements(opts) {
15125
15135
  }).join("");
15126
15136
 
15127
15137
  var table = rows.length
15128
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Message</th><th scope=\"col\">Theme</th><th scope=\"col\">Audience</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
15138
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Message</th><th scope=\"col\">Theme</th><th scope=\"col\">Audience</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15129
15139
  : "<p class=\"empty\">No announcements" + (af === "1" ? " active right now" : " yet") + ".</p>";
15130
15140
 
15131
15141
  var themeOpts = ["urgency", "promo", "info", "success"].map(function (t) {
@@ -15199,7 +15209,7 @@ function renderAdminSearchRanking(opts) {
15199
15209
  "</tr>";
15200
15210
  }).join("");
15201
15211
  var table = weights.length
15202
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Name</th><th scope=\"col\">Weights</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
15212
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Name</th><th scope=\"col\">Weights</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
15203
15213
  : "<p class=\"empty\">No weight sets yet. Create one below and make it active to rerank storefront search.</p>";
15204
15214
 
15205
15215
  var createForm =
@@ -15234,7 +15244,7 @@ function renderAdminSearchRanking(opts) {
15234
15244
  "</form>";
15235
15245
  var pinTable = pinsQuery
15236
15246
  ? (pins.length
15237
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Product id</th><th scope=\"col\">Position</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + pinRows + "</tbody></table></div>"
15247
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Product id</th><th scope=\"col\">Position</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + pinRows + "</tbody></table>") + "</div>"
15238
15248
  : "<p class=\"empty\">No pins for &ldquo;" + _htmlEscape(pinsQuery) + "&rdquo; yet.</p>")
15239
15249
  : "";
15240
15250
  var pinCreateForm = pinsQuery
@@ -15309,7 +15319,7 @@ function renderAdminTrustBadges(opts) {
15309
15319
  "</tr>";
15310
15320
  }).join("");
15311
15321
  var table = badges.length
15312
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\">Kind</th><th scope=\"col\">Placements</th><th scope=\"col\">Priority</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
15322
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\">Kind</th><th scope=\"col\">Placements</th><th scope=\"col\">Priority</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
15313
15323
  : "<p class=\"empty\">No trust badges yet. Create one below.</p>";
15314
15324
 
15315
15325
  var placementChecks = trustBadgesModule.ALLOWED_PLACEMENTS.map(function (pl) {
@@ -15449,11 +15459,11 @@ function renderAdminPreorders(opts) {
15449
15459
  }).join("");
15450
15460
 
15451
15461
  var table = rows.length
15452
- ? "<div class=\"panel\"><table><thead><tr>" +
15462
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr>" +
15453
15463
  "<th scope=\"col\">Slug</th><th scope=\"col\">SKU</th><th scope=\"col\">Ships</th>" +
15454
15464
  "<th scope=\"col\">Price</th><th scope=\"col\">Reserved / cap</th><th scope=\"col\">Remaining</th>" +
15455
15465
  "<th scope=\"col\">Status</th><th scope=\"col\">Actions</th>" +
15456
- "</tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
15466
+ "</tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15457
15467
  : "<p class=\"empty\">No pre-order campaigns yet.</p>";
15458
15468
 
15459
15469
  var createForm =
@@ -15622,7 +15632,7 @@ function renderAdminHelp(opts) {
15622
15632
  }).join("");
15623
15633
 
15624
15634
  var table = rows.length
15625
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Category</th><th scope=\"col\">Status</th><th scope=\"col\">Views</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
15635
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Category</th><th scope=\"col\">Status</th><th scope=\"col\">Views</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15626
15636
  : "<p class=\"empty\">No articles" + (sf ? " " + _htmlEscape(sf) : " yet") + ". Write your first one.</p>";
15627
15637
 
15628
15638
  var newBtn = "<div class=\"actions-row\"><a class=\"btn\" href=\"/admin/help/new\">New article</a></div>";
@@ -15774,7 +15784,7 @@ function renderAdminBlog(opts) {
15774
15784
  }).join("");
15775
15785
 
15776
15786
  var table = rows.length
15777
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Published</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
15787
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Published</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15778
15788
  : "<p class=\"empty\">No posts" + (sf ? " " + _htmlEscape(sf) : " yet") + ". Write your first one.</p>";
15779
15789
 
15780
15790
  var newBtn = "<div class=\"actions-row\"><a class=\"btn\" href=\"/admin/blog/new\">New post</a></div>";
@@ -15975,7 +15985,7 @@ function renderAdminPages(opts) {
15975
15985
  }).join("");
15976
15986
 
15977
15987
  var table = rows.length
15978
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Published</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
15988
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Published</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15979
15989
  : "<p class=\"empty\">No pages" + (sf ? " " + _htmlEscape(sf) : " yet") + ". Write your first one.</p>";
15980
15990
 
15981
15991
  var newBtn = "<div class=\"actions-row\"><a class=\"btn\" href=\"/admin/pages/new\">New page</a></div>";
@@ -16129,7 +16139,7 @@ function renderAdminSurveys(opts) {
16129
16139
  }).join("");
16130
16140
 
16131
16141
  var table = rows.length
16132
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Kind</th><th scope=\"col\">Trigger</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
16142
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Kind</th><th scope=\"col\">Trigger</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
16133
16143
  : "<p class=\"empty\">No surveys" + (af === "1" ? " active" : " yet") + ".</p>";
16134
16144
 
16135
16145
  var kindOpts = [["nps", "NPS — recommend score"], ["csat", "CSAT — satisfaction"], ["ces", "CES — ease of effort"], ["custom", "Custom — open feedback"]]
@@ -16192,7 +16202,7 @@ function renderAdminSurveyDetail(opts) {
16192
16202
  var rollupPanel =
16193
16203
  "<div class=\"panel\">" +
16194
16204
  "<p class=\"meta\">" + _htmlEscape(String(roll.response_count)) + " response(s)." + (headline ? " " + headline : "") + "</p>" +
16195
- (qRows ? "<table><thead><tr><th scope=\"col\">Question</th><th scope=\"col\">Kind</th><th scope=\"col\">Result</th></tr></thead><tbody>" + qRows + "</tbody></table>" : "") +
16205
+ (qRows ? _tableWrap("<table><thead><tr><th scope=\"col\">Question</th><th scope=\"col\">Kind</th><th scope=\"col\">Result</th></tr></thead><tbody>" + qRows + "</tbody></table>") : "") +
16196
16206
  "</div>";
16197
16207
 
16198
16208
  var enc = _htmlEscape(encodeURIComponent(survey.slug));
@@ -16258,7 +16268,7 @@ function renderAdminHours(opts) {
16258
16268
  }).join("");
16259
16269
 
16260
16270
  var table = rows.length
16261
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Schedule</th><th scope=\"col\">Timezone</th><th scope=\"col\" class=\"num\">Days</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
16271
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Schedule</th><th scope=\"col\">Timezone</th><th scope=\"col\" class=\"num\">Days</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
16262
16272
  : "<p class=\"empty\">No schedules yet.</p>";
16263
16273
 
16264
16274
  var dayFields = DOW.map(function (label, d) {
@@ -16358,7 +16368,7 @@ function renderAdminCollection(opts) {
16358
16368
  "</tr>";
16359
16369
  }).join("");
16360
16370
  var memberTable = members.length
16361
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\" class=\"num\">#</th><th scope=\"col\">Product id</th><th scope=\"col\"></th></tr></thead><tbody>" + memberRows + "</tbody></table></div>"
16371
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\" class=\"num\">#</th><th scope=\"col\">Product id</th><th scope=\"col\"></th></tr></thead><tbody>" + memberRows + "</tbody></table>") + "</div>"
16362
16372
  : "<p class=\"empty\">No members yet — add a product below.</p>";
16363
16373
 
16364
16374
  // Reorder: a single field of the current ids, comma-joined, that the
@@ -16398,7 +16408,7 @@ function renderAdminCollection(opts) {
16398
16408
  "</tr>";
16399
16409
  }).join("");
16400
16410
  var previewTable = preview.length
16401
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th></tr></thead><tbody>" + previewCards + "</tbody></table></div>"
16411
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th></tr></thead><tbody>" + previewCards + "</tbody></table>") + "</div>"
16402
16412
  : "<p class=\"empty\">No products match these rules yet.</p>";
16403
16413
  detailBody = "<section class=\"mt\"><h3 class=\"fs-105\">Matched products (live preview)</h3>" +
16404
16414
  "<p class=\"meta\">The first " + _htmlEscape(String(preview.length)) + " products the rules match right now.</p>" +
@@ -16490,7 +16500,7 @@ function renderAdminQuantityDiscounts(opts) {
16490
16500
  }).join("");
16491
16501
 
16492
16502
  var table = rows.length
16493
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Scope</th><th scope=\"col\">Scope id</th><th scope=\"col\" class=\"num\">Tiers</th><th scope=\"col\">Stacking</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
16503
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Scope</th><th scope=\"col\">Scope id</th><th scope=\"col\" class=\"num\">Tiers</th><th scope=\"col\">Stacking</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
16494
16504
  : "<p class=\"empty\">No quantity breaks" + (af === "1" ? " archived" : af === "0" ? " active" : " yet") + ".</p>";
16495
16505
 
16496
16506
  var scopeOpts = scopes.map(function (sc) {
@@ -16581,7 +16591,7 @@ function renderAdminQuantityDiscount(opts) {
16581
16591
  "</tr>";
16582
16592
  }).join("");
16583
16593
  var previewTable = previewRows
16584
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\" class=\"num\">Min qty</th><th scope=\"col\">Kind</th><th scope=\"col\" class=\"num\">Value</th><th scope=\"col\" class=\"num\">Unit @ min</th><th scope=\"col\" class=\"num\">Line saved</th><th scope=\"col\" class=\"num\">Line total</th></tr></thead><tbody>" + previewRows + "</tbody></table></div>"
16594
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\" class=\"num\">Min qty</th><th scope=\"col\">Kind</th><th scope=\"col\" class=\"num\">Value</th><th scope=\"col\" class=\"num\">Unit @ min</th><th scope=\"col\" class=\"num\">Line saved</th><th scope=\"col\" class=\"num\">Line total</th></tr></thead><tbody>" + previewRows + "</tbody></table>") + "</div>"
16585
16595
  : "<p class=\"empty\">No active rules to preview for this scope.</p>";
16586
16596
  var previewForm =
16587
16597
  "<form method=\"get\" action=\"/admin/quantity-discounts/" + _htmlEscape(enc) + "\" class=\"actions-row m-04\">" +
@@ -16664,8 +16674,8 @@ function renderAdminLoyalty(opts) {
16664
16674
  "<p class=\"meta\">Lifetime points (never decremented) place a customer in a tier. " +
16665
16675
  "Earning $1 grants " + _htmlEscape(String(opts.points_per_usd)) + " points; " +
16666
16676
  _htmlEscape(String(opts.redemption_points_per_usd)) + " points redeem for $1 of value.</p>" +
16667
- "<table><thead><tr><th scope=\"col\">Tier</th><th scope=\"col\" class=\"num\">Lifetime points to reach</th></tr></thead>" +
16668
- "<tbody>" + tierRows + "</tbody></table></div>";
16677
+ _tableWrap("<table><thead><tr><th scope=\"col\">Tier</th><th scope=\"col\" class=\"num\">Lifetime points to reach</th></tr></thead>" +
16678
+ "<tbody>" + tierRows + "</tbody></table>") + "</div>";
16669
16679
 
16670
16680
  // Earn-rule summary.
16671
16681
  var earnSummary;
@@ -16686,7 +16696,7 @@ function renderAdminLoyalty(opts) {
16686
16696
  earnSummary = "<div class=\"panel\"><div class=\"actions-row\"><h3 class=\"subhead\">Earn rules</h3>" +
16687
16697
  "<a class=\"btn btn--ghost\" href=\"/admin/loyalty/earn-rules\">Manage earn rules</a></div>" +
16688
16698
  (earnRules.length
16689
- ? "<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Trigger</th><th scope=\"col\" class=\"num\">Points/unit</th><th scope=\"col\">Status</th></tr></thead><tbody>" + earnRows + "</tbody></table>"
16699
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Trigger</th><th scope=\"col\" class=\"num\">Points/unit</th><th scope=\"col\">Status</th></tr></thead><tbody>" + earnRows + "</tbody></table>")
16690
16700
  : "<p class=\"empty\">No earn rules yet. Customers only earn points from active rules.</p>") +
16691
16701
  "</div>";
16692
16702
  }
@@ -16710,7 +16720,7 @@ function renderAdminLoyalty(opts) {
16710
16720
  rewardSummary = "<div class=\"panel\"><div class=\"actions-row\"><h3 class=\"subhead\">Rewards catalog</h3>" +
16711
16721
  "<a class=\"btn btn--ghost\" href=\"/admin/loyalty/rewards\">Manage rewards</a></div>" +
16712
16722
  (rewards.length
16713
- ? "<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\" class=\"num\">Point cost</th><th scope=\"col\">Status</th></tr></thead><tbody>" + rewardRows + "</tbody></table>"
16723
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\" class=\"num\">Point cost</th><th scope=\"col\">Status</th></tr></thead><tbody>" + rewardRows + "</tbody></table>")
16714
16724
  : "<p class=\"empty\">No rewards yet. Customers only see active rewards in their catalog.</p>") +
16715
16725
  "</div>";
16716
16726
  }
@@ -16778,7 +16788,7 @@ function renderAdminLoyaltyEarnRules(opts) {
16778
16788
  "</tr>";
16779
16789
  }).join("");
16780
16790
  var table = rules.length
16781
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Trigger</th><th scope=\"col\" class=\"num\">Points/unit</th><th scope=\"col\" class=\"num\">Max/event</th><th scope=\"col\">Statuses</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
16791
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Trigger</th><th scope=\"col\" class=\"num\">Points/unit</th><th scope=\"col\" class=\"num\">Max/event</th><th scope=\"col\">Statuses</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
16782
16792
  : "<p class=\"empty\">No earn rules yet.</p>";
16783
16793
 
16784
16794
  var triggerOpts = triggers.map(function (t) {
@@ -16881,7 +16891,7 @@ function renderAdminLoyaltyRewards(opts) {
16881
16891
  "</tr>";
16882
16892
  }).join("");
16883
16893
  var table = rewards.length
16884
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\">Kind</th><th scope=\"col\">Value</th><th scope=\"col\" class=\"num\">Point cost</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
16894
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\">Kind</th><th scope=\"col\">Value</th><th scope=\"col\" class=\"num\">Point cost</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
16885
16895
  : "<p class=\"empty\">No rewards yet.</p>";
16886
16896
 
16887
16897
  var kindOpts = kinds.map(function (k) {
@@ -17039,7 +17049,7 @@ function renderAdminProduct(opts) {
17039
17049
  "</tr>";
17040
17050
  }).join("");
17041
17051
  var histTable = (pc.history && pc.history.length)
17042
- ? "<table><thead><tr><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\">From</th><th scope=\"col\">Until</th></tr></thead><tbody>" + histRows + "</tbody></table>"
17052
+ ? _tableWrap("<table><thead><tr><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\">From</th><th scope=\"col\">Until</th></tr></thead><tbody>" + histRows + "</tbody></table>")
17043
17053
  : "";
17044
17054
  return "<div class=\"m-04\">" +
17045
17055
  "<span class=\"u-mute\">" + _htmlEscape(pc.currency) + "</span> · " + current +
@@ -1,13 +1,13 @@
1
1
  {
2
- "version": "0.3.54",
2
+ "version": "0.3.55",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
- "integrity": "sha384-/NizdENQkTc96romD0kmnl8220NAvW32QI0aSFuGiPbWE5lYbRxLXU5shsjlx5Ek",
6
- "fingerprinted": "css/admin.f028b2e410387546.css"
5
+ "integrity": "sha384-6k53cvkRrxMgmeStLIoLjVXZQHqIJgTmv1Izd8TYhh1HOC4POgE6GCvx1bsalyEP",
6
+ "fingerprinted": "css/admin.44eb97700c660798.css"
7
7
  },
8
8
  "css/main.css": {
9
- "integrity": "sha384-9rtEz2wKJbpfALccIipRdo/BWVQZi5O4IufqaLO/7jFyLK1xxCaDc/65Apjy8dbs",
10
- "fingerprinted": "css/main.1a1a379d6f58e2c0.css"
9
+ "integrity": "sha384-2nlE5YDlOjcnP/l1EY5kcYwvbV/TboHx+Hi6fAaDNyaau3nzQppJnNyXoLiFE8w1",
10
+ "fingerprinted": "css/main.59afbe779336867a.css"
11
11
  },
12
12
  "js/announcement.js": {
13
13
  "integrity": "sha384-z4zcEMn+tScoVnYRE4nEf8N/oyvpxdpaxTNrT4QO/jURChid4+qjAvWkzatCaAPq",
package/lib/storefront.js CHANGED
@@ -2192,6 +2192,49 @@ function _buildPreorderNotice(marker) {
2192
2192
  return "<p class=\"" + cls + "\" role=\"" + role + "\">" + b.template.escapeHtml(n.copy) + "</p>\n ";
2193
2193
  }
2194
2194
 
2195
+ // Map a thrown validator TypeError to the single form field it rejected, so a
2196
+ // re-render can mark exactly that input with aria-invalid + a per-field error
2197
+ // span (WCAG 3.3.1 Error Identification / 3.3.3 Error Suggestion) — IN ADDITION
2198
+ // to the page-top role="alert" summary banner the routes already render.
2199
+ //
2200
+ // The shop's validators throw "<module>: <field> <reason>" (or
2201
+ // "<module>.<method>: <field> <reason>") where the leading token after the
2202
+ // prefix IS the form `name`. This reads that already-thrown error; it does NOT
2203
+ // re-validate (the backend stays the single validator). `modulePrefix` is the
2204
+ // thrower's prefix (e.g. "addresses", "reviews", "productQA", "supportTickets")
2205
+ // and `formFields` is the set of `name`s the form actually renders, so an error
2206
+ // on a non-form internal (e.g. customer_id) returns null — the page-top banner
2207
+ // still shows, but no input is falsely marked.
2208
+ //
2209
+ // Returns { field, message } with the module prefix stripped from `message`
2210
+ // (so the per-field span carries just the human reason), or null when the
2211
+ // rejected token isn't one of this form's fields.
2212
+ function _fieldFromValidatorError(e, modulePrefix, formFields) {
2213
+ var raw = (e && e.message) || "";
2214
+ var prefixRe = new RegExp("^" + modulePrefix + "(?:\\.\\w+)?:\\s+");
2215
+ var m = new RegExp("^" + modulePrefix + "(?:\\.\\w+)?:\\s+([a-z_]+)\\b").exec(raw);
2216
+ var field = m && m[1];
2217
+ if (!field || !formFields || !Object.prototype.hasOwnProperty.call(formFields, field)) return null;
2218
+ return { field: field, message: raw.replace(prefixRe, "") };
2219
+ }
2220
+
2221
+ // Build the aria-invalid + aria-describedby attribute fragment for an input
2222
+ // when `inv` (a { field, message } from _fieldFromValidatorError) names this
2223
+ // field. Empty string otherwise. `idPrefix` + field is the static, escaped id.
2224
+ function _fieldAriaAttr(idPrefix, name, inv) {
2225
+ if (!inv || inv.field !== name) return "";
2226
+ return " aria-invalid=\"true\" aria-describedby=\"" + b.template.escapeHtml(idPrefix + name) + "\"";
2227
+ }
2228
+
2229
+ // Build the adjacent per-field error span (role="alert") for the rejected
2230
+ // field; empty string otherwise. The reason is escaped operator-validator
2231
+ // prose; the id is the static, escaped `idPrefix + name`.
2232
+ function _fieldErrorSpan(idPrefix, name, inv) {
2233
+ if (!inv || inv.field !== name) return "";
2234
+ return "<span class=\"form-field__error\" id=\"" + b.template.escapeHtml(idPrefix + name) +
2235
+ "\" role=\"alert\">" + b.template.escapeHtml(inv.message == null ? "" : String(inv.message)) + "</span>";
2236
+ }
2237
+
2195
2238
  var PRODUCT_PAGE =
2196
2239
  "<section class=\"pdp\">\n" +
2197
2240
  " <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n" +
@@ -2486,17 +2529,20 @@ var REVIEW_FORM_PAGE =
2486
2529
  " <h1 class=\"review-form-page__title\">Review {{title}}</h1>\n" +
2487
2530
  " RAW_NOTICE_PLACEHOLDER\n" +
2488
2531
  " <form class=\"review-form\" method=\"post\" action=\"/products/{{slug}}/review\">\n" +
2489
- " <fieldset class=\"review-form__rating\">\n" +
2532
+ " <fieldset class=\"review-form__rating\"RAW_RATING_ARIA_PLACEHOLDER>\n" +
2490
2533
  " <legend>Your rating</legend>\n" +
2491
2534
  " RAW_STARS_PLACEHOLDER\n" +
2535
+ " RAW_RATING_ERROR_PLACEHOLDER\n" +
2492
2536
  " </fieldset>\n" +
2493
2537
  " <label class=\"form-field\">\n" +
2494
2538
  " <span class=\"form-field__label\">Title</span>\n" +
2495
- " <input type=\"text\" name=\"title\" maxlength=\"120\" required autocomplete=\"off\">\n" +
2539
+ " <input type=\"text\" name=\"title\" maxlength=\"120\" required autocomplete=\"off\"RAW_TITLE_ARIA_PLACEHOLDER>\n" +
2540
+ " RAW_TITLE_ERROR_PLACEHOLDER\n" +
2496
2541
  " </label>\n" +
2497
2542
  " <label class=\"form-field\">\n" +
2498
2543
  " <span class=\"form-field__label\">Your review</span>\n" +
2499
- " <textarea name=\"body\" maxlength=\"4000\" rows=\"6\"></textarea>\n" +
2544
+ " <textarea name=\"body\" maxlength=\"4000\" rows=\"6\"RAW_BODY_ARIA_PLACEHOLDER></textarea>\n" +
2545
+ " RAW_BODY_ERROR_PLACEHOLDER\n" +
2500
2546
  " </label>\n" +
2501
2547
  " <button type=\"submit\" class=\"btn-primary\">Submit review</button>\n" +
2502
2548
  " </form>\n" +
@@ -2519,12 +2565,17 @@ function renderReviewForm(opts) {
2519
2565
  var notice = opts.notice
2520
2566
  ? "<p class=\"form-notice form-notice--error\" role=\"alert\">" + esc(String(opts.notice)) + "</p>"
2521
2567
  : "";
2522
- var body = _render(REVIEW_FORM_PAGE, {
2523
- title: opts.product.title,
2524
- slug: slug,
2525
- })
2568
+ // On a validation re-render, mark the one rejected control. The rating is a
2569
+ // <fieldset> (the aria goes on the group, per the existing fieldset/legend
2570
+ // shape); title/body are ordinary inputs. ids are static "review-err-<name>".
2571
+ var inv = opts.invalid_field || null;
2572
+ var body = _render(REVIEW_FORM_PAGE, { title: opts.product.title, slug: slug })
2526
2573
  .replace("RAW_NOTICE_PLACEHOLDER", notice)
2527
2574
  .replace("RAW_STARS_PLACEHOLDER", stars);
2575
+ ["rating", "title", "body"].forEach(function (name) {
2576
+ body = _spliceRaw(body, "RAW_" + name.toUpperCase() + "_ARIA_PLACEHOLDER", _fieldAriaAttr("review-err-", name, inv));
2577
+ body = _spliceRaw(body, "RAW_" + name.toUpperCase() + "_ERROR_PLACEHOLDER", _fieldErrorSpan("review-err-", name, inv));
2578
+ });
2528
2579
  return _wrap({
2529
2580
  title: "Review " + opts.product.title,
2530
2581
  shop_name: opts.shop_name || "blamejs.shop",
@@ -2631,7 +2682,8 @@ var QA_FORM_PAGE =
2631
2682
  " <form class=\"review-form\" method=\"post\" action=\"/products/{{slug}}/question\">\n" +
2632
2683
  " <label class=\"form-field\">\n" +
2633
2684
  " <span class=\"form-field__label\">Your question</span>\n" +
2634
- " <textarea name=\"body\" maxlength=\"4000\" rows=\"6\" required></textarea>\n" +
2685
+ " <textarea name=\"body\" maxlength=\"4000\" rows=\"6\" requiredRAW_BODY_ARIA_PLACEHOLDER></textarea>\n" +
2686
+ " RAW_BODY_ERROR_PLACEHOLDER\n" +
2635
2687
  " </label>\n" +
2636
2688
  " <button type=\"submit\" class=\"btn-primary\">Submit question</button>\n" +
2637
2689
  " </form>\n" +
@@ -2645,11 +2697,16 @@ function renderQuestionForm(opts) {
2645
2697
  var notice = opts.notice
2646
2698
  ? "<p class=\"form-notice form-notice--error\" role=\"alert\">" + esc(String(opts.notice)) + "</p>"
2647
2699
  : "";
2700
+ // Single field (`body`); on a re-render, mark it with aria-invalid + an
2701
+ // adjacent error span. id is the static "qa-err-body".
2702
+ var inv = opts.invalid_field || null;
2648
2703
  var body = _render(QA_FORM_PAGE, {
2649
2704
  title: opts.product.title,
2650
2705
  slug: opts.product.slug,
2651
2706
  })
2652
2707
  .replace("RAW_NOTICE_PLACEHOLDER", notice);
2708
+ body = _spliceRaw(body, "RAW_BODY_ARIA_PLACEHOLDER", _fieldAriaAttr("qa-err-", "body", inv));
2709
+ body = _spliceRaw(body, "RAW_BODY_ERROR_PLACEHOLDER", _fieldErrorSpan("qa-err-", "body", inv));
2653
2710
  return _wrap({
2654
2711
  title: "Ask about " + opts.product.title,
2655
2712
  shop_name: opts.shop_name || "blamejs.shop",
@@ -3545,32 +3602,59 @@ function _addrField(name, labelText, value, opts) {
3545
3602
  // The `*` is a color-only visual cue; pair it with a visually-hidden
3546
3603
  // "(required)" so a screen reader announces the field's requiredness.
3547
3604
  var req = opts.required ? " <span class=\"form-field__req\" aria-hidden=\"true\">*</span><span class=\"sr-only\">(required)</span>" : "";
3605
+ // On a validation re-render, the rejected field carries aria-invalid +
3606
+ // aria-describedby pointing at an adjacent error span (WCAG 3.3.1/3.3.3).
3607
+ // The id is a static prefix + field name (attacker-uncontrolled) but is
3608
+ // still escaped for defense-in-depth; the reason is operator-validator
3609
+ // prose, escaped via esc().
3610
+ var errSpan = "";
3611
+ if (opts.invalid) {
3612
+ attrs += " aria-invalid=\"true\" aria-describedby=\"" + esc(opts.error_id) + "\"";
3613
+ errSpan = "<span class=\"form-field__error\" id=\"" + esc(opts.error_id) +
3614
+ "\" role=\"alert\">" + esc(opts.error_msg == null ? "" : String(opts.error_msg)) + "</span>";
3615
+ }
3548
3616
  return "<label class=\"form-field\">" +
3549
3617
  "<span class=\"form-field__label\">" + esc(labelText) + req + "</span>" +
3550
3618
  "<input type=\"text\" name=\"" + esc(name) + "\" value=\"" + esc(value == null ? "" : String(value)) + "\"" + attrs + ">" +
3619
+ errSpan +
3551
3620
  "</label>";
3552
3621
  }
3553
3622
 
3554
3623
  // Shared add/edit address form. `addr` pre-fills for edit (null = add).
3555
- function _addressForm(action, addr, submitLabel) {
3624
+ // `invalidField` (optional) is a { field, message } picked off the
3625
+ // backend-thrown validator error so the one rejected input renders with
3626
+ // aria-invalid + a per-field error span (WCAG 3.3.1/3.3.3). When null, every
3627
+ // field renders byte-identically to the no-error path.
3628
+ function _addressForm(action, addr, submitLabel, invalidField) {
3556
3629
  var esc = b.template.escapeHtml;
3557
3630
  addr = addr || {};
3558
3631
  function _checked(v) { return Number(v) === 1 ? " checked" : ""; }
3632
+ // Merge the per-field invalid marker into a field's opts when it is the one
3633
+ // the validator rejected. The id is a static "addr-err-<name>" so it is
3634
+ // attacker-uncontrolled.
3635
+ function _mark(name, opts) {
3636
+ if (invalidField && invalidField.field === name) {
3637
+ opts.invalid = true;
3638
+ opts.error_id = "addr-err-" + name;
3639
+ opts.error_msg = invalidField.message;
3640
+ }
3641
+ return opts;
3642
+ }
3559
3643
  return "<form class=\"address-form form-stack\" method=\"post\" action=\"" + esc(action) + "\">" +
3560
- _addrField("recipient_name", "Recipient name", addr.recipient_name, { required: true, maxlength: 120, autocomplete: "name" }) +
3561
- _addrField("label", "Label (e.g. Home, Work)", addr.label, { maxlength: 60 }) +
3562
- _addrField("company", "Company", addr.company, { maxlength: 120, autocomplete: "organization" }) +
3563
- _addrField("street_line1", "Street address", addr.street_line1, { required: true, maxlength: 200, autocomplete: "address-line1" }) +
3564
- _addrField("street_line2", "Apt / suite / unit", addr.street_line2, { maxlength: 200, autocomplete: "address-line2" }) +
3644
+ _addrField("recipient_name", "Recipient name", addr.recipient_name, _mark("recipient_name", { required: true, maxlength: 120, autocomplete: "name" })) +
3645
+ _addrField("label", "Label (e.g. Home, Work)", addr.label, _mark("label", { maxlength: 60 })) +
3646
+ _addrField("company", "Company", addr.company, _mark("company", { maxlength: 120, autocomplete: "organization" })) +
3647
+ _addrField("street_line1", "Street address", addr.street_line1, _mark("street_line1", { required: true, maxlength: 200, autocomplete: "address-line1" })) +
3648
+ _addrField("street_line2", "Apt / suite / unit", addr.street_line2, _mark("street_line2", { maxlength: 200, autocomplete: "address-line2" })) +
3565
3649
  "<div class=\"form-row form-row--inline\">" +
3566
- _addrField("city", "City", addr.city, { required: true, maxlength: 120, autocomplete: "address-level2" }) +
3567
- _addrField("region", "State / region", addr.region, { maxlength: 120, autocomplete: "address-level1" }) +
3650
+ _addrField("city", "City", addr.city, _mark("city", { required: true, maxlength: 120, autocomplete: "address-level2" })) +
3651
+ _addrField("region", "State / region", addr.region, _mark("region", { maxlength: 120, autocomplete: "address-level1" })) +
3568
3652
  "</div>" +
3569
3653
  "<div class=\"form-row form-row--inline\">" +
3570
- _addrField("postal_code", "Postal code", addr.postal_code, { required: true, maxlength: 32, autocomplete: "postal-code" }) +
3571
- _addrField("country", "Country (ISO 3166-1)", addr.country || "US", { required: true, maxlength: 2, pattern: "[A-Za-z]{2}", autocomplete: "country" }) +
3654
+ _addrField("postal_code", "Postal code", addr.postal_code, _mark("postal_code", { required: true, maxlength: 32, autocomplete: "postal-code" })) +
3655
+ _addrField("country", "Country (ISO 3166-1)", addr.country || "US", _mark("country", { required: true, maxlength: 2, pattern: "[A-Za-z]{2}", autocomplete: "country" })) +
3572
3656
  "</div>" +
3573
- _addrField("phone", "Phone", addr.phone, { maxlength: 40, autocomplete: "tel" }) +
3657
+ _addrField("phone", "Phone", addr.phone, _mark("phone", { maxlength: 40, autocomplete: "tel" })) +
3574
3658
  "<label class=\"address-form__check\"><input type=\"checkbox\" name=\"is_default_shipping\" value=\"1\"" + _checked(addr.is_default_shipping) + "> Default shipping address</label>" +
3575
3659
  "<label class=\"address-form__check\"><input type=\"checkbox\" name=\"is_default_billing\" value=\"1\"" + _checked(addr.is_default_billing) + "> Default billing address</label>" +
3576
3660
  "<button type=\"submit\" class=\"btn-primary\">" + esc(submitLabel) + "</button>" +
@@ -3642,7 +3726,7 @@ function renderAddresses(opts) {
3642
3726
  listHtml +
3643
3727
  "<h2 class=\"account-addresses__form-title\">" + esc(formHeading) + "</h2>" +
3644
3728
  notice +
3645
- _addressForm(formAction, editing, editing ? "Save changes" : "Add address") +
3729
+ _addressForm(formAction, editing, editing ? "Save changes" : "Add address", opts.invalid_field || null) +
3646
3730
  "</section>";
3647
3731
  return _wrap({
3648
3732
  title: "Addresses",
@@ -4615,6 +4699,13 @@ function renderSupportNew(opts) {
4615
4699
  var notice = opts.notice
4616
4700
  ? "<p class=\"form-notice form-notice--error\" role=\"alert\">" + esc(String(opts.notice)) + "</p>"
4617
4701
  : "";
4702
+ // On a validation re-render, mark the one rejected field with aria-invalid +
4703
+ // an adjacent error span (WCAG 3.3.1/3.3.3) via the shared field helpers.
4704
+ // `opts.invalid_field` is the { field, message } the route picked off the
4705
+ // backend-thrown error; ids are static "support-err-<name>".
4706
+ var inv = opts.invalid_field || null;
4707
+ function _supAria(name) { return _fieldAriaAttr("support-err-", name, inv); }
4708
+ function _supErr(name) { return _fieldErrorSpan("support-err-", name, inv); }
4618
4709
  var body =
4619
4710
  "<section class=\"return-form-page\">" +
4620
4711
  "<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
@@ -4626,14 +4717,14 @@ function renderSupportNew(opts) {
4626
4717
  notice +
4627
4718
  "<form class=\"return-form form-stack\" method=\"post\" action=\"/account/support\">" +
4628
4719
  "<label class=\"form-field\"><span class=\"form-field__label\">Email for our reply</span>" +
4629
- "<input type=\"email\" name=\"customer_email\" value=\"" + esc(v.customer_email == null ? "" : String(v.customer_email)) + "\" required autocomplete=\"email\" maxlength=\"254\"></label>" +
4720
+ "<input type=\"email\" name=\"customer_email\" value=\"" + esc(v.customer_email == null ? "" : String(v.customer_email)) + "\" required autocomplete=\"email\" maxlength=\"254\"" + _supAria("customer_email") + ">" + _supErr("customer_email") + "</label>" +
4630
4721
  "<label class=\"form-field\"><span class=\"form-field__label\">Category</span>" +
4631
- "<select name=\"category\" required>" + categoryOpts + "</select></label>" +
4722
+ "<select name=\"category\" required" + _supAria("category") + ">" + categoryOpts + "</select>" + _supErr("category") + "</label>" +
4632
4723
  orderField +
4633
4724
  "<label class=\"form-field\"><span class=\"form-field__label\">Subject</span>" +
4634
- "<input type=\"text\" name=\"subject\" value=\"" + esc(v.subject == null ? "" : String(v.subject)) + "\" required maxlength=\"200\"></label>" +
4725
+ "<input type=\"text\" name=\"subject\" value=\"" + esc(v.subject == null ? "" : String(v.subject)) + "\" required maxlength=\"200\"" + _supAria("subject") + ">" + _supErr("subject") + "</label>" +
4635
4726
  "<label class=\"form-field\"><span class=\"form-field__label\">How can we help?</span>" +
4636
- "<textarea name=\"body\" required maxlength=\"8000\" rows=\"6\">" + esc(v.body == null ? "" : String(v.body)) + "</textarea></label>" +
4727
+ "<textarea name=\"body\" required maxlength=\"8000\" rows=\"6\"" + _supAria("body") + ">" + esc(v.body == null ? "" : String(v.body)) + "</textarea>" + _supErr("body") + "</label>" +
4637
4728
  "<button type=\"submit\" class=\"btn-primary\">Send</button>" +
4638
4729
  "</form>" +
4639
4730
  "</section>";
@@ -14567,7 +14658,7 @@ function mount(router, deps) {
14567
14658
  if (kind === "default-billing") return "Default billing address updated.";
14568
14659
  return null;
14569
14660
  }
14570
- async function _renderAddrPage(req, res, auth, editAddr, notice, code) {
14661
+ async function _renderAddrPage(req, res, auth, editAddr, notice, code, invalidField) {
14571
14662
  var rows = await deps.addresses.listForCustomer(auth.customer_id, { limit: 50 });
14572
14663
  var cartCount = await _cartCountForReq(req);
14573
14664
  var url = req.url ? new URL(req.url, "http://localhost") : null;
@@ -14577,15 +14668,24 @@ function mount(router, deps) {
14577
14668
  // when the ?undo=<id> marker round-trips a real owned address id.
14578
14669
  var undoId = (okKind === "removed" && url) ? url.searchParams.get("undo") : null;
14579
14670
  _send(res, code || 200, renderAddresses({
14580
- addresses: rows,
14581
- edit: editAddr || null,
14582
- notice: notice || null,
14583
- success: success,
14584
- undo_id: undoId || null,
14585
- shop_name: shopName,
14586
- cart_count: cartCount,
14671
+ addresses: rows,
14672
+ edit: editAddr || null,
14673
+ notice: notice || null,
14674
+ success: success,
14675
+ undo_id: undoId || null,
14676
+ invalid_field: invalidField || null,
14677
+ shop_name: shopName,
14678
+ cart_count: cartCount,
14587
14679
  }));
14588
14680
  }
14681
+ // The address form's renderable fields, by `name`. An error on a
14682
+ // non-form internal (customer_id, address_id) returns null from the
14683
+ // shared extractor, so the page-top banner still shows but no input is
14684
+ // falsely marked.
14685
+ var _ADDR_FORM_FIELDS = {
14686
+ recipient_name: 1, label: 1, company: 1, street_line1: 1, street_line2: 1,
14687
+ city: 1, region: 1, postal_code: 1, country: 1, phone: 1,
14688
+ };
14589
14689
  function _addrInput(body, customerId) {
14590
14690
  return {
14591
14691
  customer_id: customerId,
@@ -14622,7 +14722,10 @@ function mount(router, deps) {
14622
14722
  try {
14623
14723
  await deps.addresses.add(_addrInput(req.body || {}, auth.customer_id));
14624
14724
  } catch (e) {
14625
- if (e instanceof TypeError) return _renderAddrPage(req, res, auth, null, (e && e.message) || "Please check the address.", 400);
14725
+ if (e instanceof TypeError) {
14726
+ var inv = _fieldFromValidatorError(e, "addresses", _ADDR_FORM_FIELDS);
14727
+ return _renderAddrPage(req, res, auth, null, (e && e.message) || "Please check the address.", 400, inv);
14728
+ }
14626
14729
  throw e;
14627
14730
  }
14628
14731
  res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=added");
@@ -14637,7 +14740,8 @@ function mount(router, deps) {
14637
14740
  } catch (e) {
14638
14741
  if (e instanceof TypeError) {
14639
14742
  var merged = Object.assign({}, addr, _addrInput(req.body || {}, auth.customer_id));
14640
- return _renderAddrPage(req, res, auth, merged, (e && e.message) || "Please check the address.", 400);
14743
+ var inv = _fieldFromValidatorError(e, "addresses", _ADDR_FORM_FIELDS);
14744
+ return _renderAddrPage(req, res, auth, merged, (e && e.message) || "Please check the address.", 400, inv);
14641
14745
  }
14642
14746
  throw e;
14643
14747
  }
@@ -15730,11 +15834,12 @@ function mount(router, deps) {
15730
15834
  } catch (e) {
15731
15835
  if (e instanceof TypeError) {
15732
15836
  return _send(res, 400, renderSupportNew({
15733
- orders: await _orders(),
15734
- values: body,
15735
- notice: (e && e.message || "").replace(/^supportTickets[.:]\s*/, "") || "Please check your ticket and try again.",
15736
- shop_name: shopName,
15737
- cart_count: cartCount,
15837
+ orders: await _orders(),
15838
+ values: body,
15839
+ notice: (e && e.message || "").replace(/^supportTickets[.:]\s*/, "") || "Please check your ticket and try again.",
15840
+ invalid_field: _fieldFromValidatorError(e, "supportTickets", { customer_email: 1, category: 1, subject: 1, body: 1 }),
15841
+ shop_name: shopName,
15842
+ cart_count: cartCount,
15738
15843
  }));
15739
15844
  }
15740
15845
  throw e;
@@ -16418,10 +16523,11 @@ function mount(router, deps) {
16418
16523
  // anything else is a real 500.
16419
16524
  if (e instanceof TypeError) {
16420
16525
  return _send(res, 400, renderReviewForm({
16421
- product: { title: ctx.product.title, slug: ctx.product.slug },
16422
- notice: (e && e.message) || "Please check your review and try again.",
16423
- shop_name: shopName,
16424
- cart_count: ctx.cartCount,
16526
+ product: { title: ctx.product.title, slug: ctx.product.slug },
16527
+ notice: (e && e.message) || "Please check your review and try again.",
16528
+ invalid_field: _fieldFromValidatorError(e, "reviews", { rating: 1, title: 1, body: 1 }),
16529
+ shop_name: shopName,
16530
+ cart_count: ctx.cartCount,
16425
16531
  }));
16426
16532
  }
16427
16533
  throw e;
@@ -16489,10 +16595,11 @@ function mount(router, deps) {
16489
16595
  // anything else is a real 500.
16490
16596
  if (e instanceof TypeError) {
16491
16597
  return _send(res, 400, renderQuestionForm({
16492
- product: { title: ctx.product.title, slug: ctx.product.slug },
16493
- notice: (e && e.message) || "Please check your question and try again.",
16494
- shop_name: shopName,
16495
- cart_count: ctx.cartCount,
16598
+ product: { title: ctx.product.title, slug: ctx.product.slug },
16599
+ notice: (e && e.message) || "Please check your question and try again.",
16600
+ invalid_field: _fieldFromValidatorError(e, "productQA", { body: 1 }),
16601
+ shop_name: shopName,
16602
+ cart_count: ctx.cartCount,
16496
16603
  }));
16497
16604
  }
16498
16605
  throw e;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.3.54",
3
+ "version": "0.3.55",
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": {